⭕ SWRing Reference

A SketchWave shape class for animated circle outlines — a stroke-only ring with independent radius, thickness, and alpha breathing animations, plus hue cycling

SWRing Demo

Quick Reference

SWRing is a SketchWave shape class representing a circle outline (ring) — a stroke-only circle with no fill. It is defined by a center SWPoint, a radius (in user units when drawn on a grid), a stroke thickness (pixels), and a stroke SWColor. An optional center-dot marker can be shown and dragged to reposition the ring.

  • Extends: Nothing (standalone class)
  • Dependencies: SWPoint, SWColor, SWGrid, p5.js
  • Fill: None — noFill() is always applied
  • Key Features: Three independent breathing animations (radius, thickness, alpha), hue cycling via SWSinusoid, draggable center dot, deep copy, full reset
  • Common Uses: Pulsing rings, ripple effects, radar sweeps, orbit markers, energy ring building blocks

Overview

Drawing Model

An SWRing draws a single circle outline using p5.js circle() with noFill() and the stroke color set from its strokeColor (SWColor). The ring can be drawn in two ways:

MethodCoordinate spaceRadius interpretation
draw() Screen pixels — center.x / center.y and radius are raw pixel values Pixels
drawOnGrid(grid) User units — center is mapped through SWGrid; radius is scaled by grid.xScale User units

The standard workflow in SketchWaveJS demos is always drawOnGrid(grid), which keeps the ring positioned correctly as the canvas resizes.

Color Model

SWRing uses a single SWColor applied to the stroke (and to the center-dot fill when the marker is visible). Alpha is part of the SWColor and can be animated independently via breatheAlpha() or set directly with setStrokeAlpha(alpha).

  • HSB color space — h ∈ [0, 360), s ∈ [0, 100], b ∈ [0, 100], a ∈ [0, 100]
  • Hue can be animated by cycling ring.strokeColor.h through an SWSinusoid each frame before drawing
  • If no stroke color is provided, defaults to opaque black

Breathing Animations

The three breathing methods each accept an SWSinusoid and the current elapsed time in seconds. They are completely independent and composable — all three can run simultaneously.

MethodAnimatesClamp
breatheRadius(sin, t)this.radiusmin 0.01
breatheThickness(sin, t)this.thicknessmin 0.1 px
breatheAlpha(sin, t)this.strokeColor.a[0, 100]

Call the appropriate breathe method after drawOnGrid() each frame so the animated value takes effect on the next frame. Use the elapsed-time pause/resume pattern (see Animation Guide) so animations resume smoothly after being toggled off and back on.

Typical Workflow

  1. Create an SWColor for the stroke: SWColor.fromHex('#1A6CB5', 90, 'ring')
  2. Instantiate: new SWRing(new SWPoint(0, 0), 4, 4, strokeColor)
  3. Call ring.drawOnGrid(grid) each frame inside draw()
  4. Optionally call a breathe method after draw: ring.breatheRadius(sinusoid, t)
  5. Handle UI events with setter methods
  6. Call reset() to restore all original geometry and color

Constructor

new SWRing(center, radius, thickness, strokeColor)

Creates a new SWRing. The stroke color is deep-copied at construction to prevent shared-reference mutations. Original values are stored for reset().

Parameters
ParameterTypeDefaultDescription
center SWPoint required Center of the ring in user coordinates (for drawOnGrid) or pixels (for draw).
radius number required Ring radius in user units (> 0). Mapped through grid.xScale by drawOnGrid().
thickness number 3 Stroke weight in pixels. Animated by breatheThickness().
strokeColor SWColor undefined Stroke color (SWColor instance). Deep-copied at construction. If omitted, defaults to opaque black when drawing.
Constructor Examples
// Minimal ring — black stroke, radius 4, thickness 3
let ring = new SWRing(new SWPoint(0, 0), 4);

// With color and thickness
let sc = SWColor.fromHex('#1A6CB5', 90, 'ring');
let ring2 = new SWRing(new SWPoint(0, 0), 4, 4, sc);

// Ring offset from center
let ring3 = new SWRing(new SWPoint(3, -2), 2, 2, sc);

Properties

centerSWPoint

The center of the ring in user coordinates. Change center.x and center.y to move the ring. When the center dot is enabled, the user can drag the ring to a new position in the demo.

radiusnumber

Current ring radius in user units. Modified frame-by-frame by breatheRadius(). Prefer setRadius(r) to set it directly. Clamped ≥ 0.01 by the breathe method.

