Commit graph

168 commits

Author SHA1 Message Date
Olive Vaughn
208dddee07 Put the top bar back to words
The icon run in the top bar read worse than what it replaced. Restore the
earlier top bar — pane words, new, open ▾, undo ▾ redo, the edit menu,
snapshots ▾, title, status, export… — and its CSS. The stage bar's overlay
switches, the palette chooser and `menu/popover` stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 03:16:09 -04:00
Olive Vaughn
f238ff4f62 Put the top bar on the surface as icons, and give palettes a real chooser
Top bar: every document command is an icon button in a run — panes, new/open,
undo/history/redo, cut/copy/paste/duplicate, snapshots — with the title and
status in the middle and export, share and account on the right. Pane toggles
are pictures of the window with that pane filled in.

Stage bar: tracing, onion and passepartout are all the same toggle-plus-settings
split button, followed by one zoom group, with fit inside it.

Popovers: one `menu/popover`, measured against its button and dismissed by a
scrim, replaces the <details> popouts that never closed on an outside click.

Palette: the strip ends in a palette button whose popover lists every palette
with its swatch strip, renames the open one in place, and marks the ones on
the stage. An info badge says when the palette being edited is not one the
stage draws in at the playhead; both ends of a palette-lane blend count.
`clip/palette-at` is the resolver's root palette lookup, pulled out so the
strip asks the same question the renderer answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 03:08:21 -04:00
Olive Vaughn
eca1a96b82 correction things 2026-10-04 02:47:29 -04:00
Olive Vaughn
064a3d7c19 Cover large timeline keyframe selections in the browser suite 2026-10-04 02:14:02 -04:00
Olive Vaughn
6de71c4c66 Align stage multi-selection modifiers with the timeline 2026-10-04 02:12:06 -04:00
Olive Vaughn
9bc406f413 Show project settings only when the root is selected 2026-10-04 02:12:06 -04:00
Olive Vaughn
1c520e6a68 Add instance playback controls to the inspector 2026-10-04 02:12:06 -04:00
Olive Vaughn
52f25e0bb1 Fix inspector automation clock for looped symbol instances 2026-10-04 02:11:17 -04:00
Olive Vaughn
4d441ae606 Keep overview keyframes passive and edit expanded automation lanes 2026-10-04 02:05:22 -04:00
Olive Vaughn
606055382b Add per-symbol onion skinning controls and cel ghosts 2026-10-04 02:04:00 -04:00
Olive Vaughn
f2fc261221 Fix stage zoom anchoring and continuous passepartout shading 2026-10-04 00:59:08 -04:00
Olive Vaughn
e5b61bfcdd Fix playback stopping when fallback audio ends 2026-10-04 00:58:51 -04:00
Olive Vaughn
ee603a351b Simplify stage passepartout control 2026-10-04 00:49:28 -04:00
Olive Vaughn
981bef98f9 Start playback at the playhead 2026-10-04 00:48:43 -04:00
Olive Vaughn
6df315b73d Pan with trackpad scrolling and zoom with pinch gestures 2026-10-04 00:44:47 -04:00
Olive Vaughn
484b4f1698 Move palette controls to sidebar and expand stage navigation 2026-10-04 00:40:20 -04:00
Olive Vaughn
1ae49a4015 Group tracing layers in the pool and fit new drops to the stage 2026-10-04 00:16:03 -04:00
Olive Vaughn
f4dd047642 Merge master drawing tools with tracing layers 2026-10-04 00:05:16 -04:00
Olive Vaughn
17e1b4f403 Represent tracing media as symbols and add pool thumbnails 2026-10-04 00:02:12 -04:00
Olive Vaughn
353cb6e050 Add remap ink, eraser cuts, and improved brush geometry 2026-10-04 00:00:09 -04:00
Olive Vaughn
1f0b4d9918 Draw with tools: a pen, a brush that makes polygons, and knockouts
One tool at a time, picked from a strip down the left of the stage or by its
letter, as in Photoshop, Illustrator and Flash: V selects and transforms, P is
the pen, B the brush, E the eraser. The options bar above the stage holds the
tool's settings, and the palette is a grid under the tools with the colour a
new shape gets above it, as Deluxe Paint's was.

