▱ SketchWave Trapezoid Class Reference

Geometric Trapezoid • Three Breathing Modes • Rotation • Grid Support

▶ Try SWTrapezoid Demo

Overview

SWTrapezoid is a SketchWave shape class that draws a trapezoid — a quadrilateral with exactly one pair of parallel sides called the bases. The shape is positioned by its bounding-box centre, with the longer bottom base at the bottom and the shorter top base centered above (or offset to one side).

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

Where a is the top base, b is the bottom base, h is the perpendicular height, and offset is the horizontal shift of the top base from the vertical axis (0 when regular / isosceles).

regular = true

Isosceles trapezoid — top base is centred; both legs are equal length

regular = false

General trapezoid — offset shifts the top base left or right, creating unequal legs

a → 0

Approaches a triangle as the top base shrinks toward zero

a = b

Top and bottom bases are equal — the shape becomes a rectangle

Features

  • Regular / Generalregular = true (default) forces an isosceles trapezoid; set regular = false and provide an offset for a skewed shape
  • Fill & stroke — independent SWColor objects; pass null to suppress either layer
  • Three breathing modesbreathe() scales all dimensions uniformly, breatheTop() scales only the top base, breatheBottom() scales only the bottom base
  • Rotation — the shape spins about its bounding-box centre; supports a static pose angle separate from continuous spin
  • DragsetCenter(cx, cy) repositions the shape to any canvas location
  • Grid supportdrawOnGrid(grid) maps coordinates through a SWGrid for user-space drawings
  • Live geometryarea, perimeter, leftLeg, rightLeg, and isIsosceles are computed properties that update automatically during animation

Constructor

let trap = new SWTrapezoid(cx, cy, topBase, bottomBase, height, fillColor, options);

Required Parameters

ParameterTypeDescription
cx number Centre x of the bounding box — pixels when using draw(), user units when using drawOnGrid().
cy number Centre y of the bounding box. Same coordinate system as cx.
topBase number (≥ 1) Length of the shorter top base in pixels. Clamped to a minimum of 1.
bottomBase number (≥ topBase) Length of the longer bottom base in pixels. Clamped to be at least topBase.
height number (≥ 1) Perpendicular distance between the two bases in pixels. Clamped to a minimum of 1.
fillColor SWColor | null Fill colour of the trapezoid. Pass null for no fill (outline only).

Options Object

Pass any combination of these as a plain JS object in the seventh 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.
regular boolean true When true, the trapezoid is isosceles — top base is centred and offset is forced to 0. When false, offset controls the horizontal shift.
offset number 0 Horizontal shift of the top base from the centre axis in pixels. Only used when regular = false. Positive shifts the top base to the right.

Constructor Example

let trap;

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

    const fillC   = new SWColor(135, 60, 78, 100, "trapFill");
    const strokeC = new SWColor(135, 80, 36, 100, "trapStroke");

    // Regular isosceles trapezoid centred on the canvas
    trap = new SWTrapezoid(width / 2, height / 2, 100, 180, 80, fillC, {
        strokeColor:  strokeC,
        strokeWeight: 2,
        regular:      true
    });
}

function draw() {
    background(150, 20, 95);
    trap.draw();
}

// General (skewed) trapezoid — top base shifted 20 px to the right
trap = new SWTrapezoid(320, 240, 100, 180, 80, fillC, {
    strokeColor: strokeC,
    regular:     false,
    offset:      20
});

Key Properties

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

cx / cy

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

topBase / bottomBase / height

Type: number — The original baseline dimensions set at construction. These are the values that breathing animations scale around. Use setTopBase(), setBottomBase(), or setHeight() to update all three related fields at once.

_currentTop / _currentBottom / _currentHeight

Type: number — Live animated values set by the breathe*() methods. Read these (not topBase etc.) when you need the on-screen size during animation. All three are reset to their originals by reset().

regular

Type: boolean — true = isosceles (default); false = general trapezoid with independent leg lengths. Change via setRegular(v), which also zeros the offset when switching back to true.

offset

Type: number — Horizontal shift of the top base from the centre axis. Only meaningful when regular = false; forced to 0 when regular = true. Change via setOffset(off).

_rotAngle

Type: number (degrees) — Current rotation angle in degrees. Updated by rotate(). Reset to 0 by reset(). You can directly assign a value here to carry over a rotation angle or to apply a static pose angle when rebuilding.

fillColor / strokeColor

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

Computed Geometry (Read-Only)

These are JavaScript getters. They read _currentTop, _currentBottom, _currentHeight, and offset on every access, so they always reflect the live animated state:

PropertyFormulaNotes
area (a + b) / 2 × h Standard trapezoid area formula.
leftLeg √((b/2 − a/2 + offset)² + h²) Distance from bottom-left to top-left vertex.
rightLeg √((b/2 − a/2 − offset)² + h²) Distance from bottom-right to top-right vertex. Equals leftLeg when regular = true.
perimeter a + b + leftLeg + rightLeg Sum of all four sides.
isIsosceles |leftLeg − rightLeg| < 0.001 Returns true when both legs are equal (within floating-point tolerance).

Methods

Drawing Methods

draw()

Renders the trapezoid in screen (pixel) coordinates using the current animated values (_currentTop, _currentBottom, _currentHeight, _rotAngle). Call once per draw() loop frame.

function draw() {
    background(150, 20, 95);
    trap.draw();
}

drawOnGrid(grid)

Parameters: grid (SWGrid)

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

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

Animation Methods

breathe(sinusoid, t)

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

Scales all three dimensions proportionally: _currentTop, _currentBottom, and _currentHeight all multiply by sinusoid.getValue(t). The overall shape is preserved. Build the sinusoid with a phase of -Math.PI/2 so it 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
    breatheSin = new SWSinusoid(amp, freq, mid, -Math.PI / 2);
}

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

breatheTop(sinusoid, t)

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

Scales only the top base while bottom and height stay fixed. As the top base shrinks toward zero the shape morphs toward a triangle; as it grows toward the bottom base the shape approaches a rectangle. The bottom base and height remain at their original values.

// Top base oscillates — bottom and height stay fixed
trap.breatheTop(breatheSin, t);
trap.draw();

breatheBottom(sinusoid, t)

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

Scales only the bottom base while top and height stay fixed. As the bottom shrinks the shape becomes a taller, narrower trapezoid; at zero the shape becomes a triangle pointing downward.

// Bottom base oscillates — top and height stay fixed
trap.breatheBottom(breatheSin, t);
trap.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 always the bounding-box centre (cx, cy). In the demo sketch, _rotAngle is set directly rather than through this method so that a static pose angle can be added to the spinning rotation.

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

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

transform(options)

Combines breathing and rotation in a single call.

OptionTypeDefaultEffect
sinusoidSWSinusoidnullUniform breathing (null = skip)
sinusoidTopSWSinusoidnullTop-base breathing (null = skip)
sinusoidBottomSWSinusoidnullBottom-base breathing (null = skip)
tnumber0Time in seconds
degPerSecnumbernullRotation rate (null = skip)
// Uniform breathe + spin in one call
trap.transform({
    sinusoid:  breatheSin,
    t:         totalTime,
    degPerSec: 45
});

// Top-base breathe only
trap.transform({ sinusoidTop: topSin, t: totalTime });

reset()

Resets all animation state to initial values: _rotAngle returns to 0; _currentTop, _currentBottom, and _currentHeight return to their original baseline values. Does not reset property changes made via setters (colours, regular, offset, etc.).

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

Setter Methods

MethodParameterNotes
setCenter(cx, cy) two numbers Move the shape’s bounding-box centre. Safe to call every frame for mouse drag.
setTopBase(a) number Updates topBase, originalTopBase, and _currentTop. Minimum enforced at 1.
setBottomBase(b) number Updates bottomBase, originalBottomBase, and _currentBottom. Clamped to be at least topBase.
setHeight(h) number Updates height, originalHeight, and _currentHeight. Minimum enforced at 1.
setRegular(v) boolean Toggle isosceles mode. Passing true also forces offset to 0.
setOffset(off) number Set horizontal offset of the top base. Only effective when regular = false.
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.

Utility Methods

toString()

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

console.log(trap.toString());
// → "SWTrapezoid(cx:320.0, cy:240.0, top:100.0, bottom:180.0, h:80.0, regular:true)"

Local Coordinate Layout

SWTrapezoid uses a local coordinate system centred at the bounding-box centre. The shape is drawn using translate(cx, cy) then rotate(radians(_rotAngle)), so the four vertices in local space are:

Bottom-left: (−b/2,       +h/2)
Bottom-right: (+b/2,       +h/2)
Top-right:   (+a/2 + offset,  −h/2)
Top-left:    (−a/2 + offset, −h/2)

In p5.js screen coordinates, positive y points downward — so the vertex at +h/2 appears at the bottom of the canvas and the vertex at −h/2 appears at the top. The bottom base (b) is wider and sits below; the top base (a) is narrower and sits above.

When regular = true, offset = 0 and the shape is symmetric about the y-axis. When regular = false, a positive offset shifts the entire top base to the right, making the left leg longer than the right.