thicknessnumber

Current stroke weight in pixels. Modified frame-by-frame by breatheThickness(). Prefer setThickness(w) to set it directly. Clamped ≥ 0.1 px by the breathe method.

strokeColorSWColor

The stroke color for both the ring outline and the center-dot marker. Use setStrokeColor(swColor) or setStrokeAlpha(alpha) to update. The hue (ring.strokeColor.h) can be advanced each frame to animate color cycling.

shouldShowCenterboolean

When true, a small filled dot (6 px diameter) is drawn at the center of the ring using the stroke color at full opacity. Defaults to false. Toggle with setShowCenter(show). When visible, the dot can be dragged in the demo to reposition the ring.

Original-State Properties (used by reset())

Stored from the constructor arguments; used to restore the ring. Do not modify directly.

  • originalRadius
  • originalThickness
  • originalStrokeColor (deep copy of strokeColor at construction)

Methods

Drawing

draw()

Draws the ring in screen (pixel) coordinates. center.x / center.y and radius are treated as raw pixel values. Use drawOnGrid() for user-coordinate rendering in demos that use an SWGrid.

drawOnGrid(grid)

Draws the ring mapped through the given SWGrid. The center position is converted from user units to screen pixels via grid.userToScreen(), and the radius is scaled by grid.xScale. This is the standard call for all SketchWaveJS grid-based demos.

ParameterTypeDescription
gridSWGridThe coordinate grid for user-to-pixel mapping
// Typical in draw():
if (shouldShowGrid) grid.draw();
ring.drawOnGrid(grid);

Breathing Animations

Each breathing method accepts an SWSinusoid and elapsed time t (seconds). Call them after drawOnGrid() so the animated value applies on the next frame.

breatheRadius(sinusoid, t)

Modulates this.radius from the sinusoid's current value at time t. Clamped to a minimum of 0.01 user units.

ParameterTypeDescription
sinusoidSWSinusoidSinusoid configured with the desired period, min, and max
tnumberElapsed time in seconds (e.g., radiusBreathElapsed + millis()/1000 - radiusBreathStartTime)
// After drawOnGrid():
if (shouldBreatheRadius) {
    const t = radiusBreathElapsed + millis() / 1000 - radiusBreathStartTime;
    ring.breatheRadius(radiusSinusoid, t);
}

breatheThickness(sinusoid, t)

Modulates this.thickness (stroke weight in pixels). Clamped to a minimum of 0.1 px. The sinusoid should be configured with pixel values as min/max (e.g., min: 1, max: 16).

breatheAlpha(sinusoid, t)

Modulates this.strokeColor.a (opacity 0–100). The sinusoid should output values in [0, 100]. Clamped to [0, 100].

// Combine all three animations simultaneously:
if (shouldBreatheRadius)    ring.breatheRadius(radiusSinusoid, tR);
if (shouldBreatheThickness) ring.breatheThickness(thicknessSinusoid, tT);
if (shouldBreatheAlpha)     ring.breatheAlpha(alphaSinusoid, tA);

Setters

setRadius(r)

Sets the ring radius directly (user units). Does not update originalRadius.

setThickness(w)

Sets the stroke weight in pixels directly. Does not update originalThickness.

setStrokeColor(swColor)

Replaces the stroke color with a deep copy of the provided SWColor. Affects both the ring outline and the center-dot fill.

ring.setStrokeColor(SWColor.fromHex('#cc2200', 80, 'newColor'));

setStrokeAlpha(alpha)

Sets the stroke opacity (0–100) without replacing the full color. Clamped to [0, 100]. Useful for a quick fade without changing hue, saturation, or brightness.

ring.setStrokeAlpha(50);   // 50% opacity

setShowCenter(show)

Shows or hides the center-dot marker. Pass true (default) to show, false to hide. When visible in a demo, the dot can be clicked and dragged to reposition the ring.

ring.setShowCenter(true);    // show dot
ring.setShowCenter(false);   // hide dot
ring.setShowCenter();        // same as true (default argument)

Reset Method

reset()

Restores radius, thickness, and strokeColor to the originals stored at construction. Does not move the center position or clear shouldShowCenter.

// Stop animations and snap ring back to original geometry + color:
shouldBreatheRadius    = false;
shouldBreatheThickness = false;
shouldBreatheAlpha     = false;
shouldCycleHue         = false;
ring.reset();

Static Methods

static copy(other)

Returns a deep copy of the given SWRing instance, including its current animated state and its original-value snapshot (so the copy's reset() behavior mirrors the original's).

