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>
This commit is contained in:
2026-07-16 23:17:59 +02:00
parent f9a5c84ab9
commit f8e77910ea
56 changed files with 1323 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

+557
View File
@@ -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
});
})();
+610
View File
@@ -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 &amp; 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
});
})();