Commit graph

18 commits

Author SHA1 Message Date
Olive Vaughn
ae03b61dca sound stuff 2026-09-30 03:31:19 -04:00
Olive Vaughn
309c47e0a7 Trace a face over its footage, and choose where its origin goes
A face's :head carries :trace {:frames :origin}: the frames its photo holds
on, and whether the head reads every frame, jumps to the trace frames, or
holds frame 0. It replaces :anchors, so which measured frame a head reads is
one stored fact. An instance's :underlay shows the tracing stills over every
face at or below it, registered through each face's own head, at an opacity,
unkeyed. The clip resolver answers where a row path went on its last frame,
so the paint loop reads the photo's matrix instead of resolving again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:49:06 -04:00
Olive Vaughn
550cfe91e5 Select, move, turn and scale on the stage, at any depth
A click selects the thing in the open symbol, a double-click goes one
level in, ⌘-click goes to the shape itself and Esc comes back out —
Figma's rule — and a click inside the selection keeps it, so a shape
several symbols down can be dragged. The selection is the one a
timeline row makes, so the inspector shows it and its row opens and
scrolls into view.

A drag writes what the inspector writes: a key on the node's own frame
where the channel has keys, its one value where it has none. It is
previewed like a bar being slid and let go as one edit, so one undo
step. Measured transforms refuse. A shape's points are edited by
double-clicking it, and new shapes turn about their middle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:12:31 -04:00
Olive Vaughn
da7e293814 Solo an instance from its timeline row, shift-click for more than one
A nested mark is now named by the flat path its row has, [a b mark] rather
than [a [b mark]], so a soloed row is a prefix of what it shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:44:24 -04:00
Olive Vaughn
277c0c3b63 Keep the timeline's ruler and playhead dot in view while its rows scroll
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:22:01 -04:00
Olive Vaughn
c139e4d747 Rename the project from the top bar; export from a dialog
The title is a button that becomes a field, saved on Enter or blur. Export's
target and scale move out of the bar into a dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:12:08 -04:00
Olive Vaughn
4b8123d5e2 Delete a timeline row with everything in it
Delete or Backspace, or the × on the selected row, takes the node out of its
symbol along with every node hanging off it. One edit, so undo brings it back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:11:16 -04:00
Olive Vaughn
7246534505 Key a node's transform and visibility from the inspector
Each of vis, anchor, pos, rot, scale and skew is a row: ◆ keys the channel on
the node's own frame, or takes the key there off, and a typed value is written
on blur or Enter — a key on a keyed channel, the one value on one that is not.
Rotation reads in degrees. Dense and other channels keep their readout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:11:16 -04:00
Olive Vaughn
38850a29ba Slide rows along time and restack them, at any depth
A node's bar drags along the timeline: one write to its :at, for every node
alike, carried down through the instances above it. The stage and rows show
the slide live through ::render/clip, and it lands as one edit on release.

A row dropped on another's top or bottom edge goes in front of it or behind:
one write to :z, between its new neighbours (symbol/z-between). Onto the edge
of a row in another symbol, it moves there first. Neither needs anything on
screen — nest/down walks a row path by structure, without a frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:51:34 -04:00
Olive Vaughn
6a53adb5e0 Projects live at URLs, have owners, and are edited together live
A project is only ever at /p/<id>/<slug>; / is the index of the projects
you own or edit. Every project has an owner, who can name editors;
anyone with the link can view. Every edit saves itself, one request in
flight at a time, as a patch of the leaves that changed, and a websocket
(channels + daphne) carries presence and each committed write to
everyone else in the project. The first write to a leaf wins, and the
loser is told.

Undo is per person: a step undoes only if the leaves it touched still
hold what it left, so it never takes a collaborator's work with it.
Named snapshots replace saving, and restore as an ordinary write.

An empty symbol now survives the leaf round trip with `:nodes {}`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:04:03 -04:00
Olive Vaughn
490460bf45 Drag timeline rows into symbols, into groups, and back out
A node's row can be dragged onto an instance's row, to go inside the symbol
it places; onto any other node's row, to be grouped with it into a new
symbol; or onto empty label space, to come back to the top of the open
symbol. All three keep the picture and the timing as they are, and a refused
move says why in the status line. Pool drop targets now only take things out
of the pool, and row drags set a move effect so the browser delivers the drop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:50:34 -04:00
Olive Vaughn
41b4bdf110 Make a symbol from a dropped video
Dropping footage on the stage or the timeline asks which frames and what name:
a dialog plays the video with start and end handles that seek it. Detection
runs on that range only (decoding stops at the end, frames before the start
are skipped) and the range is part of the analysis address, so a partial
analysis is never served as a whole one. The frozen take comes in as one
named symbol, carrying its sound as an audio node, placed where it was
dropped; tracking and tuning come with it when the document has no analysis
of its own.

