〰️ SWLissajous Reference

A SketchWave parametric class for Lissajous figures — x(t) = ampX·sin(freqX·t + φ),  y(t) = ampY·sin(freqY·t)

Back to SWLissajous Demo 📐 About Parametric Equations

Quick Reference

SWLissajous is a SketchWave parametric class that draws a Lissajous figure — a closed curve defined by two perpendicular sinusoidal oscillations with frequencies freqX and freqY and a phase offset phaseDeg. The shape is sampled at SAMPLE_COUNT = 500 points and drawn as a filled and/or stroked closed polygon. The distinctive phase drift animation continuously morphs the figure through all of its possible configurations by incrementing the phase each frame.

  • Design Pattern: Parametric curve (not composition or inheritance)
  • Equations: x = ampX·sin(freqX·t + φ),   y = ampY·sin(freqY·t),   t ∈ [0, 2π)
  • Closed curve guarantee: For integer freqX and freqY, the curve closes exactly at t = 2π
  • Internal Structure: Cartesian coordinates sampled at SAMPLE_COUNT = 500 points
  • Dependencies: SWPoint, SWColor, SWGrid, p5.js
  • Key Features: Adjustable frequencies & amplitudes, phase control, phase drift animation, spin animation, fill & stroke with independent alpha, draggable center
  • Default figure: freqX:freqY = 3:2, phase = 90° → classic three-lobe pretzel

Overview

The SWLissajous class draws a Lissajous figure as a closed polygon. In user (grid) coordinates, with the parametric variable t ranging from 0 to 2π:

x(t) = ampX · sin(freqX · t + phaseDeg_rad)
y(t) = ampY · sin(freqY · t)

user_x = center.x + scale · x(t)
user_y = center.y + scale · y(t)

The shape of the figure depends on:

  • The ratio freqX:freqY — determines the number of lobes and overall topology
  • phaseDeg — rotates one oscillation relative to the other; at 0° many figures collapse to a line, at 90° they reach their most open symmetric form
  • ampX, ampY — independently scale the horizontal and vertical extent
Shape Families
freqX : freqY Phase = 0° Phase = 45° Phase = 90°
1 : 1Diagonal lineEllipse (tilted)Circle
1 : 2Parabola (arc)Figure-8 (tilted)Figure-8 (upright)
2 : 1Parabola (rotated)Figure-8 (tilted)Figure-8 (sideways)
2 : 3Closed figureTransitional3-lobe pretzel ← default
3 : 4Closed figureTransitional4-loop braid
5 : 4Complex figureTransitionalIntricate 5-lobe form
Default 'good Lissajous' settings: freqX = 3, freqY = 2, ampX = 1, ampY = 1, phaseDeg = 90°, scale = 5
At these values the figure spans ±5 grid units and fits neatly within a standard [−10, 10] × [−10, 10] SWGrid.

Constructor

new SWLissajous(center, freqX, freqY, ampX, ampY, phaseDeg, fillColor, strokeColor, thickness, scale, rotationDeg)
ParameterTypeDefaultDescription
centerSWPointrequiredCenter of the figure in user (grid) coordinates
freqXinteger3Horizontal oscillation frequency (≥ 1)
freqYinteger2Vertical oscillation frequency (≥ 1)
ampXnumber1Horizontal amplitude in parametric units
ampYnumber1Vertical amplitude in parametric units
phaseDegnumber90Phase offset for x oscillation in CCW degrees
fillColorSWColor|undefinedundefinedFill color; undefined = no fill
strokeColorSWColor|undefinedundefinedStroke / outline color
thicknessnumber2Stroke weight in pixels
scalenumber5Maps parametric coords to grid units
rotationDegnumber0Static base rotation in CCW degrees
// Minimal — stroke only, default 3:2 figure
const fig = new SWLissajous(
    new SWPoint(0, 0),
    3, 2,             // freqX, freqY
    1, 1,             // ampX, ampY
    90,               // phaseDeg
    undefined,        // no fill
    SWColor.fromHex('#00b4ff', 100, 'stroke'),
    2,                // thickness
    5                 // scale
);

Properties

PropertyTypeNotes
centerSWPointCenter in user (grid) coordinates; drag it live
freqXintegerHorizontal frequency; set via setFreqX()
freqYintegerVertical frequency; set via setFreqY()
ampXnumberHorizontal amplitude; set via setAmpX()
ampYnumberVertical amplitude; set via setAmpY()
phaseDegnumberPhase in degrees (unbounded during drift — may exceed 360)
scalenumberGrid-unit multiplier; controls overall size
thicknessnumberStroke weight in pixels
rotationDegnumberStatic base rotation; does not clear on reset
rotationnumberAccumulated spin rotation; cleared by reset()
fillColorSWColor|undefinedFill color object; undefined = no fill pass
strokeColorSWColor|undefinedStroke color object
originalFreqXvariousRead-only originals stored at construction; used by reset()

Methods

draw()

Draws the figure in raw screen (pixel) coordinates without a grid. Prefer drawOnGrid() for standard use.

drawOnGrid(grid)

Draws the figure through the given SWGrid's coordinate system. This is the standard method to call in a p5.js draw() loop. Handles the math-space ↔ screen y-flip automatically. Also draws the center dot if center.shouldShow is true.

rotate(deltaAngle)

Accumulates deltaAngle degrees into this.rotation. CCW positive, CW negative. Call once per frame: liss.rotate(spinSpeed * deltaT).

driftPhase(deltaDeg) Unique to SWLissajous

Advances phaseDeg by deltaDeg degrees. Call once per frame to morph the figure through all its possible configurations. See the Phase Drift Animation section below for details.

// In draw() — drift at 36°/second
liss.driftPhase(driftSpeed * deltaT);
Setters
MethodDescription
setFreqX(fx)Set horizontal frequency (clamped to integer ≥ 1)
setFreqY(fy)Set vertical frequency (clamped to integer ≥ 1)
setAmpX(ax)Set horizontal amplitude (min 0.01)
setAmpY(ay)Set vertical amplitude (min 0.01)
setPhase(deg)Set phase offset in degrees
setScale(s)Set scale (min 0.01)
setRotation(deg)Set static base rotation in CCW degrees
setFillColor(fc)Set fill SWColor (pass undefined to clear)
setStrokeColor(sc)Set stroke SWColor
setStrokeWeight(w)Set stroke thickness in pixels
setFillAlpha(a)Set fill alpha 0–100 and rebuild color
setStrokeAlpha(a)Set stroke alpha 0–100 and rebuild color
reset()

Restores all animated/slider-driven properties to the values passed to the constructor. Clears accumulated spin rotation (this.rotation = 0). Does not move the center position.

static copy(other)

Creates a deep copy of the given SWLissajous, preserving all current and original state including the center point.

const backup = SWLissajous.copy(liss);
toString()

Returns a human-readable string summary for debugging:

// Example output:
SWLissajous(center=SWPoint(0.0, 0.0), freqX=3, freqY=2, ampX=1.00, ampY=1.00,
            phaseDeg=90.0, scale=5.00, rotationDeg=0.0, rotation=0.0)

Phase Drift Animation

The phase drift animation is the most distinctive feature of a Lissajous figure. By continuously incrementing the phase offset, the figure smoothly morphs through all of its possible configurations for the current frequency ratio.

Physical analogy: On an oscilloscope, if the two input frequencies are very slightly mismatched, the Lissajous figure slowly rotates and morphs. This is caused by the continuously changing phase relationship between the two signals — exactly what driftPhase() simulates.
How it works
// In the p5.js draw() loop:
const t      = millis() / 1000;
const deltaT = (prevT > 0) ? (t - prevT) : 0;
prevT = t;

if (shouldDrift) {
    liss.driftPhase(driftSpeed * deltaT);  // advance phase
    // optionally mirror back to the slider:
    const displayPhase = ((liss.phaseDeg % 360) + 360) % 360;
    phaseDegSlider.value = String(Math.round(displayPhase));
}

liss.drawOnGrid(grid);
Effect of drift speed
driftSpeed (°/sec)Full cycle periodEffect
1360 seconds (6 min)Imperceptibly slow; good for art installations
1036 secondsGentle, meditative morph
3610 secondsDefault — satisfying hypnotic rhythm
904 secondsFast, energetic oscilloscope feel
1802 secondsRapid — almost flickering
Trail effect with Phase Drift

Setting the background opacity to a low value (10–30) combined with phase drift creates a glowing motion-trail effect. Each frame, the old figure fades slightly instead of being fully erased, leaving luminous trails:

// In draw():
background(bgColor);   // bgColor alpha ≈ 10–30 (from SWColor with low alpha)
if (shouldDrift) liss.driftPhase(36 * deltaT);
liss.drawOnGrid(grid);

Code Examples

1. Basic Lissajous on a Grid (3:2 pretzel)
let grid, liss;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();
    grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
    const stroke = SWColor.fromHex('#00b4ff', 100, 'stroke');
    const fill   = SWColor.fromHex('#6c00ff',  15, 'fill');
    liss = new SWLissajous(new SWPoint(0, 0), 3, 2, 1, 1, 90, fill, stroke, 2, 5);
}

