Commit graph

69 commits

Author SHA1 Message Date
Your Name
9f06b51f76 feat: wiggly-paint drawings bound to marks
A drawing is a first-class :type :drawing entity (normalized strokes +
seed), bound to a mark via mark :drawings [gid] — same referrer-pointer
model as notes. It renders as a boiling SVG overlay on the video whenever
the playhead is in the bound mark's span (the same hook that lights an
annotation active), including when that annotation is the pushed context.

- Author in an overlay on the video monitor: pencil, eraser (deletes a
  whole stroke), undo; the 🖼 button on each mark row enters draw mode
  and seeks to the mark's frame.
- Boil is procedural: 3 seeded jitter variants of the base paths cycled
  at ~7fps; only the strokes persist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:03:44 -04:00
Your Name
b4924aa992 feat: script notes — named PDF-region annotations bound to marks
Replace the old per-annotation :script rects with a first-class
:script-note entity (name, color, id'd regions), persisted/synced
through the same generic scene channel as every other group.

- Author regions by selecting text on the pdf.js text layer; a region
  stores char offsets + per-line rects + per-range commentary.
- Bind notes to an annotation or a specific mark (drag-drop); a bound
  note shows »»» while the playhead is within its span.
- Script pane: note rail with per-note eye toggles, fit-to-width + zoom
  controls, plain-highlighter rendering (multiply blend, no borders).
- Click a highlight to open + focus its note; click a note chip on an
  annotation card to jump to its first passage.
- Schema v2 migration drops the legacy :script rects.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:34:29 -04:00
Your Name
89533a13cd fix: prevent scroll reset when hovering over hidden annotation
Keep button container always rendered but use visibility:hidden to hide
buttons instead of conditionally rendering them. This preserves DOM
structure and prevents scroll position reset on hover.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-30 16:51:35 -04:00
Your Name
06b28e2a9c refine: hidden annotations show edit + expand buttons on hover
Remove the unhide button. Instead show Edit (✎) and Expand (⤢) buttons
on hover so users can edit for any reason or expand to zoom in.

Make title text smaller (10px) and reduce card height to 24px.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-30 16:49:37 -04:00
Your Name
4ffd831551 feat: show hidden annotations as collapsed cards in annotation pane
Hidden annotations appear in the annotation pane as small, disabled-looking
cards with the dither pattern overlay (var(--desktop)) at 40% opacity.

On hover, an "Unhide" button appears to remove the hidden flag.
Full content, controls, and metadata are hidden.

Hidden annotations still work in marks/autocomplete and maintain their marks.
They appear as normal bars in the timeline.

Add 'Hide from timeline' checkbox in the annotation edit form.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-30 16:38:38 -04:00
Your Name
1ea675567c feat: add hidden annotation meta for decluttering timeline
Add 'hidden' meta flag to annotations to hide them from the timeline lane
while keeping them available for linking. Hidden annotations appear as
small 6px colored rectangles with faint color and dither pattern overlay.

Click on a hidden rectangle to unhide it. Hidden annotations' marks are
not resolved/used, and they don't trigger scroll-to-clip during playback.

Add 'Hide from timeline' checkbox in annotation create/edit form.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-30 16:27:19 -04:00
Your Name
e964347e51 fix: adjust timeline layout for time ruler height
Increase gutter-lanes height by 24px to align with the time ruler above the
annotation lanes. Update timeline-body-scroll padding to match.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-30 16:10:26 -04:00
Your Name
1cdb0de042 fix: scale time ruler tick marks to prevent overlap, use HH:MM:SS format
Dynamically calculate tick intervals to maintain ~80px minimum spacing between
marks regardless of zoom level. Prevents overlapping labels at high zoom.

Change time format from adaptive (m/s/·) to consistent HH:MM:SS or MM:SS format.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-30 16:07:05 -04:00
Your Name
d93b92f4a5 feat: add time ruler with tick marks above timeline
Display adaptive tick marks and time labels above the timeline to indicate
intervals. Tick spacing automatically adjusts based on zoom level:
- < 10 px/sec: 30s intervals
- < 20 px/sec: 10s intervals
- < 40 px/sec: 5s intervals
- < 80 px/sec: 1s intervals
- >= 80 px/sec: 0.5s intervals

Labels show minutes (m), seconds (s), or deciseconds (·) depending on interval.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-30 14:15:11 -04:00
Your Name
52c700b843 fix: scroll timeline to deep-linked playhead on first load
When a project opens with a ?f=<frame> query parameter, the playhead position
is now visible in the timeline view. Previously, the playhead was positioned
correctly in the UI but the timeline didn't scroll to show it. This triggered
scroll-to-seg-track on first render when playhead > 0 and segments are loaded.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-30 14:06:20 -04:00
Your Name
d91ea6c78a fix: dedupe scroll-to-clip by bar boundary instead of track
When an annotation with scroll-to enabled spans multiple bars (discontinuities),
only scroll when the playhead enters a new bar, not for every mark. Track
[ann-id bar-idx] pairs instead of just the current track to avoid excessive
scrolling as the playhead moves within a bar.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-30 14:05:58 -04:00
Your Name
6a02b0665b feat: clear project db on nav + clean editor loading/error states
Stale/fail flashes came from the editor rendering the previous project's
:project/:scene/:view (and a stale :load error) while the next loaded. Reset
those keys when opening a project or leaving to the list/create page, and render
the editor body only when :ready — otherwise a centered spinner ("Loading…") or
error message, with the top bar (and back button) still available.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:29:47 -04:00
Your Name
d9059ad132 fix: start video at deep-linked time via #t= media fragment (iOS)
The real iOS issue: it won't load the media until the first tap and ignores JS
currentTime until the element is seekable (populated very late), so every
load/seek hack started playback from 0. A #t=<seconds> media fragment on the
src is honoured by the media loader during the tap-triggered load itself, so it
starts at the right frame (and renders that frame instead of black). Bake the
deep-linked playhead into the src fragment, cached per clip so live playhead
changes don't reload the element; currentTime still drives in-session seeks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:21:33 -04:00
Your Name
a62af2584b Revert "fix: play from the playhead, not currentTime (iOS deep-link seek)"
This reverts commit 5c58af9036.
2026-06-30 11:17:42 -04:00
Your Name
484351746f Revert "fix: defer video seek until seekable, flush on loadedmetadata (iOS)"
This reverts commit 7585dd4e7f.
2026-06-30 11:17:42 -04:00
Your Name
7585dd4e7f fix: defer video seek until seekable, flush on loadedmetadata (iOS)
The previous fix still failed on iOS: at the first tap the element isn't
seekable (readyState 0), so the seek in engage-play! was dropped, and the
existing on-loadedmetadata seek is guarded by (when-not @play) — already true by
then — so it was skipped too, playing from 0. Now seek-video! stashes the target
when the element isn't seekable yet, and loadedmetadata/canplay flush it
(regardless of play state).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:02:54 -04:00
Your Name
5c58af9036 fix: play from the playhead, not currentTime (iOS deep-link seek)
On iOS the element doesn't load (so a deep-linked pre-seek doesn't stick) until
the user's first tap, so engage-play! read currentTime=0 and played from the
top. Seek to the playhead inside the play gesture and drive from there; for
normal pause/resume the playhead already equals currentTime, so it's a no-op.
Drops now-unused seg-at-src.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:54:53 -04:00
Your Name
28aa2622f6 ui: unhighlight between marks; translucent clip label
- ::active-annotations no longer falls back to the nearest annotation, so when
  the playhead is outside every mark nothing is highlighted.
