🌙 SWCircleSegment Reference

A SketchWave class for representing circular segments — the region between a chord and its arc

Back to SWCircleSegment Demo

Quick Reference

SWCircleSegment is a SketchWave class for representing circular segment shapes in 2D space. A circle segment is the crescent-shaped region bounded by a chord and its subtended arc — unlike a sector, it does not connect to the center of the generating circle. It is defined by a center point, a radius, and a central angle theta. It supports spin animation, radius breathing, theta breathing, and hue cycling — all independently or simultaneously.

  • Extends: Nothing (standalone class)
  • Dependencies: SWPoint, SWColor, SWSinusoid, SWGrid, p5.js
  • Key Features: Custom fill/stroke colors, arc length / chord length / sagitta / area calculations, spin rotation, radius and theta breathing animations, hue cycling, center point display
  • Common Uses: Crescent shapes, lens silhouettes, architectural arches, animated "bite taken out of a circle" effects, geometric explorations
  • p5.js Arc Mode: CHORD — the arc is closed by a straight chord, not connected to the center

Overview

The SWCircleSegment class represents a circular segment — the region bounded by a chord and the arc it subtends — drawn on an SWGrid. Think of it as a "bite taken out of a circle": the chord is the straight edge and the arc is the curved edge. Unlike SWSector (pizza slice), there are no lines connecting to the center — just the arc and the chord closing it directly. The segment's position on the circle is controlled by startAngle, and it can be continuously spun via rotate().

SWCircleSegment vs. SWSector: Both use the same angle parameters, but SWSector draws in PIE mode (two radii extend to the center), while SWCircleSegment draws in CHORD mode (the arc endpoints are joined by a straight chord). The center property stores the center of the generating circle, but it is not a vertex of the drawn shape.

Angle Convention

SWCircleSegment uses math-space (user-space) angles: angles are measured counterclockwise (CCW) from the positive x-axis, and y increases upward — the standard Cartesian convention. Because p5.js uses a y-down screen space, all angles are negated internally when calling arc(). You never need to worry about this conversion; just pass CCW degrees from +x.

// Default: startAngle=0, theta=120 → segment spans from +x CCW through 120°
let seg = new SWCircleSegment(new SWPoint(0,0), 5, 120);

// startAngle=90 → segment starts at +y axis and sweeps CCW into Q2
let seg2 = new SWCircleSegment(new SWPoint(0,0), 5, 120, 90);

Key Capabilities

  • Chord-Closed Arc: Uses p5's CHORD mode — no connection to the circle's center
  • Flexible Positioning: Center defined by an SWPoint instance (center of the generating circle)
  • Full Styling Control: Independent fill and stroke colors using SWColor
  • Rich Geometric Properties: Automatic arc length, chord length, sagitta, and segment area calculations
  • Spin Animation: Continuous rotation about the center via rotate()
  • Breathing Animations: Oscillate radius or theta independently with SWSinusoid
  • Hue Cycling: Animate fill color hue externally via SWSinusoid
  • Dual Coordinate Systems: Draw in screen pixels or grid coordinates
  • Center Point Display: Optional visualization of the circle's center

Typical Workflow

  1. Create an SWCircleSegment with center point, radius, theta, startAngle, and colors
  2. Draw the segment each frame using drawOnGrid()
  3. Call rotate() before drawing to spin; call breatheRadius() or breatheTheta() after drawing to modulate next frame
  4. Use the elapsed-time pattern for pause/resume of each animation
  5. Call reset() to restore the original geometry and color

Constructor

new SWCircleSegment(center, radius, theta, startAngle, thickness, fillColor, strokeColor)

Creates a new SWCircleSegment instance with the given geometry and styling.

Parameters
Parameter Type Default Description
center SWPoint required Center of the generating circle (NOT a vertex of the drawn shape)
radius number required Radius in user units (> 0)
theta number required Central angle / angular size in degrees; clamped to [0, 360]
startAngle number 0 Arc start angle in degrees CCW from +x axis; sets initial orientation
thickness number 2 Border (stroke) thickness in pixels
fillColor SWColor undefined Interior fill color (no fill if undefined)
strokeColor SWColor undefined Border color — applied to both the arc and the chord (no stroke if undefined)
Constructor Examples
// Minimal: center, radius, and theta
let seg1 = new SWCircleSegment(new SWPoint(0, 0), 5, 120);

