scroll triggers

This commit is contained in:
2026-03-14 18:35:04 +01:00
parent b5291caa6e
commit 57978d81e1
6 changed files with 102 additions and 66 deletions

View File

@@ -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<HTMLDivElement>(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 (
<div ref={el} className="opacity-0">
{children}

View File

@@ -7,14 +7,14 @@ const AnimatedPageTitle = (
) => {
const el = useRef<HTMLHeadingElement>(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 (
<h1 className="text-4xl opacity-0 font-bold text-balance w-full" ref={el}> {text} </h1>
)

View File

@@ -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<gsap.core.Timeline | null>(null)
const { contextSafe } = useGSAP(() => {
const scrollerRef = useRef<Element | Window | null>(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 (
<GsapContext.Provider value={{ addAnimation, resetTimeline,resumeTimeline }}>
<GsapContext.Provider value={{ addAnimation, resetTimeline, resumeTimeline, getScroller }}>
{children}
</GsapContext.Provider>
)

View File

@@ -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<GSAPTweenTarget>('.gsapan');
let dir = Direction.Left;
@@ -47,7 +48,7 @@ export default function CvPage() {
return (
<>
<SidebarProvider ref={container}>
{(sidebarCategories.data?.length ? sidebarCategories.data?.length : 0) > 0 ?
{sidebarCategories.data &&
<>
<SidebarTriggerDisappearsOnMobile />
<Sidebar className="gsapan ">
@@ -61,8 +62,7 @@ export default function CvPage() {
})}
</SidebarContent>
</Sidebar>
</> :
<></>
</>
}
<div className="h-full w-full flex flex-wrap flex-row p-4 pt-8 ">
<div id="mainwrap" className="flex w-full flex-col gap-4 lg:px-[15vw]">

View File

@@ -1,39 +1,29 @@
'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 (<>
<div className="w-full h-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4">
const randdata = Array.from({ length: 100 }, (_, i) => ({ id: i, value: Math.floor(Math.random() * 50) }));
useTimeLine(tracks)
return (
<ScrollArea className="w-full h-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4">
<AnimatedPageTitle position={0} text="Just Some Music I Made" />
<AnimateTextIn position={0.5}>
<div className="flex flex-row h-8 content-center items-center">
<p className="mr-[1em]">All works on this page are licensed under:</p>
<div className="flex flex-col lg:flex-row h-fit content-center">
<p className="break-after-avoid mr-[1em]">All works on this page are licensed under:</p>
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>
<div className="flex flex-row">
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" />
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" />
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" alt="" />
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" />
</div>
</div>
</AnimateTextIn>
{tracks && tracks.map((track, i) => (
<Card.AnimatedCard key={track.id} position={i + 1}>
@@ -52,14 +42,26 @@ export default function MusicPage() {
</Card.CardContent>
</Card.AnimatedCard>
))}
{randdata.map((d, i) => (
<Card.AnimatedCard key={d.id} position={(i + 1) * 0.3}>
<Card.CardHeader>
<AnimateTextIn position={(i + 1.5) * 0.3} animation="slide">
<Card.CardTitle>{d.value}</Card.CardTitle>
</AnimateTextIn>
</Card.CardHeader>
<Card.CardContent className="flex flex-col gap-3">
{d.value}
</Card.CardContent>
</Card.AnimatedCard>
))}
{!isLoading && !tracks?.length &&
<div className="flex justify-center items-center min-h-[200px] text-muted-foreground">
<div className="flex justify-center items-center text-muted-foreground">
No music yet.
</div>
}
{isLoading && <div className="w-full h-full items-center flex flex-row content-center gap-4 justify-center">
<Spinner /> Loading Tracks
</div>}
</div>
</>);
</ScrollArea>
);
}

View File

@@ -31,12 +31,17 @@ function AnimatedCard({
const gsapContext = useGsapContext()
const ref = useRef<HTMLDivElement|null>(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 (
<div
ref={ref}