/* 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(); }); })();