■□ SWTwoTonedSquare Reference

A SketchWave composite class for animated two-toned squares — four geometric split patterns, spin, breathe, and hue cycling

SWTwoTonedSquare Demo

Quick Reference

SWTwoTonedSquare is a SketchWave composite class for a square visually divided into two differently-colored regions. The integer parameter n (1–4) selects the geometric split pattern. All drawing uses raw p5.js primitives (rect(), beginShape/vertex/endShape) inside a single push/translate/rotate/pop block, so the entire composite always spins and breathes as a unit. The class supports spin, size breathing, and hue-cycling animations.

  • Extends: Nothing (standalone composite class)
  • Dependencies: SWPoint, SWColor, SWSinusoid, SWGrid, p5.js
  • Drawing model: Raw p5.js primitives in local (translated + rotated) coordinate space — no SWRectangle or SWTriangle sub-objects
  • Key Features: 4 split patterns, independent Color 1 / Color 2 fills, spin, breathe (size), hue cycling, drag-to-reposition center
  • Common Uses: Logo halftones, geometric badges, animated tile designs, pattern exploration

Overview

Drawing Model

SWTwoTonedSquare draws everything inside a single coordinate-transform block:

  1. push() — saves the current transform state
  2. translate(cx_s, cy_s) — moves the origin to the square's screen-pixel center
  3. rotate(-radians(startAngle + rotation)) — applies the total orientation (negated for CCW convention)
  4. Draws the base square (Color 1) with rectMode(CENTER); rect(0, 0, side, side)
  5. Draws the secondary shape(s) (Color 2) using rect() or beginShape/vertex/endShape
  6. pop() — restores the transform state

Because all shapes are defined in local coordinates (origin = center, rotation already applied), raw p5.js primitives are simpler and more reliable than constructing sub-objects that would need coordinate re-mapping every time size or rotation changes. There are also no independent outline arcs requiring separate color management — a key reason why SWSector/SWArc sub-objects were appropriate for SWTwoTonedDisk but are not needed here.

Pattern Parameter n

The integer n (1–4) selects how the square is geometrically divided. Color 1 always fills the full base square first; Color 2 is painted on top as the secondary shape(s).

nPatternColor 2 fills...
1 Horizontal split The upper half — a rectangle spanning the full width, height = ½ side
2 Diagonal split The upper-right right-isosceles triangle; hypotenuse = top-left → bottom-right diagonal; right angle at top-right
3 Opposite quadrant squares Two smaller squares (side = ½ main) at the top-left and bottom-right quadrants; inner corners meet at the center
4 Opposite triangles Two triangles: one with base on the top side, one with base on the bottom side; both apices meet at the center

All four patterns share identical spin, breathe, and hue-cycling behavior. You can switch n live — even while the shape is spinning — and the new pattern takes effect immediately on the next frame.

Color Model

The two colors are independent:

  • Color 1 (color1) — fills the entire base square. Set via setColor1() / setColor1Alpha().
  • Color 2 (color2) — fills the overlaid secondary shape(s). Set via setColor2() / setColor2Alpha().

Both colors use noStroke() — there are no outline strokes. Alpha values range from 0 (transparent) to 100 (opaque) using p5's HSB 100-scale. Reducing Color 2's alpha lets Color 1 show through in the overlapping region.

Angle and Orientation Convention

SWTwoTonedSquare uses the standard SketchWaveJS math-space angle convention: CCW positive from the +x axis, y increases upward. The effective orientation when drawing is startAngle + rotation, negated to convert to p5's y-down screen space.

// Default orientation: bottom of the square faces down (+y screen).
let sq = new SWTwoTonedSquare(new SWPoint(0, 0), 5);

// startAngle=45: square is tilted 45° CCW.
let sq2 = new SWTwoTonedSquare(new SWPoint(0, 0), 5, 1, color1, color2, 45);
  • startAngle: static orientation set once at construction or via setStartAngle()
  • rotation: accumulated spin added by each rotate(delta) call
  • Effective orientation = startAngle + rotation

