⌒ SWArc Reference

A SketchWave class for animated circular arcs — the "crust" of a circle

Back to SWArc Demo

Quick Reference

SWArc is a SketchWave class for drawing circular arcs — the curved "crust" of a circle — with no interior fill. An arc is defined by a center SWPoint, a radius, an angular size theta, a startAngle, a stroke thickness, and a stroke color. It supports five independent animations: spin, breathe radius, breathe theta, breathe thickness (unique to SWArc!), and hue cycling.

  • Extends: Nothing (standalone class)
  • Dependencies: SWPoint, SWColor, SWSinusoid, SWGrid, p5.js
  • Key Features: Stroke-only arc (no fill), configurable stroke cap, arc length calculation, spin, breathe radius, breathe theta, breathe thickness, and hue cycling
  • Common Uses: Loading spinners, progress rings, radar sweeps, rainbow-band effects, celestial orbit paths, lens flares

Overview

The SWArc class represents a circular arc — the curved boundary of a sector, drawn as a stroke with no fill. Think of an SWSector pizza slice where the interior filling and the two straight-edge cuts are discarded, leaving only the outer crust.

Because the arc has no fill, its thickness (stroke weight) becomes the primary visual variable — thick arcs look like solid ribbons, thin arcs look like fine lines. This makes breatheThickness() especially interesting: the arc's visual mass pulses.

Angle Convention

SWArc uses the same math-space (user-space) angle convention as SWSector: angles are measured counterclockwise (CCW) from the positive x-axis, and y-values increase upward. Because p5.js uses a y-down screen space, all angles are negated internally. Pass CCW degrees from +x; the class handles the rest.

// startAngle=0, theta=180 → semicircle opening upward (into Q1 and Q2)
let arc1 = new SWArc(new SWPoint(0,0), 5, 180);

// startAngle=90, theta=90 → quarter arc from +y axis leftward (Q2)
let arc2 = new SWArc(new SWPoint(0,0), 5, 90, 90);

SWArc vs SWSector

FeatureSWSectorSWArc
Pivot pointvertex (tip)center (circle center)
FillYes (fillColor)No (stroke only)
StrokeOptional borderPrimary visual element
Areaarea propertyNone (no interior)
p5 arc modePIEOPEN
Stroke capN/AcapStyle: round / butt / square
Unique animationNonebreatheThickness()

Stroke Cap Styles

The capStyle property controls how the two arc endpoints are rendered. It accepts the raw HTML Canvas lineCap string values (which correspond to p5.js constants):

capStyle valuep5 constantAppearance
'round'ROUNDSemicircular ends — smooth, organic look. Default.
'butt'SQUAREFlat ends, flush with the arc endpoint. Clean, precise.
'square'PROJECTFlat ends extending half-thickness past the endpoint. Makes short arcs appear slightly longer.

Typical Workflow

  1. Create an SWArc with center, radius, theta, startAngle, thickness, and strokeColor
  2. Call drawOnGrid(grid) each frame
  3. For animations, call timing helpers each frame:
    • rotate(delta) before draw (takes effect immediately)
    • breatheRadius(), breatheTheta(), breatheThickness() after draw (affect next frame)
    • Hue cycling: mutate arc.strokeColor.h and rebuild arc.strokeColor.col before draw
  4. Use the elapsed-time pattern for pause/resume of each animation
  5. Call reset() to restore original geometry and color

Constructor

new SWArc(center, radius, theta, startAngle, thickness, strokeColor, capStyle)

Creates a new SWArc instance with the given geometry and styling.

Parameters
ParameterTypeDefaultDescription
center SWPoint required Center of the underlying circle (not the vertex tip)
radius number required Radius in user units (> 0)
theta number required Angular size in degrees; clamped to [0, 360]
startAngle number 0 Arc start angle in degrees CCW from +x axis
thickness number 6 Stroke weight in pixels (the primary visual variable)
strokeColor SWColor undefined Arc color; no arc drawn if undefined
capStyle string 'round' End-cap style: 'round', 'butt', or 'square'
Constructor Examples
// Minimal: center, radius, theta (defaults to 6px thick, no color)
let a1 = new SWArc(new SWPoint(0, 0), 5, 180);

// With start angle: arc opens from the top (90°) sweeping 90° CCW
let a2 = new SWArc(new SWPoint(0, 0), 4, 90, 90);

// Full constructor: cobalt blue, 10px thick, round caps
let center = new SWPoint(0, 0);
let arcColor = new SWColor(213, 100, 80, 90, "cobalt");
let a3 = new SWArc(center, 5, 180, 0, 10, arcColor, 'round');

