SketchWave Line Class Reference

Styled line segments for dynamic geometry and animation

Back to SWLine Demo 1 Back to SWLine Demo 2 Back to SWLine Demo 3

Overview

SWLine represents a line segment between two points in 2D space, designed for use with the SketchWaveJS framework and p5.js. Unlike a simple p5.js line, SWLine is a rich geometric object that tracks its endpoints, midpoint, length, slope, and provides advanced features like rotation, breathing animations, and interactive manipulation.

Key Features

  • Endpoint Management: References two SWPoint objects that define the line segment
  • Automatic Midpoint: Calculates and maintains a labeled midpoint (SWPoint instance)
  • Geometric Properties: Automatically computes length, slope, and orientation (vertical/horizontal)
  • Dual Coordinate Systems: Can be drawn in screen or grid (user) coordinates
  • Animation Support: Built-in methods for breathing (length modulation) and rotation
  • Interactive Dragging: Endpoints and midpoint can be made draggable for user interaction
  • Customizable Appearance: Control thickness, color, and visibility of endpoints and midpoint

Design Philosophy

SWLine is designed as a reference-based geometry object. It doesn't store point coordinates internally; instead, it maintains references to SWPoint objects. This means when you move an endpoint, the line automatically updates. This design enables:

  • Real-time geometric transformations and animations
  • Interactive manipulation where users can drag points or the entire segment
  • Dynamic recalculation of properties (length, slope, midpoint) as endpoints move
  • Clean separation between geometric data (points) and geometric relationships (lines)
💡 Tip: SWLine shines in educational contexts (teaching geometry concepts), data visualization (connecting related points), animations (rotating clock hands, oscilloscopes), and interactive applications (user-drawn diagrams).

Dependencies

SWLine requires the following SketchWaveJS classes and libraries:

  • p5.js: Core drawing and animation library
  • SWPoint: Endpoint and midpoint representation
  • SWColor: Color management for line stroke and midpoint
  • SWGrid: Grid coordinate system for drawOnGrid()
  • SWSinusoid: (Optional) For breathing animations

Constructor

new SWLine(ptA, ptB, thickness, strokeColor)

Creates a new SWLine instance connecting two SWPoint objects.

Parameters

  • ptA (SWPoint) - First endpoint
  • ptB (SWPoint) - Second endpoint
  • thickness (number, optional) - Line stroke weight (default: 2)
  • strokeColor (SWColor, optional) - Line color (default: current stroke)

Automatic Initialization

When created, SWLine automatically:

  • Calculates the segment length using the distance formula
  • Computes the slope and determines if the line is vertical or horizontal
  • Creates a midpoint SWPoint with label "M" and a slightly darker color
  • Stores original endpoint positions for rotation animations
  • Sets the midpoint weight to 2× the line thickness for visual prominence

Examples

// Simple line with defaults
let lineAB = new SWLine(new SWPoint(0, 0), new SWPoint(5, 5));

// Styled line
let lineCD = new SWLine(
    new SWPoint(-5, 0), 
    new SWPoint(5, 0), 
    3, 
    swRed
);

// Using existing points
let ptA = new SWPoint(-3, 4, undefined, 10, swBlue, "A");
let ptB = new SWPoint(3, -4, undefined, 10, swRed, "B");
let lineAB = new SWLine(ptA, ptB, 4, swPurple);

Properties

All properties can be read directly. Some can be modified, which will affect subsequent drawing and behavior.

Endpoint Properties

Property Type Description
ptA SWPoint Reference to the first endpoint. Modify ptA.x or ptA.y to move the endpoint.
ptB SWPoint Reference to the second endpoint. Modify ptB.x or ptB.y to move the endpoint.
originalA SWPoint Stored copy of ptA's original position (used for rotation animations).
originalB SWPoint Stored copy of ptB's original position (used for rotation animations).

Geometric Properties

Property Type Description
length number Distance between ptA and ptB. Computed in constructor; recalculate after moving endpoints.
slope number Rise over run: (ptB.y - ptA.y) / (ptB.x - ptA.x). Set to Infinity for vertical lines.
isVertical boolean true if the line is vertical (slope = Infinity).
isHorizontal boolean true if the line is horizontal (slope ≈ 0).
midpoint SWPoint The line's midpoint as an SWPoint instance with label "M". Update after moving endpoints.

Appearance Properties

Property Type Description
thickness number Stroke weight of the line segment (default: 2).
strokeColor SWColor Color of the line segment (SWColor instance or undefined).
shouldShowSegment boolean If true, the line segment is drawn. Set to false to hide the line but still show endpoints/midpoint.
shouldShowEndPoints boolean If true, ptA and ptB are drawn when the line is drawn (default: true).
shouldShowMidpoint boolean If true, the midpoint is drawn when the line is drawn (default: true).
📝 Note: When you manually change endpoint positions, remember to recalculate length, slope, and midpoint coordinates for accurate geometry. The demo applications show how to do this dynamically.

Methods

Drawing Methods

draw() → void

Draws the line segment in screen coordinates using p5.js primitives.

Behavior

  • Applies strokeColor and thickness
  • Draws the segment if shouldShowSegment is true
  • Draws endpoints if shouldShowEndPoints is true
  • Draws midpoint if shouldShowMidpoint is true
