✦ 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:
- Hypocycloid: generated by a point on a circle of radius a/4 rolling inside a circle of radius a.
- Trammel of Archimedes: the astroid is the envelope of line segments of constant length 2a whose endpoints slide along the coordinate axes. Any tangent line to the astroid is divided into a segment of total length 2a by the axes.
- Area: (3/8)πa² (exactly 3/8 of the circumscribed circle).
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:
- (x/rx) = sPow(cos(t), p) ⇒ |x/rx|1/p = |cos(t)|
- Squaring: |x/rx|2/p = cos²(t)
- Similarly: |y/ry|2/p = sin²(t)
- Adding: (|x|/rx)2/p + (|y|/ry)2/p = 1
This is the Lamé super-ellipse with exponent n = 2/p.
Special Cases
| p | n = 2/p | Shape (rx = ry = a) | Shape description |
|---|---|---|---|
| 1 | 2 | Circle | Standard ellipse when rx ≠ ry |
| 3 | 2/3 | Astroid | 4-cusp hypocycloid; the classic "spire" shape |
| 5 | 2/5 | Sharper star | Cusps tighter; arms more cross-like |
| 7 | 2/7 | Very pointed | Arms nearly touching; deep concavities |
| 9+ | < 2/9 | Approaching cross | Almost a 4-pointed cross / pinwheel at p = 15 |
| ∞ | 0 | 4-pointed cross | Theoretical 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."
Constructor
new SWSpire(center, radiusX, radiusY, power, fillColor, strokeColor, thickness, rotationDeg)
| Parameter | Type | Default | Description |
|---|---|---|---|
center | SWPoint | required | Center point in user (grid) coordinates |
radiusX | number | 3 | Half-width of bounding box (grid units) |
radiusY | number | 5 | Half-height of bounding box (grid units) |
power | number | 3 | Shape exponent; odd integer ≥ 1 recommended |
fillColor | SWColor | undefined | Fill color; undefined = no fill |
strokeColor | SWColor | undefined | Stroke color; undefined = no stroke |
thickness | number | 2 | Stroke weight in pixels |
rotationDeg | number | 0 | Static 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
| Property | Type | Description |
|---|---|---|
center | SWPoint | Center in user coordinates; drag to reposition |
radiusX | number | Current half-width of bounding box |
radiusY | number | Current half-height of bounding box |
power | number | Current shape exponent (may be fractional during Morph) |
fillColor | SWColor | Current fill color |
strokeColor | SWColor | Current stroke color |
thickness | number | Stroke weight in pixels |
rotationDeg | number | Static base rotation (CCW°); set via setRotation() |
rotation | number | Accumulated spin rotation (CCW°); incremented by rotate() |
originalRadiusX | number | Constructor radiusX; restored by reset() |
originalRadiusY | number | Constructor radiusY; restored by reset() |
originalPower | number | Constructor power; restored by reset() |
originalFillColor | SWColor | Constructor fill; restored by reset() |
originalStrokeColor | SWColor | Constructor stroke; restored by reset() |
originalThickness | number | Constructor thickness; restored by reset() |
originalRotationDeg | number | Constructor rotation; restored by reset() |
Methods
Drawing
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
rotate(deltaAngle) | Spins the spire by deltaAngle degrees (CCW+, CW−). Accumulates into this.rotation. Call once per frame: spire.rotate(speed * deltaT). |
Setters
| Method | Parameter | Description |
|---|---|---|
setRadiusX(rx) | number ≥ 0.01 | Sets the half-width |
setRadiusY(ry) | number ≥ 0.01 | Sets the half-height |
setRadii(rx, ry) | two numbers | Sets both radii at once |
setPower(p) | number ≥ 1 | Sets the shape exponent; fractional values produce intermediate Morph shapes |
setRotation(deg) | number | Sets the static base rotation (CCW degrees) |
setFillColor(fc) | SWColor or undefined | Sets fill color |
setStrokeColor(sc) | SWColor or undefined | Sets stroke color |
setStrokeWeight(w) | number | Sets stroke thickness in pixels |
setFillAlpha(alpha) | 0–100 | Sets fill opacity; rebuilds p5 color object |
setStrokeAlpha(alpha) | 0–100 | Sets stroke opacity; rebuilds p5 color object |
Reset and Utility
| Method | Description |
|---|---|
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
| Setting | Low value | High value |
|---|---|---|
| Speed (Hz) | 0.1 Hz — slow, dreamlike morph | 2.0 Hz — rapid, flickering morph |
| Amount | 0.5 — subtle ellipse flutter | 6.0 — sweeps ellipse ↔ sharp star |
| Base power | 1 (ellipse) — morphs toward astroid | 9+ — 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
- SAMPLE_COUNT = 500 vs. SWFigure8's 400: The spire has cusps at all four cardinal points. Near each cusp, the curve changes direction rapidly, so extra samples reduce visible jaggedness at high zoom or large radii.
- _sPow vs. Math.pow: For odd integer powers, both are equivalent.
_sPowis needed for the Morph animation where fractional powers are used —Math.pow(-0.7, 2.4)returns NaN in JavaScript, but_sPow(-0.7, 2.4)returns a valid negative number. - Fractional powers in Morph: Non-integer powers produce valid smooth intermediate Lamé curves. The resulting n = 2/p is simply irrational; the shape is still a well-defined smooth closed curve.
- rotationDeg vs. rotation:
rotationDegis the static base angle set by the user slider;rotationis the accumulated spin angle incremented each frame. The total effective angle is their sum.reset()clearsrotationbut keepsrotationDeg. - Fill before stroke: Both drawing passes use
beginShape()/endShape(CLOSE). Fill is drawn first (withnoStroke()) so the stroke outline always appears on top, which is critical for low-alpha fills. - Power slider step = 2: In the demo, the power slider uses step = 2 with min = 1, naturally producing only the odd integers {1, 3, 5, …, 15}. During Morph, the slider is frozen at the base power while the live display span shows the real-time fractional value.
- "Morph" vs. "Pulse": SWFigure8 uses "Pulse" (oscillating scale — a uniform size change). SWSpire uses "Morph" (oscillating p — a topological shape change). These are distinct phenomena: Pulse keeps the curve shape but varies size; Morph changes the fundamental shape of the curve.
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