Public
Edited
Sep 13, 2023
Paused
Comments locked
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
Insert cell
Insert cell
Insert cell
Insert cell
weave = Weave.from(drawdown)
Insert cell
Insert cell
reverse = (($) => $.reverse($.from(drawdown)))(Weave)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
patterns = [
{ type: "plain", generator: /* dummy */ () => {} },
{
type: "twill",
generator: (width, length) =>
Array.from({ length }, (_, i) =>
Array.from({ length: width }, (_, j) => !((i + j) % 3 == 2 ? 1 : 0))
)
},
{
type: "1/3 twill",
generator: (width, length) =>
Array.from({ length }, (_, i) =>
Array.from({ length: width }, (_, j) => !((i + j) % 4 == 3 ? 1 : 0))
)
},
{
type: "2/2 twill",
generator: (width, length) =>
Array.from({ length }, (_, i) =>
Array.from({ length: width }, (_, j) =>
j % 4 == i % 4 || j % 4 == (i + 1) % 4 ? 1 - (i & 1) : i & 1
)
)
}
]
Insert cell
Diagram = ({
borderWidth = 1,
tileSize = 26,
yarnWidth = 16,

margin = 20,
scaleFactor = 1,

backgroundColor = "#f6f6f6",
borderColor = "#444",
warpColor = "hsl(200, 100%, 70%)",
weftColor = "hsl(320, 100%, 70%)",

ends = 14,
picks = 7
} = {}) => {
borderWidth *= scaleFactor;
tileSize = Math.max(
tileSize * scaleFactor,
16 * scaleFactor + borderWidth * 2
);
yarnWidth = Math.max(
16 * scaleFactor,
Math.min(yarnWidth * scaleFactor, tileSize - borderWidth * 2)
);
margin *= scaleFactor;

function render({ hasLegend = false, pattern = () => {} } = {}) {
let extent = 0.8;
let offset = {
top: margin + tileSize * 2.5,
bottom: margin + tileSize / 2,
right: margin + Math.ceil(tileSize * (hasLegend ? 5 : extent)),
left: margin + tileSize * 3
};
let width = ends * tileSize + offset.left + offset.right;
let height = picks * tileSize + offset.top + offset.bottom;

let ctx = DOM.context2d(width, height);

ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, width, height);

let fabric = Fabric(ctx, ends, picks, {
left: offset.left,
top: offset.top,
tileSize,
yarnWidth,
borderWidth,
borderColor,
warpColor,
weftColor,
pattern
});

fabric.drawPattern(true);

for (let i = 0; i < ends; i++) {
// extend warp threads on the top
fabric.drawWarp(
offset.left + i * tileSize,
margin,
warpColor,
(offset.top - margin) / tileSize
);
// extend warp threads on the bottom
fabric.drawWarp(
offset.left + i * tileSize,
offset.top + picks * tileSize,
warpColor,
(offset.bottom - margin) / tileSize
);
}

// extend weft thread on the top left
fabric.drawWeft(
margin,
offset.top,
weftColor,
(offset.left - margin - (tileSize + yarnWidth + borderWidth * 2) / 2) /
tileSize
);

// label warp/weft
let fontSize = tileSize / 2;

ctx.fillStyle = "black";
ctx.font = `${fontSize}px sans-serif`;
ctx.textBaseline = "middle";
ctx.fillText("weft", 2 * margin, offset.top + tileSize / 2);
ctx.save();
ctx.rotate(-Math.PI / 2);
ctx.textAlign = "end";
ctx.fillText(
"warp",
-2 * margin,
offset.left + tileSize / 2 - fontSize * 0.12
);
ctx.restore();

if (hasLegend) {
let gap = 5 * scaleFactor;
let X =
offset.left + Math.ceil((ends + extent) * tileSize) + margin + gap;
let Y = 1.5 * tileSize;

ctx.strokeStyle = "white";
ctx.lineWidth = gap;
ctx.beginPath();
ctx.moveTo(X - gap / 2, 0);
ctx.lineTo(X - gap / 2, height);
ctx.moveTo(X - gap / 2, height / 2);
ctx.lineTo(width, height / 2);
ctx.stroke();

for (
let lift = -1,
left = X + (width - X - tileSize) / 2,
top = tileSize * 2.25;
lift++ < 1;
top += (height + gap) / 2
) {
fabric.drawInterlacement(
left,
top,
warpColor,
weftColor,
1 - lift,
1.25
);
}

// label legend
X += (width - X) / 2;

ctx.fillStyle = "black";
ctx.textAlign = "center";
ctx.textBaseline = "alphabetic";
ctx.fillText("warp-facing", X, Y);
ctx.fillText("weft-facing", X, Y + (height + gap) / 2);
}

return ctx.canvas;
}

