◯ SWBumpyCircle Reference

Polar Curves — Sinusoidal Radius Modulation — SketchWaveJS

◯ Polar Curve Mathematics

What is a Polar Curve?

A polar curve expresses the radius r as a function of the angle θ, rather than defining x and y independently. The Cartesian coordinates follow from x = r(θ)·cos(θ) and y = r(θ)·sin(θ). Because both coordinates share the same r(θ), the curve's shape is entirely determined by how the radius varies with angle — making polar equations a natural language for rotationally symmetric figures.

The SWBumpyCircle Equation

SWBumpyCircle modulates a plain circle's radius with a sinusoidal perturbation:

r(θ) = radius + amplitude · sin(frequency · θ)power
x(θ) = r(θ) · cos(θ)
y(θ) = r(θ) · sin(θ)

θ ∈ [0, 2π × numRevolutions),  1000 sample points per revolution

The key insight: the base circle provides the underlying structure, and sin(frequency · θ) is the wave that rides on top of it, adding bumps and dips as the angle sweeps around.

Understanding Each Parameter

ParameterRole in the EquationVisual Effect
radius The constant term — the baseline around which sin oscillates Controls the overall size of the figure
amplitude Multiplier on the sin term — how far the wave deviates from the baseline Controls bump height; 0 = plain circle, large = dramatic peaks
frequency Argument multiplier inside sin — how many full sine cycles occur in one revolution Controls bump count; frequency = 6 → 6 bumps per revolution
power Exponent applied to the entire sin term after evaluation Controls bump shape (see Power section below)
numRevolutions How many full 2π cycles θ traverses before the curve closes Required for non-integer frequencies to achieve closure

The Power Parameter: Bump Shape Control

The power exponent is applied to the entire sin() result, which means it interacts with the sign of sin in important ways:

power valueEffect on negative sin valuesResult
1 (odd) sin passes through unchanged; negative sin produces inward dips Alternating outward bumps and inward dents — smooth sinusoidal wave
2 (even) sin² ≥ 0 always — dips become bumps All-outward bumps only; twice the apparent frequency; rounder profiles
3 (odd > 1) Negative cubed is still negative, but magnitude suppressed near zero Pointed bumps with subtler, flattened dents
4+ (even) Always positive; zero crossings of sin become very flat Well-separated dome-like bumps with flat valleys between them

Practical tip: With power = 1 and frequency = 6, you get 6 outward bumps and 6 inward dents. Switching to power = 2 produces 12 bumps all pointing outward (the period of sin² is half that of sin). This is why rotating by 180° with odd frequency turns bumps into dents — you're now at the opposite phase of the sine wave.

Closure and numRevolutions

A bumpy circle closes exactly when r(θ) returns to its starting value after a full traversal. For integer frequency, sin(frequency · θ) completes exactly frequency full cycles in one revolution [0, 2π), so the curve always closes perfectly with numRevolutions = 1. For a fractional frequency p/q (in lowest terms), you need numRevolutions = q to traverse enough of the angle space for closure. For example, frequency = 2.5 = 5/2 requires numRevolutions = 2.

Famous Polar Curves

SWBumpyCircle belongs to a rich mathematical family of polar curves:

CurveEquationNotes
Circler = c (constant)SWBumpyCircle with amplitude = 0
Rose curvesr = cos(nθ) or r = sin(nθ)n petals (n odd) or 2n petals (n even)
Limaçonr = b + a·cos(θ)Cardioid when a = b; similar to SWBumpyCircle with frequency = 1
Cardioidr = 1 − cos(θ)Heart-shaped; traced by a point on a rolling circle
Archimedean spiralr = a + bθEqual spacing between turns (not a closed curve)
SWBumpyCircler = radius + amplitude · sin(fθ)pGeneralized bumpy circle; closes for integer f
SWShamrockr = SCALE · radius · (sin(3θ/2) + sin(9θ/2)/5)²Three organic lobes via half-angle squaring

SWBumpyCircle vs. SWShamrock

