fix judder without blur

Hunter
Wednesday, February 4th, 2026 at 4:47 PM

resources/page-flip-animation.js+21-38
resources/spread-layout.js+7-12
resources/viewer-generator.js+9-19

resources/page-flip-animation.js
 70  	updateBookPosition(0, false);
 71  }
 72  
    -// 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) {
    -		console.error('updateBookPosition: bookContainer is null!');
    -		return;
    -	}
    -
 73 +// Calculate the shift amount for a given spread
 74 +function getShiftAmountForSpread(spreadIndex) {
 75  	// Spread 0 (front cover): single right page, shift left to center
 76  	// Spread 4 (back cover): single left page, shift right to center
    -	// Half page width = 1.375in
    -	let shiftAmount = '0in';
 77 +	// margin-left is doubled to compensate for flex re-centering, so the net visual
 78 +	// shift is half the margin value (net visual shift = half a page width = 1.375in)
 79  	if (spreadIndex === 0) {
    -		shiftAmount = '-1.375in';
 80 +		return '-2.75in';
 81  	} else if (spreadIndex === 4) {
    -		shiftAmount = '1.375in';
 82 +		return '2.75in';
 83  	}
 84 +	return '0in';
 85 +}
 86  
    -	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) {
    -		// Clear any existing transition first to ensure clean state
    -		bookContainer.style.transition = '';
    -		// Force a reflow
    -		bookContainer.offsetHeight;
 87 +// Update the book container horizontal position based on spread
 88 +// Spreads 0 and 4 (single pages) should be centered, others at normal position
 89 +// Uses margin-left instead of translateX to avoid conflicts with preserve-3d and zoom
 90 +function updateBookPosition(spreadIndex, animated = true) {
 91 +	if (!bookContainer) return;
 92  
    -		// 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`);
 93 +	const shiftAmount = getShiftAmountForSpread(spreadIndex);
 94  
    -		// 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})`;
    -		});
 95 +	if (animated) {
 96 +		bookContainer.style.transition = 'margin-left 0.6s cubic-bezier(0.645, 0.045, 0.355, 1.000)';
 97 +		bookContainer.style.marginLeft = shiftAmount;
 98  
 99  		// Remove transition after animation completes
100  		setTimeout(() => {
101  			bookContainer.style.transition = '';
    -		}, 600);
102 +		}, 650);
103  	} else {
104  		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;
105 +		bookContainer.style.marginLeft = shiftAmount;
106 +		bookContainer.offsetHeight;
107 +		bookContainer.style.transition = '';
108  	}
109  }
110  
resources/spread-layout.js
 79  	// Store the current zoom on the book container for use by updateBookPosition
 80  	bookContainer.dataset.currentZoom = scale;
 81  
    -	// 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 || '';
    -
 82  	// Notify parent of spread position and grid size for background grid
 83  	if (doc.defaultView?.parent && doc.defaultView.parent !== doc.defaultView) {
 84  		const rect = bookContainer.getBoundingClientRect();

 88  		const gridHeight = rect.height / 11;
 89  		const gridWidth = gridHeight * (30.25 / 29.75);
 90  
    -		// Remove translateX shift so grid always anchors to the un-shifted book position
    -		// 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;
 91 +		// Remove the net visual shift so grid always anchors to the un-shifted book position
 92 +		// margin-left is doubled to compensate for flex re-centering, so the net visual
 93 +		// shift is half the margin value; getBoundingClientRect already reflects this
 94 +		const marginLeft = bookContainer.style.marginLeft || '0in';
 95 +		const marginMatch = marginLeft.match(/([-.0-9]+)in/);
 96 +		const netShiftPx = marginMatch ? (parseFloat(marginMatch[1]) / 2) * 96 * scale : 0;
 97  
 98  		doc.defaultView.parent.postMessage({
 99  			type: 'scaleChange',
100  			gridWidth,
101  			gridHeight,
    -			spreadLeft: rect.left - translatePx,
102 +			spreadLeft: rect.left - netShiftPx,
103  			spreadTop: rect.top
104  		}, '*');
105  	}
resources/viewer-generator.js
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
    -			// Half page width = 1.375in
117 +			// Doubled to compensate for flex re-centering (net visual shift = half)
118  			let shiftAmount = '0in';
119  			if (spreadIndex === 0) {
    -				shiftAmount = '-1.375in';
120 +				shiftAmount = '-2.75in';
121  			} else if (spreadIndex === 4) {
    -				shiftAmount = '1.375in';
122 +				shiftAmount = '2.75in';
123  			}
124  
125  			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 + ')';
    -				});
126 +				bookContainer.style.transition = 'margin-left 0.6s cubic-bezier(0.645, 0.045, 0.355, 1.000)';
127 +				bookContainer.style.marginLeft = shiftAmount;
128  
129  				// Remove transition after animation completes
130  				setTimeout(function() {
131  					bookContainer.style.transition = '';
    -				}, 600);
132 +				}, 650);
133  			} else {
134  				bookContainer.style.transition = 'none';
    -				// For non-animated, use explicit translateX
    -				bookContainer.style.transform = shiftAmount !== '0in' ? 'translateX(' + shiftAmount + ')' : 'translateX(0in)';
135 +				bookContainer.style.marginLeft = shiftAmount;
136 +				bookContainer.offsetHeight;
137 +				bookContainer.style.transition = '';
138  			}
139  		}
140