📶 SWWifi Reference

A SketchWave composite class that renders a WiFi / signal-strength symbol from concentric arcs and a filled center dot

Back to SWWifi Demo

Quick Reference

SWWifi is a SketchWave 🧩 Composite class that renders a WiFi / signal-strength symbol: a set of concentric arcs (conceptually equivalent to SWArc instances) plus a filled center dot (conceptually equivalent to an SWDisk) — all drawn together as a single coordinated group. Unlike individual shape classes, SWWifi owns the drawing logic for both its arcs and its dot and manages signal-strength state, three independent animations, and interactive dragging.

  • Design Pattern: Composite — one class owns N arc draws + 1 dot draw
  • Component Equivalents: arcs ≈ SWArc  |  center dot ≈ SWDisk  |  layout ≈ SWRing
  • Dependencies: SWPoint, SWColor, SWSinusoid, SWGrid, p5.js
  • Key Features: Signal-strength active/inactive arcs, independent brightness + opacity for inactive arcs, pulse / breathe / spin animations, interactive drag via center handle
  • Common Uses: WiFi signal indicators, radar sweeps, loading animations, signal-strength UI elements in p5.js sketches

Overview

Composition Design

SWWifi is built using the composition pattern: instead of inheriting from or storing references to SWArc or SWDisk instances, it manages its arc and dot drawing internally. Conceptually, each arc is an SWArc (an open circular arc of a given radius, span, and direction), and the center dot is an SWDisk (a filled circle at the origin). SWWifi handles coordinate conversion, color management, and animation state for all of them as a unified group.

Why composition and not separate instances?
Managing N separate SWArc objects and a SWDisk would require manually keeping all their positions, colors, rotations, and opacity in sync on every frame. SWWifi centralizes that state so the caller only works with one object.

Arc Layout

Arcs are drawn from innermost (index 0) to outermost (index numArcs − 1). The radius of arc i is:

arcRadius(i) = innerRadius + i × arcSpacing

Every arc spans theta degrees symmetrically about direction (default 90° = pointing straight up). The dot sits at the center — the signal origin.

Arc Angle Convention

SWWifi uses math (user-space) angles: CCW from the +x axis, y increases upward. direction is the angle the arc group points. The p5.js arc call uses y-flipped CW angles; the class converts internally:

p5Start = −radians(direction + theta/2)
p5Stop  = −radians(direction − theta/2)

Signal Strength

activeCount (0–numArcs) controls how many arcs glow at full color and opacity. Arcs at index < activeCount are active; the rest are inactive. Inactive arcs are independently controlled by two properties:

  • dimFactor — scales HSB brightness (0 = black, 1 = same as active)
  • inactiveOpacity — sets alpha 0–100 independently of brightness

Setting both to high values makes inactive arcs nearly identical to active ones; setting both to zero makes them completely invisible.

Key Capabilities

  • Composite rendering: N arcs + center dot drawn as one coordinated group
  • Signal strength: Active/inactive arc distinction via activeCount, dimFactor, and inactiveOpacity
  • Pulsing: Sweeps one lit arc inner-to-outer in sequence using pulseIndex
  • Breathing: Oscillates arc stroke weight via SWSinusoid using breatheThickness()
  • Spinning: Rotates the whole group about its center via rotateAboutCenter()
  • Dragging: Interactive repositioning via the center handle when showCenter is true
  • Reset: reset() restores all original parameter values and clears animation state

Typical Workflow

  1. Create SWColor instances for arc and dot colors
  2. Construct an SWWifi with desired geometry and signal-strength settings
  3. Call drawOnGrid(grid) each frame
  4. Drive animations: call breatheThickness() each frame, or rotateAboutCenter(), or set pulseIndex
  5. Call reset() to restore factory state
  6. Forward p5 mouse events to handleMousePressed(), handleMouseDragged(), handleMouseReleased() to enable dragging

Constructor

new SWWifi({ center, numArcs, innerRadius, arcSpacing, dotRadius, theta, direction, rotation, thickness, arcColor, dotColor, fillOpacity, activeCount, dimFactor, inactiveOpacity, showCenter })

Creates a new SWWifi instance. All parameters are passed as a named-property options object. Originals of every parameter are captured at construction for reset().

Parameters (all optional — all have defaults)
ParameterTypeDefaultDescription
center SWPoint SWPoint(0,0) Anchor of the group in user/grid coordinates
numArcs number 3 Number of concentric arcs to draw
innerRadius number 1.5 Radius of the innermost arc in user units
arcSpacing number 1.2 Radial gap between consecutive arcs in user units
dotRadius number 0.4 Radius of the filled center dot in user units
theta number 90 Angular span of each arc in degrees. 90° = classic WiFi; 360° = full rings
direction number 90 Direction the arcs point, in °CCW from +x axis. Default 90 = straight up
rotation number 0 Static rotation offset in degrees applied before any animation
thickness number 6 Arc stroke weight in pixels
arcColor SWColor sky-blue HSB(210,80,92) Color for all arcs. Deep-copied at construction
dotColor SWColor sky-blue HSB(210,80,92) Color for the center dot. Deep-copied at construction
fillOpacity number 100 Alpha 0–100 for active arcs and the dot
activeCount number = numArcs How many arcs (from innermost) glow at full brightness. 0 = no signal, numArcs = full signal
dimFactor number 0.25 HSB brightness multiplier for inactive arcs (0 = black, 1 = same color as active). Clamped to [0, 1]
inactiveOpacity number 35 Alpha 0–100 for inactive arcs, independent of dimFactor. Clamped to [0, 100]
showCenter boolean false When true, draws a crosshair handle at the anchor — enables interactive dragging
Constructor Examples
// Minimal: all defaults (3 arcs, sky-blue, pointing up)
let wifi = new SWWifi();