Example
function draw() {
    background(220);
    lineAB.draw(); // Draws in screen coordinates
}
drawOnGrid(grid) → void

Draws the line in user (grid) coordinates, converting to screen coordinates via the provided SWGrid.

Parameters

  • grid (SWGrid) - The grid providing coordinate transformation

Example

function draw() {
    background(220);
    grid.draw();
    lineAB.drawOnGrid(grid); // Draws in grid coordinates
}

Animation Methods

breathe(sinusoid, t) → void

Modulates the line's length over time using a sinusoid, keeping the midpoint fixed while moving endpoints symmetrically.

Parameters

  • sinusoid (SWSinusoid) - Sinusoid defining the new length over time
  • t (number) - Time parameter (typically frameCount / framerate)

How It Works

The method evaluates sinusoid.getValue(t) to get the new length, then moves ptA and ptB symmetrically away from or toward the midpoint along the line's direction.

Example

// Create a breathing sinusoid: oscillates from length 4 to 12
// SWSinusoid(amplitude, frequency, verticalShift, phaseShift)
const minLength = 4;
const maxLength = 12;
const period = 3.0; // seconds

const amp = (maxLength - minLength) / 2;  // 4
const freq = (2 * Math.PI) / period;      // ~2.09
const mid = (minLength + maxLength) / 2;  // 8
let breathe = new SWSinusoid(amp, freq, mid, 0);

function draw() {
    background(220);
    grid.draw();
    
    // Apply breathing animation
    lineAB.breathe(breathe, frameCount * 0.02);
    lineAB.drawOnGrid(grid);
}
💡 Tip: Use breathing for visual effects like pulsing connectors, oscilloscope traces, or to demonstrate harmonic motion in educational contexts.
breatheAbout(fixedPt, sinusoid, t) → void

Modulates the line's length while keeping one endpoint fixed, creating a "bungee rope" effect where the fixed endpoint stays anchored and the other endpoint oscillates toward/away from it.

Parameters

  • fixedPt (SWPoint) - The endpoint to keep fixed (must be this.ptA or this.ptB)
  • sinusoid (SWSinusoid) - Sinusoid defining the new length over time
  • t (number) - Time parameter (typically frameCount / framerate)

How It Works

The method keeps fixedPt at its original position while moving the other endpoint along the line's direction to achieve the length specified by sinusoid.getValue(t). This differs from breathe() which keeps the midpoint fixed.

Example

// Create a radius line rotating about a fixed center
let center = new SWPoint(-5, 0, undefined, 8, swBlack, "C");
let radius = new SWPoint(-2, 0, undefined, 8, swRed, "R");
let lineCR = new SWLine(center, radius, 4, swRed);

// Length oscillates from 1 to 5 (center fixed, radius breathes)
const minLength = 1, maxLength = 5, period = 4.0;
const amp = (maxLength - minLength) / 2;  // 2
const freq = (2 * Math.PI) / period;      // ~1.57
const mid = (minLength + maxLength) / 2;  // 3
let breathe = new SWSinusoid(amp, freq, mid, 0);

function draw() {
    background(220);
    grid.draw();
    
    // Center stays fixed, radius endpoint oscillates
    lineCR.breatheAbout(lineCR.ptA, breathe, frameCount * 0.02);
    lineCR.drawOnGrid(grid);
}
💡 Tip: Perfect for unit circle demos, clock hands, or any animation where one end should remain anchored while the other end moves.
rotateAboutMidPoint(degPerSec, t) → void

Rotates the line segment around its midpoint at a constant angular velocity.

Parameters

  • degPerSec (number) - Angular velocity in degrees per second (positive = counterclockwise)
  • t (number) - Time in seconds

Behavior

Rotates from the originalA and originalB positions, ensuring consistent rotation regardless of prior transformations. The midpoint remains stationary. Positive degPerSec rotates counterclockwise, negative values rotate clockwise.

Example

function draw() {
    background(220);
    grid.draw();
    
    // Rotate at 45 degrees per second
    lineAB.rotateAboutMidPoint(45, frameCount / frameRate());
    lineAB.drawOnGrid(grid);
}
rotateAbout(fixedPt, degPerSec, t) → void

Rotates the line about a specified endpoint (either ptA or ptB), keeping that point fixed.

Parameters

  • fixedPt (SWPoint) - The endpoint to rotate around (must be this.ptA or this.ptB)
  • degPerSec (number) - Angular velocity in degrees per second
  • t (number) - Time in seconds

Example

function draw() {
    background(220);
    grid.draw();
    
    // Rotate about endpoint A (like a clock hand)
    lineAB.rotateAbout(lineAB.ptA, 90, frameCount / frameRate());
    lineAB.drawOnGrid(grid);
}
transform({sinusoid, t, degPerSec}) → void

Combines breathing and rotation in a single transformation, applying both effects from the original endpoint positions.

Parameters (Object)

  • sinusoid (SWSinusoid, optional) - For breathing effect
  • t (number) - Time parameter
  • degPerSec (number, optional) - For rotation effect

Example

// Breathing from length 2 to 14, period 4 seconds
const minLength = 2;
const maxLength = 14;
const period = 4.0;

const amp = (maxLength - minLength) / 2;  // 6
const freq = (2 * Math.PI) / period;      // ~1.57
const mid = (minLength + maxLength) / 2;  // 8
let breathe = new SWSinusoid(amp, freq, mid, 0);

function draw() {
    background(220);
    grid.draw();
    
    // Combine breathing and rotation (30 deg/sec)
    lineAB.transform({
        sinusoid: breathe,
        degPerSec: 30,
        t: frameCount * 0.02
    });
    
    lineAB.drawOnGrid(grid);
}
transformAbout(fixedPt, {sinusoid, breatheTime, degPerSec, rotateTime}) → void

Combines breathing and rotation about a fixed endpoint with independent time parameters for each effect.

Parameters

  • fixedPt (SWPoint) - The endpoint to keep fixed (must be this.ptA or this.ptB)
  • sinusoid (SWSinusoid, optional) - For breathing effect
  • breatheTime (number, optional) - Time parameter for breathing
  • degPerSec (number, optional) - Angular velocity for rotation
  • rotateTime (number, optional) - Time parameter for rotation

Why Separate Time Parameters?

Having separate breatheTime and rotateTime allows independent control of each effect. You can pause one animation while continuing the other, or have them run at different rates.

Example

// Unit circle with breathing radius that rotates
let center = new SWPoint(-5, 0, undefined, 8, swBlack, "C");
let radius = new SWPoint(-2, 0, undefined, 8, swRed, "R");
let lineCR = new SWLine(center, radius, 4, swRed);

const minLength = 1, maxLength = 5, period = 4.0;
const amp = (maxLength - minLength) / 2;
const freq = (2 * Math.PI) / period;
const mid = (minLength + maxLength) / 2;
let breathe = new SWSinusoid(amp, freq, mid, 0);

let breatheTime = 0, rotateTime = 0;
let breathingOn = true, rotatingOn = true;

function draw() {
    background(220);
    grid.draw();
    
    // Update times independently
    if (breathingOn) breatheTime += deltaTime / 1000;
    if (rotatingOn) rotateTime += deltaTime / 1000;
    
    // Combine both effects with separate time controls
    lineCR.transformAbout(lineCR.ptA, {
        sinusoid: breathe,
        breatheTime: breatheTime,
        degPerSec: 90,
        rotateTime: rotateTime
    });
    
    lineCR.drawOnGrid(grid);
}
💡 Tip: This method is essential for complex animations where you need both breathing and rotation about a fixed point, such as unit circle demos or planetary motion simulations.

Utility Methods

toString() → string

Returns a string representation of the line with key properties.

Example

console.log(lineAB.toString());
// Output: "SWLine(ptA: SWPoint(...), ptB: SWPoint(...), thickness: 3, 
//          strokeColor: SWColor(...), length: 7.07, midpoint: SWPoint(...))"

Usage Examples

Example 1: Simple Static Line

let grid, lineAB;

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)});
    
    let ptA = new SWPoint(-5, -5, undefined, 8, swRed, "A");
    let ptB = new SWPoint(5, 5, undefined, 8, swBlue, "B");
    lineAB = new SWLine(ptA, ptB, 3, swPurple);
    
    noLoop();
}

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

Example 2: Interactive Draggable Line

Enable users to drag endpoints or the midpoint to reposition the line.

let grid, lineAB;
let draggedPoint = null;
let isDraggingMidpoint = false;

function setup() {
    createCanvas(600, 600);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();
    
    grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
    
    let ptA = new SWPoint(-3, 2, undefined, 12, swRed, "A");
    let ptB = new SWPoint(4, -3, undefined, 12, swBlue, "B");
    ptA.setDraggable(true);
    ptB.setDraggable(true);
    
    lineAB = new SWLine(ptA, ptB, 4, swGreen);
    lineAB.midpoint.strokeColor = swOrange;
}

function draw() {
    background(240);
    grid.draw();
    lineAB.drawOnGrid(grid);
}

function mousePressed() {
    // Check midpoint first
    if (lineAB.midpoint.containsPoint(mouseX, mouseY, grid, 15)) {
        isDraggingMidpoint = true;
        return;
    }
    
    // Check endpoints
    if (lineAB.ptA.containsPoint(mouseX, mouseY, grid, 15)) {
        draggedPoint = lineAB.ptA;
    } else if (lineAB.ptB.containsPoint(mouseX, mouseY, grid, 15)) {
        draggedPoint = lineAB.ptB;
    }
}

