SketchWave Arrow Class Reference

Directed arrows with animated shafts and arrowheads for dynamic geometry

Back to SWArrow Demo 1

Overview

SWArrow represents a directed arrow between two points in 2D space, designed for use with the SketchWaveJS framework and p5.js. The shaft is a styled line segment; the arrowhead is a V formed by two barb lines extending backward from the tip. All animation methods mirror the SWLine API for ecosystem consistency, making SWArrow a natural upgrade from SWLine any time directionality matters.

Key Features

  • Directed Geometry: ptA is always the tail (blunt end); ptB is always the tip (arrowhead end)
  • Configurable Arrowhead: tipAngle controls barb spread (narrow = pointer; wide = broad); tipFactor controls barb length relative to shaft
  • Auto Midpoint: A draggable SWPoint midpoint is computed and maintained automatically
  • Dual Coordinate Systems: draw() uses screen pixels; drawOnGrid(grid) uses user (math) coordinates via SWGrid
  • Animation Suite: Built-in breathing (length oscillation) and spinning (rotation about midpoint or tail)
  • Drag to Reposition: Drag the tail dot (ptA) or midpoint dot to move the entire arrow
  • Start Angle Control: setStartAngle(deg) sets orientation in degrees (CCW positive) before or during use
  • Customizable Appearance: Shaft thickness, color, stroke cap, and visibility of tip/tail/midpoint are all independently controllable

Design Philosophy

Like SWLine, SWArrow is a reference-based geometry object that holds SWPoint references rather than raw coordinates. Changing an endpoint's .x / .y is immediately reflected on the next draw call β€” no rebuilding the object needed. SWArrow adds:

  • A direction (tail β†’ tip) encoded into the arrowhead geometry
  • An originalA / originalB pose snapshot so all rotation and breathing methods pivot from a stable reference frame
  • _updateDerived() which keeps length, angle, and the midpoint SWPoint in sync after any positional change
πŸ’‘ Tip: SWArrow excels at vector field visualization, unit-circle direction indicators, physics diagrams, clock hands with direction cues, and any animation where the endpoint you're pointing toward has visual significance.

Dependencies

SWArrow requires the following SketchWaveJS classes and libraries:

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

Constructor

new SWArrow(ptA, ptB, thickness, strokeColor, tipAngle, tipFactor)

Creates a new SWArrow instance from a tail SWPoint to a tip SWPoint.

Parameters

  • ptA (SWPoint) β€” Tail (blunt end) of the arrow
  • ptB (SWPoint) β€” Tip (arrowhead end) of the arrow
  • thickness (number, optional, default 3) β€” Shaft stroke weight in pixels
  • strokeColor (SWColor, optional) β€” Shaft and arrowhead color
  • tipAngle (number, optional, default 25) β€” Half-angle between shaft and each barb, in degrees. Narrower = sharper; wider = broader head.
  • tipFactor (number, optional, default 0.3) β€” Barb length as a fraction of total arrow length (0.3 = barbs are 30% of shaft length)

Automatic Initialization

When created, SWArrow automatically:

  • Computes startAngleDeg from the initial ptAβ†’ptB direction using Math.atan2
  • Stores originalA and originalB snapshots for rotation-anchor math
  • Creates a midpoint SWPoint with a slightly darker shade of strokeColor and a size of max(thickness Γ— 2, 8) px
  • Runs _updateDerived() to compute length, angle, and the midpoint position
  • Defaults shouldShowTip, shouldShowTailPoint, and shouldShowMidpoint all to true

Examples

// Arrow pointing right with defaults
let arrowAB = new SWArrow(
    new SWPoint(-5, 0),
    new SWPoint(5, 0)
);

// Styled arrow
let arrowCD = new SWArrow(
    new SWPoint(-3, -3),
    new SWPoint(3, 3),
    5,          // thickness
    swMedGreen, // color
    30,         // tipAngle (degrees)
    0.25        // tipFactor
);

// Using existing named points
let ptA = new SWPoint(-5, 0, undefined, 12, swOrange, "A");
let ptB = new SWPoint(5, 0, undefined, 8, swBlue, "B");
let myArrow = new SWArrow(ptA, ptB, 4, swPurple, 25, 0.30);

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 The tail (blunt end) of the arrow. Modifying ptA.x / ptA.y moves the tail; call _updateDerived() afterward if you need accurate length / angle.
ptB SWPoint The tip (arrowhead end). Modifying ptB.x / ptB.y moves the arrowhead.
originalA SWPoint Snapshot of ptA at construction time (or after the most recent setStartAngle() / drag). Used as the rotation/breathing anchor β€” never drawn.
originalB SWPoint Snapshot of ptB at construction time (or after the most recent setStartAngle() / drag). Used as the rotation/breathing anchor β€” never drawn.

Geometric Properties

