Dimension Changer
A 3-component input vector enters a 2×3 matrix "lens." Two values emerge on the other side and the third input component is projected away — it contributed to both dot products but landed in neither output slot.
The walkthrough runs as a three-phase narrative — idle, animating, revealed — advanced by a single action button or by a controlling parent via the phase / onPhaseChange API. The visual is the mental model behind every dimensionality-reduction technique: even if your input has more components than your matrix has rows, the dot-product machinery still runs across every input — the rows just stop earlier.
A 3D vector is about to enter a 2×3 matrix lens. The matrix has 2 rows — it can only produce 2 outputs. One dimension will not make it through.
Installation
npx shadcn@latest add https://craftbits.dev/r/dimension-changer.jsonUsage
import { DimensionChanger } from "@craft-bits/viz/dimension-changer";
<DimensionChanger />Drive the walkthrough from a parent:
const [phase, setPhase] = useState<DimensionChangerPhase>("idle");
<DimensionChanger phase={phase} onPhaseChange={setPhase} />Render a custom action button instead of the built-in one:
<DimensionChanger
hideAction
renderAction={({ label, disabled, onClick }) => (
<MyButton onClick={onClick} disabled={disabled}>{label}</MyButton>
)}
/>Swap in your own matrix and vector — any 2×3 / 3-vector pair renders correctly:
<DimensionChanger
matrix={[
[1, 0, 0],
[0, 1, 0],
]}
vector={[5, -3, 9]}
/>Hide the narration when embedding inside an article that already explains the projection:
<DimensionChanger hideNarration />Understanding the component
- Layout in three zones. The SVG splits horizontally into an input column (three stacked slots, one per vector component), a matrix lens framed by serif brackets, and an output column with two slots.
- Row-2 fades into the lens. When the visitor clicks "Send through", the third input slot translates right and fades — a physical metaphor for the component that will not survive the projection.
- Flow lines draw, cells highlight column-by-column. A dashed accent line draws from the input column into the lens, then row 0's three cells light up left-to-right at 200ms intervals, followed by row 1's three cells. The staggered highlights trace the two dot products literally.
- Outputs fan out and count up. Two fan lines draw to the output column, then each output text counter animates from
0to its result. The third input slot stays gone; an italic "projected away" annotation pins underneath where it used to be. - Breakdown below the SVG. Once revealed, two monospace lines show each dot product term-by-term. The factor that came from
x[2]is coloured so the visitor can see exactly which contribution to each row came from the projected-away component. - Reduced motion. Under
prefers-reduced-motion: reduce, the entire timeline snaps to its revealed end-state without animating — the visual conclusion is preserved.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
matrix | [[n,n,n],[n,n,n]] | [[1,0,2],[3,1,-1]] | The 2×3 lens. Row count fixed at 2; column count fixed at 3. |
vector | [n,n,n] | [1,2,4] | The 3-component input vector. |
phase | "idle" | "animating" | "revealed" | — | Controlled phase. Pair with onPhaseChange. |
defaultPhase | "idle" | "animating" | "revealed" | "idle" | Initial phase for the uncontrolled API. |
onPhaseChange | (next) => void | — | Fires after the component commits to the next phase. |
transition | Transition | SPRINGS.snap | Override the spring for the row-2 input fade. |
actionLabels | Partial<Record<phase, string>> | — | Override the action-button label per phase. |
narrationCopy | Partial<Record<phase, string>> | — | Override the narration copy per phase. |
hideNarration | boolean | false | Hide the narration paragraph. |
hideBreakdown | boolean | false | Hide the per-row dot-product breakdown. |
hideAction | boolean | false | Hide the built-in action button. |
renderAction | ({ phase, label, disabled, onClick }) => ReactNode | — | Render slot for a custom action button. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The root carries
aria-labelledbypointing to a hidden<span>summary of the current phase, so screen readers hear the narrative state without parsing the SVG. - The SVG itself is
role="img"with the samearia-label, so consumers who slot the SVG into another tree still surface a readable name. - An
aria-live="assertive"region under the SVG announces the revealed result ("Dimensionality reduction complete. Output vector: …") so non-sighted users hear the conclusion. - The action button is a real
<button type="button">with:focus-visibleoutline,disabledstate during animation, and standard keyboard activation. - Status is never the only signal — colour signals (accent for row 0, warning for row 1) are reinforced by row position and the
projected awayannotation. - Motion respects
prefers-reduced-motion: reduce— the entire timeline collapses to an instant attribute set with no springs running.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/math/DimensionChanger.tsx). The library extract drops the project-specificSvgLabelandChallengeBtnchrome in favour of plain<text>and acn()-styled<button>, swaps the per-tone--color-accent-*/--color-warn-*/--color-ink-*palette for the canonical--cb-accent/--cb-warning/--cb-fgtoken vars, replaces the inlineSPRINGS.snappyreference withSPRINGS.snap, and adds a controlledphase/onPhaseChangeAPI plusrenderAction/narrationCopy/actionLabelsslots so consumers can drive the walkthrough from a parent stepper, a keyboard, or a custom button.