Glossar: 12 neue interaktive Widgets (Tourismus + Geo/Energie/Reise)

tourismus.js: beschneiung (Temp→Schnee), diversifizierung, kapazitaet, saisonalitaet,
uebertourismus, bevoelkerungsdichte
geowelt.js: gletscher, lawinenwarnstufe, lichtgeschwindigkeit, gigawattstunde,
netzspannung, kartenmaßstab
Einbindung in glossar.php (Registry via Object.assign).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 02:36:55 +02:00
parent aae0c82c64
commit 2e50aaf49f
3 changed files with 1132 additions and 0 deletions
@@ -0,0 +1,576 @@
/**
* Glossar-Experimente — Gruppe „geowelt".
*
* Kleine interaktive SVG-Widgets für das Detail-Modal eines Glossar-Eintrags
* (Abschnitt „Selbst ausprobieren"). Registrierung am Ende der Datei via
* window.GlossarExperiments[key_slug] = fn.
*
* Richtlinien: Vanilla JS, keine Libraries, nur bestehende .ge-*-CSS-Klassen.
* Grosse Tap-Ziele, keine Hover-Abhängigkeit. rAF-Schleifen stoppen, sobald das
* Modal geschlossen ist (root.isConnected-Guard). Zahlen quellenbar (ge-note).
*/
(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 fmtInt = n => Math.round(n).toLocaleString('de-AT');
const fmtDec = (n, d) => n.toLocaleString('de-AT', { minimumFractionDigits: d, maximumFractionDigits: d });
// =====================================================================
// gletscher — Zeit-Schieberegler 1900 → heute, Gletscher zieht sich zurück
// =====================================================================
function gletscher(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🏔 Wie ein Alpengletscher schrumpft</h3>
<p>Schiebe durch die Jahre. Beobachte, wie sich die Gletscherzunge das Tal hinaufzieht und der Eis-Vorrat sinkt.</p>
</div>
<svg class="ge-gh-svg" viewBox="0 0 600 300" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-slider">
<label for="ge-gl-range">Jahr: <strong id="ge-gl-year">1900</strong></label>
<input id="ge-gl-range" type="range" min="1900" max="2023" value="1900" step="1">
<div class="ge-gh-markers"><span>1900</span><span>1950</span><span>2000</span><span>heute</span></div>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill">Eis-Vorrat (1900 = 100 %): <strong data-ref="vol">100 %</strong></div>
<div class="ge-gh-pill" data-ref="verdict" data-state="good" aria-live="polite">noch fast unberührt</div>
</div>
<p class="ge-note">Werte veranschaulichen den Volumen-Rückgang der Alpengletscher seit 1900. Allein zwischen 2022 und 2023 verloren die Schweizer Gletscher rund 10 % ihres Volumens. Quelle: GLAMOS (Gletschermessnetz Schweiz).</p>
</div>
`;
const svg = root.querySelector('.ge-gh-svg');
const rng = root.querySelector('#ge-gl-range');
const yearOut = root.querySelector('#ge-gl-year');
const volOut = root.querySelector('[data-ref="vol"]');
const verdict = root.querySelector('[data-ref="verdict"]');
if (!svg || !rng) return;
// Statische Szene: Himmel, Berge, Tal
svgEl('rect', { x: 0, y: 0, width: 600, height: 300, fill: '#eaf2f6' }, svg);
svgEl('polygon', { points: '0,300 130,70 260,300', fill: '#b9c3c9' }, svg);
svgEl('polygon', { points: '180,300 320,50 470,300', fill: '#a7b3ba' }, svg);
svgEl('polygon', { points: '380,300 520,90 600,300', fill: '#b9c3c9' }, svg);
// Schneekappen
svgEl('polygon', { points: '130,70 108,105 152,105', fill: '#ffffff' }, svg);
svgEl('polygon', { points: '320,50 292,95 348,95', fill: '#ffffff' }, svg);
// Talboden (grün)
svgEl('rect', { x: 0, y: 250, width: 600, height: 50, fill: '#6f9c5f' }, svg);
// Gletscher als dicke Linie von der Quelle (oben) zur wandernden Zunge (unten)
const SRC = { x: 300, y: 78 }, TIP = { x: 300, y: 258 };
const glacierShade = svgEl('line', { x1: SRC.x, y1: SRC.y, x2: TIP.x, y2: TIP.y, stroke: '#9cc4e4', 'stroke-width': '34', 'stroke-linecap': 'round' }, svg);
const glacier = svgEl('line', { x1: SRC.x, y1: SRC.y, x2: TIP.x, y2: TIP.y, stroke: '#e2eef7', 'stroke-width': '24', 'stroke-linecap': 'round' }, svg);
const tipLabel = svgEl('text', { x: 300, y: 250, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '800', fill: '#1f4e5a' }, svg);
tipLabel.textContent = 'Zunge';
// Volumen-Balken rechts
svgEl('rect', { x: 552, y: 60, width: 26, height: 200, fill: '#ffffff', stroke: '#1f4e5a', 'stroke-width': '1.4', rx: '6' }, svg);
const volBar = svgEl('rect', { x: 555, y: 63, width: 20, height: 194, fill: '#4a9cc4', rx: '5' }, svg);
svgEl('text', { x: 565, y: 52, 'text-anchor': 'middle', 'font-size': '10', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = 'Eis';
const anchors = [[1900, 100], [1950, 88], [1980, 78], [2000, 70], [2010, 60], [2020, 48], [2022, 44], [2023, 40]];
function volAt(y) {
if (y <= anchors[0][0]) return anchors[0][1];
for (let i = 0; i < anchors.length - 1; i++) {
const [y0, v0] = anchors[i], [y1, v1] = anchors[i + 1];
if (y <= y1) { const t = (y - y0) / (y1 - y0); return v0 + (v1 - v0) * t; }
}
return anchors[anchors.length - 1][1];
}
function update() {
const year = +rng.value;
const vol = volAt(year);
yearOut.textContent = year;
volOut.textContent = Math.round(vol) + ' %';
// Zunge wandert nach oben, je weniger Eis
const frac = vol / 100;
const tipY = SRC.y + (TIP.y - SRC.y) * frac;
glacier.setAttribute('y2', tipY);
glacierShade.setAttribute('y2', tipY);
tipLabel.setAttribute('y', Math.min(255, tipY + 22));
// Balken
const full = 194;
const h = full * frac;
volBar.setAttribute('y', 63 + (full - h));
volBar.setAttribute('height', h);
let txt, state;
if (vol >= 85) { txt = 'noch fast unberührt'; state = 'good'; }
else if (vol >= 65) { txt = 'Eis geht spürbar zurück'; state = 'now'; }
else if (vol >= 45) { txt = 'Zunge zieht sich stark zurück'; state = 'warn'; }
else { txt = 'rasanter Rückgang — allein 2022 2023 rund 10 %'; state = 'bad'; }
verdict.textContent = txt;
verdict.dataset.state = state;
}
rng.addEventListener('input', update);
update();
}
// =====================================================================
// lawinenwarnstufe — Regler Stufe 1 5, Gefahr steigt sprunghaft
// =====================================================================
function lawinenwarnstufe(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🏔 Die Lawinen-Warnstufen</h3>
<p>Stelle die Warnstufe ein. Achte darauf: Die Gefahr steigt nicht gleichmäßig — der Sprung von Stufe 3 auf 4 ist besonders groß.</p>
</div>
<svg class="ge-gh-svg" viewBox="0 0 600 300" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-slider">
<label for="ge-lw-range">Warnstufe: <strong id="ge-lw-num">1</strong> — <strong id="ge-lw-name">gering</strong></label>
<input id="ge-lw-range" type="range" min="1" max="5" value="1" step="1">
<div class="ge-gh-markers"><span>1</span><span>2</span><span>3</span><span>4</span><span>5</span></div>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill">Gefahr: <strong data-ref="risk">gering</strong></div>
<div class="ge-gh-pill" data-ref="info" data-state="good" aria-live="polite"></div>
</div>
<p class="ge-note">Fünfstufige Europäische Lawinen-Gefahrenskala (EAWS). Die Skala ist nicht linear: Jede Stufe steht ungefähr für eine Verdopplung der Gefahr, der Schritt von 3 (erheblich) auf 4 (groß) ist besonders deutlich. Quelle: European Avalanche Warning Services.</p>
</div>
`;
const svg = root.querySelector('.ge-gh-svg');
const rng = root.querySelector('#ge-lw-range');
const numOut = root.querySelector('#ge-lw-num');
const nameOut = root.querySelector('#ge-lw-name');
const riskOut = root.querySelector('[data-ref="risk"]');
const info = root.querySelector('[data-ref="info"]');
if (!svg || !rng) return;
const levels = {
1: { name: 'gering', risk: 8, state: 'good', txt: 'Meist sichere Verhältnisse. Auslösen nur bei sehr großer Belastung an wenigen Steilstellen.' },
2: { name: 'mäßig', risk: 20, state: 'now', txt: 'An einzelnen steilen Hängen möglich. Sorgfältige Routenwahl nötig.' },
3: { name: 'erheblich', risk: 45, state: 'warn', txt: 'An vielen Steilhängen möglich — oft reicht schon eine Person. Die meisten Unfälle passieren bei Stufe 3.' },
4: { name: 'groß', risk: 80, state: 'bad', txt: 'An den meisten Steilhängen wahrscheinlich, auch von selbst. Sehr gefährlich — Touren nur mit viel Erfahrung.' },
5: { name: 'sehr groß', risk: 100, state: 'bad', txt: 'Zahlreiche große, auch spontane Lawinen bis ins flache Gelände. Extrem — Touren tabu.' }
};
// Statische Szene: Himmel + Hang
svgEl('rect', { x: 0, y: 0, width: 600, height: 300, fill: '#eaf2f6' }, svg);
// Hang als weißes Schnee-Dreieck
svgEl('polygon', { points: '0,300 0,120 600,260 600,300', fill: '#f4f8fb', stroke: '#c4d4de', 'stroke-width': '1.5' }, svg);
// Fels-Untergrund unten
svgEl('polygon', { points: '0,300 600,300 600,278 0,150', fill: '#8a7d6b', opacity: '0.5' }, svg);
// Bäume am Hangfuß
[ [90, 262], [150, 274] ].forEach(([x, y]) => {
svgEl('polygon', { points: `${x},${y - 26} ${x - 11},${y} ${x + 11},${y}`, fill: '#3a6b3e' }, svg);
});
// Dynamische Gruppen: Risse + abgehendes Schneebrett
const cracksG = svgEl('g', {}, svg);
const slabG = svgEl('g', {}, svg);
// Gefahren-Balken
svgEl('rect', { x: 40, y: 40, width: 520, height: 20, fill: '#ffffff', stroke: '#1f4e5a', 'stroke-width': '1.2', rx: '10' }, svg);
const riskBar = svgEl('rect', { x: 42, y: 42, width: 10, height: 16, fill: '#6f9c5f', rx: '8' }, svg);
svgEl('text', { x: 40, y: 32, 'font-size': '11', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = 'Gefahr';
const stateColor = { good: '#6f9c5f', now: '#4a7c8a', warn: '#e08a2a', bad: '#c85c4a' };
function update() {
const n = +rng.value;
const L = levels[n];
numOut.textContent = n;
nameOut.textContent = L.name;
riskOut.textContent = L.name + ' (Stufe ' + n + ')';
info.textContent = L.txt;
info.dataset.state = L.state;
// Balken (nicht-linear → zeigt den Sprung 3→4)
riskBar.setAttribute('width', Math.max(8, (516 * L.risk) / 100));
riskBar.setAttribute('fill', stateColor[L.state]);
// Risse proportional zur Stufe
cracksG.innerHTML = '';
const nCracks = n - 1;
for (let i = 0; i < nCracks; i++) {
const x = 220 + i * 70 + Math.sin(i * 2) * 20;
const y = 150 + i * 18;
svgEl('path', { d: `M ${x} ${y} q 14 12 8 30 q -6 14 10 26`, stroke: '#7c8a94', 'stroke-width': '2', fill: 'none', 'stroke-linecap': 'round' }, cracksG);
}
// Abgehendes Schneebrett ab Stufe 4
slabG.innerHTML = '';
if (n >= 4) {
svgEl('polygon', { points: '300,175 470,205 460,240 300,215', fill: '#dbe7ef', stroke: '#c85c4a', 'stroke-width': '2.5', 'stroke-dasharray': n >= 5 ? '0' : '6 4' }, slabG);
// Bruchkante
svgEl('line', { x1: 300, y1: 173, x2: 300, y2: 213, stroke: '#c85c4a', 'stroke-width': '3', 'stroke-linecap': 'round' }, slabG);
if (n >= 5) {
svgEl('polygon', { points: '150,225 340,250 330,285 150,260', fill: '#eef4f8', stroke: '#c85c4a', 'stroke-width': '2' }, slabG);
}
}
}
rng.addEventListener('input', update);
update();
}
// =====================================================================
// lichtgeschwindigkeit — Ziel-Auswahl, Lichtlaufzeit-Uhr
// =====================================================================
function lichtgeschwindigkeit(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>💡 Wie lange braucht das Licht?</h3>
<p>Wähle ein Ziel. Ein Lichtblitz startet auf der Erde. Sieh, wie lange das Licht bis dorthin unterwegs ist.</p>
</div>
<div class="ge-albedo-choice" role="group">
<button class="ge-btn ge-btn-primary" data-key="mond">🌙 Mond</button>
<button class="ge-btn" data-key="sonne">☀ Sonne</button>
<button class="ge-btn" data-key="mars">🔴 Mars</button>
<button class="ge-btn" data-key="proxima">⭐ Proxima Centauri</button>
</div>
<svg class="ge-gh-svg" viewBox="0 0 600 200" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-result">
<div class="ge-gh-pill">Entfernung: <strong data-ref="dist">384.400 km</strong></div>
<div class="ge-gh-pill" data-ref="time" data-state="now" aria-live="polite">Lichtlaufzeit: 1,3 Sekunden</div>
</div>
<p class="ge-note">Licht legt rund 299.792 km pro Sekunde zurück — extrem schnell. Und doch braucht es bis zum nächsten Stern über 4 Jahre: Das All ist unvorstellbar groß. Entfernungen gerundet, Quelle: NASA.</p>
</div>
`;
const svg = root.querySelector('.ge-gh-svg');
const distOut = root.querySelector('[data-ref="dist"]');
const timeOut = root.querySelector('[data-ref="time"]');
if (!svg) return;
const targets = {
mond: { name: 'Mond', dist: '384.400 km', time: '1,3 Sekunden', emoji: '🌙' },
sonne: { name: 'Sonne', dist: '149,6 Mio. km', time: '8 Minuten 20 Sekunden', emoji: '☀' },
mars: { name: 'Mars', dist: 'im Schnitt 225 Mio. km', time: 'rund 12,5 Minuten (3 22 min je nach Bahn)', emoji: '🔴' },
proxima: { name: 'Proxima Centauri', dist: 'rund 40 Billionen km', time: 'etwa 4,2 Jahre', emoji: '⭐' }
};
// Statische Szene: Erde links, Ziel rechts, Bahn dazwischen
svgEl('rect', { x: 0, y: 0, width: 600, height: 200, fill: '#10233a' }, svg);
// ein paar Sterne
for (let i = 0; i < 26; i++) {
svgEl('circle', { cx: 20 + Math.random() * 560, cy: 10 + Math.random() * 180, r: Math.random() * 1.3 + 0.4, fill: '#dfe8f2', opacity: (0.4 + Math.random() * 0.5).toFixed(2) }, svg);
}
svgEl('line', { x1: 70, y1: 100, x2: 520, y2: 100, stroke: '#3a5a7a', 'stroke-width': '2', 'stroke-dasharray': '5 6' }, svg);
svgEl('circle', { cx: 60, cy: 100, r: 22, fill: '#3a7cc4' }, svg);
svgEl('circle', { cx: 60, cy: 100, r: 22, fill: 'none', stroke: '#6fae5e', 'stroke-width': '3', 'stroke-dasharray': '10 8' }, svg);
svgEl('text', { x: 60, y: 150, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '700', fill: '#dfe8f2' }, svg).textContent = 'Erde';
const targetIcon = svgEl('text', { x: 530, y: 108, 'text-anchor': 'middle', 'font-size': '34' }, svg);
const targetLabel = svgEl('text', { x: 530, y: 150, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '700', fill: '#dfe8f2' }, svg);
// Lichtblitz (Photon)
const photon = svgEl('circle', { cx: 82, cy: 100, r: 6, fill: '#f4e04a' }, svg);
const photonGlow = svgEl('circle', { cx: 82, cy: 100, r: 12, fill: '#f4e04a', opacity: '0.35' }, svg);
const X0 = 82, X1 = 508;
let animId = null, current = 'mond';
function animate() {
cancelAnimationFrame(animId);
const dur = 2200;
const start = performance.now();
function tick(now) {
if (!root.isConnected) { cancelAnimationFrame(animId); return; }
const raw = (now - start) / dur;
const t = raw % 1;
const x = X0 + (X1 - X0) * t;
photon.setAttribute('cx', x);
photonGlow.setAttribute('cx', x);
const op = t > 0.9 ? (1 - t) * 10 : 1;
photon.setAttribute('opacity', op.toFixed(2));
photonGlow.setAttribute('opacity', (op * 0.35).toFixed(2));
animId = requestAnimationFrame(tick);
}
animId = requestAnimationFrame(tick);
}
function select(key) {
const T = targets[key] || targets.mond;
current = key;
distOut.textContent = T.dist;
timeOut.textContent = 'Lichtlaufzeit: ' + T.time;
timeOut.dataset.state = key === 'proxima' ? 'bad' : (key === 'sonne' || key === 'mars' ? 'warn' : 'now');
targetIcon.textContent = T.emoji;
targetLabel.textContent = T.name;
root.querySelectorAll('.ge-albedo-choice .ge-btn').forEach(b => {
b.classList.toggle('ge-btn-primary', b.dataset.key === key);
});
animate();
}
root.querySelectorAll('.ge-albedo-choice .ge-btn').forEach(b => {
b.addEventListener('click', () => select(b.dataset.key));
});
select('mond');
}
// =====================================================================
// gigawattstunde — Schieberegler GWh, „Strom für X Haushalte einen Tag"
// =====================================================================
function gigawattstunde(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>⚡ Wie viel ist eine Gigawattstunde?</h3>
<p>Schiebe den Regler. Sieh, für wie viele Haushalte diese Strommenge einen ganzen Tag lang reicht.</p>
</div>
<svg class="ge-gh-svg" viewBox="0 0 600 180" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-slider">
<label for="ge-gw-range">Energiemenge: <strong id="ge-gw-val">1</strong> GWh</label>
<input id="ge-gw-range" type="range" min="1" max="100" value="1" step="1">
<div class="ge-gh-markers"><span>1</span><span>25</span><span>50</span><span>75</span><span>100</span></div>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill">= <strong data-ref="kwh">1.000.000 kWh</strong></div>
<div class="ge-gh-pill" data-ref="hh" data-state="now" aria-live="polite"></div>
</div>
<p class="ge-note">Ein durchschnittlicher österreichischer Haushalt verbraucht rund 3.500 kWh Strom pro Jahr, also etwa 9,6 kWh pro Tag. Quelle: E-Control. Schon 1 GWh ist eine riesige Strommenge.</p>
</div>
`;
const svg = root.querySelector('.ge-gh-svg');
const rng = root.querySelector('#ge-gw-range');
const valOut = root.querySelector('#ge-gw-val');
const kwhOut = root.querySelector('[data-ref="kwh"]');
const hhOut = root.querySelector('[data-ref="hh"]');
if (!svg || !rng) return;
const PER_DAY = 3500 / 365; // ≈ 9,589 kWh pro Haushalt und Tag
const housesG = svgEl('g', {}, svg);
// jedes Symbol steht für eine feste Anzahl Haushalte
const HOUSES_PER_ICON = 5000;
const COLS = 20;
function drawHouse(x, y, s) {
const g = svgEl('g', { transform: `translate(${x},${y})` }, housesG);
svgEl('rect', { x: -s / 2, y: -s * 0.55, width: s, height: s * 0.7, fill: '#f4c94e' }, g);
svgEl('polygon', { points: `${-s / 2 - 1},${-s * 0.55} 0,${-s * 1.05} ${s / 2 + 1},${-s * 0.55}`, fill: '#c85c4a' }, g);
}
function update() {
const gwh = +rng.value;
valOut.textContent = gwh;
kwhOut.textContent = fmtInt(gwh * 1e6) + ' kWh';
const households = (gwh * 1e6) / PER_DAY;
hhOut.textContent = 'Strom für ca. ' + fmtInt(households) + ' Haushalte einen Tag lang';
housesG.innerHTML = '';
const nIcons = Math.max(1, Math.min(60, Math.round(households / HOUSES_PER_ICON)));
const s = 20;
for (let i = 0; i < nIcons; i++) {
const c = i % COLS, r = Math.floor(i / COLS);
drawHouse(30 + c * 28, 60 + r * 46, s);
}
svgEl('text', { x: 300, y: 168, 'text-anchor': 'middle', 'font-size': '11', 'font-weight': '600', fill: '#4a7c8a' }, housesG)
.textContent = '1 Haus-Symbol ≈ ' + fmtInt(HOUSES_PER_ICON) + ' Haushalte';
}
rng.addEventListener('input', update);
update();
}
// =====================================================================
// netzspannung — Land-Buttons, Volt + Steckertyp + Lade-Hinweis
// =====================================================================
function netzspannung(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🔌 Netzspannung rund um die Welt</h3>
<p>Tippe ein Land an. Sieh die Spannung, die Steckerform und ob dein Handy-Netzteil dort direkt lädt.</p>
</div>
<div class="ge-albedo-choice" role="group">
<button class="ge-btn ge-btn-primary" data-key="at">🇦🇹 Österreich / EU</button>
<button class="ge-btn" data-key="us">🇺🇸 USA</button>
<button class="ge-btn" data-key="jp">🇯🇵 Japan</button>
<button class="ge-btn" data-key="gb">🇬🇧 Großbritannien</button>
</div>
<svg class="ge-gh-svg" viewBox="0 0 600 160" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-result">
<div class="ge-gh-pill">Spannung: <strong data-ref="volt">230 V</strong></div>
<div class="ge-gh-pill">Steckertyp: <strong data-ref="plug">Typ C / F</strong></div>
<div class="ge-gh-pill" data-ref="charge" data-state="good" aria-live="polite"></div>
</div>
<p class="ge-note">Nennspannungen und Steckertypen nach IEC: EU 230 V, USA 120 V, Japan 100 V, Großbritannien 230 V. Handy-Netzteile sind fast immer für 100 240 V gebaut und gleichen die Spannung selbst aus — nur die Steckerform ist weltweit verschieden.</p>
</div>
`;
const svg = root.querySelector('.ge-gh-svg');
const voltOut = root.querySelector('[data-ref="volt"]');
const plugOut = root.querySelector('[data-ref="plug"]');
const chargeOut = root.querySelector('[data-ref="charge"]');
if (!svg) return;
const lands = {
at: { volt: 230, plug: 'Typ C / F (Schuko)', home: true },
us: { volt: 120, plug: 'Typ A / B', home: false },
jp: { volt: 100, plug: 'Typ A / B', home: false },
gb: { volt: 230, plug: 'Typ G', home: false }
};
// Spannungs-Skala als Balken (Bezug 240 V)
svgEl('rect', { x: 40, y: 60, width: 520, height: 26, fill: '#ffffff', stroke: '#1f4e5a', 'stroke-width': '1.2', rx: '13' }, svg);
const voltBar = svgEl('rect', { x: 42, y: 62, width: 100, height: 22, fill: '#4a7c8a', rx: '11' }, svg);
const voltLabel = svgEl('text', { x: 300, y: 78, 'text-anchor': 'middle', 'font-size': '14', 'font-weight': '800', fill: '#ffffff' }, svg);
svgEl('text', { x: 40, y: 50, 'font-size': '11', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = '0 V';
svgEl('text', { x: 560, y: 50, 'text-anchor': 'end', 'font-size': '11', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = '240 V';
// Blitz-Icon + Steckerform
const plugIcon = svgEl('text', { x: 300, y: 135, 'text-anchor': 'middle', 'font-size': '13', 'font-weight': '700', fill: '#1f4e5a' }, svg);
function select(key) {
const L = lands[key] || lands.at;
voltOut.textContent = L.volt + ' V';
plugOut.textContent = L.plug;
voltBar.setAttribute('width', Math.max(20, (516 * L.volt) / 240));
voltBar.setAttribute('fill', L.volt >= 220 ? '#c85c4a' : '#4a7c8a');
voltLabel.textContent = L.volt + ' V';
plugIcon.textContent = 'Steckerform: ' + L.plug;
if (L.home) {
chargeOut.textContent = 'Dein Netzteil lädt direkt — und der Stecker passt (deine Heimat-Norm).';
chargeOut.dataset.state = 'good';
} else {
chargeOut.textContent = 'Dein Handy-Netzteil (100 240 V) lädt direkt — aber für die Steckerform brauchst du einen Reise-Adapter.';
chargeOut.dataset.state = 'warn';
}
root.querySelectorAll('.ge-albedo-choice .ge-btn').forEach(b => {
b.classList.toggle('ge-btn-primary', b.dataset.key === key);
});
}
root.querySelectorAll('.ge-albedo-choice .ge-btn').forEach(b => {
b.addEventListener('click', () => select(b.dataset.key));
});
select('at');
}
// =====================================================================
// kartenmaßstab — Maßstab-Schieberegler, gleicher Ort in versch. Ausschnitten
// =====================================================================
function kartenmassstab(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🗺 Der Kartenmaßstab</h3>
<p>Schiebe den Regler vom Detail bis zum Landausschnitt. Sieh, wie derselbe Ort immer kleiner wird — und das Lineal, wie weit 1 cm auf der Karte in echt sind.</p>
</div>
<svg class="ge-gh-svg" viewBox="0 0 600 260" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-slider">
<label for="ge-km-range">Maßstab: <strong>1 : <span id="ge-km-scale">100</span></strong></label>
<input id="ge-km-range" type="range" min="0" max="12" value="0" step="1">
<div class="ge-gh-markers"><span>Detail</span><span>Ort</span><span>Region</span><span>Land</span></div>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill">Lineal: <strong data-ref="ruler">1 cm = 1 m</strong></div>
<div class="ge-gh-pill" data-ref="view" data-state="now" aria-live="polite"></div>
</div>
<p class="ge-note">Rechenweg: 1 cm auf der Karte · Maßstabszahl = Strecke in echt. Merke: großer Maßstab (kleine Zahl) = kleiner Ausschnitt mit vielen Details, kleiner Maßstab (große Zahl) = großer Ausschnitt mit wenig Details.</p>
</div>
`;
const svg = root.querySelector('.ge-gh-svg');
const rng = root.querySelector('#ge-km-range');
const scaleOut = root.querySelector('#ge-km-scale');
const rulerOut = root.querySelector('[data-ref="ruler"]');
const viewOut = root.querySelector('[data-ref="view"]');
if (!svg || !rng) return;
const scales = [100, 200, 500, 1000, 2000, 5000, 10000, 25000, 50000, 100000, 250000, 500000, 1000000];
const sceneG = svgEl('g', {}, svg);
const rulerG = svgEl('g', {}, svg);
function levelOf(scale) {
if (scale <= 500) return 0; // Haus
if (scale <= 5000) return 1; // Straßenzug
if (scale <= 50000) return 2; // Ort / Viertel
if (scale <= 250000) return 3; // Region
return 4; // Land
}
function drawScene(level) {
sceneG.innerHTML = '';
svgEl('rect', { x: 20, y: 20, width: 560, height: 180, fill: '#eef4ee', stroke: '#c4d4ca', 'stroke-width': '1.5', rx: '8' }, sceneG);
const cx = 300, cy = 110;
if (level === 0) {
// Ein Haus mit Garten
svgEl('rect', { x: 40, y: 40, width: 520, height: 140, fill: '#d8ead0' }, sceneG);
svgEl('rect', { x: 210, y: 70, width: 180, height: 100, fill: '#f4c94e', stroke: '#1f4e5a', 'stroke-width': '2' }, sceneG);
svgEl('polygon', { points: '200,70 300,20 400,70', fill: '#c85c4a' }, sceneG);
svgEl('rect', { x: 285, y: 120, width: 30, height: 50, fill: '#6b4a2e' }, sceneG);
svgEl('polygon', { points: '110,175 128,135 146,175', fill: '#3a6b3e' }, sceneG);
svgEl('polygon', { points: '450,175 468,135 486,175', fill: '#3a6b3e' }, sceneG);
} else if (level === 1) {
// Straßenzug mit mehreren Häusern
svgEl('rect', { x: 40, y: 100, width: 520, height: 28, fill: '#c9cdd2' }, sceneG);
svgEl('line', { x1: 40, y1: 114, x2: 560, y2: 114, stroke: '#ffffff', 'stroke-width': '2', 'stroke-dasharray': '16 12' }, sceneG);
for (let i = 0; i < 6; i++) {
const x = 70 + i * 80;
svgEl('rect', { x, y: 55, width: 44, height: 34, fill: '#f4c94e', stroke: '#1f4e5a', 'stroke-width': '1.2' }, sceneG);
svgEl('polygon', { points: `${x - 3},55 ${x + 22},38 ${x + 47},55`, fill: '#c85c4a' }, sceneG);
svgEl('rect', { x: x + 6, y: 138, width: 36, height: 26, fill: '#a9c99a' }, sceneG);
}
} else if (level === 2) {
// Ort / Viertel: Häuserblöcke + Straßenraster
svgEl('rect', { x: 40, y: 40, width: 520, height: 140, fill: '#dfe8df' }, sceneG);
for (let gx = 0; gx < 5; gx++) for (let gy = 0; gy < 3; gy++) {
svgEl('rect', { x: 70 + gx * 96, y: 55 + gy * 42, width: 70, height: 28, fill: '#c4a97a', stroke: '#8a7256', 'stroke-width': '1' }, sceneG);
}
for (let i = 1; i < 5; i++) svgEl('line', { x1: 40 + i * 104, y1: 40, x2: 40 + i * 104, y2: 180, stroke: '#c9cdd2', 'stroke-width': '6' }, sceneG);
for (let i = 1; i < 3; i++) svgEl('line', { x1: 40, y1: 40 + i * 47, x2: 560, y2: 40 + i * 47, stroke: '#c9cdd2', 'stroke-width': '6' }, sceneG);
} else if (level === 3) {
// Region: Grün-/Wasserflächen, Fluss, Ortspunkte
svgEl('rect', { x: 40, y: 40, width: 520, height: 140, fill: '#cfe3c4' }, sceneG);
svgEl('path', { d: 'M 40 60 Q 200 90 300 70 T 560 120', stroke: '#4a9cc4', 'stroke-width': '9', fill: 'none', 'stroke-linecap': 'round' }, sceneG);
svgEl('polygon', { points: '380,90 470,110 440,175 350,150', fill: '#b7d59e' }, sceneG);
[ [140, 120], [300, 150], [470, 70], [230, 90] ].forEach(([x, y]) => {
svgEl('circle', { cx: x, cy: y, r: 7, fill: '#c85c4a', stroke: '#fff', 'stroke-width': '1.5' }, sceneG);
});
} else {
// Land: großer Ausschnitt mit Städten
svgEl('rect', { x: 40, y: 40, width: 520, height: 140, fill: '#cfe3c4' }, sceneG);
svgEl('path', { d: 'M 60 150 Q 120 60 240 90 Q 360 120 440 70 Q 520 40 545 120 L 545 175 L 60 175 Z', fill: '#b7d59e', stroke: '#6f9c5f', 'stroke-width': '2' }, sceneG);
svgEl('path', { d: 'M 40 40 Q 120 55 180 45 L 180 40 Z', fill: '#9cc4e4' }, sceneG);
[ [140, 130, 'groß'], [300, 100, 'mittel'], [430, 130, 'klein'], [230, 155, 'klein'] ].forEach(([x, y, sz]) => {
const r = sz === 'groß' ? 8 : (sz === 'mittel' ? 6 : 4);
svgEl('circle', { cx: x, cy: y, r, fill: '#c85c4a', stroke: '#fff', 'stroke-width': '1.5' }, sceneG);
});
}
}
function drawRuler(scale) {
rulerG.innerHTML = '';
// Referenzlinie „1 cm auf der Karte" (fixe Länge 90 px als Anschauung)
const x0 = 40, x1 = 130, y = 218;
svgEl('line', { x1: x0, y1: y, x2: x1, y2: y, stroke: '#1f4e5a', 'stroke-width': '3', 'stroke-linecap': 'round' }, rulerG);
svgEl('line', { x1: x0, y1: y - 6, x2: x0, y2: y + 6, stroke: '#1f4e5a', 'stroke-width': '3' }, rulerG);
svgEl('line', { x1: x1, y1: y - 6, x2: x1, y2: y + 6, stroke: '#1f4e5a', 'stroke-width': '3' }, rulerG);
svgEl('text', { x: 85, y: y + 24, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '700', fill: '#1f4e5a' }, rulerG).textContent = '1 cm auf der Karte';
const realM = scale / 100; // 1 cm * scale = scale cm = scale/100 m
let real;
if (realM >= 1000) real = fmtDec(realM / 1000, realM % 1000 === 0 ? 0 : 1) + ' km';
else real = fmtInt(realM) + ' m';
svgEl('text', { x: 150, y: y + 4, 'font-size': '14', 'font-weight': '800', fill: '#c85c4a' }, rulerG).textContent = '= ' + real + ' in echt';
return real;
}
function update() {
const scale = scales[+rng.value];
scaleOut.textContent = fmtInt(scale);
const level = levelOf(scale);
drawScene(level);
const real = drawRuler(scale);
rulerOut.textContent = '1 cm = ' + real;
const viewNames = ['Einzelnes Haus mit Garten', 'Ein Straßenzug', 'Ein Ort / Stadtviertel', 'Eine ganze Region', 'Großer Landausschnitt'];
const big = scale <= 5000;
viewOut.textContent = viewNames[level] + ' — ' + (big ? 'großer Maßstab, viele Details' : 'kleiner Maßstab, wenig Details');
viewOut.dataset.state = big ? 'good' : (level >= 3 ? 'warn' : 'now');
}
rng.addEventListener('input', update);
update();
}
// Registry — Keys entsprechen key_slug aus DB.
window.GlossarExperiments = Object.assign(window.GlossarExperiments || {}, {
'gletscher': gletscher,
'lawinenwarnstufe': lawinenwarnstufe,
'lichtgeschwindigkeit': lichtgeschwindigkeit,
'gigawattstunde': gigawattstunde,
'netzspannung': netzspannung,
'kartenmaßstab': kartenmassstab
});
})();
@@ -0,0 +1,554 @@
/**
* Glossar-Experimente Tourismus.
*
* Registrierung wie in glossar-experiments.js:
* window.GlossarExperiments[key_slug] = function (root) { }
* Der Renderer (glossar.php) hängt einen leeren Container ein und ruft die
* Funktion mit diesem Container als einzigem Argument auf.
*
* Richtlinien:
* - Keine externen Libraries, Vanilla JS + SVG.
* - Mobile-/Tap-freundlich: grosse Buttons, keine Hover-Abhängigkeit.
* - Zahlen quellenbar, kurze Begründung in der UI sichtbar (.ge-note).
* - Rechenzeichen: Malpunkt · und Doppelpunkt : (nie ×, ÷, *).
* - Deutsche Zahlen via toLocaleString('de-AT').
*/
(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 fmtInt = n => Math.round(n).toLocaleString('de-AT');
const fmtDec = (n, digits) => n.toLocaleString('de-AT', {
minimumFractionDigits: digits, maximumFractionDigits: digits
});
// =====================================================================
// Experiment: Beschneiung — Schneekanone und die Feuchtkugel-Grenze
// =====================================================================
function beschneiung(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3> Wann kann die Schneekanone arbeiten?</h3>
<p>Eine Schneekanone verwandelt Wasser nur dann in Schnee, wenn es kalt genug ist.
Schiebe den Regler und beobachte, ab welcher Temperatur echter Schnee entsteht.</p>
</div>
<svg class="ge-gh-svg" viewBox="0 0 600 260" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-slider">
<label for="ge-bs-range">Lufttemperatur: <strong id="ge-bs-temp">5 °C</strong></label>
<input id="ge-bs-range" type="range" min="-8" max="4" value="-5" step="1">
<div class="ge-gh-markers"><span>8 °C</span><span>2 °C</span><span>0 °C</span><span>+4 °C</span></div>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill" id="ge-bs-state" data-state="good">🟢 Schneekanone AN</div>
<div class="ge-gh-pill" id="ge-bs-snow">Schneehöhe: <strong></strong></div>
</div>
<p class="ge-note">Faustregel der Pisten-Technik: Beschneiung gelingt erst unterhalb einer
Feuchtkugeltemperatur von rund 2 °C. Je kälter (und trockener) die Luft, desto mehr Schnee
je Stunde. Über der Grenze fällt das Wasser nur nass herunter. Quelle: Seilbahnen Österreich,
Beschneiungsleitfaden.</p>
</div>
`;
const svg = root.querySelector('.ge-gh-svg');
const rng = root.querySelector('#ge-bs-range');
const tempOut = root.querySelector('#ge-bs-temp');
const stateP = root.querySelector('#ge-bs-state');
const snowP = root.querySelector('#ge-bs-snow');
const THRESHOLD = -2;
// Himmel + Berg
svgEl('rect', { x: 0, y: 0, width: 600, height: 260, fill: '#eff5f6' }, svg);
svgEl('polygon', { points: '360,260 470,90 520,140 600,60 600,260', fill: '#cddbe0' }, svg);
// Piste (Boden)
const PISTE_Y = 210;
svgEl('rect', { x: 0, y: PISTE_Y, width: 600, height: 50, fill: '#8aa39a' }, svg);
// Schneekanone links
svgEl('rect', { x: 60, y: PISTE_Y - 22, width: 16, height: 22, fill: '#4a4a4a', rx: '2' }, svg);
svgEl('rect', { x: 74, y: PISTE_Y - 30, width: 48, height: 18, fill: '#1f4e5a', rx: '6' }, svg);
svgEl('circle', { cx: 122, cy: PISTE_Y - 21, r: 11, fill: '#4a7c8a', stroke: '#1f4e5a', 'stroke-width': '2' }, svg);
svgEl('text', { x: 68, y: PISTE_Y - 40, 'font-size': '11', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = 'Schneekanone';
// Schneeband auf der Piste (wächst mit Kälte)
const snowBand = svgEl('rect', { x: 140, y: PISTE_Y, width: 440, height: 0, fill: '#ffffff', stroke: '#b5c9cf', 'stroke-width': '1' }, svg);
// Dynamische Partikel-Gruppe (Schnee oder Tropfen)
const spray = svgEl('g', {}, svg);
// Status-Text in der Szene
const sceneMsg = svgEl('text', { x: 360, y: 40, 'text-anchor': 'middle', 'font-size': '15', 'font-weight': '800', fill: '#1f4e5a' }, svg);
function update() {
const temp = +rng.value;
tempOut.textContent = (temp > 0 ? '+' : '') + temp + ' °C';
spray.innerHTML = '';
const on = temp <= THRESHOLD;
// coldFactor: 0 an der Grenze (2), 1 bei 8
const coldFactor = Math.max(0, Math.min(1, (THRESHOLD - temp) / 6));
if (on) {
stateP.textContent = '🟢 Schneekanone AN';
stateP.dataset.state = 'good';
// Schneeband wächst
const h = 6 + coldFactor * 42;
snowBand.setAttribute('y', PISTE_Y - h);
snowBand.setAttribute('height', h);
// Schneeflocken aus der Kanone
const nFlakes = 10 + Math.round(coldFactor * 30);
for (let i = 0; i < nFlakes; i++) {
const t = i / nFlakes;
const px = 130 + t * 300 + (Math.random() * 30 - 15);
const py = (PISTE_Y - 25) - Math.sin(t * Math.PI) * 70 + (Math.random() * 16 - 8);
svgEl('circle', { cx: px, cy: py, r: 3.2, fill: '#ffffff', stroke: '#cddbe0', 'stroke-width': '.8' }, spray);
}
sceneMsg.textContent = '❄ Echter Schnee entsteht';
const cmPerH = 2 + coldFactor * 10; // anschaulicher Richtwert
snowP.innerHTML = 'Schnee-Leistung: <strong>' + fmtDec(cmPerH, 0) + ' cm je Stunde</strong>';
} else {
stateP.textContent = '🟠 Schneekanone AUS';
stateP.dataset.state = 'warn';
snowBand.setAttribute('height', 0);
// Nasse Tropfen fallen einfach herunter
for (let i = 0; i < 14; i++) {
const px = 130 + Math.random() * 300;
const py = (PISTE_Y - 20) - Math.random() * 40;
svgEl('ellipse', { cx: px, cy: py, rx: 2.4, ry: 4, fill: '#4a7c8a', opacity: '0.75' }, spray);
}
sceneMsg.textContent = '💧 Zu warm — nur nasses Wasser';
snowP.innerHTML = 'Schnee-Leistung: <strong>0 cm — kein Schnee</strong>';
}
}
rng.addEventListener('input', update);
update();
}
// =====================================================================
// Experiment: Diversifizierung — mehrere Standbeine sichern den Umsatz
// =====================================================================
function diversifizierung(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🌗 Ein Standbein oder mehrere?</h3>
<p>Ein Urlaubsort lebt vom Winter. Stelle ein, wie gut der Winter ausfällt, und schalte
Sommer-Standbeine dazu. Beobachte, was mit dem Jahresumsatz passiert.</p>
</div>
<div class="ge-gh-slider">
<label for="ge-dv-range">Winter: <strong id="ge-dv-winter">guter Winter</strong></label>
<input id="ge-dv-range" type="range" min="0" max="100" value="80" step="5">
<div class="ge-gh-markers"><span>schneearm</span><span></span><span>guter Winter</span></div>
</div>
<div class="ge-controls">
<button class="ge-btn" data-act="summer"> Sommer-Standbeine dazuschalten</button>
</div>
<svg class="ge-bal-svg" viewBox="0 0 600 140" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-result">
<div class="ge-gh-pill">Jahresumsatz: <strong id="ge-dv-sum"></strong></div>
<div class="ge-gh-pill" id="ge-dv-verdict"></div>
</div>
<p class="ge-note">Modellzahlen: höchstmöglicher Jahresumsatz 500.000 . Ohne zweites Standbein
hängt alles am Winter ein schneearmer Winter bricht den Umsatz fast völlig weg. Mit
Sommer-Angeboten (Wandern, Bike, See) bleibt rund die Hälfte des Umsatzes auch ohne Schnee
erhalten. Genau das meint Diversifizierung: das Risiko auf mehrere Standbeine verteilen.</p>
</div>
`;
const rng = root.querySelector('#ge-dv-range');
const winterOut = root.querySelector('#ge-dv-winter');
const sumOut = root.querySelector('#ge-dv-sum');
const verdict = root.querySelector('#ge-dv-verdict');
const btn = root.querySelector('[data-act="summer"]');
const svg = root.querySelector('.ge-bal-svg');
const MAX = 500000, BAR_X = 130, BAR_W = 400, BAR_Y = 40, BAR_H = 34;
let summer = false;
// Balken-Rahmen
svgEl('text', { x: BAR_X - 10, y: BAR_Y + 22, 'text-anchor': 'end', 'font-size': '13', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = 'Umsatz';
svgEl('rect', { x: BAR_X, y: BAR_Y, width: BAR_W, height: BAR_H, fill: '#eff5f6', rx: '8' }, svg);
const bar = svgEl('rect', { x: BAR_X, y: BAR_Y, width: 0, height: BAR_H, fill: '#5a8a5e', rx: '8' }, svg);
const barTxt = svgEl('text', { x: BAR_X + 8, y: BAR_Y + 22, 'font-size': '13', 'font-weight': '800', fill: '#1f4e5a' }, svg);
// Winter-Anteil-Markierung (bei summer der Sockel)
svgEl('text', { x: BAR_X, y: 108, 'font-size': '11', fill: '#4a4a4a' }, svg).textContent = '0 €';
svgEl('text', { x: BAR_X + BAR_W, y: 108, 'text-anchor': 'end', 'font-size': '11', fill: '#4a4a4a' }, svg).textContent = '500.000 €';
const sockelLine = svgEl('line', { x1: BAR_X, y1: BAR_Y - 6, x2: BAR_X, y2: BAR_Y + BAR_H + 6, stroke: '#e8833a', 'stroke-width': '2', 'stroke-dasharray': '4 3', opacity: '0' }, svg);
const sockelTxt = svgEl('text', { x: BAR_X, y: BAR_Y - 10, 'text-anchor': 'middle', 'font-size': '10', 'font-weight': '700', fill: '#e8833a', opacity: '0' }, svg);
function update() {
const winter = +rng.value; // 0..100
const wf = winter / 100;
winterOut.textContent = winter >= 66 ? 'guter Winter' : (winter >= 33 ? 'mittlerer Winter' : 'schneearmer Winter');
let umsatz, summerFloor;
if (summer) {
// 40 % feste Sommerbasis + 60 % vom Winter
summerFloor = MAX * 0.4;
umsatz = summerFloor + MAX * 0.6 * wf;
} else {
summerFloor = 0;
umsatz = MAX * wf;
}
const w = (umsatz / MAX) * BAR_W;
bar.setAttribute('width', w);
bar.setAttribute('fill', umsatz < MAX * 0.25 ? '#c85c4a' : (umsatz < MAX * 0.55 ? '#e8833a' : '#5a8a5e'));
barTxt.setAttribute('x', Math.min(BAR_X + w + 8, BAR_X + BAR_W - 92));
barTxt.textContent = fmtInt(umsatz) + ' €';
// Sommer-Sockel-Linie
if (summer) {
const sx = BAR_X + (summerFloor / MAX) * BAR_W;
sockelLine.setAttribute('x1', sx); sockelLine.setAttribute('x2', sx); sockelLine.setAttribute('opacity', '1');
sockelTxt.setAttribute('x', sx); sockelTxt.setAttribute('opacity', '1');
sockelTxt.textContent = 'Sommer-Sockel';
} else {
sockelLine.setAttribute('opacity', '0');
sockelTxt.setAttribute('opacity', '0');
}
sumOut.textContent = fmtInt(umsatz) + ' €';
btn.classList.toggle('ge-btn-primary', summer);
btn.textContent = summer ? '☀ Sommer-Standbeine AN (zum Abschalten tippen)' : '☀ Sommer-Standbeine dazuschalten';
if (summer && winter < 33) {
verdict.textContent = '🟢 Schneearm — trotzdem stabil dank Sommer';
verdict.dataset.state = 'good';
} else if (!summer && winter < 33) {
verdict.textContent = '🔴 Nur Winter — schneearm bricht den Umsatz weg';
verdict.dataset.state = 'bad';
} else if (summer) {
verdict.textContent = '🟢 Zwei Standbeine — sicher aufgestellt';
verdict.dataset.state = 'good';
} else {
verdict.textContent = '🟡 Ein Standbein — alles hängt am Winter';
verdict.dataset.state = 'warn';
}
}
rng.addEventListener('input', update);
btn.addEventListener('click', () => { summer = !summer; update(); });
update();
}
// =====================================================================
// Experiment: Kapazität — 30 Plätze sind die Obergrenze
// =====================================================================
function kapazitaet(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3> Das Café hat 30 Plätze</h3>
<p>Ein Bergcafé hat genau 30 Stühle. Stelle ein, wie viele Gäste in einer Stunde kommen
und beobachte, wer noch Platz findet und wer warten muss.</p>
</div>
<svg class="ge-bal-svg" viewBox="0 0 600 240" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-slider">
<label for="ge-kp-range">Gäste pro Stunde: <strong id="ge-kp-guests">24</strong></label>
<input id="ge-kp-range" type="range" min="0" max="55" value="24" step="1">
<div class="ge-gh-markers"><span>0</span><span>30 = Kapazität</span><span>55</span></div>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill">Sitzen: <strong id="ge-kp-sit">24</strong> · Warten: <strong id="ge-kp-wait">0</strong></div>
<div class="ge-gh-pill" id="ge-kp-verdict"></div>
</div>
<p class="ge-note">Die Kapazität ist die feste Obergrenze: Ab dem 31. Gast finden keine weiteren
mehr Platz sie müssen warten oder bekommen eine Absage. Mehr Nachfrage füllt die vorhandenen
Plätze, erhöht aber nie die Kapazität. Modellbeispiel Bergcafé mit 30 Sitzplätzen.</p>
</div>
`;
const svg = root.querySelector('.ge-bal-svg');
const rng = root.querySelector('#ge-kp-range');
const guestsOut = root.querySelector('#ge-kp-guests');
const sitOut = root.querySelector('#ge-kp-sit');
const waitOut = root.querySelector('#ge-kp-wait');
const verdict = root.querySelector('#ge-kp-verdict');
const CAP = 30;
// Café-Bereich (Stuhl-Raster 6 x 5) links, Warteschlange rechts
svgEl('rect', { x: 14, y: 14, width: 372, height: 212, fill: '#eff5f6', stroke: '#b5c9cf', 'stroke-width': '1.5', rx: '10' }, svg);
svgEl('text', { x: 24, y: 34, 'font-size': '12', 'font-weight': '800', fill: '#1f4e5a' }, svg).textContent = '☕ Café — 30 Plätze';
svgEl('text', { x: 470, y: 34, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '800', fill: '#c85c4a' }, svg).textContent = 'Warteschlange';
const seats = [];
const COLS = 6, ROWS = 5;
for (let i = 0; i < CAP; i++) {
const c = i % COLS, r = Math.floor(i / COLS);
const cx = 46 + c * 58, cy = 66 + r * 34;
// Stuhl-Slot
svgEl('circle', { cx, cy, r: 13, fill: '#ffffff', stroke: '#b5c9cf', 'stroke-width': '1.5' }, svg);
const occ = svgEl('circle', { cx, cy, r: 10, fill: '#5a8a5e', opacity: '0' }, svg);
const head = svgEl('circle', { cx, cy: cy - 4, r: 3.4, fill: '#ffffff', opacity: '0' }, svg);
seats.push({ occ, head });
}
const queueG = svgEl('g', {}, svg);
function update() {
const guests = +rng.value;
const sit = Math.min(guests, CAP);
const wait = Math.max(0, guests - CAP);
guestsOut.textContent = guests;
sitOut.textContent = sit;
waitOut.textContent = wait;
seats.forEach((s, i) => {
const on = i < sit ? '1' : '0';
s.occ.setAttribute('opacity', on);
s.head.setAttribute('opacity', on);
});
// Warteschlange zeichnen (max 25 Figuren sichtbar)
queueG.innerHTML = '';
const shown = Math.min(wait, 25);
for (let i = 0; i < shown; i++) {
const c = i % 5, r = Math.floor(i / 5);
const x = 418 + c * 30, y = 60 + r * 32;
svgEl('circle', { cx: x, cy: y, r: 9, fill: '#c85c4a' }, queueG);
svgEl('circle', { cx: x, cy: y - 3, r: 3, fill: '#ffffff' }, queueG);
}
if (wait > 25) {
svgEl('text', { x: 470, y: 216, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '800', fill: '#c85c4a' }, queueG).textContent = '+ ' + (wait - 25) + ' weitere';
}
if (wait === 0 && guests < CAP) {
verdict.textContent = '🟢 Alle ' + guests + ' Gäste finden Platz';
verdict.dataset.state = 'good';
} else if (wait === 0) {
verdict.textContent = '🟡 Café voll — genau an der Kapazität';
verdict.dataset.state = 'warn';
} else {
verdict.textContent = '🔴 ' + wait + ' Gäste müssen warten oder absagen';
verdict.dataset.state = 'bad';
}
}
rng.addEventListener('input', update);
update();
}
// =====================================================================
// Experiment: Saisonalität — Ganzjahrestourismus glättet die Kurve
// =====================================================================
function saisonalitaet(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>📅 Volle und leere Monate</h3>
<p>Viele Bergorte leben fast nur vom Winter. Schalte Sommer-Angebote dazu und beobachte,
wie sich die Gäste übers Jahr verteilen.</p>
</div>
<svg class="ge-bal-svg" viewBox="0 0 600 220" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-controls">
<button class="ge-btn" data-act="summer"> Sommer-Angebote einschalten</button>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill">Höchster Monat: <strong id="ge-sa-max"></strong> · Schwächster: <strong id="ge-sa-min"></strong></div>
<div class="ge-gh-pill" id="ge-sa-verdict"></div>
</div>
<p class="ge-note">Die starke Ballung auf wenige Monate heißt Saisonalität. Ganzjahrestourismus
(Wandern, Bike, Seen, Kultur im Sommer) verteilt die Nachfrage gleichmäßiger übers Jahr das
sichert Arbeitsplätze und entlastet die Spitzen. Modellhafte Nachfragewerte, angelehnt an das
Muster alpiner Wintersportorte.</p>
</div>
`;
const svg = root.querySelector('.ge-bal-svg');
const btn = root.querySelector('[data-act="summer"]');
const maxOut = root.querySelector('#ge-sa-max');
const minOut = root.querySelector('#ge-sa-min');
const verdict = root.querySelector('#ge-sa-verdict');
const months = ['Jän', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'];
const winterOnly = [85, 90, 70, 30, 18, 22, 26, 30, 22, 20, 42, 80];
const withSummer = [85, 90, 72, 42, 48, 70, 80, 82, 66, 48, 45, 80];
let summer = false;
const BASE_Y = 180, MAXH = 140, BW = 34, GAP = 12, X0 = 60;
svgEl('line', { x1: 40, y1: BASE_Y, x2: 580, y2: BASE_Y, stroke: '#b5c9cf', 'stroke-width': '1.5' }, svg);
const bars = months.map((m, i) => {
const x = X0 + i * (BW + GAP);
const bar = svgEl('rect', { x, y: BASE_Y, width: BW, height: 0, fill: '#4a7c8a', rx: '4' }, svg);
svgEl('text', { x: x + BW / 2, y: BASE_Y + 16, 'text-anchor': 'middle', 'font-size': '10', 'font-weight': '600', fill: '#4a4a4a' }, svg).textContent = m;
return bar;
});
function update() {
const data = summer ? withSummer : winterOnly;
let maxV = -1, minV = 999, maxI = 0, minI = 0;
data.forEach((v, i) => {
if (v > maxV) { maxV = v; maxI = i; }
if (v < minV) { minV = v; minI = i; }
});
bars.forEach((bar, i) => {
const h = data[i] / 100 * MAXH;
bar.setAttribute('y', BASE_Y - h);
bar.setAttribute('height', h);
// Sommermonate hervorheben, wenn eingeschaltet
const isSummer = i >= 4 && i <= 9;
bar.setAttribute('fill', summer && isSummer ? '#5a8a5e' : '#4a7c8a');
});
maxOut.textContent = months[maxI];
minOut.textContent = months[minI];
btn.classList.toggle('ge-btn-primary', summer);
btn.textContent = summer ? '☀ Sommer-Angebote AN (zum Abschalten tippen)' : '☀ Sommer-Angebote einschalten';
const spread = maxV - minV;
if (summer) {
verdict.textContent = '🟢 Gleichmäßiger — Unterschied nur noch ' + spread + ' Punkte';
verdict.dataset.state = 'good';
} else {
verdict.textContent = '🔴 Winterlastig — Unterschied ' + spread + ' Punkte';
verdict.dataset.state = 'bad';
}
}
btn.addEventListener('click', () => { summer = !summer; update(); });
update();
}
// =====================================================================
// Experiment: Übertourismus — Gäste je Einwohner (Hallstatt)
// =====================================================================
function uebertourismus(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🏘 Wie viele Gäste verträgt ein kleiner Ort?</h3>
<p>Hallstatt in Oberösterreich hat rund <strong>760 Einwohner:innen</strong>. Schiebe den
Regler und sieh, wie viele Gäste je Einwohner:in an einem Tag im Ort sind.</p>
</div>
<svg class="ge-bal-svg" viewBox="0 0 600 130" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-slider">
<label for="ge-ut-range">Gäste pro Tag: <strong id="ge-ut-guests">3.000</strong></label>
<input id="ge-ut-range" type="range" min="0" max="10000" value="3000" step="100">
<div class="ge-gh-markers"><span>0</span><span>5.000</span><span>10.000</span></div>
</div>
<div class="ge-gh-result">
<div class="ge-gh-pill">Je Einwohner:in: <strong id="ge-ut-ratio"></strong> Gäste</div>
<div class="ge-gh-pill" id="ge-ut-verdict"></div>
</div>
<p class="ge-note">Hallstatt: rund 760 Einwohner:innen, an Spitzentagen bis zu 10.000 Gäste
das sind über 13 Gäste je Einwohner:in. So viel Andrang überlastet Wege, Verkehr und
Alltag der Menschen vor Ort. Das nennt man Übertourismus. Quellen: Gemeinde Hallstatt,
Statistik Austria.</p>
</div>
`;
const svg = root.querySelector('.ge-bal-svg');
const rng = root.querySelector('#ge-ut-range');
const guestsOut = root.querySelector('#ge-ut-guests');
const ratioOut = root.querySelector('#ge-ut-ratio');
const verdict = root.querySelector('#ge-ut-verdict');
const RESIDENTS = 760, MAX_GUESTS = 10000;
const BAR_X = 20, BAR_W = 560, BAR_Y = 44, BAR_H = 40;
svgEl('text', { x: BAR_X, y: 30, 'font-size': '12', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = 'Belastung des Ortes';
svgEl('rect', { x: BAR_X, y: BAR_Y, width: BAR_W, height: BAR_H, fill: '#eff5f6', stroke: '#b5c9cf', 'stroke-width': '1', rx: '10' }, svg);
const bar = svgEl('rect', { x: BAR_X, y: BAR_Y, width: 0, height: BAR_H, fill: '#5a8a5e', rx: '10' }, svg);
const barTxt = svgEl('text', { x: BAR_X + BAR_W / 2, y: BAR_Y + 26, 'text-anchor': 'middle', 'font-size': '14', 'font-weight': '800', fill: '#1f4e5a' }, svg);
svgEl('text', { x: BAR_X, y: 108, 'font-size': '11', fill: '#5a8a5e', 'font-weight': '700' }, svg).textContent = '🟢 entspannt';
svgEl('text', { x: BAR_X + BAR_W, y: 108, 'text-anchor': 'end', 'font-size': '11', fill: '#c85c4a', 'font-weight': '700' }, svg).textContent = 'überlastet 🔴';
function update() {
const guests = +rng.value;
const ratio = guests / RESIDENTS;
guestsOut.textContent = fmtInt(guests);
ratioOut.textContent = fmtDec(ratio, 1);
const w = (guests / MAX_GUESTS) * BAR_W;
bar.setAttribute('width', w);
barTxt.textContent = fmtDec(ratio, 1) + ' Gäste je Einwohner:in';
let color, v, state;
if (ratio < 2) { color = '#5a8a5e'; v = '🟢 Entspannt — der Ort verkraftet das gut'; state = 'good'; }
else if (ratio < 6) { color = '#e8833a'; v = '🟡 Viel los — Wege und Verkehr werden eng'; state = 'warn'; }
else { color = '#c85c4a'; v = '🔴 Übertourismus — der Ort ist überlastet'; state = 'bad'; }
bar.setAttribute('fill', color);
verdict.textContent = v;
verdict.dataset.state = state;
}
rng.addEventListener('input', update);
update();
}
// =====================================================================
// Experiment: Bevölkerungsdichte — Menschen je Quadratkilometer
// =====================================================================
function bevoelkerungsdichte(root) {
root.innerHTML = `
<div class="ge-wrap">
<div class="ge-head">
<h3>🧑🤝🧑 Wie eng wohnen die Menschen?</h3>
<p>Jedes Feld unten ist genau <strong>1 km²</strong> groß. Wähle einen Ort und sieh, wie viele
Menschen dort auf diesem einen Quadratkilometer leben.</p>
</div>
<div class="ge-albedo-choice" role="tablist">
<button class="ge-btn" data-key="tal">Bergtal</button>
<button class="ge-btn ge-btn-primary" data-key="at">Österreich </button>
<button class="ge-btn" data-key="wien">Stadt (Wien)</button>
<button class="ge-btn" data-key="monaco">Monaco</button>
</div>
<svg class="ge-bal-svg" viewBox="0 0 600 300" preserveAspectRatio="xMidYMid meet" aria-hidden="true"></svg>
<div class="ge-gh-result">
<div class="ge-gh-pill">Dichte: <strong id="ge-bd-val"></strong> Menschen je km²</div>
<div class="ge-gh-pill" id="ge-bd-verdict"></div>
</div>
<p class="ge-note">Bevölkerungsdichte = Menschen : Fläche. Bergtal rund 5, Österreich im Schnitt
109, Wien rund 4.600 und Monaco rund 26.000 Menschen je km². Ein Punkt steht hier für
50 Menschen. Quellen: Statistik Austria (2024), Statista.</p>
</div>
`;
const svg = root.querySelector('.ge-bal-svg');
const valOut = root.querySelector('#ge-bd-val');
const verdict = root.querySelector('#ge-bd-verdict');
const PER_DOT = 50;
const places = {
tal: { label: 'Bergtal', density: 5, v: '🟢 Sehr dünn besiedelt — viel Platz', state: 'good' },
at: { label: 'Österreich ⌀', density: 109, v: '🟢 Ländlich-durchschnittlich', state: 'good' },
wien: { label: 'Stadt (Wien)', density: 4600, v: '🟡 Dicht — typische Großstadt', state: 'warn' },
monaco: { label: 'Monaco', density: 26000, v: '🔴 Extrem dicht — dichtestes Land der Welt', state: 'bad' }
};
// 1-km²-Feld (Quadrat)
const FX = 175, FY = 20, FS = 260;
svgEl('rect', { x: FX, y: FY, width: FS, height: FS, fill: '#eef4ef', stroke: '#5a8a5e', 'stroke-width': '2', rx: '6' }, svg);
svgEl('text', { x: FX + FS / 2, y: FY + FS + 20, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '700', fill: '#1f4e5a' }, svg).textContent = '1 km² · 1 Punkt = 50 Menschen';
const dotG = svgEl('g', {}, svg);
// Feste Punkt-Positionen (Raster mit Jitter), damit der Vergleich fair bleibt
const CAP = 600; // max sichtbare Punkte (25 x 24)
const positions = [];
const gcols = 25, grows = 24;
const cellW = (FS - 16) / gcols, cellH = (FS - 16) / grows;
for (let r = 0; r < grows; r++) {
for (let c = 0; c < gcols; c++) {
positions.push({
x: FX + 8 + c * cellW + cellW / 2,
y: FY + 8 + r * cellH + cellH / 2
});
}
}
function setPlace(key) {
const p = places[key] || places.at;
const nDots = Math.min(CAP, Math.round(p.density / PER_DOT));
dotG.innerHTML = '';
for (let i = 0; i < nDots; i++) {
const pos = positions[i];
svgEl('circle', { cx: pos.x, cy: pos.y, r: 2.6, fill: '#c85c4a', opacity: '0.9' }, dotG);
}
if (nDots === 0) {
// wenigstens 1 Mensch sichtbar bei sehr geringer Dichte
svgEl('circle', { cx: positions[0].x, cy: positions[0].y, r: 3, fill: '#5a8a5e' }, dotG);
}
valOut.textContent = fmtInt(p.density);
verdict.textContent = p.v;
verdict.dataset.state = p.state;
root.querySelectorAll('.ge-albedo-choice .ge-btn').forEach(b => {
b.classList.toggle('ge-btn-primary', b.dataset.key === key);
});
}
root.querySelectorAll('.ge-albedo-choice .ge-btn').forEach(b => {
b.addEventListener('click', () => setPlace(b.dataset.key));
});
setPlace('at');
}
// Registry — Keys entsprechen key_slug aus der Tabelle `glossar`
window.GlossarExperiments = Object.assign(window.GlossarExperiments || {}, {
'beschneiung': beschneiung,
'diversifizierung': diversifizierung,
'kapazitaet': kapazitaet,
'saisonalitaet': saisonalitaet,
'uebertourismus': uebertourismus,
'bevoelkerungsdichte': bevoelkerungsdichte
});
})();
+2
View File
@@ -647,6 +647,8 @@ include __DIR__ . '/_partials/erkundungs_subnav.php';
<script src="<?= $bp ?>/assets/js/glossar-experiments-natur.js?v=<?= @filemtime(__DIR__ . '/../assets/js/glossar-experiments-natur.js') ?: time() ?>"></script>
<script src="<?= $bp ?>/assets/js/glossar-experiments-wirtschaft.js?v=<?= @filemtime(__DIR__ . '/../assets/js/glossar-experiments-wirtschaft.js') ?: time() ?>"></script>
<script src="<?= $bp ?>/assets/js/glossar-experiments-markt.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-markt.js") ?: time() ?>"></script>
<script src="<?= $bp ?>/assets/js/glossar-experiments-tourismus.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-tourismus.js") ?: time() ?>"></script>
<script src="<?= $bp ?>/assets/js/glossar-experiments-geowelt.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-geowelt.js") ?: time() ?>"></script>
<footer class="gl-footer">
<img src="<?= $bp ?>/assets/img/bildLogo.png" alt="">