Skip to content

Draggable nool: captured-geometry drag rewrites, plus navigation, sound, undo, and persistence - #6

Merged
disconcision merged 72 commits into
mainfrom
draggable
Jul 31, 2026
Merged

Draggable nool: captured-geometry drag rewrites, plus navigation, sound, undo, and persistence#6
disconcision merged 72 commits into
mainfrom
draggable

Conversation

@disconcision

Copy link
Copy Markdown
Owner

The complete draggable branch (64 commits): direct-manipulation rewriting driven by captured geometry, and the session-work that grew around it.

Drag system (captured geometry)

  • Box-snapshot motion layer replaces view transitions: retargetable whole-scene morphs for clicks and drags, rigid-subtree grouping, context shells, provenance-driven enter/exit (clone/grow, merge/absorb), head-origin anchoring.
  • Knob-on-rails drag mechanic (continuity by construction; commit as topology), hidden-probe candidate measurement, prefer-the-grabbed-copy id splicing.
  • Pattern-level moverhood: drag grips are decided by the rule's own patterns (fixed points park; the shared factor grips factoring in both directions).
  • Click-path parity: button-applied transforms now animate with the same provenance as drags.

Interaction & UI

  • Screen-space columnar arrow navigation (banded, rootward-to-parent, cone fallback) replacing tree-hardcoded moves; drag mode disables the selection mechanic wholesale.
  • Root-selection metaphor restored (outline morphs to the window; corner-rounding press feedback).
  • Noolbox: native snap scrolling, whole-row loadout toggles, glowing equals-sign state, drag-rule glow tied to the live rail parameter with flashbang commit.
  • Undo/redo (Cmd+Z / Cmd+Shift+Z) over world states; session persistence across refresh (localStorage) with a reset button; icon corner reorganization.

Sound

  • Drags speak in plucks: quantized chords at the rail's quarter points whose pitch content is the rule's operator multiset (walked source → result; octave stacking for multiplicity), chosen after a six-way bake-off (granular scrub, detents, tension, and more). Rewrite samples remain button-mode sounds. Harmony experiment behind Shift+A.

Design docs

  • design/captured-geometry.md (architecture + append-only findings log), design/sound.md (channel model + derivation ladder), design/drag-target-decomposition.md (tree-edit vs layout-reaction), TODO.md index, rail-variants figure.

🤖 Generated with Claude Code

disconcision and others added 30 commits July 28, 2026 18:11
- Animate.tsx: dynamic view-transition-name assignment/cleanup instead
  of pregenerated CSS rules for ids 0-99
- App.tsx: defer setHover actions during active view transitions,
  replay after finish; reassign transition names post-DOM-update
- index.css: drop dead static view-transition rules, tweak light-theme
  transform-view shadow colors

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Living record of the capture-vs-internalize investigation: dragology
engine findings, decision log (morph over floating, flat clone overlay
over in-place nested FLIP, capture-first/SVG-never), substrate contract,
nool prerequisites, plan, open questions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- src/motion/Motion.tsx: flat overlay of shallow per-id clones tweening
  measured boxes (left/top/size/font/opacity), depth-stacked, with
  jank-free retargeting (origin = current blend) and enter/exit fades;
  no-change guard skips non-geometric actions
- remove View Transitions API entirely (Animate.tsx, inject branching,
  hover deferral, ::view-transition CSS, action-name-class rules)
- componentize views for DOM stability: Seed/StageView/ToolsView/
  ExpView/PreView are real components; expression kids id-keyed via
  <For>; toolbox rows keyed by transform index reading reactively;
  AdjacentPossible early-return reactivity fixed; hover mask memoized
- verified via CDP: transform morphs run and tear down cleanly, 0px
  retarget jump, hover updates stage classes with zero DOM replacement