Property Type Description
length number Current shaft length in whichever coordinate system is active (user units for drawOnGrid). Recomputed by _updateDerived().
angle number Current shaft angle in radians, measured CCW from the positive X-axis. Recomputed by _updateDerived(). For degrees: arrow.angle * 180 / Math.PI.
startAngleDeg number The arrow's "home" orientation in degrees (CCW positive, 0Β° = pointing right). Set automatically from the constructor positions; updated by setStartAngle(deg). All animation methods spin relative to this orientation.
midpoint SWPoint A live SWPoint at the center of the shaft. Position is mutated in place by _updateDerived(). Visible when shouldShowMidpoint is true; drag it to move the whole arrow.

Arrowhead Properties

Property Type Default Description
tipAngle number 25 Half-angle (in degrees) between the shaft axis and each barb. Range 5–70Β°. Small values (5–15) produce a needle-sharp head; large values (45–70) produce a broad, open head.
tipFactor number 0.3 Barb length as a fraction of the total shaft length. Range 0.05–0.60. At 0.3 the barbs are 30% of the shaft; at 0.6 the barbs dominate half the visual length.
capStyle string 'ROUND' p5.js stroke-cap style applied to the shaft ends. Options: 'ROUND' (default), 'SQUARE', 'PROJECT'. Change via setStrokeCap().

Appearance & Visibility Properties

Property Type Default Description
thickness number 3 Shaft stroke weight in pixels. Both the shaft and the barb lines use this weight.
strokeColor SWColor undefined Color used for the shaft and arrowhead barbs. If undefined, the current p5.js stroke color is used.
shouldShowTip boolean true If true, the two arrowhead barbs are drawn. Set to false to convert the arrow into a plain line segment.
shouldShowTailPoint boolean true If true, the tail (ptA) SWPoint dot is drawn. When visible, the dot is draggable β€” dragging it moves the entire arrow.
shouldShowMidpoint boolean true If true, the center (midpoint) SWPoint dot is drawn. When visible, the dot is draggable β€” dragging it moves the entire arrow.
πŸ“ Note: When you manually assign new values to ptA.x, ptA.y, ptB.x, or ptB.y without going through an animation method, call arrow._updateDerived() afterward to keep length, angle, and the midpoint position accurate. The animation methods (breathe, rotate*, transform*) all call it automatically.

Methods

Drawing Methods

draw() β†’ void

Draws the arrow using p5.js in screen coordinates. ptA.x/y and ptB.x/y are treated directly as pixel positions.

Behavior

  • Applies strokeColor and thickness
  • Applies capStyle to shaft ends
  • Draws the shaft from ptA to ptB
  • Draws the two arrowhead barbs from ptB if shouldShowTip is true
  • Draws the tail dot (ptA.draw()) if shouldShowTailPoint is true
  • Draws the midpoint dot if shouldShowMidpoint is true
Example
function draw() {
    background(220);
    myArrow.draw(); // Draws in screen pixel coordinates
}
drawOnGrid(grid) β†’ void

Draws the arrow in user (grid) coordinates, converting all positions to screen via the provided SWGrid. Barb geometry is computed in user space before projection, keeping barb proportions correct regardless of grid scale.

Parameters

  • grid (SWGrid) β€” The grid providing the user-to-screen coordinate transformation

Example

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

Internal Helpers

_updateDerived() β†’ void

Recomputes length, angle, and midpoint.x/y from the current ptA and ptB positions. Called automatically by all animation methods. Call it manually after directly mutating endpoint positions.

// After manually moving the tip:
myArrow.ptB.x = 8;
myArrow.ptB.y = 3;
myArrow._updateDerived(); // resync length, angle, midpoint

Animation Methods

breathe(sinusoid, t) β†’ void

Oscillates the arrow's shaft length over time using a SWSinusoid, keeping the midpoint fixed while both endpoints move symmetrically inward and outward.

Parameters

  • sinusoid (SWSinusoid) β€” Defines length as a function of time; evaluated as sinusoid.getValue(t)
  • t (number) β€” Time in seconds (typically frameCount / frameRate())

How It Works

The current midpoint is held fixed. A unit direction vector is computed along the shaft. The new half-length from sinusoid.getValue(t) determines how far each endpoint is placed from the midpoint.

Example

// Arrow breathes between length 3 and 9, period 4 seconds
const minLen = 3, maxLen = 9, period = 4.0;
const amp  = (maxLen - minLen) / 2;   // 3
const freq = (2 * Math.PI) / period;  // ~1.57
const mid  = (minLen + maxLen) / 2;   // 6
let breatheSin = new SWSinusoid(amp, freq, mid, 0);

function draw() {
    background(220);
    grid.draw();
    myArrow.breathe(breatheSin, frameCount / frameRate());
    myArrow.drawOnGrid(grid);
}
πŸ’‘ Tip: Set breatheMin close to zero for a dramatic pulse effect where the arrow nearly collapses to a point before expanding again.
rotateAboutMidPoint(degPerSec, t) β†’ void

