◯ 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:
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
| Parameter | Role in the Equation | Visual 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 value | Effect on negative sin values | Result |
|---|---|---|
| 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:
| Curve | Equation | Notes |
|---|---|---|
| Circle | r = c (constant) | SWBumpyCircle with amplitude = 0 |
| Rose curves | r = cos(nθ) or r = sin(nθ) | n petals (n odd) or 2n petals (n even) |
| Limaçon | r = b + a·cos(θ) | Cardioid when a = b; similar to SWBumpyCircle with frequency = 1 |
| Cardioid | r = 1 − cos(θ) | Heart-shaped; traced by a point on a rolling circle |
| Archimedean spiral | r = a + bθ | Equal spacing between turns (not a closed curve) |
| SWBumpyCircle | r = radius + amplitude · sin(fθ)p | Generalized bumpy circle; closes for integer f |
| SWShamrock | r = SCALE · radius · (sin(3θ/2) + sin(9θ/2)/5)² | Three organic lobes via half-angle squaring |
SWBumpyCircle vs. SWShamrock
| Feature | SWBumpyCircle | SWShamrock |
|---|---|---|
| Equation type | Additive perturbation of circle | Squared sum of half-angle sines |
| Bump count | Controlled by frequency (1–20+) | Fixed at 3 lobes |
| Bump direction | Outward + inward (odd power) or all outward (even power) | Always outward (squaring) |
| Silhouette | Geometric; perfectly regular with integer frequency | Organic, slightly asymmetric |
| SAMPLE_COUNT | 1000 per revolution | 600 |
| Stem | No | Yes (SWSpire composite) |
| Amplitude control | Yes — separate parameter | Fixed by squaring non-linearity |
Constructor
new SWBumpyCircle(center, radius, amplitude, frequency, power, fillColor, strokeColor, thickness, rotationDeg, numRevolutions)
| Parameter | Type | Default | Description |
|---|---|---|---|
center | SWPoint | required | Center position in user (grid) coordinates. The center dot is displayed by default; set center.shouldShow = false to hide it. |
radius | number | 5 | Base circle radius in grid units (min 0.01). The curve oscillates around this value. |
amplitude | number | 1.5 | Peak height of bumps above (and depth of dips below) the base circle. 0 = plain circle. |
frequency | number | 6 | Number of full sine cycles per revolution. Integer values always produce closed curves in one revolution. |
power | number | 1 | Exponent on sin term. Odd = bumps and dents; even = all-outward bumps. Clamped to positive integer ≥ 1. |
fillColor | SWColor | undefined | Fill color; undefined = no fill. Deep-copied internally. |
strokeColor | SWColor | undefined | Stroke color; undefined = no stroke. Deep-copied internally. |
thickness | number | 2 | Stroke weight in pixels. |
rotationDeg | number | 0 | Static base rotation in CCW degrees. Preserved across reset(). |
numRevolutions | number | 1 | Full θ 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
| Property | Type | Description |
|---|---|---|
center | SWPoint | Center position in user (grid) coordinates. Drag to reposition in the demo; set center.shouldShow = false to hide the dot. |
radius | number | Base circle radius (grid units). The sinusoidal perturbation oscillates around this value. |
amplitude | number | Bump peak height in grid units above (or below for odd power) the base circle. 0 = plain circle. |
frequency | number | Sine cycles per revolution. Integer values produce closed curves with numRevolutions = 1. |
power | number | Exponent on the sin term. Odd = bumps + dents; even = all-outward bumps only. |
fillColor | SWColor | Fill color (undefined = transparent fill). |
strokeColor | SWColor | Stroke color (undefined = no outline). |
thickness | number | Stroke weight in pixels. |
rotationDeg | number | Static base rotation (CCW degrees). Set via setRotation(). Preserved across reset(). |
rotation | number | Accumulated spin rotation (degrees). Incremented by rotate(). Cleared to 0 by reset(). |
numRevolutions | number | Full θ revolutions traced per draw call. Read-only after construction; use setNumRevolutions() to change. |
Static Properties
| Property | Value | Description |
|---|---|---|
SWBumpyCircle.SAMPLE_COUNT | 1000 | Sample points per revolution of θ. Total points = SAMPLE_COUNT × numRevolutions. |
Methods
Drawing
| Method | Description |
|---|---|
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
| Method | Parameters | Description |
|---|---|---|
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
| Method | Parameter | Description |
|---|---|---|
setRadius(r) | number ≥ 0.01 | Sets base circle radius. Use during Breathe (radius) animation. |
setAmplitude(a) | number ≥ 0 | Sets bump amplitude. Use during Breathe (amplitude) animation. 0 = plain circle. |
setFrequency(f) | number ≥ 0 | Sets bump frequency. 0 = plain circle (amplitude is ignored). |
setPower(p) | number ≥ 1 | Sets sin exponent. Clamped to positive integer. |
setRotation(deg) | number (degrees) | Sets static base rotation (CCW). Does not clear accumulated rotation. |
setNumRevolutions(n) | integer ≥ 1 | Sets the revolution count (for fractional frequency closure). |
setFillColor(fc) | SWColor | Replaces fill color (deep copy stored). |
setStrokeColor(sc) | SWColor | Replaces stroke color (deep copy stored). |
setStrokeWeight(w) | number | Sets stroke thickness in pixels. |
setFillAlpha(alpha) | 0–100 | Updates fill opacity and rebuilds the p5 color object. |
setStrokeAlpha(alpha) | 0–100 | Updates stroke opacity and rebuilds the p5 color object. |
Reset & Utility
| Method | Description |
|---|---|
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