🌀 SWSpiral Reference

A SketchWave parametric class for representing an Archimedean spiral defined by r = k·θ / π

Back to SWSpiral Demo

Quick Reference

SWSpiral is a SketchWave parametric class that represents an Archimedean spiral defined by the polar equation r = k·θ/π, where k controls the spacing between spiral arms and revolutions sets how many full turns the spiral traces. The drawn shape is a closed polygon — the spiral arc sampled at SAMPLE_COUNT × revolutions points, connected back to the center with a straight closing line. SWSpiral is not a composite class; all drawing, animation, and styling is handled directly.

  • Design Pattern: Parametric curve (not composition or inheritance)
  • Equation: r = k·θ/π — Archimedean spiral with linear arm spacing
  • Internal Structure: Polar coordinates sampled at SAMPLE_COUNT = 120 points per revolution, closed back to center
  • Dependencies: SWPoint, SWColor, SWGrid, p5.js
  • Key Features: Configurable arm spacing (k), revolution count, CW/CCW direction, fill & stroke colors with independent alpha, spin animation, draggable center
  • Common Uses: Spiral art, nautilus/galaxy shapes, hypnotic animations, geometric pattern exploration, mathematical curve illustration

Overview

The SWSpiral class draws an Archimedean spiral as a closed polygon. The spiral is fully defined by a center point, growth coefficient k, and the number of revolutions. In polar coordinates, the curve is:

r = k · θ / π     (θ in radians, r in user-space units)
Reading the formula r = k·θ/π
At θ = π (half-turn, 180°): r = k — the radius equals k units.
At θ = 2π (one full turn): r = 2k.
After n full turns: r = 2kn.
So k is precisely the radius at the half-turn point, and also the gap between successive arms measured at the half-turn crossing.

Fill Semantics

The spiral is drawn as a closed polygon: the arc from the center outward to the tip, then a straight line back to the center. When a fill color is set, p5.js fills the entire interior of this polygon using non-zero winding. With multiple revolutions the fill creates a layered effect — the regions between arms take on the fill color, which shows through most visibly when the fill is semi-transparent.

To highlight the space between spiral arms, use a semi-transparent fill (opacity 40–70%) combined with a distinct stroke color.

Winding Direction

Set clockwise = false (default) for a CCW spiral that opens to the right. Set clockwise = true to mirror the winding direction. Both produce an Archimedean spiral; the direction only affects which way the arms wind outward.

Rotation

SWSpiral supports two layers of rotation, both applied about the center point:

  • rotationDeg — Static base rotation set by setRotation(). Persists across frames; survives reset().
  • rotation — Accumulated rotation incremented by rotate(). Starts at 0; cleared by reset().

Effective rotation = rotationDeg + rotation. All angles are CCW positive (user-space convention).

Key Capabilities

  • Archimedean Spiral: Mathematically precise r = k·θ/π; evenly spaced arms
  • Configurable Arm Spacing: k directly controls how far apart each revolution is
  • CW / CCW Winding: Toggle the direction of the spiral without rebuilding
  • Spin Animation: Rotate continuously about the center using rotate()
  • Fill & Stroke Colors: Independent color pickers and alpha channels for stroke (outline) and fill (interior)
  • Draggable Center: Move the entire spiral by repositioning the center SWPoint
  • Dual Coordinate Systems: Draw in screen pixels (draw()) or grid user coordinates (drawOnGrid())

Constructor

new SWSpiral(center, k, revolutions, strokeColor, fillColor, thickness, clockwise, rotationDeg)

Creates a new SWSpiral instance. All constructor values are saved as originals for reset().

Parameters
ParameterTypeDefaultDescription
center SWPoint required The origin of the spiral in user (grid) coordinates. The spiral starts here (r = 0 at θ = 0) and winds outward.
k number required Growth coefficient. Controls the distance between successive arms (r = k at the half-turn). Larger k = wider spacing.
revolutions number 3 Number of full 360° turns to draw. Must be > 0; values below 0.25 are clamped to 0.25.
strokeColor SWColor | undefined undefined Spiral outline color. undefined = no stroke drawn.
fillColor SWColor | undefined undefined Fill color for the spiral interior polygon. undefined = no fill (transparent interior).
thickness number 2 Stroke weight in pixels.
clockwise boolean false Winding direction. false = CCW (default, opens to the right); true = CW (mirrored).
rotationDeg number 0 Static base rotation in CCW degrees; applied in addition to accumulated rotation.
Constructor Examples
// Basic blue spiral, 3 turns, CCW
const stroke = new SWColor(220, 70, 60, 100, "spiralStroke");
const fill   = new SWColor(200, 40, 90,  60, "spiralFill");
let spiral = new SWSpiral(new SWPoint(0, 0), 1.5, 3, stroke, fill, 2);

