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

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