The pen's draft is filled into the picture as it is drawn, so the edge on
screen is the pixels the shape will be. Click the first point, Enter, Esc or
leaving the pen closes it; the pen stays the tool. Editing a shape's points is
the pen ON that shape — Figma's vector edit mode — so the separate points mode
is gone: click an edge to add a point, ⌥-click one to delete it, on every key
at once so tweens keep meaning something.

The brush paints a mask, and what it will be is shown while painting: the
stroke is traced round its pixel edges, holes and all, and simplified to a
point every so many pixels of outline by the same function the saved shapes
come from. A hole is bridged into the one ring along a whole-pixel row, which
the fill never samples. Blender's Adjust Last Operation re-traces the last
stroke from what it was made of.

A shape coloured CLEAR is a knockout: its symbol is drawn into a layer of its
own and the knockout clears it, every colour or one. The eraser makes one in
the symbol it starts on, of the colour it starts on, or of every colour with
⌥. A click goes through a knockout to what shows. Previews are drawn in the
stacking context of what they will land in.

The polygon's points no longer stay behind when the shape is moved: they were
read off the saved document while the box handles read the one mid-drag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:43:28 -04:00
Your Name
551d572347 idk man something hopefully helpful 2026-10-03 04:48:03 -04:00
Your Name
5b5b9ae4c3 Play the audio mixdown instead of encoding a WAV of it
Opening the 8625 study froze the main thread for 4.7 seconds and settled at
615MB of heap. A profile put three quarters of a project open inside
mix/wav-bytes, which playback had no business calling at all.

Two separate causes. The peak scan built a lazy sequence of one boxed double
per SAMPLE -- ten million of them for a seven-minute mix -- to compute a
single maximum over data already sitting in Float32Arrays; the hand-written
loop is 73x faster and agrees to the bit. The rest was structural: the WAV
existed only because an <audio> element can hold a URL and nothing else, and
the element existed only to be the clock. So a mixdown that was already
rendered got encoded to 73MB of 16-bit PCM, on the main thread, on open, on
every tab switch and on every edit to a track -- and a symbol with no sound
got silence synthesized and encoded full length so the element had a duration
to report.

arthur.clock keeps its interface and all of its arithmetic; the position now
comes from a backend behind a protocol. clock.graph plays the AudioBuffer
through an AudioBufferSourceNode and derives the frame from the context's own
clock, which is the audio device's position in double precision rather than
whatever the media pipeline last published. clock.element is the old path,
kept switchable while the new one earns trust -- BACKEND, or use-backend! --
which is also why every one of the original clock tests passes unchanged: the
derivation they assert is shared, and the backends can only disagree about the
position under it. 6.5s to 1.8s, 4.7s of blocking to 370ms, 615MB to 68MB.

THE POSITION IS COMPENSATED FOR OUTPUT LATENCY, and piecewise because of it.
currentTime is the quantum being rendered, which the speaker is tens of
milliseconds behind; report the renderer and the picture leads the sound,
which in a lip-sync tool is the only artefact that matters. Audio already
rendered cannot be re-rated, though, so reading it back at a new rate jumped
the playhead backwards by three latencies on every press of the rate button.
Each play, pause, seek and rate change now records a segment and a position is
read against whichever segment was in force when that audio was rendered.

One duplicate fell out of this. Opening a project asked for its clock twice --
once from ::opened and once from a ::refresh-clock the shell raised because it
compared symbol ids, and two different documents both open on :main. The
sounds subscription carries the clip id now, so "an edit under the same
symbol" means what it says.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 04:47:35 -04:00
Your Name
90b1fbe2f8 Create in a lane at the frame double-clicked, palette lanes included
The gesture was handed the pointer's frame and threw it away, creating at
the playhead instead; and the palette row carried :lane? only once its
track existed, so the first double-click on a palette lane -- the one that
has to make the track -- dispatched nothing at all.