// With explicit startAngle (segment starts at +y axis, sweeps CCW)
let seg2 = new SWCircleSegment(new SWPoint(0, 0), 5, 120, 90);

// With fill color and border (magenta segment)
let fillCol   = new SWColor(300, 100, 100, 80, "magenta");
let strokeCol = new SWColor(300, 100, 50, 100, "darkMagenta");
let seg3 = new SWCircleSegment(new SWPoint(0, 0), 5, 180, 0, 2, fillCol, strokeCol);

// Repositioned segment — a thin crescent at top-right
let c = new SWPoint(3, 2);
let seg4 = new SWCircleSegment(c, 4, 60, 45, 3, fillCol, strokeCol);

Properties

center SWPoint

The center of the generating circle. Changing this moves the whole segment. Note: this is the geometric center of the circle, not a corner or endpoint of the segment itself.

seg.center.x = 3; seg.center.y = -2;
radius number

The radius of the generating circle in user units. Changing this directly does not update computed geometry — use setRadius() for that.

seg.setRadius(8); // also updates arcLength, chordLength, sagitta, area
theta number

The central angle of the segment in degrees, clamped to [0, 360]. Small theta → narrow sliver; theta near 360° → nearly a full circle with a tiny gap at the chord. Use setTheta() to keep geometry in sync.

seg.setTheta(90); // quarter-circle segment
startAngle number

The static starting angle (degrees CCW from +x) for the arc. This is the orientation before any rotation accumulates. Use setStartAngle() to change it.

seg.setStartAngle(45); // arc begins at 45°
rotation number

Accumulated rotation in degrees (CCW positive). Set to 0 by the constructor; incremented by rotate(). The effective arc start is startAngle + rotation.

console.log(seg.rotation.toFixed(1) + "°");
thickness number

The stroke (border) thickness in pixels. Applied to both the arc and the chord. Use setStrokeWeight() to update.

seg.setStrokeWeight(4);
fillColor SWColor

The interior fill color as an SWColor instance. Colors are always copied to avoid shared-mutation bugs.

seg.setFillColor(new SWColor(300, 100, 100, 80, "magenta"));
strokeColor SWColor

The border color as an SWColor instance. This color is applied to both the curved arc edge and the straight chord.

seg.setStrokeColor(swBlack);
originalRadius / originalTheta / originalStartAngle / originalRotation / originalFillColor various restore targets

Snapshot values taken at construction. reset() uses all of these to restore the segment to its initial state.

// Read-only; used internally by reset()
shouldShowCenter boolean

Whether to draw the center SWPoint marker. Default is true. Useful for interactive demos where users drag the center to reposition the segment.

seg.setShowCenter(false); // hide center dot
arcLength number computed

The arc length of the segment's curved edge: (theta / 360) × 2πr. Updated automatically when radius or theta changes.

console.log(`Arc length: ${seg.arcLength.toFixed(2)}`);
chordLength number computed

The length of the straight chord: 2r × sin(theta/2). This is the length of the segment's straight edge. Updated automatically when radius or theta changes.

console.log(`Chord length: ${seg.chordLength.toFixed(2)}`);
sagitta number computed

The sagitta (height) of the segment — the distance from the midpoint of the chord to the midpoint of the arc: r × (1 − cos(theta/2)). Updated automatically when radius or theta changes.

console.log(`Sagitta: ${seg.sagitta.toFixed(2)}`);
area number computed

The area of the circular segment (not the full sector): r² / 2 × (theta_rad − sin(theta_rad)). This is strictly the area of the crescent-shaped region between chord and arc. Updated automatically when radius or theta changes.

console.log(`Segment area: ${seg.area.toFixed(2)}`);

Methods

Core Drawing Methods

draw()