FeatureSWBumpyCircleSWShamrock
Equation typeAdditive perturbation of circleSquared sum of half-angle sines
Bump countControlled by frequency (1–20+)Fixed at 3 lobes
Bump directionOutward + inward (odd power) or all outward (even power)Always outward (squaring)
SilhouetteGeometric; perfectly regular with integer frequencyOrganic, slightly asymmetric
SAMPLE_COUNT1000 per revolution600
StemNoYes (SWSpire composite)
Amplitude controlYes — separate parameterFixed by squaring non-linearity

Constructor

new SWBumpyCircle(center, radius, amplitude, frequency, power,
                  fillColor, strokeColor, thickness, rotationDeg, numRevolutions)
ParameterTypeDefaultDescription
centerSWPointrequiredCenter position in user (grid) coordinates. The center dot is displayed by default; set center.shouldShow = false to hide it.
radiusnumber5Base circle radius in grid units (min 0.01). The curve oscillates around this value.
amplitudenumber1.5Peak height of bumps above (and depth of dips below) the base circle. 0 = plain circle.
frequencynumber6Number of full sine cycles per revolution. Integer values always produce closed curves in one revolution.
powernumber1Exponent on sin term. Odd = bumps and dents; even = all-outward bumps. Clamped to positive integer ≥ 1.
fillColorSWColorundefinedFill color; undefined = no fill. Deep-copied internally.
strokeColorSWColorundefinedStroke color; undefined = no stroke. Deep-copied internally.
thicknessnumber2Stroke weight in pixels.
rotationDegnumber0Static base rotation in CCW degrees. Preserved across reset().
numRevolutionsnumber1Full θ revolutions to trace for curve closure. Use 1 for integer frequency; use q for fractional frequency p/q.
// Basic bumpy circle with 6 bumps, ocean-blue colors
const center = new SWPoint(0, 0, undefined, 8, new SWColor(210, 70, 40, 100));
const fill   = SWColor.fromHex('#1a7abf', 40, 'fill');
const stroke = SWColor.fromHex('#0c3d7a', 100, 'stroke');
const bc = new SWBumpyCircle(center, 5, 1.5, 6, 1, fill, stroke, 2);
bc.drawOnGrid(grid);

Properties

PropertyTypeDescription
centerSWPointCenter position in user (grid) coordinates. Drag to reposition in the demo; set center.shouldShow = false to hide the dot.
radiusnumberBase circle radius (grid units). The sinusoidal perturbation oscillates around this value.
amplitudenumberBump peak height in grid units above (or below for odd power) the base circle. 0 = plain circle.
frequencynumberSine cycles per revolution. Integer values produce closed curves with numRevolutions = 1.
powernumberExponent on the sin term. Odd = bumps + dents; even = all-outward bumps only.
fillColorSWColorFill color (undefined = transparent fill).
strokeColorSWColorStroke color (undefined = no outline).
thicknessnumberStroke weight in pixels.
rotationDegnumberStatic base rotation (CCW degrees). Set via setRotation(). Preserved across reset().
rotationnumberAccumulated spin rotation (degrees). Incremented by rotate(). Cleared to 0 by reset().
numRevolutionsnumberFull θ revolutions traced per draw call. Read-only after construction; use setNumRevolutions() to change.

Static Properties

PropertyValueDescription
SWBumpyCircle.SAMPLE_COUNT1000Sample points per revolution of θ. Total points = SAMPLE_COUNT × numRevolutions.

Methods

Drawing

MethodDescription
draw() Draws in raw pixel (screen) coordinates. Use center.x/y as pixel offsets; y increases downward. Prefer drawOnGrid() for standard use.
drawOnGrid(grid) Draws mapped through the given SWGrid. Handles the math-space ↔ screen y-flip automatically. Draws the shape then the center dot. Use this in the p5.js draw loop.

Animation

MethodParametersDescription
rotate(deltaAngle) deltaAngle: degrees (CCW+, CW−) Accumulates spin rotation. Call once per frame: bc.rotate(speed * deltaT). With odd frequency, notice bumps and dents exchange positions at 180°.

Setters

