☀️ SketchWave Sun Class Reference

Central disk • Radiating rays • Glow • Breathing • Rotation • Ripple

▶ Try SWSun Demo

Overview

SWSun is a SketchWave composite class that draws a stylised sun (or moon) on a p5.js canvas. It combines a central SWDisk ball with up to 36 evenly-spaced radiating shapes, surrounded by a soft multi-layer glow. Four ray types are built in:

line

Straight stroke from ball edge outward (SWLine style)

arrow

SWArrow pointing outward with configurable tip shape

triangle

SWTriangle isosceles spike — tip outward, base at ball edge

sinusoid

Wavy line with amplitude tapering at both ends (default)

Features

  • Ball — central filled & stroked disk; optional hide with showBall
  • Glow — concentric semi-transparent rings; adjustable scale, layers, and alpha
  • Ray colours — 1–5 colours cycle across rays for a multi-coloured corona effect
  • Breathingbreathe(sinusoid, t) scales ball + rays together via SWSinusoid
  • RotationrotateRays(degPerSec, t) spins all rays continuously
  • RippleanimateSinusoid(phaseDegPerSec, t) makes sinusoid rays travel outward
  • DragsetCenter(cx, cy) lets you move the sun to any canvas position
  • Grid supportdrawOnGrid(grid) maps to user coordinates via SWGrid

Constructor

let sun = new SWSun(cx, cy, ballRadius, ballFillColor, options);

Required Parameters

ParameterTypeDescription
cx number Centre x — pixels when using draw(), user units when using drawOnGrid()
cy number Centre y — same coordinate system as cx
ballRadius number Radius of the central disk in the same units as cx/cy
ballFillColor SWColor Fill colour of the central ball. Also used as default for glow and rays if those options are omitted.

Options Object

Pass any combination of these as a plain JS object in the fifth argument:

Ball Options

KeyTypeDefaultDescription
ballStrokeColorSWColorundefined Border colour of the ball. Omit for no stroke.
ballThicknessnumber2 Border stroke weight in pixels.
showBallbooleantrue Whether to draw the central disk. Setting false hides the ball but keeps the glow and rays.
Glow Options
KeyTypeDefaultDescription
showGlowbooleantrue Draw the soft concentric glow rings.
glowColorSWColorballFillColor Colour tint of the glow rings.
glowScalenumber2.5 Outermost glow ring radius = glowScale × ballRadius. Increase for a wider aura.
glowLayersnumber5 Number of concentric rings drawn. More layers = smoother gradient.
glowAlphanumber35 Peak alpha (0–100) of the innermost ring. Outermost ring fades to 0 automatically.
Ray Options
KeyTypeDefaultDescription
showRaysbooleantrue Draw the radiating shapes.
rayTypestring"sinusoid" Ray shape. One of: "line", "arrow", "triangle", "sinusoid".
rayCountnumber8 Number of evenly-spaced rays around the circle.
rayLengthnumberballRadius × 1.5 Length of each ray in the same units as cx/cy.
rayThicknessnumber3 Stroke weight for line, arrow, and sinusoid rays.
rayColorsSWColor[][ballFillColor] Array of 1–5 SWColor instances. Colours cycle across rays (ray 0 gets colour 0, ray 1 gets colour 1, etc., wrapping around).
Sinusoid Ray Options
KeyTypeDefaultDescription
sinAmplitudenumberballRadius × 0.25 Wave amplitude in pixels. Amplitude tapers to zero at both ends of each ray.
sinPeriodsnumber2.5 Number of full sine-wave cycles along the ray length.
rippleOutwardbooleantrue When true (default), waves travel away from the sun when animateSinusoid() is called. Set to false to reverse direction (waves travel toward the sun).
Triangle Ray Options
KeyTypeDefaultDescription
triWidthnumberballRadius × 0.5 Width of the triangle base (at the ball edge). The tip always points outward.
Arrow Ray Options
KeyTypeDefaultDescription
arrowTipAnglenumber25 Arrowhead half-angle in degrees. Larger = wider barbs.
arrowTipFactornumber0.35 Arrowhead barb length as a fraction (0–1) of the total ray length.

