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 + tighten;
ctx.shadowBlur = Math.floor(thickness / 4);
ctx.shadowColor = tint;
let nudge = 0;
if (devicePixelRatio % 1 === 0) {
nudge = 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;
})()