111 lines
3.4 KiB
CSS
111 lines
3.4 KiB
CSS
/*
|
|
* 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;
|
|
}
|
|
}
|