☘ SWShamrock Reference

Polar Curves — Three-Lobed Shamrock — 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 mechanically from x = r(θ)·cos(θ) and y = r(θ)·sin(θ). Because both coordinates share the same r(θ), the curve's shape is entirely determined by how that radius grows and shrinks with angle.

SWShamrock Polar Equations

The shamrock uses a squared sum of two sine terms with half-angle arguments:

SCALE = 1.31  (empirical sizing constant)

r(θ) = SCALE · radius · (sin(3θ/2) + sin(9θ/2)/5)²
x(θ) = r(θ) · cos(θ)
y(θ) = r(θ) · sin(θ)

θ ∈ [0, 2π),  600 sample points

Why Three Lobes?

The dominant term sin(3θ/2) has period 4π/3 and equals zero at θ = 0, 2π/3, 4π/3, and 2π. Because the formula squares the bracketed expression, these zeros become the pinch points — the three spots where the curve passes through the origin, separating the three lobes. The secondary term sin(9θ/2)/5 (amplitude 20% of the dominant term, period 4π/9) adds subtle ruffling and slight asymmetry to each lobe, giving the shamrock its characteristic organic silhouette.

The Squaring Trick

Normally, half-angle arguments like sin(3θ/2) would produce a curve that requires θ ∈ [0, 4π) for a full traversal (because the period is 4π/3 × 3 = 4π). The squaring(sin(3θ/2) + …)² — folds all negative-radius half-cycles back into positive ones, so the three lobes all appear in a single [0, 2π) pass. This is the key that makes the shamrock a true closed curve on the standard interval.

Scaling Constant

The SCALE = 1.31 factor is empirically determined from the original TNTShamrock legacy code. It ensures that the radius API parameter approximates the visual petal-tip distance from the center, despite the squaring non-linearity that would otherwise cause the peak radius to be smaller than radius.

Comparison with SWClover

FeatureSWCloverSWShamrock
Equation typeEpicycloid (Cartesian parametric)Polar parametric
Lobe countVariable (2–8)Fixed (3)
Lobe tipsSharp cuspsSoft, rounded
SilhouetteGeometric / symmetricalOrganic / slightly ruffled
Default rotationDeg30° (one lobe points up)
StemNoYes (SWSpire composite)
SAMPLE_COUNT500600

Famous Polar Curves

SWShamrock belongs to a rich family of polar curves studied in mathematics:

CurveEquationNotes
Rose curvesr = cos(nθ) or r = sin(nθ)n petals (n odd) or 2n petals (n even)
Cardioidr = 1 − cos(θ)Heart-shaped; traced by rolling a circle of equal radius
Lemniscate of Bernoullir² = cos(2θ)The figure-eight / infinity symbol (∞)
Archimedean spiralr = a + bθEqual spacing between turns
SWShamrockr = SCALE·radius·(sin(3θ/2) + sin(9θ/2)/5)²Three organic lobes via half-angle squaring

Constructor

new SWShamrock(center, radius, fillColor, strokeColor, thickness, rotationDeg,
              shouldShowStem, stemLength, stemWidth, stemPower, stemRotationOffset)
ParameterTypeDefaultDescription
centerSWPointrequiredCenter position in user (grid) coordinates
radiusnumber5Approximate petal-tip distance from center in grid units (min 0.01); SCALE_FACTOR applied internally
fillColorSWColorundefinedFill color; undefined = no fill
strokeColorSWColorundefinedStroke color; undefined = no stroke
thicknessnumber2Stroke weight in pixels
rotationDegnumber30Static base rotation in CCW degrees; 30° orients one lobe upward; preserved across reset()
shouldShowStembooleantrueWhether to draw the SWSpire stem
stemLengthnumber6.0Stem arch height from center to cusp tip in grid units (min 0.1)
stemWidthnumber1.5Stem arch half-width at base in grid units (min 0.05)
stemPowernumber5Spire shape exponent; odd integers give cusped tips (1 = round, 11 = sharp)
stemRotationOffsetnumber150Additional CCW rotation of the stem relative to the body in degrees; 0 = directly opposite the top lobe
// Basic shamrock with default green colors
const center = new SWPoint(0, 0, undefined, 8, new SWColor(120, 70, 30, 100));
const fill   = SWColor.fromHex('#1a8c1a', 40, 'fill');
const stroke = SWColor.fromHex('#0a4a0a', 100, 'stroke');
const shamrock = new SWShamrock(center, 5, fill, stroke, 2, 30);
shamrock.drawOnGrid(grid);

Properties

Body Properties

PropertyTypeDescription
centerSWPointCenter position. Drag to reposition in the demo; set center.shouldShow = false to hide the dot
radiusnumberApproximate petal-tip radius in grid units (SCALE_FACTOR applied internally)
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()

Stem Properties