let ringCopy = SWRing.copy(ring);

Utility Methods

toString()

Returns a human-readable description of the ring's current state.

console.log(ring.toString());
// → SWRing | center: (0.00, 0.00) | radius: 4.00 | thickness: 4.0 | color: SWColor(...)

Animation Guide

Elapsed-Time Pause/Resume Pattern

SketchWaveJS breathing animations use an elapsed-time accumulator so that pausing and resuming an animation continues smoothly where it left off, instead of jumping. Each independent animation needs its own start-time and elapsed-time variables.

// Globals per animation:
let shouldBreatheRadius    = false;
let radiusBreathStartTime  = 0;         // millis when animation last started
let radiusBreathElapsed    = 0;         // total accumulated time (seconds)

// Toggle animation on/off:
function toggleBreatheRadius() {
    shouldBreatheRadius = !shouldBreatheRadius;
    if (shouldBreatheRadius) {
        radiusBreathStartTime = millis() / 1000;  // record start
    } else {
        // Accumulate elapsed time when pausing
        radiusBreathElapsed += millis() / 1000 - radiusBreathStartTime;
    }
}

// In draw():
if (shouldBreatheRadius && radiusSinusoid) {
    const t = radiusBreathElapsed + millis() / 1000 - radiusBreathStartTime;
    ring.breatheRadius(radiusSinusoid, t);
}

Setting Up a Sinusoid

// In setup():
radiusSinusoid = SWSinusoid.copy(UNIT_SINUSOID);
radiusSinusoid.setPeriod(4);                         // 4-second cycle
radiusSinusoid.adjustWaveUsingExtrema(1, 8);         // radius oscillates between 1 and 8

thicknessSinusoid = SWSinusoid.copy(UNIT_SINUSOID);
thicknessSinusoid.setPeriod(3);
thicknessSinusoid.adjustWaveUsingExtrema(1, 16);     // thickness: 1 to 16 px

alphaSinusoid = SWSinusoid.copy(UNIT_SINUSOID);
alphaSinusoid.setPeriod(2);
alphaSinusoid.adjustWaveUsingExtrema(0, 100);        // alpha: 0 to 100

Hue Cycling

Hue cycling is implemented by updating ring.strokeColor.h directly each frame before drawing, using an SWSinusoid outputting values in [0, 360].

// Globals:
let shouldCycleHue   = false;
let hueSinusoid;
let hueCycleStartTime = 0;
let hueCycleElapsed   = 0;

// In setup():
hueSinusoid = SWSinusoid.copy(UNIT_SINUSOID);
hueSinusoid.setPeriod(4);
hueSinusoid.adjustWaveUsingExtrema(0, 360);

// In draw() — update BEFORE ring.drawOnGrid():
if (shouldCycleHue && hueSinusoid && ring && ring.strokeColor) {
    const t = hueCycleElapsed + millis() / 1000 - hueCycleStartTime;
    ring.strokeColor.h = hueSinusoid.getValue(t);
}

Combining All Four Animations

function draw() {
    const t = millis() / 1000;

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

    // Hue cycling: update color BEFORE drawing
    if (shouldCycleHue && ring.strokeColor) {
        const tH = hueCycleElapsed + t - hueCycleStartTime;
        ring.strokeColor.h = hueSinusoid.getValue(tH);
    }

    ring.drawOnGrid(grid);

    // Breathe animations: applied AFTER drawing (take effect next frame)
    if (shouldBreatheRadius) {
        ring.breatheRadius(radiusSinusoid,
            radiusBreathElapsed + t - radiusBreathStartTime);
    }
    if (shouldBreatheThickness) {
        ring.breatheThickness(thicknessSinusoid,
            thicknessBreathElapsed + t - thicknessBreathStartTime);
    }
    if (shouldBreatheAlpha) {
        ring.breatheAlpha(alphaSinusoid,
            alphaBreathElapsed + t - alphaBreathStartTime);
    }

    grid.updateScreenBounds();
}

Usage Examples

Minimal Ring

let grid, ring;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();

    grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
    ring = new SWRing(new SWPoint(0, 0), 4, 3);
}

function draw() {
    background(0, 0, 93);
    grid.draw();
    ring.drawOnGrid(grid);
    grid.updateScreenBounds();
}

Colored Ring with Center Dot

let grid, ring;
const sc = SWColor.fromHex('#1A6CB5', 90, 'ring');

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();

    grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
    ring = new SWRing(new SWPoint(0, 0), 4, 4, sc);
    ring.setShowCenter(true);
}

