From c8382771de937b293aefdd18aadbdbdb755482ab Mon Sep 17 00:00:00 2001 From: Hiren Kangad Date: Mon, 9 Mar 2026 23:15:40 +0530 Subject: [PATCH] Add ImageSprite with LOD tier switching and lazy loading Introduces ImageSprite (extends Sprite) that manages per-image LOD texture swapping via TextureManager. Shows a placeholder rect until the first texture loads, then upgrades/downgrades tiers based on zoom. --- frontend/src/canvas/sprites/ImageSprite.ts | 93 ++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 frontend/src/canvas/sprites/ImageSprite.ts diff --git a/frontend/src/canvas/sprites/ImageSprite.ts b/frontend/src/canvas/sprites/ImageSprite.ts new file mode 100644 index 0000000..c024374 --- /dev/null +++ b/frontend/src/canvas/sprites/ImageSprite.ts @@ -0,0 +1,93 @@ +import { Sprite, Texture, Graphics } from "pixi.js"; +import { TextureManager, LODTier } from "../TextureManager"; + +/** + * A Sprite subclass that manages LOD tier switching and lazy loading. + * Shows a placeholder shimmer rect until the first texture tier loads, + * then swaps textures as zoom level changes. + */ +export class ImageSprite extends Sprite { + readonly assetKey: string; + + private textures: TextureManager; + private currentTier: LODTier | null = null; + private loading = false; + private placeholder: Graphics | null = null; + private _naturalWidth: number; + private _naturalHeight: number; + + constructor( + assetKey: string, + w: number, + h: number, + textures: TextureManager, + ) { + super(Texture.EMPTY); + + this.assetKey = assetKey; + this.textures = textures; + this._naturalWidth = w; + this._naturalHeight = h; + + this.width = w; + this.height = h; + + // Create placeholder: dark rect shown until first texture loads + const placeholder = new Graphics(); + placeholder.rect(0, 0, w, h).fill(0x2a2a2a); + this.placeholder = placeholder; + this.addChild(placeholder); + + // Immediately start loading the thumbnail tier + this.loadTier("thumb"); + } + + /** + * Load a specific LOD tier texture for this sprite. + * Skips if already at the requested tier or currently loading. + */ + async loadTier(tier: LODTier): Promise { + if (this.currentTier === tier || this.loading) return; + + this.loading = true; + try { + const tex = await this.textures.load(this.assetKey, tier); + this.texture = tex; + this.width = this._naturalWidth; + this.height = this._naturalHeight; + this.currentTier = tier; + + // Remove placeholder after first successful load + if (this.placeholder) { + this.removeChild(this.placeholder); + this.placeholder.destroy(); + this.placeholder = null; + } + } catch (err) { + console.warn( + `[ImageSprite] Failed to load tier "${tier}" for "${this.assetKey}":`, + err, + ); + } finally { + this.loading = false; + } + } + + /** + * Evaluate the current zoom level and switch LOD tier if needed. + */ + updateLOD(zoom: number): void { + const needed = this.textures.tierForZoom(zoom); + if (needed !== this.currentTier) { + this.loadTier(needed); + } + } + + /** + * Release the current texture (for off-screen sprites to free GPU memory). + */ + unloadTexture(): void { + this.texture = Texture.EMPTY; + this.currentTier = null; + } +}