// Custom color and geometry
const arcCol = SWColor.fromHex('#30c2f2', 100, "arcColor");
const dotCol = SWColor.fromHex('#30c2f2', 100, "dotColor");
let wifi = new SWWifi({
    center:      new SWPoint(0, 0),
    numArcs:     4,
    innerRadius: 1.2,
    arcSpacing:  1.0,
    dotRadius:   0.5,
    theta:       100,
    direction:   90,
    thickness:   8,
    arcColor:    arcCol,
    dotColor:    dotCol,
    fillOpacity: 100,
    activeCount: 2,      // only 2 of 4 arcs lit at start
    dimFactor:   0.2,
    inactiveOpacity: 25,
});

// Full rings (theta = 360)
let rings = new SWWifi({ theta: 360, numArcs: 5, arcSpacing: 0.8 });

// Pointing sideways (direction = 0 = right)
let sideways = new SWWifi({ direction: 0 });

// Show center handle for dragging
let draggable = new SWWifi({ showCenter: true });

Properties

center SWPoint

The anchor of the WiFi group in user/grid coordinates. All arcs and the dot are positioned relative to this point. Moving center.x / center.y repositions the entire shape. When dragging, center is updated via SWPoint.setPosition().

wifi.center.setPosition(2, 3);
numArcs number

Number of concentric arcs to draw. Changing this at runtime immediately affects how many arcs are rendered. Also clamps activeCount to [0, numArcs] when you update it.

wifi.numArcs = 5; // draw 5 arcs
innerRadius / arcSpacing number

innerRadius is the radius of the innermost arc (arc 0). arcSpacing is the radial gap between each pair of consecutive arcs. Together they determine all arc radii: arcRadius(i) = innerRadius + i × arcSpacing.

wifi.innerRadius = 1.0; wifi.arcSpacing = 1.5;
dotRadius number

Radius of the filled center dot in user units. The dot is always drawn at full fillOpacity — it is never affected by dimFactor or inactiveOpacity.

wifi.dotRadius = 0.6;
theta number (degrees)

Angular span of each arc in degrees. 90° gives the classic WiFi wedge; 180° gives a semicircle; 360° turns each arc into a full ring. All arcs share the same span.

wifi.theta = 120; // wider arcs
direction number (degrees)

The angle the group of arcs points, in °CCW from the +x axis (user-space convention). Default 90° = straight up. Change this to tilt the whole signal without spinning.

wifi.direction = 0; // point right
wifi.direction = 270; // point down
rotation number (degrees)

Static rotation offset added to the current animation rotation before drawing. Combine with _animRotDeg (managed by rotateAboutCenter()) for the effective drawing orientation. Restored by reset().

wifi.rotation = 45; // tilted 45° at rest
thickness number (pixels)

Arc stroke weight in pixels. The internal _thickness field can differ during breathing animation; it reverts to thickness when breathing is off. Restored by reset().

wifi.thickness = 10;
arcColor / dotColor SWColor

The base color for arcs and the center dot respectively. arcColor is used directly for active arcs; inactive arcs receive a derived color via arcColor.createDarkerColor(dimFactor). The dot always uses dotColor at full opacity. Deep-copied originals are held for reset().

wifi.arcColor = SWColor.fromHex('#ff6600', 100, "orange");
fillOpacity number (0–100)

Alpha value for active arcs and the center dot. Does not affect inactive arcs (those use inactiveOpacity). Restored by reset().

wifi.fillOpacity = 75;
activeCount number (0–numArcs)

How many arcs (counting from the innermost, index 0) glow at full brightness and opacity. Arcs at index ≥ activeCount are inactive. Set to 0 for "no signal", numArcs for "full signal". Has no effect during pulsing (when pulseIndex ≥ 0). Restored by reset().

wifi.activeCount = 2; // 2-bar signal
dimFactor number (0–1)

Brightness multiplier for inactive arcs via SWColor.createDarkerColor(). 0 = black, 1 = same color as active arcs. Only affects the HSB brightness channel — opacity is controlled separately by inactiveOpacity. Clamped to [0,1]. Restored by reset().

wifi.dimFactor = 0.1; // very dark inactive arcs
inactiveOpacity number (0–100)

Alpha value for inactive arcs, independent of dimFactor. 0 = fully transparent (invisible), 100 = fully opaque. Clamped to [0,100]. Restored by reset().

wifi.inactiveOpacity = 0; // hide inactive arcs entirely
wifi.inactiveOpacity = 50; // half-transparent
pulseIndex number