function mouseDragged() {
    if (isDraggingMidpoint) {
        // Move entire segment
        const userCoords = grid.screenToUser(mouseX, mouseY);
        const dx = userCoords.x - lineAB.midpoint.x;
        const dy = userCoords.y - lineAB.midpoint.y;
        
        lineAB.ptA.setPosition(lineAB.ptA.x + dx, lineAB.ptA.y + dy);
        lineAB.ptB.setPosition(lineAB.ptB.x + dx, lineAB.ptB.y + dy);
        lineAB.midpoint.x = userCoords.x;
        lineAB.midpoint.y = userCoords.y;
    } else if (draggedPoint) {
        // Move one endpoint
        const userCoords = grid.screenToUser(mouseX, mouseY);
        draggedPoint.setPosition(userCoords.x, userCoords.y);
        
        // Recalculate line properties
        lineAB.length = Math.sqrt(
            Math.pow(lineAB.ptB.x - lineAB.ptA.x, 2) + 
            Math.pow(lineAB.ptB.y - lineAB.ptA.y, 2)
        );
        lineAB.slope = (lineAB.ptB.y - lineAB.ptA.y) / (lineAB.ptB.x - lineAB.ptA.x);
        lineAB.midpoint.x = (lineAB.ptA.x + lineAB.ptB.x) / 2;
        lineAB.midpoint.y = (lineAB.ptA.y + lineAB.ptB.y) / 2;
    }
    redraw();
}

function mouseReleased() {
    draggedPoint = null;
    isDraggingMidpoint = false;
}

Example 3: Rotating Clock Hand

let grid, hourHand, minuteHand;

function setup() {
    createCanvas(500, 500);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();
    
    grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
    
    // Clock center
    let center = new SWPoint(0, 0, undefined, 15, swBlack, "");
    
    // Hour hand (shorter)
    let hourEnd = new SWPoint(0, 5, undefined, 8, swBlack);
    hourHand = new SWLine(center, hourEnd, 6, swBlack);
    hourHand.shouldShowMidpoint = false;
    
    // Minute hand (longer)
    let minEnd = new SWPoint(0, 8, undefined, 8, swDarkGray);
    minuteHand = new SWLine(center, minEnd, 3, swDarkGray);
    minuteHand.shouldShowMidpoint = false;
    
    frameRate(30);
}

function draw() {
    background(250);
    grid.draw();
    
    const t = frameCount / frameRate();
    
    // Hour hand: 30 deg/hour = 0.5 deg/min = 0.008333 deg/sec
    hourHand.rotateAbout(hourHand.ptA, 0.008333 * 60, t);
    
    // Minute hand: 360 deg/hour = 6 deg/min = 0.1 deg/sec
    minuteHand.rotateAbout(minuteHand.ptA, 0.1 * 60, t);
    
    hourHand.drawOnGrid(grid);
    minuteHand.drawOnGrid(grid);
}

Example 4: Breathing Line Animation

let grid, line;
let breatheSinusoid;

function setup() {
    createCanvas(600, 400);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();
    
    grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
    
    let ptA = new SWPoint(-4, 0, undefined, 10, swCyan, "A");
    let ptB = new SWPoint(4, 0, undefined, 10, swMagenta, "B");
    line = new SWLine(ptA, ptB, 5, swBlue);
    
    // Length oscillates from 5 to 11 (center: 8, amplitude: 3)
    // SWSinusoid(amplitude, frequency, verticalShift, phaseShift)
    const minLen = 5, maxLen = 11, period = 2.0;
    const amp = (maxLen - minLen) / 2;        // 3
    const freq = (2 * Math.PI) / period;      // ~3.14
    const mid = (minLen + maxLen) / 2;        // 8
    breatheSinusoid = new SWSinusoid(amp, freq, mid, 0);
    
    frameRate(30);
}

function draw() {
    background(230);
    grid.draw();
    
    line.breathe(breatheSinusoid, frameCount * 0.02);
    line.drawOnGrid(grid);
    
    // Display current length
    fill(0);
    textAlign(LEFT);
    text(`Length: ${line.length.toFixed(2)}`, 10, 20);
}

Example 5: Spinning and Breathing Together

let grid, line;
let breatheSinusoid;

function setup() {
    createCanvas(600, 600);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();
    
    grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
    
    let ptA = new SWPoint(-5, 0, undefined, 10, swOrange, "A");
    let ptB = new SWPoint(5, 0, undefined, 10, swPurple, "B");
    line = new SWLine(ptA, ptB, 6, swTeal);
    
    // Length oscillates from 4 to 16 (center: 10, amplitude: 6)
    const minLen = 4, maxLen = 16, period = 3.0;
    const amp = (maxLen - minLen) / 2;        // 6
    const freq = (2 * Math.PI) / period;      // ~2.09
    const mid = (minLen + maxLen) / 2;        // 10
    breatheSinusoid = new SWSinusoid(amp, freq, mid, 0);
    
    frameRate(30);
}

function draw() {
    background(240);
    grid.draw();
    
    // Combine breathing and rotation
    line.transform({
        sinusoid: breatheSinusoid,
        degPerSec: 60,
        t: frameCount * 0.02
    });
    
    line.drawOnGrid(grid);
}

Example 6: Network Graph Connections

Use SWLine to connect multiple points in a network or constellation.

let grid;
let nodes = [];
let connections = [];