Typical Workflow

  1. Create an SWGrid and an SWTwoTonedSquare with center, size, n, Color 1, Color 2, and optional startAngle
  2. Call drawOnGrid(grid) each frame inside draw()
  3. For animations each frame:
    • Call rotate(delta) before draw (takes effect immediately)
    • Call breathe(sinusoid, t) to oscillate the side length
    • For hue cycling, mutate Color 1's HSB values, rebuild its p5 color, and push via setColor1()
  4. Use the elapsed-time pattern for pauseable animations (see Animation Guide)
  5. Call reset() to restore all geometry and colors to construction-time originals

Constructor

new SWTwoTonedSquare(center, size, n, color1, color2, startAngle)

Creates a new SWTwoTonedSquare. Stores deep copies of colors for reset(). All drawing happens in a single local coordinate frame — no sub-objects are created.

Parameters
ParameterTypeDefaultDescription
center SWPoint required Center of the square. Can include a display color and pixel size for the center marker.
size number required Side length in user units (> 0). Clamped to 0.01 internally.
n number 1 Pattern selector: 1, 2, 3, or 4. Out-of-range values are clamped and rounded.
color1 SWColor undefined Fill color for the base square.
color2 SWColor undefined Fill color for the secondary shape(s) drawn on top.
startAngle number 0 Static orientation in degrees CCW from the +x axis.
Constructor Examples
// Minimal: just center and size (no colors — nothing visible)
let sq1 = new SWTwoTonedSquare(new SWPoint(0, 0), 5);

// With colors, default n=1 (horizontal split)
let c1 = SWColor.fromHex('#0066CC', 'c1').setAlphaTo(90);
let c2 = SWColor.fromHex('#CC0000', 'c2').setAlphaTo(90);
let sq2 = new SWTwoTonedSquare(new SWPoint(0, 0), 5, 1, c1, c2);

// Diagonal split (n=2) rotated 45°
let sq3 = new SWTwoTonedSquare(new SWPoint(0, 0), 5, 2, c1, c2, 45);

// Styled center marker (dark gray, 8px)
const centerPt = new SWPoint(0, 0, undefined, 8, swDarkGray);
let sq4 = new SWTwoTonedSquare(centerPt, 5, 1, c1, c2, 0);

Properties

centerSWPoint

The center of the square in user coordinates. Drag in the demo to reposition live. Used directly in _drawCore() via grid.userToScreen().

sizenumber

Current side length in user units. Oscillates when breathing. Set via setSize(s) or breathe(sinusoid, t). Clamped to 0.01.

nnumber

Current pattern selector (integer 1–4). Change at any time via setN(n). Takes effect on the next frame.

startAnglenumber

Static orientation in degrees CCW from the +x axis. Set once at construction or via setStartAngle(). Does not change when rotate() is called — that accumulates in rotation separately.

rotationnumber

Accumulated rotation in degrees (CCW positive). Added to startAngle when rendering. Incremented each frame by rotate(delta). Reset to 0 by reset().

Color 1 Properties

color1SWColor

Fill color for the base square. Set via setColor1(swColor). Opacity set via setColor1Alpha(alpha).

Color 2 Properties

color2SWColor

Fill color for the secondary shape(s). Set via setColor2(swColor). Opacity set via setColor2Alpha(alpha).

shouldShowCenterboolean

When true (default), the center SWPoint is rendered as a visible marker. Toggle with setShowCenter(show).

Original-State Properties (used by reset())

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

  • originalSize
  • originalStartAngle
  • originalColor1 (deep copy of color1 at construction)
  • originalColor2 (deep copy of color2 at construction)

Methods

Internal Drawing (private)

_drawCore(cx_s, cy_s, halfS)

Internal draw in local pixel coordinates. Called by both draw() and drawOnGrid() after center and halfS are resolved. Wraps all p5 primitives in a single push / translate / rotate / pop block. Do not call directly.

