docs(spec): animation system — Layer-1 runner + tour saga, radar-hardened#363
Merged
Conversation
Channel set ≡ CameraPose (4 channels; roll/fov + the spline camera deferred as a generator, not new channels), and the Layer-2 tour saga (discrete click-advanced beats, one clip per stop, dwell never frozen via a perpetual dwellDrift clip in the race; capture/restore via the real wiring seam). Adds the full Q1–Q16 grill transcript. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three design-time findings on the animation spec: - commit-on-edge: lift `commitsOnEdge` to the driver row instead of growing a hardcoded id OR-chain in runFrame (dissolves the branch and its future growth). - channels: one canonical CHANNEL_SPACE record read by helpers + validator; action `space` is an optional override (no triplicated map). - opacity: document why it stays a suspended bridge (two exclusive writers) rather than a camera-style arbitration table (N concurrent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
skymap | 2439175 | Commit Preview URL Branch Preview URL |
Jun 24 2026, 09:04 AM |
The grill made the clip a live-pose Resource gated by an Intent flag
(orbitDrag-shaped), on the reasoning that the 60Hz pose can't be Intent.
That conflated the pose (derived, fine) with the clip descriptor
(low-freq Intent). The focus tween is the proof: it's also a
per-frame-changing pose, yet it's a pure evaluator over a store
descriptor, not a Resource.
So the clip takes the same shape: `camera.clip: { data: ClipData }` in
the store; `pose: (s,_cam,elapsed) => evaluateClip(data, elapsed)` pure;
no live-pose register; rides cameraClock like the tween. The clipPlayer
shrinks to scene-cues + lifecycle. evaluateTween becomes the one-segment
case of evaluateClip. Buys serializable live state, near-free scrub, and
frame-rate-independent determinism; costs a closed-form ∫vel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A clip dispatches the same intents the UI does, so it trips every production saga that reacts to them. The spec un-braided one (watchFades) and hand-waved the rest. Three fixes: - focus() planted a camera.tween@60 that outlived the clip and hijacked the camera at clip-end (dormancy-by-priority ≠ suspension). Suspend watchFocusTween alongside watchFades, via one declared SUSPEND_DURING_CLIP set + whileNoClip wrapper — not a per-saga guard. - show/hide reuse the syncVisibilityFades body (add an optional durationMs; it already takes only?), not a duplicated manifest walk. - captureScene/restoreScene widen the snapshot to selection.focus so a beat's focus() + isolation dim don't leak past the tour's restore. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…model) Three-agent parallel audit + grill. Folds in: - #1 start:'live' resolved to a concrete pose at startClip from cameraRuntime.lastPose.current (mirrors focusTweenSaga baking 'from'), so evaluateClip stays pure AND the inter-clip handoff is robust to the skipped commit-on-edge. - #2 endClip() clears a stale camera.tween (suspension stops only NEW tweens); watchSelectionRows stays live (it drives the isolation dim). - #3 the suspend guard gates per-action INSIDE takeEvery's worker, not around the watcher (a watcher registers once at boot). - #4 the tour ends only via an explicit TOUR_EXIT racing the beat loop; camera input is swallowed by design (dissolves the isUserCameraInput self-abort trap). - #5 fade() writes a third composed opacity channel, clipOpacity (intentOpacity x focusRecession x clipOpacity), the shape structureFocus already uses, reset on clip end. Dissolves the intent desync, the clip-end freeze, and shrinks the suspend-set to watchFocusTween alone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dary, saga seam) - #6: 'clip joins the clock' is a triple — clipStartMs + clipElapsed + elapsedForWinner arm; omitting any leaves the clip frozen at t=0. - #7: residual rate-velocity is a within-clip layer; the single-pose handoff bakes position only, so it dies at the clip boundary by design (dwellDrift / ramp-down disciplines). - #9: BeatData.effects are plain Action[] (put verbatim, no applyIntent wrapper); capture/restore reach the existing captureSettings/restoreSettings helpers as ReconcileEffects closures via getContext('reconcile'), since a saga has no state/store in scope to pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan A (clip model): Effect/ClipData types, pure evaluateClip with the clock triple, the clip@95 driver row + commitsOnEdge, the clipPlayer Resource (tick-first cues + lifecycle), the clipOpacity channel + fade() + the full scene-verb vocabulary, re-express the flyout spike. Plan B (playClip seam): fold evaluateTween into evaluateClip (focus = the one-segment case; camera.tween stays a separate Intent), the playClip Promise seam with saturated-pose clip-end sequencing, suspendDuringClip parking watchFocusTween, verify endClip clears a dormant camera.tween. Plan C (tour saga): BeatData, visitBeat/guidedTour with TOUR_EXIT-raced cancellation, captureScene/restoreScene as ReconcileEffects closures, the dwellDrift/flyTo builders — purely Layer-2 orchestration consuming the Layer-1 vocabulary. Cross-plan boundaries reconciled to single ownership: clipOpacity keyed by VisibilityLayerKey owned by Plan A; scene verbs + SceneEffect owned by Plan A (recordings use them sans saga); suspendDuringClip owned by Plan B. Spec BeatData.focus corrected SourceRef -> SelectionRef. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rulkens
added a commit
that referenced
this pull request
Jun 24, 2026
Reconciles the squashed docs PR #363 (radar-hardened tour-saga plan) with the feat branch. Resolves two add/add plan conflicts: - clip-model plan: keep feat's completed [x] checkbox record - tour-saga plan: take main's radar-hardened Task 8 (feat never executed Plan C) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refines the committed animation-system design spec (
docs/superpowers/specs/2026-06-19-animation-system-design.md, PR #346) with the resolved Layer-1 runner design and a design-time entanglement-radar pass. Docs-only — no code.What changed
Grill resolution (Q1–Q16) — full transcript in
docs/grill-sessions/animation-runner-placement-2026-06-21.md. The clip runner is not a god-objectCameraDriver; it splits into:clip@95 driver in the table (orbitDrag-shaped:camera.clipIntent flag + aclipPlayerResource, pure pose read), so the table stays the single camera-pose authority;clipPlayersubsystem that ticks first, composesbase/vel/osc, and fires scene effects.Key decisions recorded: registration-time single-writer validation (complete because clips are static data);
show/hide/fade/scene/focusscene vocabulary with opacity single-writer + bridge suspension; structured-concurrencyfork; all-or-nothing cancellation viaendClip()+ a[CANCEL]hook; the animatable channel set ≡CameraPose(roll/fovdeferred, the spline camera is a generator not a channel); and a discrete click-advanced tour saga (dwell never frozen).Entanglement-radar pass (
docs(spec): un-braid…):commit-on-edgelifted to acommitsOnEdgedriver-row property instead of a hardcoded id OR-chain inrunFrame;CHANNEL_SPACErecord (helpers + validator read it; actionspaceis an optional override);Scope
Design only, awaiting plan. The throwaway recording spikes that proved the vocabulary live on the separate draft PR #354 and are not included here.
🤖 Generated with Claude Code