Lissajous Scope
lissajous-scopeVisualizersWhere the oscilloscope plots amplitude against time, this plots the buffer against a phase-shifted copy of itself — so one channel draws circles and rosettes with no stereo pair to feed it. The phase shift is what picks the figure: a quarter period turns the degenerate diagonal into a circle, and the offsets between sweep the ellipses and loops. Each frame washes over the last rather than clearing it, so trails decay like the phosphor persistence of a real CRT. The geometry is an exported pure function, so the same figure can be asserted on in a test or rendered without a canvas.
One mono signal against a 248° phase-shifted copy of itself. The trail decays instead of clearing, so the figure keeps a phosphor tail.
Install
Copies the source into your project. Anything it depends on comes with it.
$ npx shadcn add @syntho-ui/lissajous-scopePulls in
First time? Add the registry to your components.json once:
{
"registries": {
"@syntho-ui": "https://engine.syntho.app/r/{name}.json"
}
}Usage
Controlled or uncontrolled, whichever suits the surrounding state.
import { LissajousScope } from "@/components/ui/lissajous-scope"
<LissajousScope analyser={analyserNode} height={260} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| analyser | AnalyserNode | — | A live analyser to read time-domain data from. |
| getData | (target: Float32Array) => boolean | void | — | Supply samples yourself instead of using an analyser. |
| phaseShift | number | 248 / 360 | Offset between the X and Y reads as a fraction of the buffer, not degrees. This is what sets the figure's shape. |
| trailLength | number | 450 | How many points to plot. Clamped to half the buffer, and sampled in strides so the trail always spans the whole frame. |
| fadeSpeed | number | 0.1 | Alpha of the per-frame wash over the previous frame. Lower means longer persistence; 0 lets the figure accumulate. |
| zoomLevel | number | 0.6 | Size multiplier. 1 makes a full-scale signal span the shorter axis exactly, below 1 insets the figure, above 1 lets it overshoot. |
| lineWidth | number | 1 | Trace thickness in CSS pixels at the head of the trail. |
| enableGlow | boolean | false | Additive multi-pass bloom around the trace. Costs a pass per level. |
| glowIntensity | number | 3 | Number of bloom passes, 1–5. Only read when enableGlow is on. |
| showGrid | boolean | true | Draw the centre crosshair. |
| color | string | — | Trace colour. Defaults to the --uikit-accent token. |
| height | number | 220 | Display height in pixels; width defaults to 100%. |
| paused | boolean | false | Stop the animation loop and hold the last frame. |
Interaction
- Drag with pointer capture, so the gesture keeps tracking past the edge of the control.
- Shift while dragging for a fine pass; release it mid-gesture and coarse tracking resumes.
- Double-click resets to
defaultValue. - Arrow keys step normally, Shift+arrow steps finely, PageUp/PageDown jump by ten, Home/End go to the extremes.
- Proper
sliderandswitchroles with livearia-valuetext, so a screen reader announces the formatted value rather than the raw number. - The scroll wheel adjusts the value while the pointer is over the control; stepped ranges move at least one declared step per notch by default.