See design/captured-geometry.md (Status / Step-1 findings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Context-scoped selectors (#stage .node.comp, .node-container projection,
#seed symbols, theme rules) did not match clones mounted under #main, so
nodes lost most styling mid-morph. The overlay now lives inside the hidden
.node-container and layers punch through with visibility:visible, keeping
the genuine cascade context. Also tag layers with data-motion-id.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes mid-morph style loss: black operators, lost mask tints, head
glyph jump-and-pop. Three mechanisms:
- subtrees moving rigidly (uniform scale+translate, no internal
  enter/exit) stay whole as one full-clone layer, so filters/shadows/
  descendant selectors compose exactly as live (28 layers -> 7 on a
  root commute)
- split layers get display:contents context shells carrying real
  ancestor classes (selector matching + inheritance, zero geometry)
- ancestor filter values are copied inline onto split layers
Also freeze pulse/hover animations during measurement, group exit
ghosts (survivors stripped), and reconstruct member boxes from
group-relative coords on retarget (measured 0.001px jump).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Profiling showed morphs were smooth per-frame (8.3ms median) but paid
two ~120-150ms paint stalls: first raster of overlay layers, and a
full stage repaint at teardown. Fixes:
- dim real content (opacity 0.001 on container children, overlay
  excluded) instead of visibility:hidden, keeping its texture warm so
  teardown is compositor-only (end stall eliminated)
- drive translation-only layers with the compositable translate
  property + will-change (raster once); translate not transform so
  clone animations composing on transform are preserved
- keep animations on clones (only transitions are suppressed): fixes
  the selected-head pulse glow vanishing during morphs
- reinstate the selection-outline morph as a synthetic glow layer
  tweening between old/new selected boxes over the live stage
- raise no-change guard to 1.5px (selection border jiggle was
  triggering full morphs and shadowing the selection path)
- revert toolbox shadow to the deployed soft variant (the harder one
  was from the unpushed exolivelit css-tweaks commit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- exit ghosts sort at old depth minus 0.25 so an eliminated wrapper
  (often a large mask-tinted box) recedes UNDER the surviving subtree
  that takes its place, instead of covering the stage (the blue-flash
  bug on identity-elimination)
- tween clock now starts on the second rAF tick, after the first
  painted frame has paid layer rasterization, so the animation opening
  is no longer skipped
- Shift+S toggles body.noshadows (box/text shadows off) for perf
  comparison; measured: ~170ms raster stall per morph with shadows,
  zero stalls without - shadows are the entire press-time hitch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GPU tracing showed the morph hitch was texture re-raster, from three
sources, each treated differently:
- overlay clones: always-on box-shadow:none for .motion-layer (moving
  nodes are shadowless in flight; selection glow layer excluded).
  Always-on matters: toggling a class on the real stage per-morph
  invalidates the whole tree (~76ms) and was self-defeating
- stage content permanently promoted (will-change: opacity) so dimming
  is an opacity flip on a warm texture, not a re-raster
- toolbox rows repaint their 10-layer pat shadows whenever match
  classes flip (even keyboard transforms): will-change on .source/
  .result halves it (65->33ms, no visual change); Shift+D toggles a
  calibrated 4-layer pat shadow (max pixel delta 26/255) that takes
  it to zero stalls, for aesthetic judgment
Also: TransformView memoizes its transform by reference so unflipped
rows keep their DOM on tool use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- settings.dragging with a corner-icon toggle (below the sound icon,
  mirroring the top-right cluster) so drag and noolbox interaction
  cannot interfere; in drag mode stage pointerdown grabs instead of
  selecting
- src/drag/Drag.tsx: enumerate transforms applicable at the grabbed
  node (both directions, ids preserved + freshen, structurally
  deduped); render each candidate into a hidden probe inside #stage
  (px font-size replicating stage depth scale, center-aligned) and
  batch-measure per-id boxes; ~10ms for 5 candidates
- debug overlay: dashed box on the grabbed node, colored anchor dot
  per candidate (hollow when the grab vanishes), cleared on release;
  coincident anchors make rule conflicts visible

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Motion gains a pointer-driven manual mode sharing all layer machinery
with the clock driver (capture_before/build_layers extracted):
manual_start builds layers from current display toward probe-measured
candidate boxes, manual_set drives t, manual_release springs back, and
committing simply injects the action - animate() captures the manual
blend as origin, so the handoff is seamless.

Drag = closest-of-betweens: pointer projects onto each grab->anchor
segment (rel-offset mapped boxes), nearest wins, t past 0.5 on release
commits at the candidate's site.

Key discovery: rules applied AT the grabbed node often give zero
anchor travel (commuting your own children does not move you), so
enumeration now covers ancestor sites too - the grab becomes a moving
subterm (drag the operand to commute the parent; verified end-to-end).
MIN_TRAVEL filters self-static candidates. ~14 candidates in ~18ms.

Also: :active wobble suppressed in drag mode (#seed.dragging).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- DRAG_TOOL_IDXS temporarily narrows drag candidates to commutativity
  (+,x) and distributivity while the interaction is tuned
- track visualizer: one segment per candidate from the grab point to
  its anchor (dashed when inactive, faint when unreachable), a
  perpendicular tick at the commit threshold, the active track drawn
  solid with its dot enlarged, and a projection-foot marker carrying a
  live t readout that turns green/checked past commit
- note: chaining was never enabled; a drag commits exactly one rewrite

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nearest-segment dispatch let nearly-collinear tracks steal the drag
mid-flight (grab the die, watch it become an ancestor commute). The
active track now keeps the drag unless a challenger is 24px closer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A candidate is offered at grab-node X only if X itself moves in it:
when X's box maps rigidly through its parent's displacement (same
relative position and scale), the rule relocated an enclosing subtree
and X merely rode along - that drag belongs to the ancestor. Derived
from measured geometry (live vs probe boxes), no pattern annotations;
reproduces the animate-algebra demo's trigger discipline for wildcard
interiors. Verified: grabbing a factored sum inside a product offers
exactly commute-times and distribute; ancestor commutes that would
carry the whole product are excluded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The drag state is no longer a per-frame nearest-candidate classification
(discontinuous at decision boundaries, patched by stickiness) but the
position of a knob on a star-shaped rail network joined at the grab
point. The pointer pulls; the knob chases its projection along the
current rail at bounded speed and may change rails only within a 10px
hub radius (checked before chasing - a diagonal pull still projects
positively onto the wrong rail). Continuity is by construction;
commitment away from the hub is topology, not a tuned threshold.
STICKINESS is deleted. Debug state exposed as window.__knob.

Also: candidates where the grabbed node vanishes are no longer offered
(the demo's triggers always survive); this removes the misplaced track
caused by the candidate-root fallback anchor, whose position varied
wildly with the within-node click position.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- settings.dragMechanic with an 8th corner icon (beside sound, top
  left): Rails = knob-on-rails; Closest = the original memoryless
  nearest-segment dispatch, kept for feel comparison
- grabs are now a singleton: a new grab force-ends any previous one
  (a crashed test script demonstrated how a leaked drag loop can
  outlive its pointerup)
- design doc: dragology placement (d.rails as a dispatch combinator
  sibling of closest), the factor-affordance case where geometry is
  stricter than the demo's trigger annotations, and the merge-rule id
  survival wrinkle

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- settings.dragDebug (default on) gates the rails/anchors/t-readout
  overlay and the console table; 9th icon completes the top-left 2x2
- every settings icon now carries a title tooltip describing what it
  toggles and its current state

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
actions/cache@v2 is hard-failed by GitHub now; the exolivelit branch
already bumped checkout and cache to v4 and its deploys worked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mixing a reactive class attribute with classList lets any class
re-evaluation (projection or theme toggle) rewrite className and drop
the classList-managed names until their own values change. This is why
drag-mode shake suppression (#seed.dragging) died after switching
projection modes; #main could likewise lose .selected on theme toggle.
Both elements now build one class string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Small circles left of each rule row (drag mode only, centered in line
with the arrow) toggle whether that rule generates drag candidates,
both directions together. State lives in tools.dragActive (default:
commutativity + distributivity), driving Drag.enumerate directly;
the DRAG_TOOL_IDXS stopgap is gone. Tooltips show on/off state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
During a drag the scene morphs toward the candidate exp, whose created
nodes carry enumeration-time fresh ids. Committing by re-deriving the
rewrite minted DIFFERENT ids for the same nodes, so the handoff saw the
almost-faded-in nodes as exits plus identical enters - a double pop.
The commit now injects f = () => candidate exp (reference-guarded:
falls back to re-derivation if the stage exp changed mid-drag), giving
id continuity across the drag-commit boundary; verified that in-flight
enter ids survive into the committed DOM.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the animate-algebra emergeFrom treatment to the captured
substrate: entering nodes start AT a source box, fully visible, and
pull out; exiting subtree ghosts converge into the box of whatever
replaces the rewrite site while fading. Motion gains EmergeOpts
(emerge/converge id maps) on manual_start/build_layers; Drag computes
v1 provenance geometrically (enters emerge from the grab - the demo's
expanding-rewrite trigger rule; exits converge into the site's
replacement root). Verified: identity-intro's +/moon grow out of the
grabbed mushroom at opacity 1; identity-elim's wrap ghost is absorbed
into the survivor's destination. Pat-level provenance (clone-from-
original, prefer-grabbed-copy) recorded as the follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…enance

Created nodes that structurally equal an existing site-scoped node are
CLONES: they emerge full-size at their original (sym and head mapped,
pixel-coincident at t=0 - fixes the displaced clone dice). Nodes with
no counterpart GROW from 15% at their source, opacity 1 (the moon
scales up from nothing; no fades). Exits mirror: subtrees with a
surviving structural twin MERGE (coincide, no fade); otherwise they
ABSORB (shrink into the site's replacement while fading - the wrap
ghost is sucked into the survivor's destination). Structural equality
is provenance-lite; rewrite-recorded provenance remains the endgame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Exits are now exactly dual to enters: per-node shallow layers (whole-
subtree ghosts removed) with merge (coincide with the surviving twin,
kind-matched sym/head, opacity 1) or absorb (shrink to 15% at the
site's replacement, opacity 1 - the dual of grow). Nothing fades in
provenance-carrying morphs; the click-path fallback fade remains.
Emerging layers get z-0.4 so creation comes from behind. And prefer-
the-grabbed-copy: when a merge rule consumes the grab but keeps a
structural twin, the grabbed subtree is spliced over the twin at the
candidate level - both dice now afford factoring, and the other copy
merges into the grabbed one. Verified: factor from either die, losing
die converging at opacity 1; identity-elim collapsing at opacity 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- tap-highlight/long-press-callout/user-select suppressed on the play
  surface (the misshapen focus box on tap); touch-action:none on #seed
  so drags are not stolen by scroll/pull-to-refresh
- 100dvh (URL-bar-safe); font clamp on narrow OR short viewports
  (landscape phones are short, not narrow); portrait stacks stage above
  noolbox; coarse pointers get finger-sized icons/toggles with spread
  corner clusters
- settings.dragging defaults on for coarse pointers
- desktop (>820px wide and >520px tall) is pixel-identical
Verified in device emulation: portrait+landscape fit with no overflow,
zero icon overlaps, touch-pointer drag grabs/morphs/releases cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unselecting expands the glow to #main's box (the window-as-root-node)
instead of fading in place; selecting shrinks it back down. Holding a
background press creeps the window corners in via #main.selected:active,
reviving the border-radius transition orphaned when view transitions
were replaced (93d2e89).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
disconcision and others added 29 commits July 30, 2026 19:25
Three real cuts were ending the glow abruptly: (1) the commit flash kept
mix-blend-mode: plus-lighter on for its full duration — additively
brightening the row even after its keyframes hit zero — then snapped to
normal compositing when the class dropped; the blend mode is gone and
the blown-out peak comes from brightness instead. (2) A single fade slot
meant any new fade (rail change) hard-cleared the previous row; fades
are now per-row. (3) A constant half-life reads as a uniform wipe
(brightness perception is ~logarithmic): the half-life now grows from
40ms toward a 200ms cap, giving a rapid falloff that levels into an
afterglow clearing in about a second.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The commit flash's additive blowout returns — but on an ::after overlay
whose OPACITY animates to zero, instead of blending the row itself: an
additive layer at opacity 0 contributes nothing, so the class removal at
the end stays visually a no-op and the continuous exit is preserved.
White-hot radial core over the row, fading on the same rapid-falloff-
into-tail curve as the rest of the glow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The overlay approximation traded too much of the blowout for exit
continuity; Andrew prefers the real thing. The row itself composites
plus-lighter for the flash (the rule-press :active look, brightness back
to 1.5), accepting the compositing snap when the class drops at the end.
The filter still fades on the falloff-into-tail keyframe curve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The drag commit action (transformNode) had no case in the sound
dispatch, so drag rewrites were silent while button rewrites weren't.
Reversed candidates play the reversed sample, mirroring the button path;
gated by the sound setting like everything else.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The genuinely exciting experiment: each rewrite's commit sample gets a
GrainPlayer looping a ~120ms grain window whose position is driven by
the live rail parameter — dragging literally scrubs the rewrite's sound,
unfolding as you pull, retreating as you back off, mirrored for reversed
candidates. Volume swells toward the commit point (-24dB → -10dB); rail
changes switch samples; release fades the grains out over 80ms before
the commit sample (or silence, on revert) takes over.

Transforms now carry their sfx bank name (Tools' mk records it) so the
drag layer can find the sample; scrub players own their buffers so the
commit Players' reverse-toggling can't cross-talk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scrub read as stuck tape, and it was also double-speaking (scrub
during + full sample after). Rather than patch one variant, all the
candidate designs are now live behind a debug cycle (Shift+A, logged to
console): scrub (as before), scrub-only (no commit sample — the scrub IS
the gesture; default, to evaluate next), detents (mechanical ticks on
rail acquisition and commit-threshold arm/disarm), tension (filtered
noise rising with t), plucks (quantized notes at quarter points,
descending when receding), and commit-only (the pre-experiment
baseline). All but scrub-only still play the transform's sample on
commit; continuous layers fade out on release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verdict from the Shift+A bake-off: drags speak in plucks — quantized
notes at the rail's quarter points, lower octave when receding — and the
rewrite samples are reserved for button-mode application. Deleted: the
granular scrub (players and all), detents, tension layer, the mode
cycle + Shift+A binding, the commit-sample suppression hook, and the
Transform.sfx plumbing the scrub needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same peak and falloff shape, compressed; the class drops at 330ms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
History lives in the model as past/future stacks of stage exps; every
successful rewrite (button, drag — both funnel through update_stage) and
restart pushes the pre-state and clears the future. Undo/redo swap exps
through Stage.put_exp (statics rebuilt), unset the selection (old paths
may not exist), and reset hover. Restart is undoable: Escape can't
destroy work. Restored exps keep their old node ids, so the motion layer
animates undo as a full morph for free. Ctrl works as the modifier too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Concept 1 from the sound-mapping menu: the pluck triad is derived from
the rule's own patterns — isomorphic rearrangements (commute, associate)
pluck open fifths, growing rewrites (identity/inverse intro, double-neg,
distribute) a major triad, shrinking ones (their reverses, factoring)
minor. size(result) - size(source) decides; nothing is hand-assigned,
new rules get sounds automatically, and since flip swaps the patterns a
reversed rule is automatically its forward form's harmonic opposite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Identity-intro on a composite operand grew its new 0 and + from the
operand's geometric center — a dot materializing amid the children,
increasingly wrong as operands get big. Grow-mode emerges whose source
is a comp now anchor at the comp's HEAD box (the operator icon), and
dually, fallback absorbs into a comp target converge into its head.
Atom sources/targets are unchanged (they are their own head).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Documents the drag-era sound design: one meaning per audio channel
(register=depth, contour=direction, quality=structural effect landed;
key/content and pan open), the six-way bake-off verdict, and the
non-arbitrariness ladder (size delta → operator multiset → pattern
melody) with the current toolbox's operator multisets tabulated.
TODO.md indexes sound.md and drag-target-decomposition.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ladder rung 2 (supersedes the quality triads): each operator symbol has
a pitch class — the sound alphabet, the one axiomatic choice (➕→C,
✖️→G, ➖→Eb, other→D, empty→A3) — and a rule's quarter-point plucks
walk its content from source to result: ¼ = source multiset, ½ = the
invariant core (∩), ¾ = result. Multiplicity stacks octaves, so
associativity is literally the plus-note doubled while commutativity is
it alone; created content audibly arrives near commit and consumed
content departs; flip swaps the walk. Advancing plucks crescendo,
receding replays an octave down. sound.md updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Default drag loadout gains associativity (+ and ×) and additive
identity alongside the commutes and distribute.

The reported hard-to-reproduce stuck-mid-tween state resisted three
scripted chaos campaigns (rail flapping through the hub, extreme holds,
commits with depth rescale, regrabs mid-commit-morph — all clean, no
errors). Two real holes closed on inspection regardless: undo/redo now
no-ops during an active drag (it would yank the world out from under
the drag's probes and tween — a brand-new hazard since Cmd+Z landed),
and manual_start's silent failure paths (empty before-capture, overlay
mount failure) now warn, since either would freeze the previous overlay
exactly as described.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Toggle between 'content' (multiset chords as landed) and
'content+harmony': each pluck chord gains one note, a third above its
root — major when the rewrite grows (operator count delta), minor when
it shrinks, none when isomorphic. The content letters are untouched;
the third colors the mood, restoring the retired quality channel as a
layer under the content skeleton rather than a competitor to it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drag mode: clicking anywhere on a rule row toggles whether it generates
drag candidates (the pattern-press handlers step aside and let the press
bubble). The side dot indicator is gone; the equals sign carries the
state instead, glowing white when the rule is on.

Wheel scrolling: the old handler constructed a fresh throttle closure
per event, so it throttled nothing — every trackpad micro-delta stepped
a full rule. Replaced with distance accumulation: one rule per ~100px of
wheel travel, remainder carried, so a notchy mouse wheel still steps
1:1 per click and a two-finger sweep moves about one rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ring-buffer window is gone: all rules render and the noolbox is a
real scroll container — continuous native scrolling (momentum, rubber
band) with CSS scroll-snap settling on a rule when the gesture ends,
replacing per-notch discrete stepping that felt stiff against thick
rows. tools.size still sets the box height (shift+wheel resizes, now
reading deltaX too for macOS's shifted-trackpad quirk); the wheelTools
action and ToolBox.offset are deleted. Bonus: every rule row now always
exists in the DOM, so the drag rule-glow reaches previously off-window
rules (distribute).

The equals glow is now the retired dot's look on the glyph itself:
brightness(0) invert(1) renders the emoji as solid white, with a tight
double drop-shadow halo instead of the diffuse text-shadow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Snap goes proximity -> mandatory: still fully smooth mid-gesture, but
every rest position is row-aligned at the top — the middle ground
between smooth and discrete, and no truncated top row at rest. The
bottom partial row (unavoidable with variable row heights and a fixed
box height, short of JS-driven resizing) now fades out via a mask, so
clipping reads as the rules receding into a slot rather than being cut.

Also fixes the left-edge hard line: becoming a scroll container forced
horizontal clipping (a non-visible overflow-y drags overflow-x along),
slicing the rule shadows that used to bleed past the box; 0.7em of left
padding gives them room to fall off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-stop

The mask-fade made the box look LESS recessed — the hard clip lines at
top and bottom were doing the work of the slot's lip. Reverted. And
scroll-snap-stop: always goes in as a trial: every flick advances at
most one rule, no sailing past snap points; remove if too notchy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The model's own state — world exp, selection, settings, drag loadout,
box size, and the full undo history — persists to localStorage (a few
KB of plain JSON; synchronous storage keeps boot simple, IndexedDB
would buy nothing at this size), debounced 250ms after every applied
action. Boot loads it back through Stage.put_exp (statics rebuilt);
the ID counter is persisted and bumped so restored exps' minted ids
can't collide with fresh ones. Undo works across reloads. hardReset
returns everything to factory and clears the stored session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Corner reorganization (three per corner, no 2x2 squares): top-left
sound + adjacent-possible preview + the new reset-everything button
(confirm-guarded, wearing the previously-unused dead icon); top-right
theme/projection/symbols unchanged; bottom-left takes the drag trio
(mode, mechanic, debug); bottom-right is empty — the motion icon is
retired, since motion is load-bearing now and Off is no longer a mode
worth a corner. Mobile coarse-pointer offsets updated to match.

Also: the adjacent-possible previews were permanently blurred — the
rest-state filter carries !important and the hover (sharpen) rule
didn't, so hover never won; fixed. The scroll-snap-stop trial is
removed (imperceptible at 9 rules). Shift+wheel box resize is now
continuous (fractional size, smooth max-height) instead of stepped.
TODO records the recess-lip observations and the exact-fit-height
verdict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Button-applied rewrites (noolbox clicks, keyboard 1-4/space, preview
clicks) animated their enters as in-place half-size fade-ins and exits
as in-place fade-outs, while drags had full provenance (clone/grow from
sources, merge/absorb into targets, head-origin anchoring). Now
App.inject hands animate() a provenance closure for transform actions
at the selection: the site is the selection and the selected node plays
the grab's trigger role, so identity, inverse, double-neg, and
distribute look identical clicked or dragged. Drag commits pass through
untouched (selection is unselected in drag mode; their provenance
resolved during the manual morph). Closes the design doc's open
click-path-emerge item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Double-neg elimination merged its dying negations onto the mushroom's
minus — any same-headed comp surviving in the site qualified as a merge
twin, and the dying ➖ glyphs independently twin-matched any structurally
equal head atom. Intro had the dual flaw (new negations cloned out of
the bystander minus). Two tightenings: a comp only clones from / merges
into a same-headed comp that itself CHANGED in the rewrite (participants,
not bystanders — factor's product merge and distribute's clone still
qualify since the surviving twin's content changes), and comp heads
never twin-match on their own — they follow their comp or grow/absorb
with it. Verified by trajectory sampling: the dying negation head now
absorbs toward the site replacement's head, away from the bystander.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clones bake in the post-state's .selected decoration, so a rewrite that
relocates the selection (factoring: the surviving times lands in the
plus's seat) flashed a fully-formed white outline + pulsing head on a
node still mid-flight. Now full morphs suppress the in-scene decoration
(selection-morphing extends to the head's pulse) and fly the synthetic
glow frame from the old selected box to the new one — it lands exactly
as the live decoration returns at teardown, so there's no pop either.
Unselecting morphs (undo) send the glow out to the window, per the
root-selection metaphor. Glow-layer construction and the #main box are
factored into helpers shared with the pure-selection morph.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The traveling-glow treatment now applies only when the selection MOVES
between nodes (pre !== post, or an interrupted glow needs to finish).
A stationary selection — commute at the selected node: same node, only
its children rearrange — keeps its baked outline and head pulse riding
the clone, which is the truthful rendering; suppressing it read as the
highlight flickering off mid-operation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
captured-geometry: bystander-twin tightening findings entry; the
flat-layer backdrop-tint observation with its candidate fix. sound.md:
the shrink-at-half pluck asymmetry and queued follow-ups (pan, commit
accent). TODO: pending verdicts (harmony, previews, die tint), the
stuck-tween watch instructions, and the Iconify icon-lab workflow idea.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the first fold

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Margin animation re-laid-out and repainted both shadow-heavy neighbors
every frame. #stage itself stays transform-free so the fixed-position
motion overlay keeps its viewport anchoring; the stage side rides its
already-layerized root node.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clicking the seed now sounds in both modes (it previously borrowed the
background click's unselect pluck, inert in drag mode): membrane boom on
C2 with the pluck alphabet's open fifth ringing above through a long
tail. Still clears selections like a background click.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@disconcision
disconcision merged commit 87d80c0 into main Jul 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant