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
This commit is contained in:
2026-03-29 13:41:33 +02:00
parent 5a6ba70dab
commit e1fc5eb579
+51 -1
View File
@@ -1,6 +1,55 @@
import type { Progress, LevelStatus } from "../types"; import type { Progress, LevelStatus } from "../types";
import { levels } from "../game/levels"; 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<void> {
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( export function getLevelStatus(
levelNum: number, levelNum: number,
@@ -62,6 +111,7 @@ export async function initForestScreen(
if (!progress) return; if (!progress) return;
renderLevelMap(mapContainer, progress, onStartLesson); renderLevelMap(mapContainer, progress, onStartLesson);
await renderForestScene(db);
const allComplete = levels.every((l) => const allComplete = levels.every((l) =>
progress.completedLevels.includes(l.level), progress.completedLevels.includes(l.level),