SWClover Reference
Epicycloid Curves — Parametric Class — SketchWaveJS
☘ Epicycloid Mathematics
What is an Epicycloid?
An epicycloid is the curve traced by a point on the rim of a small circle of radius a rolling without slipping around the outside of a fixed circle of radius R. The ratio R/a determines the number of cusps (petals) in the resulting curve. SWClover exposes this via the numPetals API parameter.
SWClover Parametric Equations
Internally, SWClover uses n = numPetals + 1 and a = radius / numPetals so that each petal tip lies exactly radius grid units from the center:
a = radius / numPetals
x(t) = a · (n · cos(t) − cos(n · t))
y(t) = a · (n · sin(t) − sin(n · t))
t ∈ [0, 2π), 500 sample points
The numPetals cusps (petal tips) occur at t = 2πk / numPetals for k = 0, 1, …, numPetals − 1. At each cusp, both parametric derivatives are simultaneously zero, producing the sharp pointed tips characteristic of the clover shape.
Scale Derivation
Setting a = radius / numPetals is key: at the cusp at t = 0, the rightmost tip is at x = a(n−1) = a ċ numPetals = radius, so all tips land exactly at the specified radius distance from the center regardless of petal count.
Special Values of numPetals
| numPetals | n (internal) | Classic Name | Shape Description |
|---|---|---|---|
| 2 | 3 | Nephroid | 2-cusp kidney / figure-8 shape; first described by Huygens (1678) |
| 3 | 4 | 3-cusp epicycloid | Three-lobed club ♣ shape; resembles a playing card suit |
| 4 | 5 | 4-cusp epicycloid | Four-leaf clover ☘ (default); classic good-luck symbol |
| 5 | 6 | 5-cusp epicycloid | Five-petal rosette; star-like |
| 6 | 7 | 6-cusp epicycloid | Six-petal flower; snowflake-like symmetry |
| 7–8 | 8–9 | — | Dense multi-petal rosettes; overlapping petal geometry |
Historical Note
Epicycloids have a rich history in mathematics and engineering. The nephroid (numPetals = 2) was studied by Christian Huygens (1678) and Tschirnhaus (1690); it also appears as the caustic curve of light reflected in a coffee cup. Ole Rømer (c. 1674) used epicycloidal gear profiles to reduce friction, a design still used today. The Spirograph toy (patented 1965) draws hypotrochoids and epitrochoids — relatives of the epicycloid. Epicycloids appear in the Ptolemaic system of planetary motion, where planets were believed to move on small circles (epicycles) rolling around larger deferents.
Constructor
new SWClover(center, radius, numPetals, fillColor, strokeColor, thickness, rotationDeg)
| Parameter | Type | Default | Description |
|---|---|---|---|
center | SWPoint | required | Center position in user (grid) coordinates |
radius | number | 5 | Distance from center to each petal tip, in grid units (min 0.01) |
numPetals | integer | 4 | Number of petals/cusps (min 2); fractional values are rounded |
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 in CCW degrees; preserved across reset() |
// Basic 4-leaf clover const center = new SWPoint(0, 0, undefined, 8, new SWColor(120, 60, 25, 100)); const fill = SWColor.fromHex('#2d6a2d', 30, 'fill'); const stroke = SWColor.fromHex('#1a4a1a', 100, 'stroke'); const clover = new SWClover(center, 5, 4, fill, stroke, 2, 0); clover.drawOnGrid(grid);
Properties
Live Properties
| Property | Type | Description |
|---|---|---|
center | SWPoint | Center position. Drag to reposition; set center.shouldShow = false to hide the dot |
radius | number | Petal-tip radius in grid units |
numPetals | integer | Number of petals/cusps (≥ 2) |
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() |
rotation | number | Accumulated spin rotation (degrees); incremented by rotate(); cleared by reset() |
Static Property
| Property | Value | Description |
|---|---|---|
SWClover.SAMPLE_COUNT | 500 | Number of parametric sample points per full t ∈ [0, 2π) revolution |
Methods
Drawing
| Method | Description |
|---|---|
draw() |
Draws in raw pixel (screen) coordinates. Use center.x/y as pixel offsets. Prefer drawOnGrid() for standard use. |
drawOnGrid(grid) |
Draws mapped through the given SWGrid. Handles the math-space ↔ screen y-flip automatically. 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: clover.rotate(speed * deltaT) |
Setters
| Method | Parameter | Description |
|---|---|---|
setRadius(r) | number ≥ 0.01 | Sets petal-tip radius. Use during Breathe animation. |
setNumPetals(n) | integer ≥ 2 | Sets petal count; fractional values are rounded. |
setRotation(deg) | number (degrees) | Sets static base rotation (CCW). Does not clear accumulated rotation. |
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, numPetals, rotationDeg, colors, and thickness to constructor values. Clears accumulated spin rotation. Does not move center. |
SWClover.copy(other) (static) |
Returns a deep copy of other (including center SWPoint, colors, and accumulated rotation). |
toString() |
Returns a human-readable string: SWClover(center:(x, y), radius:r, numPetals:n, rotation:θ°) |
Code Examples
1. Basic Setup (p5.js global mode)
let grid, clover; 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, undefined, 8, new SWColor(120, 60, 25, 100)); const fill = SWColor.fromHex('#2d6a2d', 30, 'fill'); const stroke = SWColor.fromHex('#1a4a1a', 100, 'stroke'); clover = new SWClover(center, 5, 4, fill, stroke, 2, 0); } function draw() { background(0, 0, 93); grid.draw(); clover.drawOnGrid(grid); grid.updateScreenBounds(); }
2. Spin Animation
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(); clover.rotate(SPIN_SPEED * deltaT); // CCW spin clover.drawOnGrid(grid); grid.updateScreenBounds(); }
3. Breathe Animation (Radius Oscillation)
const BASE_RADIUS = 5.0; const BREATHE_SPEED = 0.5; // Hz const BREATHE_AMOUNT = 1.5; // grid units function draw() { const t = millis() / 1000; background(0, 0, 93); grid.draw(); // Sinusoidal radius oscillation const r = BASE_RADIUS + BREATHE_AMOUNT * Math.sin(2 * Math.PI * BREATHE_SPEED * t); clover.setRadius(Math.max(0.5, r)); clover.drawOnGrid(grid); grid.updateScreenBounds(); }
4. Club Shape (numPetals = 3)
// Three-lobed club — ♣ playing card suit shape const center = new SWPoint(0, 0, undefined, 8, new SWColor(0, 0, 10, 100)); const fill = new SWColor(0, 0, 15, 80); // dark gray fill const stroke = new SWColor(0, 0, 5, 100); // near-black stroke const club = new SWClover(center, 5, 3, fill, stroke, 2, 0);
5. Copy and Reset
// Deep copy const copy = SWClover.copy(clover); // Reset to constructor values clover.reset(); // Center position is preserved; radius, numPetals, rotation are restored // toString console.log(clover.toString()); // → SWClover(center:(0.00, 0.00), radius:5.00, numPetals:4, rotation:0.0°)
Source Code
Show / Hide swClover.js source
/*
File: swClover.js
Date: 2026-04-28
Author: klp
App: SketchWaveTNT2026-04-21-Stg8
Purpose: SWClover class for SketchWaveJS
SWClover draws a clover / multi-petal epicycloid defined by the parametric equations:
x(t) = a · (n · cos(t) − cos(n · t))
y(t) = a · (n · sin(t) − sin(n · t))
for t ∈ [0, 2π), where:
n = numPetals + 1 (internal epicycloid parameter)
a = radius / numPetals (scale so each petal tip is exactly `radius` from center)
The API parameter numPetals directly controls the number of visible petals / cusps:
numPetals = 2 → nephroid (2 cusps, kidney-bean shape)
numPetals = 3 → 3-petal club (♣ card suit shape)
numPetals = 4 → 4-leaf clover (the default)
numPetals = 5 → 5-petal rosette
...
Mathematical note: An epicycloid traced by a small circle of radius a rolling around
the outside of a fixed circle of radius R has n = R/a + 1 cusps. Setting n = numPetals + 1
and a = radius / numPetals ensures each cusp (petal tip) lands exactly `radius` units
from the center.
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.
SWClover handles the y-flip internally; always pass CCW degrees.
Dependencies: p5.js, SWColor, SWPoint, SWGrid.
*/
console.log("[swClover.js] SWClover class loaded.");
class SWClover {
static SAMPLE_COUNT = 500; // sample points around the full t ∈ [0, 2π) curve
/**
* @param {SWPoint} center - Center in user (grid) coordinates
* @param {number} [radius=5] - Distance from center to each petal tip (grid units)
* @param {number} [numPetals=4] - Number of petals/cusps (integer ≥ 2)
* @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 (CCW degrees)
*/
constructor(center, radius = 5, numPetals = 4,
fillColor = undefined, strokeColor = undefined,
thickness = 2, rotationDeg = 0) {
this.center = center;
this.radius = Math.max(0.01, radius);
this.numPetals = Math.max(2, Math.round(numPetals));
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(); cleared by reset()
// —— Originals for reset() ———————————————————————————————————————————————
this.originalRadius = this.radius;
this.originalNumPetals = this.numPetals;
this.originalFillColor = fillColor ? SWColor.copy(fillColor) : undefined;
this.originalStrokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
this.originalThickness = thickness;
this.originalRotationDeg = rotationDeg;
}//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 for t ∈ [0, 2π).
*
* Epicycloid parametric equations:
* n = numPetals + 1
* a = radius / numPetals
* x(t) = a · (n · cos(t) − cos(n · t))
* y(t) = a · (n · sin(t) − sin(n · t))
*
* @returns {{ x: number, y: number }[]}
*/
_buildUserPts() {
const cx = this.center.x;
const cy = this.center.y;
const N = SWClover.SAMPLE_COUNT;
const n = this.numPetals + 1;
const a = this.radius / this.numPetals;
const pts = [];
for (let i = 0; i < N; i++) {
const t = (i / N) * (2 * Math.PI);
const lx = a * (n * Math.cos(t) - Math.cos(n * t));
const ly = a * (n * Math.sin(t) - Math.sin(n * t));
const rot = this._rotateLocal(lx, ly);
pts.push({ x: cx + rot.x, y: cy + rot.y });
}
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(pt => grid.userToScreen(pt.x, pt.y));
}
/**
* Builds screen-space { x, y } points for draw() (no grid).
* The math-space y-displacement is negated for correct screen-space y (down).
*/
_buildScreenPtsDirect() {
const cx = this.center.x;
const cy = this.center.y;
const N = SWClover.SAMPLE_COUNT;
const n = this.numPetals + 1;
const a = this.radius / this.numPetals;
const pts = [];
for (let i = 0; i < N; i++) {
const t = (i / N) * (2 * Math.PI);
const lx = a * (n * Math.cos(t) - Math.cos(n * t));
const ly = a * (n * Math.sin(t) - Math.sin(n * t));
const rot = this._rotateLocal(lx, ly);
pts.push({ x: cx + rot.x, y: cy - rot.y }); // y-flip for screen coords
}
return pts;
}
/**
* Executes fill + stroke passes from pre-computed screen points.
* The clover is always drawn as a closed shape (CLOSE).
*/
_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) {
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);
}
// —— Drawing ——————————————————————————————————————————————————————
/**
* Draws the clover in raw screen (pixel) coordinates.
* Prefer drawOnGrid() for standard canvas use.
*/
draw() {
const screenPts = this._buildScreenPtsDirect();
this._drawShape(screenPts);
if (this.center && this.center.shouldShow !== false && this.center.draw) {
this.center.draw(this.strokeColor);
}
}//end draw
/**
* Draws the clover 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.shouldShow !== false && this.center.drawOnGrid) {
this.center.drawOnGrid(grid, this.strokeColor);
}
}//end drawOnGrid
// —— Animation ————————————————————————————————————————————————————
/**
* Spins the clover 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 ———————————————————————————————————————————————————————
/** Sets the petal-tip radius (min 0.01). */
setRadius(r) { this.radius = Math.max(0.01, r); }
/** Sets the number of petals (integer, min 2). */
setNumPetals(n) { this.numPetals = Math.max(2, Math.round(n)); }
/** Sets the static base rotation in CCW degrees. Does not affect accumulated rotation. */
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; }
/**
* 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 radius, numPetals, rotation, and colors to the constructor values.
* Clears accumulated spin rotation. Does NOT move the center position.
*/
reset() {
this.radius = this.originalRadius;
this.numPetals = this.originalNumPetals;
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;
}//end reset
/**
* Returns a deep copy of the given SWClover instance.
* @param {SWClover} other
* @returns {SWClover}
*/
static copy(other) {
if (!(other instanceof SWClover)) {
throw new Error('Argument to SWClover.copy must be an SWClover instance');
}
const c = new SWClover(
SWPoint.copy(other.center),
other.radius,
other.numPetals,
other.fillColor ? SWColor.copy(other.fillColor) : undefined,
other.strokeColor ? SWColor.copy(other.strokeColor) : undefined,
other.thickness,
other.rotationDeg
);
c.rotation = other.rotation;
return c;
}//end copy
toString() {
return `SWClover(center:(${this.center.x.toFixed(2)}, ${this.center.y.toFixed(2)}), ` +
`radius:${this.radius.toFixed(2)}, numPetals:${this.numPetals}, ` +
`rotation:${(this.rotationDeg + this.rotation).toFixed(1)}°)`;
}//end toString
}//end class SWClover