Create animated, narrated programming courses with synchronized visuals
Lessons in Handhold are narrated, animated presentations that teach programming concepts through synchronized visuals and audio. Each lesson is authored in a custom Markdown format that compiles into an interactive presentation with precise timing control.
A lesson consists of steps (top-level sections) that contain narration blocks and visualization blocks. Each step is a self-contained teaching unit with its own scenes and animations.
---title: Binary Search Trees---# IntroductionA {{binary search tree}} maintains the invariant that {{show: tree}} left children are smaller than their parent.```data:tree type=tree variant=bst 8 / \ 3 10 / \ \1 6 14
### Key Components<AccordionGroup> <Accordion title="Frontmatter"> YAML frontmatter at the top of the file defines the lesson title and metadata. </Accordion> <Accordion title="Steps (H1 Headings)"> Each `#` heading creates a new step. Steps are the primary navigation units in the presentation. </Accordion> <Accordion title="Narration Paragraphs"> Regular text paragraphs become narration. Text inside `{{...}}` markers creates trigger points for animations. </Accordion> <Accordion title="Visualization Blocks"> Code fences with special language identifiers (`code`, `data`, `diagram`, `math`, `chart`, `preview`) define visual elements. </Accordion></AccordionGroup>## Visualization TypesHandhold supports rich visualization blocks that render programming concepts:### Code BlocksStandard code blocks with syntax highlighting, focus ranges, and inline annotations:```markdown```javascript:quicksort lang=javascriptfunction quicksort(arr) { if (arr.length <= 1) return arr; // ! Base case const pivot = arr[0]; return [...quicksort(left), pivot, ...quicksort(right)];}
**Parameters:**- `:name` - Named identifier for show/hide triggers- `lang=` - Language for syntax highlighting- `// !` - Inline annotations (appear as callouts)### Data StructuresVisualize dynamic data structures with the `data` block type:```markdown```data:tree type=tree variant=bst 5 / \ 3 8 / \ / \1 4 6 9
**Supported structures:** `array`, `linked-list`, `tree`, `graph`, `stack`, `queue`, `hash-map`, `skip-list`, `b-tree`, `trie`, `bloom-filter`, and more (see src/types/lesson.ts:162-395).**Tree variants:** `bst`, `avl`, `red-black`, `heap-min`, `heap-max`, `splay`, `segment`, `merkle`### System DiagramsCreate architecture diagrams with nodes and edges:```markdown```diagram:archclient -> api-gateway: HTTP Requestapi-gateway -> service: gRPCservice -> database: Query
**Chart types:** `bar`, `line`, `scatter`, `area`, `pie`, `radar`, `radial`### Live PreviewsRender HTML or React components:```markdown```preview:demo template=reactimport { useState } from 'react';export default () => { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>;};
## Animation TriggersNarration text contains triggers that control what appears on screen and when. Triggers are embedded in `{{...}}` markers.### Show/Hide Verbs```markdownFirst we {{show: array slide 0.5s}} create an array.Then {{hide: array fade}} we remove it.
We start with an unsorted {{show: unsorted}} array.After sorting, it becomes {{transform: unsorted -> sorted}} this.```data:unsorted type=array[5, 2, 8, 1, 9]
type=array
[1, 2, 5, 8, 9]
## Regions and FocusVisualization blocks can define named **regions** that enable fine-grained animation control:```markdown```javascript:codefunction search(arr, target) { // #region loop for (let i = 0; i < arr.length; i++) { if (arr[i] === target) return i; } // #endregion return -1;}
The loop iterates through the array.
Regions work with `focus`, `annotate`, `pulse`, `trace`, `pan`, and `draw` verbs.## Scene SystemThe parser compiles narration and triggers into a **scene sequence**. Each trigger advances to the next scene. Scenes define:- Which visualization slots are visible- Transition effects (fade, slide, instant)- Enter/exit animations per slot- Focus, pulse, trace, and annotation state- Transform sources and targetsScenes are computed at parse time (see src/parser/build-scenes.ts) and drive the presentation renderer.## Playback IntegrationLessons integrate with the TTS narration system:1. Parser extracts narration text from all paragraphs2. TTS synthesizes audio with word-level timing data3. Timeline builder (src/presentation/build-timeline.ts) maps trigger points to audio timestamps4. Playback orchestrator advances scenes synchronized with audio playbackThe presentation store (src/presentation/store.ts) manages:- Current step index- Current scene index- Current word index (for highlighting)- Playback status (idle, playing, paused)- Playback rate## DiagnosticsThe parser validates lessons and emits warnings for:- Unknown block targets in show/hide verbs- Unknown regions in focus/annotate verbs- Transform operations between incompatible block types- Narration paragraphs with no visual triggers- Steps with visuals but no narrationDiagnostics include precise location information (step, paragraph, trigger index) for debugging.## Best Practices<Steps> <Step title="One concept per step"> Each H1 section should teach a single, focused idea. Steps are the natural navigation boundary. </Step> <Step title="Name your blocks"> Use explicit names (`:name`) instead of relying on auto-generated names for clarity. </Step> <Step title="Use regions for code focus"> Define `#region` markers in code to enable precise highlighting without duplicating blocks. </Step> <Step title="Coordinate animations with narration"> Place triggers at natural speech boundaries. Use `slide` for spatial reveals, `fade` for concept transitions. </Step> <Step title="Test transform pairs"> Ensure transform source and target have compatible structures (same visualization kind). </Step></Steps>## Example Lesson```markdown---title: Quick Sort Visualization---# Algorithm OverviewQuicksort is a {{show: code slide}} divide-and-conquer algorithm that {{focus: partition}} partitions the array around a pivot.```javascript:code lang=javascriptfunction quicksort(arr, lo, hi) { if (lo >= hi) return; // #region partition const p = partition(arr, lo, hi); // #endregion quicksort(arr, lo, p - 1); quicksort(arr, p + 1, hi);}
{{clear fade}} Let's {{show: unsorted slide-up}} sort this array.\`\`\`data:unsorted type=array[7, 2, 1, 6, 8, 5, 3, 4]\`\`\`After partitioning around pivot 4, we get {{transform: unsorted -> partitioned spring}}.\`\`\`data:partitioned type=array[2, 1, 3, 4, 7, 6, 8, 5]\`\`\`
Lesson Markdown files are parsed by src/parser/parse-lesson.ts into a typed IR (ParsedLesson) that enforces structural validity and enables compile-time guarantees.