Public
Edited
Apr 9
Paused
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
tint = "#234"
Insert cell
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: 60
}),
swap: Inputs.toggle({ label: "Swap yarn colors", value: 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 }
),
gap: Inputs.range([0.3, 0.6], {
label: "Gap between yarns",
step: 0.05,
value: 0.45
}),
toggle: Inputs.toggle({ label: "Weft-faced", value: false })
})
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
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