Flipbook
Anonymous
plain_text
04/16/2026 5:09 PM
13.2 KB
16
Indexable
import { useState } from "react";
export default function FlipBook({
pages,
onImageClick,
fullscreen = false,
}: {
pages: string[];
onImageClick?: () => void;
fullscreen?: boolean;
}) {
const [spread, setSpread] = useState(0);
const [flipping, setFlipping] = useState<"forward" | "back" | null>(null);
const [displaySpread, setDisplaySpread] = useState(0);
const totalSpreads = Math.ceil(pages.length / 2);
const leftPage = pages[displaySpread * 2] ?? null;
const rightPage = pages[displaySpread * 2 + 1] ?? null;
const goNext = () => {
if (spread >= totalSpreads - 1 || flipping) return;
const nextSpread = spread + 1;
setSpread(nextSpread);
setFlipping("forward");
setTimeout(() => setDisplaySpread(nextSpread), 400);
setTimeout(() => setFlipping(null), 800);
};
const goPrev = () => {
if (spread <= 0 || flipping) return;
const prevSpread = spread - 1;
setSpread(prevSpread);
setFlipping("back");
setTimeout(() => setDisplaySpread(prevSpread), 400);
setTimeout(() => setFlipping(null), 800);
};
const pageWidth = fullscreen ? "47vw" : "clamp(320px, 42vw, 700px)";
const pageHeight = fullscreen ? "82vh" : "clamp(360px, 48vw, 780px)";
return (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: "20px 0" }}>
<div style={{
display: "flex",
alignItems: "center",
position: "relative",
perspective: "3000px",
}}>
{/* LEFT ARROW */}
<button
onClick={goPrev}
disabled={spread === 0 || !!flipping}
style={{
background: "rgba(0,0,0,0.35)",
color: "white",
border: "none",
borderRadius: "50%",
width: "48px",
height: "48px",
fontSize: "20px",
cursor: spread === 0 ? "not-allowed" : "pointer",
opacity: spread === 0 ? 0.3 : 1,
marginRight: "20px",
flexShrink: 0,
transition: "background 0.2s",
}}
>
←
</button>
{/* BOOK SPREAD */}
<div style={{
display: "flex",
boxShadow: "0 20px 60px rgba(0,0,0,0.35)",
borderRadius: "4px",
position: "relative",
transformStyle: "preserve-3d",
}}>
{/* LEFT PAGE */}
<div style={{
width: pageWidth,
height: pageHeight,
overflow: "hidden",
borderRight: "2px solid #c9a227",
backgroundColor: "#f5eedc",
cursor: "zoom-in",
position: "relative",
transformOrigin: "right center",
transformStyle: "preserve-3d",
animationName: flipping === "back" ? "pageUnfoldLeft" : "none",
animationDuration: "800ms",
animationTimingFunction: "ease-in-out",
animationFillMode: "both",
}}
onClick={onImageClick}
>
{leftPage ? (
<img
src={leftPage}
alt="left-page"
style={{ width: "100%", height: "100%", objectFit: "contain", display: "block", backgroundColor: "#f5eedc" }}
/>
) : (
<div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "#ccc" }}>—</div>
)}
{/* Shadow overlay during flip */}
{flipping && (
<div style={{
position: "absolute", inset: 0,
background: flipping === "forward"
? "linear-gradient(to left, rgba(0,0,0,0.3), transparent)"
: "linear-gradient(to right, rgba(0,0,0,0.3), transparent)",
pointerEvents: "none",
animationName: "shadowFade",
animationDuration: "800ms",
animationTimingFunction: "ease-in-out",
}} />
)}
</div>
{/* RIGHT PAGE */}
<div style={{
width: pageWidth,
height: pageHeight,
overflow: "hidden",
backgroundColor: "#f5eedc",
cursor: "zoom-in",
position: "relative",
transformOrigin: "left center",
transformStyle: "preserve-3d",
animationName: flipping === "forward" ? "pageUnfoldRight" : "none",
animationDuration: "800ms",
animationTimingFunction: "ease-in-out",
animationFillMode: "both",
}}
onClick={onImageClick}
>
{rightPage ? (
<img
src={rightPage}
alt="right-page"
style={{ width: "100%", height: "100%", objectFit: "contain", display: "block", backgroundColor: "#f5eedc" }}
/>
) : (
<div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "#ccc" }}>—</div>
)}
{/* Shadow overlay during flip */}
{flipping && (
<div style={{
position: "absolute", inset: 0,
background: flipping === "forward"
? "linear-gradient(to right, rgba(0,0,0,0.3), transparent)"
: "linear-gradient(to left, rgba(0,0,0,0.3), transparent)",
pointerEvents: "none",
animationName: "shadowFade",
animationDuration: "800ms",
animationTimingFunction: "ease-in-out",
}} />
)}
</div>
{/* SPINE */}
<div style={{
position: "absolute",
left: "50%",
top: 0,
bottom: 0,
width: "8px",
transform: "translateX(-50%)",
background: "linear-gradient(to right, rgba(0,0,0,0.2), rgba(201,162,39,0.4), rgba(0,0,0,0.2))",
pointerEvents: "none",
zIndex: 2,
}} />
{/* PAGE TURN FLAP — forward */}
{flipping === "forward" && (
<div style={{
position: "absolute",
right: 0,
top: 0,
width: pageWidth,
height: pageHeight,
transformOrigin: "left center",
animationName: "pageFoldForward",
animationDuration: "800ms",
animationTimingFunction: "ease-in-out",
animationFillMode: "both",
zIndex: 10,
backgroundColor: "#f5eedc",
overflow: "hidden",
boxShadow: "-8px 0 20px rgba(0,0,0,0.3)",
}}>
<div style={{
width: "100%", height: "100%",
background: "linear-gradient(to right, #e8dcc8, #f5eedc)",
}} />
</div>
)}
{/* PAGE TURN FLAP — back */}
{flipping === "back" && (
<div style={{
position: "absolute",
left: 0,
top: 0,
width: pageWidth,
height: pageHeight,
transformOrigin: "right center",
animationName: "pageFoldBack",
animationDuration: "800ms",
animationTimingFunction: "ease-in-out",
animationFillMode: "both",
zIndex: 10,
backgroundColor: "#f5eedc",
overflow: "hidden",
boxShadow: "8px 0 20px rgba(0,0,0,0.3)",
}}>
<div style={{
width: "100%", height: "100%",
background: "linear-gradient(to left, #e8dcc8, #f5eedc)",
}} />
</div>
)}
</div>
{/* RIGHT ARROW */}
<button
onClick={goNext}
disabled={spread >= totalSpreads - 1 || !!flipping}
style={{
background: "rgba(0,0,0,0.35)",
color: "white",
border: "none",
borderRadius: "50%",
width: "48px",
height: "48px",
fontSize: "20px",
cursor: spread >= totalSpreads - 1 ? "not-allowed" : "pointer",
opacity: spread >= totalSpreads - 1 ? 0.3 : 1,
marginLeft: "20px",
flexShrink: 0,
transition: "background 0.2s",
}}
>
→
</button>
</div>
{/* PAGE COUNTER */}
<p style={{
marginTop: "16px",
fontSize: "13px",
color: "#777",
letterSpacing: "1px",
fontFamily: "Georgia, serif",
}}>
Pages {displaySpread * 2 + 1}–{Math.min(displaySpread * 2 + 2, pages.length)} of {pages.length}
</p>
<style>{`
@keyframes pageFoldForward {
0% { transform: rotateY(0deg); opacity: 1; }
15% { transform: rotateY(-20deg); opacity: 1; }
50% { transform: rotateY(-90deg); opacity: 0.6; }
85% { transform: rotateY(-160deg); opacity: 0.9; }
100% { transform: rotateY(-180deg); opacity: 0; }
}
@keyframes pageFoldBack {
0% { transform: rotateY(0deg); opacity: 1; }
15% { transform: rotateY(20deg); opacity: 1; }
50% { transform: rotateY(90deg); opacity: 0.6; }
85% { transform: rotateY(160deg); opacity: 0.9; }
100% { transform: rotateY(180deg); opacity: 0; }
}
@keyframes pageUnfoldRight {
0% { transform: rotateY(30deg); opacity: 0.7; }
50% { transform: rotateY(10deg); opacity: 0.9; }
100% { transform: rotateY(0deg); opacity: 1; }
}
@keyframes pageUnfoldLeft {
0% { transform: rotateY(-30deg); opacity: 0.7; }
50% { transform: rotateY(-10deg); opacity: 0.9; }
100% { transform: rotateY(0deg); opacity: 1; }
}
@keyframes shadowFade {
0% { opacity: 0; }
30% { opacity: 1; }
70% { opacity: 1; }
100% { opacity: 0; }
}
`}</style>
</div>
);
}Editor is loading...
Leave a Comment