⬡ SWRegularPolygon Class Reference

Regular n-Sided Polygon with Breathing & Spinning — SketchWaveJS Stage

Overview

SWRegularPolygon is a standalone SketchWaveJS class for drawing a regular n-sided polygon on a p5.js canvas. A regular polygon has all sides of equal length and all interior angles equal, and is fully specified by a center point, a circumradius (the distance from center to any vertex), and a number of sides n ≥ 3.

The class is modelled after SWEllipse — it is not a subclass of SWRectangle. Vertices are computed on the fly from the circumradius and current animation state, so no vertex array is stored between frames.

FeatureDetails
Extends none (standalone)
Drawing beginShape / vertex / endShape (sharp) or curveVertex (rounded)
Animation Breathing via single SWSinusoid, rotation via _animRotDeg
First vertex Top (−90° before rotation) — triangle points up by default
Draggable Yes, via centerContainsPoint()

Dependency Chain

p5.js → SWColor → SWPoint → SWGrid → SWSinusoid → SWRegularPolygon

Constructor

new SWRegularPolygon(center, radius, numSides, fillColor, options)
ParameterTypeDescription
center SWPoint Center of the polygon in user coordinates
radius number Circumradius in user units (center → vertex)
numSides number Number of sides; automatically clamped to ≥ 3 and rounded to integer
fillColor SWColor Interior fill color
options Object See table below

Options Object

KeyTypeDefaultDescription
cornerRadius number 0 0 = sharp corners; >0 activates curveVertex rounding
strokeColor SWColor Border color (omit for no border)
strokeWeight number 2 Border thickness in pixels
showCenter boolean false Draw the center SWPoint dot
showVertices boolean false Draw a small dot at each vertex
rotation number 0 Initial static rotation in degrees CCW
const poly = new SWRegularPolygon(
    new SWPoint(0, 0),          // center at origin
    5,                          // circumradius = 5 user units
    6,                          // hexagon
    SWColor.fromHex("#3dd6f5"), // teal fill
    {
        cornerRadius:  0,
        strokeColor:   SWColor.fromHex("#0369a1"),
        strokeWeight:  2,
        showVertices:  false,
        rotation:      0
    }
);

Properties

Instance Properties

center : SWPoint
Center point in user coordinates. Draggable at runtime.
radius : number
Circumradius in user units. The actual drawn radius is radius × _scaleR. Change this at runtime to resize the polygon; call reset() to revert.
numSides : number (integer ≥ 3)
Number of sides. Change this at runtime — the polygon will immediately redraw with the new side count. Note: modifying numSides after construction does not rebuild the center or color; it only changes vertex geometry.
cornerRadius : number
Corner rounding radius in user units. 0 = sharp corners (vertex); any positive value activates quadratic bezier chamfering: the corner is cut cornerRadius user-units along each adjacent edge, and a smooth arc is drawn through the original vertex as the control point. The slider range 0–2 gives visible, smoothly progressive rounding across the full range.
rotation : number (degrees)
Static rotation applied before any animation rotation. CCW positive. The first vertex is at the top (−90°) before this is applied.
fillColor : SWColor  |  strokeColor : SWColor  |  strokeWeight : number
Appearance properties — can be changed at any time and will take effect on the next drawOnGrid() call.

Animation State (read only in practice)

_scaleR : number
Current breathing scale for the circumradius. Set exclusively by breathe() and transform(). Minimum 0.1.
_animRotDeg : number
Cumulative animation rotation in degrees CCW. Set exclusively by rotateAboutCenter() and transform(). Total displayed rotation = rotation + _animRotDeg.

Geometric Properties (Getters)

All getters use currentRadius = radius × _scaleR, so they reflect the live animated state.