// Flat cap arc useful for tick marks or progress indicators
let a4 = new SWArc(new SWPoint(0, 0), 7, 45, 270, 4, arcColor, 'butt');

Properties

centerSWPoint

The center of the underlying circle. Drag this point to reposition the arc. The arc draws around this point at the given radius.

radiusnumber

Radius of the arc in user units. Modified by breatheRadius() or setRadius(). Also updates arcLength.

thetanumber

Angular size of the arc in degrees, clamped to [0, 360]. 180° = semicircle, 360° = full circle. Modified by breatheTheta() or setTheta().

startAnglenumber

In degrees CCW from the +x axis; sets the arc's initial orientation (where it begins). Does not change with rotate(); rotation is accumulated separately in rotation.

rotationnumber

Accumulated rotation in degrees (CCW positive). Added to startAngle when rendering. Modified each frame by rotate(delta).

thicknessnumber

Stroke weight in pixels — this is the primary visual variable for arcs. Thick arcs look like solid bands; thin arcs look like fine lines. Modified by breatheThickness() or setStrokeWeight().

strokeColorSWColor

The arc's visible color — the only color property (no fill). Modify strokeColor.h then rebuild strokeColor.col for hue cycling. Set via setStrokeColor().

capStylestring

End-cap style for the arc endpoints. One of 'round', 'butt', or 'square'. Passed directly to the HTML Canvas lineCap property via p5's strokeCap().

arcLengthnumber (computed, read-only)

The arc length in user units: (theta / 360) × 2π × radius. Automatically recalculated whenever radius or theta changes.

shouldShowCenterboolean

When true (default), the center SWPoint is rendered as a visible marker. Toggle with setShowCenter().

Original-state Properties (used by reset())

Set from constructor arguments and updated by slider interactions. Used to restore the arc to its initial state.

  • originalRadius
  • originalTheta
  • originalStartAngle
  • originalRotation (always 0 at construction)
  • originalThickness
  • originalStrokeColor (deep copy of strokeColor at construction)

Methods

Drawing

draw()

Draws the arc in screen (pixel) coordinates, using the center's raw x/y as pixel positions. Use drawOnGrid() for user-coordinate rendering.

drawOnGrid(grid)

Draws the arc mapped through the given SWGrid. User coordinates (center.x, center.y, radius) are converted to screen pixels via the grid's scale and offset. Use this in a demo that has an SWGrid.

// Typical setup in draw()
grid.draw();
arc1.drawOnGrid(grid);

Rotation Animation

rotate(deltaAngle)

Increments rotation by deltaAngle degrees. CCW positive, CW negative. Call before draw each frame to spin the arc.

ParameterTypeDescription
deltaAnglenumberDegrees to add to rotation; negative = clockwise
// In draw(): spin at 45 deg/sec
arc1.rotate(45 * deltaT);   // deltaT = seconds since last frame
arc1.drawOnGrid(grid);

Breathing Animations

All breathing animations use the elapsed-time pattern: accumulate paused time so pause/resume keeps the wave in sync.

breatheRadius(sinusoid, t)

Sets radius to the value of sinusoid.getValue(t). Configure the sinusoid's amplitude and period to control the range and speed of the breathing. Clamps to 0.01 minimum.

ParameterTypeDescription
sinusoidSWSinusoidConfigured with desired period and min/max values
tnumberElapsed time in seconds (use accumulated elapsed time for pause/resume)

breatheTheta(sinusoid, t)

Sets theta to the value of sinusoid.getValue(t), then clamps it to [0, 360] and updates arcLength. Makes the arc span open and close like a mouth.

// Setup (once):
let thetaSin = SWSinusoid.copy(UNIT_SINUSOID);
thetaSin.setPeriod(3);
thetaSin.adjustWaveUsingExtrema(10, 350);   // swings 10°–350°

// In draw():
arc1.breatheTheta(thetaSin, elapsed + (t - startTime));

breatheThickness(sinusoid, t)Unique to SWArc

Sets thickness to the value of sinusoid.getValue(t). The arc line itself swells and thins. Clamps to 1 pixel minimum. Combine with breatheRadius() for a pulsing-rope effect.

ParameterTypeDescription
sinusoidSWSinusoidConfigured sinusoid (use adjustWaveUsingExtrema for intuitive min/max pixel values)
tnumberElapsed animated time in seconds
// Setup (once):
let thickSin = SWSinusoid.copy(UNIT_SINUSOID);
thickSin.setPeriod(2.5);
thickSin.adjustWaveUsingExtrema(2, 20);   // 2px thin → 20px thick

