📐 SWTriangle Class Reference

A comprehensive guide to the SketchWaveJS Triangle class

📐 Overview

The SWTriangle class is part of the SketchWaveJS framework, designed for creating interactive, animated triangles in p5.js. A triangle consists of three SWPoint vertices, has a computed centroid, supports custom fill and stroke colors, and includes built-in geometric calculations for area, perimeter, side lengths, angles, and triangle classifications.

💡 Key Features:
  • Three SWPoint vertices with automatic centroid calculation
  • Built-in geometric properties: area, perimeter, side lengths, angles
  • Automatic triangle classification: equilateral, isosceles, scalene, acute, right, obtuse, 30-60-90
  • Customizable fill and stroke colors using SWColor
  • Optional vertex and centroid visualization
  • Dual coordinate system support (screen and grid coordinates)
  • Animation methods: breathing, rotation, and combined transformations
  • Compatible with SWGrid for Desmos-style coordinate mapping

Dependencies

  • SWPoint - For vertices and centroid
  • SWColor - For fill and stroke colors
  • SWGrid - For grid-based coordinate mapping
  • SWSinusoid - For animation effects
  • p5.js - Graphics library (v1.6.0+)

🔨 Constructor

new SWTriangle(vA, vB, vC, fillColor, options)

Creates a new triangle with three vertex points.

Parameters

  • vA (SWPoint) - First vertex (typically labeled A)
  • vB (SWPoint) - Second vertex (typically labeled B)
  • vC (SWPoint) - Third vertex (typically labeled C)
  • fillColor (SWColor) - Fill color for the triangle interior
  • options (Object, optional) - Configuration object:
    • strokeColor (SWColor) - Border color (optional, no border if undefined)
    • strokeWeight (number) - Border thickness in pixels (default: 2)
    • showVertices (boolean) - Display vertex points (default: true)
    • showCentroid (boolean) - Display centroid point (default: false)

Examples

// Simple triangle (no border, vertices shown)
let ptA = new SWPoint(-3, 4, undefined, 8, swRed, "A");
let ptB = new SWPoint(3, 4, undefined, 8, swGreen, "B");
let ptC = new SWPoint(0, -2, undefined, 8, swBlue, "C");
let triangleABC = new SWTriangle(ptA, ptB, ptC, swYellow);

// Styled triangle with border
let triangle2 = new SWTriangle(
    new SWPoint(-5, 5), 
    new SWPoint(5, 5), 
    new SWPoint(0, -5),
    swCyan,  // fill color
    {
        strokeColor: swBlue,     // border color
        strokeWeight: 3,         // border thickness
        showVertices: true,      // show vertices
        showCentroid: true       // show centroid
    }
);

// Triangle without visible vertices
let triangle3 = new SWTriangle(
    new SWPoint(-2, 0),
    new SWPoint(2, 0),
    new SWPoint(0, 3),
    swMagenta,
    { showVertices: false }
);

📊 Properties

Basic Properties

vA, vB, vC (SWPoint)

The three vertex points of the triangle. These are mutable and can be repositioned to reshape the triangle.

// Access vertices
console.log(triangle.vA.x, triangle.vA.y);
console.log(triangle.vB.x, triangle.vB.y);
console.log(triangle.vC.x, triangle.vC.y);

// Move a vertex (reshapes triangle)
triangle.vA.x = 5;
triangle.vA.y = 10;
centroid (SWPoint, read-only)

The computed center point of the triangle (average of the three vertices). This is automatically calculated by computeCentroid().

// Access centroid
console.log(`Centroid: (${triangle.centroid.x}, ${triangle.centroid.y})`);

// Centroid is automatically updated if you call computeCentroid()
triangle.centroid = triangle.computeCentroid();
fillColor (SWColor)

The fill color for the triangle's interior.

triangle.fillColor = swRed;
triangle.fillColor = new SWColor(180, 70, 80, 100, "customCyan");
strokeColor (SWColor or undefined)

The border color for the triangle. If undefined, no border is drawn.

triangle.strokeColor = swBlue;
triangle.strokeColor = undefined; // No border
strokeWeight (number)

The thickness of the border in pixels (default: 2).

triangle.strokeWeight = 5;
showVertices, showCentroid (boolean)

Control whether to display vertex points and/or the centroid point when drawing.

triangle.showVertices = false;  // Hide vertices
triangle.showCentroid = true;   // Show centroid
triangle.setShowVertices(true); // Using setter method
triangle.setShowCentroid(false);

Geometric Properties (Getters)

These properties are computed on-the-fly and cannot be directly assigned.

sideAB, sideBC, sideCA (number, getter)

