Files
gui-video-clipper/chat-summaries/2026-09-21_14-39-perf-captions-resize-ux-summary.md
cottongin 8ad2f1c800 chore: stage all pending work — caption styling, media server, processing modal, docs, summaries
Includes:
- Extended caption styling (font, shadow, dimmed color, bg toggle)
- Media server, subtitle downloader, VTT parser, processing modal
- Waveform tiers, thumbnail/timeline improvements, transport controls
- Hybrid download model, dependency management, clip export enhancements
- 21 chat summaries, 2 implementation plans, 2 design specs

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 10:48:16 -04:00

4.6 KiB

Performance, Captions, Clip UX, and Resizable Panels

Date: 2026-09-21 14:39
Task: Fix timeline performance, caption positioning, clip creation workflow, resizable panels, and deselect UX

Changes Made

1. Performance Optimization (3 files)

src/lib/components/VideoPlayer.svelte:

  • Throttled handleTimeUpdate to ~15fps (66ms interval) using performance.now() check. Previously every ontimeupdate event triggered a reactive cascade through session.currentTime.

src/lib/components/Timeline.svelte:

  • In-flight guard: Added detailFetchInFlight flag to prevent overlapping waveform range extraction subprocess calls. Only one ffmpeg call runs at a time.
  • Increased debounce: Waveform detail fetch debounce increased from 300ms to 500ms.
  • Playhead-only redraws: Split the monolithic redraw $effect into two:
    • Structural changes (clips, zoom, waveform data, thumbnails) trigger a full drawMainCanvas() which saves an ImageData snapshot.
    • session.currentTime changes trigger a lightweight drawPlayheadOnly() that restores the snapshot and draws only the playhead — avoiding the expensive thumbnail/waveform/clip rendering on every time update.
  • Thumbnail load callback: Registered a callback via setThumbnailLoadCallback so thumbnail image loads trigger a targeted redraw instead of relying on the next reactive cycle.

src/lib/timeline/thumbnailRenderer.ts:

  • Added pendingLoads Set to prevent creating duplicate Image objects for the same path across rapid redraws.
  • Added setThumbnailLoadCallback API so the Timeline can request a redraw when thumbnails finish loading asynchronously.
  • Image onerror handler cleans up the pending state.

2. Caption Positioning (1 file)

src/lib/components/VideoPlayer.svelte:

  • Added :global(video::cue) CSS to center captions at the bottom of the video with a semi-transparent black background, white text, and 16px font size. Used :global() to bypass Svelte scoping since ::cue is a browser-level pseudo-element.
  • Added position: relative to the <video> element to scope cue rendering.

3. Pending In-Point Preservation (1 file)

src/lib/stores/clips.svelte.ts:

  • Modified selectClip() to only clear pendingInPoint when id !== null (selecting a specific clip). When id === null (deselecting via empty timeline click), the pending in-point is preserved. This fixes the workflow: press I → click timeline to seek → press O to complete the clip.

4. Resizable Panels (3 files)

src/App.svelte:

  • Wrapped <Timeline /> and <ClipList /> in a new .timeline-clip-area container with flex column layout.
  • Added a 5px .resize-handle divider between them with cursor: ns-resize and accent color on hover/active.
  • Implemented handleResizeStart/Move/End with mousedown/mousemove/mouseup on <svelte:window> to drag-resize. Timeline height is clamped between 80px and (total - 60px).
  • timelineHeight stored in local $state (defaults to 180px).

src/lib/components/Timeline.svelte:

  • Changed .timeline-container from height: 160px to flex: 1; min-height: 80px so it fills the space given by the parent.
  • .timeline-wrapper set to height: 100%.

src/lib/components/ClipList.svelte:

  • Changed from max-height: 200px to height: 100%; min-height: 40px with box-sizing: border-box.

5. Deselect UX (1 file)

src/lib/components/ClipList.svelte:

  • Added handleContainerClick on the .clip-list div that calls selectClip(null) when clicking empty space (not a child clip row).
  • Replaced the single "Export All" button with an .action-buttons row containing a conditional "Deselect" button (shown only when a clip is selected) alongside "Export All".
  • Changed role="button" to role="option" on clip rows and role="listbox" on the container for better semantics.

Build Status

  • Frontend: builds cleanly (0 errors)
  • Rust: compiles with only pre-existing unused variant warnings

Lessons Learned

  • Svelte's scoped CSS doesn't apply to ::cue pseudo-elements since they're browser-level shadow constructs — :global() wrapper is needed.
  • Canvas getImageData/putImageData is an effective technique for "snapshot and overlay" rendering patterns, avoiding expensive full redraws when only one layer (like a playhead) changes.
  • ontimeupdate fires at varying rates across browsers/engines; throttling to a target frame rate prevents reactive cascades from dominating the UI thread.
  • The pendingInPoint clearing bug was a subtle consequence of selectClip(null) being used for two different intents (deselecting a clip vs. resetting all clip state).