function setup() {
    createCanvas(600, 600);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();
    
    grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
    
    // Create 8 nodes in a circle
    const numNodes = 8;
    const radius = 7;
    
    for (let i = 0; i < numNodes; i++) {
        const angle = (i / numNodes) * TWO_PI;
        const x = radius * cos(angle);
        const y = radius * sin(angle);
        const hue = (i / numNodes) * 360;
        const color = new SWColor(hue, 80, 90, 100, `node${i}`);
        const node = new SWPoint(x, y, undefined, 12, color, `${i}`);
        nodes.push(node);
    }
    
    // Connect each node to the next two nodes
    for (let i = 0; i < numNodes; i++) {
        const line1 = new SWLine(nodes[i], nodes[(i + 1) % numNodes], 2, swLightGray);
        const line2 = new SWLine(nodes[i], nodes[(i + 2) % numNodes], 2, swLightGray);
        line1.shouldShowEndPoints = false;
        line1.shouldShowMidpoint = false;
        line2.shouldShowEndPoints = false;
        line2.shouldShowMidpoint = false;
        connections.push(line1, line2);
    }
    
    noLoop();
}

function draw() {
    background(240);
    grid.draw();
    
    // Draw connections first (behind nodes)
    connections.forEach(conn => conn.drawOnGrid(grid));
    
    // Draw nodes on top
    nodes.forEach(node => node.drawOnGrid(grid));
}

Example 7: Slope Visualization Tool

Educational tool showing how slope changes as you drag endpoints.

let grid, line;
let draggedPoint = null;

function setup() {
    createCanvas(700, 500);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();
    
    grid = new SWGrid({UL: new SWPoint(-10, 10), LR: new SWPoint(10, -10)});
    
    let ptA = new SWPoint(-5, -3, undefined, 14, swRed, "A");
    let ptB = new SWPoint(4, 5, undefined, 14, swBlue, "B");
    ptA.setDraggable(true);
    ptB.setDraggable(true);
    
    line = new SWLine(ptA, ptB, 4, swGreen);
}

function draw() {
    background(250);
    grid.draw();
    line.drawOnGrid(grid);
    
    // Display slope information
    fill(0);
    textSize(16);
    textAlign(LEFT);
    
    const rise = line.ptB.y - line.ptA.y;
    const run = line.ptB.x - line.ptA.x;
    
    text(`Point A: (${line.ptA.x.toFixed(1)}, ${line.ptA.y.toFixed(1)})`, 10, 25);
    text(`Point B: (${line.ptB.x.toFixed(1)}, ${line.ptB.y.toFixed(1)})`, 10, 50);
    text(`Rise: ${rise.toFixed(2)}`, 10, 75);
    text(`Run: ${run.toFixed(2)}`, 10, 100);
    
    if (line.isVertical) {
        text(`Slope: undefined (vertical line)`, 10, 125);
    } else if (Math.abs(line.slope) < 0.001) {
        text(`Slope: 0 (horizontal line)`, 10, 125);
    } else {
        text(`Slope: ${line.slope.toFixed(3)}`, 10, 125);
    }
    
    text(`Length: ${line.length.toFixed(2)}`, 10, 150);
}

function mousePressed() {
    if (line.ptA.containsPoint(mouseX, mouseY, grid, 15)) {
        draggedPoint = line.ptA;
    } else if (line.ptB.containsPoint(mouseX, mouseY, grid, 15)) {
        draggedPoint = line.ptB;
    }
}

function mouseDragged() {
    if (draggedPoint) {
        const userCoords = grid.screenToUser(mouseX, mouseY);
        draggedPoint.setPosition(userCoords.x, userCoords.y);
        
        // Recalculate
        lineAB.length = Math.sqrt(
            Math.pow(line.ptB.x - line.ptA.x, 2) + 
            Math.pow(line.ptB.y - line.ptA.y, 2)
        );
        
        if (line.ptB.x - line.ptA.x === 0) {
            line.slope = Infinity;
            line.isVertical = true;
            line.isHorizontal = false;
        } else {
            line.slope = (line.ptB.y - line.ptA.y) / (line.ptB.x - line.ptA.x);
            line.isVertical = false;
            line.isHorizontal = Math.abs(line.slope) < 0.001;
        }
        
        line.midpoint.x = (line.ptA.x + line.ptB.x) / 2;
        line.midpoint.y = (line.ptA.y + line.ptB.y) / 2;
        
        redraw();
    }
}

function mouseReleased() {
    draggedPoint = null;
}

Best Practices

1. Always Recalculate After Manual Endpoint Movement

When you manually change ptA.x, ptA.y, ptB.x, or ptB.y, remember to update the line's geometric properties:

// After moving endpoints manually
line.length = Math.sqrt(
    Math.pow(line.ptB.x - line.ptA.x, 2) + 
    Math.pow(line.ptB.y - line.ptA.y, 2)
);

if (line.ptB.x === line.ptA.x) {
    line.slope = Infinity;
    line.isVertical = true;
    line.isHorizontal = false;
} else {
    line.slope = (line.ptB.y - line.ptA.y) / (line.ptB.x - line.ptA.x);
    line.isVertical = false;
    line.isHorizontal = Math.abs(line.slope) < 0.001;
}

line.midpoint.x = (line.ptA.x + line.ptB.x) / 2;
line.midpoint.y = (line.ptA.y + line.ptB.y) / 2;

2. Use Animation Methods for Transformations

Instead of manually calculating rotations or length changes, use the built-in methods (breathe, rotateAboutMidPoint, transform) which handle the math and maintain consistency.

3. Control Endpoint and Midpoint Visibility

For cleaner visualizations, hide endpoints or midpoints when they're not needed:

// Hide everything except the line segment
line.shouldShowEndPoints = false;
line.shouldShowMidpoint = false;

// Or hide just the midpoint
line.shouldShowMidpoint = false;

4. Coordinate System Consistency

  • Use draw() when working purely in screen pixels (rare)
  • Use drawOnGrid(grid) for mathematical/geometric applications where user coordinates matter
  • Don't mix the two in the same sketch

5. Interactive Applications

For draggable lines:

  • Check midpoint containment first (before endpoints) for intuitive drag priority
  • Set isDraggable on endpoints for clear state management
  • Use containsPoint() with a generous tolerance (e.g., 15 pixels) for easy clicking
  • Always call redraw() or use continuous draw() loops for real-time updates

6. Performance Considerations

  • Avoid creating new SWLine instances every frame; create once in setup()
  • For static diagrams, use noLoop() and redraw() on events
  • When drawing many lines, consider hiding midpoints and using thinner strokes

7. Educational Uses

SWLine is excellent for teaching:

  • Slope: Let students drag endpoints and watch slope update in real time
  • Distance Formula: Show live length calculations
  • Midpoint Formula: Visualize the midpoint as endpoints move
  • Transformations: Demonstrate rotations and dilations
  • Harmonic Motion: Use breathing animations to show oscillations

8. Color and Style

// Give different visual weights to different parts
line.thickness = 4;
line.strokeColor = swPurple;
line.ptA.strokeColor = swRed;       // Red endpoint
line.ptB.strokeColor = swBlue;      // Blue endpoint
line.midpoint.strokeColor = swGreen; // Green midpoint

// Midpoint is automatically darker; you can override:
line.midpoint.strokeColor = new SWColor(120, 70, 60, 100, "darkGreen");

9. Combining with Other Classes

SWLine integrates seamlessly with other SketchWaveJS classes:

  • SWDisk: Draw circles at endpoints or midpoint for emphasis
  • SWSinusoid: Animate line properties (length, rotation)
  • SWGrid: Essential for mathematical/coordinate-based work

10. Debugging Tips

// Use toString() for quick inspection
console.log(line.toString());

// Check slope for vertical/horizontal
if (line.isVertical) {
    console.log("Line is vertical");
} else if (line.isHorizontal) {
    console.log("Line is horizontal");
} else {
    console.log(`Slope: ${line.slope}`);
}

// Verify midpoint calculation
console.log(`Midpoint: (${line.midpoint.x}, ${line.midpoint.y})`);

Source Code

Below is the complete source code for the SWLine class. You can copy it directly or view it to understand the implementation details.

swLine.js
// swLine.js
// SWLine: A styled line segment class for SketchWaveJS
// Author: TechNoviceTools (TNT)
// Date: 2026-01-26
//
// This class represents a line segment between two SWPoints, with thickness and color.
// It is designed to be compatible with p5.js, SWColor, SWPoint, and SWGrid.
//
// Dependencies: SWColor, SWPoint, SWGrid, p5.js

console.log("[swLine.js] SWLine class loaded.");

class SWLine {
        
    /**
     * @param {SWPoint} ptA - First endpoint (SWPoint)
     * @param {SWPoint} ptB - Second endpoint (SWPoint)
     * @param {number} [thickness=2] - Stroke weight
     * @param {SWColor} [strokeColor] - Stroke color (SWColor instance)
     */
    constructor(ptA, ptB, thickness = 2, strokeColor = undefined) {
        this.ptA = ptA;
        this.ptB = ptB;
        this.thickness = thickness;
        this.strokeColor = strokeColor;
        this.shouldShowEndPoints = true;
        this.length = Math.sqrt(Math.pow(ptB.x - ptA.x, 2) + Math.pow(ptB.y - ptA.y, 2));
        if (ptB.x - ptA.x === 0) {
            this.slope = Infinity; //vertical line
            this.isVertical = true;
            this.isHorizontal = false;
        } else {
            this.slope = (ptB.y - ptA.y) / (ptB.x - ptA.x);
            this.isHorizontal = true;
            this.isVertical = false;
        }

        this.shouldShowSegment = true;

        // Store original endpoints for consistent rotation
        this.originalA = new SWPoint(ptA.x, ptA.y, ptA.z, ptA.strokeWeight, ptA.strokeColor);
        this.originalB = new SWPoint(ptB.x, ptB.y, ptB.z, ptB.strokeWeight, ptB.strokeColor);

        // Calculate midpoint
        const MIDPT_FACTOR = 2; //will be 2x thickness of line
        const midX = (ptA.x + ptB.x) / 2;
        const midY = (ptA.y + ptB.y) / 2;
        // Create a copy of strokeColor and darken it by 20%
        let midColor = strokeColor ? SWColor.copy(strokeColor) : undefined;
        if (midColor) midColor.darkenBy(0.2);
        this.midpoint = new SWPoint(midX, midY, undefined, MIDPT_FACTOR * thickness, midColor, "M");
        this.shouldShowMidpoint = true;
        this.midpoint.showLabel = true;
        if(this.midpoint.showLabel) {
            this.midpoint.setLabelOffsetFromSlope(this.slope, 15);
        }
    }//end constructor