Both are one rule now. ::new-symbol-at uses the frame it is given and
resolves every row through drop-destination, with the destination deciding
what is created: a clip of a palette track is a palette symbol, a clip of
any other lane is blank. A row with no path of its own resolves to the
symbol it names, which is what lets a palette track -- hanging off its
owner by :palette-track rather than placed in it -- be reached without a
special case; it also stops a palette cel's slide resolving against the
open symbol and looking like a transfer out of the track. The one thing
left that knows about palettes is materializing the lane a palette row
names before anything asks where the row leads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 03:52:39 -04:00
Your Name
09b74de162 Duplicate a palette from the palette bar
Making a variant meant 16 colour pickers from black: the only button was
"+", which seeds an untitled palette from the built-in defaults. The copy
is selected on creation, so the next colour edit lands on it rather than
on the palette it came from.

`pal/palettes` is the source, so the implicit default — a project that
has never had a palette asset of its own — duplicates like any other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 03:51:17 -04:00
Your Name
2460dce0a5 Refuse a symbol placed inside itself, and label instances by what they place
A paste put an instance of "bg" inside "bg" itself. It saved, loaded, and then
threw "symbol cycle in audio" out of nest/audio-tracks, because a container that
contains itself has no finite expansion.

place-symbol and ui/drag already refused that, each by asking
clip/contains-symbol?. Paste asks clip/problems instead, and problems did not
encode the invariant at all -- it checked missing symbols, pose tracks and audio
links, but never the placement graph. So the rule goes where every command is
already checked: paste, cut, duplicate, correction and the span ops all gate on
problems, so one rule covers them all.

Why it looked like a reasonable thing to do is the other half. place-symbol
copied the symbol's name onto the instance it made, and that copy went stale on
the next rename: the symbol read "bg" in its tab while an instance of it still
read "symbol-18", which is its id from before it was named. One object under two
names, with nothing on screen to connect them.

So instances are no longer given a name at creation, and clip/node-label reads
the symbol's name through on every render. :name on an instance now means only
what a person typed, which is what tells two instances of one symbol apart --
"8625 left" and "8625 right" of one "face" -- so an authored name still wins and
read-through is the fallback. The label logic was duplicated across four call
sites with three different fallback orders; location.cljs already read through
and the others did not. They now share one function.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 03:43:07 -04:00
Your Name
0d49db793b fix some stuff 2026-10-03 02:23:54 -04:00
Your Name
a90e0cfb61 fixed creating in 2026-10-03 02:15:27 -04:00
Your Name
2697401aad Scope inspector controls and key shape colors 2026-10-03 01:48:26 -04:00
Your Name
a834ccb1e2 Unify lane symbol creation and palette automation 2026-10-03 01:35:33 -04:00
Your Name
15deaea19e Support multiple regeneratable footage analyses 2026-10-03 01:24:45 -04:00
Your Name
5bcf22e458 perfect target area thing 2026-10-03 00:39:07 -04:00
Your Name
664252e0fc idk 2026-10-02 17:44:15 -04:00
Your Name
edca82cd5d Add multi-selection clipboard commands 2026-10-02 16:13:03 -04:00
Your Name
987e289f89 Enable browser source maps 2026-10-02 16:11:32 -04:00
Your Name
7c49c08bc7 Fix palette swap backgrounds and timeline drop jitter 2026-10-02 10:24:23 -04:00
Your Name
26fab9392e Model palette tracks as typed symbol lanes 2026-10-02 10:06:05 -04:00
Your Name
dff23d7994 Add multi-object stage selection and transforms 2026-10-02 09:09:18 -04:00
Your Name
b41180db08 Add project palette assets and overrides 2026-10-02 09:08:53 -04:00
Your Name
8f09b7b47f Unify lane overlap and nested timing 2026-10-02 09:06:28 -04:00
Your Name
1b2b4ad3d2 Unify root timing and persistent lane targets 2026-10-02 01:11:55 -04:00
Your Name
f5a39aee39 Keep footage audio with generated face symbols 2026-10-01 23:38:16 -04:00
Your Name
0cb9d0ebf6 Unify timeline placement and gesture geometry 2026-10-01 23:35:22 -04:00
Your Name
4e5e02c856 The window's shape is editor state, not a constant
The grid was five panes at three fixed widths and one fixed height, written as
lengths in the stylesheet. That is a claim about every screen the tool is ever
opened on, and it was wrong on two of them: a phone, where 210 + 250 pixels of
side pane leave nothing for a 320x200 stage, and a large display, where the
inspector is the width somebody once typed rather than the width their work
wants.

