Overview
SWRoundedTrapezoid extends SWTrapezoid
by replacing each sharp vertex with a smooth rounded corner. All breathing modes,
rotation, grid support, and drag behaviour are inherited unchanged — only the
internal drawing method _drawAtPx() is overridden. The one new property
is cornerRadius: the rounding amount in pixels.
SWRoundedTrapezoid extends SWTrapezoid is the correct OOP design. The child
class inherits all the capabilities of the parent and adds only what is new. You
can use a SWRoundedTrapezoid anywhere a SWTrapezoid is expected.
What Is Inherited
The following are all inherited from SWTrapezoid with no changes:
breathe(), breatheTop(), breatheBottom(), and transform() all work identically
rotate() and the _rotAngle property work identically; pivot is always the bounding-box centre
drawOnGrid() and setCenter() are unchanged; all coordinate mapping comes from the parent
All parent setters, area, perimeter, leftLeg, rightLeg, and isIsosceles are available
What Is New
cornerRadius— the amount of rounding at each corner in pixels (clamped to ≥ 0 and auto-clamped at draw time to prevent arcs from overlapping)setCornerRadius(r)— setter that updates both the current and original radius soreset()returns to the new value_drawAtPx()— fully overridden to usebeginShape()/vertex()/quadraticVertex()instead ofquad()reset()— callssuper.reset()then also restorescornerRadiustoString()— includes ther:field in its output
📐 How the Curves Are Made
This section explains, step by step, how _drawAtPx() turns a plain
trapezoid into one with smooth rounded corners. The technique uses
quadratic Bézier curves via p5.js’s built-in
quadraticVertex() function.
Step 1 — The Underlying Sharp Trapezoid
The parent class, SWTrapezoid, draws a sharp four-vertex shape using
p5’s quad() function. The four corners in local coordinates
(before translation and rotation) are:
BR = (+b/2, +h/2) bottom-right
TR = (+a/2 + off, −h/2) top-right
TL = (−a/2 + off, −h/2) top-left
Where a = top base, b = bottom base,
h = height, and off = offset (0 when
regular = true).
Step 2 — What Is a Quadratic Bézier Curve?
A quadratic Bézier curve is defined by exactly three points:
📌 The Three Points
- P0 (start) — Where the curve begins. The pen is already here when you call
quadraticVertex(). - P1 (control point) — The “magnet”. The curve is pulled toward this point but usually does not pass through it.
- P2 (end) — Where the curve arrives.
Think of it as stretching a rubber band between P0 and P2 while someone at P1 pulls the band toward them. The more they pull, the tighter the curve.
In p5.js, you use it like this:
beginShape();
vertex(P0.x, P0.y); // move to start
quadraticVertex(P1.x, P1.y, P2.x, P2.y); // draw curve to end, pulled by control
// ... more vertex / quadraticVertex calls
endShape(CLOSE);
The key insight: if P1 sits exactly at the sharp corner, and P0 and P2 sit on the two edges approaching that corner, then the curve smoothly rounds the corner. The distance from the corner to P0 (and corner to P2) is the corner radius — bigger distance means a wider, gentler curve.
The red dot P1 is the sharp corner. P0 and P2 are pulled back along each edge by distance r. The green curve replaces the corner.
Step 3 — Finding P0 and P2 with Unit Vectors
To place P0 and P2 exactly r pixels back along each edge from the
corner, the code uses a unit vector — a direction-only vector
with length 1.
🧭 Unit Vector Recipe
To get a unit vector pointing from point V toward point A:
- Subtract:
dx = A.x − V.x,dy = A.y − V.y - Find length:
len = √(dx² + dy²) - Divide:
ux = dx / len,uy = dy / len - Result
(ux, uy)points from V toward A with length exactly 1
Multiplying that unit vector by r gives a point exactly r
pixels away from the corner:
// Unit vector from corner V toward the previous vertex (prev)
function unitTo(a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const len = Math.sqrt(dx * dx + dy * dy);
if (len === 0) return { x: 0, y: 0 };
return { x: dx / len, y: dy / len };
}
// P0: r pixels back along the "incoming" edge (V toward prev)
const u1 = unitTo(V, prev);
const P0 = { x: V.x + r * u1.x, y: V.y + r * u1.y };
// P2: r pixels forward along the "outgoing" edge (V toward next)
const u2 = unitTo(V, next);
const P2 = { x: V.x + r * u2.x, y: V.y + r * u2.y };
// P1: the corner itself
const P1 = V; // the control point = the sharp corner
Step 4 — The roundedCorner() Helper
The class bundles this logic into a tiny helper that draws one rounded corner from a previous vertex, through a corner vertex, toward the next vertex. It adds a straight-line segment from the current pen position to P0, then draws the Bézier to P2:
function roundedCorner(prev, V, next) {
const u1 = unitTo(V, prev);
const u2 = unitTo(V, next);
// Move to P0 (r pixels back from corner along incoming edge)
vertex(V.x + r * u1.x, V.y + r * u1.y);
// Quadratic Bézier: control = V (the corner), end = P2
quadraticVertex(V.x, V.y,
V.x + r * u2.x, V.y + r * u2.y);
}
Calling this for each corner in order produces a complete rounded shape:
beginShape();
roundedCorner(TL, BL, BR); // bottom-left corner
roundedCorner(BL, BR, TR); // bottom-right corner
roundedCorner(BR, TR, TL); // top-right corner
roundedCorner(TR, TL, BL); // top-left corner
endShape(CLOSE);
p5.js automatically draws straight lines between the end of one Bézier and the
start of the next vertex() call, so the flat edges of the trapezoid
connect the rounded corners for free.
Step 5 — Radius Clamping
If r is too large, the two rounded arcs at opposite ends of a short edge
will overlap, creating a figure-eight or other visual glitch. To prevent this, the
radius is clamped every time the shape is drawn:
const r = Math.max(0, Math.min(
this.cornerRadius, // what the user set
maxByHalfTop / 2, // no more than half of the top base ÷ 2
maxByHalfBottom / 2, // no more than half of the bottom base ÷ 2
maxByHalfH / 2, // no more than half of the height ÷ 2
leftLen / 2, // no more than half the left leg
rightLen / 2 // no more than half the right leg
));
This means you can drag the Corner Radius slider all the way to its maximum and the
shape will still look clean — the radius silently reduces to the largest safe
value. The cornerRadius property always holds the requested
value; the clamped value is used only during drawing.
When cornerRadius is small relative to
the shape’s dimensions, the Bézier arcs only clip the
tips of the corners. The flat edges dominate. The shape looks
like a normal trapezoid with just slightly softened corners.
When cornerRadius approaches the maximum
safe value, the flat edges nearly disappear and the shape begins to
look more like a rounded pill or stadium shape. The clamping ensures
the arcs never go past the midpoints of the edges.
Why Not Use p5’s arc()?
p5.js’s rect() function accepts corner-radius arguments, but it
only works for axis-aligned rectangles. For a trapezoid (which has non-right angles
at the corners, especially when it is skewed or rotated), rect() cannot
be used.
An alternative would be to use four arc() calls — one per corner —
each inscribed in the angle between the two edges. This works but requires computing
the bisecting angle at each corner, which involves atan2() and careful
angle arithmetic.
The quadratic Bézier approach used here is simpler: it only requires basic subtraction and a square root to find unit vectors. The resulting curve is not a true circular arc (a quadratic Bézier is a parabola, not a circle), but visually the difference is imperceptible for typical corner radii, and the code is much easier to understand and maintain.
Constructor
let rt = new SWRoundedTrapezoid(cx, cy, topBase, bottomBase, height, fillColor, options);
The constructor signature is identical to SWTrapezoid except for one
additional key in the options object: cornerRadius.
Required Parameters
Same as SWTrapezoid — see the
SWTrapezoid Reference for full details.
Options Object
All SWTrapezoid options apply, plus:
| Key | Type | Default | Description |
|---|---|---|---|
cornerRadius |
number | 10 | Corner rounding amount in pixels. Clamped to ≥ 0 in the constructor; further clamped at draw time to prevent arc overlap. |
strokeColor |
SWColor | null | null | Inherited from SWTrapezoid — outline colour. |
strokeWeight |
number | 2 | Inherited from SWTrapezoid — stroke width in pixels. |
regular |
boolean | true | Inherited from SWTrapezoid — isosceles mode. |
offset |
number | 0 | Inherited from SWTrapezoid — top-base horizontal shift. Only active when regular = false. |
Constructor Example
let rt;
function setup() {
createCanvas(640, 480);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
const fillC = new SWColor(135, 60, 78, 100, "rtFill");
const strokeC = new SWColor(135, 80, 36, 100, "rtStroke");
// Rounded isosceles trapezoid, 20 px corner radius
rt = new SWRoundedTrapezoid(width / 2, height / 2, 100, 180, 80, fillC, {
strokeColor: strokeC,
strokeWeight: 2,
regular: true,
cornerRadius: 20
});
}
function draw() {
background(150, 20, 95);
rt.draw();
}
// Skewed with large rounded corners
rt = new SWRoundedTrapezoid(320, 240, 80, 200, 90, fillC, {
strokeColor: strokeC,
regular: false,
offset: 25,
cornerRadius: 30
});
New Property: cornerRadius
cornerRadius
Type: number — The requested corner rounding radius in pixels.
Set at construction via options.cornerRadius (default 10), clamped to ≥ 0.
Use setCornerRadius(r) to change it after construction.
Note: This is the requested value. The
actual radius applied during drawing may be smaller if the shape’s dimensions
would otherwise cause arc overlap. Read cornerRadius for the slider value;
the clamped draw-time value is not stored separately.
originalCornerRadius
Type: number — The corner radius at the time it was last set
(via constructor or setCornerRadius()). Used by reset()
to restore the radius after an animation or rebuild. Updated by
setCornerRadius().
New & Overridden Methods
New Setter
setCornerRadius(r)
Parameter: r (number) — New corner radius in pixels; clamped to ≥ 0.
Updates both cornerRadius and originalCornerRadius so that
a subsequent reset() call returns to this new value rather than the
constructor’s value.
rt.setCornerRadius(30); // increase rounding
rt.setCornerRadius(0); // remove rounding — looks identical to SWTrapezoid
Overridden Methods
_drawAtPx(cx, cy, top, bottom, h)
Private — do not call directly. Called internally by
both draw() and drawOnGrid(). Overrides the parent’s
version to use beginShape() / vertex() /
quadraticVertex() / endShape(CLOSE) instead of
quad(). All corner rounding and radius clamping happens here.
reset()
Calls super.reset() (which restores rotation, top base, bottom base,
and height to their original values) and then additionally restores
cornerRadius to originalCornerRadius.
rt.reset(); // restores dimensions, rotation, AND cornerRadius
toString()
Returns: string — Human-readable summary that includes the
r: field for the current corner radius.
console.log(rt.toString());
// → "SWRoundedTrapezoid(cx:320.0, cy:240.0, top:100.0, bottom:180.0, h:80.0, r:20.0, regular:true)"
All Inherited Methods (Unchanged)
See the SWTrapezoid Reference for full documentation on these inherited methods:
| Method | What it does |
|---|---|
draw() | Render in screen (pixel) coordinates |
drawOnGrid(grid) | Render mapped through a SWGrid |
breathe(sinusoid, t) | Scale all dimensions uniformly |
breatheTop(sinusoid, t) | Scale top base only |
breatheBottom(sinusoid, t) | Scale bottom base only |
rotate(degPerSec, t) | Spin about bounding-box centre |
transform(options) | Breathe + rotate in one call |
setCenter(cx, cy) | Move the shape |
setTopBase(a) | Change top base |
setBottomBase(b) | Change bottom base |
setHeight(h) | Change height |
setRegular(v) | Toggle isosceles mode |
setOffset(off) | Shift top base horizontally |
setFillColor(c) | Change fill colour (null = no fill) |
setStrokeColor(c) | Change stroke colour (null = no stroke) |
setStrokeWeight(w) | Change stroke weight |
Geometry Note
The area and perimeter getters are inherited from
SWTrapezoid and compute the geometry of the underlying sharp
trapezoid — they do not account for the fact that the corners are
actually curves. This is an accepted simplification: for typical corner radii
the difference is small, and the formulas remain easy to understand and teach.
Left leg = √((b/2 − a/2 + offset)² + h²)
Right leg = √((b/2 − a/2 − offset)² + h²)
Perimeter = a + b + left leg + right leg
Source Code
Show / Hide Source Code
/*
File: swRoundedTrapezoid.js
Date: 2026-05-18
Author: klp
Workspace: SketchWaveTNT2026-05-01-Stg9
Purpose: SWRoundedTrapezoid — extends SWTrapezoid with rounded corners.
SWRoundedTrapezoid inherits all animation (breathe, breatheTop, breatheBottom,
transform, rotate), grid support, dragging, and setter logic from SWTrapezoid.
It overrides _drawAtPx() to draw smooth rounded corners instead of sharp
vertices, using p5's beginShape() / vertex() / quadraticVertex() approach.
Key design notes:
- cornerRadius is in pixels (same coordinate space as topBase, bottomBase, height).
- The radius is clamped at draw-time to prevent corner arcs from overlapping.
- Only _drawAtPx(), reset(), and toString() are overridden; everything else
(breathing, rotation, setters, grid mapping) is inherited unchanged.
Dependencies: p5.js, swColor.js, swSinusoid.js, swGrid.js, swTrapezoid.js
Notes: assumes p5.js colorMode(HSB, 360, 100, 100, 100)
*/
console.log("[swRoundedTrapezoid.js] SWRoundedTrapezoid class loaded.");
class SWRoundedTrapezoid extends SWTrapezoid {
constructor(cx, cy, topBase, bottomBase, height, fillColor, options = {}) {
super(cx, cy, topBase, bottomBase, height, fillColor, options);
this.cornerRadius = Math.max(0, options.cornerRadius ?? 10);
this.originalCornerRadius = this.cornerRadius;
}
_drawAtPx(cx, cy, top, bottom, h) {
const off = this.regular ? 0 : this.offset;
push();
if (this.fillColor) { fill(this.fillColor.col); }
else { noFill(); }
if (this.strokeColor) {
stroke(this.strokeColor.col);
strokeWeight(this.strokeWeight);
strokeJoin(ROUND);
} else { noStroke(); }
translate(cx, cy);
rotate(radians(this._rotAngle));
const hh = h / 2, hb = bottom / 2, ht = top / 2;
const BL = { x: -hb, y: +hh };
const BR = { x: +hb, y: +hh };
const TR = { x: +ht + off, y: -hh };
const TL = { x: -ht + off, y: -hh };
const dxLeft = BL.x - TL.x, dyLeft = BL.y - TL.y;
const dxRight = BR.x - TR.x, dyRight = BR.y - TR.y;
const leftLen = Math.sqrt(dxLeft * dxLeft + dyLeft * dyLeft);
const rightLen = Math.sqrt(dxRight * dxRight + dyRight * dyRight);
const r = Math.max(0, Math.min(
this.cornerRadius,
top / 2 / 2, bottom / 2 / 2, h / 2 / 2,
leftLen / 2, rightLen / 2
));
function unitTo(a, b) {
const dx = b.x - a.x, dy = b.y - a.y;
const len = Math.sqrt(dx * dx + dy * dy);
if (len === 0) return { x: 0, y: 0 };
return { x: dx / len, y: dy / len };
}
function roundedCorner(prev, V, next) {
const u1 = unitTo(V, prev), u2 = unitTo(V, next);
vertex(V.x + r * u1.x, V.y + r * u1.y);
quadraticVertex(V.x, V.y, V.x + r * u2.x, V.y + r * u2.y);
}
beginShape();
roundedCorner(TL, BL, BR);
roundedCorner(BL, BR, TR);
roundedCorner(BR, TR, TL);
roundedCorner(TR, TL, BL);
endShape(CLOSE);
pop();
}
reset() {
super.reset();
this.cornerRadius = this.originalCornerRadius;
}
setCornerRadius(r) {
this.cornerRadius = this.originalCornerRadius = Math.max(0, r);
}
toString() {
return `SWRoundedTrapezoid(cx:${this.cx.toFixed(1)}, cy:${this.cy.toFixed(1)}, ` +
`top:${this._currentTop.toFixed(1)}, bottom:${this._currentBottom.toFixed(1)}, ` +
`h:${this._currentHeight.toFixed(1)}, r:${this.cornerRadius.toFixed(1)}, ` +
`regular:${this.regular})`;
}
}//end class SWRoundedTrapezoid