The lengths of the three sides of the triangle. These use SWPoint.distanceTo() internally.

  • sideAB - Distance from vertex A to vertex B
  • sideBC - Distance from vertex B to vertex C
  • sideCA - Distance from vertex C to vertex A
console.log(`Side AB: ${triangle.sideAB.toFixed(2)}`);
console.log(`Side BC: ${triangle.sideBC.toFixed(2)}`);
console.log(`Side CA: ${triangle.sideCA.toFixed(2)}`);
perimeter (number, getter)

The total perimeter of the triangle (sum of all three side lengths).

console.log(`Perimeter: ${triangle.perimeter.toFixed(2)}`);
area (number, getter)

The area of the triangle, computed using the cross product formula (shoelace method):

Area = |x₁(y₂ - y₃) + x₂(y₃ - y₁) + x₃(y₁ - y₂)| / 2

console.log(`Area: ${triangle.area.toFixed(2)} square units`);
angleA, angleB, angleC (number, getter)

The interior angles of the triangle in degrees, computed using the Law of Cosines:

  • angleA - Angle at vertex A (between sides AB and CA)
  • angleB - Angle at vertex B (between sides AB and BC)
  • angleC - Angle at vertex C (between sides BC and CA)

Formula: cos(A) = (b² + c² - a²) / (2bc)

console.log(`Angle A: ${triangle.angleA.toFixed(1)}°`);
console.log(`Angle B: ${triangle.angleB.toFixed(1)}°`);
console.log(`Angle C: ${triangle.angleC.toFixed(1)}°`);

// Verify: sum should be 180°
let sum = triangle.angleA + triangle.angleB + triangle.angleC;
console.log(`Sum of angles: ${sum.toFixed(1)}°`);

Classification Properties (Getters)

These boolean getters automatically determine the type of triangle based on its sides and angles.

Side-Based Classifications

isEquilateral (boolean, getter)

Returns true if all three sides are approximately equal (within 1% relative tolerance).

if (triangle.isEquilateral) {
    console.log("This is an equilateral triangle!");
}
isIsosceles (boolean, getter)

Returns true if at least two sides are approximately equal (within 1% relative tolerance). Note: Equilateral triangles are also isosceles.

if (triangle.isIsosceles && !triangle.isEquilateral) {
    console.log("This is an isosceles (but not equilateral) triangle!");
}
Scalene Triangle (no dedicated getter)

A scalene triangle has all sides of different lengths. You can check for this by verifying that the triangle is not isosceles (and therefore not equilateral).

let isScalene = !triangle.isIsosceles;
if (isScalene) {
    console.log("This is a scalene triangle!");
}

Angle-Based Classifications

isAcuteTriangle (boolean, getter)

Returns true if all three angles are less than 90°.

if (triangle.isAcuteTriangle) {
    console.log("All angles are acute (< 90°)");
}
isRightTriangle (boolean, getter)

Returns true if one of the angles is approximately 90° (within 0.5° tolerance).

if (triangle.isRightTriangle) {
    console.log("This triangle has a right angle!");
}
isObtuseTriangle (boolean, getter)

Returns true if one of the angles is greater than 90°.

if (triangle.isObtuseTriangle) {
    console.log("This triangle has an obtuse angle!");
}
is30_60_90 (boolean, getter)

Returns true if the triangle is a special 30-60-90 right triangle (within 0.5° tolerance for each angle).

if (triangle.is30_60_90) {
    console.log("This is a 30-60-90 special right triangle!");
}
💡 Classification Tips:
  • A triangle is classified by both sides (scalene/isosceles/equilateral) AND angles (acute/right/obtuse)
  • Example: A scalene triangle can be acute, right, or obtuse
  • An equilateral triangle is always acute (all angles are 60°)
  • The sum of all angles in a triangle always equals 180°

⚙️ Methods

Drawing Methods

draw()

Draws the triangle in screen coordinates using the current fill and stroke colors.

Example

function setup() {
    createCanvas(600, 600);
    colorMode(HSB, 360, 100, 100, 100);
    initializeSWColors();
}

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

Draws the triangle in grid (user) coordinates, mapping vertices to screen coordinates using the specified SWGrid.

Parameters

  • grid (SWGrid) - The grid for coordinate mapping

Example

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

function draw() {
    background(220);
    grid.draw();
    triangle.drawOnGrid(grid); // Draws in grid coordinates
}
computeCentroid(strokeWeight, strokeColor)

Computes and returns the centroid of the triangle as a new SWPoint. The centroid is the average of the three vertices.