So the tracks are custom properties and `ui/layout` owns the numbers. A drag of
an edge is one `assoc-in` and one property on `.app`; no pane's contents
re-render, which is the same reason the picture is painted by `ui/player`'s loop
rather than by anything reactive. Sizes stay in PIXELS and not fractions —
three of these panes hold fixed-width rows, so what you drag an edge to is the
width you meant at any window size.

State, events and widgets are one namespace, which is not this repo's shape
anywhere else. They are the same six integers: `:pool` is a width, a grid track,
a drag's clamp and a button's label at once, and splitting six integers across
`db`, `events/` and `ui/` is more wiring than state.

Three things follow from making it state at all:

SHUTTING A PANE. Pool and inspector unmount; the timeline collapses to its
transport strip instead, because play, pause and the frame readout live in that
strip and a window with nowhere to press play is broken rather than small. The
toggles are in the top bar because a shut pane has no head left to carry its
own handle.

A NARROW WINDOW. Below 760px the grid is one column and the side panes are
drawers over the stage rather than columns beside it. The breakpoint is read
once, at load, and only decides what the layout OPENS at: throwing away the
sizes somebody dragged because they turned a tablet sideways is worse than a
layout that is briefly the wrong shape.

ZOOM. The stage's was already a constant 2 in `ui/stage`; it is now an integer
in [1 8], still CSS over a canvas that is the raster's own size, so the browser
suite still reads 320x200 of real pixels off `canvas.stage`. Whole pixels
only — a fractional scale under `image-rendering: pixelated` draws some rows of
the raster thicker than others, which misrepresents the one thing the preview
exists to judge. The timeline's is the stylesheet's entirely: every mark in the
tracks is positioned as a percentage of their width, so one multiplier on that
width spreads the grid, the spans, the keys, the ruler and the playhead apart
together. Both readouts are the button back to normal, accented while there is
something to return from, because the quick way back belongs in the one place
you already look to find out where you are.

One trap, paid for and then found: the top strip has to scroll sideways on a
narrow screen, and `overflow-x: auto` makes an element a clipping container on
BOTH axes. Every top-bar menu — open, snapshots, share, sign in — was then cut
off by a 30px-tall bar and drawn at whatever x the strip happened to be
scrolled to, which reads as the button doing nothing at all. They are fixed to
the viewport there, as `.menu-drop` already was one pane down for the same
reason. The same shape of bug ate the polygon tool and both zoom readouts: a
flex row that does not fit shrinks every item and pushes the last ones off the
end, so nothing in a strip shrinks now and the strip scrolls as a strip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 21:50:05 -04:00
Your Name
93f5112bb3 Unify clip placement across symbol modes 2026-10-01 20:09:35 -04:00
Your Name
e459307a4a Make lanes explicit symbol views 2026-10-01 19:40:53 -04:00
Your Name
fb38990090 Loop is two things, and the one that matters is unreachable
Loop playback — the transport repeating the open symbol — is editor state and
the document has never heard of it. A looping INSTANCE is a node repeating the
symbol it places, which is the four-frame tire turning for the hundred and
twenty frames it is on screen, and it lives in the document as
`:playback {:end :loop}`. Same word, two scopes; named apart here before
anything is built on either.

The status of the second one is the surprise: it already works and cannot be
asked for. `node/placed-frame` does the modulo, `node/problems` already admits
`:end :loop`, `audio-tracks` already expands the periods — and no control sets
it anywhere in the UI. So the tire is three pieces of work, not one: somewhere
to edit an instance's playback, a block that draws its repeats rather than
only its first pass as the timeline's own docstring admits it does, and the
audio period guard.