    /**
     * Modulates the length of the line using a SWSinusoid instance, keeping the midpoint fixed.
     * @param {SWSinusoid} sinusoid - The sinusoid to apply
     * @param {number} t - The time or parameter to pass to the sinusoid
     */
    breathe(sinusoid, t) {
        // Find the current midpoint
        const midX = (this.ptA.x + this.ptB.x) / 2;
        const midY = (this.ptA.y + this.ptB.y) / 2;
        // Direction vector from ptA to ptB
        const dx = this.ptB.x - this.ptA.x;
        const dy = this.ptB.y - this.ptA.y;
        const len = Math.sqrt(dx * dx + dy * dy);
        if (len === 0) return; // Avoid division by zero
        // Unit direction vector
        const ux = dx / len;
        const uy = dy / len;
        // New half-length from sinusoid
        const newHalfLen = sinusoid.getValue(t) / 2;
        // Set new endpoints, keeping midpoint fixed
        this.ptA.x = midX - ux * newHalfLen;
        this.ptA.y = midY - uy * newHalfLen;
        this.ptB.x = midX + ux * newHalfLen;
        this.ptB.y = midY + uy * newHalfLen;
        // Update length and midpoint
        this.length = Math.sqrt(Math.pow(this.ptB.x - this.ptA.x, 2) + Math.pow(this.ptB.y - this.ptA.y, 2));
        this.midpoint.x = (this.ptA.x + this.ptB.x) / 2;
        this.midpoint.y = (this.ptA.y + this.ptB.y) / 2;
    }//end breathe

    /**
     * Draws the line using p5.js in screen coordinates
     */
    draw() {
        if (this.strokeColor && this.strokeColor.col) {
            stroke(this.strokeColor.col);
        }
        strokeWeight(this.thickness);
        if (this.shouldShowSegment){
            line(this.ptA.x, this.ptA.y, this.ptB.x, this.ptB.y);
        }
        noStroke();
        strokeWeight(1);
        // Optionally, draw the midpoint
        if (this.midpoint && this.shouldShowMidpoint) {
            this.midpoint.draw();
        }
        // Optionally, draw the endpoints
        if (this.shouldShowEndPoints) {
            this.ptA.draw();
            this.ptB.draw();
        }
    }//end draw

    /**
     * Draws the line using user (grid) coordinates mapped by the given SWGrid
     * @param {SWGrid} grid
     */
    drawOnGrid(grid) {
        const {x: x1, y: y1} = grid.userToScreen(this.ptA.x, this.ptA.y);
        const {x: x2, y: y2} = grid.userToScreen(this.ptB.x, this.ptB.y);
        if (this.strokeColor && this.strokeColor.col) {
            stroke(this.strokeColor.col);
        }
        strokeWeight(this.thickness);
        if (this.shouldShowSegment){
            line(x1, y1, x2, y2);
        }
        noStroke();
        strokeWeight(1);
        // Optionally, draw the midpoint
        if (this.midpoint && this.shouldShowMidpoint) {
            this.midpoint.drawOnGrid(grid);
        }
        // Optionally, draw the endpoints
        if (this.shouldShowEndPoints) {
            this.ptA.drawOnGrid(grid);
            this.ptB.drawOnGrid(grid);
        }
    }//end drawOnGrid

    /**
     * Rotates the line about its midpoint at a given angular velocity (deg/sec).
     * Positive degPerSec is counterclockwise, negative is clockwise.
     * @param {number} degPerSec - Angular velocity in degrees per second
     * @param {number} t - Time in seconds (or frameCount/framerate)
     *
     * Reasoning:
     * - The midpoint remains fixed.
     * - The endpoints are rotated around the midpoint by angle = degPerSec * t (converted to radians).
     * - This is useful for animating a spinning line, e.g., for oscilloscopes, clock hands, or dynamic geometry.
     *
     * Application:
     *   Call line.rotateAboutMidPoint(45, t) in your animation loop to rotate at 45 deg/sec.
     */
    rotateAboutMidPoint(degPerSec, t) {
        // Calculate the angle to rotate (in radians)
        const angleDeg = degPerSec * t;
        const angleRad = angleDeg * Math.PI / 180;
        // Always rotate from the original endpoints
        const midX = (this.originalA.x + this.originalB.x) / 2;
        const midY = (this.originalA.y + this.originalB.y) / 2;
        // Vector from midpoint to originalA and originalB
        const dxA = this.originalA.x - midX;
        const dyA = this.originalA.y - midY;
        const dxB = this.originalB.x - midX;
        const dyB = this.originalB.y - midY;
        // Rotate both endpoints around the midpoint
        this.ptA.x = midX + (dxA * Math.cos(angleRad) - dyA * Math.sin(angleRad));
        this.ptA.y = midY + (dxA * Math.sin(angleRad) + dyA * Math.cos(angleRad));
        this.ptB.x = midX + (dxB * Math.cos(angleRad) - dyB * Math.sin(angleRad));
        this.ptB.y = midY + (dxB * Math.sin(angleRad) + dyB * Math.cos(angleRad));
        // Update midpoint (should remain the same, but recalc for safety)
        this.midpoint.x = (this.ptA.x + this.ptB.x) / 2;
        this.midpoint.y = (this.ptA.y + this.ptB.y) / 2;
    }//end rotateAboutMidPoint