Parameters

  • strokeWeight (number, optional) - Stroke weight for centroid point (default: 4)
  • strokeColor (SWColor, optional) - Stroke color for centroid point (default: swBlack)

Returns

SWPoint - The centroid point

Example

// Recalculate centroid after moving vertices
triangle.vA.x = 10;
triangle.centroid = triangle.computeCentroid();

Animation Methods

breathe(sinusoid, t)

Makes the triangle 'breathe' by moving each vertex radially from the centroid. The distance is modulated by the provided sinusoid, creating a pulsing effect.

Parameters

  • sinusoid (SWSinusoid) - Controls the breathing amplitude and frequency
  • t (number) - Time parameter in seconds

Example

// SWSinusoid(amplitude, frequency, verticalShift, phaseShift)
// Create sinusoid that scales triangle from 0.5× to 1.5× over 3 seconds
const minScale = 0.5;
const maxScale = 1.5;
const period = 3.0; // seconds

const amp = (maxScale - minScale) / 2;  // 0.5
const freq = (2 * Math.PI) / period;    // ~2.09
const mid = (minScale + maxScale) / 2;  // 1.0
let breathe = new SWSinusoid(amp, freq, mid, 0);

function draw() {
    background(220);
    grid.draw();
    
    // Apply breathing animation
    triangle.breathe(breathe, frameCount * 0.02);
    triangle.drawOnGrid(grid);
}
💡 Tip: The breathe method scales the triangle from its centroid, maintaining its shape while changing size. The sinusoid's vertical shift determines the average scale factor, and the amplitude determines the variation.
rotateAboutCentroid(degPerSec, t)

Rotates the triangle about its centroid. The rotation is applied to the original vertex positions.

Parameters

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

Example

let triangle;

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, 4, undefined, 8, swRed, "A");
    let ptB = new SWPoint(3, 4, undefined, 8, swGreen, "B");
    let ptC = new SWPoint(0, -2, undefined, 8, swBlue, "C");
    triangle = new SWTriangle(ptA, ptB, ptC, swYellow, {strokeColor: swOrange, strokeWeight: 2});
}

function draw() {
    background(220);
    grid.draw();
    
    // Rotate at 45 degrees per second
    triangle.rotateAboutCentroid(45, frameCount / frameRate());
    triangle.drawOnGrid(grid);
}
rotateAboutCentroidBy(angleDeg)

Rotates the triangle about its centroid by a specific angle (not time-based).

Parameters

  • angleDeg (number) - Angle in degrees (counterclockwise is positive)

Example

// Rotate by 30 degrees
triangle.rotateAboutCentroidBy(30);
transform({sinusoid, t, degPerSec})

Applies breathing (scaling), rotation, or both to the triangle simultaneously. This method combines effects without interference, always starting from the original vertices.

Parameters (Object)

  • sinusoid (SWSinusoid, optional) - Controls breathing/scaling effect
  • t (number) - Time parameter in seconds
  • degPerSec (number, optional) - Angular velocity for rotation

Examples

// Example 1: Breathing only
const minScale = 0.7, maxScale = 1.3, period = 4.0;
const amp = (maxScale - minScale) / 2;
const freq = (2 * Math.PI) / period;
const mid = (minScale + maxScale) / 2;
let breathe = new SWSinusoid(amp, freq, mid, 0);

function draw() {
    background(220);
    grid.draw();
    
    triangle.transform({
        sinusoid: breathe,
        t: frameCount * 0.02
    });
    
    triangle.drawOnGrid(grid);
}

// Example 2: Rotation only
function draw() {
    background(220);
    grid.draw();
    
    triangle.transform({
        degPerSec: 30,
        t: frameCount / frameRate()
    });
    
    triangle.drawOnGrid(grid);
}

// Example 3: Combined breathing and rotation
function draw() {
    background(220);
    grid.draw();
    
    triangle.transform({
        sinusoid: breathe,
        degPerSec: 45,
        t: frameCount * 0.02
    });
    
    triangle.drawOnGrid(grid);
}
💡 Tip: The transform() method always applies breathing first, then rotation. Both effects are computed from the original vertex positions, ensuring smooth, predictable animations.

Utility Methods

reset()

Resets the triangle to its original vertex positions (stored when the triangle was created). This is useful after animations to restore the initial state.

Example

// Reset triangle after animation
triangle.reset();
setShowVertices(show)

Sets whether to display vertex points when drawing the triangle.

Parameters

  • show (boolean) - True to show vertices, false to hide (default: true)

Example

triangle.setShowVertices(false); // Hide vertices
setShowCentroid(show)

Sets whether to display the centroid point when drawing the triangle.

