685b4700f8
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>
310 lines
14 KiB
JavaScript
310 lines
14 KiB
JavaScript
/**
|
||
* 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
|
||
});
|
||
})();
|