Constructor Example

let sun;

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

    const fill   = new SWColor(42,  90, 100, 100, "sunFill");
    const stroke = new SWColor(25,  90,  80, 100, "sunStroke");
    const glow   = new SWColor(42,  80, 100, 100, "sunGlow");
    const ray1   = new SWColor(42,  90, 100, 100, "ray1");
    const ray2   = new SWColor(25,  90,  80, 100, "ray2");

    sun = new SWSun(width / 2, height / 2, 60, fill, {
        ballStrokeColor: stroke,
        ballThickness:   3,
        showGlow:        true,
        glowColor:       glow,
        glowScale:       2.5,
        glowLayers:      5,
        glowAlpha:       35,
        rayType:         'sinusoid',
        rayCount:        12,
        rayLength:       140,
        rayThickness:    3,
        rayColors:       [ray1, ray2],
        sinAmplitude:    16,
        sinPeriods:      2.5
    });
}

function draw() {
    background(200, 20, 95);
    sun.draw();
}

Key Properties

After construction, the following properties are readable and can be modified via their setter methods. Directly setting a property bypasses the setter guard logic, so prefer setters for safety.

cx / cy

Type: number — Centre coordinates. Use setCenter(cx, cy) to move the sun.

ballRadius / originalBallRadius

Type: number — ballRadius is the current radius; originalBallRadius is the baseline that breathing animates around. Use setBallRadius(r) to update both at once.

_currentRadius / _currentRayLength

Type: number — Live animated values set by breathe(). Read these (not ballRadius) if you need the current on-screen size.

showBall / showGlow / showRays

Type: boolean — Visibility flags. Toggle these directly to show/hide each layer without rebuilding the object.

rayType

Type: string — One of "line", "arrow", "triangle", "sinusoid". Change via setRayType(type). Takes effect immediately on the next draw() call.

glowScale

Type: number — Outermost glow ring = glowScale × _currentRadius. When breathing is on, the glow radius breathes proportionally. The hit-test for mouse drag also uses _currentRadius × glowScale as the click radius.

rayColors

Type: SWColor[] — Array of 1–5 colours. Ray i uses rayColors[i % rayColors.length]. Use setRayColors(arr) to replace the array.

_rotationAngle

Type: number (degrees) — Angular offset added to every ray's base angle. Updated by rotateRays(); reset to 0 by reset().

sinPhase

Type: number (radians) — Phase offset added to the sinusoid wave for each sinusoid-type ray. Updated by animateSinusoid(); controls the ripple effect.

Methods

Drawing Methods

draw()

Renders the sun in screen (pixel) coordinates. Call once per draw() loop frame.

Rendering order (back to front): glow rings → rays → central ball.

function draw() {
    background(200, 20, 95);
    sun.draw();
}

drawOnGrid(grid)

Parameters: grid (SWGrid)

Maps cx, cy, and all radii/lengths from user coordinates to screen pixels using the provided SWGrid, then renders normally.

// sun.cx / sun.cy are in user (math) units
sun.drawOnGrid(myGrid);

Animation Methods

breathe(sinusoid, t)

Parameters: sinusoid (SWSinusoid), t (number — elapsed seconds)

Scales both the ball radius and the ray length by sinusoid.getValue(t). Configure the sinusoid with adjustWaveUsingExtrema(minScale, maxScale) to set the breathing range. The glow automatically breathes with the ball because glow radius is glowScale × _currentRadius.

let breatheSin;

function setup() {
    // ...
    breatheSin = new SWSinusoid();
    breatheSin.adjustWaveUsingExtrema(0.75, 1.25);  // 75% – 125% of base size
    breatheSin.setPeriod(3);                         // 3-second cycle
}

function draw() {
    let t = millis() / 1000;
    sun.breathe(breatheSin, t);
    sun.draw();
}

rotateRays(degPerSec, t)

Parameters: degPerSec (number), t (number — elapsed seconds)

Rotates all rays by degPerSec × t degrees from their original positions. Positive values rotate clockwise (screen coordinates). The ball and glow are not affected — only the ray angles change.

// Spin at 20°/sec clockwise
sun.rotateRays(20, totalRotTime);

// Spin counter-clockwise
sun.rotateRays(-30, totalRotTime);

animateSinusoid(phaseDegPerSec, t)

Parameters: phaseDegPerSec (number — phase advance rate in degrees/sec), t (number — elapsed seconds)

Advances sinPhase by radians(phaseDegPerSec) × t, which makes sinusoid-type rays appear to travel outward (ripple effect). Has no visual effect on other ray types.

// Ripple at 180°/sec
sun.animateSinusoid(180, totalRippleTime);

transform(options)

Combines breathing, rotation, and ripple in a single call. Mirrors the transform() API on SWLine, SWArrow, and SWTriangle.

OptionTypeDefaultEffect
sinusoidSWSinusoidnullBreathing (null = skip)
tnumber0Time in seconds
degPerSecnumbernullRotation rate (null = skip)
phaseDegPerSecnumbernullRipple rate (null = skip)
// All three animations in one call
sun.transform({
    sinusoid:       breatheSin,
    t:              totalTime,
    degPerSec:      20,
    phaseDegPerSec: 180
});

reset()

Resets all animation state to initial values: _currentRadius and _currentRayLength return to their originals, _rotationAngle returns to 0, and sinPhase returns to 0. Does not reset property changes made via setters (colours, counts, etc.).

sun.reset();   // stop animation effects and return to baseline

Setter Methods

MethodParameterNotes
setCenter(cx, cy) two numbers Move the sun's centre. Safe to call every frame for mouse drag.
setBallRadius(r) number Updates ballRadius, originalBallRadius, _currentRadius, and the internal disk radius together.
setBallFillColor(swColor) SWColor Updates both the SWSun property and the internal SWDisk fill.
setBallStrokeColor(swColor) SWColor Updates both the SWSun property and the internal SWDisk stroke.
setGlowColor(swColor) SWColor
setGlowAlpha(a) number 0–100 Clamped to [0, 100].
setGlowScale(s) number Minimum enforced at 1.05 (glow must extend beyond ball).
setRayType(type) string "line" | "arrow" | "triangle" | "sinusoid"
setRayCount(count) number Rounded to integer; minimum 0.
setRayLength(len) number Updates rayLength, originalRayLength, and _currentRayLength together.
setRayThickness(w) number Minimum 1.
setRayColors(colorsArr) SWColor[] Replaces the colours array; accepts 1–5 elements.
setSinAmplitude(a) number Minimum 0. Applies to sinusoid ray type only.
setSinPeriods(p) number Minimum 0.25. Applies to sinusoid ray type only.
setRippleOutward(v) boolean true = waves travel away from sun (default). false = waves travel toward sun. Takes effect immediately — no rebuild needed.
setTriWidth(w) number Minimum 1. Applies to triangle ray type only.

Utility Methods

toString()

Returns: string — human-readable summary of the sun's state.

console.log(sun.toString());
// → "SWSun(cx:320.0, cy:240.0, r:60.0, rays:12×sinusoid)"

Ray Types in Detail

line

A straight stroke from the ball edge to the ray tip. Uses p5's line() with ROUND cap. The gap at the ball edge equals ballRadius + ballThickness/2 so the line starts just beyond the ball border. Clean and fast — good for a classic sun icon look.

Relevant options: rayThickness

arrow

Uses a SWArrow instance per ray — tail near the ball, arrowhead pointing outward. The arrowhead barb is drawn as two lines diverging from a point at arrowTipFactor × rayLength back from the tip. Good for "energy radiating outward" effects.

Relevant options: rayThickness, arrowTipAngle, arrowTipFactor

triangle

