Published
Edited
Dec 21, 2021
10 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
restart;
const w = 1024,
h = 768,
sz = 400;
const n = 2000; // Subdivisions
const sep = 10; // How much to separate pieces when they break up
const relaxSteps = 2; // Collision detection and response steps after reach split
const shuffleRate = 100; // Shuffle the pieces after this many splits

let ctx = DOM.context2d(w, h, 1);

const square = [
[0, 0],
[sz, 0],
[sz, sz],
[0, sz]
];
const rand = d3.randomUniform(-1, 1);
const [hmin, hmax, hvar] = [0, 360, 180 * colorVar],
[smin, smax, svar] = [50, 100, 50 * colorVar],
[lmin, lmax, lvar] = [30, 80, 50 * colorVar];
const baseColor = {
h: d3.randomInt(hmin, hmax)(),
s: d3.randomInt(smin, smax)(),
l: d3.randomInt(lmin, lmax)()
};
const varyColor = ({ h, s, l }) => ({
h: Math.min(Math.max(h + rand() * hvar, hmin), hmax),
s: Math.min(Math.max(s + rand() * svar, smin), smax),
l: Math.min(Math.max(l + rand() * lvar, lmin), lmax)
});
let pieces = [{ poly: square, color: baseColor }];

const draw = () => {
ctx.save();
ctx.fillStyle = "black";
ctx.fillRect(0, 0, w, h);
ctx.translate((w - sz) / 2, (h - sz) / 2);

for (let { poly, color } of pieces) {
ctx.fillStyle = `hsl(${color.h},${color.s}%,${color.l}%)`;
ctx.beginPath();
for (let [x, y] of poly) ctx.lineTo(x, y);
ctx.closePath();
ctx.fill();
}

ctx.restore();
};

draw();
yield ctx.canvas;
if (slowStart.length) await Promises.delay(1000);

for (let i = 0; i < n; i++) {
let j = d3.randomInt(0, pieces.length)();
const piece = pieces.shift();
if (i % shuffleRate == 0) pieces = d3.shuffle(pieces);
let { poly, color } = piece;

// Split the piece
let a = centroid(poly);
let v = PCA(poly);
v = [-v[1], v[0]];
vec2.rotate(v, v, [0, 0], (rand() * (splitDirVar * Math.PI)) / 2);
let b = vec2.add([], a, v);
let [left, right] = split(poly, a, b);
let [leftColor, rightColor] = [varyColor(color), varyColor(color)];

for (let poly of [left, right]) {
const c = centroid(poly);
const rotAng = rand() * rotVar * Math.PI;
const shiftAng = Math.random() * Math.PI * 2;
const shift = [sep * Math.cos(shiftAng), sep * Math.sin(shiftAng)];
for (let p of poly) {
vec2.rotate(p, p, c, rotAng);
//vec2.add(p, p, shift);
}
}

pieces.push(
{ poly: left, color: leftColor },
{ poly: right, color: rightColor }
);
let polys = pieces.map((p) => p.poly);
for (let j = 0; j < relaxSteps; j++) {
separatePolygons(polys);
pieces.forEach((piece, i) => (piece.poly = polys[i]));
draw();
yield ctx.canvas;
}

if (slowStart.length) await Promises.delay(Math.max(1, 500 - i * 5));
}
}
Insert cell
function centroid(poly) {
let r = [0, 0];
for (let p of poly) vec2.add(r, r, p);
return vec2.scale(r, r, 1 / poly.length);
}
Insert cell
function PCA(poly) {
let c = centroid(poly);
let vectors = poly.map((p) => vec2.sub([], p, c));
let m = [0, 0, 0, 0];
for (let [x, y] of vectors) {
mat2.add(m, m, [x * x, x * y, x * y, y * y]);
}
mat2.multiplyScalar(m, m, 1 / vectors.length);

// Power method to find the dominant eigenvector
// let v = [1, 0];
// for (let i = 0; i < 10; i++) {
// vec2.transformMat2(v, v, m);
// }
// vec2.normalize(v, v);
// return v;

let [a, b, _, d] = m;
let l1 = (a + d + Math.sqrt((a - d) ** 2 + 4 * b * b)) / 2;
let v = [b, l1 - a];
if (vec2.sqrLen(v) < 0.001) return [0, 1]; // No dominant eigenvector
vec2.normalize(v, v);
return v;
}
Insert cell
Insert cell
import { glmatrix, vec2 } from "@esperanc/vec2-utils"
Insert cell
mat2 = glmatrix.mat2
Insert cell
import { split, polygonClip } from "@esperanc/sutherland-hodgman-clipping"
Insert cell
import { separatePolygons } from "@esperanc/shape-matching"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more