💧 SketchWave TearDrop Class Reference

Parametric Teardrop • Breathing • Centre & Tip Rotation • Grid Support

▶ Try SWTearDrop Demo

Overview

SWTearDrop is a SketchWave shape class that draws a smooth parametric teardrop (water-drop) curve on a p5.js canvas. The shape is defined by two parametric equations with a single curve-power parameter that controls tip sharpness:

x(t) = b · sin(t) · sinm(t / 2)
y(t) = −a · cos(t)     t ∈ [0, 2π]

The tip sits at the top at (0, −a) when t = 0. The bottom of the bulge is at (0, +a) when t = π. The widest point of the shape occurs near t ≈ 109°, about one-third of the way down from the bounding-box centre.

m = 0

sin0(t/2) = 1 everywhere → plain ellipse

m = 1

Classic teardrop — smooth pointed tip

m = 2

Elongated drop with sharper tip

m ≥ 3

Needle-like tip — bulge becomes narrower

Features

  • Fill & stroke — independent SWColor objects; pass null to suppress either
  • Curve powerm parameter shapes tip sharpness continuously from 0 (ellipse) upward
  • SmoothnessnumPoints controls vertex count; more vertices = smoother outline
  • Breathingbreathe(sinusoid, t) scales both a and b proportionally via SWSinusoid, preserving the shape’s aspect ratio
  • Rotation (centre pivot) — the whole shape spins in place; the tip traces a circle around the bounding-box centre
  • Rotation (tip pivot) — the tip stays fixed; the bulge swings around it like a pendulum
  • DragsetCenter(cx, cy) lets you move the shape to any canvas position
  • Grid supportdrawOnGrid(grid) maps coordinates through a SWGrid for user-space drawings

Constructor

let tear = new SWTearDrop(cx, cy, a, b, fillColor, options);

Required Parameters

ParameterTypeDescription
cx number Centre x — pixels when using draw(), user units when using drawOnGrid(). The centre is the midpoint between the tip and the bottom of the bulge.
cy number Centre y — same coordinate system as cx.
a number (≥ 1) Half-height: distance from the centre to the tip (top) and to the bottom of the bulge. The full height of the bounding box is 2a.
b number (≥ 1) Width parameter: maximum half-width ≈ 0.77 × b (for m = 1). Scales the horizontal extent of the bulge.
fillColor SWColor | null Fill colour of the teardrop. Pass null for no fill (outline only).

Options Object

Pass any combination of these as a plain JS object in the sixth argument:

KeyTypeDefaultDescription
strokeColor SWColor | null null Outline colour. Omit or pass null for no stroke.
strokeWeight number 2 Stroke width in pixels. Ignored when strokeColor is null.
m number (≥ 0) 1.0 Curve-power exponent. 0 = ellipse, 1 = classic teardrop, 2+ = sharper tip.
numPoints number (≥ 3) 120 Number of polygon vertices for the outline. Higher values give a smoother curve. 3 gives a triangle.
rotOrigin 'center' | 'tip' 'center' Rotation pivot. 'center': shape spins in place. 'tip': tip stays fixed, bulge swings around it.

Constructor Example

let tear;

function setup() {
    createCanvas(640, 480);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();

    const fillC   = new SWColor(210, 80, 100, 100, "tearFill");
    const strokeC = new SWColor(220, 90,  55, 100, "tearStroke");

    tear = new SWTearDrop(width / 2, height / 2, 120, 80, fillC, {
        strokeColor:  strokeC,
        strokeWeight: 2,
        m:            3.0,
        numPoints:    120,
        rotOrigin:    'center'
    });
}

function draw() {
    background(200, 20, 95);
    tear.draw();
}

Key Properties

After construction, these properties are readable. Use setter methods to modify them safely so internal state stays consistent.

cx / cy

Type: number — Centre coordinates in the same units as the constructor. Use setCenter(cx, cy) to move the shape.