Parameters

  • show (boolean) - True to show centroid, false to hide (default: false)

Example

triangle.setShowCentroid(true); // Show centroid
horizShiftBy(xInc)

Shifts the entire triangle horizontally by the specified amount. All vertices and the centroid are updated.

Parameters

  • xInc (number) - Amount to shift in the x direction

Example

// Move triangle 5 units to the right
triangle.horizShiftBy(5);

// Move triangle 3 units to the left
triangle.horizShiftBy(-3);
toString()

Returns a string representation of the triangle with vertex coordinates, centroid, area, and perimeter.

Returns

string - String representation of the triangle

Example

console.log(triangle.toString());
// Output: "SWTriangle(vA: (x, y), vB: (x, y), vC: (x, y), 
//          centroid: (x, y), area: A, perimeter: P)"

📝 Usage Examples

Example 1: Basic Static Triangle

Create and display a simple triangle with colored vertices.

let grid, triangle;

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, -5, undefined, 10, swRed, "A");
    let ptB = new SWPoint(5, -5, undefined, 10, swGreen, "B");
    let ptC = new SWPoint(0, 5, undefined, 10, swBlue, "C");
    
    triangle = new SWTriangle(ptA, ptB, ptC, swYellow, {
        strokeColor: swOrange,
        strokeWeight: 3,
        showVertices: true,
        showCentroid: true
    });
    
    noLoop();
}

function draw() {
    background(220);
    grid.draw();
    triangle.drawOnGrid(grid);
    
    // Display geometric properties
    fill(0);
    textAlign(LEFT, TOP);
    textSize(14);
    text(`Area: ${triangle.area.toFixed(2)}`, 10, 10);
    text(`Perimeter: ${triangle.perimeter.toFixed(2)}`, 10, 30);
    text(`Angles: ${triangle.angleA.toFixed(1)}°, ${triangle.angleB.toFixed(1)}°, ${triangle.angleC.toFixed(1)}°`, 10, 50);
}

Example 2: Interactive Draggable Triangle

Enable users to drag vertices or the centroid to reshape the triangle.

let grid, triangle;
let draggedPoint = null;

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, 4, undefined, 12, swRed, "A");
    let ptB = new SWPoint(3, 4, undefined, 12, swGreen, "B");
    let ptC = new SWPoint(0, -3, undefined, 12, swBlue, "C");
    ptA.setDraggable(true);
    ptB.setDraggable(true);
    ptC.setDraggable(true);
    
    triangle = new SWTriangle(ptA, ptB, ptC, swCyan, {
        strokeColor: swBlue,
        strokeWeight: 2,
        showCentroid: true
    });
}

function draw() {
    background(240);
    grid.draw();
    triangle.drawOnGrid(grid);
    
    // Display real-time properties
    fill(0);
    textAlign(LEFT, TOP);
    textSize(14);
    text(`Area: ${triangle.area.toFixed(2)}`, 10, 10);
    text(`Perimeter: ${triangle.perimeter.toFixed(2)}`, 10, 30);
    
    let typeStr = '';
    if (triangle.isEquilateral) typeStr = 'Equilateral';
    else if (triangle.isIsosceles) typeStr = 'Isosceles';
    else typeStr = 'Scalene';
    
    if (triangle.isRightTriangle) typeStr += ', Right';
    else if (triangle.isObtuseTriangle) typeStr += ', Obtuse';
    else if (triangle.isAcuteTriangle) typeStr += ', Acute';
    
    text(`Type: ${typeStr}`, 10, 50);
}

function mousePressed() {
    if (triangle.vA.containsPoint(mouseX, mouseY, grid, 15)) {
        draggedPoint = triangle.vA;
    } else if (triangle.vB.containsPoint(mouseX, mouseY, grid, 15)) {
        draggedPoint = triangle.vB;
    } else if (triangle.vC.containsPoint(mouseX, mouseY, grid, 15)) {
        draggedPoint = triangle.vC;
    }
}

function mouseDragged() {
    if (draggedPoint) {
        let userCoords = grid.screenToUser(mouseX, mouseY);
        draggedPoint.x = userCoords.x;
        draggedPoint.y = userCoords.y;
        triangle.centroid = triangle.computeCentroid();
        redraw();
    }
}

function mouseReleased() {
    draggedPoint = null;
}

Example 3: Breathing Triangle Animation

