GAP Collapse Viz

A teaching primitive for Global Average Pooling. Render an H × W × C feature map as one mini heatmap per channel; as progress advances from 0 to 1, every cell shrinks toward its grid centre, the per-cell labels fade, and a single bold scalar — the channel mean — emerges in the middle of each grid. The result is the GAP output: C numbers, zero parameters, any spatial size collapsed to 1 × 1. Use it to teach CNN classifier heads, why GAP replaces fully-connected layers in modern architectures, or how spatial information is intentionally discarded in favour of channel-wise summaries.

Global Average Pooling · 6×6×4 → 1×1×4Global average pooling. 6 by 6 by 4 feature map collapsing to a 1 by 1 by 4 vector. Progress 0 percent.
Global Average Pooling visualisation. Progress 0 percent.6×6×4 → 1×1×4ch 1ch 2ch 3ch 4
Customize
Playback
1800
Scrub (autoplay off)
0.00

Installation

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

Usage

import { GAPCollapseViz } from "@craft-bits/core";
 
<GAPCollapseViz defaultPlaying />;

Drive the collapse from outside — e.g. synced to a scroll step:

const [progress, setProgress] = useState(0);
 
<GAPCollapseViz
  progress={progress}
  onProgressChange={setProgress}
/>;

Provide your own feature map (shape [H][W][C]):

const featureMap: number[][][] = /* H × W × C numbers */ [];
 
<GAPCollapseViz inputMap={featureMap} defaultPlaying playSpeed={2200} />;

Understanding the component

  1. Three nested arrays. inputMap is shaped [H][W][C] — height, then width, then channels. Every channel is a separate H × W heatmap; the component renders one grid per channel, all stacked side by side, so the collapse happens in parallel.
  2. Progress is the animation phase. progress = 0 shows the full spatial feature map; progress = 1 shows each grid collapsed to a single bold scalar. The shrink uses an ease-in curve () so the collapse feels accelerating, like a real reduction.
  3. GAP is the channel mean. For each channel the component precomputes mean(map[:, :, c]) once. As cells shrink and slide toward their channel's centre, that scalar fades in — visually the cell soup becomes the scalar.
  4. Per-channel magnitudes for the heatmap. Cell opacity is normalised by each channel's own max |value|, so a near-empty channel reads as faint and a peaky channel reads as bright. The teaching point — that a strong-feature channel ends up with a high scalar and a weak-feature channel with a low one — survives without a global colour scale.
  5. Default 6×6×4 fixture. When inputMap is omitted, a deterministic feature map is synthesised with four channels that peak differently (left edge, centre blob, diagonal stripe, low background noise). Each channel's GAP average lands at a clearly different magnitude, so the collapsed output reads as four distinct numbers.
  6. Controlled or uncontrolled progress. Pass progress + onProgressChange to drive from outside (e.g. synced to a scroll scrubber), or leave it uncontrolled and pair with playing / defaultPlaying for autoplay.
  7. requestAnimationFrame autoplay. When playing, the component advances progress smoothly via a RAF loop scaled to playSpeed ms per sweep, holds briefly at 1 for the collapsed reveal, then snaps back to 0 for the next loop.
  8. Reduced-motion fallback. prefers-reduced-motion: reduce disables autoplay automatically and replaces the SPRINGS.smooth motion with a duration: 0 snap.

Props

PropTypeDefaultDescription
inputMapreadonly number[][][]synthesised 6×6×4Feature map shaped [H][W][C].
progressnumberControlled animation phase in [0, 1].
defaultProgressnumber0Uncontrolled initial progress.
onProgressChange(progress: number) => voidFires on every autoplay tick or scrub.
playingbooleanControlled play state. Pair with onPlayingChange.
defaultPlayingbooleanfalseUncontrolled initial play state.
onPlayingChange(playing: boolean) => voidFires when play / pause flips.
playSpeednumber1800Milliseconds for one 0 → 1 sweep.
transitionTransitionSPRINGS.smoothSpring used for per-channel label transitions.
classNamestringMerged onto the root via cn().

Accessibility

  • The figure is role="figure" with an aria-labelledby heading announcing the input shape (H × W × C → 1 × 1 × C) and an aria-live="polite" summary that announces the current progress and — once collapsed — the per-channel averages.
  • The <svg> carries role="img" with a <title> describing the current progress percentage, so non-sighted users get a textual snapshot even without the surrounding heading.
  • Color is never the only signal — channel labels (ch 1, ch 2, …) and the bold collapsed scalars are textual; the output band names the fact-sheet (C numbers · zero params).
  • prefers-reduced-motion: reduce disables autoplay and replaces the SPRINGS.smooth motion with an instant snap; users can still scrub through the collapse via the controlled progress prop.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/nn/GAPCollapseViz.tsx). Stripped the lesson-specific phase-narration state machine, the bespoke 4×4×3 fixture with hand-coded channel names (Edge / Blob / Background), the ChallengeBtn controls, and the slider chrome — generalised to a pure GAP primitive that accepts an arbitrary H × W × C feature map and exposes controlled / uncontrolled progress + playing so consumers can drive the collapse from scroll, a parent stepper, or a debug panel. Channel labels are now generic ch N, opacity is per-channel normalised, and autoplay uses a RAF sweep instead of a setTimeout ladder.