✦ SWSpire Reference

Lamé Curve Spire • Parametric Equations • Morph Animation

⚡ Quick Reference

File
shapeClasses/swSpire.js
SAMPLE_COUNT
500 — sample points around t ∈ [0, 2π)
Parametric form
x(t) = rx · sPow(cos(t), p)
y(t) = ry · sPow(sin(t), p)
Cartesian form
(|x|/rx)^(2/p) + (|y|/ry)^(2/p) = 1
Lamé curve with exponent n = 2/p
Signed power
sPow(b, p) = sign(b) · |b|^p
Handles negative base; required for fractional-p Morph animation
Default settings
radiusX=3, radiusY=5, power=3 — tall spire on a [−10,10] grid
Dependencies
p5.js, SWColor, SWPoint, SWGrid

Historical Context

Gabriel Lamé and the Super-Ellipse

French mathematician and engineer Gabriel Lamé (1795–1870) introduced the family of curves now bearing his name while studying the theory of elasticity and the geometry of curved surfaces. A Lamé curve (also called a super-ellipse) satisfies:

(|x| / a)n + (|y| / b)n = 1

For n = 2 this is the standard ellipse; for n ≠ 2 the shape deforms in interesting ways. Lamé was also responsible for proving Fermat's Last Theorem for the exponent n = 7, and contributed to the theory of heat diffusion, differential geometry, and curvilinear coordinates.

Piet Hein and the Squircle

