T45c Site · Home: a full-screen hero, five live features beside their miniatures, counts on the beat, the chapters as a city, a closing call; smooth scroll, reveals from scroll, a fade between routes; the OG image from the new hero

This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 08:38:05 -04:00
1 parent 7fd05df4dc
commit b47f2267e4
10 files changed
+355 -49

No files matched your search

+28 -7
View File
@@ -5,22 +5,34 @@ import type { Block } from "./engine";
const REDUCED = matchMedia("(prefers-reduced-motion: reduce)");
function useStill(): boolean {
/** Whether the system asks for reduced motion, kept current. */
export function useStill(): boolean {
return useSyncExternalStore(
(listen) => (REDUCED.addEventListener("change", listen), () => REDUCED.removeEventListener("change", listen)),
() => REDUCED.matches,
);
}
/** Grows once it shows; a press grows it again. */
export function Iso({ model, label, className }: { model: Block[]; label: string; className?: string }) {
interface Props {
model: Block[];
label: string;
className?: string;
onHover?: (index: number | null) => void;
onPick?: (index: number) => void;
lift?: number | null;
}
/** Grows once shown; a press regrows it or picks a block. */
export function Iso({ model, label, className, onHover, onPick, lift }: Props) {
const canvas = useRef<HTMLCanvasElement>(null);
const mini = useRef<ReturnType<typeof miniature> | null>(null);
const hover = useRef(onHover);
hover.current = onHover;
const theme = useMode();
const still = useStill();
useEffect(() => {
const m = miniature(canvas.current!, model, still);
const m = miniature(canvas.current!, model, still, (i) => hover.current?.(i));
mini.current = m;
const seen = new IntersectionObserver(([e]) => e.isIntersecting && m.start(), { threshold: 0.35 });
seen.observe(canvas.current!);
@@ -31,11 +43,20 @@ export function Iso({ model, label, className }: { model: Block[]; label: string
}, [model, still]);
useEffect(() => mini.current?.repaint(), [theme]);
useEffect(() => {
if (lift != null) mini.current?.lift(lift);
}, [lift]);
// Still, there is nothing to replay.
if (still) return <canvas ref={canvas} className={`iso ${className ?? ""}`} role="img" aria-label={label} />;
const press = (x: number, y: number) => {
const at = onPick && mini.current?.pick(x, y);
if (at != null) onPick!(at);
else mini.current?.replay();
};
const name = `iso ${className ?? ""}`;
// Still, nothing replays; picks stay the pointer's.
if (still) return <canvas ref={canvas} className={name} role="img" aria-label={label} onClick={(e) => press(e.clientX, e.clientY)} />;
return (
<button className={`iso ${className ?? ""}`} aria-label={`${label}. Grow it again`} onClick={() => mini.current?.replay()}>
<button className={name} aria-label={`${label}. Grow it again`} onClick={(e) => (e.detail ? press(e.clientX, e.clientY) : mini.current?.replay())}>
<canvas ref={canvas} />
</button>
);