Live overlay compositing for the REAL-TIME (MediaRecorder) capture path.
The offline loop composites overlays by re-reading the framebuffer per
frame (renderFrameToCanvas), which the real-time path cannot afford —
it films at wall-clock speed. So instead of handing
canvas.captureStream() the WebGL canvas (which carries no DOM
overlays), the recorder captures a MIRROR 2D canvas that this class
refreshes once per rendered frame: blit the GL canvas, then run the
same compositeOverlays() the screenshot path uses.
Why frame-end and not requestAnimationFrame
The renderer runs with preserveDrawingBuffer: false (see
config/sections/webgl/data.ts), so the drawing buffer is cleared once
the browser composites the frame. drawImage(glCanvas) therefore only
sees pixels while still inside the SAME task that issued the draw calls.
frame-end is emitted by AnimationController.animate() on the line
after postProcessing.render(), which is exactly that window. A plain
rAF callback runs in a later task and blits a fully black frame —
measured, not assumed.
Cost
One drawImage of the whole canvas plus the canvas-2D overlay draws.
Measured at 0.1 ms median / 0.3 ms max on a 2942x1602 (4.7 Mpx) canvas,
i.e. below the noise floor of a 60 FPS budget. It is only attached when
the capture actually has overlays to draw.
What it cannot do
HTML overlays are rasterized through an async <svg><foreignObject>
data URL, and compositeHtmlOverlay only draws one that already
happens to be decoded — the same best-effort behaviour the screenshot
and offline paths have. Text, image, and video overlays are exact.
Live overlay compositing for the REAL-TIME (MediaRecorder) capture path.
The offline loop composites overlays by re-reading the framebuffer per frame (
renderFrameToCanvas), which the real-time path cannot afford — it films at wall-clock speed. So instead of handingcanvas.captureStream()the WebGL canvas (which carries no DOM overlays), the recorder captures a MIRROR 2D canvas that this class refreshes once per rendered frame: blit the GL canvas, then run the samecompositeOverlays()the screenshot path uses.Why
frame-endand notrequestAnimationFrameThe renderer runs with
preserveDrawingBuffer: false(seeconfig/sections/webgl/data.ts), so the drawing buffer is cleared once the browser composites the frame.drawImage(glCanvas)therefore only sees pixels while still inside the SAME task that issued the draw calls.frame-endis emitted byAnimationController.animate()on the line afterpostProcessing.render(), which is exactly that window. A plain rAF callback runs in a later task and blits a fully black frame — measured, not assumed.Cost
One
drawImageof the whole canvas plus the canvas-2D overlay draws. Measured at 0.1 ms median / 0.3 ms max on a 2942x1602 (4.7 Mpx) canvas, i.e. below the noise floor of a 60 FPS budget. It is only attached when the capture actually has overlays to draw.What it cannot do
HTML overlays are rasterized through an async
<svg><foreignObject>data URL, andcompositeHtmlOverlayonly draws one that already happens to be decoded — the same best-effort behaviour the screenshot and offline paths have. Text, image, and video overlays are exact.