function draw() {
    background(0, 0, 93);
    grid.draw();
    ring.drawOnGrid(grid);
    grid.updateScreenBounds();
}

Pulsing Ring (Breathe Radius)

let grid, ring, radiusSinusoid;
let shouldBreathe = false;
let breatheStart  = 0;
let breatheElapsed = 0;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();

    grid = new SWGrid({ UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10) });
    const sc = SWColor.fromHex('#1A6CB5', 90, 'ring');
    ring = new SWRing(new SWPoint(0, 0), 4, 4, sc);

    radiusSinusoid = SWSinusoid.copy(UNIT_SINUSOID);
    radiusSinusoid.setPeriod(3);
    radiusSinusoid.adjustWaveUsingExtrema(1, 8);
}

function draw() {
    background(0, 0, 93);
    grid.draw();
    ring.drawOnGrid(grid);
    if (shouldBreathe) {
        const t = breatheElapsed + millis() / 1000 - breatheStart;
        ring.breatheRadius(radiusSinusoid, t);
    }
    grid.updateScreenBounds();
}

function keyPressed() {
    if (key === 'b') {
        shouldBreathe = !shouldBreathe;
        if (shouldBreathe) {
            breatheStart = millis() / 1000;
        } else {
            breatheElapsed += millis() / 1000 - breatheStart;
        }
    }
    if (key === 'r') { ring.reset(); shouldBreathe = false; }
}

Loading Dependencies

<!-- p5.js -->
<script src="https://cdn.jsdelivr.net/npm/p5@1.6.0/lib/p5.js"></script>

<!-- SketchWaveJS classes (order matters) -->
<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/swRing.js"></script>

<!-- Your sketch -->
<script src="sketches/yourSketch.js"></script>

Source Code

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

/*
File: swRing.js
Date: 2026-03-30
Author: klp
App:  SketchWaveTNT2026-03-19-Stg7
Purpose: SWRing class for SketchWaveJS

SWRing represents a single circular ring (circle outline, no fill) defined by:
  - A center SWPoint (the center of the ring)
  - A radius (in user units when using drawOnGrid; pixels when using draw)
  - A thickness (stroke weight in pixels)
  - A strokeColor (SWColor)

Breathing animations (independent, composable):
  - breatheRadius(sinusoid, t):    oscillates the radius
  - breatheThickness(sinusoid, t): oscillates the stroke weight
  - breatheAlpha(sinusoid, t):     oscillates the stroke alpha

Notes:
  - Assumes p5.js, SWColor, SWPoint, SWGrid, SWSinusoid are loaded.
  - Consistent API with SWSector, SWDisk, SWArc, etc.
  - setStrokeAlpha clamps alpha to [0, 100].
  - A ring is purely a stroke shape; noFill() is always applied.
  - draw() uses center.x/y and radius as raw pixel coordinates.
  - drawOnGrid() maps center through SWGrid and scales radius by grid.xScale.
*/

console.log("[swRing.js] SWRing class loaded.");

class SWRing {

    /**
     * @param {SWPoint} center        - Center of the ring (SWPoint instance)
     * @param {number}  radius        - Radius in user units (drawOnGrid) or pixels (draw)
     * @param {number}  [thickness=3] - Stroke weight in pixels
     * @param {SWColor} [strokeColor] - Stroke color (SWColor instance)
     */
    constructor(center, radius, thickness = 3, strokeColor = undefined) {
        this.center      = center;
        this.radius      = radius;
        this.thickness   = thickness;
        this.strokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;

        // Originals for reset()
        this.originalRadius      = radius;
        this.originalThickness   = thickness;
        this.originalStrokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;

        // Show the center point marker (hidden by default)
        this.shouldShowCenter = false;
    }//end constructor

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

    /**
     * Draws the ring in screen (pixel) coordinates.
     * center.x / center.y are treated as pixel positions.
     * Use drawOnGrid() for user-coordinate rendering.
     */
    draw() {
        this._drawRing(this.center.x, this.center.y, this.radius);
    }//end draw

    /**
     * Draws the ring mapped through the given SWGrid.
     * center.x / center.y are user-space coordinates.
     * @param {SWGrid} grid
     */
    drawOnGrid(grid) {
        const pos = grid.userToScreen(this.center.x, this.center.y);
        const r   = this.radius * grid.xScale;
        this._drawRing(pos.x, pos.y, r);
    }//end drawOnGrid