Danish designer and inventor Piet Hein (1905–1996) popularized the super-ellipse with exponent n ≈ 2.5 as a design shape in the 1960s. He called shapes with n = 4 squircles (square + circle), prized for their visual balance between a rectangle and an ellipse. His super-elliptical designs appear in furniture, dishware, and urban planning — notably Sergels Torg (Stockholm's central plaza, 1967), whose central fountain and traffic layout use the super-ellipse as the organizing geometry. The squircle is now ubiquitous in digital UI design (iOS app icons use n ≈ 4).

The Astroid: p = 3

The most celebrated Lamé curve in the concave family (n < 2) is the astroid, corresponding to p = 3, n = 2/3, and rx = ry = a:

x(t) = a cos³(t),   y(t) = a sin³(t)

The name astroid (from Greek aster = star) was popularized by the Viennese astronomer Johann Georg von Littrow around 1838. The astroid has remarkable geometric properties:

Why "SWSpire"?

A spire (also called a pinnacle or finial) is the tall, slender, pointed ornament crowning Gothic and Neo-Gothic cathedrals. When the SWSpire class is drawn with its default settings (rx = 3, ry = 5, p = 3), the shape is taller than it is wide, with four sharp cusps — the two vertical cusps at (0, ±5) forming the "pinnacle" top and bottom, and the two lateral cusps at (±3, 0) forming narrower flanks. The deeply concave sides sweep dramatically inward from each cusp, producing a cross-section that closely mimics the plan view of a Gothic cathedral pinnacle.

The Math

Parametric Equations
x(t) = rx · sPow(cos(t), p)
y(t) = ry · sPow(sin(t), p)

where sPow(b, p) = sign(b) · |b|p
and t ∈ [0, 2π)
Signed Power Definition

The standard Math.pow(base, exp) in JavaScript returns NaN for a negative base with a non-integer exponent. For the Morph animation, the power oscillates through fractional values, so a robust version is needed:

static _sPow(base, exp) {
    if (base === 0) return 0;
    return Math.sign(base) * Math.pow(Math.abs(base), exp);
}

For odd integer exponents, sPow(b, p) === Math.pow(b, p). The signed-power definition also ensures the curve occupies all four quadrants for even-integer p, keeping the shape a closed curve rather than a single-quadrant arc.

Deriving the Cartesian Form

Starting from the parametric form and using the identity cos²(t) + sin²(t) = 1:

  1. (x/rx) = sPow(cos(t), p)  ⇒  |x/rx|1/p = |cos(t)|
  2. Squaring: |x/rx|2/p = cos²(t)
  3. Similarly: |y/ry|2/p = sin²(t)
  4. Adding: (|x|/rx)2/p + (|y|/ry)2/p = 1

This is the Lamé super-ellipse with exponent n = 2/p.

Special Cases
pn = 2/pShape (rx = ry = a)Shape description
12CircleStandard ellipse when rx ≠ ry
32/3Astroid4-cusp hypocycloid; the classic "spire" shape
52/5Sharper starCusps tighter; arms more cross-like
72/7Very pointedArms nearly touching; deep concavities
9+< 2/9Approaching crossAlmost a 4-pointed cross / pinwheel at p = 15
04-pointed crossTheoretical limit
Cusps at the Cardinal Tips

For any p > 1, both parametric derivatives vanish at t = 0, π/2, π, 3π/2:

dx/dt = -rx · p · sPow(cos(t), p-1) · sin(t)
dy/dt =  ry · p · sPow(sin(t), p-1) · cos(t)

At t = 0: sin(0) = 0, so dx/dt = 0; cos(0) = 1, sPow(sin(0), p-1) = 0, so dy/dt = 0.
Both derivatives zero → cusp at (rx, 0).

These cusps are the characteristic pointed tips of the SWSpire shape, giving each arm its sharp "spire point."

Default "good" settings: radiusX = 3, radiusY = 5, power = 3 → the shape spans ±3 in x and ±5 in y on a [−10, 10] SWGrid, fitting comfortably with visible margins.

Constructor

new SWSpire(center, radiusX, radiusY, power, fillColor, strokeColor, thickness, rotationDeg)
ParameterTypeDefaultDescription
centerSWPointrequiredCenter point in user (grid) coordinates
radiusXnumber3Half-width of bounding box (grid units)
radiusYnumber5Half-height of bounding box (grid units)
powernumber3Shape exponent; odd integer ≥ 1 recommended
fillColorSWColorundefinedFill color; undefined = no fill
strokeColorSWColorundefinedStroke color; undefined = no stroke
thicknessnumber2Stroke weight in pixels
rotationDegnumber0Static base rotation, CCW degrees
Minimal Usage
// Minimal: center only (uses all defaults)
const center = new SWPoint(0, 0);
const spire  = new SWSpire(center);
spire.drawOnGrid(grid);
Full Construction
const strokeCol = SWColor.fromHex('#e8a820', 100, 'stroke');
const fillCol   = SWColor.fromHex('#7a4e00', 25,  'fill');
const center    = new SWPoint(0, 0);

const spire = new SWSpire(
    center,
    3,          // radiusX
    5,          // radiusY
    3,          // power  (astroid)
    fillCol, strokeCol,
    2,          // thickness
    0           // rotationDeg
);

Properties

PropertyTypeDescription
centerSWPointCenter in user coordinates; drag to reposition
radiusXnumberCurrent half-width of bounding box
radiusYnumberCurrent half-height of bounding box
powernumberCurrent shape exponent (may be fractional during Morph)
fillColorSWColorCurrent fill color
strokeColorSWColorCurrent stroke color
thicknessnumberStroke weight in pixels
rotationDegnumberStatic base rotation (CCW°); set via setRotation()
rotationnumberAccumulated spin rotation (CCW°); incremented by rotate()
originalRadiusXnumberConstructor radiusX; restored by reset()
originalRadiusYnumberConstructor radiusY; restored by reset()
originalPowernumberConstructor power; restored by reset()
originalFillColorSWColorConstructor fill; restored by reset()
originalStrokeColorSWColorConstructor stroke; restored by reset()
originalThicknessnumberConstructor thickness; restored by reset()
originalRotationDegnumberConstructor rotation; restored by reset()

Methods

Drawing
MethodDescription
draw()Draws in raw screen (pixel) coordinates. Prefer drawOnGrid().
drawOnGrid(grid)Draws the spire through the SWGrid coordinate mapping. Standard method for p5 draw loops.
Animation
MethodDescription
rotate(deltaAngle)Spins the spire by deltaAngle degrees (CCW+, CW−). Accumulates into this.rotation. Call once per frame: spire.rotate(speed * deltaT).
Setters
MethodParameterDescription
setRadiusX(rx)number ≥ 0.01Sets the half-width
setRadiusY(ry)number ≥ 0.01Sets the half-height
setRadii(rx, ry)two numbersSets both radii at once
setPower(p)number ≥ 1Sets the shape exponent; fractional values produce intermediate Morph shapes
setRotation(deg)numberSets the static base rotation (CCW degrees)
setFillColor(fc)SWColor or undefinedSets fill color
setStrokeColor(sc)SWColor or undefinedSets stroke color
setStrokeWeight(w)numberSets stroke thickness in pixels
setFillAlpha(alpha)0–100Sets fill opacity; rebuilds p5 color object
setStrokeAlpha(alpha)0–100Sets stroke opacity; rebuilds p5 color object
Reset and Utility
MethodDescription
reset()Restores all constructor values; clears rotation. Does not move the center.
static copy(other)Deep copy of an SWSpire preserving all current and original state. Returns a new SWSpire.
toString()Returns a human-readable string with all key property values.

Morph Animation

SWSpire's signature animation is Morph, which oscillates the power parameter p sinusoidally around a base value. Because setPower() accepts fractional values, the shape transitions continuously through a family of Lamé curves — from a soft ellipse at low p, through the classic astroid at p = 3, to sharper spires at p = 5, 7, …

// In the draw() loop — simplified Morph logic:
const t          = millis() / 1000;
const rawPower   = morphBasePower + morphAmount * Math.sin(2 * Math.PI * morphSpeed * t);
const clamped    = Math.max(1.0, rawPower);  // never below 1 (floor at ellipse)
spire.setPower(clamped);
powerValueSpan.textContent = clamped.toFixed(1);  // live display (not the slider)
Effect of Morph Settings
SettingLow valueHigh value
Speed (Hz)0.1 Hz — slow, dreamlike morph2.0 Hz — rapid, flickering morph
Amount0.5 — subtle ellipse flutter6.0 — sweeps ellipse ↔ sharp star
Base power1 (ellipse) — morphs toward astroid9+ — morphs between extreme star forms
Morph + Trail Effect

Set Background opacity to a low value (e.g., 8–15) and enable Morph: each frame draws a ghosted copy of the current shape, building up an organic "afterimage" trail. Add Spin for layered spiral-trail patterns.

Code Examples

Example 1: Basic SWSpire on a Grid
let grid, spire;

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('#e8a820', 100, 'stroke');
    const fill   = SWColor.fromHex('#7a4e00', 25,  'fill');
    spire = new SWSpire(new SWPoint(0, 0), 3, 5, 3, fill, stroke, 2);
}

