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:

n = numPetals + 1
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

numPetalsn (internal)Classic NameShape Description
23Nephroid2-cusp kidney / figure-8 shape; first described by Huygens (1678)
343-cusp epicycloidThree-lobed club ♣ shape; resembles a playing card suit
454-cusp epicycloidFour-leaf clover ☘ (default); classic good-luck symbol
565-cusp epicycloidFive-petal rosette; star-like
676-cusp epicycloidSix-petal flower; snowflake-like symmetry
7–88–9Dense 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)
ParameterTypeDefaultDescription
centerSWPointrequiredCenter position in user (grid) coordinates
radiusnumber5Distance from center to each petal tip, in grid units (min 0.01)
numPetalsinteger4Number of petals/cusps (min 2); fractional values are rounded
fillColorSWColorundefinedFill color; undefined = no fill
strokeColorSWColorundefinedStroke color; undefined = no stroke
thicknessnumber2Stroke weight in pixels
rotationDegnumber0Static 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

PropertyTypeDescription
centerSWPointCenter position. Drag to reposition; set center.shouldShow = false to hide the dot
radiusnumberPetal-tip radius in grid units
numPetalsintegerNumber of petals/cusps (≥ 2)
fillColorSWColorFill color (undefined = transparent fill)
strokeColorSWColorStroke color (undefined = no outline)
thicknessnumberStroke weight in pixels
rotationDegnumberStatic base rotation (CCW degrees); set via setRotation()
rotationnumberAccumulated spin rotation (degrees); incremented by rotate(); cleared by reset()

Static Property

PropertyValueDescription
SWClover.SAMPLE_COUNT500Number of parametric sample points per full t ∈ [0, 2π) revolution

Methods

Drawing

MethodDescription
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

MethodParametersDescription
rotate(deltaAngle) deltaAngle: degrees (CCW+, CW−) Accumulates spin rotation. Call once per frame: clover.rotate(speed * deltaT)

Setters

MethodParameterDescription
setRadius(r)number ≥ 0.01Sets petal-tip radius. Use during Breathe animation.
setNumPetals(n)integer ≥ 2Sets petal count; fractional values are rounded.
setRotation(deg)number (degrees)Sets static base rotation (CCW). Does not clear accumulated rotation.
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, 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