Merge Viz

A primitive for the moment two things become one. Left source, right source, merged result in the middle; arrows point inward; as progress advances from 0 to 1 the two sources shrink, slide toward the centre, and fade out while the merged box fades in. Used to teach byte-pair encoding ("l" + "o""lo"), token concatenation, vocabulary merges, and LoRA / model merging — anywhere the lesson is "two ingredients combine into one product."

Merge visualisation: a and b into merged.Merge of a = l and b = o into merged = lo. Progress 0 percent.
amergedb
0%
Customize
Pair
l + o
Playback
1200

Installation

npx shadcn@latest add https://craftbits.dev/r/merge-viz.json

Usage

import { MergeViz } from "@craft-bits/core";
 
<MergeViz a="l" b="o" defaultPlaying />

Drive the progress externally from a narration step:

const [t, setT] = useState(0);
 
<MergeViz
  a="un"
  b="happy"
  merged="unhappy"
  progress={t}
  onProgressChange={setT}
/>

Relabel the slots — useful when the merge isn't really a / b / merged:

<MergeViz
  a="W_base"
  b="ΔW_lora"
  merged="W_merged"
  labelA="base"
  labelB="adapter"
  labelMerged="deployed"
/>

Understanding the component

  1. Three slots, one direction. The visual reads strictly left → centre ← right. Source a sits on the left, source b on the right, the merged result lives in the centre. Two arrows point inward so the eye knows where the energy is going before any animation happens.
  2. Progress drives geometry. A single progress value in [0, 1] interpolates everything — the source boxes' x, opacity, and scale, and the merged box's opacity and scale. The interpolation is smoothstepped so even a linear scrub feels eased.
  3. Sources fade as the merged box arrives. Sources fade to ~15% opacity and shrink to ~82% scale at full progress; the merged box stays invisible until t ≈ 0.45 and then springs in. The result is that the eye is never asked to track three solid boxes at once.
  4. Controlled and uncontrolled — for both progress and playing. Pair progress + onProgressChange to drive from a parent (narration step, scrubber, lesson phase). Or pass defaultProgress + defaultPlaying and the component manages its own state.
  5. Autoplay sweeps 0 → 1 and resets. When playing, an internal requestAnimationFrame loop ramps progress toward 1 over playSpeed ms, holds at 1 for a beat, then snaps back to 0 for the next sweep. The loop is cancelled on unmount and when playing flips off — no orphaned timers.
  6. Reduced motion is honest. With prefers-reduced-motion: reduce, autoplay is force-stopped and every spring collapses to instant — the diagram still tells the story; it just doesn't move.

Props

PropTypeDefaultDescription
astringrequiredLeft source value.
bstringrequiredRight source value.
mergedstringa + bResult rendered in the centre box.
progressnumberControlled animation phase in [0, 1].
defaultProgressnumber0Uncontrolled initial progress.
onProgressChange(progress: number) => voidFires when progress changes (autoplay tick or scrub).
playingbooleanControlled play state.
defaultPlayingbooleanfalseUncontrolled initial play state.
onPlayingChange(playing: boolean) => voidFires when play / pause flips.
playSpeednumber1200Milliseconds for one full 0 → 1 sweep.
labelAstring"a"Label above the left source.
labelBstring"b"Label above the right source.
labelMergedstring"merged"Label above the merged box.
transitionTransitionSPRINGS.smoothSpring for the box position / size transitions.
classNamestringMerged onto the root via cn().

Accessibility

  • The outer element is role="figure" with an aria-label and a visually hidden aria-live="polite" summary — screen readers hear the two sources, the merged value, and the current progress percentage whenever it changes.
  • Colour is never the only signal — the merged box is also distinguished by a thicker accent stroke and bolder text, and arrows point inward toward it independent of colour.
  • The play / pause button carries aria-pressed and a descriptive aria-label; the scrubber is an <input type="range"> with its own aria-label="Merge progress".
  • prefers-reduced-motion: reduce force-stops autoplay and collapses every spring transition to an instant swap. The button is also disabled in that mode.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/nn/MergeViz.tsx). The source was a LoRA-merging widget — a 2×2 matrix scene with an alpha slider for W_merged = W + α·A·B, per-cell colour-coded deltas, a phase machine, and a narration block. The library version drops the matrix-arithmetic conceit and ships the prior primitive every merge lesson actually needs: two source slots collapsing into one centre slot under a single progress parameter. Matrix-arithmetic merges remain a thin wrapper layer that any consumer can build on top by relabelling the slots and driving progress from an α knob.