function draw() {
    background(0, 0, 93);
    grid.draw();
    spire.drawOnGrid(grid);
    grid.updateScreenBounds();
}
Example 2: Continuous Spin
let prevT = 0;
const SPIN_SPEED = 45;  // degrees per second

function draw() {
    const t      = millis() / 1000;
    const deltaT = (prevT > 0) ? (t - prevT) : 0;
    prevT = t;

    background(0, 0, 93);
    grid.draw();

    spire.rotate(SPIN_SPEED * deltaT);  // deltaAngle in degrees
    spire.drawOnGrid(grid);
    grid.updateScreenBounds();
}
Example 3: Morph Animation with Trail
let morphBasePower = 3;
const MORPH_SPEED  = 0.5;   // Hz
const MORPH_AMOUNT = 2.0;   // power units

function draw() {
    const t = millis() / 1000;

    // Trail effect: semi-transparent background
    background(0, 0, 93, 12);  // alpha = 12/100
    grid.draw();

    // Morph: oscillate power sinusoidally
    const rawPower = morphBasePower + MORPH_AMOUNT * Math.sin(2 * Math.PI * MORPH_SPEED * t);
    spire.setPower(Math.max(1.0, rawPower));
    spire.drawOnGrid(grid);
    grid.updateScreenBounds();
}
Example 4: Multiple Spires at Different Powers
let spires = [];

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 powers = [1, 3, 5, 7];
    const hues   = [200, 42, 300, 120];
    powers.forEach((p, i) => {
        const sc = new SWColor(hues[i], 85, 90, 100, 's');
        const fc = new SWColor(hues[i], 80, 50, 20, 'f');
        spires.push(new SWSpire(new SWPoint(0, 0), 4, 4, p, fc, sc, 1.5));
    });
}

function draw() {
    background(0, 0, 93);
    grid.draw();
    spires.forEach(s => s.drawOnGrid(grid));
    grid.updateScreenBounds();
}

Design Notes

Source Code