When ≥ 0, overrides activeCount: only the arc at this index is active; all others are inactive. −1 means pulsing is off and activeCount is used instead. Set by the sketch each frame during a pulse animation; cleared to −1 by reset().

wifi.pulseIndex = 1; // only arc index 1 glows
showCenter boolean

When true, draws a small crosshair handle (8 px radius circle with crosshair lines) at the anchor point. This handle is the drag target for interactive repositioning — dragging only works when showCenter is true and isDraggable is true.

wifi.showCenter = true; // enable drag handle
isDraggable boolean

Master switch for drag interaction. Default true. Set to false to lock position programmatically while still showing the center handle.

wifi.isDraggable = false; // show handle but prevent moving
isDragging read-only getter

Returns true while the user is actively dragging this instance. Useful for suppressing other click actions (e.g. toggling grid visibility) when the drag has consumed the event.

if (wifi.isDragging) { /* drag in progress */ }
maxRadius read-only getter

Returns the radius of the outermost arc in user units. Useful for bounding-box calculations or clamping the shape to the canvas.

console.log(wifi.maxRadius.toFixed(2)); // e.g. "3.90"

Methods

Core Drawing Methods

drawOnGrid(grid)

Draws the WiFi group mapped through the given SWGrid. Converts center from user coordinates to screen pixels, then delegates to _drawShape(). This is the standard method to call in a p5.js draw() loop.

Parameters
  • grid (SWGrid) — the active coordinate grid
Example
function draw() {
    background(0, 0, 93);
    grid.draw();
    wifi.drawOnGrid(grid);
}
draw()

Draws in raw screen (pixel) coordinates — center.x / center.y are treated as pixels and scale = 1. Rarely used directly; prefer drawOnGrid().

Example
wifi.center.setPosition(200, 200); // screen pixels
wifi.draw();

Computed Helpers

arcRadius(i)

Returns the radius of arc at index i (0 = innermost) in user units.

Returns

number

Example
for (let i = 0; i < wifi.numArcs; i++) {
    console.log(`Arc ${i} radius: ${wifi.arcRadius(i).toFixed(2)}`);
}

Animation Methods

breatheThickness(sinusoid, t)

Oscillates the arc stroke weight using an SWSinusoid. The sinusoid's getValue(t) should return pixel values within a desired min/max range. Updates the internal _thickness field, which is used for rendering that frame. Call each frame while breathing is active.

Parameters
  • sinusoid (SWSinusoid) — controls thickness oscillation
  • t (number) — elapsed seconds since breathing started
Example
// Thickness oscillates between 2 px and 14 px over 1.5 seconds
const amp  = (14 - 2) / 2;
const freq = (2 * Math.PI) / 1.5;
const mid  = (14 + 2) / 2;
const breatheSin = new SWSinusoid(amp, freq, mid, -Math.PI / 6);

// In draw():
if (breathingOn) {
    wifi.breatheThickness(breatheSin, breatheElapsed);
}
rotateAboutCenter(degPerSec, t)

Rotates the entire WiFi group about its center. Sets the internal _animRotDeg = degPerSec × t. Call each frame while spinning is active, passing the elapsed time since spin started.

Parameters
  • degPerSec (number) — rotation speed; positive = CCW, negative = CW
  • t (number) — elapsed seconds since spinning started
Example
// Spin at 20 deg/sec CCW
if (spinOn) {
    const t = millis() / 1000 - spinStartTime;
    wifi.rotateAboutCenter(20, t);
}
Pulsing — pulseIndex pattern

Pulsing is implemented by the sketch, not by a method. Each frame, set wifi.pulseIndex to the index of the arc that should light up. When pulseIndex ≥ 0, activeCount is ignored — only the specified arc is active. Cycle through 0 → numArcs−1 → 0 to create the sweeping effect. Set to −1 to stop pulsing.

Example
// In draw():
if (pulseOn) {
    const pt    = millis() / 1000 - pulseStartTime;
    const rate  = 2.0; // arcs per second
    wifi.pulseIndex = Math.floor(pt * rate) % wifi.numArcs;
} else {
    wifi.pulseIndex = -1;
}

Reset Method

reset()

Restores all geometry, color, opacity, and animation properties to the originals captured at construction. Also clears _animRotDeg = 0 and pulseIndex = −1. Does not move center — position is considered user-driven and intentional.

Example
wifi.reset(); // full restore

Drag Interaction Methods

handleMousePressed(mx, my, grid [, tolerance])

Call from the p5.js mousePressed() function. Hit-tests the center handle (8 px radius). Returns true if the click hit the handle and a drag was started — the caller should then skip other click-based actions. Only fires if showCenter and isDraggable are both true.

Parameters
  • mx, my (number) — p5 mouseX, mouseY
  • grid (SWGrid) — the active grid
  • tolerance (number, optional) — extra pixel margin around the handle (default 0)
Returns

boolean — true if drag started

handleMouseDragged(mx, my, grid)

Call from the p5.js mouseDragged() function. While dragging, converts mouse screen coords to user coords and moves center while preserving the grab offset (no jump). Returns true while a drag is active.

Returns

boolean

handleMouseReleased()

Call from the p5.js mouseReleased() function. Ends the drag operation.

Example
function mousePressed()  { if (wifi && grid) wifi.handleMousePressed(mouseX, mouseY, grid, 6); }
function mouseDragged()  { if (wifi && grid) wifi.handleMouseDragged(mouseX, mouseY, grid); }
function mouseReleased() { if (wifi)         wifi.handleMouseReleased(); }

Utility

toString()

Returns a string summarizing key properties: center, numArcs, innerRadius, arcSpacing, dotRadius, theta, direction, and activeCount.

Returns

string

Example
console.log(wifi.toString());
// "SWWifi(center:(0.00,0.00), 3 arcs, innerR:1.50, spacing:1.20, dotR:0.40, theta:90°, dir:90°, active:3/3)"

Usage Examples

Example 1: Basic WiFi Shape

let grid, wifi;

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 col = SWColor.fromHex('#30c2f2', 100, "arcColor");
    wifi = new SWWifi({
        center:   new SWPoint(0, 0),
        arcColor: col,
        dotColor: SWColor.copy(col),
    });
}

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

Example 2: Signal Strength Indicator

// Show 2-of-4 bars lit
let wifi = new SWWifi({
    numArcs:         4,
    activeCount:     2,
    dimFactor:       0.2,
    inactiveOpacity: 30,
});

// Change signal level at runtime
wifi.activeCount = 4; // full signal
wifi.activeCount = 0; // no signal

Example 3: Pulsing Animation

let grid, wifi;
let pulseOn = false, pulseStartTime = 0;
const PULSE_RATE = 2.0; // arcs per second

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) });
    wifi = new SWWifi();
}

function draw() {
    background(0, 0, 93);
    grid.draw();

    if (pulseOn) {
        const pt = millis() / 1000 - pulseStartTime;
        wifi.pulseIndex = Math.floor(pt * PULSE_RATE) % wifi.numArcs;
    } else {
        wifi.pulseIndex = -1;
    }

    wifi.drawOnGrid(grid);
}

function keyPressed() {
    if (key === 'p') {
        pulseOn = !pulseOn;
        if (pulseOn) pulseStartTime = millis() / 1000;
    }
}

Example 4: Breathing Thickness

let grid, wifi, breatheSin;
let breathingOn = false, breatheStartTime = 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) });
    wifi = new SWWifi({ thickness: 6 });

    const minT = 2, maxT = 14, period = 1.5;
    const amp  = (maxT - minT) / 2;
    const freq = (2 * Math.PI) / period;
    const mid  = (minT + maxT) / 2;
    breatheSin = new SWSinusoid(amp, freq, mid, -Math.PI / 6);
}

function draw() {
    background(0, 0, 93);
    grid.draw();

    if (breathingOn) {
        const t = millis() / 1000 - breatheStartTime;
        wifi.breatheThickness(breatheSin, t);
    }

    wifi.drawOnGrid(grid);
}

function keyPressed() {
    if (key === 'b') {
        breathingOn = !breathingOn;
        if (breathingOn) breatheStartTime = millis() / 1000;
        else wifi._thickness = wifi.thickness; // restore static weight
    }
}

Example 5: Spinning

let grid, wifi;
let spinOn = false, spinStartTime = 0;
const SPIN_RATE = 20; // deg/sec

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) });
    wifi = new SWWifi();
}

function draw() {
    background(0, 0, 93);
    grid.draw();

    if (spinOn) {
        const t = millis() / 1000 - spinStartTime;
        wifi.rotateAboutCenter(SPIN_RATE, t);
    }

    wifi.drawOnGrid(grid);
}

function keyPressed() {
    if (key === 's') {
        spinOn = !spinOn;
        if (spinOn) {
            spinStartTime = millis() / 1000;
        } else {
            wifi._animRotDeg = 0;
        }
    }
}

Example 6: Pulse + Breathe + Spin Combined

// All three animations running simultaneously
// (see swWifiDemo.html for the full interactive version)

function draw() {
    background(0, 0, 93);
    grid.draw();

    // Pulsing: set pulseIndex each frame
    if (pulseOn) {
        const pt = millis() / 1000 - pulseStartTime;
        wifi.pulseIndex = Math.floor(pt * PULSE_RATE) % wifi.numArcs;
    } else {
        wifi.pulseIndex = -1;
    }

    // Breathing: modulate thickness each frame
    if (breathingOn) {
        const bt = millis() / 1000 - breatheStartTime;
        wifi.breatheThickness(breatheSin, bt);
    }

    // Spinning: update accumulated rotation
    if (spinOn) {
        const st = millis() / 1000 - spinStartTime;
        wifi.rotateAboutCenter(SPIN_RATE, st);
    }

    wifi.drawOnGrid(grid);
}

Example 7: Interactive Dragging

// 1. Enable center handle at construction (or set showCenter = true later)
let wifi = new SWWifi({ showCenter: true });

// 2. Forward p5 mouse events
function mousePressed()  { if (wifi && grid) wifi.handleMousePressed(mouseX, mouseY, grid, 6); }
function mouseDragged()  { if (wifi && grid) wifi.handleMouseDragged(mouseX, mouseY, grid); }
function mouseReleased() { if (wifi)         wifi.handleMouseReleased(); }