// Tight spiral with many turns (small k, many revolutions)
const tStroke = new SWColor(0, 80, 80, 100, "tightStroke");
let tight = new SWSpiral(new SWPoint(0, 0), 0.5, 6, tStroke, undefined, 1);

// Clockwise spiral, no fill, thick stroke
const cwStroke = new SWColor(30, 90, 90, 100, "cwStroke");
let cw = new SWSpiral(new SWPoint(2, 1), 2.0, 2, cwStroke, undefined, 3, true);

// Pre-rotated spiral (45° tilt at construction)
let tilted = new SWSpiral(new SWPoint(0, 0), 1.5, 3, stroke, fill, 2, false, 45);

// Stroke-only spiral with default revolutions
const minStroke = new SWColor(0, 0, 20, 100, "dark");
let minimal = new SWSpiral(new SWPoint(0, 0), 1.0, undefined, minStroke);

Properties

center SWPoint

The spiral's origin in user (grid) coordinates. The spiral always starts here (r = 0) and winds outward. Moving center.x or center.y repositions the entire spiral.

spiral.center.x = 3; spiral.center.y = -2;
k number

Growth coefficient controlling arm spacing. At θ = π, r = k. After n full turns, the outer radius is 2kn. Larger k = wider spiral arms. Use setK() to change it.

spiral.setK(0.5); // tight spiral
spiral.setK(3.0); // wide spiral
revolutions number

Number of full turns the spiral draws. Values below 0.25 are automatically clamped to 0.25. Use setRevolutions() to change it.

spiral.setRevolutions(5); // 5 full turns
spiral.setRevolutions(0.5); // half a turn only
clockwise boolean

Winding direction of the spiral. false (default) = CCW (mathematically positive direction); true = CW (mirrored). Use setClockwise() to toggle.

spiral.setClockwise(true); // wind clockwise
spiral.setClockwise(false); // wind counter-clockwise
rotationDeg number

Static base rotation in CCW degrees. Set by setRotation(); survives reset(). Added to the accumulated rotation to produce the effective drawing rotation.

spiral.setRotation(45); // tilt 45° CCW
rotation number

Accumulated rotation in degrees, incremented each frame by rotate(). Starts at 0; cleared by reset().

// Cleared automatically by reset(); read-only in normal use
SWSpiral.SAMPLE_COUNT static

Number of arc sample points per full revolution (default: 120). The total number of polygon vertices equals SAMPLE_COUNT × revolutions + 1 (plus the closing center point). Higher values produce smoother curves; lower values reveal the polygon facets. Can be set at any time.

SWSpiral.SAMPLE_COUNT = 24; // faceted, angular look
SWSpiral.SAMPLE_COUNT = 180; // very smooth

Methods

Drawing Methods

draw()

Draws the spiral in raw screen (pixel) coordinates. The center SWPoint is interpreted as screen pixels. Prefer drawOnGrid() for standard canvas use with an SWGrid.

function draw() {
    background(220);
    spiral.draw();
}
drawOnGrid(grid)

Draws the spiral mapped through the given SWGrid's coordinate system. This is the standard method; the grid handles converting user-space coordinates to screen pixels and the y-flip (math up → screen down).

function draw() {
    background(220);
    grid.draw();
    spiral.drawOnGrid(grid);
}

Rotation Animation

rotate(deltaAngle)

Spins the spiral about its center by deltaAngle degrees (CCW+, CW−). Accumulates into this.rotation. Call once per frame in draw() before calling drawOnGrid().

// Spin at 45°/second
const SPIN_SPEED = 45;
let prevT = 0;

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

    spiral.rotate(SPIN_SPEED * deltaT);  // call BEFORE drawOnGrid
    spiral.drawOnGrid(grid);
}

Breathe Animation

Breathe — sinusoidal k oscillation

The breathe effect is implemented in the sketch layer using setK(). Each frame, k is driven by a sine wave centered on a base value:

currentK = baseK + depth · sin(2π · speed · t)

baseK is the steady-state value (typically from a slider). depth (Δk) is the amplitude of oscillation. speed is the frequency in Hz (cycles per second). The value is clamped to a minimum of 0.05 so the spiral never collapses. Breathe and Spin can run simultaneously.