PropertyFormulaNotes
currentRadius r × _scaleR Circumradius at the current animation scale
sideLength 2r · sin(π/n) Length of one side
apothem r · cos(π/n) Perpendicular distance from center to a side midpoint (inradius)
area ½ · n · r² · sin(2π/n) Area of the polygon
perimeter n · sideLength Total perimeter
interiorAngle (n − 2) · 180 / n Each interior angle in degrees; e.g. 60° for triangle, 120° for hexagon
exteriorAngle 360 / n Each exterior angle in degrees; all exterior angles of any convex polygon sum to 360°

Drawing Methods

drawOnGrid(grid)
Method

The primary draw method. Computes all vertex positions in user coordinates, maps them to screen pixels via grid.userToScreen(), then draws the polygon path using either vertex or curveVertex depending on cornerRadius.

If showVertices is true, a small filled dot is drawn at each vertex. If showCenter is true, the center SWPoint is drawn.

draw()
Method

Pixel-space draw — treats center.x/y and radius as raw pixel values. Use when you are not working with an SWGrid.

Sharp vs. Rounded Corner Detail

ModecornerRadiusp5 API usedVisual result
Sharp 0 beginShape / vertex / endShape(CLOSE) Hard corners exactly at vertex positions
Rounded >0 beginShape / vertex + quadraticVertex / endShape(CLOSE) Each corner is chamfered by cornerRadius user-units; arc size scales proportionally with the slider

Animation Methods

breathe(sinusoid, t)
Method

Sets _scaleR to sinusoid.getValue(t) (clamped to ≥ 0.1). Pass null for sinusoid to freeze at the current time with _scaleR = 1.

poly.breathe(sinusoid, t - breathingStartTime);
rotateAboutCenter(degPerSec, t)
Method

Sets _animRotDeg = degPerSec × t. Use with a frame-accumulating timer (totalRotationTime) so the polygon spins smoothly without resetting. CCW positive.

poly.rotateAboutCenter(rotationRate, totalRotationTime);
totalRotationTime += 1 / frameRate();
transform({ sinusoid, t, degPerSec })
Method

Convenience method that sets both _scaleR and _animRotDeg in one call. Equivalent to calling breathe() and rotateAboutCenter() separately.

reset()
Method

Restores radius to originalRadius, resets _scaleR = 1, _animRotDeg = 0, and restores the center to originalCenter. Does not reset the static rotation property.

Utility Methods

getVerticesUserCoords()
Method

Returns an array of { x, y } objects representing vertex positions in user coordinates, accounting for the current _scaleR and total rotation. Useful for computing distances to vertices or placing annotations.

drawVerticesOnGrid(grid, dotSize, dotColor)
Method

Draws a filled circle at each vertex position. dotSize defaults to 8 px; dotColor defaults to the polygon's stroke color.

centerContainsPoint(px, py, grid, tolerance)
Method

Returns true if the screen point (px, py) is within tolerance pixels of the polygon's center. Used to implement drag-to-reposition in the sketch.

toString()
Method

Returns a human-readable summary string.

console.log(poly.toString());
// → SWRegularPolygon(center: (0.00, 0.00), radius: 5.00,
//     sides: 6, sideLength: 5.00, area: 64.95, rotation: 0.0°)

Examples

Basic Setup — Hexagon

let grid, poly;

function setup() {
    createCanvas(600, 500);
    grid = new SWGrid({ UL: new SWPoint(-12, 10), LR: new SWPoint(12, -10) });
    grid.init(width, height);

    const fill   = SWColor.fromHex("#3dd6f5");
    fill.setAlphaTo(70);
    const stroke = SWColor.fromHex("#0369a1");

    poly = new SWRegularPolygon(
        new SWPoint(0, 0), // center
        5,                 // circumradius
        6,                 // hexagon
        fill,
        { strokeColor: stroke, strokeWeight: 2 }
    );
}

function draw() {
    background(240);
    grid.drawOnScreen();
    poly.drawOnGrid(grid);
}

Breathing

let sinusoid;
let breathingOn = false, startTime;

