Fabric = (
context,
ends,
picks,
{
tileSize = 26,
borderColor = "#333",
borderWidth = 1,
yarnWidth = 16,
warpColor = "hsl(216, 70%, 80%)",
weftColor = "hsl(216, 70%, 70%)",
pattern = null,
left = 0,
top = 0
} = {}
) => {
const HALF_PI = Math.PI / 2;
borderWidth = Math.max(
0,
Math.min(borderWidth, Math.floor(tileSize / 2) - 1)
);
yarnWidth = Math.max(2, Math.min(yarnWidth, tileSize - borderWidth * 2));
if ("function" !== typeof pattern || 2 !== pattern.length) {
pattern = (width, length) =>
Array.from({ length }, (_, i) =>
Array.from({ length: width }, (_, j) => !((j & 1) ^ (i & 1)))
);
}
function drawPattern(drawWeftCurls = false) {
let curlOnLeft = false;
let drawdown = pattern(ends, picks);
for (let pick = 0; pick < picks; pick++) {
for (let end = 0; end < ends; end++) {
// when drawdown[pick][end] is true, yarn interlacing is warp-facing
drawInterlacement(
left + end * tileSize,
top + pick * tileSize,
warpColor,
weftColor,
drawdown[pick][end]
);
}
if (drawWeftCurls && pick < picks - 1) {
// link picks together
drawWeftCurl(
curlOnLeft ? left : left + ends * tileSize,
top + pick * tileSize,
weftColor,
curlOnLeft
);
curlOnLeft = !curlOnLeft;
}
}
if (drawWeftCurls) {
// draw "stubs" to make the top/bottom picks look as wide as the curled wefts
let stubSize = borderWidth + (tileSize + yarnWidth) / 2;
// top pick
drawWeft(left - stubSize, top, weftColor, stubSize / tileSize);
// bottom pick
drawWeft(
(picks & 1) === 0 ? left - stubSize : left + ends * tileSize,
top + (picks - 1) * tileSize,
weftColor,
stubSize / tileSize
);
}
return drawdown;
}
function drawInterlacement(
x,
y,
warpColor,
weftColor,
isWarpFacing = true,
length = 1
) {
// when length ≠ 1, extend/shrink all arms of the yarn by the appropriate percentage (length × 100%)
let offset = (1 - length) * tileSize;
let interlacement = [{ draw: drawWarp, args: [x, y + offset, warpColor] }];
interlacement[isWarpFacing ? "unshift" : "push"]({
draw: drawWeft,
args: [x + offset, y, weftColor]
});
length = 1 + (length - 1) * 2;
for (let yarn of interlacement) {
yarn.args.push(length) && yarn.draw.apply(null, yarn.args);
}
}
function drawWarp(
x,
y,
yarnColor,
/* percent of <tileSize> to draw */ length = 1
) {
context.fillStyle = yarnColor;
context.fillRect(
x + (tileSize - yarnWidth) / 2,
y,
yarnWidth,
tileSize * length
);
context.fillStyle = borderColor;
context.fillRect(
x + (tileSize - yarnWidth) / 2 - borderWidth,
y,
borderWidth,
tileSize * length
);
context.fillRect(
x + (tileSize + yarnWidth) / 2,
y,
borderWidth,
tileSize * length
);
}
function drawWeft(
x,
y,
yarnColor,
/* percent of <tileSize> to draw */ length = 1
) {
context.fillStyle = yarnColor;
context.fillRect(
x,
y + (tileSize - yarnWidth) / 2,
tileSize * length,
yarnWidth
);
context.fillStyle = borderColor;
context.fillRect(
x,
y + (tileSize - yarnWidth) / 2 - borderWidth,
tileSize * length,
borderWidth
);
context.fillRect(
x,
y + (tileSize + yarnWidth) / 2,
tileSize * length,
borderWidth
);
}
function drawWeftCurl(
x,
y,
yarnColor,
/* draw to the left of x, with loose ends on the right */ drawToLeft = true
/* or draw to the right of x, with loose ends on the left */
) {
context.fillStyle = yarnColor;
context.beginPath();
context.arc(
x,
y + tileSize,
(tileSize + yarnWidth) / 2,
HALF_PI,
3 * HALF_PI,
!drawToLeft
);
context.arc(
x,
y + tileSize,
(tileSize - yarnWidth) / 2,
3 * HALF_PI,
HALF_PI,
drawToLeft
);
context.closePath();
context.fill();
context.fillStyle = borderColor;
context.beginPath();
// outer border
context.arc(
x,
y + tileSize,
(tileSize + yarnWidth) / 2,
HALF_PI,
3 * HALF_PI,
!drawToLeft
);
context.arc(
x,
y + tileSize,
(tileSize + yarnWidth) / 2 + borderWidth,
3 * HALF_PI,
HALF_PI,
drawToLeft
);
// inner border
context.arc(
x,
y + tileSize,
(tileSize - yarnWidth) / 2,
HALF_PI,
3 * HALF_PI,
!drawToLeft
);
context.arc(
x,
y + tileSize,
(tileSize - yarnWidth) / 2 - borderWidth,
3 * HALF_PI,
HALF_PI,
drawToLeft
);
context.closePath();
context.fill();
}
return Object.freeze({
drawInterlacement,
drawPattern,
drawWarp,
drawWeft
});
}