/** * minigames/spotdiff.js * πŸ” Fehler finden β€” Finde alle Unterschiede zwischen den zwei Bildern! */ window.MG_spotdiff = (function() { const ID = 'spotdiff'; const EMOJI = 'πŸ”'; const NAME = 'Fehler finden'; const DESC = 'Finde alle Unterschiede zwischen den zwei Bildern!'; const CONTROLS = 'Mausklick'; const MULTI = 1; // ── Bild-Sets (canvas-gezeichnete Szenen) ──────────────────────────────── // Jede Szene besteht aus drawA(ctx,W,H) und drawB(ctx,W,H), // plus einer Liste von Unterschied-Hotspots { x, y, r } (relativ, 0–1) const SCENES = [ { name: 'Bauernhof', drawA(ctx, W, H) { // Himmel ctx.fillStyle = '#7dd3fc'; ctx.fillRect(0, 0, W, H * 0.55); // Gras ctx.fillStyle = '#4ade80'; ctx.fillRect(0, H * 0.55, W, H * 0.45); // Sonne ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(W*0.15, H*0.18, H*0.1, 0, Math.PI*2); ctx.fill(); // Haus ctx.fillStyle = '#f87171'; ctx.fillRect(W*0.3, H*0.3, W*0.25, H*0.28); ctx.fillStyle = '#7f1d1d'; ctx.beginPath(); ctx.moveTo(W*0.27,H*0.3); ctx.lineTo(W*0.425,H*0.12); ctx.lineTo(W*0.58,H*0.3); ctx.fill(); // Fenster (2) ctx.fillStyle = '#bae6fd'; ctx.fillRect(W*0.34, H*0.38, W*0.06, H*0.07); ctx.fillStyle = '#bae6fd'; ctx.fillRect(W*0.45, H*0.38, W*0.06, H*0.07); // TΓΌr ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.39, H*0.46, W*0.05, H*0.12); // Baum (3 Kreise) ctx.fillStyle = '#16a34a'; ctx.beginPath(); ctx.arc(W*0.75, H*0.38, W*0.06, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#15803d'; ctx.beginPath(); ctx.arc(W*0.72, H*0.46, W*0.05, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#14532d'; ctx.beginPath(); ctx.arc(W*0.79, H*0.44, W*0.05, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.74, H*0.5, W*0.02, H*0.08); // Wolke ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(W*0.55, H*0.14, W*0.05, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(W*0.62, H*0.11, W*0.06, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(W*0.69, H*0.14, W*0.05, 0, Math.PI*2); ctx.fill(); // Vogel (klein, V-Form im Himmel) ctx.strokeStyle = '#1f2937'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(W*0.85, H*0.23); ctx.lineTo(W*0.88, H*0.20); ctx.lineTo(W*0.91, H*0.23); ctx.stroke(); }, drawB(ctx, W, H) { // Himmel β€” IDENTISCH zu A ctx.fillStyle = '#7dd3fc'; ctx.fillRect(0, 0, W, H * 0.55); // Gras ctx.fillStyle = '#4ade80'; ctx.fillRect(0, H * 0.55, W, H * 0.45); // Sonne (UNTERSCHIED 1: weiter oben) ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(W*0.15, H*0.10, H*0.1, 0, Math.PI*2); ctx.fill(); // Haus ctx.fillStyle = '#f87171'; ctx.fillRect(W*0.3, H*0.3, W*0.25, H*0.28); ctx.fillStyle = '#7f1d1d'; ctx.beginPath(); ctx.moveTo(W*0.27,H*0.3); ctx.lineTo(W*0.425,H*0.12); ctx.lineTo(W*0.58,H*0.3); ctx.fill(); // Fenster (UNTERSCHIED 2: zweites Fenster ist ROT statt blau) ctx.fillStyle = '#bae6fd'; ctx.fillRect(W*0.34, H*0.38, W*0.06, H*0.07); ctx.fillStyle = '#f87171'; ctx.fillRect(W*0.45, H*0.38, W*0.06, H*0.07); // TΓΌr (UNTERSCHIED 3: fehlt β€” Wand bleibt durchgehend rot) // Baum (UNTERSCHIED 4: kleiner) ctx.fillStyle = '#16a34a'; ctx.beginPath(); ctx.arc(W*0.75, H*0.42, W*0.04, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#15803d'; ctx.beginPath(); ctx.arc(W*0.72, H*0.48, W*0.035, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#14532d'; ctx.beginPath(); ctx.arc(W*0.79, H*0.46, W*0.035, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.74, H*0.5, W*0.02, H*0.08); // Wolke (UNTERSCHIED 5: fehlt) // Vogel β€” bleibt ctx.strokeStyle = '#1f2937'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(W*0.85, H*0.23); ctx.lineTo(W*0.88, H*0.20); ctx.lineTo(W*0.91, H*0.23); ctx.stroke(); }, spots: [ { x: 0.15, y: 0.10, r: 0.10, label: 'Sonne (weiter oben)' }, { x: 0.48, y: 0.41, r: 0.06, label: 'Rotes Fenster' }, { x: 0.42, y: 0.52, r: 0.05, label: 'Fehlende TΓΌr' }, { x: 0.75, y: 0.44, r: 0.08, label: 'Kleinerer Baum' }, { x: 0.62, y: 0.13, r: 0.10, label: 'Fehlende Wolke' }, ], }, // ── Szene 2: STRAND ────────────────────────────────────────────────── { name: 'Strand', drawA(ctx, W, H) { // Himmel ctx.fillStyle = '#7dd3fc'; ctx.fillRect(0, 0, W, H * 0.6); // Meer ctx.fillStyle = '#0e7490'; ctx.fillRect(0, H * 0.55, W, H * 0.12); // Sand ctx.fillStyle = '#fde68a'; ctx.fillRect(0, H * 0.67, W, H * 0.33); // Sonne (oben rechts) ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(W*0.8, H*0.18, H*0.08, 0, Math.PI*2); ctx.fill(); // Palme (links) ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.13, H*0.4, W*0.022, H*0.4); ctx.fillStyle = '#15803d'; for (let i = 0; i < 5; i++) { ctx.save(); ctx.translate(W*0.14, H*0.4); ctx.rotate((i - 2) * 0.35); ctx.beginPath(); ctx.ellipse(0, -H*0.04, W*0.09, H*0.025, 0, 0, Math.PI*2); ctx.fill(); ctx.restore(); } // KokosnΓΌsse (2) ctx.fillStyle = '#451a03'; ctx.beginPath(); ctx.arc(W*0.155, H*0.41, W*0.012, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(W*0.13, H*0.43, W*0.012, 0, Math.PI*2); ctx.fill(); // Sandburg (Mitte unten) ctx.fillStyle = '#d97706'; ctx.fillRect(W*0.42, H*0.74, W*0.18, H*0.14); // TΓΌrme links + rechts + Mitte ctx.fillRect(W*0.41, H*0.7, W*0.04, H*0.04); ctx.fillRect(W*0.49, H*0.66, W*0.04, H*0.08); ctx.fillRect(W*0.57, H*0.7, W*0.04, H*0.04); // Flagge am mittleren Turm ctx.fillStyle = '#dc2626'; ctx.beginPath(); ctx.moveTo(W*0.51, H*0.62); ctx.lineTo(W*0.55, H*0.64); ctx.lineTo(W*0.51, H*0.66); ctx.fill(); ctx.strokeStyle = '#1f2937'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(W*0.51, H*0.62); ctx.lineTo(W*0.51, H*0.7); ctx.stroke(); // Schiff am Horizont (rechts) ctx.fillStyle = '#1f2937'; ctx.fillRect(W*0.78, H*0.58, W*0.08, H*0.025); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.moveTo(W*0.82, H*0.58); ctx.lineTo(W*0.82, H*0.5); ctx.lineTo(W*0.85, H*0.58); ctx.fill(); // MΓΆwe (V) ctx.strokeStyle = '#1f2937'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(W*0.36, H*0.22); ctx.lineTo(W*0.4, H*0.24); ctx.lineTo(W*0.44, H*0.22); ctx.stroke(); }, drawB(ctx, W, H) { // Himmel β€” IDENTISCH zu A ctx.fillStyle = '#7dd3fc'; ctx.fillRect(0, 0, W, H * 0.6); // Meer ctx.fillStyle = '#0e7490'; ctx.fillRect(0, H * 0.55, W, H * 0.12); // Sand ctx.fillStyle = '#fde68a'; ctx.fillRect(0, H * 0.67, W, H * 0.33); // Sonne (UNTERSCHIED 1: weiter mittig) ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(W*0.5, H*0.18, H*0.08, 0, Math.PI*2); ctx.fill(); // Palme (links) ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.13, H*0.4, W*0.022, H*0.4); ctx.fillStyle = '#15803d'; for (let i = 0; i < 5; i++) { ctx.save(); ctx.translate(W*0.14, H*0.4); ctx.rotate((i - 2) * 0.35); ctx.beginPath(); ctx.ellipse(0, -H*0.04, W*0.09, H*0.025, 0, 0, Math.PI*2); ctx.fill(); ctx.restore(); } // KokosnΓΌsse (UNTERSCHIED 2: nur EINE statt zwei) ctx.fillStyle = '#451a03'; ctx.beginPath(); ctx.arc(W*0.155, H*0.41, W*0.012, 0, Math.PI*2); ctx.fill(); // Sandburg ctx.fillStyle = '#d97706'; ctx.fillRect(W*0.42, H*0.74, W*0.18, H*0.14); ctx.fillRect(W*0.41, H*0.7, W*0.04, H*0.04); ctx.fillRect(W*0.49, H*0.66, W*0.04, H*0.08); ctx.fillRect(W*0.57, H*0.7, W*0.04, H*0.04); // Flagge fehlt (UNTERSCHIED 3) ctx.strokeStyle = '#1f2937'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(W*0.51, H*0.62); ctx.lineTo(W*0.51, H*0.7); ctx.stroke(); // Schiff fehlt (UNTERSCHIED 4) // MΓΆwe fehlt (UNTERSCHIED 5) }, spots: [ { x: 0.50, y: 0.18, r: 0.10, label: 'Sonne (weiter mittig)' }, { x: 0.155, y: 0.43, r: 0.04, label: 'Fehlende Kokosnuss' }, { x: 0.52, y: 0.64, r: 0.05, label: 'Fehlende Flagge' }, { x: 0.82, y: 0.55, r: 0.08, label: 'Fehlendes Schiff' }, { x: 0.40, y: 0.23, r: 0.07, label: 'Fehlende MΓΆwe' }, ], }, // ── Szene 3: WELTRAUM ───────────────────────────────────────────────── { name: 'Weltraum', drawA(ctx, W, H) { // Hintergrund: tiefes Nachtblau ctx.fillStyle = '#0c0a2e'; ctx.fillRect(0, 0, W, H); // Sterne (10 StΓΌck) ctx.fillStyle = '#fff'; const stars = [[0.10,0.15],[0.22,0.32],[0.35,0.10],[0.48,0.25],[0.60,0.12], [0.72,0.30],[0.85,0.18],[0.18,0.55],[0.90,0.50],[0.05,0.75]]; stars.forEach(([sx,sy])=>{ ctx.beginPath(); ctx.arc(sx*W, sy*H, 2, 0, Math.PI*2); ctx.fill(); }); // Großer Planet rechts mit Ringen (Saturn-Style) const px = W*0.78, py = H*0.45, pr = H*0.13; ctx.fillStyle = '#fb923c'; ctx.beginPath(); ctx.arc(px, py, pr, 0, Math.PI*2); ctx.fill(); // Ring ctx.strokeStyle = '#fde68a'; ctx.lineWidth = 4; ctx.beginPath(); ctx.ellipse(px, py, pr*1.7, pr*0.35, 0.3, 0, Math.PI*2); ctx.stroke(); // Rakete (links, schrΓ€g) ctx.save(); ctx.translate(W*0.2, H*0.6); ctx.rotate(-0.5); ctx.fillStyle = '#e5e7eb'; ctx.fillRect(-W*0.025, -H*0.08, W*0.05, H*0.16); // Spitze ctx.fillStyle = '#dc2626'; ctx.beginPath(); ctx.moveTo(-W*0.025, -H*0.08); ctx.lineTo(0, -H*0.13); ctx.lineTo(W*0.025, -H*0.08); ctx.fill(); // Fenster ctx.fillStyle = '#3b82f6'; ctx.beginPath(); ctx.arc(0, -H*0.03, W*0.012, 0, Math.PI*2); ctx.fill(); // Flossen ctx.fillStyle = '#dc2626'; ctx.beginPath(); ctx.moveTo(-W*0.025, H*0.04); ctx.lineTo(-W*0.05, H*0.08); ctx.lineTo(-W*0.025, H*0.08); ctx.fill(); ctx.beginPath(); ctx.moveTo(W*0.025, H*0.04); ctx.lineTo(W*0.05, H*0.08); ctx.lineTo(W*0.025, H*0.08); ctx.fill(); // Flamme ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.moveTo(-W*0.015, H*0.08); ctx.lineTo(0, H*0.16); ctx.lineTo(W*0.015, H*0.08); ctx.fill(); ctx.restore(); // Astronaut (Mitte unten) ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(W*0.48, H*0.75, H*0.05, 0, Math.PI*2); ctx.fill(); // Helm ctx.fillStyle = '#1f2937'; ctx.beginPath(); ctx.ellipse(W*0.48, H*0.75, H*0.03, H*0.025, 0, 0, Math.PI*2); ctx.fill(); // Visier ctx.fillStyle = '#e5e7eb'; ctx.fillRect(W*0.45, H*0.78, W*0.06, H*0.1); // KΓΆrper // UFO (oben links) ctx.fillStyle = '#9ca3af'; ctx.beginPath(); ctx.ellipse(W*0.15, H*0.25, W*0.05, H*0.018, 0, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#67e8f9'; ctx.beginPath(); ctx.ellipse(W*0.15, H*0.225, W*0.025, H*0.018, 0, Math.PI, 2*Math.PI); ctx.fill(); }, drawB(ctx, W, H) { // Hintergrund: GLEICH wie A (kein Hintergrund-Unterschied) ctx.fillStyle = '#0c0a2e'; ctx.fillRect(0, 0, W, H); // Sterne β€” eine fehlt (UNTERSCHIED 1: nur 9 statt 10, der bei 0.48/0.25 fehlt) ctx.fillStyle = '#fff'; const stars = [[0.10,0.15],[0.22,0.32],[0.35,0.10],[0.60,0.12], [0.72,0.30],[0.85,0.18],[0.18,0.55],[0.90,0.50],[0.05,0.75]]; stars.forEach(([sx,sy])=>{ ctx.beginPath(); ctx.arc(sx*W, sy*H, 2, 0, Math.PI*2); ctx.fill(); }); // Planet (UNTERSCHIED 2: kein Ring) const px = W*0.78, py = H*0.45, pr = H*0.13; ctx.fillStyle = '#fb923c'; ctx.beginPath(); ctx.arc(px, py, pr, 0, Math.PI*2); ctx.fill(); // Rakete (UNTERSCHIED 3: keine Flamme) ctx.save(); ctx.translate(W*0.2, H*0.6); ctx.rotate(-0.5); ctx.fillStyle = '#e5e7eb'; ctx.fillRect(-W*0.025, -H*0.08, W*0.05, H*0.16); ctx.fillStyle = '#dc2626'; ctx.beginPath(); ctx.moveTo(-W*0.025, -H*0.08); ctx.lineTo(0, -H*0.13); ctx.lineTo(W*0.025, -H*0.08); ctx.fill(); ctx.fillStyle = '#3b82f6'; ctx.beginPath(); ctx.arc(0, -H*0.03, W*0.012, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#dc2626'; ctx.beginPath(); ctx.moveTo(-W*0.025, H*0.04); ctx.lineTo(-W*0.05, H*0.08); ctx.lineTo(-W*0.025, H*0.08); ctx.fill(); ctx.beginPath(); ctx.moveTo(W*0.025, H*0.04); ctx.lineTo(W*0.05, H*0.08); ctx.lineTo(W*0.025, H*0.08); ctx.fill(); ctx.restore(); // Astronaut (UNTERSCHIED 4: ROTER Anzug statt grau) ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(W*0.48, H*0.75, H*0.05, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#1f2937'; ctx.beginPath(); ctx.ellipse(W*0.48, H*0.75, H*0.03, H*0.025, 0, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#dc2626'; ctx.fillRect(W*0.45, H*0.78, W*0.06, H*0.1); // UFO (UNTERSCHIED 5: an anderer Position - weiter rechts oben) ctx.fillStyle = '#9ca3af'; ctx.beginPath(); ctx.ellipse(W*0.42, H*0.15, W*0.05, H*0.018, 0, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#67e8f9'; ctx.beginPath(); ctx.ellipse(W*0.42, H*0.125, W*0.025, H*0.018, 0, Math.PI, 2*Math.PI); ctx.fill(); }, spots: [ { x: 0.48, y: 0.25, r: 0.05, label: 'Fehlender Stern' }, { x: 0.78, y: 0.45, r: 0.13, label: 'Fehlender Ring' }, { x: 0.21, y: 0.74, r: 0.06, label: 'Fehlende Flamme' }, { x: 0.48, y: 0.83, r: 0.06, label: 'Anderer Anzug (rot)' }, { x: 0.42, y: 0.15, r: 0.07, label: 'UFO an anderer Stelle' }, ], }, ]; // Pro Aufruf andere Szene (rotiert) β€” so bekommt jeder Spielzug bei multi:3 eine neue Szene let _sceneCounter = 0; function run(wrap, W, H, cfg, onDone) { const scene = SCENES[_sceneCounter % SCENES.length]; _sceneCounter++; const theme = cfg.theme || { primary: '#84cc16' }; // Zwei Canvas nebeneinander const halfW = Math.floor((W - 8) / 2); const imgH = H - 64; const container = document.createElement('div'); container.style.cssText = 'position:relative;width:100%;'; wrap.appendChild(container); const canvasA = document.createElement('canvas'); canvasA.width = halfW; canvasA.height = imgH; canvasA.style.cssText = `display:inline-block;border-radius:8px;cursor:default;`; const canvasB = document.createElement('canvas'); canvasB.width = halfW; canvasB.height = imgH; canvasB.style.cssText = `display:inline-block;border-radius:8px;cursor:crosshair;margin-left:8px;`; container.appendChild(canvasA); container.appendChild(canvasB); // HUD-Canvas const hud = document.createElement('canvas'); hud.width = W; hud.height = 48; hud.style.display = 'block'; container.appendChild(hud); const ctxA = canvasA.getContext('2d'); const ctxB = canvasB.getContext('2d'); const ctxH = hud.getContext('2d'); scene.drawA(ctxA, halfW, imgH); scene.drawB(ctxB, halfW, imgH); let found = new Set(); let marks = []; // { x, y, ok } let stopped = false; let endTimer; function drawMarks() { scene.drawB(ctxB, halfW, imgH); // neu zeichnen marks.forEach(m => { ctxB.strokeStyle = m.ok ? '#22c55e' : '#ef4444'; ctxB.lineWidth = 3; ctxB.beginPath(); ctxB.arc(m.x, m.y, 18, 0, Math.PI * 2); ctxB.stroke(); if (m.ok) { ctxB.fillStyle = 'rgba(34,197,94,0.25)'; ctxB.fill(); } }); } function drawHUD() { ctxH.clearRect(0, 0, W, 48); MGAPI.text(ctxH, `πŸ” ${found.size} / ${scene.spots.length} Unterschiede gefunden`, W/2, 16, { size: 13, color: theme.primary }); MGAPI.text(ctxH, 'Klicke auf die Unterschiede im rechten Bild', W/2, 36, { size: 11, color: 'rgba(255,255,255,0.4)' }); } function hitsSpot(s, mx, my) { if (s.shape === 'rect') { const x = s.x * halfW, y = s.y * imgH; const w = s.w * halfW, h = s.h * imgH; return mx >= x && mx < x + w && my >= y && my < y + h; } const sx = s.x * halfW, sy = s.y * imgH; const d = Math.sqrt((mx - sx) ** 2 + (my - sy) ** 2); return d < s.r * Math.min(halfW, imgH); } canvasB.addEventListener('click', e => { if (found.size >= scene.spots.length) return; const rect = canvasB.getBoundingClientRect(); const mx = (e.clientX - rect.left) * (canvasB.width / rect.width / (window.devicePixelRatio || 1)); const my = (e.clientY - rect.top) * (canvasB.height / rect.height); // Reihenfolge = PrioritΓ€t: erste passende Spot gewinnt (radial-Spots vor rect-Catchall) let hit = null; for (let i = 0; i < scene.spots.length; i++) { if (found.has(i)) continue; if (hitsSpot(scene.spots[i], mx, my)) { hit = i; break; } } if (hit !== null) { found.add(hit); // Treffer-Marker an Klick-Position (statt Spot-Mitte) β€” funktioniert fΓΌr Kreis UND Rechteck marks.push({ x: mx, y: my, ok: true }); // Auch in Bild A markieren (gleiche Klick-Position) ctxA.strokeStyle = '#22c55e'; ctxA.lineWidth = 3; ctxA.beginPath(); ctxA.arc(mx, my, 18, 0, Math.PI * 2); ctxA.stroke(); ctxA.fillStyle = 'rgba(34,197,94,0.2)'; ctxA.fill(); } else { marks.push({ x: mx, y: my, ok: false }); setTimeout(() => { marks = marks.filter(m => m.ok); drawMarks(); drawHUD(); }, 600); } drawMarks(); drawHUD(); if (found.size >= scene.spots.length && !endTimer) { endTimer = setTimeout(() => onDone(true), 900); // Overlay ctxH.fillStyle = 'rgba(16,185,129,0.9)'; MGAPI.roundRect(ctxH, W/2-120, 4, 240, 38, 8, 'rgba(16,185,129,0.9)', null); MGAPI.text(ctxH, 'πŸŽ‰ Alle Unterschiede gefunden!', W/2, 24, { size: 14, family: "'Fredoka One',cursive", color: '#fff' }); } }); drawHUD(); return { stop() { stopped = true; clearTimeout(endTimer); }, }; } function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; })();