function setup() {
    // ... grid and poly as above ...
    const amp  = (1.5 - 0.7) / 2;           // 0.4
    const freq = (2 * Math.PI) / 2.0;       // period = 2 s
    const mid  = (0.7 + 1.5) / 2;           // 1.1
    sinusoid   = new SWSinusoid(amp, freq, mid, -Math.PI / 6);
}

function draw() {
    background(240);
    grid.drawOnScreen();
    if (breathingOn) poly.breathe(sinusoid, millis() / 1000 - startTime);
    poly.drawOnGrid(grid);
}

function keyPressed() {
    if (key === 'b') {
        breathingOn = !breathingOn;
        if (breathingOn) startTime = millis() / 1000;
    }
}

Spinning + Breathing Together

let totalRotT = 0;
const FR = 30;

function draw() {
    background(240);
    grid.drawOnScreen();
    poly.breathe(sinusoid, millis() / 1000 - startTime);
    poly.rotateAboutCenter(45, totalRotT); // 45°/s
    totalRotT += 1 / FR;
    poly.drawOnGrid(grid);
}

Rounded Corners — Slime Amoeba Effect

// Set cornerRadius > 0 at construction or at runtime:
poly.cornerRadius = 1;   // any positive value activates curveVertex mode

// Combined with breathing and a triangle (3 sides):
const amoeba = new SWRegularPolygon(
    new SWPoint(0, 0), 4, 3, fill,
    { cornerRadius: 1, strokeWeight: 0 }
);
// Animate with a slow sinusoid → organic, amoeba-like pulsing

Changing Sides at Runtime

// Slide numSides from 3 to 12 and rebuild:
function changeNumSides(n) {
    const cx = poly.center.x, cy = poly.center.y;
    poly = new SWRegularPolygon(new SWPoint(cx, cy), polyRadius, n, fill, opts);
}
// The polygon seamlessly transitions shape each time numSides changes.

Vertex Inspection

// Get vertex positions (user coords):
const verts = poly.getVerticesUserCoords();
for (const v of verts) {
    console.log(`(${v.x.toFixed(2)}, ${v.y.toFixed(2)})`);
}

// Draw dots at vertices:
poly.drawVerticesOnGrid(grid, 10, SWColor.fromHex("#ff6b00"));

Tips & Best Practices

