Maßgeschneiderte Website für Olzinger Haustechnik

Neuer statischer Webauftritt für die Olzinger GmbH (Heizung, Sanitär,
Lüftung, regenerative Energien – Ergolding bei Landshut, seit 1985).

- Hero mit live berechnetem WebGL-„Wärmebild“: Maus als Wärmequelle,
  Wärmespur, Isothermen und Temperaturanzeige; Preloader „Thermostat“
- Gestapelte Leistungskarten mit animierten SVG-Illustrationen
- Interaktives Energie-Haus (Wärmepumpe, Solarthermie, Speicher,
  Fußbodenheizung, Bad, Lüftung) mit Tabs, Hotspots und Autoplay
- Heizungs-Finder mit Empfehlung und Übernahme ins Kontaktformular
- Ablauf als sich füllende Rohrleitung, Zeitleiste mit Zählwerk,
  Live-Öffnungsstatus inkl. bayerischer Feiertage
- Kontaktformular mit PHP-Versand (Honeypot, Zeitfalle) und
  mailto-Fallback; Impressum, Datenschutz, 404-Seite
- Smooth-Scroll (Lenis) und GSAP-Animationen, alles lokal gehostet;
  reduzierte Bewegung, Betrieb ohne JavaScript, axe ohne Befunde
- SEO: Schema.org HVACBusiness, Open-Graph-Bild, Sitemap, robots.txt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kjd7UMh3HKyVziCgiZ9A2i
This commit is contained in:
Claude committed 2026-10-03 16:49:05 +00:00
commit 82dc9599cc
34 files changed
+5377

No files matched your search

+504
View File
@@ -0,0 +1,504 @@
/* ==========================================================================
Olzinger – Hauptskript
Preloader, Hero-Intro, Smooth-Scroll, Navigation und Scroll-Animationen.
========================================================================== */
(function (w, d) {
'use strict';
var html = d.documentElement;
var $ = function (s, c) { return (c || d).querySelector(s); };
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;
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);
});
/* ---------- Wärmebild ---------- */
var thermal = null;
var canvas = $('[data-thermal]');
if (canvas && w.OlzThermal) {
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 {
html.classList.add('no-webgl');
}
/* ---------- Mobiles Menü (auch ohne GSAP funktionsfähig) ---------- */
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' });
}
d.addEventListener('click', function (e) {
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);
if (history.replaceState) history.replaceState(null, '', id === '#top' ? location.pathname : id);
});
/* ---------- 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 ---------- */
var nav = $('[data-nav]');
ScrollTrigger.create({
start: 0, end: 'max',
onUpdate: function (self) {
var y = self.scroll();
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'); });
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');
}
});
});
/* ---------- 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 };
ScrollTrigger.create({
trigger: tape, start: 'top bottom', end: 'bottom top',
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); }
});
}
});
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 () {
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
}
});
});
});
// 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);
}
})(window, document);