add page flip buffering

Hunter
Monday, February 2nd, 2026 at 6:40 PM

resources/page-flip-animation.js+51-5
resources/spread-layout.js+3-4
resources/zine-styles.js+2 

resources/page-flip-animation.js
  1  // Page flip animation state
  2  let isAnimating = false;
  3  let currentSpreadIndex = 0;
  4 +let pendingFlipQueue = [];
  5  
  6  // Get animation state
  7  export function getIsAnimating() {

 95  
 96  }
 97  
    -// Animate page flip
    -export function animatePageFlip(fromSpread, toSpread, container, doc, onComplete) {
    -	if (isAnimating) return;
 98 +// Process next queued flip if any
 99 +function processNextFlip() {
100 +	if (pendingFlipQueue.length === 0) {
101 +		isAnimating = false;
102 +		return;
103 +	}
104 +
105 +	const nextFlip = pendingFlipQueue.shift();
106 +	// Use current spread index as fromSpread since we're now at a different position
107 +	executePageFlip(currentSpreadIndex, nextFlip.toSpread, nextFlip.container, nextFlip.doc, nextFlip.onComplete);
108 +}
109  
110 +// Execute a single page flip (internal function)
111 +function executePageFlip(fromSpread, toSpread, container, doc, onComplete) {
112  	isAnimating = true;
113  	currentSpreadIndex = toSpread;
114  

131  			});
132  
133  			// Clean up after animation
134 +			let cleanupCalled = false;
135  			const cleanup = () => {
136 +				if (cleanupCalled) return;
137 +				cleanupCalled = true;
138 +
139  				leafToFlip.dataset.state = 'open';
140  				leafToFlip.style.transition = '';
141  				// Set z-index for open state (lower than closed leaves)
142  				leafToFlip.style.zIndex = String(fromSpread + 1);
    -				isAnimating = false;
143  				if (onComplete) onComplete();
144  				leafToFlip.removeEventListener('transitionend', cleanup);
145 +				clearTimeout(timeoutId);
146 +				// Process next flip in queue
147 +				processNextFlip();
148  			};
149  			leafToFlip.addEventListener('transitionend', cleanup);
150 +
151 +			// Fallback timeout in case transitionend doesn't fire
152 +			const timeoutId = setTimeout(cleanup, 800);
153  		}
154  	} else {
155  		// Flipping backward (left to right)

167  			});
168  
169  			// Clean up after animation
170 +			let cleanupCalled = false;
171  			const cleanup = () => {
172 +				if (cleanupCalled) return;
173 +				cleanupCalled = true;
174 +
175  				leafToFlip.dataset.state = 'closed';
176  				leafToFlip.style.transition = '';
177  				// Set z-index for closed state (higher than open leaves)
178  				leafToFlip.style.zIndex = String(20 - toSpread);
    -				isAnimating = false;
179  				if (onComplete) onComplete();
180  				leafToFlip.removeEventListener('transitionend', cleanup);
181 +				clearTimeout(timeoutId);
182 +				// Process next flip in queue
183 +				processNextFlip();
184  			};
185  			leafToFlip.addEventListener('transitionend', cleanup);
186 +
187 +			// Fallback timeout in case transitionend doesn't fire
188 +			const timeoutId = setTimeout(cleanup, 800);
189  		}
190  	}
191  }
192  
193 +// Animate page flip with input buffering
194 +export function animatePageFlip(fromSpread, toSpread, container, doc, onComplete) {
195 +	// If already animating, queue this flip request
196 +	if (isAnimating) {
197 +		// Check if there's already a queued flip to the same target - if so, skip this one
198 +		// This prevents the queue from growing unnecessarily when users spam the same direction
199 +		const lastQueued = pendingFlipQueue[pendingFlipQueue.length - 1];
200 +		if (!lastQueued || lastQueued.toSpread !== toSpread) {
201 +			pendingFlipQueue.push({ fromSpread, toSpread, container, doc, onComplete });
202 +		}
203 +		return;
204 +	}
205 +
206 +	// Start the flip immediately
207 +	executePageFlip(fromSpread, toSpread, container, doc, onComplete);
208 +}
209 +
210  // Set current spread without animation (for initial load)
211  export function setSpreadImmediate(spreadIndex, doc) {
212  	currentSpreadIndex = spreadIndex;
resources/spread-layout.js
47  // Navigate to a spread with optional flip animation
48  export function navigateToSpread(container, spreadIndex, doc, useFlipAnimation = false, onComplete) {
49  	if (useFlipAnimation && container.querySelector('.zine-book')) {
   -		if (!getIsAnimating()) {
   -			animatePageFlip(currentFlipSpread, spreadIndex, container, doc, onComplete);
   -			currentFlipSpread = spreadIndex;
   -		}
50 +		// Always call animatePageFlip - it now handles queueing internally
51 +		animatePageFlip(currentFlipSpread, spreadIndex, container, doc, onComplete);
52 +		currentFlipSpread = spreadIndex;
53  	} else {
54  		// Original non-animated behavior
55  		const spread = SPREADS[spreadIndex];
resources/zine-styles.js
61  		transform-origin: left center;
62  		transform-style: preserve-3d;
63  		transition: none;
64 +		will-change: transform;
65  	}
66  
67  	/* When flipping, use high z-index so it's on top during animation */

77  		height: 100%;
78  		backface-visibility: hidden;
79  		-webkit-backface-visibility: hidden;
80 +		transform: translateZ(0);
81  	}
82  
83  	.zine-leaf-front {