📌 Circumradius vs. Inradius
The radius parameter is the circumradius — the distance from the center to a vertex. The apothem getter gives you the inradius — the distance from the center to the midpoint of a side (useful for collision detection against sides).
📌 First Vertex Orientation
The first vertex is always placed at the top (−90° = 12 o'clock) before rotation is applied. A triangle therefore points up by default. Rotate by 30° to get a flat-top hexagon; rotate by 45° to get a diamond from a square.
📌 Rebuilding After Side-Count Change
You can change poly.numSides directly, but the center-point style (strokeColor, strokeWeight) will not be refreshed. For a clean rebuild, call buildPoly() with the new side count — the sketch pattern shown in the demo is the recommended approach.
📌 Corner Rounding is Quadratic Bezier Chamfering
Setting cornerRadius > 0 cuts each vertex by cornerRadius user-units along both adjacent edges, then draws a quadratic arc through the original vertex as the control point. The full slider range (0–2) gives smoothly progressive rounding — small values produce subtle chamfers, large values approach a circle. The radius is automatically clamped to 45% of the shorter adjacent edge so corners never overlap.
📌 Breathing vs. Independent X/Y Scale
SWRegularPolygon scales the circumradius uniformly (single _scaleR). If you need asymmetric deformation (e.g., squash-and-stretch), apply separate X/Y scaling to the pixel coordinates after the userToScreen mapping — this is not built in and would produce an irregular polygon.
📌 Performance — Vertex Computation
_computeUserVertices() is called on every drawOnGrid() call. For most uses (up to ~60 fps, 12 sides) this is negligible. If you are drawing many polygons simultaneously, cache the result with getVerticesUserCoords() if you need it multiple times in the same frame.

Source Code

Complete source for swRegularPolygon.js:

/*
File:    swRegularPolygon.js
Date:    2026-02-28
Author:  klp + GitHub Copilot
App:     SketchWaveTNT2026-02-28-Stg6
Purpose: SWRegularPolygon — a regular n-sided polygon with optional rounded
         corners, breathing, and rotation animation.

  A regular polygon is defined by a center SWPoint, a circumradius (the distance
  from the center to any vertex), and a number of sides (>= 3).  Vertices are
  computed on the fly from these three values plus the current animation state —
  no stored vertex array is needed between frames.

  Two drawing modes are supported:
    sharp   (cornerRadius = 0) — uses beginShape / vertex / endShape(CLOSE)
    rounded (cornerRadius > 0) — uses curveVertex for a closed Catmull-Rom spline

  Breathing scales the circumradius uniformly via a single SWSinusoid (_scaleR).
  Rotation accumulates in _animRotDeg (degrees CCW).

  Dependencies: p5.js, SWColor, SWPoint, SWGrid, SWSinusoid
*/

console.log("[swRegularPolygon.js] SWRegularPolygon class loaded.");

class SWRegularPolygon {

    constructor(center, radius, numSides, fillColor, options = {}) {
        this.center       = center;
        this.radius       = radius;
        this.numSides     = Math.max(3, Math.round(numSides));
        this.fillColor    = fillColor;
        this.cornerRadius = options.cornerRadius  !== undefined ? options.cornerRadius  : 0;
        this.strokeColor  = options.strokeColor   || undefined;
        this.strokeWeight = options.strokeWeight  !== undefined ? options.strokeWeight  : 2;
        this.showCenter   = options.showCenter    !== undefined ? options.showCenter    : false;
        this.showVertices = options.showVertices  !== undefined ? options.showVertices  : false;
        this.rotation     = options.rotation      || 0;

        const cwt = this.strokeWeight > 0 ? this.strokeWeight : 4;
        const cc  = (this.strokeColor && this.strokeColor.col)
                        ? this.strokeColor
                        : (typeof swBlack !== 'undefined' ? swBlack : new SWColor(0, 0, 0, 100, "black"));
        this.center.strokeWeight = cwt;
        this.center.strokeColor  = cc;

        this._scaleR     = 1;
        this._animRotDeg = 0;
        this.originalRadius = radius;
        this.originalCenter = new SWPoint(center.x, center.y, undefined, cwt, cc);
    }

    get currentRadius() { return this.radius * this._scaleR; }
    get sideLength()    { return 2 * this.currentRadius * Math.sin(Math.PI / this.numSides); }
    get apothem()       { return this.currentRadius * Math.cos(Math.PI / this.numSides); }
    get area()          { const r = this.currentRadius; return 0.5 * this.numSides * r * r * Math.sin(2 * Math.PI / this.numSides); }
    get perimeter()     { return this.numSides * this.sideLength; }
    get interiorAngle() { return ((this.numSides - 2) * 180) / this.numSides; }
    get exteriorAngle() { return 360 / this.numSides; }

    _computeUserVertices() {
        const r = this.currentRadius;
        const totalRad  = (this.rotation + this._animRotDeg) * Math.PI / 180;
        const baseAngle = -Math.PI / 2;
        return Array.from({ length: this.numSides }, (_, i) => {
            const angle = baseAngle + totalRad + (2 * Math.PI * i / this.numSides);
            return { x: this.center.x + r * Math.cos(angle),
                     y: this.center.y + r * Math.sin(angle) };
        });
    }

    _applyFillStroke() {
        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(); }
    }

    _drawSharp(pts) {
        beginShape();
        for (const p of pts) vertex(p.x, p.y);
        endShape(CLOSE);
    }

    _drawRounded(pts) {
        const n = pts.length;
        beginShape();
        curveVertex(pts[n - 1].x, pts[n - 1].y);
        for (const p of pts) curveVertex(p.x, p.y);
        curveVertex(pts[0].x, pts[0].y);
        curveVertex(pts[Math.min(1, n - 1)].x, pts[Math.min(1, n - 1)].y);
        endShape();
    }

    draw() {
        const r = this.currentRadius;
        const totalRad = (this.rotation + this._animRotDeg) * Math.PI / 180;
        const pts = Array.from({ length: this.numSides }, (_, i) => {
            const angle = -Math.PI / 2 + totalRad + (2 * Math.PI * i / this.numSides);
            return { x: this.center.x + r * Math.cos(angle),
                     y: this.center.y + r * Math.sin(angle) };
        });
        this._applyFillStroke();
        if (this.cornerRadius > 0) this._drawRounded(pts); else this._drawSharp(pts);
        noStroke(); noFill(); strokeWeight(1);
        if (this.showCenter && this.center && this.center.draw) this.center.draw();
    }

    drawOnGrid(grid) {
        const pts = this._computeUserVertices().map(v => grid.userToScreen(v.x, v.y));
        this._applyFillStroke();
        if (this.cornerRadius > 0) this._drawRounded(pts); else this._drawSharp(pts);
        noStroke(); noFill(); strokeWeight(1);
        if (this.showVertices) {
            const dotFill = (this.strokeColor && this.strokeColor.col)
                                ? this.strokeColor.col : color(40, 100, 100);
            fill(dotFill); noStroke();
            for (const p of pts) ellipse(p.x, p.y, 8, 8);
            noFill();
        }
        if (this.showCenter && this.center && this.center.drawOnGrid) this.center.drawOnGrid(grid);
    }

    breathe(sinusoid, t) {
        this._scaleR = sinusoid ? Math.max(0.1, sinusoid.getValue(t)) : 1;
    }

    rotateAboutCenter(degPerSec, t) {
        this._animRotDeg = degPerSec * t;
    }

    transform({ sinusoid = null, t = 0, degPerSec = null } = {}) {
        this._scaleR     = sinusoid ? Math.max(0.1, sinusoid.getValue(t)) : 1;
        this._animRotDeg = degPerSec !== null ? degPerSec * t : 0;
    }

    reset() {
        this.radius      = this.originalRadius;
        this._scaleR     = 1;
        this._animRotDeg = 0;
        this.center.x    = this.originalCenter.x;
        this.center.y    = this.originalCenter.y;
    }

    getVerticesUserCoords() { return this._computeUserVertices(); }

    drawVerticesOnGrid(grid, dotSize = 8, dotColor = undefined) {
        const fillCol = (dotColor && dotColor.col) ? dotColor.col
                      : (this.strokeColor && this.strokeColor.col) ? this.strokeColor.col
                      : color(40, 100, 100);
        fill(fillCol); noStroke();
        for (const v of this._computeUserVertices()) {
            const s = grid.userToScreen(v.x, v.y);
            ellipse(s.x, s.y, dotSize, dotSize);
        }
        noFill();
    }

    centerContainsPoint(px, py, grid, tolerance = 12) {
        const { x: cx, y: cy } = grid.userToScreen(this.center.x, this.center.y);
        const dx = px - cx, dy = py - cy;
        return (dx * dx + dy * dy) <= tolerance * tolerance;
    }

    toString() {
        const totalDeg = this.rotation + this._animRotDeg;
        return `SWRegularPolygon(center: (${this.center.x.toFixed(2)}, ${this.center.y.toFixed(2)}), ` +
               `radius: ${this.radius.toFixed(2)}, sides: ${this.numSides}, ` +
               `sideLength: ${this.sideLength.toFixed(2)}, area: ${this.area.toFixed(2)}, ` +
               `rotation: ${totalDeg.toFixed(1)}\u00b0)`;
    }

}//end class SWRegularPolygon