return Object.freeze({
render
});
}
Insert cell
Fabric = (
/* canvas */ context,
/* how many warp yarns? */ ends,
/* how many weft yarns? */ 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 = /* plain weave */ (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
});
}
Insert cell
Grid = (
data,
{ /* canvas */ context, /* offset */ left = 0, top = 0 } = {}
) => {
if (void 0 === data) {
throw "Grid is not defined";
}

if (
!Array.isArray(data) ||
!data.length ||
!Array.isArray(data[0]) ||
!data[0].length
) {
throw "Grid is empty or is not in the expected format";
}

function render({
/* dimensions */
innerThickness = 1,
outerThickness = 2,
tileSize = 26,
/* colors */
off = "white",
on = "darkgrey",
stroke = "#000",
/* crosshatching? */
crosshatching = 0
} = {}) {
if (null == context) {
left = top = 0;
}

innerThickness = Math.max(innerThickness, 0);
outerThickness = Math.max(outerThickness, 0);
tileSize = Math.max(tileSize, 12);

const cols = data[0].length;
const rows = data.length;

let hasStroke = ![null, "none"].includes(stroke);

if (!hasStroke) {
outerThickness = 0;
}

crosshatching = +crosshatching
? ((size = tileSize - 1, swatch = [off, hasStroke ? stroke : on]) => {
let tile = DOM.context2d(size, size);

for (let i = 0, thickness = 2; i < size; i++) {
let delta = i * thickness + thickness / 2 + 0.5;
tile.lineWidth = thickness;
tile.lineCap = "square";
tile.strokeStyle = swatch[i & 1];
tile.beginPath();
if (crosshatching > 0) {
/* line through bottom left to top right corners */
tile.moveTo(0, delta);
tile.lineTo(size, delta - size);
} else {
/* line through top left to bottom right corners */
tile.moveTo(delta - size, 0);
tile.lineTo(delta, size);
}
tile.stroke();
}

return {
pattern: tile.canvas,
extent: tile.canvas.width / window.devicePixelRatio
};
})()
: false;

let toggle = [off, on];

let width = 0;
let height = 0;
let delta = 0;

if (crosshatching) {
width = height = 1;
delta = tileSize - crosshatching.extent;
if (outerThickness > 0) {
outerThickness--;
}
}

width += outerThickness * 2 + cols * tileSize;
height += outerThickness * 2 + rows * tileSize;

let ctx = context || DOM.context2d(width, height);

if (hasStroke) {
ctx.strokeStyle = ctx.fillStyle = stroke;
ctx.beginPath();
if (innerThickness > 0) {
ctx.lineWidth = innerThickness;
}
if (outerThickness > 0) {
ctx.fillRect(left, top, width, height);
}
} else {
ctx.fillStyle = on;
}

if (crosshatching) {
ctx.fillRect(left, top, width, height);
}

for (
let row = 0,
skip = !!crosshatching || !hasStroke || innerThickness <= 0,
L = left + outerThickness,
T = top + outerThickness;
row < rows;

) {
let y = T + row * tileSize;
for (let col = 0; col < cols; ) {
let x = L + col * tileSize;
let hue = toggle[clamp(data[row][col])];
ctx.fillStyle = !crosshatching ? hue : hasStroke ? stroke : on;
ctx.fillRect(x, y, tileSize, tileSize);
if (crosshatching) {
if (hue === on) {
ctx.drawImage(
crosshatching.pattern,
x + delta,
y + delta,
crosshatching.extent,
crosshatching.extent
);
} else {
ctx.fillStyle = hue;
ctx.fillRect(
x + delta,
y + delta,
crosshatching.extent,
crosshatching.extent
);
}
}
if (++col === cols || skip) continue;
x = L + col * tileSize;
ctx.moveTo(x, T);
ctx.lineTo(x, T + rows * tileSize);
}
if (++row === rows || skip) continue;
y = T + row * tileSize;
ctx.moveTo(L, y);
ctx.lineTo(L + cols * tileSize, y);
}

if (hasStroke) {
ctx.closePath();
ctx.stroke();
}

return ctx.canvas;
}

return Object.freeze({
render
});
}
Insert cell
Weave = {
yield ((instance) => Object.freeze(Object.assign(() => instance, instance)))(
Object.freeze({
from(grid) {
return Array.from(
new Set(
grid.map(
(x) =>
x
.map((x) => +x)
.join("")
.match(/^(.+?)\1*$/)[1]
)
),
(x) => x.split("").map((x) => +x)
);
},
reverse(grid) {
return grid.map((x) => x.map((x) => x ^ 1));
}
})
);
}
Insert cell
clamp = (x) =>
/* return 0 if scalar value is undefined, null, NaN, false, or 0; 1 otherwise */
Math.ceil(Math.abs(1 / (1 + Math.exp(-(+x || 0))) - 0.5))
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more