☘ 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:
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
| Feature | SWClover | SWShamrock |
|---|---|---|
| Equation type | Epicycloid (Cartesian parametric) | Polar parametric |
| Lobe count | Variable (2–8) | Fixed (3) |
| Lobe tips | Sharp cusps | Soft, rounded |
| Silhouette | Geometric / symmetrical | Organic / slightly ruffled |
| Default rotationDeg | 0° | 30° (one lobe points up) |
| Stem | No | Yes (SWSpire composite) |
| SAMPLE_COUNT | 500 | 600 |
Famous Polar Curves
SWShamrock belongs to a rich family of polar curves studied in mathematics:
| Curve | Equation | Notes |
|---|---|---|
| Rose curves | r = cos(nθ) or r = sin(nθ) | n petals (n odd) or 2n petals (n even) |
| Cardioid | r = 1 − cos(θ) | Heart-shaped; traced by rolling a circle of equal radius |
| Lemniscate of Bernoulli | r² = cos(2θ) | The figure-eight / infinity symbol (∞) |
| Archimedean spiral | r = a + bθ | Equal spacing between turns |
| SWShamrock | r = 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)
| Parameter | Type | Default | Description |
|---|---|---|---|
center | SWPoint | required | Center position in user (grid) coordinates |
radius | number | 5 | Approximate petal-tip distance from center in grid units (min 0.01); SCALE_FACTOR applied internally |
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 | 30 | Static base rotation in CCW degrees; 30° orients one lobe upward; preserved across reset() |
shouldShowStem | boolean | true | Whether to draw the SWSpire stem |
stemLength | number | 6.0 | Stem arch height from center to cusp tip in grid units (min 0.1) |
stemWidth | number | 1.5 | Stem arch half-width at base in grid units (min 0.05) |
stemPower | number | 5 | Spire shape exponent; odd integers give cusped tips (1 = round, 11 = sharp) |
stemRotationOffset | number | 150 | Additional 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
| Property | Type | Description |
|---|---|---|
center | SWPoint | Center position. Drag to reposition in the demo; set center.shouldShow = false to hide the dot |
radius | number | Approximate petal-tip radius in grid units (SCALE_FACTOR applied internally) |
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() |
Stem Properties
| Property | Type | Description |
|---|---|---|
shouldShowStem | boolean | Whether the SWSpire stem is drawn |
stemLength | number | Stem arch height from center to cusp tip (grid units) |
stemWidth | number | Stem arch half-width at base (grid units) |
stemPower | number | Spire shape exponent (1 = round, higher = sharper cusp) |
stemRotationOffset | number | Additional CCW degrees the stem is rotated relative to the body orientation |
Static Properties
| Property | Value | Description |
|---|---|---|
SWShamrock.SAMPLE_COUNT | 600 | Number of sample points per full θ ∈ [0, 2π) revolution |
SWShamrock.SCALE_FACTOR | 1.31 | Empirical scale applied to radius so the visual petal size matches the API value |
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. Draws the stem (if enabled) first, then the body, 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. The stem rotates with the body automatically. Call once per frame: shamrock.rotate(speed * deltaT) |
Body Setters
| Method | Parameter | Description |
|---|---|---|
setRadius(r) | number ≥ 0.01 | Sets 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) | 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. |
Stem Setters
| Method | Parameter | Description |
|---|---|---|
setShouldShowStem(v) | boolean | Shows or hides the SWSpire stem. |
setStemLength(l) | number ≥ 0.1 | Sets stem arch height from center to cusp tip (grid units). |
setStemWidth(w) | number ≥ 0.05 | Sets stem arch half-width at base (grid units). |
setStemPower(p) | number ≥ 1 | Sets 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
| Method | Description |
|---|---|
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