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