- clip label background is translucent (color-mix paper 70%) like the lane
  bars, so the thumbnail reads through while text stays legible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:47:33 -04:00
Your Name
d877655b7a ui: bolder/colored active annotation, readable clip labels, multi-active
- active annotation is now loud: thick colour spine, a wash of its own colour,
  a hard offset shadow, bold title (was just a faint 2px shadow). Annotation
  colour drives it via a --ann-color custom property; added a colour swatch by
  each title so the commentary reads its colours.
- clip name is a solid bordered label chip pinned top-left instead of black
  text + white halo, which muddied over busy thumbnails.
- highlight ALL annotations whose marks contain the playhead, not just one
  (new ::active-annotations set sub), so overlapping marks light up together.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:45:34 -04:00
Your Name
7e32dc70dd feat: PWA restores last project + playhead/stack on reopen
A homescreen launch opens the manifest start_url ("/") with no route, so the
deep state was lost. Persist the in-app URL (already kept current with stack +
playhead) to localStorage, and on a standalone cold launch with a bare hash,
re-enter it before the router reads the URL. Browser tabs are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:45:34 -04:00
Your Name
b031a167a4 fix: clip-follow only when playhead is inside a scroll-to annotation's bar
::active-annotation falls back to the nearest annotation (it drives the
commentary highlight), so gating playback clip-follow on it fired for every
clip. Gate on strict bar containment of a :scroll-to annotation instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:37:49 -04:00
Your Name
baa46cb734 feat: jumps always scroll to clip; scroll-to flag = clip-follow on playback
Reframe the per-annotation :scroll-to flag. Jumps/links now ALWAYS scroll the
target clip into view (unconditional, no flag). The flag instead drives normal
playback: while the playhead plays through an annotation that opts in, each of
its clips is scrolled into view as the playhead crosses onto that clip's track
(deduped to fire only on track change; reset when playback stops). Form label
updated to "Follow clips during playback".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:33:21 -04:00
Your Name
a99485ff48 refactor: scroll-to-clip via scrollIntoView on the gutter label
Replace the manual lane-h/row-h/clientHeight math (fragile, clamped oddly for
top rows) with scrollIntoView on the target track's gutter label. The label
lives in the fixed track-name column, so the browser scrolls the vertical axis
only — clean, no geometry, and structurally can't fight the horizontal follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:28:00 -04:00
Your Name
602c710377 feat: per-annotation scroll-to flag gating vertical scroll on jump
Add :meta {:scroll-to bool} to annotations, toggled by a "Scroll to clips on
jump" checkbox in the annotation form. goto! now separates follow? (horizontal
recenter) from scroll-to? (vertical scroll to the clip's track); the vertical
scroll only fires for annotations that opt in, resolved from the canonical
scene group at each jump site (jump button/popover, script highlight, link
chips). Persists for free via editable-group + the backend's opaque group
merge; round-trips through json keywordize, so no migration needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:19:01 -04:00
Your Name
0c305e42ea feat: smooth horizontal + vertical scroll on jump/link
follow! gains an optional smooth flag. The jump/link path (goto! with follow?)
now glides both axes; playback-follow stays instant (smooth would fight the
per-frame updates) and scrubbing doesn't scroll at all, so clicking stays snappy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:10:55 -04:00
Your Name
967ca97e84 feat: scroll vertically to the clip's track when jumping via link/jump
goto! with follow? already recentred horizontally on the playhead. Now it also
scrolls the tracks container vertically so the clip under the landing frame is
brought into view — for link chips, annotation jump buttons, and script
highlights. No-op when the tracks already fit. Accounts for the pinned
annotation-lane header height in the scroll math.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:04:25 -04:00
Your Name
96da4a6bd8 feat: HyperCard-style top title bar (logo, project name, share, user menu)
Replace the mid-page .project-bar with a proper top nav bar pinned above the
video/timeline, styled like a classic Mac window title bar (centered title
flanked by racing stripes).

- left: TL wordmark + thumbnail-status indicator
- center: ‹ back-to-projects + project name, between the title stripes
- right: Share popover and a user dropdown with Log out
- Share offers two clearly named links, copied to clipboard:
  "Copy link to this moment" (current context + playhead) and
  "Copy link to the whole project" (root)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:58:54 -04:00
Your Name
28aafc047f fix: thumbnail parser overflow, autostop-killed jobs, unified playhead
Thumbnails failed immediately on any clip past ~2 min. The one-pass select
expression joined frames with a flat a+b+c+... chain, which overflows ffmpeg's
recursive expression parser past ~120 terms ("Error while parsing expression").
Join the same terms as a balanced tree (depth O(log N)) instead — identical
selection, still one decode pass, exact per-tile frames preserved.

- generate_thumbnails management command: runs synchronously and streams each
  phase to stdout, so jobs are watchable without tailing app logs
- surface ffmpeg's stderr instead of swallowing it in CalledProcessError
- fly: min_machines_running = 1. Background thumbnail jobs outlive the HTTP
  request that triggers them, so scale-to-zero let the proxy autostop the VM
  mid-job and leave projects stuck "running"
- timeline: replace the two split playhead bars (pinned lanes vs scrolling
  tracks) with one full-height overlay, positioned in JS to track horizontal
  scroll and sit above the translucent header

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:48:08 -04:00
Your Name
20ae2b4316 actually scroll to playhead 2026-06-30 02:52:38 -04:00
Your Name
96639554e4 translucent stuff etc 2026-06-30 02:42:31 -04:00
Your Name
5bc53c03f3 fix: generate thumbnails in one ffmpeg pass, not ~N/30
ffmpeg's select filter re-decodes the whole clip on every pass, so chunking by 30
turned a 670-frame job into 23 full decodes (minutes on shared CPU — long enough
to be killed mid-run and leave thumbnail_status stuck at 'running'). One pass now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:36:27 -04:00
Your Name
b05b440922 feat: upload a script PDF from the script pane
Backend: POST /api/projects/<id>/script/ (owner/collaborator) saves the script
FileField and returns the refreshed detail. Frontend: when a project has no
script, the script pane shows a PDF file picker that uploads on selection; the
returned detail carries the new URL so the pane renders it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:09:16 -04:00
Your Name
9c4b56dbc6 feat: per-annotation schema version + migration framework
Each stored annotation carries :v (schema-version, currently 1). tl.scene gains
a transformer registry and migrate fn that chains version upgrades on load;
save-group and open-draft stamp the current version. Empty transformer set at
v1 — bump schema-version and add a transformer when the model changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:06:14 -04:00
Your Name
7e47ec6a2d feat: upload progress on new-project form
Swap the create-project upload from :http-xhrio to a raw XHR effect with
upload.onprogress, surface it as a 1-bit progress bar; disable Create while
uploading.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:03:42 -04:00
Your Name
533d14ce49 ui: monochrome unicode glyphs + zoom arrow icons
- Zoom X/Y sliders labelled ↔ / ↕
- dark toggle ◐/◑, pause ❚❚, warnings △, drop 🔗 — no color emoji in the
  1-bit theme

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:01:27 -04:00
Your Name
ed5db0ce17 rename app UI to tl (Fly app/domain stay lumet)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:56:22 -04:00
Your Name
e6d150c375 feat: production deploy (Fly + R2) + frame controls and picker glow
Deploy:
- Multi-stage Dockerfile (shadow-cljs release → Python+ffmpeg runtime), fly.toml
  (one machine, scale-to-zero, SQLite on a volume), .dockerignore, DEPLOY.md
- Same-origin release build: api-port defaults "9001" for dev, "" in release
  (no CORS in prod)
- env-driven settings (SECRET_KEY/DEBUG/ALLOWED_HOSTS/DB_PATH/CSRF), WhiteNoise
  serving static + the SPA, R2 (S3-compatible) media storage when R2_* is set
- thumbnails reworked storage-agnostic: download clip, ffmpeg select-pass, upload
  tiles via default_storage (R2 in prod, disk in dev)
- pin channels/daphne; add whitenoise/django-storages/boto3

UI:
- frame step buttons flanking play (disabled at timeline bounds)
- Add-annotation button moved into the toolbar, made prominent
- picker mode pulses a 1-bit glow on clips / frame readout / (while linking)
  annotation bars; linking an annotation bar inserts a timeline link

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:56:09 -04:00
Your Name
e00a056209 feat: HyperCard theme + dark mode
- Retheme the whole app 1-bit HyperCard: white cards, hard 1px black borders,
  Chicago/Geneva/Monaco fonts (with web fallbacks; real fonts go in /fonts, see
  fonts/README.md), inverted-on-press buttons, a 50% desktop dither, black Mac
  title bar. Palette runs on CSS variables.
- Dark mode toggle (toolbar + home header): flips the palette vars via a class
  on <html>, persisted in localStorage and applied before first paint.
- Per-annotation fat colored left stripe in the annotation pane; annotation name
  bumped above the md h1; full-color (no longer veiled/desaturated) thumbnails.
- Jump popover now labels each part with the clip name + crop-adjusted mark-time
  frame (e.g. "CU Tashi (1) 45f"), matching the annotation editor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:23:34 -04:00
Your Name
fc25dfc016 feat: timeline links, network error handling, Tailscale access, mobile fixes
Backend / dev:
- Bind dev server to 0.0.0.0 and allow Tailscale MagicDNS + tailnet-IP origins
  in ALLOWED_HOSTS / CORS / CSRF so the app is reachable over Tailscale
- Unified, idempotent dev launcher (Django + media + shadow watch), hot reload,
  no manual compiles

Frontend:
- Migrate fetches to re-frame :http-xhrio with failure handling; surface
  network / load / save errors where the user expects them
- Extract markdown + link formatting into tl.md: parse/serialize round-trip
  (recursive chip serialization fix), block markdown (headings/lists/paragraphs)
- Timeline-opening links: navigate the stack to any reachable timeline;
  autocomplete lists all timelines (root included, orphans dropped) with a live
  preview that pushes the stack and reverts on commit/cancel
- Annotation pane shows the current context's description with edit-in-context
  (drops into the parent timeline for marks, pops back when done) + root content
- Fixes: deep-link playhead seek, replay-from-end, mobile input zoom, notch
  safe-area insets, autofocus the content field, freeze form to snapshot context

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 19:24:40 -04:00
Your Name
3d3b031332 feat: add route state and merge patches 2026-06-29 14:24:49 -04:00
Your Name
5b206635fb feat: reuse marks picker for annotation links 2026-06-29 12:14:08 -04:00
Your Name
d3c8dcbdf2 Script pane, realtime peer sync, and logout button
- Script pane: PDF tab beside annotations; "Add script annotation" arms
  highlight mode on the draft, ✓/✕ to confirm/cancel, auto-scrolls to the
  active annotation's highlights during playback. Adds Project.script.
- Realtime: Django Channels websocket per project broadcasts attributed
  scene deltas after each save; peers merge them in (keeping local drafts).
- Logout link in the editor project-bar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:52:46 -04:00
Your Name
595ac5ba72 json bug fix 2026-06-29 04:01:52 -04:00
Your Name
1b0dd21fb1 Show thumbnails in expanded timelines 2026-06-29 03:36:07 -04:00
Your Name
caa2aa6b9b thumbnails 2026-06-29 03:19:47 -04:00
Your Name
5861113aeb Drive playhead from the video element; reseek on stack moves
- the <video>'s own play/pause now engage/disengage the segment-aware tick
  (:on-play/:on-pause), so the native controls drive the playhead and follow the
  multicam cuts, not just the toolbar button (which now just toggles the video)
- entering/leaving a context (expand/collapse/pop-to) pauses and seeks the video
  to that context's remembered playhead ([:view :playheads ctx], 0 = its start),
  so the playhead tracks the timeline stack instead of ticking a stale context

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:42:40 -04:00
Your Name
8ee2cdf7b9 Lay root clips at timeline positions, not source positions
The root timeline placed each clip at its media-in (source) frame, which is
fractional (media-offset 50641.358…), so adjacent clips of a contiguous multicam
edit showed ~1-frame gaps; clicking a gap hit no segment and local->source
returned the last frame ("glitch to the end"). The OTIO timeline is gapless —
clips tile by their accumulated timeline :start. Carry :start through from-otio
and lay content-segments at [start, start+len] (src stays the source range), so
the assembled program is contiguous and maps cleanly to scattered source frames.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:34:11 -04:00
Your Name
0d73b204e6 Coalesce continuous annotation bars into one jump point
A continuous selection across N clips is N marks → N bars, and OTIO's fractional
media offsets leave ~1-frame gaps between adjacent clips, so the jump control
showed "jump (5)" for a continuous stretch. merge-bars snaps bar endpoints to
whole frames and joins touching ranges; only a real >=1-frame gap splits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:25:59 -04:00
Your Name
fcac22fe83 Give new annotations unique ids (gensym reset clobbered old ones)
open-draft used (gensym "ann-"), whose counter resets on every page load, so the
first annotation added after a refresh reused a prior gid (ann-2, …) and the
per-id merge overwrote the existing annotation. Use a uuid-based gid instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:20:24 -04:00
Your Name
ceccec8060 Fix annotations not persisting across reload (keyword round-trip)
Keyword *values* don't survive JSON: clj->js stringifies :annotation/:root/clip
refs and js->clj only re-keywordizes keys, so a reloaded annotation had string
:type/:parent/:ref and was filtered out of the scene. Re-keywordize those fields
on load (scene/restore-annotations) and give marks string ids (random-uuid was
serializing as a UUID object).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:10:15 -04:00