Files

91 lines
4.0 KiB
JavaScript

/*
* 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();
}
})();