// 3. Read current position after drag
console.log(wifi.center.x.toFixed(2), wifi.center.y.toFixed(2));

Best Practices

1. Breathing Timing

  • Pass elapsed time in seconds (not frame count) for frame-rate-independent animation
  • Track breatheStartTime separately so breathing can be paused and resumed independently of spinning and pulsing
  • When turning breathing off, restore the static weight: wifi._thickness = wifi.thickness

2. Pulse vs. Active Count

  • pulseIndex overrides activeCount entirely while ≥ 0
  • When pulsing ends, set pulseIndex = −1 to return control to activeCount
  • Pulsing and spinning combine naturally — the arcs sweep while the whole group rotates
  • Pulsing with inactiveOpacity = 0 creates a single-bar sweeping effect with no ghost arcs

3. Signal Strength Design

  • Use dimFactor + inactiveOpacity together for the right visual balance — low brightness + low opacity creates a subtle "ghost bar" effect
  • Setting both to high values makes the inactive bars nearly identical to active ones; this is intentional for some UI styles
  • Setting inactiveOpacity = 0 hides inactive arcs entirely, focusing attention on the active count

4. Coordinate Convention

  • All user-facing angles (direction, rotation) use math convention: CCW from +x, y-up. The class handles the p5.js y-flip internally
  • Move the shape by calling wifi.center.setPosition(x, y) — direct assignment to wifi.center works too but replaces the SWPoint object

5. Dragging Notes

  • Drag only activates when both showCenter and isDraggable are true
  • The hit radius is 8 px by default; use the tolerance parameter of handleMousePressed() to widen the target area on touch screens
  • Use isDragging getter to prevent other click actions (e.g., toggling grid) from firing while a drag is in progress
  • Position is not reset by reset() — move the shape back manually or set the center at construction

Integration with Other SketchWave Classes

Script Loading Order

SWWifi depends on SWColor, SWPoint, SWGrid, and SWSinusoid (for breathing):

<!-- 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/swWifi.js"></script>

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

Composition: Arcs ≈ SWArc, Dot ≈ SWDisk

The arcs drawn by SWWifi are conceptually identical to individual SWArc instances — open circular arcs with a configurable angular span, rendered with strokeCap(ROUND). The center dot corresponds to an SWDisk — a filled circle. SWWifi internalizes both so you don't need to manage multiple objects:

SWWifi conceptEquivalent standalone classSWWifi handles it via…
Each concentric arcSWArcarc() calls in _drawShape()
Center dotSWDiskellipse() call with dotRadius
Full-ring mode (theta=360)SWRingsame arc call — p5 closes at 360°
Color managementSWColorarcColor.createDarkerColor(dimFactor)
Sinusoidal breathingSWSinusoidbreatheThickness(sinusoid, t)
Grid coordinate conversionSWGridgrid.userToScreen() / screenToUser()
Anchor positionSWPointcenter.setPosition(x, y)

Working with SWColor

SWWifi uses SWColor for all arc and dot color management:

  • Colors are deep-copied at construction — external mutations to the source object have no effect
  • arcColor.createDarkerColor(dimFactor) returns a new SWColor with the HSB brightness channel multiplied by dimFactor; the original arcColor is not modified
  • The dot is always drawn at fillOpacity with the unmodified dotColor

Working with SWSinusoid

breatheThickness() accepts any SWSinusoid whose getValue(t) returns pixel values:

// Thickness oscillates between 2 and 14 px with 1.5-second period
const minT = 2, maxT = 14, period = 1.5;
const amp  = (maxT - minT) / 2;          // 6
const freq = (2 * Math.PI) / period;
const mid  = (minT + maxT) / 2;          // 8
const breatheSin = new SWSinusoid(amp, freq, mid, -Math.PI / 6);

Working with SWGrid

SWWifi uses SWGrid in two ways:

  • drawOnGrid(grid) — converts center from user to screen with grid.userToScreen() and uses grid.xScale to convert radii from user units to pixels
  • Drag methods — grid.userToScreen() for hit-testing; grid.screenToUser() to convert mouse position back to user coords while dragging

Comparing SWWifi with Related Classes

FeatureSWWifiSWArcSWRing
Number of shapesN arcs + 1 dot (composite)1 arc1 full ring
Angular spanConfigurable (theta, default 90°)Configurable (theta)Always 360°
Signal-strength controlYes (activeCount, dimFactor, inactiveOpacity)NoNo
Pulse animationYes (pulseIndex)NoNo
Breathing animationbreatheThicknessbreatheRadiusbreatheRadius
Spin animationrotateAboutCenterYesYes
Interactive dragYes (via center handle)NoNo
Center dotYes (built-in)NoNo

Source Code

The complete SWWifi class implementation:

