Public
Edited
Jan 5, 2024
1 fork
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function drawTiles({ stroke, strokeWidth, width, height, cols, rows, side }) {
const id = "a-tile";
const tiles = Array.from({ length: rows }).flatMap((_, j) =>
Array.from({ length: cols }).map((_, i) => {
if (i === 0 && j === 0) {
const gap = strokeWidth * 2.5;
return svg`<g id=${id}>
<rect width=${side} height=${side} fill="none" stroke=${stroke} stroke-width=${strokeWidth}></rect>
</g>`;
}

return svg`<use href=${`#${id}`} transform=${`translate(${i * side},${
j * side
})`}></use>`;
})
);
console.log(tiles);
return svg`<g class="tiles">
${tiles}
</g>`;
}
Insert cell
function drawDebugMarkers(debug, { width, height, data } = {}) {
if (!debug) return;

return data.map(
({ cx, cy }) => svg`<circle cx=${cx} cy=${cy} r="3" fill="#f0f"></circle>`
);
}
Insert cell
data = {
const rnd = CSRandom.createRandom(randomize);
const { width, height } = dimensions;
const size = Math.min(width, height);
const maxRippleSize = size / 3;

let points = Array.from({ length: drops }).map(() => [
rnd.range(0, width),
rnd.range(0, height)
]);

points = spreadPoints(points, { width, height });

points = points.map(([cx, cy]) => ({ cx, cy }));

const ripples = points.map((p) => {
const { cx, cy } = p;

return generateRandomRipple({
cx,
cy,
strength: math.clamp01(rnd.gaussian() * 0.25 * 0.5 + 0.5),
t: rnd.value(),
maxSize: maxRippleSize,

seed: rnd.value()
});
});

return ripples;
}
Insert cell
function spreadPoints(
points,
{ width, height, iterations = 12, gap = 10 } = {}
) {
const delaunay = d3.Delaunay.from(points);
const voronoi = delaunay.voronoi([0, 0, width, height]);

for (let k = 0; k < iterations; k++) {
for (let i = 0; i < delaunay.points.length; i += 2) {
const cell = voronoi.cellPolygon(i >> 1);
if (cell === null) continue;

const x0 = delaunay.points[i];
const y0 = delaunay.points[i + 1];

const [x1, y1] = d3.polygonCentroid(cell);

delaunay.points[i] = x0 + (x1 - x0) * 1;
delaunay.points[i + i] = y0 + (y1 - y0) * 1;
}

voronoi.update();
}

const centeredPts = []; //chunk(delaunay.points, 2);
for (let p of voronoi.cellPolygons()) {
const centroid = d3.polygonCentroid(p);
centeredPts.push(centroid);
}

// FIX: Sometimes centeredPts don't have any points
// For timebeing, return the points
if (centeredPts.length === 0) return points;

return centeredPts;
}
Insert cell
Insert cell
Insert cell
Insert cell
sampleRipples = {
const size = 400;
const cx = size / 2;
const cy = size / 2;

const ripple = generateRandomRipple({
cx,
cy,
maxSize: size / 2,
t: sampleT,
strength: sampleStrength
});

return svg`<svg viewBox=${[0, 0, size, size]} width=${size} height=${size}>
<rect width=${size} height=${size} fill=${palette.bg}></rect>
${drawRipples(ripple)}
</svg>`;
}
Insert cell
function drawRipples(specs, { strokeWidth } = {}) {
const { cx, cy, rings } = specs;
const ringElements = rings.map((spec) =>
drawDashedCircle({ ...spec, cx, cy, strokeWidth })
);

return svg`<g class="ripple">
${ringElements}
</g>`;
}
Insert cell
function generateRandomRipple({
cx,
cy,
t = 0.5, // 0...1
strength = 0.5, // 0...1
maxSize = 100,

seed = "1"
} = {}) {
if (cx === undefined || cy === undefined) {
throw Error("Required options missing: cx, cy");
}

const rippleRnd = CSRandom.createRandom(seed);

// more strength:
// - more rings
// - more spread, i.e. more max radius

const rings = Math.round(math.lerp(3, 7, strength));
const maxRadius = math.lerp(maxSize / 3, maxSize, strength);
const minRadius = math.lerp(1, maxRadius, t);

const ringsData = math
.linspace(rings, true)
.map((uv, i, arr) => {
const rt = delayedT(t, i + 1, rings);
if (rt === 1) return;
const r = math.lerp(0, maxRadius, rt) || 0.5;

const circum = 2 * Math.PI * r;
const dashT = math.mapRange(r, minRadius, maxRadius, 0, 1, true);
const dash = math.lerp(circum / 40, circum / 10, dashT);
const gap = dash * 3;
const offset = rippleRnd.range(0, circum / 3);
const dasharray = r < minRadius ? [] : [dash, gap, dash];

return {
r,
dasharray,
offset //circum
};
})
.filter(Boolean);

return { rings: ringsData, cx, cy };
}
Insert cell
function delayedT(t, i = 1, N = 1) {
const dt = 1 / N;

let u = math.mapRange(
t,
((i - 1) * dt) / 4,
// 1 - dt,
1 - ((N - i) * dt) / 2,
0,
1,
true
);
u = d3.easeSinOut(u);
return u;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function drawDashedCircle({
cx = 0,
cy = 0,
r = 100,
stroke = palette.fg,
dasharray = [2, 1],
offset = 0,
strokeWidth = 1,
linecap = "round"
} = {}) {
const totalDashLength = d3.sum(dasharray);
const circum = 2 * Math.PI * r;
const parts = Math.floor(circum / totalDashLength);
const partLength = circum / parts;

const adjustedDasharray = dasharray.map(
(d) => (d * partLength) / totalDashLength
);

return svg`<circle cx=${cx} cy=${cy} r=${r} fill="none"
stroke=${stroke}
stroke-width=${strokeWidth}
stroke-linecap=${linecap}
stroke-dasharray=${adjustedDasharray}
stroke-dashoffset=${offset}
></circle>`;
}
Insert cell
Insert cell
strokeWidthHalf = strokeWidth / 2
Insert cell
aspectRatio = 4 / 3
Insert cell
dimensions = {
const w = Math.max(480, width);
const h = Math.floor(width / aspectRatio);

const grid = squaresInRectangle(w, h, 6);

return {
aspectRatio,
width: grid.side * grid.cols,
height: grid.side * grid.rows,
...grid
};
}
Insert cell
palette = ({
bg: "hsl(0,0%,95%)",
fg: "hsl(0,0%,5%)"
})
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