From 57978d81e1464e4045befb300675f3e928d98ff9 Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Sat, 14 Mar 2026 18:35:04 +0100 Subject: [PATCH] scroll triggers --- src/app/_components/Animated/AnimateIn.tsx | 29 ++++---- .../Animated/AnimatedPageTitle.tsx | 4 +- src/app/_providers/GsapProvicer.tsx | 40 +++++++++-- src/app/cv/page.tsx | 8 +-- src/app/music/page.tsx | 70 ++++++++++--------- src/components/ui/card.tsx | 17 +++-- 6 files changed, 102 insertions(+), 66 deletions(-) diff --git a/src/app/_components/Animated/AnimateIn.tsx b/src/app/_components/Animated/AnimateIn.tsx index 915abf9..6741860 100644 --- a/src/app/_components/Animated/AnimateIn.tsx +++ b/src/app/_components/Animated/AnimateIn.tsx @@ -1,28 +1,25 @@ import { useGSAP } from "@gsap/react"; -import { useEffect, -useLayoutEffect, -useRef, type ReactNode } from "react"; +import { useRef, type ReactNode } from "react"; import { useGsapContext } from "~/app/_providers/GsapProvicer"; import { SplitText } from "gsap/SplitText"; import gsap from 'gsap' const AnimateTextIn = ({children,animation="type",position}:{children:ReactNode,animation?:"type"|"slide",position:gsap.Position}) => { const el = useRef(null) const gsapContext = useGsapContext(); - useLayoutEffect(() => { - console.log("aniamte text with:",position) - const tl = gsap.timeline(); + useGSAP(() => { + const rect = el.current?.getBoundingClientRect() + const isInView = rect && rect.top < window.innerHeight const chars = new SplitText(el.current,{type:'chars'}) - tl.to(el.current,{opacity:100, duration:0}) - switch(animation) { - case "slide": - tl.from(chars.chars,{opacity:0, x:-10, duration: 0.2, stagger: {each: 0.08}, ease:'bounce.inOut', scrollTrigger: el.current }) - break - case "type": - tl.from(chars.chars,{opacity:0, duration: 0.01, stagger: {each: 0.04}, ease: 'bounce.inOut', scrollTrigger: el.current }) - break + gsapContext?.addAnimation(gsap.to(el.current,{opacity:100, duration:0}),0) + const fromVars = animation === "slide" + ? {opacity:0, x:-10, duration: 0.2, stagger: {each: 0.08}, ease:'bounce.inOut'} + : {opacity:0, duration: 0.01, stagger: {each: 0.04}, ease: 'bounce.inOut'} + if (isInView) { + gsapContext?.addAnimation(gsap.from(chars.chars, fromVars),position) + } else { + gsap.from(chars.chars, { ...fromVars, scrollTrigger: { trigger: el.current, start: 'top 85%', scroller: gsapContext?.getScroller() } }) } - gsapContext?.addAnimation(tl,position) - }) + }, { dependencies: [] }) return (
{children} diff --git a/src/app/_components/Animated/AnimatedPageTitle.tsx b/src/app/_components/Animated/AnimatedPageTitle.tsx index 850ebab..43b650a 100644 --- a/src/app/_components/Animated/AnimatedPageTitle.tsx +++ b/src/app/_components/Animated/AnimatedPageTitle.tsx @@ -7,14 +7,14 @@ const AnimatedPageTitle = ( ) => { const el = useRef(null) const gsapContext = useGsapContext(); - useLayoutEffect(() => { + useEffect(() => { console.log("add animated title with:",position) const split = new SplitText(el.current, { type: "chars" }) gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100 }),position) gsapContext?.addAnimation(gsap.from(split.chars, { stagger: 0.05, rotate: -90, opacity: 0, x: -10 }),'>') - }) + },[]) return (

{text}

) diff --git a/src/app/_providers/GsapProvicer.tsx b/src/app/_providers/GsapProvicer.tsx index c20101f..031cb98 100644 --- a/src/app/_providers/GsapProvicer.tsx +++ b/src/app/_providers/GsapProvicer.tsx @@ -3,7 +3,7 @@ import { useGSAP } from '@gsap/react' import gsap from 'gsap' import { SplitText } from 'gsap/SplitText' import { ScrollTrigger } from 'gsap/all' -import { createContext, useCallback, useContext, useRef, useState, type ReactNode } from 'react' +import { createContext, useCallback, useContext, useEffect, useRef, type ReactNode } from 'react' gsap.registerPlugin(useGSAP) gsap.registerPlugin(ScrollTrigger) @@ -14,16 +14,47 @@ const GsapContext = createContext<{ position: gsap.Position ) => void, resetTimeline: () => void, - resumeTimeline: () => void + resumeTimeline: () => void, + getScroller: () => Element | Window | null } | null>(null) export function useGsapContext() { return useContext(GsapContext) } +export const useTimeLine = (dep:any,all?:boolean) => { + const gsapContext = useGsapContext() + useEffect(() => { + if (dep instanceof Array && all) { + let acc = true; + let allDepsSatisfied = dep.reduce((p,c) => c !== undefined && p ,acc ) + if (allDepsSatisfied) { + gsapContext?.resumeTimeline() + } + } else { + if (dep) { + gsapContext?.resumeTimeline() + } + } + },[dep]) + useEffect(() => { + return () => { + gsapContext?.resetTimeline() + } + },[]) +} + export default function GsapProvider({ children }: { children: ReactNode }) { const tl = useRef(null) - const { contextSafe } = useGSAP(() => { + const scrollerRef = useRef(null) + const getScroller = useCallback(() => { + const cached = scrollerRef.current + if (!cached || (cached instanceof Element && !document.contains(cached))) { + scrollerRef.current = document.querySelector('[data-slot="scroll-area-viewport"]') ?? window + } + return scrollerRef.current + }, []) + useGSAP(() => { if (!tl.current) { tl.current = gsap.timeline({ paused: true }) } @@ -37,6 +68,7 @@ export default function GsapProvider({ children }: { children: ReactNode }) { const resetTimeline = useCallback(() => { tl.current?.kill() tl.current?.revert() + ScrollTrigger.getAll().forEach(st => st.kill()) tl.current = gsap.timeline({paused:true}) },[]) const resumeTimeline = useCallback(() => { @@ -44,7 +76,7 @@ export default function GsapProvider({ children }: { children: ReactNode }) { tl.current?.resume() },[]) return ( - + {children} ) diff --git a/src/app/cv/page.tsx b/src/app/cv/page.tsx index f032cea..7d3f369 100644 --- a/src/app/cv/page.tsx +++ b/src/app/cv/page.tsx @@ -1,6 +1,6 @@ 'use client' import { useGSAP } from "@gsap/react"; -import { useGsapContext } from "../_providers/GsapProvicer"; +import { useGsapContext,useTimeLine } from "../_providers/GsapProvicer"; import { trpc } from "../_trpc/Client"; import { useRef } from "react"; import { SidebarContent, SidebarProvider, Sidebar } from "~/components/ui/sidebar"; @@ -32,6 +32,7 @@ export default function CvPage() { return { y: 100, opacity: 0, duration: 0.5 } } } + useTimeLine(col2Categories) useGSAP(() => { const items = gsap?.utils.toArray('.gsapan'); let dir = Direction.Left; @@ -47,7 +48,7 @@ export default function CvPage() { return ( <> - {(sidebarCategories.data?.length ? sidebarCategories.data?.length : 0) > 0 ? + {sidebarCategories.data && <> @@ -61,8 +62,7 @@ export default function CvPage() { })} - : - <> + }
diff --git a/src/app/music/page.tsx b/src/app/music/page.tsx index 37e1a51..1d0d177 100644 --- a/src/app/music/page.tsx +++ b/src/app/music/page.tsx @@ -1,44 +1,34 @@ 'use client' -import { useGSAP } from "@gsap/react"; -import { useEffect, -useEffectEvent, -useLayoutEffect, -useRef } from "react"; import { trpc } from "~/app/_trpc/Client"; import * as Card from "~/components/ui/card"; -import { useGsapContext } from "../_providers/GsapProvicer"; +import { useTimeLine } from "../_providers/GsapProvicer"; import AnimatedPageTitle from "../_components/Animated/AnimatedPageTitle"; import { Spinner } from "~/components/ui/spinner"; import AnimateTextIn from "../_components/Animated/AnimateIn"; +import { ScrollArea } from "~/components/ui/scroll-area"; export default function MusicPage() { const { data: tracks, isLoading } = trpc.music.list.useQuery(); - const gsapContext = useGsapContext(); - useEffect(() => { - if (tracks) { - gsapContext?.resumeTimeline() - } - return () => { - console.log("page cleanup") - gsapContext?.resetTimeline() - } - },[tracks]); - return (<> -
- + const randdata = Array.from({ length: 100 }, (_, i) => ({ id: i, value: Math.floor(Math.random() * 50) })); + useTimeLine(tracks) + return ( + + -
-

All works on this page are licensed under:

- CC BY-NC-SA 4.0 - - - - -
+
+

All works on this page are licensed under:

+ CC BY-NC-SA 4.0 +
+ + + + +
+
- {tracks && tracks.map((track,i) => ( - + {tracks && tracks.map((track, i) => ( + - + {track.title} @@ -52,14 +42,26 @@ export default function MusicPage() { ))} + {randdata.map((d, i) => ( + + + + {d.value} + + + + {d.value} + + + ))} {!isLoading && !tracks?.length && -
+
No music yet.
} {isLoading &&
- Loading Tracks + Loading Tracks
} -
- ); +
+ ); } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 969f79e..4a99739 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -31,12 +31,17 @@ function AnimatedCard({ const gsapContext = useGsapContext() const ref = useRef(null) useGSAP(() => { - gsapContext?.addAnimation(gsap.from(ref.current,{ - x: -100, - opacity: 0, - duration: 0.5 - }),position) - }) + const rect = ref.current?.getBoundingClientRect() + const isInView = rect && rect.top < window.innerHeight + const fromVars = { x: -100, opacity: 0, duration: 0.5 } + if (isInView) { + gsapContext?.addAnimation(gsap.from(ref.current, fromVars), position) + } else { + const scroller = gsapContext?.getScroller() + console.log('scroller:', scroller) + gsap.from(ref.current, { ...fromVars, scrollTrigger: { trigger: ref.current, start: 'top 85%', scroller, markers: true } }) + } + }, { dependencies: [] }) return (