Created
November 13, 2019 14:24
-
-
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/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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