Rotates the arrow about its midpoint at a constant angular velocity. Positive values rotate counterclockwise (CCW); negative values rotate clockwise (CW).

Parameters

  • degPerSec (number) β€” Angular velocity in degrees per second
  • t (number) β€” Time in seconds

Behavior

Always rotates by degPerSec Γ— t degrees relative to the originalA / originalB pose. The midpoint stays fixed. Safe to call repeatedly each frame β€” no accumulated drift.

Example

function draw() {
    background(220);
    grid.draw();
    // Rotate at 45 deg/sec counterclockwise
    myArrow.rotateAboutMidPoint(45, frameCount / frameRate());
    myArrow.drawOnGrid(grid);
}
rotateAbout(fixedPt, degPerSec, t) β†’ void

Rotates the arrow about a fixed endpoint (either ptA or ptB), keeping that endpoint pinned while the other end sweeps an arc. This is the clock-hand or compass-needle pattern.

Parameters

  • fixedPt (SWPoint) β€” The endpoint to hold fixed; must be this.ptA or this.ptB
  • degPerSec (number) β€” Angular velocity in degrees per second
  • t (number) β€” Time in seconds

Example

// Arrow rotates about its tail β€” like a clock hand
function draw() {
    background(220);
    grid.draw();
    myArrow.rotateAbout(myArrow.ptA, 90, frameCount / frameRate());
    myArrow.drawOnGrid(grid);
}
πŸ’‘ Tip: Passing myArrow.ptB as the fixed point keeps the arrowhead anchored while the tail sweeps β€” useful for "compass pointing at a target" or rotating about a known destination.
transform({sinusoid, t, degPerSec}) β†’ void

Applies breathing and/or midpoint rotation simultaneously from the original pose. Use this instead of calling breathe() and rotateAboutMidPoint() separately β€” those would interfere with each other.

Parameters (object destructuring)

  • sinusoid (SWSinusoid, optional) β€” For length oscillation; null to skip
  • t (number) β€” Time in seconds
  • degPerSec (number, optional) β€” Angular velocity; null to skip rotation

Calling Patterns

// Breathing + rotation (most common)
myArrow.transform({ sinusoid: breatheSin, degPerSec: 45, t });

// Breathing only
myArrow.transform({ sinusoid: breatheSin, t });

// Rotation only
myArrow.transform({ degPerSec: 45, t });

Full Example

const minLen = 3, maxLen = 9, period = 4.0;
const breatheSin = new SWSinusoid(
    (maxLen - minLen) / 2,
    (2 * Math.PI) / period,
    (maxLen + minLen) / 2,
    0
);

function draw() {
    background(220);
    grid.draw();
    const t = frameCount / frameRate();
    myArrow.transform({ sinusoid: breatheSin, degPerSec: 60, t });
    myArrow.drawOnGrid(grid);
}
transformAbout(fixedPt, {sinusoid, breatheTime, degPerSec, rotateTime}) β†’ void

Combines breathing and rotation about a fixed endpoint with independent time parameters. The fixed endpoint stays pinned; the other end breathes and/or sweeps an arc.

Parameters

  • fixedPt (SWPoint) β€” The endpoint to hold fixed; must be this.ptA or this.ptB
  • sinusoid (SWSinusoid, optional) β€” For length oscillation
  • breatheTime (number) β€” Independent time parameter for breathing
  • degPerSec (number, optional) β€” Angular velocity
  • rotateTime (number) β€” Independent time parameter for rotation

Why Separate Time Parameters?

Individual time values let you pause breathing while rotation continues (or vice versa), or have both run at completely different rates.

Example

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

function draw() {
    background(220);
    grid.draw();

    if (breathingOn) breatheTime += deltaTime / 1000;
    if (spinningOn)  rotateTime  += deltaTime / 1000;

    // Tail pinned; tip breathes and sweeps
    myArrow.transformAbout(myArrow.ptA, {
        sinusoid:    breatheSin,
        breatheTime: breatheTime,
        degPerSec:   90,
        rotateTime:  rotateTime
    });

    myArrow.drawOnGrid(grid);
}

Setter Methods

setStartAngle(deg) β†’ void

Rotates the arrow to a new starting orientation, keeping the midpoint and length invariant. Updates both the live positions (ptA, ptB) and the original pose snapshots (originalA, originalB), so all subsequent animation methods pivot from the new orientation.

Parameters

  • deg (number) β€” Target angle in degrees, CCW positive. 0Β° = right, 90Β° = up, βˆ’90Β° = down, Β±180Β° = left.

Example

// Point the arrow straight up before starting spin
myArrow.setStartAngle(90);

// Then spin from that new "home" orientation
myArrow.rotateAboutMidPoint(45, t);
πŸ’‘ Tip: Call setStartAngle() while the arrow is not spinning to set a new base orientation. After calling it, reset your time accumulator to 0 so the animation starts from the freshly-set pose.
setThickness(w) β†’ void

