/* global React, window */ // ============================================================ // BOOK ENGINE — CSS 3D single-leaf flip + shell + cover // ============================================================ function CoverArt({ t, variant }) { const { PixelTuxedoCat, PixelGirl } = window; const Cat = (size) => (
); const base = { position: "absolute", inset: 0, borderRadius: "4px 8px 8px 4px", background: "linear-gradient(135deg,#d9c9ac,#cdb893)", color: "#2a1a0c", boxShadow: "inset 0 0 0 1px rgba(255,255,255,.25), inset 0 0 60px rgba(90,55,30,.25)", overflow: "hidden", display: "flex", flexDirection: "column", }; const spine = (
); const grain = (
); if (variant === "minimal") { return (
{spine}{grain}
{t.fieldBook}
{Cat(108)}
{t.name}
{t.role}
); } if (variant === "stamp") { return (
{spine}{grain}
{Cat(118)}
{t.name}
{t.role}
); } // default: embossed broadsheet return (
{spine}{grain}
{t.fieldBook}
{Cat(132)}
{t.name}
{t.role}
{t.place}
); } function Face({ realm, side, children }) { return (
{children}
); } function Book({ lang, setLang, t, turnMs, sound, coverStyle, playTurn }) { const { Pages, RisoPages } = window; const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; const SPREADS = React.useMemo(() => [ { L: Pages.PgWelcome, R: Pages.PgRealms, realmL: "analog", realmR: "analog" }, { L: Pages.PgObjective, R: Pages.PgExperience1, realmL: "analog", realmR: "analog" }, { L: Pages.PgExperience2, R: Pages.PgSkills, realmL: "analog", realmR: "analog" }, { L: Pages.PgEducation, R: RisoPages.PgRisoIntro, realmL: "analog", realmR: "risograph" }, { L: RisoPages.PgProjects, R: RisoPages.PgStudio, realmL: "risograph", realmR: "analog" }, { L: RisoPages.PgContact, R: RisoPages.PgColophon, realmL: "analog", realmR: "analog" }, ], [Pages, RisoPages]); const CHAPTERS = [ { en: "Analog · Field Notes", ar: "التناظري · ملاحظات", sp: [0, 1, 2] }, { en: "Risograph · Print Room", ar: "الريزوغراف · المطبعة", sp: [3, 4] }, { en: "The Studio", ar: "الاستوديو", sp: [5] }, ]; const [phase, setPhase] = React.useState("closed"); const [opening, setOpening] = React.useState(false); const [shown, setShown] = React.useState(false); const [spread, setSpread] = React.useState(0); const [caseP, setCaseP] = React.useState(null); const [flip, setFlip] = React.useState(null); // {visual:'next'|'prev', target} const [active, setActive] = React.useState(false); const [hero, setHero] = React.useState(null); // cover→welcome cat transition const committed = React.useRef(false); const timers = React.useRef([]); const ctx = { t, lang, go: (key) => goRealm(key), openCase: setCaseP, onLang: setLang }; const face = (i, side) => { const sp = SPREADS[i]; const C = side === "left" ? sp.L : sp.R; return { realm: side === "left" ? sp.realmL : sp.realmR, node: }; }; const openBook = () => { const fromEl = document.getElementById("cover-cat-anchor"); const from = fromEl ? fromEl.getBoundingClientRect() : null; setOpening(true); if (!reduce && from) setHero({ from, to: null, fly: false }); setTimeout(() => { setPhase("open"); setTimeout(() => { setShown(true); if (!reduce && from) { setTimeout(() => { const toEl = document.getElementById("welcome-cat-anchor"); const to = toEl ? toEl.getBoundingClientRect() : null; if (to) { setHero({ from, to, fly: true }); timersHero(setTimeout(() => setHero(null), 820)); } else { setHero(null); } }, 80); } }, 30); }, reduce ? 10 : 620); }; const timersHero = (id) => { committed._h = id; }; const closeBook = () => { setShown(false); setTimeout(() => { setPhase("closed"); setOpening(false); }, 300); }; const commit = (target) => { if (committed.current) return; committed.current = true; timers.current.forEach(clearTimeout); timers.current = []; setSpread(target); setFlip(null); setActive(false); }; const animate = (target) => { if (flip || target < 0 || target >= SPREADS.length || target === spread) return; const fwd = target > spread; const visual = lang === "ar" ? (fwd ? "prev" : "next") : (fwd ? "next" : "prev"); if (sound) playTurn(); if (reduce) { setSpread(target); return; } committed.current = false; setActive(false); setFlip({ visual, target }); // kick the rotation on the next frame, then guarantee a commit timers.current.push(setTimeout(() => setActive(true), 30)); timers.current.push(setTimeout(() => commit(target), turnMs + 220)); }; const forward = () => animate(spread + 1); const backward = () => animate(spread - 1); const goRealm = (key) => { const target = key === "risograph" ? 3 : key === "analog" ? 0 : 0; animate(target); }; const onFlipEnd = (e) => { if (e.propertyName !== "transform" || !flip) return; commit(flip.target); }; React.useEffect(() => { if (phase !== "open") return; const k = (e) => { if (caseP) return; if (e.key === "ArrowRight") forward(); else if (e.key === "ArrowLeft") backward(); else if (e.key === " ") { e.preventDefault(); forward(); } }; window.addEventListener("keydown", k); return () => window.removeEventListener("keydown", k); }, [phase, spread, flip, caseP, lang]); // compute faces let underL, underR, flipSide, frontFace, backFace; if (flip) { const s = spread, target = flip.target; if (flip.visual === "next") { underL = face(s, "left"); underR = face(target, "right"); flipSide = "next"; frontFace = face(s, "right"); backFace = face(target, "left"); } else { underL = face(target, "left"); underR = face(s, "right"); flipSide = "prev"; frontFace = face(s, "left"); backFace = face(target, "right"); } } else { underL = face(spread, "left"); underR = face(spread, "right"); } const chap = CHAPTERS.find(c => c.sp.includes(spread)) || CHAPTERS[0]; const turnSec = (turnMs / 1000) + "s"; const flipTransform = flipSide === "next" ? (active ? "rotateY(-180deg)" : "rotateY(0deg)") : (active ? "rotateY(180deg)" : "rotateY(0deg)"); const fwdSideStyle = lang === "ar" ? { left: "max(18px,3vw)" } : { right: "max(18px,3vw)" }; const backSideStyle = lang === "ar" ? { right: "max(18px,3vw)" } : { left: "max(18px,3vw)" }; return (
{phase === "closed" && (
)} {phase === "open" && (
{underL.node} {underR.node}
{flip && (
{frontFace.node}
{backFace.node}
)}
{lang === "ar" ? chap.ar : chap.en}
{SPREADS.map((_, i) => (
)} {caseP && setCaseP(null)} />} {hero && (() => { const f = hero.from, to = hero.to; const s = hero.fly && to ? (to.width / f.width) : 1; const dx = hero.fly && to ? (to.left - f.left) : 0; const dy = hero.fly && to ? (to.top - f.top) : 0; return (
); })()}
); } window.Book = Book;