This commit is contained in:
Felix Klement committed 2026-09-20 12:45:33 +02:00
1 parent b244287f6a
commit 5a2d49c17f
77 files changed
+2289 -31

No files matched your search

+54 -27
View File
@@ -55,7 +55,10 @@ function plane(n: number, u: number, y: number, cy: number, cx = ISO.cx): string
return out;
}
const lines: string[] = [];
/* Each drawing is split in two: the lines that carry its idea, and the grid around them. A
hover lets the grid recede so the idea stands on its own. */
const base: string[] = [];
const accent: string[] = [];
if (kind === 'sphere') {
const R = 150, tilt = (22 * Math.PI) / 180, N = 48;
@@ -70,15 +73,15 @@ if (kind === 'sphere') {
const lat = (d * Math.PI) / 180;
const pts: [number, number][] = [];
for (let i = 0; i <= N; i++) pts.push(p3(lat, (i / N) * 2 * Math.PI));
lines.push(poly(pts));
(d === 0 ? accent : base).push(poly(pts));
}
for (let m = 0; m < 8; m++) {
const lon = (m * Math.PI) / 8;
const pts: [number, number][] = [];
for (let i = 0; i <= N; i++) pts.push(p3((i / N) * 2 * Math.PI - Math.PI, lon));
lines.push(poly(pts));
(m === 0 ? accent : base).push(poly(pts));
}
lines.push(`circle:${R}`);
accent.push(`circle:${R}`);
track(ISO.cx - R, ISO.cy - R);
track(ISO.cx + R, ISO.cy + R);
}
@@ -87,12 +90,14 @@ if (kind === 'cube') {
const n = 4, u = 46, L = n * u, cy = ISO.cy + L * 0.5;
for (let i = 0; i <= n; i++) {
const t = i * u;
lines.push(poly([iso(t, L, 0, cy), iso(t, L, L, cy)]));
lines.push(poly([iso(0, L, t, cy), iso(L, L, t, cy)]));
lines.push(poly([iso(t, 0, 0, cy), iso(t, L, 0, cy)]));
lines.push(poly([iso(0, t, 0, cy), iso(L, t, 0, cy)]));
lines.push(poly([iso(0, 0, t, cy), iso(0, L, t, cy)]));
lines.push(poly([iso(0, t, 0, cy), iso(0, t, L, cy)]));
/* The first and last line of each face are the cube's own edges. */
const target = i === 0 || i === n ? accent : base;
target.push(poly([iso(t, L, 0, cy), iso(t, L, L, cy)]));
target.push(poly([iso(0, L, t, cy), iso(L, L, t, cy)]));
target.push(poly([iso(t, 0, 0, cy), iso(t, L, 0, cy)]));
target.push(poly([iso(0, t, 0, cy), iso(L, t, 0, cy)]));
target.push(poly([iso(0, 0, t, cy), iso(0, L, t, cy)]));
target.push(poly([iso(0, t, 0, cy), iso(0, t, L, cy)]));
}
}
@@ -100,28 +105,30 @@ if (kind === 'plane') {
const n = 6, u = 34, L = n * u, cy = ISO.cy + 20, h = 96, tx = 2, tz = 2;
for (let i = 0; i <= n; i++) {
const t = i * u;
lines.push(poly([iso(t, 0, 0, cy), iso(t, 0, L, cy)]));
lines.push(poly([iso(0, 0, t, cy), iso(L, 0, t, cy)]));
base.push(poly([iso(t, 0, 0, cy), iso(t, 0, L, cy)]));
base.push(poly([iso(0, 0, t, cy), iso(L, 0, t, cy)]));
}
/* The tile that has been lifted out for inspection. */
const c: [number, number][] = [[tx, tz], [tx + 1, tz], [tx + 1, tz + 1], [tx, tz + 1]];
const top = c.map(([x, z]) => iso(x * u, h, z * u, cy));
lines.push(poly([...top, top[0]]));
c.forEach(([x, z], i) => lines.push(poly([iso(x * u, 0, z * u, cy), top[i]])));
accent.push(poly([...top, top[0]]));
c.forEach(([x, z], i) => accent.push(poly([iso(x * u, 0, z * u, cy), top[i]])));
}
if (kind === 'wave') {
const nx = 12, nz = 8, u = 26, cy = ISO.cy + 10, A = 26;
const yAt = (x: number, z: number) => A * Math.sin((x / nx) * Math.PI * 2 - 0.6) * Math.cos((z / nz) * Math.PI * 0.9);
const P = (x: number, z: number) => iso((x - nx / 2) * u, yAt(x, z), (z - nz / 2) * u, cy);
/* The lines along x carry the wave itself; the cross lines are the grid it is read against. */
for (let z = 0; z <= nz; z++) {
const pts: [number, number][] = [];
for (let x = 0; x <= nx; x++) pts.push(P(x, z));
lines.push(poly(pts));
accent.push(poly(pts));
}
for (let x = 0; x <= nx; x++) {
const pts: [number, number][] = [];
for (let z = 0; z <= nz; z++) pts.push(P(x, z));
lines.push(poly(pts));
base.push(poly(pts));
}
}
@@ -131,29 +138,33 @@ if (kind === 'steps') {
const total = heights.length * w + (heights.length - 1) * gap;
heights.forEach((h, i) => {
const x0 = -total / 2 + i * (w + gap);
cuboid(x0, 0, -depth / 2, w, h, depth, cy).forEach((l) => lines.push(l));
/* The first column is the one to fix first. */
cuboid(x0, 0, -depth / 2, w, h, depth, cy).forEach((l) => (i === 0 ? accent : base).push(l));
});
const L = total + 48;
lines.push(poly([iso(-L / 2, 0, -depth / 2 - 24, cy), iso(L / 2, 0, -depth / 2 - 24, cy)]));
base.push(poly([iso(-L / 2, 0, -depth / 2 - 24, cy), iso(L / 2, 0, -depth / 2 - 24, cy)]));
}
if (kind === 'nested') {
/* A structure inside its own walls: a cube inside a cube, corners tied together. */
const O = 200, I = 96, cy = ISO.cy + 84;
cuboid(-O / 2, 0, -O / 2, O, O, O, cy).forEach((l) => lines.push(l));
cuboid(-O / 2, 0, -O / 2, O, O, O, cy).forEach((l) => base.push(l));
const off = (O - I) / 2;
cuboid(-I / 2, off, -I / 2, I, I, I, cy).forEach((l) => lines.push(l));
cuboid(-I / 2, off, -I / 2, I, I, I, cy).forEach((l) => accent.push(l));
const outer = [[-O / 2, 0, -O / 2], [O / 2, 0, -O / 2], [O / 2, 0, O / 2], [-O / 2, 0, O / 2], [-O / 2, O, -O / 2], [O / 2, O, -O / 2], [O / 2, O, O / 2], [-O / 2, O, O / 2]];
const inner = [[-I / 2, off, -I / 2], [I / 2, off, -I / 2], [I / 2, off, I / 2], [-I / 2, off, I / 2], [-I / 2, off + I, -I / 2], [I / 2, off + I, -I / 2], [I / 2, off + I, I / 2], [-I / 2, off + I, I / 2]];
outer.forEach(([x, y, z], k) => lines.push(poly([iso(x, y, z, cy), iso(inner[k][0], inner[k][1], inner[k][2], cy)])));
outer.forEach(([x, y, z], k) => base.push(poly([iso(x, y, z, cy), iso(inner[k][0], inner[k][1], inner[k][2], cy)])));
}
if (kind === 'stack') {
/* Layers one on top of the other, each a record kept whole. */
const n = 4, u = 38, L = n * u, gapY = 46, layers = 4, cy = ISO.cy + 60;
for (let k = 0; k < layers; k++) plane(n, u, k * gapY, cy).forEach((l) => lines.push(l));
/* The newest layer sits on top of the ones already kept. */
for (let k = 0; k < layers; k++) {
plane(n, u, k * gapY, cy).forEach((l) => (k === layers - 1 ? accent : base).push(l));
}
const corners: [number, number][] = [[0, 0], [L, 0], [L, L], [0, L]];
corners.forEach(([x, z]) => lines.push(poly([iso(x, 0, z, cy), iso(x, (layers - 1) * gapY, z, cy)])));
corners.forEach(([x, z]) => base.push(poly([iso(x, 0, z, cy), iso(x, (layers - 1) * gapY, z, cy)])));
}
/* A square box centred on the drawing: wide drawings still fill the width, and every object
keeps the same footprint, so the four cards of a row stay even. The padding covers the
@@ -165,10 +176,17 @@ const side = Math.max(width, height) + PAD * 2;
const viewBox = [minX + width / 2 - side / 2, minY + height / 2 - side / 2, side, side].map(f).join(' ');
---
<svg class:list={['grid-object', className]} viewBox={viewBox} aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-width="1.25" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke">
{lines.map((l) => l.startsWith('circle:')
? <circle cx={S / 2} cy={S / 2} r={Number(l.slice(7))} />
: <polyline points={l} />)}
<g class="grid-object__lines" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke">
<g class="grid-object__base">
{base.map((l) => l.startsWith('circle:')
? <circle cx={S / 2} cy={S / 2} r={Number(l.slice(7))} />
: <polyline points={l} />)}
</g>
<g class="grid-object__accent">
{accent.map((l) => l.startsWith('circle:')
? <circle cx={S / 2} cy={S / 2} r={Number(l.slice(7))} />
: <polyline points={l} />)}
</g>
</g>
</svg>
@@ -177,5 +195,14 @@ const viewBox = [minX + width / 2 - side / 2, minY + height / 2 - side / 2, side
width: 100%;
height: auto;
color: var(--ink);
/* Both are driven by the card around it, so the card owns the interaction and the drawing
owns how it answers. */
transform: scale(var(--object-scale, 1));
transform-origin: 50% 50%;
transition: transform var(--dur) var(--ease);
}
.grid-object__base {
opacity: var(--object-grid, 1);
transition: opacity var(--dur) var(--ease);
}
</style>
+7
View File
@@ -52,6 +52,13 @@ const objects = ['sphere', 'cube', 'plane', 'wave'] as const;
justify-self: center;
align-self: center;
}
/* Hovering a card lets its grid recede, so the shape that carries the idea stands alone. */
@media (hover: hover) {
.stages__item:hover {
--object-scale: 1.03;
--object-grid: 0.26;
}
}
.stages__body { max-width: 24em; }
@media (min-width: 48rem) {
.stages__list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+7
View File
@@ -41,6 +41,13 @@ const objects = ['steps', 'nested', 'stack'] as const;
hyphens: auto;
}
.vcards__item :global(.vcards__object) { width: 100%; max-width: 14rem; justify-self: center; align-self: center; }
/* Hovering a card lets its grid recede, so the shape that carries the idea stands alone. */
@media (hover: hover) {
.vcards__item:hover {
--object-scale: 1.03;
--object-grid: 0.26;
}
}
.vcards__body { max-width: 26em; }
@media (min-width: 48rem) {
.vcards__list { grid-template-columns: repeat(3, minmax(0, 1fr)); }