let grid, triangle, 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(-4, -3, undefined, 8, swRed, "A");
    let ptB = new SWPoint(4, -3, undefined, 8, swGreen, "B");
    let ptC = new SWPoint(0, 4, undefined, 8, swBlue, "C");
    
    triangle = new SWTriangle(ptA, ptB, ptC, swMagenta, {
        strokeColor: swPurple,
        strokeWeight: 2
    });
    
    // Scale from 0.6× to 1.4× over 3 seconds
    const minScale = 0.6, maxScale = 1.4, period = 3.0;
    const amp = (maxScale - minScale) / 2;
    const freq = (2 * Math.PI) / period;
    const mid = (minScale + maxScale) / 2;
    breatheSinusoid = new SWSinusoid(amp, freq, mid, 0);
}

function draw() {
    background(220);
    grid.draw();
    
    triangle.breathe(breatheSinusoid, frameCount * 0.02);
    triangle.drawOnGrid(grid);
    
    // Show current area
    fill(0);
    textAlign(LEFT, TOP);
    textSize(14);
    text(`Area: ${triangle.area.toFixed(2)}`, 10, 10);
}

Example 4: Rotating Triangle

let grid, triangle;

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 right triangle
    let ptA = new SWPoint(-3, -2, undefined, 8, swRed, "A");
    let ptB = new SWPoint(3, -2, undefined, 8, swGreen, "B");
    let ptC = new SWPoint(3, 4, undefined, 8, swBlue, "C");
    
    triangle = new SWTriangle(ptA, ptB, ptC, swCyan, {
        strokeColor: swBlue,
        strokeWeight: 2,
        showCentroid: true
    });
}

function draw() {
    background(220);
    grid.draw();
    
    // Rotate at 60 degrees per second
    triangle.rotateAboutCentroid(60, frameCount / frameRate());
    triangle.drawOnGrid(grid);
}

Example 5: Combined Breathing and Rotation

let grid, triangle, 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(-4, -3, undefined, 8, swRed, "A");
    let ptB = new SWPoint(4, -3, undefined, 8, swGreen, "B");
    let ptC = new SWPoint(0, 4, undefined, 8, swBlue, "C");
    
    triangle = new SWTriangle(ptA, ptB, ptC, swYellow, {
        strokeColor: swOrange,
        strokeWeight: 2
    });
    
    const minScale = 0.7, maxScale = 1.3, period = 4.0;
    const amp = (maxScale - minScale) / 2;
    const freq = (2 * Math.PI) / period;
    const mid = (minScale + maxScale) / 2;
    breatheSinusoid = new SWSinusoid(amp, freq, mid, 0);
}

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

Example 6: Triangle Classification Display

let grid, triangle;
let draggedPoint = null;

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, 12, swRed, "A");
    let ptB = new SWPoint(5, 0, undefined, 12, swGreen, "B");
    let ptC = new SWPoint(0, 7, undefined, 12, swBlue, "C");
    ptA.setDraggable(true);
    ptB.setDraggable(true);
    ptC.setDraggable(true);
    
    triangle = new SWTriangle(ptA, ptB, ptC, swCyan, {
        strokeColor: swBlue,
        strokeWeight: 2
    });
}

function draw() {
    background(240);
    grid.draw();
    triangle.drawOnGrid(grid);
    
    // Display comprehensive classification
    fill(0);
    textAlign(LEFT, TOP);
    textSize(14);
    let y = 10;
    
    text(`Area: ${triangle.area.toFixed(2)} sq units`, 10, y); y += 20;
    text(`Perimeter: ${triangle.perimeter.toFixed(2)} units`, 10, y); y += 20;
    text(`Sides: ${triangle.sideAB.toFixed(2)}, ${triangle.sideBC.toFixed(2)}, ${triangle.sideCA.toFixed(2)}`, 10, y); y += 20;
    text(`Angles: ${triangle.angleA.toFixed(1)}°, ${triangle.angleB.toFixed(1)}°, ${triangle.angleC.toFixed(1)}°`, 10, y); y += 25;
    
    // Side-based classification
    text('Side Classification:', 10, y); y += 20;
    if (triangle.isEquilateral) {
        text('  ✓ Equilateral', 10, y);
    } else if (triangle.isIsosceles) {
        text('  ✓ Isosceles', 10, y);
    } else {
        text('  ✓ Scalene', 10, y);
    }
    y += 25;
    
    // Angle-based classification
    text('Angle Classification:', 10, y); y += 20;
    if (triangle.is30_60_90) {
        text('  ✓ 30-60-90 Special Right Triangle', 10, y);
    } else if (triangle.isRightTriangle) {
        text('  ✓ Right Triangle', 10, y);
    } else if (triangle.isObtuseTriangle) {
        text('  ✓ Obtuse Triangle', 10, y);
    } else if (triangle.isAcuteTriangle) {
        text('  ✓ Acute Triangle', 10, y);
    }
}

