fix(parallax): prime parallax position with pre-Blazor init script to kill Server->WASM position pop
This commit is contained in:
@@ -141,6 +141,14 @@ export function register(element: HTMLElement, options: RegisterOptions): string
|
||||
|
||||
setupAutoHeight(handle);
|
||||
|
||||
// Prime position synchronously so enhanced navigation and WASM handoff have
|
||||
// zero-frame gap. The init script covers cold page load; this covers nav.
|
||||
// Skip under reduced motion — parallax.ts never drives position then.
|
||||
if (!reducedMotion()) {
|
||||
element.setAttribute('data-parallax-active', '');
|
||||
applyParallax(handle);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* parallax-init — synchronous pre-Blazor primer for ParallaxImage.
|
||||
*
|
||||
* Why this exists (the Server→WASM "position pop"):
|
||||
* App.razor uses InteractiveAuto. On a cold WASM cache the page renders
|
||||
* server-side, the Server circuit hydrates and parallax.js register()s
|
||||
* (setting data-parallax-active, which cancels the CSS view() animation),
|
||||
* then WASM takes over: Blazor disposes the Server component → DisposeAsync
|
||||
* → unregister() → data-parallax-active is REMOVED. For the multi-frame gap
|
||||
* until WASM's new instance re-register()s (its IntersectionObserver fires
|
||||
* asynchronously), the CSS animation resumes and drives background-position-y
|
||||
* to its own view()-timeline value, which differs from the JS math — that
|
||||
* brief reversion is the visible pop.
|
||||
*
|
||||
* This script runs ONCE, synchronously, after the DOM is parsed but before
|
||||
* Blazor boots, and sets data-parallax-active on every parallax window. That
|
||||
* attribute is never tied to a component instance, so it survives the
|
||||
* Server→WASM handoff — the CSS animation stays cancelled the whole time and
|
||||
* parallax.js remains the sole writer of background-position-y across both
|
||||
* render modes. register()/unregister() lifecycle is unchanged; on genuine
|
||||
* navigation the element leaves the DOM entirely, so a stale attribute is moot.
|
||||
*
|
||||
* Why plain JS, not TypeScript:
|
||||
* A synchronous pre-Blazor primer must be a classic <script> (no module
|
||||
* graph, no async import). The RCL's TS pipeline emits ESNext modules, which
|
||||
* are deferred — too late to beat the handoff. This file is therefore a
|
||||
* hand-authored static asset, deliberately outside Interop/.
|
||||
*
|
||||
* Math parity with parallax.ts applyParallax():
|
||||
* speed and direction are recovered from the inline custom properties the
|
||||
* component already sets (--parallax-from / --parallax-to encode
|
||||
* ParallaxSpeed and InvertDirection), then the SAME progress→position formula
|
||||
* is applied. Any divergence here would itself be a pop, so the two must stay
|
||||
* in lockstep — change one, change the other.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function prime(win) {
|
||||
var styles = getComputedStyle(win);
|
||||
var from = parseFloat(styles.getPropertyValue('--parallax-from')) || 0;
|
||||
var to = parseFloat(styles.getPropertyValue('--parallax-to')) || 0;
|
||||
|
||||
// Component encodes: non-inverted → from:0, to:end; inverted → from:end, to:0.
|
||||
// Recover the original inputs from whichever endpoint carries the magnitude.
|
||||
var invert = from > to;
|
||||
var speed = clamp(Math.max(from, to) / 100, 0, 1);
|
||||
|
||||
var rect = win.getBoundingClientRect();
|
||||
var viewportH = window.innerHeight;
|
||||
|
||||
var progress = invert ? rect.top / viewportH : 1 - rect.top / viewportH;
|
||||
progress = clamp(progress, 0, 1);
|
||||
|
||||
var pos = progress * speed * 100;
|
||||
|
||||
var layers = win.querySelectorAll(':scope > .layer');
|
||||
for (var i = 0; i < layers.length; i++) {
|
||||
layers[i].style.backgroundPositionY = pos + '%';
|
||||
}
|
||||
|
||||
// Cancel the CSS animation and adopt the JS value atomically.
|
||||
win.setAttribute('data-parallax-active', '');
|
||||
}
|
||||
|
||||
function primeAll() {
|
||||
// Match parallax.ts: under reduced motion the module skips its scroll
|
||||
// listener and never sets data-parallax-active, leaving the layer at
|
||||
// --parallax-from (CSS animation is already `none` via media query).
|
||||
// Do the same here so the two paths stay consistent.
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
var windows = document.querySelectorAll('.parallax-window');
|
||||
for (var i = 0; i < windows.length; i++) {
|
||||
prime(windows[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', primeAll);
|
||||
} else {
|
||||
primeAll();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user