diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 9e65db4..14401b7 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -87,7 +87,7 @@ Plans: 4. Each block has a color picker and editable parameters (speed, colors, direction, length); changes persist in the show file 5. The playback cursor moves in real time across the timeline as audio plays, reflecting audio position accurately 6. A beat calibration offset control shifts all beat marks backward by a configurable amount to compensate for librosa's systematic latency bias -**Plans**: 3 plans +**Plans**: 4 plans Plans: - [ ] 04-01: Timeline canvas — per-device tracks, time axis, block rendering, playback cursor diff --git a/.planning/phases/04-timeline-editor/04-01-PLAN.md b/.planning/phases/04-timeline-editor/04-01-PLAN.md new file mode 100644 index 0000000..174b370 --- /dev/null +++ b/.planning/phases/04-timeline-editor/04-01-PLAN.md @@ -0,0 +1,556 @@ +--- +phase: 04-timeline-editor +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - lightsync/models/show.py + - lightsync/frontend/index.html + - lightsync/frontend/style.css + - lightsync/frontend/timeline/timeline.js +autonomous: true +requirements: [TL-01, TL-05] + +must_haves: + truths: + - "Per-device horizontal tracks render on a time axis with device name labels" + - "Playback cursor moves across the timeline in realtime as audio plays" + - "Waveform peaks render as a background layer behind tracks" + - "CueModel has a duration field that defaults to 4.0" + artifacts: + - path: "lightsync/models/show.py" + provides: "CueModel with duration field, AnalysisBlock with calibration_offset" + contains: "duration: float = 4.0" + - path: "lightsync/frontend/timeline/timeline.js" + provides: "TimelineCanvas class with rendering loop" + exports: ["TimelineCanvas"] + min_lines: 150 + - path: "lightsync/frontend/index.html" + provides: "Timeline canvas element, toolbar, inspector placeholder" + contains: "timeline-canvas" + - path: "lightsync/frontend/style.css" + provides: "Timeline layout styles, toolbar, inspector strip" + contains: "#timeline-canvas" + key_links: + - from: "lightsync/frontend/timeline/timeline.js" + to: "HTML5 audio element" + via: "requestAnimationFrame reads audio.currentTime for cursor position" + pattern: "audio\\.currentTime" + - from: "lightsync/frontend/timeline/timeline.js" + to: "/api/audio/waveform" + via: "fetch waveform peaks on audio load" + pattern: "api/audio/waveform" + - from: "lightsync/frontend/timeline/timeline.js" + to: "/api/devices" + via: "fetch device list to build tracks" + pattern: "api/devices" +--- + + +Build the timeline canvas foundation — data model changes, HTML layout restructure, canvas rendering with per-device tracks, time axis, waveform background, and realtime playback cursor. + +Purpose: This is the visual backbone of the DAW UI. Everything else (block interactions, beat overlay, inspector) builds on top of this canvas and coordinate system. +Output: A visible, scrollable timeline with device tracks and a moving playback cursor synced to the HTML5 audio element. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/04-timeline-editor/04-CONTEXT.md +@.planning/phases/04-timeline-editor/04-RESEARCH.md +@.planning/phases/04-timeline-editor/04-UI-SPEC.md + +@lightsync/models/show.py +@lightsync/frontend/index.html +@lightsync/frontend/style.css +@lightsync/frontend/app.js + + + +class CueModel(BaseModel): + id: UUID = Field(default_factory=uuid4) + timestamp: float + mode: Literal["animation", "frame_sequence"] = "animation" + animation: str | None = None + params: dict[str, Any] = Field(default_factory=dict) + +class AnalysisBlock(BaseModel): + analysed_at: datetime | None = None + tempo_bpm: float | None = None + beat_times: list[float] = Field(default_factory=list) + onset_times: list[float] = Field(default_factory=list) + + +// WebSocket client: client.send({ type, ... }) +// Audio: document.getElementById('player') — HTML5 audio element +// API: apiPost(path, data), apiDelete(path) +// Devices: loadDevices() populates #device-list + + +--bg-primary: #0a0a0a; --bg-panel: #0f0f0f; --bg-panel-dark: #080808; +--border-dim: #1a4a4a; --accent: #00ffff; --text-primary: #cccccc; +--text-dim: #555555; --text-accent: var(--accent); --font-mono: JetBrains Mono chain + + + + + + + Task 1: Data model changes + HTML layout restructure + lightsync/models/show.py, lightsync/frontend/index.html + lightsync/models/show.py, lightsync/frontend/index.html, lightsync/frontend/style.css + +**1. lightsync/models/show.py — Add duration field to CueModel and calibration_offset to AnalysisBlock:** + +```python +class CueModel(BaseModel): + id: UUID = Field(default_factory=uuid4) + timestamp: float + duration: float = 4.0 # block length in seconds; defaults for old files + mode: Literal["animation", "frame_sequence"] = "animation" + animation: str | None = None + params: dict[str, Any] = Field(default_factory=dict) +``` + +```python +class AnalysisBlock(BaseModel): + analysed_at: datetime | None = None + tempo_bpm: float | None = None + beat_times: list[float] = Field(default_factory=list) + onset_times: list[float] = Field(default_factory=list) + calibration_offset: float = 0.03 # seconds; compensates librosa latency bias +``` + +Do NOT change schema_version (stays at 1). Duration defaults gracefully for old files via Pydantic. + +**2. lightsync/frontend/index.html — Replace the animation grid in main-area with timeline layout:** + +Replace the entire content of `
` (the `.section-header` ANIMATIONS div and the `#animation-grid` div) with this structure: + +```html +
+
+ +
+ +
+ +
+ + +
+``` + +Move the ANIMATIONS section into the sidebar. Add a second panel BELOW the existing DEVICES panel (inside `