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);