Uses a SWTriangle instance per ray — an isosceles spike with the tip pointing outward and the base at the ball edge. The base width is triWidth. Good for spiky sun / compass-rose styles. The fill colour matches the ray colour.

Relevant options: triWidth

sinusoid (default)

Draws 40 vertex() points along each ray using inline wave math (sin(bFreq × d + sinPhase)). Amplitude tapers smoothly to zero at both the ball end and the tip via a sin(t × π) envelope. The animateSinusoid() method advances sinPhase over time, making the waves travel outward (ripple). Does not require a loaded SWSinusoid instance.

Relevant options: rayThickness, sinAmplitude, sinPeriods

Usage Examples

Example 1: Simple Static Sun

let sun;

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

    const yellow = new SWColor(42, 85, 100, 100, "yellow");
    const orange = new SWColor(25, 90,  80, 100, "orange");

    sun = new SWSun(200, 200, 50, yellow, {
        ballStrokeColor: orange,
        rayType:  'line',
        rayCount: 12,
        rayLength: 80
    });
}

function draw() {
    background(200, 30, 90);
    sun.draw();
}

Example 2: Breathing Sun

let sun, breatheSin;

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

    const fill = new SWColor(42, 85, 100, 100, "fill");
    sun = new SWSun(200, 200, 50, fill, {
        rayType:  'sinusoid',
        rayCount: 12,
        rayLength: 80
    });

    breatheSin = new SWSinusoid();
    breatheSin.adjustWaveUsingExtrema(0.80, 1.20);
    breatheSin.setPeriod(3);
}

function draw() {
    background(220, 20, 95);
    const t = millis() / 1000;
    sun.breathe(breatheSin, t);
    sun.draw();
}

Example 3: Spinning Triangle-Ray Sun

let sun;
let rotElapsed = 0;
let lastT = 0;

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

    const gold = new SWColor(42, 90, 100, 100, "gold");
    sun = new SWSun(200, 200, 55, gold, {
        rayType:  'triangle',
        rayCount: 8,
        rayLength: 90,
        triWidth:  20
    });
}

function draw() {
    background(220, 30, 90);

    // Accumulate elapsed time for rotation
    const nowT = millis() / 1000;
    rotElapsed += (nowT - lastT);
    lastT = nowT;

    sun.rotateRays(30, rotElapsed);   // 30°/sec CW
    sun.draw();
}

Example 4: Rippling Sinusoid Rays

let sun;
let rippleElapsed = 0;
let lastT = 0;

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

    const fill = new SWColor(50, 80, 100, 100, "fill");
    sun = new SWSun(200, 200, 50, fill, {
        rayType:      'sinusoid',
        rayCount:     16,
        rayLength:    100,
        sinAmplitude: 10,
        sinPeriods:   3
    });
}

function draw() {
    background(200, 20, 92);

    const nowT = millis() / 1000;
    rippleElapsed += (nowT - lastT);
    lastT = nowT;

    sun.animateSinusoid(180, rippleElapsed);   // waves travel outward
    sun.draw();
}

Example 5: Multi-colour Corona + Mouse Drag

let sun;

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

    const fill   = new SWColor(42,  90, 100, 100, "fill");
    const c1     = new SWColor(42,  90, 100, 100, "c1");
    const c2     = new SWColor(25,  90,  80, 100, "c2");
    const c3     = new SWColor(55,  80, 100, 100, "c3");

    sun = new SWSun(250, 250, 60, fill, {
        rayType:   'arrow',
        rayCount:  12,
        rayLength: 100,
        rayColors: [c1, c2, c3],      // 3 colours cycling
        glowScale: 2.8,
        glowAlpha: 40
    });
}

function draw() {
    background(215, 30, 88);
    sun.draw();
}

function mouseDragged() {
    // Drag the sun anywhere on the canvas
    if (dist(mouseX, mouseY, sun.cx, sun.cy) <= sun._currentRadius * sun.glowScale) {
        sun.setCenter(mouseX, mouseY);
    }
}