a / originalA

Type: number — a is the current half-height baseline. originalA is the value breathing animates around. Use setA(a) to update both at once.

b / originalB

Type: number — b is the current width-parameter baseline. originalB is the breathing baseline. Use setB(b) to update both at once.

_currentA / _currentB

Type: number — Live animated values set by breathe(). Read these (not a or b) if you need the current on-screen size during animation. Reset to originalA/originalB by reset().

m

Type: number (≥ 0) — Curve-power exponent. Change via setM(m). Takes effect immediately on the next draw() call; no rebuild needed.

numPoints

Type: number (≥ 3) — Polygon vertex count. Change via setNumPoints(n). Lower values produce a faceted or angular appearance; 3 gives a triangle.

rotOrigin

Type: string — 'center' or 'tip'. Change via setRotOrigin(o). Switching pivot while the shape is rotating causes a visual jump in position (the rotation angle is preserved, but the pivot point moves). To avoid this, call reset() before switching.

_rotAngle

Type: number (degrees) — Current rotation offset, updated by rotate(). Reset to 0 by reset(). You can directly set this to carry over a rotation angle when rebuilding the shape.

fillColor / strokeColor

Type: SWColor | null — Current colour objects. Change via setFillColor(c) and setStrokeColor(c); pass null to suppress the layer.

Methods

Drawing Methods

draw()

Renders the teardrop in screen (pixel) coordinates using the current animated values (_currentA, _currentB, _rotAngle). Call once per draw() loop frame.

function draw() {
    background(200, 20, 95);
    tear.draw();
}

drawOnGrid(grid)

Parameters: grid (SWGrid)

Maps cx, cy, and the current a/b values from user coordinates to screen pixels using the provided SWGrid, then renders normally. The rotation angle is applied in screen space.

// tear.cx / tear.cy are in user (math) units
tear.drawOnGrid(myGrid);

Animation Methods

breathe(sinusoid, t)

Parameters: sinusoid (SWSinusoid), t (number — elapsed seconds)

Scales both _currentA and _currentB by sinusoid.getValue(t). Both dimensions scale by the same factor, preserving the shape’s aspect ratio. Build the sinusoid with new SWSinusoid(amp, freq, mid, phase) where phase -Math.PI/2 starts at scale 1.0 and rises first.

let breatheSin;

function setup() {
    // ...
    // Breathe between 75% and 125% of original size, 3-second period
    const amp  = (1.25 - 0.75) / 2;         // 0.25
    const freq = (2 * Math.PI) / 3.0;       // 3-second period
    const mid  = (1.25 + 0.75) / 2;         // 1.0 (no net scale change)
    breatheSin = new SWSinusoid(amp, freq, mid, -Math.PI / 2);
}

function draw() {
    const t = millis() / 1000;
    tear.breathe(breatheSin, t);
    tear.draw();
}

rotate(degPerSec, t)

Parameters: degPerSec (number), t (number — elapsed seconds)

Sets _rotAngle = degPerSec × t. Positive values rotate clockwise in screen coordinates. The pivot is determined by rotOrigin: 'center' spins the shape in place; 'tip' keeps the tip fixed while the bulge swings around it.

// Spin at 45°/sec clockwise
tear.rotate(45, totalRotTime);

// Spin counter-clockwise
tear.rotate(-90, totalRotTime);

transform(options)

Combines breathing and rotation in a single call. Mirrors the transform() API on SWSun, SWLine, and SWArrow.

OptionTypeDefaultEffect
sinusoidSWSinusoidnullBreathing (null = skip)
tnumber0Time in seconds
degPerSecnumbernullRotation rate (null = skip)
// Both animations in one call
tear.transform({
    sinusoid:  breatheSin,
    t:         totalTime,
    degPerSec: 45
});

reset()

Resets all animation state to initial values: _rotAngle returns to 0; _currentA and _currentB return to originalA/originalB. Does not reset property changes made via setters (colours, m, numPoints, etc.).

tear.reset();   // stop animation effects and return to baseline

Setter Methods

MethodParameterNotes
setCenter(cx, cy) two numbers Move the shape’s centre. Safe to call every frame for mouse drag.
setA(a) number Updates a, originalA, and _currentA together. Minimum enforced at 1.
setB(b) number Updates b, originalB, and _currentB together. Minimum enforced at 1.
setM(m) number Curve-power exponent. Minimum enforced at 0 (0 = ellipse).
setNumPoints(n) number Vertex count. Floored to integer; minimum enforced at 3.
setFillColor(c) SWColor | null Pass null to remove fill.
setStrokeColor(c) SWColor | null Pass null to remove stroke.
setStrokeWeight(w) number Minimum enforced at 0.
setRotOrigin(o) string 'center' or 'tip'. Consider calling reset() before switching to avoid a visual jump.

Utility Methods

toString()

Returns: string — human-readable summary of the teardrop’s current state.

console.log(tear.toString());
// → "SWTearDrop(cx:320.0, cy:240.0, a:120.0, b:80.0, m:3)"

Rotation Pivot: Centre vs. Tip

'center' (default)

The pivot is the shape’s bounding-box centre — the midpoint between the tip and the bottom of the bulge. The teardrop spins in place. The tip traces a full circle around the centre point. This feels like a drop of water spinning on a surface.

Internally: translate(cx, cy), then parametric vertices are drawn as vy = −a·cos(t)

'tip'

The pivot is the tip point itself at (cx, cy − a). The tip stays fixed while the bulge swings around it like a pendulum — or like a tear hanging from a point. The translation shifts to the tip; vertices are offset by +a so the tip is at the local origin.

Internally: translate(cx, cy−a), then vy = a·(1 − cos(t))

Usage Examples

Example 1: Simple Static Teardrop

let tear;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();

    const fill   = new SWColor(210, 80, 100, 100, "fill");
    const stroke = new SWColor(220, 90,  55, 100, "stroke");

    tear = new SWTearDrop(200, 200, 120, 80, fill, {
        strokeColor:  stroke,
        strokeWeight: 2,
        m:            3.0
    });
}

function draw() {
    background(200, 20, 95);
    tear.draw();
}

Example 2: Breathing Teardrop

let tear, breatheSin;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();

    const fill = new SWColor(210, 80, 100, 100, "fill");
    tear = new SWTearDrop(200, 200, 100, 65, fill, { m: 2.0 });

    // Breathe between 80% and 120% of size; 3-second period
    const amp  = 0.20;
    const freq = (2 * Math.PI) / 3.0;
    breatheSin = new SWSinusoid(amp, freq, 1.0, -Math.PI / 2);
}

function draw() {
    background(200, 15, 95);
    tear.breathe(breatheSin, millis() / 1000);
    tear.draw();
}

Example 3: Spinning (Centre Pivot)

let tear;
let rotElapsed = 0;
let lastT = 0;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();

    const fill = new SWColor(200, 75, 100, 100, "fill");
    tear = new SWTearDrop(200, 200, 110, 75, fill, {
        m:         3.0,
        rotOrigin: 'center'
    });
}

function draw() {
    background(210, 20, 95);

    const now = millis() / 1000;
    rotElapsed += (now - lastT);
    lastT = now;

    tear.rotate(45, rotElapsed);   // 45°/sec clockwise
    tear.draw();
}

Example 4: Pendulum Spin (Tip Pivot)

// The tip stays fixed; the bulge swings like a pendulum
let tear;
let rotElapsed = 0;
let lastT = 0;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();

    const fill = new SWColor(195, 70, 100, 100, "fill");
    // Position the tip near the top of the canvas
    tear = new SWTearDrop(200, 80, 120, 80, fill, {
        m:         2.0,
        rotOrigin: 'tip'    // tip at (200, 80 - 120) = (200, -40)
                            // ... use a lower cy so the tip is on-screen
    });

    // Better: place the shape so the tip stays on canvas
    // tear.cy - tear.a = tipY
    tear = new SWTearDrop(200, 160, 120, 80, fill, {
        m:         2.0,
        rotOrigin: 'tip'    // tip at (200, 160 - 120) = (200, 40) — near top
    });
}

function draw() {
    background(210, 20, 95);

    const now = millis() / 1000;
    rotElapsed += (now - lastT);
    lastT = now;

    tear.rotate(30, rotElapsed);   // 30°/sec clockwise around the tip
    tear.draw();
}

Example 5: Breathing + Spinning Together

let tear, breatheSin;
let rotElapsed = 0, lastT = 0;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();

    const fill   = new SWColor(210, 80, 100, 100, "fill");
    const stroke = new SWColor(220, 90,  55, 100, "stroke");

    tear = new SWTearDrop(200, 200, 100, 68, fill, {
        strokeColor:  stroke,
        strokeWeight: 2,
        m:            3.0
    });

    breatheSin = new SWSinusoid(0.20, (2*Math.PI)/3, 1.0, -Math.PI/2);
}

function draw() {
    background(205, 20, 94);

    const now = millis() / 1000;
    rotElapsed += (now - lastT);
    lastT = now;

    // Use transform() to apply both in one call
    tear.transform({
        sinusoid:  breatheSin,
        t:         millis() / 1000,
        degPerSec: 45
    });

    tear.draw();
}

Example 6: Teardrop on a Grid

// Place the teardrop in user (math) coordinates via SWGrid
let myGrid, tear;

function setup() {
    createCanvas(480, 480);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();

    myGrid = new SWGrid(/* options for a centred grid */);

    const fill = new SWColor(210, 80, 100, 100, "fill");
    // cx / cy / a / b are all in grid (user) units here
    tear = new SWTearDrop(0, 0, 3, 2, fill, { m: 2.0 });
}

function draw() {
    background(210, 15, 96);
    myGrid.draw();         // draw axes
    tear.drawOnGrid(myGrid);
}

Integration with SketchWave

Loading Order

SWTearDrop only depends on swColor.js. Load swSinusoid.js if you call breathe() and swGrid.js if you call drawOnGrid():

<script src="https://cdn.jsdelivr.net/npm/p5@1.6.0/lib/p5.js"></script>

<!-- SketchWave dependencies -->
<script src="../shapeClasses/swSinusoid.js"></script>   <!-- needed for breathe() -->
<script src="../shapeClasses/swColor.js"></script>      <!-- required -->
<script src="../shapeClasses/swPoint.js"></script>
<script src="../shapeClasses/swGrid.js"></script>       <!-- needed for drawOnGrid() -->
<script src="../shapeClasses/swTearDrop.js"></script>

<!-- Your sketch -->
<script src="../sketches/yourSketch.js"></script>

colorMode Requirement

SWTearDrop uses SWColor, which requires p5.js to be in HSB mode:

function setup() {
    createCanvas(width, height);
    colorMode(HSB, 360, 100, 100, 100);   // required
    initializeSWColors();                  // required
    // ...
}

Animation Timer Pattern (Pause/Resume)

SWTearDrop animations take an accumulated elapsed-time parameter. Track it separately from millis() so you can pause and resume cleanly:

let rotationOn   = false;
let totalRotTime = 0;
let lastRotSnap  = 0;   // millis()/1000 when rotation was last active

function draw() {
    background(210, 15, 96);

    if (rotationOn) {
        const now = millis() / 1000;
        totalRotTime += (now - lastRotSnap);
        lastRotSnap = now;
        tear.rotate(45, totalRotTime);
    }

    tear.draw();
}

function toggleRotation() {
    rotationOn = !rotationOn;
    if (rotationOn) {
        lastRotSnap = millis() / 1000;   // reset delta reference on resume
    }
}

Breathing Pause/Resume Pattern

let breathingOn = false;
let breathingStartTime    = 0;   // millis()/1000 when last started
let breathingPausedElapsed = 0;  // seconds already accumulated

function toggleBreathe() {
    breathingOn = !breathingOn;
    if (breathingOn) {
        // Resume: start counting from where we left off
        breathingStartTime = millis() / 1000;
    } else {
        // Pause: save elapsed so far
        breathingPausedElapsed += (millis() / 1000) - breathingStartTime;
    }
}

function draw() {
    background(210, 15, 96);

    if (breathingOn) {
        const elapsed = breathingPausedElapsed + (millis() / 1000 - breathingStartTime);
        tear.breathe(breatheSin, elapsed);
    }

    tear.draw();
}

Carry-Over Rotation When Rebuilding

When you change a property (like m or numPoints) the demo rebuilds the SWTearDrop object. To avoid a visual snap back to 0°, carry the rotation angle over to the new object:

function buildTearDrop() {
    myTear = new SWTearDrop(cx, cy, aVal, bVal, fillC, options);

    // Preserve rotation across rebuild
    myTear._rotAngle = totalRotTime * rotationRate;
}

Source Code

Show / Hide Source Code
/*
File:    swTearDrop.js
Date:    2026-05-19
Author:  klp
Workspace: SketchWaveTNT2026-05-01-Stg9
Purpose: SWTearDrop class — a parametric teardrop (water-drop) shape based on:

           x(t) = b · sin(t) · sin^m(t / 2)
           y(t) = –a · cos(t)

         where t ∈ [0, 2π].

         Parameters:
           a  — half-height (distance from bounding-box centre to tip, and
                to the bottom of the bulge)
           b  — width scale (max half-width ≈ 0.77 × b for m = 1)
           m  — curve-power exponent (m ≥ 0)
                  0    → perfect ellipse
                  1.0  → classic teardrop
                  2+   → elongated, more pointed tip

         Default orientation: tip at TOP (minimum y), bulge at BOTTOM.
         At t = 0:  (0, –a) → tip at the top ✓
         At t = π:  (0, +a) → bottom of bulge ✓
         Width is maximum near t ≈ 109° (y ≈ +a/3, one-third down from centre).

Features:
  - Configurable fill colour, stroke colour, and stroke weight
  - Breathing animation via SWSinusoid (scales both a and b proportionally)
  - Rotation about the shape's bounding-box centre OR about its tip:
      'center' — the tip traces a circle around the centre point
      'tip'    — the tip stays fixed; the bulge swings like a pendulum
  - draw()          — pixel coordinates (screen space)
  - drawOnGrid(grid) — user coordinates mapped through a SWGrid

Dependencies: p5.js, swColor.js, swSinusoid.js (for breathe()), swGrid.js (for drawOnGrid())
Notes: assumes p5.js is running in colorMode(HSB, 360, 100, 100, 100)
*/

console.log("[swTearDrop.js] SWTearDrop class loaded.");

class SWTearDrop {