Show/Hide Source Code
/*
File:    swWifi.js
Date:    2026-04-23
Author:  klp + GitHub Copilot
App:     SketchWaveTNT2026-04-21-Stg8
Purpose: SWWifi — a WiFi signal composite shape made of concentric arcs
         and a center dot, all rendered as a single group.

=== Layout ===
  The shape is centered on `center` (an SWPoint in user/grid coords).
  `numArcs` concentric arcs are drawn with increasing radii:
    radius[i] = innerRadius + i * arcSpacing   (i = 0 is innermost)
  Each arc spans `theta` degrees, symmetrically centered on `direction`.
  A filled disk of radius `dotRadius` sits at the center (origin point).

=== Arc Angle Convention ===
  SketchWave uses math angles: CCW from +x axis, y increases upward.
  `direction` is the angle the signal points (default 90 = straight up).
  Each arc spans from (direction − theta/2) to (direction + theta/2).
  Conversion to p5 screen angles (y-axis flipped):
    p5Start = −radians(direction + theta/2)
    p5Stop  = −radians(direction − theta/2)

=== Signal Strength / Active Count ===
  `activeCount` (0..numArcs) controls how many arcs glow at full brightness.
  Arcs beyond activeCount are drawn dimmed by `dimFactor`.
  Set activeCount to 0 for “no signal”, numArcs for “full signal”.

=== Design Parameters (user/grid units unless noted) ===
  center           — SWPoint: anchor of the group (default origin)
  numArcs          — number of concentric arcs (default 3)
  innerRadius      — innermost arc radius in user units (default 1.5)
  arcSpacing       — radial gap between consecutive arcs (default 1.2)
  dotRadius        — center dot radius in user units (default 0.4)
  theta            — arc angular span in degrees (default 90)
  direction        — direction the signal points, °CCW from +x (default 90 = up)
  rotation         — static rotation offset in degrees (default 0)
  thickness        — arc stroke weight in pixels (default 6)
  arcColor         — SWColor for arcs (default sky-blue)
  dotColor         — SWColor for center dot (default sky-blue)
  fillOpacity      — alpha 0–100 for arcs and dot (default 100)
  activeCount      — arcs drawn at full brightness (default = numArcs)
  dimFactor        — brightness multiplier for inactive arcs (default 0.25)
  inactiveOpacity  — alpha 0–100 for inactive arcs (default 35)
  showCenter       — draw anchor crosshair (default false)

=== Interaction: Drag ===
  When showCenter is true a small crosshair handle is visible at the anchor.
  Grabbing that handle moves the shape in user-space coords.
    isDraggable              — enable/disable drag (default true)
    handleMousePressed(mx, my, grid [,tolerance]) → bool
    handleMouseDragged(mx, my, grid)              → bool
    handleMouseReleased()
    isDragging (read-only getter)

=== Animation ===
  breatheThickness(sinusoid, t)   — oscillate arc stroke weight
  rotateAboutCenter(degPerSec, t) — spin the whole group; accumulates _animRotDeg
  reset()                         — restore all original values

=== Drawing ===
  drawOnGrid(grid)  — preferred: center.x/y treated as user-space coords
  draw()            — raw: center.x/y treated as screen pixels (scale = 1)

=== Dependencies ===
  p5.js, SWColor, SWPoint, SWGrid, SWSinusoid
*/

console.log("[swWifi.js] SWWifi class loaded.");

class SWWifi {

    /**
     * @param {object}  opts
     * @param {SWPoint} opts.center           — anchor of the group (default origin)
     * @param {number}  opts.numArcs          — number of concentric arcs (default 3)
     * @param {number}  opts.innerRadius      — innermost arc radius in user units (default 1.5)
     * @param {number}  opts.arcSpacing       — radial gap between arcs in user units (default 1.2)
     * @param {number}  opts.dotRadius        — center dot radius in user units (default 0.4)
     * @param {number}  opts.theta            — arc angular span in degrees (default 90)
     * @param {number}  opts.direction        — signal direction, °CCW from +x axis (default 90 = up)
     * @param {number}  opts.rotation         — static rotation offset in degrees (default 0)
     * @param {number}  opts.thickness        — arc stroke weight in pixels (default 6)
     * @param {SWColor} opts.arcColor         — color for all arcs (default sky-blue)
     * @param {SWColor} opts.dotColor         — color for the center dot (default sky-blue)
     * @param {number}  opts.fillOpacity      — alpha 0–100 for arcs and dot (default 100)
     * @param {number}  opts.activeCount      — arcs at full brightness (default = numArcs)
     * @param {number}  opts.dimFactor        — brightness factor for inactive arcs 0–1 (default 0.25)
     * @param {number}  opts.inactiveOpacity  — alpha 0–100 for inactive arcs (default 35)
     * @param {boolean} opts.showCenter       — draw anchor crosshair (default false)
     */
    constructor({
        center      = new SWPoint(0, 0),
        numArcs     = 3,
        innerRadius = 1.5,
        arcSpacing  = 1.2,
        dotRadius   = 0.4,
        theta       = 90,
        direction   = 90,
        rotation    = 0,
        thickness   = 6,
        arcColor    = undefined,
        dotColor    = undefined,
        fillOpacity = 100,
        activeCount = undefined,
        dimFactor       = 0.25,
        inactiveOpacity = 35,
        showCenter      = false,
    } = {}) {
        this.center      = center;
        this.numArcs     = numArcs;
        this.innerRadius = innerRadius;
        this.arcSpacing  = arcSpacing;
        this.dotRadius   = dotRadius;
        this.theta       = theta;
        this.direction   = direction;
        this.rotation    = rotation;
        this.thickness   = thickness;
        this.fillOpacity = fillOpacity;
        this.activeCount = (activeCount !== undefined) ? activeCount : numArcs;
        this.dimFactor       = Math.max(0, Math.min(1, dimFactor));
        this.inactiveOpacity = Math.max(0, Math.min(100, inactiveOpacity));
        this.showCenter      = showCenter;
        this.pulseIndex      = -1;   // -1 = off (use activeCount); >=0 = single lit arc

        // Drag state
        this.isDraggable  = true;
        this._isDragging  = false;
        this._dragOffsetX = 0;      // user-unit offset at drag start
        this._dragOffsetY = 0;

        // Default color: sky-blue HSB(210, 80, 92)
        this.arcColor = arcColor
            ? SWColor.copy(arcColor)
            : new SWColor(210, 80, 92, fillOpacity, "arcColor");
        this.dotColor = dotColor
            ? SWColor.copy(dotColor)
            : new SWColor(210, 80, 92, fillOpacity, "dotColor");

        // Animated thickness — may differ from this.thickness when breathe is active
        this._thickness  = thickness;
        // Accumulated spin angle (degrees, CCW)
        this._animRotDeg = 0;

        // Store originals for reset()
        this._origNumArcs     = numArcs;
        this._origInnerRadius = innerRadius;
        this._origArcSpacing  = arcSpacing;
        this._origDotRadius   = dotRadius;
        this._origTheta       = theta;
        this._origDirection   = direction;
        this._origRotation    = rotation;
        this._origThickness   = thickness;
        this._origFillOpacity = fillOpacity;
        this._origActiveCount = (activeCount !== undefined) ? activeCount : numArcs;
        this._origDimFactor       = this.dimFactor;
        this._origInactiveOpacity = this.inactiveOpacity;
        this._origArcColor        = SWColor.copy(this.arcColor);
        this._origDotColor        = SWColor.copy(this.dotColor);
    }//end constructor

