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:
| Method | Coordinate space | Radius 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.hthrough 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.
| Method | Animates | Clamp |
|---|---|---|
breatheRadius(sin, t) | this.radius | min 0.01 |
breatheThickness(sin, t) | this.thickness | min 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
- Create an SWColor for the stroke:
SWColor.fromHex('#1A6CB5', 90, 'ring') - Instantiate:
new SWRing(new SWPoint(0, 0), 4, 4, strokeColor) - Call
ring.drawOnGrid(grid)each frame insidedraw() - Optionally call a breathe method after draw:
ring.breatheRadius(sinusoid, t) - Handle UI events with setter methods
- 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().
| Parameter | Type | Default | Description |
|---|---|---|---|
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. |
// 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
center — SWPoint
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.
radius — number
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.
thickness — number
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.
strokeColor — SWColor
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.
shouldShowCenter — boolean
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.
originalRadiusoriginalThicknessoriginalStrokeColor(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.
| Parameter | Type | Description |
|---|---|---|
grid | SWGrid | The 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.
| Parameter | Type | Description |
|---|---|---|
sinusoid | SWSinusoid | Sinusoid configured with the desired period, min, and max |
t | number | Elapsed 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