Shadow Caster

A drag-to-discover visualisation of the geometric dot product a · b = |a||b| cos θ. Vector a lies fixed along the x-axis; the learner rotates a second vector b on a circle of radius |b| and watches the projection of b onto a's axis — the "shadow" — extend, vanish, and flip behind the origin as the angle changes.

Three ways to drive θ: drag the tip of b, drag the foot of the shadow along a's axis, or arrow-key the rotation. The dot product is rendered both as a tabular-numerics value under the shadow foot and as a sign-coloured wave-shaped band on the axis.

Shadow caster on a 2D plane.7.38
θ
35°
cos θ
0.82
a · b
7.38

a · b = |a| · |b| · cos θ = 3.0 · 3.0 · cos(35°) = 7.38

Customize
Vector magnitudes
3.0
3.0
Behaviour
0.15
5
Canvas
320 px

Installation

npx shadcn@latest add https://craftbits.dev/r/shadow-caster.json

Usage

import { ShadowCaster } from "@craft-bits/viz/shadow-caster";
 
<ShadowCaster />

Controlled — own the angle of b:

import { useState } from "react";
import { ShadowCaster } from "@craft-bits/viz/shadow-caster";
 
function Demo() {
  const [theta, setTheta] = useState(0.61);
  return <ShadowCaster theta={theta} onThetaChange={setTheta} />;
}

Override magnitudes when you want different a · b extrema:

<ShadowCaster magA={4} magB={2} />

Hide the textual readouts and formula for a pure visual:

<ShadowCaster hideReadouts hideFormula />

Understanding the component

  1. The plane and the two vectors. Centred 2D grid spanning ±5 in math units with dashed axes. Vector a is drawn along the positive x-axis in the accent palette, vector b rotates on a dashed arc of radius |b| in the warning palette.
  2. The shadow. A drop-line falls from the tip of b straight down to a's axis. The segment from the origin to the foot of that drop-line is the shadow — its signed length is |b| cos θ. Under the segment, a sine-wave band fills with var(--cb-success) (positive shadow) or var(--cb-error) (negative shadow, behind the origin).
  3. The dot-product readout. A monospaced tabular number tracks beneath the shadow foot. The value is tweened by motion's imperative animate() on SPRINGS.snap (overridable via transition) so the digit changes smoothly without re-rendering the SVG every frame.
  4. The perpendicular indicator. When |a · b| drops below perpendicularThreshold (default 0.15), a small right-angle marker appears at the origin in the success palette. The shadow has vanished — b is perpendicular to a.
  5. Three drag handles. The tip of b (rotation along the arc), the arc track itself (rotation by direct click), and the foot of the shadow (1-D drag along a's axis that projects back to an angle). All three are role="slider" elements reachable via Tab.
  6. Keyboard. ArrowLeft / ArrowDown decrease θ; ArrowRight / ArrowUp increase. Home resets to 0. Shift triples the step. On the shadow-foot handle, ArrowLeft / ArrowRight move the foot left/right along the axis with the angle following.
  7. Reduced motion. Under prefers-reduced-motion: reduce, every spring collapses to an instant attribute set.

Props

PropTypeDefaultDescription
thetanumberControlled angle of b in radians, CCW from a's axis. Pair with onThetaChange.
defaultThetanumber0.61Initial angle when uncontrolled.
onThetaChange(next) => voidFires on drag and arrow-key updates.
magAnumber3Length of vector a (fixed along x).
magBnumber3Length of vector b (arc radius).
perpendicularThresholdnumber0.15`
keyboardStepDegnumber5Per-arrow-press step in degrees. Shift triples.
sizenumber320Side length (px) of the SVG viewBox.
hideReadoutsbooleanfalseHide the θ / cos θ / a · b readout strip.
hideFormulabooleanfalseHide the formula line under the canvas.
transitionTransitionSPRINGS.snapOverride the spring used by the dot-product readout.
classNamestringMerged onto the root via cn().

Accessibility

  • The SVG is role="img" with an aria-label reporting a's length, b's angle and length, and the current dot product.
  • All three drag targets (b-tip, arc track, shadow foot) are role="slider" elements with aria-valuenow / aria-valuetext. Reachable via Tab, draggable with the pointer, movable with arrow keys (hold Shift for step).
  • Colour is never the only signal: sign is encoded in the data-sign="positive" | "zero" | "negative" attribute on the root and in the right-angle marker that springs in at perpendicular.
  • Focus state is rendered as a visible accent-toned cursor change on each drag handle.
  • Motion respects prefers-reduced-motion: reduce — the dot-product readout snaps to the new value rather than tweening.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/math/ShadowCaster.tsx). The source was a lesson-bound component — it baked in three modes (explore / predict / challenge) wired to the lesson chrome, an in-component scoring system, and imports of SvgLabel / ChallengeBtn / FeedbackBadge / ScoreDots / DoneCard / ModeStrip from the project's lesson primitives. The viz extract drops every piece of mode UI and lesson chrome, generalises magA / magB / perpendicularThreshold / keyboardStepDeg / size into props, exposes a Radix-style controlled/uncontrolled theta + onThetaChange API, removes the SVG-internal SvgLabel axis labels, remaps every colour to var(--cb-*) semantic tokens so consumer themes repaint freely, and replaces the source's non-existent SPRINGS.snappy with SPRINGS.snap.