/* 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 (
);
}
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 (
);
}
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 && (
)}
{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;