Blueprint loader
- Source
- After bencho.dev ↗
- Author
- Interaction after bencho.dev
- Licence
- Original code — interaction after bencho.dev
Loading…
A box is drafted line by line; move to shift the sheet, click to redraw.
Code
"use client";
import { animate, motion, useMotionValue, useMotionValueEvent, useSpring } from "motion/react";
import { useEffect, useState } from "react";
import { Surface } from "../ui";
/* An isometric box, drawn line by line. Each path appears over its slice of the progress. */
const PATHS = [
"M100 150 L40 115 L40 55 L100 20 L160 55 L160 115 Z", // outline
"M40 55 L100 90 L160 55", // top edges
"M100 90 L100 150", // front edge
"M70 37.5 L130 72.5", // lid seam
"M130 37.5 L70 72.5",
"M52 128 L22 111 M22 111 L22 51 M18 58 L22 51 L26 58 M18 104 L22 111 L26 104", // height dimension
"M106 160 L162 128 M110 153 L106 160 L114 160", // width dimension
];
export default function BlueprintLoader() {
const progress = useMotionValue(0);
const [p, setP] = useState(0);
const [round, setRound] = useState(0);
const tx = useSpring(0, { stiffness: 120, damping: 16 });
const ty = useSpring(0, { stiffness: 120, damping: 16 });
useMotionValueEvent(progress, "change", setP);
useEffect(() => {
let alive = true;
const run = async () => {
while (alive) {
progress.jump(0);
await animate(progress, 1, { duration: 4.2, ease: [0.45, 0.05, 0.4, 1] });
if (!alive) break;
await new Promise((r) => setTimeout(r, 900));
}
};
run();
return () => {
alive = false;
progress.stop();
};
}, [progress, round]);
const pct = Math.round(p * 100);
return (
<Surface
className="cursor-pointer"
onPointerMove={(e) => {
const r = e.currentTarget.getBoundingClientRect();
tx.set(((e.clientX - r.left) / r.width - 0.5) * 16);
ty.set(((e.clientY - r.top) / r.height - 0.5) * 16);
}}
onPointerLeave={() => {
tx.set(0);
ty.set(0);
}}
onPointerDown={() => setRound((n) => n + 1)}
style={{
backgroundColor: "#f4f6fc",
backgroundImage:
"linear-gradient(rgba(43,70,184,0.12) 1px, transparent 1px), linear-gradient(90deg, rgba(43,70,184,0.12) 1px, transparent 1px), linear-gradient(rgba(43,70,184,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(43,70,184,0.05) 1px, transparent 1px)",
backgroundSize: "4em 4em, 4em 4em, 1em 1em, 1em 1em",
backgroundPosition: "center",
}}
>
<div className="flex flex-col items-center gap-[1em]">
<motion.svg viewBox="0 0 180 170" className="h-[11em] w-[11.6em] overflow-visible" style={{ x: tx, y: ty }}>
{PATHS.map((d, i) => {
const start = (i / PATHS.length) * 0.85;
const local = Math.min(1, Math.max(0, (p - start) / (0.85 / PATHS.length + 0.1)));
return (
<path
key={i}
d={d}
pathLength={1}
fill="none"
stroke="#2B46B8"
strokeWidth={i >= 5 ? 0.9 : 1.6}
strokeLinecap="round"
strokeLinejoin="round"
strokeDasharray="1 1"
strokeDashoffset={1 - local}
opacity={i >= 5 ? 0.7 : 1}
/>
);
})}
<motion.path
d="M100 90 L160 55 L160 115 L100 150 Z"
fill="#2B46B8"
animate={{ opacity: p > 0.92 ? 0.1 : 0 }}
/>
<text x="6" y="84" fontSize="7" fill="#2B46B8" fontFamily="ui-monospace, monospace" opacity={p > 0.8 ? 0.8 : 0}>
60
</text>
<text x="140" y="154" fontSize="7" fill="#2B46B8" fontFamily="ui-monospace, monospace" opacity={p > 0.9 ? 0.8 : 0}>
70
</text>
</motion.svg>
<div className="flex w-[12em] items-center gap-[0.8em] font-mono text-[0.72em] text-[#2B46B8]">
<span className="tabular-nums">{String(pct).padStart(3, "0")}%</span>
<div className="h-px flex-1 bg-[#2B46B8]/20">
<div className="h-px bg-[#2B46B8]" style={{ width: `${pct}%` }} />
</div>
<span>{pct >= 100 ? "done" : "drafting"}</span>
</div>
</div>
</Surface>
);
}
Prompt
Build "Blueprint loader", a loader animation. What it does: A box is drafted line by line; move to shift the sheet, click to redraw. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using motion. Technique: an HTML canvas, the Motion library, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://bencho.dev (Interaction after bencho.dev, Original code — interaction after bencho.dev).