scaling fixed; judder when opening/closing covers
Hunter
Wednesday, February 4th, 2026 at 2:56 PM
73 // Update the book container horizontal position based on spread
74 // Spreads 0 and 4 (single pages) should be centered, others at normal position
75 function updateBookPosition(spreadIndex, animated = true) {
- if (!bookContainer) return;
76 + if (!bookContainer) {
77 + console.error('updateBookPosition: bookContainer is null!');
78 + return;
79 + }
80
81 // Spread 0 (front cover): single right page, shift left to center
82 // Spread 4 (back cover): single left page, shift right to center
88 shiftAmount = '1.375in';
89 }
90
91 + const currentTransform = bookContainer.style.transform;
92 + const currentTransition = bookContainer.style.transition;
93 + console.log(`updateBookPosition(spread=${spreadIndex}, animated=${animated})`);
94 + console.log(` Current transform: "${currentTransform}"`);
95 + console.log(` Current transition: "${currentTransition}"`);
96 + console.log(` Target: translateX(${shiftAmount})`);
97 +
98 if (animated) {
- bookContainer.style.transition = 'transform 0.6s cubic-bezier(0.645, 0.045, 0.355, 1.000)';
- } else {
- bookContainer.style.transition = 'none';
- }
99 + // Clear any existing transition first to ensure clean state
100 + bookContainer.style.transition = '';
101 + // Force a reflow
102 + bookContainer.offsetHeight;
103
- // Preserve any existing scale from scaleSpreadToFit
- const currentTransform = bookContainer.style.transform || '';
- const scaleMatch = currentTransform.match(/scale\([^)]+\)/);
- const scale = scaleMatch ? scaleMatch[0] : '';
104 + // Now set the transition for this animation
105 + bookContainer.style.transition = 'transform 0.6s cubic-bezier(0.645, 0.045, 0.355, 1.000)';
106 + console.log(` Set transition, will animate in rAF`);
107
- bookContainer.style.transform = scale ? `${scale} translateX(${shiftAmount})` : `translateX(${shiftAmount})`;
108 + // Use requestAnimationFrame to set transform (same as leaf flip animation)
109 + // This ensures both animations start in the same frame
110 + requestAnimationFrame(() => {
111 + console.log(` rAF: Setting transform to translateX(${shiftAmount})`);
112 + bookContainer.style.transform = `translateX(${shiftAmount})`;
113 + });
114
- // Remove transition after animation completes
- if (animated) {
115 + // Remove transition after animation completes
116 setTimeout(() => {
117 bookContainer.style.transition = '';
118 }, 600);
119 + } else {
120 + bookContainer.style.transition = 'none';
121 + // For non-animated, use explicit translateX or none
122 + const newTransform = shiftAmount !== '0in' ? `translateX(${shiftAmount})` : 'translateX(0in)';
123 + console.log(` Non-animated: Setting transform to ${newTransform}`);
124 + bookContainer.style.transform = newTransform;
125 }
126 }
127
170 isAnimating = true;
171 currentSpreadIndex = toSpread;
172
173 + console.log(`\n=== executePageFlip: ${fromSpread} → ${toSpread} ===`);
174 +
175 // Update book position synchronously with the flip animation
176 updateBookPosition(toSpread, true);
177
178 const direction = toSpread > fromSpread ? 'forward' : 'backward';
179 + console.log(`Direction: ${direction}`);
180 const leaves = doc.querySelectorAll('.zine-leaf');
181
182 if (direction === 'forward') {
273
274 // Set current spread without animation (for initial load)
275 export function setSpreadImmediate(spreadIndex, doc) {
276 + console.log(`\n=== setSpreadImmediate(${spreadIndex}) ===`);
277 currentSpreadIndex = spreadIndex;
278 updateLeafStates(spreadIndex, doc);
279 updateBookPosition(spreadIndex, false);
125
126 // Set initial spread state without animation
127 setTimeout(() => {
128 + scaleSpreadToFit(container, doc); // Must call this BEFORE setSpreadImmediate so zoom is set
129 setSpreadImmediate(currentSpread, doc);
- const scaleToFit = () => scaleSpreadToFit(container, doc);
- scaleToFit();
- doc.defaultView.addEventListener('resize', scaleToFit);
130 +
131 + // On resize, update zoom and reposition book
132 + const handleResize = () => {
133 + scaleSpreadToFit(container, doc);
134 + // Get the actual current spread from the leaf states
135 + const leaves = doc.querySelectorAll('.zine-leaf');
136 + let activeSpread = 0;
137 + leaves.forEach((leaf, index) => {
138 + if (leaf.dataset.state === 'open') {
139 + activeSpread = index + 1;
140 + }
141 + });
142 + setSpreadImmediate(activeSpread, doc);
143 + };
144 + doc.defaultView.addEventListener('resize', handleResize);
145 }, 0);
146 } else {
147 // Original non-animated mode
73 const scaleY = (vh - 40) / spreadHeightPx;
74 const scale = Math.min(scaleX, scaleY);
75
76 + // Use CSS zoom for high-resolution rendering instead of transform scale
77 + bookContainer.style.zoom = scale;
78 +
79 + // Store the current zoom on the book container for use by updateBookPosition
80 + bookContainer.dataset.currentZoom = scale;
81 +
82 // Preserve any existing translateX from book positioning
83 const currentTransform = bookContainer.style.transform || '';
84 const translateMatch = currentTransform.match(/translateX\([^)]+\)/);
85 const translateX = translateMatch ? translateMatch[0] : '';
86
- bookContainer.style.transform = translateX ? `scale(${scale}) ${translateX}` : `scale(${scale})`;
87 + bookContainer.style.transform = translateX || '';
88
89 // Notify parent of spread position and grid size for background grid
90 if (doc.defaultView?.parent && doc.defaultView.parent !== doc.defaultView) {
96 const gridWidth = gridHeight * (30.25 / 29.75);
97
98 // Remove translateX shift so grid always anchors to the un-shifted book position
- // The translateX is in inches and gets multiplied by scale
99 + // With CSS zoom, translateX values are in the zoomed coordinate space
100 const translateInchMatch = translateX.match(/translateX\(([^)]+)in\)/);
101 const translatePx = translateInchMatch ? parseFloat(translateInchMatch[1]) * 96 * scale : 0;
102
104 const scaleX = (vw - 40) / spreadWidthPx;
105 const scaleY = (vh - 40) / spreadHeightPx;
106 const scale = Math.min(scaleX, scaleY);
- bookContainer.style.transform = 'scale(' + scale + ')';
107 + bookContainer.style.zoom = scale;
108 + bookContainer.dataset.currentZoom = scale;
109 + }
110 +
111 + function updateBookPosition(spreadIndex, animated) {
112 + const bookContainer = container.querySelector('.zine-book');
113 + if (!bookContainer) return;
114 +
115 + // Spread 0 (front cover): single right page, shift left to center
116 + // Spread 4 (back cover): single left page, shift right to center
117 + // Half page width = 1.375in
118 + let shiftAmount = '0in';
119 + if (spreadIndex === 0) {
120 + shiftAmount = '-1.375in';
121 + } else if (spreadIndex === 4) {
122 + shiftAmount = '1.375in';
123 + }
124 +
125 + if (animated) {
126 + // Clear any existing transition first to ensure clean state
127 + bookContainer.style.transition = '';
128 + // Force a reflow
129 + bookContainer.offsetHeight;
130 +
131 + // Now set the transition for this animation
132 + bookContainer.style.transition = 'transform 0.6s cubic-bezier(0.645, 0.045, 0.355, 1.000)';
133 +
134 + // Use requestAnimationFrame to set transform (same as leaf flip animation)
135 + // This ensures both animations start in the same frame
136 + requestAnimationFrame(function() {
137 + bookContainer.style.transform = 'translateX(' + shiftAmount + ')';
138 + });
139 +
140 + // Remove transition after animation completes
141 + setTimeout(function() {
142 + bookContainer.style.transition = '';
143 + }, 600);
144 + } else {
145 + bookContainer.style.transition = 'none';
146 + // For non-animated, use explicit translateX
147 + bookContainer.style.transform = shiftAmount !== '0in' ? 'translateX(' + shiftAmount + ')' : 'translateX(0in)';
148 + }
149 }
150
151 function initFlipMode() {
182 if (isAnimating) return;
183 isAnimating = true;
184
185 + // Update book position synchronously with the flip animation
186 + updateBookPosition(toSpread, true);
187 +
188 const direction = toSpread > fromSpread ? 'forward' : 'backward';
189 const leaves = document.querySelectorAll('.zine-leaf');
190
363 leaf.style.zIndex = String(index + 1);
364 }
365 });
366 + scaleToFit();
367 + updateBookPosition(currentSpread, false);
368 } else {
369 showSpread(currentSpread, false);
370 }
- scaleToFit();
371
372 const blob = new Blob([html], { type: 'text/html' });
373 const url = URL.createObjectURL(blob);
379 }
380 });
381
- window.addEventListener('resize', scaleToFit);
382 + window.addEventListener('resize', function() {
383 + scaleToFit();
384 + if (USE_FLIP_ANIMATION) {
385 + updateBookPosition(currentSpread, false);
386 + }
387 + });
388
389 // Initialize flip mode
390 if (USE_FLIP_ANIMATION) {
393 document.getElementById('zine-prev').disabled = true;
394 document.getElementById('zine-next').disabled = false;
395 scaleToFit();
396 + updateBookPosition(0, false);
397 } else {
398 showSpread(0, false);
399 }