// In draw():
arc1.breatheThickness(thickSin, elapsed + (t - startTime));

Color Methods

setStrokeAlpha(alpha)

Sets the arc color's alpha (transparency) value, clamped to [0, 100]. Updates both strokeColor.a and strokeColor.col.

setStrokeColor(swColor)

Sets a new arc stroke color. Deep-copies the provided SWColor to avoid aliasing of shared color objects.

Reset Methods

reset()

Restores all animated values — radius, theta, startAngle, rotation, thickness, and strokeColor — to the originals stored at construction. Also recomputes arcLength.

resetStrokeColor()

Restores only the stroke color to the original stored at construction. Useful when stopping hue cycling without resetting geometry.

Setter Methods

setStrokeWeight(w)

Sets the arc's stroke weight (thickness) in pixels directly.

setRadius(r)

Sets the radius and recomputes arcLength.

setTheta(degrees)

Sets the angular size and recomputes arcLength (which clamps theta to [0, 360]).

setStartAngle(degrees)

Sets the arc's starting angle (CCW from +x) without affecting the accumulated rotation.

setCapStyle(cap)

Sets the endpoint cap style. Accepts 'round', 'butt', or 'square'.

setShowCenter(show)

Shows or hides the center marker point. Defaults to true.

Static Methods

static copy(other)

Returns a deep copy of the given SWArc instance: deep-copies center, color, and all geometry. Throws if argument is not an SWArc.

let arcCopy = SWArc.copy(arc1);

Utility Methods

toString()

Returns a human-readable description: center, radius, theta, startAngle, rotation, thickness, capStyle, and arcLength.

console.log(arc1.toString());
// → SWArc(center: SWPoint(0.00, 0.00), radius: 5.00, theta: 180.0°,
//         startAngle: 0°, rotation: 0.0°, thickness: 8.0, cap: 'round', arcLength: 15.71)

Animation Guide

Elapsed-Time Pattern

All three breathe animations use the same pause/resume approach: accumulate elapsed seconds when paused, so the wave doesn't jump when you resume.

// Globals:
let shouldBreatheThickness = false;
let thicknessSin;
let thicknessBreathStartTime = 0, thicknessBreathElapsed = 0;

// Toggle on/off:
function toggleBreatheThickness() {
    const t = millis() / 1000;
    if (!shouldBreatheThickness) {
        thicknessBreathStartTime = t;
        shouldBreatheThickness   = true;
    } else {
        thicknessBreathElapsed  += (t - thicknessBreathStartTime);
        shouldBreatheThickness   = false;
    }
}

// In draw():
if (shouldBreatheThickness) {
    const bwt = thicknessBreathElapsed + (millis()/1000 - thicknessBreathStartTime);
    arc1.breatheThickness(thicknessSin, bwt);
}

Hue Cycling

Hue is cycled externally — mutate strokeColor.h then rebuild strokeColor.col before the draw call:

// In draw() — cycle hue smoothly:
if (shouldCycleHue) {
    const hueT = hueCycleElapsed + (millis()/1000 - hueCycleStartTime);
    arc1.strokeColor.h = hueSinusoid.getValue(hueT);
    arc1.strokeColor.col = color(
        arc1.strokeColor.h,
        arc1.strokeColor.s,
        arc1.strokeColor.b,
        arc1.strokeColor.a
    );
}
arc1.drawOnGrid(grid);

Running All Five Animations Simultaneously

// In draw() order — spin + hue BEFORE draw, breathe AFTER draw:
const t      = millis() / 1000;
const deltaT = t - prevT;   prevT = t;

background(bgColor);
if (showGrid) grid.draw();

// 1. Spin
if (shouldSpin) arc1.rotate(spinSpeed * deltaT);

// 2. Hue cycle
if (shouldCycleHue) {
    arc1.strokeColor.h = hueSin.getValue(hueCycleElapsed + t - hueCycleStart);
    arc1.strokeColor.col = color(arc1.strokeColor.h, arc1.strokeColor.s,
                                  arc1.strokeColor.b, arc1.strokeColor.a);
}

// 3. Draw
arc1.drawOnGrid(grid);

// 4. Breathe (take effect next frame)
if (shouldBreatheRadius)    arc1.breatheRadius(radiusSin, ...);
if (shouldBreatheTheta)     arc1.breatheTheta(thetaSin, ...);
if (shouldBreatheThickness) arc1.breatheThickness(thickSin, ...);

Source Code

The full swArc.js source code is shown below for reference.