Drawing

draw()

Draws in screen (pixel) coordinates, using the center's raw x/y as pixel positions and size/2 as the half-side in pixels. Use drawOnGrid() for user-coordinate rendering in demos that use an SWGrid.

drawOnGrid(grid)

Draws mapped through the given SWGrid. Center position is converted via grid.userToScreen() and the half-side is computed as scale × (size/2), where scale is the average of the grid's x and y scales. This is the standard call for all SketchWaveJS demos.

// Typical in draw():
grid.draw();
sq1.drawOnGrid(grid);

Rotation Animation

rotate(deltaAngle)

Increments rotation by deltaAngle degrees (CCW positive, CW negative). Call before draw each frame to spin the square about its center. All pattern shapes rotate together with the base square.

ParameterTypeDescription
deltaAnglenumberDegrees to add to rotation; negative = clockwise
// In draw(): spin at 45 deg/sec
sq1.rotate(45 * deltaT);   // deltaT = seconds since last frame
sq1.drawOnGrid(grid);

Breathing Animation

breathe(sinusoid, t)

Sets size to the current value of the sinusoid, making the square grow and shrink. All pattern shapes scale uniformly since they are computed from halfS = size/2 each frame. Minimum clamped to 0.01.

ParameterTypeDescription
sinusoidSWSinusoidConfigured with desired period and min/max size values
tnumberElapsed animated time in seconds
// Setup (once in setup()):
breatheSinusoid = SWSinusoid.copy(UNIT_SINUSOID);
breatheSinusoid.setPeriod(4);
breatheSinusoid.adjustWaveUsingExtrema(2, 8);   // size oscillates 2–8 units

// In draw():
if (shouldBreathe) {
    const bt = breatheElapsed + (millis()/1000 - breatheStartTime);
    sq1.breathe(breatheSinusoid, bt);
}

Pattern Setter

setN(n)

Changes the pattern selector. Valid values: 1, 2, 3, 4. Out-of-range values are clamped; non-integers are rounded. Takes effect immediately on the next draw call — safe to call mid-animation.

Size Setter

setSize(s)

Sets the side length directly. Clamped to 0.01. Useful when the UI controls size independently of breathing.

Fill Color Methods

setColor1(swColor)

Sets the base square's fill color. Deep-copies the provided SWColor.

setColor2(swColor)

Sets the secondary shape(s) fill color. Deep-copies the provided SWColor.

setColor1Alpha(alpha)

Sets Color 1's alpha, clamped to [0, 100]. Rebuilds the p5 color object without changing hue, saturation, or brightness.

setColor2Alpha(alpha)

Sets Color 2's alpha, clamped to [0, 100]. Rebuilds the p5 color object without changing hue, saturation, or brightness.

Other Setters

setStartAngle(degrees)

Sets the static orientation (CCW from +x axis) without affecting accumulated rotation.

setShowCenter(show)

Shows or hides the center marker point. Pass true (default) to show, false to hide.

Reset

reset()

Restores all animated/slider-driven values — rotation, startAngle, size, color1, color2 — to the originals stored at construction. Does not move the center position or reset n.

Static Methods

static copy(other)

Returns a deep copy of the given SWTwoTonedSquare instance. Deep-copies the center point and both colors. The copy has the same current size, rotation, and n as the original. Throws if the argument is not an SWTwoTonedSquare.

let sqCopy = SWTwoTonedSquare.copy(sq1);

Utility Methods

toString()

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

console.log(sq1.toString());
// → SWTwoTonedSquare(center: SWPoint(0.00, 0.00), size: 5.00, n: 1,
//   startAngle: 0°, rotation: 0.0°)

Animation Guide

Elapsed-Time Pattern (Pause/Resume)

All continuous animations use the same elapsed-time pattern: accumulate elapsed seconds while paused, so the wave doesn't jump when you resume.

// Globals:
let shouldBreathe = false;
let breatheStartTime = 0, breatheElapsed = 0;