PropertyTypeDescription
shouldShowStembooleanWhether the SWSpire stem is drawn
stemLengthnumberStem arch height from center to cusp tip (grid units)
stemWidthnumberStem arch half-width at base (grid units)
stemPowernumberSpire shape exponent (1 = round, higher = sharper cusp)
stemRotationOffsetnumberAdditional CCW degrees the stem is rotated relative to the body orientation

Static Properties

PropertyValueDescription
SWShamrock.SAMPLE_COUNT600Number of sample points per full θ ∈ [0, 2π) revolution
SWShamrock.SCALE_FACTOR1.31Empirical scale applied to radius so the visual petal size matches the API value

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. Draws the stem (if enabled) first, then the body, then the center dot. Use this in the p5.js draw loop.

Animation

MethodParametersDescription
rotate(deltaAngle) deltaAngle: degrees (CCW+, CW−) Accumulates spin rotation. The stem rotates with the body automatically. Call once per frame: shamrock.rotate(speed * deltaT)

Body Setters

MethodParameterDescription
setRadius(r)number ≥ 0.01Sets petal radius. Use during Breathe animation. Does not auto-scale the stem.
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.

Stem Setters

MethodParameterDescription
setShouldShowStem(v)booleanShows or hides the SWSpire stem.
setStemLength(l)number ≥ 0.1Sets stem arch height from center to cusp tip (grid units).
setStemWidth(w)number ≥ 0.05Sets stem arch half-width at base (grid units).
setStemPower(p)number ≥ 1Sets spire exponent; odd integers recommended (1 = round, 11 = very sharp).
setStemRotationOffset(deg)number (degrees)Sets the stem's additional CCW rotation relative to the body.

Reset & Utility

MethodDescription
reset() Restores radius, rotationDeg, colors, thickness, shouldShowStem, and all four stem parameters to constructor values. Clears accumulated spin rotation. Does not move center.
SWShamrock.copy(other) (static) Returns a deep copy of other (including center SWPoint, colors, accumulated rotation, and all stem parameters).
toString() Returns a human-readable string: SWShamrock(center:(x, y), radius:r, rotation:θ°, stem:true, stemLength:l, …)

Code Examples

1. Basic Setup (p5.js global mode)

let grid, shamrock;

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, 70, 30, 100));
    const fill   = SWColor.fromHex('#1a8c1a', 40, 'fill');
    const stroke = SWColor.fromHex('#0a4a0a', 100, 'stroke');
    shamrock = new SWShamrock(center, 5, fill, stroke, 2, 30);
}

function draw() {
    background(0, 0, 93);
    grid.draw();
    shamrock.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();
    shamrock.rotate(SPIN_SPEED * deltaT);  // stem rotates with the body
    shamrock.drawOnGrid(grid);
    grid.updateScreenBounds();
}

3. Breathe Animation (Radius Oscillation) with Proportional Stem

const BASE_RADIUS    = 5.0;
const BASE_STEM_LEN  = 6.0;
const BASE_STEM_WID  = 1.5;
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();

    // Oscillate radius sinusoidally
    const r     = Math.max(0.5, BASE_RADIUS + BREATHE_AMOUNT * Math.sin(2 * Math.PI * BREATHE_SPEED * t));
    const scale = r / BASE_RADIUS;
    shamrock.setRadius(r);
    shamrock.setStemLength(BASE_STEM_LEN * scale);  // keep stem proportional
    shamrock.setStemWidth(BASE_STEM_WID  * scale);

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

4. Shamrock Without a Stem

// Pass shouldShowStem = false, or call setShouldShowStem(false) later
const shamrock = new SWShamrock(
    center, 5, fill, stroke, 2, 30,
    false   // shouldShowStem
);

// Toggle at any time:
shamrock.setShouldShowStem(true);
shamrock.setShouldShowStem(false);

5. Custom Stem Geometry

// Long, narrow, very sharp stem pointing straight down (offset = 0°)
const shamrock = new SWShamrock(
    center,          // center
    5,               // radius
    fill, stroke, 2, // colors, thickness
    30,              // rotationDeg
    true,            // shouldShowStem
    8.0,             // stemLength (long)
    0.8,             // stemWidth (narrow)
    9,               // stemPower (very sharp)
    0                // stemRotationOffset (straight down)
);

// Adjust stem interactively:
shamrock.setStemLength(4);
shamrock.setStemWidth(2);
shamrock.setStemPower(1);           // round tip
shamrock.setStemRotationOffset(90); // point stem to the right

6. Copy and Reset

// Deep copy — all body and stem parameters included
const copy = SWShamrock.copy(shamrock);

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

// toString
console.log(shamrock.toString());
// → SWShamrock(center:(0.00, 0.00), radius:5.00, rotation:30.0°,
//              stem:true, stemLength:6.00, stemWidth:1.50, stemPower:5, stemOffset:150.0°)

