Files
geograsim/App/assets/js/glossar-experiments-neu2.js
Adminator f8e77910ea 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>
2026-07-16 23:17:59 +02:00

611 lines
31 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Glossar-Experimente „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
});
})();