    // —— Computed helpers —————————————————————————————————————————————

    /** Radius of arc i (0 = innermost) in user units. */
    arcRadius(i) {
        return this.innerRadius + i * this.arcSpacing;
    }//arcRadius

    /** Outer radius of the outermost arc in user units. */
    get maxRadius() {
        return this.arcRadius(Math.max(0, this.numArcs - 1));
    }//get maxRadius

    // —— Animation ——————————————————————————————————————————————————————————————

    /**
     * Oscillate arc stroke weight using a sinusoid.
     * The sinusoid’s getValue(t) should return pixel values (e.g. 2–14).
     * @param {SWSinusoid} sinusoid
     * @param {number}     t — elapsed time in seconds since breath started
     */
    breatheThickness(sinusoid, t) {
        this._thickness = Math.max(0.5, sinusoid.getValue(t));
    }//breatheThickness

    /**
     * Rotate the WiFi group about its center.
     * @param {number} degPerSec — CCW positive; negative = CW
     * @param {number} t — elapsed time in seconds since spin started
     */
    rotateAboutCenter(degPerSec, t) {
        this._animRotDeg = degPerSec * t;
    }//rotateAboutCenter

    /** Restore all original parameter values and clear animation state. */
    reset() {
        this.numArcs     = this._origNumArcs;
        this.innerRadius = this._origInnerRadius;
        this.arcSpacing  = this._origArcSpacing;
        this.dotRadius   = this._origDotRadius;
        this.theta       = this._origTheta;
        this.direction   = this._origDirection;
        this.rotation    = this._origRotation;
        this.thickness   = this._origThickness;
        this.fillOpacity = this._origFillOpacity;
        this.activeCount = this._origActiveCount;
        this.dimFactor       = this._origDimFactor;
        this.inactiveOpacity = this._origInactiveOpacity;
        this.arcColor        = SWColor.copy(this._origArcColor);
        this.dotColor    = SWColor.copy(this._origDotColor);
        this._thickness  = this._origThickness;
        this._animRotDeg = 0;
        this.pulseIndex  = -1;
    }//reset

    // —— Interaction: Drag —————————————————————————————————————————————————————————————

    /**
     * Call from the p5.js mousePressed() function.
     * Starts a drag if the click is within the center handle (visible only when
     * showCenter is true). Returns true if this instance consumed the event.
     *
     * @param {number} mx            - p5 mouseX (screen pixels)
     * @param {number} my            - p5 mouseY (screen pixels)
     * @param {SWGrid} grid          - the active SWGrid
     * @param {number} [tolerance=0] - extra pixel margin around the 8 px handle
     * @returns {boolean}
     */
    handleMousePressed(mx, my, grid, tolerance = 0) {
        if (!this.isDraggable || !this.showCenter) return false;
        const s  = grid.userToScreen(this.center.x, this.center.y);
        const dx = mx - s.x;
        const dy = my - s.y;
        const hitR = 8 + tolerance;
        if (dx * dx + dy * dy > hitR * hitR) return false;
        const u = grid.screenToUser(mx, my);
        this._isDragging  = true;
        this._dragOffsetX = u.x - this.center.x;
        this._dragOffsetY = u.y - this.center.y;
        return true;
    }//end handleMousePressed

