🌈 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 | center | SWPoint(0,0) | Anchor point in user/grid coords |
numArcs | 7 | 1–10+ arcs | |
innerRadius | 1.5 | Radius of innermost arc (user units) | |
arcSpacing | 1.0 | Radial gap between arcs (user units) | |
dotRadius | 0.3 | Center dot radius; 0 = no dot | |
theta | 180 | Arc span in degrees | |
direction | 90 | °CCW from +x; 90 = up | |
rotation | 0 | Static extra rotation offset (°) | |
thickness | 8 | Stroke weight in pixels | |
fillOpacity | 100 | Alpha 0–100 for all arcs and dot | |
colors | ROYGBIV | SWColor[] 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_PALETTE | 7×[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:
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/Method | SWWifi Purpose | Useful to SWRainbow? |
|---|---|---|
arcColor | Single color for all arcs | ✘ No — needs one color per arc |
activeCount | How many arcs are "lit" (signal strength) | ✘ No — all arcs are always lit |
dimFactor | Brightness of inactive arcs | ✘ No — no inactive arcs |
inactiveOpacity | Transparency of inactive arcs | ✘ No — no inactive arcs |
pulseIndex | Which 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:
| Claim | Valid? | 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
SWRainbowsees only rainbow concepts. - A student reading
SWWifisees 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.
| Parameter | Type | Default | Description |
|---|---|---|---|
center | SWPoint | SWPoint(0,0) | Anchor point in user/grid coordinates |
numArcs | number | 7 | Number of concentric arcs |
innerRadius | number | 1.5 | Radius of the innermost arc in user units |
arcSpacing | number | 1.0 | Radial gap between successive arcs in user units |
dotRadius | number | 0.3 | Radius of filled center dot (0 = no dot) |
theta | number | 180 | Arc span in degrees (180 = half-circle rainbow) |
direction | number | 90 | °CCW from +x axis; 90 = pointing up |
rotation | number | 0 | Static additional rotation offset in degrees |
thickness | number | 8 | Arc stroke weight in screen pixels |
fillOpacity | number | 100 | Alpha 0–100 for all arcs and center dot |
showCenter | boolean | false | Draw the anchor crosshair handle |
colors | SWColor[] | ROYGBIV | Per-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
colors — SWColor[]
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().
numArcs — number
Number of arcs to draw. Can be updated at runtime; if the colors array is shorter than numArcs, the palette wraps automatically.
selectedArc — number (getter)
Read-only. Index of the most recently clicked arc (0 = innermost), or -1 if no arc has been hit. Set automatically by handleMousePressed().
maxRadius — number (getter)
Computed radius of the outermost arc in user units. Equals innerRadius + (numArcs - 1) × arcSpacing.
showCenter — boolean
When true, draws a small crosshair handle at the anchor point. Useful for positioning the shape interactively.
SWRainbow.DEFAULT_PALETTE — static 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.
| # | Name | H | S | B | Swatch |
|---|---|---|---|---|---|
| 0 | Red | 0 | 100 | 100 | |
| 1 | Orange | 30 | 100 | 100 | |
| 2 | Yellow | 60 | 100 | 100 | |
| 3 | Green | 120 | 100 | 70 | |
| 4 | Blue | 210 | 100 | 100 | |
| 5 | Violet | 300 | 100 | 70 | |
| 6 | Red-Violet | 330 | 100 | 80 |
⚙️ 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 = innermostcolor(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 = CWt(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— p5mouseX,mouseY(screen pixels)grid— the active SWGridtolerance(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 unitsto prevent arcs from overlapping visually. - Use
drawOnGrid()in all grid-based sketches. The rawdraw()method is only useful when you manage your own pixel math. - Store
breatheThicknessresults frame-by-frame — do not callbuildSinusoid()insidedraw(); create it once insetup(). - Call
updateSelectedArcLabel()(or equivalent UI update) immediately afterhandleMousePressed()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...