From e1fc5eb579e602c562a75163f1be99e259552c38 Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Sun, 29 Mar 2026 13:41:33 +0200 Subject: [PATCH] feat(03-03): render forest elements from IndexedDB in grid - renderForestScene() loads elements from IDB and renders as images - Object URLs tracked and revoked on re-render to prevent memory leaks - Random +-10-20px offset for natural element placement - New element animation class applied via newElementId parameter - Integrated into initForestScreen alongside renderLevelMap --- src/forest/scene.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/forest/scene.ts b/src/forest/scene.ts index 40e29b8..d573284 100644 --- a/src/forest/scene.ts +++ b/src/forest/scene.ts @@ -1,6 +1,55 @@ import type { Progress, LevelStatus } from "../types"; import { levels } from "../game/levels"; -import { getProgress } from "../storage/db"; +import { getProgress, getForestElements } from "../storage/db"; + +let activeObjectUrls: string[] = []; + +export async function renderForestScene( + db: IDBDatabase, + newElementId?: number, +): Promise { + const grid = document.getElementById("forest-grid"); + if (!grid) return; + + // Revoke old object URLs to prevent memory leaks + for (const url of activeObjectUrls) { + URL.revokeObjectURL(url); + } + activeObjectUrls = []; + + const elements = await getForestElements(db); + grid.innerHTML = ""; + + // Render up to 12 elements (4x3 grid) + const maxSlots = 12; + const toRender = elements.slice(0, maxSlots); + + for (const element of toRender) { + const slot = document.createElement("div"); + slot.className = "forest__element"; + + const img = document.createElement("img"); + const objectUrl = URL.createObjectURL(element.imageBlob); + activeObjectUrls.push(objectUrl); + img.src = objectUrl; + img.alt = element.description; + + // Random offset +-10-20px for natural placement + const offsetX = + (Math.random() > 0.5 ? 1 : -1) * (10 + Math.random() * 10); + const offsetY = + (Math.random() > 0.5 ? 1 : -1) * (10 + Math.random() * 10); + img.style.transform = `translate(${Math.round(offsetX)}px, ${Math.round(offsetY)}px)`; + + // Animate newly added element + if (element.id === newElementId) { + slot.classList.add("forest__element--new"); + } + + slot.appendChild(img); + grid.appendChild(slot); + } +} export function getLevelStatus( levelNum: number, @@ -62,6 +111,7 @@ export async function initForestScreen( if (!progress) return; renderLevelMap(mapContainer, progress, onStartLesson); + await renderForestScene(db); const allComplete = levels.every((l) => progress.completedLevels.includes(l.level),