sampleSnapShotBottomColor accepted a frame aspect but cached by URL alone, so a second aspect would have read the first one's result. Every surface renders the capture in the same frame, so the parameter is gone and the per-URL cache is correct by construction.
Unified · Split
| @@ -107,7 +107,7 @@ export function coverCrop( · 106 unmodified lines | |||
| 107 | return { x: 0, y: (height - cropHeight) / 2, width, height: cropHeight }; | 107 | return { x: 0, y: (height - cropHeight) / 2, width, height: cropHeight }; |
| 108 | } | 108 | } |
| 109 | 109 | ||
| 110 | function readBottomBand(image: HTMLImageElement, frameAspect: number): RgbColor | null { | 110 | function readBottomBand(image: HTMLImageElement): RgbColor | null { |
| 111 | const { naturalWidth: width, naturalHeight: height } = image; | 111 | const { naturalWidth: width, naturalHeight: height } = image; |
| 112 | if (width === 0 || height === 0) return null; | 112 | if (width === 0 || height === 0) return null; |
| 113 | const canvas = document.createElement("canvas"); | 113 | const canvas = document.createElement("canvas"); |
| @@ -115,7 +115,7 @@ function readBottomBand(image: HTMLImageElement, frameAspect: number): RgbColor · 1 unmodified line | |||
| 115 | canvas.height = SAMPLE_HEIGHT; | 115 | canvas.height = SAMPLE_HEIGHT; |
| 116 | const context = canvas.getContext("2d", { willReadFrequently: true }); | 116 | const context = canvas.getContext("2d", { willReadFrequently: true }); |
| 117 | if (context === null) return null; | 117 | if (context === null) return null; |
| 118 | const crop = coverCrop(width, height, frameAspect); | 118 | const crop = coverCrop(width, height, SNAP_SHOT_FRAME_ASPECT); |
| 119 | const bandHeight = Math.max(1, crop.height * SAMPLE_BAND); | 119 | const bandHeight = Math.max(1, crop.height * SAMPLE_BAND); |
| 120 | // Drawing the band into a tiny canvas lets the rasterizer do the averaging. | 120 | // Drawing the band into a tiny canvas lets the rasterizer do the averaging. |
| 121 | context.drawImage( | 121 | context.drawImage( |
| @@ -139,22 +139,18 @@ function readBottomBand(image: HTMLImageElement, frameAspect: number): RgbColor · 17 unmodified lines | |||
| 139 | 139 | ||
| 140 | /** | 140 | /** |
| 141 | * Average color of the bottom band of the region the thumbnail frame shows, or | 141 | * Average color of the bottom band of the region the thumbnail frame shows, or |
| 142 | * null when the image cannot be loaded or read. Results are memoized per URL | 142 | * null when the image cannot be loaded or read. Every surface renders the |
| 143 | * so the surfaces that show the same capture decode it once. | 143 | * capture in the same frame, so results are memoized per URL and the surfaces |
| 144 | * that show the same capture decode it once. | ||
| 144 | */ | 145 | */ |
| 145 | export function sampleSnapShotBottomColor( | 146 | export function sampleSnapShotBottomColor(src: string): Promise<RgbColor | null> { |
| 146 | src: string, | ||
| 147 | frameAspect = SNAP_SHOT_FRAME_ASPECT, | ||
| 148 | ): Promise<RgbColor | null> { | ||
| 149 | const cached = sampleCache.get(src); | 147 | const cached = sampleCache.get(src); |
| 150 | if (cached) return cached; | 148 | if (cached) return cached; |
| 151 | const pending = new Promise<RgbColor | null>((resolve) => { | 149 | const pending = new Promise<RgbColor | null>((resolve) => { |
| 152 | const image = new Image(); | 150 | const image = new Image(); |
| 153 | image.crossOrigin = "anonymous"; | 151 | image.crossOrigin = "anonymous"; |
| 154 | image.decoding = "async"; | 152 | image.decoding = "async"; |
| 155 | image.addEventListener("load", () => resolve(readBottomBand(image, frameAspect)), { | 153 | image.addEventListener("load", () => resolve(readBottomBand(image)), { once: true }); |
| 156 | once: true, | ||
| 157 | }); | ||
| 158 | image.addEventListener("error", () => resolve(null), { once: true }); | 154 | image.addEventListener("error", () => resolve(null), { once: true }); |
| 159 | image.src = src; | 155 | image.src = src; |
| 160 | }); | 156 | }); |