Source Code

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

SWShamrock draws a three-lobed shamrock shape using a polar parametric equation
derived from the original TNTShamrock legacy code:

  r(θ) = SCALE · radius · (sin(3θ/2) + sin(9θ/2)/5)²
  x(θ) = r(θ) · cos(θ)
  y(θ) = r(θ) · sin(θ)

for θ ∈ [0, 2π), where SCALE = 1.31 (empirically determined so that the
nominal `radius` parameter approximates the visual petal-tip distance from center).

The curve passes through the origin at θ = 0, 2π/3, 4π/3, and 2π, tracing
three rounded lobes — the classic shamrock / three-leaf clover shape ☘.

The polar formula can be understood as:
  The dominant term sin(3θ/2) has period 4π/3, producing three zero crossings
  in [0, 2π] that separate the three lobes.
  The secondary term sin(9θ/2)/5 (period 4π/9) adds subtle asymmetric ruffling
  to each lobe, creating the characteristic shamrock silhouette rather than a
  smooth epicycloid petal.

Rotation:
  rotationDeg -- static base rotation (CCW degrees); default = 30 orients
                 one lobe pointing upward.  Persists across reset().
  rotation    -- accumulated rotation (degrees); incremented by rotate().
                 Starts at 0; reset() returns it to 0.
  Effective rotation = rotationDeg + rotation.

Stem:
  When shouldShowStem is true, a SWSpire arch stem is drawn below the figure.
  The SWSpire uses halfShape=true so only the top arch (t∈[0,π]) is drawn:
  the cusp apex is anchored at the shamrock center; the arch body extends
  away in the stem direction. The stem color and thickness match the body.
  Parameters:
    stemLength  – height of the arch from center to cusp tip (grid units)
    stemWidth   – half-width of the arch at the flat base (grid units)
    stemPower   – shape exponent; odd integers give cusped tips (5 = default)
    stemRotationOffset – additional CCW rotation of the stem relative to the
                         shamrock body (degrees)

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

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

console.log("[swShamrock.js] SWShamrock class loaded.");

class SWShamrock {

    static SAMPLE_COUNT = 600;
    static SCALE_FACTOR = 1.31;

    constructor(center, radius = 5,
                fillColor = undefined, strokeColor = undefined,
                thickness = 2, rotationDeg = 30, shouldShowStem = true,
                stemLength = 6.0, stemWidth = 1.5, stemPower = 5,
                stemRotationOffset = 150) {

        this.center         = center;
        this.radius         = Math.max(0.01, radius);
        this.fillColor      = fillColor   ? SWColor.copy(fillColor)   : undefined;
        this.strokeColor    = strokeColor ? SWColor.copy(strokeColor) : undefined;
        this.thickness      = thickness;
        this.rotationDeg    = rotationDeg;
        this.rotation       = 0;
        this.shouldShowStem = shouldShowStem;

        this.stemLength         = Math.max(0.1,  stemLength);
        this.stemWidth          = Math.max(0.05, stemWidth);
        this.stemPower          = Math.max(1,    stemPower);
        this.stemRotationOffset = stemRotationOffset;

        this.originalRadius         = this.radius;
        this.originalFillColor      = fillColor   ? SWColor.copy(fillColor)   : undefined;
        this.originalStrokeColor    = strokeColor ? SWColor.copy(strokeColor) : undefined;
        this.originalThickness      = thickness;
        this.originalRotationDeg    = rotationDeg;
        this.originalShouldShowStem = shouldShowStem;
        this.originalStemLength         = this.stemLength;
        this.originalStemWidth          = this.stemWidth;
        this.originalStemPower          = this.stemPower;
        this.originalStemRotationOffset = this.stemRotationOffset;

        const stemCenter = new SWPoint(center.x, center.y);
        stemCenter.shouldShow = false;
        this._stem = new SWSpire(
            stemCenter,
            this.stemWidth, this.stemLength, this.stemPower,
            fillColor   ? SWColor.copy(fillColor)   : undefined,
            strokeColor ? SWColor.copy(strokeColor) : undefined,
            thickness,
            rotationDeg + 180 + stemRotationOffset,
            true
        );
    }

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

