/**
* Glossar-Experimente — Wirtschaft & Energie.
*
* Registrierung wie in glossar-experiments.js:
* 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.
*
* Richtlinien:
* - Keine externen Libraries, Vanilla JS + SVG.
* - Mobile-/Tap-freundlich: grosse Buttons, keine Hover-Abhängigkeit.
* - Zahlen quellenbar, kurze Begründung in der UI sichtbar (.ge-note).
* - Rechenzeichen: Malpunkt · und Doppelpunkt : (nie ×, ÷, *).
*/
(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;
};
// Ganzzahl mit Tausenderpunkt: 56250 → "56.250"
const fmtInt = n => Math.round(n).toLocaleString('de-AT');
// Dezimalzahl mit Komma: 22.5 → "22,50" (digits Nachkommastellen)
const fmtDec = (n, digits) => n.toLocaleString('de-AT', {
minimumFractionDigits: digits, maximumFractionDigits: digits
});
// =====================================================================
// Experiment: Preiselastizität — der beste Skipass-Preis
// =====================================================================
function priceElasticity(root) {
root.innerHTML = `
🎿 Finde den besten Skipass-Preis
Je teurer der Skipass, desto weniger Gäste kommen — die Nachfrage reagiert auf den Preis.
Der Umsatz ist Preis · Gäste . Wo ist er am größten?
Rechnung: 25 € · 2.000 Gäste = 50.000 €
…
Vereinfachtes lineares Modell wie in der Tourismustal-Simulation:
Gäste = 3.000 − 40 · Preis. Echte Nachfragekurven sind komplizierter, aber das Prinzip
der Preiselastizität gilt überall: Der höchste Preis bringt nicht automatisch den
höchsten Umsatz.
`;
const svg = root.querySelector('.ge-bal-svg');
const rng = root.querySelector('#ge-pe-range');
const priceOut = root.querySelector('#ge-pe-price');
const calcOut = root.querySelector('#ge-pe-calc');
const verdict = root.querySelector('#ge-pe-verdict');
const BAR_X = 120, BAR_W = 400;
const MAX_GUESTS = 2400, MAX_REV = 60000, BEST_REV = 56250; // Maximum bei 37,50 €
// Zeile 1: Gäste
svgEl('text', { x: BAR_X - 10, y: 50, 'text-anchor': 'end', 'font-size': '13', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = 'Gäste';
svgEl('rect', { x: BAR_X, y: 32, width: BAR_W, height: 26, fill: '#eff5f6', rx: '6' }, svg);
const guestBar = svgEl('rect', { x: BAR_X, y: 32, width: 0, height: 26, fill: '#4a7c8a', rx: '6' }, svg);
const guestTxt = svgEl('text', { x: BAR_X, y: 50, 'font-size': '13', 'font-weight': '800', fill: '#1f4e5a' }, svg);
// Zeile 2: Umsatz
svgEl('text', { x: BAR_X - 10, y: 118, 'text-anchor': 'end', 'font-size': '13', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = 'Umsatz';
svgEl('rect', { x: BAR_X, y: 100, width: BAR_W, height: 26, fill: '#eff5f6', rx: '6' }, svg);
const revBar = svgEl('rect', { x: BAR_X, y: 100, width: 0, height: 26, fill: '#5a8a5e', rx: '6' }, svg);
const revTxt = svgEl('text', { x: BAR_X, y: 118, 'font-size': '13', 'font-weight': '800', fill: '#1f4e5a' }, svg);
// Markierung: bestmöglicher Umsatz
const bestX = BAR_X + (BEST_REV / MAX_REV) * BAR_W;
svgEl('line', { x1: bestX, y1: 88, x2: bestX, y2: 138, stroke: '#c85c4a', 'stroke-width': '2', 'stroke-dasharray': '4 3' }, svg);
const bestLbl = svgEl('text', { x: bestX, y: 82, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '700', fill: '#c85c4a' }, svg);
bestLbl.textContent = 'Maximum: 56.250 €';
function update() {
const p = +rng.value;
const guests = Math.max(0, 3000 - 40 * p);
const rev = p * guests;
priceOut.textContent = p + ' €';
calcOut.textContent = p + ' € · ' + fmtInt(guests) + ' Gäste = ' + fmtInt(rev) + ' €';
const gw = (guests / MAX_GUESTS) * BAR_W;
guestBar.setAttribute('width', gw);
guestTxt.setAttribute('x', BAR_X + gw + 8);
guestTxt.textContent = fmtInt(guests);
const rw = (rev / MAX_REV) * BAR_W;
revBar.setAttribute('width', rw);
revTxt.setAttribute('x', BAR_X + rw + 8);
revTxt.textContent = fmtInt(rev) + ' €';
// Text nicht mit der Maximum-Linie kollidieren lassen
revTxt.setAttribute('opacity', (BAR_X + rw + 75 > bestX && BAR_X + rw < bestX + 10) ? '0.35' : '1');
let v, state;
if (Math.abs(p - 37.5) <= 2) { v = '🎯 Fast genau das Maximum!'; state = 'good'; }
else if (p < 30) { v = '🟡 Zu billig — volle Pisten, wenig Umsatz'; state = 'warn'; }
else if (p <= 45) { v = '🟢 Guter Bereich'; state = 'now'; }
else { v = '🔴 Zu teuer — die Gäste bleiben aus'; state = 'bad'; }
verdict.textContent = v;
verdict.dataset.state = state;
}
rng.addEventListener('input', update);
update();
}
// =====================================================================
// Experiment: Engpass — die Kette ist so stark wie ihr schwächstes Glied
// =====================================================================
function bottleneckChain(root) {
root.innerHTML = `
`;
const svg = root.querySelector('.ge-bal-svg');
const ROAD_Y = 185;
const stations = [
{ key: 'anreise', label: 'Anreise', emoji: '🚌', x: 150 },
{ key: 'betten', label: 'Betten', emoji: '🛏', x: 310 },
{ key: 'personal', label: 'Personal', emoji: '👩🍳', x: 470 }
];
const caps = { anreise: 160, betten: 80, personal: 120 };
// Straße
svgEl('line', { x1: 10, y1: ROAD_Y, x2: 590, y2: ROAD_Y, stroke: '#dae8ec', 'stroke-width': '14', 'stroke-linecap': 'round' }, svg);
svgEl('text', { x: 14, y: ROAD_Y - 16, 'font-size': '11', fill: '#4a4a4a', 'font-weight': '600' }, svg).textContent = 'Gäste →';
svgEl('text', { x: 588, y: ROAD_Y - 16, 'text-anchor': 'end', 'font-size': '11', fill: '#5a8a5e', 'font-weight': '700' }, svg).textContent = 'zufrieden 😊';
// Stations-Boxen über der Straße
stations.forEach(s => {
s.box = svgEl('rect', { x: s.x - 55, y: 30, width: 110, height: 92, rx: '10', fill: '#ffffff', stroke: '#b5c9cf', 'stroke-width': '2' }, svg);
const em = svgEl('text', { x: s.x, y: 66, 'text-anchor': 'middle', 'font-size': '26' }, svg);
em.textContent = s.emoji;
const lbl = svgEl('text', { x: s.x, y: 90, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '800', fill: '#1f4e5a' }, svg);
lbl.textContent = s.label;
s.capTxt = svgEl('text', { x: s.x, y: 108, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '600', fill: '#4a4a4a' }, svg);
// Schranke auf der Straße
svgEl('line', { x1: s.x, y1: ROAD_Y - 12, x2: s.x, y2: ROAD_Y + 12, stroke: '#4a7c8a', 'stroke-width': '3', 'stroke-dasharray': '4 3' }, svg);
});
const dotGroup = svgEl('g', {}, svg);
// --- Mini-Verkehrssimulation -------------------------------------
const DAY_MS = 12000; // 12 s entsprechen einem Tag
const PER_DOT = 10; // 1 Punkt entspricht 10 Gästen
const INFLOW = 240; // Interessierte Gäste pro Tag (Nachfrage)
const SPEED = 0.085; // px pro ms
const GAP = 14; // Mindestabstand der Punkte
let particles = []; // { x, el }
let lastSpawn = 0;
const lastPass = { anreise: 0, betten: 0, personal: 0 };
let animId = null, lastT = 0;
function bottleneck() {
let minKey = 'anreise';
for (const k in caps) if (caps[k] < caps[minKey]) minKey = k;
return minKey;
}
function refreshUI() {
const minKey = bottleneck();
const minVal = caps[minKey];
const bLabel = stations.find(s => s.key === minKey).label;
root.querySelector('#ge-en-out').textContent =
'Minimum(' + caps.anreise + '; ' + caps.betten + '; ' + caps.personal + ') = ' + minVal;
const verdict = root.querySelector('#ge-en-verdict');
verdict.textContent = 'Engpass: ' + bLabel;
verdict.dataset.state = minVal >= 160 ? 'good' : (minVal >= 80 ? 'warn' : 'bad');
stations.forEach(s => {
s.capTxt.textContent = caps[s.key] + ' / Tag';
const isMin = s.key === minKey;
s.box.setAttribute('stroke', isMin ? '#c85c4a' : '#b5c9cf');
s.box.setAttribute('stroke-width', isMin ? '3' : '2');
});
}
function spawn(now) {
const interval = DAY_MS / (INFLOW / PER_DOT);
if (now - lastSpawn < interval || particles.length >= 70) return;
// Startplatz frei?
if (particles.some(p => p.x < 24)) return;
lastSpawn = now;
const el = svgEl('circle', { cx: 10, cy: ROAD_Y + (Math.random() * 8 - 4), r: 5, fill: '#4a7c8a', opacity: '0.9' }, dotGroup);
particles.push({ x: 10, el });
}
function step(now, dt) {
spawn(now);
// vorderste zuerst (größtes x)
particles.sort((a, b) => b.x - a.x);
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
let nx = p.x + SPEED * dt;
// Nicht auf den Vordermann auffahren
if (i > 0) nx = Math.min(nx, particles[i - 1].x - GAP);
// Nächste Schranke vor uns?
for (const s of stations) {
if (p.x < s.x && nx >= s.x) {
const interval = DAY_MS / (caps[s.key] / PER_DOT);
if (now - lastPass[s.key] >= interval) {
lastPass[s.key] = now; // durchgelassen
} else {
nx = s.x - 0.5; // warten → Stau bildet sich
}
break;
}
}
if (nx > p.x) p.x = nx; // nur vorwärts, nie rückwärts
p.el.setAttribute('cx', p.x);
}
// Ziel erreicht
particles = particles.filter(p => {
if (p.x > 585) { p.el.remove(); return false; }
return true;
});
}
function tick(now) {
if (!root.isConnected) { cancelAnimationFrame(animId); return; }
const dt = lastT ? Math.min(50, now - lastT) : 16;
lastT = now;
step(now, dt);
animId = requestAnimationFrame(tick);
}
animId = requestAnimationFrame(tick);
stations.forEach(s => {
const rng = root.querySelector('#ge-en-' + s.key);
rng.addEventListener('input', () => {
caps[s.key] = +rng.value;
root.querySelector('#ge-en-' + s.key + '-val').textContent = rng.value;
refreshUI();
});
});
refreshUI();
}
// =====================================================================
// Experiment: Skaleneffekt — Fixkosten verteilen sich auf mehr Gäste
// =====================================================================
function scaleEffect(root) {
root.innerHTML = `
🏠 Vier Gasthäuser — einzeln oder zusammengelegt?
Küche, Verwaltung und Werbung kosten 120.000 € im Jahr — egal ob für
ein Haus oder für vier. Lege Gasthäuser zu einem Betrieb zusammen und beobachte die
Kosten pro Gast .
Modellzahlen: zentrale Fixkosten (Küche, Verwaltung, Werbung) 120.000 € pro Jahr,
Gebäudekosten 60.000 € je Haus, 4.000 Gäste je Haus. Dieses Sinken der Stückkosten bei
wachsender Betriebsgröße heißt Skaleneffekt (Fixkostendegression) — ein Hauptgrund, warum
sich Betriebe zusammenschließen.
`;
const svg = root.querySelector('.ge-bal-svg');
const rng = root.querySelector('#ge-sk-range');
const nOut = root.querySelector('#ge-sk-n');
const calcOut = root.querySelector('#ge-sk-calc');
const verdict = root.querySelector('#ge-sk-verdict');
const CENTRAL = 120000, PER_HOUSE = 60000, GUESTS = 4000;
const costPerGuest = n => (CENTRAL + n * PER_HOUSE) / (n * GUESTS); // 45 / 30 / 25 / 22,50
function drawHouse(parent, x, y, w, h, joined) {
svgEl('rect', { x, y, width: w, height: h, fill: '#e8d5b5', stroke: '#1f4e5a', 'stroke-width': '1.5' }, parent);
svgEl('polygon', { points: (x - 3) + ',' + y + ' ' + (x + w / 2) + ',' + (y - 16) + ' ' + (x + w + 3) + ',' + y, fill: joined ? '#4a7c8a' : '#c85c4a' }, parent);
}
// Linke Hälfte: 4 einzelne Häuser (statisch), jedes mit eigener Küche
svgEl('text', { x: 150, y: 22, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '800', fill: '#1f4e5a' }, svg).textContent = 'Jedes Haus für sich';
for (let i = 0; i < 4; i++) {
const hx = 45 + i * 56;
drawHouse(svg, hx, 48, 46, 34, false);
svgEl('text', { x: hx + 23, y: 72, 'text-anchor': 'middle', 'font-size': '15' }, svg).textContent = '🍳';
}
// Rechte Hälfte: zusammengelegter Betrieb (dynamisch)
svgEl('text', { x: 450, y: 22, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '800', fill: '#1f4e5a' }, svg).textContent = 'Zusammengelegt';
const mergedG = svgEl('g', {}, svg);
// Balken: Stückkosten je Gast (Skala 0 – 50 €, Höhe 110 px)
const BAR_BASE = 240, BAR_MAXH = 110, SCALE = 50;
svgEl('line', { x1: 30, y1: BAR_BASE, x2: 570, y2: BAR_BASE, stroke: '#b5c9cf', 'stroke-width': '1.5' }, svg);
// Referenzbalken links: 45 € (statisch)
const refH = 45 / SCALE * BAR_MAXH;
svgEl('rect', { x: 120, y: BAR_BASE - refH, width: 60, height: refH, fill: '#c85c4a', rx: '6', opacity: '0.85' }, svg);
svgEl('text', { x: 150, y: BAR_BASE - refH - 8, 'text-anchor': 'middle', 'font-size': '13', 'font-weight': '800', fill: '#c85c4a' }, svg).textContent = '45,00 € je Gast';
// Dynamischer Balken rechts
const dynBar = svgEl('rect', { x: 420, y: BAR_BASE, width: 60, height: 0, fill: '#5a8a5e', rx: '6' }, svg);
const dynTxt = svgEl('text', { x: 450, y: BAR_BASE - 8, 'text-anchor': 'middle', 'font-size': '13', 'font-weight': '800', fill: '#5a8a5e' }, svg);
function update() {
const n = +rng.value;
nOut.textContent = n + (n === 1 ? ' Gasthaus' : ' Gasthäuser');
// Zusammengelegte Häuser zeichnen: n Häuser ohne Lücke, EINE Küche
mergedG.innerHTML = '';
const w = 46, totalW = n * w;
const startX = 450 - totalW / 2;
for (let i = 0; i < n; i++) drawHouse(mergedG, startX + i * w, 48, w, 34, true);
svgEl('text', { x: 450, y: 72, 'text-anchor': 'middle', 'font-size': '15' }, mergedG).textContent = '🍳';
const cap = svgEl('text', { x: 450, y: 100, 'text-anchor': 'middle', 'font-size': '10', fill: '#4a4a4a' }, mergedG);
cap.textContent = n === 1 ? 'eine Küche, eine Verwaltung' : 'eine Küche, eine Verwaltung für ' + n + ' Häuser';
const c = costPerGuest(n);
const h = c / SCALE * BAR_MAXH;
dynBar.setAttribute('y', BAR_BASE - h);
dynBar.setAttribute('height', h);
dynTxt.setAttribute('y', BAR_BASE - h - 8);
dynTxt.textContent = fmtDec(c, 2) + ' € je Gast';
calcOut.textContent = '(120.000 € + ' + n + ' · 60.000 €) : ' + fmtInt(n * GUESTS) + ' Gäste = ' + fmtDec(c, 2) + ' €';
if (n === 1) {
verdict.textContent = 'Ausgangslage: jedes Haus trägt alle Fixkosten allein';
verdict.dataset.state = 'now';
} else if (n < 4) {
verdict.textContent = '🟢 Stückkosten sinken: Fixkosten verteilen sich auf ' + fmtInt(n * GUESTS) + ' Gäste';
verdict.dataset.state = 'good';
} else {
verdict.textContent = '🎯 4 kleine: 45,00 € — 1 großes: 22,50 € je Gast, also halbiert!';
verdict.dataset.state = 'good';
}
}
rng.addEventListener('input', update);
update();
}
// =====================================================================
// Experiment: Modal Split — Verkehrsmittel-Mix und CO₂
// =====================================================================
function modalSplit(root) {
root.innerHTML = `
🚦 Wer fährt womit in den Urlaub?
Verteile die Anreisen auf Auto, Bus und Bahn — die Summe bleibt automatisch 100 %.
Beobachte, wie sich der CO₂-Ausstoß je Person verändert.
🚗 Auto: 60 %
🚌 Reisebus: 20 %
🚆 Bahn: 20 %
nur Bahn: 26 g nur Bus: 30 g nur Auto: 164 g
Ø CO₂: … je Personen-km
Anreise 400 km: … je Person
…
Durchschnittswerte je Personen-Kilometer: Pkw 164 g · Reisebus 30 g ·
Bahn 26 g CO₂. Quelle: Umweltbundesamt (DE), Emissionsdaten Personenverkehr 2024.
Die Aufteilung der Wege auf Verkehrsmittel heißt Modal Split — Besucherlenkung Richtung
Bus und Bahn ist einer der größten Klimahebel im Tourismus.
`;
const FACTORS = { auto: 164, bus: 30, bahn: 26 };
const shares = { auto: 60, bus: 20, bahn: 20 };
const keys = ['auto', 'bus', 'bahn'];
const sliders = {}, labels = {};
keys.forEach(k => {
sliders[k] = root.querySelector('#ge-ms-' + k);
labels[k] = root.querySelector('#ge-ms-' + k + '-val');
});
const stack = root.querySelector('#ge-ms-stack');
const co2bar = root.querySelector('#ge-ms-co2bar');
const gkmOut = root.querySelector('#ge-ms-gkm');
const tripOut = root.querySelector('#ge-ms-trip');
const verdict = root.querySelector('#ge-ms-verdict');
function render() {
keys.forEach(k => {
sliders[k].value = shares[k];
labels[k].textContent = shares[k] + ' %';
stack.querySelector('[data-seg="' + k + '"]').style.width = shares[k] + '%';
});
const gkm = (shares.auto * FACTORS.auto + shares.bus * FACTORS.bus + shares.bahn * FACTORS.bahn) / 100;
gkmOut.textContent = fmtDec(gkm, 0) + ' g';
tripOut.textContent = fmtDec(gkm * 400 / 1000, 1) + ' kg CO₂';
co2bar.style.width = Math.max(3, gkm / FACTORS.auto * 100) + '%';
if (gkm < 55) { verdict.textContent = '🟢 Klimafreundlicher Mix'; verdict.dataset.state = 'good'; }
else if (gkm < 110) { verdict.textContent = '🟠 Mittelfeld — Bahn hilft'; verdict.dataset.state = 'warn'; }
else { verdict.textContent = '🔴 Fast alles Auto'; verdict.dataset.state = 'bad'; }
}
function onInput(key) {
const val = Math.max(0, Math.min(100, Math.round(+sliders[key].value)));
const others = keys.filter(k => k !== key);
const rest = 100 - val;
const oldSum = shares[others[0]] + shares[others[1]];
let a;
if (oldSum <= 0) a = Math.round(rest / 2);
else a = Math.round(rest * shares[others[0]] / oldSum);
shares[key] = val;
shares[others[0]] = a;
shares[others[1]] = rest - a;
render();
}
keys.forEach(k => sliders[k].addEventListener('input', () => onInput(k)));
render();
}
// =====================================================================
// Experiment: Netzfrequenz — der Pulsschlag des Stromnetzes
// =====================================================================
function gridFrequency(root) {
root.innerHTML = `
⚡ Halte das Netz bei 50 Hertz
Im Stromnetz müssen Erzeugung und Verbrauch in jeder Sekunde gleich groß
sein. Passen sie nicht zusammen, driftet die Netzfrequenz weg von 50 Hz. Probiere es!
🏭 Erzeugung (Kraftwerke, Wind, Sonne): 80 GW
🏘 Verbrauch (Haushalte, Industrie): 80 GW
Bilanz: Erzeugung − Verbrauch = 0 GW
🟢 Netz stabil
↺ Zurücksetzen auf 50,0 Hz
Sollfrequenz im europäischen Verbundnetz: 50 Hz. Sinkt sie unter 49,8 Hz,
greift der 5-Stufen-Plan der Netzbetreiber: Erste Verbraucher werden automatisch
abgeworfen (Lastabwurf), um einen Blackout zu verhindern. Quellen: ENTSO-E ·
VDE-Netzregeln. Vereinfachtes Modell — echte Netze steuern mit Regelreserven binnen
Sekunden dagegen.
`;
const svg = root.querySelector('.ge-gh-svg');
const CX = 300, CY = 245, R = 185;
const rad = a => a * Math.PI / 180;
const pt = (a, r) => [CX + r * Math.sin(rad(a)), CY - r * Math.cos(rad(a))];
const freqToAngle = f => Math.max(-80, Math.min(80, (f - 50) / 0.2 * 80));
const arcPath = (a1, a2, r) => {
const [x1, y1] = pt(a1, r), [x2, y2] = pt(a2, r);
return 'M ' + x1 + ' ' + y1 + ' A ' + r + ' ' + r + ' 0 0 1 ' + x2 + ' ' + y2;
};
// Farbzonen
[
{ a1: -80, a2: -40, c: '#c85c4a' }, // 49,80 – 49,90
{ a1: -40, a2: -20, c: '#e8833a' }, // 49,90 – 49,95
{ a1: -20, a2: 20, c: '#5a8a5e' }, // 49,95 – 50,05
{ a1: 20, a2: 40, c: '#e8833a' },
{ a1: 40, a2: 80, c: '#c85c4a' }
].forEach(z => svgEl('path', { d: arcPath(z.a1, z.a2, R), stroke: z.c, 'stroke-width': '20', fill: 'none', 'stroke-linecap': 'butt', opacity: '0.9' }, svg));
// Skala
[[49.8, '49,8'], [49.9, '49,9'], [50.0, '50,0'], [50.1, '50,1'], [50.2, '50,2']].forEach(([f, lbl]) => {
const a = freqToAngle(f);
const [x1, y1] = pt(a, R - 14), [x2, y2] = pt(a, R + 14);
svgEl('line', { x1, y1, x2, y2, stroke: '#1f4e5a', 'stroke-width': '2' }, svg);
const [tx, ty] = pt(a, R + 30);
const t = svgEl('text', { x: tx, y: ty + 4, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '700', fill: '#1f4e5a' }, svg);
t.textContent = lbl;
});
svgEl('text', { x: CX, y: 60, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '600', fill: '#4a4a4a' }, svg).textContent = 'Hertz (Hz) = Schwingungen pro Sekunde';
// Zeiger
const needle = svgEl('g', { transform: 'rotate(0, ' + CX + ', ' + CY + ')' }, svg);
svgEl('line', { x1: CX, y1: CY, x2: CX, y2: CY - R + 30, stroke: '#1f4e5a', 'stroke-width': '5', 'stroke-linecap': 'round' }, needle);
svgEl('circle', { cx: CX, cy: CY, r: 10, fill: '#1f4e5a' }, svg);
// Digitalanzeige
const digital = svgEl('text', { x: CX, y: CY + 34, 'text-anchor': 'middle', 'font-size': '22', 'font-weight': '900', fill: '#1f4e5a' }, svg);
const genRng = root.querySelector('#ge-nf-gen');
const loadRng = root.querySelector('#ge-nf-load');
const genVal = root.querySelector('#ge-nf-gen-val');
const loadVal = root.querySelector('#ge-nf-load-val');
const diffOut = root.querySelector('#ge-nf-diff');
const status = root.querySelector('#ge-nf-status');
let freq = 50.0;
const K = 0.0055; // Hz pro (GW · Sekunde) — bewusst anschaulich skaliert
let animId = null, lastT = 0;
function refreshLabels() {
genVal.textContent = genRng.value;
loadVal.textContent = loadRng.value;
const d = genRng.value - loadRng.value;
diffOut.textContent = (d > 0 ? '+' : '') + d + ' GW';
}
function tick(now) {
if (!root.isConnected) { cancelAnimationFrame(animId); return; }
const dt = lastT ? Math.min(60, now - lastT) : 16;
lastT = now;
const imbalance = (+genRng.value) - (+loadRng.value);
freq = Math.max(49.78, Math.min(50.22, freq + imbalance * K * dt / 1000));
needle.setAttribute('transform', 'rotate(' + freqToAngle(freq).toFixed(2) + ', ' + CX + ', ' + CY + ')');
digital.textContent = freq.toFixed(3).replace('.', ',') + ' Hz';
if (freq <= 49.8) {
status.textContent = '🔴 Unter 49,8 Hz: Lastabwurf! Verbraucher werden abgetrennt';
status.dataset.state = 'bad';
} else if (freq >= 50.2) {
status.textContent = '🔴 Über 50,2 Hz: Kraftwerke müssen sofort drosseln';
status.dataset.state = 'bad';
} else if (Math.abs(freq - 50) > 0.05) {
status.textContent = '🟠 Warnzone — Regelenergie nötig';
status.dataset.state = 'warn';
} else {
status.textContent = '🟢 Netz stabil';
status.dataset.state = 'good';
}
animId = requestAnimationFrame(tick);
}
genRng.addEventListener('input', refreshLabels);
loadRng.addEventListener('input', refreshLabels);
root.querySelector('[data-act="reset"]').addEventListener('click', () => {
freq = 50.0;
genRng.value = 80;
loadRng.value = 80;
refreshLabels();
});
refreshLabels();
animId = requestAnimationFrame(tick);
}
// =====================================================================
// Experiment: Lageenergie / Pumpspeicher — Strom als gehobenes Wasser
// =====================================================================
function pumpedStorage(root) {
root.innerHTML = `
🏔 Der Stromspeicher im Berg
Bei Stromüberschuss pumpt das Kraftwerk Wasser ins Oberbecken — Strom wird zu
Lageenergie . Bei Strommangel rauscht das Wasser durch die Turbine
wieder hinunter. Aber: Ein Teil der Energie geht verloren.
▲ Pumpen — Strom hinein (20 kWh)
▼ Turbinieren — Strom heraus
Strom eingesetzt: 0 kWh
Strom zurückgewonnen: 0 kWh
Verlust (Wärme, Reibung): 0 kWh
Das Unterbecken ist voll — pumpe Wasser hinauf!
Pumpspeicherkraftwerke sind bis heute der einzige großtechnische
Stromspeicher: Aus 100 kWh Pumpstrom kommen rund 75 – 80 kWh wieder heraus
(Gesamtwirkungsgrad, hier gerechnet mit 78 %). Quellen: VERBUND (Malta- und
Kaprun-Kraftwerke) · Fraunhofer ISE.
`;
const svg = root.querySelector('.ge-gh-svg');
const EFF = 0.78; // Gesamtwirkungsgrad
const STEP_UNITS = 20; // Wasser-Einheiten je Klick; 1 Einheit ≙ 1 kWh Pumpstrom
// --- Szene ---------------------------------------------------------
// Berg links mit Plateau für das Oberbecken
svgEl('polygon', { points: '0,330 0,150 60,80 250,80 290,160 360,310 600,310 600,330', fill: '#5a8a5e' }, svg);
svgEl('polygon', { points: '0,330 0,240 120,200 300,260 600,315 600,330', fill: '#4a7050', opacity: '.6' }, svg);
// Oberbecken (Behälter)
svgEl('rect', { x: 66, y: 82, width: 168, height: 48, fill: '#3f5f52', rx: '4' }, svg);
const upperWater = svgEl('rect', { x: 70, y: 126, width: 160, height: 0, fill: '#4a7c8a' }, svg);
svgEl('text', { x: 150, y: 74, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '800', fill: '#1f4e5a' }, svg).textContent = 'Oberbecken';
// Fallhöhe-Markierung
svgEl('line', { x1: 320, y1: 130, x2: 320, y2: 250, stroke: '#1f4e5a', 'stroke-width': '1.2', 'stroke-dasharray': '4 3', opacity: '.6' }, svg);
svgEl('text', { x: 328, y: 195, 'font-size': '10', fill: '#1f4e5a', opacity: '.8' }, svg).textContent = 'Fallhöhe';
// Druckrohr Oberbecken → Krafthaus
const pipeA = { x: 226, y: 124 }, pipeB = { x: 408, y: 262 };
svgEl('line', { x1: pipeA.x, y1: pipeA.y, x2: pipeB.x, y2: pipeB.y, stroke: '#4a4a4a', 'stroke-width': '12', 'stroke-linecap': 'round' }, svg);
svgEl('line', { x1: pipeA.x, y1: pipeA.y, x2: pipeB.x, y2: pipeB.y, stroke: '#dae8ec', 'stroke-width': '6', 'stroke-linecap': 'round' }, svg);
// Krafthaus mit Pumpe/Turbine
svgEl('rect', { x: 385, y: 252, width: 76, height: 58, fill: '#e8d5b5', stroke: '#1f4e5a', 'stroke-width': '1.5', rx: '4' }, svg);
svgEl('polygon', { points: '382,253 423,235 464,253', fill: '#c85c4a' }, svg);
const gear = svgEl('text', { x: 423, y: 288, 'text-anchor': 'middle', 'font-size': '20' }, svg);
gear.textContent = '⚙';
const modeLbl = svgEl('text', { x: 423, y: 326, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '800', fill: '#1f4e5a' }, svg);
modeLbl.textContent = 'Pumpe / Turbine';
// Unterbecken rechts vom Krafthaus
svgEl('rect', { x: 476, y: 252, width: 112, height: 56, fill: '#3f5f52', rx: '4' }, svg);
const lowerWater = svgEl('rect', { x: 480, y: 304, width: 104, height: 0, fill: '#4a7c8a' }, svg);
svgEl('text', { x: 532, y: 244, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '800', fill: '#1f4e5a' }, svg).textContent = 'Unterbecken';
// Wasser-Punkte im Rohr (nur während der Animation sichtbar)
const flowDots = [];
for (let i = 0; i < 4; i++) flowDots.push(svgEl('circle', { r: 4.5, fill: '#7fb3c4', opacity: '0' }, svg));
// --- Zustand ---------------------------------------------------------
let upper = 0, lower = 100; // Wasser-Einheiten
let energyIn = 0, energyOut = 0; // kWh
let animId = null, animating = false;
const inOut = root.querySelector('#ge-ps-in');
const outOut = root.querySelector('#ge-ps-out');
const lossOut = root.querySelector('#ge-ps-loss');
const status = root.querySelector('#ge-ps-status');
const btnPump = root.querySelector('[data-act="pump"]');
const btnTurb = root.querySelector('[data-act="turbine"]');
function setWater(u) {
// Oberbecken: max 44 px, Unterbecken: max 48 px
const uh = u / 100 * 44;
upperWater.setAttribute('y', 126 - uh);
upperWater.setAttribute('height', uh);
const lh = (100 - u) / 100 * 48;
lowerWater.setAttribute('y', 304 - lh);
lowerWater.setAttribute('height', lh);
}
function refreshAccount() {
inOut.textContent = fmtDec(energyIn, 0) + ' kWh';
outOut.textContent = fmtDec(energyOut, 1).replace(',0', '') + ' kWh';
const stored = upper * EFF; // noch abrufbarer Strom
const loss = Math.max(0, energyIn - energyOut - stored);
lossOut.textContent = fmtDec(loss, 1).replace(',0', '') + ' kWh';
}
function refreshButtons() {
btnPump.disabled = animating || lower < STEP_UNITS;
btnTurb.disabled = animating || upper < STEP_UNITS;
btnPump.style.opacity = btnPump.disabled ? '.45' : '1';
btnTurb.style.opacity = btnTurb.disabled ? '.45' : '1';
}
function animate(mode) {
if (animating) return;
if (mode === 'pump' && lower < STEP_UNITS) return;
if (mode === 'turbine' && upper < STEP_UNITS) return;
animating = true;
refreshButtons();
modeLbl.textContent = mode === 'pump' ? 'Pumpe läuft ▲' : 'Turbine läuft ▼';
const startU = upper;
const targetU = mode === 'pump' ? upper + STEP_UNITS : upper - STEP_UNITS;
const DUR = 1400;
const t0 = performance.now();
function frame(now) {
if (!root.isConnected) { cancelAnimationFrame(animId); return; }
const t = Math.min(1, (now - t0) / DUR);
const e = 1 - Math.pow(1 - t, 3);
setWater(startU + (targetU - startU) * e);
// Wasser-Punkte im Rohr
flowDots.forEach((d, i) => {
let k = (t * 2.2 + i / flowDots.length) % 1;
if (mode === 'turbine') k = 1 - k; // bergab
d.setAttribute('cx', pipeA.x + (pipeB.x - pipeA.x) * k);
d.setAttribute('cy', pipeA.y + (pipeB.y - pipeA.y) * k);
d.setAttribute('opacity', '0.95');
});
if (t < 1) { animId = requestAnimationFrame(frame); return; }
// fertig
flowDots.forEach(d => d.setAttribute('opacity', '0'));
upper = targetU;
lower = 100 - upper;
if (mode === 'pump') {
energyIn += STEP_UNITS; // 20 kWh Strom hinein
status.textContent = '⚡ 20 kWh Strom als Lageenergie gespeichert';
status.dataset.state = 'now';
} else {
const gain = STEP_UNITS * EFF; // 15,6 kWh zurück
energyOut += gain;
status.textContent = '💡 ' + fmtDec(gain, 1) + ' kWh zurückgewonnen — ' + fmtDec(STEP_UNITS - gain, 1) + ' kWh verloren (78 %)';
status.dataset.state = 'good';
}
modeLbl.textContent = 'Pumpe / Turbine';
animating = false;
refreshAccount();
refreshButtons();
}
animId = requestAnimationFrame(frame);
}
btnPump.addEventListener('click', () => animate('pump'));
btnTurb.addEventListener('click', () => animate('turbine'));
setWater(upper);
refreshAccount();
refreshButtons();
}
// Registry — Keys entsprechen key_slug aus der Tabelle `glossar`
window.GlossarExperiments = Object.assign(window.GlossarExperiments || {}, {
'preiselastizitaet': priceElasticity,
'engpass': bottleneckChain,
'skaleneffekt': scaleEffect,
'modal-split': modalSplit,
'netzfrequenz': gridFrequency,
'hertz': gridFrequency,
'lageenergie': pumpedStorage,
'pumpspeicher': pumpedStorage
});
})();