function mousePressed() {
    if (triangle.vA.containsPoint(mouseX, mouseY, grid, 15)) {
        draggedPoint = triangle.vA;
    } else if (triangle.vB.containsPoint(mouseX, mouseY, grid, 15)) {
        draggedPoint = triangle.vB;
    } else if (triangle.vC.containsPoint(mouseX, mouseY, grid, 15)) {
        draggedPoint = triangle.vC;
    }
}

function mouseDragged() {
    if (draggedPoint) {
        let userCoords = grid.screenToUser(mouseX, mouseY);
        draggedPoint.x = userCoords.x;
        draggedPoint.y = userCoords.y;
        triangle.centroid = triangle.computeCentroid();
        redraw();
    }
}

function mouseReleased() {
    draggedPoint = null;
}

💡 Best Practices & Tips

Performance Optimization

Geometric properties (area, perimeter, angles) are computed on-the-fly via getters. For performance-critical applications where you need these values frequently in a single frame, consider storing them in local variables:

// Instead of calling getter multiple times:
if (triangle.area > 50 && triangle.area < 100) { ... }

// Store in a variable:
let area = triangle.area;
if (area > 50 && area < 100) { ... }
Updating the Centroid

If you manually modify vertex positions (e.g., triangle.vA.x = 5), you must update the centroid manually:

triangle.vA.x = 10;
triangle.vA.y = 5;
triangle.centroid = triangle.computeCentroid();
Animation Best Practices
  • Always use transform() for combined effects rather than calling breathe() and rotateAboutCentroid() separately
  • Store original vertices by calling reset() before starting new animations
  • For smooth animations, use consistent time parameters (e.g., frameCount / frameRate())
Classification Logic

Remember that triangles have two independent classification systems:

  • By sides: Scalene, Isosceles, or Equilateral
  • By angles: Acute, Right, or Obtuse

A scalene triangle can be acute, right, or obtuse. These are not mutually exclusive!

⚠️ Common Pitfalls
  • Don't forget to call computeCentroid() after manually moving vertices
  • Remember that isIsosceles returns true for equilateral triangles (since they have equal sides)
  • Angle calculations use tolerances (0.5°) to account for floating-point precision
  • If using animations, always call reset() before switching between different animation modes
Color Styling

For best visual results:

  • Use contrasting colors for fill and stroke (e.g., light fill with darker border)
  • Consider setting showVertices: false for cleaner animations
  • Use showCentroid: true when demonstrating rotation or breathing effects
  • Vertex colors are independent of the triangle's fill/stroke colors
Working with SWGrid
  • Always use drawOnGrid(grid) instead of draw() when working with coordinate systems
  • Vertices are stored in user (grid) coordinates, not screen coordinates
  • When checking for mouse interaction with vertices, pass the grid to containsPoint(mouseX, mouseY, grid, tolerance)
Debugging Tips
// Print all triangle information
console.log(triangle.toString());

// Check angles sum to 180°
let angleSum = triangle.angleA + triangle.angleB + triangle.angleC;
console.log(`Angle sum: ${angleSum.toFixed(2)}° (should be 180°)`);

// Verify Pythagorean theorem for right triangles
if (triangle.isRightTriangle) {
    let sides = [triangle.sideAB, triangle.sideBC, triangle.sideCA].sort((a,b) => a-b);
    let pyth = sides[0]**2 + sides[1]**2;
    let hyp = sides[2]**2;
    console.log(`Pythagorean check: ${pyth.toFixed(2)} ≈ ${hyp.toFixed(2)}`);
}

📄 Source Code

The complete source code for the SWTriangle class:

// swTriangle.js
// SWTriangle: A triangle with 3 SWPoint vertices, centroid, fill color, 
// optional border, and animation features
// Author: klp + GitHub Copilot
// Date: 2026-01-27
//
// Dependencies: SWPoint, SWColor, SWGrid, SWSinusoid

console.log("[swTriangle.js] SWTriangle class loaded.");

