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 90%", 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 90%" } } }; // 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; function toLight() { root.classList.remove("theme-dark"); } function toDark() { root.classList.add("theme-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 }); }); // initial 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(); gsap.set(".smiley_deco", { rotation: 180, filter: "blur(3px)", opacity: 0 }); // Rotation eigenes Ease gsap.to(".smiley_deco", { rotation: 390, ease: "elastic.out(2, 0.5)", duration: 5 }); // Opacity eigenes Ease gsap.to(".smiley_deco", { opacity: 1, filter: "blur(0px)", ease: "power3.in", duration: 1 }); // Movement separat gsap.to(".smiley_deco", { x: 100, duration: 1 }); });