🌈 SWRainbow Reference

Concentric multi-colored arcs — a ROYGBIV rainbow shape with per-arc colors, breathing, spinning, and click-to-recolor interaction

🎮 Try the Demo 📶 SWWifi (sibling class)

📋 Quick Reference Card

Category Item Default Notes
Constructor params centerSWPoint(0,0)Anchor point in user/grid coords
numArcs71–10+ arcs
innerRadius1.5Radius of innermost arc (user units)
arcSpacing1.0Radial gap between arcs (user units)
dotRadius0.3Center dot radius; 0 = no dot
theta180Arc span in degrees
direction90°CCW from +x; 90 = up
rotation0Static extra rotation offset (°)
thickness8Stroke weight in pixels
fillOpacity100Alpha 0–100 for all arcs and dot
colorsROYGBIVSWColor[] one per arc; wraps/extends automatically
Key methods drawOnGrid(grid)Draw in grid/user space (preferred)
breatheThickness(sin,t)Oscillate stroke weight via SWSinusoid
rotateAboutCenter(dps,t)Spin at dps degrees/second
handleMousePressed(mx,my,grid)Click arc → returns arc index or -1
Color management setArcColor(i,color)Change one arc's SWColor
setAllColors(arr)Replace entire palette
resetColors()Restore ROYGBIV defaults
Static SWRainbow.DEFAULT_PALETTE7×[h,s,b]Read-only ROYGBIV palette

🔍 Overview

SWRainbow renders a set of concentric arcs — each a different color — all centered on the same anchor point. The default palette is ROYGBIV (7 arcs), giving a classic rainbow arch. Each arc independently carries an SWColor object so colors can be changed at will. The shape supports breathing (sinusoidal thickness oscillation), spinning (constant rotation), and click-based arc selection so individual arcs can be recolored interactively.

SWRainbow is a Core class — it does not extend any other shape. Its geometry (arc radii, angle math) is identical to SWWifi, but the two classes are siblings, not parent and child. See the OOP Design Decisions section for an in-depth explanation of why.

Layout

Arc i (0 = innermost) has radius:

radius[i] = innerRadius + i × arcSpacing

Each arc spans theta degrees, symmetrically centered on direction. The angle convention matches all other SketchWaveJS classes: CCW from the +x axis, y increases upward.

🧠 OOP Design Decisions

This section explains a real OOP design choice made while building the SketchWaveJS library. Understanding why SWRainbow does not inherit from SWWifi is just as important as knowing how to use the class.

⚠️ The Refused Bequest Anti-Pattern

Both SWRainbow and SWWifi draw the exact same geometry: a set of concentric arcs with the same radius formula, same angle math, and the same center dot. The first instinct is to write:

class SWRainbow extends SWWifi { ... }   // ← TEMPTING but WRONG

This is a classic OOP anti-pattern called "refused bequest". The term was coined by software engineers Kent Beck and Martin Fowler in the book Refactoring. It describes a subclass that inherits a large estate of properties and methods from its parent but throws most of them away — like a child who inherits a mansion but refuses to live in it.

When a subclass overrides most of the parent's behavior, it signals that inheritance was the wrong tool. The child class does not truly "specialize" the parent — it competes with it.

🧳 What SWRainbow Would Inherit (and Not Want)

If SWRainbow extended SWWifi, it would inherit these properties — none of which make sense for a rainbow:

Property/MethodSWWifi PurposeUseful to SWRainbow?
arcColorSingle color for all arcs✘ No — needs one color per arc
activeCountHow many arcs are "lit" (signal strength)✘ No — all arcs are always lit
dimFactorBrightness of inactive arcs✘ No — no inactive arcs
inactiveOpacityTransparency of inactive arcs✘ No — no inactive arcs
pulseIndexWhich arc is currently "pulsing"✘ No — no signal-pulse concept
_drawShape()Draws with one color + dim logic✘ Must be entirely replaced

SWRainbow would inherit 6 things it doesn't need and override the one method that does all the drawing. That means zero benefit from inheriting — only confusion.

🔍 "is-a" vs "looks-like" — The Golden Rule

Inheritance should only be used when the subclass truly is a more specific version of the parent — the "is-a" relationship:

ClaimValid?Reason
SWSquare is-a SWRectangle ✔ YES A square IS a rectangle with equal sides. All rectangle behavior applies.
SWDisk is-a SWEllipse ✔ YES A disk IS an ellipse with equal axes. All ellipse behavior applies.
SWRainbow is-a SWWifi ✘ NO A rainbow looks like the WiFi geometry but does not share WiFi's identity or behavior.

The key question is: "If I use an SWRainbow wherever SWWifi is expected, will everything work correctly?" (This is called the Liskov Substitution Principle.) The answer is NO — because SWRainbow has no activeCount or dimFactor, any SWWifi-based code that calls those would break.

The rule: If a class looks like another class but wouldn't work as a substitute for it, use composition or a sibling class instead of inheritance.

✅ The Solution: Sibling Core Classes

SWRainbow and SWWifi are sibling classes — both Core classes in the SketchWaveJS library. They share the same geometric ideas but are completely independent. This means:

  • Neither class carries the other's "baggage".
  • Each class can evolve independently without breaking the other.
  • A student reading SWRainbow sees only rainbow concepts.
  • A student reading SWWifi sees only signal-strength concepts.

The shared geometry (radius formula, angle math) is simply duplicated in both classes — a small cost that buys a much cleaner design. This is sometimes called "don't reuse through inheritance when you can reuse through understanding".

Bottom line: When two classes share geometry but not identity or behavior, make them siblings — not parent and child.

🏗️ Constructor

new SWRainbow({ ... })

All parameters are passed as a single destructured object. Every parameter has a default value, so new SWRainbow() is valid.

ParameterTypeDefaultDescription
centerSWPointSWPoint(0,0)Anchor point in user/grid coordinates
numArcsnumber7Number of concentric arcs
innerRadiusnumber1.5Radius of the innermost arc in user units
arcSpacingnumber1.0Radial gap between successive arcs in user units
dotRadiusnumber0.3Radius of filled center dot (0 = no dot)
thetanumber180Arc span in degrees (180 = half-circle rainbow)
directionnumber90°CCW from +x axis; 90 = pointing up
rotationnumber0Static additional rotation offset in degrees
thicknessnumber8Arc stroke weight in screen pixels
fillOpacitynumber100Alpha 0–100 for all arcs and center dot
showCenterbooleanfalseDraw the anchor crosshair handle
colorsSWColor[]ROYGBIVPer-arc colors; shorter arrays wrap from palette
Example
// Default 7-arc ROYGBIV rainbow, pointing up
let rb = new SWRainbow();

// Custom 5-arc rainbow, pointing right, thicker arcs
let rb2 = new SWRainbow({
    center:      new SWPoint(2, 1),
    numArcs:     5,
    innerRadius: 1.2,
    arcSpacing:  0.9,
    theta:       200,
    direction:   0,    // pointing right
    thickness:   12,
});

📌 Properties

colorsSWColor[]

Array of SWColor objects, one per arc (index 0 = innermost). Wraps from SWRainbow.DEFAULT_PALETTE when the array is too short. Read/write directly or via setArcColor().

numArcsnumber

Number of arcs to draw. Can be updated at runtime; if the colors array is shorter than numArcs, the palette wraps automatically.

selectedArcnumber (getter)

Read-only. Index of the most recently clicked arc (0 = innermost), or -1 if no arc has been hit. Set automatically by handleMousePressed().

maxRadiusnumber (getter)

Computed radius of the outermost arc in user units. Equals innerRadius + (numArcs - 1) × arcSpacing.

showCenterboolean

When true, draws a small crosshair handle at the anchor point. Useful for positioning the shape interactively.

SWRainbow.DEFAULT_PALETTEstatic getter → number[][] (read-only)

The 7-entry ROYGBIV palette as an array of [h, s, b] triplets (HSB 360/100/100 space). Used internally and available for inspection.

#NameHSBSwatch
0Red0100100 
1Orange30100100 
2Yellow60100100 
3Green12010070 
4Blue210100100 
5Violet30010070 
6Red-Violet33010080 

⚙️ Methods

Drawing

drawOnGrid(grid)

Preferred drawing method. Maps center.x / center.y through the SWGrid's user-to-screen transform. Arc radii and thickness are automatically scaled to screen pixels.

Parameters:

  • grid (SWGrid) — the active grid
rainbow.drawOnGrid(grid);
draw()

Raw drawing in screen pixels. Use drawOnGrid() for grid-based sketches.

Color Management

setArcColor(i, color)

Change the color of a single arc.

  • i (number) — arc index, 0 = innermost
  • color (SWColor) — new color; a deep copy is stored
let purple = new SWColor(280, 100, 90, 100, "purple");
rainbow.setArcColor(2, purple);   // change arc 2 to purple
setAllColors(newColors)

Replace all arc colors at once. If the array has fewer entries than numArcs, remaining arcs wrap from the ROYGBIV palette.

let blues = [
    new SWColor(200, 100, 100, 100),
    new SWColor(210, 100, 90,  100),
    new SWColor(220, 100, 80,  100),
];
rainbow.setAllColors(blues);
resetColors()

Restore all arc colors to ROYGBIV defaults. Also updates the internal original-color snapshot so a subsequent reset() will return to ROYGBIV.

Animation

breatheThickness(sinusoid, t)

Oscillate the arc stroke weight using an SWSinusoid. Call each frame while breathing is active.

  • sinusoid (SWSinusoid) — e.g., new SWSinusoid(amp, freq, mid, phase)
  • t (number) — elapsed seconds since breathing started
// In setup():
//   amp = (maxThick - minThick) / 2
//   freq = 2π / period
//   mid = (maxThick + minThick) / 2
let sin = new SWSinusoid(6.5, (2 * Math.PI) / 2, 9.5, 0);

