KQ-1535 · TIDAL/PAIR

Tidal Pair

Two small spiral galaxies on a slow, close pass, drawn in Canvas 2D points: a restricted three-body encounter in which two cores swing past each other on a bound orbit and every star of their prograde, tilted disks feels both, integrated with a fixed-step kick-drift-kick leapfrog, so the near sides are drawn into a bridge of stars and the far sides fling out into tidal tails that wrap and unwind. Checkpoints filled in the background make every moment exact, so a scrub shows the same sky every time, and the frame follows the pair as they part. It plays on its own at an unhurried pace and loops with a fade; drag sideways to scrub through time 1:1, and let go — a flick carries on and its pace eases back to playback. It is a slider through time: arrows step it, Space plays and pauses, Home and End jump.

KQ-1535 · TIDAL/PAIR

Drag sideways or use the arrow keys to move through the encounter; Space plays and pauses it.

Gaugeworks · public night

Two galaxies, one close pass

Drag across the sky to scrub

playing · bridge forming

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/tidal-pair

Props

massRatio
number
default 0.5
The companion's mass against the primary's, 0.1 (a small satellite) to 1 (an equal pair).
inclination
number
default 25
How far the disks tilt from the plane of their orbit, in degrees, 0 (flat, long tails) to 90.
starCount
number
default 1600
How many stars the two disks hold between them, 200 to 3000.
colors
readonly [string, string]
default ["var(--accent-bright)", "var(--warn)"]
The primary's and the companion's stars.
duration
number
default 26000
How long the whole encounter takes to play, ms.
view
number
default 30
How far the orbit's plane is tilted from face-on, in degrees, 0 to 80.
closeness
number
default 1
How close they pass, as the closest distance over a disk's size, 0.6 (grazing) to 1.6 (wide).
playing / defaultPlaying
boolean
default true
Whether the encounter plays on by itself. Controlled or initial.
onPlayingChange
(playing: boolean) => void
default —
Fires when Space or Enter starts or stops the playback, and when the arrow keys pause it to step.
defaultTime
number
default 0.3
Where in the encounter it starts, 0 (far apart) to 1 (tails unwound).
loop
boolean
default true
At the end, hold a moment, fade, and play again.
step
number
default 0.025
How far an arrow key steps through the encounter, as a share of it.
onScrub
(time: number) => void
default —
Fires as the hand or the keys move through time, with the time from 0 to 1.
onPhaseChange
(phase: "approach" | "closest" | "bridge" | "tails") => void
default —
Fires when the encounter passes into a new phase: approach, closest approach, the bridge forming, the tails unwinding.
seed
number
default 1
Seeds the stars, so two encounters never match.
dpr
number
default 2
Cap on the canvas's device pixel ratio, at most 2.
disabled
boolean
default false
No scrubbing by hand or key.
label
string
default "Encounter time"
The accessible name of the timeline control.
className
string
default —
Sizes the box; it fills its parent by default (h-full w-full).