Sets the shaft (and barb) stroke weight in pixels.

Parameters

  • w (number) β€” Stroke weight in pixels
myArrow.setThickness(8);   // thick arrow
myArrow.setThickness(1);   // hair-thin arrow
setStrokeCap(cap) β†’ void

Sets the p5.js stroke-cap style for the shaft ends.

Parameters

  • cap (string) β€” 'ROUND' (default): rounded ends that blend nicely with barb joins
    'SQUARE': flat ends flush with endpoints
    'PROJECT': flat ends that extend slightly past endpoints
myArrow.setStrokeCap('SQUARE');   // flat shaft ends
myArrow.setStrokeCap('ROUND');    // restore default
setColor(swColor) β†’ void

Sets the stroke color for the shaft and arrowhead barbs.

Parameters

  • swColor (SWColor) β€” A SketchWaveJS SWColor instance
myArrow.setColor(swRed);
myArrow.setColor(new SWColor(200, 80, 90, 100, "skyBlue"));

Utility Methods

toString() β†’ string

Returns a human-readable string summarizing the arrow's current state.

console.log(myArrow.toString());
// Output: "SWArrow(ptA:(-5.00, 0.00) β†’ ptB:(5.00, 0.00), len:10.00, angle:0.0Β°, tipAngle:25Β°, tipFactor:0.3)"

Usage Examples

Example 1: Simple Static Arrow

let grid, myArrow;

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, 0, undefined, 10, swOrange, "tail");
    let ptB = new SWPoint(5, 0, undefined, 8, swBlue, "tip");
    myArrow = new SWArrow(ptA, ptB, 4, swMedGreen, 25, 0.30);

    noLoop();
}

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

Example 2: Spinning Arrow (Midpoint Pivot)

let grid, myArrow;

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

    myArrow = new SWArrow(
        new SWPoint(-5, 0), new SWPoint(5, 0),
        5, swMedGreen, 25, 0.30
    );
    frameRate(30);
}

function draw() {
    background(220);
    grid.draw();
    // Rotate 45 deg/sec counterclockwise about midpoint
    myArrow.rotateAboutMidPoint(45, frameCount / frameRate());
    myArrow.drawOnGrid(grid);
}

Example 3: Clock-Hand Spin (Tail Pivot)

let grid, myArrow;

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

    // Arrow starting at origin pointing right; tip is the hand end
    myArrow = new SWArrow(
        new SWPoint(0, 0),   // tail pinned at origin
        new SWPoint(7, 0),   // tip sweeps the arc
        5, swRed, 20, 0.20
    );

    // Begin pointing straight up (CCW 90Β°)
    myArrow.setStartAngle(90);

    frameRate(30);
}

function draw() {
    background(245);
    grid.draw();

    // 90 deg/sec clockwise = -90
    myArrow.rotateAbout(myArrow.ptA, -6, frameCount / frameRate());
    myArrow.drawOnGrid(grid);
}

Example 4: Breathing Arrow

let grid, myArrow, breatheSin;

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

    myArrow = new SWArrow(
        new SWPoint(-5, 0), new SWPoint(5, 0),
        5, swMedGreen
    );

    // Length oscillates from 3 to 9 over 4 seconds
    const minLen = 3, maxLen = 9, period = 4.0;
    breatheSin = new SWSinusoid(
        (maxLen - minLen) / 2,       // amplitude = 3
        (2 * Math.PI) / period,      // frequency
        (maxLen + minLen) / 2,       // vertical shift = 6
        0                            // phase shift
    );

    frameRate(30);
}

function draw() {
    background(230);
    grid.draw();
    myArrow.breathe(breatheSin, frameCount / frameRate());
    myArrow.drawOnGrid(grid);
}

Example 5: Spinning + Breathing Together

let grid, myArrow, breatheSin;

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

    myArrow = new SWArrow(
        new SWPoint(-5, 0), new SWPoint(5, 0),
        5, swMedGreen, 30, 0.25
    );

    const minLen = 3, maxLen = 9, period = 3.0;
    breatheSin = new SWSinusoid(
        (maxLen - minLen) / 2,
        (2 * Math.PI) / period,
        (maxLen + minLen) / 2,
        0
    );

    frameRate(30);
}

function draw() {
    background(230);
    grid.draw();

    // Breathe AND spin at 60 deg/sec β€” use transform(), not separate calls
    myArrow.transform({
        sinusoid: breatheSin,
        degPerSec: 60,
        t: frameCount / frameRate()
    });

    myArrow.drawOnGrid(grid);
}

Example 6: Draggable Arrow

Allow users to drag the tail dot or midpoint dot to reposition the arrow on the canvas.

let grid, myArrow;
let dragTarget = null;  // 'tail' | 'midpoint' | null

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

    myArrow = new SWArrow(
        new SWPoint(-5, 0), new SWPoint(5, 0),
        5, swMedGreen
    );

    // Give the tail dot an orange appearance
    myArrow.ptA.strokeWeight = 12;
    myArrow.ptA.strokeColor = new SWColor(15, 100, 100, 100, "orange");

    frameRate(30);
}