// Breathe at 0.3 Hz with Δk = 0.3
const BREATHE_SPEED = 0.3;  // Hz
const BREATHE_DEPTH = 0.3;  // Δk
let baseK = 0.8;

function draw() {
    const t = millis() / 1000;
    const breathedK = baseK + BREATHE_DEPTH * sin(TWO_PI * BREATHE_SPEED * t);
    spiral.setK(max(0.05, breathedK));
    spiral.drawOnGrid(grid);
}

Setter Methods

setK(k)

Updates the growth coefficient. Takes effect immediately on the next draw call.

spiral.setK(2.5);
setRevolutions(r)

Updates the number of full turns. Values below 0.25 are clamped to 0.25.

spiral.setRevolutions(4);
setClockwise(cw)

Sets the winding direction. true = CW; false = CCW.

spiral.setClockwise(true); // wind clockwise
setStrokeColor(sc)   setFillColor(fc)

Sets the stroke or fill color. Pass an SWColor instance or undefined to remove the color.

spiral.setStrokeColor(new SWColor(240, 80, 60, 100, "dark"));
spiral.setFillColor(undefined); // transparent fill
setFillAlpha(alpha)   setStrokeAlpha(alpha)

Sets the fill or stroke alpha (0–100) and rebuilds the p5 color object. Requires an existing fill/stroke color to be set first.

spiral.setFillAlpha(40); // 40% opacity fill
spiral.setStrokeAlpha(100); // fully opaque stroke
setStrokeWeight(w)

Sets the stroke thickness in pixels.

spiral.setStrokeWeight(4);
setRotation(deg)

Sets the static base rotation in CCW degrees. Does not affect the accumulated rotation.

spiral.setRotation(90); // rotates the spiral 90° CCW from default

Reset & Utility Methods

reset()

Restores all animated and slider-driven properties to their original constructor values. Clears accumulated spin rotation. Does not move the center position.

spiral.reset(); // back to factory defaults
static SWSpiral.copy(other)

Creates a deep copy of the given SWSpiral, preserving all current and original state including rotation accumulation.

const copy = SWSpiral.copy(spiral);
toString()

Returns a human-readable string describing the spiral's current state.

console.log(spiral.toString());
// "SWSpiral(center=SWPoint(x:0, y:0), k=1.50, revolutions=3.00, direction=CCW, rotationDeg=0.0, rotation=0.0)"

Code Examples

Minimal sketch (spiral on a grid)

let grid, spiral;

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 = new SWColor(220, 70, 60, 100, "s");
    const fill   = new SWColor(200, 40, 90,  60, "f");
    spiral = new SWSpiral(new SWPoint(0, 0), 1.5, 3, stroke, fill, 2);
}

function draw() {
    background(240);
    grid.draw();
    spiral.drawOnGrid(grid);
    grid.updateScreenBounds();
}

Spinning spiral

let prevT = 0;
const SPIN_SPEED = 60; // degrees per second

function setup() { /* ... create grid and spiral ... */ }

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

    background(240);
    grid.draw();
    spiral.rotate(SPIN_SPEED * deltaT);   // spin BEFORE drawing
    spiral.drawOnGrid(grid);
    grid.updateScreenBounds();
}

Changing direction at runtime

function keyPressed() {
    if (key === 'c') {
        spiral.setClockwise(!spiral.clockwise); // flip direction
    }
    if (key === 'r') {
        spiral.reset();  // back to factory defaults
    }
}

Using a color picker with SWColor.fromHex()

// In your HTML:
// <input type="color" id="strokePicker" value="#3366bb">
// <input type="range" id="alphaSlider" min="0" max="100" value="100">

const picker = document.getElementById('strokePicker');
const alpha  = document.getElementById('alphaSlider');

picker.addEventListener('input', () => {
    const col = SWColor.fromHex(picker.value, Number(alpha.value), 'spiralStroke');
    spiral.setStrokeColor(col);
});

alpha.addEventListener('input', () => {
    const col = SWColor.fromHex(picker.value, Number(alpha.value), 'spiralStroke');
    spiral.setStrokeColor(col);
});

Required script tags (in dependency order)

<script src="https://cdn.jsdelivr.net/npm/p5@1.6.0/lib/p5.js"></script>

<!-- SketchWaveJS classes in dependency order -->
<script src="../shapeClasses/swColor.js"></script>
<script src="../shapeClasses/swPoint.js"></script>
<script src="../shapeClasses/swGrid.js"></script>
<script src="../shapeClasses/swSpiral.js"></script>

