diff --git a/docs/superpowers/plans/2026-06-27-altitude-clip-pair-morph-lock.md b/docs/superpowers/plans/2026-06-27-altitude-clip-pair-morph-lock.md index 6b3bd2a..84cf9b4 100644 --- a/docs/superpowers/plans/2026-06-27-altitude-clip-pair-morph-lock.md +++ b/docs/superpowers/plans/2026-06-27-altitude-clip-pair-morph-lock.md @@ -822,6 +822,33 @@ test("zoom plays a morph, lands locked, and does not change while parked", async await page.waitForTimeout(2000); expect(await readout.textContent()).toBe(landed); }); + +test("zooming back OUT plays a reverse morph and lands locked", async ({ page }) => { + const morphs: string[] = []; + page.on("request", (r) => { + const m = r.url().match(/\/media\/(transitions\/[^?]+)/); + if (m) morphs.push(m[1]); + }); + await page.addInitScript(() => localStorage.setItem("hef.devMode", "1")); + await page.goto("/"); + const readout = page.locator("#scale-name"); + await expect(readout).toContainText("cosmos"); + + // Zoom inward one altitude, then back OUT (wheel up = -). The outward move must + // play a `.rev` morph and land back on cosmos, locked. + await page.locator("#stage").dispatchEvent("wheel", { deltaY: 120 }); + await page.waitForTimeout(2500); + morphs.length = 0; // only watch the zoom-out move + await page.locator("#stage").dispatchEvent("wheel", { deltaY: -120 }); + await page.waitForTimeout(2500); + expect(morphs.some((f) => /transitions\/cosmos__.*\.rev\.mp4/.test(f))).toBeTruthy(); + await expect(readout).toContainText("cosmos"); + + // Locked after the outward move too. + const landed = (await readout.textContent())!; + await page.waitForTimeout(2000); + expect(await readout.textContent()).toBe(landed); +}); ``` - [ ] **Step 3: Install + run** @@ -898,7 +925,7 @@ git commit -m "chore(media): regenerate 154 clip-pair morphs + manifest" **Type consistency:** `Transition(from_clip, to_clip, file, model)` (T3) used by `morph_for` (T3), `load_ring` (T3), `resolve_move` (T4), `resolved_move_to_dict` (T6). `MorphStep`/`ResolvedMove` fields (T4) match the serializer (T6) and the renderer's `step.{from_clip,to_clip,file,to_index,blended}` + `move.target_clip_id` (T7). `morphByPair` key `"from→to"` consistent in build (T7 S1) and lookup (T7 S4). -**Open items flagged for the review gate:** -1. **Storage/repo weight:** 154 short mp4s in `sample_media`. They're small, but confirm we're comfortable committing them (vs. a generate-on-setup step). Current repo already commits the 5 + bases, so this extends precedent. -2. **`build_pool_manifest.py` CLI:** Task 9 Step 1 assumes a `--media` flag; confirm the actual entrypoint flags during execution. -3. **Zoom-out E2E:** Task 8 covers zoom-in + lock; a zoom-out assertion is a cheap add if wanted. +**Open items (resolved at the review gate by the operator):** +1. **Storage/repo weight:** ✅ Commit the 154 short mp4s into `sample_media` (extends the existing precedent of committing the 5 + bases). +2. **`build_pool_manifest.py` CLI:** ✅ Approach approved; confirm the actual entrypoint flag during execution (Task 9 Step 1). +3. **Zoom-out E2E:** ✅ Added — Task 8 now covers zoom-in + lock **and** a zoom-out (`.rev` morph) + lock case.