Show / Hide swSpire.js source
/*
File: swSpire.js
Date: 2026-04-27
Author: klp
App:  SketchWaveTNT2026-04-21-Stg8
Purpose: SWSpire class for SketchWaveJS

SWSpire draws a "spire" curve defined by the parametric equations:

  x(t) = rx · sPow(cos(t), p)
  y(t) = ry · sPow(sin(t), p)

for t ∈ [0, 2π), where:
  rx     = radiusX  (half-width of bounding box, grid units)
  ry     = radiusY  (half-height of bounding box, grid units)
  p      = power    (odd positive integer recommended; fractional values OK)
  sPow   = signed power: sign(b) · |b|^p  (preserves sign for any exponent)

The shape is a generalized Lamé curve (super-ellipse) with exponent n = 2/p:

  (|x| / rx)^(2/p)  +  (|y| / ry)^(2/p)  =  1

Special cases (rx = ry = a):
  p = 1  →  circle  (ellipse when rx ≠ ry)
  p = 3  →  classical astroid  (4-cusped hypocycloid)
  p = 5  →  sharper 4-pointed star
  p = 7  →  even sharper star; approaching a 4-pointed cross
  ...  as p→∞: 4-pointed cross / pinwheel

The name "SWSpire" reflects the tall pointed shape produced when ry > rx and p ≥ 3,
resembling the profile of a Gothic cathedral pinnacle or finial.

At the four cardinal points (t = 0, π/2, π, 3π/2), the curve has cusps (both
parametric derivatives are zero), giving the characteristic sharp tips.

Morph animation: setPower() accepts non-integer values, allowing smooth continuous
morphing between ellipse (p=1), astroid (p=3), and sharper star forms (p=5,7...).

Rotation:
  rotationDeg -- static base rotation (CCW degrees); set by setRotation().
                 Persists across reset().
  rotation    -- accumulated rotation (degrees); incremented by rotate().
                 Starts at 0; reset() returns it to 0.
  Effective rotation = rotationDeg + rotation.

Angle convention (same as all SketchWaveJS classes):
  User space:  CCW positive, y increases upward (standard math/Cartesian).
  p5 screen:   CW  positive, y increases downward.
  SWSpire handles the y-flip internally; always pass CCW degrees.

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

console.log("[swSpire.js] SWSpire class loaded.");

class SWSpire {

    static SAMPLE_COUNT = 500;

    static _sPow(base, exp) {
        if (base === 0) return 0;
        return Math.sign(base) * Math.pow(Math.abs(base), exp);
    }

    constructor(center, radiusX = 3, radiusY = 5, power = 3,
                fillColor = undefined, strokeColor = undefined,
                thickness = 2, rotationDeg = 0) {

        this.center      = center;
        this.radiusX     = radiusX;
        this.radiusY     = radiusY;
        this.power       = power;
        this.fillColor   = fillColor   ? SWColor.copy(fillColor)   : undefined;
        this.strokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
        this.thickness   = thickness;
        this.rotationDeg = rotationDeg;
        this.rotation    = 0;

        this.originalRadiusX     = radiusX;
        this.originalRadiusY     = radiusY;
        this.originalPower       = power;
        this.originalFillColor   = fillColor   ? SWColor.copy(fillColor)   : undefined;
        this.originalStrokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
        this.originalThickness   = thickness;
        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   = SWSpire.SAMPLE_COUNT;
        const p   = this.power;
        const rx  = this.radiusX;
        const ry  = this.radiusY;
        const pts = [];

        for (let i = 0; i < n; i++) {
            const t   = (i / n) * 2 * Math.PI;
            const lx  = SWSpire._sPow(Math.cos(t), p) * rx;
            const ly  = SWSpire._sPow(Math.sin(t), p) * ry;
            const rot = this._rotateLocal(lx, ly);
            pts.push({ x: cx + rot.x, y: cy + rot.y });
        }
        return pts;
    }

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

    _buildScreenPtsDirect() {
        const cx  = this.center.x;
        const cy  = this.center.y;
        const n   = SWSpire.SAMPLE_COUNT;
        const p   = this.power;
        const rx  = this.radiusX;
        const ry  = this.radiusY;
        const pts = [];

        for (let i = 0; i < n; i++) {
            const t   = (i / n) * 2 * Math.PI;
            const lx  = SWSpire._sPow(Math.cos(t), p) * rx;
            const ly  = SWSpire._sPow(Math.sin(t), p) * ry;
            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; }

    setRadiusX(rx)     { this.radiusX     = Math.max(0.01, rx); }
    setRadiusY(ry)     { this.radiusY     = Math.max(0.01, ry); }
    setRadii(rx, ry)   { this.setRadiusX(rx); this.setRadiusY(ry); }
    setPower(p)        { this.power       = Math.max(1, p); }
    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.radiusX     = this.originalRadiusX;
        this.radiusY     = this.originalRadiusY;
        this.power       = this.originalPower;
        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 SWSpire(
            SWPoint.copy(other.center),
            other.originalRadiusX,
            other.originalRadiusY,
            other.originalPower,
            other.originalFillColor,
            other.originalStrokeColor,
            other.originalThickness,
            other.originalRotationDeg
        );
        c.radiusX     = other.radiusX;
        c.radiusY     = other.radiusY;
        c.power       = other.power;
        c.rotationDeg = other.rotationDeg;
        c.rotation    = other.rotation;
        return c;
    }

    toString() {
        return 'SWSpire(center=' + this.center +
               ', radiusX='     + this.radiusX.toFixed(2) +
               ', radiusY='     + this.radiusY.toFixed(2) +
               ', power='       + this.power.toFixed(1) +
               ', rotationDeg=' + this.rotationDeg.toFixed(1) +
               ', rotation='    + this.rotation.toFixed(1) + ')';
    }

}//end SWSpire class
↑ Top