Regular (isosceles) — offset = 0

Both legs are equal. The shape has a vertical axis of symmetry. isIsosceles returns true. This is the default mode and the most common trapezoid shape.

Internally: offset = 0 is enforced regardless of what you pass in the constructor.

General — offset ≠ 0

Left and right legs differ in length. Positive offset shifts the top base right (left leg gets longer). Negative offset shifts it left (right leg gets longer). isIsosceles returns false.

Set regular: false in the options to enable this mode, then use setOffset() or the offset slider in the demo.

Usage Notes

Combining Breathing Modes

The three breathing methods are mutually exclusive per call — each one independently sets its target dimension(s). If you call breatheTop() and breatheBottom() in the same frame, both will update correctly:

// Top and bottom breathe at different rates
trap.breatheTop(fastSin, t);       // top oscillates quickly
trap.breatheBottom(slowSin, t);    // bottom oscillates slowly
trap.draw();

Pose Angle + Continuous Spin

The demo sketch exposes a Rotation Angle slider that sets a static pose angle independent of the animated spin. This is implemented by setting _rotAngle directly rather than calling rotate():

// In the draw() loop — combine pose angle with ongoing spin
if (rotationOn) {
    totalRotTime += deltaT;
    myTrap._rotAngle = rotationRate * totalRotTime + poseAngle;
} else {
    myTrap._rotAngle = poseAngle;   // pose only, no spin
}

When rebuilding the shape (e.g. after a slider change), carry over the current combined angle so the shape does not visually snap:

// After new SWTrapezoid(...) — restore accumulated angle
myTrap._rotAngle = totalRotTime * rotationRate + poseAngle;

Grid Integration

To draw on a SWGrid, set cx / cy and all dimensions in user coordinates when constructing, then call drawOnGrid(grid) instead of draw():

// User-space coordinates (grid units)
const trap = new SWTrapezoid(0, 0, 2, 4, 1.5, fillC);

function draw() {
    background(150, 20, 95);
    grid.updateScreenBounds();
    grid.draw();
    trap.drawOnGrid(grid);
}

Drag Support

Use a simple bounding-box hit test in p5’s mousePressed() and update the centre in mouseDragged():

function mousePressed() {
    const halfW = myTrap._currentBottom / 2 * 1.2;
    const halfH = myTrap._currentHeight / 2 * 1.2;
    if (abs(mouseX - myTrap.cx) <= halfW &&
        abs(mouseY - myTrap.cy) <= halfH) {
        isDragging  = true;
        dragOffsetX = mouseX - myTrap.cx;
        dragOffsetY = mouseY - myTrap.cy;
    }
}

function mouseDragged() {
    if (isDragging) {
        myTrap.setCenter(mouseX - dragOffsetX, mouseY - dragOffsetY);
    }
}

function mouseReleased() { isDragging = false; }

Source Code

Show / Hide Source Code
/*
File:    swTrapezoid.js
Date:    2026-05-18
Author:  klp
Workspace: SketchWaveTNT2026-05-01-Stg9
Purpose: SWTrapezoid class — a quadrilateral with one pair of parallel sides (the bases).

         Geometry (local coordinates, bounding-box centre at origin):
           Bottom base: from (–b/2, +h/2) to (+b/2, +h/2)   [visually lower in p5]
           Top    base: from (–a/2+off, –h/2) to (+a/2+off, –h/2)  [visually upper]
           off = offset shift of the top base (0 when regular / isosceles)

         When regular = true (default):
           The trapezoid is isosceles — both bases are centred on the same
           vertical axis and both non-parallel legs have equal length.

         Key formulas:
           Area      = (a + b) / 2 × h
           Left leg  = √((b/2 – a/2 + off)² + h²)
           Right leg = √((b/2 – a/2 – off)² + h²)
           Perimeter = a + b + leftLeg + rightLeg

Features:
  - Configurable fill colour, stroke colour, and stroke weight.
  - Regular (isosceles) or general trapezoid via options.regular and options.offset.
  - Rotation about the bounding-box centre.
  - Breathing: top base only, bottom base only, or all dimensions uniformly.
  - draw()           — pixel coordinates.
  - drawOnGrid(grid) — user coordinates mapped through SWGrid.

Dependencies: p5.js, swColor.js, swSinusoid.js, swGrid.js
Notes: assumes p5.js colorMode(HSB, 360, 100, 100, 100)
*/

console.log("[swTrapezoid.js] SWTrapezoid class loaded.");

class SWTrapezoid {