Draws the circle segment in screen (pixel) coordinates using p5.js. Rarely used directly — prefer drawOnGrid().

Returns

void

Example
function draw() {
    background(220);
    seg.draw(); // center.x/y treated as screen pixels
}
drawOnGrid(grid)

Draws the circle segment in user (grid) coordinates. Converts the center position and radius through the SWGrid's coordinate mapping. The average of grid.xScale and grid.yScale is used for the radius so the segment stays circular even on non-square grids.

Parameters
  • grid (SWGrid) — the coordinate grid
Returns

void

Example
function draw() {
    background(220);
    grid.draw();
    seg.drawOnGrid(grid);
}

Rotation Animation

rotate(deltaAngle)

Increments the segment's accumulated rotation by deltaAngle degrees (CCW positive, CW negative). Call each frame to spin the segment about its center. Because the shape is a chord-closed arc, spinning it traces a ring-like path.

Parameters
  • deltaAngle (number) — degrees to add to rotation
Example
// Spin at 45°/second using elapsed time (deltaT)
seg.rotate(spinSpeed * deltaT);  // call BEFORE drawOnGrid

// Or use a fixed increment per frame
seg.rotate(1); // 1 degree per frame

Breathing Animations

breatheRadius(sinusoid, t)

Modulates the radius using an SWSinusoid. The radius is set to the sinusoid's value at time t; clamped to a minimum of 0.01. Automatically updates chordLength, sagitta, arcLength, and area. Call after drawOnGrid() so the new value takes effect on the next frame.

Parameters
  • sinusoid (SWSinusoid) — controls radius oscillation
  • t (number) — elapsed time in seconds
Example
// Radius oscillates between 2 and 8 over 4 seconds
let radSin = new SWSinusoid(5, 3, 1/4, 0); // center=5, amp=3, freq=0.25 Hz
seg.drawOnGrid(grid);
seg.breatheRadius(radSin, elapsedSeconds);
breatheTheta(sinusoid, t)

Modulates the central angle (theta) using an SWSinusoid. Call after drawOnGrid(). _updateGeometry() is called automatically to clamp theta and refresh all computed properties. At small theta the segment is a thin sliver; near 360° it nearly encloses the full circle.

Parameters
  • sinusoid (SWSinusoid) — controls theta oscillation
  • t (number) — elapsed time in seconds
Example
// Theta oscillates between 20° and 340° over 3 seconds
let thetaSin = new SWSinusoid(180, 160, 1/3, 0); // center=180°, amp=160°, freq=0.33 Hz
seg.drawOnGrid(grid);
seg.breatheTheta(thetaSin, elapsedSeconds);

Color and Styling Methods

setFillColor(swColor)

Sets the fill color. A copy is made automatically.

Parameters
  • swColor (SWColor) — new fill color
Example
seg.setFillColor(new SWColor(300, 100, 100, 80, "magenta"));
resetFillColor()

Restores the fill color to the original stored at construction.

Example
seg.resetFillColor();
setStrokeColor(swColor)

Sets the border (stroke) color, applied to both the arc and the chord.

Parameters
  • swColor (SWColor) — new stroke color
Example
seg.setStrokeColor(swBlack);
setStrokeWeight(w)

Sets the border thickness in pixels.

Parameters
  • w (number) — thickness in pixels
Example
seg.setStrokeWeight(4);
setFillAlpha(alpha)

Sets the alpha (transparency) of the fill color. Clamped to [0, 100]; updates the underlying p5.js color immediately.

Parameters
  • alpha (number) — 0 = fully transparent, 100 = fully opaque
Example
seg.setFillAlpha(60); // 60% opaque fill
setStrokeAlpha(alpha)

Sets the alpha of the stroke (border) color. Clamped to [0, 100].

Parameters
  • alpha (number) — 0 = fully transparent, 100 = fully opaque
Example
seg.setStrokeAlpha(80);

Geometry Methods

setRadius(r)

Sets the radius and automatically updates arcLength, chordLength, sagitta, and area.

Parameters
  • r (number) — new radius in user units