// In draw():
rainbow.breatheThickness(sin, t);
rotateAboutCenter(degPerSec, t)

Rotate the entire rainbow group at a constant angular speed.

  • degPerSec (number) — positive = CCW; negative = CW
  • t (number) — elapsed seconds since spin started
rainbow.rotateAboutCenter(20, t);   // 20 °/s CCW
rainbow.rotateAboutCenter(-45, t);  // 45 °/s CW

Interaction

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

Hit-test a mouse click against all arc bands. Sets selectedArc and returns the arc index, or -1 if the click misses all arcs.

  • mx, my — p5 mouseX, mouseY (screen pixels)
  • grid — the active SWGrid
  • tolerance (optional, default 4) — extra pixel margin for easier clicking
// In p5 mousePressed():
function mousePressed() {
    let idx = rainbow.handleMousePressed(mouseX, mouseY, grid, 6);
    if (idx >= 0) {
        console.log("Clicked arc", idx);
        // open color picker, update UI, etc.
    }
}

Reset

reset()

Restore all parameters (geometry, colors, opacity, animation state) to the values supplied at construction time. Does not re-run the constructor — it uses a snapshot taken during new SWRainbow().

Computed Helpers

arcRadius(i)

Returns the radius of arc i in user units: innerRadius + i × arcSpacing.

📝 Usage Examples

1. Basic rainbow on a grid

function setup() {
    createCanvas(450, 450);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();
    grid    = new SWGrid({ UL: new SWPoint(-10,10), LR: new SWPoint(10,-10) });
    rainbow = new SWRainbow();
}

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

2. Custom colors at construction

let blues = [
    new SWColor(200, 80, 100, 100, "sky"),
    new SWColor(210, 90,  90, 100, "cerulean"),
    new SWColor(220, 100, 80, 100, "cobalt"),
];
let rb = new SWRainbow({ numArcs: 3, colors: blues });

3. Click to recolor

function mousePressed() {
    let idx = rainbow.handleMousePressed(mouseX, mouseY, grid, 6);
    if (idx >= 0) {
        // Change the clicked arc to a random hue
        let h   = random(360);
        let col = new SWColor(h, 100, 100, 100, "custom");
        rainbow.setArcColor(idx, col);
    }
}

4. Breathing thickness

let breatheSin;
let breatheStart;
let breathingOn = false;

function setup() {
    // ...
    breatheSin = new SWSinusoid(5, (2*Math.PI)/2, 8, 0);
}

function draw() {
    // ...
    if (breathingOn) {
        rainbow.breatheThickness(breatheSin, (millis()/1000) - breatheStart);
    } else {
        rainbow._thickness = rainbow.thickness;
    }
    rainbow.drawOnGrid(grid);
}

function keyPressed() {
    if (key === 'b') {
        breathingOn  = !breathingOn;
        breatheStart  = millis() / 1000;
    }
}

5. Spinning

let spinStart = 0;
let spinOn    = false;

function draw() {
    if (spinOn) {
        rainbow.rotateAboutCenter(20, (millis()/1000) - spinStart);
    } else {
        rainbow._animRotDeg = 0;
    }
    rainbow.drawOnGrid(grid);
}

function keyPressed() {
    if (key === 's') {
        spinOn    = !spinOn;
        spinStart = millis() / 1000;
    }
}

6. Dynamically adding more arcs

// When slider changes numArcs:
rainbow.numArcs = 9;
// Colors array auto-extends from ROYGBIV palette on next draw.
// Or manually extend:
while (rainbow.colors.length < rainbow.numArcs) {
    const palette = SWRainbow.DEFAULT_PALETTE;
    const [h, s, b] = palette[rainbow.colors.length % palette.length];
    rainbow.colors.push(new SWColor(h, s, b, rainbow.fillOpacity));
}

7. Restoring defaults

// Full factory reset (all parameters):
rainbow.reset();

// Reset colors only, keep geometry changes:
rainbow.resetColors();

✅ Best Practices

  • Keep arcSpacing ≥ thickness / 2 in user units to prevent arcs from overlapping visually.
  • Use drawOnGrid() in all grid-based sketches. The raw draw() method is only useful when you manage your own pixel math.
  • Store breatheThickness results frame-by-frame — do not call buildSinusoid() inside draw(); create it once in setup().
  • Call updateSelectedArcLabel() (or equivalent UI update) immediately after handleMousePressed() so the control panel reflects the new selection.
  • When converting a hex color picker value to SWColor, use SWColor.fromHex(hexStr, opacity, name).
  • After calling reset(), sync all control panel sliders back to factory values so the UI matches the shape state.

🔗 Integration

Script load order

<!-- p5.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.min.js"></script>

<!-- SketchWaveJS dependencies (in 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/swRainbow.js"></script>

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

p5.js colorMode requirement

SWRainbow relies on colorMode(HSB, 360, 100, 100, 100) being set at the top of setup(). Without it, all colors will render incorrectly.

📂 Source Code

📄 Show / Hide swRainbow.js Source
Loading source...