animation stuff

This commit is contained in:
2026-03-30 14:13:04 +02:00
parent 9c5aec01e0
commit dfaba3a24e
9 changed files with 212 additions and 104 deletions

1
.gitignore vendored
View File

@@ -47,3 +47,4 @@ yarn-error.log*
# clerk configuration (can include secrets) # clerk configuration (can include secrets)
/.clerk/ /.clerk/
.worktrees .worktrees
.claudesession

View File

@@ -1,9 +1,20 @@
import { useGSAP } from "@gsap/react"; import { useGSAP } from "@gsap/react";
import { useRef, type ReactNode } from "react"; import { useRef, type HTMLAttributes, 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}) => { import { cn } from "~/lib/utils";
const AnimateTextIn = ({
children,
animation = "type",
position,
className
}: {
children: ReactNode,
animation?: "type" | "slide",
position: gsap.Position,
className?:HTMLAttributes<HTMLDivElement>['className']
}) => {
const el = useRef<HTMLDivElement>(null) const el = useRef<HTMLDivElement>(null)
const gsapContext = useGsapContext(); const gsapContext = useGsapContext();
useGSAP(() => { useGSAP(() => {
@@ -12,8 +23,8 @@ const AnimateTextIn = ({children,animation="type",position}:{children:ReactNode,
const chars = new SplitText(el.current, { type: 'chars' }) const chars = new SplitText(el.current, { type: 'chars' })
gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100, duration: 0 }), 0) gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100, duration: 0 }), 0)
const fromVars = animation === "slide" const fromVars = animation === "slide"
? {opacity:0, x:-10, duration: 0.2, stagger: {each: 0.08}, ease:'bounce.inOut'} ? { opacity: 0, x: -10, duration: 0.2, stagger: { each: 0.08 }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
: {opacity:0, duration: 0.01, stagger: {each: 0.04}, ease: 'bounce.inOut'} : { opacity: 0, duration: 0.01, stagger: { each: 0.04 }, ease: 'bounce.inOut', onComplete: () => chars.revert() }
if (isInView) { if (isInView) {
gsapContext?.addAnimation(gsap.from(chars.chars, fromVars), position) gsapContext?.addAnimation(gsap.from(chars.chars, fromVars), position)
} else { } else {
@@ -21,7 +32,7 @@ const AnimateTextIn = ({children,animation="type",position}:{children:ReactNode,
} }
}, { dependencies: [] }) }, { dependencies: [] })
return ( return (
<div ref={el} className="opacity-0"> <div ref={el} className={cn(className,"opacity-0")}>
{children} {children}
</div> </div>
) )

View File

@@ -0,0 +1,22 @@
import { type HTMLAttributes, type ReactNode } from "react";
import AnimatedDiv from "./AnimatedDiv";
import { cn } from "~/lib/utils";
const AnimatePopUp = ({
children,
position,
className,
duration=1,
ease='elastic'
}:{
children:ReactNode
position:gsap.Position,
className?:HTMLAttributes<HTMLDivElement>['className']
duration?:number,
ease?:gsap.EaseString|gsap.EaseFunction
}) => {
return (
<AnimatedDiv children={children} position={position} className={cn(className,'h-0 translate-y-[50] overflow-hidden')} height='auto' y={0} overflow='' ease={ease} duration={duration} />
)
}
export default AnimatePopUp;

View File

@@ -0,0 +1,41 @@
import gsap from "gsap";
import { type HTMLAttributes,
type ReactNode, useLayoutEffect, useRef } from "react";
import { useGsapContext } from "~/app/_providers/GsapProvicer";
const AnimatedDiv = (
{
children,
position,
className,
animationMode='to',
...tweenVars
}:
gsap.TweenVars & {
children:ReactNode,
position:gsap.Position,
animationMode?: 'from'|'to',
className?:HTMLAttributes<HTMLDivElement>['className']
}
) => {
const div = useRef<HTMLDivElement>(null);
const gsapContext = useGsapContext()
useLayoutEffect(() => {
let tween:gsap.core.Tween;
switch(animationMode) {
case 'from':
tween = gsap.from(div.current,tweenVars);
break;
case 'to':
tween = gsap.to(div.current,tweenVars);
break;
}
gsapContext?.addAnimation(tween,position)
},[])
return (
<div ref={div} className={className}>
{children}
</div>
)
}
export default AnimatedDiv;

View File

@@ -1,22 +1,21 @@
import { useGSAP } from "@gsap/react"; import { useEffect, useLayoutEffect, useRef } from "react"; import { useGSAP } from "@gsap/react"; import { useEffect, 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 AnimatedPageTitle = ( const AnimatedPageTitle = (
{ text, position }: { text: string, position:gsap.Position } { children, position }: { children: ReactNode, position:gsap.Position }
) => { ) => {
const el = useRef<HTMLHeadingElement>(null) const el = useRef<HTMLHeadingElement>(null)
const gsapContext = useGsapContext(); const gsapContext = useGsapContext();
useEffect(() => { useLayoutEffect(() => {
console.log("add animated title with:",position) const split = new SplitText(el.current, { type: "lines,chars", autoSplit:true })
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, { id: 'titlesplit',
stagger: 0.05, rotate: -90, opacity: 0, x: -10 stagger: 0.05, rotate: -90, opacity: 0, x: -10, onComplete: () => {split.revert()}
}),'>') }),'>')
},[]) },[])
return ( return (
<h1 className="text-4xl opacity-0 font-bold text-balance w-full" ref={el}> {text} </h1> <h1 className="text-4xl break-keep opacity-0 font-bold text-balance w-full" ref={el}> {children} </h1>
) )
} }

View File

@@ -7,7 +7,7 @@ import { ThemeSwitch } from "./ThemeSwitch"
export default function TopNav() { export default function TopNav() {
return ( return (
<div className="fixed lg:w-full right-0 z-50 lg:bg-background"> <div className="fixed backdrop-blur-md lg:w-full right-0 z-50">
<nav className="flex flex-col-reverse lg:flex-row flex-wrap w-20 lg:w-full outline-1 lg:h-10 h-full"> <nav className="flex flex-col-reverse lg:flex-row flex-wrap w-20 lg:w-full outline-1 lg:h-10 h-full">
<div className="flex flex-wrap lg:h-full w-20 lg:w-fit lg:flex-row"> <div className="flex flex-wrap lg:h-full w-20 lg:w-fit lg:flex-row">
<Button className="flex h-10 lg:h-full w-full lg:w-20" asChild variant="outline"> <Button className="flex h-10 lg:h-full w-full lg:w-20" asChild variant="outline">

View File

@@ -2,12 +2,13 @@
import { useGSAP } from '@gsap/react' 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, GSDevTools } from 'gsap/all'
import { createContext, useCallback, useContext, useEffect, useRef, 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)
gsap.registerPlugin(SplitText) gsap.registerPlugin(SplitText)
gsap.registerPlugin(GSDevTools)
const GsapContext = createContext<{ const GsapContext = createContext<{
addAnimation: ( addAnimation: (
animation: gsap.core.TimelineChild, animation: gsap.core.TimelineChild,
@@ -56,7 +57,10 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
}, []) }, [])
useGSAP(() => { useGSAP(() => {
if (!tl.current) { if (!tl.current) {
tl.current = gsap.timeline({ paused: true }) tl.current = gsap.timeline({ paused: true, id:'mainTimeline', onComplete: ()=>{
console.log('timeline revert')
gsap.getById('mainTimeline')?.revert()
} })
} }
return () => { console.log("gsap cleanup") } return () => { console.log("gsap cleanup") }
}) })

View File

@@ -6,26 +6,31 @@ 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"; import { ScrollArea } from "~/components/ui/scroll-area";
import AnimatePopUp from "../_components/Animated/AnimatePopUp";
export default function MusicPage() { export default function MusicPage() {
const { data: tracks, isLoading } = trpc.music.list.useQuery(); const { data: tracks, isLoading } = trpc.music.list.useQuery();
useTimeLine(tracks) useTimeLine(tracks)
return ( return (
<ScrollArea className="w-full h-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4"> <ScrollArea className="px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
<AnimatedPageTitle position={0} text="Just Some Music I Made" /> <AnimatedPageTitle position={0}><span>Just Some </span> <span>Music I Made</span> </AnimatedPageTitle>
<AnimateTextIn position={0.5}> <div className="flex flex-wrap h-fit content-center">
<div className="flex flex-col lg:flex-row h-fit content-center"> <AnimateTextIn className="flex flex-wrap mr-[1em]" position={0.5}>
<p className="break-after-avoid mr-[1em]">All works on this page are licensed under:</p> <div><p className="break-after-avoid mr-[1em]">All works on this page are licensed under:</p></div>
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a> <div><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div>
</AnimateTextIn>
<AnimatePopUp position={2} className="items-center content-center">
<div className="flex flex-row"> <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]" 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>
</AnimatePopUp>
</div> </div>
</AnimateTextIn> <div className="pt-10" />
{tracks && tracks.map((track, i) => ( {tracks && tracks.map((track, i) => (
<Card.AnimatedCard key={track.id} position={i + 1}> <div key={track.id}>
<Card.AnimatedCard position={i + 1}>
<Card.CardHeader> <Card.CardHeader>
<AnimateTextIn position={i + 1.2} animation="slide"> <AnimateTextIn position={i + 1.2} animation="slide">
<Card.CardTitle>{track.title}</Card.CardTitle> <Card.CardTitle>{track.title}</Card.CardTitle>
@@ -35,11 +40,15 @@ export default function MusicPage() {
{track.description && ( {track.description && (
<p className="text-sm text-muted-foreground gsapant">{track.description}</p> <p className="text-sm text-muted-foreground gsapant">{track.description}</p>
)} )}
<AnimatePopUp position={i + 1.3}>
<audio controls className="w-full player" src={track.fileUrl}> <audio controls className="w-full player" src={track.fileUrl}>
Your browser does not support the audio element. Your browser does not support the audio element.
</audio> </audio>
</AnimatePopUp>
</Card.CardContent> </Card.CardContent>
</Card.AnimatedCard> </Card.AnimatedCard>
<div className="pt-5" />
</div>
))} ))}
{!isLoading && !tracks?.length && {!isLoading && !tracks?.length &&
<div className="flex justify-center items-center text-muted-foreground"> <div className="flex justify-center items-center text-muted-foreground">

View File

@@ -5,10 +5,16 @@ import * as Card from "~/components/ui/card";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { StackBadge } from "~/components/StackBadge"; import { StackBadge } from "~/components/StackBadge";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import { ScrollArea } from "~/components/ui/scroll-area";
import AnimatedPageTitle from "../_components/Animated/AnimatedPageTitle";
import AnimateTextIn from "../_components/Animated/AnimateIn";
import { useTimeLine } from "../_providers/GsapProvicer";
import AnimatePopUp from "../_components/Animated/AnimatePopUp";
import { Button } from "~/components/ui/button";
export default function ProjectsPage() { export default function ProjectsPage() {
const { data: projects, isLoading } = trpc.projectv2.listWithStack.useQuery(); const { data: projects, isLoading } = trpc.projectv2.listWithStack.useQuery();
useTimeLine(projects)
if (isLoading) { if (isLoading) {
return ( return (
<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">
@@ -26,17 +32,22 @@ export default function ProjectsPage() {
} }
return ( return (
<div className="w-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4"> <ScrollArea className="px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
{projects.map((project) => ( <AnimatedPageTitle position={0}><span>Project I've Been</span><span> Working on</span> </AnimatedPageTitle>
<Card.Card key={project.id}> <div className="pt-10" />
{projects.map((project, i) => (
<div key={i}>
<Card.AnimatedCard position={i + 1.2} key={project.id}>
<Card.CardHeader> <Card.CardHeader>
<div className="flex items-start justify-between gap-2 flex-wrap"> <div className="flex items-start justify-between gap-2 flex-wrap">
<Card.CardTitle>{project.title}</Card.CardTitle> <AnimateTextIn position={i + 1.4} animation="slide"><Card.CardTitle>{project.title}</Card.CardTitle></AnimateTextIn>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{project.sourceType && ( {project.sourceType && (
<AnimatePopUp position={i + 2} duration={2}>
<Badge variant={project.sourceType === "open" ? "secondary" : "outline"}> <Badge variant={project.sourceType === "open" ? "secondary" : "outline"}>
{project.sourceType === "open" ? "Open Source" : "Closed Source"} {project.sourceType === "open" ? "Open Source" : "Closed Source"}
</Badge> </Badge>
</AnimatePopUp>
)} )}
{project.releaseStatus && ( {project.releaseStatus && (
<Badge variant={project.releaseStatus === "released" ? "default" : "outline"}> <Badge variant={project.releaseStatus === "released" ? "default" : "outline"}>
@@ -50,44 +61,54 @@ export default function ProjectsPage() {
<Card.CardContent className="flex flex-col gap-3"> <Card.CardContent className="flex flex-col gap-3">
{project.description && ( {project.description && (
<div className="prose prose-sm dark:prose-invert max-w-none text-muted-foreground"> <div className="prose prose-sm dark:prose-invert max-w-none text-muted-foreground">
<Markdown>{project.description}</Markdown> <AnimatePopUp position={i + 1.4} duration={project.description.length / 20}>
<AnimateTextIn position={i + 1.5} animation="slide"><Markdown>{project.description}</Markdown></AnimateTextIn></AnimatePopUp>
</div>
)}
<div className="flex flex-row">
{project.techStack?.stackItems && project.techStack.stackItems.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{project.techStack.stackItems.map((item, k) => (
<AnimatePopUp key={k} position={(i + 2) + k * 0.5}> <StackBadge key={item} item={item} /> </AnimatePopUp>
))}
</div> </div>
)} )}
{(project.sourceLink || project.releaseLink) && ( {(project.sourceLink || project.releaseLink) && (
<div className="flex gap-3 flex-wrap"> <div className="ml-auto flex-col lg:flex-row justify-center gap-5">
{project.sourceLink && ( {project.sourceLink &&
<Button variant='outline' className="cursor-pointer mb-3 lg:mb-0 lg:mr-3 min-w-18">
<a <a
href={project.sourceLink} href={project.sourceLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors" className='items-center'
> >
Source Source
</a> </a>
)} </Button>
{project.releaseLink && ( }
{project.releaseLink &&
<Button variant='default' className="cursor-pointer min-w-18 items-center">
<a <a
href={project.releaseLink} href={project.releaseLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors" className='items-center'
> >
Live Live
</a> </a>
)} </Button>
}
</div> </div>
)} )}
{project.techStack?.stackItems && project.techStack.stackItems.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{project.techStack.stackItems.map((item) => (
<StackBadge key={item} item={item} />
))}
</div> </div>
)}
</Card.CardContent> </Card.CardContent>
)} )}
</Card.Card> </Card.AnimatedCard>
))} <div className="pt-5" />
</div> </div>
))}
</ScrollArea>
); );
} }