Example
seg.setRadius(7);
console.log(`chord: ${seg.chordLength.toFixed(2)}, sagitta: ${seg.sagitta.toFixed(2)}`);
setTheta(degrees)

Sets the central angle (theta) and automatically updates all computed geometry. Value is clamped to [0, 360].

Parameters
  • degrees (number) — new angular size in degrees
Example
seg.setTheta(180); // half-circle segment — a perfect semicircle
setStartAngle(degrees)

Sets the static starting angle (degrees CCW from +x) without affecting accumulated rotation.

Parameters
  • degrees (number) — new start angle
Example
seg.setStartAngle(180); // arc begins on the left side
setShowCenter(show)

Controls whether the center SWPoint dot is drawn. The center dot is useful for dragging/repositioning in interactive demos.

Parameters
  • show (boolean) — true to show, false to hide (default: true)
Example
seg.setShowCenter(false);

Reset Methods

reset()

Restores all animated properties — radius, theta, startAngle, rotation, and fillColor — to their originals captured at construction.

Example
seg.reset(); // full restore to factory state

Utility Methods

static copy(other)

Returns a deep copy of an SWCircleSegment instance. All geometry and color values are independently duplicated.

Parameters
  • other (SWCircleSegment) — the segment to copy
Returns

SWCircleSegment — a new independent instance

Example
let copy = SWCircleSegment.copy(seg1);
toString()

Returns a string representation of the segment with all its key properties, including chordLength and sagitta.

Returns

string

Example
console.log(seg.toString());
// "SWCircleSegment(center: SWPoint(x: 0, y: 0), radius: 5.00, theta: 120.0°, startAngle: 0°, rotation: 0.0°, chordLength: 8.66, sagitta: 2.50, area: 9.06, arcLength: 10.47)"

Usage Examples

Example 1: Basic Segment with Grid

let grid;
let seg;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);

    grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});

    let center    = new SWPoint(0, 0);
    let fillColor = new SWColor(300, 100, 100, 80, "magenta");
    let strokeCol = new SWColor(300, 100, 50, 100, "darkMagenta");
    seg = new SWCircleSegment(center, 5, 120, 0, 2, fillColor, strokeCol);
}

function draw() {
    background(0, 0, 95);
    grid.draw();
    seg.drawOnGrid(grid);
}

Example 2: Spinning Segment (Elapsed-Time Approach)

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

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
    seg  = new SWCircleSegment(new SWPoint(0, 0), 5, 120, 0, 2,
                               new SWColor(300, 100, 100, 80, "magenta"), swBlack);
}

function draw() {
    background(0, 0, 95);
    grid.draw();

    // Compute deltaT in seconds; spin BEFORE drawing
    const t = millis() / 1000;
    const deltaT = (prevT > 0) ? (t - prevT) : 0;
    prevT = t;

    seg.rotate(SPIN_SPEED * deltaT);  // CCW positive
    seg.drawOnGrid(grid);
}

Example 3: Breathing Radius

let grid, seg, radSin;
let breathStart = 0, breathElapsed = 0;
let shouldBreathe = false;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    grid   = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
    seg    = new SWCircleSegment(new SWPoint(0, 0), 5, 120, 0, 2,
                                 new SWColor(300, 100, 100, 80, "magenta"), swBlack);
    // Center=5, amplitude=3 → range [2, 8], period=4 sec → freq=0.25 Hz
    radSin = new SWSinusoid(5, 3, 0.25, 0);
}

function draw() {
    background(0, 0, 95);
    grid.draw();
    seg.drawOnGrid(grid);

    if (shouldBreathe) {
        const t = millis() / 1000;
        breathElapsed += (t - breathStart);
        breathStart = t;
        seg.breatheRadius(radSin, breathElapsed);  // call AFTER draw
    }
    text(`radius: ${seg.radius.toFixed(2)}  chord: ${seg.chordLength.toFixed(2)}`, 10, 20);
}

function keyPressed() {
    if (key === 'b') {
        shouldBreathe = !shouldBreathe;
        breathStart = millis() / 1000;
    }
    if (key === 'r') { seg.reset(); breathElapsed = 0; }
}