MethodParameterDescription
setRadius(r)number ≥ 0.01Sets base circle radius. Use during Breathe (radius) animation.
setAmplitude(a)number ≥ 0Sets bump amplitude. Use during Breathe (amplitude) animation. 0 = plain circle.
setFrequency(f)number ≥ 0Sets bump frequency. 0 = plain circle (amplitude is ignored).
setPower(p)number ≥ 1Sets sin exponent. Clamped to positive integer.
setRotation(deg)number (degrees)Sets static base rotation (CCW). Does not clear accumulated rotation.
setNumRevolutions(n)integer ≥ 1Sets the revolution count (for fractional frequency closure).
setFillColor(fc)SWColorReplaces fill color (deep copy stored).
setStrokeColor(sc)SWColorReplaces stroke color (deep copy stored).
setStrokeWeight(w)numberSets stroke thickness in pixels.
setFillAlpha(alpha)0–100Updates fill opacity and rebuilds the p5 color object.
setStrokeAlpha(alpha)0–100Updates stroke opacity and rebuilds the p5 color object.

Reset & Utility

MethodDescription
reset() Restores radius, amplitude, frequency, power, rotationDeg, colors, thickness, and numRevolutions to constructor values. Clears accumulated spin rotation. Does not move the center.
SWBumpyCircle.copy(other) (static) Returns a deep copy of other, including center SWPoint, all parameters, and accumulated rotation.
toString() Returns a human-readable summary string: SWBumpyCircle(center:(x, y), radius:r, amplitude:a, frequency:f, power:p, rotation:θ°, numRevolutions:n)

Code Examples

1. Basic Setup (p5.js global mode)

let grid, bc;

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);
    const fill   = SWColor.fromHex('#1a7abf', 40, 'fill');
    const stroke = SWColor.fromHex('#0c3d7a', 100, 'stroke');
    // radius=5, amplitude=1.5, frequency=6, power=1
    bc = new SWBumpyCircle(center, 5, 1.5, 6, 1, fill, stroke, 2);
}

function draw() {
    background(0, 0, 93);
    grid.draw();
    bc.drawOnGrid(grid);
    grid.updateScreenBounds();
}

2. Spin Animation

let prevT = 0;
const SPIN_SPEED = 45; // degrees per second (CCW)

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

    background(0, 0, 93);
    grid.draw();
    bc.rotate(SPIN_SPEED * deltaT);  // accumulate CCW rotation each frame
    bc.drawOnGrid(grid);
    grid.updateScreenBounds();
}

3. Breathe: Radius Oscillation

The base radius gently expands and contracts, while the bump count and shape stay fixed.

const BASE_RADIUS    = 5.0;
const BREATHE_SPEED  = 0.5;  // Hz (cycles per second)
const BREATHE_AMOUNT = 1.5;  // grid units of swing

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

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

    // Oscillate radius sinusoidally; clamp to avoid collapsing to a point
    const r = Math.max(0.5, BASE_RADIUS + BREATHE_AMOUNT * Math.sin(2 * Math.PI * BREATHE_SPEED * t));
    bc.setRadius(r);

    bc.drawOnGrid(grid);
    grid.updateScreenBounds();
}

4. Breathe: Amplitude Oscillation

The bump height itself pulses over time — bumps grow tall and then flatten back toward a plain circle, then repeat.

const BASE_AMP      = 1.5;
const AMP_SPEED     = 0.5;  // Hz
const AMP_SWING     = 1.0;  // how far amplitude oscillates above/below BASE_AMP

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

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

    // Oscillate amplitude; clamp to 0 so shape never inverts unexpectedly
    const a = Math.max(0, BASE_AMP + AMP_SWING * Math.sin(2 * Math.PI * AMP_SPEED * t));
    bc.setAmplitude(a);

    bc.drawOnGrid(grid);
    grid.updateScreenBounds();
}

5. Combining Both Breathe Modes

Run radius and amplitude oscillation at independent speeds for complex, unpredictable pulsing. Use different frequencies for interesting beats.