    /**
     * Call from the p5.js mouseDragged() function.
     * Moves the center while preserving the original grab offset.
     * Returns true while this instance is being dragged.
     *
     * @param {number} mx   - p5 mouseX (screen pixels)
     * @param {number} my   - p5 mouseY (screen pixels)
     * @param {SWGrid} grid - the active SWGrid
     * @returns {boolean}
     */
    handleMouseDragged(mx, my, grid) {
        if (!this._isDragging) return false;
        const u = grid.screenToUser(mx, my);
        this.center.setPosition(
            u.x - this._dragOffsetX,
            u.y - this._dragOffsetY
        );
        return true;
    }//end handleMouseDragged

    /**
     * Call from the p5.js mouseReleased() function.
     * Ends the drag operation.
     */
    handleMouseReleased() {
        this._isDragging = false;
    }//end handleMouseReleased

    /** True while the user is actively dragging this instance. @type {boolean} */
    get isDragging() { return this._isDragging; }

    // —— Drawing ——————————————————————————————————————————————————————————————————————
     /* center.x / center.y are treated as pixel positions; scale = 1 px/unit.
     */
    draw() {
        this._drawShape(this.center.x, this.center.y, 1);
    }//draw

    /**
     * Draw mapped through the given SWGrid.
     * center.x / center.y are user-space coordinates.
     * @param {SWGrid} grid
     */
    drawOnGrid(grid) {
        const s = grid.userToScreen(this.center.x, this.center.y);
        this._drawShape(s.x, s.y, grid.xScale);
    }//drawOnGrid

    /**
     * Internal rendering kernel.
     * @param {number} cx    — screen x of center
     * @param {number} cy    — screen y of center
     * @param {number} scale — pixels per user unit
     */
    _drawShape(cx, cy, scale) {
        const totalDeg = this.rotation + this._animRotDeg;
        const rotRad   = -totalDeg * (Math.PI / 180);   // CCW user → CW p5 (y-flipped)

        push();
        translate(cx, cy);
        rotate(rotRad);

        // —— Arcs (innermost first so outer arcs overlay inner on overlap) ——
        for (let i = 0; i < this.numArcs; i++) {
            const r        = this.arcRadius(i) * scale;
            // Pulse mode: only one arc lit at a time; otherwise use activeCount range
            const isActive = this.pulseIndex >= 0
                ? (i === this.pulseIndex)
                : (i < this.activeCount);

            // Active arcs: full color / opacity; inactive: dimmed brightness + reduced alpha
            const col   = isActive
                ? this.arcColor
                : this.arcColor.createDarkerColor(this.dimFactor);
            const alpha = isActive
                ? this.fillOpacity
                : this.inactiveOpacity;

            noFill();
            stroke(col.h, col.s, col.b, alpha);
            strokeWeight(this._thickness);
            strokeCap(ROUND);

            // Arc centered on `direction`, spanning `theta` degrees.
            //   User-space: arc goes CCW from (direction − theta/2) to (direction + theta/2)
            //   p5 screen (y-flipped, CW angles):
            //     p5Start = −radians(direction + theta/2)
            //     p5Stop  = −radians(direction − theta/2)
            //   Since theta > 0: p5Start < p5Stop → p5 draws the short CW arc ✓
            const half    = this.theta / 2;
            const p5Start = -((this.direction + half) * (Math.PI / 180));
            const p5Stop  = -((this.direction - half) * (Math.PI / 180));
            arc(0, 0, 2 * r, 2 * r, p5Start, p5Stop, OPEN);
        }

        // —— Center dot ——————————————————————————————————————————————
        const dr = this.dotRadius * scale;
        noStroke();
        // Dot is always “active” (lit) — it marks the signal origin
        fill(this.dotColor.h, this.dotColor.s, this.dotColor.b, this.fillOpacity);
        ellipse(0, 0, 2 * dr, 2 * dr);

        noStroke();
        noFill();
        pop();

        // —— Center-handle marker (drawn outside push/pop, at screen anchor) ——
        if (this.showCenter) {
            const r = 8;
            push();
            translate(cx, cy);
            strokeWeight(2);
            stroke(0, 0, 15, 100);
            fill(0, 0, 100, 85);
            ellipse(0, 0, r * 2, r * 2);
            stroke(0, 0, 15, 100);
            strokeWeight(1.5);
            line(-(r - 2), 0, r - 2, 0);
            line(0, -(r - 2), 0, r - 2);
            noStroke();
            noFill();
            pop();
        }
    }//_drawShape

    // —— toString ————————————————————————————————————————————————————————————————

    toString() {
        return `SWWifi(center:(${this.center.x.toFixed(2)},${this.center.y.toFixed(2)}), ` +
               `${this.numArcs} arcs, innerR:${this.innerRadius.toFixed(2)}, ` +
               `spacing:${this.arcSpacing.toFixed(2)}, dotR:${this.dotRadius.toFixed(2)}, ` +
               `theta:${this.theta.toFixed(0)}°, dir:${this.direction.toFixed(0)}°, ` +
               `active:${this.activeCount}/${this.numArcs})`;
    }//toString

}//end class SWWifi