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.
| Feature | Details |
|---|---|
| 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
| Parameter | Type | Description |
|---|---|---|
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
| Key | Type | Default | Description |
|---|---|---|---|
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
radius × _scaleR. Change this at runtime to resize the polygon; call reset() to revert.numSides after construction does not rebuild the center or color; it only changes vertex geometry.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.drawOnGrid() call.Animation State (read only in practice)
breathe() and transform(). Minimum 0.1.rotateAboutCenter() and transform(). Total displayed rotation = rotation + _animRotDeg.Geometric Properties (Getters)
All getters use currentRadius = radius × _scaleR, so they reflect the live animated state.
| Property | Formula | Notes |
|---|---|---|
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
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.
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
| Mode | cornerRadius | p5 API used | Visual 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
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);
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();
Convenience method that sets both _scaleR and _animRotDeg in one call. Equivalent to calling breathe() and rotateAboutCenter() separately.
Restores radius to originalRadius, resets _scaleR = 1, _animRotDeg = 0, and restores the center to originalCenter. Does not reset the static rotation property.
Utility Methods
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.
Draws a filled circle at each vertex position. dotSize defaults to 8 px; dotColor defaults to the polygon's stroke color.
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.
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
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).
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.
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.
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.
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.
_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