Public
Edited
Sep 11, 2024
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

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