Files
geograsim/App/assets/js/glossar-experiments.js
T
Adminator a425ce31e6 Glossar: Komplettpaket (28 Eintraege, 19 SVGs, 5 DALL-E-Bilder, iPad, Experimente)
- Mindmap mit Force-directed Layout (pages/mindmap.php) + Cross-Links
- 28 Eintraege im Fachartikel-Stil mit 113 Quellen + Inline-Referenzen [^n]
- Leichte-Sprache-Version fuer alle 28 Eintraege (short_easy, text_easy, examples.text_easy)
- 19 Infografiken (12 neu): co2-Keeling, klimawandel-Temp, kohlenstoff-Kreislauf, fotosynthese, energiemix-AT, methan-GWP, photovoltaik, wasserkraft, kipppunkt, biodiversitaet-LPI, permafrost, treibhausgas-GWP + bestehende (treibhauseffekt, ppm, windrad, albedo, fossile, kwh, co2-fussabdruck)
- 5 DALL-E-Reprasentationsbilder im Querformat (nachhaltigkeit, klimaneutralitaet, erneuerbare-energie, emissionen, pariser-abkommen)
- Alphabetische Sprungnavigation A-Z mit Buchstabenbannern
- Lightbox-Vollbild auf allen Infografiken mit expliziten SVG-Dimensionen (Safari/iPad-Fix)
- Easy-Sprache-Toggle pro Card mit localStorage-Persistenz
- 4 interaktive Experimente: kWh-Rennen, ppm-Finder, albedo-Slider, CO2-Fussabdruck-Rechner
- iPad-Optimierung: Touch-Ziele >=38px, Hover in @media (hover), touch-action
- Modul-Metadaten aus DB-Tabelle module_info (keine Hardcodes)
- Cache-Buster auf JS-Asset
- Schema-Erweiterung: glossar_sources, examples.text_easy

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 00:48:29 +02:00

