Public
Edited
Sep 11, 2024
Comments locked
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
inputs = (({ draft, hue, swap, toggle, warpColor, weftColor, ...rest }) => {
let hslColors = [warpColor, weftColor].map((x) => d3.hsl(x));

for (let hslColor of hslColors) {
hslColor = adjust(hslColor, hue);
}

if (swap) {
hslColors.reverse();
}

[warpColor, weftColor] = hslColors;

return Object.freeze({
pattern: draft
.reduce((accumulator, value) => {
const index = Math.floor(value / slots.length);
if (accumulator.length - 1 < index) {
accumulator[index] = [];
}
accumulator[index].push(value % slots.length);
return accumulator;
}, eval("[" + Array(slots.length).fill("[]").join(",") + "]") /* essential!? */)
.map((x) =>
x.reduce((accumulator, value) => {
accumulator[value] = 1 - toggle;
return accumulator;
}, Array(slots.length).fill(0 + toggle))
),
warpUp: warpColor.toString(),
warpDown: warpColor.darker(0.5).toString(),
weftUp: weftColor.toString(),
weftDown: weftColor.darker(0.5).toString(),
...rest
});
})(widget)
Insert cell
Insert cell
Insert cell
Insert cell
fabric = {
const frame = d3.select(canvas).append("g");
const [lower, upper, hatch] = Array.from({ length: 3 }, () =>
frame.append("g")
);

return { frame, lower, upper, hatch };
}
Insert cell
Insert cell
{
const layers = { lower: 0, upper: 1 };
const components = {
weft: [weftData, weftPick],
warp: [warpData, warpEnd]
};

fabric.hatch.selectAll("*").remove();

RENDER: for (const container of Object.keys(layers)) {
const callback = (d) => d[0].z === layers[container];

for (const [yarn, component] of Object.entries(components)) {
const [data, commands] = component;

fabric[container]
.selectAll(`.${container}.${yarn}`)
.data(data.filter(callback))
.join("path")
.attr("class", (d) =>
`${container} ${yarn} ${d[0].treadle || ""}`.trim()
)
.attr("d", (d) => commands(d));
}
}

const { x, y, width: w, height: h } = fabric.frame.node().getBBox();

fabric.hatch
.append("rect")
.attr("class", "hatch")
.attr("x", x)
.attr("y", y)
.attr("width", w)
.attr("height", h)
.attr("opacity", Math.min(16 / picks, 1));

fabric.frame.attr(
"transform",
`translate(${(width - w) / 2 - x}, ${(height - h) / 2 - y})`
);

yield [x, y, w, h];
}
Insert cell
Insert cell
weftData = d3
.range(picks)
.map((weft) =>
d3.range(ends - 1).map((warp) => {
const base = { x: warp, y: weft };
const [current, next] = [
1 - patternAt(warp, weft),
1 - patternAt(warp + 1, weft)
];
const transitionZ = +!!(current && next);
const treadle = direction(current, next);

return [
[
{
...base,
offset: offset.start(warp),
size: current,
z: current
},
{
...base,
offset: offset.first(warp),
size: current,
z: current
}
],
[
// Use 4 points to define the curve of this transitional shape
{
...base,
offset: offset.first(warp),
size: current,
z: transitionZ,
treadle
},
{
...base,
offset: offset.first(warp) + 0.1,
size: current,
z: transitionZ,
treadle
},
{
...base,
offset: offset.second(warp) - 0.1,
size: next,
z: transitionZ
},
{
...base,
offset: offset.second(warp),
size: next,
z: transitionZ
}
],
[
{ ...base, offset: offset.second(warp), size: next, z: next },
{
...base,
offset: offset.end(warp, ends),
size: next,
z: next
}
]
];
})
)
.flat(2)
Insert cell
warpData = d3
.range(ends)
.map((warp) =>
d3.range(picks - 1).map((weft) => {
const base = { x: warp, y: weft };
const [current, next] = [
patternAt(warp, weft),
patternAt(warp, weft + 1)
];
const transitionZ = +!!(current && next);
const treadle = direction(current, next);

return [
[
{ ...base, offset: offset.start(weft), size: current, z: current },
{ ...base, offset: offset.first(weft), size: current, z: current }
],
[
// Use 4 points to define the curve of this transitional shape
{
...base,
offset: offset.first(weft),
size: current,
z: transitionZ,
treadle
},
{
...base,
offset: offset.first(weft) + 0.1,
size: current,
z: transitionZ,
treadle
},
{
...base,
offset: offset.second(weft) - 0.1,
size: next,
z: transitionZ
},
{
...base,
offset: offset.second(weft),
size: next,
z: transitionZ
}
],
[
{
...base,
offset: offset.second(weft),
size: next,
z: next
},
{
...base,
offset: offset.end(weft, picks),
size: next,
z: next
}
]
];
})
)
.flat(2)
Insert cell
Insert cell
Insert cell
Insert cell
offset = ({
start(x) {
return x === 0 ? -1 : 0;
},
first(x) {
return 0.5 - inputs.gap * 0.5 + inputs.gap * 0.1;
},
second(x) {
return 0.5 + inputs.gap * 0.5 - inputs.gap * 0.1;
},
end(x, total) {
return x === total - 2 ? 2 : 1;
}
})
Insert cell
Insert cell
patternAt = (warp, weft) =>
inputs.pattern[weft % inputs.pattern.length][warp % inputs.pattern[0].length]
Insert cell
direction = (current, next) => {
if (current === 0 && next === 1) {
return "upward";
}
if (current === 1 && next === 0) {
return "downward";
}
}
Insert cell
weftPick = d3
.area()
.curve(d3.curveBasis)
.x((d) => scale.x(d.x) + scale.x.bandwidth() * (d.offset + 0.5))
.y0((d) => scale.y(d.y) + scale.y.bandwidth() * scale.size(d.size))
.y1((d) => scale.y(d.y) + scale.y.bandwidth() * (1 - scale.size(d.size)))
.context(null)
Insert cell
warpEnd = d3
.area()
.curve(d3.curveBasis)
.x0((d) => scale.x(d.x) + scale.x.bandwidth() * scale.size(d.size))
.x1((d) => scale.x(d.x) + scale.x.bandwidth() * (1 - scale.size(d.size)))
.y((d) => scale.y(d.y) + scale.y.bandwidth() * (d.offset + 0.5))
.context(null)
Insert cell
scale = ({
x: d3.scaleBand(d3.range(-1, ends + 2), [0, innerWidth]),
y: d3.scaleBand(d3.range(-1, picks + 2), [0, innerHeight]),
size: d3.scaleLinear([0, 1], [inputs.gap / 2, inputs.gap / 2.5])
})
Insert cell
picks = inputs.size
Insert cell
ends = inputs.size
Insert cell
Insert cell
Insert cell
properties.set()
Insert cell
properties = {
const prefix = "--fabric-";
yield {
set({ draft, warpColor, weftColor, ...rest } = widget) {
for (const [property, value] of Object.entries(rest)) {
root.style.setProperty(prefix + property, value);
}
return rest;
},
zap({ draft, warpColor, weftColor, ...rest } = widget) {
for (const property of Object.keys(rest)) {
root.style.removeProperty(prefix + property);
}
}
};
}
Insert cell
options = Array.isArray(draft) && draft.length ? { draft } : {}
Insert cell
form = ({
draft = [0, 1, 2, 5, 6, 7, 8, 10, 11, 12, 13, 15],
warpColor = "#7da9e3",
weftColor = "#f7f7f7"
} = {}) =>
Inputs.form({
draft: Object.assign(
Inputs.checkbox(
Array.from({ length: slots.length ** 2 }, (_, i) => i),
{
label: "Pattern",
keyof: (d) => "",
value: draft
}
),
{ id: +new Date() }
),
warpColor: Inputs.text({
label: "Warp color",
value: warpColor,
disabled: true
}),
weftColor: Inputs.text({
label: "Weft color",
value: weftColor,
disabled: true
}),
hue: Inputs.range([0, 360], {
label: "Adjust hue",
step: 5,
value: root.style.getPropertyValue("--fabric-hue") * 1 || 60
}),
swap: Inputs.toggle({
label: "Swap yarn colors",
value: root.style.getPropertyValue("--fabric-swap") === "true" || false
}),
size: Inputs.range(
[
Math.max(slots.length, 2),
Math.max(slots.length, 2) * (Math.min(slots.length, 4) + 3)
],
{
label: "Fabric size",
step: slots.length,
value: root.style.getPropertyValue("--fabric-size") * 1 || void 0
}
),
gap: Inputs.range([0.3, 0.6], {
label: "Gap between yarns",
step: 0.05,
value: root.style.getPropertyValue("--fabric-gap") * 1 || 0.45
}),
toggle: Inputs.toggle({
label: "Weft-faced",
value: root.style.getPropertyValue("--fabric-toggle") === "true" || false
})
})
Insert cell
root = document.documentElement
Insert cell
slots = grid("X X X X")
Insert cell
grid = (/* template */ areas, value = areas.match(/\S+/g) || []) =>
Object.freeze(
Object.defineProperties(value, {
areas: { value },
template: {
get: function () {
return this + "";
}
},
toString: {
value: function () {
return '"' + this.join(" ") + '"';
}
},
valueOf: {
value: function () {
return [this].join("");
}
}
})
)
Insert cell
Insert cell
Insert cell
margin = {
const min = Math.min(width, height);
const [top, right] = [
Math.max(height - min, 0) / 2,
Math.max(width - min, 0) / 2
].map((x) => x + 10);
return {
top,
right,
bottom: top,
left: right
};
}
Insert cell
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