Glossar: 4 erklärende Widgets + 2 Einträge anschaulicher

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 01:42:20 +02:00
parent 283780994b
commit 685b4700f8
6 changed files with 889 additions and 0 deletions
@@ -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;
@@ -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 = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🌍 Die Klimazonen der Erde</h3>
<p>Von unten (Äquator, viel Sonne) nach oben (Pol, wenig Sonne) wird es kühler. Tippe auf ein Band und schau, was dort wächst.</p>
</div>
<svg class="ge-gh-svg" viewBox="0 0 600 340" preserveAspectRatio="xMidYMid meet" role="group" aria-label="Klimazonen von Äquator bis Pol"></svg>
<div class="ge-gh-result">
<div class="ge-gh-pill" id="ge-kz-info" data-state="good">Tippe auf ein Band …</div>
<div class="ge-gh-pill" id="ge-kz-key" data-state="now">Wo etwas wächst, hängt von Wärme und Regen ab.</div>
</div>
<p class="ge-note">Einteilung nach den Klimazonen der Köppen-Geiger-Klassifikation; Temperatur- und Niederschlagsbereiche sind grobe kindgerechte Richtwerte. Quelle: Klimadiagramme &amp; Köppen-Geiger-Klassifikation.</p>
</div>
`;
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 =
`<strong>${z.name}</strong> · ${z.where}` +
`<br>🌡 ${z.temp}` +
`<br>💧 Regen: ${z.rain}` +
`<br>🌱 Wächst: <strong>${z.grows}</strong>`;
// 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 = `<strong>Merke:</strong> ${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
});
})();
+194
View File
@@ -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 = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🌧 Wann fällt der Regen in Indien?</h3>
<p>Jeder Balken ist ein Monat in <strong>Mumbai</strong> (Indien). Schau, wie riesig die Balken von <strong>Juni bis September</strong> werden. In dieser kurzen Zeit fällt fast der ganze Jahresregen — davon hängt die Reisernte ab.</p>
</div>
<svg class="ge-mon-svg" viewBox="0 0 600 340" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Balkendiagramm Regen pro Monat in Mumbai"></svg>
<div class="ge-controls">
<button class="ge-btn ge-btn-primary" data-scen="normal">☔ Normales Jahr</button>
<button class="ge-btn" data-scen="weak">🌵 Schwacher Monsun</button>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill" id="ge-mon-share"></div>
<div class="ge-gh-pill" id="ge-mon-total"></div>
</div>
<p class="ge-note" id="ge-mon-note"></p>
</div>
`;
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): JuniSeptember
const MONSOON = [5, 6, 7, 8];
// Regen pro Monat in mm.
// Normal: Mumbai (Colaba) 19812010, 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 JunSep
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: <strong>${share} %</strong> vom Jahresregen`;
sharePill.dataset.state = 'warn';
totalPill.innerHTML = `Ganzes Jahr: <strong>${deAT(total)} mm</strong>`;
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
});
})();
@@ -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 = `
<div class="ge-wrap">
<div class="ge-head">
<h3 id="fp-title">🥦 Ernährungspyramide</h3>
<p id="fp-intro">Vergleiche, wie sich die Empfehlung fürs Essen verändert hat. Tippe auf eine Stufe, um mehr zu erfahren.</p>
</div>
<div class="ge-controls" role="tablist">
<button class="ge-btn ge-btn-primary" data-view="us">Alte US-Pyramide (1992)</button>
<button class="ge-btn" data-view="dach">Heute (D-A-CH)</button>
<button class="ge-btn" data-view="fehler">Was war der Denkfehler?</button>
</div>
<svg id="fp-svg" viewBox="0 0 620 380" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Ernährungspyramide"></svg>
<div id="fp-fehler" style="display:none; font-size:.85rem; line-height:1.55; color:var(--ggs-text);">
<p><strong>Der Denkfehler der alten US-Pyramide:</strong></p>
<p>Ganz unten, als größte Gruppe, standen Brot, Reis und Nudeln mit bis zu 6 11 Portionen pro Tag. Das sind sehr viele Kohlenhydrate. Dabei wurde <strong>Weißmehl und Zucker genauso behandelt wie Vollkorn</strong> — obwohl Vollkorn viel gesünder ist.</p>
<p>Außerdem galten <strong>alle Fette pauschal als „schlecht"</strong> und sollten sparsam gegessen werden — auch gesunde pflanzliche Öle. Kritiker:innen bemängelten zudem den <strong>Einfluss der Lebensmittel-Industrie</strong> auf diese Empfehlung.</p>
<p>Heute weiß man es besser: Die Basis sind <strong>Wasser, Gemüse und Obst</strong>, Getreide am besten als Vollkorn — und der Speiseplan ist <strong>überwiegend pflanzlich</strong> (rund ¾ pflanzlich, ¼ tierisch).</p>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill" id="fp-detail">Tippe auf eine Stufe der Pyramide.</div>
</div>
<p class="ge-note" id="fp-note"></p>
</div>
`;
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
});
})();
+309
View File
@@ -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 = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🍫 Von der Kakaobohne zur Schokolade</h3>
<p>Schicke die Kakaobohne über das Fließband. Tippe auf <strong>▶ Weiter</strong> und schau, was jede Maschine macht.</p>
</div>
<svg class="ge-track" viewBox="0 0 600 200" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-result">
<div class="ge-gh-pill" data-ref="stepname" data-state="now"></div>
</div>
<p class="ge-head" style="margin:0"><span data-ref="steptext" style="font-size:.85rem"></span></p>
<div class="ge-controls">
<button class="ge-btn ge-btn-primary" data-act="next">▶ Weiter</button>
<button class="ge-btn" data-act="reset">↺ Von vorn</button>
</div>
<div data-ref="mix" style="display:none; flex-direction:column; gap:.6rem;">
<div class="ge-head">
<h3>🥣 Die Mischmaschine</h3>
<p>Jetzt kommt die Kakaomasse in die Mischmaschine. Stelle mit den Reglern ein — oder tippe einen fertigen Vorschlag.</p>
</div>
<div class="ge-controls">
<button class="ge-btn" data-preset="zart">🌑 Zartbitter</button>
<button class="ge-btn" data-preset="voll">🐄 Vollmilch</button>
<button class="ge-btn" data-preset="weiss">🤍 Weiße</button>
<button class="ge-btn" data-preset="wasser">🚱 Wasser rein!</button>
</div>
<div class="ge-gh-slider">
<label>Kakao-Anteil (Kakaomasse): <strong data-ref="kv">75</strong> %</label>
<input data-ref="kakao" type="range" min="0" max="90" value="75" step="5">
<div class="ge-gh-markers"><span>keine</span><span>mittel</span><span>sehr viel</span></div>
</div>
<div class="ge-gh-slider">
<label>Zucker: <strong data-ref="zv">25</strong> %</label>
<input data-ref="zucker" type="range" min="0" max="80" value="25" step="5">
<div class="ge-gh-markers"><span>keiner</span><span>mittel</span><span>sehr viel</span></div>
</div>
<div class="ge-gh-slider">
<label>Milch (Milchpulver): <strong data-ref="mv">0</strong> %</label>
<input data-ref="milch" type="range" min="0" max="40" value="0" step="5">
<div class="ge-gh-markers"><span>keine</span><span>etwas</span><span>viel</span></div>
</div>
<svg class="ge-track" viewBox="0 0 600 220" preserveAspectRatio="xMidYMid meet" aria-hidden="true" data-ref="barsvg"></svg>
<div class="ge-gh-result">
<div class="ge-gh-pill" data-ref="verdict" aria-live="polite"></div>
</div>
<p class="ge-head" style="margin:0"><span data-ref="explain" style="font-size:.85rem"></span></p>
</div>
<p class="ge-note">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.</p>
</div>
`;
// ---------- 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
});
})();
+4
View File
@@ -651,6 +651,10 @@ include __DIR__ . '/_partials/erkundungs_subnav.php';
<script src="<?= $bp ?>/assets/js/glossar-experiments-geowelt.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-geowelt.js") ?: time() ?>"></script> <script src="<?= $bp ?>/assets/js/glossar-experiments-geowelt.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-geowelt.js") ?: time() ?>"></script>
<script src="<?= $bp ?>/assets/js/glossar-experiments-neu1.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-neu1.js") ?: time() ?>"></script> <script src="<?= $bp ?>/assets/js/glossar-experiments-neu1.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-neu1.js") ?: time() ?>"></script>
<script src="<?= $bp ?>/assets/js/glossar-experiments-neu2.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-neu2.js") ?: time() ?>"></script> <script src="<?= $bp ?>/assets/js/glossar-experiments-neu2.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-neu2.js") ?: time() ?>"></script>
<script src="<?= $bp ?>/assets/js/glossar-experiments-pyramide.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-pyramide.js") ?: time() ?>"></script>
<script src="<?= $bp ?>/assets/js/glossar-experiments-schoko.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-schoko.js") ?: time() ?>"></script>
<script src="<?= $bp ?>/assets/js/glossar-experiments-klimazonen.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-klimazonen.js") ?: time() ?>"></script>
<script src="<?= $bp ?>/assets/js/glossar-experiments-monsun.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-monsun.js") ?: time() ?>"></script>
<footer class="gl-footer"> <footer class="gl-footer">
<img src="<?= $bp ?>/assets/img/bildLogo.png" alt=""> <img src="<?= $bp ?>/assets/img/bildLogo.png" alt="">