358 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Glossar-Experimente — kleine interaktive Widgets im Detail-Modal.
*
* 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. Das Widget baut
* selbst seinen DOM und bindet seine Events.
*
* Richtlinien:
* - Keine externen Libraries.
* - Mobile-/Tap-freundlich: grosse Buttons, keine Hover-Abhängigkeit.
* - Zahlen quellenbar, kurze Begründung in der UI sichtbar.
*/
(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: kWh-Rennen — wie weit kommt man mit 1 Kilowattstunde?
// =====================================================================
function kwhRace(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🏁 Das kWh-Rennen</h3>
<p>Drei Fahrzeuge starten rechts mit <strong>derselben Energie</strong>: 1 Kilowattstunde. Wie weit kommen sie nach links?</p>
</div>
<svg class="ge-track" viewBox="0 0 600 320" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-controls">
<button class="ge-btn ge-btn-primary" data-act="start">▶ Start</button>
<button class="ge-btn" data-act="reset">↺ Zurück</button>
</div>
<ul class="ge-legend">
<li><span class="ge-dot" style="background:#5a8a5e"></span>E-Fahrrad · <strong>10 Wh/km</strong> → ca. 100 km</li>
<li><span class="ge-dot" style="background:#4a7c8a"></span>E-Auto · <strong>150 Wh/km</strong> → ca. 6,5 km</li>
<li><span class="ge-dot" style="background:#c85c4a"></span>Benzin-Auto · <strong>530 Wh/km</strong> aus Kraftstoffenergie → ca. 1,9 km</li>
</ul>
<p class="ge-note">Quellen: ADAC Stromverbrauchstests E-Auto · Hersteller-Angaben E-Bike Bosch Performance · WLTP Benziner 6 L/100 km × 8,8 kWh/L. Der Benziner verliert rund 70 % der Kraftstoffenergie als Motor-Wärme — deshalb kommt er weniger weit.</p>
</div>
`;
const svg = root.querySelector('.ge-track');
// Emoji-Fahrzeuge: auf Windows/Chrome schauen 🚴 🚗 🚙 nach links → sie fahren
// von rechts nach links, Emojis passen zur Fahrtrichtung ohne Plattform-Trick.
const vehicles = [
{ label: 'E-Fahrrad', emoji: '🚴', km: 100, color: '#5a8a5e', y: 100 },
{ label: 'E-Auto', emoji: '🚗', km: 6.5, color: '#4a7c8a', y: 190 },
{ label: 'Benzin-Auto', emoji: '🚙', km: 1.9, color: '#c85c4a', y: 280 }
];
const MAX_KM = 100;
const TRACK_L = 50, TRACK_R = 560;
const trackLen = TRACK_R - TRACK_L;
// Skala oben — Start rechts (0 km), Ziel links (100 km)
const axis = svgEl('g', { class: 'ge-axis' }, svg);
svgEl('line', { x1: TRACK_L, y1: 52, x2: TRACK_R, y2: 52, stroke: '#1f4e5a', 'stroke-width': '1.2' }, axis);
[0, 25, 50, 75, 100].forEach(km => {
const x = TRACK_R - (km / MAX_KM) * trackLen;
svgEl('line', { x1: x, y1: 48, x2: x, y2: 56, stroke: '#1f4e5a', 'stroke-width': '1' }, axis);
const t = svgEl('text', { x, y: 40, 'text-anchor': 'middle', 'font-size': '11', fill: '#4a4a4a', 'font-weight': '600' }, axis);
t.textContent = km + ' km';
});
// „Start" Marker ganz rechts
const startTxt = svgEl('text', { x: TRACK_R, y: 20, 'text-anchor': 'end', 'font-size': '11', fill: '#1f4e5a', 'font-weight': '700' }, svg);
startTxt.textContent = '← Start mit 1 kWh';
// Spuren + Fahrzeuge
const nodes = vehicles.map(v => {
const g = svgEl('g', { class: 'ge-lane' }, svg);
// Label ÜBER der Spur, rechts beim Start
const label = svgEl('text', { x: TRACK_R, y: v.y - 22, 'text-anchor': 'end', 'font-size': '12', 'font-weight': '800', fill: v.color }, g);
label.textContent = v.label;
// Straßenlinie
svgEl('line', { x1: TRACK_L, y1: v.y, x2: TRACK_R, y2: v.y, stroke: '#dae8ec', 'stroke-width': '5', 'stroke-linecap': 'round' }, g);
// Ziel-Markierung (links vom Start)
const xEnd = TRACK_R - (v.km / MAX_KM) * trackLen;
svgEl('line', { x1: xEnd, y1: v.y - 16, x2: xEnd, y2: v.y + 16, stroke: v.color, 'stroke-width': '2', 'stroke-dasharray': '3 2', opacity: '.55' }, g);
// Emoji-Fahrzeug
const icon = svgEl('text', { x: TRACK_R, y: v.y + 8, 'text-anchor': 'middle', 'font-size': '30' }, g);
icon.textContent = v.emoji;
// Ergebnis-Label LINKS vom Ziel (in Fahrtrichtung weiter)
const result = svgEl('text', { x: xEnd - 10, y: v.y + 4, 'text-anchor': 'end', 'font-size': '12', 'font-weight': '800', fill: v.color, opacity: '0' }, g);
result.textContent = v.km.toFixed(1).replace('.', ',') + ' km';
return { v, icon, result, xStart: TRACK_R, xEnd };
});
// Animation
let animId = null;
function setX(n, x) { n.icon.setAttribute('x', x); }
function animate() {
cancelAnimationFrame(animId);
const dur = 6500; // ms — bewusst langsam
const start = performance.now();
nodes.forEach(n => { setX(n, n.xStart); n.result.setAttribute('opacity', '0'); });
function tick(now) {
const t = Math.min(1, (now - start) / dur);
const e = 1 - Math.pow(1 - t, 3);
nodes.forEach(n => {
const x = n.xStart + (n.xEnd - n.xStart) * e;
setX(n, x);
if (t > 0.96) n.result.setAttribute('opacity', '1');
});
if (t < 1) animId = requestAnimationFrame(tick);
}
animId = requestAnimationFrame(tick);
}
function reset() {
cancelAnimationFrame(animId);
nodes.forEach(n => { setX(n, n.xStart); n.result.setAttribute('opacity', '0'); });
}
root.querySelector('[data-act="start"]').addEventListener('click', animate);
root.querySelector('[data-act="reset"]').addEventListener('click', reset);
}
// =====================================================================
// Experiment: ppm-Raster — 1 Teilchen von 1.000.000 findet man kaum
// =====================================================================
function ppmGrid(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🔍 Finde das eine Teilchen</h3>
<p>Unten siehst du ein Gitter mit <strong>10.000 Punkten</strong>. Ein einziger Punkt ist rot — das entspräche 100 ppm (echte 1 ppm wäre nochmal 100× feiner). Versuche, ihn zu finden und zu tippen!</p>
</div>
<div class="ge-ppm-grid" id="ge-ppm"></div>
<div class="ge-ppm-msg" aria-live="polite"></div>
<div class="ge-controls">
<button class="ge-btn ge-btn-primary" data-act="highlight">💡 Tipp zeigen</button>
<button class="ge-btn" data-act="new">🎲 Neues Teilchen</button>
</div>
<p class="ge-note">Bei der aktuellen CO₂-Konzentration von 422 ppm sind 422 von 1.000.000 Luftteilchen CO₂. Das klingt winzig — reicht aber, um die Erde messbar zu erwärmen. Quelle: NOAA Mauna Loa (2024).</p>
</div>
`;
const grid = root.querySelector('#ge-ppm');
const msg = root.querySelector('.ge-ppm-msg');
const COLS = 100, ROWS = 100;
let special = Math.floor(Math.random() * COLS * ROWS);
let found = false;
function render() {
grid.innerHTML = '';
grid.style.gridTemplateColumns = `repeat(${COLS}, 1fr)`;
for (let i = 0; i < COLS * ROWS; i++) {
const cell = document.createElement('span');
if (i === special) cell.className = 'ge-ppm-special';
grid.appendChild(cell);
}
msg.textContent = '';
msg.classList.remove('show', 'miss');
found = false;
}
function highlight() {
const el = grid.children[special];
if (!el) return;
el.classList.add('ge-ppm-flash');
setTimeout(() => el.classList.remove('ge-ppm-flash'), 1800);
}
function newOne() {
special = Math.floor(Math.random() * COLS * ROWS);
render();
}
function onHit(cell) {
if (found) return;
found = true;
cell.classList.add('ge-ppm-hit');
msg.textContent = '🎉 Getroffen! Das war 1 von 10.000 Punkten.';
msg.classList.remove('miss');
msg.classList.add('show');
}
function onMiss() {
if (found) return;
msg.textContent = 'Daneben — probier "Tipp zeigen" für eine kurze Hilfe.';
msg.classList.remove('show');
msg.classList.add('show', 'miss');
}
// Click-Handler delegiert auf das Grid (funktioniert auch bei 4-px-Zellen)
grid.addEventListener('click', ev => {
const target = ev.target;
if (target === grid) return;
if (target.classList && target.classList.contains('ge-ppm-special')) onHit(target);
else onMiss();
});
render();
root.querySelector('[data-act="highlight"]').addEventListener('click', highlight);
root.querySelector('[data-act="new"]').addEventListener('click', newOne);
}
// =====================================================================
// Experiment: Albedo-Slider — Oberfläche wechseln, Reflexion beobachten
// =====================================================================
function albedoSlider(root) {
const surfaces = [
{ key: 'snow', label: 'Frischer Schnee', albedo: 0.85, color: '#ffffff', border: '#b5c9cf' },
{ key: 'desert', label: 'Sand-Wüste', albedo: 0.35, color: '#e8d5b5', border: '#c4a97a' },
{ key: 'forest', label: 'Wald', albedo: 0.12, color: '#3a6b3e', border: '#2a5030' },
{ key: 'ocean', label: 'Ozean', albedo: 0.06, color: '#1f4e5a', border: '#0f2f3a' },
{ key: 'asphalt', label: 'Asphalt', albedo: 0.05, color: '#2a2a2a', border: '#1a1a1a' }
];
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>☀ Welche Oberfläche reflektiert wie viel?</h3>
<p>Wähle eine Oberfläche. Die Pfeile zeigen, wie viel Sonnenlicht zurückgeworfen wird.</p>
</div>
<div class="ge-albedo-choice" role="tablist">
${surfaces.map((s, i) => `
<button class="ge-btn ${i===0?'ge-btn-primary':''}" data-key="${s.key}">${s.label}</button>
`).join('')}
</div>
<svg class="ge-albedo-svg" viewBox="0 0 600 320" preserveAspectRatio="xMidYMid meet"></svg>
<div class="ge-albedo-stats">
<div>Albedo: <strong id="ge-alb-val">0,85</strong></div>
<div>Reflektiert: <strong id="ge-alb-refl">85 %</strong></div>
<div>Absorbiert: <strong id="ge-alb-abs">15 %</strong></div>
</div>
<p class="ge-note">Darum erwärmt sich die Arktis 4-mal schneller als die Erde insgesamt: Schmilzt Eis, kommt darunter dunkler Ozean zum Vorschein, der viel mehr Sonnenenergie aufnimmt. Quelle: Rantanen et al. 2022.</p>
</div>
`;
const svg = root.querySelector('.ge-albedo-svg');
const valEl = root.querySelector('#ge-alb-val');
const reflEl = root.querySelector('#ge-alb-refl');
const absEl = root.querySelector('#ge-alb-abs');
// Statischer Aufbau: Himmel + Sonne + Bodenrechteck
svgEl('rect', { x: 0, y: 0, width: 600, height: 210, fill: '#eff5f6' }, svg);
svgEl('circle', { cx: 90, cy: 70, r: 30, fill: '#f4c94e' }, svg);
const surfaceRect = svgEl('rect', { x: 0, y: 210, width: 600, height: 110, fill: '#ffffff', stroke: '#b5c9cf', 'stroke-width': '1.5' }, svg);
// Einfallender Lichtstrahl (Sonne → Boden)
svgEl('line', { x1: 110, y1: 100, x2: 300, y2: 210, stroke: '#e8833a', 'stroke-width': '3', 'stroke-linecap': 'round' }, svg);
svgEl('polygon', { points: '295,205 306,208 300,215', fill: '#e8833a' }, svg);
// Reflexions-Pfeile (dynamisch nach Albedo)
const reflG = svgEl('g', { class: 'ge-albedo-refl' }, svg);
function setSurface(key) {
const s = surfaces.find(x => x.key === key) || surfaces[0];
surfaceRect.setAttribute('fill', s.color);
surfaceRect.setAttribute('stroke', s.border);
// Anzahl reflektierter Pfeile proportional zur Albedo (1 bis 9 Pfeile)
reflG.innerHTML = '';
const nArrows = Math.max(1, Math.round(s.albedo * 10));
for (let i = 0; i < nArrows; i++) {
const angle = -60 + i * (120 / Math.max(1, nArrows - 1));
const rad = angle * Math.PI / 180;
const startX = 300, startY = 210;
const len = 120;
const endX = startX + Math.cos(rad) * len;
const endY = startY - Math.sin(Math.abs(rad) * Math.PI / 180 + 0.3) * Math.abs(len * Math.cos(rad) / 90) - 60;
// Simpler: Linien gleichmäßig nach oben gefächert
const e2x = 300 + Math.sin(rad) * len;
const e2y = 210 - Math.cos(rad) * len;
svgEl('line', { x1: startX, y1: startY, x2: e2x, y2: e2y, stroke: '#f4c94e', 'stroke-width': '2', 'stroke-linecap': 'round', opacity: '0.85' }, reflG);
}
valEl.textContent = s.albedo.toFixed(2).replace('.', ',');
reflEl.textContent = Math.round(s.albedo * 100) + ' %';
absEl.textContent = Math.round((1 - s.albedo) * 100) + ' %';
root.querySelectorAll('.ge-albedo-choice .ge-btn').forEach(b => {
b.classList.toggle('ge-btn-primary', b.dataset.key === s.key);
});
}
root.querySelectorAll('.ge-albedo-choice .ge-btn').forEach(b => {
b.addEventListener('click', () => setSurface(b.dataset.key));
});
setSurface('snow');
}
// =====================================================================
// Experiment: CO₂-Fußabdruck-Rechner — schneller Alltags-Schätzer
// =====================================================================
function footprintCalc(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🧮 Schätze deinen CO₂-Fußabdruck</h3>
<p>Einfache Auswahl pro Bereich. Das Ergebnis ist eine Richtgröße — kein genauer Wert.</p>
</div>
<div class="ge-calc">
<div class="ge-calc-row">
<div class="ge-calc-q">🏠 Wohnen</div>
<div class="ge-calc-opts" data-key="home">
<button class="ge-btn" data-val="1.0">Wärmepumpe / Fernwärme</button>
<button class="ge-btn ge-btn-primary" data-val="2.0">Gas/Öl, mittlere Größe</button>
<button class="ge-btn" data-val="3.5">Altbau, schlecht isoliert</button>
</div>
</div>
<div class="ge-calc-row">
<div class="ge-calc-q">🚗 Verkehr</div>
<div class="ge-calc-opts" data-key="car">
<button class="ge-btn" data-val="0.3">meist zu Fuß / Rad / ÖV</button>
<button class="ge-btn ge-btn-primary" data-val="1.8">Pkw mittel, kein Flug</button>
<button class="ge-btn" data-val="4.5">Pkw viel + 1 Fernflug/Jahr</button>
</div>
</div>
<div class="ge-calc-row">
<div class="ge-calc-q">🍽 Ernährung</div>
<div class="ge-calc-opts" data-key="food">
<button class="ge-btn" data-val="1.2">vegan / vegetarisch</button>
<button class="ge-btn ge-btn-primary" data-val="1.8">gemischt, wenig Fleisch</button>
<button class="ge-btn" data-val="2.6">viel Fleisch und Milchprodukte</button>
</div>
</div>
<div class="ge-calc-row">
<div class="ge-calc-q">🛒 Konsum</div>
<div class="ge-calc-opts" data-key="shop">
<button class="ge-btn" data-val="1.0">bewusst, wenig Neues</button>
<button class="ge-btn ge-btn-primary" data-val="2.0">durchschnittlich</button>
<button class="ge-btn" data-val="3.0">häufig Neues / Elektronik</button>
</div>
</div>
</div>
<div class="ge-calc-result">
<div>Dein geschätzter Fußabdruck:</div>
<strong class="ge-calc-total">7,6 t CO₂ / Jahr</strong>
<div class="ge-calc-bar"><div class="ge-calc-fill" style="width: 76%"></div></div>
<div class="ge-calc-scale"><span>0 t</span><span>Welt-Ø 4,7</span><span>AT-Ø 7,7</span><span>10 t</span></div>
<div class="ge-calc-label">Klimaneutral-Ziel: unter 1 t — Welt-Durchschnitt: 4,7 t — Österreich: 7,7 t.</div>
</div>
<p class="ge-note">Die Anteile decken sich mit den Haupt-Kategorien im Klimaschutzbericht 2024 des Umweltbundesamts Österreich. Details pro Kategorie unter <a href="?term=co2-fussabdruck#gl-source-3" class="gl-inline-link" data-key="co2-fussabdruck">CO₂-Fußabdruck</a>.</p>
</div>
`;
const totals = { home: 2.0, car: 1.8, food: 1.8, shop: 2.0 };
function update() {
const sum = Object.values(totals).reduce((a, b) => a + b, 0);
root.querySelector('.ge-calc-total').textContent = sum.toFixed(1).replace('.', ',') + ' t CO₂ / Jahr';
const pct = Math.min(100, (sum / 10) * 100);
root.querySelector('.ge-calc-fill').style.width = pct + '%';
}
root.querySelectorAll('.ge-calc-opts').forEach(group => {
const key = group.dataset.key;
group.querySelectorAll('.ge-btn').forEach(btn => {
btn.addEventListener('click', () => {
group.querySelectorAll('.ge-btn').forEach(b => b.classList.remove('ge-btn-primary'));
btn.classList.add('ge-btn-primary');
totals[key] = parseFloat(btn.dataset.val);
update();
});
});
});
update();
}
// Registry — Key entspricht key_slug aus DB
window.GlossarExperiments = {
'kilowattstunde': kwhRace,
'ppm': ppmGrid,
'albedo': albedoSlider,
'co2-fussabdruck': footprintCalc
};
})();