<!-- Your sketch -->
<script src="../sketches/yourSketch.js"></script>

Design Notes

  • Outer radius grows with both k and revolutions: The outermost point is at r = 2kn where n = revolutions. With k=1.5 and 3 revolutions, r_max = 9 — this fits neatly in a 10-unit grid. With larger k or more revolutions, the spiral will extend beyond the visible grid area.
  • SAMPLE_COUNT controls smoothness per revolution: The total polygon vertex count is SAMPLE_COUNT × revolutions. The default of 120 per revolution gives very smooth curves; reducing it to 12–24 creates a faceted polygon look.
  • Fill color applies to the full interior polygon: p5.js fills the closed polygon formed by the spiral arc and the straight closing line back to center. This is not a true "between-arms" fill — it fills the enclosed area of the whole shape. Use semi-transparent fill for the most natural appearance.
  • Rotation is applied in polar → Cartesian conversion: The rotation is applied to the computed (x, y) displacement from center before converting to screen coordinates, which correctly rotates the entire spiral about its center.
  • reset() does not move the center: This allows the center to be dragged and repositioned interactively without losing the position on reset.

Source Code

The complete SWSpiral class implementation:

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

SWSpiral represents an Archimedean spiral defined by the polar equation
r = k · θ / π  in a coordinate system centered at a given center point:

  center (SWPoint) — the origin of the spiral (where θ = 0, r = 0)
  k (number)       — growth coefficient controlling arm spacing:
                     r = k · θ / π
                     At θ = π  (half turn) : r = k
                     At θ = 2π (one full turn): r = 2k
                     After n full turns     : r = 2kn
  revolutions (number) — how many full 360° turns the spiral traces (must be > 0)
  clockwise (boolean)  — if true, spiral winds clockwise; if false, CCW (default)

In Cartesian (user-space, y-up):
  x = r · cos(θ)    y = r · sin(θ)   for CCW
  x = r · cos(θ)    y = −r · sin(θ)  for CW  (same as θ → −θ)

The drawn shape is a closed polygon: the spiral arc sampled at
SAMPLE_COUNT × revolutions points from the center outward to the tip,
then a straight line back to the center.  Fill is applied to the
enclosed interior; stroke is drawn along the full outline.

Fill semantics:
  The fill color covers the interior of the spiral polygon.  With multiple
  revolutions the p5.js non-zero-winding fill naturally shades the regions
  between the arms, giving the appearance of the fill color showing through
  the gaps in the spiral stroke.  Use a semi-transparent fill for the best
  visual layering effect.

Rotation:
  rotationDeg — static base rotation (CCW degrees), set by setRotation().
                Persists across frames; survives reset().
  rotation    — accumulated rotation (degrees), incremented by rotate().
                Starts at 0; reset() returns it to 0.
  Effective rotation = rotationDeg + rotation.  All rotation is CCW positive.
  All rotation is applied about the CENTER point.

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

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

console.log("[swSpiral.js] SWSpiral class loaded.");

class SWSpiral {

    static SAMPLE_COUNT = 120;  // sample points per full revolution

    /**
     * @param {SWPoint} center           - Center of the spiral in user (grid) coordinates
     * @param {number}  k                - Growth coefficient (r = k·θ/π). Larger = wider arm spacing.
     * @param {number}  [revolutions=3]  - Number of full turns to draw (must be > 0)
     * @param {SWColor} [strokeColor]    - Spiral line / border color (undefined = no stroke)
     * @param {SWColor} [fillColor]      - Fill color for spiral interior (undefined = no fill)
     * @param {number}  [thickness=2]    - Stroke weight in pixels
     * @param {boolean} [clockwise=false]- Wind clockwise (true) or counter-clockwise (false)
     * @param {number}  [rotationDeg=0]  - Static base rotation in CCW degrees
     */
    constructor(center, k, revolutions = 3,
                strokeColor = undefined, fillColor = undefined,
                thickness = 2, clockwise = false, rotationDeg = 0) {

        this.center      = center;
        this.k           = k;
        this.revolutions = Math.max(0.25, revolutions);
        this.strokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
        this.fillColor   = fillColor   ? SWColor.copy(fillColor)   : undefined;
        this.thickness   = thickness;
        this.clockwise   = clockwise;
        this.rotationDeg = rotationDeg;
        this.rotation    = 0;   // accumulated via rotate(); cleared by reset()

        // ── Originals for reset() ──────────────────────────────────────────────
        this.originalK           = k;
        this.originalRevolutions = this.revolutions;
        this.originalStrokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
        this.originalFillColor   = fillColor   ? SWColor.copy(fillColor)   : undefined;
        this.originalThickness   = thickness;
        this.originalClockwise      = clockwise;
        this.originalRotationDeg    = rotationDeg;
        this.showClosingLine        = false;   // default: no line from tip back to center
        this.originalShowClosingLine = false;

    }//end constructor