    _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 };
    }

    _buildUserPts() {
        const cx = this.center.x;
        const cy = this.center.y;
        const N  = SWShamrock.SAMPLE_COUNT;
        const pR = SWShamrock.SCALE_FACTOR * this.radius;
        const pts = [];
        for (let i = 0; i < N; i++) {
            const t  = (i / N) * (2 * Math.PI);
            const s1 = Math.sin(3 * t / 2);
            const s2 = Math.sin(9 * t / 2) / 5;
            const r  = pR * (s1 + s2) * (s1 + s2);
            const lx = r * Math.cos(t);
            const ly = r * Math.sin(t);
            const rot = this._rotateLocal(lx, ly);
            pts.push({ x: cx + rot.x, y: cy + rot.y });
        }
        return pts;
    }

    _buildScreenPtsGrid(grid) {
        return this._buildUserPts().map(pt => grid.userToScreen(pt.x, pt.y));
    }

    _buildScreenPtsDirect() {
        const cx = this.center.x;
        const cy = this.center.y;
        const N  = SWShamrock.SAMPLE_COUNT;
        const pR = SWShamrock.SCALE_FACTOR * this.radius;
        const pts = [];
        for (let i = 0; i < N; i++) {
            const t  = (i / N) * (2 * Math.PI);
            const s1 = Math.sin(3 * t / 2);
            const s2 = Math.sin(9 * t / 2) / 5;
            const r  = pR * (s1 + s2) * (s1 + s2);
            const lx = r * Math.cos(t);
            const ly = r * Math.sin(t);
            const rot = this._rotateLocal(lx, ly);
            pts.push({ x: cx + rot.x, y: cy - rot.y });
        }
        return pts;
    }

    _drawShape(screenPts) {
        if (screenPts.length < 2) return;
        if (this.fillColor && this.fillColor.col) {
            fill(this.fillColor.col);
            noStroke();
            beginShape();
            for (const sp of screenPts) vertex(sp.x, sp.y);
            endShape(CLOSE);
        }
        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);
    }

    _syncStem() {
        const totalRot     = this._totalRotDeg();
        const effectiveRot = totalRot + 180 + this.stemRotationOffset;
        const effectiveRad = effectiveRot * Math.PI / 180;
        this._stem.center.x    = this.center.x + this.stemLength * Math.sin(effectiveRad);
        this._stem.center.y    = this.center.y - this.stemLength * Math.cos(effectiveRad);
        this._stem.radiusX     = this.stemWidth;
        this._stem.radiusY     = this.stemLength;
        this._stem.power       = this.stemPower;
        this._stem.rotationDeg = effectiveRot;
        this._stem.rotation    = 0;
        this._stem.halfShape   = true;
        this._stem.fillColor   = this.fillColor;
        this._stem.strokeColor = this.strokeColor;
        this._stem.thickness   = this.thickness;
    }

    draw() {
        if (this.shouldShowStem) { this._syncStem(); this._stem.draw(); }
        const screenPts = this._buildScreenPtsDirect();
        this._drawShape(screenPts);
        if (this.center && this.center.shouldShow !== false && this.center.draw) {
            this.center.draw(this.strokeColor);
        }
    }

    drawOnGrid(grid) {
        if (this.shouldShowStem) { this._syncStem(); this._stem.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);
        }
    }

    rotate(deltaAngle) { this.rotation += deltaAngle; }

    setRadius(r)               { this.radius               = Math.max(0.01, r); }
    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; }
    setShouldShowStem(v)       { this.shouldShowStem        = !!v; }
    setStemLength(l)           { this.stemLength            = Math.max(0.1,  l); }
    setStemWidth(w)            { this.stemWidth             = Math.max(0.05, w); }
    setStemPower(p)            { this.stemPower             = Math.max(1,    p); }
    setStemRotationOffset(deg) { this.stemRotationOffset    = deg; }

    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() {
        this.radius         = this.originalRadius;
        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.shouldShowStem         = this.originalShouldShowStem;
        this.stemLength             = this.originalStemLength;
        this.stemWidth              = this.originalStemWidth;
        this.stemPower              = this.originalStemPower;
        this.stemRotationOffset     = this.originalStemRotationOffset;
    }

    static copy(other) {
        if (!(other instanceof SWShamrock)) {
            throw new Error('Argument to SWShamrock.copy must be an SWShamrock instance');
        }
        const c = new SWShamrock(
            SWPoint.copy(other.center),
            other.radius,
            other.fillColor   ? SWColor.copy(other.fillColor)   : undefined,
            other.strokeColor ? SWColor.copy(other.strokeColor) : undefined,
            other.thickness,
            other.rotationDeg,
            other.shouldShowStem,
            other.stemLength,
            other.stemWidth,
            other.stemPower,
            other.stemRotationOffset
        );
        c.rotation = other.rotation;
        return c;
    }

    toString() {
        return `SWShamrock(center:(${this.center.x.toFixed(2)}, ${this.center.y.toFixed(2)}), ` +
               `radius:${this.radius.toFixed(2)}, rotation:${(this.rotationDeg + this.rotation).toFixed(1)}°, ` +
               `stem:${this.shouldShowStem}, stemLength:${this.stemLength.toFixed(2)}, ` +
               `stemWidth:${this.stemWidth.toFixed(2)}, stemPower:${this.stemPower}, ` +
               `stemOffset:${this.stemRotationOffset.toFixed(1)}°)`;
    }

}//end class SWShamrock
↑ Top