function draw() {
    background(230);
    grid.draw();
    myArrow.drawOnGrid(grid);
}

function mousePressed() {
    const HIT = 14; // pixel hit tolerance

    if (myArrow.shouldShowTailPoint) {
        const s = grid.userToScreen(myArrow.ptA.x, myArrow.ptA.y);
        if (dist(mouseX, mouseY, s.x, s.y) < HIT) {
            dragTarget = 'tail';
            return;
        }
    }

    if (myArrow.shouldShowMidpoint) {
        const s = grid.userToScreen(myArrow.midpoint.x, myArrow.midpoint.y);
        if (dist(mouseX, mouseY, s.x, s.y) < HIT) {
            dragTarget = 'midpoint';
        }
    }
}

function mouseDragged() {
    if (!dragTarget) return;

    // Compute the delta in user coordinates
    const prev = grid.screenToUser(pmouseX, pmouseY);
    const curr = grid.screenToUser(mouseX, mouseY);
    const dx = curr.x - prev.x;
    const dy = curr.y - prev.y;

    // Translate all four points by the same delta
    myArrow.ptA.x += dx;   myArrow.ptA.y += dy;
    myArrow.ptB.x += dx;   myArrow.ptB.y += dy;
    myArrow.originalA.x += dx;  myArrow.originalA.y += dy;
    myArrow.originalB.x += dx;  myArrow.originalB.y += dy;
    myArrow._updateDerived();
}

function mouseReleased() {
    dragTarget = null;
}

Example 7: Set Start Angle Before Spinning

Orient the arrow before animation starts so it spins from a specific "home" direction.

let grid, myArrow;

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

    myArrow = new SWArrow(
        new SWPoint(-5, 0), new SWPoint(5, 0),
        5, swMedGreen
    );

    // Orient upward (90Β°) before spinning begins
    myArrow.setStartAngle(90);

    frameRate(30);
}

function draw() {
    background(230);
    grid.draw();

    // Spins from the 90Β° "up" orientation
    myArrow.rotateAboutMidPoint(45, frameCount / frameRate());
    myArrow.drawOnGrid(grid);
}

Example 8: Multiple Arrows (Simple Vector Field)

let grid;
let arrows = [];

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

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

    // 5Γ—5 grid of small arrows, each pointing right
    for (let row = -2; row <= 2; row++) {
        for (let col = -2; col <= 2; col++) {
            const cx = col * 2;
            const cy = row * 2;
            const a = new SWArrow(
                new SWPoint(cx - 0.6, cy),
                new SWPoint(cx + 0.6, cy),
                2, swMedGreen, 25, 0.35
            );
            a.shouldShowTailPoint = false;
            a.shouldShowMidpoint  = false;
            arrows.push(a);
        }
    }

    noLoop();
}

function draw() {
    background(245);
    grid.draw();
    for (let a of arrows) {
        a.drawOnGrid(grid);
    }
}

Best Practices

1. Always Call _updateDerived() After Manual Endpoint Changes

If you modify ptA.x/y or ptB.x/y directly (outside of animation methods), resync the derived properties:

myArrow.ptB.x = 8;
myArrow.ptB.y = 4;
myArrow._updateDerived(); // keeps length, angle, midpoint accurate

2. Use transform() for Combined Animations

Never call breathe() and rotateAboutMidPoint() in the same frame β€” they will fight over endpoint positions. Use transform() instead:

// ❌ Wrong β€” these two calls interfere
myArrow.breathe(sin, t);
myArrow.rotateAboutMidPoint(45, t);

// βœ… Correct β€” combined in one call
myArrow.transform({ sinusoid: sin, degPerSec: 45, t });

3. Update originalA/B When Dragging

When implementing drag-to-reposition, always translate originalA and originalB by the same delta as ptA and ptB. If you forget, the arrow will snap back to its prior position the next time an animation method runs (since those methods compute from the originals):

// βœ… Translate originals too
myArrow.ptA.x += dx;   myArrow.ptA.y += dy;
myArrow.ptB.x += dx;   myArrow.ptB.y += dy;
myArrow.originalA.x += dx;  myArrow.originalA.y += dy;
myArrow.originalB.x += dx;  myArrow.originalB.y += dy;
myArrow._updateDerived();

4. Choose the Right Animation Method for the Pivot

  • Spin about center β†’ rotateAboutMidPoint() or transform()
  • Spin about tail (clock-hand) β†’ rotateAbout(myArrow.ptA, …) or transformAbout(myArrow.ptA, …)
  • Spin about tip β†’ rotateAbout(myArrow.ptB, …)

5. Use drawOnGrid() for Math Work

  • Use draw() only when ptA/ptB are already in screen pixels
  • Use drawOnGrid(grid) for all coordinate-system-aware work β€” it keeps barb proportions correct regardless of grid zoom
  • Don't mix the two in the same sketch