/*
File: swArc.js
Date: 2026-03-28
Author: klp
App:  SketchWaveTNT2026-03-19-Stg7
Purpose: SWArc class for SketchWaveJS

SWArc represents a circular arc — just the "crust" of the pizza, without the
filling.  It is the stroke-only counterpart to SWSector.  Defined by:
  - A center SWPoint (the center of the underlying circle)
  - A radius (in user units)
  - A theta (angular size in degrees)
  - A startAngle (degrees CCW from the +x axis, where the arc begins)
  - A rotation offset (degrees, accumulated via rotate())
  - A thickness (stroke weight in pixels — the primary visual variable)
  - A strokeColor (SWColor instance)
  - A capStyle (end-cap type: 'round' | 'butt' | 'square')

Key difference from SWSector:
  SWArc has NO FILL — it is drawn in OPEN arc mode (pure stroke).
  The arc's thickness becomes the primary visual/animation variable.

New animation vs SWSector:
  breatheThickness(sinusoid, t): oscillates the stroke weight.

All SWSector animations also apply:
  rotate(delta):                spin the arc about its center.
  breatheRadius(sinusoid, t):   oscillate the radius.
  breatheTheta(sinusoid, t):    oscillate the angular size.

Angle convention (identical to SWSector):
  User space (math):   angles are CCW from +x axis, y increases upward.
  p5 / screen space:   angles are CW  from +x axis, y increases downward.
  Conversion:          p5_angle = -user_angle  (negate because y is flipped).

Drawing an arc of size theta starting at totalAngle = startAngle + rotation:
  p5 arc start = -radians(totalAngle + theta)
  p5 arc stop  = -radians(totalAngle)
  arc(cx, cy, 2r, 2r, p5Start, p5Stop, OPEN)   — OPEN mode = no lines to center.

capStyle values (raw HTML canvas lineCap strings accepted by p5 strokeCap()):
  'round'  = ROUND   — smooth semicircular ends
  'butt'   = SQUARE  — flat ends, flush with arc endpoints
  'square' = PROJECT — flat ends that extend past endpoint by half thickness
*/

console.log("[swArc.js] SWArc class loaded.");

class SWArc {

    /**
     * @param {SWPoint} center         - Center of the underlying circle (SWPoint)
     * @param {number}  radius         - Radius in user units (> 0)
     * @param {number}  theta          - Angular size in degrees (clamped 0–360)
     * @param {number}  [startAngle=0]    - Arc start angle (degrees CCW from +x axis)
     * @param {number}  [thickness=6]     - Stroke weight in pixels (the primary visual variable)
     * @param {SWColor} [strokeColor]     - Arc color (SWColor instance)
     * @param {string}  [capStyle='round'] - End-cap: 'round', 'butt', or 'square'
     */
    constructor(center, radius, theta, startAngle = 0, thickness = 6,
                strokeColor = undefined, capStyle = 'round') {

        this.center     = center;
        this.radius     = radius;
        this.theta      = theta;
        this.startAngle = startAngle;
        this.rotation   = 0;               // accumulated rotation (degrees, CCW)
        this.thickness  = thickness;

        // Always copy color to avoid mutating shared objects
        this.strokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
        this.capStyle    = capStyle;       // 'round' | 'butt' | 'square'

        // Originals for reset()
        this.originalRadius      = radius;
        this.originalTheta       = theta;
        this.originalStartAngle  = startAngle;
        this.originalRotation    = 0;
        this.originalThickness   = thickness;
        this.originalStrokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;

        this.shouldShowCenter = true;      // draw center point marker

        this._updateGeometry();
    }//end constructor

    // ─── Geometry ──────────────────────────────────────────────────────────────

    _updateGeometry() {
        this.theta     = Math.max(0, Math.min(360, this.theta));
        this.arcLength = (this.theta / 360) * 2 * Math.PI * this.radius;
    }//end _updateGeometry

    // ─── Drawing ───────────────────────────────────────────────────────────────

    draw() {
        this._drawArc(this.center.x, this.center.y, this.radius);
        if (this.shouldShowCenter && this.center && this.center.draw) {
            this.center.draw(this.strokeColor);
        }
    }//end draw

    drawOnGrid(grid) {
        const { x: cx, y: cy } = grid.userToScreen(this.center.x, this.center.y);
        const rScreen = (grid.xScale * this.radius + grid.yScale * this.radius) / 2;
        this._drawArc(cx, cy, rScreen);
        if (this.shouldShowCenter && this.center && this.center.drawOnGrid) {
            this.center.drawOnGrid(grid, this.strokeColor);
        }
    }//end drawOnGrid

