commit 98ec6608fb25192a41230207b485b4ea17ee5744
parent 479562d2c65cbb5edc4ab583985faa7f0a32f34c
Author: Hunter
Date:   Wed,  4 Feb 2026 14:56:39 -0500

scaling fixed; judder when opening/closing covers

Diffstat:
Mresources/page-flip-animation.js | 47+++++++++++++++++++++++++++++++++++------------
Mresources/preview-manager.js | 19++++++++++++++++---
Mresources/spread-layout.js | 10++++++++--
Mresources/viewer-generator.js | 57++++++++++++++++++++++++++++++++++++++++++++++++++++++---
4 files changed, 113 insertions(+), 20 deletions(-)

diff --git a/resources/page-flip-animation.js b/resources/page-flip-animation.js @@ -73,7 +73,10 @@ export function initPageFlip(container, doc) { // Update the book container horizontal position based on spread // Spreads 0 and 4 (single pages) should be centered, others at normal position function updateBookPosition(spreadIndex, animated = true) { - if (!bookContainer) return; + if (!bookContainer) { + console.error('updateBookPosition: bookContainer is null!'); + return; + } // Spread 0 (front cover): single right page, shift left to center // Spread 4 (back cover): single left page, shift right to center @@ -85,24 +88,40 @@ function updateBookPosition(spreadIndex, animated = true) { shiftAmount = '1.375in'; } + const currentTransform = bookContainer.style.transform; + const currentTransition = bookContainer.style.transition; + console.log(`updateBookPosition(spread=${spreadIndex}, animated=${animated})`); + console.log(` Current transform: "${currentTransform}"`); + console.log(` Current transition: "${currentTransition}"`); + console.log(` Target: translateX(${shiftAmount})`); + if (animated) { - bookContainer.style.transition = 'transform 0.6s cubic-bezier(0.645, 0.045, 0.355, 1.000)'; - } else { - bookContainer.style.transition = 'none'; - } + // Clear any existing transition first to ensure clean state + bookContainer.style.transition = ''; + // Force a reflow + bookContainer.offsetHeight; - // Preserve any existing scale from scaleSpreadToFit - const currentTransform = bookContainer.style.transform || ''; - const scaleMatch = currentTransform.match(/scale\([^)]+\)/); - const scale = scaleMatch ? scaleMatch[0] : ''; + // Now set the transition for this animation + bookContainer.style.transition = 'transform 0.6s cubic-bezier(0.645, 0.045, 0.355, 1.000)'; + console.log(` Set transition, will animate in rAF`); - bookContainer.style.transform = scale ? `${scale} translateX(${shiftAmount})` : `translateX(${shiftAmount})`; + // Use requestAnimationFrame to set transform (same as leaf flip animation) + // This ensures both animations start in the same frame + requestAnimationFrame(() => { + console.log(` rAF: Setting transform to translateX(${shiftAmount})`); + bookContainer.style.transform = `translateX(${shiftAmount})`; + }); - // Remove transition after animation completes - if (animated) { + // Remove transition after animation completes setTimeout(() => { bookContainer.style.transition = ''; }, 600); + } else { + bookContainer.style.transition = 'none'; + // For non-animated, use explicit translateX or none + const newTransform = shiftAmount !== '0in' ? `translateX(${shiftAmount})` : 'translateX(0in)'; + console.log(` Non-animated: Setting transform to ${newTransform}`); + bookContainer.style.transform = newTransform; } } @@ -151,10 +170,13 @@ function executePageFlip(fromSpread, toSpread, container, doc, onComplete) { isAnimating = true; currentSpreadIndex = toSpread; + console.log(`\n=== executePageFlip: ${fromSpread} → ${toSpread} ===`); + // Update book position synchronously with the flip animation updateBookPosition(toSpread, true); const direction = toSpread > fromSpread ? 'forward' : 'backward'; + console.log(`Direction: ${direction}`); const leaves = doc.querySelectorAll('.zine-leaf'); if (direction === 'forward') { @@ -251,6 +273,7 @@ export function animatePageFlip(fromSpread, toSpread, container, doc, onComplete // Set current spread without animation (for initial load) export function setSpreadImmediate(spreadIndex, doc) { + console.log(`\n=== setSpreadImmediate(${spreadIndex}) ===`); currentSpreadIndex = spreadIndex; updateLeafStates(spreadIndex, doc); updateBookPosition(spreadIndex, false); diff --git a/resources/preview-manager.js b/resources/preview-manager.js @@ -125,10 +125,23 @@ export function updatePreview(editorView, isEditorFocused) { // Set initial spread state without animation setTimeout(() => { + scaleSpreadToFit(container, doc); // Must call this BEFORE setSpreadImmediate so zoom is set setSpreadImmediate(currentSpread, doc); - const scaleToFit = () => scaleSpreadToFit(container, doc); - scaleToFit(); - doc.defaultView.addEventListener('resize', scaleToFit); + + // On resize, update zoom and reposition book + const handleResize = () => { + scaleSpreadToFit(container, doc); + // Get the actual current spread from the leaf states + const leaves = doc.querySelectorAll('.zine-leaf'); + let activeSpread = 0; + leaves.forEach((leaf, index) => { + if (leaf.dataset.state === 'open') { + activeSpread = index + 1; + } + }); + setSpreadImmediate(activeSpread, doc); + }; + doc.defaultView.addEventListener('resize', handleResize); }, 0); } else { // Original non-animated mode diff --git a/resources/spread-layout.js b/resources/spread-layout.js @@ -73,12 +73,18 @@ export function scaleSpreadToFit(container, doc, bottomPadding = 0) { const scaleY = (vh - 40) / spreadHeightPx; const scale = Math.min(scaleX, scaleY); + // Use CSS zoom for high-resolution rendering instead of transform scale + bookContainer.style.zoom = scale; + + // Store the current zoom on the book container for use by updateBookPosition + bookContainer.dataset.currentZoom = scale; + // Preserve any existing translateX from book positioning const currentTransform = bookContainer.style.transform || ''; const translateMatch = currentTransform.match(/translateX\([^)]+\)/); const translateX = translateMatch ? translateMatch[0] : ''; - bookContainer.style.transform = translateX ? `scale(${scale}) ${translateX}` : `scale(${scale})`; + bookContainer.style.transform = translateX || ''; // Notify parent of spread position and grid size for background grid if (doc.defaultView?.parent && doc.defaultView.parent !== doc.defaultView) { @@ -90,7 +96,7 @@ export function scaleSpreadToFit(container, doc, bottomPadding = 0) { const gridWidth = gridHeight * (30.25 / 29.75); // Remove translateX shift so grid always anchors to the un-shifted book position - // The translateX is in inches and gets multiplied by scale + // With CSS zoom, translateX values are in the zoomed coordinate space const translateInchMatch = translateX.match(/translateX\(([^)]+)in\)/); const translatePx = translateInchMatch ? parseFloat(translateInchMatch[1]) * 96 * scale : 0; diff --git a/resources/viewer-generator.js b/resources/viewer-generator.js @@ -104,7 +104,48 @@ export function generateViewerCode() { const scaleX = (vw - 40) / spreadWidthPx; const scaleY = (vh - 40) / spreadHeightPx; const scale = Math.min(scaleX, scaleY); - bookContainer.style.transform = 'scale(' + scale + ')'; + bookContainer.style.zoom = scale; + bookContainer.dataset.currentZoom = scale; + } + + function updateBookPosition(spreadIndex, animated) { + const bookContainer = container.querySelector('.zine-book'); + if (!bookContainer) return; + + // Spread 0 (front cover): single right page, shift left to center + // Spread 4 (back cover): single left page, shift right to center + // Half page width = 1.375in + let shiftAmount = '0in'; + if (spreadIndex === 0) { + shiftAmount = '-1.375in'; + } else if (spreadIndex === 4) { + shiftAmount = '1.375in'; + } + + if (animated) { + // Clear any existing transition first to ensure clean state + bookContainer.style.transition = ''; + // Force a reflow + bookContainer.offsetHeight; + + // Now set the transition for this animation + bookContainer.style.transition = 'transform 0.6s cubic-bezier(0.645, 0.045, 0.355, 1.000)'; + + // Use requestAnimationFrame to set transform (same as leaf flip animation) + // This ensures both animations start in the same frame + requestAnimationFrame(function() { + bookContainer.style.transform = 'translateX(' + shiftAmount + ')'; + }); + + // Remove transition after animation completes + setTimeout(function() { + bookContainer.style.transition = ''; + }, 600); + } else { + bookContainer.style.transition = 'none'; + // For non-animated, use explicit translateX + bookContainer.style.transform = shiftAmount !== '0in' ? 'translateX(' + shiftAmount + ')' : 'translateX(0in)'; + } } function initFlipMode() { @@ -141,6 +182,9 @@ export function generateViewerCode() { if (isAnimating) return; isAnimating = true; + // Update book position synchronously with the flip animation + updateBookPosition(toSpread, true); + const direction = toSpread > fromSpread ? 'forward' : 'backward'; const leaves = document.querySelectorAll('.zine-leaf'); @@ -319,10 +363,11 @@ export function generateViewerCode() { leaf.style.zIndex = String(index + 1); } }); + scaleToFit(); + updateBookPosition(currentSpread, false); } else { showSpread(currentSpread, false); } - scaleToFit(); const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); @@ -334,7 +379,12 @@ export function generateViewerCode() { } }); - window.addEventListener('resize', scaleToFit); + window.addEventListener('resize', function() { + scaleToFit(); + if (USE_FLIP_ANIMATION) { + updateBookPosition(currentSpread, false); + } + }); // Initialize flip mode if (USE_FLIP_ANIMATION) { @@ -343,6 +393,7 @@ export function generateViewerCode() { document.getElementById('zine-prev').disabled = true; document.getElementById('zine-next').disabled = false; scaleToFit(); + updateBookPosition(0, false); } else { showSpread(0, false); }