cloth = (() => {
const COUNT = 2 + Math.max(threads, 1);
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;
})()