[object Object]

← back to Quadrille Showroom

Age View Phase-5.5 (contrarian pass): coalesce avatar rebuild, tighten in-page gate, capture screenshots

bb091f8be641e9105e24f73cbb30c80b2fddfb2a · 2026-07-01 10:01:10 -0700 · Steve

Contrarian final gate found three real holes; fixed the two code ones:
 1. setAvatarAge() rebuilt the whole primitive avatar on EVERY slider input tick →
    dozens of full teardown/rebuilds per second mid-drag (the real FPS-gate failure
    mechanism). Now coalesced to ≤1 rebuild/animation-frame via coalesceAvatarAge();
    buildAvatar() stays synchronous + byte-for-byte (boot + OFF-reset call it directly).
 2. In-page seniorGate() used coarse type-size steps and never checked HEAD size — a
    softer bar than the spec, so its 'all-pass' would've been green even before the
    band type-size fix. Retightened to the interpolated 65→85 bar incl. heads, matching
    verify-ageview-gate.mjs. Still all-pass (the bands genuinely meet the stricter bar).
 3. Cross-engine harness now captures a screenshot per engine/age (was counters-only).

Re-ran both verifiers post-edit (validation-loop): static gate ALL 10 PASS; cross-engine
PASS on Chromium/Firefox/WebKit, 0 console errors, tightened seniorGate all-pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit bb091f8be641e9105e24f73cbb30c80b2fddfb2a
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 1 10:01:10 2026 -0700

    Age View Phase-5.5 (contrarian pass): coalesce avatar rebuild, tighten in-page gate, capture screenshots
    
    Contrarian final gate found three real holes; fixed the two code ones:
     1. setAvatarAge() rebuilt the whole primitive avatar on EVERY slider input tick →
        dozens of full teardown/rebuilds per second mid-drag (the real FPS-gate failure
        mechanism). Now coalesced to ≤1 rebuild/animation-frame via coalesceAvatarAge();
        buildAvatar() stays synchronous + byte-for-byte (boot + OFF-reset call it directly).
     2. In-page seniorGate() used coarse type-size steps and never checked HEAD size — a
        softer bar than the spec, so its 'all-pass' would've been green even before the
        band type-size fix. Retightened to the interpolated 65→85 bar incl. heads, matching
        verify-ageview-gate.mjs. Still all-pass (the bands genuinely meet the stricter bar).
     3. Cross-engine harness now captures a screenshot per engine/age (was counters-only).
    
    Re-ran both verifiers post-edit (validation-loop): static gate ALL 10 PASS; cross-engine
    PASS on Chromium/Firefox/WebKit, 0 console errors, tightened seniorGate all-pass.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 public/js/ageview.js                   | 11 ++++++++++-
 public/js/showroom.js                  | 17 ++++++++++++++++-
 scripts/verify-ageview-crossengine.mjs |  4 +++-
 3 files changed, 29 insertions(+), 3 deletions(-)