    /**
     * Returns a string representation
     */
    toString() {
        return `SWLine(ptA: ${this.ptA.toString()}, ptB: ${this.ptB.toString()}, thickness: ${this.thickness}, strokeColor: ${this.strokeColor ? this.strokeColor.toString() : 'none'}, length: ${this.length.toFixed(2)}, midpoint: ${this.midpoint ? this.midpoint.toString() : 'none'})`;
    }//end toString

    /**
     * Applies breathing (length modulation), rotation, or both to the line, starting from the original endpoints.
     * Call this in your animation loop to combine effects without interference.
     *
     * @param {Object} options - { sinusoid, t, degPerSec }
     *   sinusoid: SWSinusoid instance (optional, for breathing)
     *   t: time in seconds
     *   degPerSec: angular velocity (optional, for rotation)
     *
     * Usage:
     *   line.transform({sinusoid, t, degPerSec}); // both
     *   line.transform({sinusoid, t}); // just breathing
     *   line.transform({degPerSec, t}); // just rotation
     */
    transform({sinusoid = null, t = 0, degPerSec = null} = {}) {
        // 1. Start from original endpoints
        const ax0 = this.originalA.x;
        const ay0 = this.originalA.y;
        const bx0 = this.originalB.x;
        const by0 = this.originalB.y;
        // 2. Find original midpoint
        const midX = (ax0 + bx0) / 2;
        const midY = (ay0 + by0) / 2;
        // 3. Vector from midpoint to each endpoint
        let dax = ax0 - midX;
        let day = ay0 - midY;
        let dbx = bx0 - midX;
        let dby = by0 - midY;
        // 4. Breathing: scale vectors if sinusoid is provided
        if (sinusoid) {
            const origLen = Math.sqrt((bx0 - ax0) ** 2 + (by0 - ay0) ** 2);
            const newLen = sinusoid.getValue(t);
            const scale = (origLen === 0) ? 1 : (newLen / origLen);
            dax *= scale / 2;
            day *= scale / 2;
            dbx *= scale / 2;
            dby *= scale / 2;
        }
        // 5. Rotation: rotate vectors if degPerSec is provided
        if (degPerSec !== null) {
            const angleRad = (degPerSec * t) * Math.PI / 180;
            const cosA = Math.cos(angleRad);
            const sinA = Math.sin(angleRad);
            const rotate = (dx, dy) => [dx * cosA - dy * sinA, dx * sinA + dy * cosA];
            [dax, day] = rotate(dax, day);
            [dbx, dby] = rotate(dbx, dby);
        }
        // 6. Set new endpoints
        this.ptA.x = midX + dax;
        this.ptA.y = midY + day;
        this.ptB.x = midX + dbx;
        this.ptB.y = midY + dby;
        // 7. Update midpoint
        this.midpoint.x = (this.ptA.x + this.ptB.x) / 2;
        this.midpoint.y = (this.ptA.y + this.ptB.y) / 2;
        // 8. Update length
        this.length = Math.sqrt((this.ptB.x - this.ptA.x) ** 2 + (this.ptB.y - this.ptA.y) ** 2);
    }//end transform

    /**
     * Rotates the line about a designated endpoint (fixedPt) at a given angular velocity (deg/sec).
     * Usage: line.rotateAbout(line.ptA, degPerSec, t) or line.rotateAbout(line.ptB, degPerSec, t)
     * @param {SWPoint} fixedPt - The endpoint to rotate about (must be ptA or ptB)
     * @param {number} degPerSec - Angular velocity in degrees per second
     * @param {number} t - Time in seconds
     */
    rotateAbout(fixedPt, degPerSec, t) {
        // Determine which endpoint is fixed and which is moving
        let isA = (fixedPt === this.ptA);
        let origFixed = isA ? this.originalA : this.originalB;
        let origMoving = isA ? this.originalB : this.originalA;
        // Calculate angle in radians
        const angleRad = degPerSec * t * Math.PI / 180;
        // Vector from fixed to moving (original positions)
        const dx = origMoving.x - origFixed.x;
        const dy = origMoving.y - origFixed.y;
        // Rotate moving endpoint around fixed
        const newX = origFixed.x + (dx * Math.cos(angleRad) - dy * Math.sin(angleRad));
        const newY = origFixed.y + (dx * Math.sin(angleRad) + dy * Math.cos(angleRad));
        // Set endpoints
        if (isA) {
            this.ptA.x = origFixed.x;
            this.ptA.y = origFixed.y;
            this.ptB.x = newX;
            this.ptB.y = newY;
        } else {
            this.ptB.x = origFixed.x;
            this.ptB.y = origFixed.y;
            this.ptA.x = newX;
            this.ptA.y = newY;
        }
        // Update midpoint and length
        this.midpoint.x = (this.ptA.x + this.ptB.x) / 2;
        this.midpoint.y = (this.ptA.y + this.ptB.y) / 2;
        this.length = Math.sqrt((this.ptB.x - this.ptA.x) ** 2 + (this.ptB.y - this.ptA.y) ** 2);
    }//end rotateAbout

}//end SWLine class


// Export for module use (if needed)
// export default SWLine;