Quick Reference
SWArch is a SketchWave parametric class that represents a parabolic arch (or bowl) shape defined by the quadratic function y = a·x² in a local coordinate system centered at the vertex. By default, the drawn shape is a closed polygon — a smooth parabolic arc sampled at 60 points, connected by a straight base line. Set shouldCloseShape = false to draw only the open arc with no base line. SWArch is not a composite class; it owns no internal sub-shapes. All drawing, animation, and styling is handled directly by the class.
- Design Pattern: Parametric curve (not composition or inheritance)
- Internal Structure: Local coordinate system at vertex; curve sampled at
SAMPLE_COUNT = 60points - Dependencies: SWPoint, SWColor, SWSinusoid, SWGrid, p5.js
- Key Features: Parabolic shape (y = a·x²), vertex-centered local coords, asymmetric extents (xLeft, xRight), open or closed shape (
shouldCloseShape), static rotationDeg + accumulated rotation, breatheA (curvature oscillation), breatheBase (width oscillation), fill/stroke colors, optional vertex/endpoint markers - Factory Methods:
SWArch.fromThreePoints()constructs an arch by fitting a parabola through three user-space points;SWArch.fitParabolaToThreePoints()returns just the coefficients - Common Uses: Arch shapes, bridge graphics, rainbow curves, bowl/lens forms, open arcs, optical illusion arcs, geometric art
Overview
The SWArch class represents a parabolic arch drawn as a closed or open shape. The arch is defined by a vertex point and a quadratic coefficient a. In the local coordinate system (origin at vertex), the curve traces y = a·x² from x = −xLeft to x = xRight. When shouldCloseShape is true (the default), the path closes with a straight base line back to the starting endpoint. When false, only the parabolic arc is drawn.
aWhen
a < 0, the parabola opens downward — the vertex is the highest point, producing the classic arch (∩) or rainbow shape.When
a > 0, the parabola opens upward — the vertex is the lowest point, producing a bowl (∪) shape.Larger
|a| makes the curve sharper; smaller |a| makes it flatter.
shouldCloseShapeWhen
shouldCloseShape is true (default), p5's endShape(CLOSE) draws a straight base line connecting the two endpoints — forming a filled, closed polygon.When
false, endShape() is used instead — only the parabolic arc is drawn, with no base line. Fill is still applied to the arc interior when open, but the visual result is just the curved stroke/arc outline.
Local Coordinate System
SWArch works in a local coordinate system centered at the vertex. In local space:
- Left endpoint:
(−xLeft, a · xLeft²) - Right endpoint:
( xRight, a · xRight²) - Curve:
y = a · x²forx ∈ [−xLeft, xRight]
The arch can be asymmetric: set xLeft ≠ xRight to shift the vertex horizontally relative to the base. The base line always connects the two endpoints directly.
Rotation
SWArch supports two layers of rotation, both applied about the vertex:
rotationDeg— Static base rotation set bysetRotation(). Persists across frames; survivesreset().rotation— Accumulated rotation incremented byrotate(). Starts at 0; cleared byreset().
The effective rotation used for drawing is rotationDeg + rotation. All rotation is CCW positive (user-space convention).
// Static tilt: arch leans 15° to the right (CCW = left)
arch.setRotation(-15);
// Spin at 30°/second in draw loop
arch.rotate(30 * deltaT); // call BEFORE drawOnGrid
Key Capabilities
- Parabolic Shape: Smooth curve sampled at 60 points
- Open or Closed:
shouldCloseShapetoggles between a closed polygon (with base line) and an open arc (curve only) - Asymmetric Wings: Independent
xLeftandxRightextents let you shift the arch peak horizontally - Spin Animation: Continuously rotate the arch about its vertex with
rotate() - Breathe A: Oscillate the curvature coefficient
a— arch opens/closes, or flips between arch and bowl asacrosses zero - Breathe Base: Oscillate
xLeftandxRightsymmetrically — wings expand and contract like a bird in flight - Fill & Stroke: Independent fill color and stroke (border) color; fill opacity control via
setFillAlpha() - Point Markers: Optional display of vertex dot and endpoint dots (using SWPoint)
- Dual Coordinate Systems: Draw in screen pixels (
draw()) or grid user coordinates (drawOnGrid()) - 3-Point Factory:
SWArch.fromThreePoints()builds an arch by fitting a parabola through three user-space points automatically
Typical Workflow
- Create fill and stroke SWColor instances
- Construct an SWArch with vertex, coefficient a, xLeft, xRight, and optional colors — or use
SWArch.fromThreePoints() - Optionally set
arch.shouldCloseShape = falseto draw an open arc - Draw each frame using
drawOnGrid(grid) - Call
rotate()before drawing to spin; callbreatheA()orbreatheBase()after drawing to animate - Call
reset()to restore all original values
Constructor
new SWArch(vertex, a, xLeft, xRight, fillColor, strokeColor, thickness, rotationDeg)Creates a new SWArch instance. The vertex position, coefficient, extents, and colors define the arch's initial state. Originals are captured for reset(). shouldCloseShape is always initialized to true; set it directly after construction if you want an open arc.
| Parameter | Type | Default | Description |
|---|---|---|---|
vertex |
SWPoint | required | Arch extremum in user (grid) coordinates — highest point when a < 0, lowest when a > 0 |
a |
number | required | Quadratic coefficient: a < 0 gives arch (∩), a > 0 gives bowl (∪). Larger |a| = sharper curve. |
xLeft |
number | 3 | Horizontal distance from vertex to the left endpoint (positive; absolute value taken internally) |
xRight |
number | 3 | Horizontal distance from vertex to the right endpoint (positive; absolute value taken internally) |
fillColor |
SWColor | undefined | undefined | Fill color for the arch interior; undefined = no fill (transparent) |
strokeColor |
SWColor | undefined | undefined | Border (outline) color; undefined = no stroke drawn |
thickness |
number | 2 | Stroke weight in pixels |
rotationDeg |
number | 0 | Static base rotation in CCW degrees; applied in addition to accumulated rotation |
// Classic blue arch at origin
const fill = new SWColor(200, 55, 92, 100, "archFill");
const border = new SWColor(210, 70, 50, 100, "archStroke");
let arch = new SWArch(new SWPoint(0, 2), -0.3, 5, 5, fill, border, 2);
// Bowl shape (a > 0), vertex at bottom
const bowlFill = new SWColor(30, 55, 92, 100, "bowlFill");
const bowlBorder = new SWColor(40, 70, 50, 100, "bowlStroke");
let bowl = new SWArch(new SWPoint(0, -2), 0.4, 4, 4, bowlFill, bowlBorder);
// Asymmetric arch: peak shifted right, no fill
let asymArch = new SWArch(new SWPoint(1, 3), -0.25, 6, 3, undefined, border, 2);
// Pre-tilted arch (rotationDeg set at construction)
let tiltedArch = new SWArch(new SWPoint(0, 0), -0.3, 5, 5, fill, border, 2, 45);
// Minimal: stroke-only arch with defaults
const minBorder = new SWColor(0, 0, 20, 100, "darkBorder");
let minimal = new SWArch(new SWPoint(0, 1), -0.2, undefined, undefined, undefined, minBorder);
// Open arc (parabolic curve only, no base line)
let openArc = new SWArch(new SWPoint(0, 2), -0.3, 5, 5, undefined, border, 3);
openArc.shouldCloseShape = false;
Properties
vertex SWPointThe arch's extremum in user (grid) coordinates. When a < 0, this is the highest point; when a > 0, the lowest. All rotation is applied about this point. Moving vertex.x or vertex.y repositions the entire arch.
arch.vertex.x = 2; arch.vertex.y = 3;
a numberThe quadratic coefficient controlling curvature and orientation. Negative = arch (∩), positive = bowl (∪). Larger absolute value = sharper curve. Use setA() to change it, or let breatheA() animate it each frame.
arch.setA(-0.5); // sharper arch
arch.setA(0.3); // switch to bowl
xLeft numberThe horizontal distance from the vertex to the left endpoint in local space (always positive). The left endpoint is at local (−xLeft, a · xLeft²). Use setXLeft() to change, or let breatheBase() animate it.
arch.setXLeft(7); // wider left wing
xRight numberThe horizontal distance from the vertex to the right endpoint in local space (always positive). The right endpoint is at local (xRight, a · xRight²). Use setXRight() to change, or let breatheBase() animate it.
arch.setXRight(3); // narrower right wing
shouldCloseShape boolean newControls whether the shape is drawn as a closed polygon or an open arc. Default is true.
true(default):endShape(CLOSE)adds a straight base line from the right endpoint back to the left endpoint, forming a filled, closed polygon. Fill color fills the entire enclosed region.false:endShape()leaves the shape open — only the parabolic arc is drawn with no connecting base line. This is ideal for drawing just the curved outline, such as a smile, a bracket, or a rainbow arc.
Set directly on the instance; there is no dedicated setter method.
arch.shouldCloseShape = false; // open arc — no base line
arch.shouldCloseShape = true; // closed polygon (default)
rotationDeg numberStatic base rotation in CCW degrees. Set at construction or via setRotation(). Does not change with rotate() calls and is preserved by reset(). Use this for a fixed tilt that survives the reset.
arch.setRotation(30); // tilt 30° CCW permanently
rotation numberAccumulated rotation in degrees (CCW positive), incremented by each rotate() call. Starts at 0; cleared by reset(). The effective drawing rotation is rotationDeg + rotation.
console.log(arch.rotation.toFixed(1) + "°");
fillColor SWColor | undefinedThe fill color for the arch interior. undefined = no fill. Use setFillColor() to replace it, or setFillAlpha() to change its transparency without replacing the color object. Note: when shouldCloseShape is false, fill is applied but only the arc stroke is visually dominant.
arch.setFillColor(new SWColor(120, 55, 92, 80, "greenFill"));
arch.setFillColor(undefined); // remove fill
strokeColor SWColor | undefinedThe border (outline) color. undefined = no stroke drawn. Use setStrokeColor() to replace it.
arch.setStrokeColor(new SWColor(210, 70, 50, 100, "border"));
arch.setStrokeColor(undefined); // no border
thickness numberThe stroke weight in pixels. Use setStrokeWeight() to change.
arch.setStrokeWeight(4);
shouldShowVertex booleanWhether to draw the vertex SWPoint marker. Default is true. Use setShowVertex() or setShowPoints() to change.
arch.setShowVertex(false); // hide vertex dot
shouldShowEndpoints booleanWhether to draw SWPoint markers at the left and right endpoints of the arch base. Default is true. Use setShowEndpoints() or setShowPoints() to change.
arch.setShowEndpoints(false); // hide endpoint dots
originalA / originalXLeft / originalXRight / originalFillColor / originalStrokeColor / originalThickness / originalRotationDeg various restore targetsSnapshot values captured at construction. reset() uses all of these to restore the arch to its initial state. originalFillColor and originalStrokeColor are deep copies made at construction time. Note: shouldCloseShape is not captured as an original; it always initializes to true and is not changed by reset().
// Read-only; used internally by reset()
Methods
Core Drawing Methods
draw()Draws the arch in raw screen (pixel) coordinates. The vertex.x and vertex.y are treated as pixel positions. Respects shouldCloseShape. Rarely used directly — prefer drawOnGrid() for standard canvas rendering.
void
function draw() {
background(220);
arch.draw(); // vertex treated as screen pixels
}
drawOnGrid(grid)Draws the arch mapped through the given SWGrid's coordinate system. Converts the vertex and all sampled curve points from user units to screen pixels via grid.userToScreen(). Respects shouldCloseShape. This is the standard method to call in a p5.js draw() loop.
grid(SWGrid) — the coordinate grid
void
function draw() {
background(220);
grid.draw();
arch.drawOnGrid(grid);
}
Rotation Animation
rotate(deltaAngle)Increments the arch's accumulated rotation by deltaAngle degrees about the vertex (CCW positive, CW negative). Call each frame before drawOnGrid().
deltaAngle(number) — degrees to add torotation
// Spin at 45°/second using elapsed time (deltaT)
arch.rotate(spinSpeed * deltaT); // call BEFORE drawOnGrid
// Fixed increment per frame
arch.rotate(1); // 1 degree per frame, CCW
Breathing Animations
breatheA(sinusoid, t)Modulates the a coefficient using an SWSinusoid, opening and closing the arch's curvature. Larger |a| = sharper; smaller |a| = flatter. If the sinusoid crosses zero, the arch flips between ∩ and ∪ shapes mid-animation. Call after drawOnGrid() so the new value takes effect next frame.
sinusoid(SWSinusoid) — controlsaoscillation; configure with desired min/max a valuest(number) — elapsed time in seconds
// 'a' oscillates between -0.1 (flat) and -0.8 (sharp) over 4 seconds
let aSin = SWSinusoid.copy(UNIT_SINUSOID);
aSin.setPeriod(4);
aSin.adjustWaveUsingExtrema(-0.1, -0.8);
arch.drawOnGrid(grid);
arch.breatheA(aSin, elapsedSeconds); // call AFTER draw
breatheBase(sinusoid, t)Modulates both xLeft and xRight symmetrically using an SWSinusoid, expanding and contracting the arch's base width like wings opening and closing. The sinusoid value is passed through Math.abs() to ensure extents are always positive. Call after drawOnGrid().
sinusoid(SWSinusoid) — controls width oscillation; configure with positive min/max extent valuest(number) — elapsed time in seconds
// Base oscillates between 1 (narrow) and 6 (wide) over 3 seconds
let baseSin = SWSinusoid.copy(UNIT_SINUSOID);
baseSin.setPeriod(3);
baseSin.adjustWaveUsingExtrema(1, 6);
arch.drawOnGrid(grid);
arch.breatheBase(baseSin, elapsedSeconds); // call AFTER draw
Endpoint Methods
getLeftEndpoint()Computes and returns the left endpoint of the arch as an SWPoint in user (grid) space, accounting for the current rotation. The point has strokeWeight = 7 for a visible dot. This is a computed value — it reflects the current xLeft, a, and rotation every time it is called.
SWPoint (in user coordinates)
const lep = arch.getLeftEndpoint();
console.log(`Left endpoint: (${lep.x.toFixed(2)}, ${lep.y.toFixed(2)})`);
getRightEndpoint()Computes and returns the right endpoint of the arch as an SWPoint in user (grid) space, accounting for the current rotation. Mirrors getLeftEndpoint().
SWPoint (in user coordinates)
const rep = arch.getRightEndpoint();
console.log(`Right endpoint: (${rep.x.toFixed(2)}, ${rep.y.toFixed(2)})`);
Setter Methods
setA(a)Sets the quadratic coefficient immediately. Negative = arch, positive = bowl.
a(number) — new coefficient value
arch.setA(-0.5); // sharper arch
setXLeft(v) / setXRight(v)Sets the left or right horizontal extent from the vertex. The absolute value is taken internally so negative inputs are safe. Call both to set a symmetric arch; call one at a time for an asymmetric wing adjustment.
v(number) — new extent value (absolute value used)
arch.setXLeft(4);
arch.setXRight(6); // asymmetric: left narrower than right
setRotation(deg)Sets the static base rotation (rotationDeg) in CCW degrees. Does not affect the accumulated rotation; both are combined when drawing.
deg(number) — new static rotation in CCW degrees
arch.setRotation(90); // arch now points right
setFillColor(fc)Sets the fill color. Pass an SWColor to set a new fill; pass undefined to remove fill (transparent interior).
fc(SWColor | undefined)
arch.setFillColor(new SWColor(60, 55, 92, 100, "yellowFill"));
arch.setFillColor(undefined); // no fill
setStrokeColor(sc)Sets the border color. Pass undefined to remove the stroke entirely.
sc(SWColor | undefined)
arch.setStrokeColor(new SWColor(0, 0, 20, 100, "darkBorder"));
setStrokeWeight(w)Sets the border thickness in pixels.
w(number) — thickness in pixels
arch.setStrokeWeight(3);
setFillAlpha(alpha)Sets the fill opacity (0–100) by updating the fill color's alpha channel and rebuilding the p5 color object. Does nothing if fillColor is undefined. Clamped to [0, 100].
alpha(number) — 0 = fully transparent, 100 = fully opaque
arch.setFillAlpha(60); // 60% opaque fill
setShowVertex(show) / setShowEndpoints(show) / setShowPoints(show)Controls visibility of the vertex and/or endpoint SWPoint markers. setShowPoints() sets both simultaneously.
show(boolean) — true to show, false to hide (default: true)
arch.setShowVertex(false); // hide vertex dot only
arch.setShowEndpoints(false); // hide endpoint dots only
arch.setShowPoints(false); // hide all point markers
Reset Method
reset()Restores all animated and slider-driven properties — a, xLeft, xRight, rotationDeg, thickness, fillColor, and strokeColor — to their originals captured at construction. Clears accumulated spin rotation (rotation = 0). Does not move the vertex position. Does not change shouldCloseShape — that setting persists across resets.
arch.reset(); // full restore to factory state (shouldCloseShape unchanged)
Utility Methods
static copy(other)Returns a deep copy of an SWArch instance. All geometry, color values, and original state are independently duplicated. The current rotation, rotationDeg, shouldShowVertex, and shouldShowEndpoints are preserved in the copy. Note: shouldCloseShape is not explicitly transferred by copy(); the copy initializes it to true (the constructor default) regardless of the source.
other(SWArch) — the arch to copy
SWArch — a new independent instance
let archCopy = SWArch.copy(arch1);
// If arch1 was open, set it manually on the copy:
archCopy.shouldCloseShape = arch1.shouldCloseShape;
toString()Returns a string summarizing the arch's key properties: vertex, a, xLeft, xRight, rotationDeg, and rotation.
string
console.log(arch.toString());
// "SWArch(vertex=SWPoint(x:0, y:2), a=-0.3000, xLeft=5.00, xRight=5.00, rotationDeg=0.0, rotation=0.0)"
Static Factory Methods new
static SWArch.fromThreePoints(p1, p2, p3, fillColor, strokeColor, thickness, rotationDeg)Constructs an SWArch by fitting a standard-form parabola y = ax² + bx + c through three user-space points, then converting to vertex form y = a(x − h)² + k. This eliminates the need to manually calculate the vertex, a, xLeft, and xRight — you just supply three points on the desired parabola.
How it works:
- Calls
fitParabolaToThreePoints()to solve the 3×3 linear system for a, b, c - Converts to vertex form:
h = −b/(2a),k = c − b²/(4a) - Computes local x-coordinates for all three points relative to the vertex (h, k); uses the leftmost and rightmost as xLeft and xRight
- Builds and returns a new SWArch with the computed vertex, a, xLeft, xRight, and the supplied style parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
p1 |
SWPoint | {x,y} | required | First point on the parabola (user/grid coordinates) |
p2 |
SWPoint | {x,y} | required | Second point on the parabola |
p3 |
SWPoint | {x,y} | required | Third point on the parabola |
fillColor |
SWColor | undefined | undefined | Fill color (optional) |
strokeColor |
SWColor | undefined | undefined | Stroke color (optional) |
thickness |
number | 2 | Stroke weight in pixels |
rotationDeg |
number | 0 | Initial static base rotation in CCW degrees |
SWArch | null — the constructed arch, or null if the points are degenerate (collinear, or any two share the same x-coordinate)
null if any two points share the same x-coordinate (singular system), or if the computed a ≈ 0 (points are effectively collinear). Always check the return value before using the result.
const fill = new SWColor(200, 55, 92, 100, "archFill");
const border = new SWColor(210, 70, 50, 100, "archStroke");
// Three points the arch must pass through
const p1 = { x: -5, y: -3 }; // left base
const p2 = { x: 0, y: 2 }; // peak
const p3 = { x: 5, y: -3 }; // right base
const arch = SWArch.fromThreePoints(p1, p2, p3, fill, border, 2);
if (!arch) {
console.warn("Could not build arch — degenerate points");
}
// In draw():
arch.drawOnGrid(grid);
static SWArch.fitParabolaToThreePoints(p1, p2, p3)Utility method that solves the 3×3 linear system to find the standard-form parabola y = ax² + bx + c passing through three given points. Used internally by fromThreePoints(), but also callable directly when you need the raw coefficients.
Math overview: Substituting each of the three points into y = ax² + bx + c yields three linear equations in three unknowns (a, b, c). These are solved by subtraction and elimination. The result is unique as long as the three x-values are distinct and the points are not collinear.
| Parameter | Type | Description |
|---|---|---|
p1 |
{x, y} | First point in user (grid) coordinates |
p2 |
{x, y} | Second point |
p3 |
{x, y} | Third point |
{ a, b, c } | null — parabola coefficients as plain numbers, or null if the system is degenerate
const coeffs = SWArch.fitParabolaToThreePoints(
{ x: -4, y: 0 },
{ x: 0, y: 4 },
{ x: 4, y: 0 }
);
if (coeffs) {
// coeffs.a = -0.25, coeffs.b = 0, coeffs.c = 4 (symmetric arch)
console.log(`y = ${coeffs.a}x² + ${coeffs.b}x + ${coeffs.c}`);
}
Usage Examples
Example 1: Basic Arch
let grid;
let arch;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
const fill = new SWColor(200, 55, 92, 100, "archFill");
const border = new SWColor(210, 70, 50, 100, "archStroke");
arch = new SWArch(new SWPoint(0, 2), -0.3, 5, 5, fill, border, 2);
}
function draw() {
background(0, 0, 93);
grid.draw();
arch.drawOnGrid(grid);
}
Example 2: Open Arc (No Base Line)
let grid;
let openArc;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
// Stroke-only, no fill — just the parabolic curve
const border = new SWColor(150, 70, 50, 100, "arcStroke");
openArc = new SWArch(new SWPoint(0, 3), -0.25, 6, 6, undefined, border, 3);
openArc.shouldCloseShape = false; // draw only the curved arc
openArc.setShowEndpoints(false); // optional: hide endpoint dots
}
function draw() {
background(0, 0, 93);
grid.draw();
openArc.drawOnGrid(grid);
}
Example 3: Arch from Three Points
let grid;
let arch;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
const fill = new SWColor(30, 55, 92, 100, "archFill");
const border = new SWColor(40, 70, 50, 100, "archStroke");
// Arch passes exactly through these three user-space points
arch = SWArch.fromThreePoints(
{ x: -6, y: -2 }, // left base
{ x: 0, y: 4 }, // peak
{ x: 4, y: 0 }, // right base (asymmetric)
fill, border, 2
);
if (!arch) {
console.warn("Degenerate points — could not build arch.");
}
}
function draw() {
background(0, 0, 93);
grid.draw();
if (arch) arch.drawOnGrid(grid);
}
Example 4: Spinning Arch (Elapsed-Time Approach)
let grid, arch;
let prevT = 0;
const SPIN_SPEED = 60; // degrees per second
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
const fill = new SWColor(30, 55, 92, 100, "archFill");
const border = new SWColor(40, 70, 50, 100, "archStroke");
arch = new SWArch(new SWPoint(0, 0), -0.3, 5, 5, fill, border, 2);
}
function draw() {
background(0, 0, 93);
grid.draw();
const t = millis() / 1000;
const deltaT = (prevT > 0) ? (t - prevT) : 0;
prevT = t;
arch.rotate(SPIN_SPEED * deltaT); // spin BEFORE drawing
arch.drawOnGrid(grid);
}
Example 5: Breathe A (Curvature Oscillation)
let grid, arch, aSin;
let breathStart = 0, breathElapsed = 0;
let shouldBreathe = false;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
const fill = new SWColor(270, 55, 92, 100, "archFill");
const border = new SWColor(280, 70, 50, 100, "archStroke");
arch = new SWArch(new SWPoint(0, 1), -0.3, 5, 5, fill, border, 2);
// 'a' oscillates between -0.1 (flat) and -0.7 (sharp) over 3 seconds
aSin = SWSinusoid.copy(UNIT_SINUSOID);
aSin.setPeriod(3);
aSin.adjustWaveUsingExtrema(-0.1, -0.7);
}
function draw() {
background(0, 0, 93);
grid.draw();
arch.drawOnGrid(grid); // draw first
if (shouldBreathe) {
const t = millis() / 1000;
breathElapsed += (t - breathStart);
breathStart = t;
arch.breatheA(aSin, breathElapsed); // breathe AFTER draw
}
}
function keyPressed() {
if (key === 'b') {
shouldBreathe = !shouldBreathe;
breathStart = millis() / 1000;
}
if (key === 'r') { arch.reset(); breathElapsed = 0; }
}
Example 6: Breathe Base (Width Oscillation)
let grid, arch, baseSin;
let baseStart = 0, baseElapsed = 0;
let shouldBreatheBase = false;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
const fill = new SWColor(120, 55, 92, 100, "archFill");
const border = new SWColor(130, 70, 50, 100, "archStroke");
arch = new SWArch(new SWPoint(0, 2), -0.4, 4, 4, fill, border, 2);
// xLeft and xRight oscillate between 1 (narrow) and 6 (wide) over 3 seconds
baseSin = SWSinusoid.copy(UNIT_SINUSOID);
baseSin.setPeriod(3);
baseSin.adjustWaveUsingExtrema(1, 6);
}
function draw() {
background(0, 0, 93);
grid.draw();
arch.drawOnGrid(grid); // draw first
if (shouldBreatheBase) {
const t = millis() / 1000;
baseElapsed += (t - baseStart);
baseStart = t;
arch.breatheBase(baseSin, baseElapsed); // breathe AFTER draw
}
}
function keyPressed() {
if (key === 'e') {
shouldBreatheBase = !shouldBreatheBase;
baseStart = millis() / 1000;
}
if (key === 'r') { arch.reset(); baseElapsed = 0; }
}
Example 7: Spin + Breathe A + Breathe Base (All Together)
let grid, arch, aSin, baseSin;
let prevT = 0;
let aStart = 0, aElapsed = 0, shouldBreatheA = false;
let baseStart = 0, baseElapsed = 0, shouldBreatheBase = false;
let shouldSpin = false;
const SPIN_SPEED = 45;
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
const fill = new SWColor(200, 55, 92, 100, "archFill");
const border = new SWColor(210, 70, 50, 100, "archStroke");
arch = new SWArch(new SWPoint(0, 0), -0.3, 5, 5, fill, border, 2);
aSin = SWSinusoid.copy(UNIT_SINUSOID);
aSin.setPeriod(4);
aSin.adjustWaveUsingExtrema(-0.1, -0.7);
baseSin = SWSinusoid.copy(UNIT_SINUSOID);
baseSin.setPeriod(3);
baseSin.adjustWaveUsingExtrema(1, 6);
}
function draw() {
background(0, 0, 93);
grid.draw();
const t = millis() / 1000;
const deltaT = (prevT > 0) ? (t - prevT) : 0;
prevT = t;
if (shouldSpin) arch.rotate(SPIN_SPEED * deltaT); // BEFORE draw
arch.drawOnGrid(grid); // draw
if (shouldBreatheA) { // AFTER draw
aElapsed += (t - aStart);
aStart = t;
arch.breatheA(aSin, aElapsed);
}
if (shouldBreatheBase) {
baseElapsed += (t - baseStart);
baseStart = t;
arch.breatheBase(baseSin, baseElapsed);
}
}
function keyPressed() {
if (key === 's') { shouldSpin = !shouldSpin; }
if (key === 'b') { shouldBreatheA = !shouldBreatheA; aStart = millis()/1000; }
if (key === 'e') { shouldBreatheBase = !shouldBreatheBase; baseStart = millis()/1000; }
if (key === 'r') { arch.reset(); aElapsed = 0; baseElapsed = 0; }
}
Example 8: Asymmetric Arch
// xLeft != xRight: arch peak is off-center relative to the base
// vertex at (1, 3) means peak is shifted 1 unit right of center
const fill = new SWColor(350, 55, 92, 100, "archFill");
const border = new SWColor( 0, 70, 50, 100, "archStroke");
// xLeft=6, xRight=2: long left wing, short right wing
let asymArch = new SWArch(new SWPoint(1, 3), -0.25, 6, 2, fill, border, 2);
// Left endpoint at user: (1 - 6, -0.25 * 36) = (-5, -9) ... below vertex far left
// Right endpoint at user: (1 + 2, -0.25 * 4) = ( 3, -1) ... just right of vertex
function draw() {
background(0, 0, 93);
grid.draw();
asymArch.drawOnGrid(grid);
// Log endpoints to understand the geometry:
console.log("L:", asymArch.getLeftEndpoint().toString());
console.log("R:", asymArch.getRightEndpoint().toString());
}
Best Practices
1. Animation Ordering
- Spin BEFORE draw:
rotate()updates the orientation used during this frame's render - Breathing AFTER draw:
breatheA()andbreatheBase()set new values that take effect on the next frame — matching the SWDisk/SWSector convention across SketchWaveJS
arch.rotate(speed * deltaT); // spin → before draw
arch.drawOnGrid(grid); // draw
arch.breatheA(aSin, aElapsed); // breathe → after draw
arch.breatheBase(baseSin, baseElapsed);
2. Choosing Closed vs. Open
- Use closed (
shouldCloseShape = true, the default) when you want a filled arch shape — bridgework, rainbow fills, geometric polygons - Use open (
shouldCloseShape = false) for a pure curved outline — smile lines, bracket curves, overlapping arc compositions - When open and
fillColoris set, p5 may still render a faint fill in the arc interior — setfillColor = undefinedfor a truly stroke-only open arc
// Pure stroke arc — no fill bleed
arch.setFillColor(undefined);
arch.shouldCloseShape = false;
3. Elapsed Time vs. frameCount
- Use elapsed seconds (not frame count) for all sinusoid time parameters; this keeps animation speed frame-rate-independent
- Track
startTimeandelapsedseparately per animation so each can be paused and resumed independently
// Pattern for pauseable elapsed-time animation
let breathStart = 0, breathElapsed = 0, isBreathing = false;
function toggleBreathe() {
isBreathing = !isBreathing;
if (isBreathing) breathStart = millis() / 1000;
}
// In draw():
if (isBreathing) {
const t = millis() / 1000;
breathElapsed += (t - breathStart);
breathStart = t;
arch.breatheA(aSin, breathElapsed);
}
4. Coordinate Convention
- Always pass user-space degrees (CCW from +x) to SWArch; the class handles the p5.js y-flip internally via
grid.userToScreen() - Positive
rotate()deltas spin CCW; negative values spin CW - The arch's vertex is its rotation pivot — repositioning the vertex after construction (or during animation) also moves the pivot point
5. The a Coefficient
- Use negative values for classic arch shapes (∩) and rainbow forms; use positive values for bowl/lens shapes (∪)
- The range
-0.5 to -0.1gives natural-looking arches on a ±10 unit grid - When using
breatheA()with a sinusoid that crosses zero, the arch will flip between arch and bowl mid-animation — which can be a dramatic visual effect or an unintended bug depending on your sinusoid setup - To keep the arch always arching (never flipping), configure the sinusoid so both
minandmaxhave the same sign
// Safe: always an arch (both negative)
aSin.adjustWaveUsingExtrema(-0.1, -0.8);
// Dramatic: flips arch <-> bowl as 'a' crosses zero
aSin.adjustWaveUsingExtrema(-0.5, 0.5);
6. SWSinusoid Setup for Breathing
- The most convenient setup uses the
UNIT_SINUSOIDglobal withsetPeriod()andadjustWaveUsingExtrema(min, max) - Or construct directly: center = (min+max)/2, amplitude = (max−min)/2, frequency = 1/period
// Base breathes between 1 and 6 with a 3-second period
let baseSin = SWSinusoid.copy(UNIT_SINUSOID);
baseSin.setPeriod(3);
baseSin.adjustWaveUsingExtrema(1, 6);
// Equivalent long form:
let baseSin2 = new SWSinusoid(3.5, 2.5, 1/3, 0);
// center=3.5, amplitude=2.5 → range [1, 6]; frequency=1/3 → period=3s
7. Using fromThreePoints
- Always check for a
nullreturn — degenerate inputs (collinear points, or two points with the same x) returnnulland log a warning to the console - The three input points define the extent of the arch: the leftmost and rightmost x-values become xLeft and xRight; the third point lands somewhere on the arc between them
- Use
fitParabolaToThreePoints()directly when you only need the coefficients (e.g., to display the equation y = ax² + bx + c in a UI)
8. Combining Multiple Animations
breatheA(),breatheBase(), androtate()are fully composable — run any combination simultaneously- Each animation has its own elapsed-time accumulator for independent pause/resume control
- Pairing spin with breatheBase creates a "flying bird" effect; pairing breatheA with breatheBase creates an organic pulsing feel
Integration with Other SketchWave Classes
Script Loading Order
SWArch depends on SWColor, SWPoint, and SWGrid. SWSinusoid is needed for breatheA() and breatheBase():
<!-- p5.js library -->
<script src="https://cdn.jsdelivr.net/npm/p5@1.6.0/lib/p5.js"></script>
<!-- SketchWaveJS classes in dependency order -->
<script src="../shapeClasses/swSinusoid.js"></script>
<script src="../shapeClasses/swColor.js"></script>
<script src="../shapeClasses/swPoint.js"></script>
<script src="../shapeClasses/swGrid.js"></script>
<script src="../shapeClasses/swArch.js"></script>
<!-- Your sketch -->
<script src="../sketches/yourSketch.js"></script>
Working with SWPoint
SWArch uses SWPoint in three ways:
- The
vertexproperty is a full SWPoint — drawn as a dot whenshouldShowVertexis true; repositioning it moves the entire arch getLeftEndpoint()andgetRightEndpoint()return computed SWPoints (reflecting currenta, extents, and rotation) — they are not stored properties- When
shouldShowEndpointsis true, endpoint dots are rendered automatically insidedrawOnGrid()
Working with SWColor
SWArch uses SWColor for all color management (HSB mode):
- Colors are deep-copied at construction and by all setter methods — external mutations to the source color have no effect after construction
- The recommended demo color convention: fill =
SWColor(hue, 55, 92, alpha, "archFill"); stroke =SWColor((hue+10)%360, 70, 50, 100, "archStroke") - Default hue for the demo is 200 (blue family); the fill hue slider shifts both fill and border (+10°) together
Working with SWSinusoid
SWArch's breatheA() and breatheBase() methods both accept a SWSinusoid instance:
- The sinusoid's
getValue(t)is called with elapsed time in seconds - Use
SWSinusoid.copy(UNIT_SINUSOID)withsetPeriod()andadjustWaveUsingExtrema()for the most convenient setup - For
breatheBase(), the value is passed throughMath.abs()internally — so even if the sinusoid goes negative, extents stay positive
Comparing SWArch and SWSector
Both are closed (or open) polygon shapes drawn directly on a grid, but they differ significantly:
| Feature | SWArch | SWSector |
|---|---|---|
| Curve type | Quadratic parabola (y = a·x²) | Circular arc (constant radius) |
| Control point | Vertex (extremum) | Center (equidistant pivot) |
| Width control | xLeft + xRight (asymmetric) | radius (symmetric) |
| Shape parameter | a coefficient | theta (arc angle) |
| Open/closed toggle | shouldCloseShape property |
not applicable |
| Breathing animation | breatheA (curvature) + breatheBase (width) | breatheRadius |
| Asymmetry | xLeft ≠ xRight shifts vertex within base | Not applicable (radially symmetric) |
| Factory constructor | fromThreePoints() |
not applicable |
| Natural use case | Arch, rainbow, bridge, open arc, lens shapes | Pie slices, fan shapes, gauges |
Source Code
The complete SWArch class implementation:
Show/Hide Source Code
/*
File: swArch.js
Date: 2026-04-21
Author: klp
App: SketchWaveTNT2026-04-21-Stg8
Purpose: SWArch class for SketchWaveJS
SWArch represents a parabolic arch shape defined by the quadratic function
y = a·x² in a local coordinate system centered at the vertex:
vertex (SWPoint) — the extremum of the arch:
a < 0 → vertex is the HIGHEST point (∩ rainbow/arch shape)
a > 0 → vertex is the LOWEST point (∪ bowl shape)
a (number) — quadratic coefficient controlling curvature and orientation
xLeft (number) — horizontal distance from vertex to the left endpoint (> 0)
xRight (number) — horizontal distance from vertex to the right endpoint (> 0)
In local space (vertex at origin):
left endpoint: (−xLeft, a · xLeft²)
right endpoint: ( xRight, a · xRight²)
curve: y = a · x² for x ∈ [−xLeft, xRight]
Rotation:
rotationDeg — static base rotation (CCW degrees from no-rotation), set by
setRotation(). Persists across frames; survives reset().
rotation — accumulated rotation (degrees), incremented by rotate().
Starts at 0; reset() returns it to 0.
Effective rotation used for drawing = rotationDeg + rotation.
All rotation is about the VERTEX.
Angle convention (same as all SketchWaveJS classes):
User space: CCW positive, y increases upward (standard math/Cartesian).
p5 screen: CW positive, y increases downward.
SWArch handles the y-flip internally; always pass CCW degrees.
The drawn shape is a closed polygon: the parabolic arc sampled at SAMPLE_COUNT
points from the left to right endpoint, closed by a straight base line.
Fill and stroke are applied to the full outline.
Animations (call once per frame in the p5 draw loop):
rotate(delta) — spins the arch about its vertex
breatheA(sinusoid, t) — oscillates ‘a’ to open/close the arch curvature
Point visibility:
shouldShowVertex — shows/hides the SWPoint dot at the vertex
shouldShowEndpoints — shows/hides SWPoint dots at the left and right endpoints
Dependencies: p5.js, SWColor, SWPoint, SWGrid.
*/
console.log("[swArch.js] SWArch class loaded.");
class SWArch {
static SAMPLE_COUNT = 60; // arc smoothness (points sampled along parabola)
/**
* @param {SWPoint} vertex - Arch extremum in user (grid) coordinates
* @param {number} a - Quadratic coefficient (a < 0 → arch ∩; a > 0 → bowl ∪)
* @param {number} [xLeft=3] - Left-side horizontal extent from vertex (positive)
* @param {number} [xRight=3] - Right-side horizontal extent from vertex (positive)
* @param {SWColor} [fillColor] - Fill color (undefined = no fill)
* @param {SWColor} [strokeColor] - Border color (undefined = no stroke)
* @param {number} [thickness=2] - Border stroke weight
* @param {number} [rotationDeg=0] - Static base rotation in CCW degrees
*/
constructor(vertex, a, xLeft = 3, xRight = 3,
fillColor = undefined, strokeColor = undefined,
thickness = 2, rotationDeg = 0) {
this.vertex = vertex;
this.a = a;
this.xLeft = Math.abs(xLeft);
this.xRight = Math.abs(xRight);
this.fillColor = fillColor ? SWColor.copy(fillColor) : undefined;
this.strokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
this.thickness = thickness;
this.rotationDeg = rotationDeg;
this.rotation = 0; // accumulated via rotate(); cleared by reset()
this.shouldShowVertex = true;
this.shouldShowEndpoints = true;
// When true (default), endShape(CLOSE) draws a straight line connecting
// the two endpoints to close the polygon (base line visible).
// Set to false to draw only the parabolic arc with no closing base line.
this.shouldCloseShape = true;
// —— Originals for reset() —————————————————————————
this.originalA = a;
this.originalXLeft = Math.abs(xLeft);
this.originalXRight = Math.abs(xRight);
this.originalFillColor = fillColor ? SWColor.copy(fillColor) : undefined;
this.originalStrokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
this.originalThickness = thickness;
this.originalRotationDeg = rotationDeg;
}//end constructor
// —— Internal helpers ————————————————————————————————
/** @returns {number} Total effective rotation in degrees. */
_totalRotDeg() { return this.rotationDeg + this.rotation; }
/**
* Rotates a local math-space point (lx, ly) by totalRotation degrees (CCW+).
* @returns {{ x: number, y: number }} rotated point in math-space
*/
_rotateLocal(lx, ly) {
const rad = this._totalRotDeg() * Math.PI / 180;
const cosR = Math.cos(rad);
const sinR = Math.sin(rad);
return {
x: lx * cosR - ly * sinR,
y: lx * sinR + ly * cosR,
};
}
/**
* Builds an array of screen-space { x, y } points for drawOnGrid().
* grid.userToScreen() handles the math-space ↔ screen y-flip.
*/
_buildScreenPtsGrid(grid) {
const pts = [];
const n = SWArch.SAMPLE_COUNT;
const span = this.xLeft + this.xRight;
const vx = this.vertex.x;
const vy = this.vertex.y;
for (let i = 0; i <= n; i++) {
const t = i / n;
const lx = -this.xLeft + t * span;
const ly = this.a * lx * lx;
const r = this._rotateLocal(lx, ly);
const sc = grid.userToScreen(vx + r.x, vy + r.y);
pts.push(sc);
}
return pts;
}
/**
* Builds an array of screen-space { x, y } points for draw() (no grid).
* r.y (math-space up) is negated to produce the correct screen-space displacement.
*/
_buildScreenPtsDirect() {
const pts = [];
const n = SWArch.SAMPLE_COUNT;
const span = this.xLeft + this.xRight;
const vx = this.vertex.x;
const vy = this.vertex.y;
for (let i = 0; i <= n; i++) {
const t = i / n;
const lx = -this.xLeft + t * span;
const ly = this.a * lx * lx;
const r = this._rotateLocal(lx, ly);
pts.push({ x: vx + r.x, y: vy - r.y }); // y-flip for screen space
}
return pts;
}
/**
* Executes beginShape / vertex / endShape from pre-computed screen points.
* If shouldCloseShape is true (default), endShape(CLOSE) adds a straight
* line back to the first point, forming a closed polygon with a visible base.
* If shouldCloseShape is false, endShape() leaves the shape open so only
* the parabolic arc is drawn — no connecting line between the endpoints.
*/
_drawShape(screenPts) {
if (this.fillColor && this.fillColor.col) {
fill(this.fillColor.col);
} else {
noFill();
}
if (this.strokeColor && this.strokeColor.col) {
stroke(this.strokeColor.col);
} else {
noStroke();
}
strokeWeight(this.thickness);
beginShape();
for (const sp of screenPts) {
vertex(sp.x, sp.y);
}
// CLOSE connects the last sampled point back to the first with a straight
// line (the base/chord). Omitting it draws only the arc.
if (this.shouldCloseShape) {
endShape(CLOSE);
} else {
endShape();
}
noStroke();
noFill();
strokeWeight(1);
}
// —— Endpoint helpers ————————————————————————————————
getLeftEndpoint() {
const lx = -this.xLeft;
const ly = this.a * this.xLeft * this.xLeft;
const r = this._rotateLocal(lx, ly);
return new SWPoint(this.vertex.x + r.x, this.vertex.y + r.y, undefined, 7);
}
getRightEndpoint() {
const rx = this.xRight;
const ry = this.a * this.xRight * this.xRight;
const r = this._rotateLocal(rx, ry);
return new SWPoint(this.vertex.x + r.x, this.vertex.y + r.y, undefined, 7);
}
// —— Drawing ————————————————————————————————————————————
draw() {
const screenPts = this._buildScreenPtsDirect();
this._drawShape(screenPts);
if (this.shouldShowVertex && this.vertex && this.vertex.draw) {
this.vertex.draw(this.strokeColor);
}
if (this.shouldShowEndpoints) {
const lx = -this.xLeft;
const ly = this.a * this.xLeft * this.xLeft;
const rx = this.xRight;
const ry = this.a * this.xRight * this.xRight;
const lr = this._rotateLocal(lx, ly);
const rr = this._rotateLocal(rx, ry);
const lpt = new SWPoint(this.vertex.x + lr.x, this.vertex.y - lr.y, undefined, 7);
const rpt = new SWPoint(this.vertex.x + rr.x, this.vertex.y - rr.y, undefined, 7);
if (lpt.draw) lpt.draw(this.strokeColor);
if (rpt.draw) rpt.draw(this.strokeColor);
}
}//end draw
drawOnGrid(grid) {
const screenPts = this._buildScreenPtsGrid(grid);
this._drawShape(screenPts);
if (this.shouldShowVertex && this.vertex && this.vertex.drawOnGrid) {
this.vertex.drawOnGrid(grid, this.strokeColor);
}
if (this.shouldShowEndpoints) {
const lep = this.getLeftEndpoint();
const rep = this.getRightEndpoint();
if (lep.drawOnGrid) lep.drawOnGrid(grid, this.strokeColor);
if (rep.drawOnGrid) rep.drawOnGrid(grid, this.strokeColor);
}
}//end drawOnGrid
// —— Animations ————————————————————————————————————————————
rotate(deltaAngle) { this.rotation += deltaAngle; }
breatheA(sinusoid, t) { this.a = sinusoid.getValue(t); }
breatheBase(sinusoid, t) {
const v = Math.abs(sinusoid.getValue(t));
this.xLeft = v;
this.xRight = v;
}
// —— Setters —————————————————————————————————————————————————————————————————
setA(a) { this.a = a; }
setXLeft(v) { this.xLeft = Math.abs(v); }
setXRight(v) { this.xRight = Math.abs(v); }
setRotation(deg) { this.rotationDeg = deg; }
setFillColor(fc) { this.fillColor = fc ? SWColor.copy(fc) : undefined; }
setStrokeColor(sc) { this.strokeColor = sc ? SWColor.copy(sc) : undefined; }
setStrokeWeight(w) { this.thickness = w; }
setFillAlpha(alpha) {
if (this.fillColor) {
this.fillColor.a = Math.max(0, Math.min(100, alpha));
this.fillColor.col = color(
this.fillColor.h, this.fillColor.s,
this.fillColor.b, this.fillColor.a
);
}
}
setShowVertex(show = true) { this.shouldShowVertex = show; }
setShowEndpoints(show = true) { this.shouldShowEndpoints = show; }
setShowPoints(show = true) {
this.shouldShowVertex = show;
this.shouldShowEndpoints = show;
}
// —— Reset & Utility ————————————————————————————————————————————————————————————
reset() {
this.a = this.originalA;
this.xLeft = this.originalXLeft;
this.xRight = this.originalXRight;
this.rotationDeg = this.originalRotationDeg;
this.rotation = 0;
this.thickness = this.originalThickness;
this.fillColor = this.originalFillColor
? SWColor.copy(this.originalFillColor) : undefined;
this.strokeColor = this.originalStrokeColor
? SWColor.copy(this.originalStrokeColor) : undefined;
}//end reset
static copy(other) {
const c = new SWArch(
SWPoint.copy(other.vertex),
other.originalA,
other.originalXLeft,
other.originalXRight,
other.originalFillColor,
other.originalStrokeColor,
other.originalThickness,
other.originalRotationDeg
);
c.a = other.a;
c.xLeft = other.xLeft;
c.xRight = other.xRight;
c.rotationDeg = other.rotationDeg;
c.rotation = other.rotation;
c.shouldShowVertex = other.shouldShowVertex;
c.shouldShowEndpoints = other.shouldShowEndpoints;
return c;
}//end copy
toString() {
return `SWArch(vertex=${this.vertex}, a=${this.a.toFixed(4)}, ` +
`xLeft=${this.xLeft.toFixed(2)}, xRight=${this.xRight.toFixed(2)}, ` +
`rotationDeg=${this.rotationDeg.toFixed(1)}, ` +
`rotation=${this.rotation.toFixed(1)})`;
}
// —— Static factory helpers ——————————————————————————————————————————————————————
static fitParabolaToThreePoints(p1, p2, p3) {
const x1 = p1.x, y1 = p1.y;
const x2 = p2.x, y2 = p2.y;
const x3 = p3.x, y3 = p3.y;
if (Math.abs(x1 - x2) < 1e-9 ||
Math.abs(x1 - x3) < 1e-9 ||
Math.abs(x2 - x3) < 1e-9) {
console.warn("SWArch.fitParabolaToThreePoints: two or more points " +
"share an x-coordinate — cannot fit a standard-form parabola.");
return null;
}
const denom1 = x2 - x1;
const denom2 = x3 - x1;
const slope12 = (y2 - y1) / denom1;
const slope13 = (y3 - y1) / denom2;
const denominatorA = x3 - x2;
if (Math.abs(denominatorA) < 1e-9) {
console.warn("SWArch.fitParabolaToThreePoints: degenerate system.");
return null;
}
const a = (slope13 - slope12) / denominatorA;
if (Math.abs(a) < 1e-9) {
console.warn("SWArch.fitParabolaToThreePoints: computed a ≈ 0 — " +
"the three points appear to be collinear.");
return null;
}
const b = slope12 - a * (x2 + x1);
const c = y1 - a * x1 * x1 - b * x1;
return { a, b, c };
}//end fitParabolaToThreePoints
static fromThreePoints(p1, p2, p3,
fillColor = undefined, strokeColor = undefined,
thickness = 2, rotationDeg = 0) {
const coeffs = SWArch.fitParabolaToThreePoints(p1, p2, p3);
if (!coeffs) return null;
const { a, b, c } = coeffs;
const h = -b / (2 * a);
const k = c - (b * b) / (4 * a);
const localXs = [p1.x - h, p2.x - h, p3.x - h];
const minLocalX = Math.min(...localXs);
const maxLocalX = Math.max(...localXs);
const xLeft = Math.abs(minLocalX);
const xRight = Math.abs(maxLocalX);
const vertexPt = new SWPoint(h, k);
return new SWArch(vertexPt, a, xLeft, xRight,
fillColor, strokeColor, thickness, rotationDeg);
}//end fromThreePoints
}//end SWArch class