Glossar: 51 neue Einträge komplett — Bilder + 12 interaktive Widgets
Bilder (Flat-Scandinavian, gpt-image-1) für alle 51 neuen Begriffe; 12 Widgets in neu1.js (strommix, eisschild, klimabilanz, megawattstunde, niederschlag, fruchtfolge) und neu2.js (halbschatten, blockabfertigung, grundfreiheiten, synergie, aufenthaltsdauer, grundwasser). image_path + module-Mappings, key_slug-basiert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 92 KiB |
@@ -0,0 +1,557 @@
|
||||
/**
|
||||
* Glossar-Experimente „neu1" — Energie, Klima, Boden.
|
||||
*
|
||||
* Registrierung wie in glossar-experiments.js:
|
||||
* window.GlossarExperiments[key_slug] = function (root) { … }
|
||||
* Der Renderer (glossar.php) übergibt einen leeren Container; das Widget baut
|
||||
* seinen DOM selbst und bindet seine Events. Styles: nur .ge-* aus glossar.php.
|
||||
*
|
||||
* Richtlinien:
|
||||
* - Keine externen Libraries, Vanilla JS + SVG.
|
||||
* - Mobile-/Tap-freundlich, keine Hover-Abhängigkeit.
|
||||
* - Zahlen mit kurzer Quellenangabe in der .ge-note.
|
||||
* - Rechenzeichen · und : (nie ×/÷), deutsche Zahlen via toLocaleString.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const NS = 'http://www.w3.org/2000/svg';
|
||||
const svgEl = (tag, attrs, parent) => {
|
||||
const el = document.createElementNS(NS, tag);
|
||||
if (attrs) for (const k in attrs) el.setAttribute(k, attrs[k]);
|
||||
if (parent) parent.appendChild(el);
|
||||
return el;
|
||||
};
|
||||
const deInt = (n) => Math.round(n).toLocaleString('de-AT');
|
||||
const deNum = (n, dec) => n.toFixed(dec === undefined ? 1 : dec).replace('.', ',');
|
||||
|
||||
// =====================================================================
|
||||
// Experiment: strommix — 5 Regler, CO₂-Ampel für den Strom-Mix
|
||||
// =====================================================================
|
||||
function strommix(root) {
|
||||
// Lebenszyklus-CO₂ je kWh (Median IPCC AR5), gerundet in g/kWh
|
||||
const sources = [
|
||||
{ key: 'wasser', label: '💧 Wasserkraft', co2: 24, color: '#4a7c8a', val: 40 },
|
||||
{ key: 'wind', label: '🌬️ Windkraft', co2: 11, color: '#5a8a5e', val: 25 },
|
||||
{ key: 'sonne', label: '☀️ Sonne (PV)', co2: 48, color: '#e8c547', val: 15 },
|
||||
{ key: 'gas', label: '🔥 Erdgas', co2: 490, color: '#e8833a', val: 15 },
|
||||
{ key: 'kohle', label: '🪨 Kohle', co2: 820, color: '#4a4a4a', val: 5 }
|
||||
];
|
||||
|
||||
root.innerHTML = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🔌 Baue deinen Strom-Mix</h3>
|
||||
<p>Stell mit den Reglern ein, woher der Strom kommt. Die Anteile werden immer auf <strong>100 %</strong> gerechnet. Beobachte die CO₂-Ampel!</p>
|
||||
</div>
|
||||
<svg class="ge-gh-svg" viewBox="0 0 600 70" preserveAspectRatio="none" aria-hidden="true"></svg>
|
||||
<div class="ge-gh-slider" id="sm-sliders"></div>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill">CO₂ je Kilowattstunde: <strong id="sm-co2">–</strong> g</div>
|
||||
<div class="ge-gh-pill" id="sm-verdict">–</div>
|
||||
</div>
|
||||
<div class="ge-calc-bar"><div class="ge-calc-fill" id="sm-bar" style="width:0%"></div></div>
|
||||
<div class="ge-calc-scale"><span>0 g</span><span>sauber</span><span>schmutzig</span><span>820 g</span></div>
|
||||
<p class="ge-note">CO₂-Werte je kWh über den ganzen Lebensweg (Median): Wind 11 g · Wasser 24 g · Sonne 48 g · Erdgas 490 g · Kohle 820 g. Quelle: IPCC AR5 (2014), Anhang III. Viel Wasser, Wind und Sonne = sehr wenig CO₂.</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const bar = svgEl('g', {}, root.querySelector('.ge-gh-svg'));
|
||||
const co2Out = root.querySelector('#sm-co2');
|
||||
const verdict = root.querySelector('#sm-verdict');
|
||||
const barFill = root.querySelector('#sm-bar');
|
||||
const sliderBox = root.querySelector('#sm-sliders');
|
||||
if (!sliderBox) return;
|
||||
|
||||
const shareEls = {};
|
||||
sources.forEach(s => {
|
||||
const row = document.createElement('div');
|
||||
row.style.marginBottom = '.55rem';
|
||||
row.innerHTML = `
|
||||
<label for="sm-${s.key}" style="display:flex;justify-content:space-between;">
|
||||
<span>${s.label}</span><strong id="sm-share-${s.key}">0 %</strong>
|
||||
</label>
|
||||
<input id="sm-${s.key}" type="range" min="0" max="100" step="5" value="${s.val}">
|
||||
`;
|
||||
sliderBox.appendChild(row);
|
||||
shareEls[s.key] = row.querySelector('#sm-share-' + s.key);
|
||||
const inp = row.querySelector('input');
|
||||
inp.addEventListener('input', () => { s.val = +inp.value; update(); });
|
||||
});
|
||||
|
||||
function update() {
|
||||
const sum = sources.reduce((a, s) => a + s.val, 0) || 1;
|
||||
// Gestapelter Farbbalken + Anteil-Texte
|
||||
bar.innerHTML = '';
|
||||
let x = 0;
|
||||
let co2mix = 0;
|
||||
sources.forEach(s => {
|
||||
const share = s.val / sum;
|
||||
co2mix += share * s.co2;
|
||||
const w = share * 600;
|
||||
if (w > 0.5) svgEl('rect', { x, y: 0, width: w, height: 70, fill: s.color }, bar);
|
||||
x += w;
|
||||
if (shareEls[s.key]) shareEls[s.key].textContent = Math.round(share * 100) + ' %';
|
||||
});
|
||||
|
||||
co2Out.textContent = deInt(co2mix);
|
||||
barFill.style.width = Math.min(100, (co2mix / 820) * 100) + '%';
|
||||
|
||||
let state, txt;
|
||||
if (co2mix < 80) { state = 'good'; txt = '🟢 sehr sauber'; }
|
||||
else if (co2mix < 250) { state = 'now'; txt = '🟡 noch ok'; }
|
||||
else if (co2mix < 500) { state = 'warn'; txt = '🟠 zu viel CO₂'; }
|
||||
else { state = 'bad'; txt = '🔴 sehr schmutzig'; }
|
||||
barFill.style.background = state === 'good' ? '#5a8a5e' : state === 'now' ? '#4a7c8a' : state === 'warn' ? '#e8833a' : '#c85c4a';
|
||||
verdict.textContent = txt;
|
||||
verdict.dataset.state = state;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Experiment: eisschild — Temperatur → Eisschmelze → Meeresspiegel
|
||||
// =====================================================================
|
||||
function eisschild(root) {
|
||||
root.innerHTML = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🧊 Wenn das Eis schmilzt</h3>
|
||||
<p>Schiebe den Regler und mach es wärmer. Das Eis auf Grönland und der Antarktis schmilzt — und das Meer steigt. Was passiert mit der Küste?</p>
|
||||
</div>
|
||||
<svg class="ge-gh-svg" viewBox="0 0 600 320" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="es-range">Erwärmung: <strong id="es-temp">+0,0 °C</strong></label>
|
||||
<input id="es-range" type="range" min="0" max="50" value="0" step="1">
|
||||
<div class="ge-gh-markers"><span>0</span><span>+1,5</span><span>+3</span><span>+5 °C</span></div>
|
||||
</div>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill">Meeresspiegel: <strong id="es-sea">+0,0 m</strong></div>
|
||||
<div class="ge-gh-pill" id="es-verdict">–</div>
|
||||
</div>
|
||||
<p class="ge-note">Vollständig geschmolzen hebt Grönland-Eis den Meeresspiegel um rund 7 m, das Antarktis-Eis um rund 58 m. Das dauert sehr lange, doch schon ab etwa +1,5 °C beginnt Grönland zu kippen. Quelle: AWI (Alfred-Wegener-Institut).</p>
|
||||
</div>
|
||||
`;
|
||||
const svg = root.querySelector('.ge-gh-svg');
|
||||
const rng = root.querySelector('#es-range');
|
||||
const tempOut = root.querySelector('#es-temp');
|
||||
const seaOut = root.querySelector('#es-sea');
|
||||
const verdict = root.querySelector('#es-verdict');
|
||||
if (!svg || !rng) return;
|
||||
|
||||
// Himmel + Sonne
|
||||
svgEl('rect', { x: 0, y: 0, width: 600, height: 320, fill: '#dce9ee' }, svg);
|
||||
const sun = svgEl('circle', { cx: 520, cy: 55, r: 30, fill: '#f4c94e' }, svg);
|
||||
|
||||
// Zwei Eisschilde als Blöcke, die von oben schrumpfen (y wächst, height sinkt)
|
||||
// Grönland links, Antarktis Mitte-rechts. Basis auf Landsockel bei y=210.
|
||||
const landY = 210;
|
||||
svgEl('rect', { x: 40, y: landY, width: 150, height: 24, fill: '#6b8e6f' }, svg); // Grönland-Land
|
||||
svgEl('rect', { x: 230, y: landY, width: 210, height: 24, fill: '#6b8e6f' }, svg); // Antarktis-Land
|
||||
const groenIce = svgEl('rect', { x: 45, y: 90, width: 140, height: 120, fill: '#f4f8fb', stroke: '#b5c9cf', 'stroke-width': '1.5' }, svg);
|
||||
const antaIce = svgEl('rect', { x: 235, y: 60, width: 200, height: 150, fill: '#f4f8fb', stroke: '#b5c9cf', 'stroke-width': '1.5' }, svg);
|
||||
svgEl('text', { x: 115, y: 82, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = 'Grönland';
|
||||
svgEl('text', { x: 335, y: 52, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = 'Antarktis';
|
||||
|
||||
// Küste rechts mit Häusern
|
||||
svgEl('rect', { x: 470, y: landY, width: 130, height: 90, fill: '#c4a97a' }, svg);
|
||||
const houses = [
|
||||
{ x: 486, base: 200, h: 26 },
|
||||
{ x: 522, base: 195, h: 32 },
|
||||
{ x: 560, base: 190, h: 38 }
|
||||
].map(hd => {
|
||||
const g = svgEl('g', {}, svg);
|
||||
svgEl('rect', { x: hd.x, y: hd.base - hd.h, width: 26, height: hd.h, fill: '#e8e4d8', stroke: '#1f4e5a', 'stroke-width': '1' }, g);
|
||||
svgEl('polygon', { points: `${hd.x - 3},${hd.base - hd.h} ${hd.x + 13},${hd.base - hd.h - 12} ${hd.x + 29},${hd.base - hd.h}`, fill: '#c85c4a' }, g);
|
||||
return { g, floodY: hd.base - hd.h + 6 };
|
||||
});
|
||||
|
||||
// Wasser (steigt) ganz vorne
|
||||
const seaMaxTop = 120, seaBottom = 320;
|
||||
const sea = svgEl('rect', { x: 0, y: seaBottom - 40, width: 600, height: 40, fill: '#4a7c8a', opacity: '0.82' }, svg);
|
||||
|
||||
function update() {
|
||||
const temp = +rng.value / 10; // 0..5 °C
|
||||
tempOut.textContent = '+' + deNum(temp, 1) + ' °C';
|
||||
|
||||
// Schmelzanteile (vereinfachte Schwellen, langfristig)
|
||||
const groenFrac = Math.max(0, Math.min(1, (temp - 1.5) / 3.5));
|
||||
const antaFrac = Math.max(0, Math.min(1, (temp - 2.0) / 6.0));
|
||||
const seaM = groenFrac * 7 + antaFrac * 58; // in Metern (langfristiges Maximum)
|
||||
|
||||
// Eisblöcke schrumpfen von oben
|
||||
const gFullY = 90, gFullH = 120;
|
||||
const gH = gFullH * (1 - groenFrac);
|
||||
groenIce.setAttribute('y', gFullY + (gFullH - gH));
|
||||
groenIce.setAttribute('height', Math.max(0, gH));
|
||||
const aFullY = 60, aFullH = 150;
|
||||
const aH = aFullH * (1 - antaFrac);
|
||||
antaIce.setAttribute('y', aFullY + (aFullH - aH));
|
||||
antaIce.setAttribute('height', Math.max(0, aH));
|
||||
|
||||
// Wasserstand: 0 m → y=280 (Höhe 40), 65 m → y=seaMaxTop
|
||||
const frac = Math.min(1, seaM / 65);
|
||||
const top = 280 - frac * (280 - seaMaxTop);
|
||||
sea.setAttribute('y', top);
|
||||
sea.setAttribute('height', seaBottom - top);
|
||||
|
||||
seaOut.textContent = '+' + deNum(seaM, seaM < 10 ? 1 : 0) + ' m';
|
||||
|
||||
houses.forEach(h => {
|
||||
const flooded = top <= h.floodY;
|
||||
h.g.setAttribute('opacity', flooded ? '0.35' : '1');
|
||||
});
|
||||
|
||||
let state, txt;
|
||||
if (temp < 1.5) { state = 'good'; txt = '🟢 Eis bleibt stabil'; }
|
||||
else if (temp < 2.5) { state = 'now'; txt = '🟡 Grönland kippt'; }
|
||||
else if (temp < 4) { state = 'warn'; txt = '🟠 Küsten in Gefahr'; }
|
||||
else { state = 'bad'; txt = '🔴 Küsten überflutet'; }
|
||||
sun.setAttribute('fill', temp > 3 ? '#e8833a' : '#f4c94e');
|
||||
verdict.textContent = txt;
|
||||
verdict.dataset.state = state;
|
||||
}
|
||||
rng.addEventListener('input', update);
|
||||
update();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Experiment: klimabilanz — Alltags-Entscheidungen → Jahres-CO₂
|
||||
// =====================================================================
|
||||
function klimabilanz(root) {
|
||||
// Grundlast (Wohnen, Konsum, Öffentliches) fix
|
||||
const BASE = 2.2;
|
||||
const cats = [
|
||||
{ key: 'food', q: '🍽️ Essen', opts: [
|
||||
{ t: 'Viel Fleisch', v: 2.6 },
|
||||
{ t: 'Gemischt', v: 1.7, sel: true },
|
||||
{ t: 'Gemüse/vegetarisch', v: 1.0 } ] },
|
||||
{ key: 'move', q: '🚗 Wege', opts: [
|
||||
{ t: 'Meist Auto', v: 3.0 },
|
||||
{ t: 'Gemischt', v: 1.5, sel: true },
|
||||
{ t: 'Rad & Bus/Bahn', v: 0.4 } ] },
|
||||
{ key: 'fly', q: '✈️ Flugreise', opts: [
|
||||
{ t: 'Ein Fernflug', v: 2.0, sel: true },
|
||||
{ t: 'Kein Flug', v: 0.0 } ] },
|
||||
{ key: 'power', q: '💡 Strom', opts: [
|
||||
{ t: 'Normalstrom', v: 1.2, sel: true },
|
||||
{ t: 'Ökostrom', v: 0.2 } ] }
|
||||
];
|
||||
const chosen = {};
|
||||
|
||||
root.innerHTML = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🌍 Deine Klimabilanz für ein Jahr</h3>
|
||||
<p>Tippe an, wie du lebst. Der Balken zählt zusammen, wie viel CO₂ du in einem Jahr verursachst.</p>
|
||||
</div>
|
||||
<div class="ge-calc" id="kb-calc"></div>
|
||||
<div class="ge-calc-result">
|
||||
<div>Dein Jahres-Ausstoß:</div>
|
||||
<strong class="ge-calc-total" id="kb-total">–</strong>
|
||||
<div class="ge-calc-bar"><div class="ge-calc-fill" id="kb-bar" style="width:0%"></div></div>
|
||||
<div class="ge-calc-scale"><span>0 t</span><span>Ziel 1 – 2 t</span><span>AT-Ø 8 t</span><span>12 t</span></div>
|
||||
<div class="ge-calc-label" id="kb-verdict"></div>
|
||||
</div>
|
||||
<p class="ge-note">In Österreich verursacht jede Person rund 8 t CO₂ pro Jahr. Fürs Klima-Ziel wären es nur etwa 1 – 2 t. Quelle: Umweltbundesamt Österreich. Die Werte sind Richtgrößen, kein Messwert.</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const box = root.querySelector('#kb-calc');
|
||||
const totalOut = root.querySelector('#kb-total');
|
||||
const barFill = root.querySelector('#kb-bar');
|
||||
const verdict = root.querySelector('#kb-verdict');
|
||||
if (!box) return;
|
||||
|
||||
cats.forEach(cat => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'ge-calc-row';
|
||||
row.innerHTML = `<div class="ge-calc-q">${cat.q}</div><div class="ge-calc-opts" data-key="${cat.key}"></div>`;
|
||||
const opts = row.querySelector('.ge-calc-opts');
|
||||
cat.opts.forEach(o => {
|
||||
const b = document.createElement('button');
|
||||
b.className = 'ge-btn' + (o.sel ? ' ge-btn-primary' : '');
|
||||
b.textContent = o.t;
|
||||
b.dataset.val = o.v;
|
||||
if (o.sel) chosen[cat.key] = o.v;
|
||||
b.addEventListener('click', () => {
|
||||
opts.querySelectorAll('.ge-btn').forEach(x => x.classList.remove('ge-btn-primary'));
|
||||
b.classList.add('ge-btn-primary');
|
||||
chosen[cat.key] = +b.dataset.val;
|
||||
update();
|
||||
});
|
||||
opts.appendChild(b);
|
||||
});
|
||||
box.appendChild(row);
|
||||
});
|
||||
|
||||
function update() {
|
||||
const sum = BASE + Object.values(chosen).reduce((a, b) => a + b, 0);
|
||||
totalOut.textContent = deNum(sum, 1) + ' t CO₂';
|
||||
barFill.style.width = Math.min(100, (sum / 12) * 100) + '%';
|
||||
let txt, col;
|
||||
if (sum <= 2.5) { txt = '🟢 Super — nahe am Klima-Ziel!'; col = '#5a8a5e'; }
|
||||
else if (sum <= 5) { txt = '🟡 Schon gut, geht noch weniger.'; col = '#4a7c8a'; }
|
||||
else if (sum <= 8) { txt = '🟠 Etwa österreichischer Durchschnitt.'; col = '#e8833a'; }
|
||||
else { txt = '🔴 Ziemlich viel — hier lässt sich sparen.'; col = '#c85c4a'; }
|
||||
barFill.style.background = col;
|
||||
verdict.textContent = txt;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Experiment: megawattstunde — wie lange reicht 1 MWh?
|
||||
// =====================================================================
|
||||
function megawattstunde(root) {
|
||||
// 1 MWh = 1000 kWh
|
||||
const items = [
|
||||
{ key: 'led', label: '💡 LED-Lampe (10 W)', detail: (1000 / 0.010), unit: 'h', color: '#e8c547' },
|
||||
{ key: 'kuehl',label: '🧊 Kühlschrank (150 kWh/Jahr)', detail: (1000 / 150) * 365, unit: 'd', color: '#4a7c8a' },
|
||||
{ key: 'haus', label: '🏠 Haushalt (3.500 kWh/Jahr)', detail: (1000 / 3500) * 365, unit: 'd', color: '#5a8a5e' },
|
||||
{ key: 'auto', label: '🚗 E-Auto (18 kWh/100 km)', detail: (1000 / 18) * 100, unit: 'km', color: '#c85c4a' }
|
||||
];
|
||||
|
||||
root.innerHTML = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>⚡ Wie weit reicht 1 Megawattstunde?</h3>
|
||||
<p>Eine Megawattstunde (1 MWh = 1.000 kWh) ist viel Energie. Tippe einen Verbraucher an und schau, wie lange oder wie weit sie reicht.</p>
|
||||
</div>
|
||||
<div class="ge-albedo-choice" id="mw-choice"></div>
|
||||
<svg class="ge-gh-svg" viewBox="0 0 600 90" preserveAspectRatio="none" aria-hidden="true"></svg>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill">1 MWh reicht für: <strong id="mw-out">–</strong></div>
|
||||
</div>
|
||||
<p class="ge-note">Ein Durchschnittshaushalt in Österreich braucht rund 3.500 kWh Strom pro Jahr. Quelle: E-Control. Rechnung: 1.000 kWh : Verbrauch.</p>
|
||||
</div>
|
||||
`;
|
||||
const choice = root.querySelector('#mw-choice');
|
||||
const svg = root.querySelector('.ge-gh-svg');
|
||||
const out = root.querySelector('#mw-out');
|
||||
if (!choice || !svg) return;
|
||||
|
||||
const bar = svgEl('rect', { x: 0, y: 25, width: 0, height: 40, fill: '#4a7c8a', rx: 6 }, svg);
|
||||
|
||||
function fmtReach(it) {
|
||||
if (it.unit === 'h') {
|
||||
const years = it.detail / 24 / 365;
|
||||
return deInt(it.detail) + ' Stunden Dauerlicht (≈ ' + deNum(years, 0) + ' Jahre)';
|
||||
}
|
||||
if (it.unit === 'd') {
|
||||
const years = it.detail / 365;
|
||||
if (years >= 1) return deNum(years, 1) + ' Jahre';
|
||||
return deInt(it.detail) + ' Tage';
|
||||
}
|
||||
return deInt(it.detail) + ' km weit fahren';
|
||||
}
|
||||
|
||||
function select(it) {
|
||||
out.textContent = fmtReach(it);
|
||||
// Balkenlänge auf logarithmischer Skala (Werte reichen von ~100 bis 100.000)
|
||||
const logv = Math.log10(Math.max(10, it.detail));
|
||||
const w = Math.min(1, (logv - 1) / 4) * 590 + 10;
|
||||
bar.setAttribute('width', w);
|
||||
bar.setAttribute('fill', it.color);
|
||||
Array.from(choice.children).forEach(b => b.classList.toggle('ge-btn-primary', b.dataset.key === it.key));
|
||||
}
|
||||
|
||||
items.forEach((it, i) => {
|
||||
const b = document.createElement('button');
|
||||
b.className = 'ge-btn' + (i === 0 ? ' ge-btn-primary' : '');
|
||||
b.textContent = it.label;
|
||||
b.dataset.key = it.key;
|
||||
b.addEventListener('click', () => select(it));
|
||||
choice.appendChild(b);
|
||||
});
|
||||
select(items[0]);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Experiment: niederschlag — Regenmenge formt die Landschaft
|
||||
// =====================================================================
|
||||
function niederschlag(root) {
|
||||
root.innerHTML = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🌧️ Regen macht die Landschaft</h3>
|
||||
<p>Schiebe den Regler von wenig zu viel Regen. Beobachte, wie aus trockener Steppe ein grüner Bergwald wird.</p>
|
||||
</div>
|
||||
<svg class="ge-track" viewBox="0 0 600 300" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="ns-range">Regen pro Jahr: <strong id="ns-mm">300 mm</strong></label>
|
||||
<input id="ns-range" type="range" min="300" max="3000" value="300" step="50">
|
||||
<div class="ge-gh-markers"><span>300<br>Steppe</span><span>1.100<br>Wiese</span><span>3.000<br>Bergwald</span></div>
|
||||
</div>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill" id="ns-verdict">–</div>
|
||||
</div>
|
||||
<p class="ge-note">In Österreich fallen im trockenen Osten (Seewinkel) unter 600 mm, im nassen Bregenzerwald über 2.000 mm Regen pro Jahr. Quelle: GeoSphere Austria.</p>
|
||||
</div>
|
||||
`;
|
||||
const svg = root.querySelector('.ge-track');
|
||||
const rng = root.querySelector('#ns-range');
|
||||
const mmOut = root.querySelector('#ns-mm');
|
||||
const verdict = root.querySelector('#ns-verdict');
|
||||
if (!svg || !rng) return;
|
||||
|
||||
const sky = svgEl('rect', { x: 0, y: 0, width: 600, height: 200, fill: '#cfe3ea' }, svg);
|
||||
const sun = svgEl('circle', { cx: 90, cy: 55, r: 28, fill: '#f4c94e' }, svg);
|
||||
const cloud = svgEl('g', { opacity: '0' }, svg);
|
||||
svgEl('ellipse', { cx: 300, cy: 45, rx: 70, ry: 26, fill: '#8fa6ad' }, cloud);
|
||||
svgEl('ellipse', { cx: 360, cy: 55, rx: 55, ry: 22, fill: '#9fb4bb' }, cloud);
|
||||
const rainG = svgEl('g', {}, svg);
|
||||
// Hügel / Boden
|
||||
const ground = svgEl('path', { d: 'M0,200 Q150,150 300,190 T600,180 L600,300 L0,300 Z', fill: '#c9b483' }, svg);
|
||||
const vegG = svgEl('g', {}, svg);
|
||||
|
||||
function update() {
|
||||
const mm = +rng.value;
|
||||
mmOut.textContent = deInt(mm) + ' mm';
|
||||
const frac = (mm - 300) / (3000 - 300); // 0..1
|
||||
|
||||
// Himmel wird grauer, Sonne blasser, Wolke + Regen erscheinen
|
||||
sky.setAttribute('fill', frac < 0.5 ? '#cfe3ea' : '#b9ccd3');
|
||||
sun.setAttribute('opacity', (1 - frac * 0.8).toFixed(2));
|
||||
cloud.setAttribute('opacity', Math.min(1, frac * 1.4).toFixed(2));
|
||||
|
||||
// Boden wird grüner
|
||||
const r = Math.round(201 - frac * 127);
|
||||
const g = Math.round(180 + frac * 20);
|
||||
const b = Math.round(131 - frac * 60);
|
||||
ground.setAttribute('fill', `rgb(${r},${g},${b})`);
|
||||
|
||||
// Regentropfen
|
||||
rainG.innerHTML = '';
|
||||
const drops = Math.round(frac * 40);
|
||||
for (let i = 0; i < drops; i++) {
|
||||
const x = 250 + Math.random() * 160;
|
||||
const y = 70 + Math.random() * 110;
|
||||
svgEl('line', { x1: x, y1: y, x2: x - 4, y2: y + 12, stroke: '#4a7c8a', 'stroke-width': '2', 'stroke-linecap': 'round', opacity: '0.7' }, rainG);
|
||||
}
|
||||
|
||||
// Vegetation: von einzelnen Grasbüscheln zu vielen Bäumen
|
||||
vegG.innerHTML = '';
|
||||
const plants = Math.round(4 + frac * 22);
|
||||
for (let i = 0; i < plants; i++) {
|
||||
const x = 20 + (i / plants) * 560 + (Math.sin(i * 3.1) * 12);
|
||||
const baseY = 205 + (i % 3) * 22;
|
||||
if (frac > 0.55 && i % 2 === 0) {
|
||||
// Baum
|
||||
const h = 26 + frac * 20;
|
||||
svgEl('rect', { x: x - 2, y: baseY - h * 0.35, width: 4, height: h * 0.35, fill: '#6b4a2e' }, vegG);
|
||||
svgEl('polygon', { points: `${x},${baseY - h} ${x - 12},${baseY - h * 0.35} ${x + 12},${baseY - h * 0.35}`, fill: '#3a6b3e' }, vegG);
|
||||
} else {
|
||||
// Grasbüschel — Farbe je nach Regen
|
||||
const col = frac < 0.4 ? '#a89a5e' : '#5a8a5e';
|
||||
svgEl('path', { d: `M${x},${baseY} l-3,-10 M${x},${baseY} l0,-13 M${x},${baseY} l3,-10`, stroke: col, 'stroke-width': '2', 'stroke-linecap': 'round', fill: 'none' }, vegG);
|
||||
}
|
||||
}
|
||||
|
||||
let state, txt;
|
||||
if (mm < 600) { state = 'warn'; txt = '🏜️ Trockene Steppe'; }
|
||||
else if (mm < 1500) { state = 'now'; txt = '🌱 Grüne Wiesen & Felder'; }
|
||||
else { state = 'good'; txt = '🌲 Dichter Bergwald'; }
|
||||
verdict.textContent = txt;
|
||||
verdict.dataset.state = state;
|
||||
}
|
||||
rng.addEventListener('input', update);
|
||||
update();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Experiment: fruchtfolge — 3 Jahre Feld planen, Bodengesundheit
|
||||
// =====================================================================
|
||||
function fruchtfolge(root) {
|
||||
// effect = Wirkung auf Boden; legume = Stickstoff-Sammler
|
||||
const plants = [
|
||||
{ key: 'weizen', label: '🌾 Weizen', effect: -6, legume: false },
|
||||
{ key: 'mais', label: '🌽 Mais', effect: -12, legume: false },
|
||||
{ key: 'kartoffel', label: '🥔 Kartoffel', effect: -12, legume: false },
|
||||
{ key: 'erbsen', label: '🫛 Erbsen', effect: 15, legume: true },
|
||||
{ key: 'klee', label: '☘️ Klee', effect: 15, legume: true }
|
||||
];
|
||||
const years = [null, null, null];
|
||||
|
||||
root.innerHTML = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🚜 Plane dein Feld für 3 Jahre</h3>
|
||||
<p>Wähle für jedes Jahr eine Pflanze. Baust du immer dasselbe an (Monokultur), wird der Boden müde. Ein Wechsel — vor allem mit Erbsen oder Klee — hält ihn gesund.</p>
|
||||
</div>
|
||||
<div class="ge-calc" id="ff-years"></div>
|
||||
<div class="ge-calc-result">
|
||||
<div>Bodengesundheit nach 3 Jahren:</div>
|
||||
<strong class="ge-calc-total" id="ff-total">60 %</strong>
|
||||
<div class="ge-calc-bar"><div class="ge-calc-fill" id="ff-bar" style="width:60%"></div></div>
|
||||
<div class="ge-calc-scale"><span>ausgelaugt</span><span></span><span>gesund</span></div>
|
||||
<div class="ge-calc-label" id="ff-verdict"></div>
|
||||
</div>
|
||||
<p class="ge-note">Erbsen und Klee sind Hülsenfrüchte: Sie sammeln mit Knöllchenbakterien Stickstoff aus der Luft und düngen so den Boden. Deshalb wechseln Bäuerinnen und Bauern die Feldfrüchte planmäßig ab (Fruchtfolge). Quelle: AGES / Landwirtschaftskammer.</p>
|
||||
</div>
|
||||
`;
|
||||
const box = root.querySelector('#ff-years');
|
||||
const totalOut = root.querySelector('#ff-total');
|
||||
const barFill = root.querySelector('#ff-bar');
|
||||
const verdict = root.querySelector('#ff-verdict');
|
||||
if (!box) return;
|
||||
|
||||
for (let y = 0; y < 3; y++) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'ge-calc-row';
|
||||
row.innerHTML = `<div class="ge-calc-q">Jahr ${y + 1}</div><div class="ge-calc-opts" data-year="${y}"></div>`;
|
||||
const opts = row.querySelector('.ge-calc-opts');
|
||||
plants.forEach(p => {
|
||||
const b = document.createElement('button');
|
||||
b.className = 'ge-btn';
|
||||
b.textContent = p.label;
|
||||
b.dataset.key = p.key;
|
||||
b.addEventListener('click', () => {
|
||||
opts.querySelectorAll('.ge-btn').forEach(x => x.classList.remove('ge-btn-primary'));
|
||||
b.classList.add('ge-btn-primary');
|
||||
years[y] = p.key;
|
||||
update();
|
||||
});
|
||||
opts.appendChild(b);
|
||||
});
|
||||
box.appendChild(row);
|
||||
}
|
||||
|
||||
function update() {
|
||||
let health = 60; // Startwert
|
||||
let mono = 0;
|
||||
for (let y = 0; y < 3; y++) {
|
||||
const key = years[y];
|
||||
if (!key) continue;
|
||||
const p = plants.find(x => x.key === key);
|
||||
health += p.effect;
|
||||
// Monokultur-Strafe: gleiche Pflanze wie im Vorjahr
|
||||
if (y > 0 && years[y - 1] === key) { health -= 10; mono++; }
|
||||
}
|
||||
health = Math.max(0, Math.min(100, health));
|
||||
totalOut.textContent = Math.round(health) + ' %';
|
||||
barFill.style.width = health + '%';
|
||||
|
||||
let txt, col;
|
||||
if (health >= 75) { txt = '🟢 Toll — der Boden ist gesund und fruchtbar.'; col = '#5a8a5e'; }
|
||||
else if (health >= 50) { txt = '🟡 Ok — mit mehr Wechsel geht es besser.'; col = '#4a7c8a'; }
|
||||
else if (health >= 30) { txt = '🟠 Der Boden wird müde.'; col = '#e8833a'; }
|
||||
else { txt = '🔴 Ausgelaugt — der Boden braucht eine Pause.'; col = '#c85c4a'; }
|
||||
if (mono >= 2) txt = '🔴 Monokultur! Immer dasselbe laugt den Boden aus.';
|
||||
barFill.style.background = col;
|
||||
verdict.textContent = txt;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
// Registry — Keys = key_slug aus DB.
|
||||
window.GlossarExperiments = Object.assign(window.GlossarExperiments || {}, {
|
||||
'strommix': strommix,
|
||||
'eisschild': eisschild,
|
||||
'klimabilanz': klimabilanz,
|
||||
'megawattstunde': megawattstunde,
|
||||
'niederschlag': niederschlag,
|
||||
'fruchtfolge': fruchtfolge
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,610 @@
|
||||
/**
|
||||
* Glossar-Experimente „neu2" — Optik/Schatten, Verkehr, EU-Binnenmarkt,
|
||||
* Tourismus und Grundwasser.
|
||||
*
|
||||
* Registrierung wie in glossar-experiments.js:
|
||||
* window.GlossarExperiments[key_slug] = function (root) { … }
|
||||
* Der Renderer (glossar.php) übergibt einen leeren Container; das Widget baut
|
||||
* seinen DOM selbst und bindet seine Events. Styles: nur .ge-* aus glossar.php.
|
||||
*
|
||||
* Richtlinien:
|
||||
* - Keine externen Libraries, Vanilla JS + SVG.
|
||||
* - Mobile-/Tap-freundlich (große Ziele, Pointer-Events, keine Hover-Pflicht).
|
||||
* - Zahlen mit kurzer Quelle in der .ge-note.
|
||||
* - Animations-Loops stoppen selbst, sobald der Container aus dem DOM fällt
|
||||
* (root.isConnected), damit kein Loop nach Modal-Schließen weiterläuft.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const NS = 'http://www.w3.org/2000/svg';
|
||||
const svgEl = (tag, attrs, parent) => {
|
||||
const el = document.createElementNS(NS, tag);
|
||||
if (attrs) for (const k in attrs) el.setAttribute(k, attrs[k]);
|
||||
if (parent) parent.appendChild(el);
|
||||
return el;
|
||||
};
|
||||
const eur = (n) => n.toLocaleString('de-AT') + ' €';
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
|
||||
// Pointer-Drag auf einem SVG-Handle. onMove bekommt SVG-Koordinaten (x,y).
|
||||
function makeDraggable(svg, handle, onMove) {
|
||||
const pt = svg.createSVGPoint();
|
||||
function toSvg(evt) {
|
||||
pt.x = evt.clientX; pt.y = evt.clientY;
|
||||
const ctm = svg.getScreenCTM();
|
||||
if (!ctm) return null;
|
||||
return pt.matrixTransform(ctm.inverse());
|
||||
}
|
||||
let active = false;
|
||||
handle.style.cursor = 'grab';
|
||||
handle.style.touchAction = 'none';
|
||||
handle.addEventListener('pointerdown', (e) => {
|
||||
active = true;
|
||||
handle.style.cursor = 'grabbing';
|
||||
if (handle.setPointerCapture) { try { handle.setPointerCapture(e.pointerId); } catch (_) {} }
|
||||
e.preventDefault();
|
||||
});
|
||||
handle.addEventListener('pointermove', (e) => {
|
||||
if (!active) return;
|
||||
const p = toSvg(e);
|
||||
if (p) onMove(p.x, p.y);
|
||||
e.preventDefault();
|
||||
});
|
||||
const stop = () => { active = false; handle.style.cursor = 'grab'; };
|
||||
handle.addEventListener('pointerup', stop);
|
||||
handle.addEventListener('pointercancel', stop);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Experiment: Halbschatten — Kernschatten + Halbschatten auf der Wand
|
||||
// =====================================================================
|
||||
function penumbra(root) {
|
||||
root.innerHTML = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🌑 Kernschatten und Halbschatten</h3>
|
||||
<p>Ziehe den <strong>Ball</strong> zwischen Lampe und Wand hin und her. Auf der Wand entsteht in der Mitte ein ganz dunkler <strong>Kernschatten</strong> und ringsum ein weicher, halbdunkler <strong>Halbschatten</strong>.</p>
|
||||
</div>
|
||||
<svg class="ge-track" viewBox="0 0 600 340" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill" data-ref="core" aria-live="polite"></div>
|
||||
<div class="ge-gh-pill" data-ref="soft"></div>
|
||||
</div>
|
||||
<p class="ge-note">Weil die Lampe (wie die Sonne) eine ausgedehnte Lichtquelle ist, entstehen zwei Zonen. Genau so bei Finsternissen: Wer im <strong>Kernschatten</strong> steht, sieht eine <strong>totale</strong> Finsternis, wer im <strong>Halbschatten</strong> steht, nur eine <strong>partielle</strong>. Quelle: NASA Eclipses – umbra & penumbra.</p>
|
||||
</div>
|
||||
`;
|
||||
const svg = root.querySelector('.ge-track');
|
||||
const coreEl = root.querySelector('[data-ref="core"]');
|
||||
const softEl = root.querySelector('[data-ref="soft"]');
|
||||
if (!svg || !coreEl || !softEl) return;
|
||||
|
||||
const Lx = 70, Ly = 170, SH = 26; // Lichtquelle: Segment (Lx, Ly±SH)
|
||||
const Wx = 545, Wy_TOP = 24, Wy_BOT = 316; // Wand
|
||||
const R = 34;
|
||||
let Bx = 300, By = 170; // Ballmittelpunkt
|
||||
|
||||
// Wand
|
||||
svgEl('rect', { x: Wx, y: Wy_TOP, width: 40, height: Wy_BOT - Wy_TOP, fill: '#d7ccbb', stroke: '#b7a98f', 'stroke-width': '1.5', rx: 3 }, svg);
|
||||
// dynamische Schattenbänder auf der Wandvorderseite
|
||||
const softBand = svgEl('rect', { x: Wx - 3, width: 8, fill: '#5b636b', opacity: '0.45' }, svg);
|
||||
const coreBand = svgEl('rect', { x: Wx - 3, width: 8, fill: '#1b2027', opacity: '0.92' }, svg);
|
||||
// Randstrahlen (Halbschatten-Grenzen)
|
||||
const ray1 = svgEl('line', { stroke: '#f4c94e', 'stroke-width': '1.4', opacity: '0.6', 'stroke-dasharray': '4 3' }, svg);
|
||||
const ray2 = svgEl('line', { stroke: '#f4c94e', 'stroke-width': '1.4', opacity: '0.6', 'stroke-dasharray': '4 3' }, svg);
|
||||
|
||||
// Lampe
|
||||
svgEl('rect', { x: Lx - 26, y: Ly - SH - 6, width: 26, height: 2 * SH + 12, fill: '#3a4750', rx: 4 }, svg);
|
||||
svgEl('line', { x1: Lx, y1: Ly - SH, x2: Lx, y2: Ly + SH, stroke: '#f4c94e', 'stroke-width': '6', 'stroke-linecap': 'round' }, svg);
|
||||
svgEl('circle', { cx: Lx - 13, cy: Ly, r: 5, fill: '#f4c94e' }, svg);
|
||||
svgEl('text', { x: Lx - 13, y: Ly + 46, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '700', fill: '#3a4750' }, svg).textContent = 'Lampe';
|
||||
|
||||
// Ball (Drag-Handle)
|
||||
const ball = svgEl('g', {}, svg);
|
||||
const ballCircle = svgEl('circle', { cx: Bx, cy: By, r: R, fill: '#4a7c8a', stroke: '#1f4e5a', 'stroke-width': '2' }, ball);
|
||||
svgEl('text', { x: Bx, y: By - R - 8, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '700', fill: '#1f4e5a', 'data-ref': 'balllabel' }, ball).textContent = 'Ball ziehen ↔';
|
||||
|
||||
const yAtWall = (Px, Py, Qx, Qy) => Py + (Qy - Py) * (Wx - Px) / (Qx - Px);
|
||||
|
||||
function update() {
|
||||
ballCircle.setAttribute('cx', Bx);
|
||||
ballCircle.setAttribute('cy', By);
|
||||
const lbl = ball.querySelector('[data-ref="balllabel"]');
|
||||
if (lbl) { lbl.setAttribute('x', Bx); lbl.setAttribute('y', By - R - 8); }
|
||||
|
||||
const St = Ly - SH, Sb = Ly + SH; // Quelle oben / unten
|
||||
const Bt = By - R, Bb = By + R; // Ball oben / unten
|
||||
// Halbschatten-Ränder (weitester Schatten)
|
||||
let penTop = yAtWall(Lx, Sb, Bx, Bt);
|
||||
let penBot = yAtWall(Lx, St, Bx, Bb);
|
||||
// Kernschatten-Ränder (nur wo ALLE Strahlen blockiert sind)
|
||||
let umbTop = yAtWall(Lx, St, Bx, Bt);
|
||||
let umbBot = yAtWall(Lx, Sb, Bx, Bb);
|
||||
|
||||
penTop = clamp(penTop, Wy_TOP, Wy_BOT);
|
||||
penBot = clamp(penBot, Wy_TOP, Wy_BOT);
|
||||
softBand.setAttribute('y', penTop);
|
||||
softBand.setAttribute('height', Math.max(0, penBot - penTop));
|
||||
|
||||
const hasCore = umbTop < umbBot;
|
||||
if (hasCore) {
|
||||
umbTop = clamp(umbTop, Wy_TOP, Wy_BOT);
|
||||
umbBot = clamp(umbBot, Wy_TOP, Wy_BOT);
|
||||
coreBand.setAttribute('y', umbTop);
|
||||
coreBand.setAttribute('height', Math.max(0, umbBot - umbTop));
|
||||
coreBand.setAttribute('opacity', '0.92');
|
||||
} else {
|
||||
coreBand.setAttribute('height', 0);
|
||||
coreBand.setAttribute('opacity', '0');
|
||||
}
|
||||
|
||||
// Randstrahlen zeichnen
|
||||
ray1.setAttribute('x1', Lx); ray1.setAttribute('y1', Sb);
|
||||
ray1.setAttribute('x2', Wx); ray1.setAttribute('y2', clamp(yAtWall(Lx, Sb, Bx, Bt), Wy_TOP, Wy_BOT));
|
||||
ray2.setAttribute('x1', Lx); ray2.setAttribute('y1', St);
|
||||
ray2.setAttribute('x2', Wx); ray2.setAttribute('y2', clamp(yAtWall(Lx, St, Bx, Bb), Wy_TOP, Wy_BOT));
|
||||
|
||||
const coreH = hasCore ? Math.round(umbBot - umbTop) : 0;
|
||||
const softH = Math.round((penBot - penTop) - coreH);
|
||||
if (hasCore) {
|
||||
coreEl.innerHTML = 'Kernschatten (ganz dunkel): <strong>total verdeckt</strong>';
|
||||
coreEl.dataset.state = 'bad';
|
||||
} else {
|
||||
coreEl.innerHTML = 'Ball zu weit weg – <strong>kein</strong> Kernschatten mehr';
|
||||
coreEl.dataset.state = 'warn';
|
||||
}
|
||||
softEl.innerHTML = 'Halbschatten (halbdunkel): <strong>teilweise verdeckt</strong>';
|
||||
softEl.dataset.state = 'now';
|
||||
}
|
||||
|
||||
makeDraggable(svg, ball, (x, y) => {
|
||||
Bx = clamp(x, Lx + 90, Wx - 70);
|
||||
By = clamp(y, 90, 250);
|
||||
update();
|
||||
});
|
||||
update();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Experiment: Blockabfertigung — Dosierung an der Grenze
|
||||
// =====================================================================
|
||||
function blockabfertigung(root) {
|
||||
root.innerHTML = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🚚 Wie viele LKW pro Stunde?</h3>
|
||||
<p>Stell den Regler ein. Die Grenze schafft nur eine bestimmte Menge. Lässt man <strong>zu viele</strong> auf einmal durch, staut es sich; <strong>dosiert</strong> man, fließt der Verkehr.</p>
|
||||
</div>
|
||||
<svg class="ge-track" viewBox="0 0 600 260" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="ge-bl-range">LKW pro Stunde: <strong id="ge-bl-val">450</strong></label>
|
||||
<input id="ge-bl-range" type="range" min="0" max="600" value="450" step="10">
|
||||
<div class="ge-gh-markers"><span>0</span><span>300<br>Richtwert</span><span>600</span></div>
|
||||
</div>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill" data-ref="flow" aria-live="polite"></div>
|
||||
<div class="ge-gh-pill" data-ref="cap"></div>
|
||||
</div>
|
||||
<p class="ge-note">Bei der „Blockabfertigung" dosiert das Land Tirol den LKW-Verkehr Richtung Brenner auf einen Richtwert von rund 300 LKW pro Stunde, damit die Autobahn nicht überlastet wird. Quelle: Land Tirol – Blockabfertigung.</p>
|
||||
</div>
|
||||
`;
|
||||
const svg = root.querySelector('.ge-track');
|
||||
const rng = root.querySelector('#ge-bl-range');
|
||||
const valEl = root.querySelector('#ge-bl-val');
|
||||
const flowEl = root.querySelector('[data-ref="flow"]');
|
||||
const capEl = root.querySelector('[data-ref="cap"]');
|
||||
if (!svg || !rng || !valEl || !flowEl || !capEl) return;
|
||||
|
||||
const CAP = 300;
|
||||
const GATEX = 430;
|
||||
// Straße + Grenzhäuschen
|
||||
svgEl('rect', { x: 0, y: 150, width: 600, height: 56, fill: '#4a4a4a' }, svg);
|
||||
svgEl('line', { x1: 0, y1: 178, x2: 600, y2: 178, stroke: '#f4c94e', 'stroke-width': '2', 'stroke-dasharray': '18 14' }, svg);
|
||||
svgEl('rect', { x: GATEX, y: 96, width: 10, height: 110, fill: '#c85c4a' }, svg);
|
||||
svgEl('rect', { x: GATEX - 34, y: 74, width: 78, height: 26, fill: '#e8d5b5', stroke: '#1f4e5a', 'stroke-width': '1.5', rx: 3 }, svg);
|
||||
svgEl('text', { x: GATEX + 5, y: 91, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '800', fill: '#1f4e5a' }, svg).textContent = 'Grenze';
|
||||
// Stau-Balken (rot, wächst nach links)
|
||||
const stauBar = svgEl('rect', { x: GATEX, y: 130, height: 14, fill: '#c85c4a', rx: 4 }, svg);
|
||||
const stauTxt = svgEl('text', { x: GATEX - 8, y: 124, 'text-anchor': 'end', 'font-size': '12', 'font-weight': '800', fill: '#c85c4a' }, svg);
|
||||
const trucks = svgEl('g', {}, svg);
|
||||
|
||||
function update() {
|
||||
const rate = +rng.value;
|
||||
valEl.textContent = rate.toLocaleString('de-AT');
|
||||
const over = Math.max(0, rate - CAP);
|
||||
const stauKm = over / 50; // grobe Illustration
|
||||
const barW = clamp((over / (600 - CAP)) * (GATEX - 30), 0, GATEX - 30);
|
||||
stauBar.setAttribute('x', GATEX - barW);
|
||||
stauBar.setAttribute('width', barW);
|
||||
|
||||
// Kolonne aus LKW-Emojis vor der Grenze
|
||||
trucks.innerHTML = '';
|
||||
const nQueue = Math.round(clamp(over / 40, 0, 10));
|
||||
for (let i = 0; i < nQueue; i++) {
|
||||
svgEl('text', { x: GATEX - 40 - i * 34, y: 172, 'font-size': '24', 'text-anchor': 'middle' }, trucks).textContent = '🚚';
|
||||
}
|
||||
// ein LKW, der gerade durchfährt
|
||||
svgEl('text', { x: GATEX + 40, y: 172, 'font-size': '24', 'text-anchor': 'middle' }, trucks).textContent = '🚚';
|
||||
|
||||
if (over <= 0) {
|
||||
stauTxt.textContent = '';
|
||||
flowEl.innerHTML = '🟢 Verkehr <strong>fließt frei</strong>';
|
||||
flowEl.dataset.state = 'good';
|
||||
} else if (stauKm < 2) {
|
||||
stauTxt.textContent = 'Stau: ' + stauKm.toFixed(1).replace('.', ',') + ' km';
|
||||
flowEl.innerHTML = '🟠 <strong>beginnender Stau</strong>';
|
||||
flowEl.dataset.state = 'warn';
|
||||
} else {
|
||||
stauTxt.textContent = 'Stau: ' + stauKm.toFixed(1).replace('.', ',') + ' km';
|
||||
flowEl.innerHTML = '🔴 <strong>langer Stau</strong> – zu viele LKW';
|
||||
flowEl.dataset.state = 'bad';
|
||||
}
|
||||
capEl.innerHTML = 'Grenze schafft rund <strong>' + CAP + ' LKW/h</strong>';
|
||||
capEl.dataset.state = 'now';
|
||||
}
|
||||
rng.addEventListener('input', update);
|
||||
update();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Experiment: Grundfreiheiten — vier Freiheiten über die offene Grenze
|
||||
// =====================================================================
|
||||
function grundfreiheiten(root) {
|
||||
root.innerHTML = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🇪🇺 Die vier Grundfreiheiten</h3>
|
||||
<p>Ziehe jedes Symbol über die <strong>offene Grenze</strong> von Land A nach Land B. Im EU-Binnenmarkt dürfen alle vier frei über Grenzen.</p>
|
||||
</div>
|
||||
<svg class="ge-track" viewBox="0 0 600 300" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill" data-ref="count" aria-live="polite"></div>
|
||||
</div>
|
||||
<div class="ge-gh-pill" data-ref="explain" style="display:block; line-height:1.5;"></div>
|
||||
<p class="ge-note">Der EU-Binnenmarkt garantiert den freien Verkehr von <strong>Waren, Personen, Dienstleistungen und Kapital</strong> – die „vier Grundfreiheiten". Quelle: Vertrag über die Arbeitsweise der EU (AEUV), Art. 26 · Europäische Kommission.</p>
|
||||
</div>
|
||||
`;
|
||||
const svg = root.querySelector('.ge-track');
|
||||
const countEl = root.querySelector('[data-ref="count"]');
|
||||
const explainEl = root.querySelector('[data-ref="explain"]');
|
||||
if (!svg || !countEl || !explainEl) return;
|
||||
|
||||
const BORDER = 300;
|
||||
// Länder
|
||||
svgEl('rect', { x: 0, y: 0, width: BORDER, height: 300, fill: '#dbe8dd' }, svg);
|
||||
svgEl('rect', { x: BORDER, y: 0, width: 600 - BORDER, height: 300, fill: '#dae4ec' }, svg);
|
||||
svgEl('line', { x1: BORDER, y1: 0, x2: BORDER, y2: 300, stroke: '#5a8a5e', 'stroke-width': '3', 'stroke-dasharray': '10 8' }, svg);
|
||||
svgEl('text', { x: 20, y: 28, 'font-size': '13', 'font-weight': '800', fill: '#3a6b3e' }, svg).textContent = 'Land A';
|
||||
svgEl('text', { x: 580, y: 28, 'font-size': '13', 'font-weight': '800', fill: '#1f4e5a', 'text-anchor': 'end' }, svg).textContent = 'Land B';
|
||||
svgEl('text', { x: BORDER, y: 292, 'font-size': '10', 'font-weight': '700', fill: '#5a8a5e', 'text-anchor': 'middle' }, svg).textContent = 'offene Grenze';
|
||||
|
||||
const items = [
|
||||
{ emoji: '📦', name: 'Warenverkehr', txt: 'Waren dürfen ohne Zölle und ohne Grenzkontrollen gehandelt werden.', y: 80 },
|
||||
{ emoji: '🧑', name: 'Personenfreizügigkeit', txt: 'Menschen dürfen in jedem EU-Land wohnen und arbeiten.', y: 140 },
|
||||
{ emoji: '🛠️', name: 'Dienstleistungsfreiheit', txt: 'Betriebe dürfen ihre Dienste in jedem EU-Land anbieten.', y: 200 },
|
||||
{ emoji: '🪙', name: 'Kapitalverkehr', txt: 'Geld und Kapital dürfen frei zwischen den Ländern fließen.', y: 260 }
|
||||
];
|
||||
let crossed = 0;
|
||||
|
||||
items.forEach((it) => {
|
||||
it.done = false;
|
||||
it.x = 60;
|
||||
const g = svgEl('g', {}, svg);
|
||||
const bg = svgEl('circle', { cx: it.x, cy: it.y, r: 26, fill: '#fff', stroke: '#1f4e5a', 'stroke-width': '2' }, g);
|
||||
const em = svgEl('text', { x: it.x, y: it.y + 9, 'text-anchor': 'middle', 'font-size': '26' }, g);
|
||||
em.textContent = it.emoji;
|
||||
const lab = svgEl('text', { x: it.x, y: it.y + 44, 'text-anchor': 'middle', 'font-size': '10', 'font-weight': '700', fill: '#1f4e5a' }, g);
|
||||
lab.textContent = it.name;
|
||||
|
||||
function place(px) {
|
||||
it.x = clamp(px, 40, 560);
|
||||
bg.setAttribute('cx', it.x); em.setAttribute('x', it.x); lab.setAttribute('x', it.x);
|
||||
const nowDone = it.x > BORDER + 6;
|
||||
if (nowDone && !it.done) {
|
||||
it.done = true; crossed++;
|
||||
bg.setAttribute('stroke', '#5a8a5e');
|
||||
bg.setAttribute('fill', '#eaf5ec');
|
||||
} else if (!nowDone && it.done) {
|
||||
it.done = false; crossed--;
|
||||
bg.setAttribute('stroke', '#1f4e5a');
|
||||
bg.setAttribute('fill', '#fff');
|
||||
}
|
||||
explainEl.innerHTML = '<strong>' + it.emoji + ' ' + it.name + ':</strong> ' + it.txt;
|
||||
explainEl.dataset.state = it.done ? 'good' : 'now';
|
||||
countEl.innerHTML = crossed >= 4
|
||||
? '🎉 <strong>Alle vier Grundfreiheiten</strong> dürfen frei über die Grenze!'
|
||||
: '<strong>' + crossed + ' von 4</strong> Freiheiten über der Grenze';
|
||||
countEl.dataset.state = crossed >= 4 ? 'good' : 'now';
|
||||
}
|
||||
makeDraggable(svg, g, (x) => place(x));
|
||||
// Init-Label ohne Zustand
|
||||
});
|
||||
countEl.innerHTML = '<strong>0 von 4</strong> Freiheiten über der Grenze';
|
||||
countEl.dataset.state = 'now';
|
||||
explainEl.innerHTML = 'Ziehe ein Symbol nach Land B, um seine Grundfreiheit zu lesen.';
|
||||
explainEl.dataset.state = 'now';
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Experiment: Synergie — zwei Betriebe zusammen bringen mehr Gäste
|
||||
// =====================================================================
|
||||
function synergie(root) {
|
||||
root.innerHTML = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🤝 Zwei Betriebe – zusammen stärker</h3>
|
||||
<p>Der <strong>Lift</strong> steht fest. Zieh das <strong>Gasthaus</strong> daneben. Getrennt bringt jeder nur wenige Gäste – nebeneinander steigt der gemeinsame Gäste-Balken über die Summe.</p>
|
||||
</div>
|
||||
<svg class="ge-track" viewBox="0 0 600 240" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill" data-ref="sum" aria-live="polite"></div>
|
||||
<div class="ge-gh-pill" data-ref="verdict"></div>
|
||||
</div>
|
||||
<p class="ge-note">Vereinfachtes Beispiel: Lift allein ~20, Gasthaus allein ~15 Gäste (Summe 35). Nebeneinander besuchen viele beide – so entstehen rund 52 Gäste. Fachbegriff: Synergie- bzw. Agglomerationseffekt (zusammen mehr als die Summe der Einzelteile).</p>
|
||||
</div>
|
||||
`;
|
||||
const svg = root.querySelector('.ge-track');
|
||||
const sumEl = root.querySelector('[data-ref="sum"]');
|
||||
const verdictEl = root.querySelector('[data-ref="verdict"]');
|
||||
if (!svg || !sumEl || !verdictEl) return;
|
||||
|
||||
const LIFT_X = 150, LIFT_Y = 90;
|
||||
const A_ALONE = 20, B_ALONE = 15, TOGETHER = 52;
|
||||
|
||||
// Gäste-Balken
|
||||
svgEl('rect', { x: 40, y: 205, width: 520, height: 20, fill: '#eef2f0', rx: 6 }, svg);
|
||||
const barFill = svgEl('rect', { x: 40, y: 205, width: 0, height: 20, fill: '#5a8a5e', rx: 6 }, svg);
|
||||
const barTxt = svgEl('text', { x: 300, y: 220, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '800', fill: '#1f4e5a' }, svg);
|
||||
const sumMark = svgEl('line', { y1: 200, y2: 230, stroke: '#c85c4a', 'stroke-width': '2', 'stroke-dasharray': '3 3' }, svg);
|
||||
svgEl('text', { x: 40 + (A_ALONE + B_ALONE) / 60 * 520, y: 198, 'text-anchor': 'middle', 'font-size': '9', 'font-weight': '700', fill: '#c85c4a' }, svg).textContent = 'Summe 35';
|
||||
|
||||
// Lift (fix)
|
||||
const liftG = svgEl('g', {}, svg);
|
||||
svgEl('circle', { cx: LIFT_X, cy: LIFT_Y, r: 34, fill: '#dae4ec', stroke: '#1f4e5a', 'stroke-width': '2' }, liftG);
|
||||
svgEl('text', { x: LIFT_X, y: LIFT_Y + 11, 'text-anchor': 'middle', 'font-size': '30' }, liftG).textContent = '🚡';
|
||||
svgEl('text', { x: LIFT_X, y: LIFT_Y + 54, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '800', fill: '#1f4e5a' }, liftG).textContent = 'Lift';
|
||||
|
||||
// Gasthaus (drag)
|
||||
let gx = 470, gy = 90;
|
||||
const gastG = svgEl('g', {}, svg);
|
||||
const gastBg = svgEl('circle', { cx: gx, cy: gy, r: 34, fill: '#f3e8d8', stroke: '#e08a3a', 'stroke-width': '2' }, gastG);
|
||||
svgEl('text', { x: 0, y: 0, 'text-anchor': 'middle', 'font-size': '30', 'data-ref': 'gem' }, gastG).textContent = '🍽️';
|
||||
svgEl('text', { x: 0, y: 0, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '800', fill: '#c86a1a', 'data-ref': 'glab' }, gastG).textContent = 'Gasthaus ziehen ↔';
|
||||
|
||||
function update() {
|
||||
gastBg.setAttribute('cx', gx); gastBg.setAttribute('cy', gy);
|
||||
const gem = gastG.querySelector('[data-ref="gem"]');
|
||||
const glab = gastG.querySelector('[data-ref="glab"]');
|
||||
if (gem) { gem.setAttribute('x', gx); gem.setAttribute('y', gy + 11); }
|
||||
if (glab) { glab.setAttribute('x', gx); glab.setAttribute('y', gy + 54); }
|
||||
|
||||
const dist = Math.hypot(gx - LIFT_X, gy - LIFT_Y);
|
||||
const together = dist < 110;
|
||||
const guests = together ? TOGETHER : (A_ALONE + B_ALONE);
|
||||
const w = (guests / 60) * 520;
|
||||
barFill.setAttribute('width', clamp(w, 0, 520));
|
||||
barFill.setAttribute('fill', together ? '#5a8a5e' : '#9bab74');
|
||||
barTxt.textContent = guests + ' Gäste';
|
||||
sumMark.setAttribute('x1', 40 + (35 / 60) * 520);
|
||||
sumMark.setAttribute('x2', 40 + (35 / 60) * 520);
|
||||
|
||||
if (together) {
|
||||
sumEl.innerHTML = 'Zusammen: <strong>' + TOGETHER + ' Gäste</strong>';
|
||||
sumEl.dataset.state = 'good';
|
||||
verdictEl.innerHTML = '🎉 mehr als die Summe (35) – <strong>Synergie!</strong>';
|
||||
verdictEl.dataset.state = 'good';
|
||||
} else {
|
||||
sumEl.innerHTML = 'Getrennt: <strong>20 + 15 = 35 Gäste</strong>';
|
||||
sumEl.dataset.state = 'now';
|
||||
verdictEl.innerHTML = 'Zieh das Gasthaus näher an den Lift …';
|
||||
verdictEl.dataset.state = 'warn';
|
||||
}
|
||||
}
|
||||
makeDraggable(svg, gastG, (x, y) => {
|
||||
gx = clamp(x, 60, 560); gy = clamp(y, 60, 130);
|
||||
update();
|
||||
});
|
||||
update();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Experiment: Aufenthaltsdauer — Ausgaben wachsen mit jeder Nacht
|
||||
// =====================================================================
|
||||
function aufenthaltsdauer(root) {
|
||||
root.innerHTML = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>🛏️ Je länger, desto mehr Ausgaben</h3>
|
||||
<p>Schieb den Regler auf die Zahl der <strong>Nächte</strong>. Jede Nacht kostet Übernachtung, Essen und Tickets – die Ausgaben eines Gastes wachsen mit jeder Nacht.</p>
|
||||
</div>
|
||||
<svg class="ge-track" viewBox="0 0 600 240" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
<div class="ge-gh-slider">
|
||||
<label for="ge-ad-range">Nächte: <strong id="ge-ad-val">3</strong></label>
|
||||
<input id="ge-ad-range" type="range" min="1" max="7" value="3" step="1">
|
||||
<div class="ge-gh-markers"><span>1</span><span>3,3<br>Ø</span><span>7</span></div>
|
||||
</div>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill" data-ref="calc" aria-live="polite"></div>
|
||||
<div class="ge-gh-pill" data-ref="total"></div>
|
||||
</div>
|
||||
<p class="ge-note">Beispielwerte pro Nacht: Übernachtung 85 €, Essen 40 €, Tickets/Aktivitäten 25 € = 150 € · Nacht. Ein Gast bleibt in Österreich im Schnitt rund <strong>3,3 Nächte</strong>. Quelle: Statistik Austria – Tourismus, Ø Aufenthaltsdauer.</p>
|
||||
</div>
|
||||
`;
|
||||
const svg = root.querySelector('.ge-track');
|
||||
const rng = root.querySelector('#ge-ad-range');
|
||||
const valEl = root.querySelector('#ge-ad-val');
|
||||
const calcEl = root.querySelector('[data-ref="calc"]');
|
||||
const totalEl = root.querySelector('[data-ref="total"]');
|
||||
if (!svg || !rng || !valEl || !calcEl || !totalEl) return;
|
||||
|
||||
const PER_NIGHT = 150; // 85 + 40 + 25
|
||||
const parts = [
|
||||
{ key: 'Übernachtung', v: 85, color: '#4a7c8a' },
|
||||
{ key: 'Essen', v: 40, color: '#5a8a5e' },
|
||||
{ key: 'Tickets', v: 25, color: '#e08a3a' }
|
||||
];
|
||||
const baseY = 200, colW = 56, gap = 14, x0 = 60;
|
||||
const bars = svgEl('g', {}, svg);
|
||||
// Legende
|
||||
const leg = svgEl('g', {}, svg);
|
||||
parts.forEach((p, i) => {
|
||||
svgEl('rect', { x: 380, y: 20 + i * 22, width: 14, height: 14, fill: p.color, rx: 3 }, leg);
|
||||
svgEl('text', { x: 400, y: 32 + i * 22, 'font-size': '11', 'font-weight': '600', fill: '#1f4e5a' }, leg).textContent = p.key + ' ' + eur(p.v);
|
||||
});
|
||||
|
||||
function update() {
|
||||
const nights = +rng.value;
|
||||
valEl.textContent = nights.toString().replace('.', ',');
|
||||
bars.innerHTML = '';
|
||||
const total = nights * PER_NIGHT;
|
||||
const maxH = 150; // Höhe einer vollen Nacht-Säule (150 €)
|
||||
for (let n = 0; n < nights; n++) {
|
||||
const x = x0 + n * (colW + gap);
|
||||
let yTop = baseY;
|
||||
parts.forEach((p) => {
|
||||
const segH = (p.v / PER_NIGHT) * maxH;
|
||||
yTop -= segH;
|
||||
svgEl('rect', { x, y: yTop, width: colW, height: segH, fill: p.color }, bars);
|
||||
});
|
||||
svgEl('text', { x: x + colW / 2, y: baseY + 16, 'text-anchor': 'middle', 'font-size': '10', 'font-weight': '700', fill: '#1f4e5a' }, bars).textContent = (n + 1) + '.';
|
||||
}
|
||||
svgEl('line', { x1: 40, y1: baseY, x2: 560, y2: baseY, stroke: '#b7a98f', 'stroke-width': '1.5' }, bars);
|
||||
|
||||
calcEl.innerHTML = nights + ' Nächte · 150 € = <strong>' + eur(total) + '</strong>';
|
||||
calcEl.dataset.state = 'now';
|
||||
totalEl.innerHTML = 'Ausgaben gesamt: <strong>' + eur(total) + '</strong>';
|
||||
totalEl.dataset.state = nights >= 4 ? 'good' : 'now';
|
||||
}
|
||||
rng.addEventListener('input', update);
|
||||
update();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Experiment: Grundwasser — Regen sickert, wird gefiltert, sammelt sich
|
||||
// =====================================================================
|
||||
function grundwasser(root) {
|
||||
root.innerHTML = `
|
||||
<div class="ge-wrap">
|
||||
<div class="ge-head">
|
||||
<h3>💧 So entsteht sauberes Grundwasser</h3>
|
||||
<p>Regentropfen sickern durch Erde und Kies nach unten. Schmutz bleibt in den Schichten hängen – <strong>gefiltert</strong>. Ganz unten sammelt sich sauberes Grundwasser.</p>
|
||||
</div>
|
||||
<svg class="ge-track" viewBox="0 0 600 300" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
<div class="ge-controls">
|
||||
<button class="ge-btn ge-btn-primary" data-act="toggle">⏸ Pause</button>
|
||||
<button class="ge-btn" data-act="reset">↺ Zurück</button>
|
||||
</div>
|
||||
<div class="ge-gh-result">
|
||||
<div class="ge-gh-pill" data-ref="level" aria-live="polite"></div>
|
||||
</div>
|
||||
<p class="ge-note">Beim Durchsickern durch Boden- und Kiesschichten wird das Wasser gereinigt. In Österreich stammt das Trinkwasser praktisch vollständig aus Grund- und Quellwasser. Quelle: Umweltbundesamt / BML – Wasser.</p>
|
||||
</div>
|
||||
`;
|
||||
const svg = root.querySelector('.ge-track');
|
||||
const toggleBtn = root.querySelector('[data-act="toggle"]');
|
||||
const resetBtn = root.querySelector('[data-act="reset"]');
|
||||
const levelEl = root.querySelector('[data-ref="level"]');
|
||||
if (!svg || !toggleBtn || !resetBtn || !levelEl) return;
|
||||
|
||||
const SKY = 70, SOIL = 150, GRAVEL = 220, BOTTOM = 300;
|
||||
// Schichten
|
||||
svgEl('rect', { x: 0, y: 0, width: 600, height: SKY, fill: '#dbeef5' }, svg);
|
||||
svgEl('rect', { x: 0, y: SKY, width: 600, height: SOIL - SKY, fill: '#8a6a3e' }, svg);
|
||||
svgEl('rect', { x: 0, y: SOIL, width: 600, height: GRAVEL - SOIL, fill: '#b8a074' }, svg);
|
||||
svgEl('rect', { x: 0, y: GRAVEL, width: 600, height: BOTTOM - GRAVEL, fill: '#c9b98f' }, svg);
|
||||
// Kies-Punkte in der Filterschicht
|
||||
for (let i = 0; i < 40; i++) {
|
||||
svgEl('circle', { cx: Math.random() * 600, cy: SOIL + 6 + Math.random() * (GRAVEL - SOIL - 12), r: 3 + Math.random() * 3, fill: '#9c8557', opacity: '0.7' }, svg);
|
||||
}
|
||||
svgEl('text', { x: 8, y: SKY + 22, 'font-size': '11', 'font-weight': '700', fill: '#fff' }, svg).textContent = 'Erde';
|
||||
svgEl('text', { x: 8, y: SOIL + 20, 'font-size': '11', 'font-weight': '700', fill: '#5a4a2a' }, svg).textContent = 'Kies (Filter)';
|
||||
// Grundwasser-Füllung (steigt)
|
||||
const water = svgEl('rect', { x: 0, y: BOTTOM, width: 600, height: 0, fill: '#3a86b5', opacity: '0.75' }, svg);
|
||||
svgEl('text', { x: 592, y: BOTTOM - 8, 'font-size': '11', 'font-weight': '800', fill: '#0f3a52', 'text-anchor': 'end' }, svg).textContent = 'Grundwasser';
|
||||
|
||||
const dropsG = svgEl('g', {}, svg);
|
||||
let drops = [];
|
||||
let level = 0; // 0..1 Füllstand
|
||||
let collected = 0;
|
||||
let running = true;
|
||||
|
||||
function spawn() {
|
||||
const dirty = Math.random() < 0.5;
|
||||
drops.push({
|
||||
el: null, x: 30 + Math.random() * 540, y: -6,
|
||||
vy: 0.9 + Math.random() * 0.8, dirty, filtered: false
|
||||
});
|
||||
}
|
||||
// Startvorrat
|
||||
for (let i = 0; i < 8; i++) spawn();
|
||||
|
||||
function draw() {
|
||||
dropsG.innerHTML = '';
|
||||
drops.forEach((d) => {
|
||||
d.el = svgEl('circle', { cx: d.x, cy: d.y, r: 4, fill: '#3a86b5' }, dropsG);
|
||||
if (d.dirty && !d.filtered) {
|
||||
svgEl('circle', { cx: d.x, cy: d.y, r: 2, fill: '#5a3a1a' }, dropsG);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function tick() {
|
||||
if (!root.isConnected) return; // Modal geschlossen → Loop stoppen
|
||||
if (running) {
|
||||
const waterTopY = BOTTOM - level * (BOTTOM - GRAVEL - 4);
|
||||
drops.forEach((d) => {
|
||||
d.y += d.vy;
|
||||
// Schmutz wird in der Kiesschicht gefiltert (bleibt hängen)
|
||||
if (d.dirty && !d.filtered && d.y >= SOIL + 20) {
|
||||
d.filtered = true; // Schmutz raus, Wasser sickert sauber weiter
|
||||
}
|
||||
// langsamer im Kies
|
||||
if (d.y > SOIL && d.y < GRAVEL) d.y += -d.vy * 0.5;
|
||||
});
|
||||
// ankommende Tropfen sammeln
|
||||
drops = drops.filter((d) => {
|
||||
if (d.y >= waterTopY - 2) {
|
||||
collected++;
|
||||
level = clamp(level + 0.006, 0, 1);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// Nachschub
|
||||
if (drops.length < 12 && Math.random() < 0.25) spawn();
|
||||
water.setAttribute('y', waterTopY);
|
||||
water.setAttribute('height', BOTTOM - waterTopY);
|
||||
draw();
|
||||
levelEl.innerHTML = 'Sauberes Grundwasser: <strong>' + Math.round(level * 100) + ' %</strong> gefüllt';
|
||||
levelEl.dataset.state = level > 0.6 ? 'good' : 'now';
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
running = !running;
|
||||
toggleBtn.textContent = running ? '⏸ Pause' : '▶ Weiter';
|
||||
});
|
||||
resetBtn.addEventListener('click', () => {
|
||||
drops = []; level = 0; collected = 0;
|
||||
for (let i = 0; i < 8; i++) spawn();
|
||||
water.setAttribute('height', 0);
|
||||
levelEl.innerHTML = 'Sauberes Grundwasser: <strong>0 %</strong> gefüllt';
|
||||
levelEl.dataset.state = 'now';
|
||||
});
|
||||
levelEl.innerHTML = 'Sauberes Grundwasser: <strong>0 %</strong> gefüllt';
|
||||
levelEl.dataset.state = 'now';
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
// Registry — Key entspricht key_slug aus DB.
|
||||
window.GlossarExperiments = Object.assign(window.GlossarExperiments || {}, {
|
||||
'halbschatten': penumbra,
|
||||
'blockabfertigung': blockabfertigung,
|
||||
'grundfreiheiten': grundfreiheiten,
|
||||
'synergie': synergie,
|
||||
'aufenthaltsdauer': aufenthaltsdauer,
|
||||
'grundwasser': grundwasser
|
||||
});
|
||||
})();
|
||||