This also pins down when the held cel can be torn out. Turning every drawing
into a one-frame looping instance is only safe once a looping instance can be
seen and edited; otherwise every drawing in the document quietly acquires a
property with no control on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:32:02 -04:00
Your Name
d029908f0b A held cel is a one-frame symbol that loops
Holding a drawing and looping a one-frame symbol are the same picture, and the
second is a case the model already carries, so the first is a special case
kept for nothing. Speed 0 goes; a cel becomes a one-frame symbol with
`:end :loop` and a span. Looping and span are already properties of the
instance and `:frames` already belongs to the symbol, so nothing moves — a
mode is deleted. Two spellings of looping collapse to one, `:time :loop?`
giving way to `:playback :end :loop`, and `extend-hold` collapses into
`resize-out`, since it exists only to refuse anything that is not frozen
before editing a span.

What it buys is one rule where there were three refusals. `nest/inside` has no
invertible clock for a hold, for `:end :hold`, or for a loop, so
shift-to-reparent refuses all three — which is most of what anybody would drag
onto. Resolving the move with the destination's map at the CURRENT frame
covers every one: a loop is affine within the period the frame falls in, a
one-frame loop is that rule with a period of one — which lands exactly where
the nesting notes argued it should from first principles — and `:end :hold` is
affine in the played part and frozen in the tail.

The trap is written down beside it, because it would be found the hard way:
`audio-tracks` expands a loop into one walk per period, and is saved today
only by the `(pos? speed)` guard that a held cel fails. Make every drawing a
loop and a drawing held for 120 frames becomes 120 walks emitting any nested
sound 120 times. An audibility precheck has to land in the same change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:31:06 -04:00
Your Name
6e7827e03f Non-overlap is an invariant, not a report
The plan asked what to draw when a symbol in lane mode holds overlapping
children, and offered to report it as a decision waiting for a person. Wrong
question: they never overlap. Placement claims time — anything placed, moved
or grown over occupied time trims, removes or splits what it lands on — so the
operation that could have made an overlap did not, and `span/finish`, which is
already the single commit path and already refuses rather than half-applying,
is where that is enforced. An overlap is then a bug in a command and not a
state to design around.

The check stays, named `symbol/overlaps` and used three ways: the commit path
refuses one, a property test asserts no command can produce one, and a
document that somehow holds one still LOADS and is drawn visibly wrong with
the status saying so. Not `problems`, which stops a document loading, and not
`conflicts`, which means somebody has a decision to make — a display hint must
never be able to keep a document from opening.

The one place a person can ask for the impossible is toggling lane mode on
over children that already overlap. That refuses and offers to trim them into
a sequence, through the `:required-frames` retry the model already uses.

Also written down, because it is the pair the modifier exists to separate:
a plain body drag is temporal and replaces, trimming extents as needed; shift
is structural and goes through `nest/move-node` into the symbol under the
pointer, which has to keep working for symbols held in a lane.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:22:33 -04:00
Your Name
10b96761fa Plan: a lane is a view, not a thing in the document
The lane model put a second container in the node map — a group with
`:layout :sequence`, with its own membership, its own validation and its own
fourteen commands — and every part of the editor then had to ask which kind of
container it was looking at. The decision recorded here is to delete all of
it: a lane becomes a way of DRAWING a symbol whose children are sequential and
non-overlapping, the display goes back to a row per symbol, and the word
survives only in the timeline and in the drag handling that re-spans a
symbol's children while it is drawn that way.

The commands are not the part being thrown away. `extend-hold`, `resize-out`,
`roll`, `blank` and the rest are what endpoint dragging IS, and their
arithmetic is right; what changes is their subject, from "the children of lane
L in symbol S" to "the children of symbol S". They belong in `span.cljs`,
which already owns re-spanning and `finish`.

The plan takes a position on the one question that decides whether this is a
simplification or a circle: lane mode is a saved hint on the symbol rather
than unsaved view state, because the drag rules follow the mode, and a toggle
the document does not record would make one gesture do two different things
to it. Nothing outside the timeline may read the hint.

It also lists what must not be lost on the way, all of which broke at least
once today: a held clip's contents reachable with no keys and no draggable
edges, double-click to open surviving the selection it leaves behind,
selection waiting for pointer-up, no drop silently deleting what it lands on,
and a sound drawn once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:16:30 -04:00