f8e77910ea
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>
611 lines
31 KiB
JavaScript
611 lines
31 KiB
JavaScript
/**
|
||
* 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
|
||
});
|
||
})();
|