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 = 60points - 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.
aWhen
a < 0, the parabola opens downward — the vertex is the highest point, producing the classic arch (∩) or rainbow shape.When
a > 0, the parabola opens upward — the vertex is the lowest point, producing a bowl (∪) shape.Larger
|a| makes the curve sharper; smaller |a| makes it flatter.
Local Coordinate System
SWArch works in a local coordinate system centered at the vertex. In local space:
- Left endpoint:
(−xLeft, a · xLeft²) - Right endpoint:
( xRight, a · xRight²) - Curve:
y = a · x²forx ∈ [−xLeft, xRight]
The arch can be asymmetric: set xLeft ≠ xRight to shift the vertex horizontally relative to the base. The base line always connects the two endpoints directly.
Rotation
SWArch supports two layers of rotation, both applied about the vertex:
rotationDeg— Static base rotation set bysetRotation(). Persists across frames; survivesreset().rotation— Accumulated rotation incremented byrotate(). Starts at 0; cleared byreset().
The effective rotation used for drawing is rotationDeg + rotation. All rotation is CCW positive (user-space convention).
// Static tilt: arch leans 15° to the right (CCW = left)
arch.setRotation(-15);
// Spin at 30°/second in draw loop
arch.rotate(30 * deltaT); // call BEFORE drawOnGrid
Key Capabilities
- Parabolic Shape: Smooth curve sampled at 60 points; closed with straight base line
- Asymmetric Wings: Independent
xLeftandxRightextents let you shift the arch peak horizontally - Spin Animation: Continuously rotate the arch about its vertex with
rotate() - Breathe A: Oscillate the curvature coefficient
a— arch opens/closes, or flips between arch and bowl asacrosses zero - Breathe Base: Oscillate
xLeftandxRightsymmetrically — wings expand and contract like a bird in flight - Fill & Stroke: Independent fill color and stroke (border) color; fill opacity control via
setFillAlpha() - Point Markers: Optional display of vertex dot and endpoint dots (using SWPoint)
- Dual Coordinate Systems: Draw in screen pixels (
draw()) or grid user coordinates (drawOnGrid())
Typical Workflow
- Create fill and stroke SWColor instances
- Construct an SWArch with vertex, coefficient a, xLeft, xRight, and optional colors
- Draw each frame using
drawOnGrid(grid) - Call
rotate()before drawing to spin; callbreatheA()orbreatheBase()after drawing to animate - Call
reset()to restore all original values
Constructor
new SWArch(vertex, a, xLeft, xRight, fillColor, strokeColor, thickness, rotationDeg)Creates a new SWArch instance. The vertex position, coefficient, extents, and colors define the arch's initial state. Originals are captured for reset().
| Parameter | Type | Default | Description |
|---|---|---|---|
vertex |
SWPoint | required | Arch extremum in user (grid) coordinates — highest point when a < 0, lowest when a > 0 |
a |
number | required | Quadratic coefficient: a < 0 gives arch (∩), a > 0 gives bowl (∪). Larger |a| = sharper curve. |
xLeft |
number | 3 | Horizontal distance from vertex to the left endpoint (positive; absolute value taken internally) |
xRight |
number | 3 | Horizontal distance from vertex to the right endpoint (positive; absolute value taken internally) |
fillColor |
SWColor | undefined | undefined | Fill color for the arch interior; undefined = no fill (transparent) |
strokeColor |
SWColor | undefined | undefined | Border (outline) color; undefined = no stroke drawn |
thickness |
number | 2 | Stroke weight in pixels |
rotationDeg |
number | 0 | Static base rotation in CCW degrees; applied in addition to accumulated rotation |
// Classic blue arch at origin
const fill = new SWColor(200, 55, 92, 100, "archFill");
const border = new SWColor(210, 70, 50, 100, "archStroke");
let arch = new SWArch(new SWPoint(0, 2), -0.3, 5, 5, fill, border, 2);
// Bowl shape (a > 0), vertex at bottom
const bowlFill = new SWColor(30, 55, 92, 100, "bowlFill");
const bowlBorder = new SWColor(40, 70, 50, 100, "bowlStroke");
let bowl = new SWArch(new SWPoint(0, -2), 0.4, 4, 4, bowlFill, bowlBorder);
// Asymmetric arch: peak shifted right, no fill
let asymArch = new SWArch(new SWPoint(1, 3), -0.25, 6, 3, undefined, border, 2);
// Pre-tilted arch (rotationDeg set at construction)
let tiltedArch = new SWArch(new SWPoint(0, 0), -0.3, 5, 5, fill, border, 2, 45);
// Minimal: stroke-only arch with defaults
const minBorder = new SWColor(0, 0, 20, 100, "darkBorder");
let minimal = new SWArch(new SWPoint(0, 1), -0.2, undefined, undefined, undefined, minBorder);
Properties
vertex SWPointThe arch's extremum in user (grid) coordinates. When a < 0, this is the highest point; when a > 0, the lowest. All rotation is applied about this point. Moving vertex.x or vertex.y repositions the entire arch.
arch.vertex.x = 2; arch.vertex.y = 3;
a numberThe quadratic coefficient controlling curvature and orientation. Negative = arch (∩), positive = bowl (∪). Larger absolute value = sharper curve. Use setA() to change it, or let breatheA() animate it each frame.
arch.setA(-0.5); // sharper arch
arch.setA(0.3); // switch to bowl
xLeft numberThe horizontal distance from the vertex to the left endpoint in local space (always positive). The left endpoint is at local (−xLeft, a · xLeft²). Use setXLeft() to change, or let breatheBase() animate it.
arch.setXLeft(7); // wider left wing
xRight numberThe horizontal distance from the vertex to the right endpoint in local space (always positive). The right endpoint is at local (xRight, a · xRight²). Use setXRight() to change, or let breatheBase() animate it.
arch.setXRight(3); // narrower right wing
rotationDeg numberStatic base rotation in CCW degrees. Set at construction or via setRotation(). Does not change with rotate() calls and is preserved by reset(). Use this for a fixed tilt that survives the reset.
arch.setRotation(30); // tilt 30° CCW permanently
rotation numberAccumulated rotation in degrees (CCW positive), incremented by each rotate() call. Starts at 0; cleared by reset(). The effective drawing rotation is rotationDeg + rotation.
console.log(arch.rotation.toFixed(1) + "°");
fillColor SWColor | undefinedThe fill color for the arch interior. undefined = no fill. Use setFillColor() to replace it, or setFillAlpha() to change its transparency without replacing the color object.
arch.setFillColor(new SWColor(120, 55, 92, 80, "greenFill"));
arch.setFillColor(undefined); // remove fill
strokeColor SWColor | undefinedThe border (outline) color. undefined = no stroke drawn. Use setStrokeColor() to replace it.
arch.setStrokeColor(new SWColor(210, 70, 50, 100, "border"));
arch.setStrokeColor(undefined); // no border
thickness numberThe stroke weight in pixels. Use setStrokeWeight() to change.
arch.setStrokeWeight(4);
shouldShowVertex booleanWhether to draw the vertex SWPoint marker. Default is true. Use setShowVertex() or setShowPoints() to change.
arch.setShowVertex(false); // hide vertex dot
shouldShowEndpoints booleanWhether to draw SWPoint markers at the left and right endpoints of the arch base. Default is true. Use setShowEndpoints() or setShowPoints() to change.
arch.setShowEndpoints(false); // hide endpoint dots
originalA / originalXLeft / originalXRight / originalFillColor / originalStrokeColor / originalThickness / originalRotationDeg various restore targetsSnapshot values captured at construction. reset() uses all of these to restore the arch to its initial state. originalFillColor and originalStrokeColor are deep copies made at construction time.
// 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.
void
function draw() {
background(220);
arch.draw(); // vertex treated as screen pixels
}
drawOnGrid(grid)Draws the arch mapped through the given SWGrid's coordinate system. Converts the vertex and all sampled curve points from user units to screen pixels via grid.userToScreen(). This is the standard method to call in a p5.js draw() loop.
grid(SWGrid) — the coordinate grid
void
function draw() {
background(220);
grid.draw();
arch.drawOnGrid(grid);
}
Rotation Animation
rotate(deltaAngle)Increments the arch's accumulated rotation by deltaAngle degrees about the vertex (CCW positive, CW negative). Call each frame before drawOnGrid().
deltaAngle(number) — degrees to add torotation
// Spin at 45°/second using elapsed time (deltaT)
arch.rotate(spinSpeed * deltaT); // call BEFORE drawOnGrid
// Fixed increment per frame
arch.rotate(1); // 1 degree per frame, CCW
Breathing Animations
breatheA(sinusoid, t)Modulates the a coefficient using an SWSinusoid, opening and closing the arch's curvature. Larger |a| = sharper; smaller |a| = flatter. If the sinusoid crosses zero, the arch flips between ∩ and ∪ shapes mid-animation. Call after drawOnGrid() so the new value takes effect next frame.
sinusoid(SWSinusoid) — controlsaoscillation; configure with desired min/max a valuest(number) — elapsed time in seconds
// 'a' oscillates between -0.1 (flat) and -0.8 (sharp) over 4 seconds
let aSin = SWSinusoid.copy(UNIT_SINUSOID);
aSin.setPeriod(4);
aSin.adjustWaveUsingExtrema(-0.1, -0.8);
arch.drawOnGrid(grid);
arch.breatheA(aSin, elapsedSeconds); // call AFTER draw
breatheBase(sinusoid, t)Modulates both xLeft and xRight symmetrically using an SWSinusoid, expanding and contracting the arch's base width like wings opening and closing. The sinusoid value is passed through Math.abs() to ensure extents are always positive. Call after drawOnGrid().
sinusoid(SWSinusoid) — controls width oscillation; configure with positive min/max extent valuest(number) — elapsed time in seconds
// Base oscillates between 1 (narrow) and 6 (wide) over 3 seconds
let baseSin = SWSinusoid.copy(UNIT_SINUSOID);
baseSin.setPeriod(3);
baseSin.adjustWaveUsingExtrema(1, 6);
arch.drawOnGrid(grid);
arch.breatheBase(baseSin, elapsedSeconds); // call AFTER draw
Endpoint Methods
getLeftEndpoint()Computes and returns the left endpoint of the arch as an SWPoint in user (grid) space, accounting for the current rotation. The point has strokeWeight = 7 for a visible dot. This is a computed value — it reflects the current xLeft, a, and rotation every time it is called.
SWPoint (in user coordinates)
const lep = arch.getLeftEndpoint();
console.log(`Left endpoint: (${lep.x.toFixed(2)}, ${lep.y.toFixed(2)})`);
getRightEndpoint()Computes and returns the right endpoint of the arch as an SWPoint in user (grid) space, accounting for the current rotation. Mirrors getLeftEndpoint().
SWPoint (in user coordinates)
const rep = arch.getRightEndpoint();
console.log(`Right endpoint: (${rep.x.toFixed(2)}, ${rep.y.toFixed(2)})`);
Setter Methods
setA(a)Sets the quadratic coefficient immediately. Negative = arch, positive = bowl.
a(number) — new coefficient value
arch.setA(-0.5); // sharper arch
setXLeft(v) / setXRight(v)Sets the left or right horizontal extent from the vertex. The absolute value is taken internally so negative inputs are safe. Call both to set a symmetric arch; call one at a time for an asymmetric wing adjustment.
v(number) — new extent value (absolute value used)
arch.setXLeft(4);
arch.setXRight(6); // asymmetric: left narrower than right
setRotation(deg)Sets the static base rotation (rotationDeg) in CCW degrees. Does not affect the accumulated rotation; both are combined when drawing.
deg(number) — new static rotation in CCW degrees
arch.setRotation(90); // arch now points right
setFillColor(fc)Sets the fill color. Pass an SWColor to set a new fill; pass undefined to remove fill (transparent interior).
fc(SWColor | undefined)
arch.setFillColor(new SWColor(60, 55, 92, 100, "yellowFill"));
arch.setFillColor(undefined); // no fill
setStrokeColor(sc)Sets the border color. Pass undefined to remove the stroke entirely.
sc(SWColor | undefined)
arch.setStrokeColor(new SWColor(0, 0, 20, 100, "darkBorder"));
setStrokeWeight(w)Sets the border thickness in pixels.
w(number) — thickness in pixels
arch.setStrokeWeight(3);
setFillAlpha(alpha)Sets the fill opacity (0–100) by updating the fill color's alpha channel and rebuilding the p5 color object. Does nothing if fillColor is undefined. Clamped to [0, 100].
alpha(number) — 0 = fully transparent, 100 = fully opaque
arch.setFillAlpha(60); // 60% opaque fill
setShowVertex(show) / setShowEndpoints(show) / setShowPoints(show)Controls visibility of the vertex and/or endpoint SWPoint markers. setShowPoints() sets both simultaneously.
show(boolean) — true to show, false to hide (default: true)
arch.setShowVertex(false); // hide vertex dot only
arch.setShowEndpoints(false); // hide endpoint dots only
arch.setShowPoints(false); // hide all point markers
Reset Method
reset()Restores all animated and slider-driven properties — a, xLeft, xRight, rotationDeg, thickness, fillColor, and strokeColor — to their originals captured at construction. Clears accumulated spin rotation (rotation = 0). Does not move the vertex position.
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.
other(SWArch) — the arch to copy
SWArch — a new independent instance
let archCopy = SWArch.copy(arch1);
toString()Returns a string summarizing the arch's key properties: vertex, a, xLeft, xRight, rotationDeg, and rotation.
string
console.log(arch.toString());
// "SWArch(vertex=SWPoint(x:0, y:2), a=-0.3000, xLeft=5.00, xRight=5.00, rotationDeg=0.0, rotation=0.0)"
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()andbreatheBase()set new values that take effect on the next frame — matching the SWDisk/SWSector convention across SketchWaveJS
arch.rotate(speed * deltaT); // spin → before draw
arch.drawOnGrid(grid); // draw
arch.breatheA(aSin, aElapsed); // breathe → after draw
arch.breatheBase(baseSin, baseElapsed);
2. Elapsed Time vs. frameCount
- Use elapsed seconds (not frame count) for all sinusoid time parameters; this keeps animation speed frame-rate-independent
- Track
startTimeandelapsedseparately per animation so each can be paused and resumed independently
// Pattern for pauseable elapsed-time animation
let breathStart = 0, breathElapsed = 0, isBreathing = false;
function toggleBreathe() {
isBreathing = !isBreathing;
if (isBreathing) breathStart = millis() / 1000;
}
// In draw():
if (isBreathing) {
const t = millis() / 1000;
breathElapsed += (t - breathStart);
breathStart = t;
arch.breatheA(aSin, breathElapsed);
}
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.1gives natural-looking arches on a ±10 unit grid - When using
breatheA()with a sinusoid that crosses zero, the arch will flip between arch and bowl mid-animation — which can be a dramatic visual effect or an unintended bug depending on your sinusoid setup - To keep the arch always arching (never flipping), configure the sinusoid so both
minandmaxhave the same sign
// Safe: always an arch (both negative)
aSin.adjustWaveUsingExtrema(-0.1, -0.8);
// Dramatic: flips arch <-> bowl as 'a' crosses zero
aSin.adjustWaveUsingExtrema(-0.5, 0.5);
5. SWSinusoid Setup for Breathing
- The most convenient setup uses the
UNIT_SINUSOIDglobal withsetPeriod()andadjustWaveUsingExtrema(min, max) - Or construct directly: center = (min+max)/2, amplitude = (max−min)/2, frequency = 1/period
// Base breathes between 1 and 6 with a 3-second period
let baseSin = SWSinusoid.copy(UNIT_SINUSOID);
baseSin.setPeriod(3);
baseSin.adjustWaveUsingExtrema(1, 6);
// Equivalent long form:
let baseSin2 = new SWSinusoid(3.5, 2.5, 1/3, 0);
// center=3.5, amplitude=2.5 → range [1, 6]; frequency=1/3 → period=3s
6. Combining Multiple Animations
breatheA(),breatheBase(), androtate()are fully composable — run any combination simultaneously- Each animation has its own elapsed-time accumulator for independent pause/resume control
- Pairing spin with breatheBase creates a "flying bird" effect; pairing breatheA with breatheBase creates an organic pulsing feel
Integration with Other SketchWave Classes
Script Loading Order
SWArch depends on SWColor, SWPoint, and SWGrid. SWSinusoid is needed for breatheA() and breatheBase():
<!-- p5.js library -->
<script src="https://cdn.jsdelivr.net/npm/p5@1.6.0/lib/p5.js"></script>
<!-- SketchWaveJS classes in dependency order -->
<script src="../shapeClasses/swSinusoid.js"></script>
<script src="../shapeClasses/swColor.js"></script>
<script src="../shapeClasses/swPoint.js"></script>
<script src="../shapeClasses/swGrid.js"></script>
<script src="../shapeClasses/swArch.js"></script>
<!-- Your sketch -->
<script src="../sketches/yourSketch.js"></script>
Working with SWPoint
SWArch uses SWPoint in three ways:
- The
vertexproperty is a full SWPoint — drawn as a dot whenshouldShowVertexis true; repositioning it moves the entire arch getLeftEndpoint()andgetRightEndpoint()return computed SWPoints (reflecting currenta, extents, and rotation) — they are not stored properties- When
shouldShowEndpointsis true, endpoint dots are rendered automatically insidedrawOnGrid()
Working with SWColor
SWArch uses SWColor for all color management (HSB mode):
- Colors are deep-copied at construction and by all setter methods — external mutations to the source color have no effect after construction
- The recommended demo color convention: fill =
SWColor(hue, 55, 92, alpha, "archFill"); stroke =SWColor((hue+10)%360, 70, 50, 100, "archStroke") - Default hue for the demo is 200 (blue family); the fill hue slider shifts both fill and border (+10°) together
Working with SWSinusoid
SWArch's breatheA() and breatheBase() methods both accept a SWSinusoid instance:
- The sinusoid's
getValue(t)is called with elapsed time in seconds - Use
SWSinusoid.copy(UNIT_SINUSOID)withsetPeriod()andadjustWaveUsingExtrema()for the most convenient setup - For
breatheBase(), the value is passed throughMath.abs()internally — so even if the sinusoid goes negative, extents stay positive
Comparing SWArch and SWSector
Both are closed 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