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