The carousel is a ring of cards at angle θ around a central pivot, drawn here from above as an orbiting ring. Dragging sets θ directly from pointer motion and records an angular velocity ω = Δθ/Δt. On release the ring keeps spinning under momentum, decaying like real rotational friction:
ω(t+dt) = ω(t) · e^(-friction · dt)
Once |ω| drops below a small threshold (or a Next/Prev/autoplay/dot click fires), the ring switches to a critically-damped spring that pulls θ to the nearest slide angle θtarget = −index·2π/N — exactly the model behind CSS scroll-snap and native app carousels:
a = k·(θ_target − θ) − c·ω
c = ζ · 2√k (ζ=1 is critical damping: fastest snap, no overshoot)
ω += a·dt ; θ += ω·dt
- ζ < 1 (underdamped) — the slide overshoots and oscillates before settling, like a bouncy UI easing curve. Watch the response chart ring like a decaying sine wave.
- ζ = 1 (critical) — the fastest snap with zero overshoot, the "snappy" feel most sliders aim for.
- ζ > 1 (overdamped) — a slow, syrupy settle with no bounce; the chart eases in without crossing zero.
- Autoplay advances the target slide index every interval and lets the same spring carry it there, just like an auto-advancing image slider.
Depth cue fix: the near/far card scaling here uses depthFactor = (1 − cos a)/2, which runs 0 (nearest card, biggest & most opaque) → 1 (farthest card, smallest & faintest) — the correct direction for perspective. A common mistake in a naive port of this formula leaves it unnormalized and inverts near/far.
Real-world relevance: this is the same spring-physics model used by iOS/Android scroll views, Framer Motion, and CSS scroll-snap-type to make dragged lists settle naturally instead of snapping instantly.