diff --git a/src/app/globals.css b/src/app/globals.css index 53adebfc..651b7283 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -210,20 +210,45 @@ code, pre, .font-mono { } } -/* Reading Mode page-flip transitions */ -@keyframes flip-in-left { - from { opacity: 0; transform: translateX(40px); } - to { opacity: 1; transform: translateX(0); } +/* Reading Mode page-flip transitions (3D book-style) */ +.page-flip-container { + perspective: 1200px; } -@keyframes flip-in-right { - from { opacity: 0; transform: translateX(-40px); } - to { opacity: 1; transform: translateX(0); } + +@keyframes flip-out-forward { + 0% { transform: rotateY(0deg); opacity: 1; } + 100% { transform: rotateY(-90deg); opacity: 0; } +} +@keyframes flip-out-backward { + 0% { transform: rotateY(0deg); opacity: 1; } + 100% { transform: rotateY(90deg); opacity: 0; } +} +@keyframes flip-in-from-right { + 0% { transform: rotateY(90deg); opacity: 0; } + 100% { transform: rotateY(0deg); opacity: 1; } +} +@keyframes flip-in-from-left { + 0% { transform: rotateY(-90deg); opacity: 0; } + 100% { transform: rotateY(0deg); opacity: 1; } +} + +/* Outgoing page: flips away */ +.page-flip-out-left { + transform-origin: left center; + animation: flip-out-forward 200ms ease-in forwards; +} +.page-flip-out-right { + transform-origin: right center; + animation: flip-out-backward 200ms ease-in forwards; } -.page-flip-left { - animation: flip-in-left 0.25s ease-out; +/* Incoming page: flips into view */ +.page-flip-in-left { + transform-origin: left center; + animation: flip-in-from-right 200ms ease-out; } -.page-flip-right { - animation: flip-in-right 0.25s ease-out; +.page-flip-in-right { + transform-origin: right center; + animation: flip-in-from-left 200ms ease-out; } /* Custom select dropdown styling */ diff --git a/src/components/ReadingMode.tsx b/src/components/ReadingMode.tsx index 67e90a5c..f9a7ec91 100644 --- a/src/components/ReadingMode.tsx +++ b/src/components/ReadingMode.tsx @@ -28,7 +28,11 @@ export function ReadingMode({ const [currentIdx, setCurrentIdx] = useState(initialChapterIndex); const [showToc, setShowToc] = useState(false); const [flipDir, setFlipDir] = useState<"left" | "right" | null>(null); + const [flipPhase, setFlipPhase] = useState<"out" | "in" | null>(null); const contentRef = useRef(null); + const touchStartX = useRef(0); + const touchStartY = useRef(0); + const flipping = useRef(false); const { isMiniApp } = usePlatformDetection(); const chapter = chapters[currentIdx]; @@ -40,14 +44,22 @@ export function ReadingMode({ }, []); const navigate = useCallback((idx: number, dir: "left" | "right" | null) => { + if (flipping.current) return; + flipping.current = true; + // Phase 1: flip out the current page setFlipDir(dir); - // Brief delay to trigger the CSS animation before content swap - requestAnimationFrame(() => { + setFlipPhase("out"); + setTimeout(() => { + // Phase 2: swap content and flip in the new page setCurrentIdx(idx); scrollToTop(); - // Clear the animation class after transition completes - setTimeout(() => setFlipDir(null), 250); - }); + setFlipPhase("in"); + setTimeout(() => { + setFlipDir(null); + setFlipPhase(null); + flipping.current = false; + }, 200); + }, 200); }, [scrollToTop]); const goPrev = useCallback(() => { @@ -110,9 +122,29 @@ export function ReadingMode({ {/* Content area */} -
+
{ + touchStartX.current = e.touches[0].clientX; + touchStartY.current = e.touches[0].clientY; + }} + onTouchEnd={(e) => { + const dx = e.changedTouches[0].clientX - touchStartX.current; + const dy = e.changedTouches[0].clientY - touchStartY.current; + // Only trigger if horizontal swipe exceeds threshold and is more horizontal than vertical + if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.5) { + if (dx < 0) goNext(); + else goPrev(); + } + }} + >
{chapter?.content ? (