class SWTriangle {
    /**
     * @param {SWPoint} vA - First vertex (SWPoint)
     * @param {SWPoint} vB - Second vertex (SWPoint)
     * @param {SWPoint} vC - Third vertex (SWPoint)
     * @param {SWColor} fillColor - Fill color (SWColor instance)
     * @param {Object} [options] - Optional: {strokeColor, strokeWeight, showVertices, showCentroid}
     */
    constructor(vA, vB, vC, fillColor, options = {}) {
        this.vA = vA;
        this.vB = vB;
        this.vC = vC;
        this.fillColor = fillColor;
        this.strokeColor = options.strokeColor || undefined;
        this.strokeWeight = options.strokeWeight || 2;
        this.showVertices = options.showVertices !== undefined ? options.showVertices : true;
        this.showCentroid = options.showCentroid !== undefined ? options.showCentroid : false;
        
        const centroidStrokeWeight = this.strokeWeight > 0 ? this.strokeWeight : 4;
        const centroidColor = (this.strokeColor && this.strokeColor.col) ? this.strokeColor : 
                              (typeof swBlack !== 'undefined' ? swBlack : new SWColor(0,0,0,100,"black"));
        this.centroid = this.computeCentroid(centroidStrokeWeight, centroidColor);
        
        this.originalA = new SWPoint(vA.x, vA.y, vA.z, vA.strokeWeight, vA.strokeColor);
        this.originalB = new SWPoint(vB.x, vB.y, vB.z, vB.strokeWeight, vB.strokeColor);
        this.originalC = new SWPoint(vC.x, vC.y, vC.z, vC.strokeWeight, vC.strokeColor);
        this.originalCentroid = new SWPoint(this.centroid.x, this.centroid.y, undefined, 
                                           centroidStrokeWeight, centroidColor);
    }

    computeCentroid(strokeWeight = 4, strokeColor = (typeof swBlack !== 'undefined' ? swBlack : 
                    new SWColor(0,0,0,100,"black"))) {
        const x = (this.vA.x + this.vB.x + this.vC.x) / 3;
        const y = (this.vA.y + this.vB.y + this.vC.y) / 3;
        return new SWPoint(x, y, undefined, strokeWeight, strokeColor);
    }

    get sideAB() { return this.vA.distanceTo(this.vB); }
    get sideBC() { return this.vB.distanceTo(this.vC); }
    get sideCA() { return this.vC.distanceTo(this.vA); }
    get perimeter() { return this.sideAB + this.sideBC + this.sideCA; }
    
    get area() {
        return Math.abs(
            this.vA.x * (this.vB.y - this.vC.y) +
            this.vB.x * (this.vC.y - this.vA.y) +
            this.vC.x * (this.vA.y - this.vB.y)
        ) / 2;
    }

    get angleA() {
        const a = this.sideBC, b = this.sideCA, c = this.sideAB;
        const cosA = (b * b + c * c - a * a) / (2 * b * c);
        return Math.acos(Math.max(-1, Math.min(1, cosA))) * 180 / Math.PI;
    }

    get angleB() {
        const a = this.sideBC, b = this.sideCA, c = this.sideAB;
        const cosB = (a * a + c * c - b * b) / (2 * a * c);
        return Math.acos(Math.max(-1, Math.min(1, cosB))) * 180 / Math.PI;
    }

    get angleC() {
        const a = this.sideBC, b = this.sideCA, c = this.sideAB;
        const cosC = (a * a + b * b - c * c) / (2 * a * b);
        return Math.acos(Math.max(-1, Math.min(1, cosC))) * 180 / Math.PI;
    }

    get isRightTriangle() {
        const tolerance = 0.5;
        return Math.abs(this.angleA - 90) < tolerance ||
               Math.abs(this.angleB - 90) < tolerance ||
               Math.abs(this.angleC - 90) < tolerance;
    }

    get isObtuseTriangle() {
        return this.angleA > 90 || this.angleB > 90 || this.angleC > 90;
    }

    get isAcuteTriangle() {
        return this.angleA < 90 && this.angleB < 90 && this.angleC < 90;
    }

    get isEquilateral() {
        const tolerance = 0.01;
        const avg = (this.sideAB + this.sideBC + this.sideCA) / 3;
        return Math.abs(this.sideAB - avg) < tolerance * avg &&
               Math.abs(this.sideBC - avg) < tolerance * avg &&
               Math.abs(this.sideCA - avg) < tolerance * avg;
    }

    get isIsosceles() {
        const tolerance = 0.01;
        const ab = this.sideAB, bc = this.sideBC, ca = this.sideCA;
        const maxSide = Math.max(ab, bc, ca);
        const tol = tolerance * maxSide;
        return Math.abs(ab - bc) < tol ||
               Math.abs(bc - ca) < tol ||
               Math.abs(ca - ab) < tol;
    }

    get is30_60_90() {
        const tolerance = 0.5;
        const angles = [this.angleA, this.angleB, this.angleC].sort((a, b) => a - b);
        return Math.abs(angles[0] - 30) < tolerance &&
               Math.abs(angles[1] - 60) < tolerance &&
               Math.abs(angles[2] - 90) < tolerance;
    }