    /**
     * @param {number}   cx          - Centre x (bounding-box midpoint, pixels for draw())
     * @param {number}   cy          - Centre y (bounding-box midpoint — midway between tip and bottom)
     * @param {number}   a           - Half-height ≥ 1: distance from centre to tip (and to bottom)
     * @param {number}   b           - Width parameter ≥ 1: max half-width ≈ 0.77 × b
     * @param {SWColor}  fillColor   - Fill colour (null = no fill)
     * @param {Object}  [options]
     *   strokeColor   {SWColor}  Outline colour (null = no stroke)        (default null)
     *   strokeWeight  {number}   Stroke weight in pixels                  (default 2)
     *   m             {number}   Curve-power exponent ≥ 0                 (default 1.0)
     *   numPoints     {number}   Vertex count for smoothness              (default 120)
     *   rotOrigin     {string}   'center' | 'tip'                         (default 'center')
     */
    constructor(cx, cy, a, b, fillColor, options = {}) {
        this.cx = cx;
        this.cy = cy;
        this.a         = max(1, a);
        this.b         = max(1, b);
        this.originalA = this.a;
        this.originalB = this.b;

        this.fillColor    = fillColor           ? SWColor.copy(fillColor)           : null;
        this.strokeColor  = options.strokeColor ? SWColor.copy(options.strokeColor) : null;
        this.strokeWeight = options.strokeWeight ?? 2;
        this.m            = max(0, options.m ?? 1.0);
        this.numPoints    = max(3, options.numPoints ?? 120);
        this.rotOrigin    = options.rotOrigin ?? 'center';  // 'center' | 'tip'

        // Animation state
        this._rotAngle  = 0;     // accumulated rotation in degrees
        this._currentA  = this.a;
        this._currentB  = this.b;
    }//end constructor

    // ─── Drawing ─────────────────────────────────────────────────────────────

    /** Draw in screen (pixel) coordinates. */
    draw() {
        this._drawAtPx(this.cx, this.cy, this._currentA, this._currentB);
    }//end draw

    /**
     * Draw using user coordinates mapped through a SWGrid.
     * @param {SWGrid} grid
     */
    drawOnGrid(grid) {
        const { x: sx, y: sy } = grid.userToScreen(this.cx, this.cy);
        const sa = this._currentA * grid.xScale;
        const sb = this._currentB * grid.xScale;
        this._drawAtPx(sx, sy, sa, sb);
    }//end drawOnGrid

    /**
     * Core rendering: translate to pivot, rotate, then draw the parametric shape.
     *
     * Parametric equations (relative to bounding-box centre at origin):
     *   vx(t) = b · sin(t) · sin^m(t / 2)
     *   vy(t) = –a · cos(t)   [tip at (0, –a); bottom at (0, +a)]
     *
     * When rotOrigin === 'tip', the pivot is the tip (cx, cy – a):
     *   We translate to the tip, rotate, then offset vy by +a so the tip
     *   sits at the pivot origin:  vy_rel = –a·cos(t) + a = a·(1 – cos(t))
     *
     * @param {number} cx   Centre x in screen pixels
     * @param {number} cy   Centre y in screen pixels
     * @param {number} a    Current half-height in screen pixels
     * @param {number} b    Current width parameter in screen pixels
     */
    _drawAtPx(cx, cy, a, b) {
        push();

        // ── Appearance ────────────────────────────────────────────────────
        if (this.fillColor) {
            fill(this.fillColor.col);
        } else {
            noFill();
        }
        if (this.strokeColor) {
            stroke(this.strokeColor.col);
            strokeWeight(this.strokeWeight);
            strokeJoin(ROUND);
            strokeCap(ROUND);
        } else {
            noStroke();
        }

        // ── Transform: translate to pivot, then rotate ────────────────────
        // pivotOffY = shift from bounding-box centre to rotation pivot
        //   'center' pivot: offset = 0   (pivot at centre)
        //   'tip'    pivot: offset = –a  (pivot at top of teardrop)
        const pivotOffY = (this.rotOrigin === 'tip') ? -a : 0;
        translate(cx, cy + pivotOffY);
        rotate(radians(this._rotAngle));

        // ── Parametric vertices (relative to pivot) ───────────────────────
        //   vx(t) = b · sin(t) · sin^m(t / 2)
        //   vy(t) = –a · cos(t) – pivotOffY
        //     For 'center': vy = –a·cos(t)           (tip at y = –a above pivot)
        //     For 'tip':    vy = –a·cos(t) + a = a·(1 – cos(t))  (tip at y = 0)
        beginShape();
        for (let i = 0; i <= this.numPoints; i++) {
            const t  = (i / this.numPoints) * TWO_PI;
            const vx = b * sin(t) * pow(sin(t / 2), this.m);
            const vy = -a * cos(t) - pivotOffY;
            vertex(vx, vy);
        }
        endShape(CLOSE);

        pop();
    }//end _drawAtPx

