T45b Site · Iso: isometric line miniatures on a canvas, hidden lines removed, cells on a marimba beat, playable under the pointer, still under reduced motion
This commit is contained in:
1 parent
ddcc8a4938
commit
02c2bbe0a1
13 files changed
+488
-15
No files matched your search
@@ -0,0 +1,162 @@
|
||||
import { bounds, faces, inside, painted, rise, type Block, type Point } from "./engine";
|
||||
import { BEAT, strike } from "./marimba";
|
||||
|
||||
const DRAW = BEAT * 2;
|
||||
const GROW = 320;
|
||||
const BUMP = 520;
|
||||
|
||||
interface Ink {
|
||||
ground: string;
|
||||
line: string;
|
||||
accent: string;
|
||||
side: string;
|
||||
shade: string;
|
||||
}
|
||||
|
||||
function ink(): Ink {
|
||||
const css = getComputedStyle(document.documentElement);
|
||||
const v = (name: string) => css.getPropertyValue(name).trim();
|
||||
return { ground: v("--bg"), line: v("--iso-line"), accent: v("--accent"), side: v("--iso-side"), shade: v("--iso-shade") };
|
||||
}
|
||||
|
||||
/** Grows cell by cell; a block under the pointer lifts and sounds. */
|
||||
export function miniature(canvas: HTMLCanvasElement, model: Block[], still: boolean) {
|
||||
const blocks = painted(model);
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
let colors = ink();
|
||||
let started: number | null = still ? -Infinity : null;
|
||||
let struck = -1;
|
||||
let frame = 0;
|
||||
let view = { scale: 1, dx: 0, dy: 0 };
|
||||
const bumps = new Map<Block, number>();
|
||||
|
||||
const fit = () => {
|
||||
const dpr = Math.min(devicePixelRatio, 2);
|
||||
const { clientWidth: w, clientHeight: h } = canvas;
|
||||
canvas.width = Math.round(w * dpr);
|
||||
canvas.height = Math.round(h * dpr);
|
||||
const [x0, y0, x1, y1] = bounds(blocks);
|
||||
const scale = Math.min((w * 0.9) / (x1 - x0), (h * 0.86) / (y1 - y0));
|
||||
view = { scale: scale * dpr, dx: (w * dpr - (x1 + x0) * scale * dpr) / 2, dy: (h * dpr - (y1 + y0) * scale * dpr) / 2 };
|
||||
};
|
||||
|
||||
const at = ([x, y]: Point): Point => [x * view.scale + view.dx, y * view.scale + view.dy];
|
||||
|
||||
const path = (poly: Point[], upto = 1) => {
|
||||
ctx.beginPath();
|
||||
const pts = poly.map(at);
|
||||
ctx.moveTo(...pts[0]);
|
||||
// The footprint draws itself edge by edge.
|
||||
const edges = pts.length * upto;
|
||||
for (let i = 1; i <= Math.floor(edges); i++) ctx.lineTo(...pts[i % pts.length]);
|
||||
const part = edges - Math.floor(edges);
|
||||
if (part > 0 && edges < pts.length) {
|
||||
const [a, b] = [pts[Math.floor(edges) % pts.length], pts[(Math.floor(edges) + 1) % pts.length]];
|
||||
ctx.lineTo(a[0] + (b[0] - a[0]) * part, a[1] + (b[1] - a[1]) * part);
|
||||
}
|
||||
};
|
||||
|
||||
const draw = (now: number) => {
|
||||
frame = 0;
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.lineJoin = "round";
|
||||
ctx.lineWidth = Math.max(1, view.scale * 0.018);
|
||||
let moving = false;
|
||||
for (const b of blocks) {
|
||||
const since = started === null ? -1 : now - started - b.beat * BEAT;
|
||||
if (since < 0) {
|
||||
moving ||= started !== null;
|
||||
continue;
|
||||
}
|
||||
const drawn = Math.min(1, since / DRAW);
|
||||
const grown = rise((since - DRAW) / GROW);
|
||||
const bump = bumps.get(b);
|
||||
const lift = bump === undefined ? 0 : Math.max(0, 1 - (now - bump) / BUMP);
|
||||
if (bump !== undefined && lift === 0) bumps.delete(b);
|
||||
moving ||= drawn < 1 || since - DRAW < GROW || lift > 0;
|
||||
const h = Math.max(b.h * grown * (1 + 0.35 * Math.sin(lift * Math.PI)), 0.0001);
|
||||
const f = faces(b, h);
|
||||
const stroke = b.accent ? colors.accent : colors.line;
|
||||
if (drawn < 1) {
|
||||
ctx.strokeStyle = stroke;
|
||||
path(f.top, drawn);
|
||||
ctx.stroke();
|
||||
continue;
|
||||
}
|
||||
for (const [poly, fill] of [[f.right, colors.shade], [f.front, colors.side], [f.top, colors.ground]] as const) {
|
||||
path(poly);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = fill;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = stroke;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
if (started !== null && !still) {
|
||||
const beat = Math.floor((now - started) / BEAT);
|
||||
for (; struck < beat; ) {
|
||||
struck++;
|
||||
const first = blocks.find((b) => b.beat === struck);
|
||||
if (first) strike(struck + Math.round(first.h * 2));
|
||||
}
|
||||
}
|
||||
if (moving) request();
|
||||
};
|
||||
|
||||
const request = () => {
|
||||
if (!frame) frame = requestAnimationFrame(draw);
|
||||
};
|
||||
|
||||
const pointer = (e: PointerEvent) => {
|
||||
if (still) return;
|
||||
const r = canvas.getBoundingClientRect();
|
||||
const dpr = canvas.width / r.width;
|
||||
const p: Point = [((e.clientX - r.left) * dpr - view.dx) / view.scale, ((e.clientY - r.top) * dpr - view.dy) / view.scale];
|
||||
for (let i = blocks.length - 1; i >= 0; i--) {
|
||||
const b = blocks[i];
|
||||
const f = faces(b, b.h);
|
||||
if (inside(p, f.top) || inside(p, f.front) || inside(p, f.right)) {
|
||||
// The table itself stays put.
|
||||
if (b.y === 0) return;
|
||||
if (!bumps.has(b)) {
|
||||
bumps.set(b, performance.now());
|
||||
strike(Math.round(b.x + b.z + b.h * 2), 0.5);
|
||||
request();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resized = new ResizeObserver(() => (fit(), request()));
|
||||
resized.observe(canvas);
|
||||
canvas.addEventListener("pointermove", pointer);
|
||||
fit();
|
||||
request();
|
||||
return {
|
||||
start() {
|
||||
// Still, it stands grown from the first frame.
|
||||
if (started !== null) return;
|
||||
started = performance.now();
|
||||
struck = -1;
|
||||
request();
|
||||
},
|
||||
replay() {
|
||||
if (still) return;
|
||||
started = performance.now();
|
||||
struck = -1;
|
||||
request();
|
||||
},
|
||||
repaint() {
|
||||
colors = ink();
|
||||
request();
|
||||
},
|
||||
stop() {
|
||||
cancelAnimationFrame(frame);
|
||||
resized.disconnect();
|
||||
canvas.removeEventListener("pointermove", pointer);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user