🌉 SWArch Reference

A SketchWave parametric class for representing a parabolic arch defined by y = a·x² in a local coordinate system centered at the vertex

Back to SWArch Demo

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 = 60 points
  • 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.

Arch vs. Bowl: the sign of a
When 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.
Closed vs. Open Shape: shouldCloseShape
When 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² for x ∈ [−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 by setRotation(). Persists across frames; survives reset().
  • rotation — Accumulated rotation incremented by rotate(). Starts at 0; cleared by reset().

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: shouldCloseShape toggles between a closed polygon (with base line) and an open arc (curve only)
  • Asymmetric Wings: Independent xLeft and xRight extents 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 as a crosses zero
  • Breathe Base: Oscillate xLeft and xRight symmetrically — 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

  1. Create fill and stroke SWColor instances
  2. Construct an SWArch with vertex, coefficient a, xLeft, xRight, and optional colors — or use SWArch.fromThreePoints()
  3. Optionally set arch.shouldCloseShape = false to draw an open arc
  4. Draw each frame using drawOnGrid(grid)
  5. Call rotate() before drawing to spin; call breatheA() or breatheBase() after drawing to animate
  6. 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.

Parameters
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
Constructor Examples
// 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 SWPoint

The 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 number

The 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 number

The 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 number

The 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 new

Controls 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 number

Static 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 number

Accumulated 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 | undefined

The 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 | undefined

The 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 number

The stroke weight in pixels. Use setStrokeWeight() to change.

arch.setStrokeWeight(4);
shouldShowVertex boolean

Whether to draw the vertex SWPoint marker. Default is true. Use setShowVertex() or setShowPoints() to change.

arch.setShowVertex(false); // hide vertex dot
shouldShowEndpoints boolean

Whether 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 targets

Snapshot 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.

Returns

void

Example
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.

Parameters
  • grid (SWGrid) — the coordinate grid
Returns

void

Example
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().

Parameters
  • deltaAngle (number) — degrees to add to rotation
Example
// 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.

Parameters
  • sinusoid (SWSinusoid) — controls a oscillation; configure with desired min/max a values
  • t (number) — elapsed time in seconds
Example
// '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().

Parameters
  • sinusoid (SWSinusoid) — controls width oscillation; configure with positive min/max extent values
  • t (number) — elapsed time in seconds
Example
// 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.

Returns

SWPoint (in user coordinates)

Example
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().

Returns

SWPoint (in user coordinates)

Example
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.

Parameters
  • a (number) — new coefficient value
Example
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.

Parameters
  • v (number) — new extent value (absolute value used)
Example
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.

Parameters
  • deg (number) — new static rotation in CCW degrees
Example
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).

Parameters
  • fc (SWColor | undefined)
Example
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.

Parameters
  • sc (SWColor | undefined)
Example
arch.setStrokeColor(new SWColor(0, 0, 20, 100, "darkBorder"));
setStrokeWeight(w)

Sets the border thickness in pixels.

Parameters
  • w (number) — thickness in pixels
Example
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].

Parameters
  • alpha (number) — 0 = fully transparent, 100 = fully opaque
Example
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.

Parameters
  • show (boolean) — true to show, false to hide (default: true)
Example
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.

Example
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.

Parameters
  • other (SWArch) — the arch to copy
Returns

SWArch — a new independent instance

Example
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.

Returns

string

Example
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:

  1. Calls fitParabolaToThreePoints() to solve the 3×3 linear system for a, b, c
  2. Converts to vertex form: h = −b/(2a), k = c − b²/(4a)
  3. Computes local x-coordinates for all three points relative to the vertex (h, k); uses the leftmost and rightmost as xLeft and xRight
  4. Builds and returns a new SWArch with the computed vertex, a, xLeft, xRight, and the supplied style parameters
Parameters
ParameterTypeDefaultDescription
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
Returns

SWArch | null — the constructed arch, or null if the points are degenerate (collinear, or any two share the same x-coordinate)

Degeneracy guard: Returns 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.
Example
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.

Parameters
ParameterTypeDescription
p1 {x, y} First point in user (grid) coordinates
p2 {x, y} Second point
p3 {x, y} Third point
Returns

{ a, b, c } | null — parabola coefficients as plain numbers, or null if the system is degenerate

Example
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() and breatheBase() 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 fillColor is set, p5 may still render a faint fill in the arc interior — set fillColor = undefined for 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 startTime and elapsed separately 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.1 gives 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 min and max have 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_SINUSOID global with setPeriod() and adjustWaveUsingExtrema(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 null return — degenerate inputs (collinear points, or two points with the same x) return null and 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(), and rotate() 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 vertex property is a full SWPoint — drawn as a dot when shouldShowVertex is true; repositioning it moves the entire arch
  • getLeftEndpoint() and getRightEndpoint() return computed SWPoints (reflecting current a, extents, and rotation) — they are not stored properties
  • When shouldShowEndpoints is true, endpoint dots are rendered automatically inside drawOnGrid()

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) with setPeriod() and adjustWaveUsingExtrema() for the most convenient setup
  • For breatheBase(), the value is passed through Math.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