clip/adopt copies symbols between documents, renaming ids that collide, and
clip/audio-tracks carries sounds out of nested instances so a placed symbol
is heard where it is placed.

Fixes the stage going blank after a conversion: ::store recomputed only when
the clip id changed, so blocks merged into the loaded entry were invisible to
the resolver. And the paint loop now schedules its next frame before
painting and reports a frame it cannot draw instead of stopping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:21:08 -04:00
Olive Vaughn
270c5a4369 Version static URLs by mtime; flatter tabs
The stylesheet and bundle were served with Last-Modified only, so a browser
could pair new JavaScript with a stale app.css and render the tab strip and
thumbnails unstyled. Their URLs now carry the file's mtime. Tabs are flat on
the chrome with the open one underlined; the close button shows on hover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:08:38 -04:00
Olive Vaughn
f590ff19cf Drag symbols onto the stage or the timeline, with previews
A symbol dragged out of the pool shows where it would land: on the stage, a
dashed outline of its first frame centred on the pointer, and on the timeline
a preview row of its own length. A stage drop places it at the playhead under
the pointer; a timeline drop places it at the frame under the pointer, in its
own coordinates. A drop that would make a cycle is refused while hovering.
Pool thumbnails are capped at 40x30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:06:38 -04:00
Olive Vaughn
d5f044c6da Media pool: this project and all assets
This project lists every symbol in the document and the video it uses or was
given this session, each with a frame from its proxy as a thumbnail. All
assets lists every upload on the server and every other saved project's
symbols, from a new /api/symbols that reads symbol leaf paths. Every row is a
drag source (symbol:, import:, footage:). Uploading no longer runs straight
into detection; it lands in the project's media.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:59:17 -04:00
Olive Vaughn
c81f91c442 Tabs: open any symbol, and the clock follows it
Double-clicking a symbol in the pool, or an instance's row in the timeline,
opens it as a tab above the stage; the stage, the rows, the transport and new
shapes follow the open tab. Each tab gets a clock as long as it is: its own
mixed tracks, the document's audio for the symbol it opens on, or silence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:54:58 -04:00
Olive Vaughn
179770d7d4 Split the shell into topbar, pool, stage, timeline and params panes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:25:46 -04:00
Olive Vaughn
9cd5243983 Serve the document from a Django backend, split into three tiers
Step 9. The tier split was the work; Django was the easy half.

Tier 1 — the authored scene — is the document, and it is addressed as
independently versioned leaves rather than saved whole, so one vertex drag
cannot clobber a collaborator's keying. `domain/leaf` is the document as
path -> value; `domain/wire` puts it on the wire as transit, because JSON
has neither integer map keys nor keywords and a save would quietly turn
`{0 v}` into `{"0" v}`.

Tier 2 — the dense channel blocks — is content-addressed by a hash over
every input, with the detector version inside every key through the
analysis the block descriptor names. `flow/address`'s `block-knobs` is the
invalidation table, and `address-test` does not trust it: it re-freezes the
take once per knob and asserts the biconditional, that a block's bytes
changed if and only if its key changed. That found `brow-pos` not depending
on `contour-avg` — the brow ring is smoothed, the raise is not.

Tier 3 — frames and audio — is served by the hash of its bytes out of the
same store. A manifest now names frames and carries a URL for each, so the
frame layout stopped being a shared secret between a shell script and a
ClojureScript namespace, and the `?v=` cache-buster went with it: a blob's
name is the hash of its contents, so a stale copy is not a thing that can
happen. The synthetic take's `audio.wav` moved to `static/arthur/` — an
asset the project owns, not an extraction that churns.

The server verifies rather than trusting a name it was handed: it
recomputes every key from the descriptor stored beside it, refuses an
analysis that declares no detector version, and refuses a document naming
blocks it does not hold. It hashes the descriptor TEXT, because JS prints
an integral double as `1` and Python as `1.0`, and a scheme where both ends
re-render the numbers disagrees on the first parameter that happens to be
whole.

Two loose ends from step 8 closed on the way. `pack` no longer takes a
`(track, frame)` predicate whose call sites each re-derived a feature from
an index — every track names the feature it follows, which deleted five
hand-maintained mappings. And `:dev-http` is gone: Django serves the page,
shadow-cljs only builds into the staticfiles tree.

227 CLJS tests, 31 Django tests, green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 01:11:41 -04:00