● SWTriDisk Reference

A SketchWave composite class for animated collinear three-disk groups — center disk flanked by light and shadow sides on a rotatable axis

SWTriDisk Demo 2

Quick Reference

SWTriDisk is a SketchWave composite class representing three collinear SWDisk objects sharing a common rotatable axis. The center disk is drawn on top; the left disk (light side) and right disk (shadow side) are offset along the axis, creating a three-sphere illusion. Spinning the axis animates the light/shadow relationship across the surface.

  • Extends: Nothing (standalone composite class)
  • Dependencies: SWPoint, SWColor, SWGrid, SWDisk, p5.js
  • Composition: rightDisk (SWDisk) + leftDisk (SWDisk) + centerDisk (SWDisk) — drawn back to front
  • Key Features: Independent colors and alpha per disk, axis-angle rotation, geometry setters (radius, k, offset, thickness), deep copy, full reset
  • Common Uses: Planetary-sphere illusions, orbit animations, 3D-style button elements, ternary color compositions

Overview

Composition Model

An SWTriDisk is built from three SWDisk sub-objects, created and managed internally:

Sub-objectRoleColor parameterDraw order
rightDisk Shadow side; offset in +axis direction rightColor 1st (back)
leftDisk Light side; offset in −axis direction leftColor 2nd
centerDisk Main subject; drawn on top; shows center-point marker centerColor 3rd (front)

The drawing order (right → left → center) ensures the center disk always occludes the side disks regardless of color, producing the shadow/light crescent illusion. Sub-objects are synchronized automatically each frame — you never need to touch centerDisk, leftDisk, or rightDisk directly. Use the setter methods and they propagate changes internally.

Color Model

Each disk owns one SWColor applied to both fill and stroke:

  • Center color (centerColor) — controls the center disk's fill and stroke. Set via setCenterColor() or setCenterAlpha().
  • Left color (leftColor) — controls the left (light) disk. Set via setLeftColor() or setLeftAlpha().
  • Right color (rightColor) — controls the right (shadow) disk. Set via setRightColor() or setRightAlpha().

At construction, colors are deep-copied from the provided SWColor objects to avoid shared-reference side effects. Alpha can be updated independently via the alpha setter methods without replacing the full color.

Geometry and Axis Convention

SWTriDisk uses the same math-space (user-space) angle convention as the rest of SketchWaveJS: angles are measured counterclockwise (CCW) from the positive x-axis, y increases upward. The axis angle is converted to screen space internally.

// Axis geometry (user coords):
leftDisk center  = center − offset × (cos(axisAngle), sin(axisAngle))
rightDisk center = center + offset × (cos(axisAngle), sin(axisAngle))
sideRadius       = k × centerRadius          // k ∈ [0, 1]

// Default orientation: axisAngle = 0 means the axis points right (+x).
// Left disk is to the left (−x), right disk is to the right (+x).
let td = new SWTriDisk(new SWPoint(0, 0), 5);

// axisAngle = 90: axis points up. Left disk is above, right disk below.
let td2 = new SWTriDisk(new SWPoint(0, 0), 5, 0.9, 5, 90, cColor, lColor, rColor);
  • axisAngle: current orientation of the axis in degrees CCW from +x, normalized to [0, 360)
  • rotate(delta): advances axisAngle by delta degrees each frame
  • setAxisAngle(angle): snaps the axis to a specific angle without animation

Typical Workflow

  1. Create SWColor objects for center, left, and right disks
  2. Instantiate: new SWTriDisk(center, r, k, offset, axisAngle, cColor, lColor, rColor, thickness)
  3. Call drawOnGrid(grid) each frame inside draw()
  4. For spin animation, call rotate(spinSpeed * deltaT) before draw each frame
  5. Handle UI events with the setter methods
  6. Call reset() to restore all original geometry and colors

Constructor

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

Creates a new SWTriDisk and builds all three internal SWDisk sub-objects. Colors are deep-copied at construction to prevent shared-reference mutations.

