Public
Edited
Jun 17, 2023
Paused
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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 + tighten;
ctx.shadowBlur = Math.floor(thickness / 4);
ctx.shadowColor = tint;

let nudge = 0;

if (devicePixelRatio % 1 === 0) {
nudge = /* fix blurriness */ ctx.lineWidth & 1 ? -0.5 : 0;
}

ctx.translate(SIZE + nudge, SIZE + nudge);
ctx.canvas.addEventListener("click", start, false);

Object.assign(ctx.canvas.style, {
display: "block",
width: `${2 * 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, width = 2 * thickness /* 2 × SIZE ÷ COUNT */;
i < COUNT;
i++
) {
for (let j = 0; j < COUNT; j++) {
grid[pattern(i, j)].rect(
-SIZE + i * width,
-SIZE + j * width,
width,
width
);
}
}

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(Array(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(Array(components.length - mirror), () => {
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;
});

if (mirror) {
indexOf.push(indexOf.slice().shift());
}

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(
Array(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 r = 0,
extent = Array.from(Array(fabric.length), init),
threads = fabric[0].length;
r < threads;
r++
) {
for (let i = 0; i < fabric.length; i++) {
let offset = extent[i][r];
let thread = fabric[i][r];
thread.inc(offset);
thread.delay(Math.round(SIZE - (4 * SIZE) / Math.abs(offset)));
}
}
}

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

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

function play(ts) {
if (paused) return;
for (
let r = 0,
threads = fabric[0].length,
warps = fabric[1],
wefts = fabric[0];
r < threads;
r++
) {
warps[r].move();
wefts[r].move();
}
weave();
t++;
let callback = play;
if (counter / 2 === COUNT - 2) {
callback = pause;
counter = 0;
t = 0;
}
frame = requestAnimationFrame(callback);
}

function pause(ts) {
if (when === 0) when = performance.now() + 1000;
let callback = pause;
if (ts >= when) {
callback = play;
colorize();
jumble();
when = 0;
}
frame = requestAnimationFrame(callback);
}

function start() {
if (paused) {
frame = requestAnimationFrame(play);
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);

colorize();

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

jumble();

start();

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

return ctx.canvas;
})()
Insert cell
tint = "#234"
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