    _drawArc(cx, cy, r) {
        const totalAngle = this.startAngle + this.rotation;
        const p5Start = -radians(totalAngle + this.theta);
        const p5Stop  = -radians(totalAngle);

        noFill();

        if (this.strokeColor && this.strokeColor.col) {
            stroke(this.strokeColor.col);
        } else {
            noStroke();
        }

        strokeWeight(this.thickness);
        strokeCap(this.capStyle);
        arc(cx, cy, 2 * r, 2 * r, p5Start, p5Stop, OPEN);

        noStroke();
        strokeWeight(1);
        strokeCap(ROUND);
    }//end _drawArc

    // ─── Rotation animation ────────────────────────────────────────────────────

    rotate(deltaAngle) {
        this.rotation += deltaAngle;
    }//end rotate

    // ─── Breathing animations ──────────────────────────────────────────────────

    breatheRadius(sinusoid, t) {
        this.radius = Math.max(0.01, sinusoid.getValue(t));
        this._updateGeometry();
    }//end breatheRadius

    breatheTheta(sinusoid, t) {
        this.theta = sinusoid.getValue(t);
        this._updateGeometry();
    }//end breatheTheta

    breatheThickness(sinusoid, t) {
        this.thickness = Math.max(1, sinusoid.getValue(t));
    }//end breatheThickness

    // ─── Color helpers ─────────────────────────────────────────────────────────

    setStrokeAlpha(alpha) {
        if (this.strokeColor) {
            this.strokeColor.a = Math.max(0, Math.min(100, alpha));
            this.strokeColor.col = color(
                this.strokeColor.h, this.strokeColor.s,
                this.strokeColor.b, this.strokeColor.a
            );
        }
    }//end setStrokeAlpha

    // ─── Reset ─────────────────────────────────────────────────────────────────

    reset() {
        this.radius     = this.originalRadius;
        this.theta      = this.originalTheta;
        this.startAngle = this.originalStartAngle;
        this.rotation   = this.originalRotation;
        this.thickness  = this.originalThickness;
        if (this.originalStrokeColor) {
            this.strokeColor = SWColor.copy(this.originalStrokeColor);
        }
        this._updateGeometry();
    }//end reset

    resetStrokeColor() {
        if (this.originalStrokeColor) {
            this.strokeColor = SWColor.copy(this.originalStrokeColor);
        }
    }//end resetStrokeColor

    // ─── Setters ───────────────────────────────────────────────────────────────

    setStrokeColor(swColor) {
        this.strokeColor = swColor ? SWColor.copy(swColor) : undefined;
    }//end setStrokeColor

    setStrokeWeight(w) {
        this.thickness = w;
    }//end setStrokeWeight

    setRadius(r) {
        this.radius = r;
        this._updateGeometry();
    }//end setRadius

    setTheta(degrees) {
        this.theta = degrees;
        this._updateGeometry();
    }//end setTheta

    setStartAngle(degrees) {
        this.startAngle = degrees;
    }//end setStartAngle

    setCapStyle(cap) {
        this.capStyle = cap;
    }//end setCapStyle

    setShowCenter(show = true) {
        this.shouldShowCenter = show;
    }//end setShowCenter

    // ─── Copy ──────────────────────────────────────────────────────────────────

    static copy(other) {
        if (!(other instanceof SWArc)) {
            throw new Error('Argument to SWArc.copy must be an SWArc instance');
        }
        const a = new SWArc(
            SWPoint.copy(other.center),
            other.originalRadius,
            other.originalTheta,
            other.originalStartAngle,
            other.originalThickness,
            other.strokeColor,
            other.capStyle
        );
        a.radius           = other.radius;
        a.theta            = other.theta;
        a.startAngle       = other.startAngle;
        a.rotation         = other.rotation;
        a.thickness        = other.thickness;
        a.shouldShowCenter = other.shouldShowCenter;
        a._updateGeometry();
        return a;
    }//end copy

    // ─── toString ──────────────────────────────────────────────────────────────

    toString() {
        return `SWArc(center: ${this.center.toString()}, ` +
               `radius: ${this.radius.toFixed(2)}, theta: ${this.theta.toFixed(1)}°, ` +
               `startAngle: ${this.startAngle}°, rotation: ${this.rotation.toFixed(1)}°, ` +
               `thickness: ${this.thickness.toFixed(1)}, cap: '${this.capStyle}', ` +
               `arcLength: ${this.arcLength.toFixed(2)})`;
    }//end toString

}//end SWArc class