Skip to content

Instantly share code, notes, and snippets.

@zapthedingbat
Created November 13, 2019 14:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save zapthedingbat/b50fb68ae290ac587b3d36bf14f1e6f3 to your computer and use it in GitHub Desktop.
Save zapthedingbat/b50fb68ae290ac587b3d36bf14f1e6f3 to your computer and use it in GitHub Desktop.
Generate watercolor style blobs: Inspired by tyler hobbs's generative watercolors- https://tylerxhobbs.com/
(function(doc) {
const TWO_PI = Math.PI * 2;
const HALF_PI = Math.PI / 2;
const RADIUS_SCALE = 0.05;
const RADIUS_SD = 15;
const POLYGON_SIDES = 5;
const POSITION_SD = 0.04;
const BASE_DEFORMATIONS = 3;
const LAYER_DEFORMATIONS = 3;
const LAYERS = 40;
const PALLETS = [
[0, 30, 40, 60],
[60, 300, 250],
[130, 220],
[230, 220, 60]
];
const MID_POINT_SD = 0.3; // How much variation around the middle
const ANGLE_SD = 0.2; // How much variation of the angle to extend out at
const MAGNITUDE_SD = 0.2;
const POLY_COUNT = 4;
const HUE_SD = 3;
const HUE_SHIFT = 15;
// Use Park-Miller PRNG so we can seed it
function createRandom(seed) {
const m = 0x7fffffff;
let i = seed % m;
if (i <= 0) i += m;
return function random() {
i = (i * 0x41a7) % m;
return i / m;
};
}
const random = createRandom(Date.now());
function randomGaussian(mean, sd) {
let y1, x1, x2, w;
do {
x1 = random() * 2 - 1;
x2 = random() * 2 - 1;
w = x1 * x1 + x2 * x2;
} while (w >= 1);
w = Math.sqrt((-2 * Math.log(w)) / w);
y1 = x1 * w;
y2 = x2 * w;
return y1 * sd + mean;
}
function createPoly(x, y, r, sides) {
const points = [];
const angle = TWO_PI / sides;
let sx, sy;
for (let i = 0; i < sides; i++) {
const a = angle * i;
sx = x + Math.cos(a) * r;
sy = y + Math.sin(a) * r;
points.push([sx, sy]);
}
return points;
}
function dividePoly(poly, dividePoint) {
const points = [];
for (let i = 0; i < poly.length; i++) {
const [x1, y1] = poly[i];
const [x2, y2] = poly[(i + 1) % poly.length];
const [xd, yd] = dividePoint(x1, y1, x2, y2);
points.push([x1, y1], [xd, yd]);
}
return points;
}
function drawPoly(ctx, poly, polyX, polyY, r, hue1, hue2, opacity) {
// Construct a gradient at a random angle from the center to the radius of the polygon
let angle = random() * TWO_PI;
let x1 = polyX + r * Math.cos(angle);
let y1 = polyY + r * Math.sin(angle);
let x2 = polyX;
let y2 = polyY;
// Offset the start and end hues slightly from the specified hue
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
gradient.addColorStop(0, `hsla(${hue1}, 100%, 50%, ${opacity})`);
gradient.addColorStop(1, `hsla(${hue2}, 100%, 50%, ${opacity * 0.5})`);
// Construct the polygon and fill it with the gradient
ctx.save();
ctx.beginPath();
for (let i = 0; i < poly.length; i++) {
const [x, y] = poly[i];
ctx.lineTo(x, y);
}
const [x, y] = poly[0];
ctx.lineTo(x, y);
ctx.fillStyle = gradient;
ctx.fill();
ctx.restore();
}
// Generated a point between the specified start and end points
function deformPoint(x1, y1, x2, y2) {
const hypot = Math.hypot(x1 - x2, y1 - y2);
// Pick a random point (around the center) along the line between the two points
const u = randomGaussian(0.5, MID_POINT_SD);
const a = Math.atan2(y1 - y2, x1 - x2);
let x = x1 - hypot * u * Math.cos(a);
let y = y1 - hypot * u * Math.sin(a);
// Angle that the deformation will extend
let angle = Math.atan2(y1 - y2, x1 - x2);
angle += HALF_PI * randomGaussian(0.5, ANGLE_SD);
// Magnitude that the deformation will extend
const magnitude = hypot * Math.abs(randomGaussian(0.25, MAGNITUDE_SD));
x += magnitude * Math.cos(angle);
y += magnitude * Math.sin(angle);
const point = [x, y];
return point;
}
function draw(ctx) {
const { height, width } = ctx.canvas;
const radiusMean = Math.min(height, width) * RADIUS_SCALE;
// Generate polygons
const hues = PALLETS[~~(random() * PALLETS.length)];
const shapes = [];
for (let index = 0; index < POLY_COUNT; index++) {
// Pick a point on the canvas to center the polygon
const radius = randomGaussian(radiusMean, RADIUS_SD);
const x = width * randomGaussian(0.5, POSITION_SD);
const y = height * randomGaussian(0.5, POSITION_SD);
const baseHue = hues[index % hues.length];
// Create the initial polygon to start with
let polygon = createPoly(x, y, radius, POLYGON_SIDES);
// Deform the base polygon
for (let index = 0; index < BASE_DEFORMATIONS; index++) {
polygon = dividePoly(polygon, deformPoint);
}
// Pick a random color
const hue1 = randomGaussian(baseHue, HUE_SD) % 360;
const hue2 = (hue1 - HUE_SHIFT) % 360;
shapes.push({
polygon,
x,
y,
radius,
hue1,
hue2
});
}
// Draw polygons interleaved
const opacity = 1 / LAYERS;
for (let layer = 0; layer < LAYERS; layer++) {
for (const shape of shapes) {
let layerPolygon = shape.polygon;
for (let d = 0; d < LAYER_DEFORMATIONS; d++) {
layerPolygon = dividePoly(layerPolygon, deformPoint);
}
drawPoly(
ctx,
layerPolygon,
shape.x,
shape.y,
shape.radius,
shape.hue1,
shape.hue2,
opacity
);
}
}
}
function generateImage(ctx) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const { height, width } = ctx.canvas;
draw(ctx, 0, 0, width, height);
}
const canvas = doc.createElement("canvas");
canvas.height = 1024;
canvas.width = 1024;
const context = canvas.getContext("2d");
document.body.appendChild(canvas);
setInterval(() => generateImage(context), 500);
generateImage(context);
})(document);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment