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.
SWRoundedRectangle → SWRectangleAll SWRectangle properties and methods are available on SWRoundedRectangle unless noted otherwise.
Constructor
new SWRoundedRectangle(center, width, height, fillColor, cornerRadius?, options?)
| Parameter | Type | Default | Description |
|---|---|---|---|
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 numberCorner 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 internaldrawOnGrid() to compute pixelWidth = width * _scaleX * grid.scaleX._scaleY number internal_animRotDeg number internalrotation property when drawing. Reset to 0 by reset().Inherited Properties
All of these are inherited from SWRectangle without modification:
| Property | Type | Description |
|---|---|---|
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. |
maxTrailLength | number | Maximum number of trail points to retain. |
Geometric Properties
These getters reflect the live, animated state of the shape (breathing scale applied).
currentWidth getterEffective width at the current breathing scale: width × _scaleX.
console.log(rr.currentWidth);
currentHeight getterheight × _scaleY.currentArea getterArea 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 gettercurrentWidth / currentHeight.Inherited Geometric Getters (from SWRectangle)
| Getter | Formula / Description |
|---|---|
area | width × height (nominal, un-scaled) |
perimeter | 2 × (width + height) (nominal) |
aspectRatio | width / height (nominal) |
diagonal | √(width² + height²) |
Classification
Inherited from SWRectangle:
isSquare gettertrue if Math.abs(width - height) < 0.001. A "rounded square" is still considered a square by this test.Drawing Methods
drawOnGrid(grid)
method
override
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
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
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
Calls super.rotateAboutCenter() and stores the accumulated angle in _animRotDeg = degPerSec × t.
rr.rotateAboutCenter(45, millis() / 1000); // 45°/s
transform(options)
method
override
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
Calls super.reset() and additionally resets _scaleX = 1, _scaleY = 1, _animRotDeg = 0.
rr.reset();
Utility Methods
toString()
method
override
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
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.
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).
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.
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.
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.
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.
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