    constructor(cx, cy, topBase, bottomBase, height, fillColor, options = {}) {

        this.cx         = cx;
        this.cy         = cy;
        this.topBase    = max(1, topBase);
        this.bottomBase = max(this.topBase, bottomBase);
        this.height     = max(1, height);

        this.originalTopBase    = this.topBase;
        this.originalBottomBase = this.bottomBase;
        this.originalHeight     = this.height;

        this.fillColor    = fillColor           ? SWColor.copy(fillColor)           : null;
        this.strokeColor  = options.strokeColor ? SWColor.copy(options.strokeColor) : null;
        this.strokeWeight = options.strokeWeight ?? 2;
        this.regular      = options.regular !== false;
        this.offset       = this.regular ? 0 : (options.offset ?? 0);

        this._rotAngle      = 0;
        this._currentTop    = this.topBase;
        this._currentBottom = this.bottomBase;
        this._currentHeight = this.height;

    }//end constructor

    get leftLeg() {
        const off = this.regular ? 0 : this.offset;
        const dx  = this._currentBottom / 2 - this._currentTop / 2 + off;
        return Math.sqrt(dx * dx + this._currentHeight * this._currentHeight);
    }

    get rightLeg() {
        const off = this.regular ? 0 : this.offset;
        const dx  = this._currentBottom / 2 - this._currentTop / 2 - off;
        return Math.sqrt(dx * dx + this._currentHeight * this._currentHeight);
    }

    get area()      { return (this._currentTop + this._currentBottom) / 2 * this._currentHeight; }
    get perimeter() { return this._currentTop + this._currentBottom + this.leftLeg + this.rightLeg; }
    get isIsosceles() { return Math.abs(this.leftLeg - this.rightLeg) < 0.001; }

    draw() {
        this._drawAtPx(this.cx, this.cy,
                       this._currentTop, this._currentBottom, this._currentHeight);
    }

    drawOnGrid(grid) {
        const { x: sx, y: sy } = grid.userToScreen(this.cx, this.cy);
        const sTop    = this._currentTop    * grid.xScale;
        const sBottom = this._currentBottom * grid.xScale;
        const sHeight = this._currentHeight * Math.abs(grid.yScale);
        this._drawAtPx(sx, sy, sTop, sBottom, sHeight);
    }

    _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;
        const hb = bottom / 2;
        const ht = top    / 2;
        quad(-hb, +hh, hb, +hh, ht + off, -hh, -ht + off, -hh);
        pop();
    }

    breathe(sinusoid, t) {
        const scale         = sinusoid.getValue(t);
        this._currentTop    = this.originalTopBase    * scale;
        this._currentBottom = this.originalBottomBase * scale;
        this._currentHeight = this.originalHeight     * scale;
    }

    breatheTop(sinusoid, t) {
        this._currentTop = this.originalTopBase * sinusoid.getValue(t);
    }

    breatheBottom(sinusoid, t) {
        this._currentBottom = this.originalBottomBase * sinusoid.getValue(t);
    }

    rotate(degPerSec, t) { this._rotAngle = degPerSec * t; }

    transform({ sinusoid = null, sinusoidTop = null, sinusoidBottom = null,
                t = 0, degPerSec = null } = {}) {
        if (sinusoid       !== null) this.breathe(sinusoid, t);
        if (sinusoidTop    !== null) this.breatheTop(sinusoidTop, t);
        if (sinusoidBottom !== null) this.breatheBottom(sinusoidBottom, t);
        if (degPerSec      !== null) this.rotate(degPerSec, t);
    }

    reset() {
        this._rotAngle      = 0;
        this._currentTop    = this.originalTopBase;
        this._currentBottom = this.originalBottomBase;
        this._currentHeight = this.originalHeight;
    }

    setCenter(cx, cy) { this.cx = cx; this.cy = cy; }

    setTopBase(a) {
        this.topBase = this.originalTopBase = this._currentTop = max(1, a);
    }
    setBottomBase(b) {
        this.bottomBase = this.originalBottomBase = this._currentBottom = max(this.topBase, b);
    }
    setHeight(h) {
        this.height = this.originalHeight = this._currentHeight = max(1, h);
    }
    setRegular(v) { this.regular = !!v; if (this.regular) this.offset = 0; }
    setOffset(off) { if (!this.regular) this.offset = off; }
    setFillColor(c)    { this.fillColor    = c ? SWColor.copy(c) : null; }
    setStrokeColor(c)  { this.strokeColor  = c ? SWColor.copy(c) : null; }
    setStrokeWeight(w) { this.strokeWeight = max(0, w); }

    toString() {
        return `SWTrapezoid(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)}, regular:${this.regular})`;
    }

}//end class SWTrapezoid