6. Control Visibility for Cleaner Displays

// Hide all decorations β€” plain arrow
myArrow.shouldShowTip       = false;  // makes it look like SWLine
myArrow.shouldShowTailPoint = false;
myArrow.shouldShowMidpoint  = false;

// Most minimal useful arrow (just shaft + head)
myArrow.shouldShowTailPoint = false;
myArrow.shouldShowMidpoint  = false;

7. Set Arrowhead Style Before Animating

Change tipAngle and tipFactor at any time β€” they take effect on the next draw call via _getBarbPoints():

myArrow.tipAngle  = 10;   // sharp needle-like barbs
myArrow.tipFactor = 0.20; // short barbs

myArrow.tipAngle  = 60;   // broad open head
myArrow.tipFactor = 0.50; // long barbs β€” very prominent head

8. Use Independent Time Accumulators for Pausable Animations

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

function draw() {
    if (breathingOn) breatheTime += deltaTime / 1000;
    if (spinningOn)  rotateTime  += deltaTime / 1000;

    myArrow.transform({ sinusoid: sin, degPerSec: 45, t: rotateTime });
}

9. Combining with Other SketchWaveJS Classes

  • SWGrid: Essential for any math-coordinate work; use drawOnGrid()
  • SWSinusoid: Powers breathing; construct once in setup()
  • SWDisk: Draw circles at ptA/ptB for emphasis, or to highlight the pivot point of a rotation
  • SWLine: SWArrow intentionally mirrors SWLine's API β€” they work identically except SWArrow adds arrowhead geometry and setStartAngle()

10. Educational Uses

SWArrow shines in teaching:

  • Vectors: Visualize magnitude (length) and direction (angle); multiple arrows form a vector field
  • Unit Circle: An arrow rotating about its tail with constant length traces the unit circle
  • Clock Hands: rotateAbout(ptA, rate, t) is the classic clock-hand pattern
  • Force Diagrams: Place arrows at a body to show applied forces; control length = magnitude
  • Time-Based Motion: Combining breathing + rotation shows compound harmonic motion

11. Debugging Tips

// Quick state check
console.log(myArrow.toString());

// Check current geometry
console.log(`Length: ${myArrow.length.toFixed(3)}`);
console.log(`Angle:  ${(myArrow.angle * 180 / Math.PI).toFixed(1)}Β°`);
console.log(`Start:  ${myArrow.startAngleDeg.toFixed(1)}Β°`);
console.log(`Mid:    (${myArrow.midpoint.x.toFixed(2)}, ${myArrow.midpoint.y.toFixed(2)})`);

// Verify originals match ptA/ptB when not animating
console.log(`ptA matches original: ${
    myArrow.ptA.x === myArrow.originalA.x &&
    myArrow.ptA.y === myArrow.originalA.y
}`);

Source Code

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

swArrow.js
// swArrow.js
// SWArrow: A directed arrow class for SketchWaveJS
// Author: TechNoviceTools (TNT)
// Date: 2026-03-30
//
// Represents a directed arrow from a tail point (ptA) to a tip point (ptB).
// The shaft is a styled line; the arrowhead is a V formed by two barb lines.
// Supports breathing (length oscillation) and spinning (rotation) animations,
// consistent with the SWLine/SWDisk/SWWheel ecosystem.
//
// Constructor:
//   SWArrow(ptA, ptB, thickness, strokeColor, tipAngle, tipFactor)
//   - ptA        : SWPoint β€” tail (blunt end)
//   - ptB        : SWPoint β€” tip (arrowhead end)
//   - thickness  : number  β€” stroke weight in pixels (default 3)
//   - strokeColor: SWColor β€” stroke color (default undefined)
//   - tipAngle   : number  β€” half-angle of arrowhead barbs in degrees (default 25)
//   - tipFactor  : number  β€” barb length as fraction of total arrow length (default 0.3)
//
// Dependencies: SWColor, SWPoint, SWGrid, SWSinusoid, p5.js

console.log("[swArrow.js] SWArrow class loaded.");

class SWArrow {

    constructor(ptA, ptB, thickness = 3, strokeColor = undefined, tipAngle = 25, tipFactor = 0.3) {
        this.ptA = ptA;
        this.ptB = ptB;
        this.thickness = thickness;
        this.strokeColor = strokeColor;
        this.tipAngle = tipAngle;
        this.tipFactor = tipFactor;
        this.capStyle = 'ROUND';

        const dx0 = ptB.x - ptA.x;
        const dy0 = ptB.y - ptA.y;
        this.startAngleDeg = Math.atan2(dy0, dx0) * 180 / Math.PI;

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

        const midPtSize = Math.max(thickness * 2, 8);
        const midColor  = strokeColor
            ? new SWColor(strokeColor.h, strokeColor.s, Math.max(strokeColor.b - 20, 20), 100, "arrowMidColor")
            : new SWColor(0, 0, 40, 100, "arrowMidGray");
        this.midpoint = new SWPoint(0, 0, undefined, midPtSize, midColor);

        this._updateDerived();

        this.shouldShowTip       = true;
        this.shouldShowTailPoint = true;
        this.shouldShowMidpoint  = true;
    }//end constructor

    _updateDerived() {
        const dx = this.ptB.x - this.ptA.x;
        const dy = this.ptB.y - this.ptA.y;
        this.length = Math.sqrt(dx * dx + dy * dy);
        this.angle  = Math.atan2(dy, dx);
        const mx = (this.ptA.x + this.ptB.x) / 2;
        const my = (this.ptA.y + this.ptB.y) / 2;
        if (this.midpoint) {
            this.midpoint.x = mx;
            this.midpoint.y = my;
        }
    }//end _updateDerived

    _getBarbPoints() {
        const barbLen      = this.length * this.tipFactor;
        const shaftAngle   = Math.atan2(this.ptB.y - this.ptA.y, this.ptB.x - this.ptA.x);
        const tipAngleRad  = this.tipAngle * Math.PI / 180;
        const barb1Angle = shaftAngle + Math.PI - tipAngleRad;
        const barb2Angle = shaftAngle + Math.PI + tipAngleRad;
        return {
            barb1: {
                x: this.ptB.x + barbLen * Math.cos(barb1Angle),
                y: this.ptB.y + barbLen * Math.sin(barb1Angle)
            },
            barb2: {
                x: this.ptB.x + barbLen * Math.cos(barb2Angle),
                y: this.ptB.y + barbLen * Math.sin(barb2Angle)
            }
        };
    }//end _getBarbPoints

