Scroll animations
There are two ways to tie motion to a scrollbar, and they answer different questions.
Scroll-linked motion asks "how far along are we?" and maps that
progress continuously onto a property - the reader scrubs the animation.
Scroll-triggered motion asks "is this on screen yet?" and plays a
normal animation once the answer changes. For the first there is a fast path -
Timeline, which hands the scrubbing to the browser outright - and a general one,
BmotionScrollTracker, for when you need the number itself.
WhileInView covers the second.
Scroll timelines: let the browser do it
A scroll handler that calls back into .NET on every scroll event is the expensive way to
scrub an animation, and it is the one thing you cannot make fast from the outside.
Timeline hands the whole thing to the browser instead: the keyframes are
pre-sampled in C# once, and progress comes from the browser's own
ScrollTimeline / ViewTimeline. After the handoff there is no
scroll handler, no frame loop and no interop at all - the animation runs on the compositor.
Where those APIs are missing, the bridge scrubs the same Web Animation from one passive
listener, so the values are still interpolated by the browser rather than round-tripped
through .NET.
BmScrollTimeline.Page() is the whole document's progress;
Container(selector) is one scroller's; View() is the animated
element's own journey through the viewport, and ViewOf(selector) is some other
element's. Only transforms and opacity can be handed over this way - they are
what the browser can interpolate for us - and while a timeline is attached it owns those
properties, so Transition does not apply: scroll position is the
progress.
Tracking the scroll position
Inject BmotionScrollTracker and call ObserveAsync to receive a
BmScrollInfo on every scroll: absolute offsets and normalised
0..1 progress on both axes. The tracker is transient and owned by the
component that injected it, which is why the page below implements
IAsyncDisposable - the browser-side listener goes away with the component
rather than outliving it.
Scroll-linked motion values
A callback that calls StateHasChanged on every scroll event is a diff per
frame. Motion values skip that entirely: bind them with the Values parameter
and their changes are flushed straight to the DOM each frame
without re-rendering the component. Transform derives new values from
them - here the same progress drives a scaleX bar directly and, mapped through
0..1 โ 0..360, a full rotation over the length of the page.
Motion templates
Some properties are not a number - they are a sentence made of numbers.
Bm.Template composes motion values into a CSS string and
StringValues binds the result to any property, so a
filter built from two independently derived values updates as one. The blur
below peaks at the middle of the page ([0, 0.5, 1] โ [0, 4, 0]) while the hue
rotates straight through.
An element's own progress
Window progress is the wrong measure for anything that is not the whole page. Pass
BmScrollOptions a TargetId and a pair of
Offset alignments instead, and progress is measured over that element's
journey through the viewport. Each offset reads
"<target edge> <container edge>", so the default
["start end", "end start"] means 0 when its top reaches the bottom of the
viewport, 1 when its bottom passes the top. Scroll the striped panel past and watch
the ring fill.
whileInView
WhileInView is the trigger half: an ordinary gesture state, activated by an
IntersectionObserver rather than a pointer. Once="true" keeps it
from replaying every time the element crosses the fold - the right default for entrance
animations, since a section that re-animates on the way back up reads as a glitch. For
finer control, Viewport takes a Margin and an Amount
("some", "all", or a 0..1 threshold).