reasonable animation system
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
import { useGSAP } from "@gsap/react";
|
import { useGSAP } from "@gsap/react";
|
||||||
import { useRef, type ReactNode } from "react";
|
import { 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"}:{children:ReactNode,animation?:"type"|"slide",index?: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();
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
|
console.log("aniamte text with:",position)
|
||||||
const tl = gsap.timeline();
|
const tl = gsap.timeline();
|
||||||
const chars = new SplitText(el.current,{type:'chars'})
|
const chars = new SplitText(el.current,{type:'chars'})
|
||||||
tl.to(el.current,{opacity:100, duration:0})
|
tl.to(el.current,{opacity:100, duration:0})
|
||||||
@@ -18,8 +20,8 @@ const AnimateTextIn = ({children,animation="type"}:{children:ReactNode,animation
|
|||||||
tl.from(chars.chars,{opacity:0, duration: 0.01, stagger: {each: 0.04}, ease: 'bounce.inOut', scrollTrigger: el.current })
|
tl.from(chars.chars,{opacity:0, duration: 0.01, stagger: {each: 0.04}, ease: 'bounce.inOut', scrollTrigger: el.current })
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
gsapContext?.addAnimation(tl)
|
gsapContext?.addAnimation(tl,position)
|
||||||
},{scope:el})
|
})
|
||||||
return (
|
return (
|
||||||
<div ref={el} className="opacity-0">
|
<div ref={el} className="opacity-0">
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import { useGSAP } from "@gsap/react"; import { useRef } from "react";
|
import { useGSAP } from "@gsap/react"; import { useEffect, useLayoutEffect, useRef } 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 AnimatedPageTitle = (
|
const AnimatedPageTitle = (
|
||||||
{ text }: { text: string }
|
{ text, position }: { text: string, position:gsap.Position }
|
||||||
) => {
|
) => {
|
||||||
const el = useRef<HTMLHeadingElement>(null)
|
const el = useRef<HTMLHeadingElement>(null)
|
||||||
const gsapContext = useGsapContext();
|
const gsapContext = useGsapContext();
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
|
console.log("add animated title with:",position)
|
||||||
const tl = gsap.timeline();
|
const tl = gsap.timeline();
|
||||||
tl.addLabel("title")
|
tl.addLabel("title")
|
||||||
const split = new SplitText(el.current, { type: "chars" })
|
const split = new SplitText(el.current, { type: "chars" })
|
||||||
tl.to(el.current, { opacity: 100 })
|
tl.from(el.current, { opacity: 0 })
|
||||||
tl.from(split.chars, {
|
tl.from(split.chars, {
|
||||||
stagger: 0.05, rotate: -90, opacity: 0, x: -10
|
stagger: 0.05, rotate: -90, opacity: 0, x: -10
|
||||||
}, '>')
|
}, '>')
|
||||||
gsapContext?.addAnimation(tl)
|
gsapContext?.addAnimation(tl,position)
|
||||||
}, { scope: el })
|
})
|
||||||
return (
|
return (
|
||||||
<h1 className="text-4xl opacity-0 font-bold text-balance w-full" ref={el}> {text} </h1>
|
<h1 className="text-4xl opacity-100 font-bold text-balance w-full" ref={el}> {text} </h1>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,33 +8,43 @@ import { createContext, useCallback, useContext, useRef, useState, type ReactNod
|
|||||||
gsap.registerPlugin(useGSAP)
|
gsap.registerPlugin(useGSAP)
|
||||||
gsap.registerPlugin(ScrollTrigger)
|
gsap.registerPlugin(ScrollTrigger)
|
||||||
gsap.registerPlugin(SplitText)
|
gsap.registerPlugin(SplitText)
|
||||||
const GsapContext = createContext<{ addAnimation: (animation: gsap.core.TimelineChild) => void, resetTimeline: () => void} | null>(null)
|
const GsapContext = createContext<{
|
||||||
|
addAnimation: (
|
||||||
|
animation: gsap.core.TimelineChild,
|
||||||
|
position: gsap.Position
|
||||||
|
) => void,
|
||||||
|
resetTimeline: () => void,
|
||||||
|
resumeTimeline: () => void
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
export function useGsapContext() {
|
export function useGsapContext() {
|
||||||
return useContext(GsapContext)
|
return useContext(GsapContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GsapProvider({ children }: { children: ReactNode }) {
|
export default function GsapProvider({ children }: { children: ReactNode }) {
|
||||||
const [tl, setTl] = useState<GSAPTimeline | undefined>();
|
const tl = useRef<gsap.core.Timeline | null>(null)
|
||||||
const indexRef = useRef<number>(0)
|
|
||||||
const { contextSafe } = useGSAP(() => {
|
const { contextSafe } = useGSAP(() => {
|
||||||
console.log("App effect (create timeline)");
|
if (!tl.current) {
|
||||||
const tl = gsap.timeline();
|
tl.current = gsap.timeline({ paused: true })
|
||||||
setTl(() => tl);
|
}
|
||||||
}, []);
|
console.log("resuming timeline:",tl.current)
|
||||||
|
return () => { console.log("gsap cleanup") }
|
||||||
|
})
|
||||||
|
|
||||||
const addAnimation = useCallback((animation: gsap.core.TimelineChild) => {
|
const addAnimation = useCallback((animation: gsap.core.TimelineChild, position: gsap.Position) => {
|
||||||
indexRef.current += 1;
|
console.log("add animation to:", position)
|
||||||
console.log(indexRef.current)
|
tl.current?.add(animation, position);
|
||||||
tl && tl.add(animation, indexRef.current * 2);
|
},[])
|
||||||
}, [tl]);
|
|
||||||
const resetTimeline = useCallback(() => {
|
const resetTimeline = useCallback(() => {
|
||||||
const tl = gsap.timeline();
|
tl.current?.kill()
|
||||||
setTl(() => tl)
|
tl.current?.revert()
|
||||||
indexRef.current = 0;
|
tl.current = gsap.timeline({paused:true})
|
||||||
},[tl])
|
},[])
|
||||||
|
const resumeTimeline = useCallback(() => {
|
||||||
|
tl.current?.resume()
|
||||||
|
},[])
|
||||||
return (
|
return (
|
||||||
<GsapContext.Provider value={{ addAnimation, resetTimeline }}>
|
<GsapContext.Provider value={{ addAnimation, resetTimeline,resumeTimeline }}>
|
||||||
{children}
|
{children}
|
||||||
</GsapContext.Provider>
|
</GsapContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,11 +33,10 @@ export default function CvPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
gsapContext?.resetTimeline()
|
|
||||||
const items = gsap?.utils.toArray<GSAPTweenTarget>('.gsapan');
|
const items = gsap?.utils.toArray<GSAPTweenTarget>('.gsapan');
|
||||||
let dir = Direction.Left;
|
let dir = Direction.Left;
|
||||||
items?.forEach(item => {
|
items?.forEach(item => {
|
||||||
gsapContext?.addAnimation(gsap.from(item, nextGsapConf(dir)))
|
gsapContext?.addAnimation(gsap.from(item, nextGsapConf(dir)),0)
|
||||||
if (dir == Direction.Down) {
|
if (dir == Direction.Down) {
|
||||||
dir = Direction.Left
|
dir = Direction.Left
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useGSAP } from "@gsap/react";
|
import { useGSAP } from "@gsap/react";
|
||||||
import { useRef } from "react";
|
import { 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 { useGsapContext } from "../_providers/GsapProvicer";
|
||||||
@@ -13,24 +14,17 @@ export default function MusicPage() {
|
|||||||
const container = useRef<HTMLDivElement>(null)
|
const container = useRef<HTMLDivElement>(null)
|
||||||
const gsapContext = useGsapContext();
|
const gsapContext = useGsapContext();
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
gsapContext?.resetTimeline()
|
gsapContext?.resumeTimeline()
|
||||||
const items = gsap.utils.toArray<HTMLElement>('.gsapan');
|
return () => {
|
||||||
const tl = gsap.timeline();
|
console.log("page cleanup")
|
||||||
items.map(item => {
|
gsapContext?.resetTimeline()
|
||||||
const player = item.querySelector('.player');
|
}
|
||||||
tl.from(
|
});
|
||||||
item, { x: -100, opacity: 0, duration: 0.5, ease: 'power2.inOut', scrollTrigger: item },'<'
|
|
||||||
).from(
|
|
||||||
player, { y: 10, opacity: 0, duration: 0.5, ease: 'power2.inOut' }
|
|
||||||
, '<0.3')
|
|
||||||
gsapContext?.addAnimation(tl);
|
|
||||||
})
|
|
||||||
}, { scope: container, dependencies: [isLoading] });
|
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<div ref={container} className="w-full h-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4">
|
<div ref={container} className="w-full h-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4">
|
||||||
<AnimatedPageTitle text="Just Some Music I Made"/>
|
<AnimatedPageTitle position={0} text="Just Some Music I Made"/>
|
||||||
<AnimateTextIn>
|
<AnimateTextIn position={0.5}>
|
||||||
<div className="flex flex-row h-8 content-center items-center">
|
<div className="flex flex-row h-8 content-center items-center">
|
||||||
<p className="mr-[1em]">All works on this page are licensed under:</p>
|
<p className="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>
|
||||||
@@ -40,10 +34,10 @@ export default function MusicPage() {
|
|||||||
<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>
|
||||||
</AnimateTextIn>
|
</AnimateTextIn>
|
||||||
{tracks && tracks.map((track) => (
|
{tracks && tracks.map((track,i) => (
|
||||||
<Card.Card key={track.id} className='gsapan'>
|
<Card.AnimatedCard key={track.id} position={i+1}>
|
||||||
<Card.CardHeader>
|
<Card.CardHeader>
|
||||||
<AnimateTextIn animation="slide">
|
<AnimateTextIn position={i+1.2} animation="slide">
|
||||||
<Card.CardTitle>{track.title}</Card.CardTitle>
|
<Card.CardTitle>{track.title}</Card.CardTitle>
|
||||||
</AnimateTextIn>
|
</AnimateTextIn>
|
||||||
</Card.CardHeader>
|
</Card.CardHeader>
|
||||||
@@ -55,7 +49,7 @@ export default function MusicPage() {
|
|||||||
Your browser does not support the audio element.
|
Your browser does not support the audio element.
|
||||||
</audio>
|
</audio>
|
||||||
</Card.CardContent>
|
</Card.CardContent>
|
||||||
</Card.Card>
|
</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 min-h-[200px] text-muted-foreground">
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import * as React from "react"
|
import { useGSAP } from "@gsap/react";import * as React from "react"
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
||||||
|
import gsap from 'gsap'
|
||||||
import { cn } from "~/lib/utils"
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
function Card({
|
function Card({
|
||||||
@@ -20,6 +22,35 @@ function Card({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AnimatedCard({
|
||||||
|
className,
|
||||||
|
position = 0,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { size?: "default" | "sm", position: gsap.Position }) {
|
||||||
|
const gsapContext = useGsapContext()
|
||||||
|
const ref = useRef<HTMLDivElement|null>(null)
|
||||||
|
useGSAP(() => {
|
||||||
|
gsapContext?.addAnimation(gsap.from(ref.current,{
|
||||||
|
x: -100,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.5
|
||||||
|
}),position)
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-slot="card"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl bg-opacity-60 backdrop-blur-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -100,4 +131,5 @@ export {
|
|||||||
CardAction,
|
CardAction,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
AnimatedCard
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
--radius-4xl: calc(var(--radius) * 2.6);
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
.dark {
|
||||||
--radius: 0;
|
--radius: 0;
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.141 0.005 285.823);
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
--foreground: oklch(0.141 0.005 285.823);
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
:root {
|
||||||
--background: oklch(0.141 0.005 285.823);
|
--background: oklch(0.141 0.005 285.823);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: oklch(0.21 0.006 285.885);
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
|||||||
Reference in New Issue
Block a user