Glaze Resize/Reframe Synchronization Plan#
Problem Statement#
During window resize on prompt.mjs (dark mode), users occasionally see both the glaze canvas and the underlying main canvas, causing a visual "double vision" or layering artifact.
Root Cause Analysis#
Canvas Stack Architecture (z-index order)#
- 3D canvas (z-index: 0) - Three.js
- WebGPU canvas (z-index: 1)
- Glaze canvas (z-index: 2) - WebGL2 post-processing
- Freeze frame canvas (z-index: 3, position: fixed)
- UI canvas (z-index: 6)
- Main 2D canvas (z-index: 7)
- Debug canvas (z-index: 8)
Current Resize Flow (bios.mjs)#
- Window resize event → debounced (80ms
REFRAME_DELAY) needsReframe = truetriggersframe()call- Freeze frame creation (lines 1016-1071):
- Copy current canvas to
freezeFrameCan - If glaze is on:
Glaze.freeze(ffCtx)- draws glaze canvas to freeze frame - Set
canvas.style.opacity = 0(hide main canvas) - Append freeze frame to wrapper
- Copy current canvas to
- Canvas resize (lines 1156-1158):
canvas.width = width; canvas.height = height- This clears the canvas!
- Glaze reload (lines 1538-1557):
Glaze.on()checks if dimensions changed- If changed: creates new Glaze instance, reloads shaders
- Glaze canvas starts with
opacity: 0(glaze.mjs line 40)
- Frame completion (lines 15938-15951):
- Remove freeze frame:
freezeFrameCan.remove() - Call
Glaze.unfreeze()- removes opacity property - If glaze off:
canvas.style.removeProperty("opacity")
- Remove freeze frame:
Identified Race Conditions#
Issue 1: Glaze shader reload timing#
In glaze.mjs on() function (line 340):
await glaze.load(() => {
offed = false;
frame(w, h, rect, nativeWidth, nativeHeight, wrapper);
loaded();
});
The glaze shaders load asynchronously, but frame() is called inside the callback. If a resize happens during shader compilation, the canvas dimensions and glaze dimensions can get out of sync.
Issue 2: Opacity transition timing (CSS)#
canvas[data-type="glaze"].first-glaze {
transition: 0.5s opacity;
}
The first-glaze class adds a 500ms opacity transition. During rapid resizes, this transition may not complete before another resize triggers, leaving both canvases partially visible.
Issue 3: Freeze frame removal timing#
In bios.mjs lines 15938-15951:
if (freezeFrame && freezeFrameFrozen) {
if (glaze.on === false) {
canvas.style.removeProperty("opacity");
}
freezeFrameCan.remove();
freezeFrame = false;
// ...
}
if (glaze.on) {
Glaze.unfreeze(); // This just removes opacity property
} else {
canvas.style.removeProperty("opacity");
}
The freeze frame is removed before we can guarantee that:
- The glaze canvas has finished its shader reload
- The glaze canvas has been properly positioned/sized
- The main canvas has been hidden (opacity: 0)
Issue 4: Missing synchronization between canvas opacity states#
When glaze is on:
- Main canvas should have
opacity: 0 - Glaze canvas should have
opacity: 1(no opacity set)
But there's no atomic transition between these states.
Proposed Solutions#
Solution A: Deferred Freeze Frame Removal (Low Risk)#
Wait for glaze to be fully ready before removing freeze frame.
Changes in bios.mjs:
// Add a flag for glaze readiness
let glazeReady = false;
// In Glaze.on() callback:
currentGlaze = Glaze.on(
canvas.width,
canvas.height,
canvasRect,
projectedWidth,
projectedHeight,
wrapper,
glaze.type,
() => {
glazeReady = true; // Mark glaze as ready
send({ type: "needs-paint" });
},
);
// In paint loop, before removing freeze frame:
if (freezeFrame && freezeFrameFrozen && (!glaze.on || glazeReady)) {
// ... remove freeze frame
glazeReady = false; // Reset for next resize
}
Solution B: Eliminate CSS Transition During Resize (Medium Risk)#
Remove the first-glaze class during resize operations.
Changes:
- In glaze.mjs
frame(): Don't addfirst-glazeclass if dimensions are changing - Or: Add
transition: noneinline style during resize, restore after
Solution C: Atomic Opacity Swap (Higher Risk, Most Robust)#
Ensure main canvas opacity and glaze canvas opacity change atomically.
Changes in bios.mjs:
// Before showing glaze canvas
requestAnimationFrame(() => {
canvas.style.opacity = 0; // Hide main canvas
Glaze.unfreeze(); // Show glaze canvas
freezeFrameCan.remove(); // Remove freeze frame
});
Solution D: Use Visibility Instead of Opacity#
Replace opacity manipulation with visibility for instant switching.
Changes:
- Replace
canvas.style.opacity = 0withcanvas.style.visibility = "hidden" - Replace
removeProperty("opacity")withstyle.visibility = "visible"
Recommended Implementation Order#
- Phase 1 (Quick Win): Implement Solution A - deferred freeze frame removal
- Phase 2 (If still issues): Add Solution B - disable transition during resize
- Phase 3 (If needed): Implement Solution C - atomic opacity swap
Testing Checklist#
- Resize window slowly in dark mode (prompt.mjs)
- Resize window rapidly in dark mode
- Resize while content is actively animating
- Test with different screen densities (devicePixelRatio)
- Test switching between light/dark mode during resize
- Test on macOS, Windows, Linux
- Test in VS Code webview (ac-electron)
Files to Modify#
- bios.mjs - Main resize/glaze coordination
- glaze.mjs - Shader loading and canvas management
- style.css - CSS transitions (if Solution B)
Performance Considerations#
- Avoid adding more async operations in the paint loop
- The current 80ms
REFRAME_DELAYis already a debounce - don't reduce it - Consider using
will-change: opacityCSS hint on glaze canvas - Monitor for memory leaks from repeated shader program creation
References#
- bios.mjs freeze frame logic: lines 1016-1071
- bios.mjs glaze.on() call: lines 1538-1557
- bios.mjs freeze frame removal: lines 15938-15951
- glaze.mjs on() function: lines 326-361
- glaze.mjs frame() function: lines 120-200
- glaze.mjs freeze/unfreeze: lines 392-405