Startseite entschlackt: weniger Text, ruhiger, besser am Handy

- Cursor-Effekt im Hero entfernt (Messfadenkreuz, Temperatur-Skala,
  Maus-Wärmespur); Wärmebild läuft nur noch ruhig im Hintergrund
- Preloader, Laufbänder, Intro, Ablauf-Block, Kennzahlen, Footer-CTA,
  kursive Akzentwörter, Verlaufstext und Nummern-Pillen entfernt
- Texte in allen Abschnitten deutlich gekürzt, schlichte Überschriften
- Leistungen: auf Handy/Tablet wischbare Kärtchen mit Zähler und Pfeilen
- Neue, zusammenhängende Illustrationen für die fünf Leistungs-Kärtchen
- Heizungs-Finder ohne Prozent-Balken, kürzere Ergebnistexte
- Energie-Haus: Tabs als Wischleiste am Handy, kein Autoplay
- GSAP, ScrollTrigger, SplitText, Lenis und Instrument Serif entfernt
- Barrierefreiheit: Kontrast, Fokusring, Fokus im Kartenstapel,
  Telefon-Button mit Namen, Menü sperrt Hintergrund
- Cache-Busting (?v=20261004) für CSS und JS, README aktualisiert

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NotPika69andClaude Opus 5.5 committed 2026-10-04 20:14:53 +02:00
1 parent 717a7de501
commit ad155124b1
20 files changed
+1182 -2147

No files matched your search

+8 -8
View File
@@ -76,14 +76,14 @@
var i;
for (i = 0; i < slots.length; i++) {
if (n.mins >= slots[i][0] && n.mins < slots[i][1]) {
return { state: 'open', text: 'Jetzt geöffnet · bis ' + fmt(slots[i][1]) + ' Uhr', wd: n.wd };
return { state: 'open', text: 'Geöffnet bis ' + fmt(slots[i][1]) + '\u00a0Uhr', wd: n.wd };
}
}
if (slots.length > 1 && n.mins >= slots[0][1] && n.mins < slots[1][0]) {
return { state: 'break', text: 'Mittagspause · ab ' + fmt(slots[1][0]) + ' Uhr wieder erreichbar', wd: n.wd };
return { state: 'break', text: 'Mittagspause bis ' + fmt(slots[1][0]) + '\u00a0Uhr', wd: n.wd };
}
if (slots.length && n.mins < slots[0][0]) {
return { state: 'closed', text: 'Geschlossen · öffnet heute um ' + fmt(slots[0][0]) + ' Uhr', wd: n.wd };
return { state: 'closed', text: 'Öffnet heute um ' + fmt(slots[0][0]) + '\u00a0Uhr', wd: n.wd };
}
var base = Date.UTC(n.y, n.m - 1, n.d);
for (i = 1; i <= 14; i++) {
@@ -91,9 +91,8 @@
var wd = dt.getUTCDay();
var s = SCHEDULE[wd];
if (!s.length || holidayName(dt.getUTCFullYear(), dt.getUTCMonth() + 1, dt.getUTCDate())) continue;
var when = i === 1 ? 'morgen' : (i < 7 ? 'am ' + DAYS[wd] : 'am ' + dt.getUTCDate() + '.' + (dt.getUTCMonth() + 1) + '.');
var prefix = hol ? 'Heute Feiertag (' + hol + ')' : 'Geschlossen';
return { state: 'closed', text: prefix + ' · öffnet ' + when + ' um ' + fmt(s[0][0]) + ' Uhr', wd: n.wd };
var when = i === 1 ? 'morgen' : (i < 7 ? DAYS[wd] : 'am ' + dt.getUTCDate() + '.' + (dt.getUTCMonth() + 1) + '.');
return { state: 'closed', text: (hol ? 'Feiertag · öffnet ' : 'Öffnet ') + when + ' um ' + fmt(s[0][0]) + '\u00a0Uhr', wd: n.wd };
}
return { state: 'closed', text: 'Geschlossen', wd: n.wd };
}
@@ -136,7 +135,6 @@
Object.keys(fields).forEach(function (k) {
var el = fields[k];
if (!el) return;
el.setAttribute('aria-describedby', 'e-' + k);
el.addEventListener(el.type === 'checkbox' ? 'change' : 'input', function () {
if (el.closest('.field').classList.contains('is-invalid')) validateField(k);
});
@@ -150,6 +148,8 @@
else ok = el.value.trim().length > 0;
el.closest('.field').classList.toggle('is-invalid', !ok);
el.setAttribute('aria-invalid', ok ? 'false' : 'true');
// Fehlertext nur verknüpfen, solange er sichtbar ist
if (ok) el.removeAttribute('aria-describedby'); else el.setAttribute('aria-describedby', 'e-' + k);
return ok;
}
@@ -170,7 +170,7 @@
selectTopic(det.topic);
if (det.message && fields.message) {
fields.message.value = det.message;
fields.message.closest('.field').classList.remove('is-invalid');
if (fields.message.closest('.field').classList.contains('is-invalid')) validateField('message');
}
});
+15 -44
View File
@@ -16,7 +16,6 @@
var barEl = app.querySelector('[data-finder-bar]');
var backBtn = app.querySelector('[data-finder-back]');
var nextBtn = app.querySelector('[data-finder-next]');
var reduce = w.matchMedia('(prefers-reduced-motion: reduce)').matches;
var total = steps.length;
var index = 0;
var answers = {};
@@ -31,19 +30,19 @@
};
var SYSTEMS = {
wp: { name: 'Wärmepumpe', title: 'Luft-Wasser-Wärmepumpe', topic: 'Wärmepumpe & Regenerativ' },
pellet: { name: 'Pelletheizung', title: 'Pelletheizung', topic: 'Wärmepumpe & Regenerativ' },
hybrid: { name: 'Hybridheizung', title: 'Hybridheizung', topic: 'Heizung' }
wp: { name: 'Wärmepumpe', title: 'Luft-Wasser-Wärmepumpe', topic: 'Heizung & Wärmepumpe' },
pellet: { name: 'Pelletheizung', title: 'Pelletheizung', topic: 'Heizung & Wärmepumpe' },
hybrid: { name: 'Hybridheizung', title: 'Hybridheizung', topic: 'Heizung & Wärmepumpe' }
};
var TEXTS = {
wp: {
neubau: 'Für Ihren Neubau ist die Wärmepumpe die naheliegende Wahl: Sie heizt mit Umweltwärme, arbeitet mit niedrigen Vorlauftemperaturen besonders effizient und harmoniert perfekt mit einer Fußbodenheizung.',
gedaemmt: 'Ihr Haus bringt gute Voraussetzungen mit: Dank Dämmung reichen niedrige Vorlauftemperaturen – genau dort arbeitet eine Wärmepumpe besonders effizient. Ob einzelne Heizkörper getauscht werden sollten, prüfen wir vor Ort.',
neubau: 'Im Neubau die naheliegende Wahl: Die Wärmepumpe heizt mit Umweltwärme und arbeitet bei niedrigen Vorlauftemperaturen besonders effizient.',
gedaemmt: 'Dank guter Dämmung reichen niedrige Vorlauftemperaturen – ideal für eine Wärmepumpe. Ob einzelne Heizkörper getauscht werden sollten, prüfen wir vor Ort.',
unsaniert: 'Mit Flächenheizung kann eine Wärmepumpe auch im älteren Haus gut funktionieren. Eine Heizlastberechnung zeigt, ob vorher kleinere Maßnahmen sinnvoll sind.'
},
pellet: 'Holzpellets heizen nahezu CO<sub>2</sub>-neutral und liefern auch die hohen Vorlauftemperaturen, die klassische Heizkörper in älteren Häusern brauchen. Den nötigen Lagerraum haben Sie bereits.',
hybrid: 'Wärmepumpe und Brennwertkessel im Team: Die Wärmepumpe übernimmt den Großteil des Jahres, der Kessel springt nur an sehr kalten Tagen ein. Oft ist auch eine Hochtemperatur-Wärmepumpe möglich – das klären wir mit einer Heizlastberechnung.'
pellet: 'Holzpellets heizen nahezu CO<sub>2</sub>-neutral und schaffen die hohen Vorlauftemperaturen, die ältere Heizkörper brauchen. Den Lagerraum haben Sie bereits.',
hybrid: 'Die Wärmepumpe übernimmt den Großteil des Jahres, ein Brennwertkessel springt nur an sehr kalten Tagen ein. Oft reicht auch eine Hochtemperatur-Wärmepumpe.'
};
/* Bewertung – bewusst einfach und nachvollziehbar gehalten */
@@ -72,8 +71,8 @@
function addons(a, top) {
var list = [];
if (a.dach === 'ja') list.push(['i-sun', 'Solarthermie für Warmwasser']);
if (a.gebaeude === 'neubau') list.push(['i-wind', 'Wohnraumlüftung mit Wärmerückgewinnung']);
if (top === 'wp' && a.verteilung !== 'fbh') list.push(['i-gauge', 'Heizkörper-Check & hydraulischer Abgleich']);
if (a.gebaeude === 'neubau') list.push(['i-wind', 'Wohnraumlüftung']);
if (top === 'wp' && a.verteilung !== 'fbh') list.push(['i-gauge', 'Hydraulischer Abgleich']);
if (top === 'pellet') list.push(['i-tank', 'Pufferspeicher für mehr Effizienz']);
if (top === 'hybrid') list.push(['i-gauge', 'Heizlastberechnung vor Ort']);
list.push(['i-check', 'Beratung zu Fördermöglichkeiten']);
@@ -104,7 +103,6 @@
if (to === index) return;
var from = panel(index);
var target = panel(to);
var dir = to > index ? 1 : -1;
index = to;
if (to >= total) renderResult();
updateChrome();
@@ -114,25 +112,13 @@
from.classList.remove('is-active');
target.hidden = false;
target.classList.add('is-active');
if (to >= total) animateBars();
if (focus) {
var f = to >= total ? result : target.querySelector('input:checked') || target.querySelector('input');
if (f) f.focus({ preventScroll: true });
}
};
if (w.gsap && !reduce) {
w.gsap.to(from, {
x: -28 * dir, opacity: 0, duration: 0.28, ease: 'power2.in',
onComplete: function () {
w.gsap.set(from, { clearProps: 'transform,opacity' });
reveal();
w.gsap.fromTo(target, { x: 36 * dir, opacity: 0 }, { x: 0, opacity: 1, duration: 0.7, ease: 'expo.out', clearProps: 'transform,opacity' });
}
});
} else {
reveal();
}
reveal();
}
function renderResult() {
@@ -142,12 +128,10 @@
var text = r.top === 'wp' ? TEXTS.wp[answers.gebaeude] || TEXTS.wp.gedaemmt : TEXTS[r.top];
result.querySelector('[data-result-text]').innerHTML = text;
var bars = result.querySelector('[data-result-bars]');
bars.innerHTML = r.order.map(function (k, i) {
return '<li class="' + (i === 0 ? 'is-top' : '') + '"><span>' + SYSTEMS[k].name + '</span>' +
'<span class="bar" role="img" aria-label="' + SYSTEMS[k].name + ': ' + r.pct[k] + ' Prozent Übereinstimmung"><i data-v="' + (r.pct[k] / 100) + '"></i></span>' +
'<b>' + r.pct[k] + '&nbsp;%</b></li>';
}).join('');
// Keine Prozentwerte – nur sinnvolle Alternativen nennen
var alt = r.order.slice(1).filter(function (k) { return r.pct[k] >= 50; })
.map(function (k) { return SYSTEMS[k].name; });
result.querySelector('[data-result-bars]').innerHTML = alt.length ? '<li>Ebenfalls denkbar: ' + alt.join(' oder ') + '</li>' : '';
result.querySelector('[data-result-addons]').innerHTML = addons(answers, r.top).map(function (a) {
return '<li>' + icon(a[0]) + a[1] + '</li>';
@@ -163,19 +147,6 @@
apply.setAttribute('data-message', summary);
}
function animateBars() {
var bars = result.querySelectorAll('.bar i');
// Zwei Frames warten, damit die Transition sicher greift
w.requestAnimationFrame(function () {
w.requestAnimationFrame(function () {
Array.prototype.forEach.call(bars, function (b, i) {
b.style.transitionDelay = (0.12 * i) + 's';
b.style.transform = 'scaleX(' + b.getAttribute('data-v') + ')';
});
});
});
}
function restart() {
answers = {};
Array.prototype.forEach.call(form.querySelectorAll('input[type="radio"]'), function (r) { r.checked = false; });
@@ -202,7 +173,7 @@
updateChrome();
if (pointerSelect) {
w.clearTimeout(advanceTimer);
advanceTimer = w.setTimeout(function () { go(index + 1, false); }, 320);
advanceTimer = w.setTimeout(function () { go(index + 1, true); }, 320);
}
});
+10 -53
View File
@@ -1,7 +1,6 @@
/* ==========================================================================
Olzinger – Energie-Haus
Tabs + Hotspots steuern, welches System im Hausschnitt hervorgehoben wird.
Solange niemand klickt, wechseln die Komponenten automatisch.
========================================================================== */
(function (w, d) {
'use strict';
@@ -13,16 +12,9 @@
var parts = Array.prototype.slice.call(root.querySelectorAll('.part'));
var hotspots = Array.prototype.slice.call(root.querySelectorAll('.hotspot'));
var systems = Array.prototype.slice.call(root.querySelectorAll('.sys'));
var progress = root.querySelector('[data-house-progress]');
var order = tabs.map(function (t) { return t.getAttribute('data-part'); });
var reduce = w.matchMedia('(prefers-reduced-motion: reduce)').matches;
var DURATION = 6500;
var current = null;
var autoplay = !reduce;
var inView = false;
var hovering = false;
var elapsed = 0, last = 0, raf = 0;
hotspots.forEach(function (h) {
h.setAttribute('aria-controls', 'part-' + h.getAttribute('data-part'));
@@ -37,6 +29,11 @@
t.setAttribute('aria-selected', on ? 'true' : 'false');
t.tabIndex = on ? 0 : -1;
if (on && focusTab) t.focus();
// Auf dem Handy liegen die Tabs in einer Wischleiste: aktiven Tab sichtbar halten
if (on && t.parentNode.scrollWidth > t.parentNode.clientWidth) {
var bar = t.parentNode;
bar.scrollTo({ left: t.offsetLeft - (bar.clientWidth - t.offsetWidth) / 2, behavior: reduce ? 'auto' : 'smooth' });
}
});
parts.forEach(function (p) {
var on = p.getAttribute('data-part') === id;
@@ -49,41 +46,13 @@
h.setAttribute('aria-pressed', on ? 'true' : 'false');
});
systems.forEach(function (s) { s.classList.toggle('is-on', s.getAttribute('data-sys') === id); });
elapsed = 0;
}
function stopAutoplay() {
autoplay = false;
if (progress) progress.style.transform = 'scaleX(0)';
if (raf) w.cancelAnimationFrame(raf);
raf = 0;
}
function tick(now) {
raf = 0;
if (!autoplay || !inView || hovering) return;
var dt = last ? now - last : 16;
last = now;
elapsed += Math.min(dt, 100);
if (progress) progress.style.transform = 'scaleX(' + Math.min(elapsed / DURATION, 1) + ')';
if (elapsed >= DURATION) {
var next = order[(order.indexOf(current) + 1) % order.length];
activate(next, false);
}
raf = w.requestAnimationFrame(tick);
}
function run() {
if (!autoplay || !inView || hovering || raf) return;
last = 0;
raf = w.requestAnimationFrame(tick);
}
tabs.forEach(function (t) {
t.addEventListener('click', function () { stopAutoplay(); activate(t.getAttribute('data-part'), false); });
t.addEventListener('click', function () { activate(t.getAttribute('data-part'), false); });
});
hotspots.forEach(function (h) {
h.addEventListener('click', function () { stopAutoplay(); activate(h.getAttribute('data-part'), false); });
h.addEventListener('click', function () { activate(h.getAttribute('data-part'), false); });
});
// Tastatursteuerung nach WAI-ARIA Tabs-Pattern
@@ -97,30 +66,18 @@
else if (e.key === 'End') to = n - 1;
if (to < 0) return;
e.preventDefault();
stopAutoplay();
activate(order[to], true);
});
}
// Pausieren, solange der Nutzer mit der Maus im Bereich ist
root.addEventListener('pointerenter', function (e) {
if (e.pointerType === 'mouse') { hovering = true; if (raf) { w.cancelAnimationFrame(raf); raf = 0; } }
});
root.addEventListener('pointerleave', function (e) {
if (e.pointerType === 'mouse') { hovering = false; run(); }
});
// SVG-Animationen (Strömungen, Lüfter) nur laufen lassen, solange das Haus sichtbar ist
if ('IntersectionObserver' in w) {
new IntersectionObserver(function (entries) {
inView = entries[0].isIntersecting;
if (inView) run();
}, { threshold: 0.35 }).observe(root);
// SVG-Animationen (Strömungen, Lüfter) nur laufen lassen, solange das Haus sichtbar ist
new IntersectionObserver(function (entries) {
root.classList.toggle('is-inview', entries[0].isIntersecting);
}, { threshold: 0 }).observe(root);
} else {
root.classList.add('is-inview');
}
if (!('IntersectionObserver' in w)) root.classList.add('is-inview');
activate(order[0], false);
})(window, document);
+152 -456
View File
@@ -1,6 +1,7 @@
/* ==========================================================================
Olzinger – Hauptskript
Preloader, Hero-Intro, Smooth-Scroll, Navigation und Scroll-Animationen.
Navigation, mobiles Menü, dezente Einblendungen, Leistungs-Karussell (mobil)
und Kartenstapel (Desktop). Bewusst ohne Bibliotheken.
========================================================================== */
(function (w, d) {
'use strict';
@@ -10,497 +11,192 @@
var $$ = function (s, c) { return Array.prototype.slice.call((c || d).querySelectorAll(s)); };
var reduce = w.matchMedia('(prefers-reduced-motion: reduce)').matches;
var finePointer = w.matchMedia('(hover: hover) and (pointer: fine)').matches;
var fallback = !!w.__olzFallback;
var hasIO = 'IntersectionObserver' in w;
w.__olzReady = true;
/* ---------- Dynamische Jahreszahlen ---------- */
var nowYear = new Date().getFullYear();
$$('[data-current-year]').forEach(function (el) { el.textContent = nowYear; });
$$('[data-years]').forEach(function (el) {
el.textContent = nowYear - 1985;
el.setAttribute('data-count', nowYear - 1985);
});
$$('[data-years]').forEach(function (el) { el.textContent = nowYear - 1985; });
/* ---------- Wärmebild ---------- */
var thermal = null;
/* ---------- Wärmebild (ruhiger Hintergrund im Hero) ---------- */
var canvas = $('[data-thermal]');
if (canvas && w.OlzThermal) {
thermal = w.OlzThermal.init(canvas, {
var thermal = w.OlzThermal.init(canvas, {
hero: $('[data-hero]'),
probe: $('[data-probe]'),
probeVal: $('[data-probe-val]'),
legendMarker: $('[data-legend-marker]'),
calm: $('.hero__aside'),
reduceMotion: reduce
});
} else {
if (thermal) thermal.heatUp();
} else if (canvas) {
html.classList.add('no-webgl');
}
/* ---------- Mobiles Menü (auch ohne GSAP funktionsfähig) ---------- */
/* ---------- Mobiles Menü ---------- */
var menu = $('[data-menu]');
var toggle = $('[data-menu-toggle]');
var menuOpen = false;
var lenis = null;
if (menu) { menu.hidden = false; menu.setAttribute('inert', ''); menu.setAttribute('aria-hidden', 'true'); }
function openMenu() {
if (!menu || menuOpen) return;
menuOpen = true;
menu.classList.add('is-open');
menu.removeAttribute('inert');
menu.setAttribute('aria-hidden', 'false');
toggle.setAttribute('aria-expanded', 'true');
toggle.querySelector('.sr-only').textContent = 'Menü schließen';
html.classList.add('menu-open');
if (lenis) lenis.stop();
if (w.gsap && !reduce) {
w.gsap.fromTo($$('.menu__links a, .menu__foot > *'), { y: 40, opacity: 0 },
{ y: 0, opacity: 1, duration: 0.9, ease: 'expo.out', stagger: 0.045, delay: 0.25 });
}
}
function closeMenu() {
if (!menu || !menuOpen) return;
menuOpen = false;
menu.classList.remove('is-open');
menu.setAttribute('inert', '');
menu.setAttribute('aria-hidden', 'true');
toggle.setAttribute('aria-expanded', 'false');
toggle.querySelector('.sr-only').textContent = 'Menü öffnen';
html.classList.remove('menu-open');
if (lenis) lenis.start();
}
if (toggle) toggle.addEventListener('click', function () { if (menuOpen) closeMenu(); else openMenu(); });
d.addEventListener('keydown', function (e) { if (e.key === 'Escape' && menuOpen) { closeMenu(); toggle.focus(); } });
/* ---------- Ohne GSAP: alles statisch sichtbar lassen ---------- */
if (!w.gsap || !w.ScrollTrigger) {
html.classList.remove('has-loader', 'js');
html.classList.add('no-js');
if (thermal) thermal.heatUp();
d.addEventListener('click', function (e) {
var a = e.target.closest('a[href^="#"]');
if (a && a.dataset.topic) w.dispatchEvent(new CustomEvent('olz:topic', { detail: a.dataset.topic }));
if (a) closeMenu();
});
return;
}
var gsap = w.gsap, ScrollTrigger = w.ScrollTrigger;
gsap.registerPlugin(ScrollTrigger);
if (w.SplitText) gsap.registerPlugin(w.SplitText);
gsap.config({ nullTargetWarn: false });
/* ---------- Smooth Scroll ---------- */
if (!reduce && typeof w.Lenis === 'function') {
lenis = new w.Lenis({ lerp: 0.105, smoothWheel: true, wheelMultiplier: 1, touchMultiplier: 1.4 });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add(function (t) { lenis.raf(t * 1000); });
gsap.ticker.lagSmoothing(0);
if (html.classList.contains('has-loader')) lenis.stop();
}
w.olzLenis = lenis;
function navHeight() {
var bar = $('.nav__bar');
return bar ? bar.getBoundingClientRect().height + 12 : 76;
}
// Natürliche Position berechnen (auch für „sticky“-Elemente)
function targetY(el) {
if (el.id === 'top') return 0;
var stack = el.closest('[data-stack]');
if (stack && el.classList.contains('svc')) {
var cards = $$('.svc', stack);
var gap = parseFloat(getComputedStyle(stack).rowGap) || 0;
var y = stack.getBoundingClientRect().top + w.scrollY;
for (var i = 0; i < cards.length && cards[i] !== el; i++) y += cards[i].offsetHeight + gap;
var stickyTop = parseFloat(getComputedStyle(el).top);
return y - (isNaN(stickyTop) ? navHeight() : stickyTop);
}
return el.getBoundingClientRect().top + w.scrollY;
}
function scrollToEl(el) {
var y = targetY(el);
if (lenis) lenis.scrollTo(y, { duration: 1.4, easing: function (t) { return t === 1 ? 1 : 1 - Math.pow(2, -10 * t); } });
else w.scrollTo({ top: y, behavior: reduce ? 'auto' : 'smooth' });
function setMenu(open) {
if (!menu || !toggle || open === menuOpen) return;
menuOpen = open;
menu.classList.toggle('is-open', open);
if (open) menu.removeAttribute('inert'); else menu.setAttribute('inert', '');
menu.setAttribute('aria-hidden', open ? 'false' : 'true');
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
toggle.querySelector('.sr-only').textContent = open ? 'Menü schließen' : 'Menü öffnen';
html.classList.toggle('menu-open', open);
// Inhalt hinter dem Menü für Tastatur und Screenreader sperren
[d.getElementById('main'), $('.footer')].forEach(function (el) { if (el) el.inert = open; });
}
if (toggle) toggle.addEventListener('click', function () { setMenu(!menuOpen); });
d.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && menuOpen) { setMenu(false); toggle.focus(); }
});
// Wird das Fenster breit genug für die Desktop-Navigation (z. B. Tablet drehen), Menü schließen
var deskMQ = w.matchMedia('(min-width: 1181px)');
var onDesk = function (e) { if (e.matches) setMenu(false); };
if (deskMQ.addEventListener) deskMQ.addEventListener('change', onDesk); else if (deskMQ.addListener) deskMQ.addListener(onDesk);
/* ---------- Anker-Links: Menü schließen, Thema ans Formular geben ---------- */
d.addEventListener('click', function (e) {
var a = e.target.closest('a[href^="#"]');
var a = e.target.closest('a[href*="#"]');
if (!a) return;
var id = a.getAttribute('href');
if (id.length < 2) return;
var target = d.getElementById(id.slice(1));
if (!target) return;
e.preventDefault();
if (a.dataset.topic) w.dispatchEvent(new CustomEvent('olz:topic', { detail: a.dataset.topic }));
var wasOpen = menuOpen;
closeMenu();
setTimeout(function () { scrollToEl(target); }, wasOpen ? 380 : 0);
try { history.replaceState(null, '', id === '#top' ? location.pathname : id); } catch (err) { /* z. B. in Sandbox-Vorschauen */ }
if (menuOpen) setMenu(false);
});
/* ---------- Seitenwechsel mit Vorhang ---------- */
var curtain = d.createElement('div');
curtain.className = 'curtain';
curtain.setAttribute('aria-hidden', 'true');
d.body.appendChild(curtain);
d.addEventListener('click', function (e) {
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
var a = e.target.closest('a[href]');
if (!a || a.target === '_blank' || a.hasAttribute('download')) return;
var url;
try { url = new URL(a.getAttribute('href'), location.href); } catch (err) { return; }
if (!/^https?:$/.test(url.protocol) || url.origin !== location.origin) return;
if (url.pathname === location.pathname) return;
e.preventDefault();
closeMenu();
html.classList.add('is-leaving');
w.setTimeout(function () { location.href = url.href; }, reduce ? 0 : 560);
});
w.addEventListener('pageshow', function () { html.classList.remove('is-leaving'); });
/* ---------- Preloader & Hero-Intro ---------- */
function heroIn() {
var tl = gsap.timeline({ defaults: { ease: 'expo.out' } });
tl.fromTo('.hero__word', { y: 0, yPercent: 112 }, { yPercent: 0, duration: 1.5, stagger: 0.1 }, 0)
.fromTo('.hero__dot', { scale: 0 }, { scale: 1, duration: 1.1, ease: 'back.out(3)', stagger: 0.1 }, 0.55)
.fromTo('.hero__tag', { opacity: 0, x: -12 }, { opacity: 1, x: 0, duration: 1.2, stagger: 0.1 }, 0.7)
.fromTo('[data-hero-fade]', { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 1.3, stagger: 0.12 }, 0.35)
.fromTo('.nav__bar', { opacity: 0, y: -16 }, { opacity: 1, y: 0, duration: 1.2 }, 0.2)
.add(function () { if (thermal) thermal.measure(); });
return tl;
}
function intro() {
if (fallback) { gsap.set('.nav__bar', { opacity: 1 }); if (thermal) thermal.heatUp(); return; }
var loader = $('.loader');
var useLoader = html.classList.contains('has-loader') && loader && !reduce;
if (reduce) {
html.classList.remove('has-loader');
gsap.set('.hero__word', { yPercent: 0, y: 0 });
gsap.set('.hero__dot', { scale: 1 });
gsap.set('.hero__tag, [data-hero-fade], .nav__bar', { opacity: 1 });
if (thermal) thermal.heatUp();
return;
}
if (!useLoader) {
html.classList.remove('has-loader');
if (loader) loader.remove();
if (thermal) thermal.heatUp();
heroIn();
return;
}
var arc = $('.loader__arc', loader);
var yearEl = $('[data-loader-year]', loader);
var len = arc.getTotalLength();
var counter = { v: 1985 };
gsap.set(arc, { strokeDasharray: len, strokeDashoffset: len });
var tl = gsap.timeline();
tl.from('.loader__center', { opacity: 0, scale: 0.92, duration: 0.6, ease: 'power2.out' }, 0)
.from('.loader__caption', { opacity: 0, y: 10, duration: 0.6, stagger: 0.08 }, 0.1)
.to(arc, { strokeDashoffset: 0, duration: 1.35, ease: 'power2.inOut' }, 0.15)
.to(counter, {
v: nowYear, duration: 1.35, ease: 'power2.inOut',
onUpdate: function () { yearEl.textContent = Math.round(counter.v); }
}, 0.15)
.to('.loader__center, .loader__caption', { opacity: 0, y: -20, duration: 0.5, ease: 'power2.in', stagger: 0.03 }, 1.65)
.add(function () { if (thermal) thermal.heatUp(); }, 1.7)
.to(loader, { clipPath: 'inset(0% 0% 100% 0%)', duration: 1.05, ease: 'expo.inOut' }, 1.85)
.add(heroIn(), 2.25)
.add(function () {
html.classList.remove('has-loader');
loader.remove();
if (lenis) lenis.start();
ScrollTrigger.refresh();
}, 2.95);
}
/* ---------- Navigation ---------- */
/* ---------- Navigation: Hintergrund ab Scroll, beim Runterscrollen ausblenden ---------- */
var nav = $('[data-nav]');
ScrollTrigger.create({
start: 0, end: 'max',
onUpdate: function (self) {
var y = self.scroll();
if (nav) {
var lastY = w.scrollY, ticking = false;
var onScroll = function () {
ticking = false;
var y = w.scrollY;
nav.classList.toggle('is-scrolled', y > 40);
if (menuOpen) return;
if (y > 420 && self.direction === 1) nav.classList.add('is-hidden');
else if (self.direction === -1 || y <= 420) nav.classList.remove('is-hidden');
}
});
nav.addEventListener('focusin', function () { nav.classList.remove('is-hidden'); });
if (!menuOpen) {
if (y > 420 && y > lastY + 4) nav.classList.add('is-hidden');
else if (y < lastY - 4 || y <= 420) nav.classList.remove('is-hidden');
}
lastY = y;
};
w.addEventListener('scroll', function () {
if (!ticking) { ticking = true; w.requestAnimationFrame(onScroll); }
}, { passive: true });
nav.addEventListener('focusin', function () { nav.classList.remove('is-hidden'); });
onScroll();
}
// Aktiven Menüpunkt markieren
var navLinks = $$('[data-nav-link]');
navLinks.forEach(function (link) {
var sec = d.getElementById(link.getAttribute('href').slice(1));
if (!sec) return;
ScrollTrigger.create({
trigger: sec, start: 'top 45%', end: 'bottom 45%',
onToggle: function (self) {
if (self.isActive) navLinks.forEach(function (l) { l.classList.toggle('is-active', l === link); });
else link.classList.remove('is-active');
if (hasIO && navLinks.length) {
var linkFor = {};
navLinks.forEach(function (l) { linkFor[l.getAttribute('href').slice(1)] = l; });
var activeIO = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
var link = linkFor[en.target.id];
if (link) link.classList.toggle('is-active', en.isIntersecting);
});
}, { rootMargin: '-45% 0px -54% 0px' });
Object.keys(linkFor).forEach(function (id) {
var sec = d.getElementById(id);
if (sec) activeIO.observe(sec);
});
}
/* ---------- Dezente Einblendungen ---------- */
var reveals = $$('[data-reveal]');
if (hasIO && !reduce) {
var revealIO = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (!en.isIntersecting) return;
en.target.classList.add('is-in');
revealIO.unobserve(en.target);
});
}, { rootMargin: '0px 0px -8% 0px' });
reveals.forEach(function (el) { revealIO.observe(el); });
} else {
reveals.forEach(function (el) { el.classList.add('is-in'); });
}
/* ---------- Leistungen: Karussell (mobil) bzw. Kartenstapel (Desktop) ---------- */
var stack = $('[data-stack]');
var cards = $$('.svc');
var stackNav = $('[data-stack-nav]');
var stackMQ = w.matchMedia('(min-width: 900px) and (min-height: 660px)');
if (stack && stackNav) {
var prevBtn = $('[data-stack-prev]', stackNav);
var nextBtn = $('[data-stack-next]', stackNav);
var currentEl = $('[data-stack-current]', stackNav);
var current = 0;
var isCarousel = function () { return stack.scrollWidth > stack.clientWidth + 2; };
var cardLeft = function (c) { return c.offsetLeft - cards[0].offsetLeft; };
var cardIndex = function () {
var x = stack.scrollLeft, best = 0, bestDist = Infinity;
if (x >= stack.scrollWidth - stack.clientWidth - 2) return cards.length - 1;
cards.forEach(function (c, i) {
var dist = Math.abs(cardLeft(c) - x);
if (dist < bestDist) { bestDist = dist; best = i; }
});
return best;
};
var updateNav = function () {
stackNav.hidden = !isCarousel();
current = cardIndex();
currentEl.textContent = current + 1;
prevBtn.disabled = current === 0;
nextBtn.disabled = current === cards.length - 1;
};
// Schritt relativ zur echten Scrollposition (bei zwei Karten nebeneinander
// liegen die letzten Karten jenseits der maximalen Scrollweite)
var step = function (dir) {
var x = stack.scrollLeft, i;
if (dir > 0) { for (i = 0; i < cards.length - 1; i++) if (cardLeft(cards[i]) > x + 2) break; }
else { for (i = cards.length - 1; i > 0; i--) if (cardLeft(cards[i]) < x - 2) break; }
stack.scrollTo({ left: Math.min(cardLeft(cards[i]), stack.scrollWidth - stack.clientWidth), behavior: reduce ? 'auto' : 'smooth' });
};
var navTick = false;
stack.addEventListener('scroll', function () {
if (!navTick) { navTick = true; w.requestAnimationFrame(function () { navTick = false; updateNav(); }); }
}, { passive: true });
prevBtn.addEventListener('click', function () { step(-1); });
nextBtn.addEventListener('click', function () { step(1); });
w.addEventListener('resize', updateNav);
updateNav();
}
// Desktop: hintere Karten treten beim Überdecken leicht zurück
if (stack && !reduce) {
var inners = cards.map(function (c) { return $('.svc__inner', c); });
var stackTick = false;
var paintStack = function () {
stackTick = false;
if (!stackMQ.matches) {
inners.forEach(function (el) { el.style.transform = ''; el.style.removeProperty('--shade'); });
return;
}
});
});
/* ---------- Animationen, die Bewegung voraussetzen ---------- */
function motion() {
// Hero: Inhalte beim Scrollen leicht anheben und ausblenden
gsap.to('.hero__inner', {
yPercent: -10, opacity: 0.15, ease: 'none',
scrollTrigger: { trigger: '.hero', start: 'top top', end: 'bottom top', scrub: true }
});
// Überschriften: Zeilen-Reveal
if (w.SplitText) {
$$('[data-split]').forEach(function (el) {
w.SplitText.create(el, {
type: 'lines', mask: 'lines', linesClass: 'split-line', autoSplit: true,
onSplit: function (self) {
return gsap.from(self.lines, {
yPercent: 110, duration: 1.25, ease: 'expo.out', stagger: 0.09,
scrollTrigger: { trigger: el, start: 'top 86%', once: true }
});
}
});
});
// Intro-Text: Wort für Wort „aufheizen“
var introText = $('[data-scrub-words]');
if (introText) {
var split = w.SplitText.create(introText, { type: 'words', wordsClass: 'scrub-word' });
gsap.fromTo(split.words, { opacity: 0.13 }, {
opacity: 1, ease: 'none', stagger: 0.1,
scrollTrigger: { trigger: introText, start: 'top 82%', end: 'bottom 50%', scrub: true }
});
gsap.from('.ichip', {
scale: 0, rotate: -25, duration: 0.9, ease: 'back.out(2.4)', stagger: 0.18,
scrollTrigger: { trigger: introText, start: 'top 70%', once: true }
});
}
}
// Allgemeine Reveals
$$('[data-reveal]').forEach(function (el) {
gsap.from(el, {
y: 44, opacity: 0, duration: 1.25, ease: 'expo.out',
scrollTrigger: { trigger: el, start: 'top 90%', once: true }
});
});
$$('[data-reveal-stagger]').forEach(function (el) {
gsap.from(el.children, {
y: 40, opacity: 0, duration: 1.15, ease: 'expo.out', stagger: 0.09,
scrollTrigger: { trigger: el, start: 'top 90%', once: true }
});
});
// Vorlauf/Rücklauf-Bänder
$$('.tape').forEach(function (tape) {
var track = $('.tape__track', tape);
var unit = track.innerHTML;
var unitWidth = track.scrollWidth;
if (!unitWidth) return;
var copies = Math.max(2, Math.ceil((tape.offsetWidth * 1.2) / unitWidth) + 1);
track.innerHTML = new Array(copies + 1).join(unit);
var dir = parseFloat(tape.getAttribute('data-tape')) || 1;
var tween = gsap.fromTo(track,
{ x: dir > 0 ? 0 : -unitWidth },
{ x: dir > 0 ? -unitWidth : 0, duration: unitWidth / 70, ease: 'none', repeat: -1 });
tween.totalTime(tween.duration() * 500);
var boost = { v: 1 };
var tapeST = ScrollTrigger.create({
trigger: tape, start: 'top bottom', end: 'bottom top',
onToggle: function (self) { if (self.isActive) tween.resume(); else tween.pause(); },
onUpdate: function (self) {
var v = Math.min(Math.abs(self.getVelocity()) / 250, 7);
var sign = self.direction === 1 ? 1 : -1;
gsap.to(boost, {
v: (1 + v) * sign, duration: 0.25, overwrite: true,
onUpdate: function () { tween.timeScale(boost.v); }
});
gsap.to(boost, {
v: sign, duration: 1.2, delay: 0.25, ease: 'power2.out',
onUpdate: function () { tween.timeScale(boost.v); }
});
}
});
if (!tapeST.isActive) tween.pause();
gsap.fromTo(tape, { xPercent: dir * -4 }, {
xPercent: dir * 4, ease: 'none',
scrollTrigger: { trigger: '.tapes', start: 'top bottom', end: 'bottom top', scrub: true }
});
});
// Leistungskarten: gestapelt, die hinteren treten zurück
var cards = $$('.svc');
var mm = gsap.matchMedia();
mm.add('(min-width: 900px) and (min-height: 660px)', function () {
var vh = w.innerHeight;
cards.forEach(function (card, i) {
var next = cards[i + 1];
if (!next) return;
var inner = $('.svc__inner', card);
gsap.to(inner, {
scale: 0.9 + i * 0.012, '--shade': 0.55, ease: 'none',
scrollTrigger: {
trigger: next, start: 'top 82%',
end: function () { return 'top ' + (parseFloat(getComputedStyle(next).top) || 100) + 'px'; },
scrub: true, invalidateOnRefresh: true
}
});
var stickyTop = parseFloat(getComputedStyle(next).top) || 100;
var start = vh * 0.85;
var p = (start - next.getBoundingClientRect().top) / (start - stickyTop);
p = Math.max(0, Math.min(1, p));
inners[i].style.transform = p ? 'scale(' + (1 - p * (0.1 - i * 0.012)) + ')' : '';
inners[i].style.setProperty('--shade', (p * 0.5).toFixed(3));
});
});
// Illustrationen: leichter Parallax
$$('.svc__art .art').forEach(function (art) {
gsap.fromTo(art, { yPercent: 4 }, {
yPercent: -4, ease: 'none',
scrollTrigger: { trigger: art.closest('.svc'), start: 'top bottom', end: 'bottom top', scrub: true }
});
});
// Ablauf: Rohr füllt sich
var steps = $('[data-steps]');
if (steps) {
var fill = $('[data-steps-fill]', steps);
var pipe = $('.steps__pipe', steps);
var items = $$('[data-step-item]', steps);
var mmSteps = gsap.matchMedia();
mmSteps.add({ desktop: '(min-width: 901px)', mobile: '(max-width: 900px)' }, function (ctx) {
var desktop = ctx.conditions.desktop;
if (!desktop) {
var nodes = items.map(function (it) { return $('.step__node', it); });
var placePipe = function () {
var top0 = steps.getBoundingClientRect().top;
var a = nodes[0].getBoundingClientRect(), b = nodes[nodes.length - 1].getBoundingClientRect();
pipe.style.top = (a.top - top0 + a.height / 2) + 'px';
pipe.style.height = (b.top - a.top) + 'px';
pipe.style.bottom = 'auto';
};
placePipe();
ScrollTrigger.addEventListener('refreshInit', placePipe);
}
gsap.fromTo(fill, desktop ? { scaleX: 0, scaleY: 1 } : { scaleY: 0, scaleX: 1 }, {
scaleX: 1, scaleY: 1, ease: 'none',
scrollTrigger: {
trigger: steps, start: 'top 72%', end: desktop ? 'bottom 62%' : 'bottom 68%', scrub: 0.6,
onUpdate: function (self) {
var p = self.progress, n = items.length;
items.forEach(function (it, i) { it.classList.toggle('is-on', p > (i / (n - 1)) * 0.97 + 0.01); });
}
}
});
return function () {
pipe.style.top = pipe.style.height = pipe.style.bottom = '';
};
});
}
// Footer-Schriftzug „heizt auf“
var word = $('[data-footer-word] span');
if (word) {
gsap.fromTo(word, { '--heat': '0%' }, {
'--heat': '100%', ease: 'none',
scrollTrigger: { trigger: '.footer', start: 'top 80%', end: 'bottom bottom', scrub: true }
});
gsap.from(word, {
yPercent: 60, opacity: 0, duration: 1.4, ease: 'expo.out',
scrollTrigger: { trigger: '[data-footer-word]', start: 'top 95%', once: true }
});
}
// Magnetische Buttons
if (finePointer) {
$$('[data-magnetic]').forEach(function (btn) {
var xTo = gsap.quickTo(btn, 'x', { duration: 0.6, ease: 'power3' });
var yTo = gsap.quickTo(btn, 'y', { duration: 0.6, ease: 'power3' });
btn.addEventListener('pointermove', function (e) {
var r = btn.getBoundingClientRect();
xTo((e.clientX - r.left - r.width / 2) * 0.22);
yTo((e.clientY - r.top - r.height / 2) * 0.32);
});
btn.addEventListener('pointerleave', function () { xTo(0); yTo(0); });
});
}
}
/* ---------- Funktionen, die immer laufen (auch bei reduzierter Bewegung) ---------- */
function essentials() {
// Illustrationen nur animieren, wenn sichtbar
if ('IntersectionObserver' in w) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (en) { en.target.classList.toggle('is-inview', en.isIntersecting); });
}, { threshold: 0.15 });
$$('.svc').forEach(function (c) { io.observe(c); });
} else {
$$('.svc').forEach(function (c) { c.classList.add('is-inview'); });
}
// Zähler
$$('[data-count]').forEach(function (el) {
var target = parseInt(el.getAttribute('data-count') || el.textContent, 10);
if (isNaN(target)) return;
if (reduce) { el.textContent = target; return; }
var from = parseInt(el.getAttribute('data-count-from') || '0', 10);
var obj = { v: from };
el.textContent = from;
gsap.to(obj, {
v: target, duration: 2.2, ease: 'power3.out',
scrollTrigger: { trigger: el, start: 'top 92%', once: true },
onUpdate: function () { el.textContent = Math.round(obj.v); }
});
});
// Historie: Zählwerk mit Jahreszahl
var hist = $('[data-history]');
if (hist) {
var strips = $$('[data-odo-digit] .odo__strip', hist);
var caption = $('[data-odo-caption]', hist);
var entries = $$('.tl', hist);
var setYear = function (year, title) {
String(year).split('').forEach(function (ch, i) {
if (!strips[i]) return;
gsap.to(strips[i], { yPercent: -10 * parseInt(ch, 10), duration: reduce ? 0 : 1.1, ease: 'expo.inOut', delay: reduce ? 0 : i * 0.06, overwrite: true });
});
if (caption) caption.textContent = title;
};
entries.forEach(function (entry) {
var y = entry.getAttribute('data-year');
var year = y === 'now' ? nowYear : parseInt(y, 10);
var title = ($('.tl__title', entry) || {}).textContent || '';
ScrollTrigger.create({
trigger: entry, start: 'top 62%', end: 'bottom 62%',
onToggle: function (self) {
if (!self.isActive) return;
entries.forEach(function (e2) { e2.classList.toggle('is-active', e2 === entry); });
setYear(year, title);
}
});
});
entries[0].classList.add('is-active');
setYear(1985, ($('.tl__title', entries[0]) || {}).textContent || '');
}
}
/* ---------- Start ---------- */
essentials();
if (!reduce) motion();
intro();
// Nach dem Laden der Schriften Positionen neu berechnen
if (d.fonts && d.fonts.ready) d.fonts.ready.then(function () { ScrollTrigger.refresh(); });
w.addEventListener('load', function () { ScrollTrigger.refresh(); });
// Deep-Link (#kontakt etc.) nach dem Aufbau korrekt anspringen
if (location.hash && location.hash.length > 1) {
var deep = d.getElementById(location.hash.slice(1));
if (deep) w.setTimeout(function () { w.scrollTo(0, targetY(deep)); }, 60);
};
var requestPaint = function () {
if (!stackTick) { stackTick = true; w.requestAnimationFrame(paintStack); }
};
w.addEventListener('scroll', requestPaint, { passive: true });
w.addEventListener('resize', requestPaint);
paintStack();
}
})(window, document);
+16 -131
View File
@@ -1,15 +1,13 @@
/* ==========================================================================
Olzinger – „Wärmebild“ (WebGL)
Ein thermografisches Feld mit Isothermen. Die Maus wirkt als Wärmequelle
und hinterlässt eine Wärmespur, die langsam abkühlt und sich ausbreitet.
Hinter Fließtext wird die Wärme gedämpft, damit alles lesbar bleibt.
Ein ruhiges thermografisches Feld mit Isothermen als Hero-Hintergrund.
Eine langsam wandernde Wärmequelle hält das Bild lebendig – ganz ohne
Maus- oder Touch-Interaktion. Hinter Fließtext wird die Wärme gedämpft.
========================================================================== */
(function (w) {
'use strict';
var MAX_HEAT = 14;
var T_MIN = 14; // °C bei Feldwert 0
var T_MAX = 28; // °C bei Feldwert 1
var VERT = 'attribute vec2 aPos; void main(){ gl_Position = vec4(aPos, 0.0, 1.0); }';
@@ -87,32 +85,6 @@
'}'
].join('\n');
/* ---------- CPU-Gegenstück des Feldes (für die Temperaturanzeige) ---------- */
function fract(x) { return x - Math.floor(x); }
function smoothstep(a, b, x) { var t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); }
function hash(x, y) {
x = fract(x * 123.34); y = fract(y * 456.21);
var dd = x * (x + 45.32) + y * (y + 45.32);
x += dd; y += dd;
return fract(x * y);
}
function noise(x, y) {
var ix = Math.floor(x), iy = Math.floor(y), fx = x - ix, fy = y - iy;
var ux = fx * fx * (3 - 2 * fx), uy = fy * fy * (3 - 2 * fy);
var a = hash(ix, iy), b = hash(ix + 1, iy), c = hash(ix, iy + 1), d = hash(ix + 1, iy + 1);
var ab = a + (b - a) * ux, cd = c + (d - c) * ux;
return ab + (cd - ab) * uy;
}
function fbm(x, y) {
var v = 0, a = 0.5;
for (var i = 0; i < 5; i++) {
v += a * noise(x, y);
var nx = 1.6 * x - 1.2 * y, ny = 1.2 * x + 1.6 * y;
x = nx; y = ny; a *= 0.5;
}
return v;
}
function compile(gl, type, src) {
var sh = gl.createShader(type);
gl.shaderSource(sh, src);
@@ -161,8 +133,6 @@
var uHeat = gl.getUniformLocation(prog, 'uHeat');
var reduce = !!opts.reduceMotion;
var finePointer = w.matchMedia('(hover: hover) and (pointer: fine)').matches;
var probe = opts.probe, probeVal = opts.probeVal, marker = opts.legendMarker;
var calmEl = opts.calm || null;
var heatData = new Float32Array(MAX_HEAT * 4);
@@ -171,12 +141,11 @@
var quality = (w.devicePixelRatio || 1) > 1.5 ? 0.62 : 0.85;
var width = 0, height = 0, rect = null;
var running = false, visible = true, raf = 0;
var time = Math.random() * 40, last = 0, frame = 0;
var time = Math.random() * 40, last = 0;
var intro = reduce ? 1 : 0, introTarget = reduce ? 1 : 0;
var frameTimes = [], tuned = false;
// Cursor in Feldkoordinaten: x ∈ [0, Seitenverhältnis], y ∈ [0, 1] (y nach oben)
var cur = { x: 0, y: 0, tx: 0, ty: 0, inside: false, lastMove: -1e9, heat: 0, clientX: 0, clientY: 0 };
// Feldkoordinaten: x ∈ [0, Seitenverhältnis], y ∈ [0, 1] (y nach oben)
var auto = { heat: 0 };
var lastEmit = 0;
@@ -204,10 +173,7 @@
gl.viewport(0, 0, width, height);
}
measureCalm();
}
function toField(clientX, clientY) {
return { x: (clientX - rect.left) / rect.height, y: 1 - (clientY - rect.top) / rect.height };
if (reduce) start(); // Größenänderung leert die Leinwand – Standbild neu zeichnen
}
function emit(x, y, i, r) {
@@ -215,75 +181,6 @@
if (trail.length > MAX_HEAT - 2) trail.pop();
}
function onMove(e) {
if (e.pointerType && e.pointerType !== 'mouse') return;
rect = canvas.getBoundingClientRect();
var f = toField(e.clientX, e.clientY);
cur.tx = f.x; cur.ty = f.y;
cur.clientX = e.clientX - rect.left;
cur.clientY = e.clientY - rect.top;
if (!cur.inside) { cur.x = f.x; cur.y = f.y; }
cur.inside = true;
cur.lastMove = performance.now();
if (probe) probe.classList.add('is-on');
}
function onLeave() {
cur.inside = false;
if (probe) probe.classList.remove('is-on');
}
function onTouch(e) {
var t = e.touches && e.touches[0];
if (!t) return;
rect = canvas.getBoundingClientRect();
var f = toField(t.clientX, t.clientY);
emit(f.x, f.y, 0.85, 0.09);
}
if (finePointer) {
hero.addEventListener('pointermove', onMove, { passive: true });
hero.addEventListener('pointerleave', onLeave, { passive: true });
}
hero.addEventListener('touchstart', onTouch, { passive: true });
function calmAt(px, py) {
var a = smoothstep(calmRect[0] - 0.08, calmRect[0] + 0.02, px) * smoothstep(calmRect[1] - 0.08, calmRect[1] + 0.02, py);
var b = (1 - smoothstep(calmRect[2] - 0.02, calmRect[2] + 0.08, px)) * (1 - smoothstep(calmRect[3] - 0.02, calmRect[3] + 0.08, py));
return a * b;
}
// Exakt dieselbe Rechnung wie im Shader – nur für einen Punkt
function fieldAt(px, py) {
var asp = width / height, t = time;
var sx = px * 1.35, sy = py * 1.35;
var qx = fbm(sx, sy + t * 0.05), qy = fbm(sx + 5.2, sy - t * 0.04);
var rx = fbm(sx + 2.4 * qx + 1.7 + t * 0.06, sy + 2.4 * qy + 9.2 + t * 0.06);
var ry = fbm(sx + 2.4 * qx + 8.3 - t * 0.05, sy + 2.4 * qy + 2.8 - t * 0.05);
var f = fbm(sx + 2.2 * rx, sy + 2.2 * ry);
var base = smoothstep(0.18, 0.86, f) * 0.46;
var zx = px - asp * 0.8, zy = py - 0.76;
base += 0.36 * Math.exp(-(zx * zx + zy * zy) / 0.11) * (0.8 + 0.2 * Math.sin(t * 0.45));
var cx = px - asp * 0.16, cy = py - 0.12;
base -= 0.12 * Math.exp(-(cx * cx + cy * cy) / 0.2);
base *= 0.5 + 0.5 * smoothstep(0, 0.95, (px / asp) * 0.85 + py * 0.55);
var heat = 0;
for (var i = 0; i < MAX_HEAT; i++) {
var o = i * 4, dx = px - heatData[o], dy = py - heatData[o + 1];
heat += heatData[o + 2] * Math.exp(-(dx * dx + dy * dy) / Math.max(heatData[o + 3] * heatData[o + 3], 0.0001));
}
heat *= 1 - 0.62 * calmAt(px, py);
var v = Math.max(base, 0) + heat * 0.66;
v = 1 - Math.exp(-v * 1.35);
return v * intro;
}
function readTemperature() {
if (!probeVal || !cur.inside) return;
var v = Math.max(0, Math.min(1, fieldAt(cur.tx, cur.ty)));
var temp = T_MIN + v * (T_MAX - T_MIN);
probeVal.textContent = temp.toFixed(1).replace('.', ',') + ' °C';
if (marker) marker.style.left = (v * 100).toFixed(1) + '%';
}
function step(now) {
raf = 0;
if (!running) return;
@@ -304,22 +201,14 @@
intro += (introTarget - intro) * (1 - Math.exp(-dt * 1.8));
var asp = width / height;
// Maus
var k = 1 - Math.exp(-dt * 9);
cur.x += (cur.tx - cur.x) * k;
cur.y += (cur.ty - cur.y) * k;
var idle = (now - cur.lastMove) > 3200;
cur.heat += ((cur.inside ? 1 : 0) - cur.heat) * (1 - Math.exp(-dt * 3));
if (cur.inside && !idle && now - lastEmit > 55) {
emit(cur.x, cur.y, 0.42, 0.075);
lastEmit = now;
}
// Autopilot: wandernde Wärmequelle oben rechts, solange niemand interagiert
var autoOn = (!cur.inside || idle) && !reduce;
// Langsam wandernde Wärmequelle oben rechts
var autoOn = !reduce;
auto.heat += ((autoOn ? 0.8 : 0) - auto.heat) * (1 - Math.exp(-dt * 1.2));
var ax = asp * (0.7 + 0.16 * Math.sin(time * 0.21)) + 0.05 * Math.sin(time * 0.53);
var ay = 0.66 + 0.16 * Math.sin(time * 0.29 + 1.3);
// Hochkant (Handy): Wärme am rechten Rand halten, damit die Überschrift lesbar bleibt
var portrait = asp < 1;
var ax = portrait ? asp * (1.0 + 0.04 * Math.sin(time * 0.21)) + 0.03 * Math.sin(time * 0.53)
: asp * (0.7 + 0.16 * Math.sin(time * 0.21)) + 0.05 * Math.sin(time * 0.53);
var ay = portrait ? 0.62 + 0.12 * Math.sin(time * 0.29 + 1.3) : 0.66 + 0.16 * Math.sin(time * 0.29 + 1.3);
if (autoOn && now - lastEmit > 140) {
emit(ax, ay, 0.3 * auto.heat, 0.09);
lastEmit = now;
@@ -331,7 +220,7 @@
trail[i].r += dt * 0.032;
}
heatData[0] = cur.x; heatData[1] = cur.y; heatData[2] = cur.heat * 0.8; heatData[3] = 0.095;
heatData[0] = 0; heatData[1] = 0; heatData[2] = 0; heatData[3] = 0.1;
heatData[4] = ax; heatData[5] = ay; heatData[6] = auto.heat * 0.7; heatData[7] = 0.13;
for (var j = 0; j < MAX_HEAT - 2; j++) {
var o = (j + 2) * 4, tp = trail[j];
@@ -346,12 +235,8 @@
gl.uniform4fv(uHeat, heatData);
gl.drawArrays(gl.TRIANGLES, 0, 3);
frame++;
if (frame % 4 === 0) readTemperature();
if (probe && cur.inside) {
probe.style.transform = 'translate3d(' + cur.clientX.toFixed(1) + 'px,' + cur.clientY.toFixed(1) + 'px,0)';
}
// Reduzierte Bewegung: Standbild – einmal zeichnen, dann ruhen
if (reduce) { running = false; return; }
raf = w.requestAnimationFrame(step);
}