diff --git a/.gitignore b/.gitignore index b7422ad..6d02cb5 100644 --- a/.gitignore +++ b/.gitignore @@ -314,4 +314,7 @@ Database/Vaults/* **/wwwroot/js/* # ...except hand-authored client JS modules (not TS compile output). !DeepDrftPublic.Client/wwwroot/js/ -!DeepDrftPublic.Client/wwwroot/js/*.js \ No newline at end of file +!DeepDrftPublic.Client/wwwroot/js/*.js +# RCL compiled JS must be committed — MapStaticAssets serves from build-time manifest; +# gitignored TS output is absent when manifest is generated, so absent from publish output. +!DeepDrftShared.Client/wwwroot/js/parallax/ \ No newline at end of file diff --git a/DeepDrftShared.Client/wwwroot/js/parallax/parallax.js b/DeepDrftShared.Client/wwwroot/js/parallax/parallax.js new file mode 100644 index 0000000..3c88f04 --- /dev/null +++ b/DeepDrftShared.Client/wwwroot/js/parallax/parallax.js @@ -0,0 +1,127 @@ +/** + * parallax - scroll-driven background-position panning for ParallaxImage. + * + * Single Responsibility: own the parallax math and scroll/observer lifecycle. + * Blazor owns the component lifecycle and calls register/unregister. When the + * IntersectionObserver fires and JS attaches the scroll listener, this module + * sets data-parallax-active and immediately primes background-position-y — + * atomically cancelling the pre-WASM CSS animation and writing the correct + * position in the same turn, so there is no flash at the handoff. + */ +const handles = new Map(); +let _handleCounter = 0; +const reducedMotion = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches; +function clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); +} +function applyParallax(handle) { + const { element, options } = handle; + const rect = element.getBoundingClientRect(); + const viewportH = window.innerHeight; + let progress = options.invertDirection + ? rect.top / viewportH + : 1 - rect.top / viewportH; + progress = clamp(progress, 0, 1); + const pos = progress * clamp(options.speed, 0, 1) * 100; + // Write background-position-y on each layer directly — the same property the + // pre-WASM CSS animation drives (now cancelled via data-parallax-active). + const layers = element.querySelectorAll(':scope > .layer'); + for (const layer of layers) { + layer.style.backgroundPositionY = `${pos}%`; + } +} +function attachScrollListener(handle) { + if (handle.scrollListener || reducedMotion()) + return; + const listener = () => { + if (handle.rafId !== null) + return; + handle.rafId = requestAnimationFrame(() => { + handle.rafId = null; + applyParallax(handle); + }); + }; + handle.scrollListener = listener; + window.addEventListener('scroll', listener, { passive: true }); + // Cancel CSS animation and prime position atomically — no gap where neither drives. + handle.element.setAttribute('data-parallax-active', ''); + applyParallax(handle); +} +function detachScrollListener(handle) { + if (handle.scrollListener) { + window.removeEventListener('scroll', handle.scrollListener); + handle.scrollListener = null; + } + if (handle.rafId !== null) { + cancelAnimationFrame(handle.rafId); + handle.rafId = null; + } +} +function setupAutoHeight(handle) { + if (!handle.options.heightFraction || !handle.options.image1) + return; + const fraction = handle.options.heightFraction; + const element = handle.element; + const applyHeight = (naturalW, naturalH) => { + const containerW = element.offsetWidth; + if (containerW === 0 || naturalW === 0) + return; + const h = Math.round(containerW * (naturalH / naturalW) * fraction); + element.style.setProperty('--window-height', `${h}px`); + }; + const probe = new Image(); + probe.onload = () => { + const nw = probe.naturalWidth; + const nh = probe.naturalHeight; + applyHeight(nw, nh); + // Watch container width changes (orientation, responsive layout, etc.) + handle.resizeObserver = new ResizeObserver(() => { + applyHeight(nw, nh); + }); + handle.resizeObserver.observe(element); + }; + probe.src = handle.options.image1; +} +export function register(element, options) { + const id = `parallax-${++_handleCounter}`; + const realObserver = new IntersectionObserver((entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + attachScrollListener(handle); + } + else { + detachScrollListener(handle); + } + } + }); + const handle = { + element, + options: { ...options, speed: clamp(options.speed, 0, 1) }, + observer: realObserver, + resizeObserver: null, + scrollListener: null, + rafId: null, + }; + handle.observer.observe(element); + handles.set(id, handle); + 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; +} +export function unregister(handleId) { + const handle = handles.get(handleId); + if (!handle) + return; + detachScrollListener(handle); + handle.element.removeAttribute('data-parallax-active'); + handle.observer.disconnect(); + handle.resizeObserver?.disconnect(); + handles.delete(handleId); +} +//# sourceMappingURL=/js/parallax/parallax.js.map \ No newline at end of file diff --git a/DeepDrftShared.Client/wwwroot/js/parallax/parallax.js.map b/DeepDrftShared.Client/wwwroot/js/parallax/parallax.js.map new file mode 100644 index 0000000..1433bce --- /dev/null +++ b/DeepDrftShared.Client/wwwroot/js/parallax/parallax.js.map @@ -0,0 +1 @@ +{"version":3,"file":"parallax.js","sourceRoot":"/Interop/","sources":["parallax/parallax.ts"],"names":[],"mappings":"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"} \ No newline at end of file