    draw() {
        if (this.strokeColor && this.strokeColor.col) {
            stroke(this.strokeColor.col);
        }
        strokeWeight(this.thickness);
        strokeCap(window[this.capStyle] ?? ROUND);

        line(this.ptA.x, this.ptA.y, this.ptB.x, this.ptB.y);

        if (this.shouldShowTip) {
            const barbs = this._getBarbPoints();
            line(this.ptB.x, this.ptB.y, barbs.barb1.x, barbs.barb1.y);
            line(this.ptB.x, this.ptB.y, barbs.barb2.x, barbs.barb2.y);
        }

        strokeCap(ROUND);
        noStroke();
        strokeWeight(1);

        if (this.shouldShowTailPoint) {
            this.ptA.draw();
        }
        if (this.shouldShowMidpoint && this.midpoint) {
            this.midpoint.draw();
        }
    }//end draw

    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);
        strokeCap(window[this.capStyle] ?? ROUND);

        line(x1, y1, x2, y2);

        if (this.shouldShowTip) {
            const barbs = this._getBarbPoints();
            const b1s = grid.userToScreen(barbs.barb1.x, barbs.barb1.y);
            const b2s = grid.userToScreen(barbs.barb2.x, barbs.barb2.y);
            line(x2, y2, b1s.x, b1s.y);
            line(x2, y2, b2s.x, b2s.y);
        }

        strokeCap(ROUND);
        noStroke();
        strokeWeight(1);

        if (this.shouldShowTailPoint) {
            this.ptA.drawOnGrid(grid);
        }
        if (this.shouldShowMidpoint && this.midpoint) {
            this.midpoint.drawOnGrid(grid);
        }
    }//end drawOnGrid

    breathe(sinusoid, t) {
        const midX = (this.ptA.x + this.ptB.x) / 2;
        const midY = (this.ptA.y + this.ptB.y) / 2;
        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;
        const ux = dx / len;
        const uy = dy / len;
        const newHalfLen = sinusoid.getValue(t) / 2;
        this.ptA.x = midX - ux * newHalfLen;
        this.ptA.y = midY - uy * newHalfLen;
        this.ptB.x = midX + ux * newHalfLen;
        this.ptB.y = midY + uy * newHalfLen;
        this._updateDerived();
    }//end breathe

    rotateAboutMidPoint(degPerSec, t) {
        const angleRad = degPerSec * t * Math.PI / 180;
        const midX = (this.originalA.x + this.originalB.x) / 2;
        const midY = (this.originalA.y + this.originalB.y) / 2;
        const dxA  = this.originalA.x - midX;
        const dyA  = this.originalA.y - midY;
        const dxB  = this.originalB.x - midX;
        const dyB  = this.originalB.y - midY;
        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));
        this._updateDerived();
    }//end rotateAboutMidPoint

    rotateAbout(fixedPt, degPerSec, t) {
        const isA       = (fixedPt === this.ptA);
        const origFixed = isA ? this.originalA : this.originalB;
        const origMove  = isA ? this.originalB : this.originalA;
        const angleRad  = degPerSec * t * Math.PI / 180;
        const dx  = origMove.x - origFixed.x;
        const dy  = origMove.y - origFixed.y;
        const newX = origFixed.x + (dx * Math.cos(angleRad) - dy * Math.sin(angleRad));
        const newY = origFixed.y + (dx * Math.sin(angleRad) + dy * Math.cos(angleRad));
        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;
        }
        this._updateDerived();
    }//end rotateAbout

    transform({ sinusoid = null, t = 0, degPerSec = null } = {}) {
        const ax0 = this.originalA.x,  ay0 = this.originalA.y;
        const bx0 = this.originalB.x,  by0 = this.originalB.y;
        const midX = (ax0 + bx0) / 2;
        const midY = (ay0 + by0) / 2;
        let dax = ax0 - midX,  day = ay0 - midY;
        let dbx = bx0 - midX,  dby = by0 - midY;

        if (sinusoid) {
            const origLen = Math.sqrt((bx0 - ax0) ** 2 + (by0 - ay0) ** 2);
            const scale   = origLen === 0 ? 1 : sinusoid.getValue(t) / origLen;
            dax *= scale;  day *= scale;
            dbx *= scale;  dby *= scale;
        }

        if (degPerSec !== null) {
            const r = (degPerSec * t) * Math.PI / 180;
            const c = Math.cos(r),  s = Math.sin(r);
            const rotate = (dx, dy) => [dx * c - dy * s, dx * s + dy * c];
            [dax, day] = rotate(dax, day);
            [dbx, dby] = rotate(dbx, dby);
        }

        this.ptA.x = midX + dax;  this.ptA.y = midY + day;
        this.ptB.x = midX + dbx;  this.ptB.y = midY + dby;
        this._updateDerived();
    }//end transform

    transformAbout(fixedPt, { sinusoid = null, breatheTime = 0, degPerSec = null, rotateTime = 0 } = {}) {
        const isA       = (fixedPt === this.ptA);
        const origFixed = isA ? this.originalA : this.originalB;
        const origMove  = isA ? this.originalB : this.originalA;
        let dx = origMove.x - origFixed.x;
        let dy = origMove.y - origFixed.y;
        const origLen = Math.sqrt(dx * dx + dy * dy);
        if (origLen === 0) return;

        if (sinusoid) {
            const scale = sinusoid.getValue(breatheTime) / origLen;
            dx *= scale;  dy *= scale;
        }

        if (degPerSec !== null) {
            const r = (degPerSec * rotateTime) * Math.PI / 180;
            const c = Math.cos(r),  s = Math.sin(r);
            const newDx = dx * c - dy * s;
            const newDy = dx * s + dy * c;
            dx = newDx;  dy = newDy;
        }

        if (isA) {
            this.ptA.x = origFixed.x;       this.ptA.y = origFixed.y;
            this.ptB.x = origFixed.x + dx;  this.ptB.y = origFixed.y + dy;
        } else {
            this.ptB.x = origFixed.x;       this.ptB.y = origFixed.y;
            this.ptA.x = origFixed.x + dx;  this.ptA.y = origFixed.y + dy;
        }
        this._updateDerived();
    }//end transformAbout

    setThickness(w) {
        this.thickness = w;
    }//end setThickness

    setStrokeCap(cap) {
        this.capStyle = cap;
    }//end setStrokeCap

    setColor(swColor) {
        this.strokeColor = swColor;
    }//end setColor

    setStartAngle(deg) {
        this.startAngleDeg = deg;
        const rad     = deg * Math.PI / 180;
        const midX    = (this.originalA.x + this.originalB.x) / 2;
        const midY    = (this.originalA.y + this.originalB.y) / 2;
        const dxO     = this.originalB.x - this.originalA.x;
        const dyO     = this.originalB.y - this.originalA.y;
        const halfLen = Math.sqrt(dxO * dxO + dyO * dyO) / 2;
        const ux = Math.cos(rad);
        const uy = Math.sin(rad);
        this.originalA.x = midX - ux * halfLen;
        this.originalA.y = midY - uy * halfLen;
        this.originalB.x = midX + ux * halfLen;
        this.originalB.y = midY + uy * halfLen;
        this.ptA.x = this.originalA.x;  this.ptA.y = this.originalA.y;
        this.ptB.x = this.originalB.x;  this.ptB.y = this.originalB.y;
        this._updateDerived();
    }//end setStartAngle

    toString() {
        const angleDeg = (this.angle * 180 / Math.PI).toFixed(1);
        return `SWArrow(` +
            `ptA:(${this.ptA.x.toFixed(2)}, ${this.ptA.y.toFixed(2)}) β†’ ` +
            `ptB:(${this.ptB.x.toFixed(2)}, ${this.ptB.y.toFixed(2)}), ` +
            `len:${this.length.toFixed(2)}, angle:${angleDeg}Β°, ` +
            `tipAngle:${this.tipAngle}Β°, tipFactor:${this.tipFactor})`;
    }//end toString

}//end SWArrow class