Lissajous Scope

lissajous-scopeVisualizers

Where 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-scope

Pulls in

use-audio-sourcecontrol-theme

First time? Add the registry to your components.json once:

components.json
{
  "registries": {
    "@syntho-ui": "https://engine.syntho.app/r/{name}.json"
  }
}

Usage

Controlled or uncontrolled, whichever suits the surrounding state.

example.tsx
import { LissajousScope } from "@/components/ui/lissajous-scope"

<LissajousScope analyser={analyserNode} height={260} />

Props

PropTypeDefaultDescription
analyserAnalyserNodeA live analyser to read time-domain data from.
getData(target: Float32Array) => boolean | voidSupply samples yourself instead of using an analyser.
phaseShiftnumber248 / 360Offset between the X and Y reads as a fraction of the buffer, not degrees. This is what sets the figure's shape.
trailLengthnumber450How many points to plot. Clamped to half the buffer, and sampled in strides so the trail always spans the whole frame.
fadeSpeednumber0.1Alpha of the per-frame wash over the previous frame. Lower means longer persistence; 0 lets the figure accumulate.
zoomLevelnumber0.6Size multiplier. 1 makes a full-scale signal span the shorter axis exactly, below 1 insets the figure, above 1 lets it overshoot.
lineWidthnumber1Trace thickness in CSS pixels at the head of the trail.
enableGlowbooleanfalseAdditive multi-pass bloom around the trace. Costs a pass per level.
glowIntensitynumber3Number of bloom passes, 1–5. Only read when enableGlow is on.
showGridbooleantrueDraw the centre crosshair.
colorstringTrace colour. Defaults to the --uikit-accent token.
heightnumber220Display height in pixels; width defaults to 100%.
pausedbooleanfalseStop 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 slider and switch roles with live aria-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.