Parameters
ParameterTypeDefaultDescription
center SWPoint required Center of the center disk in user coordinates. Can include a display color and pixel size for the center-point marker.
centerRadius number required Radius of the center disk in user units (> 0). Side disk radii are derived as k × centerRadius.
k number 0.9 Side radius factor, k ∈ [0, 1]. sideRadius = k × centerRadius. Values below 1 make side disks smaller than the center.
offset number 5 Distance in user units from the center disk to each side disk center along the axis.
axisAngle number 0 Axis orientation in degrees, CCW from +x axis (0 = right, 90 = up).
centerColor SWColor undefined Fill + stroke color for the center disk.
leftColor SWColor undefined Fill + stroke color for the left disk (light side).
rightColor SWColor undefined Fill + stroke color for the right disk (shadow side).
thickness number 2 Stroke weight in pixels applied to all three disks.
Constructor Examples
// Minimal: just a center and radius
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('#ccffdd', '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 (dark gray, 8px)
const cPt = new SWPoint(0, 0, undefined, 8, swDarkGray);
let td3 = new SWTriDisk(cPt, 5, 0.9, 5, 0, cColor, lColor, rColor, 2);

Properties

centerSWPoint

The center of the center disk in user coordinates. Move the entire tri-disk by changing center.x and center.y. All three sub-object positions sync from this point on each draw call.

centerRadiusnumber

Radius of the center disk in user units. Prefer setRadius(r) to keep all sub-object radii in sync. Side radius is derived as k × centerRadius.

sideRadiusnumber (read-only getter)

Computed radius for the two side disks: centerRadius × k. Read-only — change it by calling setRadius(r) or setK(k).

knumber

Side radius factor, k ∈ [0, 1]. Determines how large the side disks are relative to the center. Prefer setK(k) to recompute side radii immediately.

offsetnumber

Distance (user units) from the center disk to each side disk center along the axis. Prefer setOffset(offset). New positions take effect on the next draw call.

axisAnglenumber

Current axis orientation in degrees, CCW from +x axis, normalized to [0, 360). Modified each frame by rotate(). Set directly with setAxisAngle().

thicknessnumber

Stroke weight in pixels applied to all three disks. Prefer setThickness(w) to propagate to all sub-objects.

Center Disk Color Property

centerColorSWColor

Color for the center disk's fill and stroke. Set via setCenterColor() or setCenterAlpha().

Left Disk Color Property

leftColorSWColor

Color for the left (light) disk's fill and stroke. Set via setLeftColor() or setLeftAlpha().

Right Disk Color Property

rightColorSWColor

Color for the right (shadow) disk's fill and stroke. Set via setRightColor() or setRightAlpha().

shouldShowCenterboolean

When true (default), the center-point marker dot is rendered on the center disk. Toggle with setShowCenter(show).

Sub-Object References

centerDisk / leftDisk / rightDiskSWDisk

Direct references to the three internal SWDisk objects. Read-only in normal use. Rebuilt from scratch by reset(). Use setter methods rather than mutating sub-objects directly.

Original-State Properties (used by reset())

Set from constructor arguments; used to restore the tri-disk to its initial state. Do not modify directly.

  • originalCenterRadius
  • originalK
  • originalOffset
  • originalAxisAngle
  • originalThickness
  • originalCenterColor (deep copy of centerColor at construction)
  • originalLeftColor (deep copy of leftColor at construction)
  • originalRightColor (deep copy of rightColor at construction)

Methods

Drawing

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. Center position and radii are converted from user units to screen pixels via the grid's scale and offset. This is the standard call for demos that use an SWGrid.

ParameterTypeDescription
gridSWGridThe coordinate grid for user-to-pixel mapping
// Typical in draw():
grid.drawAxes();
triDisk.drawOnGrid(grid);

Rotation Animation

rotate(deltaAngle)

Advances axisAngle by deltaAngle degrees (CCW positive, CW negative). Result is normalized to [0, 360). Call before draw each frame to spin the axis — this animates the light/shadow crescent relationship.

ParameterTypeDescription
deltaAnglenumberDegrees to advance the axis angle; negative = clockwise
// In draw(): spin at 90 deg/sec
triDisk.rotate(90 * deltaT);   // deltaT = seconds since last frame
triDisk.drawOnGrid(grid);

Geometry Setters

setRadius(r)

Sets the center disk radius and immediately recomputes both side disk radii (sideRadius = k × r). Propagates to all three sub-object radii.

setK(k)

Sets the side-radius factor and immediately updates both side disk radii. Useful for dynamically controlling how much the side disks peek out from behind the center.

setOffset(offset)

Sets the offset between the center and each side disk center (user units). New positions are computed on the next draw call via internal sync.

setAxisAngle(angle)

Snaps the axis to a specific angle (degrees, CCW from +x axis), normalized to [0, 360). Useful for initializing a fixed orientation without using rotate().

setThickness(w)

Sets the stroke weight (pixels) on all three disks simultaneously.

Color Methods

setCenterColor(swColor)

Sets both the fill and stroke color of the center disk. Deep-copies the provided SWColor to prevent shared-reference side effects.

setLeftColor(swColor)

Sets both the fill and stroke color of the left disk. Deep-copies the provided SWColor.

setRightColor(swColor)

Sets both the fill and stroke color of the right disk. Deep-copies the provided SWColor.

// Set a contrasting shadow color:
let shadowColor = SWColor.fromHex('#111111', 'shadow').setAlphaTo(100);
triDisk.setRightColor(shadowColor);

Alpha Methods

setCenterAlpha(alpha)

Sets the opacity (0–100) of the center disk's fill and stroke without replacing its color. Rebuilds the p5 color object internally.

setLeftAlpha(alpha)

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

setRightAlpha(alpha)

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

// Fade side disks to 60% opacity without changing their colors:
triDisk.setLeftAlpha(60);
triDisk.setRightAlpha(60);

Other Setters

setShowCenter(show)

Shows or hides the center-point marker dot. Pass true (default) to show, false to hide. Updates both shouldShowCenter and the sub-object's flag.

Reset Method

reset()

Restores all geometry (radius, k, offset, axisAngle, thickness) and all three colors to the originals stored at construction. Rebuilds the three sub-objects cleanly. Does not move the center position.

Static Methods

static copy(other)

Returns a deep copy of the given SWTriDisk instance — current animated state and original snapshot — so the copy's reset() behavior mirrors the original's. Logs a warning and returns null if the argument is not an SWTriDisk.

let tdCopy = SWTriDisk.copy(triDisk);

Utility Methods

toString()

Returns a human-readable description of the tri-disk's current state.

console.log(triDisk.toString());
// → SWTriDisk(center:(0.00, 0.00), centerRadius:5, k:0.9, offset:5, axisAngle:135.0°)

Animation Guide

Elapsed-Time Pattern (Pause/Resume)

Use millis() to compute per-frame elapsed time (deltaT) for frame-rate-independent animations. This gives smooth, consistent motion regardless of device speed.

// Globals:
let prevT = 0;

// In draw():
const t      = millis() / 1000;        // seconds
const deltaT = (prevT > 0) ? (t - prevT) : 0;
prevT = t;

if (shouldSpin) triDisk.rotate(spinSpeed * deltaT);
triDisk.drawOnGrid(grid);

Spin Animation

// Globals:
let shouldSpin = false;
let spinSpeed  = 90;   // degrees per second (CCW positive)

// Toggle from keyPressed():
if (key === 's') shouldSpin = !shouldSpin;
if (key === 'q') { shouldSpin = false; }   // stop, keep geometry
if (key === 'r') { triDisk.reset(); shouldSpin = false; }

// In draw():
const t      = millis() / 1000;
const deltaT = (prevT > 0) ? (t - prevT) : 0;
prevT = t;

if (shouldSpin) triDisk.rotate(spinSpeed * deltaT);
triDisk.drawOnGrid(grid);

Axis-Angle Snap

// Snap the light/shadow axis to a specific angle without animating:
triDisk.setAxisAngle(45);   // 45° up-right diagonal

Dynamic Geometry During Animation

// Increase offset from a slider while spinning — takes effect next draw:
triDisk.setOffset(sliderOffset.value());

// Shrink side disks relative to center:
triDisk.setK(0.7);

// All changes stack cleanly — just call drawOnGrid() at the end of draw()
triDisk.drawOnGrid(grid);

Usage Examples

Minimal Tri-Disk

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) });
    triDisk = new SWTriDisk(new SWPoint(0, 0), 5);
}

function draw() {
    background(220, 5, 93);
    grid.drawAxes();
    triDisk.drawOnGrid(grid);
}

Fully Colored, Spinning

let prevT = 0;
const SPIN_SPEED = 90;   // degrees per second

function setup() {
    // ...
    const cColor = SWColor.fromHex('#2d8a4e', 'c').setAlphaTo(100);
    const lColor = SWColor.fromHex('#ccffdd', 'l').setAlphaTo(100);
    const rColor = SWColor.fromHex('#111111', 'r').setAlphaTo(100);
    triDisk = new SWTriDisk(new SWPoint(0, 0), 6, 0.9, 7, 0, cColor, lColor, rColor, 2);
}

function draw() {
    const t      = millis() / 1000;
    const deltaT = (prevT > 0) ? (t - prevT) : 0;
    prevT = t;

    background(140, 20, 92);
    grid.drawAxes();
    triDisk.rotate(SPIN_SPEED * deltaT);
    triDisk.drawOnGrid(grid);
}

Color and Alpha Control

// Change center disk to a new color
triDisk.setCenterColor(SWColor.fromHex('#cc0000', 'c'));

// Fade the shadow disk to 40% opacity without changing its color
triDisk.setRightAlpha(40);

// Restore all originals
triDisk.reset();

Loading Dependencies

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

<!-- SketchWaveJS classes (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/swTriDisk.js"></script>

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

Source Code

The full swTriDisk.js source code is shown below for reference.

/*
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