const BASE_RADIUS = 5.0;   const R_SPEED = 0.5;  const R_SWING = 1.5;
const BASE_AMP    = 1.5;   const A_SPEED = 0.7;  const A_SWING = 1.0;

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

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

    bc.setRadius   (Math.max(0.5, BASE_RADIUS + R_SWING * Math.sin(2 * Math.PI * R_SPEED * t)));
    bc.setAmplitude(Math.max(0,   BASE_AMP    + A_SWING * Math.sin(2 * Math.PI * A_SPEED * t)));

    bc.drawOnGrid(grid);
    grid.updateScreenBounds();
}

6. Power and Frequency Exploration

// 8 bumps, pointed (odd power > 1): bumps sharpen, dents flatten
const bc1 = new SWBumpyCircle(center, 5, 2, 8, 3, fill, stroke);

// 8 bumps, all-outward dome profile (even power)
const bc2 = new SWBumpyCircle(center, 5, 2, 8, 2, fill, stroke);

// Non-integer frequency — needs numRevolutions for closure
// frequency = 2.5 = 5/2, so numRevolutions = 2
const bc3 = new SWBumpyCircle(center, 5, 1.5, 2.5, 1, fill, stroke, 2, 0, 2);

// Change power or frequency interactively:
bc1.setPower(4);       // switch to even power — dents disappear
bc1.setFrequency(12); // double the bump count

7. Copy and Reset

// Deep copy — all parameters and accumulated rotation preserved
const copy = SWBumpyCircle.copy(bc);

// Spin the copy independently
copy.rotate(45);

// Reset original to constructor values (center position preserved)
bc.reset();

// toString
console.log(bc.toString());
// → SWBumpyCircle(center:(0.00, 0.00), radius:5.00, amplitude:1.50,
//                frequency:6, power:1, rotation:0.0°, numRevolutions:1)

Source Code

