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,30 +1,41 @@
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(() => {
const rect = el.current?.getBoundingClientRect() const rect = el.current?.getBoundingClientRect()
const isInView = rect && rect.top < window.innerHeight const isInView = rect && rect.top < window.innerHeight
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 {
gsap.from(chars.chars, { ...fromVars, scrollTrigger: { trigger: el.current, start: 'top 85%', scroller: gsapContext?.getScroller() } }) gsap.from(chars.chars, { ...fromVars, scrollTrigger: { trigger: el.current, start: 'top 85%', scroller: gsapContext?.getScroller() } })
} }
}, { dependencies: [] }) }, { dependencies: [] })
return ( return (
<div ref={el} className="opacity-0"> <div ref={el} className={cn(className,"opacity-0")}>
{children} {children}
</div> </div>
) )
} }
export default AnimateTextIn; export default AnimateTextIn;

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

@@ -1,45 +1,54 @@
'use client' 'use client'
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 { useTimeLine } 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"; 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>
</div> </AnimatePopUp>
</AnimateTextIn> </div>
<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.CardHeader> <Card.AnimatedCard position={i + 1}>
<AnimateTextIn position={i + 1.2} animation="slide"> <Card.CardHeader>
<Card.CardTitle>{track.title}</Card.CardTitle> <AnimateTextIn position={i + 1.2} animation="slide">
</AnimateTextIn> <Card.CardTitle>{track.title}</Card.CardTitle>
</Card.CardHeader> </AnimateTextIn>
<Card.CardContent className="flex flex-col gap-3"> </Card.CardHeader>
{track.description && ( <Card.CardContent className="flex flex-col gap-3">
<p className="text-sm text-muted-foreground gsapant">{track.description}</p> {track.description && (
)} <p className="text-sm text-muted-foreground gsapant">{track.description}</p>
<audio controls className="w-full player" src={track.fileUrl}> )}
Your browser does not support the audio element. <AnimatePopUp position={i + 1.3}>
</audio> <audio controls className="w-full player" src={track.fileUrl}>
</Card.CardContent> Your browser does not support the audio element.
</Card.AnimatedCard> </audio>
</AnimatePopUp>
</Card.CardContent>
</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,68 +32,83 @@ 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" />
<Card.CardHeader> {projects.map((project, i) => (
<div className="flex items-start justify-between gap-2 flex-wrap"> <div key={i}>
<Card.CardTitle>{project.title}</Card.CardTitle> <Card.AnimatedCard position={i + 1.2} key={project.id}>
<div className="flex gap-2 flex-wrap"> <Card.CardHeader>
{project.sourceType && ( <div className="flex items-start justify-between gap-2 flex-wrap">
<Badge variant={project.sourceType === "open" ? "secondary" : "outline"}> <AnimateTextIn position={i + 1.4} animation="slide"><Card.CardTitle>{project.title}</Card.CardTitle></AnimateTextIn>
{project.sourceType === "open" ? "Open Source" : "Closed Source"} <div className="flex gap-2 flex-wrap">
</Badge> {project.sourceType && (
)} <AnimatePopUp position={i + 2} duration={2}>
{project.releaseStatus && ( <Badge variant={project.sourceType === "open" ? "secondary" : "outline"}>
<Badge variant={project.releaseStatus === "released" ? "default" : "outline"}> {project.sourceType === "open" ? "Open Source" : "Closed Source"}
{project.releaseStatus === "released" ? "Released" : "Unreleased"} </Badge>
</Badge> </AnimatePopUp>
)} )}
{project.releaseStatus && (
<Badge variant={project.releaseStatus === "released" ? "default" : "outline"}>
{project.releaseStatus === "released" ? "Released" : "Unreleased"}
</Badge>
)}
</div>
</div> </div>
</div> </Card.CardHeader>
</Card.CardHeader> {(project.description || project.sourceLink || project.releaseLink || project.techStack?.stackItems?.length) && (
{(project.description || project.sourceLink || project.releaseLink || project.techStack?.stackItems?.length) && ( <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"> <AnimatePopUp position={i + 1.4} duration={project.description.length / 20}>
<Markdown>{project.description}</Markdown> <AnimateTextIn position={i + 1.5} animation="slide"><Markdown>{project.description}</Markdown></AnimateTextIn></AnimatePopUp>
</div> </div>
)} )}
{(project.sourceLink || project.releaseLink) && ( <div className="flex flex-row">
<div className="flex gap-3 flex-wrap"> {project.techStack?.stackItems && project.techStack.stackItems.length > 0 && (
{project.sourceLink && ( <div className="flex flex-wrap gap-1.5">
<a {project.techStack.stackItems.map((item, k) => (
href={project.sourceLink} <AnimatePopUp key={k} position={(i + 2) + k * 0.5}> <StackBadge key={item} item={item} /> </AnimatePopUp>
target="_blank" ))}
rel="noopener noreferrer" </div>
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors"
>
Source
</a>
)} )}
{project.releaseLink && ( {(project.sourceLink || project.releaseLink) && (
<a <div className="ml-auto flex-col lg:flex-row justify-center gap-5">
href={project.releaseLink} {project.sourceLink &&
target="_blank" <Button variant='outline' className="cursor-pointer mb-3 lg:mb-0 lg:mr-3 min-w-18">
rel="noopener noreferrer" <a
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors" href={project.sourceLink}
> target="_blank"
Live rel="noopener noreferrer"
</a> className='items-center'
>
Source
</a>
</Button>
}
{project.releaseLink &&
<Button variant='default' className="cursor-pointer min-w-18 items-center">
<a
href={project.releaseLink}
target="_blank"
rel="noopener noreferrer"
className='items-center'
>
Live
</a>
</Button>
}
</div>
)} )}
</div> </div>
)} </Card.CardContent>
{project.techStack?.stackItems && project.techStack.stackItems.length > 0 && ( )}
<div className="flex flex-wrap gap-1.5"> </Card.AnimatedCard>
{project.techStack.stackItems.map((item) => ( <div className="pt-5" />
<StackBadge key={item} item={item} /> </div>
))}
</div>
)}
</Card.CardContent>
)}
</Card.Card>
))} ))}
</div> </ScrollArea>
); );
} }