|
7 | 7 | import OverlayCompare from '$lib/battle_mode/OverlayCompare.svelte'; |
8 | 8 | import { FRAME_HEIGHT, FRAME_WIDTH } from '$lib/constants'; |
9 | 9 | import { parseTargetCode, usesTailwind } from '$utils/code'; |
| 10 | + import { captureElement } from '$utils/diff'; |
10 | 11 | import sentinel from '../../routes/(style)/(app)/battle/sentinel-dark.css?raw'; |
11 | 12 |
|
12 | 13 | type RecapTone = 'win' | 'loss' | 'neutral'; |
|
35 | 36 | target = null, |
36 | 37 | showOutcomeLabel = true, |
37 | 38 | showDiff = false, |
| 39 | + onResultPreviewReady, |
38 | 40 | children |
39 | 41 | }: { |
40 | 42 | participants?: RecapParticipant[]; |
41 | 43 | target?: BattleTarget | null; |
42 | 44 | showOutcomeLabel?: boolean; |
43 | 45 | showDiff?: boolean; |
| 46 | + onResultPreviewReady?: (pngDataUrl: string) => void; |
44 | 47 | children?: Snippet; |
45 | 48 | } = $props(); |
46 | 49 |
|
|
76 | 79 | // Diff canvas data URLs |
77 | 80 | let leftDiffCanvasSrc: string | null = $state(null); |
78 | 81 | let rightDiffCanvasSrc: string | null = $state(null); |
| 82 | + let resultPreviewEmitted = $state(false); |
| 83 | + let resultPreviewCaptureInFlight = $state(false); |
| 84 | +
|
| 85 | + const RESULT_PREVIEW_MAX_ATTEMPTS = 6; |
| 86 | + const RESULT_PREVIEW_RETRY_DELAY_MS = 250; |
| 87 | +
|
| 88 | + const delay = (ms: number) => |
| 89 | + new Promise<void>((resolve) => setTimeout(resolve, ms)); |
| 90 | +
|
| 91 | + function looksLikePlaceholderFrame(root: HTMLElement) { |
| 92 | + const body = root.querySelector('body'); |
| 93 | + if (!body) { |
| 94 | + return true; |
| 95 | + } |
| 96 | +
|
| 97 | + const text = (body.textContent ?? '').replace(/\s+/g, ' ').trim(); |
| 98 | + const normalized = text.toLowerCase(); |
| 99 | + const hasLetsBattle = |
| 100 | + normalized.includes("let's battle!") || |
| 101 | + normalized.includes('lets battle!'); |
| 102 | + const elementCount = body.querySelectorAll('*').length; |
| 103 | +
|
| 104 | + return hasLetsBattle && text.length < 120 && elementCount < 20; |
| 105 | + } |
79 | 106 |
|
80 | 107 | function handleLeftDiffCanvasUpdate(canvas: HTMLCanvasElement | null) { |
81 | 108 | if (canvas) { |
|
89 | 116 | } |
90 | 117 | } |
91 | 118 |
|
| 119 | + async function emitResultPreviewOnce() { |
| 120 | + if ( |
| 121 | + resultPreviewEmitted || |
| 122 | + resultPreviewCaptureInFlight || |
| 123 | + !onResultPreviewReady |
| 124 | + ) { |
| 125 | + return; |
| 126 | + } |
| 127 | +
|
| 128 | + if (!leftIframeElement?.contentDocument) { |
| 129 | + return; |
| 130 | + } |
| 131 | +
|
| 132 | + resultPreviewCaptureInFlight = true; |
| 133 | +
|
| 134 | + try { |
| 135 | + for ( |
| 136 | + let attempt = 1; |
| 137 | + attempt <= RESULT_PREVIEW_MAX_ATTEMPTS; |
| 138 | + attempt += 1 |
| 139 | + ) { |
| 140 | + const frameRoot = leftIframeElement?.contentDocument?.documentElement; |
| 141 | + if (!frameRoot) { |
| 142 | + await delay(RESULT_PREVIEW_RETRY_DELAY_MS); |
| 143 | + continue; |
| 144 | + } |
| 145 | +
|
| 146 | + if (looksLikePlaceholderFrame(frameRoot)) { |
| 147 | + await delay(RESULT_PREVIEW_RETRY_DELAY_MS); |
| 148 | + continue; |
| 149 | + } |
| 150 | +
|
| 151 | + try { |
| 152 | + const captured = await captureElement(frameRoot); |
| 153 | + await captured.decode?.().catch(() => {}); |
| 154 | +
|
| 155 | + const width = captured.naturalWidth || captured.width; |
| 156 | + const height = captured.naturalHeight || captured.height; |
| 157 | + if (!width || !height) { |
| 158 | + throw new Error('Captured preview had invalid dimensions'); |
| 159 | + } |
| 160 | +
|
| 161 | + const canvas = document.createElement('canvas'); |
| 162 | + canvas.width = width; |
| 163 | + canvas.height = height; |
| 164 | + const ctx = canvas.getContext('2d'); |
| 165 | + if (!ctx) { |
| 166 | + throw new Error('Could not create preview canvas context'); |
| 167 | + } |
| 168 | +
|
| 169 | + ctx.drawImage(captured, 0, 0, width, height); |
| 170 | + onResultPreviewReady(canvas.toDataURL('image/png')); |
| 171 | + resultPreviewEmitted = true; |
| 172 | + return; |
| 173 | + } catch (error) { |
| 174 | + console.error( |
| 175 | + `[BattleRecapGrid] Failed to capture result preview (attempt ${attempt}/${RESULT_PREVIEW_MAX_ATTEMPTS}):`, |
| 176 | + error |
| 177 | + ); |
| 178 | + } |
| 179 | +
|
| 180 | + if (attempt < RESULT_PREVIEW_MAX_ATTEMPTS) { |
| 181 | + await delay(RESULT_PREVIEW_RETRY_DELAY_MS); |
| 182 | + } |
| 183 | + } |
| 184 | +
|
| 185 | + console.error( |
| 186 | + '[BattleRecapGrid] Result preview capture exhausted all retry attempts' |
| 187 | + ); |
| 188 | + } catch (error) { |
| 189 | + console.error( |
| 190 | + '[BattleRecapGrid] Failed to capture result preview:', |
| 191 | + error |
| 192 | + ); |
| 193 | + } finally { |
| 194 | + resultPreviewCaptureInFlight = false; |
| 195 | + } |
| 196 | + } |
| 197 | +
|
92 | 198 | function handleLeftLoad() { |
93 | 199 | if (showDiff) { |
94 | 200 | leftDiffEngine?.triggerCompare(); |
95 | 201 | } |
| 202 | + void emitResultPreviewOnce(); |
96 | 203 | } |
97 | 204 |
|
98 | 205 | function handleRightLoad() { |
|
0 commit comments