Files
Chickago-Boys/assets/js/main.js
T
rootandClaude Opus 5.5 e791d0312d Take-away: Befunde aus der Prüfung behoben
- Restaurant-Karte sitzt rechts über dem Pin (Zentrierung wurde vom Reveal überschrieben), Spitze zeigt auf den Pin, Überschrift bleibt frei
- Liefer-Karte rechts neben dem Zuhause-Symbol, der Weg kommt von oben aufs Haus statt hinter der Karte durch; keine Überschneidung mit dem Einleitungstext
- Karte blendet an allen Kanten weich aus (keine geraden Schnittkanten mehr), Straßenname Merkurstraße östlich vom Pin, damit der Weg ihn nicht überdeckt
- Weißer Weg-Schimmer wird mit aufgedeckt, Hahn kippt nicht mehr um und blendet am Anfang/Ende weich aus
- OpenStreetMap-Hinweis sichtbar in der Bühne, dunkler, nach den Karten in der Lesereihenfolge
- Untereinander-Ansicht schon bis 1000 px, Kartenausschnitt volle Breite; Neuaufbau nach Größenänderung verzögert

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:10:21 +00:00

691 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Chickago Boys – Interaktion & Animation */
(function () {
'use strict';
var root = document.documentElement;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var hasGsap = !!(window.gsap && window.ScrollTrigger);
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
if (!hasGsap) root.classList.remove('anim');
/* ---------- Jahr ---------- */
$$('[data-year]').forEach(function (el) { el.textContent = new Date().getFullYear(); });
/* ---------- Öffnungszeiten (täglich 12–22 Uhr, Zeitzone Berlin) ---------- */
var OPEN = 12 * 60, CLOSE = 22 * 60;
function berlinNow() {
var parts = new Intl.DateTimeFormat('en-US', {
timeZone: 'Europe/Berlin', weekday: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23'
}).formatToParts(new Date());
var map = {};
parts.forEach(function (p) { map[p.type] = p.value; });
var days = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
return { day: days[map.weekday], minutes: parseInt(map.hour, 10) * 60 + parseInt(map.minute, 10) };
}
function updateOpenState() {
var now = berlinNow();
var isOpen = now.minutes >= OPEN && now.minutes < CLOSE;
var text = isOpen ? 'Jetzt geöffnet · bis 22 Uhr'
: now.minutes < OPEN ? 'Geschlossen · öffnet heute um 12 Uhr'
: 'Geschlossen · öffnet morgen um 12 Uhr';
$$('[data-open-badge]').forEach(function (el) {
el.textContent = text;
el.classList.toggle('is-open', isOpen);
el.classList.toggle('is-closed', !isOpen);
});
$$('[data-hours] tr').forEach(function (tr) {
tr.classList.toggle('is-today', parseInt(tr.getAttribute('data-day'), 10) === now.day);
});
}
updateOpenState();
setInterval(updateOpenState, 60000);
/* ---------- Smooth Scroll (Lenis) ---------- */
var lenis = null;
if (!reduce && window.Lenis) {
lenis = new window.Lenis({ duration: 1.1, smoothWheel: true });
if (hasGsap) {
lenis.on('scroll', window.ScrollTrigger.update);
window.gsap.ticker.add(function (t) { lenis.raf(t * 1000); });
window.gsap.ticker.lagSmoothing(0);
} else {
var raf = function (t) { lenis.raf(t); requestAnimationFrame(raf); };
requestAnimationFrame(raf);
}
}
function navOffset() { return -(($('#nav') || {}).offsetHeight || 0) - 12; }
function scrollToTarget(target) {
if (lenis) lenis.scrollTo(target, { offset: navOffset(), duration: 1.1 });
else target.scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'start' });
}
document.addEventListener('click', function (e) {
var a = e.target.closest && e.target.closest('a[href*="#"]');
if (!a) return;
var url = new URL(a.href, location.href);
if (url.pathname !== location.pathname || !url.hash) return;
var target = url.hash === '#top' ? document.body : document.getElementById(decodeURIComponent(url.hash.slice(1)));
if (!target) return;
e.preventDefault();
closeMenu(false);
if (url.hash === '#top') { lenis ? lenis.scrollTo(0) : window.scrollTo({ top: 0, behavior: reduce ? 'auto' : 'smooth' }); }
else scrollToTarget(target);
history.replaceState(null, '', url.hash);
});
/* ---------- Navigation ein-/ausblenden ---------- */
var nav = $('#nav');
var lastY = window.scrollY;
function onScroll() {
var y = window.scrollY;
if (!nav) return;
if (document.body.classList.contains('menu-open')) { lastY = y; return; }
if (y > 140 && y - lastY > 6) nav.classList.add('is-hidden');
else if (lastY - y > 6 || y < 140) nav.classList.remove('is-hidden');
document.body.classList.toggle('nav-visible', !nav.classList.contains('is-hidden') && y > 10);
nav.classList.toggle('is-scrolled', y > 60);
lastY = y;
}
window.addEventListener('scroll', onScroll, { passive: true });
/* ---------- Menü ---------- */
var btn = $('#menu-btn'), panel = $('#menu-panel'), overlay = $('#menu-overlay');
function setLabel(txt) { $$('.menu-label', btn).forEach(function (s) { s.textContent = txt; }); }
function openMenu() {
document.body.classList.add('menu-open');
nav.classList.add('is-open');
btn.setAttribute('aria-expanded', 'true');
setLabel('Schließen');
if (lenis) lenis.stop();
if (hasGsap && !reduce) {
window.gsap.fromTo($$('li', panel), { y: 18, opacity: 0 }, { y: 0, opacity: 1, duration: .38, stagger: .06, ease: 'power3.out', delay: .14 });
}
var first = $('a', panel);
if (first) setTimeout(function () { first.focus({ preventScroll: true }); }, 60);
}
function closeMenu(returnFocus) {
if (!document.body.classList.contains('menu-open')) return;
document.body.classList.remove('menu-open');
nav.classList.remove('is-open');
btn.setAttribute('aria-expanded', 'false');
setLabel('Menü');
if (lenis) lenis.start();
if (returnFocus !== false) btn.focus({ preventScroll: true });
}
if (btn && panel) {
btn.addEventListener('click', function () {
document.body.classList.contains('menu-open') ? closeMenu() : openMenu();
});
overlay && overlay.addEventListener('click', function () { closeMenu(); });
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeMenu(); });
$$('a', panel).forEach(function (a) { a.addEventListener('click', function () { closeMenu(false); }); });
}
/* ---------- Navigation auf dunklen Flächen hell einfärben ---------- */
var darkEls = $$('[data-nav-dark]');
if (nav && darkEls.length) {
var checkDark = function () {
var probe = (nav.offsetHeight || 70) / 2;
nav.classList.toggle('is-dark', darkEls.some(function (el) {
var r = el.getBoundingClientRect(); return r.top <= probe && r.bottom >= probe;
}));
};
window.addEventListener('scroll', checkDark, { passive: true });
window.addEventListener('resize', checkDark);
checkDark();
}
/* ---------- Mobile Bestell-Leiste ---------- */
var orderbar = $('#orderbar');
var heroEl = $('.hero__cta .btn') || $('.page-hero');
if (orderbar && heroEl && 'IntersectionObserver' in window) {
new IntersectionObserver(function (entries) {
orderbar.classList.toggle('is-visible', !entries[0].isIntersecting);
}, { threshold: 0 }).observe(heroEl);
}
/* ---------- Speisekarte: Kategorien anheften ---------- */
var catNav = $('.cat-nav');
if (catNav) {
var stuck = false;
var checkStuck = function () {
var navH = document.body.classList.contains('nav-visible') ? ((nav && nav.offsetHeight) || 0) : 0;
var top = catNav.getBoundingClientRect().top;
var shouldStick = top < navH;
if (shouldStick === stuck) return;
stuck = shouldStick;
if (stuck) catNav.style.height = catNav.offsetHeight + 'px';
else catNav.style.height = '';
catNav.classList.toggle('is-stuck', stuck);
};
window.addEventListener('scroll', checkStuck, { passive: true });
window.addEventListener('resize', function () {
if (stuck) { stuck = false; catNav.classList.remove('is-stuck'); catNav.style.height = ''; }
checkStuck();
});
checkStuck();
}
/* ---------- Speisekarte: aktive Kategorie ---------- */
if (catNav && 'IntersectionObserver' in window) {
var links = $$('a', catNav);
var list = $('ul', catNav);
var setActive = function (id) {
links.forEach(function (l) {
var on = l.getAttribute('href') === '#' + id;
l.classList.toggle('is-active', on);
if (on) {
l.setAttribute('aria-current', 'true');
list.scrollTo({ left: l.offsetLeft - list.clientWidth / 2 + l.offsetWidth / 2, behavior: reduce ? 'auto' : 'smooth' });
} else l.removeAttribute('aria-current');
});
};
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (en) { if (en.isIntersecting) setActive(en.target.id); });
}, { rootMargin: '-35% 0px -60% 0px' });
var cats = $$('.menu-cat');
cats.forEach(function (s) { io.observe(s); });
window.addEventListener('scroll', function () {
if (cats[0] && cats[0].getBoundingClientRect().top > window.innerHeight * .4) {
links.forEach(function (l) { l.classList.remove('is-active'); l.removeAttribute('aria-current'); });
}
}, { passive: true });
}
/* ---------- Zutaten rund ums Essen (Explosion beim Scrollen) ----------
Jede Zutat: [Datei, x %, y % (relativ zur Essens-Box), Größe % der Box, Drehung, Ebene, auch mobil, gespiegelt]
Ebenen: b = hinter dem Essen (leicht unscharf), f = davor, n = ganz nah an der Kamera (groß, unscharf) */
var BITS = 'assets/img/bits/';
/* Natürliche Streuung statt fester Positionen (fester Seed, damit es bei jedem Laden gleich aussieht).
- food: Umriss des Essens im Bild [Mitte x, Mitte y, Halbbreite, Halbhöhe] in % der Box (abgerundetes Rechteck)
- Die Zutaten fliegen in 2–3 Schüben aus dem Essen heraus: dicht am Rand, nach außen immer weniger
- Teile vor dem Essen berühren es höchstens am Rand, Teile dahinter schauen nur halb hervor
- left/right/top/bottom: erlaubter Bereich (% der Box), z. B. damit nichts über Text oder aus der Karte ragt
items: [Datei, Grundgröße in % der Box-Breite, auch mobil] */
var SCATTER = {
burger: [
['lettuce-3', 70, -37, 21, 24, 'b', 0, 0],
['onion-2', 27, -43, 13, 30, 'b', 0, 0],
['lettuce-2', 47, -27, 26, 12, 'f', 1, 0],
['onion-1', 14, -21, 24, -15, 'f', 1, 0],
['tomato', 81, 6, 27, 20, 'f', 1, 0],
['lettuce-1', -9, 19, 25, -30, 'f', 1, 0],
['lettuce-1', 94, 38, 27, 42, 'f', 1, 1],
['tomato', -20, 50, 20, -34, 'f', 0, 1],
['onionring', 103, 62, 23, 15, 'f', 0, 0],
['bacon', -31, 68, 26, -12, 'f', 0, 1],
['bacon', 110, 84, 24, 12, 'f', 0, 0],
['jalapeno-1', 6, 107, 16, 20, 'f', 0, 0],
['pickle', 73, 101, 16, -14, 'f', 0, 0],
['sesame-1', 33, -6, 6.5, 25, 'f', 1, 0],
['sesame-1', 39, -15, 6, -35, 'f', 0, 1],
['sesame-2', 62, -17, 8, -20, 'f', 0, 0],
['sesame-1', 58, -5, 5.5, 10, 'f', 1, 1]
],
'burger-visit': { seed: 8, food: [38, 49, 37, 47], hw: .63, reach: 26, bursts: 2, behind: .3, top: 6, bottom: 112, left: -12, items: [
['lettuce-2', 20, 1], ['tomato', 16, 1], ['bacon', 22, 1], ['onion-1', 14, 1], ['jalapeno-1', 11, 1], ['lettuce-1', 20, 0],
['pickle', 10, 0], ['tomato', 12, 0], ['sesame-1', 3.4, 0], ['sesame-2', 3.8, 0]
] },
'burger-page': { seed: 6, food: [38, 49, 37, 47], hw: .63, reach: 30, bursts: 3, behind: .3, top: -34, bottom: 112, items: [
['lettuce-2', 21, 1], ['tomato', 16, 1], ['bacon', 22, 1], ['onion-1', 14, 1], ['jalapeno-1', 11, 1], ['lettuce-1', 22, 0],
['pickle', 10, 0], ['tomato', 12, 0], ['jalapeno-2', 10, 0], ['sesame-1', 3.4, 0], ['sesame-2', 3.8, 0], ['sesame-1', 3, 0]
] },
'burger-sm': { seed: 5, fly: .4, food: [39, 49, 38, 47], hw: .72, reach: 10, bursts: 2, behind: .5, top: 8, bottom: 96, left: 5, right: 95, items: [
['lettuce-1', 17, 1], ['tomato', 13, 1], ['bacon', 17, 1], ['onion-2', 11, 0], ['sesame-1', 2.6, 0]
] },
'chicken-sm': { seed: 7, fly: .4, food: [41, 48, 40, 47], hw: .73, reach: 10, bursts: 2, behind: .5, top: 8, bottom: 96, left: 5, right: 95, items: [
['bacon', 17, 1], ['lettuce-2', 17, 1], ['tomato', 13, 1], ['crumb-1', 7, 0], ['crumb-3', 6, 0]
] },
'wrap-sm': { seed: 3, fly: .4, food: [43, 48, 42, 47], hw: .67, reach: 10, bursts: 2, behind: .5, top: 8, bottom: 96, left: 5, right: 95, items: [
['lettuce-3', 17, 1], ['tomato', 13, 1], ['jalapeno-2', 11, 1], ['onion-2', 11, 0]
] },
wings: { seed: 21, food: [50, 50, 49, 49], hw: .71, reach: 26, bursts: 3, behind: .3, top: 2, bottom: 116, items: [
['crumb-1', 10, 1], ['chili', 17, 1], ['crumb-3', 9, 1], ['jalapeno-1', 10, 1], ['crumb-6', 9, 1], ['crumb-8', 9, 1],
['crumb-4', 8, 0], ['jalapeno-2', 9, 0], ['crumb-7', 7, 0], ['crumb-2', 6, 0], ['crumb-5', 6, 0], ['crumb-2', 4.5, 0]
] },
tenders: { seed: 12, food: [47, 42, 46, 41], hw: .59, reach: 26, bursts: 3, behind: .3, top: -34, bottom: 124, left: 0, items: [
['crumb-6', 9, 1], ['chili', 16, 1], ['crumb-1', 9, 1], ['jalapeno-1', 9, 1], ['crumb-3', 8, 1],
['crumb-8', 8, 0], ['crumb-4', 7, 0], ['crumb-7', 6, 0], ['crumb-5', 5, 0]
] }
};
function seeded(a) {
return function () {
a = a + 0x6D2B79F5 | 0;
var t = Math.imul(a ^ a >>> 15, 1 | a);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
function scatter(spec) {
var r = seeded(spec.seed), placed = [], out = [];
var gauss = function () { return (r() + r() + r() - 1.5) / 1.5; };
var f = spec.food, hw = spec.hw;
// Rechnen in "Box-Breite"-Einheiten, damit Abstände in x und y vergleichbar sind
var cx = f[0], cy = f[1] * hw, rx = f[2], ry = f[3] * hw;
var bursts = [];
for (var j = 0; j < spec.bursts; j++) bursts.push(r() * Math.PI * 2);
// Abstand Mitte -> Rand des Essens in Richtung a (abgerundetes Rechteck)
var edgeAt = function (a) {
var c = Math.abs(Math.cos(a)), sn = Math.abs(Math.sin(a));
var k = 1 / Math.pow(Math.pow(c, 3) + Math.pow(sn, 3), 1 / 3);
return { x: rx * k * Math.cos(a), y: ry * k * Math.sin(a) };
};
// Abstand eines Punktes zum Umriss (negativ = innen)
var outside = function (x, y) {
var dx = x - cx, dy = y - cy, a = Math.atan2(dy / ry, dx / rx);
var e = edgeAt(a), le = Math.sqrt(e.x * e.x + e.y * e.y), lp = Math.sqrt(dx * dx + dy * dy);
return lp - le;
};
spec.items.forEach(function (it) {
for (var k = 0; k < 240; k++) {
var a = r() < .75 ? bursts[Math.floor(r() * bursts.length)] + gauss() * .7 : r() * Math.PI * 2;
var size = it[1] * (.75 + r() * .5);
var behind = r() < spec.behind && size > 5;
var e = edgeAt(a), le = Math.sqrt(e.x * e.x + e.y * e.y);
// hinter dem Essen: halb versteckt; davor: frei, dicht am Rand häufiger als weit außen
var gap = behind ? size * (r() * .25 - .1) : size * .38 + Math.pow(r(), 1.7) * spec.reach;
var x = cx + e.x / le * (le + gap), y = cy + e.y / le * (le + gap);
var yp = y / hw;
// hohe Teile (Salatblatt) ragen weiter nach oben/unten: halbe Höhe großzügig mit 0,75 × Größe ansetzen
if ((y - size * .75) / hw < spec.top || (y + size * .75) / hw > spec.bottom || x - size / 2 < (spec.left != null ? spec.left : -70) || x + size / 2 > (spec.right != null ? spec.right : 170)) continue;
if (!behind && outside(x, y) < size * .3) continue;
var free = placed.every(function (p) {
var dx = p.x - x, dy = p.y - y;
return Math.sqrt(dx * dx + dy * dy) > (p.s + size) * .48;
});
if (!free) continue;
placed.push({ x: x, y: y, s: size });
// weiter außen etwas kleiner
out.push([it[0], +x.toFixed(1), +yp.toFixed(1), +size.toFixed(1), Math.round(gauss() * 70), behind ? 'b' : 'f', it[2], r() < .5 ? 1 : 0]);
return;
}
});
return out;
}
var mobileBits = window.matchMedia('(max-width: 820px)').matches;
var bitScale = mobileBits ? 1.35 : 1;
var blasts = $$('[data-blast]').map(function (box) {
var spec = SCATTER[box.getAttribute('data-blast')];
if (!spec) return null;
// Array = von Hand gesetzte Komposition, Objekt = berechnete Streuung
var set = Array.isArray(spec) ? spec : scatter(spec);
var lazy = box.getAttribute('data-blast-mode') !== 'hero';
var holder = document.createElement('span');
holder.className = 'blast__bits';
holder.setAttribute('aria-hidden', 'true');
var bits = [];
set.forEach(function (b) {
if (mobileBits && !b[6]) return;
// Verschachtelt, damit sich nichts in die Quere kommt:
// .bit (Scroll-Explosion) > .bit__c (CSS-Zentrierung) > .bit__in (Intro) > .bit__fl (Schweben) > img (feste Drehung)
// Hero: Handy etwas weniger vergrößert (1,2 statt 1,35), damit die Fontäne den Burger nur berührt und nicht überdeckt
var size = b[3] * (mobileBits && !lazy ? 1.2 : bitScale);
var el = document.createElement('span');
el.className = 'bit bit--' + b[5];
el.style.left = b[1] + '%'; el.style.top = b[2] + '%'; el.style.width = size + '%';
var c = document.createElement('span'); c.className = 'bit__c';
var inner = document.createElement('span'); inner.className = 'bit__in';
var fl = document.createElement('span'); fl.className = 'bit__fl';
var img = document.createElement('img');
img.alt = ''; img.decoding = 'async'; img.draggable = false;
if (lazy) img.loading = 'lazy';
img.style.rotate = b[4] + 'deg';
if (b[7]) img.style.scale = '-1 1';
img.onerror = function () { el.remove(); };
// Größe als Fläche: lange, schmale Zutaten (Salatblatt, Bacon) wirken so gleich groß wie runde
img.onload = function () {
var ratio = img.naturalWidth / img.naturalHeight;
el.style.width = (size * Math.sqrt(ratio)).toFixed(2) + '%';
};
img.src = BITS + b[0] + '.webp';
fl.appendChild(img); inner.appendChild(fl); c.appendChild(inner); el.appendChild(c); holder.appendChild(el);
bits.push({ el: el, inner: inner, fl: fl, x: b[1], y: b[2], layer: b[5] });
});
box.appendChild(holder);
return { box: box, bits: bits, hero: !lazy, fly: spec.fly || 1 };
}).filter(Boolean);
/* ---------- Flammen nur animieren, solange sie sichtbar sind ---------- */
if ('IntersectionObserver' in window) {
var flameIO = new IntersectionObserver(function (entries) {
entries.forEach(function (en) { en.target.classList.toggle('is-live', en.isIntersecting); });
});
$$('.flames, .cta__fire, .fx__sparks').forEach(function (el) { flameIO.observe(el); });
} else {
$$('.flames, .cta__fire, .fx__sparks').forEach(function (el) { el.classList.add('is-live'); });
}
/* ---------- Funken (Buckets) ---------- */
if (!reduce) {
$$('[data-sparks]').forEach(function (wrap) {
for (var i = 0; i < 22; i++) {
var sp = document.createElement('i');
sp.style.left = (Math.random() * 100).toFixed(1) + '%';
sp.style.setProperty('--sz', (2 + Math.random() * 3.5).toFixed(1) + 'px');
sp.style.setProperty('--d', (4 + Math.random() * 5).toFixed(2) + 's');
sp.style.setProperty('--dl', (-Math.random() * 9).toFixed(2) + 's');
sp.style.setProperty('--dx', ((Math.random() - .5) * 140).toFixed(0) + 'px');
sp.style.setProperty('--h', (300 + Math.random() * 600).toFixed(0) + 'px');
wrap.appendChild(sp);
}
});
}
/* ---------- 3D-Neigung der Karten (nur Maus) ---------- */
if (!reduce && window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
$$('[data-tilt]').forEach(function (card) {
var r = null;
card.addEventListener('pointerenter', function () { r = card.getBoundingClientRect(); });
card.addEventListener('pointermove', function (e) {
if (!r) r = card.getBoundingClientRect();
var nx = (e.clientX - r.left) / r.width - .5, ny = (e.clientY - r.top) / r.height - .5;
card.classList.add('is-tilting');
card.style.setProperty('--ty', (nx * 14).toFixed(2) + 'deg');
card.style.setProperty('--tx', (-ny * 12).toFixed(2) + 'deg');
});
card.addEventListener('pointerleave', function () {
r = null;
card.classList.remove('is-tilting');
card.style.setProperty('--tx', '0deg'); card.style.setProperty('--ty', '0deg');
});
});
}
/* ---------- Lieferweg (Take away): vom Pin die Merkurstraße entlang, dann zum Zuhause ---------- */
// Merkurstraße ab dem Restaurant nach Westen, in % der Karte (aus OpenStreetMap, siehe map-kaiserslautern.svg)
var STREET = [[40, 62.04], [39.64, 60.93], [35.86, 62.41], [32.14, 63.33], [24.29, 65.37], [18.79, 66.85], [10.07, 68.98]];
var route = $('[data-delivery]');
var routeState = null;
function buildRoute() {
if (!route) return null;
var map = $('.tmap', route), svg = $('.troute', route);
var home = $('.tspot--home', route);
if (!map || window.getComputedStyle(map).display === 'none') return null;
var mx = map.offsetLeft, my = map.offsetTop, mw = map.offsetWidth, mh = map.offsetHeight;
var pts = STREET.map(function (p) { return [mx + p[0] / 100 * mw, my + p[1] / 100 * mh]; });
// Ziel: Oberkante des Zuhause-Symbols; die Liefer-Karte sitzt rechts daneben, der Weg kommt von oben
var end = [home.offsetLeft, home.offsetTop - 46], last = pts[pts.length - 1];
var d = 'M' + pts.map(function (p) { return p[0].toFixed(1) + ' ' + p[1].toFixed(1); }).join(' L');
// Bogen nach links über der Liefer-Karte, dann von oben auf das Zuhause
var card = $('.tcard', home), cardTop = home.offsetTop + card.offsetTop;
var c1y = Math.min(last[1] + 20, cardTop - 60);
d += ' C' + (last[0] - (last[0] - end[0]) * .5).toFixed(1) + ' ' + c1y.toFixed(1) + ' ' +
end[0].toFixed(1) + ' ' + Math.min(c1y, end[1] - 120).toFixed(1) + ' ' + end[0].toFixed(1) + ' ' + end[1].toFixed(1);
$$('path', svg).forEach(function (p) { p.setAttribute('d', d); });
var line = $('.troute__line', svg), reveal = $('.troute__reveal', svg);
var len = line.getTotalLength();
reveal.style.strokeDasharray = len + ' ' + len;
return { path: line, reveal: reveal, len: len, rider: $('.troute__rider', route) };
}
function placeRider(progress) {
if (!routeState) return;
routeState.reveal.style.strokeDashoffset = routeState.len * (1 - progress);
var at = Math.max(.02, Math.min(.98, progress));
var pt = routeState.path.getPointAtLength(routeState.len * at);
var ahead = routeState.path.getPointAtLength(Math.min(routeState.len, routeState.len * at + 4));
var angle = Math.atan2(ahead.y - pt.y, ahead.x - pt.x) * 180 / Math.PI;
if (angle < 0) angle += 360; // nach links = 180°, ohne Sprung bei ±180°
var tilt = Math.max(-25, Math.min(25, (angle - 180) * -.25));
var w = routeState.rider.offsetWidth, h = routeState.rider.offsetHeight;
// Hahn schaut in Fahrtrichtung (nach links), am Start und kurz vor dem Ziel blendet er weich ein und aus
routeState.rider.style.transform = 'translate(' + (pt.x - w / 2) + 'px,' + (pt.y - h * .85) + 'px) scaleX(-1) rotate(' + tilt + 'deg)';
routeState.rider.style.opacity = Math.max(0, Math.min(1, (progress - .03) / .06, (.94 - progress) / .06));
routeState.rider.style.visibility = 'visible';
}
/* ---------- Ohne GSAP: fertig ---------- */
if (!hasGsap) {
var rebuildStatic = function () { routeState = buildRoute(); placeRider(1); if (routeState) routeState.rider.style.visibility = 'hidden'; };
rebuildStatic();
var rt; window.addEventListener('resize', function () { clearTimeout(rt); rt = setTimeout(rebuildStatic, 250); });
return;
}
var gsap = window.gsap, ST = window.ScrollTrigger;
gsap.registerPlugin(ST);
var rand = gsap.utils.random;
function splitWords(el) {
(function walk(node) {
Array.prototype.slice.call(node.childNodes).forEach(function (ch) {
if (ch.nodeType === 3) {
var frag = document.createDocumentFragment();
ch.textContent.split(/(\s+)/).forEach(function (p) {
if (!p) return;
if (/^\s+$/.test(p)) frag.appendChild(document.createTextNode(p));
else { var s = document.createElement('span'); s.className = 'pw'; s.textContent = p; frag.appendChild(s); }
});
ch.parentNode.replaceChild(frag, ch);
} else if (ch.nodeType === 1 && ch.tagName !== 'BR') walk(ch);
});
})(el);
return $$('.pw', el);
}
function splitChars(el) {
var txt = el.textContent; el.textContent = '';
var sr = document.createElement('span'); sr.className = 'sr-only'; sr.textContent = txt; el.appendChild(sr);
return txt.split('').map(function (c) {
var s = document.createElement('span'); s.className = 'pl'; s.textContent = c === ' ' ? '\u00a0' : c;
s.setAttribute('aria-hidden', 'true'); s.style.display = 'inline-block';
el.appendChild(s); return s;
});
}
if (reduce) {
var rebuildStatic = function () { routeState = buildRoute(); placeRider(1); if (routeState) routeState.rider.style.visibility = 'hidden'; };
rebuildStatic();
var rt; window.addEventListener('resize', function () { clearTimeout(rt); rt = setTimeout(rebuildStatic, 250); });
return;
}
/* ---- Hero-Intro ---- */
var hero = $('.hero');
if (hero) {
var chars = [];
$$('[data-hero-big] .part', hero).forEach(function (p) { chars = chars.concat(splitChars(p)); });
var burgerImg = $('.hero__burger .blast__food', hero);
var tl = gsap.timeline({ delay: .05 });
tl.set($$('[data-intro]', hero), { opacity: 1 })
.fromTo('.hero__eyebrow', { y: 24, opacity: 0 }, { y: 0, opacity: 1, duration: .5, ease: 'power3.out' }, 0)
.fromTo(chars, { scale: 0, opacity: 0, y: function () { return rand(40, 100); }, rotation: function () { return rand(-18, 18); }, transformOrigin: '50% 90%' },
{ scale: 1, opacity: 1, y: 0, rotation: 0, duration: .75, stagger: .045, ease: 'back.out(2.2)' }, .05)
.fromTo(burgerImg, { scale: .45, y: 140, rotation: -12, opacity: 0 }, { scale: 1, y: 0, rotation: 0, opacity: 1, duration: 1.1, ease: 'back.out(1.5)' }, .3)
.fromTo($$('[data-sticker]', hero), { '--s': 0 }, { '--s': 1, duration: .6, stagger: .12, ease: 'back.out(3)' }, .9)
.fromTo(['.hero__bottom .lead', '.hero__cta'], { y: 30, opacity: 0 }, { y: 0, opacity: 1, duration: .7, stagger: .1, ease: 'power3.out' }, .8)
.add(function () {
gsap.to(burgerImg, { y: -14, duration: 2.4, yoyo: true, repeat: -1, ease: 'sine.inOut' });
});
if (window.matchMedia('(pointer: fine)').matches) {
var qx = gsap.quickTo(burgerImg, 'x', { duration: .8, ease: 'power3.out' });
var qr = gsap.quickTo(burgerImg, 'rotation', { duration: .8, ease: 'power3.out' });
hero.addEventListener('mousemove', function (e) {
var r = hero.getBoundingClientRect();
var nx = (e.clientX - r.left) / r.width - .5;
qx(nx * 40); qr(nx * 6);
});
}
var mm = gsap.matchMedia();
mm.add('(min-width: 821px)', function () {
var parts = $$('[data-hero-big] .part', hero);
gsap.to(parts[0], { xPercent: -10, ease: 'none', scrollTrigger: { trigger: hero, start: 'top top', end: 'bottom top', scrub: true } });
gsap.to(parts[1], { xPercent: 12, ease: 'none', scrollTrigger: { trigger: hero, start: 'top top', end: 'bottom top', scrub: true } });
// CSS zentriert mit translate(-50%, -50%), GSAP liest das als yPercent -50: also -62 = 12 % nach oben
gsap.to('.hero__burger', { yPercent: -62, ease: 'none', scrollTrigger: { trigger: hero, start: 'top top', end: 'bottom top', scrub: true } });
});
}
/* ---- Seiten-Hero (Unterseiten) ---- */
var pageHero = $('.page-hero');
if (pageHero) {
var ph = $('[data-hero-big]', pageHero);
if (ph) {
var pchars = splitChars(ph);
gsap.fromTo(pchars, { scale: 0, opacity: 0, y: function () { return rand(40, 90); }, rotation: function () { return rand(-16, 16); }, transformOrigin: '50% 90%' },
{ scale: 1, opacity: 1, y: 0, rotation: 0, duration: .75, stagger: .05, ease: 'back.out(2.2)', delay: .05 });
}
var phImg = $('.page-hero__img img', pageHero);
if (phImg) {
gsap.fromTo(phImg, { scale: .5, y: 120, rotation: 10, opacity: 0 }, { scale: 1, y: 0, rotation: 0, opacity: 1, duration: 1.1, ease: 'back.out(1.5)', delay: .25 });
gsap.to(phImg, { y: -12, duration: 2.4, yoyo: true, repeat: -1, ease: 'sine.inOut', delay: 1.4 });
}
}
/* ---- Wort-Pop für Headlines ---- */
$$('[data-pop]').forEach(function (el) {
var words = splitWords(el);
gsap.fromTo(words,
{ opacity: 0, scale: 0, y: function () { return rand(18, 40); }, rotation: function () { return rand(-14, 14); }, transformOrigin: '50% 90%' },
{ opacity: 1, scale: 1, y: 0, rotation: 0, duration: .72, stagger: .06, ease: 'back.out(2.35)',
scrollTrigger: { trigger: el, start: 'top 88%', once: true } });
});
/* ---- Reveal (nutzt translate/opacity, damit CSS-Rotationen bleiben) ---- */
var reveals = $$('[data-reveal]').filter(function (el) { return !el.closest('.hero'); });
ST.batch(reveals, {
start: 'top 90%', once: true,
onEnter: function (batch) {
gsap.fromTo(batch, { opacity: 0, '--ry': '40px' }, { opacity: 1, '--ry': '0px', duration: .9, stagger: .1, ease: 'power3.out' });
}
});
/* ---- Sticker "aufkleben" ---- */
$$('[data-sticker]').filter(function (el) { return !el.closest('.hero'); }).forEach(function (el) {
gsap.fromTo(el, { '--s': 0 }, { '--s': 1, duration: .7, ease: 'back.out(3)', scrollTrigger: { trigger: el, start: 'top 88%', once: true } });
});
/* ---- Zutaten: schweben, beim Laden aus dem Essen platzen, beim Scrollen wegfliegen ---- */
blasts.forEach(function (bl) {
var W = function () { return bl.box.offsetWidth; };
var vec = function (b) {
return { x: (b.x - 50) / 100 * W(), y: (b.y - 50) / 100 * bl.box.offsetHeight };
};
// Explosionsziel: kräftig nach außen, mit Drall zur Seite und nach oben
var away = function (b, k) {
k *= bl.fly;
var v = vec(b), len = Math.sqrt(v.x * v.x + v.y * v.y) || 1;
var side = b.side;
return {
x: v.x * .5 + (v.x / len) * W() * k + (-v.y / len) * W() * .18 * side,
y: v.y * .3 + (v.y / len) * W() * k * .55 - W() * .45 * k
};
};
// Hero = Fontäne: Der Burger wurde von unten getroffen. Alles schießt aus der Burger-Mitte nach oben,
// fächert zur Seite auf (Spin dreht nach außen) und die Scroll-Explosion setzt diese Bewegung fort.
var FX = .39, FY = .45; // Ausbruchspunkt in Box-Anteilen (Mitte des Burgers)
var spread = function (b) { // Abstand Ausbruchspunkt -> Endposition in px
return { x: (b.x / 100 - FX) * W(), y: (b.y / 100 - FY) * bl.box.offsetHeight };
};
var launch = function (b, k) { // Scroll-Ziel: weiter nach oben + nach außen fächern
var d = spread(b), sp = b.layer === 'n' ? 1.5 : b.layer === 'b' ? .7 : 1;
return { x: d.x * 1.1 * sp * k, y: -(W() * .55 + Math.max(0, -d.y) * .4) * sp * k };
};
var floats = [];
var tl = gsap.timeline({ paused: true });
bl.bits.forEach(function (b, i) {
b.side = bl.hero ? (b.x < FX * 100 ? -1 : 1) : (i % 2 ? 1 : -1);
floats.push(gsap.to(b.fl, { y: '+=' + rand(-16, -8), rotation: '+=' + rand(-8, 8), duration: rand(1.8, 3.2), yoyo: true, repeat: -1, ease: 'sine.inOut', delay: rand(0, 1.5), paused: true }));
var spin = (bl.hero ? rand(70, 210) : rand(160, 340)) * b.side;
var grow = b.layer === 'b' ? 1.15 : b.layer === 'n' ? 2.6 : bl.hero ? 1.6 : 1.9;
if (bl.hero) {
// Intro: aus der Burger-Mitte nach oben; x gleitet, y schießt über das Ziel hinaus und fällt zurück
var dist = Math.sqrt(Math.pow(b.x / 100 - FX, 2) + Math.pow((b.y / 100 - FY) * .72, 2));
var t0 = .5 + (i % 5) * .03, dur = .95 + dist * .8;
gsap.from(b.inner, { x: function () { return -spread(b).x; }, duration: dur, ease: 'power3.out', delay: t0 });
gsap.from(b.inner, { y: function () { return -spread(b).y; }, duration: dur + .35, ease: 'back.out(2.1)', delay: t0 });
gsap.from(b.inner, { scale: .2, opacity: 0, rotation: -spin / 2, duration: dur, ease: 'back.out(1.4)', delay: t0 });
// Scroll: Position schießt zuerst schnell hoch und läuft dann aus (Fontäne), Spin/Größe wachsen gleichmäßig mit
var st = b.layer === 'n' ? 0 : b.layer === 'f' ? .04 + (i % 4) * .02 : .12;
tl.to(b.el, { x: function () { return launch(b, 1).x; }, y: function () { return launch(b, 1).y; }, duration: 1 - st, ease: 'power1.out' }, st)
.to(b.el, { rotation: spin, scale: grow, duration: 1 - st, ease: 'none' }, st)
.to(b.el, { opacity: 0, duration: .3, ease: 'none' }, .6);
} else {
tl.fromTo(b.el, { x: function () { return -vec(b).x * .6; }, y: function () { return -vec(b).y * .6; }, scale: .4, opacity: 0, rotation: -spin / 3 },
{ x: 0, y: 0, scale: 1, opacity: 1, rotation: 0, duration: .4, ease: 'power2.out' }, 0)
.to(b.el, { x: function () { return away(b, .8).x; }, y: function () { return away(b, .8).y; }, scale: grow, rotation: spin, duration: .45, ease: 'power2.in' }, .55)
.to(b.el, { opacity: 0, duration: .12, ease: 'none' }, .88);
}
});
var sec = bl.box.closest('section') || bl.box;
ST.create({
animation: tl, invalidateOnRefresh: true, scrub: .6,
trigger: bl.hero ? sec : bl.box,
start: bl.hero ? 'top top' : 'top 92%',
end: bl.hero ? '65% top' : 'bottom 12%'
});
// Schweben nur, solange die Zutaten im Bild sind (spart Akku und Rechenzeit)
ST.create({
trigger: bl.box, start: 'top bottom', end: 'bottom top',
onToggle: function (self) { floats.forEach(function (t) { self.isActive ? t.play() : t.pause(); }); }
});
});
/* ---- Feuerwirbel dreht sich langsam mit ---- */
$$('[data-swirl]').forEach(function (sw) {
var sec = sw.closest('section') || sw.parentNode;
gsap.fromTo(sw, { rotation: -12 }, { rotation: 28, ease: 'none', scrollTrigger: { trigger: sec, start: 'top bottom', end: 'bottom top', scrub: true } });
});
/* ---- Parallax Fotoband ---- */
$$('[data-parallax]').forEach(function (img) {
gsap.fromTo(img, { yPercent: -7 }, { yPercent: 7, ease: 'none', scrollTrigger: { trigger: img.parentNode, start: 'top bottom', end: 'bottom top', scrub: true } });
});
/* ---- Route ---- */
if (route) {
routeState = buildRoute();
ST.create({
trigger: route, start: 'top 75%', end: 'bottom 55%', scrub: .6,
onUpdate: function (self) { placeRider(self.progress); },
onRefresh: function (self) { routeState = buildRoute(); placeRider(self.progress); }
});
placeRider(0);
}
/* ---- Footer: Zutaten fliegen von unten ins Bild und fallen wieder raus ---- */
var jug = $('[data-juggle]');
if (jug) {
var foot = jug.closest('footer') || jug;
var G = 1100; // Schwerkraft in px/s², hält Flugkurve und -zeit realistisch
var running = false;
// Versatz, ab dem eine Zutat (auch gedreht) komplett unter dem Footer-Rand steckt
var below = function (el) {
return foot.getBoundingClientRect().bottom - jug.getBoundingClientRect().top - el.offsetTop + el.offsetHeight * .3 + 2;
};
var toss = function (item, wait) {
var el = item.el;
var low = below(el);
var apex = -Math.min(window.innerHeight * .55, Math.max(jug.offsetHeight, 110) * rand(.8, 1.25) + 20);
var dir = Math.random() < .5 ? -1 : 1;
var drift = window.innerWidth * rand(.03, .08) * dir;
var rot = rand(-30, 30);
var up = Math.sqrt(2 * (low - apex) / G);
item.tl = gsap.timeline({ delay: wait, paused: !running, onComplete: function () { toss(item, rand(.15, .8)); } })
.fromTo(el, { y: low }, { y: apex, duration: up, ease: 'power2.out' })
.to(el, { y: low, duration: up, ease: 'power2.in' })
.fromTo(el, { x: -drift / 2, rotation: rot }, { x: drift / 2, rotation: rot + dir * rand(200, 480), duration: up * 2, ease: 'none' }, 0);
};
var items = $$('.juggle', jug).map(function (el, i) {
var item = { el: el, tl: null };
gsap.set(el, { y: below(el), visibility: 'visible' });
toss(item, .1 + i * .3);
return item;
});
var setRunning = function (on) {
running = on;
items.forEach(function (it) { on ? it.tl.play() : it.tl.pause(); });
};
ST.create({
trigger: jug, start: 'top bottom', end: 'bottom top',
onToggle: function (self) { setRunning(self.isActive); }
});
document.addEventListener('visibilitychange', function () {
setRunning(!document.hidden && ST.isInViewport(jug));
});
}
window.addEventListener('load', function () { ST.refresh(); });
})();