diff --git a/public/js/ageview.js b/public/js/ageview.js
index d590333..5c31394 100644
--- a/public/js/ageview.js
+++ b/public/js/ageview.js
@@ -312,18 +312,27 @@ function setOn(v) {
 // OPAQUE band hex pairs + reports type/target sizes per the spec thresholds. ----
 function seniorGate() {
   const out = {};
+  // Type-size requirement is INTERPOLATED across the spec anchors 65→85 (body 20→28,
+  // head 30→44) and the check includes HEAD — matching scripts/verify-ageview-gate.mjs.
+  // (Prior version used coarse steps + skipped heads = a softer bar than the spec;
+  //  tightened 2026-07-01 after the contrarian pass.)
+  const reqBody = a => Math.ceil(20 + (Math.min(a, 85) - 65) / 20 * 8);
+  const reqHead = a => Math.ceil(30 + (Math.min(a, 85) - 65) / 20 * 14);
+  const reqBtn  = a => (a >= 85 ? 56 : 48);
   [65, 75, 85].forEach(a => {
     const b = bandFor(a);
     const bodyLc = apcaLc(b.fg, b.panel);
     const ctaLc  = apcaLc(b.ctaFg, b.cta);
+    const rb = reqBody(a), rh = reqHead(a), rk = reqBtn(a);
     out[a] = {
       band: b.id, opaque: !!b.opaque,
       bgPair: { panel: b.panel, fg: b.fg }, ctaPair: { cta: b.cta, ctaFg: b.ctaFg },
       bodyLc, ctaLc,
       bodyAbs: Math.abs(bodyLc), ctaAbs: Math.abs(ctaLc),
       type: b.body, head: b.head, target: b.btn,
+      reqBody: rb, reqHead: rh, reqBtn: rk,
       pass: Math.abs(bodyLc) >= 60 && Math.abs(ctaLc) >= 75 && b.opaque &&
-            b.body >= (a >= 85 ? 28 : 20) && b.btn >= (a >= 85 ? 56 : 48)
+            b.body >= rb && b.head >= rh && b.btn >= rk
     };
   });
   // The slider control itself at age 85 (contrarian #2): persistent on-screen, so it's
diff --git a/public/js/showroom.js b/public/js/showroom.js
index d97c775..1cb62de 100644
--- a/public/js/showroom.js
+++ b/public/js/showroom.js
@@ -1119,6 +1119,21 @@ function ageRigParams(age) {
 let avatarAge = 28;            // current avatar age; 28 == today's rig
 let avatarUpperRig = null;     // named sub-group (torso+neck+shoulders+arms) — the stoop pivot
 
+// Age View drags the slider continuously (one 'input' per pixel), and setAvatarAge does a
+// FULL primitive teardown+rebuild — unthrottled that's dozens of rebuilds/sec + GC churn
+// mid-drag (the real FPS-gate failure mode; contrarian 2026-07-01). Coalesce rapid
+// setAvatarAge calls to at most ONE rebuild per animation frame. buildAvatar() itself stays
+// synchronous + byte-for-byte identical (boot + OFF-reset paths call it directly).
+let _avatarAgePending = null, _avatarAgeRaf = 0;
+function coalesceAvatarAge(age) {
+  _avatarAgePending = Math.max(12, Math.min(85, age));
+  if (_avatarAgeRaf) return;
+  _avatarAgeRaf = requestAnimationFrame(() => {
+    _avatarAgeRaf = 0;
+    if (_avatarAgePending != null) { const a = _avatarAgePending; _avatarAgePending = null; buildAvatar(a); }
+  });
+}
+
 function buildAvatar(age) {
   // Clean rebuild safety (rebuilds shouldn't stack bodies)
   if (avatarRig) { scene.remove(avatarRig); avatarRig = null; }
@@ -5145,7 +5160,7 @@ window._qh = {
   // setEnvForAge does CHEAP scalar/colour env tweaks only (exposure + ambient/hemi/key
   // intensity + tint) — no geometry/layout change, so FPS is untouched (contrarian #5).
   // resetEnv restores the baseline gallery light.
-  setAvatarAge(age) { buildAvatar(age); },
+  setAvatarAge(age) { coalesceAvatarAge(age); },
   get avatarAge() { return avatarAge; },
   ageRigParams,
   upperRigState() {
diff --git a/scripts/verify-ageview-crossengine.mjs b/scripts/verify-ageview-crossengine.mjs
index a3b8fd7..be1ec42 100644
--- a/scripts/verify-ageview-crossengine.mjs
+++ b/scripts/verify-ageview-crossengine.mjs
@@ -36,7 +36,9 @@ for(const [name,launcher] of engines){
         wings:(window._wingBoards&&window._wingBoards.length)||0,
         bodyClass:document.body.className
       }));
-      console.log(`  age ${String(age).padStart(2)} → readout ${snap.shownAge} · band "${snap.band}" · wings ${snap.wings} · fps ${snap.fps}`);
+      const shot=`recordings/ageview-${name}-age${age}.png`;
+      await page.screenshot({path:shot});
+      console.log(`  age ${String(age).padStart(2)} → readout ${snap.shownAge} · band "${snap.band}" · wings ${snap.wings} · fps ${snap.fps} · shot ${shot}`);
       if(snap.wings<1){ console.log(`    ✗ wing wall empty at age ${age}`); hardFails++; }
     }
     const gate=await page.evaluate(()=>window._ageview.seniorGate());

← d4295bd Age View Phase-5.3: cross-engine render gate (Chromium/Firef  ·  back to Quadrille Showroom  ·  5x sweep 1: fix duplicate id=density-wrap (7x → class); docu 604475e →