From 685b4700f8a9547c592df100f4f2e2751cf2b2c4 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 17 Jul 2026 01:42:20 +0200 Subject: [PATCH] =?UTF-8?q?Glossar:=204=20erkl=C3=A4rende=20Widgets=20+=20?= =?UTF-8?q?2=20Eintr=C3=A4ge=20anschaulicher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widgets: Ernährungspyramide (US-1992 vs DACH-2024 vs Denkfehler), Kakao-Bohne (Schoko-Fließband + Mischverhältnis→Sorte/kaputte Masse), Klimazone (Bänder Äquator→Pol mit Beispielpflanzen), Monsun (Monatsregen-Balkendiagramm Mumbai + Wien-Vergleich, Quelle IMD). Texte: off-season-import (Winter-Erdbeere ~10× CO₂, Schiff vs Flug) + lebensmittelverschwendung (70 kg = ~6 Wochen Essen; 700 Mio. Hungernde) — greifbare Vergleiche. Co-Authored-By: Claude Fable 5 --- .../2026-07-17-glossar-anschaulich.sql | 2 + .../js/glossar-experiments-klimazonen.js | 198 +++++++++++ App/assets/js/glossar-experiments-monsun.js | 194 +++++++++++ App/assets/js/glossar-experiments-pyramide.js | 182 +++++++++++ App/assets/js/glossar-experiments-schoko.js | 309 ++++++++++++++++++ App/pages/glossar.php | 4 + 6 files changed, 889 insertions(+) create mode 100644 App/Don_t_Deploy/2026-07-17-glossar-anschaulich.sql create mode 100644 App/assets/js/glossar-experiments-klimazonen.js create mode 100644 App/assets/js/glossar-experiments-monsun.js create mode 100644 App/assets/js/glossar-experiments-pyramide.js create mode 100644 App/assets/js/glossar-experiments-schoko.js diff --git a/App/Don_t_Deploy/2026-07-17-glossar-anschaulich.sql b/App/Don_t_Deploy/2026-07-17-glossar-anschaulich.sql new file mode 100644 index 0000000..8820654 --- /dev/null +++ b/App/Don_t_Deploy/2026-07-17-glossar-anschaulich.sql @@ -0,0 +1,2 @@ +UPDATE glossar SET short=CONVERT(FROM_BASE64('T2JzdCBvZGVyIEdlbcO8c2UgYXXDn2VyaGFsYiBzZWluZXIgU2Fpc29uIOKAkyBvZnQgZWluZ2VmbG9nZW4gb2RlciBhdXMgYmVoZWl6dGVuIEdld8OkY2hzaMOkdXNlcm4sIG1pdCB2aWVsIGjDtmhlcmVyIEtsaW1hYmlsYW56Lg==') USING utf8mb4), text=CONVERT(FROM_BASE64('4oCeT2ZmLVNlYXNvbuKAnCBoZWnDn3Qg4oCeYXXDn2VyaGFsYiBkZXIgU2Fpc29u4oCcLiBEYW1pdCBtZWludCBtYW4gT2JzdCB1bmQgR2Vtw7xzZSwgZGFzIGJlaSB1bnMgZ2VyYWRlIGdhciBuaWNodCB3w6RjaHN0IOKAkyB6dW0gQmVpc3BpZWwgRXJkYmVlcmVuIGltIERlemVtYmVyIG9kZXIgV2VpbnRyYXViZW4gaW0gRmVicnVhci4gRGFtaXQgc2llIHRyb3R6ZGVtIGltIFJlZ2FsIGxpZWdlbiwga29tbWVuIHNpZSBhdXMgZmVybmVuLCB3YXJtZW4gTMOkbmRlcm4gb2RlciBhdXMgYmVoZWl6dGVuIEdld8OkY2hzaMOkdXNlcm4uCgpEYXMgbWFjaHQgZsO8cnMgS2xpbWEgZWluZW4gcmllc2lnZW4gVW50ZXJzY2hpZWQuIEVpbmUgaGVpbWlzY2hlIEVyZGJlZXJlIGltIE1haSB2b20gRmVsZCB2ZXJ1cnNhY2h0IHJ1bmQgMCwzIGtnIENP4oKCIHBybyBLaWxvLiBEaWVzZWxiZSBNZW5nZSBXaW50ZXItRXJkYmVlcmVuIGF1cyBkZW0gYmVoZWl6dGVuIEdld8OkY2hzaGF1cyBvZGVyIG1pdCBkZW0gRmx1Z3pldWcgZWluZ2VmbG9nZW4ga29tbXQgYXVmIHJ1bmQgMyDigJMgNCBrZyDigJMgYWxzbyBldHdhIHplaG5tYWwgc28gdmllbCAoSUZFVSAvIFVtd2VsdGJ1bmRlc2FtdCkuCgpXaWNodGlnIGlzdCBkYWJlaSB3ZW5pZ2VyIGRpZSByZWluZSBFbnRmZXJudW5nLCBzb25kZXJuIFdJRSBldHdhcyByZWlzdC4gTWl0IGRlbSBTY2hpZmYgaXN0IHNvZ2FyIGVpbiB3ZWl0ZXIgV2VnIG9mdCBzcGFyc2FtOiBFaW4gQXBmZWwgYXVzIFPDvGRhZnJpa2EgcGVyIFNjaGlmZiBpc3Qga2F1bSBzY2hsZWNodGVyIGFscyBlaW4gaGVpbWlzY2hlciBBcGZlbCwgZGVyIGJlaSB1bnMgZGVuIFdpbnRlciDDvGJlciBpbSBLw7xobGxhZ2VyIGxhZyAoYmVpZGUgcnVuZCAwLDMg4oCTIDAsNCBrZyBDT+KCgiBwcm8gS2lsbykuIFJpY2h0aWcgdGV1ZXIgZsO8cnMgS2xpbWEgd2lyZCBlcyBpbSBGbHVnemV1ZyDigJMgc28gd2VyZGVuIEJlZXJlbiBvZGVyIFdlaW50cmF1YmVuIG1pdHRlbiBpbSBXaW50ZXIgenUgZWNodGVuIEtsaW1hc8O8bmRlcm4uCgpEaWUgZWluZmFjaHN0ZSBSZWdlbDogSXNzLCB3YXMgZ2VyYWRlIGJlaSB1bnMgU2Fpc29uIGhhdC4gRWluIFNhaXNvbmthbGVuZGVyIHplaWd0LCB3YW5uIHdhcyByZWlmIGlzdC4=') USING utf8mb4), short_easy=CONVERT(FROM_BASE64('T2JzdCwgZGFzIGJlaSB1bnMgZ2VyYWRlIG5pY2h0IHfDpGNoc3Qg4oCTIHp1bSBCZWlzcGllbCBFcmRiZWVyZW4gaW0gV2ludGVyLg==') USING utf8mb4), text_easy=CONVERT(FROM_BASE64('T2ZmLVNlYXNvbiBoZWnDn3Q6IGF1w59lcmhhbGIgZGVyIFNhaXNvbi4KWnVtIEJlaXNwaWVsIEVyZGJlZXJlbiBpbSBXaW50ZXIuCkRhbm4gd2FjaHNlbiBzaWUgYmVpIHVucyBuaWNodC4KU2llIGtvbW1lbiB2b24gd2VpdCBoZXIgb2RlciBhdXMgZGVtIHdhcm1lbiBHbGFzaGF1cy4KCkRhcyBpc3Qgc2NobGVjaHQgZsO8cnMgS2xpbWE6CkVpbmUgRXJkYmVlcmUgaW0gU29tbWVyIHZvbSBGZWxkIG1hY2h0IHdlbmlnIENP4oKCLgpFaW5lIEVyZGJlZXJlIGltIFdpbnRlciBtYWNodCBmYXN0IHplaG5tYWwgc28gdmllbC4KCkFtIGJlc3RlbiBpc3N0IGR1IE9ic3QsIHdlbm4gZXMgYmVpIHVucyBTYWlzb24gaGF0Lg==') USING utf8mb4) WHERE key_slug='off-season-import' COLLATE utf8mb4_unicode_ci; +UPDATE glossar SET short=CONVERT(FROM_BASE64('R3V0ZXMsIG5vY2ggZXNzYmFyZXMgRXNzZW4gbGFuZGV0IGltIE3DvGxsIOKAkyBpbiDDlnN0ZXJyZWljaCBydW5kIDcwIGtnIHBybyBQZXJzb24gdW5kIEphaHIu') USING utf8mb4), text=CONVERT(FROM_BASE64('U3RlbGwgZGlyIHZvciwgdm9uIHplaG4gdm9sbGVuIEphdXNlbmJyb3RlbiB3aXJmc3QgZHUgamVkZW4gVGFnIGVpbmVzIHVuZ2VnZXNzZW4gd2VnLiBVbmdlZsOkaHIgc28gdmllbCBndXRlcyBFc3NlbiBsYW5kZXQgYmVpIHVucyBpbSBNw7xsbCwgb2J3b2hsIG1hbiBlcyBub2NoIGVzc2VuIGvDtm5udGUg4oCTIGRhcyBuZW5udCBtYW4gTGViZW5zbWl0dGVsdmVyc2Nod2VuZHVuZy4gU2llIHBhc3NpZXJ0IMO8YmVyYWxsOiB6dSBIYXVzZSwgaW0gU3VwZXJtYXJrdCwgaW4gS2FudGluZW4gdW5kIEdhc3Row6R1c2Vybi4KCkluIMOWc3RlcnJlaWNoIHdpcmZ0IGplZGUgUGVyc29uIHBybyBKYWhyIHJ1bmQgNzAga2cgbm9jaCBnZW5pZcOfYmFyZSBMZWJlbnNtaXR0ZWwgd2VnIChXV0Ygw5ZzdGVycmVpY2gpLiBXaWUgdmllbCBpc3QgZGFzPyA3MCBrZyB3aWVndCB1bmdlZsOkaHIgZWluIGVyd2FjaHNlbmVyIE1lbnNjaC4gVW5kIGVpbiBNZW5zY2ggaXNzdCBhbSBUYWcgZXR3YSAxLDUga2cgTmFocnVuZyDigJMgNzAga2cgc2luZCBhbHNvIGRhcyBFc3NlbiB2b24gZmFzdCBzZWNocyBXb2NoZW4uIEplZGUgdW5kIGplZGVyIHZvbiB1bnMgd2lyZnQgc29taXQgamVkZXMgSmFociBydW5kIHNlY2hzIFdvY2hlbiBFc3NlbiBpbiBkZW4gTcO8bGwuCgpEYXMgaXN0IGRvcHBlbHQgc2NoYWRlLiBHbGVpY2h6ZWl0aWcgaGFiZW4gd2VsdHdlaXQgw7xiZXIgNzAwIE1pbGxpb25lbiBNZW5zY2hlbiB6dSB3ZW5pZyB6dSBlc3NlbiAoRkFPKS4gVmllbGVzIHZvbiBkZW0sIHdhcyBiZWkgdW5zIHdlZ2dld29yZmVuIHdpcmQsIHfDpHJlIGbDvHIgc2llIGVpbmUgcmljaHRpZ2UgTWFobHplaXQuIFVuZCBpbSBFc3NlbiBzdGVja3QgdmllbCBtZWhyIGFscyBkYXMgRXNzZW4gc2VsYnN0OiBkYXMgV2Fzc2VyIHp1bSBBbmJhdWVuLCBkYXMgRmVsZCwgZGllIEFyYmVpdCBkZXIgQsOkdWVyaW5uZW4gdW5kIEJhdWVybiB1bmQgZGVyIHdlaXRlIFRyYW5zcG9ydCDigJMgZGllIGdhbnplIFJlaXNlIHdhciB1bXNvbnN0LgoKRGFnZWdlbiBrYW5uIGplZGUgdW5kIGplZGVyIGV0d2FzIHR1bjogdm9yIGRlbSBFaW5rYXVmIGVpbmUgTGlzdGUgc2NocmVpYmVuIHVuZCBuaWNodCBodW5ncmlnIGxvc3ppZWhlbiwgUmVzdGUgYW0gbsOkY2hzdGVuIFRhZyBlc3NlbiB1bmQgd2lzc2VuLCBkYXNzIOKAnm1pbmRlc3RlbnMgaGFsdGJhciBiaXPigJwgbnVyIOKAnmFtIGJlc3RlbiBiaXPigJwgYmVkZXV0ZXQuIFZpZWxlIEpvZ2h1cnRzLCBOdWRlbG4gb2RlciBLZWtzZSBzaW5kIGF1Y2ggZGFuYWNoIG5vY2ggZ3V0IOKAkyBSaWVjaGVuIHVuZCBQcm9iaWVyZW4gdmVycsOkdCBtZWhyIGFscyBkYXMgRGF0dW0u') USING utf8mb4), short_easy=CONVERT(FROM_BASE64('R3V0ZXMgRXNzZW4gbGFuZGV0IGltIE3DvGxsIOKAkyBpbiDDlnN0ZXJyZWljaCBydW5kIDcwIEtpbG8gcHJvIFBlcnNvbiBpbSBKYWhyLg==') USING utf8mb4), text_easy=CONVERT(FROM_BASE64('T2Z0IGxhbmRldCBndXRlcyBFc3NlbiBpbSBNw7xsbC4KSW4gw5ZzdGVycmVpY2ggc2luZCBkYXMgZXR3YSA3MCBLaWxvIHBybyBQZXJzb24gaW0gSmFoci4KNzAgS2lsbyDigJMgc28gdmllbCB3aWVndCBlaW4gZXJ3YWNoc2VuZXIgTWVuc2NoLgpEYXZvbiBrw7ZubnRlIGVpbiBNZW5zY2ggZmFzdCBzZWNocyBXb2NoZW4gbGFuZyBlc3Nlbi4KCkdsZWljaHplaXRpZyBoYWJlbiB2aWVsZSBNZW5zY2hlbiBhdWYgZGVyIFdlbHQgenUgd2VuaWcgenUgZXNzZW4uClVuc2VyIE3DvGxsIHfDpHJlIGbDvHIgc2llIGVpbmUgZ3V0ZSBNYWhsemVpdC4KCkltIEVzc2VuIHN0ZWNrdCB2aWVsIEFyYmVpdCB1bmQgV2Fzc2VyLgpCZXNzZXI6IE51ciBrYXVmZW4sIHdhcyBtYW4gd2lya2xpY2ggYnJhdWNodC4KUmVzdGUgYW0gbsOkY2hzdGVuIFRhZyBlc3Nlbi4=') USING utf8mb4) WHERE key_slug='lebensmittelverschwendung' COLLATE utf8mb4_unicode_ci; diff --git a/App/assets/js/glossar-experiments-klimazonen.js b/App/assets/js/glossar-experiments-klimazonen.js new file mode 100644 index 0000000..841b9d4 --- /dev/null +++ b/App/assets/js/glossar-experiments-klimazonen.js @@ -0,0 +1,198 @@ +/** + * Glossar-Experimente — Klimazonen. + * + * Registrierung: window.GlossarExperiments['klimazone'] = function (root) { … } + * Der Renderer (glossar.php) hängt einen leeren Container ein und ruft die + * Funktion mit diesem Container als einzigem Argument auf. + * + * Nur bestehende .ge-*-Klassen aus glossar.php verwenden, kein neues CSS. + */ + +(function () { + 'use strict'; + + const NS = 'http://www.w3.org/2000/svg'; + const svgEl = (tag, attrs, parent) => { + const el = document.createElementNS(NS, tag); + if (attrs) for (const k in attrs) el.setAttribute(k, attrs[k]); + if (parent) parent.appendChild(el); + return el; + }; + + // ===================================================================== + // Experiment: Klimazonen der Erde — Querschnitt Äquator → Pol + // Bänder anklicken → Temperatur, Regen und was dort wächst. + // ===================================================================== + function climateZones(root) { + // Bänder von OBEN (Pol, kalt) nach UNTEN (Äquator, heiß). + // y-Werte passen zu viewBox 0 0 600 340. + const zones = [ + { + key: 'polar', + name: 'Polargebiete', + where: 'Arktis · Antarktis', + temp: '−30 – 0 °C', + rain: 'sehr wenig, meist Schnee', + grows: 'kein Ackerbau — zu kalt', + emoji: '🧊', + color: '#dbe9ee', text: '#2a4a55', border: '#9fbcc6', + state: 'now' + }, + { + key: 'boreal', + name: 'Kalte Zone (Nadelwald)', + where: 'Skandinavien · Kanada', + temp: '0 – 5 °C im Jahr', + rain: 'mittel, lange kalte Winter', + grows: 'Nadelwald, Beeren — kaum Ackerbau', + emoji: '🌲', + color: '#5a8a8f', text: '#ffffff', border: '#3d6b70', + state: 'now' + }, + { + key: 'temperate', + name: 'Gemäßigte Zone', + where: 'Österreich · Mitteleuropa', + temp: '8 – 12 °C im Jahr', + rain: '600 – 1.000 mm, 4 Jahreszeiten', + grows: 'Äpfel, Getreide, Erdäpfel', + emoji: '🍎', + color: '#4a7c4e', text: '#ffffff', border: '#356139', + state: 'good', + home: true + }, + { + key: 'subtropic', + name: 'Subtropen', + where: 'Mittelmeer · Kalifornien', + temp: '15 – 22 °C im Jahr', + rain: 'trockene heiße Sommer', + grows: 'Oliven, Zitrusfrüchte, Mandeln', + emoji: '🍊', + color: '#e8c547', text: '#4a3d0a', border: '#c9a516', + state: 'warn' + }, + { + key: 'tropic', + name: 'Tropen (Äquator)', + where: 'heiß und feucht', + temp: '24 – 28 °C, immer warm', + rain: '1.500 – 3.000 mm, sehr viel Regen', + grows: 'Kakao, Banane, Kaffee, Reis', + emoji: '🍌', + color: '#2e7d4f', text: '#ffffff', border: '#1f5a38', + state: 'warn' + } + ]; + + root.innerHTML = ` +
+
+

🌍 Die Klimazonen der Erde

+

Von unten (Äquator, viel Sonne) nach oben (Pol, wenig Sonne) wird es kühler. Tippe auf ein Band und schau, was dort wächst.

+
+ +
+
Tippe auf ein Band …
+
Wo etwas wächst, hängt von Wärme und Regen ab.
+
+

Einteilung nach den Klimazonen der Köppen-Geiger-Klassifikation; Temperatur- und Niederschlagsbereiche sind grobe kindgerechte Richtwerte. Quelle: Klimadiagramme & Köppen-Geiger-Klassifikation.

+
+ `; + + const svg = root.querySelector('.ge-gh-svg'); + const info = root.querySelector('#ge-kz-info'); + const keyPill = root.querySelector('#ge-kz-key'); + if (!svg || !info || !keyPill) return; + + const BAND_X = 92; + const BAND_W = 466; + const BAND_H = 54; + const TOP = 26; + + // Linke Achse: Pol oben → Äquator unten, mit Sonnen-Symbol. + const axisTop = TOP; + const axisBot = TOP + zones.length * BAND_H; + svgEl('line', { x1: 60, y1: axisTop, x2: 60, y2: axisBot, stroke: '#4a4a4a', 'stroke-width': '1.5' }, svg); + // Pfeilspitzen (oben = kalt, unten = warm) + svgEl('polygon', { points: `60,${axisTop - 8} 55,${axisTop + 2} 65,${axisTop + 2}`, fill: '#4a4a4a' }, svg); + svgEl('polygon', { points: `60,${axisBot + 8} 55,${axisBot - 2} 65,${axisBot - 2}`, fill: '#4a4a4a' }, svg); + const polTxt = svgEl('text', { x: 44, y: axisTop + 6, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '800', fill: '#2a4a55', transform: `rotate(-90, 44, ${axisTop + 40})` }, svg); + polTxt.textContent = 'Pol ❄'; + const aeqTxt = svgEl('text', { x: 44, y: axisBot - 4, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '800', fill: '#2e7d4f', transform: `rotate(-90, 44, ${axisBot - 40})` }, svg); + aeqTxt.textContent = 'Äquator ☀'; + + const nodes = []; + let current = null; + + zones.forEach((z, i) => { + const y = TOP + i * BAND_H; + const g = svgEl('g', { class: 'ge-kz-band', style: 'cursor:pointer', tabindex: '0', role: 'button', 'aria-label': z.name }, svg); + const rect = svgEl('rect', { + x: BAND_X, y: y + 2, width: BAND_W, height: BAND_H - 4, rx: 8, + fill: z.color, stroke: z.border, 'stroke-width': '1.5' + }, g); + // Emoji links im Band + svgEl('text', { x: BAND_X + 26, y: y + BAND_H / 2 + 9, 'text-anchor': 'middle', 'font-size': '26' }, g) + .textContent = z.emoji; + // Zonenname + const name = svgEl('text', { x: BAND_X + 52, y: y + BAND_H / 2 - 2, 'font-size': '14', 'font-weight': '800', fill: z.text }, g); + name.textContent = z.name; + // Wo (klein darunter) + const sub = svgEl('text', { x: BAND_X + 52, y: y + BAND_H / 2 + 15, 'font-size': '10.5', 'font-weight': '600', fill: z.text, opacity: '0.9' }, g); + sub.textContent = z.where; + // „Hier ist Österreich"-Marker + if (z.home) { + const marker = svgEl('g', {}, g); + svgEl('rect', { x: BAND_X + BAND_W - 118, y: y + BAND_H / 2 - 13, width: 112, height: 24, rx: 12, fill: '#ffffff', stroke: '#c85c4a', 'stroke-width': '1.5' }, marker); + svgEl('text', { x: BAND_X + BAND_W - 62, y: y + BAND_H / 2 + 3, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '800', fill: '#c85c4a' }, marker) + .textContent = '📍 Österreich'; + } + nodes.push({ z, rect }); + + const activate = () => select(i); + g.addEventListener('click', activate); + g.addEventListener('keydown', (ev) => { + if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); activate(); } + }); + }); + + function select(i) { + const z = zones[i]; + current = i; + // Auswahl-Rahmen setzen + nodes.forEach((n, j) => { + n.rect.setAttribute('stroke', j === i ? '#1f4e5a' : n.z.border); + n.rect.setAttribute('stroke-width', j === i ? '3.5' : '1.5'); + }); + info.dataset.state = z.state; + info.innerHTML = + `${z.name} · ${z.where}` + + `
🌡 ${z.temp}` + + `
💧 Regen: ${z.rain}` + + `
🌱 Wächst: ${z.grows}`; + + // Kernerkenntnis dynamisch auf die gewählte Zone beziehen + let msg; + if (z.key === 'tropic') { + msg = 'In den Tropen ist es heiß und feucht — nur hier wächst Kakao für Schokolade.'; + } else if (z.key === 'temperate') { + msg = 'Bei uns in Österreich ist es mild mit Regen — perfekt für Äpfel und Getreide, aber zu kühl für Kakao.'; + } else if (z.key === 'polar' || z.key === 'boreal') { + msg = 'Zu kalt und zu wenig Sonne: hier gibt es kaum oder gar keinen Ackerbau.'; + } else { + msg = 'Heiße, trockene Sommer mögen Oliven und Zitrusfrüchte — bei uns wäre es dafür zu kühl.'; + } + keyPill.dataset.state = 'now'; + keyPill.innerHTML = `Merke: ${msg}`; + } + + // Startzustand: gemäßigte Zone (Österreich) vorgewählt + select(2); + } + + // Registry — Object.assign, damit die Ladereihenfolge egal ist. + window.GlossarExperiments = Object.assign(window.GlossarExperiments || {}, { + 'klimazone': climateZones + }); +})(); diff --git a/App/assets/js/glossar-experiments-monsun.js b/App/assets/js/glossar-experiments-monsun.js new file mode 100644 index 0000000..b2fda21 --- /dev/null +++ b/App/assets/js/glossar-experiments-monsun.js @@ -0,0 +1,194 @@ +/** + * Glossar-Experimente — Gruppe „monsun". + * + * Registrierung via window.GlossarExperiments[key_slug] = fn(root). + * Eigener svgEl-Helper, nur bestehende .ge-*-CSS-Klassen (aus glossar.php). + * Keine Libraries. Mobile-/Tap-freundlich. rAF-Loops stoppen bei geschlossenem + * Modal über den root.isConnected-Guard. + */ + +(function () { + 'use strict'; + + const NS = 'http://www.w3.org/2000/svg'; + const svgEl = (tag, attrs, parent) => { + const el = document.createElementNS(NS, tag); + if (attrs) for (const k in attrs) el.setAttribute(k, attrs[k]); + if (parent) parent.appendChild(el); + return el; + }; + const deAT = (n) => Number(n).toLocaleString('de-AT'); + + // ===================================================================== + // Experiment: Monsun-Regen — Regen pro Monat in Mumbai (Balkendiagramm) + // ===================================================================== + function monsunRain(root) { + root.innerHTML = ` +
+
+

🌧 Wann fällt der Regen in Indien?

+

Jeder Balken ist ein Monat in Mumbai (Indien). Schau, wie riesig die Balken von Juni bis September werden. In dieser kurzen Zeit fällt fast der ganze Jahresregen — davon hängt die Reisernte ab.

+
+ +
+ + +
+
+
+
+
+

+
+ `; + + const svg = root.querySelector('.ge-mon-svg'); + const sharePill = root.querySelector('#ge-mon-share'); + const totalPill = root.querySelector('#ge-mon-total'); + const noteEl = root.querySelector('#ge-mon-note'); + if (!svg || !sharePill || !totalPill || !noteEl) return; + + const MONTHS = ['Jän', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']; + // Monsun-Monate (0-basiert): Juni–September + const MONSOON = [5, 6, 7, 8]; + + // Regen pro Monat in mm. + // Normal: Mumbai (Colaba) 1981–2010, India Meteorological Department. + const NORMAL = [1, 0, 1, 0, 16, 506, 769, 472, 356, 82, 9, 3]; + // Schwacher Monsun (Dürre-Jahr): Spitzen-Monate stark reduziert. + const WEAK = [1, 0, 1, 0, 10, 280, 420, 240, 180, 60, 6, 2]; + // Vergleich: Wien fällt Monat für Monat ziemlich gleichmäßig (~60 mm). + const WIEN_REF = 60; + + const NOTES = { + normal: 'Regen pro Monat in Mumbai, Normalwerte 1981 – 2010. Quelle: India Meteorological Department (über „Climate of Mumbai"). Ganzes Jahr rund 2.215 mm. Die dünne Linie zeigt Wien: dort fallen jeden Monat etwa 60 mm — ziemlich gleichmäßig.', + weak: 'Schwacher Monsun (Dürre-Jahr): geschätzte Werte, in denen die Monsun-Monate viel weniger Regen bringen. So etwas macht Bäuerinnen und Bauern Sorge — der Klimawandel lässt den Monsun unberechenbarer werden. Quelle Normalwerte: India Meteorological Department.' + }; + + // Layout + const X0 = 58, X1 = 588; // Plot links/rechts + const BASE = 278, TOP = 34; // Baseline / oberer Rand + const PLOT_H = BASE - TOP; // Zeichenhöhe + const MAX_MM = 800; // feste Skala (Balken schrumpfen sichtbar) + const PX_PER_MM = PLOT_H / MAX_MM; + const slot = (X1 - X0) / MONTHS.length; + const BAR_W = slot * 0.66; + + const yFor = (mm) => BASE - Math.min(MAX_MM, mm) * PX_PER_MM; + + // --- Statische Achsen / Gitter --- + // Y-Gitterlinien + Beschriftung + [0, 200, 400, 600, 800].forEach(mm => { + const y = yFor(mm); + svgEl('line', { x1: X0, y1: y, x2: X1, y2: y, stroke: '#dae8ec', 'stroke-width': mm === 0 ? '1.6' : '1' }, svg); + const t = svgEl('text', { x: X0 - 8, y: y + 4, 'text-anchor': 'end', 'font-size': '11', fill: '#4a4a4a', 'font-weight': '600' }, svg); + t.textContent = mm; + }); + svgEl('text', { x: X0 - 8, y: TOP - 14, 'text-anchor': 'end', 'font-size': '11', fill: '#4a4a4a', 'font-weight': '700' }, svg).textContent = 'mm'; + + // Monat-Beschriftung + farbige Monsun-Markierung am Boden + const barNodes = []; + const valNodes = []; + MONTHS.forEach((m, i) => { + const cx = X0 + slot * i + slot / 2; + const isMon = MONSOON.includes(i); + // Balken (Höhe wird animiert gesetzt) + const bar = svgEl('rect', { + x: cx - BAR_W / 2, y: BASE, width: BAR_W, height: 0, rx: 3, + fill: isMon ? '#2f7d8a' : '#a9c7cd' + }, svg); + barNodes.push(bar); + // Wert-Label über dem Balken (nur für die großen Monsun-Balken sichtbar) + const val = svgEl('text', { + x: cx, y: BASE, 'text-anchor': 'middle', 'font-size': '10', + 'font-weight': '800', fill: '#1f4e5a', opacity: '0' + }, svg); + valNodes.push(val); + // Monatsname + const lbl = svgEl('text', { + x: cx, y: BASE + 18, 'text-anchor': 'middle', 'font-size': '11', + 'font-weight': isMon ? '800' : '600', fill: isMon ? '#1f4e5a' : '#6a6a6a' + }, svg); + lbl.textContent = m; + }); + + // „Regenzeit"-Klammer über Jun–Sep + const bx0 = X0 + slot * MONSOON[0] + slot * 0.10; + const bx1 = X0 + slot * (MONSOON[MONSOON.length - 1] + 1) - slot * 0.10; + svgEl('line', { x1: bx0, y1: TOP - 6, x2: bx1, y2: TOP - 6, stroke: '#e8833a', 'stroke-width': '2', 'stroke-linecap': 'round' }, svg); + svgEl('text', { x: (bx0 + bx1) / 2, y: TOP - 12, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '800', fill: '#e8833a' }, svg).textContent = 'Regenzeit'; + + // Wien-Vergleichslinie (dünn, gestrichelt) + Label + const wienY = yFor(WIEN_REF); + svgEl('line', { x1: X0, y1: wienY, x2: X1, y2: wienY, stroke: '#c85c4a', 'stroke-width': '2', 'stroke-dasharray': '6 4' }, svg); + svgEl('text', { x: X1, y: wienY - 6, 'text-anchor': 'end', 'font-size': '10.5', 'font-weight': '800', fill: '#c85c4a' }, svg) + .textContent = 'Wien: ~60 mm pro Monat'; + + // --- Zustand + Animation --- + let animId = null; + let current = NORMAL; + + function paint(fromData, toData, scen) { + cancelAnimationFrame(animId); + const dur = 650; + const start = performance.now(); + + function tick(now) { + if (!root.isConnected) { cancelAnimationFrame(animId); return; } + const t = Math.min(1, (now - start) / dur); + const e = 1 - Math.pow(1 - t, 3); + for (let i = 0; i < barNodes.length; i++) { + const mm = fromData[i] + (toData[i] - fromData[i]) * e; + const y = yFor(mm); + barNodes[i].setAttribute('y', y); + barNodes[i].setAttribute('height', Math.max(0, BASE - y)); + // Wert nur bei den großen Balken (> 100 mm) zeigen + if (toData[i] > 100) { + valNodes[i].setAttribute('y', y - 5); + valNodes[i].textContent = Math.round(mm); + valNodes[i].setAttribute('opacity', (t).toFixed(2)); + } else { + valNodes[i].setAttribute('opacity', '0'); + } + } + if (t < 1) animId = requestAnimationFrame(tick); + } + animId = requestAnimationFrame(tick); + } + + function setScenario(scen) { + const data = scen === 'weak' ? WEAK : NORMAL; + const from = current; + current = data; + + const total = data.reduce((a, b) => a + b, 0); + const monsoonSum = MONSOON.reduce((a, i) => a + data[i], 0); + const share = Math.round((monsoonSum / total) * 100); + + sharePill.innerHTML = `Juni – September: ${share} % vom Jahresregen`; + sharePill.dataset.state = 'warn'; + totalPill.innerHTML = `Ganzes Jahr: ${deAT(total)} mm`; + totalPill.dataset.state = scen === 'weak' ? 'bad' : 'good'; + noteEl.textContent = NOTES[scen]; + + root.querySelectorAll('[data-scen]').forEach(b => { + b.classList.toggle('ge-btn-primary', b.dataset.scen === scen); + }); + + paint(from, data, scen); + } + + root.querySelectorAll('[data-scen]').forEach(b => { + b.addEventListener('click', () => setScenario(b.dataset.scen)); + }); + + // Startanimation von 0 hoch + current = NORMAL.map(() => 0); + setScenario('normal'); + } + + // Registry — Object.assign, damit Ladereihenfolge egal ist. + window.GlossarExperiments = Object.assign(window.GlossarExperiments || {}, { + 'monsun': monsunRain + }); +})(); diff --git a/App/assets/js/glossar-experiments-pyramide.js b/App/assets/js/glossar-experiments-pyramide.js new file mode 100644 index 0000000..4c5c934 --- /dev/null +++ b/App/assets/js/glossar-experiments-pyramide.js @@ -0,0 +1,182 @@ +/** + * Glossar-Experimente — Gruppe „pyramide". + * + * Registrierung: window.GlossarExperiments[key_slug] = function (root) { … } + * Der Renderer (glossar.php) hängt einen leeren Container ein und ruft die + * Funktion mit diesem Container als einzigem Argument auf. + * + * Nur bestehende .ge-*-Klassen, kein neues CSS. Vanilla JS, keine Libraries. + */ + +(function () { + 'use strict'; + + const NS = 'http://www.w3.org/2000/svg'; + const svgEl = (tag, attrs, parent) => { + const el = document.createElementNS(NS, tag); + if (attrs) for (const k in attrs) el.setAttribute(k, attrs[k]); + if (parent) parent.appendChild(el); + return el; + }; + + // ===================================================================== + // Experiment: Ernährungspyramide — früher (US 1992) vs. heute (DACH 2024) + // ===================================================================== + function foodPyramid(root) { + // Ansichten: Stufen jeweils von OBEN (Spitze) nach UNTEN (Basis). + // dark:true → weiße Schrift auf dunkler Stufe. + const VIEWS = { + us: { + btn: 'Alte US-Pyramide (1992)', + title: '🇺🇸 Die alte US-Pyramide (1992)', + intro: 'So empfahlen die USA früher zu essen. Fällt dir auf, was ganz unten die größte Gruppe ist?', + note: 'Quelle: USDA „Food Guide Pyramid" (1992). 2005 durch MyPyramid, 2011 durch MyPlate ersetzt.', + levels: [ + { lines: ['Fette, Öle & Süßes', 'sparsam'], color: '#d98c4a', dark: true, + hint: 'Ganz oben, klein: Fette und Süßes nur sparsam — aber alle Fette wurden hier in einen Topf geworfen.' }, + { lines: ['Milch · Fleisch, Fisch, Ei, Nüsse', 'je 2 – 3 Portionen'], color: '#b57f9e', dark: true, + hint: 'Milchprodukte und Eiweiß-Lebensmittel: je 2 – 3 Portionen pro Tag.' }, + { lines: ['Gemüse 3 – 5 · Obst 2 – 4', 'Portionen'], color: '#5a8a5e', dark: true, + hint: 'Gemüse und Obst — aber weiter oben als heute, also als weniger wichtig dargestellt.' }, + { lines: ['Brot, Getreide, Reis, Nudeln', '6 – 11 Portionen'], color: '#e8c547', dark: false, + hint: 'Die größte Gruppe ganz unten: 6 – 11 Portionen Kohlenhydrate pro Tag! Weißmehl zählte dabei genauso wie Vollkorn.' } + ] + }, + dach: { + btn: 'Heute (D-A-CH)', + title: '🥦 Heutige Empfehlung (D-A-CH)', + intro: 'So empfehlen Österreich, die Schweiz und Deutschland heute zu essen. Unten steht jetzt Wasser.', + note: 'Quellen: Österreichische Ernährungspyramide 2024 (BMSGPK · Gesundheit Österreich · AGES) · Schweizer Lebensmittelpyramide (SGE) · Deutschland: DGE-Empfehlungen 2024 — über ¾ pflanzlich, unter ¼ tierisch.', + levels: [ + { lines: ['Süßes, Fettes & Fast Food', 'selten'], color: '#c85c4a', dark: true, + hint: 'Ganz oben, ganz klein: Süßes, Fettiges und Fast Food nur selten und in kleinen Mengen.' }, + { lines: ['Fette & Öle', 'sparsam, pflanzlich'], color: '#d98c4a', dark: true, + hint: 'Öle und Fette sparsam — am besten pflanzliche Öle wie Rapsöl oder Olivenöl.' }, + { lines: ['Fleisch, Fisch & Ei', 'sparsam'], color: '#b56b6b', dark: true, + hint: 'Fleisch, Fisch und Ei nur sparsam: höchstens rund 300 g Fleisch pro Woche.' }, + { lines: ['Milchprodukte', '2 – 3 Portionen / Tag'], color: '#a9c0d0', dark: false, + hint: 'Milch, Joghurt und Käse: 2 – 3 Portionen pro Tag.' }, + { lines: ['Getreide & Erdäpfel', '4 Portionen · am besten Vollkorn'], color: '#e8c547', dark: false, + hint: 'Getreide, Brot und Erdäpfel: 4 Portionen pro Tag — Vollkorn ist besser als Weißmehl. Neu: auch Hülsenfrüchte wie Linsen und Bohnen.' }, + { lines: ['Gemüse & Obst', '5 Portionen / Tag'], color: '#5a8a5e', dark: true, + hint: 'Viel Gemüse und Obst: 5 Portionen pro Tag — davon 3-mal Gemüse und 2-mal Obst.' }, + { lines: ['Wasser & ungesüßte Getränke', 'rund 1,5 Liter / Tag'], color: '#4a9cc4', dark: true, + hint: 'Die neue Basis: viel trinken — Wasser oder ungesüßter Tee, rund 1,5 Liter pro Tag.' } + ] + } + }; + + root.innerHTML = ` +
+
+

🥦 Ernährungspyramide

+

Vergleiche, wie sich die Empfehlung fürs Essen verändert hat. Tippe auf eine Stufe, um mehr zu erfahren.

+
+
+ + + +
+ + +
+
Tippe auf eine Stufe der Pyramide.
+
+

+
+ `; + + const svg = root.querySelector('#fp-svg'); + const titleEl = root.querySelector('#fp-title'); + const introEl = root.querySelector('#fp-intro'); + const noteEl = root.querySelector('#fp-note'); + const detail = root.querySelector('#fp-detail'); + const fehler = root.querySelector('#fp-fehler'); + if (!svg || !titleEl || !noteEl || !detail || !fehler) return; + + // Pyramiden-Geometrie + const TOP_Y = 18, BOT_Y = 350, CX = 310, HALF_BASE = 258; + const halfAt = (y) => HALF_BASE * (y - TOP_Y) / (BOT_Y - TOP_Y); + + function drawPyramid(view) { + svg.innerHTML = ''; + const levels = view.levels; + // Gewichte: untere Stufen etwas höher (mehr Platz für Text), Spitze schmal. + const weights = levels.map((_, i) => 1 + i * 0.18); + const wSum = weights.reduce((a, b) => a + b, 0); + const totalH = BOT_Y - TOP_Y; + let y = TOP_Y; + levels.forEach((lv, i) => { + const h = (weights[i] / wSum) * totalH; + const yTop = y, yBot = y + h; + y = yBot; + const lT = CX - halfAt(yTop), rT = CX + halfAt(yTop); + const lB = CX - halfAt(yBot), rB = CX + halfAt(yBot); + const g = svgEl('g', { class: 'fp-level', style: 'cursor:pointer' }, svg); + svgEl('polygon', { + points: `${lT},${yTop} ${rT},${yTop} ${rB},${yBot} ${lB},${yBot}`, + fill: lv.color, stroke: '#ffffff', 'stroke-width': '3', 'stroke-linejoin': 'round' + }, g); + // Beschriftung mittig + const midY = (yTop + yBot) / 2; + const fill = lv.dark ? '#ffffff' : '#1f3a2a'; + const lines = lv.lines; + const startY = midY - (lines.length - 1) * 8 + 4; + const txt = svgEl('text', { + x: CX, y: startY, 'text-anchor': 'middle', + 'font-size': i === 0 ? '11' : '13', 'font-weight': '700', + fill, 'pointer-events': 'none' + }, g); + lines.forEach((ln, li) => { + const ts = svgEl('tspan', { x: CX, dy: li === 0 ? 0 : 16, 'font-weight': li === 0 ? '800' : '600', 'font-size': li === 0 ? (i === 0 ? '11' : '13') : '11' }, txt); + ts.textContent = ln; + }); + g.addEventListener('click', () => { + detail.textContent = lv.hint; + svg.querySelectorAll('polygon').forEach(p => p.setAttribute('stroke', '#ffffff')); + const poly = g.querySelector('polygon'); + if (poly) poly.setAttribute('stroke', '#1f3a2a'); + }); + }); + } + + function setView(key) { + root.querySelectorAll('[data-view]').forEach(b => + b.classList.toggle('ge-btn-primary', b.dataset.view === key)); + + if (key === 'fehler') { + svg.style.display = 'none'; + fehler.style.display = ''; + titleEl.textContent = '🤔 Was war der Denkfehler?'; + introEl.textContent = 'Warum passt die alte US-Pyramide nicht mehr zu dem, was wir heute wissen?'; + detail.textContent = 'Lies den Text oben — und vergleiche dann die beiden Pyramiden.'; + noteEl.textContent = 'Quellen: USDA Food Guide Pyramid (1992) · Österreichische Ernährungspyramide 2024 · DGE-Empfehlungen 2024.'; + return; + } + + const view = VIEWS[key]; + svg.style.display = ''; + fehler.style.display = 'none'; + titleEl.textContent = view.title; + introEl.textContent = view.intro; + noteEl.textContent = view.note; + detail.textContent = 'Tippe auf eine Stufe der Pyramide.'; + drawPyramid(view); + } + + root.querySelectorAll('[data-view]').forEach(b => + b.addEventListener('click', () => setView(b.dataset.view))); + + setView('us'); + } + + // Registry — Key entspricht key_slug aus DB. + window.GlossarExperiments = Object.assign(window.GlossarExperiments || {}, { + 'ernaehrungspyramide': foodPyramid + }); +})(); diff --git a/App/assets/js/glossar-experiments-schoko.js b/App/assets/js/glossar-experiments-schoko.js new file mode 100644 index 0000000..17579e8 --- /dev/null +++ b/App/assets/js/glossar-experiments-schoko.js @@ -0,0 +1,309 @@ +/** + * Glossar-Experimente — Gruppe „schoko". + * + * Registrierung: window.GlossarExperiments[key_slug] = function (root) { … } + * Der Renderer (glossar.php) hängt einen leeren Container ein und ruft die + * Funktion mit diesem Container als einzigem Argument auf. + * + * Nur `.ge-*`-Klassen aus glossar.php verwenden, kein neues CSS. + */ + +(function () { + 'use strict'; + + const NS = 'http://www.w3.org/2000/svg'; + const svgEl = (tag, attrs, parent) => { + const el = document.createElementNS(NS, tag); + if (attrs) for (const k in attrs) el.setAttribute(k, attrs[k]); + if (parent) parent.appendChild(el); + return el; + }; + + // ===================================================================== + // Experiment: Von der Kakaobohne zur Schokolade + // Teil 1 — Fließband mit Maschinen (Schritt für Schritt) + // Teil 2 — Mischmaschine: Kakao / Zucker / Milch -> Schoko-Sorte + // ===================================================================== + function beanToBar(root) { + root.innerHTML = ` +
+
+

🍫 Von der Kakaobohne zur Schokolade

+

Schicke die Kakaobohne über das Fließband. Tippe auf ▶ Weiter und schau, was jede Maschine macht.

+
+ +
+
+
+

+
+ + +
+ +
+
+

🥣 Die Mischmaschine

+

Jetzt kommt die Kakaomasse in die Mischmaschine. Stelle mit den Reglern ein — oder tippe einen fertigen Vorschlag.

+
+
+ + + + +
+
+ + +
keinemittelsehr viel
+
+
+ + +
keinermittelsehr viel
+
+
+ + +
keineetwasviel
+
+ +
+
+
+

+
+ +

So entsteht Schokolade: Kakaomasse + Kakaobutter + Zucker (bei Milchschokolade auch Milchpulver). Weiße Schokolade hat gar keine Kakaomasse, nur die helle Kakaobutter. Beispielwerte: dunkle Schokolade 50 – 90 % Kakao, Vollmilch 25 – 40 %, weiße 0 %. Quelle: Leitsätze für Kakao und Schokoladen · EU-Kakaoverordnung 2000/36/EG.

+
+ `; + + // ---------- Teil 1: Fließband ---------- + const svg = root.querySelector('.ge-track'); + if (!svg) return; + const stepNameEl = root.querySelector('[data-ref="stepname"]'); + const stepTextEl = root.querySelector('[data-ref="steptext"]'); + const mixBox = root.querySelector('[data-ref="mix"]'); + + const STEPS = [ + { emoji: '🫘', name: 'Kakaobohne', text: 'Die getrockneten Bohnen kommen vom Kakaobaum — noch bitter und hart.' }, + { emoji: '🔥', name: 'Rösten', text: 'Im heißen Ofen bekommen die Bohnen erst ihren Schoko-Duft.' }, + { emoji: '🥜', name: 'Schälen & Brechen', text: 'Die Schale wird entfernt. Übrig bleiben kleine Stückchen: die Kakao-Nibs.' }, + { emoji: '⚙️', name: 'Mahlen', text: 'Die Nibs werden fein zermahlen — dabei wird flüssige Kakaomasse daraus.' }, + { emoji: '🌀', name: 'Conchieren', text: 'Stundenlanges Rühren macht die Masse ganz fein und zart.' }, + { emoji: '🥣', name: 'Mischen', text: 'Jetzt kommt Zucker (und oft Milch) dazu — hier entsteht deine Schokolade!' } + ]; + const N = STEPS.length; + const X0 = 55, X1 = 545; + const gap = (X1 - X0) / (N - 1); + const beltY = 130; + + // Fließband + svgEl('rect', { x: 30, y: beltY, width: 540, height: 14, rx: 7, fill: '#c9b79a' }, svg); + for (let i = 0; i <= 18; i++) { + svgEl('line', { x1: 40 + i * 28, y1: beltY, x2: 40 + i * 28, y2: beltY + 14, stroke: '#a9906c', 'stroke-width': '1.5' }, svg); + } + + // Maschinen-Boxen + const boxes = STEPS.map((s, i) => { + const cx = X0 + i * gap; + const g = svgEl('g', {}, svg); + svgEl('rect', { x: cx - 26, y: beltY - 62, width: 52, height: 54, rx: 8, + fill: '#f2ede3', stroke: '#c9b79a', 'stroke-width': '2', 'data-box': String(i) }, g); + const ic = svgEl('text', { x: cx, y: beltY - 28, 'text-anchor': 'middle', 'font-size': '26' }, g); + ic.textContent = s.emoji; + const lb = svgEl('text', { x: cx, y: beltY + 32, 'text-anchor': 'middle', 'font-size': '9.5', + 'font-weight': '700', fill: '#5a4632' }, g); + lb.textContent = s.name; + return g.querySelector('rect'); + }); + + // wandernde Bohne + const beanX = X0; + const bean = svgEl('text', { x: beanX, y: beltY - 4, 'text-anchor': 'middle', 'font-size': '22' }, svg); + bean.textContent = '🫘'; + + let idx = 0; + let animId = null; + + function highlight() { + boxes.forEach((b, i) => { + b.setAttribute('fill', i === idx ? '#ffe9c2' : '#f2ede3'); + b.setAttribute('stroke', i === idx ? '#e0a04a' : '#c9b79a'); + b.setAttribute('stroke-width', i === idx ? '3' : '2'); + }); + const s = STEPS[idx]; + bean.textContent = idx >= 3 ? '🟤' : '🫘'; // ab dem Mahlen: Kakaomasse + if (stepNameEl) stepNameEl.textContent = (idx + 1) + '. ' + s.name; + if (stepTextEl) stepTextEl.textContent = s.text; + if (mixBox) mixBox.style.display = (idx === N - 1) ? 'flex' : 'none'; + } + + function tweenTo(targetX) { + cancelAnimationFrame(animId); + const fromX = parseFloat(bean.getAttribute('x')) || X0; + const start = performance.now(); + const dur = 480; + function frame(now) { + if (!root.isConnected) return; + const t = Math.min(1, (now - start) / dur); + const e = 1 - Math.pow(1 - t, 3); + bean.setAttribute('x', String(fromX + (targetX - fromX) * e)); + if (t < 1) animId = requestAnimationFrame(frame); + } + animId = requestAnimationFrame(frame); + } + + function goNext() { + if (idx < N - 1) { + idx++; + tweenTo(X0 + idx * gap); + highlight(); + } + } + function goReset() { + cancelAnimationFrame(animId); + idx = 0; + bean.setAttribute('x', String(X0)); + highlight(); + } + + root.querySelector('[data-act="next"]').addEventListener('click', goNext); + root.querySelector('[data-act="reset"]').addEventListener('click', goReset); + + // ---------- Teil 2: Mischmaschine ---------- + const kakaoIn = root.querySelector('[data-ref="kakao"]'); + const zuckerIn = root.querySelector('[data-ref="zucker"]'); + const milchIn = root.querySelector('[data-ref="milch"]'); + const kv = root.querySelector('[data-ref="kv"]'); + const zv = root.querySelector('[data-ref="zv"]'); + const mv = root.querySelector('[data-ref="mv"]'); + const barSvg = root.querySelector('[data-ref="barsvg"]'); + const verdict = root.querySelector('[data-ref="verdict"]'); + const explain = root.querySelector('[data-ref="explain"]'); + + let waterIn = false; + + function classify(K, Z, M) { + if (waterIn) { + return { key: 'kaputt', label: 'Klumpige, kaputte Masse', color: '#8f8577', state: 'bad', + text: 'Wasser und Fett vertragen sich nicht — die Schokolade wird klumpig und grieselig. Deshalb bleibt bei Schokolade alles trocken!' }; + } + const hatFett = K > 0 || M > 0; // Kakaomasse bringt Kakaobutter, Milchpulver bringt Milchfett + if (!hatFett) { + return { key: 'kaputt', label: 'Bröseliger Zuckerhaufen', color: '#d8c9a0', state: 'bad', + text: 'Ohne Fett (keine Kakaomasse, keine Kakaobutter, keine Milch) hält nichts zusammen — es bleibt nur trockener Zucker.' }; + } + if (Z >= 70) { + return { key: 'kaputt', label: 'Grieselige Zuckermasse', color: '#c9a86a', state: 'warn', + text: 'Viel zu viel Zucker: Die Masse wird körnig und sandig statt cremig. Weniger Zucker wäre besser.' }; + } + if (K >= 55 && M <= 5) { + return { key: 'zart', label: 'Zartbitter-Schokolade 🌑', color: '#3a2417', state: 'good', + text: 'Viel Kakao, wenig Zucker, keine Milch: kräftig-dunkle Schokolade, die leicht bitter schmeckt.' }; + } + if (K <= 15 && M >= 15) { + return { key: 'weiss', label: 'Weiße Schokolade 🤍', color: '#f2e4c4', state: 'good', + text: 'Keine Kakaomasse — nur die helle Kakaobutter, dazu Milch und Zucker. Darum ist sie hell und milchig.' }; + } + if (M >= 15) { + return { key: 'voll', label: 'Vollmilch-Schokolade 🐄', color: '#7a4a24', state: 'good', + text: 'Mittel viel Kakao mit Milch und Zucker: cremig-süß, die beliebteste Sorte.' }; + } + return { key: 'dunkel', label: 'Dunkle Schokolade', color: '#4a2f1c', state: 'now', + text: 'Kakao und Zucker ohne Milch — eine dunkle Schokolade. Mehr Milch macht sie heller, mehr Kakao herber.' }; + } + + function drawBar(res) { + barSvg.innerHTML = ''; + // Maschinen-Beschriftung oben + const cap = svgEl('text', { x: 300, y: 24, 'text-anchor': 'middle', 'font-size': '13', + 'font-weight': '800', fill: '#5a4632' }, barSvg); + cap.textContent = '🥣 heraus kommt:'; + + if (res.state === 'bad' || res.state === 'warn') { + // kaputte / grieselige Masse: unregelmäßige Klumpen + const blobs = [ + [180, 120, 34], [250, 150, 26], [320, 110, 30], [380, 155, 22], + [230, 100, 20], [300, 165, 24], [360, 115, 18], [410, 130, 26], [200, 160, 22] + ]; + blobs.forEach(([x, y, r]) => { + svgEl('circle', { cx: x, cy: y, r: r, fill: res.color, opacity: '0.9' }, barSvg); + }); + const emo = svgEl('text', { x: 300, y: 205, 'text-anchor': 'middle', 'font-size': '16' }, barSvg); + emo.textContent = waterIn ? '💧 klumpig' : '⚠️ krümelig'; + return; + } + + // gute Tafel: 5 × 3 Felder + const cols = 5, rows = 3; + const bw = 240, bh = 132, bx = 300 - bw / 2, by = 44; + const cw = bw / cols, ch = bh / rows; + svgEl('rect', { x: bx - 6, y: by - 6, width: bw + 12, height: bh + 12, rx: 12, + fill: res.color, opacity: '0.35' }, barSvg); + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + svgEl('rect', { x: bx + c * cw + 3, y: by + r * ch + 3, width: cw - 6, height: ch - 6, rx: 4, + fill: res.color, stroke: 'rgba(0,0,0,0.15)', 'stroke-width': '1' }, barSvg); + } + } + const emo = svgEl('text', { x: 300, y: 205, 'text-anchor': 'middle', 'font-size': '15', + 'font-weight': '700', fill: '#5a4632' }, barSvg); + emo.textContent = '😋 fertige Tafel'; + } + + function update() { + const K = parseInt(kakaoIn.value, 10); + const Z = parseInt(zuckerIn.value, 10); + const M = parseInt(milchIn.value, 10); + if (kv) kv.textContent = String(K); + if (zv) zv.textContent = String(Z); + if (mv) mv.textContent = String(M); + const res = classify(K, Z, M); + if (verdict) { + verdict.textContent = res.label; + verdict.setAttribute('data-state', res.state); + } + if (explain) explain.textContent = res.text; + drawBar(res); + } + + function onSlide() { waterIn = false; update(); } + [kakaoIn, zuckerIn, milchIn].forEach(inp => { + if (inp) inp.addEventListener('input', onSlide); + }); + + const presets = { + zart: { k: 75, z: 25, m: 0, w: false }, + voll: { k: 35, z: 45, m: 20, w: false }, + weiss: { k: 0, z: 50, m: 30, w: false }, + wasser: { k: 40, z: 20, m: 10, w: true } + }; + root.querySelectorAll('[data-preset]').forEach(btn => { + btn.addEventListener('click', () => { + const p = presets[btn.getAttribute('data-preset')]; + if (!p) return; + kakaoIn.value = String(p.k); + zuckerIn.value = String(p.z); + milchIn.value = String(p.m); + waterIn = p.w; + if (kv) kv.textContent = String(p.k); + if (zv) zv.textContent = String(p.z); + if (mv) mv.textContent = String(p.m); + const res = classify(p.k, p.z, p.m); + if (verdict) { verdict.textContent = res.label; verdict.setAttribute('data-state', res.state); } + if (explain) explain.textContent = res.text; + drawBar(res); + }); + }); + + // Initialzustand + highlight(); + update(); + } + + // Registry — Keys entsprechen key_slug aus der Tabelle `glossar` + window.GlossarExperiments = Object.assign(window.GlossarExperiments || {}, { + 'kakao-bohne': beanToBar + }); +})(); diff --git a/App/pages/glossar.php b/App/pages/glossar.php index dab1ba5..e51ec94 100644 --- a/App/pages/glossar.php +++ b/App/pages/glossar.php @@ -651,6 +651,10 @@ include __DIR__ . '/_partials/erkundungs_subnav.php'; + + + +