Parallax Circles
Move your pointer. Each ring drifts on its own parallax factor while counter-rotating on its own clock. The same component drives the section backgrounds elsewhere on this site.
How it works
A single pair of motion values tracks the pointer. Each ring derives its center from those values multiplied by a per-layer factor — the outer rings move more than the inner ones, which sells the depth without any 3D math.
Rotation is independent: every ring picks a random duration and direction on mount and loops forever. Strokes are dashed and use pathLength="100" so the dash pattern stays consistent regardless of radius.
Perspective model
Sizing isn't hand-tuned per ring — it falls out of a pinhole-camera formula. Each ring sits at a world depth d between focalLength (the nearest) and focalLength + worldDepth (the farthest), and its projected radius is (focalLength · maxRadius) / d.
That single ratio gives the whole composition its "lens":
- Short focal length — wide-angle. Near rings dominate, far rings shrink hard. Strong depth.
- Long focal length — telephoto. Near and far compress toward the same size. Flatter, calmer.
Parallax follows the same ratio: each ring's pointer factor is proportional to its projected radius, so near rings drift more than far ones for free.
Motion patterns
The component accepts a motionPattern prop. Three flavors of follow:
- spring — bouncy, the default. Overshoots slightly and feels alive.
- ease — exponential lerp every frame. Quieter, no overshoot.
- direct — no smoothing. Snappy, twitchier, more honest about the input.
Variants
Same component, different lenses. Each tile shares geometry — only focalLength and the motion pattern change. Move across them to feel the depth shift.