    draw() {
        if (this.fillColor && this.fillColor.col) fill(this.fillColor.col);
        else noFill();
        if (this.strokeColor && this.strokeColor.col) {
            stroke(this.strokeColor.col);
            strokeWeight(this.strokeWeight);
        } else noStroke();
        triangle(this.vA.x, this.vA.y, this.vB.x, this.vB.y, this.vC.x, this.vC.y);
        noStroke();
        noFill();
        if (this.showVertices) {
            this.vA.draw();
            this.vB.draw();
            this.vC.draw();
        }
        if (this.showCentroid) this.centroid.draw();
    }

    drawOnGrid(grid) {
        const a = grid.userToScreen(this.vA.x, this.vA.y);
        const b = grid.userToScreen(this.vB.x, this.vB.y);
        const c = grid.userToScreen(this.vC.x, this.vC.y);
        if (this.fillColor && this.fillColor.col) fill(this.fillColor.col);
        else noFill();
        if (this.strokeColor && this.strokeColor.col) {
            stroke(this.strokeColor.col);
            strokeWeight(this.strokeWeight);
        } else noStroke();
        triangle(a.x, a.y, b.x, b.y, c.x, c.y);
        noStroke();
        noFill();
        if (this.showVertices) {
            this.vA.drawOnGrid(grid);
            this.vB.drawOnGrid(grid);
            this.vC.drawOnGrid(grid);
        }
        if (this.showCentroid) this.centroid.drawOnGrid(grid);
    }

    rotateAboutCentroidBy(angleDeg) {
        const angleRad = angleDeg * Math.PI / 180;
        const cx = this.centroid.x, cy = this.centroid.y;
        [this.vA, this.vB, this.vC].forEach((v, i) => {
            const orig = [this.originalA, this.originalB, this.originalC][i];
            const dx = orig.x - cx, dy = orig.y - cy;
            v.x = cx + dx * Math.cos(angleRad) - dy * Math.sin(angleRad);
            v.y = cy + dx * Math.sin(angleRad) + dy * Math.cos(angleRad);
        });
    }

    rotateAboutCentroid(degPerSec, t) {
        this.rotateAboutCentroidBy(degPerSec * t);
    }

    breathe(sinusoid, t) {
        const scaleFactor = sinusoid.evaluate(t);
        const cx = this.originalCentroid.x, cy = this.originalCentroid.y;
        this.vA.x = cx + (this.originalA.x - cx) * scaleFactor;
        this.vA.y = cy + (this.originalA.y - cy) * scaleFactor;
        this.vB.x = cx + (this.originalB.x - cx) * scaleFactor;
        this.vB.y = cy + (this.originalB.y - cy) * scaleFactor;
        this.vC.x = cx + (this.originalC.x - cx) * scaleFactor;
        this.vC.y = cy + (this.originalC.y - cy) * scaleFactor;
        this.centroid.x = cx;
        this.centroid.y = cy;
    }

    transform({sinusoid = null, t = 0, degPerSec = null} = {}) {
        this.reset();
        if (sinusoid) this.breathe(sinusoid, t);
        if (degPerSec !== null) this.rotateAboutCentroidBy(degPerSec * t);
    }

    reset() {
        this.vA.x = this.originalA.x; this.vA.y = this.originalA.y;
        this.vB.x = this.originalB.x; this.vB.y = this.originalB.y;
        this.vC.x = this.originalC.x; this.vC.y = this.originalC.y;
        this.centroid.x = this.originalCentroid.x;
        this.centroid.y = this.originalCentroid.y;
    }

    setShowVertices(show = true) { this.showVertices = show; }
    setShowCentroid(show = true) { this.showCentroid = show; }

    toString() {
        return `SWTriangle(vA: (${this.vA.x.toFixed(2)}, ${this.vA.y.toFixed(2)}), ` +
               `vB: (${this.vB.x.toFixed(2)}, ${this.vB.y.toFixed(2)}), ` +
               `vC: (${this.vC.x.toFixed(2)}, ${this.vC.y.toFixed(2)}), ` +
               `centroid: (${this.centroid.x.toFixed(2)}, ${this.centroid.y.toFixed(2)}), ` +
               `area: ${this.area.toFixed(2)}, perimeter: ${this.perimeter.toFixed(2)})`;
    }

    horizShiftBy(xInc) {
        this.vA.x += xInc; this.vB.x += xInc; this.vC.x += xInc;
        this.originalA.x += xInc; this.originalB.x += xInc; this.originalC.x += xInc;
        this.centroid.x += xInc;
        this.originalCentroid.x += xInc;
    }
}

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