Merge branch 'p7-w5-parallax-prerender-pos' into dev
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
<link rel="stylesheet" href="@Assets["_content/MudBlazor/MudBlazor.min.css"]" />
|
<link rel="stylesheet" href="@Assets["_content/MudBlazor/MudBlazor.min.css"]" />
|
||||||
<link rel="stylesheet" href="@Assets["DeepDrftPublic.styles.css"]"/>
|
<link rel="stylesheet" href="@Assets["DeepDrftPublic.styles.css"]"/>
|
||||||
<link rel="stylesheet" href="@Assets["_content/DeepDrftShared.Client/styles/deepdrft-tokens.css"]" />
|
<link rel="stylesheet" href="@Assets["_content/DeepDrftShared.Client/styles/deepdrft-tokens.css"]" />
|
||||||
|
<link rel="stylesheet" href="@Assets["_content/DeepDrftShared.Client/css/parallax.css"]" />
|
||||||
<link rel="stylesheet" href="@Assets["styles/deepdrft-styles.css"]" />
|
<link rel="stylesheet" href="@Assets["styles/deepdrft-styles.css"]" />
|
||||||
<ImportMap />
|
<ImportMap />
|
||||||
<link rel="icon" type="image/ico" href="deepdrft-logo.ico" />
|
<link rel="icon" type="image/ico" href="deepdrft-logo.ico" />
|
||||||
|
|||||||
@@ -87,11 +87,11 @@ public abstract class ParallaxImageBase : ComponentBase, IAsyncDisposable
|
|||||||
private string? _handle;
|
private string? _handle;
|
||||||
|
|
||||||
// --parallax-from/--parallax-to are inherited custom properties read by the
|
// --parallax-from/--parallax-to are inherited custom properties read by the
|
||||||
// CSS @keyframes parallax-pan (scroll-driven animation on .parallax-window).
|
// CSS @keyframes parallax-pan, which animates background-position-y on each
|
||||||
// They encode ParallaxSpeed and InvertDirection. --parallax-pos itself is
|
// .layer (scroll-driven, pre-WASM). They encode ParallaxSpeed and
|
||||||
// never set inline — an inline value would beat the CSS animation in the
|
// InvertDirection. After WASM boots, parallax.js sets data-parallax-active to
|
||||||
// cascade and defeat the pre-WASM scroll-driven parallax. JS sets it inline
|
// cancel that animation and drives background-position-y on the layers
|
||||||
// only after WASM boots, transparently taking over (inline > animation).
|
// directly — a clean handoff with no competing writers.
|
||||||
private string ParallaxVars()
|
private string ParallaxVars()
|
||||||
{
|
{
|
||||||
var end = (int)Math.Round(ParallaxSpeed * 100);
|
var end = (int)Math.Round(ParallaxSpeed * 100);
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
@property --parallax-pos {
|
|
||||||
syntax: '<percentage>';
|
|
||||||
inherits: true;
|
|
||||||
initial-value: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax-window {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
height: var(--window-height, 300px);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax-window.full-width {
|
|
||||||
width: 100vw;
|
|
||||||
position: relative;
|
|
||||||
left: 50%;
|
|
||||||
right: 50%;
|
|
||||||
margin-left: -50vw;
|
|
||||||
margin-right: -50vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: 50% var(--parallax-pos, 50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-1 {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-2 {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 700ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax-window:hover .layer-2 {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Cascade interaction for --parallax-pos:
|
|
||||||
*
|
|
||||||
* Before WASM (SSR / hydration):
|
|
||||||
* @property initial-value: 0%
|
|
||||||
* → CSS animation (view timeline) overrides → correct position from scroll
|
|
||||||
*
|
|
||||||
* After WASM (JS running):
|
|
||||||
* JS element.style.setProperty('--parallax-pos', ...) [inline style]
|
|
||||||
* → inline style beats animation → JS takes over seamlessly
|
|
||||||
*
|
|
||||||
* prefers-reduced-motion:
|
|
||||||
* animation: none → @property initial-value 0% used → static image
|
|
||||||
* JS also skips scroll listener
|
|
||||||
*/
|
|
||||||
@supports (animation-timeline: view()) {
|
|
||||||
@keyframes parallax-pan {
|
|
||||||
from { --parallax-pos: var(--parallax-from, 0%); }
|
|
||||||
to { --parallax-pos: var(--parallax-to, 0%); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax-window {
|
|
||||||
animation: parallax-pan linear both;
|
|
||||||
animation-timeline: view();
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.parallax-window {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.parallax-window {
|
|
||||||
--parallax-pos: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-2 {
|
|
||||||
transition-duration: 0ms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,11 @@
|
|||||||
* parallax - scroll-driven background-position panning for ParallaxImage.
|
* parallax - scroll-driven background-position panning for ParallaxImage.
|
||||||
*
|
*
|
||||||
* Single Responsibility: own the parallax math and scroll/observer lifecycle.
|
* Single Responsibility: own the parallax math and scroll/observer lifecycle.
|
||||||
* Blazor owns the component lifecycle and calls register/unregister; this module
|
* Blazor owns the component lifecycle and calls register/unregister. When the
|
||||||
* writes only the `--parallax-pos` CSS custom property — never concrete style.
|
* 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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface RegisterOptions {
|
interface RegisterOptions {
|
||||||
@@ -44,7 +47,12 @@ function applyParallax(handle: Handle): void {
|
|||||||
progress = clamp(progress, 0, 1);
|
progress = clamp(progress, 0, 1);
|
||||||
|
|
||||||
const pos = progress * clamp(options.speed, 0, 1) * 100;
|
const pos = progress * clamp(options.speed, 0, 1) * 100;
|
||||||
element.style.setProperty('--parallax-pos', `${pos}%`);
|
// 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<HTMLElement>(':scope > .layer');
|
||||||
|
for (const layer of layers) {
|
||||||
|
layer.style.backgroundPositionY = `${pos}%`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachScrollListener(handle: Handle): void {
|
function attachScrollListener(handle: Handle): void {
|
||||||
@@ -60,7 +68,8 @@ function attachScrollListener(handle: Handle): void {
|
|||||||
|
|
||||||
handle.scrollListener = listener;
|
handle.scrollListener = listener;
|
||||||
window.addEventListener('scroll', listener, { passive: true });
|
window.addEventListener('scroll', listener, { passive: true });
|
||||||
// Prime position immediately so entry isn't a frame behind the first scroll.
|
// Cancel CSS animation and prime position atomically — no gap where neither drives.
|
||||||
|
handle.element.setAttribute('data-parallax-active', '');
|
||||||
applyParallax(handle);
|
applyParallax(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +149,7 @@ export function unregister(handleId: string): void {
|
|||||||
if (!handle) return;
|
if (!handle) return;
|
||||||
|
|
||||||
detachScrollListener(handle);
|
detachScrollListener(handle);
|
||||||
|
handle.element.removeAttribute('data-parallax-active');
|
||||||
handle.observer.disconnect();
|
handle.observer.disconnect();
|
||||||
handle.resizeObserver?.disconnect();
|
handle.resizeObserver?.disconnect();
|
||||||
handles.delete(handleId);
|
handles.delete(handleId);
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* ParallaxImage styles — served as a plain static asset via
|
||||||
|
* _content/DeepDrftShared.Client/css/parallax.css.
|
||||||
|
*
|
||||||
|
* Why global, not scoped (.razor.css):
|
||||||
|
* DeepDrftShared.Client is a WASM RCL referenced only by DeepDrftPublic.Client,
|
||||||
|
* not by the DeepDrftPublic server host. Blazor merges scoped-CSS bundles only
|
||||||
|
* from RCLs the *host* references, so this component's scoped bundle is absent
|
||||||
|
* from DeepDrftPublic.styles.css and never reaches the SSR first paint — it
|
||||||
|
* arrives only after WASM boots. Structural rules AND the scroll-driven
|
||||||
|
* animation must be present at first paint, so they live here as global CSS,
|
||||||
|
* delivered as a static web asset regardless of which project references the RCL.
|
||||||
|
*
|
||||||
|
* ParallaxImage is the sole producer of .parallax-window / .layer, so unscoped
|
||||||
|
* class selectors are unambiguous.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.parallax-window {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
height: var(--window-height, 300px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parallax-window.full-width {
|
||||||
|
width: 100vw;
|
||||||
|
position: relative;
|
||||||
|
left: 50%;
|
||||||
|
right: 50%;
|
||||||
|
margin-left: -50vw;
|
||||||
|
margin-right: -50vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position-x: 50%;
|
||||||
|
background-position-y: var(--parallax-from, 0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-1 {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-2 {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 700ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parallax-window:hover .layer-2 {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Scroll-driven parallax, present at SSR first paint (no JS, no custom-property
|
||||||
|
* inheritance chain):
|
||||||
|
*
|
||||||
|
* Before WASM:
|
||||||
|
* The view() timeline animates background-position-y on each .layer directly,
|
||||||
|
* from --parallax-from to --parallax-to (both percentages set inline on
|
||||||
|
* .parallax-window by the component, encoding ParallaxSpeed/InvertDirection).
|
||||||
|
* The layer pans as the window scrolls through the viewport — correct from
|
||||||
|
* first paint.
|
||||||
|
*
|
||||||
|
* After WASM:
|
||||||
|
* JS sets data-parallax-active on .parallax-window, which cancels the CSS
|
||||||
|
* animation (animation: none). JS then drives background-position-y via the
|
||||||
|
* scroll listener. One writer at a time — the two never compete.
|
||||||
|
*
|
||||||
|
* prefers-reduced-motion:
|
||||||
|
* animation: none → static image at --parallax-from. JS also skips its
|
||||||
|
* scroll listener (see parallax.ts), so the image stays put.
|
||||||
|
*/
|
||||||
|
@supports (animation-timeline: view()) {
|
||||||
|
@keyframes parallax-pan {
|
||||||
|
from { background-position-y: var(--parallax-from, 0%); }
|
||||||
|
to { background-position-y: var(--parallax-to, 0%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animate layers directly — no --parallax-pos inheritance chain.
|
||||||
|
.parallax-window uses overflow: hidden, which establishes a block
|
||||||
|
formatting context but NOT a scroll container (that needs overflow:
|
||||||
|
scroll/auto), so view() correctly resolves to the root scroller. */
|
||||||
|
.parallax-window > .layer {
|
||||||
|
animation: parallax-pan linear both;
|
||||||
|
animation-timeline: view();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JS takes over on register: cancel the CSS animation so the two writers
|
||||||
|
to background-position-y never compete. */
|
||||||
|
.parallax-window[data-parallax-active] > .layer {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.parallax-window > .layer {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.layer-2 {
|
||||||
|
transition-duration: 0ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user