▱ SketchWave Rounded Trapezoid Class Reference

Extends SWTrapezoid • Quadratic Bézier Corners • Inherited Breathing & Rotation

▶ Try SWRoundedTrapezoid Demo ▱ Parent: SWTrapezoid Reference

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.

What Is Inherited

The following are all inherited from SWTrapezoid with no changes:

Breathing

breathe(), breatheTop(), breatheBottom(), and transform() all work identically

Rotation

rotate() and the _rotAngle property work identically; pivot is always the bounding-box centre

Grid & Drag

drawOnGrid() and setCenter() are unchanged; all coordinate mapping comes from the parent

Setters & Geometry

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 so reset() returns to the new value
  • _drawAtPx() — fully overridden to use beginShape() / vertex() / quadraticVertex() instead of quad()
  • reset() — calls super.reset() then also restores cornerRadius
  • toString() — includes the r: 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:

BL = (−b/2,      +h/2)     bottom-left
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.

P0 P2 P1 r r Sharp (no rounding): Rounded (r = 40):

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:

  1. Subtract: dx = A.x − V.x,  dy = A.y − V.y
  2. Find length: len = √(dx² + dy²)
  3. Divide: ux = dx / len,  uy = dy / len
  4. 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.

Small radius

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.

Large (clamped) radius

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:

KeyTypeDefaultDescription
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:

MethodWhat 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.

Area    = (a + b) / 2 × h
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