// Toggle:
function toggleBreathe() {
    const t = millis() / 1000;
    if (!shouldBreathe) {
        breatheStartTime = t;
        shouldBreathe    = true;
    } else {
        breatheElapsed  += (t - breatheStartTime);
        shouldBreathe    = false;
    }
}

// In draw():
if (shouldBreathe) {
    const bt = breatheElapsed + (millis()/1000 - breatheStartTime);
    sq1.breathe(breatheSinusoid, bt);
}

Spin Animation

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

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

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

Hue Cycling (Color 1 and Color 2)

Hue cycling is implemented externally: compute the hue from a sinusoid, rebuild the SWColor's p5 color, then push it to the square. Color 2 gets Color 1's hue plus a fixed offset — the default offset of 180° gives complementary colors.

// In draw() — cycle color1 + color2 hues:
if (shouldCycleHue) {
    const ht = hueCycleElapsed + (millis()/1000 - hueCycleStartTime);

    // Color 1 hue from sinusoid
    color1.h   = hueSinusoid.getValue(ht);
    color1.col = color(color1.h, color1.s, color1.b, color1.a);
    sq1.setColor1(color1);

    // Color 2 hue = Color 1 hue + offset (default 180° = complementary)
    color2.h   = (color1.h + hueOffset) % 360;
    color2.col = color(color2.h, color2.s, color2.b, color2.a);
    sq1.setColor2(color2);
}
sq1.drawOnGrid(grid);

Running All Animations Simultaneously

// In draw() — spin + hue BEFORE draw; breathe can be before or after:
const t      = millis() / 1000;
const deltaT = (prevT > 0) ? (t - prevT) : 0;
prevT = t;

background(bgColor);
if (shouldShowGrid) grid.draw();

// 1. Spin
if (shouldSpin) sq1.rotate(spinSpeed * deltaT);

// 2. Hue cycle
if (shouldCycleHue) {
    const ht = hueCycleElapsed + (t - hueCycleStartTime);
    color1.h   = hueSinusoid.getValue(ht);
    color1.col = color(color1.h, color1.s, color1.b, color1.a);
    sq1.setColor1(color1);
    color2.h   = (color1.h + hueOffset) % 360;
    color2.col = color(color2.h, color2.s, color2.b, color2.a);
    sq1.setColor2(color2);
}

// 3. Breathe
if (shouldBreathe) {
    const bt = breatheElapsed + (t - breatheStartTime);
    sq1.breathe(breatheSinusoid, bt);
}

// 4. Draw
sq1.drawOnGrid(grid);

Usage Examples

Minimal Square

let sq, grid;

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) });

    const c1 = SWColor.fromHex('#0066CC', 'c1').setAlphaTo(90);
    const c2 = SWColor.fromHex('#CC0000', 'c2').setAlphaTo(90);
    sq = new SWTwoTonedSquare(new SWPoint(0, 0), 5, 1, c1, c2);
}

function draw() {
    background(240, 5, 93);
    grid.draw();
    sq.drawOnGrid(grid);
}

Switching Patterns Live

// Switch to the diagonal split while the shape may already be spinning:
sq.setN(2);   // takes effect next frame — safe at any time

Spinning Square (Time-Based)

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

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

    background(240, 5, 93);
    grid.draw();
    sq.rotate(SPIN_SPEED * deltaT);
    sq.drawOnGrid(grid);
}

Breathing Size

let breatheSin;
let breatheStart = 0, breatheElapsed = 0, shouldBreathe = false;

function setup() {
    // ...square construction...
    breatheSin = SWSinusoid.copy(UNIT_SINUSOID);
    breatheSin.setPeriod(4);
    breatheSin.adjustWaveUsingExtrema(2, 8);
}

function draw() {
    const t = millis() / 1000;
    // ...
    if (shouldBreathe) {
        sq.breathe(breatheSin, breatheElapsed + (t - breatheStart));
    }
    sq.drawOnGrid(grid);
}

