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).
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).
Isosceles trapezoid — top base is centred; both legs are equal length
General trapezoid — offset shifts the top base left or right, creating unequal legs
Approaches a triangle as the top base shrinks toward zero
Top and bottom bases are equal — the shape becomes a rectangle
Features
- Regular / General —
regular = true(default) forces an isosceles trapezoid; setregular = falseand provide anoffsetfor a skewed shape - Fill & stroke — independent SWColor objects; pass
nullto suppress either layer - Three breathing modes —
breathe()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
- Drag —
setCenter(cx, cy)repositions the shape to any canvas location - Grid support —
drawOnGrid(grid)maps coordinates through a SWGrid for user-space drawings - Live geometry —
area,perimeter,leftLeg,rightLeg, andisIsoscelesare computed properties that update automatically during animation
swColor.js, and optionally swSinusoid.js
(for the breathe*() methods) and swGrid.js
(for drawOnGrid()).
Constructor
let trap = new SWTrapezoid(cx, cy, topBase, bottomBase, height, fillColor, options);
Required Parameters
| Parameter | Type | Description |
|---|---|---|
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:
| Key | Type | Default | Description |
|---|---|---|---|
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:
| Property | Formula | Notes |
|---|---|---|
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.
| Option | Type | Default | Effect |
|---|---|---|---|
sinusoid | SWSinusoid | null | Uniform breathing (null = skip) |
sinusoidTop | SWSinusoid | null | Top-base breathing (null = skip) |
sinusoidBottom | SWSinusoid | null | Bottom-base breathing (null = skip) |
t | number | 0 | Time in seconds |
degPerSec | number | null | Rotation 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
| Method | Parameter | Notes |
|---|---|---|
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-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.
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.
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