Example 4: Breathing Theta (Crescent Pulsing)

let grid, seg, thetaSin;
let thetaStart = 0, thetaElapsed = 0;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    grid     = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
    seg      = new SWCircleSegment(new SWPoint(0, 0), 5, 180, 0, 2,
                                   new SWColor(300, 100, 100, 80, "magenta"), swBlack);
    // Theta oscillates between 20° and 340°, period=3 sec
    thetaSin = new SWSinusoid(180, 160, 1/3, 0);
    thetaStart = millis() / 1000;
}

function draw() {
    background(0, 0, 20); // dark background
    grid.draw();
    seg.drawOnGrid(grid);
    const t = millis() / 1000;
    thetaElapsed += (t - thetaStart);
    thetaStart = t;
    seg.breatheTheta(thetaSin, thetaElapsed);
}

Example 5: Simultaneous Spin + Radius Breathe + Hue Cycle

let grid, seg, radSin, hueSin;
let prevT = 0;
let rStart = 0, rElapsed = 0;
let hStart = 0, hElapsed = 0;

function setup() {
    createCanvas(400, 400);
    colorMode(HSB, 360, 100, 100, 100);
    grid   = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
    seg    = new SWCircleSegment(new SWPoint(0, 0), 5, 120, 0, 2,
                                 new SWColor(300, 100, 100, 80, "magenta"), swBlack);
    radSin = new SWSinusoid(5, 3, 0.25, 0); // radius 2–8
    hueSin = new SWSinusoid(180, 180, 1/3, 0); // hue 0–360
}

function draw() {
    background(0, 0, 95);
    grid.draw();

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

    // 1. Apply spin BEFORE draw
    seg.rotate(45 * deltaT);

    // 2. Apply hue cycling BEFORE draw
    hElapsed += deltaT;
    seg.fillColor.h = hueSin.getValue(hElapsed);
    seg.fillColor.col = color(seg.fillColor.h,
                               seg.fillColor.s,
                               seg.fillColor.b,
                               seg.fillColor.a);

    seg.drawOnGrid(grid);

    // 3. Apply breathing AFTER draw
    rElapsed += deltaT;
    seg.breatheRadius(radSin, rElapsed);
}

Example 6: Ring of Segments

let grid;
let segments = [];

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

    // 6 segments evenly distributed around the circle
    const count = 6;
    const theta = 40; // degrees — gap between segments
    for (let i = 0; i < count; i++) {
        const startAngle = i * (360 / count);
        const hue        = (i / count) * 360;
        const fillColor  = new SWColor(hue, 80, 90, 80, `seg${i}`);
        const strokeCol  = new SWColor(hue, 80, 50, 100, `stroke${i}`);
        segments.push(new SWCircleSegment(
            new SWPoint(0, 0), 6, theta, startAngle, 2, fillColor, strokeCol
        ));
    }
}

function draw() {
    background(0, 0, 95);
    grid.draw();
    segments.forEach(s => s.drawOnGrid(grid));
}

Best Practices

1. Animation Ordering

  • Spin/hue changes BEFORE draw: These affect what is rendered this frame
  • Breathing AFTER draw: The new value takes effect next frame, matching SWSector/SWDisk convention
seg.rotate(speed * deltaT);   // spin  → before draw
seg.drawOnGrid(grid);          // draw
seg.breatheRadius(sin, t);     // breathe → after draw

2. Elapsed Time vs. frameCount

  • Use elapsed seconds (not frame count) for all sinusoid time parameters; this decouples animation speed from frame rate
  • Track startTime and elapsed separately per animation so each can be paused and resumed independently
// Pattern for pauseable elapsed-time animation
let breathStart = 0, breathElapsed = 0, isBrething = false;

function toggleBreathe() {
    isBrething = !isBrething;
    if (isBrething) breathStart = millis() / 1000;
}

// In draw():
if (isBrething) {
    const t = millis() / 1000;
    breathElapsed += (t - breathStart);
    breathStart = t;
    seg.breatheRadius(radSin, breathElapsed);
}

