Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa3bbef0d6 | |||
| 64e8f658f6 | |||
| 760fdc7550 | |||
| e62382c5e5 | |||
| 31956bfdfc | |||
| 5a08d1dd35 | |||
| 432fbd7703 | |||
| 5f8b8f14e6 | |||
| 59c1c3a895 | |||
| 582183db2f | |||
| c13ce36787 | |||
| 21830069ad | |||
| 50366c056e |
@@ -0,0 +1,94 @@
|
||||
# Session 0027.0 — Transcript
|
||||
|
||||
> App: human-experience-filter-art
|
||||
> Start: 2026-06-29T22-04 (PST)
|
||||
> End: 2026-06-30T06-47 (PST)
|
||||
> Type: planning-and-executing
|
||||
> Posture: yolo
|
||||
> Claude-Session: 3e51cce1-9023-4a16-b607-99894dfe0597
|
||||
> Checkout: /Users/benstull/git/benstull.org/benstull/human-experience-filter-art
|
||||
> Status: **FINALIZED**
|
||||
|
||||
## Launch prompt
|
||||
|
||||
```
|
||||
Add left and right brain annotations (including localization) for any that are missing.
|
||||
```
|
||||
|
||||
## Plan
|
||||
|
||||
> Anchor: operator direct instruction (standalone leaf content-fill task, §4.3 R2b).
|
||||
|
||||
**Goal:** Author the missing **left-brain** factual `LABELS` (tiered general→scientific→+fact
|
||||
labels) for the rotating-pool clips that lack them, plus their **es/fr/ja** translations,
|
||||
then rebuild + merge the manifest. Right-brain `affect` is auto-generated per scale for
|
||||
every clip via `_affect_for_clip` — nothing missing there.
|
||||
|
||||
## Pre-state
|
||||
|
||||
- `main` at `5beee55` locally but **diverged from `origin/main`**: 11 merged-but-unpushed
|
||||
commits (the entire i18n localization feature + a video-loop safety-net fix) stranded
|
||||
locally; `origin/main` still at `eff43bd` (PR #29). Plus 3 files with an uncommitted
|
||||
"boots-silent / no auto-start" change. Two stale `--INPROGRESS` placeholders (0015, 0017).
|
||||
- Annotation reality (from a read-only survey of `build_pool_manifest.py` + catalogs):
|
||||
right-brain `affect` present on all 41 clips (auto-per-scale); left-brain `LABELS`
|
||||
present on only 13 of 41. 28 pool clips had `"annotations": []`.
|
||||
|
||||
## Arc
|
||||
|
||||
1. **Scoped the task** — explored the annotation/i18n system; established that the gap was
|
||||
left-brain only (right-brain is auto-generated), 28 clips, + their es/fr/ja strings.
|
||||
2. **Claimed session 0027** (peek showed the two stale placeholders; noted + proceeded).
|
||||
3. **Surfaced the unclean baseline**; operator chose *"Push stranded, also commit dirty."*
|
||||
- Stashed the 3 dirty files, merged `origin/main`'s claim commit, **pushed the 11
|
||||
stranded commits** (`020219f`).
|
||||
- Committed the boots-silent change on `fix/boot-silent-no-autostart`, verified
|
||||
(299 pytest pass), **merged + pushed** (`7bd6c9a`).
|
||||
4. **Annotation work** on `feat/left-brain-annotations`:
|
||||
- Added 28 `LABELS` entries (static tiered labels) to `build_pool_manifest.py`
|
||||
(Round-5 block); rebuilt manifest → all 41 clips annotated; extracted en catalog.
|
||||
- Dispatched **3 parallel translator subagents** (es/fr/ja) for the 67 new keys,
|
||||
matching existing catalog voice; fixed 2 ja `measure.*` values to the `約`/localized
|
||||
convention; merged fragments into catalogs; merged all 3 langs into the manifest.
|
||||
- Verified: 41 clips × en/es/fr/ja full key + tier-length parity; 299 pytest pass
|
||||
(3 pre-existing `cv2` env failures only); 37 i18n/manifest tests green; spot-checked
|
||||
translations. **Merged + pushed** (`934f60c`), deleted both branches.
|
||||
|
||||
## Cut state
|
||||
|
||||
- `main` == `origin/main` @ `934f60c`, clean tree, branches removed.
|
||||
- 41/41 clips carry left+right annotations with full en/es/fr/ja parity.
|
||||
- §9: local tests green. No PPE/E2E machinery exists for this app yet (pipeline is policy,
|
||||
not automated — §10.6); prod promotion stays operator-gated. Visual review deferred to
|
||||
operator (no Chrome on box).
|
||||
|
||||
## Operator plate
|
||||
|
||||
- **Review by eye** the 28 newly-labelled clips across all 4 languages in the running sim.
|
||||
- The labels are **static**; the abyss/reef *creature* clips would benefit from a later
|
||||
**tracked-label** upgrade via author mode (couldn't auto-file as a tracker issue — the
|
||||
capture token returned 401 in this env; full context is in the session-0027 memory note).
|
||||
- A few species IDs were inferred from clip META, not the frame (`reef_redsea`→anthias,
|
||||
`reef_flowergarden`→jack/scad+sergeant-major) — worth confirming at review.
|
||||
- Two stale `--INPROGRESS` sessions (0015, 0017) remain unfinalized — likely the crashed
|
||||
sessions that stranded the 11 commits; left untouched for the operator.
|
||||
|
||||
## Next-session prompt
|
||||
|
||||
```
|
||||
/goal Review the new left-brain annotations (all 28 newly-labelled pool clips, across en/es/fr/ja) by eye in the running simulator; correct any species/box that reads wrong, and decide whether the abyss/reef creature clips should get tracked labels via author mode.
|
||||
```
|
||||
|
||||
## Deferred decisions
|
||||
|
||||
- **Static labels, not tracked.** Authored all 28 clips' left-brain labels as fixed-box
|
||||
`static_label`s rather than motion-tracked labels. Alternative: tracked labels (as the
|
||||
reef/abyss showcase clips use) so the box follows drifting creatures. Chose static because
|
||||
the footage wasn't eyeballed — fabricating track coordinates would claim precision I don't
|
||||
have. The abyss/reef creature clips are the ones that would most benefit from a later
|
||||
tracked-label upgrade via author mode.
|
||||
- **Some species IDs inferred from clip metadata, not the footage.** `reef_redsea`→anthias,
|
||||
`reef_flowergarden`→jack/scad + sergeant-major — read from the clip META title/source.
|
||||
High-confidence from provenance, but worth an eyeball check.
|
||||
- **`O-type` kept verbatim in es/fr/ja** (per the translation brief) rather than the more
|
||||
natural `tipo O` / `type O`. Minor; flagged by the es translator.
|
||||
@@ -1,28 +0,0 @@
|
||||
# Session 0027.0 — Transcript
|
||||
|
||||
> App: human-experience-filter-art
|
||||
> Start: 2026-06-29T22-04 (PST)
|
||||
> Type: planning-and-executing
|
||||
> Posture: yolo
|
||||
> Claude-Session: 3e51cce1-9023-4a16-b607-99894dfe0597
|
||||
> Checkout: /Users/benstull/git/benstull.org/benstull/human-experience-filter-art
|
||||
> Status: **PLACEHOLDER — claimed at session start; finalized at session end.**
|
||||
>
|
||||
> This file reserves session ID 0027 for human-experience-filter-art. The driver replaces this
|
||||
> body with the full transcript and renames the file to its final
|
||||
> SESSION-0027.0-TRANSCRIPT-2026-06-29T22-04--<end>.md form at session end.
|
||||
|
||||
## Launch prompt
|
||||
|
||||
```
|
||||
Add left and right brain annotations (including localization) for any that are missing.
|
||||
|
||||
Resolved scope: right-brain `affect` is auto-generated per scale for every clip (nothing missing). The gap is left-brain `LABELS` (factual/scientific tiered labels) on 28 rotating-pool clips that have no entry in simulator/build_pool_manifest.py, plus their es/fr/ja translations in tools/i18n/catalogs. Author them following the existing static_label pattern, rebuild the manifest, merge translations, run tests.
|
||||
|
||||
```
|
||||
|
||||
## Deferred decisions
|
||||
|
||||
_Autonomous-mode low-confidence calls the driver made and would have
|
||||
liked operator input on. Appended as the session runs; surfaced at
|
||||
finalize. Empty if none._
|
||||
@@ -0,0 +1,125 @@
|
||||
# Session 0028.0 — Transcript
|
||||
|
||||
> App: human-experience-filter-art
|
||||
> Start: 2026-06-30T06-53 (PST)
|
||||
> Type: planning-and-executing
|
||||
> Posture: yolo
|
||||
> Claude-Session: d9da7994-e036-4b7c-b2d4-d2675ac4e5a1
|
||||
> Checkout: /Users/benstull/git/benstull.org/benstull/human-experience-filter-art
|
||||
> Status: **FINALIZED.**
|
||||
> Worktree: `worktree-session-0028` (isolated; concurrent session in flight). Torn down at finalize.
|
||||
> Outcome: D3 reverse-landing frame jump FIXED, app-side. PR #30 → main (`582183d`).
|
||||
|
||||
## Launch prompt
|
||||
|
||||
`/goal next` — operator then directed: "There's a concurrent session — do this one out
|
||||
of a worktree in a different dir." Session 0028 runs isolated in worktree
|
||||
`worktree-session-0028` while a concurrent session owns the simulator startup ("Run
|
||||
simulation" button: `index.html`/`style.css`/`i18n.js`).
|
||||
|
||||
## Plan
|
||||
|
||||
> Anchor: leaf bug — D3 reverse-landing frame jump (no tracker issue; the s0027 capture
|
||||
> token returned 401. Residual flagged in the altitude-morph-and-scrub memory note +
|
||||
> scrub-driven-transitions design.)
|
||||
|
||||
**Goal:** kill the ~3-second frame jump when ASCENDING the altitude dial and landing on
|
||||
the higher-altitude clip.
|
||||
|
||||
**Root cause (verified by reading the bake + player):** each baked morph
|
||||
`transitions/{h}__{l}.mp4` spans `src(h)@0s` (frac 0) → `dst(l)@~LOOP_TAIL_S` (frac 1),
|
||||
and the scrub uses this single forward file in BOTH directions. The steady loop element
|
||||
always seeks to `LOOP_TAIL_S` (3.0s) on landing. So:
|
||||
- DESCEND (dir>0) lands on `dst` at frac 1 → morph ends `dst@3` ≈ loop@3 → seamless ✓
|
||||
(the s0026 "loop-from-tail" fix).
|
||||
- ASCEND (dir<0) lands on `src` at frac 0 → morph shows `src@0`, but loop seeks `src@3`
|
||||
→ ~3s jump ✗ (D3).
|
||||
|
||||
**Fix (app-side, no media re-bake — beats re-baking 154 LFS morphs):** make the loop
|
||||
landing frame DIRECTION-AWARE. On a reverse landing seek the loop to 0 (= the morph's
|
||||
frac-0 frame) and wrap `[0,dur]` (the base is already a seamless crossfade-loop); forward
|
||||
keeps `[LOOP_TAIL_S,dur]`. Pure decision extracted to `scrub.js loopLandFrame(dir,
|
||||
loopTailS)` + node tests; wired through `loadLoop`/settle/wrap handlers in `app.js`.
|
||||
|
||||
**Tasks:**
|
||||
1. TDD `HEFScrub.loopLandFrame(dir, loopTailS)` in `scrub.js` (+ `scrub.test.js`).
|
||||
2. Wire `app.js`: `loadLoop(clip, landFrame)`, directional settle seek, `dataset.loopStart`
|
||||
in the `timeupdate`/`ended` wrap handlers.
|
||||
3. Add a reverse-landing e2e (mirror of the existing forward "loops from tail" test).
|
||||
4. Verify: node unit tests + pytest + e2e (best-effort) all green.
|
||||
5. Branch → PR → merge (yolo). PPE/E2E machinery not built for this app (§10.6) — local
|
||||
green is the bar; prod promotion stays operator-gated.
|
||||
|
||||
## Deferred decisions
|
||||
|
||||
_Autonomous-mode low-confidence calls the driver made and would have
|
||||
liked operator input on. Appended as the session runs; surfaced at
|
||||
finalize. Empty if none._
|
||||
|
||||
- **Fix shape: app-side direction-aware loop landing, NOT a 154-morph re-bake.** The
|
||||
s0026 memory note guessed the D3 fix would be "per-clip loop files." Reading the bake +
|
||||
player showed the root cause is simpler: the morph's frac-0 frame is `src@0` but the
|
||||
loop always seeked the tail (`LOOP_TAIL_S`). Fixed entirely in `app.js`/`scrub.js` by
|
||||
landing the loop on the morph's actual frac-0 frame (0) when ascending. This avoids
|
||||
re-baking 154 LFS morphs (no media churn, a tiny reviewable diff, the base clips are
|
||||
already seamless crossfade-loops so `[0,dur]` wraps cleanly). Alternative considered:
|
||||
re-bake morphs to start at `src@LOOP_TAIL_S` — heavier, no benefit.
|
||||
- **Pre-existing e2e failure left as-is:** `loop-recovery.spec.ts` fails in this
|
||||
environment on the CLEAN baseline too (verified by stashing my changes). It boots
|
||||
without enabling video, so the loop's `loopTail` is never armed and the `ended` guard
|
||||
returns early. Not introduced here; my change preserves that guard exactly. Worth a
|
||||
separate look (the test likely needs to `enableVideo` first), but out of scope for D3.
|
||||
- **Could not verify by eye.** The fix is logic- and test-verified (node + the new
|
||||
reverse-landing e2e asserting the loop anchors at the head, not the tail); the actual
|
||||
visual seamlessness of an ascending landing still wants an operator eyeball in the
|
||||
running sim.
|
||||
|
||||
## Session arc
|
||||
|
||||
1. Claimed session **0028** (race-free, via git). Two stale `--INPROGRESS` placeholders
|
||||
(0015, 0017) noted as orphaned, left untouched.
|
||||
2. Baseline survey surfaced uncommitted `run-sim` button edits + main behind by 2
|
||||
(session commits). Operator confirmed a **concurrent session** owns that startup work
|
||||
and directed isolation → created worktree `worktree-session-0028` off `origin/main`,
|
||||
leaving the concurrent session's edits in the canonical clone untouched.
|
||||
3. Orientation: in-repo `docs/ROADMAP.md` is stale (formal frontier = deferred hardware);
|
||||
live frontier is in memory. Stored `/goal next` (0027's by-eye annotation review) is
|
||||
operator-eyes work I can't complete; the run-sim button is the concurrent session's.
|
||||
Asked the operator to pick 0028's disjoint item → **D3 reverse-landing fix**.
|
||||
4. Read the bake (`build_pool_manifest.transition_cmd`) + player (`app.js` loop/morph) to
|
||||
pin the root cause (morph spans `src@0`→`dst@tail`; loop always seeks the tail).
|
||||
5. TDD: `HEFScrub.loopLandFrame` (red → green), then wired `app.js`
|
||||
(`loadLoop(clip,landFrame)`, `dataset.loopStart`, directional settle, wrap handlers).
|
||||
6. Added a reverse-landing e2e. Verified: node 16/16, **altitude-lock e2e 12/12** (forward
|
||||
no-regression + new reverse test), pytest 299 pass. Confirmed `loop-recovery.spec.ts`
|
||||
fails on the clean baseline too (pre-existing, not mine) by stashing.
|
||||
7. Committed → PR #30 → merged to `main` (`582183d`) → deleted branch. Updated memory.
|
||||
|
||||
## Cut state
|
||||
|
||||
- **On `main` (`582183d`):** D3 fix — `simulator/static/scrub.js` (+`loopLandFrame`),
|
||||
`simulator/static/app.js` (direction-aware loop landing), `simulator/unit/scrub.test.js`,
|
||||
`simulator/e2e/tests/altitude-lock.spec.ts` (reverse-landing test).
|
||||
- **Tests:** node 16/16; altitude-lock e2e 12/12; pytest 299 pass / 3 pre-existing
|
||||
env-only failures (real-ffprobe/ffmpeg + ML detect; untouched — no Python changed) / 4
|
||||
skipped. `loop-recovery.spec.ts` is a pre-existing baseline failure in this env.
|
||||
- **§9 / deploy:** this whole project is **exempt** from flotilla/PPE/§9 (memory:
|
||||
project-exempt-from-wiggleverse-deploy; public target is static Cloudflare). Local-green
|
||||
is the bar — met. No PPE stage to run.
|
||||
- **No plan artifact archived:** fused leaf fix, plan inline in this transcript; app has
|
||||
**no content repo** (`CONTENT_REMOTE` empty) — nothing to archive.
|
||||
|
||||
## Operator plate
|
||||
|
||||
- **Review by eye** an ASCENDING altitude landing in the running sim — confirm the
|
||||
~3s reverse-landing jump is gone (logic/test-verified only; no Chrome on box here).
|
||||
- Still standing from s0027: by-eye review of the 28 left-brain annotations (4 langs);
|
||||
decide on tracked labels for abyss/reef creature clips.
|
||||
- A concurrent session was shipping the "Run simulation" startup button — reconcile/land
|
||||
that separately (its edits are in the canonical clone, not touched here).
|
||||
|
||||
## Next-session prompt
|
||||
|
||||
```
|
||||
/goal Review by eye in the running simulator: (1) an ASCENDING altitude landing — confirm the D3 reverse-landing frame jump is gone; (2) the 28 left-brain annotations across en/es/fr/ja, correcting any species/box that reads wrong; then decide whether the abyss/reef creature clips should get tracked labels via author mode.
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
# Session 0029.0 — Transcript
|
||||
|
||||
> App: human-experience-filter-art
|
||||
> Start: 2026-06-30T08-09 (PST)
|
||||
> End: 2026-06-30T08-41 (PST)
|
||||
> Type: planning-and-executing
|
||||
> Posture: yolo
|
||||
> Claude-Session: c87a07d3-917c-44bc-82cf-c6d8edee952b
|
||||
> Checkout: /Users/benstull/git/benstull.org/benstull/human-experience-filter-art
|
||||
> Branch: design/cloudflare-static-publish
|
||||
> Status: **FINALIZED**
|
||||
|
||||
## Launch prompt
|
||||
|
||||
"We need to make sure credits for Creative Commons licensed videos are compliant.
|
||||
What are some options?" → after presenting options the operator chose **Option A**
|
||||
(manifest-driven static credits/colophon page surfaced to the public) + **Option D1**
|
||||
(declare the altered footage CC BY-SA 4.0). Non-commercial.
|
||||
|
||||
## Pre-state
|
||||
|
||||
- Branch `design/cloudflare-static-publish` (static-publish work, pushed). Working
|
||||
tree had uncommitted, unrelated in-flight work: a "Run simulation" start button
|
||||
(6 files) **plus** an `r2-cors.json` reformat (surfaced via stash, 7 total).
|
||||
- Per-clip `license`/`source` already in `clips.json` (baked from `/api/clips`) but
|
||||
only shown in the dev panel + internal review pages — public experience showed none.
|
||||
- A concurrent session was live in worktree `fix+audio-and-tracking-labels` (off main);
|
||||
the operator was also committing directly to `design/cloudflare-static-publish`.
|
||||
|
||||
## Arc
|
||||
|
||||
1. **Audited** the license surface: `tools/licensing.py`, `pipeline/provenance.py`,
|
||||
manifest, frontend. Found credits exist in data + review tooling but not the public
|
||||
experience → that's the gap. Presented options A–D; operator chose A + D1.
|
||||
2. **Claimed session 0029** (planning-and-executing, yolo). Two stale `--INPROGRESS`
|
||||
placeholders (0015, 0017) noted as long-dead, left alone.
|
||||
3. **Operator-requested pre-work:** the uncommitted in-flight work. Stashed it,
|
||||
created an isolated worktree (`.claude/worktrees/session-0029` off the branch HEAD,
|
||||
since the static work lives there not on main), applied the stash.
|
||||
- Discovered the branch had advanced under me to `8cc472c` (operator's direct
|
||||
commit). It **intentionally** keeps `r2-cors.json` in S3-array form alongside a
|
||||
new `r2-cors.wrangler.json` (runbook documents both) → **dropped** my conflicting
|
||||
`r2-cors.json` reformat commit; kept + rebased only the Run-simulation button.
|
||||
- Committed the button (`3a45e33`), ff-merged + pushed.
|
||||
4. **Built Option A + D1** (TDD): node test for the pure credits logic first (red) →
|
||||
`credits.js` (UMD: `creditEntries`/`creditsListHtml`, escaped, sorted) → `credits.html`
|
||||
colophon (D1 CC BY-SA declaration + "altered" note; JS-filled video attributions;
|
||||
PD/CC0 audio courtesy block; back link) → header "ⓘ Credits" link (`credits.link`
|
||||
i18n en/es/fr/ja) → `style.css` → `PUBLIC_ASSETS` in `build_static.py` + its test →
|
||||
Playwright e2e (`tests/test_e2e_credits.py`).
|
||||
5. **Verified:** node 28/28, pytest 315 passed / 2 skipped. Real build render: 41/41
|
||||
clips credited, all 7 CC-BY/BY-SA clips show required attribution, 0 missing.
|
||||
Committed (`1c7f2b7`), ff-merged + pushed, removed the worktree, deleted the branch.
|
||||
|
||||
## End state
|
||||
|
||||
- `design/cloudflare-static-publish` @ `1c7f2b7`, clean, pushed (origin in sync).
|
||||
- Public site now attribution-compliant: credits reachable from the work; altered-work
|
||||
license declared. §9 is **exempt** for this project (static Cloudflare target), so
|
||||
merged+pushed to the design branch is the completion state — no PPE/prod.
|
||||
|
||||
## Deferred decisions
|
||||
|
||||
- **Dropped my `r2-cors.json` reformat** rather than merging it — concluded from the
|
||||
operator's `8cc472c` + runbook that the S3-array form is intentional. (Alternative:
|
||||
keep both forms / ask. Chose to honor the documented two-file split.)
|
||||
- **Audio credits as a static courtesy block** in `credits.html` rather than plumbing
|
||||
`audio_license` through the manifest/API. Rationale: audio is all PD/CC0 (no legal
|
||||
attribution duty) and not machine-readable; data-plumbing was out of scope. (Alternative:
|
||||
add audio license fields — deferred, low value for non-commercial PD/CC0.)
|
||||
|
||||
## Next /goal
|
||||
|
||||
```
|
||||
/goal operator by-eye review of the credits colophon + Run-simulation button on design/cloudflare-static-publish, then the actual Cloudflare deploy (provision R2 human-experience-simulator-media + CORS, run tools.build_static, Pages deploy) per deploy/cloudflare/README.md
|
||||
```
|
||||
@@ -0,0 +1,128 @@
|
||||
# Session 0030.0 — Transcript
|
||||
|
||||
> App: human-experience-filter-art
|
||||
> Start: 2026-06-30T08-10 (PST)
|
||||
> End: 2026-06-30T08-50 (PST)
|
||||
> Type: planning-and-executing
|
||||
> Posture: yolo
|
||||
> Claude-Session: c4cec6c6-26d8-46f3-80ee-4ff4035a4f72
|
||||
> Checkout: /Users/benstull/git/benstull.org/benstull/human-experience-filter-art
|
||||
> Date: 2026-06-30
|
||||
> Goal: Add an obvious "Run simulation" start button to the simulator's main
|
||||
> screen (shown after the "Loading Universe…" preload); it turns Video on and
|
||||
> sets Audio to level 2. Turning Video on directly should also start the
|
||||
> experience, snap Audio to 2, and dismiss the button. Move the default
|
||||
> first-Video-on audio coupling from 3/10 → 2/10.
|
||||
> Outcome: Built + verified (303 pytest, 8 pytest-playwright incl. 3 new, 12
|
||||
> Playwright TS incl. 1 updated + 1 new, node unit tests green). Work committed
|
||||
> as 3a45e33 and pushed to design/cloudflare-static-publish — **the commit was
|
||||
> landed by a CONCURRENT session (0029)** that swept up this session's
|
||||
> working-tree changes while doing the CC-credits/Cloudflare-publish work.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Map the splash/loading flow and the Video/Audio toggle wiring (Explore agent
|
||||
+ direct reads of `simulator/static/{index.html,app.js,style.css,i18n.js}`).
|
||||
2. Add a `#run-sim` button inside the stage `.screen`, hidden until media
|
||||
preloads, styled to sit above the black cover (`z-index: 60`).
|
||||
3. Add a `beginExperience()` helper: Video on + audio snapped to a
|
||||
`START_AUDIO_LEVEL = "2"` + button dismissed + soundtrack played in-gesture
|
||||
(Safari unlock). Wire it to both the button click and the first direct
|
||||
Video-on (`!videoEverOn`).
|
||||
4. Reveal the button after `hideLoading()`; change the old 3/10 coupling → 2/10.
|
||||
5. Localize `run.button` in en/es/fr/ja (4-lang parity invariant).
|
||||
6. Update E2E coverage (pytest-playwright + Playwright TS) from the old
|
||||
"couples to 3" assertions to 2; add button-shown / button-starts /
|
||||
direct-video-on tests.
|
||||
7. Run the full suite + confirm a pre-existing failure is not mine.
|
||||
|
||||
## Pre-session state
|
||||
|
||||
- Branch `main` clean at `934f60c` (left-brain annotations merged). Simulator
|
||||
boots SILENT (video off, audio 0) and waited for the operator to flip the
|
||||
unlabelled **Video** toggle as the start gesture — not obvious to a first-time
|
||||
viewer. First-Video-on coupled audio to **3/10**.
|
||||
- No session-init gate was run at the top of this session — it was treated as a
|
||||
small, fully-specified frontend change (see Deferred decisions).
|
||||
|
||||
## Turn-by-turn arc
|
||||
|
||||
1. **Explore + direct reads.** Dispatched an `Explore` subagent to map the
|
||||
splash + AV-toggle code; in parallel read `index.html`, `app.js`
|
||||
(`main()`/`hideLoading()`/`beginExperience` region, the audio dial, the
|
||||
`videoEverOn` coupling), `style.css` (`#loading`, `.black z-index:50`,
|
||||
`.hidden`), and `i18n.js` (UI_STRINGS shape, 4 languages). Confirmed the
|
||||
stage `.screen` is `position: relative` and the black cover is `z-index: 50`,
|
||||
so a start button needs `z-index > 50` inside the screen and the side panel
|
||||
(Video toggle) stays reachable.
|
||||
2. **Implement.** Added `#run-sim` to the stage; `.run-sim` CSS (centered pill,
|
||||
`z-index: 60`); `run.button` i18n string (en/es/fr/ja); `beginExperience()`
|
||||
unifying both entry paths; revealed the button after `hideLoading()`;
|
||||
simplified the Video-toggle handler to call `beginExperience()` on first
|
||||
on; changed coupling 3 → 2.
|
||||
3. **Update tests.** Replaced the old `test_video_on_couples_audio_to_three…`
|
||||
(pytest) and the "couples audio to 3/10" Playwright TS test with 2/10
|
||||
variants; added button-shown-after-load, button-starts-video+audio, and
|
||||
direct-Video-on-dismisses-button cases on both harnesses.
|
||||
4. **Verify.** `303 passed, 2 skipped` (non-E2E pytest); `8 passed`
|
||||
pytest-playwright (incl. 3 new); `12 passed` Playwright `altitude-lock.spec`
|
||||
(incl. updated + new). Node unit tests (i18n / index-i18n / scrub) green; the
|
||||
i18n completeness invariant confirms `run.button` has all 4 langs.
|
||||
5. **Isolate a failure.** `loop-recovery.spec.ts` failed; stashed only the
|
||||
source files and re-ran on a clean baseline — it **fails identically without
|
||||
my changes**, so it's pre-existing, not introduced here. (During that stash
|
||||
round-trip a `test-results/.last-run.json` artifact briefly blocked
|
||||
`git stash pop`; recovered by `git checkout`-ing the artifact and popping —
|
||||
no source lost.)
|
||||
6. **Handed the working tree back uncommitted** for an operator by-eye review
|
||||
(the project's usual cadence — no Chrome on the box). Offered to branch + PR.
|
||||
|
||||
## Cut state (end of session)
|
||||
|
||||
| Item | State |
|
||||
| --- | --- |
|
||||
| Run-simulation button + audio-2 change | Committed `3a45e33` on `design/cloudflare-static-publish`, **pushed** |
|
||||
| Committed by | Concurrent **session 0029** (swept up this session's working-tree changes) |
|
||||
| Files | `simulator/static/{index.html,app.js,style.css,i18n.js}`, `simulator/e2e/tests/altitude-lock.spec.ts`, `tests/test_e2e_audio.py` |
|
||||
| This session's working tree | Clean; nothing of mine left to commit/push/merge |
|
||||
| Branch `design/cloudflare-static-publish` | Not merged to `main` — intentional; awaits operator review + Cloudflare deploy (session 0029's frontier) |
|
||||
| Deploy pipeline (§9) | **N/A** — project is exempt (static Cloudflare target; see memory `project-exempt-from-wiggleverse-deploy`) |
|
||||
| Verification | 303 pytest + 8 pytest-playwright + 12 Playwright TS + node units green; `loop-recovery.spec.ts` red **pre-existing** |
|
||||
|
||||
## What lands on the operator's plate
|
||||
|
||||
- **By-eye / by-ear review** of the Run-simulation button in a real browser —
|
||||
especially the **in-gesture audio unlock on real Safari/iOS** (headless tests
|
||||
relax autoplay, so it's wired-but-unconfirmed on a real device).
|
||||
- The button work lives on `design/cloudflare-static-publish` alongside the
|
||||
CC-credits + Cloudflare-publish work; it merges to `main` / ships when that
|
||||
branch does (session 0029's frontier), not separately.
|
||||
- Note the concurrency: this session authored the change; session 0029 committed
|
||||
it. Two transcripts (0029, 0030) touch the same single commit `3a45e33` —
|
||||
expected, not a duplicate of the commit.
|
||||
|
||||
## Prompt the operator can paste into the next session
|
||||
|
||||
```
|
||||
/goal operator eyeball review of the static build (incl. the new Run-simulation button) + execute the real Cloudflare deploy to benstull.art/human-experience-simulator, per the cloudflare-static-publish design on branch design/cloudflare-static-publish
|
||||
```
|
||||
|
||||
## Deferred decisions
|
||||
|
||||
_Autonomous-mode low-confidence calls; surfaced at finalize._
|
||||
|
||||
1. **No session-init gate at the top.** Treated the task as a small,
|
||||
fully-specified frontend tweak and implemented directly without claiming a
|
||||
transcript first. Consequence: the work landed on a shared checkout and was
|
||||
committed by a concurrent session (0029) rather than carrying its own
|
||||
end-to-end branch→PR. Alternative: claim a session + isolated worktree up
|
||||
front. Flagged because shared-checkout work is what the worktree rule guards
|
||||
against.
|
||||
2. **`beginExperience()` snaps audio to 2 unconditionally** on first Video-on,
|
||||
overriding a viewer who pre-set a higher level before pressing start /
|
||||
flipping Video. Matched the operator's explicit "snap to audio level 2"
|
||||
wording; alternative was to only raise from 0 (the prior 3/10 behavior). Easy
|
||||
to soften to "only if currently 0" if undesired.
|
||||
3. **Real-device audio autoplay unverified.** Headless Chromium ran with
|
||||
`--autoplay-policy=no-user-gesture-required`, masking real Safari/iOS
|
||||
behavior; the in-gesture unlock is wired but needs a by-ear check.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Session 0031.0 — Transcript
|
||||
|
||||
> App: human-experience-filter-art
|
||||
> Start: 2026-06-30T09-06 (PST)
|
||||
> End: 2026-06-30T09-11 (PST)
|
||||
> Type: planning-and-executing
|
||||
> Posture: yolo
|
||||
> Claude-Session: c87a07d3-917c-44bc-82cf-c6d8edee952b
|
||||
> Checkout: /Users/benstull/git/benstull.org/benstull/human-experience-filter-art
|
||||
> Branch: design/cloudflare-static-publish
|
||||
> Status: **FINALIZED**
|
||||
|
||||
## Launch prompt
|
||||
|
||||
Follow-up to session 0029's credits work. Operator's first ask was to rename the
|
||||
credits URL → "about" (one page holding both about + credits) and add an
|
||||
Accessibility table. Mid-clarification the operator redirected: **keep About and
|
||||
Credits separate** (the About page is owned by a parallel session) and simply
|
||||
**"Remove the 'About this work' from the credits page. I'll merge that into the
|
||||
About page on the other session."**
|
||||
|
||||
## What I found before acting
|
||||
|
||||
- The Cloudflare site is now **LIVE** (`benstull.art/human-experience-simulator`);
|
||||
the branch had advanced several commits since s0029 (live deploy, progressive
|
||||
boot, an audio mediaBase fix), and moved again mid-session (`a68d0bc`→`d10ffca`).
|
||||
- A **live concurrent session** owns `feat/accessibility-pass` with an
|
||||
approved-pending design spec (`docs/superpowers/specs/2026-06-30-accessibility-and-about-page-design.md`):
|
||||
a full WCAG 2.1 AA pass + a **separate `about.html`**. I surfaced the overlap with
|
||||
the operator's original (one-page) ask; the operator resolved it by keeping the
|
||||
pages separate and narrowing my task to the section removal.
|
||||
|
||||
## Arc
|
||||
|
||||
1. Surveyed git + the concurrent worktrees; read the a11y design spec.
|
||||
2. Started to raise a coordination question; operator interrupted with the narrow
|
||||
instruction (remove "About this work" from credits; they move it to About).
|
||||
3. Claimed **session 0031**, isolated worktree off the live branch tip (`d10ffca`)
|
||||
— only `credits.html` + its tests touched, so no overlap with the a11y session's
|
||||
files (`index.html`/`style.css`/`app.js`/`about.html`).
|
||||
4. Removed the `<section class="declaration">` "About this work" block from
|
||||
`credits.html`; rewrote the footage note to be self-contained ("All footage is
|
||||
altered in real time in the experience" — keeps the per-clip modification cue,
|
||||
drops the dangling "as described above").
|
||||
5. Updated tests to the new intent: `credits.test.js` and `tests/test_e2e_credits.py`
|
||||
now assert the page notes footage is altered but the **CC BY-SA 4.0 declaration is
|
||||
NOT** on credits (it moved to About).
|
||||
6. Left the `.declaration` CSS rule in `style.css` (the About page reuses these styles).
|
||||
7. Verified: node 28/28, pytest 315 passed / 2 skipped. Committed `8277417`,
|
||||
ff-merged + pushed, removed the worktree, deleted the branch.
|
||||
|
||||
## End state
|
||||
|
||||
- `design/cloudflare-static-publish` @ `8277417`, clean, pushed.
|
||||
- Credits page = per-clip attributions + "footage altered" note + audio courtesy
|
||||
block. The CC BY-SA work-license declaration (D1) is **no longer on credits** —
|
||||
it now belongs on the About page (operator + `feat/accessibility-pass` own it).
|
||||
- Site is live; this change reaches `benstull.art` only after a **redeploy**.
|
||||
|
||||
## Deferred decisions
|
||||
|
||||
- **Kept a brief "footage is altered" note on the credits page** rather than
|
||||
stripping all alteration mention. Rationale: CC BY/BY-SA attribution wants the
|
||||
modification indicated alongside the credit; the full license declaration is what
|
||||
moved to About. (Alternative: remove every alteration mention from credits —
|
||||
judged worse for the per-clip attribution's "indicate changes" cue.)
|
||||
- **Left `.declaration` CSS in `style.css`** though now unused on credits — the
|
||||
About page (other session) will reuse it; removing it would break that page.
|
||||
|
||||
## Compliance note
|
||||
|
||||
D1 (the altered-work CC BY-SA declaration) now depends on the About page actually
|
||||
carrying it. **Confirm it landed on about.html before considering D1 done** — it is
|
||||
currently in neither place on the live site until the About page ships + a redeploy.
|
||||
|
||||
## Next /goal
|
||||
|
||||
```
|
||||
/goal confirm the About page (feat/accessibility-pass) carries the CC BY-SA 4.0 work-license declaration (D1, moved off credits in s0031), then redeploy design/cloudflare-static-publish to benstull.art per deploy/cloudflare/README.md
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
# Session 0032.0 — Transcript
|
||||
|
||||
> App: human-experience-filter-art
|
||||
> Start: 2026-06-30T09-20 (PST)
|
||||
> End: 2026-06-30T09-24 (PST)
|
||||
> Type: planning-and-executing
|
||||
> Posture: yolo
|
||||
> Claude-Session: c87a07d3-917c-44bc-82cf-c6d8edee952b
|
||||
> Checkout: /Users/benstull/git/benstull.org/benstull/human-experience-filter-art
|
||||
> Branch: design/cloudflare-static-publish
|
||||
> Status: **FINALIZED**
|
||||
|
||||
## Launch prompt
|
||||
|
||||
After confirming that the "About this work" section removed in s0031 *was* the D1
|
||||
compliance (the CC BY-SA share-alike declaration, not the per-clip attribution),
|
||||
the operator: "Ok let's add that content back but can we name the section something
|
||||
other than 'About This Work' so it isn't confused with the about page?"
|
||||
|
||||
## Arc
|
||||
|
||||
1. Confirmed for the operator that "About this work" carried D1 specifically — the
|
||||
ShareAlike license declaration for the BY-SA source clips — while the per-clip
|
||||
attribution (the original goal) was untouched and stayed on credits.
|
||||
2. Claimed **session 0032**, isolated worktree off the branch tip (`8277417`).
|
||||
3. Re-added the CC BY-SA 4.0 declaration to `credits.html` under the heading
|
||||
**"License & reuse"** (deliberately not "About this work" — that name is reserved
|
||||
for the separate About page from `feat/accessibility-pass`). Restored the footage
|
||||
note's "altered as described above" now that the section is back above it. Reused
|
||||
the `.declaration` CSS left in place by s0031.
|
||||
4. Reverted the s0031 test changes: `credits.test.js` + `tests/test_e2e_credits.py`
|
||||
now assert the declaration IS present, plus pin the rename (heading ≠ "About this
|
||||
work", contains "License & reuse").
|
||||
5. Verified: node 28/28, pytest 315 passed / 2 skipped (e2e confirms "CC BY-SA 4.0"
|
||||
visible in-browser again). Committed `ee7c65a`, ff-merged + pushed, removed the
|
||||
worktree, deleted the branch.
|
||||
|
||||
## End state
|
||||
|
||||
- `design/cloudflare-static-publish` @ `ee7c65a`, clean, pushed.
|
||||
- Credits page now carries BOTH halves of CC compliance: per-clip CC-BY/BY-SA
|
||||
attribution **and** the CC BY-SA 4.0 derivative declaration (under "License &
|
||||
reuse"). The s0031 gap (live site missing the share-alike declaration) is closed.
|
||||
- Site is live; reaches `benstull.art` only after a **redeploy**.
|
||||
|
||||
## Deferred decisions
|
||||
|
||||
- **Section heading "License & reuse"** chosen (operator delegated the name). It pairs
|
||||
with the "Credits & licenses" page title and is unambiguous vs. the About page.
|
||||
(Alternatives weighed: "This work's license", "License & alteration".)
|
||||
|
||||
## Next /goal
|
||||
|
||||
```
|
||||
/goal redeploy design/cloudflare-static-publish to benstull.art (CC-BY/BY-SA credits + "License & reuse" D1 declaration now both on the credits page) per deploy/cloudflare/README.md; the About page lands separately via the feat/accessibility-pass session
|
||||
```
|
||||
@@ -79,5 +79,20 @@
|
||||
},
|
||||
"0027": {
|
||||
"title": ""
|
||||
},
|
||||
"0028": {
|
||||
"title": ""
|
||||
},
|
||||
"0029": {
|
||||
"title": ""
|
||||
},
|
||||
"0030": {
|
||||
"title": ""
|
||||
},
|
||||
"0031": {
|
||||
"title": ""
|
||||
},
|
||||
"0032": {
|
||||
"title": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,38 @@ test("a landed clip loops from the morph-tail offset (no jump-back to frame 0)",
|
||||
expect(v.t).toBeGreaterThanOrEqual(2.5); // looping from ~3s, not 0
|
||||
});
|
||||
|
||||
test("ascending re-lands anchored at the clip head (no reverse-landing jump)", async ({ page }) => {
|
||||
// D3: a morph spans src@0 (frac 0) -> dst@~3s (frac 1). Descending lands on dst at its
|
||||
// tail (~3s); ASCENDING lands on src at its HEAD (~0s). The steady loop must continue
|
||||
// from 0 there — seeking to the tail (~3s) was the ~3s reverse-landing jump.
|
||||
await boot(page);
|
||||
await enableVideo(page); // base loop only loads when video is shown
|
||||
// Descend one altitude (cosmos -> orbit), then ascend back (orbit -> cosmos): the
|
||||
// ascent is the reverse landing under test.
|
||||
const start = (await page.locator("#scale-name").textContent())!;
|
||||
await wheelOnStage(page, 60); // descend
|
||||
await page.waitForFunction((s) => document.querySelector("#scale-name")?.textContent !== s, start, { timeout: 20000 });
|
||||
const mid = (await page.locator("#scale-name").textContent())!;
|
||||
expect(mid).toContain("orbit");
|
||||
await wheelOnStage(page, -60); // ascend back
|
||||
await page.waitForFunction((s) => document.querySelector("#scale-name")?.textContent !== s, mid, { timeout: 20000 });
|
||||
expect(await page.locator("#scale-name").textContent()).toContain("cosmos");
|
||||
// The loop element is now anchored at the HEAD (loopStart "0"), playing — not jumped to
|
||||
// the ~3s tail the descending case uses.
|
||||
await page.waitForFunction(
|
||||
() => { const v = document.querySelector("#vid-loop") as HTMLVideoElement; return v.dataset.loopTail === "1" && v.dataset.loopStart === "0"; },
|
||||
null,
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
const v = await page.evaluate(() => {
|
||||
const el = document.querySelector("#vid-loop") as HTMLVideoElement;
|
||||
return { loopStart: el.dataset.loopStart, loopTail: el.dataset.loopTail, paused: el.paused };
|
||||
});
|
||||
expect(v.loopTail).toBe("1"); // tail-loop machinery armed
|
||||
expect(v.loopStart).toBe("0"); // anchored at the head, not the morph tail
|
||||
expect(v.paused).toBe(false); // the loop is actually playing
|
||||
});
|
||||
|
||||
test("zoom in plays the matching morph, lands locked, and stays put while parked", async ({ page }) => {
|
||||
await boot(page);
|
||||
const start = (await page.locator("#scale-name").textContent())!;
|
||||
|
||||
+49
-20
@@ -238,20 +238,42 @@ function preloadChip() {
|
||||
// to the loop's first frame, and the morph-covered intro never re-shows at rest.
|
||||
const LOOP_TAIL_S = 3.0;
|
||||
|
||||
// Load a clip's base into the LOOP element (#vid-loop), seeked to the tail and PAUSED
|
||||
// on that exact frame. Idempotent per clip. Called during a scrub to PRELOAD the clip
|
||||
// we're about to land on, so settle just plays an already-decoded element at the morph's
|
||||
// last frame — no reload, no seek, no stall. `playLoop()` starts it at landing.
|
||||
function loadLoop(clip) {
|
||||
if (!clip || loopVid.dataset.clip === clip.id) return;
|
||||
// The frame the current loop both LANDS on and WRAPS back to — stored per load so the
|
||||
// wrap handlers honor a direction-aware landing (forward=LOOP_TAIL_S, reverse=0).
|
||||
function loopStartFrame() {
|
||||
const s = parseFloat(loopVid.dataset.loopStart);
|
||||
return Number.isFinite(s) ? s : LOOP_TAIL_S;
|
||||
}
|
||||
|
||||
// Load a clip's base into the LOOP element (#vid-loop), seeked to `landFrame` and PAUSED
|
||||
// on that exact frame. `landFrame` is the frame the morph last showed for THIS landing:
|
||||
// LOOP_TAIL_S when descending (lands on the morph's dst-tail), 0 when ascending (lands on
|
||||
// the morph's src-head — fixes the D3 reverse-landing jump). Omitted → LOOP_TAIL_S, the
|
||||
// forward default for non-scrub paths (initial load, knob changes). Idempotent per clip;
|
||||
// an explicit `landFrame` re-seeks the already-loaded clip if the travel direction flipped
|
||||
// the landing phase, but a bare call (ensureClipMedia) never clobbers a phase a scrub set.
|
||||
// Called during a scrub to PRELOAD the clip we're about to land on, so settle just plays
|
||||
// an already-decoded element at the right frame — no reload, no seek, no stall.
|
||||
function loadLoop(clip, landFrame) {
|
||||
if (!clip) return;
|
||||
const seekTo = (t) => { try { loopVid.currentTime = t; } catch (e) { /* not seekable yet */ } };
|
||||
if (loopVid.dataset.clip === clip.id) {
|
||||
if (landFrame != null && loopVid.dataset.loopStart !== String(landFrame)) {
|
||||
loopVid.dataset.loopStart = String(landFrame);
|
||||
if (loopVid.readyState >= 1) seekTo(landFrame);
|
||||
else loopVid.addEventListener("loadedmetadata", () => seekTo(landFrame), { once: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
const lf = landFrame == null ? LOOP_TAIL_S : landFrame;
|
||||
loopVid.dataset.clip = clip.id;
|
||||
loopVid.dataset.loopTail = "1"; // arm the [LOOP_TAIL_S, duration] wrap handler
|
||||
loopVid.dataset.loopTail = "1"; // arm the [loopStart, duration] wrap handler
|
||||
loopVid.dataset.loopStart = String(lf);
|
||||
loopVid.src = mediaUrl(clip.base_file);
|
||||
loopVid.loop = false; // native loop restarts at 0; we loop from the tail instead
|
||||
loopVid.loop = false; // native loop restarts at 0; we loop from loopStart
|
||||
loopVid.muted = true;
|
||||
const seek = () => { try { loopVid.currentTime = LOOP_TAIL_S; } catch (e) { /* not seekable yet */ } };
|
||||
if (loopVid.readyState >= 1) seek();
|
||||
else loopVid.addEventListener("loadedmetadata", seek, { once: true });
|
||||
if (loopVid.readyState >= 1) seekTo(lf);
|
||||
else loopVid.addEventListener("loadedmetadata", () => seekTo(lf), { once: true });
|
||||
}
|
||||
|
||||
function playLoop() {
|
||||
@@ -267,21 +289,22 @@ function ensureClipMedia() {
|
||||
}
|
||||
|
||||
// The custom loop on the LOOP element: when a base loop reaches the end, jump back to
|
||||
// the tail offset (not 0). The morph element (#vid) is driven directly by the scrub.
|
||||
// its landing offset (LOOP_TAIL_S forward, 0 reverse — see loopStartFrame), not a fixed
|
||||
// tail. The morph element (#vid) is driven directly by the scrub.
|
||||
loopVid.addEventListener("timeupdate", () => {
|
||||
if (loopVid.dataset.loopTail === "1" && loopVid.duration && loopVid.currentTime >= loopVid.duration - 0.08) {
|
||||
try { loopVid.currentTime = LOOP_TAIL_S; } catch (e) { /* not seekable */ }
|
||||
try { loopVid.currentTime = loopStartFrame(); } catch (e) { /* not seekable */ }
|
||||
}
|
||||
});
|
||||
|
||||
// Safety net: the timeupdate wrap above is a narrow window (the last 0.08 s). Under
|
||||
// real-browser decode/GPU load a `timeupdate` can skip past it, the clip fires
|
||||
// `ended`, and — with native loop off — freezes on its last frame ("not looping").
|
||||
// `ended` GUARANTEES recovery: seek back to the tail and resume. Headless rarely
|
||||
// hits this; loaded real machines do.
|
||||
// `ended` GUARANTEES recovery: seek back to the landing offset and resume. Headless
|
||||
// rarely hits this; loaded real machines do.
|
||||
loopVid.addEventListener("ended", () => {
|
||||
if (loopVid.dataset.loopTail !== "1") return;
|
||||
try { loopVid.currentTime = LOOP_TAIL_S; } catch (e) { /* not seekable */ }
|
||||
try { loopVid.currentTime = loopStartFrame(); } catch (e) { /* not seekable */ }
|
||||
loopVid.play().catch(() => {});
|
||||
});
|
||||
|
||||
@@ -835,7 +858,12 @@ function setPos(next) {
|
||||
busy = false;
|
||||
delete vid.dataset.morph; // clear so re-entering the same segment reloads the morph (not the base)
|
||||
currentClipId = null; // force ensureClipMedia to reload + loop the locked clip
|
||||
playLoop(); // the loop element was preloaded to this clip @ tail during the scrub → instant, no reload
|
||||
// Land the loop on the frame the morph last showed for THIS travel direction:
|
||||
// descending lands on the morph's dst-tail (LOOP_TAIL_S), ascending on its src-head
|
||||
// (0). Explicit (not relying on the scrub's preload) so a reverse landing never
|
||||
// jumps even if the heading clip wasn't warmed in time. (D3 fix.)
|
||||
loadLoop(activeClip(), HEFScrub.loopLandFrame(scrubDir, LOOP_TAIL_S));
|
||||
playLoop(); // the loop element was preloaded to this clip during the scrub → instant, no reload
|
||||
showActiveSource();
|
||||
setNeedle(ringIndex * dialStep());
|
||||
renderScaleReadout();
|
||||
@@ -846,10 +874,11 @@ function setPos(next) {
|
||||
}
|
||||
busy = true; // mid-morph: block update() from reloading the base clip under us
|
||||
if (!activeSeg || activeSeg.lo !== lo) rebuildSegment(lo, ringIndex);
|
||||
// Preload the clip we're heading toward into the LOOP element (paused @ tail) so the
|
||||
// landing is a swap, not a reload+seek. dir>0 lands on clipHi, dir<0 on clipLo.
|
||||
// Preload the clip we're heading toward into the LOOP element (paused on its landing
|
||||
// frame) so the landing is a swap, not a reload+seek. dir>0 lands on clipHi at the
|
||||
// morph's dst-tail (LOOP_TAIL_S); dir<0 lands on clipLo at the morph's src-head (0).
|
||||
const headingId = dir > 0 ? activeSeg.clipHi : activeSeg.clipLo;
|
||||
loadLoop(clipsById[headingId]);
|
||||
loadLoop(clipsById[headingId], HEFScrub.loopLandFrame(dir, LOOP_TAIL_S));
|
||||
showActiveSource(); // morph element is the live source while scrubbing
|
||||
setNeedle(pos * dialStep());
|
||||
blendAudio(lo, frac);
|
||||
|
||||
@@ -29,6 +29,18 @@
|
||||
return { from: 1 - f, to: f };
|
||||
}
|
||||
|
||||
// The base-loop frame a landing must continue from so the steady loop picks up
|
||||
// the EXACT frame the morph last showed — no jump. A morph spans src@0 (frac 0)
|
||||
// -> dst@loopTailS (frac 1), scrubbed bidirectionally. Descending (dir>0) lands
|
||||
// on dst at frac 1, whose frame is dst@loopTailS, so the loop resumes at loopTailS
|
||||
// (and the morph-covered intro never re-shows). Ascending (dir<0) lands on src at
|
||||
// frac 0, whose frame is src@0, so the loop must resume at 0 — seeking to loopTailS
|
||||
// there is the ~3s reverse-landing jump (D3). dir 0 defaults to the forward/tail
|
||||
// case (matches the loadLoop default used by non-scrub paths).
|
||||
function loopLandFrame(dir, loopTailS) {
|
||||
return dir < 0 ? 0 : loopTailS;
|
||||
}
|
||||
|
||||
// Integers strictly crossed moving prevPos -> pos, in travel order. Landing
|
||||
// exactly on an integer counts as crossing it (it commits that altitude).
|
||||
function integerCrossings(prevPos, pos) {
|
||||
@@ -41,5 +53,5 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
return { clamp01, wrapIndex, segmentOf, accumToPos, fracToTime, crossfadeGains, integerCrossings };
|
||||
return { clamp01, wrapIndex, segmentOf, accumToPos, fracToTime, crossfadeGains, loopLandFrame, integerCrossings };
|
||||
});
|
||||
|
||||
@@ -58,3 +58,17 @@ test("integerCrossings: multiple crossings in travel order", () => {
|
||||
assert.deepEqual(S.integerCrossings(2.4, 4.1), [{ index: 3, dir: 1 }, { index: 4, dir: 1 }]);
|
||||
assert.deepEqual(S.integerCrossings(3.2, 1.8), [{ index: 3, dir: -1 }, { index: 2, dir: -1 }]);
|
||||
});
|
||||
|
||||
test("loopLandFrame: descend lands on the morph's tail frame, ascend on its head", () => {
|
||||
// A morph spans src@0 (frac 0) -> dst@loopTailS (frac 1). Descending (dir>0) lands
|
||||
// on dst at frac 1 -> the base loop must continue from loopTailS. Ascending (dir<0)
|
||||
// lands on src at frac 0 -> the loop must continue from 0 (NOT loopTailS — that was
|
||||
// the D3 reverse-landing jump).
|
||||
assert.equal(S.loopLandFrame(1, 3), 3); // descend -> tail
|
||||
assert.equal(S.loopLandFrame(-1, 3), 0); // ascend -> head
|
||||
// dir 0 (no travel) is treated as a forward/tail landing (the default everywhere else).
|
||||
assert.equal(S.loopLandFrame(0, 3), 3);
|
||||
// honors any tail value
|
||||
assert.equal(S.loopLandFrame(1, 2.5), 2.5);
|
||||
assert.equal(S.loopLandFrame(-1, 2.5), 0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user