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 = 500points - 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 : 1 | Diagonal line | Ellipse (tilted) | Circle |
| 1 : 2 | Parabola (arc) | Figure-8 (tilted) | Figure-8 (upright) |
| 2 : 1 | Parabola (rotated) | Figure-8 (tilted) | Figure-8 (sideways) |
| 2 : 3 | Closed figure | Transitional | 3-lobe pretzel ← default |
| 3 : 4 | Closed figure | Transitional | 4-loop braid |
| 5 : 4 | Complex figure | Transitional | Intricate 5-lobe form |
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)
| Parameter | Type | Default | Description |
|---|---|---|---|
center | SWPoint | required | Center of the figure in user (grid) coordinates |
freqX | integer | 3 | Horizontal oscillation frequency (≥ 1) |
freqY | integer | 2 | Vertical oscillation frequency (≥ 1) |
ampX | number | 1 | Horizontal amplitude in parametric units |
ampY | number | 1 | Vertical amplitude in parametric units |
phaseDeg | number | 90 | Phase offset for x oscillation in CCW degrees |
fillColor | SWColor|undefined | undefined | Fill color; undefined = no fill |
strokeColor | SWColor|undefined | undefined | Stroke / outline color |
thickness | number | 2 | Stroke weight in pixels |
scale | number | 5 | Maps parametric coords to grid units |
rotationDeg | number | 0 | Static 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
| Property | Type | Notes |
|---|---|---|
center | SWPoint | Center in user (grid) coordinates; drag it live |
freqX | integer | Horizontal frequency; set via setFreqX() |
freqY | integer | Vertical frequency; set via setFreqY() |
ampX | number | Horizontal amplitude; set via setAmpX() |
ampY | number | Vertical amplitude; set via setAmpY() |
phaseDeg | number | Phase in degrees (unbounded during drift — may exceed 360) |
scale | number | Grid-unit multiplier; controls overall size |
thickness | number | Stroke weight in pixels |
rotationDeg | number | Static base rotation; does not clear on reset |
rotation | number | Accumulated spin rotation; cleared by reset() |
fillColor | SWColor|undefined | Fill color object; undefined = no fill pass |
strokeColor | SWColor|undefined | Stroke color object |
originalFreqX… | various | Read-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
| Method | Description |
|---|---|
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.
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 period | Effect |
|---|---|---|
| 1 | 360 seconds (6 min) | Imperceptibly slow; good for art installations |
| 10 | 36 seconds | Gentle, meditative morph |
| 36 | 10 seconds | Default — satisfying hypnotic rhythm |
| 90 | 4 seconds | Fast, energetic oscilloscope feel |
| 180 | 2 seconds | Rapid — 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) andendShape(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 tophaseDegwithout wrapping. SinceMath.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