Baktiyar Ablaikhan

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).