Public
Edited
May 22, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
padding = 50
Insert cell
cloth = (() => {
const COUNT = 2 + Math.max(threads, 1); // an unsigned integer ≥ 3
const HALF_COUNT = Math.round(COUNT / 2);
const SIZE = thickness * COUNT;
const EDGE = SIZE - thickness;

let ctx = DOM.context2d(2 * SIZE, 2 * SIZE);

ctx.lineCap = "round";
ctx.lineWidth = thickness;
ctx.shadowBlur = Math.floor(thickness / 4);
ctx.shadowColor = tint;
ctx.translate(SIZE, SIZE);
ctx.canvas.addEventListener("click", start, false);

Object.assign(ctx.canvas.style, {
display: "block",
width: `${SIZE}px`,
margin: "0 auto",
background: `${tint} none`
});

let components = ["warp", "weft"];
let counter = 0;

let grid = [new Path2D(), new Path2D()];
let lift = grid.map(Number.call, Number)[invert ? "reverse" : "slice"]();

for (let i = 0, SE = (2 * SIZE) / COUNT; i < COUNT; i++) {
for (let j = 0; j < COUNT; j++) {
grid[(i + j) & 1].rect(-SIZE + i * SE, -SIZE + j * SE, SE, SE);
}
}

let colorTable;
let indexOf;

function colorize() {
let count = randomInt(2, COUNT - 1);
let arc = Math.round(360 / count);
let hue = randomInt(0, 90, true) + 30;

colorTable = Array.from({ length: count }, (_, i) => {
let delta = arc * i + randomInt(-45, 46);
let saturation = 60 + randomInt(0, 31);
let lightness = 60 + randomInt(0, 41);
return (
"hsl(" +
[(hue + delta) % 360, saturation + "%", lightness + "%"].join(", ") +
")"
);
});

indexOf = Array.from(components, () => {
let index = [];
for (
let seed = randomInt(0, colorTable.length), i = seed;
i < seed + HALF_COUNT - 1;
i++
) {
index.push(i % colorTable.length);
}
shuffle(index);
return index;
});

document.dispatchEvent(
new CustomEvent("weave", {
detail: Object.freeze(
Object.fromEntries(
components.map((_, i) => [
components[i],
indexOf[i].map((r) => colorTable[r])
])
)
)
})
);
}

function jumble() {
function init() {
let bundle = Array.from(
{ length: HALF_COUNT - 1 },
() => Math.round(6 * (random() + 0.6)) * [-1, 1][randomInt(0, 2)]
);
if (random() < 0.4) {
bundle.sort((b, a) => {
return Math.abs(a) - Math.abs(b);
});
} else if (random() < 0.5) {
bundle.sort((a, b) => {
return Math.abs(a) - Math.abs(b);
});
}
for (let i = HALF_COUNT - 1 - (COUNT & 1 ? 1 : 0); i > 0; ) {
bundle.push(-bundle[--i]);
}
return bundle;
}

for (let threads of fabric) {
for (let i = 0, set = init(); i < set.length; i++) {
threads[i].inc(set[i]);
threads[i].delay(Math.round(SIZE - (4 * SIZE) / Math.abs(set[i])));
}
}
}

function weave() {
ctx.clearRect(-SIZE, -SIZE, 2 * SIZE, 2 * SIZE);
for (let i = 0, threads = fabric; i < lift.length; i++) {
ctx.save();
ctx.clip(grid[lift[i]]);
for (let thread of threads[i]) {
thread.draw();
}
ctx.shadowOffsetX = ctx.shadowOffsetY = thickness / 8;
for (let thread of threads[1 - i]) {
thread.draw();
}
ctx.restore();
}
}

let frame = null;
let paused = true;
let when = 0;
let t = 0;

function animate(ts) {
if (paused) return;
for (let threads of fabric) {
for (let thread of threads) {
thread.move();
}
}
weave();
t++;
let callback = animate;
if (counter / 2 == COUNT - 2) {
callback = pause;
colorize();
jumble();
counter = 0;
t = 0;
}
frame = requestAnimationFrame(callback);
}

function pause(ts) {
if (paused) return;
if (when == 0) when = performance.now() + 1000;
let callback = pause;
if (ts >= when) {
callback = animate;
when = 0;
}
frame = requestAnimationFrame(callback);
}

function start() {
if (paused) {
frame = requestAnimationFrame(animate);
when = 0;
}
paused = !paused;
}

function Thread(kind, x, y, r) {
let component = components.indexOf(kind);
let color = colorTable[indexOf[component][r]];
let delay = 0;
let inc = 0;
let o = 0;

return Object.freeze(
Object.assign(
{
delay(amount) {
if ("undefined" === typeof amount) return;
delay = amount;
},
inc(amount) {
if ("undefined" === typeof amount) return;
inc = amount;
},
move() {
if (Math.abs(o) > 2 * SIZE) {
color = colorTable[indexOf[component][r]];
o = -o;
}
if (t > delay) {
o += inc;
if (inc != 0 && Math.abs(o) < 0.1) {
inc = o = 0;
counter++;
}
}
}
},
component
? {
draw(
zz = o *
Math.sin((Math.PI / (SIZE - delay)) * Math.max(0, t - delay))
) {
ctx.beginPath();
ctx.moveTo(-EDGE + zz, y);
ctx.lineTo(EDGE + zz, y);
ctx.strokeStyle = color;
ctx.stroke();
}
}
: {
draw(
zz = o *
Math.sin((Math.PI / (SIZE - delay)) * Math.max(0, t - delay))
) {
ctx.beginPath();
ctx.moveTo(x, -EDGE + zz);
ctx.lineTo(x, EDGE + zz);
ctx.strokeStyle = color;
ctx.stroke();
}
}
)
);
}

const shuttle = (e) => (mutable colors = e.detail);

document.addEventListener("weave", shuttle);

for (let i = 0, SE = (2 * SIZE) / COUNT; i < COUNT; i++) {
for (let j = 0; j < COUNT; j++) {
grid[pattern(i, j)].rect(-SIZE + i * SE, -SIZE + j * SE, SE, SE);
}
}

colorize();

/* {{{ warp and weft… */
var fabric = [
(function wefts() {
let a = [],
b = [];
for (let i = 0; i < HALF_COUNT - 1; i++) {
a.push(
Thread("weft", 0, -SIZE + thickness + ((i + 1) * 2 * SIZE) / COUNT, i)
);
if (COUNT & 1 && i == HALF_COUNT - 2) continue;
b.unshift(
Thread("weft", 0, SIZE + thickness - ((i + 2) * 2 * SIZE) / COUNT, i)
);
}
return a.concat(b);
})(),
(function warps() {
let a = [],
b = [];
for (let i = 0; i < HALF_COUNT - 1; i++) {
a.push(
Thread("warp", -SIZE + thickness + ((i + 1) * 2 * SIZE) / COUNT, 0, i)
);
if (COUNT & 1 && i == HALF_COUNT - 2) continue;
b.unshift(
Thread("warp", SIZE + thickness - ((i + 2) * 2 * SIZE) / COUNT, 0, i)
);
}
return a.concat(b);
})()
];
/* }}} */

jumble();

start();

invalidation.then(
() => frame && cancelAnimationFrame(frame),
document.removeEventListener("weave", shuttle)
);

return ctx.canvas;
})()
Insert cell
tint = "#333"
Insert cell
pattern = (i, j) => (i + j) & 1
Insert cell
function shuffle(array) {
let m = array.length,
i;
while (m) {
i = Math.floor(random() * m--);
array[i] = [array[m], (array[m] = array[i])].shift();
}
}
Insert cell
randomInt = (min, max, low) =>
Math.floor((low ? random() : 1) * random() * (max - min) + min)
Insert cell
random = (function () {
const crypto = self["crypto" || "msCrypto"];
return crypto && crypto.getRandomValues
? () => crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)
: Math.random;
})()
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