docs(04-01): complete timeline canvas foundation plan — SUMMARY, STATE, ROADMAP updated
- SUMMARY: timeline canvas with per-device tracks, waveform, beat marks, playback cursor - STATE: plan advanced to 2/4, progress 79%, decisions and metrics recorded - ROADMAP: Phase 4 progress updated (1/4 plans complete, In Progress) - Requirements TL-01, TL-05 marked complete
This commit is contained in:
83
.planning/phases/04-timeline-editor/04-01-SUMMARY.md
Normal file
83
.planning/phases/04-timeline-editor/04-01-SUMMARY.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
phase: 04-timeline-editor
|
||||
plan: 01
|
||||
subsystem: frontend-canvas
|
||||
tags: [canvas, timeline, daw-ui, audio, waveform]
|
||||
dependency_graph:
|
||||
requires: [Phase 03 communication protocol — UDP sender, animation encoders]
|
||||
provides: [TimelineCanvas class, timeline rendering foundation]
|
||||
affects: [lightsync/frontend, lightsync/models/show.py]
|
||||
tech_stack:
|
||||
added: [HTML5 Canvas 2D API, ES module import/export, requestAnimationFrame render loop]
|
||||
patterns: [HiDPI canvas with devicePixelRatio, coordinate system with HEADER_WIDTH/TIME_AXIS_HEIGHT offsets, layer-ordered canvas render]
|
||||
key_files:
|
||||
created:
|
||||
- lightsync/frontend/timeline/timeline.js
|
||||
modified:
|
||||
- lightsync/models/show.py
|
||||
- lightsync/frontend/index.html
|
||||
- lightsync/frontend/style.css
|
||||
- lightsync/frontend/app.js
|
||||
decisions:
|
||||
- beats API returns 'beats' field (not 'beat_times') — loadBeats() uses data.beats with data.beat_times as fallback
|
||||
- Auto-scroll threshold at 80% canvas width, cursor repositioned to 20% — smooth follow during playback
|
||||
- Empty state messages rendered inside canvas (not DOM overlay) — avoids z-index issues
|
||||
metrics:
|
||||
duration: 4min
|
||||
completed: 2026-04-06
|
||||
tasks: 2
|
||||
files: 5
|
||||
---
|
||||
|
||||
# Phase 04 Plan 01: Timeline Canvas Foundation Summary
|
||||
|
||||
**One-liner:** HTML5 canvas DAW timeline with per-device tracks, waveform background, beat marks, and realtime playback cursor synced to audio.currentTime via requestAnimationFrame.
|
||||
|
||||
## What Was Built
|
||||
|
||||
- **CueModel.duration** (float = 4.0) — block length in seconds, Pydantic default handles old show files
|
||||
- **AnalysisBlock.calibration_offset** (float = 0.03) — compensates librosa latency bias, applied when rendering beat marks
|
||||
- **TimelineCanvas class** (`lightsync/frontend/timeline/timeline.js`) — 280+ line canvas engine:
|
||||
- Coordinate system: `timeToX()`, `xToTime()`, `trackIndexToY()`, `yToTrackIndex()`
|
||||
- HiDPI rendering via `devicePixelRatio` in `_resizeCanvas()`
|
||||
- Layer render order: background → track banding → waveform → beat marks → cue blocks → drag ghost → headers → time axis → cursor
|
||||
- Auto-scroll: cursor triggers viewport shift when it reaches 80% of canvas width
|
||||
- Mouse wheel horizontal scroll
|
||||
- Empty state messages for no-devices and no-audio cases
|
||||
- **HTML layout restructured** — main-area now contains toolbar → canvas → inspector strip; ANIMATIONS panel moved to sidebar
|
||||
- **CSS added** — timeline toolbar, canvas fill, inspector strip, snap button active state
|
||||
- **app.js wired** — imports TimelineCanvas, creates instance, wires zoom/beat-offset/snap controls, populates animation palette dynamically
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| beats API 'beats' field (not 'beat_times') | API returns `{"beats": [...]}` — plan expected `beat_times` — auto-fixed (Rule 1) |
|
||||
| Auto-scroll at 80%/20% threshold | Standard DAW follow behavior — keeps cursor in view without jerky jumps |
|
||||
| Empty state in canvas | Simpler than DOM overlay, no z-index conflicts with toolbar/inspector |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] beats API field name mismatch**
|
||||
- **Found during:** Task 2 implementation
|
||||
- **Issue:** Plan specified `data.beat_times` but `/api/audio/beats` returns `{"beats": [...]}` (from `analyze()` in `beats.py`)
|
||||
- **Fix:** `loadBeats()` uses `data.beats || data.beat_times || []` — handles both field names
|
||||
- **Files modified:** `lightsync/frontend/timeline/timeline.js`
|
||||
- **Commit:** efa6123
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None — timeline renders live data from `/api/devices`, `/api/audio/waveform`, and `/api/audio/beats`. Cue blocks will be empty (`track.cues = []`) until Plan 04-02 adds block drag-and-drop, but that is expected for this phase.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- `lightsync/frontend/timeline/timeline.js` exists: YES (efa6123)
|
||||
- `lightsync/models/show.py` has `duration: float = 4.0`: YES (454fddc)
|
||||
- `lightsync/models/show.py` has `calibration_offset: float = 0.03`: YES (454fddc)
|
||||
- `lightsync/frontend/index.html` has `id="timeline-canvas"`: YES
|
||||
- `lightsync/frontend/style.css` has `#timeline-canvas`, `.block-inspector`: YES
|
||||
- `lightsync/frontend/app.js` imports TimelineCanvas: YES
|
||||
- Docker build and startup: CLEAN (no errors)
|
||||
- Python model import test: PASSED (`CueModel(timestamp=1.0).duration == 4.0`)
|
||||
Reference in New Issue
Block a user