Vector Stretcher

A drag-to-discover visualisation of vector magnitude. The learner moves a vector tip around a 2D grid while a live |v| = √(x² + y²) readout, a dashed magnitude circle, and a faint Pythagorean triangle reveal how the components determine length. When the tip reaches the target vector, the palette swings to success and a sonar ring pulses on the magnitude circle — the visual reward for "doubling every component doubles the length."

Vector stretcher on a 2D grid.

|v| = √(3² + 4²) = 5

Drag the arrow tip to stretch the vector. Watch how the length changes as you move it.

not reached yet
Customize
Target tip
6
8
Grid
9
Behaviour

Installation

npx shadcn@latest add https://craftbits.dev/r/vector-stretcher.json

Usage

import { VectorStretcher } from "@craft-bits/viz/vector-stretcher";
 
<VectorStretcher />

Controlled — own the state of the tip:

import { useState } from "react";
import {
  VectorStretcher,
  type VectorStretcherVec,
} from "@craft-bits/viz/vector-stretcher";
 
function Demo() {
  const [vec, setVec] = useState<VectorStretcherVec>({ x: 3, y: 4 });
  return <VectorStretcher vec={vec} onVecChange={setVec} />;
}

Subscribe to the moment of discovery:

<VectorStretcher
  target={{ x: 6, y: 8 }}
  onTargetReached={(v) => analytics.track("vector-stretched", v)}
/>

Understanding the component

  1. The grid and the vector. A 2D grid spanning domain.min…domain.max with dashed axes and integer tick labels. A single vector originates at the origin; the learner manipulates its tip via drag or arrow keys.
  2. The magnitude readout. Below the canvas, a monospaced strip prints |v| = √(x² + y²) = <value>. The numeric value is tweened by motion's imperative animate() on SPRINGS.snap (overridable via transition) so the digit changes smoothly without re-rendering the SVG.
  3. The dashed magnitude circle. A circle of radius |v| centred on the origin traces every point at the same distance — the geometric "what does magnitude even mean" answer.
  4. The Pythagorean triangle. Dashed legs from the tip down to the x-axis and along to the origin, with a small right-angle marker at the corner and x / y labels on each leg.
  5. The target celebration. When the tip lands near target (default [6, 8] — the doubled [3, 4]), the palette swings to var(--cb-success), the arrow head flips, a faint fill paints inside the magnitude circle, and a sonar ring breathes on the circle stroke.
  6. Narration phases. idlezerounitaxistripletargetfree. The triple phase specifically calls out integer Pythagorean triples and narrates the squared-sum identity in full.
  7. Reduced motion. Under prefers-reduced-motion: reduce, the magnitude readout snaps to its target without spring and the sonar ring on the magnitude circle disables.

Props

PropTypeDefaultDescription
vec{ x: number; y: number }Controlled tip position. Pair with onVecChange.
defaultVec{ x: number; y: number }{ x: 3, y: 4 }Initial tip position when uncontrolled.
onVecChange(next) => voidFires on drag and arrow-key updates.
target{ x: number; y: number }{ x: 6, y: 8 }Celebration target.
onTargetReached(vec) => voidFires once when the tip first lands near target.
domain{ min: number; max: number }{ min: 0, max: 9 }Mathematical range along each axis.
snapStepnumber1Snap step for drag (math units).
keyboardStepnumber1Per-arrow-press step. Shift doubles.
widthnumber360Width of the SVG viewBox.
heightnumber320Height of the SVG viewBox.
transitionTransitionSPRINGS.snapOverride the spring used by the readout.
classNamestringMerged onto the root via cn().

Accessibility

  • The SVG is role="img" with an aria-label reporting the current vector and length.
  • The drag handle is a role="slider" element with a position-specific aria-label and aria-valuetext. Reachable via Tab, draggable with the pointer, movable with ArrowUp / ArrowDown / ArrowLeft / ArrowRight (hold Shift for step).
  • The narration paragraph is aria-live="polite" and mutes during drag.
  • Colour is never the only signal: phase is encoded in narration prose and the data-phase attribute on the root.
  • Focus state is rendered as a visible accent-toned ring around the drag handle.
  • Motion respects prefers-reduced-motion: reduce.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/math/VectorStretcher.tsx). The source was a lesson-bound component — it imported SvgLabel from the lesson chrome, hardcoded the [3, 4][6, 8] doubling story, used the non-existent SPRINGS.snappy, and depended on per-track palette tokens. The viz extract drops SvgLabel for raw <text> with --cb-* semantic tokens, generalises defaultVec, target, domain, and the snap and keyboard steps into props, exposes a Radix-style controlled/uncontrolled vec + onVecChange API, surfaces the moment of discovery as onTargetReached, swaps SPRINGS.snappy for the canonical SPRINGS.snap, and lets callers override it via transition.