Published
Edited
Sep 29, 2022
6 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
// Sets the random see so that, when `showGrid` is changed
// new points are not generated.
random.setSeed(seed + randomize);

clear();

drawLines(linesSet);

if (debug) {
drawBounds();
drawSquares(linesSet);
drawPts(linesSet);
}

return `Iteration: ${randomize}`;
}
Insert cell
Insert cell
Insert cell
lineLengthUV = 1 / 8
Insert cell
linesSet = {
randomize;

const set = math
.linspace(rows, true)
.map((v, j) =>
math.linspace(cols, true).map((u, i) => {
const index = j * cols + i;
const lineDirections = getDirections(index);
const minDistanceUV = math.inverseLerp(1, 4, lineDirections.length);
const minDistance = math.lerp(1 / 15, 1 / 25, minDistanceUV);
const pts = poissonSampling(1, 1, { minDistance });
const shuffledPts = random.shuffle(pts);
let lines = [];

const chunkSize = Math.floor(pts.length / lineDirections.length);
const chunkedPts = chunk(shuffledPts, chunkSize);

lineDirections.forEach((dir, i) => {
let dirLines = generateLinesAtPts(chunkedPts[i], dir);
dirLines = geom.clipPolylinesToBox(
dirLines,
[0, 0, 1, 1],
false,
false
);
dirLines = dirLines.map((l) => [...l, dir]);
lines = [...lines, ...dirLines];
});

return {
midpoint: [u, v],
pts,
lines
};
})
)
.flat();

return set;
}
Insert cell
generateLinesAtPts = (pts, direction) => {
const angle = getAngleFromDirection(direction);

const lines = pts.map(([u, v]) => {
const u1 = u + lineLengthUV * Math.cos(angle);
const v1 = v + lineLengthUV * Math.sin(angle);

const u2 = u - lineLengthUV * Math.cos(angle);
const v2 = v - lineLengthUV * Math.sin(angle);

return [
[u1, v1],
[u2, v2]
];
});

return lines;
}
Insert cell
drawBounds = () => {
ctx.beginPath();
ctx.strokeStyle = palette.debug;
ctx.lineCap = "round";
ctx.lineWidth = 1;
ctx.rect(marginLeft, marginTop, gridWidth, gridHeight);
ctx.stroke();
}
Insert cell
uv2xy = (u, v, xMin, xMax, yMin, yMax) => {
return [math.lerp(xMin, xMax, u), math.lerp(yMin, yMax, v)];
}
Insert cell
drawLines = (linesSet) => {
linesSet.forEach(({ midpoint, lines }) => {
const { x, y, size } = getSquareBounds(...midpoint);
lines.forEach((l) => {
let [[u1, v1], [u2, v2], dir] = l;

let [x1, y1] = uv2xy(u1, v1, x, x + size, y, y + size);
let [x2, y2] = uv2xy(u2, v2, x, x + size, y, y + size);

drawLineWithOvershoot(
x1,
y1,
x2,
y2,
lineWidth,
palette[getColorFromDirection(dir)]
);
});
});
}
Insert cell
drawSquares = (linesSet) => {
ctx.beginPath();
ctx.strokeStyle = palette.debug;
ctx.lineCap = "round";
ctx.lineWidth = 1;
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const index = i * rows + j;
const { x, y, size } = getSquareBounds(...linesSet[index].midpoint);

ctx.rect(x, y, size, size);
}
}
ctx.stroke();
}
Insert cell
drawPts = (linesSet) => {
ctx.beginPath();
ctx.fillStyle = palette.debug;
linesSet.forEach(({ midpoint, pts }) => {
const { x, y, size } = getSquareBounds(...midpoint);

pts.forEach(([u, v]) => {
const px = math.lerp(x, x + size, u);
const py = math.lerp(y, y + size, v);
ctx.moveTo(px, py);
ctx.arc(px, py, 1, 0, 2 * Math.PI);
});
});
ctx.fill();
}
Insert cell
getSquareBounds = (u, v) => {
const cx = math.lerp(
marginLeft + cellSize / 2,
marginLeft + gridWidth - cellSize / 2,
u
);
const cy = math.lerp(
marginTop + cellSize / 2,
marginTop + gridHeight - cellSize / 2,
v
);

return {
x: cx - cellSize / 2,
y: cy - cellSize / 2,
size: cellSize
};
}
Insert cell
directions = ({
vertical: { color: "yellow", angle: Math.PI / 2 },
horizontal: { color: "black", angle: 0 },
diagDown: { color: "blue", angle: Math.PI / 4 },
diagUp: { color: "red", angle: -Math.PI / 4 }
})
Insert cell
palette = ({
paper: `hsla(42, 32%, 95%, 1.00)`, //`hsl(26, 26%, 98%)`,
black: `hsl(26,15%,15%)`,
yellow: `hsla(44, 79%, 58%, 1.00)`,
red: `hsla(5, 55%, 45%, 1.00)`,
blue: `hsla(214, 62%, 44%, 1.00)`,
debug: "#FF00FF"
})
Insert cell
getAngleFromDirection = (dir) => directions[dir].angle || 0
Insert cell
getColorFromDirection = (dir) => directions[dir].color || 0
Insert cell
getDirections = (index) =>
isShowingSingle()
? random.pick(allDirectionCombinations)
: allDirectionCombinations[index] || []
Insert cell
Insert cell
lineWidth = isShowingSingle() ? 1.5 : 1
Insert cell
drawLineWithOvershoot = (...args) => {
const [x1, y1, x2, y2, lineWidth, ...rest] = args;
const m = slope(x1, y1, x2, y2);
const angle = Math.atan(m);

const maxExtU = 1 - math.clamp01(math.inverseLerp(5, 1, lineWidth));
const maxExt = lineWidth * math.lerp(4, 0.5, maxExtU);

const ext1 = random.value();
const ext2 = random.value();

let xe1 = x1,
ye1 = y1,
xe2 = x2,
ye2 = y2;
if (m >= 0) {
// line move upwards when x moves from left to right
xe1 = x1 - maxExt * ext1 * Math.cos(angle);
ye1 = y1 - maxExt * ext1 * Math.sin(angle);

xe2 = x2 + maxExt * ext2 * Math.cos(angle);
ye2 = y2 + maxExt * ext2 * Math.sin(angle);
}

drawLine(xe1, ye1, xe2, ye2, lineWidth, ...rest);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
ratio = rows / cols
Insert cell
Insert cell
width
Insert cell
maxSqSize = Math.min(width / cols, height / rows)
Insert cell
maxGridWidth = maxSqSize * cols
Insert cell
maxGridHeight = maxSqSize * rows
Insert cell
maxSize = Math.min(height, width)
Insert cell
Insert cell
Insert cell
Insert cell
gridWidth = width - 2 * marginLeft
Insert cell
gridHeight = height - 2 * marginTop
Insert cell
Insert cell
cellSize = {
const cellWidth = gridHeight / rows;
return isShowingSingle() ? cellWidth : cellWidth - cellPadding / 2;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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