From a97e74ac0cd47adc8dddd416bc4c2f435545654e Mon Sep 17 00:00:00 2001 From: DCCA Date: Sat, 27 Jun 2026 18:10:49 -0300 Subject: [PATCH] fix(capture): keep the capture notice out of the screenshot The on-page notice leaked into captured frames: SB_SET_OVERLAY responded inside a single requestAnimationFrame, which runs *before* that frame's paint, so captureVisibleTab could fire before the overlay was actually painted hidden. Wait for the next paint (double-rAF) before responding to SB_SET_OVERLAY, and add a short settle (60ms) after the hide before capturing to cover compositor lag. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/content.ts | 13 ++++++++++++- src/lib/capture.ts | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/content.ts b/src/content.ts index 0f1107e..519adc6 100644 --- a/src/content.ts +++ b/src/content.ts @@ -94,6 +94,16 @@ function removeCaptureOverlay(): void { captureOverlay = null; } +/** + * Run after the next paint. A single rAF fires *before* the frame is painted, + * so the overlay's display change would not yet be on screen; a second rAF + * guarantees the change has been painted before we report back — otherwise the + * notice can leak into the captured frame. + */ +function afterPaint(callback: () => void): void { + window.requestAnimationFrame(() => window.requestAnimationFrame(callback)); +} + chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message?.type === "SB_CAPTURE_BEGIN") { ensureCaptureOverlay().style.display = "flex"; @@ -103,7 +113,8 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message?.type === "SB_SET_OVERLAY") { setCaptureOverlayDisplay(Boolean(message.visible)); - window.requestAnimationFrame(() => sendResponse({ ok: true })); + // Wait for the hide/show to actually paint before the orchestrator captures. + afterPaint(() => sendResponse({ ok: true })); return true; } diff --git a/src/lib/capture.ts b/src/lib/capture.ts index 292f369..8ca8024 100644 --- a/src/lib/capture.ts +++ b/src/lib/capture.ts @@ -169,7 +169,10 @@ export async function captureFullPage( await sendMessage(tabId, { type: "SB_SCROLL_TO", y }); await wait(120); // Hide the notice so it is not baked into this frame, then capture. + // notify resolves only after the hide has painted (double-rAF in the + // content script); a short extra settle covers compositor lag. await notify(tabId, { type: "SB_SET_OVERLAY", visible: false }); + await wait(60); const dataUrl = await chrome.tabs.captureVisibleTab(windowId, { format: "png" }); segments.push({ y, dataUrl }); onProgress?.(i + 1, steps.length);