Quick Reference
SWHeart is a SketchWave parametric class that represents a heart shape defined by the equations x(t) = a·sin³(t) and y(t) = b·cos(t) + c·cos(2t) + d·cos(3t) + e·cos(4t) for t ∈ [0, 2π]. The curve is naturally closed (t returns to its starting point) and is drawn as a filled and/or stroked polygon sampled at SAMPLE_COUNT = 200 points. SWHeart is not a composite class; all drawing, animation, and styling is handled directly.
- Design Pattern: Parametric curve (not composition or inheritance)
- Equations: x = a·sin³(t), y = b·cos(t) + c·cos(2t) + d·cos(3t) + e·cos(4t)
- Internal Structure: Cartesian coordinates sampled at
SAMPLE_COUNT = 200points around the closed curve - Dependencies: SWPoint, SWColor, SWGrid, p5.js
- Key Features: Five shape coefficients (a–e), scale, fill & stroke colors with independent alpha, spin animation, beat (heartbeat) animation, draggable center
- Common Uses: Valentine art, love-themed graphics, mathematical curve illustration, interactive shape exploration
Overview
The SWHeart class draws a heart shape as a closed polygon. The heart is fully defined by five coefficients, a center point, and a scale factor. In Cartesian coordinates, with the parametric variable t ranging from 0 to 2π:
x(t) = a · sin³(t)
y(t) = b · cos(t) + c · cos(2t) + d · cos(3t) + e · cos(4t)
user_x = center.x + scale · x(t)
user_y = center.y + scale · y(t)
At these values the heart spans ±8 grid units wide and fits neatly within a standard [−10, 10] × [−10, 10] SWGrid.
Orientation: Two lobes sit at the top (positive y in user space), with the sharp tip at the bottom. The parametric origin (0, 0) — mapped to
center — lies at roughly mid-height of the heart.
Fill Semantics
The heart curve is a naturally closed shape (at t = 0 and t = 2π the curve returns to exactly the same point). When a fill color is set, p5.js fills the entire interior of the closed polygon. The heart is drawn in two passes: a fill pass (no stroke) followed by a stroke pass (no fill), so the outline sits precisely on top of the filled region with no color-mixing artifacts.
Rotation
SWHeart supports two layers of rotation, both applied about the center point:
rotationDeg— Static base rotation set bysetRotation(). Persists across frames; survivesreset().rotation— Accumulated rotation incremented byrotate(). Starts at 0; cleared byreset().
Effective rotation = rotationDeg + rotation. All angles are CCW positive (standard math convention). The y-flip for screen coordinates is handled internally.
Beat Animation
The beat animation simulates a heartbeat by sinusoidally oscillating the scale property each frame via setScale(). The formula is:
beatScale = baseScale + depth · sin(2π · speed · t)
At speed = 1.2 Hz this produces approximately 72 BPM — a normal resting heart rate. Beat is implemented entirely in the sketch layer; the SWHeart class only needs setScale() called each frame.
Key Capabilities
- Parametric Heart Curve: Mathematically precise; the five coefficients independently control width, lobe height, cleft depth, tip sharpness, and fine detail
- Scale: Maps parametric coordinates to grid units; easily resizes the heart without changing shape
- Spin Animation: Rotate continuously about the center using
rotate() - Beat Animation: Oscillate
scaleviasetScale()to simulate a heartbeat at any BPM - Fill & Stroke Colors: Independent color pickers and alpha channels for stroke (outline) and fill (interior)
- Draggable Center: Move the entire heart by repositioning the center SWPoint
- Dual Coordinate Systems: Draw in screen pixels (
draw()) or grid user coordinates (drawOnGrid())
Constructor
new SWHeart(center, a, b, c, d, e, fillColor, strokeColor, thickness, scale, rotationDeg)Creates a new SWHeart instance. All constructor values are saved as originals for reset().
| Parameter | Type | Default | Description |
|---|---|---|---|
center |
SWPoint | required | The origin of the heart in user (grid) coordinates. All parametric displacements are added to this point. |
a |
number | 16 | Width coefficient for the sin³(t) term. Controls how wide the heart is; the x-extent is ±a·scale grid units. |
b |
number | 13 | Primary cosine coefficient. Dominates the overall height of the top lobes. |
c |
number | −5 | Second cosine coefficient. Negative values deepen the cleft between the two lobes. |
d |
number | −2 | Third cosine coefficient. Negative values help sharpen the bottom tip. |
e |
number | −1 | Fourth cosine coefficient. Fine-detail sculpting; subtle at default value. |
fillColor |
SWColor | undefined | Fill color for the interior. Pass undefined for no fill (stroke-only outline). |
strokeColor |
SWColor | undefined | Stroke color for the outline. Pass undefined for no stroke (fill-only silhouette). |
thickness |
number | 2 | Stroke weight in pixels. |
scale |
number | 0.5 | Maps parametric coordinates to grid units. Default 0.5 fits the standard heart within a [−10, 10] grid. |
rotationDeg |
number | 0 | Static base rotation in CCW degrees. Survives reset(). |
// Classic red heart — default 'good heart' coefficients
const fill = new SWColor(350, 70, 87, 90, "heartFill"); // #dd2255
const stroke = new SWColor(350, 100, 53, 100, "heartStroke"); // #880022
const center = new SWPoint(0, 0, undefined, 8,
new SWColor(0, 0, 20, 100, "center"));
const heart = new SWHeart(center, 16, 13, -5, -2, -1,
fill, stroke, 2, 0.5, 0);
// Silhouette only — fill, no stroke
const fill = new SWColor(350, 80, 70, 100, "f");
const heart = new SWHeart(center, 16, 13, -5, -2, -1,
fill, undefined, 0, 0.5, 0);
// Using SWColor.fromHex() — useful with color pickers
const fill = SWColor.fromHex('#dd2255', 90, 'heartFill');
const stroke = SWColor.fromHex('#880022', 100, 'heartStroke');
const heart = new SWHeart(center, 16, 13, -5, -2, -1,
fill, stroke, 2, 0.5, 0);
Properties
center SWPointThe heart's origin in user (grid) coordinates. All parametric displacements are added to this point. Moving center.x or center.y repositions the entire heart without rebuilding it.
heart.center.x = 3; heart.center.y = -2;a numberWidth coefficient for the sin³(t) term. Controls the horizontal extent of the heart; the x-range spans from −a·scale to +a·scale grid units. Use setA() to change it.
heart.setA(8); // half-width heart
heart.setA(24); // wider heartb numberPrimary cosine coefficient. Dominates the overall y-range; increasing b raises the top lobes. Use setB() to change it.
heart.setB(20); // taller lobesc numberSecond cosine coefficient. Negative values deepen the cleft between the two lobes; values near zero produce a rounder top. Use setC() to change it.
heart.setC(-12); // deep cleft
heart.setC(0); // round topd numberThird cosine coefficient. Interacts with b and c to shape the tip region. Use setD() to change it.
heart.setD(-5); // sharper lower tipe numberFourth cosine coefficient. Adds fine-detail sculpting. Subtle at the default value of −1. Use setE() to change it.
heart.setE(-3); // more pronounced detailscale numberMaps parametric coordinates to grid units. At scale = 0.5 the default heart spans ±8 units wide, fitting in a [−10, 10] grid. Use setScale() to change it; minimum clamped to 0.01.
heart.setScale(0.3); // smaller
heart.setScale(0.8); // largerrotationDeg numberStatic base rotation in CCW degrees. Set by setRotation(); survives reset(). Added to the accumulated rotation to produce the effective drawing rotation. In the demo, the Rotation (°) slider covers 0–360° and sets this value directly. Useful for orienting the heart before any spin animation begins — Spin accumulates on top of this base angle.
heart.setRotation(45); // tilt 45° CCW
heart.setRotation(180); // flip upside-down
heart.setRotation(0); // restore uprightrotation numberAccumulated rotation in degrees, incremented each frame by rotate(). Starts at 0; cleared by reset().
// Cleared automatically by reset(); read-only in normal useSWHeart.SAMPLE_COUNT staticNumber of polygon sample points around the full t ∈ [0, 2π] curve (default: 200). Higher values produce smoother curves; lower values reveal the polygon facets. Can be set at any time.
SWHeart.SAMPLE_COUNT = 24; // faceted, angular look
SWHeart.SAMPLE_COUNT = 400; // very smooth
Methods
Drawing Methods
draw()Draws the heart in raw screen (pixel) coordinates. The center SWPoint is interpreted as screen pixels. Prefer drawOnGrid() for standard canvas use with an SWGrid.
function draw() {
background(220);
heart.draw();
}
drawOnGrid(grid)Draws the heart mapped through the given SWGrid's coordinate system. This is the standard method; the grid handles converting user-space coordinates to screen pixels and the y-flip (math up → screen down).
function draw() {
background(220);
grid.draw();
heart.drawOnGrid(grid);
}
Rotation Animation
rotate(deltaAngle)Spins the heart about its center by deltaAngle degrees (CCW+, CW−). Accumulates into this.rotation. Call once per frame in draw() before calling drawOnGrid().
// Spin at 45°/second
const SPIN_SPEED = 45;
let prevT = 0;
function draw() {
const t = millis() / 1000;
const deltaT = prevT > 0 ? t - prevT : 0;
prevT = t;
heart.rotate(SPIN_SPEED * deltaT); // call BEFORE drawOnGrid
heart.drawOnGrid(grid);
}
Beat Animation
The beat effect simulates a heartbeat by oscillating scale
sinusoidally using setScale() each frame:
beatScale = baseScale + depth · sin(2π · speed · t)
baseScale is the steady-state value (typically from a slider). depth (Δscale) is the amplitude of oscillation. speed is the frequency in Hz (cycles per second). At 1.2 Hz this is approximately 72 BPM. The value is clamped to a minimum of 0.01 so the heart never fully collapses. Beat and Spin can run simultaneously.
// Heartbeat at 1.2 Hz (≈ 72 BPM)
const BEAT_SPEED = 1.2; // Hz
const BEAT_DEPTH = 0.08; // Δscale
let baseScale = 0.5;
function draw() {
const t = millis() / 1000;
const beatScale = baseScale + BEAT_DEPTH * sin(TWO_PI * BEAT_SPEED * t);
heart.setScale(max(0.01, beatScale));
heart.drawOnGrid(grid);
}
Setter Methods
setA(a) setB(b) setC(c) setD(d) setE(e)Update the individual cosine/cubic coefficients. Take effect immediately on the next draw call.
heart.setA(12); heart.setC(-8);setScale(s)Sets the scale factor (minimum 0.01). Use this each frame for the beat animation.
heart.setScale(0.7);setStrokeColor(sc) setFillColor(fc)Sets the stroke or fill color. Pass an SWColor instance or undefined to remove the color.
heart.setFillColor(SWColor.fromHex('#ff6688', 80, 'f'));
heart.setStrokeColor(undefined); // remove outline
setFillAlpha(alpha) setStrokeAlpha(alpha)Sets the fill or stroke alpha (0–100) and rebuilds the p5 color object. Requires an existing fill/stroke color to be set first.
heart.setFillAlpha(60); // 60% opacity fill
heart.setStrokeAlpha(100); // fully opaque stroke
setStrokeWeight(w)Sets the stroke thickness in pixels.
heart.setStrokeWeight(4);setRotation(deg)Sets the static base rotation in CCW degrees. Does not affect the accumulated rotation.
heart.setRotation(90); // rotates the heart 90° CCWReset & Utility Methods
reset()Restores all animated and slider-driven properties to their original constructor values. Clears accumulated spin rotation. Does not move the center position.
heart.reset(); // back to factory defaultsstatic SWHeart.copy(other)Creates a deep copy of the given SWHeart, preserving all current and original state including rotation accumulation.
const copy = SWHeart.copy(heart);toString()Returns a human-readable string describing the heart's current state.
console.log(heart.toString());
// "SWHeart(center=SWPoint(x:0, y:0), a=16, b=13, c=-5, d=-2, e=-1, scale=0.500, rotationDeg=0.0, rotation=0.0)"
Code Examples
Minimal sketch (heart on a grid)
let grid, heart;
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 fill = SWColor.fromHex('#dd2255', 90, 'heartFill');
const stroke = SWColor.fromHex('#880022', 100, 'heartStroke');
const center = new SWPoint(0, 0);
heart = new SWHeart(center, 16, 13, -5, -2, -1, fill, stroke, 2, 0.5, 0);
}
function draw() {
background(240);
grid.draw();
heart.drawOnGrid(grid);
grid.updateScreenBounds();
}
Spinning heart
let prevT = 0;
const SPIN_SPEED = 60; // degrees per second
function setup() { /* ... create grid and heart ... */ }
function draw() {
const t = millis() / 1000;
const deltaT = prevT > 0 ? t - prevT : 0;
prevT = t;
background(240);
grid.draw();
heart.rotate(SPIN_SPEED * deltaT); // spin BEFORE drawing
heart.drawOnGrid(grid);
grid.updateScreenBounds();
}
Heartbeat animation (beat)
const BEAT_SPEED = 1.2; // Hz ≈ 72 BPM
const BEAT_DEPTH = 0.08; // ±0.08 scale units
const BASE_SCALE = 0.5;
function draw() {
const t = millis() / 1000;
const beatScale = BASE_SCALE + BEAT_DEPTH * sin(TWO_PI * BEAT_SPEED * t);
heart.setScale(max(0.01, beatScale));
background(240);
grid.draw();
heart.drawOnGrid(grid);
grid.updateScreenBounds();
}
Spin + Beat running simultaneously
let prevT = 0;
const SPIN_SPEED = 30;
const BEAT_SPEED = 1.2;
const BEAT_DEPTH = 0.08;
const BASE_SCALE = 0.5;
function draw() {
const t = millis() / 1000;
const deltaT = prevT > 0 ? t - prevT : 0;
prevT = t;
// Spin (rotation accumulates)
heart.rotate(SPIN_SPEED * deltaT);
// Beat (scale oscillates around base)
const beatScale = BASE_SCALE + BEAT_DEPTH * sin(TWO_PI * BEAT_SPEED * t);
heart.setScale(max(0.01, beatScale));
background(240);
grid.draw();
heart.drawOnGrid(grid);
grid.updateScreenBounds();
}
Using a color picker with SWColor.fromHex()
// In your HTML:
// <input type="color" id="fillPicker" value="#dd2255">
// <input type="range" id="alphaSlider" min="0" max="100" value="90">
const picker = document.getElementById('fillPicker');
const alpha = document.getElementById('alphaSlider');
picker.addEventListener('input', () => {
const col = SWColor.fromHex(picker.value, Number(alpha.value), 'heartFill');
heart.setFillColor(col);
});
alpha.addEventListener('input', () => {
const col = SWColor.fromHex(picker.value, Number(alpha.value), 'heartFill');
heart.setFillColor(col);
});
Required script tags (in dependency order)
<script src="https://cdn.jsdelivr.net/npm/p5@1.6.0/lib/p5.js"></script>
<!-- SketchWaveJS classes in dependency order -->
<script src="../shapeClasses/swColor.js"></script>
<script src="../shapeClasses/swPoint.js"></script>
<script src="../shapeClasses/swGrid.js"></script>
<script src="../shapeClasses/swHeart.js"></script>
<!-- Your sketch -->
<script src="../sketches/yourSketch.js"></script>
Design Notes
- Coefficient interplay: The five coefficients are not independent in their visual effect — they all contribute to the y-curve at every point. Start from the recommended defaults (a=16, b=13, c=−5, d=−2, e=−1) and adjust one at a time to understand each coefficient's role. Moving
ctoward zero flattens the cleft; makingblarger amplifies the lobes. - Scale vs. a: Changing
astretches only the x-dimension (since x = a·sin³(t)), creating a narrow or wide heart. Changingscaleuniformly scales both x and y, preserving the shape's proportions. Useato adjust the aspect ratio; usescaleto resize uniformly. - SAMPLE_COUNT controls smoothness: The default of 200 points produces a very smooth curve with no visible faceting. Reducing to 20–30 creates an interesting angular, crystalline look. Unlike the spiral, the heart is a single closed curve so there is no per-revolution factor.
- Fill color semantics: Because the heart is a single closed convex-ish curve, p5.js fills the full interior naturally. Unlike the spiral, there are no layering effects to worry about. A solid fill with 80–100% opacity gives a clean silhouette.
- Rotation is applied in Cartesian space: The rotation is applied to the computed (x, y) displacement from center before mapping to screen coordinates. This correctly rotates the heart about its center in user space.
- reset() does not move the center: This allows the center to be dragged and repositioned interactively without losing the position on reset. The center SWPoint's
strokeWeightcan be set to 0 to hide the center dot. - Rotation slider vs. Spin: The demo's Rotation (°) slider (0–360) sets
rotationDeg— a static base orientation that persists across frames. Spin then accumulates on top of it. At factory reset both return to 0. For a code-only setup, you can pass any CCW angle to the constructor or callsetRotation()at any time; there is no enforced 0–360 limit in the class itself. - Beat vs. Breathe terminology: This class uses "beat" rather than "breathe" to match the heart metaphor. The underlying mechanism is identical: sinusoidal oscillation of a size parameter via a setter called once per frame.
Source Code
The complete SWHeart class implementation:
Show/Hide Source Code
/*
File: swHeart.js
Date: 2026-04-27
Author: klp
App: SketchWaveTNT2026-04-21-Stg8
Purpose: SWHeart class for SketchWaveJS
SWHeart represents a heart shape defined by the parametric equations:
x(t) = a · sin³(t)
y(t) = b · cos(t) + c · cos(2t) + d · cos(3t) + e · cos(4t)
for t ∈ [0, 2π], scaled by 'scale' and offset by 'center':
user_x = center.x + scale · x(t)
user_y = center.y + scale · y(t)
center (SWPoint) — origin of the heart in user (grid) coordinates
a (number) — width coefficient; sin³(t) gives the x-curve its
characteristic smooth lobe shape
b, c, d, e — cosine coefficients that together shape the y-curve;
their interplay produces the two top lobes and the sharp
bottom tip of the heart
scale (number) — maps parametric coordinates to grid units
Suggested 'good heart' defaults:
a=16, b=13, c=−5, d=−2, e=−1, scale=0.5
At these defaults the heart spans approximately ±8 grid units wide and
fits comfortably within a standard [−10, 10] × [−10, 10] SWGrid.
Orientation (at default values):
Two lobes sit at the top (positive y in user space), the sharp tip at
the bottom. The parametric origin (0, 0) lies at roughly mid-height.
Beat animation:
Oscillate 'scale' sinusoidally to simulate a heartbeat:
beatScale = baseScale + depth · sin(2π · speed · t)
At speed = 1.2 Hz this is approximately 72 BPM — a normal resting heart
rate. Implemented in the sketch layer via setScale().
Rotation:
rotationDeg — static base rotation (CCW degrees), set by setRotation().
Persists across frames; survives reset().
rotation — accumulated rotation (degrees), incremented by rotate().
Starts at 0; reset() returns it to 0.
Effective rotation = rotationDeg + rotation. All rotation is CCW positive.
All rotation is applied about the CENTER point.
Angle convention (same as all SketchWaveJS classes):
User space: CCW positive, y increases upward (standard math/Cartesian).
p5 screen: CW positive, y increases downward.
SWHeart handles the y-flip internally; always pass CCW degrees.
Dependencies: p5.js, SWColor, SWPoint, SWGrid.
*/
console.log("[swHeart.js] SWHeart class loaded.");
class SWHeart {
static SAMPLE_COUNT = 200; // sample points around the full t ∈ [0, 2π] curve
/**
* @param {SWPoint} center - Center of the heart in user (grid) coordinates
* @param {number} [a=16] - Width coefficient (sin³ term)
* @param {number} [b=13] - Primary cosine coefficient
* @param {number} [c=-5] - Second cosine coefficient
* @param {number} [d=-2] - Third cosine coefficient
* @param {number} [e=-1] - Fourth cosine coefficient
* @param {SWColor} [fillColor] - Fill color (undefined = no fill)
* @param {SWColor} [strokeColor] - Stroke / border color (undefined = no stroke)
* @param {number} [thickness=2] - Stroke weight in pixels
* @param {number} [scale=0.5] - Maps parametric coords to grid units
* @param {number} [rotationDeg=0] - Static base rotation in CCW degrees
*/
constructor(center, a = 16, b = 13, c = -5, d = -2, e = -1,
fillColor = undefined, strokeColor = undefined,
thickness = 2, scale = 0.5, rotationDeg = 0) {
this.center = center;
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.e = e;
this.fillColor = fillColor ? SWColor.copy(fillColor) : undefined;
this.strokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
this.thickness = thickness;
this.scale = scale;
this.rotationDeg = rotationDeg;
this.rotation = 0; // accumulated via rotate(); cleared by reset()
// ── Originals for reset() ──────────────────────────────────────────────
this.originalA = a;
this.originalB = b;
this.originalC = c;
this.originalD = d;
this.originalE = e;
this.originalFillColor = fillColor ? SWColor.copy(fillColor) : undefined;
this.originalStrokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
this.originalThickness = thickness;
this.originalScale = scale;
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π].
* @returns {{ x: number, y: number }[]}
*/
_buildUserPts() {
const cx = this.center.x;
const cy = this.center.y;
const n = SWHeart.SAMPLE_COUNT;
const pts = [];
for (let i = 0; i <= n; i++) {
const t = (i / n) * 2 * Math.PI;
const sinT = Math.sin(t);
const lx = this.scale * this.a * sinT * sinT * sinT;
const ly = this.scale * (
this.b * Math.cos(t) +
this.c * Math.cos(2 * t) +
this.d * Math.cos(3 * t) +
this.e * Math.cos(4 * 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(p => grid.userToScreen(p.x, p.y));
}
/**
* Builds screen-space { x, y } points for draw() (no grid).
* The math-space y-displacement is negated to produce correct screen-space y (down).
*/
_buildScreenPtsDirect() {
const cx = this.center.x;
const cy = this.center.y;
const n = SWHeart.SAMPLE_COUNT;
const pts = [];
for (let i = 0; i <= n; i++) {
const t = (i / n) * 2 * Math.PI;
const sinT = Math.sin(t);
const lx = this.scale * this.a * sinT * sinT * sinT;
const ly = this.scale * (
this.b * Math.cos(t) +
this.c * Math.cos(2 * t) +
this.d * Math.cos(3 * t) +
this.e * Math.cos(4 * t)
);
const rot = this._rotateLocal(lx, ly);
pts.push({ x: cx + rot.x, y: cy - rot.y }); // y-flip for screen
}
return pts;
}
/**
* Executes fill + stroke passes from pre-computed screen points.
* The heart is always a closed shape.
*/
_drawShape(screenPts) {
// ── 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 heart in raw screen (pixel) coordinates.
* Prefer drawOnGrid() for standard canvas use.
*/
draw() {
const screenPts = this._buildScreenPtsDirect();
this._drawShape(screenPts);
if (this.center && this.center.draw) {
this.center.draw(this.strokeColor);
}
}//end draw
/**
* Draws the heart 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.drawOnGrid) {
this.center.drawOnGrid(grid, this.strokeColor);
}
}//end drawOnGrid
// ── Animation ─────────────────────────────────────────────────────────────
/**
* Spins the heart 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 ───────────────────────────────────────────────────────────────
setA(a) { this.a = a; }
setB(b) { this.b = b; }
setC(c) { this.c = c; }
setD(d) { this.d = d; }
setE(e) { this.e = e; }
setScale(s) { this.scale = Math.max(0.01, s); }
/** 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 all animated/slider-driven properties to original constructor values.
* Clears accumulated spin rotation. Does NOT move the center position.
*/
reset() {
this.a = this.originalA;
this.b = this.originalB;
this.c = this.originalC;
this.d = this.originalD;
this.e = this.originalE;
this.scale = this.originalScale;
this.rotationDeg = this.originalRotationDeg;
this.rotation = 0;
this.thickness = this.originalThickness;
this.fillColor = this.originalFillColor
? SWColor.copy(this.originalFillColor) : undefined;
this.strokeColor = this.originalStrokeColor
? SWColor.copy(this.originalStrokeColor) : undefined;
}//end reset
/**
* Creates a deep copy of the given SWHeart, preserving all current and original state.
* @param {SWHeart} other
* @returns {SWHeart}
*/
static copy(other) {
const c = new SWHeart(
SWPoint.copy(other.center),
other.originalA, other.originalB, other.originalC,
other.originalD, other.originalE,
other.originalFillColor, other.originalStrokeColor,
other.originalThickness, other.originalScale,
other.originalRotationDeg
);
c.a = other.a;
c.b = other.b;
c.c = other.c;
c.d = other.d;
c.e = other.e;
c.scale = other.scale;
c.rotationDeg = other.rotationDeg;
c.rotation = other.rotation;
return c;
}//end copy
toString() {
return `SWHeart(center=${this.center}, a=${this.a}, b=${this.b}, c=${this.c}, ` +
`d=${this.d}, e=${this.e}, scale=${this.scale.toFixed(3)}, ` +
`rotationDeg=${this.rotationDeg.toFixed(1)}, rotation=${this.rotation.toFixed(1)})`;
}
}//end SWHeart class