const Levels = (function () { const meta = { 1: { title: "Zaaien & water geven", instruction: "Plant vijf zaadjes en geef ze water. Kies je gereedschap en klik op de potten." }, 2: { title: "Vuurvliegjes vangen", instruction: "Tik op de gloeiende vuurvliegjes en vang ze alle acht in de jampot." }, 3: { title: "Sterrenbeelden verbinden", instruction: "Verbind de sterren op volgorde. Een verkeerde ster doet geen pijn." }, 4: { title: "Taart versieren", instruction: "Kies een topping en leg hem op het juiste silhouet. Als alle plekjes gevuld zijn, gaat het kaarsje aan." }, 5: { title: "Boeket schikken", instruction: "Kijk naar het voorbeeld en pluk de juiste bloemen. Kleur รฉn soort moeten kloppen." }, 6: { title: "Tuinmemory", instruction: "Draai twee kaartjes om. Vind alle zes bloemenparen โ€” een foutje doet geen pijn." }, 7: { title: "Herfstblaadjes", instruction: "Sleep de acht blaadjes naar de mand met je vinger of het trackpad." } }; function el(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; } function sound(name) { if (window.UI && typeof window.UI.play === "function") window.UI.play(name); } function wiggle(node) { if (!node) return; node.classList.remove("wrong"); void node.offsetWidth; node.classList.add("wrong"); window.setTimeout(function () { node.classList.remove("wrong"); }, 520); } function shuffle(items) { const result = items.slice(); for (let i = result.length - 1; i > 0; i -= 1) { const j = Math.floor(Math.random() * (i + 1)); const tmp = result[i]; result[i] = result[j]; result[j] = tmp; } return result; } function makeTimers() { const timers = []; return { later: function (fn, ms) { const id = window.setTimeout(fn, ms); timers.push(id); return id; }, clear: function () { timers.forEach(function (id) { window.clearTimeout(id); }); timers.length = 0; } }; } const FLOWER_TYPES = { tulp: { name: "tulp" }, roos: { name: "roos" }, madeliefje: { name: "madeliefje" } }; const FLOWER_COLORS = { roze: { name: "roze", hex: "#f1a7bc" }, geel: { name: "geel", hex: "#f2cf72" }, lavendel: { name: "lavendel", hex: "#bfaee6" }, wit: { name: "wit", hex: "#fffaf2" }, koraal: { name: "koraal", hex: "#f09a86" } }; function flowerLabel(type, color) { return FLOWER_COLORS[color].name + " " + FLOWER_TYPES[type].name; } function flowerSVG(type, color) { const hex = FLOWER_COLORS[color].hex; let petals = ""; if (type === "roos") { for (let i = 0; i < 8; i += 1) { petals += ''; } petals += ''; } else if (type === "madeliefje") { for (let i = 0; i < 7; i += 1) { petals += ''; } } else { petals = '' + '' + ''; } return ''; } function buildGarden(stage, ctx) { stage.classList.add("stage-garden"); const timers = makeTimers(); const scene = el("div", "garden-scene"); const toolbar = el("div", "tool-row"); const label = el("span", "tool-label", "Gereedschap"); const seedBtn = el("button", "tool-btn"); seedBtn.type = "button"; seedBtn.innerHTML = "๐ŸŒฑZaadje"; seedBtn.setAttribute("aria-pressed", "true"); const waterBtn = el("button", "tool-btn"); waterBtn.type = "button"; waterBtn.innerHTML = "๐Ÿ’งGieter"; waterBtn.setAttribute("aria-pressed", "false"); toolbar.append(label, seedBtn, waterBtn); const bed = el("div", "pot-row"); const colors = [ ["#f4a7b9", "#f9cfd8"], ["#f2ce7a", "#f8e6b4"], ["#bea9e6", "#dcd0f2"], ["#f29b8b", "#f8c9bf"], ["#fff7ef", "#f3e6d8"] ]; let tool = "seed"; let seeds = 0; let watered = 0; let done = false; const pots = []; function selectTool(next) { tool = next; seedBtn.classList.toggle("selected", tool === "seed"); waterBtn.classList.toggle("selected", tool === "water"); seedBtn.setAttribute("aria-pressed", tool === "seed" ? "true" : "false"); waterBtn.setAttribute("aria-pressed", tool === "water" ? "true" : "false"); } function onPot(pot, index) { if (done) return; if (pot.watered) { ctx.hint("Deze bloem bloeit al. Kijk eens hoe mooi."); return; } if (tool === "seed") { if (pot.seeded) { ctx.hint("Hier ligt al een zaadje. Pak de gieter om water te geven."); return; } pot.seeded = true; pot.node.classList.add("seeded"); pot.node.setAttribute("aria-label", "Bloempot " + (index + 1) + ", zaadje geplant"); seeds += 1; sound("seed"); ctx.hint("Zaadje " + seeds + " van 5 geplant."); if (seeds === 5) { selectTool("water"); ctx.hint("Alle zaadjes liggen klaar. Geef ze nu water met de gieter."); sound("chime"); } return; } if (!pot.seeded) { wiggle(pot.node); ctx.hint("Eerst een zaadje planten. Pak het zaadje."); return; } pot.watered = true; pot.node.classList.add("watering"); pot.node.classList.add("watered"); pot.node.setAttribute("aria-label", "Bloempot " + (index + 1) + ", bloem groeit"); timers.later(function () { pot.node.classList.remove("watering"); }, 550); timers.later(function () { pot.node.classList.add("bloomed"); }, 420); watered += 1; sound("water"); ctx.hint("Water gegeven! " + watered + " van 5 bloemen."); if (watered === 5) { done = true; ctx.hint("Wat een mooie bloementuin! Alles staat in bloei."); sound("complete"); timers.later(ctx.onComplete, 1200); } } for (let i = 0; i < 5; i += 1) { const node = el("button", "pot"); node.type = "button"; node.setAttribute("aria-label", "Bloempot " + (i + 1)); node.style.setProperty("--bloom", colors[i][0]); node.style.setProperty("--bloom-2", colors[i][1]); node.innerHTML = '" + ''; const pot = { node: node, seeded: false, watered: false }; pots.push(pot); node.addEventListener("click", function () { onPot(pot, i); }); bed.append(node); } seedBtn.addEventListener("click", function () { selectTool("seed"); ctx.hint("Kies een pot en plant een zaadje."); }); waterBtn.addEventListener("click", function () { selectTool("water"); ctx.hint("Kies een pot met een zaadje en geef water."); }); const sun = el("span", "garden-sun"); sun.setAttribute("aria-hidden", "true"); scene.append(sun, toolbar, bed); stage.append(scene); ctx.hint("Begin met een zaadje: klik op een pot."); return { destroy: function () { timers.clear(); pots.length = 0; } }; } function buildFireflies(stage, ctx) { stage.classList.add("stage-fireflies"); const timers = makeTimers(); const scene = el("div", "firefly-scene"); for (let i = 0; i < 18; i += 1) { const star = el("span", "tiny-star"); star.style.left = (4 + Math.random() * 92).toFixed(1) + "%"; star.style.top = (4 + Math.random() * 60).toFixed(1) + "%"; star.style.animationDelay = (Math.random() * 4).toFixed(2) + "s"; scene.append(star); } const counter = el("p", "scene-counter", "0 van 8 vuurvliegjes"); const jar = el("div", "jar"); jar.innerHTML = '' + '' + '' + '0'; const jarDots = jar.querySelector(".jar-dots"); const jarLabel = jar.querySelector(".jar-label"); scene.append(counter, jar); const total = 8; const flies = []; let caught = 0; let done = false; let running = true; let raf = 0; const start = performance.now(); function frame(now) { if (!running) return; const t = (now - start) / 1000; flies.forEach(function (fly) { if (fly.caught) return; const x = fly.x + Math.sin(t * fly.sx + fly.phase) * fly.ampX; const y = fly.y + Math.cos(t * fly.sy + fly.phase) * fly.ampY; fly.node.style.left = x.toFixed(2) + "%"; fly.node.style.top = y.toFixed(2) + "%"; }); raf = window.requestAnimationFrame(frame); } function catchFly(fly) { if (fly.caught || done) return; fly.caught = true; caught += 1; fly.node.classList.add("caught"); fly.node.disabled = true; fly.node.style.left = "50%"; fly.node.style.top = "82%"; sound("catch"); counter.textContent = caught + " van " + total + " vuurvliegjes"; ctx.hint(caught === total ? "Alle vuurvliegjes zitten in de pot!" : "Gevangen! Nog " + (total - caught) + " te gaan."); timers.later(function () { fly.node.hidden = true; const dot = el("span", "glow-dot"); dot.style.left = (16 + Math.random() * 68).toFixed(1) + "%"; dot.style.top = (16 + Math.random() * 62).toFixed(1) + "%"; dot.style.animationDelay = (Math.random() * 1.4).toFixed(2) + "s"; jarDots.append(dot); jar.classList.add("has-light"); jarLabel.textContent = String(caught); }, 430); if (caught === total) { done = true; sound("complete"); timers.later(function () { running = false; window.cancelAnimationFrame(raf); ctx.onComplete(); }, 1150); } } for (let i = 0; i < total; i += 1) { const node = el("button", "firefly"); node.type = "button"; node.setAttribute("aria-label", "Vuurvliegje " + (i + 1)); const fly = { node: node, x: 8 + Math.random() * 84, y: 10 + Math.random() * 46, phase: Math.random() * Math.PI * 2, sx: 0.45 + Math.random() * 0.8, sy: 0.4 + Math.random() * 0.7, ampX: 2.5 + Math.random() * 4.5, ampY: 1.5 + Math.random() * 3.5, caught: false }; node.style.left = fly.x.toFixed(1) + "%"; node.style.top = fly.y.toFixed(1) + "%"; node.addEventListener("click", function () { catchFly(fly); }); flies.push(fly); scene.append(node); } stage.append(scene); raf = window.requestAnimationFrame(frame); ctx.hint("Tik op een vuurvliegje om het te vangen."); return { destroy: function () { running = false; window.cancelAnimationFrame(raf); timers.clear(); } }; } function buildStars(stage, ctx) { stage.classList.add("stage-stars"); const timers = makeTimers(); const scene = el("div", "starfield"); const svgNS = "http://www.w3.org/2000/svg"; for (let i = 0; i < 22; i += 1) { const star = el("span", "tiny-star"); star.style.left = (3 + Math.random() * 94).toFixed(1) + "%"; star.style.top = (3 + Math.random() * 90).toFixed(1) + "%"; star.style.animationDelay = (Math.random() * 4).toFixed(2) + "s"; scene.append(star); } const svg = document.createElementNS(svgNS, "svg"); svg.setAttribute("class", "constellation-lines"); svg.setAttribute("viewBox", "0 0 100 100"); svg.setAttribute("preserveAspectRatio", "none"); svg.setAttribute("aria-hidden", "true"); scene.append(svg); const points = [ { x: 30, y: 22 }, { x: 46, y: 15 }, { x: 62, y: 22 }, { x: 67, y: 45 }, { x: 46, y: 55 }, { x: 25, y: 45 }, { x: 46, y: 75 } ]; const starNodes = []; let next = 0; let done = false; function drawLine(a, b) { const line = document.createElementNS(svgNS, "line"); line.setAttribute("x1", a.x); line.setAttribute("y1", a.y); line.setAttribute("x2", b.x); line.setAttribute("y2", b.y); line.setAttribute("class", "star-line"); svg.append(line); } function onStar(index) { if (done) return; if (index === next) { starNodes[index].classList.add("connected"); starNodes[index].classList.remove("next"); starNodes[index].setAttribute("aria-label", "Ster " + (index + 1) + ", verbonden"); sound("star"); if (next > 0) drawLine(points[next - 1], points[next]); next += 1; if (next < points.length) { starNodes[next].classList.add("next"); ctx.hint("Mooi. Verbind nu ster " + (next + 1) + "."); } else { done = true; scene.classList.add("constellation-done"); ctx.hint("Een sterrenkat! Hij kijkt tevreden naar je."); sound("complete"); timers.later(ctx.onComplete, 1200); } return; } wiggle(starNodes[index]); sound("nope"); ctx.hint("Bijna! Verbind de sterren op volgorde. Nu is ster " + (next + 1) + " aan de beurt."); } points.forEach(function (point, index) { const node = el("button", "star"); node.type = "button"; node.style.left = point.x + "%"; node.style.top = point.y + "%"; node.setAttribute("aria-label", "Ster " + (index + 1)); node.innerHTML = '"; node.addEventListener("click", function () { onStar(index); }); starNodes.push(node); scene.append(node); }); starNodes[0].classList.add("next"); stage.append(scene); ctx.hint("Begin bij ster 1 en volg de nummers."); return { destroy: function () { timers.clear(); } }; } function buildCake(stage, ctx) { stage.classList.add("stage-cake"); const timers = makeTimers(); const scene = el("div", "cake-scene"); const cake = el("div", "cake"); const art = el("div", "cake-art"); art.setAttribute("aria-hidden", "true"); art.innerHTML = '' + '' + '' + '' + '' + '' + "" + ''; const slotDefs = [ { type: "aardbei", emoji: "๐Ÿ“", left: 25, top: 30 }, { type: "bloem", emoji: "๐ŸŒผ", left: 50, top: 25 }, { type: "room", emoji: "๐Ÿฆ", left: 75, top: 30 }, { type: "blad", emoji: "๐Ÿƒ", left: 30, top: 56 }, { type: "ster", emoji: "โญ", left: 50, top: 60 }, { type: "kers", emoji: "๐Ÿ’", left: 70, top: 56 } ]; const slotRow = el("div", "cake-slots"); const slots = slotDefs.map(function (def, index) { const node = el("button", "cake-slot"); node.type = "button"; node.dataset.slot = String(index); node.style.left = def.left + "%"; node.style.top = def.top + "%"; node.setAttribute("aria-label", "Plek voor " + def.type); node.innerHTML = '"; slotRow.append(node); return { def: def, node: node, filled: false }; }); cake.append(art, slotRow); const trayLabel = el("p", "tray-label", "Kies een topping"); const tray = el("div", "topping-tray"); const toppingDefs = [ { type: "aardbei", emoji: "๐Ÿ“", name: "Aardbei" }, { type: "bloem", emoji: "๐ŸŒผ", name: "Bloemetje" }, { type: "room", emoji: "๐Ÿฆ", name: "Slagroom" }, { type: "blad", emoji: "๐Ÿƒ", name: "Blaadje" }, { type: "ster", emoji: "โญ", name: "Sterretje" }, { type: "kers", emoji: "๐Ÿ’", name: "Kers" } ]; const toppings = {}; let selected = null; let filledCount = 0; let done = false; function selectTopping(type) { if (done || toppings[type].used) return; selected = type; Object.keys(toppings).forEach(function (key) { toppings[key].node.classList.toggle("selected", key === type); }); sound("click"); ctx.hint("Kies een plekje op de taart voor de " + toppings[type].name.toLowerCase() + "."); } function place(index) { const slot = slots[index]; if (!slot || slot.filled || done) return; if (!selected) { ctx.hint("Kies eerst een topping uit het dienblad."); return; } if (selected !== slot.def.type) { wiggle(slot.node); sound("nope"); ctx.hint("Deze topping past niet op dat plekje. Kijk naar het silhouet."); return; } slot.filled = true; slot.node.classList.add("filled"); slot.node.innerHTML = '"; slot.node.setAttribute("aria-label", slot.def.type + " geplaatst"); slot.node.disabled = true; toppings[selected].used = true; toppings[selected].node.classList.add("used"); toppings[selected].node.classList.remove("selected"); toppings[selected].node.disabled = true; selected = null; filledCount += 1; sound("place"); if (filledCount === 6) { done = true; cake.classList.add("cake-done"); ctx.hint("De taart is klaar. Het kaarsje brandt โ€” maak een wens."); sound("complete"); timers.later(ctx.onComplete, 1300); } else { ctx.hint("Mooi! Nog " + (6 - filledCount) + " plekjes te vullen."); } } toppingDefs.forEach(function (def) { const node = el("button", "topping"); node.type = "button"; node.dataset.topping = def.type; node.setAttribute("aria-label", def.name); node.innerHTML = '' + def.name + ""; node.addEventListener("pointerdown", function (event) { if (node.disabled) return; selectTopping(def.type); drag = { x: event.clientX, y: event.clientY }; }); toppings[def.type] = { node: node, name: def.name, used: false }; tray.append(node); }); slots.forEach(function (slot, index) { slot.node.addEventListener("click", function () { place(index); }); }); let drag = null; function onPointerUp(event) { if (!drag) return; const moved = Math.hypot(event.clientX - drag.x, event.clientY - drag.y) > 8; drag = null; if (!moved) return; const target = document.elementFromPoint(event.clientX, event.clientY); const slotEl = target && target.closest ? target.closest(".cake-slot") : null; if (slotEl) place(Number(slotEl.dataset.slot)); } scene.addEventListener("pointerup", onPointerUp); scene.append(cake, trayLabel, tray); stage.append(scene); ctx.hint("Kies een topping en tik op het bijbehorende silhouet."); return { destroy: function () { timers.clear(); scene.removeEventListener("pointerup", onPointerUp); } }; } function buildBouquet(stage, ctx) { stage.classList.add("stage-bouquet"); const timers = makeTimers(); const scene = el("div", "bouquet-scene"); const pattern = [ { type: "tulp", color: "roze" }, { type: "roos", color: "geel" }, { type: "madeliefje", color: "lavendel" }, { type: "tulp", color: "wit" }, { type: "roos", color: "koraal" } ]; const extras = [ { type: "roos", color: "roze" }, { type: "madeliefje", color: "geel" }, { type: "tulp", color: "lavendel" }, { type: "madeliefje", color: "koraal" } ]; function flowerKey(flower) { return flower.type + "-" + flower.color; } const patternPanel = el("div", "pattern-panel"); patternPanel.innerHTML = '

Het voorbeeld

'; const patternRow = el("div", "pattern-row"); pattern.forEach(function (flower) { const item = el("span", "pattern-flower"); item.setAttribute("role", "img"); item.setAttribute("aria-label", flowerLabel(flower.type, flower.color)); item.innerHTML = flowerSVG(flower.type, flower.color); patternRow.append(item); }); patternPanel.append(patternRow); const vasePanel = el("div", "vase-panel"); const vaseLabel = el("p", "panel-label", "Jouw boeket"); const holder = el("div", "bouquet-holder"); const slotsRow = el("div", "vase-slots"); const slots = pattern.map(function (required, index) { const node = el("button", "vase-slot"); node.type = "button"; node.dataset.slot = String(index); node.setAttribute("aria-label", "Plek voor " + flowerLabel(required.type, required.color)); const ghost = el("span", "slot-flower slot-flower-ghost"); ghost.innerHTML = flowerSVG(required.type, required.color); node.append(ghost); slotsRow.append(node); return { required: required, node: node, filled: false }; }); const vase = el("div", "vase"); vase.setAttribute("aria-hidden", "true"); vase.innerHTML = ''; holder.append(slotsRow, vase); vasePanel.append(vaseLabel, holder); const gardenPanel = el("div", "garden-panel"); gardenPanel.innerHTML = '

Pluk uit de tuin

'; const gardenGrid = el("div", "garden-grid"); const gardenFlowers = shuffle(pattern.concat(extras)); const gardenByKey = {}; const gardenButtons = []; pattern.concat(extras).forEach(function (flower) { gardenByKey[flowerKey(flower)] = flower; }); let selectedKey = null; let placed = 0; let done = false; function selectFlower(key, node) { if (done || node.disabled) return; selectedKey = key; gardenButtons.forEach(function (button) { button.classList.toggle("selected", button.dataset.flower === key); }); sound("click"); ctx.hint("Mooi, een " + flowerLabel(gardenByKey[key].type, gardenByKey[key].color) + ". Kies een plekje in de vaas."); } function place(index) { const slot = slots[index]; if (!slot || slot.filled || done) return; if (!selectedKey) { ctx.hint("Kies eerst een bloem uit de tuin."); return; } const flower = gardenByKey[selectedKey]; if (flower.type !== slot.required.type || flower.color !== slot.required.color) { wiggle(slot.node); sound("nope"); ctx.hint("Deze bloem lijkt niet op dat plekje. Kijk naar kleur รฉn soort."); return; } slot.filled = true; slot.node.classList.add("filled"); slot.node.innerHTML = '' + flowerSVG(flower.type, flower.color) + ""; slot.node.setAttribute("aria-label", flowerLabel(flower.type, flower.color) + " geplaatst"); slot.node.disabled = true; const button = gardenButtons.find(function (item) { return item.dataset.flower === selectedKey; }); if (button) { button.disabled = true; button.classList.add("used"); button.classList.remove("selected"); } selectedKey = null; placed += 1; sound("place"); if (placed === 5) { done = true; scene.classList.add("bouquet-done"); ctx.hint("Een prachtig boeket โ€” precies zoals het voorbeeld."); sound("complete"); timers.later(ctx.onComplete, 1300); } else { ctx.hint("Prachtig. Nog " + (5 - placed) + " bloemen te plukken."); } } gardenFlowers.forEach(function (flower) { const key = flowerKey(flower); const node = el("button", "garden-flower"); node.type = "button"; node.dataset.flower = key; node.setAttribute("aria-label", flowerLabel(flower.type, flower.color)); node.innerHTML = flowerSVG(flower.type, flower.color); node.addEventListener("click", function () { selectFlower(key, node); }); gardenButtons.push(node); gardenGrid.append(node); }); slots.forEach(function (slot, index) { slot.node.addEventListener("click", function () { place(index); }); }); gardenPanel.append(gardenGrid); scene.append(patternPanel, vasePanel, gardenPanel); stage.append(scene); ctx.hint("Kies een bloem die ook in het voorbeeld staat."); return { destroy: function () { timers.clear(); } }; } function buildMemory(stage, ctx) { stage.classList.add("stage-memory"); const timers = makeTimers(); const scene = el("div", "memory-scene"); const counter = el("p", "scene-counter memory-counter", "0 van 6 paren"); const grid = el("div", "memory-grid"); const pairs = [ { type: "tulp", color: "roze" }, { type: "roos", color: "geel" }, { type: "madeliefje", color: "lavendel" }, { type: "tulp", color: "wit" }, { type: "roos", color: "koraal" }, { type: "madeliefje", color: "roze" } ]; const deck = shuffle(pairs.concat(pairs)); const cards = []; let first = null; let locked = false; let matched = 0; let done = false; function flip(card) { if (done || locked || card.matched || card === first) return; card.node.classList.add("flipped"); card.node.setAttribute("aria-label", flowerLabel(card.flower.type, card.flower.color) + ", open"); sound("click"); if (!first) { first = card; return; } const second = card; if (first.key === second.key) { first.matched = true; second.matched = true; first.node.classList.add("matched"); second.node.classList.add("matched"); first.node.disabled = true; second.node.disabled = true; first.node.setAttribute("aria-label", flowerLabel(first.flower.type, first.flower.color) + ", gevonden"); second.node.setAttribute("aria-label", flowerLabel(second.flower.type, second.flower.color) + ", gevonden"); matched += 1; counter.textContent = matched + " van 6 paren"; sound("place"); first = null; ctx.hint(matched === 6 ? "Alle paren gevonden. Wat een lieve tuin." : "Een paar! Nog " + (6 - matched) + " te gaan."); if (matched === 6) { done = true; sound("complete"); timers.later(ctx.onComplete, 1150); } return; } locked = true; sound("nope"); ctx.hint("Bijna โ€” deze twee horen niet bij elkaar."); const a = first; const b = second; timers.later(function () { [a, b].forEach(function (card) { if (!card) return; card.node.classList.remove("flipped"); card.node.setAttribute("aria-label", "Omgedraaide kaart"); }); first = null; locked = false; ctx.hint("Probeer het nog eens rustig."); }, 950); } deck.forEach(function (flower, index) { const node = el("button", "memory-card"); node.type = "button"; node.setAttribute("aria-label", "Omgedraaide kaart"); node.innerHTML = '' + '' + '" + ""; const card = { node: node, flower: flower, key: flower.type + "-" + flower.color, matched: false }; node.dataset.key = card.key; node.addEventListener("click", function () { flip(card); }); cards.push(card); grid.append(node); }); scene.append(counter, grid); stage.append(scene); ctx.hint("Draai twee kaartjes om en zoek dezelfde bloemen."); return { destroy: function () { timers.clear(); } }; } function buildLeaves(stage, ctx) { stage.classList.add("stage-leaves"); const timers = makeTimers(); const scene = el("div", "leaves-scene"); const counter = el("p", "scene-counter leaves-counter", "0 van 8 blaadjes"); const basket = el("div", "leaf-basket"); basket.innerHTML = '' + 'De mand'; scene.append(counter, basket); const spots = [ [12, 32], [30, 22], [50, 32], [70, 24], [18, 60], [40, 52], [62, 62], [83, 54] ]; const emojis = ["๐Ÿ‚", "๐Ÿ", "๐Ÿƒ", "๐Ÿ‚", "๐Ÿ", "๐Ÿƒ", "๐Ÿ‚", "๐Ÿ"]; const leaves = []; let collected = 0; let done = false; let dragging = null; function collect(leaf) { if (leaf.collected || done) return; leaf.collected = true; collected += 1; leaf.node.classList.add("collected"); leaf.node.disabled = true; const sceneRect = scene.getBoundingClientRect(); const basketRect = basket.getBoundingClientRect(); const basketX = ((basketRect.left + basketRect.width / 2) - sceneRect.left) / sceneRect.width * 100; const basketY = ((basketRect.top + basketRect.height / 2) - sceneRect.top) / sceneRect.height * 100; leaf.node.style.left = basketX + "%"; leaf.node.style.top = basketY + "%"; leaf.node.style.transform = "translate(-50%, -50%) scale(0.4) rotate(20deg)"; leaf.node.style.opacity = "0"; counter.textContent = collected + " van 8 blaadjes"; basket.classList.add("basket-full"); timers.later(function () { basket.classList.remove("basket-full"); }, 400); sound("place"); ctx.hint(collected === 8 ? "De mand zit vol herfstblaadjes!" : "Mooi, nog " + (8 - collected) + " blaadjes."); if (collected === 8) { done = true; sound("complete"); timers.later(ctx.onComplete, 1150); } } function startDrag(event, leaf) { if (done || leaf.collected) return; event.preventDefault(); dragging = leaf; leaf.node.classList.add("dragging"); if (leaf.node.setPointerCapture) { leaf.node.setPointerCapture(event.pointerId); } } function moveDrag(event, leaf) { if (dragging !== leaf || leaf.collected) return; const rect = scene.getBoundingClientRect(); const x = ((event.clientX - rect.left) / rect.width) * 100; const y = ((event.clientY - rect.top) / rect.height) * 100; leaf.node.style.left = Math.max(4, Math.min(96, x)) + "%"; leaf.node.style.top = Math.max(6, Math.min(94, y)) + "%"; } function endDrag(event, leaf) { if (dragging !== leaf) return; dragging = null; leaf.node.classList.remove("dragging"); if (leaf.collected) return; const basketRect = basket.getBoundingClientRect(); const inBasket = event.clientX >= basketRect.left && event.clientX <= basketRect.right && event.clientY >= basketRect.top && event.clientY <= basketRect.bottom; if (inBasket) { collect(leaf); } else { ctx.hint("Sleep het blaadje in de mand."); } } spots.forEach(function (spot, index) { const node = el("button", "leaf"); node.type = "button"; node.style.left = spot[0] + "%"; node.style.top = spot[1] + "%"; node.style.setProperty("--rot", (Math.random() * 50 - 25).toFixed(0) + "deg"); node.setAttribute("aria-label", "Herfstblad " + (index + 1)); node.innerHTML = '"; const leaf = { node: node, collected: false }; node.addEventListener("pointerdown", function (event) { startDrag(event, leaf); }); node.addEventListener("pointermove", function (event) { moveDrag(event, leaf); }); node.addEventListener("pointerup", function (event) { endDrag(event, leaf); }); node.addEventListener("pointercancel", function (event) { endDrag(event, leaf); }); node.addEventListener("keydown", function (event) { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); collect(leaf); } }); leaves.push(leaf); scene.append(node); }); stage.append(scene); ctx.hint("Sleep de blaadjes รฉรฉn voor รฉรฉn naar de mand."); return { destroy: function () { timers.clear(); } }; } const builders = { 1: buildGarden, 2: buildFireflies, 3: buildStars, 4: buildCake, 5: buildBouquet, 6: buildMemory, 7: buildLeaves }; function mount(id, stage, ctx) { stage.className = "stage"; stage.innerHTML = ""; const builder = builders[id]; if (!builder) { stage.textContent = "Dit level kon niet worden gevonden."; return { destroy: function () {} }; } const api = builder(stage, ctx) || {}; return { destroy: function () { try { if (typeof api.destroy === "function") api.destroy(); } finally { stage.innerHTML = ""; } } }; } return { mount: mount, meta: meta, count: Object.keys(meta).length, flowerSVG: flowerSVG }; })();