Glossar visuell komplett: 162 Bilder, 13 Kapitel-Icons, 12 interaktive Widgets
- 162 neue Glossar-Bilder (Flat-Scandinavian, gpt-image-1) → alle 402 Einträge bebildert; image_path-Update: App/Don_t_Deploy/2026-07-16-glossar-bilder.sql - 13 Kapitel-Icons (icon-<id>.webp) ersetzen die Emojis in Buch-Übersicht + Kapitel-h1 (buchKapIcon-Helper mit Emoji-Fallback) - 12 interaktive Glossar-Experimente in 2 Modulen: natur (Mondrotation, Kernschatten, Keeling-Kurve, Pegel/HQ100, Wärmeausdehnung, Klimaträgheit) + wirtschaft (Preiselastizität, Engpass, Skaleneffekt, Modal Split, Netzfrequenz/Hertz, Lageenergie/Pumpspeicher); Registry via Object.assign, Script-Einbindung in glossar.php Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,741 @@
|
||||
/**
|
||||
* 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 = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🎿 Finde den besten Skipass-Preis</h3>
|
||||
<p>Je teurer der Skipass, desto weniger Gäste kommen — die Nachfrage reagiert auf den Preis.
|
||||
Der Umsatz ist <strong>Preis · Gäste</strong>. Wo ist er am größten?</p>
|
||||
</div>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="ge-pe-range">Skipass-Preis: <strong id="ge-pe-price">25 €</strong></label>
|
||||
<input id="ge-pe-range" type="range" min="15" max="60" value="25" step="1">
|
||||
<div class="ge-gh-markers"><span>15 €</span><span>30 €</span><span>45 €</span><span>60 €</span></div>
|
||||
</div>
|
||||
<svg class="ge-bal-svg" viewBox="0 0 600 170" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill">Rechnung: <strong id="ge-pe-calc">25 € · 2.000 Gäste = 50.000 €</strong></div>
|
||||
<div class="ge-gh-pill" id="ge-pe-verdict">…</div>
|
||||
</div>
|
||||
<p class="ge-note">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.</p>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>⛓ Wo klemmt es im Urlaubsort?</h3>
|
||||
<p>Gäste durchlaufen drei Stationen: <strong>Anreise → Betten → Personal</strong>.
|
||||
Stelle die Kapazitäten ein und beobachte, wo sich die Punkte stauen.</p>
|
||||
</div>
|
||||
<svg class="ge-bal-svg" viewBox="0 0 600 230" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="ge-en-anreise">🚌 Anreise (Busse, Bahn, Straße): <strong id="ge-en-anreise-val">160</strong> Gäste pro Tag</label>
|
||||
<input id="ge-en-anreise" type="range" min="20" max="200" value="160" step="10">
|
||||
</div>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="ge-en-betten">🛏 Betten (Hotels, Pensionen): <strong id="ge-en-betten-val">80</strong> Gäste pro Tag</label>
|
||||
<input id="ge-en-betten" type="range" min="20" max="200" value="80" step="10">
|
||||
</div>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="ge-en-personal">👩🍳 Personal (Küche, Service, Reinigung): <strong id="ge-en-personal-val">120</strong> Gäste pro Tag</label>
|
||||
<input id="ge-en-personal" type="range" min="20" max="200" value="120" step="10">
|
||||
</div>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill">Zufriedene Gäste: <strong id="ge-en-out">Minimum(160; 80; 120) = 80</strong> pro Tag</div>
|
||||
<div class="ge-gh-pill" id="ge-en-verdict" data-state="warn">Engpass: Betten</div>
|
||||
</div>
|
||||
<p class="ge-note">Prinzip der Engpass-Theorie („Theory of Constraints", E. M. Goldratt 1984):
|
||||
Der Durchsatz einer Kette wird allein vom schwächsten Glied bestimmt. Mehr Kapazität an
|
||||
anderen Stellen bringt nichts, solange der Engpass bleibt. Modellzahlen vereinfacht.</p>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🏠 Vier Gasthäuser — einzeln oder zusammengelegt?</h3>
|
||||
<p>Küche, Verwaltung und Werbung kosten <strong>120.000 € im Jahr</strong> — egal ob für
|
||||
ein Haus oder für vier. Lege Gasthäuser zu einem Betrieb zusammen und beobachte die
|
||||
Kosten <strong>pro Gast</strong>.</p>
|
||||
</div>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="ge-sk-range">Zusammengelegt: <strong id="ge-sk-n">1 Gasthaus</strong></label>
|
||||
<input id="ge-sk-range" type="range" min="1" max="4" value="1" step="1">
|
||||
<div class="ge-gh-markers"><span>1</span><span>2</span><span>3</span><span>4</span></div>
|
||||
</div>
|
||||
<svg class="ge-bal-svg" viewBox="0 0 600 260" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill">Rechnung: <strong id="ge-sk-calc">…</strong></div>
|
||||
<div class="ge-gh-pill" id="ge-sk-verdict">…</div>
|
||||
</div>
|
||||
<p class="ge-note">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.</p>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🚦 Wer fährt womit in den Urlaub?</h3>
|
||||
<p>Verteile die Anreisen auf Auto, Bus und Bahn — die Summe bleibt automatisch 100 %.
|
||||
Beobachte, wie sich der CO₂-Ausstoß je Person verändert.</p>
|
||||
</div>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="ge-ms-auto">🚗 Auto: <strong id="ge-ms-auto-val">60 %</strong></label>
|
||||
<input id="ge-ms-auto" type="range" min="0" max="100" value="60" step="1">
|
||||
</div>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="ge-ms-bus">🚌 Reisebus: <strong id="ge-ms-bus-val">20 %</strong></label>
|
||||
<input id="ge-ms-bus" type="range" min="0" max="100" value="20" step="1">
|
||||
</div>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="ge-ms-bahn">🚆 Bahn: <strong id="ge-ms-bahn-val">20 %</strong></label>
|
||||
<input id="ge-ms-bahn" type="range" min="0" max="100" value="20" step="1">
|
||||
</div>
|
||||
<div id="ge-ms-stack" style="display:flex;height:26px;border-radius:8px;overflow:hidden;border:1px solid #b5c9cf">
|
||||
<div data-seg="auto" style="background:#c85c4a;transition:width .3s ease"></div>
|
||||
<div data-seg="bus" style="background:#e8833a;transition:width .3s ease"></div>
|
||||
<div data-seg="bahn" style="background:#5a8a5e;transition:width .3s ease"></div>
|
||||
</div>
|
||||
<div class="ge-calc-bar"><div class="ge-calc-fill" id="ge-ms-co2bar" style="width:100%"></div></div>
|
||||
<div class="ge-calc-scale"><span>nur Bahn: 26 g</span><span>nur Bus: 30 g</span><span>nur Auto: 164 g</span></div>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill">Ø CO₂: <strong id="ge-ms-gkm">…</strong> je Personen-km</div>
|
||||
<div class="ge-gh-pill">Anreise 400 km: <strong id="ge-ms-trip">…</strong> je Person</div>
|
||||
<div class="ge-gh-pill" id="ge-ms-verdict">…</div>
|
||||
</div>
|
||||
<p class="ge-note">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.</p>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>⚡ Halte das Netz bei 50 Hertz</h3>
|
||||
<p>Im Stromnetz müssen Erzeugung und Verbrauch <strong>in jeder Sekunde</strong> gleich groß
|
||||
sein. Passen sie nicht zusammen, driftet die Netzfrequenz weg von 50 Hz. Probiere es!</p>
|
||||
</div>
|
||||
<svg class="ge-gh-svg" viewBox="0 0 600 290" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="ge-nf-gen">🏭 Erzeugung (Kraftwerke, Wind, Sonne): <strong id="ge-nf-gen-val">80</strong> GW</label>
|
||||
<input id="ge-nf-gen" type="range" min="60" max="100" value="80" step="1">
|
||||
</div>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="ge-nf-load">🏘 Verbrauch (Haushalte, Industrie): <strong id="ge-nf-load-val">80</strong> GW</label>
|
||||
<input id="ge-nf-load" type="range" min="60" max="100" value="80" step="1">
|
||||
</div>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill">Bilanz: Erzeugung − Verbrauch = <strong id="ge-nf-diff">0 GW</strong></div>
|
||||
<div class="ge-gh-pill" id="ge-nf-status" data-state="good">🟢 Netz stabil</div>
|
||||
</div>
|
||||
<div class="ge-controls">
|
||||
<button class="ge-btn" data-act="reset">↺ Zurücksetzen auf 50,0 Hz</button>
|
||||
</div>
|
||||
<p class="ge-note">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.</p>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🏔 Der Stromspeicher im Berg</h3>
|
||||
<p>Bei Stromüberschuss pumpt das Kraftwerk Wasser ins Oberbecken — Strom wird zu
|
||||
<strong>Lageenergie</strong>. Bei Strommangel rauscht das Wasser durch die Turbine
|
||||
wieder hinunter. Aber: Ein Teil der Energie geht verloren.</p>
|
||||
</div>
|
||||
<svg class="ge-gh-svg" viewBox="0 0 600 330" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
|
||||
<div class="ge-controls">
|
||||
<button class="ge-btn ge-btn-primary" data-act="pump">▲ Pumpen — Strom hinein (20 kWh)</button>
|
||||
<button class="ge-btn" data-act="turbine">▼ Turbinieren — Strom heraus</button>
|
||||
</div>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill">Strom eingesetzt: <strong id="ge-ps-in">0 kWh</strong></div>
|
||||
<div class="ge-gh-pill">Strom zurückgewonnen: <strong id="ge-ps-out">0 kWh</strong></div>
|
||||
<div class="ge-gh-pill">Verlust (Wärme, Reibung): <strong id="ge-ps-loss">0 kWh</strong></div>
|
||||
<div class="ge-gh-pill" id="ge-ps-status" data-state="now">Das Unterbecken ist voll — pumpe Wasser hinauf!</div>
|
||||
</div>
|
||||
<p class="ge-note">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.</p>
|
||||
</div>
|
||||
`;
|
||||
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
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user