function draw() {
    background(color(0, 0, 93, 100));
    grid.draw();
    liss.drawOnGrid(grid);
}
2. Phase Drift Animation (morphing figure)
let grid, liss, prevT = 0;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();
    grid  = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
    liss  = new SWLissajous(new SWPoint(0, 0), 3, 2, 1, 1, 90,
                            undefined,
                            SWColor.fromHex('#00ffcc', 100, 'stroke'),
                            2, 5);
}

function draw() {
    const t = millis() / 1000;
    const deltaT = (prevT > 0) ? t - prevT : 0;
    prevT = t;
    background(color(0, 0, 93, 100));
    grid.draw();
    liss.driftPhase(36 * deltaT);   // 36°/second = one full cycle in 10 seconds
    liss.drawOnGrid(grid);
}
3. Drift + Low Background Opacity (trail effect)
let bgColor, grid, liss, prevT = 0;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();
    const bg = new SWColor(0, 0, 5, 20, 'bgTrail');   // alpha = 20 for trails
    bgColor = bg.col;
    grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
    liss = new SWLissajous(new SWPoint(0, 0), 5, 3, 1, 1, 90,
                           undefined,
                           SWColor.fromHex('#ff6600', 100, 'stroke'),
                           3, 4);
}

function draw() {
    const t = millis() / 1000;
    const deltaT = prevT > 0 ? t - prevT : 0;
    prevT = t;
    background(bgColor);           // semi-transparent bg creates trails
    liss.driftPhase(45 * deltaT);  // faster drift
    liss.drawOnGrid(grid);
}
4. Multiple Lissajous Figures
let grid, figures;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();
    grid    = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
    const center = new SWPoint(0, 0);
    figures = [
        new SWLissajous(center, 1, 2, 1, 1, 90, undefined, SWColor.fromHex('#ff2244', 100, 's1'), 2, 3),
        new SWLissajous(center, 2, 3, 1, 1, 90, undefined, SWColor.fromHex('#22aaff', 100, 's2'), 2, 5),
        new SWLissajous(center, 3, 4, 1, 1, 90, undefined, SWColor.fromHex('#44ff88', 100, 's3'), 2, 7),
    ];
}

function draw() {
    background(color(0, 0, 93, 100));
    grid.draw();
    for (const fig of figures) fig.drawOnGrid(grid);
}

