⬜ SWRoundedRectangle Class Reference

Rectangle with Rounded Corners, Breathing & Spinning — SketchWaveJS Stage

Overview

SWRoundedRectangle extends SWRectangle with a single new property — cornerRadius — that produces smoothly rounded corners using p5's built-in rect(x, y, w, h, r) overload. All animation (breathing, rotation, transform), dragging, and trail support is inherited unchanged. The only thing this subclass changes is the drawing mechanism: instead of mapping four rotated corner vertices through quad(), it draws inside a push()/translate()/rotate()/pop() block and calls rectMode(CENTER) — the same technique used by SWEllipse.

⬜ Why a Subclass?

p5's quad() function — used by SWRectangle — has no corner-radius parameter. Adding rounding to SWRectangle itself would need a conditional branch in the drawing method: a violation of the Single Responsibility Principle (SRP). A subclass is the clean solution: it overrides only what needs to change and inherits everything else.

📐 Corner Radius Scaling

cornerRadius is stored in user (grid) units and multiplied by the grid's scaleX at draw time. It is automatically clamped to min(pixelWidth/2, pixelHeight/2), so you can never over-round a rectangle — sliding to the maximum still produces a "stadium" or circle shape, never an invalid geometry.

Constructor

new SWRoundedRectangle(center, width, height, fillColor, cornerRadius?, options?)
new SWRoundedRectangle(center, width, height, fillColor, cornerRadius = 1.5, options = {})
ParameterTypeDefaultDescription
center SWPoint Center of the shape in user (grid) coordinates.
width number Full width in user units.
height number Full height in user units.
fillColor SWColor Interior fill color.
cornerRadius number 1.5 Corner radius in user units. Clamped to half the shorter side at draw time.
options Object {} Passed to SWRectangle constructor: {strokeColor, strokeWeight, showCenter, rotation}.
Example
// Create a 10×8 rounded rectangle at the origin with lavender fill
const rr = new SWRoundedRectangle(
    new SWPoint(0, 0),          // center
    10, 8,                      // width, height
    SWColor.fromHex("#c8a8f5"), // fillColor
    1.5,                        // cornerRadius
    { strokeWeight: 2, rotation: 0 }
);

Own Properties

These properties are defined by SWRoundedRectangle itself, in addition to everything it inherits from SWRectangle.

cornerRadius  number

Corner radius of the rectangle in user (grid) units. Converted to pixels at draw time by multiplying by grid.scaleX and then clamped to Math.min(pixelWidth / 2, pixelHeight / 2).

Set to 0 for perfectly sharp corners (equivalent to drawing with SWRectangle at zero rotation, but note that SWRectangle uses quad() while this class always uses rect()).

rr.cornerRadius = 2; // change to 2 user units at runtime

Internal Animation Trackers

These private-convention properties are set by the overridden breathe(), rotateAboutCenter(), and transform() methods. They are read-only in practice — set them only indirectly through the animation methods.

_scaleX  number internal
Current X-axis breathing scale factor (1 = no scaling). Used by drawOnGrid() to compute pixelWidth = width * _scaleX * grid.scaleX.
_scaleY  number internal
Current Y-axis breathing scale factor (1 = no scaling).
_animRotDeg  number internal
Accumulated animation rotation in degrees (CCW positive). Added to the static rotation property when drawing. Reset to 0 by reset().

Inherited Properties

All of these are inherited from SWRectangle without modification:

PropertyTypeDescription
rCenter SWPoint Center of the shape in user coordinates.
width number Nominal (un-scaled) width in user units.
height number Nominal (un-scaled) height in user units.
fillColor SWColor Interior fill color object.
strokeColor SWColor Border stroke color object.
strokeWeight number Border thickness in pixels.
showCenter boolean Whether to draw the center point dot.
rotation number Static rotation angle in degrees (CCW positive).
showVertices boolean Inherited but silently ignored — corners are rounded, vertex dots would mislead.
trailPoints Array Array of past center positions for the trail.
maxTrailLengthnumber Maximum number of trail points to retain.

Geometric Properties

These getters reflect the live, animated state of the shape (breathing scale applied).

currentWidth  getter

Effective width at the current breathing scale: width × _scaleX.

console.log(rr.currentWidth);
currentHeight  getter
Effective height at the current breathing scale: height × _scaleY.
currentArea  getter

Area of the live bounding rectangle: currentWidth × currentHeight.

Note: this is the area of the bounding rectangle. The true area of a rounded rectangle is slightly smaller (corners replaced by quarter-circles), but the bounding-rectangle approximation is used here for simplicity.

currentAspectRatio  getter
Aspect ratio of the live shape: currentWidth / currentHeight.

Inherited Geometric Getters (from SWRectangle)

GetterFormula / Description
area width × height (nominal, un-scaled)
perimeter 2 × (width + height) (nominal)
aspectRatio width / height (nominal)
diagonal √(width² + height²)

Classification

Inherited from SWRectangle:

isSquare  getter
Returns true if Math.abs(width - height) < 0.001. A "rounded square" is still considered a square by this test.

Drawing Methods

drawOnGrid(grid)  method  override
drawOnGrid(grid: SWGrid): void

The core override. Draws the rounded rectangle using push()translate(px, py)rotate(rotRad)rectMode(CENTER)rect(0, 0, pw, ph, cr)pop().

Rotation uses CCW-positive user convention converted to p5's CW convention: rotRad = -(rotation + _animRotDeg) × π / 180.

Corner radius in pixels = Math.min(cornerRadius × grid.xScale, pw/2, ph/2).

rr.drawOnGrid(myGrid);
draw()  method  override
draw(): void

Screen-pixel version of drawOnGrid(). Uses raw pixel coordinates from rCenter.x/y and treats width/height as pixel values directly (no grid mapping). Useful for HUD overlays or thumbnails.

Animation Methods

All animation methods are overridden to maintain the internal scale/rotation trackers and then delegate to the parent implementation.

breathe(sinusoidX, sinusoidY, t)  method  override
breathe(sinusoidX: SWSinusoid|null, sinusoidY: SWSinusoid|null, t: number): void

Calls super.breathe() then records the live scale factors from the sinusoids into _scaleX and _scaleY. Call once per frame while the animation is active.

rr.breathe(sinX, sinY, millis() / 1000);
rotateAboutCenter(degPerSec, t)  method  override
rotateAboutCenter(degPerSec: number, t: number): void

Calls super.rotateAboutCenter() and stores the accumulated angle in _animRotDeg = degPerSec × t.

rr.rotateAboutCenter(45, millis() / 1000); // 45°/s
transform(options)  method  override
transform({ sinusoidX?, sinusoidY?, t?, degPerSec? }): void

Combines breathing and spinning in one call. Updates _scaleX, _scaleY, and _animRotDeg in addition to calling the parent implementation.

rr.transform({ sinusoidX: sinX, sinusoidY: sinY, t: t, degPerSec: 30 });
reset()  method  override
reset(): void

Calls super.reset() and additionally resets _scaleX = 1, _scaleY = 1, _animRotDeg = 0.

rr.reset();

Utility Methods

toString()  method  override
toString(): string

Returns a human-readable summary including center, width, height, cornerRadius, area, and total rotation (static + animated).

// SWRoundedRectangle(center: (0.00, 0.00), width: 10.00, height: 8.00, cornerRadius: 1.50, area: 80.00, rotation: 0.0°)
console.log(rr.toString());

Examples

Basic: Round-cornered Rectangle on a Grid

// In your p5 sketch:
let grid, rr;

function setup() {
    createCanvas(600, 500);
    grid = new SWGrid(new SWPoint(-12, 10), new SWPoint(12, -10));
    grid.init(width, height);

    const fill = SWColor.fromHex("#c8a8f5");
    const stroke = SWColor.fromHex("#4a0090");
    stroke.setAlphaTo(200);

    rr = new SWRoundedRectangle(
        new SWPoint(0, 0),
        10, 8,
        fill,
        2.0,              // 2-unit corner radius
        { strokeColor: stroke, strokeWeight: 2 }
    );
}

function draw() {
    background(240);
    grid.drawOnScreen();
    rr.drawOnGrid(grid);
}

Breathing

let sinX, sinY, isBreathing = false, startTime;

function setup() {
    // ... grid and rr setup as above ...
    sinX = new SWSinusoid(1.0, 2.0, 1.6, 0.8, 0); // period, amp, max, min, phase
    sinY = sinX; // uniform breathing: same sinusoid for both axes
}

function draw() {
    background(240);
    grid.drawOnScreen();

    if (isBreathing) {
        const t = (millis() - startTime) / 1000;
        rr.breathe(sinX, sinY, t);
    }
    rr.drawOnGrid(grid);
}

function keyPressed() {
    if (key === 'b') {
        isBreathing = !isBreathing;
        if (isBreathing) startTime = millis();
        else rr.reset();
    }
}

Spinning

let isSpinning = false, spinStartTime, rotationRate = 45; // °/s

function draw() {
    background(240);
    grid.drawOnScreen();

    if (isSpinning) {
        const t = (millis() - spinStartTime) / 1000;
        rr.rotateAboutCenter(rotationRate, t);
    }
    rr.drawOnGrid(grid);
}

Corner Radius = 0: Sharp Corners

// For a rounded rect that looks like a regular rectangle:
rr.cornerRadius = 0;
rr.drawOnGrid(grid); // draws with sharp corners, same as SWRectangle at zero rotation

Tips & Best Practices

📌 Corner Radius in User Units
Always set cornerRadius in grid user units rather than pixels. This way the shape scales correctly when the canvas is resized. A radius of 1.0 means one grid unit of rounding — easy to reason about.
📌 Automatic Clamping
You do not need to guard against over-rounding. drawOnGrid() clamps the pixel corner radius to Math.min(pixelWidth/2, pixelHeight/2). Slide the corner radius all the way up and the shape gracefully becomes pill-shaped (or circular if it is a square).
📌 Reset After Stopping Animation
Always call rr.reset() when stopping breathing or spinning. This clears _scaleX, _scaleY, and _animRotDeg along with restoring the parent's vertex positions, so the shape returns cleanly to its factory state.
📌 Vertex Dots Are Suppressed
Setting showVertices = true (inherited from SWRectangle) has no visible effect on this class. The four corner vertices are tracked internally for trail/inheritance purposes but are not drawn, because vertex dots on rounded corners would be misleading — the actual visual corners are offset inward by the radius.
📌 Breathing + Spinning Together
Use transform({ sinusoidX, sinusoidY, t, degPerSec }) when you want both effects active at the same time. Calling breathe() and rotateAboutCenter() separately in the same frame will work but may cause slight inconsistencies in the internal trackers.
📌 Trails
Set rr.maxTrailLength = 50 (or higher) and push center positions each frame to create a trailing path. Only the center point is tracked for trails in this class; edge/corner trails are not defined.
📌 SRP and Subclassing
The design of SWRoundedRectangle follows the Single Responsibility Principle: SWRectangle is responsible for rectangular geometry and animation; SWRoundedRectangle is responsible for the rounded-corner drawing variant. Neither class is burdened with the other's concerns, and both can be tested and understood independently.

Source Code

Complete source for swRoundedRectangle.js:

/*
File:    swRoundedRectangle.js
Date:    2026-02-28
Author:  klp + GitHub Copilot
App:     SketchWaveTNT2026-02-28-Stg6
Purpose: SWRoundedRectangle — extends SWRectangle with corner rounding.

  SWRoundedRectangle inherits all animation (breathe, transform, rotateAboutCenter),
  vertex tracking, dragging, and trail support from SWRectangle.  It overrides
  draw() and drawOnGrid() to use p5's push/translate/rotate/rect()/pop() pattern,
  which supports the `cornerRadius` property natively — something quad() cannot do.

  Key design notes:
  - cornerRadius is in user (grid) units, scaled to pixels at draw time.
  - Three internal scalars (_scaleX, _scaleY, _animRotDeg) are maintained by
    overriding breathe(), transform(), and rotateAboutCenter() so drawOnGrid()
    always knows the live dimensions and rotation without re-deriving them from
    vertex positions.
  - showVertices is silently ignored: the four inherited vertices track scaling and
    rotation correctly for trail purposes but are not drawn (corners are rounded).
*/

console.log("[swRoundedRectangle.js] SWRoundedRectangle class loaded.");

class SWRoundedRectangle extends SWRectangle {

    /**
     * @param {SWPoint}  center        — Center of the shape (user coordinates)
     * @param {number}   width         — Full width in user units
     * @param {number}   height        — Full height in user units
     * @param {SWColor}  fillColor     — Interior fill color
     * @param {number}  [cornerRadius] — Corner radius in user units (default 1.5)
     * @param {Object}  [options]      — Passed to SWRectangle:
     *                                   strokeColor, strokeWeight, showCenter, rotation
     */
    constructor(center, width, height, fillColor, cornerRadius = 1.5, options = {}) {
        super(center, width, height, fillColor, options);
        this.cornerRadius = cornerRadius;

        // ── Internal animation-state trackers ─────────────────────────────────
        this._scaleX     = 1;  // current X-axis breathing scale factor
        this._scaleY     = 1;  // current Y-axis breathing scale factor
        this._animRotDeg = 0;  // additional rotation from animation (degrees CCW)
    }//end constructor

    // ── Override breathe ──────────────────────────────────────────────────────
    /**
     * Scales the shape using independent SWSinusoid objects and tracks scale
     * factors for use in drawOnGrid().
     * @param {SWSinusoid|null} sinusoidX
     * @param {SWSinusoid|null} sinusoidY
     * @param {number}          t — time in seconds
     */
    breathe(sinusoidX, sinusoidY, t) {
        super.breathe(sinusoidX, sinusoidY, t);
        const minScale = 0.1;
        this._scaleX = Math.max(minScale, sinusoidX ? sinusoidX.getValue(t) : 1);
        this._scaleY = Math.max(minScale, sinusoidY ? sinusoidY.getValue(t) : 1);
    }//end breathe

    // ── Override rotateAboutCenter ────────────────────────────────────────────
    /**
     * Rotates the shape about its center and tracks the angle for drawOnGrid().
     * @param {number} degPerSec — angular velocity (CCW positive)
     * @param {number} t         — time in seconds
     */
    rotateAboutCenter(degPerSec, t) {
        super.rotateAboutCenter(degPerSec, t);
        this._animRotDeg = degPerSec * t;
    }//end rotateAboutCenter

    // ── Override transform ────────────────────────────────────────────────────
    /**
     * Applies simultaneous breathing and rotation, tracking scalars for drawOnGrid().
     * @param {Object} options — {sinusoidX, sinusoidY, t, degPerSec}
     */
    transform({ sinusoidX = null, sinusoidY = null, t = 0, degPerSec = null } = {}) {
        super.transform({ sinusoidX, sinusoidY, t, degPerSec });
        const minScale = 0.1;
        this._scaleX     = sinusoidX ? Math.max(minScale, sinusoidX.getValue(t)) : 1;
        this._scaleY     = sinusoidY ? Math.max(minScale, sinusoidY.getValue(t)) : 1;
        this._animRotDeg = degPerSec !== null ? degPerSec * t : 0;
    }//end transform

    // ── Override reset ────────────────────────────────────────────────────────
    /**
     * Restores original dimensions, resets animation trackers.
     */
    reset() {
        super.reset();
        this._scaleX     = 1;
        this._scaleY     = 1;
        this._animRotDeg = 0;
    }//end reset

    // ── drawOnGrid (key override) ─────────────────────────────────────────────
    /**
     * Draws the rounded rectangle using push/translate/rotate/rect()/pop().
     * This is the fundamental departure from SWRectangle's quad() approach —
     * it is what enables corner rounding and is why this subclass exists.
     * @param {SWGrid} grid
     */
    drawOnGrid(grid) {
        const s  = grid.userToScreen(this.rCenter.x, this.rCenter.y);
        const pw = this.width  * this._scaleX * grid.xScale;  // pixel width
        const ph = this.height * this._scaleY * grid.yScale;  // pixel height
        const cr = Math.min(this.cornerRadius * grid.xScale,  // pixel corner radius
                            pw / 2, ph / 2);                  // clamped to half side

        // Total rotation = static initial rotation + accumulated animation rotation
        // Negate because p5 uses CW-positive; user convention is CCW-positive.
        const totalDeg = this.rotation + this._animRotDeg;
        const rotRad   = -totalDeg * Math.PI / 180;

        push();
        translate(s.x, s.y);
        rotate(rotRad);

        if (this.fillColor && this.fillColor.col) {
            fill(this.fillColor.col);
        } else {
            noFill();
        }
        if (this.strokeColor && this.strokeColor.col) {
            stroke(this.strokeColor.col);
            strokeWeight(this.strokeWeight);
        } else {
            noStroke();
        }

        rectMode(CENTER);
        rect(0, 0, pw, ph, cr);
        pop();
        rectMode(CORNER); // restore p5 default

        noStroke();
        noFill();

        // Optionally draw center point
        if (this.showCenter) {
            this.rCenter.drawOnGrid(grid);
        }
    }//end drawOnGrid

    // ── draw (screen-pixel version) ───────────────────────────────────────────
    /**
     * Draws using raw pixel coordinates (no grid mapping).
     */
    draw() {
        const pw = this.width  * this._scaleX;
        const ph = this.height * this._scaleY;
        const cr = Math.min(this.cornerRadius, pw / 2, ph / 2);
        const rotRad = -(this.rotation + this._animRotDeg) * Math.PI / 180;

        push();
        translate(this.rCenter.x, this.rCenter.y);
        rotate(rotRad);

        if (this.fillColor && this.fillColor.col)   { fill(this.fillColor.col); }   else { noFill(); }
        if (this.strokeColor && this.strokeColor.col) {
            stroke(this.strokeColor.col);
            strokeWeight(this.strokeWeight);
        } else { noStroke(); }

        rectMode(CENTER);
        rect(0, 0, pw, ph, cr);
        pop();
        rectMode(CORNER);
    }//end draw

    // ── Convenience getters ───────────────────────────────────────────────────

    /** Effective width at the current breathing scale */
    get currentWidth()       { return this.width  * this._scaleX; }
    /** Effective height at the current breathing scale */
    get currentHeight()      { return this.height * this._scaleY; }
    /** Area of the live (scaled) bounding rectangle */
    get currentArea()        { return this.currentWidth * this.currentHeight; }
    /** Aspect ratio of the live (scaled) shape */
    get currentAspectRatio() { return this.currentWidth / this.currentHeight; }

    // ── toString ──────────────────────────────────────────────────────────────
    toString() {
        const totalDeg = this.rotation + this._animRotDeg;
        return `SWRoundedRectangle(center: (${this.rCenter.x.toFixed(2)}, ${this.rCenter.y.toFixed(2)}), ` +
               `width: ${this.width.toFixed(2)}, height: ${this.height.toFixed(2)}, ` +
               `cornerRadius: ${this.cornerRadius.toFixed(2)}, area: ${this.area.toFixed(2)}, ` +
               `rotation: ${totalDeg.toFixed(1)}\u00b0)`;
    }//end toString

}//end class SWRoundedRectangle