    // ─── Animation ────────────────────────────────────────────────────────────

    /**
     * Scales the teardrop proportionally via a SWSinusoid.
     * Both a and b scale together, preserving the shape's aspect ratio.
     * @param {SWSinusoid} sinusoid
     * @param {number}     t - Elapsed time in seconds
     */
    breathe(sinusoid, t) {
        const scale    = sinusoid.getValue(t);
        this._currentA = this.originalA * scale;
        this._currentB = this.originalB * scale;
    }//end breathe

    /**
     * Rotates the teardrop. Accumulates continuously as t increases.
     * Positive values rotate clockwise in screen coordinates.
     * Pivot is set by this.rotOrigin ('center' or 'tip').
     * @param {number} degPerSec - Rotation speed (degrees per second)
     * @param {number} t         - Accumulated elapsed time in seconds
     */
    rotate(degPerSec, t) {
        this._rotAngle = degPerSec * t;
    }//end rotate

    /**
     * Combines breathing and rotation in one call.
     * Mirrors the transform() API on SWSun, SWLine, and SWArrow.
     * @param {Object}     [options]
     * @param {SWSinusoid} [options.sinusoid=null]  - Breathing sinusoid (null = no breathe)
     * @param {number}     [options.t=0]            - Time in seconds
     * @param {number}     [options.degPerSec=null] - Rotation rate in deg/sec (null = no rotate)
     */
    transform({ sinusoid = null, t = 0, degPerSec = null } = {}) {
        if (sinusoid  !== null) this.breathe(sinusoid, t);
        if (degPerSec !== null) this.rotate(degPerSec, t);
    }//end transform

    /**
     * Resets all animation state to initial (factory) values.
     */
    reset() {
        this._rotAngle = 0;
        this._currentA = this.originalA;
        this._currentB = this.originalB;
    }//end reset

    // ─── Setters ──────────────────────────────────────────────────────────────

    /** Move the shape's centre point. */
    setCenter(cx, cy) {
        this.cx = cx;
        this.cy = cy;
    }//end setCenter

    /** Set the half-height and reset the breathing baseline. */
    setA(a) {
        this.a         = max(1, a);
        this.originalA = this.a;
        this._currentA = this.a;
    }//end setA

    /** Set the width parameter and reset the breathing baseline. */
    setB(b) {
        this.b         = max(1, b);
        this.originalB = this.b;
        this._currentB = this.b;
    }//end setB

    /** Set the curve-power exponent (≥ 0; 0 = perfect ellipse). */
    setM(m)            { this.m            = max(0, m); }//end setM

    /** Set the number of vertices for the shape outline (≥ 3). */
    setNumPoints(n)    { this.numPoints    = max(3, floor(n)); }//end setNumPoints

    /** Set the fill colour (pass null for no fill). */
    setFillColor(c)    { this.fillColor    = c ? SWColor.copy(c) : null; }//end setFillColor

    /** Set the stroke colour (pass null for no stroke). */
    setStrokeColor(c)  { this.strokeColor  = c ? SWColor.copy(c) : null; }//end setStrokeColor

    /** Set the stroke weight in pixels. */
    setStrokeWeight(w) { this.strokeWeight = max(0, w); }//end setStrokeWeight

    /** Set the rotation pivot: 'center' or 'tip'. */
    setRotOrigin(o)    { this.rotOrigin    = o; }//end setRotOrigin

    /** @returns {string} */
    toString() {
        return `SWTearDrop(cx:${this.cx.toFixed(1)}, cy:${this.cy.toFixed(1)}, ` +
               `a:${this._currentA.toFixed(1)}, b:${this._currentB.toFixed(1)}, m:${this.m})`;
    }//end toString

}//end class SWTearDrop