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 3Table of Contents
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)
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). |
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
strokeColorandthickness - Draws the segment if
shouldShowSegmentis true - Draws endpoints if
shouldShowEndPointsis true - Draws midpoint if
shouldShowMidpointis 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);
}
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.ptAorthis.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);
}
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.ptAorthis.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.ptAorthis.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);
}
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
isDraggableon endpoints for clear state management - Use
containsPoint()with a generous tolerance (e.g., 15 pixels) for easy clicking - Always call
redraw()or use continuousdraw()loops for real-time updates
6. Performance Considerations
- Avoid creating new SWLine instances every frame; create once in
setup() - For static diagrams, use
noLoop()andredraw()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: 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;