"use client"; import * as React from "react"; import { motion } from "framer-motion"; /** * PathScenes: one small animated drawing per step of a service path. * They explain what happens at that step; they are not decoration. * All scenes share a 480 x 300 canvas and the same line language. */ const INK = "#f8fafc"; const PAPER = "#0f172a"; const LINE = "#475569"; const SOFT = "#1e293b"; const MARK = "#fde68a"; const LIVE = "#10b981"; /** A lit surface: a light tint instead of a solid white block. */ const TINT = "rgba(248, 250, 252, 0.14)"; const RIM = "#64748b"; const FINE = 1.1; const MONO = { fontFamily: "var(--font-jetbrains-mono), ui-monospace, monospace", // Set on the svg: larger on phones, where the drawing is scaled down. fontSize: "var(--scene-label, 10px)", letterSpacing: "0.14em", textTransform: "uppercase" as const, fill: "#94a3b8", }; const draw = (delay: number) => ({ initial: { pathLength: 0, opacity: 0 }, animate: { pathLength: 1, opacity: 1 }, transition: { duration: 0.6, delay, ease: "easeOut" as const }, }); const appear = (delay: number, y = 6) => ({ initial: { opacity: 0, y }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.4, delay, ease: "easeOut" as const }, }); type Pt = readonly [number, number]; /** Points along a cubic curve, for packets that travel on it. */ const along = (p0: Pt, p1: Pt, p2: Pt, p3: Pt, samples = 12) => { const xs: number[] = []; const ys: number[] = []; for (let i = 0; i <= samples; i++) { const t = i / samples; const u = 1 - t; xs.push( u * u * u * p0[0] + 3 * u * u * t * p1[0] + 3 * u * t * t * p2[0] + t * t * t * p3[0], ); ys.push( u * u * u * p0[1] + 3 * u * u * t * p1[1] + 3 * u * t * t * p2[1] + t * t * t * p3[1], ); } return { xs, ys }; }; /** A small packet that keeps travelling a route. */ const Packet: React.FC<{ xs: readonly number[]; ys: readonly number[]; delay?: number; duration?: number; pause?: number; }> = ({ xs, ys, delay = 0, duration = 1.2, pause = 1.4 }) => ( (i === 0 || i === xs.length - 1 ? 0 : 1)), }} transition={{ duration, delay, repeat: Infinity, repeatDelay: pause, ease: "easeInOut", }} /> ); const Person: React.FC<{ x: number; label: string; filled?: boolean }> = ({ x, label, filled = false, }) => ( {filled && } {label} ); /** Step 1 of every path: a short conversation. */ const Talk: React.FC = () => ( {[ { x: 150, y: 86, w: 120, side: "left", delay: 0.2 }, { x: 210, y: 124, w: 120, side: "right", delay: 0.7 }, { x: 150, y: 162, w: 96, side: "left", delay: 1.2 }, ].map((b) => ( ))} {[0, 1, 2].map((dot) => ( ))} about 20 minutes ); const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri"] as const; /** Developer, step 2: agreed days of the week. */ const WeeklyHours: React.FC = () => ( {DAYS.map((day, d) => ( {day} {Array.from({ length: 8 }, (_, h) => { const reserved = d < 3; return ( ); })} ))} for example three days a week ); /** Developer, step 3: the list gets worked through and comes back for review. */ const WorkList: React.FC = () => ( {[0, 1, 2, 3].map((row) => { const done = row < 3; const y = 60 + row * 46; return ( {done && ( )} {done && ( you )} ); })} finished parts go to you for review ); /** Developer, step 4: two weeks notice to the end of a month. */ const Notice: React.FC = () => ( {Array.from({ length: 5 }, (_, w) => ( {w === 4 ? "end of month" : `week ${w + 1}`} ))} two weeks notice ); const NODES: readonly (readonly [number, number])[] = [ [90, 80], [200, 56], [320, 76], [410, 120], [130, 170], [250, 140], [350, 190], [220, 220], ]; const EDGES: readonly (readonly [number, number])[] = [ [0, 1], [1, 2], [2, 3], [0, 4], [1, 5], [4, 5], [5, 2], [5, 6], [3, 6], [4, 7], [7, 6], ]; /** Rescue, step 2: fragile connections become solid over the week. */ const RepairWeek: React.FC = () => ( {EDGES.map(([a, b], i) => { const from = NODES[a]!; const to = NODES[b]!; return ( ); })} {NODES.map(([x, y], i) => ( ))} fragile parts get tests and fixes ); /** Rescue, step 3: a written summary. */ const Summary: React.FC = () => ( {["found", "changed", "next"].map((label, s) => ( {label} {[0, 1, 2].map((l) => ( ))} ))} written so you can follow it ); /** Rescue, step 4: the client decides how to continue. */ const Decide: React.FC = () => ( you {[ { y: 70, label: "continue with me" }, { y: 150, label: "your own team" }, { y: 230, label: "stop here" }, ].map((option, i) => ( {option.label} ))} ); const Browser: React.FC<{ children: React.ReactNode; url?: string; live?: boolean; }> = ({ children, url, live = false }) => ( {[0, 1, 2].map((d) => ( ))} {url && ( {url} )} {live && ( online )} {children} ); const BLOCKS: readonly { x: number; y: number; w: number; h: number }[] = [ { x: 88, y: 72, w: 304, h: 14 }, { x: 88, y: 98, w: 190, h: 64 }, { x: 290, y: 98, w: 102, h: 64 }, { x: 88, y: 174, w: 94, h: 62 }, { x: 193, y: 174, w: 94, h: 62 }, { x: 298, y: 174, w: 94, h: 62 }, ]; /** What a finished page holds: a headline, a button, and a line of text per card. */ const PageDetail: React.FC<{ delay: number }> = ({ delay }) => ( {[BLOCKS[2], BLOCKS[3], BLOCKS[4], BLOCKS[5]].map( (block) => block && ( ), )} ); /** Website, step 2: a first design as outlines. */ const Wireframe: React.FC = () => ( {BLOCKS.map((b, i) => ( ))} a first version to click through ); /** Website, step 3: built and online. */ const Built: React.FC = () => ( {BLOCKS.map((b, i) => ( ))} you approve it before it goes live ); /** Website, step 4: a change request by email. */ const ChangeByEmail: React.FC = () => ( {BLOCKS.map((b, i) => ( ))} you send an email, I make the change ); const TOOLS: readonly { x: number; y: number; label: string }[] = [ { x: 90, y: 80, label: "Shop" }, { x: 390, y: 80, label: "Accounting" }, { x: 90, y: 200, label: "Email" }, { x: 390, y: 200, label: "CRM" }, ]; const Tool: React.FC<{ x: number; y: number; label: string }> = ({ x, y, label, }) => ( {label} ); /** Automation, step 2: today a person carries the data from tool to tool. */ const ByHand: React.FC = () => ( {TOOLS.map((tool) => ( ))} {TOOLS.map((tool, i) => ( ))} today: copied by hand ); const WIRES: readonly string[] = [ "M 146 80 H 334", "M 90 102 V 178", "M 390 102 V 178", "M 146 200 H 334", ]; /** Automation, step 3: the systems are connected one step at a time. */ const Wiring: React.FC = () => ( {WIRES.map((d, i) => ( ))} {TOOLS.map((tool) => ( ))} connected one step at a time ); /** Automation, step 4: the work travels between the systems on its own. */ const Running: React.FC = () => ( {WIRES.map((d) => ( ))} {[ { from: [146, 80], to: [334, 80], delay: 0 }, { from: [390, 102], to: [390, 178], delay: 0.7 }, { from: [334, 200], to: [146, 200], delay: 1.4 }, { from: [90, 178], to: [90, 102], delay: 2.1 }, ].map((dot) => ( ))} {TOOLS.map((tool) => ( ))} the steps run without anyone copying ); const SCENES: Readonly> = { developer: [Talk, WeeklyHours, WorkList, Notice], rescue: [Talk, RepairWeek, Summary, Decide], automation: [Talk, ByHand, Wiring, Running], website: [Talk, Wireframe, Built, ChangeByEmail], }; export const PathScene: React.FC<{ pathId: string; step: number; title: string; }> = ({ pathId, step, title }) => { const Scene = SCENES[pathId]?.[step]; if (!Scene) return null; return ( {/* Remount per step so each drawing plays from the start. */} ); };