From f3ec583c89050c4befc97cfea5642af3c91cab59 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 20:42:30 +0000 Subject: [PATCH 1/2] fix(holiday): use standard Wikimedia thumbnail size and load images once Wikimedia only serves thumbnails at its standard widths, so the 640px pumpkin and ghost URLs returned 400. Switch them to 120px; the sprites render at about 30px. The animation also created a new Image for every sprite on every frame (about 1000 requests/sec), so a failing URL was requested again on every frame. Load each image once and skip ones that haven't loaded or failed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AjBJekNgjZKdL2oSVY4PUe --- config/default.json | 4 ++-- src/features/holiday/HolidayAnimations.js | 23 ++++++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/config/default.json b/config/default.json index 67aa516f5..9aef69048 100644 --- a/config/default.json +++ b/config/default.json @@ -216,8 +216,8 @@ "name": "Halloween", "enabled": true, "images": [ - "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dc/Fluent_Emoji_flat_1f383.svg/640px-Fluent_Emoji_flat_1f383.svg.png", - "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Ghost.svg/640px-Ghost.svg.png" + "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dc/Fluent_Emoji_flat_1f383.svg/120px-Fluent_Emoji_flat_1f383.svg.png", + "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Ghost.svg/120px-Ghost.svg.png" ], "imageScale": 2, "startDay": 29, diff --git a/src/features/holiday/HolidayAnimations.js b/src/features/holiday/HolidayAnimations.js index ec566e39d..8ad345660 100644 --- a/src/features/holiday/HolidayAnimations.js +++ b/src/features/holiday/HolidayAnimations.js @@ -9,6 +9,8 @@ export class HolidayAnimations { this.imageWidth = 15 * scale this.baseImages = images this.images = [] + /** @type {Map} */ + this.loaded = new Map() this.minScale = 0.3 this.interval = null this.canvas = /** @type {HTMLCanvasElement} */ ( @@ -39,12 +41,10 @@ export class HolidayAnimations { this.setCanvasSize() this.ctx.clearRect(0, 0, this.w, this.h) this.images.forEach((item) => { - item.image = new Image() - item.image.style.height = item.height - item.image.style.width = item.width - item.image.src = item.src + const image = this.loaded.get(item.src) + if (!image || !image.complete || !image.naturalWidth) return this.ctx.globalAlpha = item.opacity - this.ctx.drawImage(item.image, item.x, item.y, item.width, item.height) + this.ctx.drawImage(image, item.x, item.y, item.width, item.height) }) this.move() } @@ -54,6 +54,19 @@ export class HolidayAnimations { return } this.setCanvasSize() + // Load each source once; creating a new Image every frame triggered + // a network request per item per frame (and endless retries on failure) + this.baseImages.forEach((src) => { + if (this.loaded.has(src)) return + const image = new Image() + image.onerror = () => { + this.loaded.delete(src) + // eslint-disable-next-line no-console + console.warn('[HolidayEffect] failed to load image', src) + } + image.src = src + this.loaded.set(src, image) + }) for (let a = 0; a < 30; a += 1) { const scale = Math.random() * (1 - this.minScale) + this.minScale this.images.push({ From e31bbf6fac995bb03e303830db7db5b5791cf483 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 1 Nov 2026 00:37:41 -0400 Subject: [PATCH 2/2] fix(holiday): rely on the naturalWidth check for failed images Chrome marks a failed image complete before its onerror runs, and drawImage throws on it, so the naturalWidth check is what keeps draw() safe. Deleting the map entry in onerror was redundant; drop it and the !image check it required. Also correct the comment: same-URL loads are merged, so the old code re-requested each failed URL once per frame, not once per sprite. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/features/holiday/HolidayAnimations.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/features/holiday/HolidayAnimations.js b/src/features/holiday/HolidayAnimations.js index 8ad345660..cb4e15740 100644 --- a/src/features/holiday/HolidayAnimations.js +++ b/src/features/holiday/HolidayAnimations.js @@ -42,7 +42,7 @@ export class HolidayAnimations { this.ctx.clearRect(0, 0, this.w, this.h) this.images.forEach((item) => { const image = this.loaded.get(item.src) - if (!image || !image.complete || !image.naturalWidth) return + if (!image.complete || !image.naturalWidth) return this.ctx.globalAlpha = item.opacity this.ctx.drawImage(image, item.x, item.y, item.width, item.height) }) @@ -54,13 +54,12 @@ export class HolidayAnimations { return } this.setCanvasSize() - // Load each source once; creating a new Image every frame triggered - // a network request per item per frame (and endless retries on failure) + // Load each source once; creating a new Image every frame re-requested + // any URL that failed to load, every frame this.baseImages.forEach((src) => { if (this.loaded.has(src)) return const image = new Image() image.onerror = () => { - this.loaded.delete(src) // eslint-disable-next-line no-console console.warn('[HolidayEffect] failed to load image', src) }