3. Angle Convention

  • Always pass user-space degrees (CCW from +x) to SWCircleSegment; the class handles the p5.js y-flip internally
  • startAngle=0 means the arc begins at the +x axis and sweeps CCW by theta degrees
  • Positive rotate() deltas spin CCW; negative values spin CW

4. Color Management

  • Always pass SWColor instances; the constructor copies them to prevent shared-mutation bugs
  • For hue cycling, modify seg.fillColor.h directly and rebuild .col before drawing
  • Call reset() or resetFillColor() to cleanly restore original colors

5. SWSinusoid Setup for Breathing

  • The sinusoid's center value is the midpoint of the animation range
  • The amplitude is half the desired peak-to-peak swing
  • Example: range [2, 8] → center = 5, amplitude = 3
// Radius breathes between minVal and maxVal
const center    = (minVal + maxVal) / 2;
const amplitude = (maxVal - minVal) / 2;
const frequency = 1 / period;   // Hz (cycles per second)
let radSin = new SWSinusoid(center, amplitude, frequency, 0);

6. Theta Range for Breathe Theta

  • Keep the max theta below 360° (e.g., 340°) — at exactly 360° the chord collapses and the shape becomes a full circle with no visible chord
  • Keep the min theta above 0° — at 0° the segment disappears entirely

Integration with Other SketchWave Classes

Script Loading Order

Load dependencies before SWCircleSegment:

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

<!-- SketchWaveJS classes in dependency 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/swCircleSegment.js"></script>

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

Working with SWPoint

SWCircleSegment uses SWPoint for its center:

  • The center is a full SWPoint instance — it is drawn as a small dot when shouldShowCenter is true
  • Drag or reposition the segment by changing seg.center.x and seg.center.y
  • The center is the center of the generating circle, not a vertex of the chord; the segment does not include this point in its filled region
// Move center to user coordinates (2, -3)
seg.center.x = 2;
seg.center.y = -3;

Working with SWColor

SWCircleSegment uses SWColor for all color management (HSB mode):

  • Colors are automatically copied to prevent shared-mutation bugs
  • For hue cycling, mutate seg.fillColor.h then rebuild seg.fillColor.col
// Hue cycling by direct mutation (done before drawOnGrid)
seg.fillColor.h   = hueSin.getValue(hElapsed);
seg.fillColor.col = color(seg.fillColor.h,
                           seg.fillColor.s,
                           seg.fillColor.b,
                           seg.fillColor.a);

Working with SWGrid

SWCircleSegment integrates with SWGrid for coordinate mapping:

  • Use drawOnGrid(grid) to draw in user units — the grid converts both position and radius to screen pixels
  • The average of grid.xScale and grid.yScale is used for the radius, keeping the segment circular even on non-square grids

Working with SWSinusoid

SWCircleSegment's breathing methods accept SWSinusoid instances:

  • One sinusoid for breatheRadius(), a separate one for breatheTheta()
  • The sinusoid's getValue(t) is called with elapsed time in seconds

Comparing SWCircleSegment and SWSector

These two classes share the same constructor signature and animations but draw fundamentally different shapes:

Feature SWSector SWCircleSegment
Shape Pizza slice (two radii + arc) Crescent (chord + arc only)
p5 arc mode PIE CHORD
Anchor point vertex — the tip (included in shape) center — the circle center (outside the filled region)
Computed area (theta/360) × πr² (sector area) r²/2 × (θ − sin θ) (segment area)
Extra properties arcLength, area arcLength, chordLength, sagitta, area
Show anchor setShowVertex() setShowCenter()

Source Code

The complete SWCircleSegment class implementation:

Show/Hide Source Code
/*
File: swCircleSegment.js
Date: 2026-04-21
Author: klp
App:  SketchWaveTNT2026-04-21-Stg8
Purpose: SWCircleSegment class for SketchWaveJS

SWCircleSegment represents a circular segment -- the region between a chord
and its subtended arc -- defined by:
  - A center SWPoint (center of the generating circle)
  - A radius (in user units)
  - A theta (angular size / central angle in degrees, clamped 0-360)
  - A startAngle (degrees CCW from the +x axis, where the arc begins)
  - A rotation offset (degrees, accumulated via rotate())

Unlike SWSector (pizza slice), a segment does NOT connect to the center.
The shape is closed by a chord drawn directly from one arc endpoint to the
other. In p5.js this is the CHORD arc mode.

Default orientation: arc begins along the +x axis and sweeps CCW through
theta degrees. Set startAngle to establish a different static starting
position before rotation begins.

Animations (all independent, all composable):
  - rotate(delta): spins the segment about the circle's center.
  - breatheRadius(sinusoid, t): oscillates the radius with an SWSinusoid.
  - breatheTheta(sinusoid, t):  oscillates the angular size with an SWSinusoid.

Color cycling: mutate fillColor.h and rebuild fillColor.col before drawing,
  just like SWSector / SWDisk.

Angle convention:
  User space (math):   angles are CCW from +x axis, y increases upward.
  p5 / screen space:   angles are CW  from +x axis, y increases downward.
  Conversion:   p5_angle = -user_angle  (negate because y is flipped).

Drawing a segment whose arc start is at totalAngle = startAngle + rotation,
spanning theta degrees CCW:
  p5 arc start = -radians(totalAngle + theta)
  p5 arc stop  = -radians(totalAngle)
  arc(cx, cy, 2r, 2r, p5Start, p5Stop, CHORD)  -- p5 arc is clockwise.

Geometric extras (recomputed by _updateGeometry):
  arcLength   = (theta/360) * 2*PI * r
  chordLength = 2 * r * sin(theta_rad / 2)
  sagitta     = r * (1 - cos(theta_rad / 2))   (height of the segment)
  area        = (theta_rad/2 - sin(theta_rad)/2) * r^2  (segment area formula)

Notes:
  - Assumes p5.js, SWColor, SWPoint, SWGrid, SWSinusoid are loaded.
  - Consistent API with SWSector, SWDisk, SWLine, SWTriangle, etc.
  - setFillAlpha / setStrokeAlpha clamp alpha to [0, 100].
*/

console.log("[swCircleSegment.js] SWCircleSegment class loaded.");

class SWCircleSegment {

    constructor(center, radius, theta, startAngle = 0, thickness = 2,
                fillColor = undefined, strokeColor = undefined) {

        this.center     = center;
        this.radius     = radius;
        this.theta      = theta;
        this.startAngle = startAngle;
        this.rotation   = 0;
        this.thickness  = thickness;

        this.fillColor   = fillColor   ? SWColor.copy(fillColor)   : undefined;
        this.strokeColor = strokeColor ? SWColor.copy(strokeColor) : undefined;

        this.originalRadius     = radius;
        this.originalTheta      = theta;
        this.originalStartAngle = startAngle;
        this.originalRotation   = 0;
        this.originalFillColor  = fillColor ? SWColor.copy(fillColor) : undefined;

        this.shouldShowCenter = true;

        this._updateGeometry();
    }

    _updateGeometry() {
        this.theta          = Math.max(0, Math.min(360, this.theta));
        const thetaRad      = this.theta * Math.PI / 180;
        this.arcLength      = (this.theta / 360) * 2 * Math.PI * this.radius;
        this.chordLength    = 2 * this.radius * Math.sin(thetaRad / 2);
        this.sagitta        = this.radius * (1 - Math.cos(thetaRad / 2));
        this.area           = 0.5 * this.radius * this.radius * (thetaRad - Math.sin(thetaRad));
    }

    draw() {
        this._drawArc(this.center.x, this.center.y, this.radius);
        if (this.shouldShowCenter && this.center && this.center.draw) {
            this.center.draw(this.strokeColor);
        }
    }

    drawOnGrid(grid) {
        const { x: cx, y: cy } = grid.userToScreen(this.center.x, this.center.y);
        const rScreen = (grid.xScale * this.radius + grid.yScale * this.radius) / 2;
        this._drawArc(cx, cy, rScreen);
        if (this.shouldShowCenter && this.center && this.center.drawOnGrid) {
            this.center.drawOnGrid(grid, this.strokeColor);
        }
    }