    /**
     * Internal: applies style and draws the ring circle.
     * @param {number} cx - screen x of center
     * @param {number} cy - screen y of center
     * @param {number} r  - radius in screen pixels
     */
    _drawRing(cx, cy, r) {
        push();
        noFill();
        if (this.strokeColor) {
            stroke(this.strokeColor.h, this.strokeColor.s,
                   this.strokeColor.b, this.strokeColor.a);
        } else {
            stroke(0, 0, 0, 100);
        }
        strokeWeight(this.thickness);
        circle(cx, cy, 2 * r);

        if (this.shouldShowCenter) {
            // Small filled dot at the center using stroke hue at full opacity
            fill(
                this.strokeColor ? this.strokeColor.h : 0,
                this.strokeColor ? this.strokeColor.s : 0,
                this.strokeColor ? this.strokeColor.b : 0,
                100
            );
            noStroke();
            circle(cx, cy, 6);
        }
        pop();
    }//end _drawRing

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

    /**
     * Modulates the radius using an SWSinusoid instance.
     * Clamps to a minimum of 0.01.
     * @param {SWSinusoid} sinusoid
     * @param {number} t - elapsed time in seconds
     */
    breatheRadius(sinusoid, t) {
        this.radius = Math.max(0.01, sinusoid.getValue(t));
    }//end breatheRadius

    /**
     * Modulates the stroke thickness using an SWSinusoid instance.
     * Clamps to a minimum of 0.1 pixels.
     * @param {SWSinusoid} sinusoid
     * @param {number} t - elapsed time in seconds
     */
    breatheThickness(sinusoid, t) {
        this.thickness = Math.max(0.1, sinusoid.getValue(t));
    }//end breatheThickness

    /**
     * Modulates the stroke alpha using an SWSinusoid instance.
     * Sinusoid should output values in [0, 100].
     * @param {SWSinusoid} sinusoid
     * @param {number} t - elapsed time in seconds
     */
    breatheAlpha(sinusoid, t) {
        if (this.strokeColor) {
            this.strokeColor.a = Math.max(0, Math.min(100, sinusoid.getValue(t)));
        }
    }//end breatheAlpha

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

    /**
     * Sets the stroke alpha value. Clamped to [0, 100].
     * @param {number} alpha
     */
    setStrokeAlpha(alpha) {
        if (this.strokeColor) {
            this.strokeColor.a = Math.max(0, Math.min(100, alpha));
        }
    }//end setStrokeAlpha

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

    /**
     * Resets all animated values to the originals stored at construction time.
     */
    reset() {
        this.radius    = this.originalRadius;
        this.thickness = this.originalThickness;
        if (this.originalStrokeColor) {
            this.strokeColor = SWColor.copy(this.originalStrokeColor);
        }
    }//end reset

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

    /**
     * @param {number} r - new radius in user units
     */
    setRadius(r) {
        this.radius = r;
    }//end setRadius

    /**
     * @param {number} w - stroke weight in pixels
     */
    setThickness(w) {
        this.thickness = w;
    }//end setThickness

    /**
     * @param {SWColor} swColor
     */
    setStrokeColor(swColor) {
        this.strokeColor = SWColor.copy(swColor);
    }//end setStrokeColor

    /**
     * @param {boolean} [show=true]
     */
    setShowCenter(show = true) {
        this.shouldShowCenter = show;
    }//end setShowCenter

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

    /**
     * Returns a deep copy of the given SWRing instance.
     * @param {SWRing} other
     * @returns {SWRing}
     */
    static copy(other) {
        const newCenter = new SWPoint(other.center.x, other.center.y);
        const r = new SWRing(
            newCenter,
            other.radius,
            other.thickness,
            other.strokeColor ? SWColor.copy(other.strokeColor) : undefined
        );
        r.shouldShowCenter    = other.shouldShowCenter;
        r.originalRadius      = other.originalRadius;
        r.originalThickness   = other.originalThickness;
        r.originalStrokeColor = other.originalStrokeColor
            ? SWColor.copy(other.originalStrokeColor) : undefined;
        return r;
    }//end copy

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

    toString() {
        return `SWRing | center: (${this.center.x.toFixed(2)}, ${this.center.y.toFixed(2)}) | ` +
               `radius: ${this.radius.toFixed(2)} | thickness: ${this.thickness.toFixed(1)} | ` +
               `color: ${this.strokeColor}`;
    }//end toString

}//end SWRing class