document.addEventListener("DOMContentLoaded", () => { const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; // --- REVEAL (unverändert, nur markers ausgeschaltet) --- gsap.utils.toArray(".reveal").forEach((el) => { let mask = el.querySelector(".reveal_mask"); const img = el.querySelector("img"); if (!mask) { mask = document.createElement("span"); mask.className = "reveal_mask"; el.appendChild(mask); } gsap.set(el, { overflow: "hidden", position: "relative" }); gsap.set(mask, { transformOrigin: "bottom", scaleY: 1, position: "absolute", inset: 0, zIndex: 1 }); gsap.set(img, { scale: 1.05 }); if (reduceMotion) { gsap.set(mask, { scaleY: 0 }); gsap.set(img, { scale: 1, yPercent: 0 }); return; } gsap.timeline({ scrollTrigger: { trigger: el, start: "top 70%", toggleActions: "play none none none", once: false, invalidateOnRefresh: true }, defaults: { ease: "power2.inOut", duration: 1.6 } }) .to(mask, { scaleY: 0 }) .from(img, { yPercent: -8 }, "<") .to(img, { scale: 1 }, "<"); }); // Presets je Animationstyp const ANIMATIONS = { "text-size-large": { from: { y: 0, filter: "blur(3px)", opacity: 0, autoAlpha: 0 }, to: { y: 0, filter: "blur(0px)", opacity: 1, autoAlpha: 1 }, split: { type: "lines" }, defaults: { stagger: 0.10, duration: 1.2, ease: "power3.in" }, scrollTrigger: { start: "top 95%", end: "top 35%", scrub: 1, markers: false, toggleActions: "play none none reverse" } }, "paragraph": { from: { y: 0, filter: "blur(0px)", opacity: 0, autoAlpha: 0 }, to: { y: 0, filter: "blur(0px)", opacity: 1, autoAlpha: 1 }, split: { type: "lines" }, defaults: { stagger: 0.04, duration: 1, ease: "power3.in" }, scrollTrigger: { start: "top 85%", markers: false, toggleActions: "play none none none" } }, "heading-style-h2": { // Zeichen-für-Zeichen from: { y: 0, filter: "blur(3px)", scale: 3, opacity: 0 }, to: { y: 0, filter: "blur(0px)", scale: 1, opacity: 1 }, split: { type: "chars" }, defaults: { stagger: 0.03, duration: 0.5, ease: "power3.in" }, scrollTrigger: { start: "top 95%", end: "top 20%", markers: false, toggleActions: "play none none none" } }, "heading-style-h3": { // Deine H3-Animation, ohne SplitText (nur Gesamt-Element) from: { filter: "blur(3px)", opacity: 0 }, to: { filter: "blur(0px)", opacity: 1 }, split: null, // kein Split defaults: { duration: 1, ease: "power3.in" }, scrollTrigger: { start: "top 85%" } } }; // Optional: Overrides aus data-Attributen function readOverrides(el, base) { const o = { ...base }; if (el.dataset.stagger) o.stagger = parseFloat(el.dataset.stagger); if (el.dataset.duration) o.duration = parseFloat(el.dataset.duration); if (el.dataset.delay) o.delay = parseFloat(el.dataset.delay); o.scrollTrigger = { ...(base.scrollTrigger || {}) }; if (el.dataset.start) o.scrollTrigger.start = el.dataset.start; if (el.dataset.end) o.scrollTrigger.end = el.dataset.end; if (el.dataset.scrub) o.scrollTrigger.scrub = el.dataset.scrub === "true" ? true : parseFloat(el.dataset.scrub); if (el.dataset.markers) o.scrollTrigger.markers = el.dataset.markers === "true"; if (el.dataset.toggle) o.scrollTrigger.toggleActions = el.dataset.toggle; return o; } // Initialisierung document.querySelectorAll("[data-animation]").forEach((el) => { const type = el.dataset.animation; const cfg = ANIMATIONS[type]; if (!cfg) return; let targets; if (cfg.split) { const split = SplitText.create(el, cfg.split); const key = cfg.split.type; targets = split[key]; } else { targets = el; // keine Split-Animation, ganzes Element } gsap.set(targets, cfg.from); const opts = readOverrides(el, { ...cfg.defaults, scrollTrigger: cfg.scrollTrigger }); const toVars = { ...cfg.to, ...opts }; toVars.scrollTrigger = { ...(toVars.scrollTrigger || {}), trigger: el }; gsap.to(targets, toVars); }); //Dark-mode Scroll Trigger Animation const root = document.documentElement; // Light-Startzustand (kein .dark-Klassenwechsel mehr nötig) function toLight(){ gsap.to(root,{ duration: 0.2, ease: "power3.Out", /* white + white-smoke bleiben hell mit anth-Text */ "--bg-white": "#ffffff", "--fg-white": "#252422", "--bg-white-smoke": "#f4f4f4", "--fg-white-smoke": "#252422", /* anth wird dunkel mit hellem Text */ "--bg-anth": "#252422", "--fg-anth": "#f3f3f3", "--divider-color": "#252422", }); } // Dark-Mapping: white/white-smoke -> anth + sand; anth -> white-smoke + anth function toDark(){ gsap.to(root,{ duration: 0.2, ease: "power3.Out", "--bg-white": "#252422", "--fg-white": "#d6a77a", "--bg-white-smoke": "#252422", "--fg-white-smoke": "#d6a77a", "--bg-anth": "#f4f4f4", "--fg-anth": "#252422", "--divider-color": "#d6a77a50", }); } // ScrollTrigger: aktiviert Darkmode in Bereichen mit data-mode="dark" const activeDark = new Set(); document.querySelectorAll('[data-mode="dark"]').forEach((section, i) => { const id = i; ScrollTrigger.create({ trigger: section, start: "top 40%", end: "bottom 50%", onEnter(){ activeDark.add(id); toDark(); }, onEnterBack(){ activeDark.add(id); toDark(); }, onLeave(){ activeDark.delete(id); if(activeDark.size===0) toLight(); }, onLeaveBack(){ activeDark.delete(id); if(activeDark.size===0) toLight(); } // markers:true }); }); // sicherstellen: bei Erstaufruf Light toLight(); // Ultra-smooth Divider Animation - OHNE HAKELN // Optimiert für perfekte 60fps Performance const sel = '[data-animation="divider"]'; gsap.utils.toArray(sel).forEach((el) => { const part = 0.25; // GPU-Acceleration aktivieren für butterweiche Performance gsap.set(el, { scaleX: 0.05, x: 0, transformOrigin: '0% 50%', borderRadius: '4px', opacity: 0.95, filter: 'blur(0px) brightness(1)', force3D: true, // GPU-Beschleunigung willChange: 'transform, filter' // Browser-Optimierung }); const tl = gsap.timeline(); // EINE durchgehende Bewegung von links nach rechts - KEIN HAKELN mehr! tl.to(el, { x: () => el.offsetWidth * part, scaleX: 0.25, opacity: 1, filter: 'brightness(1.1)', duration: 1.0, // Kürzer für weniger Pause ease: 'power2.out', // Einheitliches Easing = keine Haker force3D: true }) // Direkter Rückzug zur finalen Position - STARTET SCHON FRÜHER! .to(el, { x: 0, scaleX: 1, filter: 'brightness(1)', duration: 0.8, ease: 'power3.out', force3D: true }, '-=0.5') // Startet 0.2s vor Ende der ersten Animation! // Glow-Effekt - separater Layer um Transform nicht zu stören .to(el, { boxShadow: '0 0 25px rgba(255,255,255,0.4)', duration: 0.3, ease: 'none' // Kein Easing = kein Hakeln }, '-=1.5') .to(el, { boxShadow: '0 0 0px rgba(255,255,255,0)', duration: 0.8, ease: 'none' }); }); // ALTERNATIVE: Perfekte Lösung ohne Hakeln // Verwendet nur noch eine einzige X-Animation pro Richtung const perfectSmooth = () => { gsap.utils.toArray(sel).forEach((el) => { const part = 0.25; // Optimale Performance-Settings gsap.set(el, { scaleX: 0.04, x: 0, transformOrigin: '0% 50%', borderRadius: '4px', willChange: 'transform', backfaceVisibility: 'hidden', // Anti-Flicker perspective: 1000, // 3D-Optimierung force3D: true }); // Master Timeline ohne überlappende X-Animationen const masterTL = gsap.timeline(); // Nur EINE X-Bewegung nach rechts - komplett smooth! masterTL.to(el, { x: () => el.offsetWidth * part, duration: 1.0, // Kürzer für weniger Pause ease: 'power2.out' // Konsistente Kurve = kein Hakeln }) // ScaleX läuft parallel aber beeinflusst nicht die X-Bewegung .to(el, { scaleX: 0.25, duration: 1.0, // Auch hier kürzer ease: 'power2.out' }, 0) // Startet gleichzeitig // Dann direkt zur finalen Position - STARTET FRÜHER .to(el, { x: 0, scaleX: 1, duration: 0.7, ease: 'power3.out' }, '-=0.15'); // Beginnt 0.15s vor Ende der ersten Animation }); }; // Aufruf der Premium-Version für ultimative Smoothness // premiumSmooth(); });