Example 6: Time-of-Day Moon (showBall hidden)

// A "moon" — glow and sinusoid rays visible, no solid disk
const silver = new SWColor(210, 15, 92, 100, "silver");
const blueGlow = new SWColor(220, 30, 90, 100, "blueGlow");

moon = new SWSun(cx, cy, 55, silver, {
    showBall:        false,           // hide the disk — glow shines through
    showGlow:        true,
    glowColor:       blueGlow,
    glowScale:       3.0,
    glowAlpha:       50,
    rayType:         'sinusoid',
    rayCount:        20,
    rayLength:       110,
    sinAmplitude:    8,
    rayColors:       [silver]
});

Integration with SketchWave

Loading Order

SWSun must be loaded after all of its dependencies:

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

<!-- SketchWave dependencies (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/swDisk.js"></script>
<script src="../shapeClasses/swLine.js"></script>
<script src="../shapeClasses/swArrow.js"></script>
<script src="../shapeClasses/swTriangle.js"></script>
<script src="../shapeClasses/swSun.js"></script>

<!-- Your sketch -->
<script src="../sketches/yourSketch.js"></script>
Note: The "sinusoid" ray type computes its wave inline — it does not create SWSinusoid instances. However, swSinusoid.js must still be loaded before SWSun because the breathe() method accepts a SWSinusoid object.

colorMode Requirement

SWSun relies on SWColor which requires p5.js to be in HSB mode:

function setup() {
    createCanvas(width, height);
    colorMode(HSB, 360, 100, 100, 100);   // required
    initializeSWColors();                  // required (populates predefined SW colours)
    // ...
}

Animation Timer Pattern

SWSun animations take an accumulated elapsed-time parameter rather than raw millis(). This lets you pause animations cleanly:

let rotationOn   = false;
let totalRotTime = 0;
let lastRotSnap  = 0;   // millis() when rotation was last on

function draw() {
    background(220, 20, 95);

    if (rotationOn) {
        const now = millis() / 1000;
        totalRotTime += (now - lastRotSnap);
        lastRotSnap = now;
        sun.rotateRays(20, totalRotTime);
    }

    sun.draw();
}

function toggleRotation() {
    rotationOn = !rotationOn;
    if (rotationOn) {
        lastRotSnap = millis() / 1000;  // reset delta reference on resume
    }
}

Using with SWSinusoid for Breathing

Build the sinusoid in setup() and call breathe() each frame with the elapsed time:

let breatheSin;
let breatheStart = 0;

function setup() {
    // ...
    breatheSin = new SWSinusoid();
    breatheSin.adjustWaveUsingExtrema(0.75, 1.25);
    breatheSin.setPeriod(3);
}

function draw() {
    background(220, 20, 95);
    const elapsed = (millis() / 1000) - breatheStart;
    sun.breathe(breatheSin, elapsed);
    sun.draw();
}

// Pause/resume breathing by saving elapsed before pause
// and subtracting pause duration from breatheStart on resume.

Source Code

Show / Hide Source Code
/*
File:    swSun.js
Date:    2026-05-18
Author:  klp
Workspace: SketchWaveTNT2026-05-01-Stg9
Purpose: SWSun class — a styled sun (or moon) with a central SWDisk ball,
         optional radiating shapes, a soft multi-layer glow, and support
         for breathing, rotation, and sinusoid ripple animations.

Dependencies: p5.js, swColor.js, swPoint.js, swDisk.js,
              swLine.js, swArrow.js, swTriangle.js
*/

console.log("[swSun.js] SWSun class loaded.");

class SWSun {

    constructor(cx, cy, ballRadius, ballFillColor, options = {}) {
        this.cx = cx;
        this.cy = cy;
        this.ballRadius         = ballRadius;
        this.originalBallRadius = ballRadius;

        // ── Ball styling ──────────────────────────────────────────────────
        this.ballFillColor   = ballFillColor           ? SWColor.copy(ballFillColor)           : undefined;
        this.ballStrokeColor = options.ballStrokeColor ? SWColor.copy(options.ballStrokeColor) : undefined;
        this.ballThickness   = options.ballThickness  ?? 2;

        // ── Ball visibility ────────────────────────────────────────────────
        this.showBall   = options.showBall   ?? true;

        // ── Glow ──────────────────────────────────────────────────────────
        this.showGlow   = options.showGlow   ?? true;
        this.glowColor  = options.glowColor
                          ? SWColor.copy(options.glowColor)
                          : (ballFillColor ? SWColor.copy(ballFillColor) : undefined);
        this.glowScale  = options.glowScale  ?? 2.5;
        this.glowLayers = options.glowLayers ?? 5;
        this.glowAlpha  = options.glowAlpha  ?? 35;

        // ── Ray styling ───────────────────────────────────────────────────
        this.showRays       = options.showRays      ?? true;
        this.rayType        = options.rayType       ?? "sinusoid";
        this.rayCount       = options.rayCount      ?? 8;
        this.rayLength      = options.rayLength     ?? ballRadius * 1.5;
        this.originalRayLength = this.rayLength;
        this.rayThickness   = options.rayThickness  ?? 3;

        if (options.rayColors && options.rayColors.length > 0) {
            this.rayColors = options.rayColors.slice(0, 5).map(c => c ? SWColor.copy(c) : undefined);
        } else {
            this.rayColors = [ballFillColor ? SWColor.copy(ballFillColor) : undefined];
        }

        // ── Sinusoid ray options ──────────────────────────────────────────
        this.sinAmplitude  = options.sinAmplitude  ?? ballRadius * 0.25;
        this.sinPeriods   = options.sinPeriods   ?? 2.5;
        this.sinPhase     = 0;
        this.rippleOutward = options.rippleOutward ?? true;   // true = waves travel away from sun

        // ── Triangle ray options ──────────────────────────────────────────
        this.triWidth = options.triWidth ?? ballRadius * 0.5;

        // ── Arrow ray options ─────────────────────────────────────────────
        this.arrowTipAngle  = options.arrowTipAngle  ?? 25;
        this.arrowTipFactor = options.arrowTipFactor ?? 0.35;

        // ── Internal animation state ──────────────────────────────────────
        this._rotationAngle    = 0;
        this._currentRadius    = ballRadius;
        this._currentRayLength = this.rayLength;

        this._buildDisk();
    }//end constructor

    _buildDisk() {
        const center = new SWPoint(this.cx, this.cy, undefined, 0, this.ballStrokeColor);
        this.disk = new SWDisk(
            center, this._currentRadius,
            this.ballThickness, this.ballFillColor, this.ballStrokeColor
        );
        this.disk.shouldShowCenter = false;
    }

    draw() {
        this._drawAtPosition(this.cx, this.cy, this._currentRadius, this._currentRayLength);
    }

    drawOnGrid(grid) {
        const { x: sx, y: sy } = grid.userToScreen(this.cx, this.cy);
        const sr  = this._currentRadius    * grid.xScale;
        const srl = this._currentRayLength * grid.xScale;
        this._drawAtPosition(sx, sy, sr, srl);
    }

    _drawAtPosition(cx, cy, ballR, rayLen) {
        if (this.showGlow) this._drawGlow(cx, cy, ballR);
        if (this.showRays && this.rayCount > 0 && rayLen > 0) this._drawRays(cx, cy, ballR, rayLen);
        this.disk.center.x = cx;
        this.disk.center.y = cy;
        this.disk.radius   = ballR;
        if (this.showBall) {
            fill(0, 0, 0, 0);
            this.disk.draw();
        }
    }

    _drawGlow(cx, cy, ballR) {
        const gc = this.glowColor;
        if (!gc) return;
        push();
        noStroke();
        for (let i = 0; i < this.glowLayers; i++) {
            const t = (this.glowLayers > 1) ? i / (this.glowLayers - 1) : 1;
            const r = ballR * lerp(this.glowScale, 1.08, t);
            const a = lerp(0, this.glowAlpha, t);
            if (a > 0.4) { fill(gc.h, gc.s, gc.b, a); ellipse(cx, cy, r * 2, r * 2); }
        }
        pop();
    }

    _drawRays(cx, cy, ballR, rayLen) {
        const angleStep = 360 / this.rayCount;
        for (let i = 0; i < this.rayCount; i++) {
            const angleDeg = i * angleStep + this._rotationAngle;
            const col      = this.rayColors[i % this.rayColors.length];
            this._drawSingleRay(cx, cy, ballR, rayLen, angleDeg, col);
        }
    }

    _drawSingleRay(cx, cy, ballR, rayLen, angleDeg, swColor) {
        switch (this.rayType) {
            case "line":     this._drawLineRay    (cx, cy, ballR, rayLen, angleDeg, swColor); break;
            case "arrow":    this._drawArrowRay   (cx, cy, ballR, rayLen, angleDeg, swColor); break;
            case "triangle": this._drawTriangleRay(cx, cy, ballR, rayLen, angleDeg, swColor); break;
            case "sinusoid":
            default:         this._drawSinusoidRay(cx, cy, ballR, rayLen, angleDeg, swColor); break;
        }
    }

    _drawLineRay(cx, cy, ballR, rayLen, angleDeg, swColor) {
        const ar  = radians(angleDeg);
        const gap = ballR + this.ballThickness * 0.5;
        push();
        if (swColor && swColor.col) stroke(swColor.col); else noStroke();
        strokeWeight(this.rayThickness);
        strokeCap(ROUND);
        noFill();
        line(cx + cos(ar)*gap, cy + sin(ar)*gap,
             cx + cos(ar)*(gap+rayLen), cy + sin(ar)*(gap+rayLen));
        pop();
    }

    _drawArrowRay(cx, cy, ballR, rayLen, angleDeg, swColor) {
        const ar  = radians(angleDeg);
        const gap = ballR + this.ballThickness * 0.5;
        const ptA = new SWPoint(cx+cos(ar)*gap,         cy+sin(ar)*gap,         undefined, 0, swColor);
        const ptB = new SWPoint(cx+cos(ar)*(gap+rayLen),cy+sin(ar)*(gap+rayLen),undefined, 0, swColor);
        push();
        const arrow = new SWArrow(ptA, ptB, this.rayThickness, swColor,
                                  this.arrowTipAngle, this.arrowTipFactor);
        arrow.shouldShowMidpoint  = false;
        arrow.shouldShowTailPoint = false;
        arrow.draw();
        pop();
    }

    _drawTriangleRay(cx, cy, ballR, rayLen, angleDeg, swColor) {
        const ar    = radians(angleDeg);
        const perp  = ar + HALF_PI;
        const gap   = ballR + this.ballThickness * 0.5;
        const halfW = this.triWidth / 2;
        const tipX  = cx + cos(ar) * (gap + rayLen);
        const tipY  = cy + sin(ar) * (gap + rayLen);
        const baseX = cx + cos(ar) * gap;
        const baseY = cy + sin(ar) * gap;
        const ptTip   = new SWPoint(tipX, tipY);
        const ptLeft  = new SWPoint(baseX + cos(perp)*halfW, baseY + sin(perp)*halfW);
        const ptRight = new SWPoint(baseX - cos(perp)*halfW, baseY - sin(perp)*halfW);
        push();
        const fillC = swColor || new SWColor(55, 80, 100, 100, "rayFill");
        const tri   = new SWTriangle(ptTip, ptLeft, ptRight, fillC,
                                     { strokeColor:fillC, strokeWeight:1,
                                       showVertices:false, showCentroid:false });
        tri.draw();
        pop();
    }

    _drawSinusoidRay(cx, cy, ballR, rayLen, angleDeg, swColor) {
        const ar    = radians(angleDeg);
        const perp  = ar + HALF_PI;
        const gap   = ballR + this.ballThickness * 0.5;
        const nPts  = 40;
        const bFreq = (TWO_PI * this.sinPeriods) / rayLen;
        push();
        if (swColor && swColor.col) stroke(swColor.col); else noStroke();
        strokeWeight(this.rayThickness);
        strokeCap(ROUND);
        strokeJoin(ROUND);
        noFill();
        beginShape();
        for (let i = 0; i <= nPts; i++) {
            const t      = i / nPts;
            const d      = gap + t * rayLen;
            const taper  = sin(t * PI);
            const phaseSign = this.rippleOutward ? -1 : 1;
            const offset = sin(bFreq * t * rayLen + phaseSign * this.sinPhase) * this.sinAmplitude * taper;
            vertex(cx + cos(ar)*d + cos(perp)*offset,
                   cy + sin(ar)*d + sin(perp)*offset);
        }
        endShape();
        pop();
    }

    breathe(sinusoid, t) {
        const scale            = sinusoid.getValue(t);
        this._currentRadius    = this.originalBallRadius * scale;
        this._currentRayLength = this.originalRayLength  * scale;
        this.disk.radius       = this._currentRadius;
    }

    rotateRays(degPerSec, t)      { this._rotationAngle = degPerSec * t; }
    animateSinusoid(phaseDegPerSec, t) { this.sinPhase = radians(phaseDegPerSec) * t; }

    transform({ sinusoid=null, t=0, degPerSec=null, phaseDegPerSec=null } = {}) {
        if (sinusoid       !== null) this.breathe(sinusoid, t);
        if (degPerSec      !== null) this.rotateRays(degPerSec, t);
        if (phaseDegPerSec !== null) this.animateSinusoid(phaseDegPerSec, t);
    }

    reset() {
        this._currentRadius    = this.originalBallRadius;
        this._currentRayLength = this.originalRayLength;
        this._rotationAngle    = 0;
        this.sinPhase          = 0;
        this.disk.radius       = this.originalBallRadius;
    }

    setCenter(cx, cy)     { this.cx = cx; this.cy = cy; }
    setBallRadius(r)      {
        this.ballRadius = this.originalBallRadius = this._currentRadius = r;
        this.disk.setRadius(r);
    }
    setBallFillColor(swColor)   { this.ballFillColor   = swColor ? SWColor.copy(swColor) : undefined; this.disk.setFillColor(swColor);   }
    setBallStrokeColor(swColor) { this.ballStrokeColor = swColor ? SWColor.copy(swColor) : undefined; this.disk.setStrokeColor(swColor); }
    setGlowColor(swColor)  { this.glowColor  = swColor ? SWColor.copy(swColor) : undefined; }
    setGlowAlpha(a)        { this.glowAlpha  = constrain(a, 0, 100); }
    setGlowScale(s)        { this.glowScale  = max(1.05, s); }
    setRayType(type)       { this.rayType    = type; }
    setRayCount(count)     { this.rayCount   = max(0, Math.round(count)); }
    setRayLength(len)      {
        this.rayLength = this.originalRayLength = this._currentRayLength = len;
    }
    setRayThickness(w)     { this.rayThickness  = max(1, w); }
    setRayColors(arr)      { if (arr && arr.length) this.rayColors = arr.slice(0,5).map(c => c ? SWColor.copy(c) : undefined); }
    setSinAmplitude(a)     { this.sinAmplitude  = max(0, a); }
    setSinPeriods(p)       { this.sinPeriods    = max(0.25, p); }
    setRippleOutward(v)    { this.rippleOutward = !!v; }
    setTriWidth(w)         { this.triWidth      = max(1, w); }

    toString() {
        return `SWSun(cx:${this.cx.toFixed(1)}, cy:${this.cy.toFixed(1)}, ` +
               `r:${this.ballRadius.toFixed(1)}, rays:${this.rayCount}×${this.rayType})`;
    }

}//end class SWSun