function keyPressed() {
    if (key === 'b') {
        const t = millis() / 1000;
        if (!shouldBreathe) { breatheStart = t; shouldBreathe = true; }
        else { breatheElapsed += t - breatheStart; shouldBreathe = false; }
    }
    if (key === 'r') { sq.reset(); breatheElapsed = 0; }
}

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/swTwoTonedSquare.js"></script>

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

Note: swSector.js, swArc.js, swRectangle.js, and swTriangle.js are not needed — SWTwoTonedSquare uses only raw p5.js drawing primitives internally.

Source Code

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

/*
File: swTwoTonedSquare.js
Date: 2026-03-29
Author: klp
App:  SketchWaveTNT2026-03-19-Stg7
Purpose: SWTwoTonedSquare class for SketchWaveJS
Comment(s):

SWTwoTonedSquare is a composite shape: a square visually divided into two
differently-colored regions.  The division type is selected by the integer
parameter n (1–4):

  n = 1  Horizontal split — base square (color1) + a rectangle covering
         the upper half (color2).

  n = 2  Diagonal split — base square (color1) + a right-isosceles triangle
         (color2) whose hypotenuse runs along the top-left → bottom-right
         diagonal.  The right angle sits at the top-right corner.

  n = 3  Opposite quadrant squares — base square (color1) + two smaller
         squares (color2, side = ½ main side) placed in the top-left and
         bottom-right quadrants.  Their inner corners touch the center.

  n = 4  Opposite triangles — base square (color1) + two triangles (color2)
         whose bases are the top and bottom sides of the square and whose
         apex meets at the center.

Animations (same conventions as the rest of the SketchWaveJS family):
  rotate(deltaAngle)   — spin the entire composite about its center (CCW +, deg)
  breathe(sinusoid, t) — oscillate the overall side length using an SWSinusoid

Color model:
  color1 — fills the base square
  color2 — fills the overlaid secondary shape(s)

Drawing model:
  All shapes are drawn with raw p5.js primitives (rect, beginShape/vertex/endShape)
  inside a single push/translate/rotate/pop block.  This is the right choice because
  all coordinates are defined in local space (origin = center, rotation applied), so
  raw primitives need no extra coordinate mapping.  No SWRectangle or SWTriangle
  sub-objects are used — adding them would require re-deriving sub-shape positions
  whenever size or rotation changes, with no benefit for this geometry.

Angle convention:
  User / math space: CCW positive, y increases upward.
  p5 / screen space: CW  positive, y increases downward.
  → rotate() negates the angle when calling p5's rotate() in _drawCore().

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

console.log("[swTwoTonedSquare.js] SWTwoTonedSquare class loaded.");

class SWTwoTonedSquare {

    /**
     * @param {SWPoint} center         - Center of the square (user coordinates)
     * @param {number}  size           - Side length in user units (> 0)
     * @param {number}  [n=1]          - Pattern selector: 1, 2, 3, or 4
     * @param {SWColor} [color1]       - Base (background) fill color
     * @param {SWColor} [color2]       - Secondary shape fill color
     * @param {number}  [startAngle=0] - Static orientation in degrees (CCW from +x)
     */
    constructor(center, size, n = 1,
                color1 = undefined, color2 = undefined,
                startAngle = 0) {

        this.center     = center;
        this.size       = Math.max(0.01, size);
        this.n          = Math.max(1, Math.min(4, Math.round(n)));
        this.startAngle = startAngle;
        this.rotation   = 0;               // accumulated spin (degrees)

        this.color1 = color1 ? SWColor.copy(color1) : undefined;
        this.color2 = color2 ? SWColor.copy(color2) : undefined;

        // Originals stored at construction time for reset()
        this.originalSize       = this.size;
        this.originalStartAngle = startAngle;
        this.originalColor1     = color1 ? SWColor.copy(color1) : undefined;
        this.originalColor2     = color2 ? SWColor.copy(color2) : undefined;

        this.shouldShowCenter = true;

    }//end constructor

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

    /**
     * Internal draw in local pixel coordinates.
     * push / translate(cx_s, cy_s) / rotate wraps all shape calls so the
     * entire composite spins as a unit.
     *
     * @param {number} cx_s  - Center x in screen pixels
     * @param {number} cy_s  - Center y in screen pixels
     * @param {number} halfS - Half the side length in screen pixels
     */
    _drawCore(cx_s, cy_s, halfS) {
        const totalAngle = this.startAngle + this.rotation;

        push();
        translate(cx_s, cy_s);
        // Negate: CCW user convention → visually CCW in p5's y-down space
        rotate(-radians(totalAngle));
        noStroke();

        // ── Base square ──────────────────────────────────────────────────────
        if (this.color1 && this.color1.col) {
            fill(this.color1.col);
        } else {
            noFill();
        }
        rectMode(CENTER);
        rect(0, 0, halfS * 2, halfS * 2);

        // ── Secondary shape(s) ───────────────────────────────────────────────
        if (this.color2 && this.color2.col) {
            fill(this.color2.col);
        } else {
            noFill();
        }

        if (this.n === 1) {
            // ── n=1: upper rectangle (top half of the square) ────────────────
            // p5 y-down: "upper" = negative y.  Top-half center is at y = -halfS/2.
            rectMode(CENTER);
            rect(0, -halfS / 2, halfS * 2, halfS);

        } else if (this.n === 2) {
            // ── n=2: right-isosceles triangle, hypotenuse = main diagonal ────
            // Diagonal runs top-left (-halfS,-halfS) to bottom-right (halfS,halfS).
            // Right angle sits at top-right (halfS, -halfS).
            beginShape();
            vertex(-halfS, -halfS);   // top-left   (45° angle)
            vertex( halfS, -halfS);   // top-right  (right angle vertex)
            vertex( halfS,  halfS);   // bottom-right (45° angle)
            endShape(CLOSE);

        } else if (this.n === 3) {
            // ── n=3: two quadrant squares in opposite corners ────────────────
            // Each sub-square has side = halfS; its inner corner touches (0,0).
            rectMode(CENTER);
            rect(-halfS / 2, -halfS / 2, halfS, halfS);   // top-left quadrant
            rect( halfS / 2,  halfS / 2, halfS, halfS);   // bottom-right quadrant

        } else if (this.n === 4) {
            // ── n=4: two triangles on top and bottom sides ───────────────────
            // Each triangle has its base on one full side; apex is the center (0,0).
            beginShape();
            vertex(-halfS, -halfS);   // top-left
            vertex( halfS, -halfS);   // top-right
            vertex(0, 0);             // center (apex)
            endShape(CLOSE);

            beginShape();
            vertex(-halfS,  halfS);   // bottom-left
            vertex( halfS,  halfS);   // bottom-right
            vertex(0, 0);             // center (apex)
            endShape(CLOSE);
        }

        pop();
    }//end _drawCore

    /**
     * Draws in screen (pixel) coordinates.
     * center.x / center.y are treated as screen pixels; size as screen pixels.
     */
    draw() {
        const halfS = this.size / 2;
        this._drawCore(this.center.x, this.center.y, halfS);
        if (this.shouldShowCenter && this.center && this.center.draw) {
            this.center.draw();
        }
    }//end draw

    /**
     * Draws mapped through an SWGrid (user → screen).
     * @param {SWGrid} grid
     */
    drawOnGrid(grid) {
        const { x: cx_s, y: cy_s } = grid.userToScreen(this.center.x, this.center.y);
        const scale = (grid.xScale + grid.yScale) / 2;
        const halfS = scale * (this.size / 2);
        this._drawCore(cx_s, cy_s, halfS);
        if (this.shouldShowCenter && this.center && this.center.drawOnGrid) {
            this.center.drawOnGrid(grid);
        }
    }//end drawOnGrid

    // ─── Spin animation ─────────────────────────────────────────────────────────

    /**
     * Increments the accumulated rotation by deltaAngle degrees (CCW positive).
     * Call once per frame before drawOnGrid() to spin the shape.
     * @param {number} deltaAngle
     */
    rotate(deltaAngle) {
        this.rotation += deltaAngle;
    }//end rotate

    // ─── Breathe animation ──────────────────────────────────────────────────────

    /**
     * Modulates the overall side length using a sinusoid.
     * @param {SWSinusoid} sinusoid
     * @param {number}     t  - Elapsed time in seconds
     */
    breathe(sinusoid, t) {
        this.size = Math.max(0.01, sinusoid.getValue(t));
    }//end breathe

    // ─── Setters ────────────────────────────────────────────────────────────────

    setSize(s) {
        this.size = Math.max(0.01, s);
    }//end setSize

    setN(n) {
        this.n = Math.max(1, Math.min(4, Math.round(n)));
    }//end setN

    setColor1(swColor) {
        this.color1 = swColor ? SWColor.copy(swColor) : undefined;
    }//end setColor1

    setColor2(swColor) {
        this.color2 = swColor ? SWColor.copy(swColor) : undefined;
    }//end setColor2

    /**
     * Updates color1 alpha without changing hue/sat/bri.
     * @param {number} alpha - 0–100
     */
    setColor1Alpha(alpha) {
        if (!this.color1) return;
        this.color1.a   = Math.max(0, Math.min(100, alpha));
        this.color1.col = color(this.color1.h, this.color1.s, this.color1.b, this.color1.a);
    }//end setColor1Alpha

    /**
     * Updates color2 alpha without changing hue/sat/bri.
     * @param {number} alpha - 0–100
     */
    setColor2Alpha(alpha) {
        if (!this.color2) return;
        this.color2.a   = Math.max(0, Math.min(100, alpha));
        this.color2.col = color(this.color2.h, this.color2.s, this.color2.b, this.color2.a);
    }//end setColor2Alpha

    setStartAngle(degrees) {
        this.startAngle = degrees;
    }//end setStartAngle

    setShowCenter(show = true) {
        this.shouldShowCenter = show;
    }//end setShowCenter

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

    /**
     * Resets all animated / slider-driven values to construction-time originals.
     * Does NOT move the center position.
     */
    reset() {
        this.rotation   = 0;
        this.startAngle = this.originalStartAngle;
        this.size       = this.originalSize;
        if (this.originalColor1) this.color1 = SWColor.copy(this.originalColor1);
        if (this.originalColor2) this.color2 = SWColor.copy(this.originalColor2);
    }//end reset

    // ─── Static copy ────────────────────────────────────────────────────────────

    /**
     * Returns a deep copy of the given SWTwoTonedSquare instance.
     * @param {SWTwoTonedSquare} other
     * @returns {SWTwoTonedSquare}
     */
    static copy(other) {
        if (!(other instanceof SWTwoTonedSquare)) {
            throw new Error('SWTwoTonedSquare.copy: argument must be an SWTwoTonedSquare instance');
        }
        const sq = new SWTwoTonedSquare(
            SWPoint.copy(other.center),
            other.originalSize,
            other.n,
            other.color1,
            other.color2,
            other.originalStartAngle
        );
        sq.size             = other.size;
        sq.startAngle       = other.startAngle;
        sq.rotation         = other.rotation;
        sq.shouldShowCenter = other.shouldShowCenter;
        return sq;
    }//end copy

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

    toString() {
        return `SWTwoTonedSquare(center: ${this.center.toString()}, ` +
               `size: ${this.size.toFixed(2)}, n: ${this.n}, ` +
               `startAngle: ${this.startAngle}°, rotation: ${this.rotation.toFixed(1)}°)`;
    }//end toString

}//end class SWTwoTonedSquare

// export default SWTwoTonedSquare;