    _drawArc(cx, cy, r) {
        const totalAngle = this.startAngle + this.rotation;
        const p5Start = -radians(totalAngle + this.theta);
        const p5Stop  = -radians(totalAngle);

        if (this.fillColor && this.fillColor.col) {
            fill(this.fillColor.col);
        } else {
            noFill();
        }
        if (this.strokeColor && this.strokeColor.col) {
            stroke(this.strokeColor.col);
        } else {
            noStroke();
        }
        strokeWeight(this.thickness);
        arc(cx, cy, 2 * r, 2 * r, p5Start, p5Stop, CHORD);
        noStroke();
        noFill();
        strokeWeight(1);
    }

    rotate(deltaAngle) {
        this.rotation += deltaAngle;
    }

    breatheRadius(sinusoid, t) {
        this.radius = Math.max(0.01, sinusoid.getValue(t));
        this._updateGeometry();
    }

    breatheTheta(sinusoid, t) {
        this.theta = sinusoid.getValue(t);
        this._updateGeometry();
    }

    setFillAlpha(alpha) {
        if (this.fillColor) {
            this.fillColor.a = Math.max(0, Math.min(100, alpha));
            this.fillColor.col = color(
                this.fillColor.h, this.fillColor.s,
                this.fillColor.b, this.fillColor.a
            );
        }
    }

    setStrokeAlpha(alpha) {
        if (this.strokeColor) {
            this.strokeColor.a = Math.max(0, Math.min(100, alpha));
            this.strokeColor.col = color(
                this.strokeColor.h, this.strokeColor.s,
                this.strokeColor.b, this.strokeColor.a
            );
        }
    }

    reset() {
        this.radius     = this.originalRadius;
        this.theta      = this.originalTheta;
        this.startAngle = this.originalStartAngle;
        this.rotation   = this.originalRotation;
        if (this.originalFillColor) {
            this.fillColor = SWColor.copy(this.originalFillColor);
        }
        this._updateGeometry();
    }

    resetFillColor() {
        if (this.originalFillColor) {
            this.fillColor = SWColor.copy(this.originalFillColor);
        }
    }

    setFillColor(swColor)    { this.fillColor   = swColor ? SWColor.copy(swColor) : undefined; }
    setStrokeColor(swColor)  { this.strokeColor = swColor ? SWColor.copy(swColor) : undefined; }
    setStrokeWeight(w)       { this.thickness   = w; }
    setRadius(r)             { this.radius      = r; this._updateGeometry(); }
    setTheta(degrees)        { this.theta       = degrees; this._updateGeometry(); }
    setStartAngle(degrees)   { this.startAngle  = degrees; }
    setShowCenter(show = true) { this.shouldShowCenter = show; }

    static copy(other) {
        if (!(other instanceof SWCircleSegment)) {
            throw new Error('Argument to SWCircleSegment.copy must be an SWCircleSegment instance');
        }
        const s = new SWCircleSegment(
            SWPoint.copy(other.center),
            other.originalRadius,
            other.originalTheta,
            other.originalStartAngle,
            other.thickness,
            other.fillColor,
            other.strokeColor
        );
        s.radius           = other.radius;
        s.theta            = other.theta;
        s.startAngle       = other.startAngle;
        s.rotation         = other.rotation;
        s.shouldShowCenter = other.shouldShowCenter;
        s._updateGeometry();
        return s;
    }

    toString() {
        return `SWCircleSegment(center: ${this.center.toString()}, ` +
               `radius: ${this.radius.toFixed(2)}, theta: ${this.theta.toFixed(1)}°, ` +
               `startAngle: ${this.startAngle}°, rotation: ${this.rotation.toFixed(1)}°, ` +
               `chordLength: ${this.chordLength.toFixed(2)}, sagitta: ${this.sagitta.toFixed(2)}, ` +
               `area: ${this.area.toFixed(2)}, arcLength: ${this.arcLength.toFixed(2)})`;
    }

}//end SWCircleSegment class