SketchWave Arrow Class Reference
Directed arrows with animated shafts and arrowheads for dynamic geometry
Back to SWArrow Demo 1Table of Contents
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:
ptAis always the tail (blunt end);ptBis always the tip (arrowhead end) - Configurable Arrowhead:
tipAnglecontrols barb spread (narrow = pointer; wide = broad);tipFactorcontrols barb length relative to shaft - Auto Midpoint: A draggable
SWPointmidpoint 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/originalBpose snapshot so all rotation and breathing methods pivot from a stable reference frame _updateDerived()which keepslength,angle, and the midpoint SWPoint in sync after any positional change
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
startAngleDegfrom the initial ptAβptB direction usingMath.atan2 - Stores
originalAandoriginalBsnapshots for rotation-anchor math - Creates a midpoint SWPoint with a slightly darker shade of
strokeColorand a size ofmax(thickness Γ 2, 8)px - Runs
_updateDerived()to computelength,angle, and the midpoint position - Defaults
shouldShowTip,shouldShowTailPoint, andshouldShowMidpointall totrue
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. |
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
strokeColorandthickness - Applies
capStyleto shaft ends - Draws the shaft from
ptAtoptB - Draws the two arrowhead barbs from
ptBifshouldShowTipis true - Draws the tail dot (
ptA.draw()) ifshouldShowTailPointis true - Draws the midpoint dot if
shouldShowMidpointis 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);
}
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.ptAorthis.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);
}
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;
nullto skip - t (number) β Time in seconds
- degPerSec (number, optional) β Angular velocity;
nullto 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.ptAorthis.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);
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()ortransform() - Spin about tail (clock-hand) β
rotateAbout(myArrow.ptA, β¦)ortransformAbout(myArrow.ptA, β¦) - Spin about tip β
rotateAbout(myArrow.ptB, β¦)
5. Use drawOnGrid() for Math Work
- Use
draw()only whenptA/ptBare 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: 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