Template
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:
1 parent
717a7de501
commit
ad155124b1
20 files changed
+1182
-2147
No files matched your search
+152
-456
@@ -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);
|
||||
Reference in new issue
Block a user