Overview
SWEllipse is a standalone SketchWaveJS shape class that draws an ellipse defined by a center SWPoint and two independent radii: radiusX (horizontal semi-axis) and radiusY (vertical semi-axis). Because an ellipse has no corner vertices, rotation is implemented via p5.js's push()/translate()/rotate()/pop() pattern — the ellipse displays correctly at any angle without recalculating any coordinates.
🌀 Ellipse Geometry
An ellipse is the set of all points where the sum of the distances to two fixed points (the foci) is constant. When the two foci coincide, the ellipse becomes a circle.
📐 Coordinate Convention
All positions are in user (grid) coordinates. The drawOnGrid(grid) method converts to screen pixels automatically. Rotation is in degrees CCW (counter-clockwise), matching standard math convention.
Quick Start
// In setup():
const center = new SWPoint(0, 0);
const myEllipse = new SWEllipse(center, 9, 6, swLightGreen);
// In draw():
myEllipse.drawOnGrid(grid);
Constructor
| Parameter | Type | Default | Description |
|---|---|---|---|
center |
SWPoint | required | Center point of the ellipse (user coordinates) |
radiusX |
number | required | Horizontal semi-axis length (user units) |
radiusY |
number | required | Vertical semi-axis length (user units) |
fillColor |
SWColor | required | Fill color for the ellipse interior |
options.strokeColor |
SWColor | auto (darker fill) | Border / outline color |
options.strokeWeight |
number | 2 | Border thickness in pixels |
options.showCenter |
boolean | false | Show center SWPoint when drawing |
options.rotation |
number | 0 | Initial rotation in degrees CCW |
Example
const fillColor = new SWColor(152, 68, 83, 70, "teal");
const borderColor = fillColor.createDarkerColor(0.75);
const center = new SWPoint(2, -1);
const e = new SWEllipse(center, 8, 5, fillColor, {
strokeColor: borderColor,
strokeWeight: 3,
showCenter: true,
rotation: 30 // 30° counter-clockwise
});
Basic Properties
These are direct read/write properties set by the constructor and modifiable at any time.
center SWPoint
The center point of the ellipse. Moving center.x/center.y repositions the ellipse. Used for drag, trail, and hit-testing.
radiusX radiusY number
Current horizontal and vertical semi-axis lengths (mutable). During breathing, these are set to originalRadiusX × scaleX etc. Set directly to resize without animation.
originalRadiusX originalRadiusY number
The baseline radii stored at construction time (or when rebuilt from the sketch). breathe() multiplies these by a sinusoid scale factor.
rotation number
Current rotation of the ellipse in degrees CCW. Set this every frame in the sketch to animate rotation: ellipseABC.rotation = staticRotation + totalRotationDeg;
fillColor strokeColor SWColor
Fill and border colors. Reassign to apply new colors; the next drawOnGrid() call uses the updated values.
strokeWeight number
Border thickness in pixels. Set to 0 to draw with no border.
showCenter boolean
If true, the center SWPoint is rendered inside drawOnGrid(). Use setShowCenter(bool) for a fluent setter.
Geometric Properties
All geometric getters are read-only and recalculate from the current radiusX/radiusY values each time they are accessed.
area getter
Returns π × radiusX × radiusY. This is the exact formula for ellipse area.
console.log(e.area.toFixed(2)); // e.g. "169.65"
perimeter getter
Returns an approximation using Ramanujan's second formula:
P ≈ π × [3(a+b) − √((3a+b)(a+3b))]
where a = semiMajorAxis, b = semiMinorAxis. This is accurate to within 0.02% for most practical ellipses; exact for circles.
semiMajorAxis semiMinorAxis getter
semiMajorAxis = Math.max(radiusX, radiusY) — the longer radius.
semiMinorAxis = Math.min(radiusX, radiusY) — the shorter radius.
eccentricity getter
Measures how "stretched" the ellipse is:
e = √(1 − (b/a)²)
e = 0— perfect circle0 < e < 1— standard ellipse- approaches 1 as the ellipse becomes very long and thin
console.log(e.eccentricity.toFixed(4)); // e.g. "0.7416"
focalDistance getter
The distance c from the center to each focus along the major axis:
c = √(a² − b²)
For a circle, focalDistance === 0. Used internally by getFociUserCoords().
aspectRatio getter
Returns radiusX / radiusY. Greater than 1 = wider than tall; less than 1 = taller than wide; equal to 1 = circle.
Classification
isCircle getter
Returns true when |radiusX − radiusY| < 1% × max(radiusX, radiusY). Useful for conditionally showing/hiding the foci, since a circle's foci coincide at its center.
if (myEllipse.isCircle) {
console.log("This is a circle; eccentricity ≈ 0");
}
Drawing Methods
drawOnGrid(grid) method
Primary draw method. Converts the center SWPoint from user (grid) coordinates to screen pixels, scales radiusX/radiusY by the grid's pixel-per-unit values, then uses p5's push()/translate()/rotate()/ellipse()/pop() to render the shape.
rotate(-rotation × π/180).
// In draw():
ellipseABC.drawOnGrid(grid);
draw() method
Draws the ellipse in screen (pixel) coordinates directly. Use this when you are not using a SWGrid and instead positioning the ellipse in raw canvas pixels. center.x/center.y are treated as pixel values.
Animation Methods
breathe(sinusoidX, sinusoidY, t) method
Scales radiusX and/or radiusY using sinusoidal functions evaluated at time t (seconds). Pass null for an axis to leave it unaffected.
| Mode | Call Pattern |
|---|---|
| Uniform | breathe(sin, sin, t) |
| Horizontal | breathe(sinX, null, t) |
| Vertical | breathe(null, sinY, t) |
| Independent | breathe(sinX, sinY, t) |
// In draw(), example:
const t = (millis() - breathingStartTime) / 1000;
ellipseABC.breathe(breatheSinusoidX, null, t); // horizontal only
reset() method
Restores radiusX/radiusY to their original values and resets the center position to originalCenter. Does not reset rotation (manage totalRotationDeg in your sketch).
Foci Methods
getFociUserCoords() method
Returns the positions of both focal points in user (grid) coordinates, accounting for the current rotation. The foci lie along the major axis; when the ellipse is rotated, the focal positions rotate correspondingly.
const { f1, f2 } = ellipseABC.getFociUserCoords();
console.log(`F1 = (${f1.x.toFixed(2)}, ${f1.y.toFixed(2)})`);
console.log(`F2 = (${f2.x.toFixed(2)}, ${f2.y.toFixed(2)})`);
drawFociOnGrid(grid, dotSize, dotColor) method
Draws the two focal points as small filled dots on the canvas. Returns immediately (no drawing) if isCircle is true. The dot color defaults to orange if no dotColor is supplied.
// In draw(), after drawOnGrid():
if (showFoci) {
const fociColor = new SWColor(24, 90, 100, 100, "orange");
ellipseABC.drawFociOnGrid(grid, 8, fociColor);
}
Vertices Methods
The four vertex extrema are the points at the ends of the major (X) and minor (Y) axes — i.e. the rightmost, leftmost, topmost, and bottommost points of the ellipse in its own coordinate frame. When the ellipse is rotated, the vertices rotate with it.
getVerticesUserCoords() method
Returns the positions of all four vertex extrema in user (grid) coordinates, accounting for the current rotation. Each vertex is an {x, y} plain object.
| Key | Axis | Unrotated offset |
|---|---|---|
vRight | +X (major end) | (+radiusX, 0) |
vLeft | −X (major end) | (−radiusX, 0) |
vTop | +Y (minor end) | (0, +radiusY) |
vBottom | −Y (minor end) | (0, −radiusY) |
const { vRight, vLeft, vTop, vBottom } = ellipseABC.getVerticesUserCoords();
console.log(`Right vertex: (${vRight.x.toFixed(2)}, ${vRight.y.toFixed(2)})`);
drawVerticesOnGrid(grid, dotSize, dotColor) method
Draws the four vertex extrema as small filled dots on the canvas. The dot color defaults to blue (HSB 210, 75, 90) if no dotColor is supplied, visually distinguishing them from the orange foci dots.
// In draw(), after drawOnGrid():
if (showVertices) {
const verticesColor = new SWColor(210, 75, 90, 100, "vertexBlue");
ellipseABC.drawVerticesOnGrid(grid, 8, verticesColor);
}
Utility Methods
setShowCenter(bool) method
Shows or hides the center SWPoint when drawOnGrid() is called.
ellipseABC.setShowCenter(true); // show
ellipseABC.setShowCenter(false); // hide
centerContainsPoint(px, py, grid, tolerance) method
Returns true if the screen point (px, py) (e.g. p5's mouseX, mouseY) is within tolerance pixels of the ellipse's center. Used for mouse hit-testing to enable center-drag.
// In mousePressed():
if (ellipseABC.centerContainsPoint(mouseX, mouseY, grid, 15)) {
isDraggingCenter = true;
}
toString() method
Returns a descriptive string including center, radii, area, and rotation.
console.log(ellipseABC.toString());
// SWEllipse(center: (0.00, 0.00), radiusX: 9.00, radiusY: 6.00, area: 169.65, rotation: 45.0°)
Examples
Basic Ellipse in a Sketch
let myEllipse, grid;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
initializeSWColors();
grid = new SWGrid({ UL: new SWPoint(-12, 10), LR: new SWPoint(12, -10) });
const fill = new SWColor(152, 68, 83, 70, "teal");
const border = fill.createDarkerColor(0.75);
const center = new SWPoint(0, 0, undefined, 6, border);
myEllipse = new SWEllipse(center, 8, 5, fill, {
strokeColor: border,
strokeWeight: 2
});
}
function draw() {
background(0, 0, 93);
grid.draw();
myEllipse.drawOnGrid(grid);
}
Breathing + Rotation + Foci
let sinX, sinY;
let totalRotationDeg = 0;
let lastUpdate = 0;
let fociColor;
function setup() {
// ... (grid and ellipse setup as above)
const amp = (1.6 - 0.8) / 2;
const freq = (2 * Math.PI) / 2.0; // 2-second period
const mid = (0.8 + 1.6) / 2;
sinX = new SWSinusoid(amp, freq, mid, -Math.PI / 6);
sinY = new SWSinusoid(amp, freq, mid, +Math.PI / 6); // phase-shifted
fociColor = new SWColor(24, 90, 100, 100, "orange");
lastUpdate = millis();
}
function draw() {
background(0, 0, 93);
grid.draw();
// Breathing
const t = millis() / 1000;
myEllipse.breathe(sinX, sinY, t); // independent mode
// Rotation (40°/s)
const now = millis();
totalRotationDeg += 40 * (now - lastUpdate) / 1000;
lastUpdate = now;
myEllipse.rotation = totalRotationDeg;
myEllipse.drawOnGrid(grid);
// Foci
myEllipse.drawFociOnGrid(grid, 8, fociColor);
}
Making a Circle
// Equal radii → isCircle === true
const circle = new SWEllipse(center, 7, 7, swLightBlue);
console.log(circle.isCircle); // true
console.log(circle.eccentricity); // 0
console.log(circle.focalDistance); // 0
Tips & Best Practices
SWEllipse accepts rotation in degrees and converts to p5 radians internally. Keep
rotation in degrees in your sketch code for readability.
When using the demo sketch pattern, call
buildEllipse() to fully reconstruct the instance when sliders change. Direct assignment to radiusX/radiusY works for animation but skips the originalRadiusX/Y update.
Always check
!ellipseABC.isCircle before calling drawFociOnGrid() if you have not already (the method handles it internally, but checking avoids unnecessary calls). Vertices, by contrast, are always well-defined — even a circle has four axis-end points.
breathe() only changes radiusX/radiusY; rotation is a separate scalar. You can breathe and spin simultaneously without special handling.
SWEllipse has no corner vertices, but three sets of special points can leave trails: the
center (draggable), the two foci (toggled with F), and the four vertex extrema (toggled with V). All three track the live positions of their respective points as the ellipse breathes and rotates.
ellipse() takes diameters, not radii.Internally,
drawOnGrid() calls ellipse(0, 0, 2*rx, 2*ry) — the 3rd and 4th arguments are widths, i.e. twice the radii. This is handled automatically; just pass radii to the SWEllipse constructor.
Source Code
Complete source for swEllipse.js:
/*
File: swEllipse.js
Date: 2026-02-28
Author: klp + GitHub Copilot
Workspace: SketchWaveTNT2026-02-28-Stg6
Purpose: SWEllipse class for SketchWaveJS
Comment(s):
SWEllipse: An ellipse with a center SWPoint, radiusX (semi-width), radiusY (semi-height),
fill color, optional border, rotation, and animation features.
Unlike SWRectangle, SWEllipse has a continuous boundary — no corner vertices.
Rotation is achieved via p5.js push/translate/rotate/pop, so it is purely visual
and does not recompute point arrays, making it efficient for complex animations.
Breathing scales radiusX and/or radiusY independently from originalRadiusX/Y.
Rotation tracks a cumulative angle (degrees, CCW in user/math convention).
Dependencies: p5.js, SWColor, SWPoint, SWGrid, SWSinusoid
*/
console.log("[swEllipse.js] SWEllipse class loaded.");
class SWEllipse {
/**
* @param {SWPoint} center - Center point (SWPoint instance)
* @param {number} radiusX - Semi-width (horizontal radius, user units)
* @param {number} radiusY - Semi-height (vertical radius, user units)
* @param {SWColor} fillColor - Fill color (SWColor instance)
* @param {Object} [options]
* strokeColor : SWColor — border color (optional)
* strokeWeight : number — border thickness (default 2)
* showCenter : boolean — show center SWPoint (default false)
* rotation : number — initial rotation in degrees CCW (default 0)
*/
constructor(center, radiusX, radiusY, fillColor, options = {}) {
this.center = center;
this.radiusX = radiusX;
this.radiusY = radiusY;
this.fillColor = fillColor;
this.strokeColor = options.strokeColor || undefined;
this.strokeWeight = options.strokeWeight !== undefined ? options.strokeWeight : 2;
this.showCenter = options.showCenter !== undefined ? options.showCenter : false;
this.rotation = options.rotation || 0; // degrees CCW
// Set center-point styling
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;
// Store originals for animation & drag-reset
this.originalRadiusX = radiusX;
this.originalRadiusY = radiusY;
this.originalCenter = new SWPoint(
center.x, center.y, undefined, cwt, cc
);
}//end constructor
// ── Geometric getters ──────────────────────────────────────────────
/** Area = π × radiusX × radiusY */
get area() {
return Math.PI * this.radiusX * this.radiusY;
}//end area
/**
* Perimeter approximation (Ramanujan's second formula).
* Exact only for circles; very accurate for most ellipses.
*/
get perimeter() {
const a = Math.max(this.radiusX, this.radiusY);
const b = Math.min(this.radiusX, this.radiusY);
return Math.PI * (3 * (a + b) - Math.sqrt((3 * a + b) * (a + 3 * b)));
}//end perimeter
/** radiusX / radiusY */
get aspectRatio() {
return this.radiusX / this.radiusY;
}//end aspectRatio
/** Larger of radiusX, radiusY */
get semiMajorAxis() {
return Math.max(this.radiusX, this.radiusY);
}//end semiMajorAxis
/** Smaller of radiusX, radiusY */
get semiMinorAxis() {
return Math.min(this.radiusX, this.radiusY);
}//end semiMinorAxis
/**
* Eccentricity e = √(1 − (b/a)²).
* e = 0 for a circle; approaches 1 as the ellipse becomes very elongated.
*/
get eccentricity() {
const a = this.semiMajorAxis, b = this.semiMinorAxis;
if (a < 0.0001) return 0;
return Math.sqrt(1 - (b * b) / (a * a));
}//end eccentricity
/** Distance from center to each focus along the major axis: c = √(a² − b²) */
get focalDistance() {
const a = this.semiMajorAxis, b = this.semiMinorAxis;
return Math.sqrt(Math.max(0, a * a - b * b));
}//end focalDistance
/**
* True when radiusX ≈ radiusY (within 1 % relative tolerance).
*/
get isCircle() {
const mx = Math.max(this.radiusX, this.radiusY);
return mx < 0.0001 || Math.abs(this.radiusX - this.radiusY) < 0.01 * mx;
}//end isCircle
// ── Setters ─────────────────────────────────────────────────────────
/** Show or hide the center SWPoint */
setShowCenter(show = true) {
this.showCenter = show;
}//end setShowCenter
// ── Rendering ─────────────────────────────────────────────────────
/**
* Draws the ellipse in screen (pixel) coordinates.
* radiusX and radiusY are treated as pixel values.
*/
draw() {
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(); }
push();
translate(this.center.x, this.center.y);
rotate(-this.rotation * Math.PI / 180); // p5 CW = user CCW
ellipse(0, 0, 2 * this.radiusX, 2 * this.radiusY);
pop();
noStroke(); noFill(); strokeWeight(1);
if (this.showCenter && this.center && this.center.draw) {
this.center.draw();
}
}//end draw
/**
* Draws the ellipse in user / grid coordinates via the given SWGrid.
* @param {SWGrid} grid
*/
drawOnGrid(grid) {
const { x: cx, y: cy } = grid.userToScreen(this.center.x, this.center.y);
const rx = grid.xScale * this.radiusX;
const ry = grid.yScale * this.radiusY;
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(); }
push();
translate(cx, cy);
rotate(-this.rotation * Math.PI / 180); // convert CCW user-angle → CW screen-angle
ellipse(0, 0, 2 * rx, 2 * ry);
pop();
noStroke(); noFill(); strokeWeight(1);
if (this.showCenter && this.center && this.center.drawOnGrid) {
this.center.drawOnGrid(grid);
}
}//end drawOnGrid
// ── Animation ─────────────────────────────────────────────────────
/**
* Scales radiusX and/or radiusY using SWSinusoid instances.
* Pass null for an axis to leave it unscaled.
*
* Modes:
* Uniform: breathe(sin, sin, t) — both axes share one sinusoid
* Horizontal: breathe(sinX, null, t) — only X grows/shrinks
* Vertical: breathe(null, sinY, t) — only Y grows/shrinks
* Independent: breathe(sinX, sinY, t) — separate sinusoids per axis
*
* @param {SWSinusoid|null} sinusoidX - Horizontal scale driver (or null)
* @param {SWSinusoid|null} sinusoidY - Vertical scale driver (or null)
* @param {number} t - Elapsed time in seconds
*/
breathe(sinusoidX, sinusoidY, t) {
const scaleX = sinusoidX ? sinusoidX.getValue(t) : 1.0;
const scaleY = sinusoidY ? sinusoidY.getValue(t) : 1.0;
this.radiusX = this.originalRadiusX * scaleX;
this.radiusY = this.originalRadiusY * scaleY;
}//end breathe
/**
* Resets radiusX/Y to their original values and restores the center position.
* Does NOT reset rotation (handled externally by the sketch).
*/
reset() {
this.radiusX = this.originalRadiusX;
this.radiusY = this.originalRadiusY;
this.center.x = this.originalCenter.x;
this.center.y = this.originalCenter.y;
}//end reset
// ── Foci helpers ──────────────────────────────────────────────────
/**
* Returns the two focal points in user coordinates, accounting for rotation.
* If isCircle, both points coincide at the center.
* @returns {{f1: {x, y}, f2: {x, y}}}
*/
getFociUserCoords() {
const fd = this.focalDistance;
const rotRad = this.rotation * Math.PI / 180;
const cx = this.center.x, cy = this.center.y;
let dx, dy;
if (this.radiusX >= this.radiusY) {
// Major axis is horizontal before rotation
dx = fd * Math.cos(rotRad);
dy = fd * Math.sin(rotRad);
} else {
// Major axis is vertical before rotation; rotate 90° extra
dx = -fd * Math.sin(rotRad);
dy = fd * Math.cos(rotRad);
}
return {
f1: { x: cx + dx, y: cy + dy },
f2: { x: cx - dx, y: cy - dy }
};
}//end getFociUserCoords
/**
* Draws the two foci as small filled dots on a grid.
* @param {SWGrid} grid
* @param {number} [dotSize=8] — screen-pixel diameter of each focus dot
* @param {SWColor} [dotColor] — fill color for the dots
*/
drawFociOnGrid(grid, dotSize = 8, dotColor = undefined) {
if (this.isCircle) return; // no distinct foci for a circle
const { f1, f2 } = this.getFociUserCoords();
const s1 = grid.userToScreen(f1.x, f1.y);
const s2 = grid.userToScreen(f2.x, f2.y);
if (dotColor && dotColor.col) { fill(dotColor.col); } else { fill(255, 120, 0); }
noStroke();
ellipse(s1.x, s1.y, dotSize, dotSize);
ellipse(s2.x, s2.y, dotSize, dotSize);
noFill();
}//end drawFociOnGrid
// ── Vertex extrema helpers ────────────────────────────────────────────
/**
* Returns the four vertex extrema in user coordinates, accounting for rotation.
* These are the points at the ends of the major (X) and minor (Y) axes.
* @returns {{ vRight, vLeft, vTop, vBottom }}
*/
getVerticesUserCoords() {
const rotRad = this.rotation * Math.PI / 180;
const cx = this.center.x, cy = this.center.y;
const rX = this.radiusX, rY = this.radiusY;
const cosR = Math.cos(rotRad), sinR = Math.sin(rotRad);
return {
vRight: { x: cx + rX * cosR, y: cy + rX * sinR },
vLeft: { x: cx - rX * cosR, y: cy - rX * sinR },
vTop: { x: cx - rY * sinR, y: cy + rY * cosR },
vBottom: { x: cx + rY * sinR, y: cy - rY * cosR }
};
}//end getVerticesUserCoords
/**
* Draws the four vertex extrema as small filled dots on a grid.
* @param {SWGrid} grid
* @param {number} [dotSize=8] — screen-pixel diameter
* @param {SWColor} [dotColor] — fill color (default: blue)
*/
drawVerticesOnGrid(grid, dotSize = 8, dotColor = undefined) {
const { vRight, vLeft, vTop, vBottom } = this.getVerticesUserCoords();
if (dotColor && dotColor.col) { fill(dotColor.col); } else { fill(210, 75, 90); }
noStroke();
[vRight, vLeft, vTop, vBottom].forEach(v => {
const s = grid.userToScreen(v.x, v.y);
ellipse(s.x, s.y, dotSize, dotSize);
});
noFill();
}//end drawVerticesOnGrid
// ── Utility ─────────────────────────────────────────────────────────────
/**
* Returns true if screen point (px, py) is inside or near the ellipse center
* (within `tolerance` pixels). Used for mouse hit-testing the center handle.
* @param {number} px
* @param {number} py
* @param {SWGrid} grid
* @param {number} [tolerance=12]
*/
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;
}//end centerContainsPoint
/**
* String representation
*/
toString() {
return `SWEllipse(center: (${this.center.x.toFixed(2)}, ${this.center.y.toFixed(2)}), ` +
`radiusX: ${this.radiusX.toFixed(2)}, radiusY: ${this.radiusY.toFixed(2)}, ` +
`area: ${this.area.toFixed(2)}, rotation: ${this.rotation.toFixed(1)}\u00b0)`;
}//end toString
}//end class SWEllipse
// export default SWEllipse;