    // ── Internal helpers ──────────────────────────────────────────────────────

    /** @returns {number} Total effective rotation in degrees. */
    _totalRotDeg() { return this.rotationDeg + this.rotation; }

    /**
     * Rotates a local math-space displacement (lx, ly) by totalRotation degrees (CCW+).
     * @returns {{ x: number, y: number }} rotated displacement in math-space
     */
    _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,
        };
    }

    /**
     * Builds an array of { x, y } positions in user (math) space.
     * The final entry is the center point, closing the polygon back to the origin.
     * @returns {{ x: number, y: number }[]}
     */
    _buildUserPts() {
        const cx         = this.center.x;
        const cy         = this.center.y;
        const maxTheta   = 2 * Math.PI * this.revolutions;
        const totalSteps = Math.max(3, Math.round(SWSpiral.SAMPLE_COUNT * this.revolutions));
        const dir        = this.clockwise ? -1 : 1;
        const pts        = [];

        for (let i = 0; i <= totalSteps; i++) {
            const theta = (i / totalSteps) * maxTheta;
            const r     = (this.k * theta) / Math.PI;
            const lx    = r * Math.cos(dir * theta);
            const ly    = r * Math.sin(dir * theta);
            const rot   = this._rotateLocal(lx, ly);
            pts.push({ x: cx + rot.x, y: cy + rot.y });
        }

        // Close back to center so the polygon fills correctly
        pts.push({ x: cx, y: cy });
        return pts;
    }

    /**
     * Builds screen-space { x, y } points using the given SWGrid.
     * grid.userToScreen() handles the math-space ↔ screen y-flip.
     */
    _buildScreenPtsGrid(grid) {
        return this._buildUserPts().map(p => grid.userToScreen(p.x, p.y));
    }

    /**
     * Builds screen-space { x, y } points for draw() (no grid).
     * The math-space y-displacement is negated to produce correct screen-space y (down).
     */
    _buildScreenPtsDirect() {
        const cx         = this.center.x;
        const cy         = this.center.y;
        const maxTheta   = 2 * Math.PI * this.revolutions;
        const totalSteps = Math.max(3, Math.round(SWSpiral.SAMPLE_COUNT * this.revolutions));
        const dir        = this.clockwise ? -1 : 1;
        const pts        = [];

        for (let i = 0; i <= totalSteps; i++) {
            const theta = (i / totalSteps) * maxTheta;
            const r     = (this.k * theta) / Math.PI;
            const lx    = r * Math.cos(dir * theta);
            const ly    = r * Math.sin(dir * theta);
            const rot   = this._rotateLocal(lx, ly);
            pts.push({ x: cx + rot.x, y: cy - rot.y });   // y-flip for screen
        }

        // Close back to center
        pts.push({ x: cx, y: cy });
        return pts;
    }

    /**
     * Executes two-pass draw: fill (always closed) then stroke (closed only when showClosingLine).
     * screenPts includes the explicit center closing point at the end.
     */
    _drawShape(screenPts) {
        // ── Fill pass: always close so the interior fills correctly ──────────────
        if (this.fillColor && this.fillColor.col) {
            fill(this.fillColor.col);
            noStroke();
            beginShape();
            for (const sp of screenPts) vertex(sp.x, sp.y);
            endShape(CLOSE);
        }

        // ── Stroke pass: close (tip→center) only when showClosingLine is true ────
        if (this.strokeColor && this.strokeColor.col) {
            noFill();
            stroke(this.strokeColor.col);
            strokeWeight(this.thickness);
            if (this.showClosingLine) {
                // Draw full polygon including the closing line tip→center
                beginShape();
                for (const sp of screenPts) vertex(sp.x, sp.y);
                endShape(CLOSE);
            } else {
                // Draw only the spiral arc — omit the explicit closing center point
                beginShape();
                const arcPts = screenPts.slice(0, -1);
                for (const sp of arcPts) vertex(sp.x, sp.y);
                endShape();
            }
        }

        noStroke();
        noFill();
        strokeWeight(1);
    }

    // ── Drawing ───────────────────────────────────────────────────────────────

    /**
     * Draws the spiral in raw screen (pixel) coordinates.
     * Prefer drawOnGrid() for standard canvas use.
     */
    draw() {
        const screenPts = this._buildScreenPtsDirect();
        this._drawShape(screenPts);
        if (this.center && this.center.draw) {
            this.center.draw(this.strokeColor);
        }
    }//end draw

    /**
     * Draws the spiral mapped through the given SWGrid's coordinate system.
     * This is the standard drawing method to use in a p5.js draw() loop.
     * @param {SWGrid} grid
     */
    drawOnGrid(grid) {
        const screenPts = this._buildScreenPtsGrid(grid);
        this._drawShape(screenPts);
        if (this.center && this.center.drawOnGrid) {
            this.center.drawOnGrid(grid, this.strokeColor);
        }
    }//end drawOnGrid

    // ── Animation ─────────────────────────────────────────────────────────────

    /**
     * Spins the spiral about its center by deltaAngle degrees (CCW+, CW−).
     * Accumulates into this.rotation.  Call once per frame in draw().
     * @param {number} deltaAngle  degrees per frame (typically speed × deltaT)
     */
    rotate(deltaAngle) { this.rotation += deltaAngle; }

    // ── Setters ───────────────────────────────────────────────────────────────

    setK(k)            { this.k           = k; }
    setRevolutions(r)  { this.revolutions  = Math.max(0.25, r); }
    setClockwise(cw)   { this.clockwise    = cw; }

    /** Sets the static base rotation in CCW degrees. Does not affect accumulated rotation. */
    setRotation(deg)   { this.rotationDeg  = deg; }

    setStrokeColor(sc)       { this.strokeColor   = sc ? SWColor.copy(sc) : undefined; }
    setFillColor(fc)         { this.fillColor     = fc ? SWColor.copy(fc) : undefined; }
    setStrokeWeight(w)       { this.thickness     = w; }
    setShowClosingLine(val)  { this.showClosingLine = val; }

    /**
     * Sets the fill alpha (0–100) and rebuilds the p5 color object.
     * @param {number} alpha  0 = transparent, 100 = opaque
     */
    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
            );
        }
    }

    /**
     * Sets the stroke alpha (0–100) and rebuilds the p5 color object.
     * @param {number} alpha  0 = transparent, 100 = opaque
     */
    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 & Utility ───────────────────────────────────────────────────────

    /**
     * Restores all animated/slider-driven properties to original constructor values.
     * Clears accumulated spin rotation.  Does NOT move the center position.
     */
    reset() {
        this.k           = this.originalK;
        this.revolutions = this.originalRevolutions;
        this.clockwise   = this.originalClockwise;
        this.rotationDeg = this.originalRotationDeg;
        this.rotation    = 0;
        this.thickness   = this.originalThickness;
        this.strokeColor      = this.originalStrokeColor
            ? SWColor.copy(this.originalStrokeColor) : undefined;
        this.fillColor       = this.originalFillColor
            ? SWColor.copy(this.originalFillColor)   : undefined;
        this.showClosingLine = this.originalShowClosingLine;
    }//end reset

    /**
     * Creates a deep copy of the given SWSpiral, preserving all current and original state.
     * @param {SWSpiral} other
     * @returns {SWSpiral}
     */
    static copy(other) {
        const c = new SWSpiral(
            SWPoint.copy(other.center),
            other.originalK,
            other.originalRevolutions,
            other.originalStrokeColor,
            other.originalFillColor,
            other.originalThickness,
            other.originalClockwise,
            other.originalRotationDeg
        );
        c.k               = other.k;
        c.revolutions     = other.revolutions;
        c.clockwise       = other.clockwise;
        c.rotationDeg     = other.rotationDeg;
        c.rotation        = other.rotation;
        c.showClosingLine = other.showClosingLine;
        return c;
    }//end copy

    toString() {
        const dir = this.clockwise ? 'CW' : 'CCW';
        return `SWSpiral(center=${this.center}, k=${this.k.toFixed(2)}, ` +
               `revolutions=${this.revolutions.toFixed(2)}, direction=${dir}, ` +
               `rotationDeg=${this.rotationDeg.toFixed(1)}, rotation=${this.rotation.toFixed(1)})`;
    }

}//end SWSpiral class