Design Notes

  • SAMPLE_COUNT = 500 (vs. SWHeart's 200) — Lissajous figures with high frequency ratios (e.g., 9:10) require more points to render smoothly. With 500 samples and a maximum frequency of 10, each oscillation cycle is covered by ≈50 points, which is visually smooth.
  • Integer frequencies → closed curves — For any integers freqX and freqY, both x(t) and y(t) return to their starting values at t = 2π, guaranteeing a closed loop. Using i < n (exclusive end) and endShape(CLOSE) exploits this cleanly.
  • phaseDeg, not phaseRad — The public API uses degrees throughout (consistent with all other SketchWave classes). The radian conversion is done internally in _buildUserPts().
  • phaseDeg is unbounded during drift — driftPhase() simply adds to phaseDeg without wrapping. Since Math.sin() is naturally periodic, this is correct and avoids discontinuities. The display wraps to [0, 360) only for the slider readout.
  • Fill pass before stroke pass — Mirrors the SWHeart pattern. Rendering fill first, then stroke on top, prevents the stroke from being half-covered by the fill, giving a clean outline at any opacity.
  • driftPhase() vs. rotate() — These are independent animations. Spin rotates the entire figure in space; phase drift morphs its internal shape. Combining both creates a rich, constantly-changing oscilloscope display.
  • center SWPoint — The center point supports drag interaction, center dot display (shouldShow), and the snap-to-grid feature. The Lissajous can be repositioned on the canvas by moving the center.

Source Code

▼ Show / Hide swLissajous.js Source
/*
File: swLissajous.js
Date: 2026-04-27
Author: klp
App:  SketchWaveTNT2026-04-21-Stg8
Purpose: SWLissajous class for SketchWaveJS

SWLissajous draws a Lissajous figure defined by the parametric equations:

  x(t) = ampX · sin(freqX · t + phaseDeg_rad)
  y(t) = ampY · sin(freqY · t)

for t ∈ [0, 2π], scaled by 'scale' and offset by 'center':

  user_x = center.x + scale · ampX · sin(freqX · t + phaseDeg_rad)
  user_y = center.y + scale · ampY · sin(freqY · t)

Parameters:
  center   (SWPoint) -- origin in user (grid) coordinates
  freqX    (integer) -- horizontal frequency (1-10 typical)
  freqY    (integer) -- vertical frequency   (1-10 typical)
  ampX     (number)  -- horizontal amplitude (1 = unit scale)
  ampY     (number)  -- vertical amplitude   (1 = unit scale)
  phaseDeg (number)  -- phase offset for the x oscillation, in CCW degrees (0-360)
  scale    (number)  -- maps parametric units to grid units

Shape families at phaseDeg = 90 deg:
  freqX:freqY = 1:1  -- circle
  freqX:freqY = 1:2  -- figure-8 / parabola
  freqX:freqY = 2:3  -- classic three-lobe pretzel  (default)
  freqX:freqY = 3:4  -- complex four-loop figure
  Any integer ratio  -- closed curve when t in [0, 2pi]

Phase drift animation:
  Continuously incrementing phaseDeg (via driftPhase()) causes the figure
  to morph smoothly through all of its possible configurations for the
  current frequency ratio.

Rotation:
  rotationDeg -- static base rotation (CCW degrees), set by setRotation().
  rotation    -- accumulated rotation (degrees), incremented by rotate().
  Effective rotation = rotationDeg + rotation.

Dependencies: p5.js, SWColor, SWPoint, SWGrid.
*/

console.log("[swLissajous.js] SWLissajous class loaded.");

class SWLissajous {

    static SAMPLE_COUNT = 500;

    constructor(center, freqX = 3, freqY = 2, ampX = 1, ampY = 1, phaseDeg = 90,
                fillColor = undefined, strokeColor = undefined,
                thickness = 2, scale = 5, rotationDeg = 0) {

        this.center      = center;
        this.freqX       = freqX;
        this.freqY       = freqY;
        this.ampX        = ampX;
        this.ampY        = ampY;
        this.phaseDeg    = phaseDeg;
        this.fillColor   = fillColor   ? SWColor.copy(fillColor)   : undefined;
        this.strokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
        this.thickness   = thickness;
        this.scale       = scale;
        this.rotationDeg = rotationDeg;
        this.rotation    = 0;

        this.originalFreqX       = freqX;
        this.originalFreqY       = freqY;
        this.originalAmpX        = ampX;
        this.originalAmpY        = ampY;
        this.originalPhaseDeg    = phaseDeg;
        this.originalFillColor   = fillColor   ? SWColor.copy(fillColor)   : undefined;
        this.originalStrokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
        this.originalThickness   = thickness;
        this.originalScale       = scale;
        this.originalRotationDeg = rotationDeg;

    }//end constructor

    _totalRotDeg() { return this.rotationDeg + this.rotation; }

    _rotateLocal(lx, ly) {
        const rad  = this._totalRotDeg() * Math.PI / 180;
        const cosR = Math.cos(rad);
        const sinR = Math.sin(rad);
        return { x: lx * cosR - ly * sinR, y: lx * sinR + ly * cosR };
    }

    _buildUserPts() {
        const cx  = this.center.x;
        const cy  = this.center.y;
        const n   = SWLissajous.SAMPLE_COUNT;
        const phR = this.phaseDeg * Math.PI / 180;
        const pts = [];
        for (let i = 0; i < n; i++) {
            const t   = (i / n) * 2 * Math.PI;
            const lx  = this.scale * this.ampX * Math.sin(this.freqX * t + phR);
            const ly  = this.scale * this.ampY * Math.sin(this.freqY * t);
            const rot = this._rotateLocal(lx, ly);
            pts.push({ x: cx + rot.x, y: cy + rot.y });
        }
        return pts;
    }

    _buildScreenPtsGrid(grid) {
        return this._buildUserPts().map(p => grid.userToScreen(p.x, p.y));
    }

    _buildScreenPtsDirect() {
        const cx  = this.center.x;
        const cy  = this.center.y;
        const n   = SWLissajous.SAMPLE_COUNT;
        const phR = this.phaseDeg * Math.PI / 180;
        const pts = [];
        for (let i = 0; i < n; i++) {
            const t   = (i / n) * 2 * Math.PI;
            const lx  = this.scale * this.ampX * Math.sin(this.freqX * t + phR);
            const ly  = this.scale * this.ampY * Math.sin(this.freqY * t);
            const rot = this._rotateLocal(lx, ly);
            pts.push({ x: cx + rot.x, y: cy - rot.y });
        }
        return pts;
    }

    _drawShape(screenPts) {
        if (screenPts.length < 2) return;
        if (this.fillColor && this.fillColor.col) {
            fill(this.fillColor.col);
            noStroke();
            beginShape();
            for (const sp of screenPts) vertex(sp.x, sp.y);
            endShape(CLOSE);
        }
        if (this.strokeColor && this.strokeColor.col) {
            noFill();
            stroke(this.strokeColor.col);
            strokeWeight(this.thickness);
            beginShape();
            for (const sp of screenPts) vertex(sp.x, sp.y);
            endShape(CLOSE);
        }
        noStroke(); noFill(); strokeWeight(1);
    }

    draw() {
        const screenPts = this._buildScreenPtsDirect();
        this._drawShape(screenPts);
        if (this.center && this.center.draw) this.center.draw(this.strokeColor);
    }

    drawOnGrid(grid) {
        const screenPts = this._buildScreenPtsGrid(grid);
        this._drawShape(screenPts);
        if (this.center && this.center.drawOnGrid) this.center.drawOnGrid(grid, this.strokeColor);
    }

    rotate(deltaAngle)   { this.rotation += deltaAngle; }
    driftPhase(deltaDeg) { this.phaseDeg += deltaDeg; }

    setFreqX(fx) { this.freqX = Math.max(1, Math.round(fx)); }
    setFreqY(fy) { this.freqY = Math.max(1, Math.round(fy)); }
    setAmpX(ax)  { this.ampX  = Math.max(0.01, ax); }
    setAmpY(ay)  { this.ampY  = Math.max(0.01, ay); }
    setPhase(deg)      { this.phaseDeg    = deg; }
    setScale(s)        { this.scale       = Math.max(0.01, s); }
    setRotation(deg)   { this.rotationDeg = deg; }
    setFillColor(fc)   { this.fillColor   = fc ? SWColor.copy(fc) : undefined; }
    setStrokeColor(sc) { this.strokeColor = sc ? SWColor.copy(sc) : undefined; }
    setStrokeWeight(w) { this.thickness   = w; }

    setFillAlpha(alpha) {
        if (this.fillColor) {
            this.fillColor.a   = Math.max(0, Math.min(100, alpha));
            this.fillColor.col = color(this.fillColor.h, this.fillColor.s,
                                       this.fillColor.b, this.fillColor.a);
        }
    }

    setStrokeAlpha(alpha) {
        if (this.strokeColor) {
            this.strokeColor.a   = Math.max(0, Math.min(100, alpha));
            this.strokeColor.col = color(this.strokeColor.h, this.strokeColor.s,
                                         this.strokeColor.b, this.strokeColor.a);
        }
    }

    reset() {
        this.freqX       = this.originalFreqX;
        this.freqY       = this.originalFreqY;
        this.ampX        = this.originalAmpX;
        this.ampY        = this.originalAmpY;
        this.phaseDeg    = this.originalPhaseDeg;
        this.scale       = this.originalScale;
        this.rotationDeg = this.originalRotationDeg;
        this.rotation    = 0;
        this.thickness   = this.originalThickness;
        this.fillColor   = this.originalFillColor
            ? SWColor.copy(this.originalFillColor) : undefined;
        this.strokeColor = this.originalStrokeColor
            ? SWColor.copy(this.originalStrokeColor) : undefined;
    }

    static copy(other) {
        const c = new SWLissajous(
            SWPoint.copy(other.center),
            other.originalFreqX, other.originalFreqY,
            other.originalAmpX,  other.originalAmpY,
            other.originalPhaseDeg,
            other.originalFillColor, other.originalStrokeColor,
            other.originalThickness, other.originalScale,
            other.originalRotationDeg
        );
        c.freqX = other.freqX;   c.freqY = other.freqY;
        c.ampX  = other.ampX;    c.ampY  = other.ampY;
        c.phaseDeg    = other.phaseDeg;
        c.scale       = other.scale;
        c.rotationDeg = other.rotationDeg;
        c.rotation    = other.rotation;
        return c;
    }

    toString() {
        return 'SWLissajous(center=' + this.center +
               ', freqX=' + this.freqX + ', freqY=' + this.freqY +
               ', ampX='  + this.ampX.toFixed(2) +
               ', ampY='  + this.ampY.toFixed(2) +
               ', phaseDeg=' + this.phaseDeg.toFixed(1) +
               ', scale=' + this.scale.toFixed(2) +
               ', rotationDeg=' + this.rotationDeg.toFixed(1) +
               ', rotation=' + this.rotation.toFixed(1) + ')';
    }

}//end SWLissajous class