Public
Edited
Aug 16, 2023
6 stars
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
rows = size
Insert cell
cols = size
Insert cell
Insert cell
rules = ({
// In the order top, right, bottom, left (clockwise order)
[tile.blank]: [
[tile.blank, tile.up], // top
[tile.blank, tile.right], // right
[tile.blank, tile.down], // bottom
[tile.blank, tile.left] //left
],
[tile.up]: [
[tile.right, tile.left, tile.down], // top
[tile.up, tile.down, tile.left], // right
[tile.blank, tile.down], // bottom
[tile.down, tile.up, tile.right] // left
],
[tile.down]: [
[tile.blank, tile.up], // top
[tile.left, tile.up, tile.down], // right
[tile.right, tile.left, tile.up], // bottom
[tile.up, tile.down, tile.right] // left
],
[tile.right]: [
[tile.right, tile.left, tile.down], // top
[tile.left, tile.up, tile.down], // right
[tile.right, tile.left, tile.up], // bottom
[tile.blank, tile.left] // left
],
[tile.left]: [
[tile.right, tile.left, tile.down], // top
[tile.blank, tile.right], // right
[tile.right, tile.left, tile.up], // bottom
[tile.up, tile.down, tile.right] // left
]
})
Insert cell
function drawTile(pattern, { palette, props } = {}) {
const { tileWidth, tileHeight, strokeWidth = 15 } = props;
const highlightStrokeWidth = strokeWidth / 10;
let paths = [];
switch (pattern) {
case tile.down: // down
paths.push(svg`<path d="
M0,${tileHeight / 2} h ${tileWidth}
M${tileWidth / 2},${tileHeight / 2} v ${tileHeight / 2}"></path>`);
paths.push(svg`<path d="
M${tileWidth / 2},${tileHeight}
Q${tileWidth / 2},${tileHeight / 2} 0,${tileHeight / 2}
M${tileWidth / 2},${tileHeight}
Q${tileWidth / 2},${tileHeight / 2} ${tileWidth},${tileHeight / 2}
"></path>`);
break;
case tile.up: // up
paths.push(svg`<path d="
M0,${tileHeight / 2} h ${tileWidth}
M${tileWidth / 2},${tileHeight / 2} v ${-tileHeight / 2}"></path>`);
break;
case tile.right: // right
paths.push(svg`<path d="
M${tileWidth / 2},0 v ${tileHeight}
M${tileWidth / 2},${tileHeight / 2} h ${tileWidth / 2}"></path>`);
paths.push(svg`<path d="
M${tileWidth / 2},${tileHeight}
Q${tileWidth / 2},${tileHeight / 2} ${tileWidth},${tileHeight / 2}
"></path>`);
break;
case tile.left: // left
paths.push(svg`<path d="
M${tileWidth / 2},0 v ${tileHeight}
M${tileWidth / 2},${tileHeight / 2} h ${-tileWidth / 2}"></path>`);
paths.push(svg`<path d="
M${tileWidth / 2},${tileHeight}
Q${tileWidth / 2},${tileHeight / 2} 0,${tileHeight / 2}
"></path>`);
break;
}
// stroke-linecap="round"
return svg`<g fill="none"
stroke=${palette.fg}
stroke-width=${strokeWidth}
stroke-linecap="round"
>
<rect width=${tileWidth} height=${tileHeight}
stroke="none" fill=${palette.bg}></rect>
${paths}
</g>`;
}
Insert cell
function drawTiles({ data = [], props, palette, strokeWidth }) {
const { tileWidth, tileHeight } = props;
const tiles = data
.filter((d) => d.collapsed)
.map(({ xIndex, yIndex, options }) => {
return svg`<g transform="translate(${xIndex * tileWidth}, ${
yIndex * tileHeight
})">
${drawTile(options[0] ?? 0, { palette, props, strokeWidth })}
</g>`;
});
return svg`<g class="tiles">${tiles}</g>`;
}
Insert cell
function removeInvalidOptions(options, neighborsOptions, dirIndex) {
let newOptions = neighborsOptions.flatMap((no) =>
options.filter((possible) => rules[no][dirIndex].includes(possible))
);
newOptions = _.uniq(newOptions);
if (newOptions.length === 0) {
console.log("Non fitting tile", options, newOptions);
return options;
}
return newOptions;
}
Insert cell
data = {
// To make sure the random things are always generated in the order:
// 1. color palette
// 2. data
// 3. wave collapsing
randomPalette;

let base = d3.range(rows).flatMap((j) => {
return d3.range(cols).map((i) => {
return {
xIndex: i,
yIndex: j,
collapsed: false,
options: Object.values(tile)
};
});
});

while (base.filter((d) => !d.collapsed).length) {
const next = _.cloneDeep(base).map((d, index, arr) => {
if (d.collapsed) return d;

const i = index % cols;
const j = Math.floor(index / cols);
const options = Object.values(tile);

// Look up
if (j > 0) {
let up = arr[i + (j - 1) * cols];
d.options = removeInvalidOptions(options, up.options, 2);
}
// look right
if (i < cols - 1) {
let right = arr[i + 1 + j * cols];
d.options = removeInvalidOptions(d.options, right.options, 3);
}
// look down
if (j < rows - 1) {
let down = arr[i + (j + 1) * cols];
d.options = removeInvalidOptions(d.options, down.options, 0);
}
// look left
if (i > 0) {
let left = arr[i - 1 + j * cols];
d.options = removeInvalidOptions(d.options, left.options, 1);
}

return d;
});

base = _.cloneDeep(next);

// Pick a cell with least entropy
// base[2].options = [0, 1];
// base[7].options = [0, 1];
let copy = base
.slice()
.filter((d) => !d.collapsed)
.sort((a, b) => a.options.length - b.options.length);
const baseLength = copy[0].options.length;
copy = copy.filter((c) => c.options.length === baseLength);

const cell = rnd.pick(copy);
if (cell.options.length === 0) {
console.log(cell, copy);
return base;
}
cell.collapsed = true;
cell.options = [rnd.pick(cell.options)];

yield base;
// await Promises.delay(250);
}
}
Insert cell
function drawDebugMarks(debug = false, { props, palette, cols, rows }) {
if (!debug) return "";
const { tileWidth, tileHeight } = props;
const cells = d3.range(rows).flatMap((j) => {
return d3.range(cols).map((i) => {
return svg`<rect width=${tileWidth}
height=${tileHeight}
x=${i * tileWidth}
y=${j * tileHeight}
stroke=${palette.debug}
fill="none"
></rect>`;
});
});

return svg`<g class="debug">
${cells}
</g>`;
}
Insert cell
aspectRatio = 1
Insert cell
props = {
const w = Math.max(480, width);
const h = Math.floor(width / aspectRatio);
const tileWidth = w / cols;
const tileHeight = h / rows;

const strokeWidth = math.lerp(0.5, w / 35, strokeWidthUV);
return {
aspectRatio,
width: w,
height: h,
tileWidth,
tileHeight,
strokeWidth
};
}
Insert cell
randomPalette = rnd.pick(colors)
Insert cell
palette = {
const debug = "#f0f";
const base = {
// greyscale
bg: "hsl(0,0%,95%)",
fg: "hsl(0,0%,5%)",
debug
};

if (colourScheme === "Greyscale") return base;

const { primary, accent } = randomPalette;
return { ...base, bg: primary, fg: accent };
}
Insert cell
rnd = {
CSRandom.setSeed(randomize);
return CSRandom;
}
Insert cell
svg = htl.svg
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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