diff --git a/simulator/e2e/tests/mood-grade.spec.ts b/simulator/e2e/tests/mood-grade.spec.ts new file mode 100644 index 0000000..1f97027 --- /dev/null +++ b/simulator/e2e/tests/mood-grade.spec.ts @@ -0,0 +1,46 @@ +import { test, expect, Page } from "@playwright/test"; + +// Bug: the Dark/Light Mood grade was dropped DURING an altitude morph — the morph +// showed ungraded ("normal") and the grade snapped back only on landing. Both render +// paths were at fault: the WebGL canvas set filter "none" while `busy`, and the +// fallback set #vid's filter "none" when a morph loaded. The grade must ride through. + +async function boot(page: Page) { + await page.goto("/"); + await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); + await page.locator("#welcome-launch").click(); + await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); + await expect(page.locator("#scale-name")).toBeVisible(); +} + +test("Dark Mood grade stays applied to the morph while scrubbing", async ({ page }) => { + await boot(page); + // Drive Mood hard dark; wait for the debounced render to build the grade. + await page.evaluate(() => { + const m = document.querySelector("#mood") as HTMLInputElement; + m.value = "-4"; + m.dispatchEvent(new Event("input", { bubbles: true })); + }); + await page.waitForTimeout(300); + + // Begin a drag → a morph plays (busy = true). Sample the VISIBLE morph surface: + // the paint canvas (WebGL path) or #vid (fallback). #vid-loop is hidden mid-morph, + // so we deliberately exclude it. + const box = (await page.locator("#dial").boundingBox())!; + const cx = box.x + box.width / 2, cy = box.y + box.height / 2; + await page.mouse.move(cx, cy - 30); + await page.mouse.down(); + await page.mouse.move(cx + 18, cy - 24, { steps: 8 }); + await page.waitForFunction(() => (document.querySelector("#vid") as HTMLVideoElement).currentTime > 0, null, { timeout: 5000 }); + + const filters = await page.evaluate(() => [ + (document.querySelector("#paint") as HTMLElement).style.filter, + (document.querySelector("#vid") as HTMLElement).style.filter, + ]); + await page.mouse.up(); + + // Dark grade is brightness(<1)… — at least one visible surface must carry it. + // Before the fix both were "none" here. + const graded = filters.some((f) => /brightness\(/.test(f) && !/brightness\(1(\.0+)?\)/.test(f)); + expect(graded, `visible morph surface must carry the dark grade; saw ${JSON.stringify(filters)}`).toBe(true); +}); diff --git a/simulator/static/app.js b/simulator/static/app.js index 18004dc..1a0ce84 100644 --- a/simulator/static/app.js +++ b/simulator/static/app.js @@ -451,11 +451,13 @@ function paintLoop() { if (paint.width !== w || paint.height !== h) { paint.width = w; paint.height = h; } gl.viewport(0, 0, w, h); try { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, src); } catch (_) {} - // No painterly restyle during a ring transition (busy) — show it raw. + // No painterly DREAM during a ring transition (busy) — show the morph crisp. + // The Dark/Light mood GRADE, however, must ride through the transition too + // (else the morph shows ungraded and the grade snaps back only on landing). gl.uniform1f(uAmt, busy ? 0 : dreamIntensity); gl.uniform2f(uTexel, 1 / w, 1 / h); gl.drawArrays(gl.TRIANGLES, 0, 3); - paint.style.filter = busy ? "none" : gradeFilter; + paint.style.filter = gradeFilter; } // Animate per-frame: re-place keyframed tracks AND re-evaluate time-windowed // labels (a label enters/leaves frame as the loop plays) against playback time. @@ -983,7 +985,10 @@ function rebuildSegment(lo, enteredFrom) { overlay.style.opacity = "0"; affectLayer.style.opacity = "0"; tint.style.opacity = "0"; - vid.style.filter = "none"; + // WebGL path: the graded canvas sits on top, so #vid stays raw (no double-grade). + // Fallback path: #vid IS the visible surface, so it must carry the mood grade + // through the morph (setting "none" here was the "morph ignores Mood" bug). + vid.style.filter = paintOK ? "none" : gradeFilter; vid.loop = false; if (vid.dataset.morph !== file) { vid.dataset.morph = file;