● SWTriDisk Reference

A collinear three-disk composite for SketchWaveJS — center disk flanked by light and shadow side disks along a rotatable axis.

SWTriDisk Demo 2 Updated:

⚡ Quick Reference

Construct

new SWTriDisk(center, centerRadius, k, offset, axisAngle, centerColor, leftColor, rightColor, thickness)

Draw each frame

triDisk.rotate(spinSpeed * deltaT); // optional triDisk.drawOnGrid(grid);

Geometry

setRadius(r) setK(k) setOffset(offset) setAxisAngle(angle) setThickness(w)

Colors

setCenterColor(swColor) setLeftColor(swColor) setRightColor(swColor) setCenterAlpha(a) setLeftAlpha(a) setRightAlpha(a)

Utility

reset() SWTriDisk.copy(other) toString() setShowCenter(bool)

Overview

SWTriDisk is a composite shape made of three collinear SWDisk objects. The center disk sits at the declared center point and is drawn last (on top). The left disk and right disk are offset in opposite directions along a rotatable axis, creating a three-sphere illusion when colored as light and shadow.

Composition Model
Sub-object Role Color parameter Drawn
centerDisk Main subject; shows center-point marker centerColor 3rd (top)
leftDisk Light side; offset in −axis direction leftColor 2nd
rightDisk Shadow side; offset in +axis direction rightColor 1st (back)
Geometry

All three disk centers lie on a common axis defined by axisAngle (degrees CCW from +x axis; 0° = right, 90° = up).

leftDisk center = center − offset × (cos(axisAngle), sin(axisAngle)) rightDisk center = center + offset × (cos(axisAngle), sin(axisAngle)) sideRadius = k × centerRadius // k ∈ [0, 1] Drawing order: rightDisk → leftDisk → centerDisk (back to front)
Color Model

Each disk owns one SWColor applied to both fill and stroke. Use the color setter methods to update. Alpha can be changed independently via the alpha setters without replacing the color.

Typical Workflow
  1. Create the color objects (SWColor.fromHex()).
  2. Instantiate: new SWTriDisk(center, r, k, offset, angle, cColor, lColor, rColor, thickness)
  3. In draw(): call rotate(speed * deltaT), then drawOnGrid(grid).
  4. Handle UI events with the setter methods.
  5. Call reset() to restore all original values.

Constructor

new SWTriDisk(center, centerRadius, k, offset, axisAngle, centerColor, leftColor, rightColor, thickness)
ParameterTypeDefaultDescription
centerSWPointCenter of the center disk (user coords)
centerRadiusnumberRadius of the center disk (user units, > 0)
knumber0.9Side radius factor: sideRadius = k × centerRadius, k ∈ [0, 1]
offsetnumber5Distance from center to each side disk center (user units)
axisAnglenumber0Axis orientation in degrees, CCW from +x axis (0 = right)
centerColorSWColorundefinedFill + stroke color for the center disk
leftColorSWColorundefinedFill + stroke color for the left disk
rightColorSWColorundefinedFill + stroke color for the right disk
thicknessnumber2Stroke weight in pixels for all three disks

Constructor examples

// Minimal: center and radius only let td = new SWTriDisk(new SWPoint(0, 0), 5); // With all colors specified let cColor = SWColor.fromHex('#2d8a4e', 'cFill').setAlphaTo(100); let lColor = SWColor.fromHex('#ffffff', 'lFill').setAlphaTo(100); let rColor = SWColor.fromHex('#111111', 'rFill').setAlphaTo(100); let td2 = new SWTriDisk(new SWPoint(0, 0), 5, 0.9, 5, 0, cColor, lColor, rColor, 2); // Styled center-point marker (SWPoint with size + color) const cPt = new SWPoint(0, 0, undefined, 8, swDarkGray); let td3 = new SWTriDisk(cPt, 5, 0.9, 5, 0, cColor, lColor, rColor, 2);

Properties

