🌉 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. The drawn shape is a closed polygon — a smooth parabolic arc sampled at 60 points, connected by a straight 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), static rotationDeg + accumulated rotation, breatheA (curvature oscillation), breatheBase (width oscillation), fill/stroke colors, optional vertex/endpoint markers
  • Common Uses: Arch shapes, bridge graphics, rainbow curves, bowl/lens forms, optical illusion arcs, geometric art

Overview

The SWArch class represents a parabolic arch drawn as a closed polygon. 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, then closes with a straight base line back to the starting endpoint.

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.

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; closed with straight base line
  • 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())

Typical Workflow

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

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);

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

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.

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

Example
arch.reset(); // full restore to factory state

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 and rotationDeg are preserved in the copy.

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

SWArch — a new independent instance

Example
let archCopy = SWArch.copy(arch1);
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)"

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: 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 3: 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 4: 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 5: 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 6: 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. 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);
}

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

4. 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);

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

6. 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 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)
Breathing animation breatheA (curvature) + breatheBase (width) breatheRadius
Asymmetry xLeft ≠ xRight shifts vertex within base Not applicable (radially symmetric)
Natural use case Arch, rainbow, bridge, 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
  breatheBase(sinusoid, t)  — oscillates xLeft and xRight to expand/contract base width

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;

        // —— 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(CLOSE) from pre-computed screen points. */
    _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);
        }
        endShape(CLOSE);
        noStroke();
        noFill();
        strokeWeight(1);
    }

    // —— Endpoint helpers ————————————————————————————————————————————————————————————————

    /**
     * Returns the left endpoint as an SWPoint in user (grid) space.
     * strokeWeight=7 gives a visible dot; color is supplied at draw time.
     * @returns {SWPoint}
     */
    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);
    }

    /**
     * Returns the right endpoint as an SWPoint in user (grid) space.
     * strokeWeight=7 gives a visible dot; color is supplied at draw time.
     * @returns {SWPoint}
     */
    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 —————————————————————————————————————————————————————————————————————————————————————————————

    /**
     * Draws the arch in raw screen (pixel) coordinates.
     * Prefer drawOnGrid() for standard canvas use.
     */
    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

    /**
     * Draws the arch mapped through the given SWGrid's coordinate system.
     * This is the standard drawing method to use in a p5.js draw() loop.
     * @param {SWGrid} grid
     */
    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 ————————————————————————————————————————————————————————————————————————————————————————————

    /**
     * Spins the arch about its vertex by deltaAngle degrees (CCW+, CW−).
     * Accumulates into this.rotation.  Call once per frame in draw().
     * @param {number} deltaAngle  degrees per frame (typically speed × deltaT)
     */
    rotate(deltaAngle) { this.rotation += deltaAngle; }

    /**
     * Oscillates the ‘a’ coefficient to open and close the arch curvature.
     * Larger |a| → sharper arch; smaller |a| → flatter arch; a crossing zero
     * → arch flips between ∩ and ∪.
     * Call each frame; when the animation is disabled, restore arch.a via
     * setA() or reset().
     * @param {SWSinusoid} sinusoid  configured with min/max a values
     * @param {number}     t         elapsed time in seconds
     */
    breatheA(sinusoid, t) { this.a = sinusoid.getValue(t); }

    /**
     * Oscillates xLeft and xRight together symmetrically to expand and contract
     * the base of the arch — like wings opening and closing.
     * The sinusoid should be configured with positive min/max extent values.
     * Call each frame; when the animation is disabled, restore xLeft and xRight
     * via setXLeft()/setXRight() or reset().
     * @param {SWSinusoid} sinusoid  configured with min/max extent values (both > 0)
     * @param {number}     t         elapsed time in seconds
     */
    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); }

    /** Sets the static base rotation in CCW degrees. Does not affect accumulated rotation. */
    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; }

    /**
     * Sets the fill alpha (0–100) and rebuilds the p5 color object.
     * @param {number} alpha  0 = transparent, 100 = opaque
     */
    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; }

    /** Convenience: show/hide both the vertex marker and the endpoint markers. */
    setShowPoints(show = true) {
        this.shouldShowVertex    = show;
        this.shouldShowEndpoints = show;
    }

    // —— Reset & Utility ——————————————————————————————————————————————————————————————————————————————————————————————————

    /**
     * Restores all animated/slider-driven properties to original constructor values.
     * Clears accumulated spin rotation.  Does NOT move the vertex position.
     */
    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

    /**
     * Creates a deep copy of the given SWArch, preserving all current and original state.
     * @param {SWArch} other
     * @returns {SWArch}
     */
    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)})`;
    }

}//end SWArch class