Show / Hide swBumpyCircle.js source
/*
File: swBumpyCircle.js
Date: 2026-05-10
Author: klp
App:  SketchWaveTNT2026-05-01-Stg9
Purpose: SWBumpyCircle class for SketchWaveJS

SWBumpyCircle draws a polar curve where the radius oscillates sinusoidally
around a base circle, adapted from the legacy TNTBumpyCircle class:

  r(θ) = radius + amplitude · sin(frequency · θ)^power
  x(θ) = r(θ) · cos(θ)
  y(θ) = r(θ) · sin(θ)
  θ ∈ [0, 2π) × numRevolutions

Parameters:
  radius        – base circle radius (grid units)
  amplitude     – peak height of each bump above the base circle
  frequency     – number of full sine cycles per revolution (integer recommended);
                  for integer frequency, one revolution closes the curve perfectly
  power         – exponent applied to sin(frequency · θ):
                  power = 1 → smooth sinusoidal bumps and dents
                  power even → only bumps outward (negative sin^even is positive)
                  power odd > 1 → more pointed bumps with subtler dents
  numRevolutions – full θ revolutions to trace before closing;
                   use 1 for integer frequency; for frequency p/q (lowest terms), use q

Angle convention (same as all SketchWaveJS classes):
  User space: CCW positive, y increases upward.
  p5 screen:  CW  positive, y increases downward.
  SWBumpyCircle handles the y-flip internally in draw() and drawOnGrid().

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

console.log("[swBumpyCircle.js] SWBumpyCircle class loaded.");

class SWBumpyCircle {

    static SAMPLE_COUNT = 1000;  // samples per revolution

    /**
     * @param {SWPoint} center              – Center in user (grid) coordinates
     * @param {number}  [radius=5]          – Base circle radius (grid units, min 0.01)
     * @param {number}  [amplitude=1.5]     – Bump height in grid units (0 = plain circle)
     * @param {number}  [frequency=6]       – Bumps per revolution (integer recommended)
     * @param {number}  [power=1]           – Exponent on sin term (positive integer; 1 = smooth)
     * @param {SWColor} [fillColor]         – Fill color (undefined = no fill)
     * @param {SWColor} [strokeColor]       – Stroke color (undefined = no stroke)
     * @param {number}  [thickness=2]       – Stroke weight in pixels
     * @param {number}  [rotationDeg=0]     – Static base rotation in CCW degrees
     * @param {number}  [numRevolutions=1]  – Full θ revolutions to trace for closure
     */
    constructor(
        center,
        radius         = 5,
        amplitude      = 1.5,
        frequency      = 6,
        power          = 1,
        fillColor      = undefined,
        strokeColor    = undefined,
        thickness      = 2,
        rotationDeg    = 0,
        numRevolutions = 1
    ) {
        this.center         = center;
        this.radius         = Math.max(0.01, radius);
        this.amplitude      = amplitude;
        this.frequency      = Math.max(0, frequency);
        this.power          = Math.max(1, Math.round(Math.abs(power)));
        this.fillColor      = fillColor   ? SWColor.copy(fillColor)   : undefined;
        this.strokeColor    = strokeColor ? SWColor.copy(strokeColor) : undefined;
        this.thickness      = thickness;
        this.rotationDeg    = rotationDeg;
        this.rotation       = 0;          // accumulated via rotate()
        this.numRevolutions = Math.max(1, Math.round(numRevolutions));

        // Originals for reset()
        this.originalRadius         = this.radius;
        this.originalAmplitude      = this.amplitude;
        this.originalFrequency      = this.frequency;
        this.originalPower          = this.power;
        this.originalFillColor      = fillColor   ? SWColor.copy(fillColor)   : undefined;
        this.originalStrokeColor    = strokeColor ? SWColor.copy(strokeColor) : undefined;
        this.originalThickness      = thickness;
        this.originalRotationDeg    = rotationDeg;
        this.originalNumRevolutions = this.numRevolutions;

    }//end constructor

    // — Internal helpers ———————————————————————————————————————————————————————————

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

    /**
     * Compute the bumped radius at angle theta (in radians).
     * r(θ) = radius + amplitude · sin(frequency · θ)^power
     * Note: Math.pow preserves sign when power is odd, always positive when even.
     */
    _rFlux(theta) {
        if (this.frequency === 0 || this.amplitude === 0) return this.radius;
        const s = Math.sin(this.frequency * theta);
        return this.radius + this.amplitude * Math.pow(s, this.power);
    }

    /**
     * Build an array of {x, y} points in user (grid) space, rotation applied.
     */
    _buildUserPts() {
        const N      = SWBumpyCircle.SAMPLE_COUNT * this.numRevolutions;
        const cx     = this.center.x;
        const cy     = this.center.y;
        const rotRad = this._totalRotDeg() * Math.PI / 180;
        const cosR   = Math.cos(rotRad);
        const sinR   = Math.sin(rotRad);
        const pts    = [];
        const sweep  = this.numRevolutions * 2 * Math.PI;

        for (let i = 0; i <= N; i++) {
            const theta = (i / N) * sweep;
            const r     = this._rFlux(theta);
            const lx    = r * Math.cos(theta);
            const ly    = r * Math.sin(theta);
            // Apply CCW rotation in user space
            pts.push({
                x: cx + lx * cosR - ly * sinR,
                y: cy + lx * sinR + ly * cosR
            });
        }
        return pts;
    }

    /**
     * Map user points to screen pixels via the SWGrid (handles y-flip).
     */
    _buildScreenPtsGrid(grid) {
        return this._buildUserPts().map(pt => grid.userToScreen(pt.x, pt.y));
    }

    /**
     * Build screen points for direct pixel drawing (no grid): y-flip applied manually.
     * In this mode center.x / center.y are treated as pixel offsets.
     */
    _buildScreenPtsDirect() {
        const N      = SWBumpyCircle.SAMPLE_COUNT * this.numRevolutions;
        const cx     = this.center.x;
        const cy     = this.center.y;
        const rotRad = this._totalRotDeg() * Math.PI / 180;
        const cosR   = Math.cos(rotRad);
        const sinR   = Math.sin(rotRad);
        const pts    = [];
        const sweep  = this.numRevolutions * 2 * Math.PI;

        for (let i = 0; i <= N; i++) {
            const theta = (i / N) * sweep;
            const r     = this._rFlux(theta);
            const lx    = r * Math.cos(theta);
            const ly    = r * Math.sin(theta);
            pts.push({
                x:  cx + lx * cosR - ly * sinR,
                y:  cy - (lx * sinR + ly * cosR)   // y-flip for screen
            });
        }
        return pts;
    }

    /**
     * Draw filled and/or stroked shape from an array of {x,y} screen points.
     */
    _drawShape(screenPts) {
        if (screenPts.length < 2) return;

        // Fill pass
        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
        if (this.strokeColor && this.strokeColor.col && this.thickness > 0) {
            noFill();
            stroke(this.strokeColor.col);
            strokeWeight(this.thickness);
            beginShape();
            for (const sp of screenPts) vertex(sp.x, sp.y);
            endShape(CLOSE);
        }

        // Reset drawing state
        noStroke();
        noFill();
        strokeWeight(1);
    }

    // — Public drawing API —————————————————————————————————————————————————————————

    /**
     * Draw in raw pixel space (center.x/y are pixel coordinates, y increases downward).
     */
    draw() {
        const screenPts = this._buildScreenPtsDirect();
        this._drawShape(screenPts);
        if (this.center && this.center.shouldShow !== false && this.center.draw) {
            this.center.draw(this.strokeColor);
        }
    }

    /**
     * Draw mapped through the given SWGrid (center.x/y are user/grid coordinates).
     */
    drawOnGrid(grid) {
        const screenPts = this._buildScreenPtsGrid(grid);
        this._drawShape(screenPts);
        if (this.center && this.center.shouldShow !== false && this.center.drawOnGrid) {
            this.center.drawOnGrid(grid, this.strokeColor);
        }
    }

    // — Animation ———————————————————————————————————————————————————————————————————

    /** Accumulate a CCW rotation (degrees). */
    rotate(deltaAngle) { this.rotation += deltaAngle; }

    // — Setters ———————————————————————————————————————————————————————————————————————————————

    setRadius(r)         { this.radius    = Math.max(0.01, r); }
    setAmplitude(a)      { this.amplitude = a; }
    setFrequency(f)      { this.frequency = Math.max(0, f); }
    setPower(p)          { this.power     = Math.max(1, Math.round(Math.abs(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; }
    setNumRevolutions(n) { this.numRevolutions = Math.max(1, Math.round(n)); }

    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 —————————————————————————————————————————————————————————————————————————————————————

    reset() {
        this.radius         = this.originalRadius;
        this.amplitude      = this.originalAmplitude;
        this.frequency      = this.originalFrequency;
        this.power          = this.originalPower;
        this.fillColor      = this.originalFillColor   ? SWColor.copy(this.originalFillColor)   : undefined;
        this.strokeColor    = this.originalStrokeColor ? SWColor.copy(this.originalStrokeColor) : undefined;
        this.thickness      = this.originalThickness;
        this.rotationDeg    = this.originalRotationDeg;
        this.rotation       = 0;
        this.numRevolutions = this.originalNumRevolutions;
    }

    // — Static copy —————————————————————————————————————————————————————————————————————————————————

    static copy(other) {
        if (!(other instanceof SWBumpyCircle)) {
            throw new Error('Argument to SWBumpyCircle.copy must be an SWBumpyCircle instance.');
        }
        const c = new SWBumpyCircle(
            SWPoint.copy(other.center),
            other.radius,
            other.amplitude,
            other.frequency,
            other.power,
            other.fillColor   ? SWColor.copy(other.fillColor)   : undefined,
            other.strokeColor ? SWColor.copy(other.strokeColor) : undefined,
            other.thickness,
            other.rotationDeg,
            other.numRevolutions
        );
        c.rotation = other.rotation;
        return c;
    }

    // — Utility —————————————————————————————————————————————————————————————————————————————————————

    toString() {
        return `SWBumpyCircle(center:(${this.center.x.toFixed(2)}, ${this.center.y.toFixed(2)}), ` +
               `radius:${this.radius.toFixed(2)}, amplitude:${this.amplitude.toFixed(2)}, ` +
               `frequency:${this.frequency}, power:${this.power}, ` +
               `rotation:${(this.rotationDeg + this.rotation).toFixed(1)}°, ` +
               `numRevolutions:${this.numRevolutions})`;
    }

}//end class SWBumpyCircle
↑ Top