392 lines
19 KiB
JavaScript
392 lines
19 KiB
JavaScript
/**
|
||
* 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 };
|
||
|
||
})();
|