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.
not reached yet|v| = √(3² + 4²) = 5
Drag the arrow tip to stretch the vector. Watch how the length changes as you move it.
Customize
Target tip
6
8
Grid
9
Behaviour
Installation
npx shadcn@latest add https://craftbits.dev/r/vector-stretcher.jsonUsage
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
- The grid and the vector. A 2D grid spanning
domain.min…domain.maxwith dashed axes and integer tick labels. A single vector originates at the origin; the learner manipulates its tip via drag or arrow keys. - The magnitude readout. Below the canvas, a monospaced strip prints
|v| = √(x² + y²) = <value>. The numeric value is tweened bymotion's imperativeanimate()onSPRINGS.snap(overridable viatransition) so the digit changes smoothly without re-rendering the SVG. - 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. - 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/ylabels on each leg. - The target celebration. When the tip lands near
target(default[6, 8]— the doubled[3, 4]), the palette swings tovar(--cb-success), the arrow head flips, a faint fill paints inside the magnitude circle, and a sonar ring breathes on the circle stroke. - Narration phases.
idle→zero→unit→axis→triple→target→free. Thetriplephase specifically calls out integer Pythagorean triples and narrates the squared-sum identity in full. - 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
| Prop | Type | Default | Description |
|---|---|---|---|
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) => void | — | Fires on drag and arrow-key updates. |
target | { x: number; y: number } | { x: 6, y: 8 } | Celebration target. |
onTargetReached | (vec) => void | — | Fires once when the tip first lands near target. |
domain | { min: number; max: number } | { min: 0, max: 9 } | Mathematical range along each axis. |
snapStep | number | 1 | Snap step for drag (math units). |
keyboardStep | number | 1 | Per-arrow-press step. Shift doubles. |
width | number | 360 | Width of the SVG viewBox. |
height | number | 320 | Height of the SVG viewBox. |
transition | Transition | SPRINGS.snap | Override the spring used by the readout. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The SVG is
role="img"with anaria-labelreporting the current vector and length. - The drag handle is a
role="slider"element with a position-specificaria-labelandaria-valuetext. Reachable via Tab, draggable with the pointer, movable withArrowUp/ArrowDown/ArrowLeft/ArrowRight(holdShiftfor2×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-phaseattribute 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 importedSvgLabelfrom the lesson chrome, hardcoded the[3, 4]→[6, 8]doubling story, used the non-existentSPRINGS.snappy, and depended on per-track palette tokens. The viz extract dropsSvgLabelfor raw<text>with--cb-*semantic tokens, generalisesdefaultVec,target,domain, and the snap and keyboard steps into props, exposes a Radix-style controlled/uncontrolledvec+onVecChangeAPI, surfaces the moment of discovery asonTargetReached, swapsSPRINGS.snappyfor the canonicalSPRINGS.snap, and lets callers override it viatransition.