center  SWPoint Center of the center disk in user coordinates. Move the entire tri-disk by changing center.x / center.y.
centerRadius  number Radius of the center disk in user units. Prefer setRadius(r) to keep sub-objects in sync.
sideRadius  getter (read-only) Computed side disk radius: centerRadius × k.
k  number Side radius factor, k ∈ [0, 1]. Prefer setK(k) to recompute side radii immediately.
offset  number Distance (user units) from center to each side disk center along the axis. Prefer setOffset(offset).
axisAngle  number Current axis orientation in degrees, CCW from +x, normalized to [0, 360). Modified each frame by rotate().
thickness  number Stroke weight in pixels for all three disks. Prefer setThickness(w).
centerColor / leftColor / rightColor  SWColor | undefined Color objects for each disk. Prefer the color setter methods to update and keep sub-objects in sync.
shouldShowCenter  boolean Whether the center-point dot is shown on the center disk. Prefer setShowCenter(bool).
centerDisk / leftDisk / rightDisk  SWDisk Direct references to the three SWDisk sub-objects. Read-only in normal use; rebuilt by reset().
originalCenterRadius, originalK, originalOffset, originalAxisAngle, originalThickness, originalCenterColor, originalLeftColor, originalRightColor  various Snapshot of constructor values used by reset(). Do not modify directly.

Methods

draw()

Draws all three disks in screen (pixel) coordinates. Use drawOnGrid() for user-coordinate rendering.

Drawing order: rightDisk → leftDisk → centerDisk (back to front).

drawOnGrid(grid)

Draws all three disks mapped through the given SWGrid.

ParameterTypeDescription
gridSWGridThe coordinate grid for user-to-pixel mapping
rotate(deltaAngle)

Advances axisAngle by deltaAngle degrees (CCW positive, CW negative). Result is normalized to [0, 360). Call once per frame before drawOnGrid().

ParameterTypeDescription
deltaAnglenumberDegrees to advance the axis angle this frame
// Frame-rate-independent spin triDisk.rotate(spinSpeed * deltaT); triDisk.drawOnGrid(grid);
setRadius(r)

Sets the center disk radius and recomputes both side disk radii (sideRadius = k × r).

setK(k)

Sets the side-radius factor and immediately updates both side disk radii.

setOffset(offset)

Sets the offset between center and side disk centers (user units). New positions take effect on the next draw call.

setAxisAngle(angle)

Sets the axis angle (degrees, CCW from +x axis) normalized to [0, 360). Useful for snapping the axis to a fixed angle without animation.

setThickness(w)

Sets the stroke weight in pixels on all three disks.

setCenterColor(swColor)  /  setLeftColor(swColor)  /  setRightColor(swColor)

Sets both the fill and stroke color of the respective disk. Copies the passed SWColor to avoid shared-reference side effects.

triDisk.setCenterColor(SWColor.fromHex('#2d8a4e', 'c')); triDisk.setLeftColor(SWColor.fromHex('#ccffdd', 'l')); triDisk.setRightColor(SWColor.fromHex('#111111', 'r'));
setCenterAlpha(alpha)  /  setLeftAlpha(alpha)  /  setRightAlpha(alpha)

Sets the opacity (0–100) of the respective disk's fill and stroke without replacing its color.

triDisk.setCenterAlpha(80); // 80% opaque triDisk.setLeftAlpha(60); triDisk.setRightAlpha(40);
setShowCenter(show = true)

Shows (true) or hides (false) the center-point marker dot on the center disk. Updates both shouldShowCenter and the sub-object's flag.

reset()

Restores all geometry, thickness, and colors to the values passed to the constructor. Rebuilds the three sub-objects cleanly. Does not move the center position.

static SWTriDisk.copy(other)SWTriDisk

Returns a deep copy of other — current state and original snapshot — so the copy's reset() matches the original's.

const td2 = SWTriDisk.copy(td);
toString()string

Returns a compact debug string, e.g.: SWTriDisk(center:(0.00, 0.00), centerRadius:5, k:0.9, offset:5, axisAngle:135.0°)

Animation Guide

Elapsed-time pattern (frame-rate independent)

Always compute elapsed time (deltaT) from millis() rather than assuming a fixed frame rate.

let prevT = 0; function draw() { const t = millis() / 1000; // seconds const deltaT = (prevT > 0) ? (t - prevT) : 0; prevT = t; background(30); grid.drawAxes(); if (shouldSpin) triDisk.rotate(spinSpeed * deltaT); triDisk.drawOnGrid(grid); }
Spin control
// Typical spin-speed variable driven by a slider (degrees per second) let spinSpeed = 90; // 90°/s = one full rotation in 4 seconds function keyPressed() { if (key === 's') shouldSpin = !shouldSpin; if (key === 'q') { shouldSpin = false; } if (key === 'r') { triDisk.reset(); shouldSpin = false; } }
Axis-angle snap
// Snap to a specific angle without affecting spin state triDisk.setAxisAngle(45);

Usage Examples

Minimal tri-disk
let td = new SWTriDisk(new SWPoint(0, 0), 5); td.drawOnGrid(grid);
Fully colored, spinning tri-disk
const cColor = SWColor.fromHex('#2d8a4e', 'c').setAlphaTo(100); const lColor = SWColor.fromHex('#ccffdd', 'l').setAlphaTo(100); const rColor = SWColor.fromHex('#111111', 'r').setAlphaTo(100); const td = new SWTriDisk(new SWPoint(0, 0), 6, 0.9, 7, 0, cColor, lColor, rColor, 2); // In draw(): td.rotate(90 * deltaT); td.drawOnGrid(grid);
Color and alpha control
// Change center disk to red td.setCenterColor(SWColor.fromHex('#cc0000', 'c')); // Fade the left disk to 50% opacity without changing its color td.setLeftAlpha(50); // Restore all originals td.reset();
Loading dependencies
<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/swTriDisk.js"></script> <script src="sketches/yourSketch.js"></script>

Source Code

swTriDisk.js
swTriDisk.js — full source
/*
File: swTriDisk.js
Date: 2026-03-30
Author: klp
App:  SketchWaveTNT2026-03-19-Stg7
Purpose: SWTriDisk class for SketchWaveJS

SWTriDisk represents a composition of three collinear SWDisk objects:
  - centerDisk  : drawn on top (the main subject)
  - leftDisk    : offset in the −axis direction  (light side by default)
  - rightDisk   : offset in the +axis direction  (shadow side by default)

Geometry:
  All three disk centers lie on a common line called the axis.
  The axis is defined by axisAngle (degrees CCW from +x axis, 0 = right).

    leftDisk center  = center − offset × (cos(axisAngle), sin(axisAngle))
    rightDisk center = center + offset × (cos(axisAngle), sin(axisAngle))
    sideRadius       = k × centerRadius,   k ∈ [0, 1]

Drawing order (back → front): rightDisk, leftDisk, centerDisk.
This ensures the center always occludes the side disks regardless of color,
producing the shadow/light crescent illusion.

Animations:
  rotate(delta)  — advance axisAngle by delta degrees (CCW positive, CW negative).
                   Call once per frame before drawOnGrid() to spin the axis.

Color model:
  Each disk has ONE SWColor applied to both fill and stroke.
  Use setCenterColor / setLeftColor / setRightColor to update.
  Alpha can be set independently with setCenterAlpha / setLeftAlpha / setRightAlpha.

Dependencies: p5.js, SWColor, SWPoint, SWGrid, SWDisk.
*/

console.log("[swTriDisk.js] SWTriDisk class loaded.");

class SWTriDisk {

    /**
     * @param {SWPoint} center        - Center of the centerDisk (user coords)
     * @param {number}  centerRadius  - Radius of the center disk (user units, > 0)
     * @param {number}  [k=0.9]       - Side disk radius = k × centerRadius, k ∈ [0, 1]
     * @param {number}  [offset=5]    - Distance from center to each side disk center (user units)
     * @param {number}  [axisAngle=0] - Axis orientation (degrees, CCW from +x axis; 0 = right)
     * @param {SWColor} [centerColor] - Fill + stroke color for the center disk
     * @param {SWColor} [leftColor]   - Fill + stroke color for the left disk
     * @param {SWColor} [rightColor]  - Fill + stroke color for the right disk
     * @param {number}  [thickness=2] - Stroke weight for all three disks (pixels)
     */
    constructor(center, centerRadius, k = 0.9, offset = 5, axisAngle = 0,
                centerColor = undefined, leftColor = undefined, rightColor = undefined,
                thickness = 2) {

        this.center       = center;
        this.centerRadius = centerRadius;
        this.k            = k;
        this.offset       = offset;
        this.axisAngle    = axisAngle;
        this.thickness    = thickness;

        // Always copy colors to avoid mutating shared SWColor objects
        this.centerColor = centerColor ? SWColor.copy(centerColor) : undefined;
        this.leftColor   = leftColor   ? SWColor.copy(leftColor)   : undefined;
        this.rightColor  = rightColor  ? SWColor.copy(rightColor)  : undefined;

        // Originals stored at construction — used by reset()
        this.originalCenterRadius = centerRadius;
        this.originalK            = k;
        this.originalOffset       = offset;
        this.originalAxisAngle    = axisAngle;
        this.originalThickness    = thickness;
        this.originalCenterColor  = centerColor ? SWColor.copy(centerColor) : undefined;
        this.originalLeftColor    = leftColor   ? SWColor.copy(leftColor)   : undefined;
        this.originalRightColor   = rightColor  ? SWColor.copy(rightColor)  : undefined;

        this.shouldShowCenter = true;

        this._buildSubObjects();
    }//end constructor

    // ─── Convenience getter ────────────────────────────────────────────────────

    /** Side disk radius (centerRadius × k) */
    get sideRadius() {
        return this.centerRadius * this.k;
    }

    // ─── Internal Construction ─────────────────────────────────────────────────

    /**
     * Creates the three SWDisk sub-objects from current property values.
     * Called once at construction and again by reset().
     */
    _buildSubObjects() {
        const cx   = this.center.x;
        const cy   = this.center.y;
        const rad  = SWTriDisk._toRadians(this.axisAngle);
        const dx   = this.offset * Math.cos(rad);
        const dy   = this.offset * Math.sin(rad);
        const sRad = this.sideRadius;

        // Center disk (with a visible center-point marker)
        const cCenter = new SWPoint(cx, cy, undefined, 8, swDarkGray);
        this.centerDisk = new SWDisk(
            cCenter, this.centerRadius, this.thickness,
            this.centerColor,
            this.centerColor ? SWColor.copy(this.centerColor) : undefined
        );
        this.centerDisk.shouldShowCenter = this.shouldShowCenter;

        // Left disk  (offset in the −axis direction)
        this.leftDisk = new SWDisk(
            new SWPoint(cx - dx, cy - dy),
            sRad, this.thickness,
            this.leftColor,
            this.leftColor ? SWColor.copy(this.leftColor) : undefined
        );
        this.leftDisk.shouldShowCenter = false;

        // Right disk (offset in the +axis direction)
        this.rightDisk = new SWDisk(
            new SWPoint(cx + dx, cy + dy),
            sRad, this.thickness,
            this.rightColor,
            this.rightColor ? SWColor.copy(this.rightColor) : undefined
        );
        this.rightDisk.shouldShowCenter = false;
    }//end _buildSubObjects

    /**
     * Pushes the current center position, axisAngle, and offset into the
     * three sub-object center coordinates.  Called before every draw.
     * Cheap: no allocations — just number assignments.
     */
    _syncSubObjects() {
        // Sync center disk position and center-marker flag
        this.centerDisk.center.x         = this.center.x;
        this.centerDisk.center.y         = this.center.y;
        this.centerDisk.shouldShowCenter = this.shouldShowCenter;

        // Recompute side disk centers from current geometry
        const rad = SWTriDisk._toRadians(this.axisAngle);
        const dx  = this.offset * Math.cos(rad);
        const dy  = this.offset * Math.sin(rad);
        this.leftDisk.center.x  = this.center.x - dx;
        this.leftDisk.center.y  = this.center.y - dy;
        this.rightDisk.center.x = this.center.x + dx;
        this.rightDisk.center.y = this.center.y + dy;
    }//end _syncSubObjects

    // ─── Private helper ────────────────────────────────────────────────────────

    /** Converts degrees to radians without depending on p5.js globals */
    static _toRadians(degrees) {
        return degrees * Math.PI / 180;
    }

    // ─── Drawing ───────────────────────────────────────────────────────────────

    /**
     * Draws the tri-disk group in screen (pixel) coordinates.
     * Use drawOnGrid() for user-coordinate rendering.
     * Drawing order: rightDisk → leftDisk → centerDisk (back to front).
     */
    draw() {
        this._syncSubObjects();
        this.rightDisk.draw();
        this.leftDisk.draw();
        this.centerDisk.draw();
    }//end draw

    /**
     * Draws the tri-disk group mapped through the given SWGrid.
     * Drawing order: rightDisk → leftDisk → centerDisk (back to front).
     * @param {SWGrid} grid
     */
    drawOnGrid(grid) {
        this._syncSubObjects();
        this.rightDisk.drawOnGrid(grid);
        this.leftDisk.drawOnGrid(grid);
        this.centerDisk.drawOnGrid(grid);
    }//end drawOnGrid

    // ─── Rotation animation ────────────────────────────────────────────────────

    /**
     * Advances axisAngle by deltaAngle degrees (CCW positive, CW negative).
     * Result is normalized to [0, 360).
     * Call once per frame before draw/drawOnGrid to spin the axis.
     * @param {number} deltaAngle
     */
    rotate(deltaAngle) {
        this.axisAngle = ((this.axisAngle + deltaAngle) % 360 + 360) % 360;
    }//end rotate

    // ─── Geometry setters ──────────────────────────────────────────────────────

    /**
     * Sets the center disk radius and recomputes both side disk radii.
     * @param {number} r - user units
     */
    setRadius(r) {
        this.centerRadius = r;
        if (this.centerDisk) this.centerDisk.setRadius(r);
        const sRad = this.sideRadius;
        if (this.leftDisk)  this.leftDisk.setRadius(sRad);
        if (this.rightDisk) this.rightDisk.setRadius(sRad);
    }//end setRadius

    /**
     * Sets k (side radius factor) and recomputes side radii.
     * @param {number} k - value in [0, 1]
     */
    setK(k) {
        this.k = k;
        const sRad = this.sideRadius;
        if (this.leftDisk)  this.leftDisk.setRadius(sRad);
        if (this.rightDisk) this.rightDisk.setRadius(sRad);
    }//end setK

    /**
     * Sets the offset between the center disk and each side disk center.
     * Side positions are recalculated on the next _syncSubObjects() call (i.e. next draw).
     * @param {number} offset - user units
     */
    setOffset(offset) {
        this.offset = offset;
    }//end setOffset

    /**
     * Sets the axis angle (degrees, CCW from +x axis; 0 = right).
     * Normalized to [0, 360). Side positions are recalculated on the next _syncSubObjects().
     * @param {number} angle
     */
    setAxisAngle(angle) {
        this.axisAngle = ((angle % 360) + 360) % 360;
    }//end setAxisAngle

    /**
     * Sets the stroke weight (pixels) for all three disks.
     * @param {number} w
     */
    setThickness(w) {
        this.thickness = w;
        if (this.centerDisk) this.centerDisk.thickness = w;
        if (this.leftDisk)   this.leftDisk.thickness   = w;
        if (this.rightDisk)  this.rightDisk.thickness  = w;
    }//end setThickness

    // ─── Color setters ─────────────────────────────────────────────────────────

    /**
     * Sets the fill AND stroke color of the center disk.
     * @param {SWColor} swColor
     */
    setCenterColor(swColor) {
        this.centerColor = swColor ? SWColor.copy(swColor) : undefined;
        if (this.centerDisk) {
            this.centerDisk.setFillColor(swColor);
            this.centerDisk.setStrokeColor(swColor ? SWColor.copy(swColor) : undefined);
        }
    }//end setCenterColor

    /**
     * Sets the fill AND stroke color of the left disk.
     * @param {SWColor} swColor
     */
    setLeftColor(swColor) {
        this.leftColor = swColor ? SWColor.copy(swColor) : undefined;
        if (this.leftDisk) {
            this.leftDisk.setFillColor(swColor);
            this.leftDisk.setStrokeColor(swColor ? SWColor.copy(swColor) : undefined);
        }
    }//end setLeftColor

    /**
     * Sets the fill AND stroke color of the right disk.
     * @param {SWColor} swColor
     */
    setRightColor(swColor) {
        this.rightColor = swColor ? SWColor.copy(swColor) : undefined;
        if (this.rightDisk) {
            this.rightDisk.setFillColor(swColor);
            this.rightDisk.setStrokeColor(swColor ? SWColor.copy(swColor) : undefined);
        }
    }//end setRightColor

    // ─── Alpha setters ─────────────────────────────────────────────────────────

    /**
     * Sets the opacity (0–100) of the center disk's fill and stroke.
     * @param {number} alpha
     */
    setCenterAlpha(alpha) {
        if (this.centerDisk) {
            this.centerDisk.setFillAlpha(alpha);
            if (this.centerDisk.strokeColor) this.centerDisk.setStrokeAlpha(alpha);
        }
    }//end setCenterAlpha

    /**
     * Sets the opacity (0–100) of the left disk's fill and stroke.
     * @param {number} alpha
     */
    setLeftAlpha(alpha) {
        if (this.leftDisk) {
            this.leftDisk.setFillAlpha(alpha);
            if (this.leftDisk.strokeColor) this.leftDisk.setStrokeAlpha(alpha);
        }
    }//end setLeftAlpha

    /**
     * Sets the opacity (0–100) of the right disk's fill and stroke.
     * @param {number} alpha
     */
    setRightAlpha(alpha) {
        if (this.rightDisk) {
            this.rightDisk.setFillAlpha(alpha);
            if (this.rightDisk.strokeColor) this.rightDisk.setStrokeAlpha(alpha);
        }
    }//end setRightAlpha

    // ─── Center marker ─────────────────────────────────────────────────────────

    /**
     * Shows or hides the center disk's center-point marker.
     * @param {boolean} [show=true]
     */
    setShowCenter(show = true) {
        this.shouldShowCenter = show;
        if (this.centerDisk) this.centerDisk.shouldShowCenter = show;
    }//end setShowCenter

    // ─── Reset ─────────────────────────────────────────────────────────────────

    /**
     * Resets all geometry, thickness, and colors to the originals stored
     * at construction time.  Does NOT move the center position.
     */
    reset() {
        this.centerRadius = this.originalCenterRadius;
        this.k            = this.originalK;
        this.offset       = this.originalOffset;
        this.axisAngle    = this.originalAxisAngle;
        this.thickness    = this.originalThickness;

        // Restore color references
        if (this.originalCenterColor) this.centerColor = SWColor.copy(this.originalCenterColor);
        if (this.originalLeftColor)   this.leftColor   = SWColor.copy(this.originalLeftColor);
        if (this.originalRightColor)  this.rightColor  = SWColor.copy(this.originalRightColor);

        // Rebuild sub-objects cleanly from restored values
        this._buildSubObjects();
    }//end reset

    // ─── Copy ──────────────────────────────────────────────────────────────────

    /**
     * Returns a deep copy of the given SWTriDisk.
     * @param {SWTriDisk} other
     * @returns {SWTriDisk}
     */
    static copy(other) {
        if (!(other instanceof SWTriDisk)) {
            console.warn("SWTriDisk.copy: argument is not an SWTriDisk");
            return null;
        }
        // Construct from originals so the copy's reset() behaves like the original's
        const d = new SWTriDisk(
            SWPoint.copy(other.center),
            other.originalCenterRadius,
            other.originalK,
            other.originalOffset,
            other.originalAxisAngle,
            other.centerColor,
            other.leftColor,
            other.rightColor,
            other.originalThickness
        );
        // Mirror current animated state
        d.centerRadius = other.centerRadius;
        d.k            = other.k;
        d.offset       = other.offset;
        d.axisAngle    = other.axisAngle;
        d.thickness    = other.thickness;
        d.setRadius(d.centerRadius);
        d.shouldShowCenter = other.shouldShowCenter;
        d._syncSubObjects();
        return d;
    }//end copy

    // ─── toString ──────────────────────────────────────────────────────────────

    toString() {
        return `SWTriDisk(center:(${this.center.x.toFixed(2)}, ${this.center.y.toFixed(2)}), ` +
               `centerRadius:${this.centerRadius}, k:${this.k}, ` +
               `offset:${this.offset}, axisAngle:${this.axisAngle.toFixed(1)}°)`;
    }//end toString

}//end SWTriDisk class