scroll triggers
This commit is contained in:
@@ -1,28 +1,25 @@
|
|||||||
import { useGSAP } from "@gsap/react";
|
import { useGSAP } from "@gsap/react";
|
||||||
import { useEffect,
|
import { useRef, type ReactNode } from "react";
|
||||||
useLayoutEffect,
|
|
||||||
useRef, type ReactNode } from "react";
|
|
||||||
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
||||||
import { SplitText } from "gsap/SplitText";
|
import { SplitText } from "gsap/SplitText";
|
||||||
import gsap from 'gsap'
|
import gsap from 'gsap'
|
||||||
const AnimateTextIn = ({children,animation="type",position}:{children:ReactNode,animation?:"type"|"slide",position:gsap.Position}) => {
|
const AnimateTextIn = ({children,animation="type",position}:{children:ReactNode,animation?:"type"|"slide",position:gsap.Position}) => {
|
||||||
const el = useRef<HTMLDivElement>(null)
|
const el = useRef<HTMLDivElement>(null)
|
||||||
const gsapContext = useGsapContext();
|
const gsapContext = useGsapContext();
|
||||||
useLayoutEffect(() => {
|
useGSAP(() => {
|
||||||
console.log("aniamte text with:",position)
|
const rect = el.current?.getBoundingClientRect()
|
||||||
const tl = gsap.timeline();
|
const isInView = rect && rect.top < window.innerHeight
|
||||||
const chars = new SplitText(el.current,{type:'chars'})
|
const chars = new SplitText(el.current,{type:'chars'})
|
||||||
tl.to(el.current,{opacity:100, duration:0})
|
gsapContext?.addAnimation(gsap.to(el.current,{opacity:100, duration:0}),0)
|
||||||
switch(animation) {
|
const fromVars = animation === "slide"
|
||||||
case "slide":
|
? {opacity:0, x:-10, duration: 0.2, stagger: {each: 0.08}, ease:'bounce.inOut'}
|
||||||
tl.from(chars.chars,{opacity:0, x:-10, duration: 0.2, stagger: {each: 0.08}, ease:'bounce.inOut', scrollTrigger: el.current })
|
: {opacity:0, duration: 0.01, stagger: {each: 0.04}, ease: 'bounce.inOut'}
|
||||||
break
|
if (isInView) {
|
||||||
case "type":
|
gsapContext?.addAnimation(gsap.from(chars.chars, fromVars),position)
|
||||||
tl.from(chars.chars,{opacity:0, duration: 0.01, stagger: {each: 0.04}, ease: 'bounce.inOut', scrollTrigger: el.current })
|
} else {
|
||||||
break
|
gsap.from(chars.chars, { ...fromVars, scrollTrigger: { trigger: el.current, start: 'top 85%', scroller: gsapContext?.getScroller() } })
|
||||||
}
|
}
|
||||||
gsapContext?.addAnimation(tl,position)
|
}, { dependencies: [] })
|
||||||
})
|
|
||||||
return (
|
return (
|
||||||
<div ref={el} className="opacity-0">
|
<div ref={el} className="opacity-0">
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ const AnimatedPageTitle = (
|
|||||||
) => {
|
) => {
|
||||||
const el = useRef<HTMLHeadingElement>(null)
|
const el = useRef<HTMLHeadingElement>(null)
|
||||||
const gsapContext = useGsapContext();
|
const gsapContext = useGsapContext();
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
console.log("add animated title with:",position)
|
console.log("add animated title with:",position)
|
||||||
const split = new SplitText(el.current, { type: "chars" })
|
const split = new SplitText(el.current, { type: "chars" })
|
||||||
gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100 }),position)
|
gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100 }),position)
|
||||||
gsapContext?.addAnimation(gsap.from(split.chars, {
|
gsapContext?.addAnimation(gsap.from(split.chars, {
|
||||||
stagger: 0.05, rotate: -90, opacity: 0, x: -10
|
stagger: 0.05, rotate: -90, opacity: 0, x: -10
|
||||||
}),'>')
|
}),'>')
|
||||||
})
|
},[])
|
||||||
return (
|
return (
|
||||||
<h1 className="text-4xl opacity-0 font-bold text-balance w-full" ref={el}> {text} </h1>
|
<h1 className="text-4xl opacity-0 font-bold text-balance w-full" ref={el}> {text} </h1>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useGSAP } from '@gsap/react'
|
|||||||
import gsap from 'gsap'
|
import gsap from 'gsap'
|
||||||
import { SplitText } from 'gsap/SplitText'
|
import { SplitText } from 'gsap/SplitText'
|
||||||
import { ScrollTrigger } from 'gsap/all'
|
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(useGSAP)
|
||||||
gsap.registerPlugin(ScrollTrigger)
|
gsap.registerPlugin(ScrollTrigger)
|
||||||
@@ -14,16 +14,47 @@ const GsapContext = createContext<{
|
|||||||
position: gsap.Position
|
position: gsap.Position
|
||||||
) => void,
|
) => void,
|
||||||
resetTimeline: () => void,
|
resetTimeline: () => void,
|
||||||
resumeTimeline: () => void
|
resumeTimeline: () => void,
|
||||||
|
getScroller: () => Element | Window | null
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
export function useGsapContext() {
|
export function useGsapContext() {
|
||||||
return useContext(GsapContext)
|
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 }) {
|
export default function GsapProvider({ children }: { children: ReactNode }) {
|
||||||
const tl = useRef<gsap.core.Timeline | null>(null)
|
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) {
|
if (!tl.current) {
|
||||||
tl.current = gsap.timeline({ paused: true })
|
tl.current = gsap.timeline({ paused: true })
|
||||||
}
|
}
|
||||||
@@ -37,6 +68,7 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
|
|||||||
const resetTimeline = useCallback(() => {
|
const resetTimeline = useCallback(() => {
|
||||||
tl.current?.kill()
|
tl.current?.kill()
|
||||||
tl.current?.revert()
|
tl.current?.revert()
|
||||||
|
ScrollTrigger.getAll().forEach(st => st.kill())
|
||||||
tl.current = gsap.timeline({paused:true})
|
tl.current = gsap.timeline({paused:true})
|
||||||
},[])
|
},[])
|
||||||
const resumeTimeline = useCallback(() => {
|
const resumeTimeline = useCallback(() => {
|
||||||
@@ -44,7 +76,7 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
|
|||||||
tl.current?.resume()
|
tl.current?.resume()
|
||||||
},[])
|
},[])
|
||||||
return (
|
return (
|
||||||
<GsapContext.Provider value={{ addAnimation, resetTimeline,resumeTimeline }}>
|
<GsapContext.Provider value={{ addAnimation, resetTimeline, resumeTimeline, getScroller }}>
|
||||||
{children}
|
{children}
|
||||||
</GsapContext.Provider>
|
</GsapContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useGSAP } from "@gsap/react";
|
import { useGSAP } from "@gsap/react";
|
||||||
import { useGsapContext } from "../_providers/GsapProvicer";
|
import { useGsapContext,useTimeLine } from "../_providers/GsapProvicer";
|
||||||
import { trpc } from "../_trpc/Client";
|
import { trpc } from "../_trpc/Client";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { SidebarContent, SidebarProvider, Sidebar } from "~/components/ui/sidebar";
|
import { SidebarContent, SidebarProvider, Sidebar } from "~/components/ui/sidebar";
|
||||||
@@ -32,6 +32,7 @@ export default function CvPage() {
|
|||||||
return { y: 100, opacity: 0, duration: 0.5 }
|
return { y: 100, opacity: 0, duration: 0.5 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
useTimeLine(col2Categories)
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
const items = gsap?.utils.toArray<GSAPTweenTarget>('.gsapan');
|
const items = gsap?.utils.toArray<GSAPTweenTarget>('.gsapan');
|
||||||
let dir = Direction.Left;
|
let dir = Direction.Left;
|
||||||
@@ -47,7 +48,7 @@ export default function CvPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SidebarProvider ref={container}>
|
<SidebarProvider ref={container}>
|
||||||
{(sidebarCategories.data?.length ? sidebarCategories.data?.length : 0) > 0 ?
|
{sidebarCategories.data &&
|
||||||
<>
|
<>
|
||||||
<SidebarTriggerDisappearsOnMobile />
|
<SidebarTriggerDisappearsOnMobile />
|
||||||
<Sidebar className="gsapan ">
|
<Sidebar className="gsapan ">
|
||||||
@@ -61,8 +62,7 @@ export default function CvPage() {
|
|||||||
})}
|
})}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</> :
|
</>
|
||||||
<></>
|
|
||||||
}
|
}
|
||||||
<div className="h-full w-full flex flex-wrap flex-row p-4 pt-8 ">
|
<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]">
|
<div id="mainwrap" className="flex w-full flex-col gap-4 lg:px-[15vw]">
|
||||||
|
|||||||
@@ -1,39 +1,29 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useGSAP } from "@gsap/react";
|
|
||||||
import { useEffect,
|
|
||||||
useEffectEvent,
|
|
||||||
useLayoutEffect,
|
|
||||||
useRef } from "react";
|
|
||||||
import { trpc } from "~/app/_trpc/Client";
|
import { trpc } from "~/app/_trpc/Client";
|
||||||
import * as Card from "~/components/ui/card";
|
import * as Card from "~/components/ui/card";
|
||||||
import { useGsapContext } from "../_providers/GsapProvicer";
|
import { useTimeLine } from "../_providers/GsapProvicer";
|
||||||
import AnimatedPageTitle from "../_components/Animated/AnimatedPageTitle";
|
import AnimatedPageTitle from "../_components/Animated/AnimatedPageTitle";
|
||||||
import { Spinner } from "~/components/ui/spinner";
|
import { Spinner } from "~/components/ui/spinner";
|
||||||
import AnimateTextIn from "../_components/Animated/AnimateIn";
|
import AnimateTextIn from "../_components/Animated/AnimateIn";
|
||||||
|
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||||
export default function MusicPage() {
|
export default function MusicPage() {
|
||||||
const { data: tracks, isLoading } = trpc.music.list.useQuery();
|
const { data: tracks, isLoading } = trpc.music.list.useQuery();
|
||||||
const gsapContext = useGsapContext();
|
const randdata = Array.from({ length: 100 }, (_, i) => ({ id: i, value: Math.floor(Math.random() * 50) }));
|
||||||
useEffect(() => {
|
useTimeLine(tracks)
|
||||||
if (tracks) {
|
return (
|
||||||
gsapContext?.resumeTimeline()
|
<ScrollArea className="w-full h-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4">
|
||||||
}
|
|
||||||
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">
|
|
||||||
<AnimatedPageTitle position={0} text="Just Some Music I Made" />
|
<AnimatedPageTitle position={0} text="Just Some Music I Made" />
|
||||||
<AnimateTextIn position={0.5}>
|
<AnimateTextIn position={0.5}>
|
||||||
<div className="flex flex-row h-8 content-center items-center">
|
<div className="flex flex-col lg:flex-row h-fit content-center">
|
||||||
<p className="mr-[1em]">All works on this page are licensed under:</p>
|
<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>
|
<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/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/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/nc.svg" alt="" />
|
||||||
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" />
|
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</AnimateTextIn>
|
</AnimateTextIn>
|
||||||
{tracks && tracks.map((track, i) => (
|
{tracks && tracks.map((track, i) => (
|
||||||
<Card.AnimatedCard key={track.id} position={i + 1}>
|
<Card.AnimatedCard key={track.id} position={i + 1}>
|
||||||
@@ -52,14 +42,26 @@ export default function MusicPage() {
|
|||||||
</Card.CardContent>
|
</Card.CardContent>
|
||||||
</Card.AnimatedCard>
|
</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 &&
|
{!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.
|
No music yet.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{isLoading && <div className="w-full h-full items-center flex flex-row content-center gap-4 justify-center">
|
{isLoading && <div className="w-full h-full items-center flex flex-row content-center gap-4 justify-center">
|
||||||
<Spinner /> Loading Tracks
|
<Spinner /> Loading Tracks
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</ScrollArea>
|
||||||
</>);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,17 @@ function AnimatedCard({
|
|||||||
const gsapContext = useGsapContext()
|
const gsapContext = useGsapContext()
|
||||||
const ref = useRef<HTMLDivElement|null>(null)
|
const ref = useRef<HTMLDivElement|null>(null)
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
gsapContext?.addAnimation(gsap.from(ref.current,{
|
const rect = ref.current?.getBoundingClientRect()
|
||||||
x: -100,
|
const isInView = rect && rect.top < window.innerHeight
|
||||||
opacity: 0,
|
const fromVars = { x: -100, opacity: 0, duration: 0.5 }
|
||||||
duration: 0.5
|
if (isInView) {
|
||||||
}),position)
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
Reference in New Issue
Block a user