MynduiMyndui
0

Anatomy of Beam Draw

How scroll progress becomes a spring-smoothed SVG pathLength, and why four fan paths share one motion value.

01/04Four paths, one origin
Origin
shared left endpoint at (0, 200) on the 1000×400 viewBox
Fan paths
DEFAULT_PATHS — four cubics ending at y 40 / 150 / 250 / 360
Ends
right-edge destinations; token bars stand in for node labels
tsx
const DEFAULT_PATHS = ["M0 200 C 250 200, 350 60, 600 60 S 900 40, 1000 40","M0 200 C 250 200, 350 140, 600 140 S 900 150, 1000 150","M0 200 C 250 200, 350 260, 600 260 S 900 250, 1000 250","M0 200 C 250 200, 350 340, 600 340 S 900 360, 1000 360",];
01

Four paths, one origin

BeamDraw doesn't animate a stroke dash by hand each frame. It maps the SVG's own scroll progress through a spring, then hands the result to every path as pathLength. Geometry is static; only how much of each path is revealed changes.

The default set lives on a 1000×400 viewBox. All four cubics start at (0, 200) and fan to the right edge at y = 40 / 150 / 250 / 360 — a left core feeding four destinations. Pass your own paths array to replace them; the motion plumbing stays identical.

Each <motion.path> gets the same pathLength MotionValue. There's no per-path stagger in the component — the fan reads as simultaneous draw because the curves diverge spatially, not temporally.

02/04Scroll window → pathLength
00.10.81
Dead zone
scrollYProgress < 0.1 → pathLength stays 0
Active window
useTransform([0.1, 0.8], [0, 1])
pathLength
SVG stroke fill fraction — 0 empty, 1 complete
tsx
const { scrollYProgress } = useScroll({target: containerRef as unknown as React.RefObject<HTMLElement>,offset: ["start end", "end start"],});const pathLength = useSpring(useTransform(scrollYProgress, [0.1, 0.8], [0, 1]),SPRING,);
02

Scroll window → pathLength

useScroll tracks the SVG with offset: ["start end", "end start"] — the full travel from entering the viewport bottom to leaving the top. useTransform then squeezes that into a narrower active band:

Below 0.1 the beams stay empty; above 0.8 they're fully drawn. The dead zones keep the stroke from flickering on the first/last pixels of scroll — you're past "just entered" before ink starts, and finished before the element exits.

03/04Spring first, then glow
Raw map
useTransform alone — hard edges at 0.1 / 0.8
Spring
useSpring({ stiffness: 320, damping: 32, mass: 0.9 })
Glow
drop-shadow with color-mix(primary 50%, transparent)
tsx
const SPRING = { stiffness: 320, damping: 32, mass: 0.9 } as const;// …className={`w-full [filter:drop-shadow(0_0_8px_color-mix(in_oklch,var(--primary)_50%,transparent))] ${className ?? ""}`}style={{ pathLength: reduce ? 1 : pathLength }}
03

Spring first, then glow

Raw scroll progress is jerky. The mapped value feeds useSpring({ stiffness: 320, damping: 32, mass: 0.9 })SPRING.smooth — so pathLength eases instead of tracking the wheel 1:1. The SVG itself carries a primary-tinted drop-shadow; color-mix keeps the glow on-theme without a second paint layer.

04/04Result
04

The result

One MotionValue, four static paths, a spring between scroll and stroke — that's the whole machine.

Reduced motion

useReducedMotion() short-circuits the spring entirely: every path renders at pathLength: 1. The glow stays; the draw-on-scroll gesture doesn't. You still see the fan — it just arrives complete.

Motion Score

Beam DrawSSCompositor-only
SfilterFilter (blur / brightness)
SpathLengthSVG pathLength reveal
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →