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/
.worktrees
.claudesession

View File

@@ -1,30 +1,41 @@
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 { SplitText } from "gsap/SplitText";
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 gsapContext = useGsapContext();
useGSAP(() => {
const rect = el.current?.getBoundingClientRect()
const isInView = rect && rect.top < window.innerHeight
const chars = new SplitText(el.current,{type:'chars'})
gsapContext?.addAnimation(gsap.to(el.current,{opacity:100, duration:0}),0)
const chars = new SplitText(el.current, { type: 'chars' })
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'}
? { 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', onComplete: () => chars.revert() }
if (isInView) {
gsapContext?.addAnimation(gsap.from(chars.chars, fromVars),position)
gsapContext?.addAnimation(gsap.from(chars.chars, fromVars), position)
} else {
gsap.from(chars.chars, { ...fromVars, scrollTrigger: { trigger: el.current, start: 'top 85%', scroller: gsapContext?.getScroller() } })
}
}, { dependencies: [] })
return (
<div ref={el} className="opacity-0">
<div ref={el} className={cn(className,"opacity-0")}>
{children}
</div>
)
)
}
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 { SplitText } from "gsap/SplitText";
import gsap from 'gsap'
const AnimatedPageTitle = (
{ text, position }: { text: string, position:gsap.Position }
{ children, position }: { children: ReactNode, position:gsap.Position }
) => {
const el = useRef<HTMLHeadingElement>(null)
const gsapContext = useGsapContext();
useEffect(() => {
console.log("add animated title with:",position)
const split = new SplitText(el.current, { type: "chars" })
useLayoutEffect(() => {
const split = new SplitText(el.current, { type: "lines,chars", autoSplit:true })
gsapContext?.addAnimation(gsap.to(el.current, { opacity: 100 }),position)
gsapContext?.addAnimation(gsap.from(split.chars, {
stagger: 0.05, rotate: -90, opacity: 0, x: -10
gsapContext?.addAnimation(gsap.from(split.chars, { id: 'titlesplit',
stagger: 0.05, rotate: -90, opacity: 0, x: -10, onComplete: () => {split.revert()}
}),'>')
},[])
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() {
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">
<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">

View File

@@ -2,12 +2,13 @@
import { useGSAP } from '@gsap/react'
import gsap from 'gsap'
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'
gsap.registerPlugin(useGSAP)
gsap.registerPlugin(ScrollTrigger)
gsap.registerPlugin(SplitText)
gsap.registerPlugin(GSDevTools)
const GsapContext = createContext<{
addAnimation: (
animation: gsap.core.TimelineChild,
@@ -56,7 +57,10 @@ export default function GsapProvider({ children }: { children: ReactNode }) {
}, [])
useGSAP(() => {
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") }
})

View File

@@ -1,45 +1,54 @@
'use client'
import { trpc } from "~/app/_trpc/Client";
import * as Card from "~/components/ui/card";
import { useTimeLine } 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";
import AnimatePopUp from "../_components/Animated/AnimatePopUp";
export default function MusicPage() {
const { data: tracks, isLoading } = trpc.music.list.useQuery();
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-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>
<ScrollArea className="px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
<AnimatedPageTitle position={0}><span>Just Some </span> <span>Music I Made</span> </AnimatedPageTitle>
<div className="flex flex-wrap h-fit content-center">
<AnimateTextIn className="flex flex-wrap mr-[1em]" position={0.5}>
<div><p className="break-after-avoid mr-[1em]">All works on this page are licensed under:</p></div>
<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">
<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/nc.svg" alt="" />
<img className="max-w-[1em] ml-[1em]" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" />
</div>
</div>
</AnimateTextIn>
</AnimatePopUp>
</div>
<div className="pt-10" />
{tracks && tracks.map((track, i) => (
<Card.AnimatedCard key={track.id} position={i + 1}>
<Card.CardHeader>
<AnimateTextIn position={i + 1.2} animation="slide">
<Card.CardTitle>{track.title}</Card.CardTitle>
</AnimateTextIn>
</Card.CardHeader>
<Card.CardContent className="flex flex-col gap-3">
{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.
</audio>
</Card.CardContent>
</Card.AnimatedCard>
<div key={track.id}>
<Card.AnimatedCard position={i + 1}>
<Card.CardHeader>
<AnimateTextIn position={i + 1.2} animation="slide">
<Card.CardTitle>{track.title}</Card.CardTitle>
</AnimateTextIn>
</Card.CardHeader>
<Card.CardContent className="flex flex-col gap-3">
{track.description && (
<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}>
Your browser does not support the audio element.
</audio>
</AnimatePopUp>
</Card.CardContent>
</Card.AnimatedCard>
<div className="pt-5" />
</div>
))}
{!isLoading && !tracks?.length &&
<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 { StackBadge } from "~/components/StackBadge";
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() {
const { data: projects, isLoading } = trpc.projectv2.listWithStack.useQuery();
useTimeLine(projects)
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-[200px] text-muted-foreground">
@@ -26,68 +32,83 @@ export default function ProjectsPage() {
}
return (
<div className="w-full max-w-4xl mx-auto px-4 py-8 flex flex-col gap-4">
{projects.map((project) => (
<Card.Card key={project.id}>
<Card.CardHeader>
<div className="flex items-start justify-between gap-2 flex-wrap">
<Card.CardTitle>{project.title}</Card.CardTitle>
<div className="flex gap-2 flex-wrap">
{project.sourceType && (
<Badge variant={project.sourceType === "open" ? "secondary" : "outline"}>
{project.sourceType === "open" ? "Open Source" : "Closed Source"}
</Badge>
)}
{project.releaseStatus && (
<Badge variant={project.releaseStatus === "released" ? "default" : "outline"}>
{project.releaseStatus === "released" ? "Released" : "Unreleased"}
</Badge>
)}
<ScrollArea className="px-10 lg:px-0 w-full h-full max-w-4xl mx-auto pt-10">
<AnimatedPageTitle position={0}><span>Project I've Been</span><span> Working on</span> </AnimatedPageTitle>
<div className="pt-10" />
{projects.map((project, i) => (
<div key={i}>
<Card.AnimatedCard position={i + 1.2} key={project.id}>
<Card.CardHeader>
<div className="flex items-start justify-between gap-2 flex-wrap">
<AnimateTextIn position={i + 1.4} animation="slide"><Card.CardTitle>{project.title}</Card.CardTitle></AnimateTextIn>
<div className="flex gap-2 flex-wrap">
{project.sourceType && (
<AnimatePopUp position={i + 2} duration={2}>
<Badge variant={project.sourceType === "open" ? "secondary" : "outline"}>
{project.sourceType === "open" ? "Open Source" : "Closed Source"}
</Badge>
</AnimatePopUp>
)}
{project.releaseStatus && (
<Badge variant={project.releaseStatus === "released" ? "default" : "outline"}>
{project.releaseStatus === "released" ? "Released" : "Unreleased"}
</Badge>
)}
</div>
</div>
</div>
</Card.CardHeader>
{(project.description || project.sourceLink || project.releaseLink || project.techStack?.stackItems?.length) && (
<Card.CardContent className="flex flex-col gap-3">
{project.description && (
<div className="prose prose-sm dark:prose-invert max-w-none text-muted-foreground">
<Markdown>{project.description}</Markdown>
</div>
)}
{(project.sourceLink || project.releaseLink) && (
<div className="flex gap-3 flex-wrap">
{project.sourceLink && (
<a
href={project.sourceLink}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors"
>
Source
</a>
</Card.CardHeader>
{(project.description || project.sourceLink || project.releaseLink || project.techStack?.stackItems?.length) && (
<Card.CardContent className="flex flex-col gap-3">
{project.description && (
<div className="prose prose-sm dark:prose-invert max-w-none text-muted-foreground">
<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>
)}
{project.releaseLink && (
<a
href={project.releaseLink}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 transition-colors"
>
Live
</a>
{(project.sourceLink || project.releaseLink) && (
<div className="ml-auto flex-col lg:flex-row justify-center gap-5">
{project.sourceLink &&
<Button variant='outline' className="cursor-pointer mb-3 lg:mb-0 lg:mr-3 min-w-18">
<a
href={project.sourceLink}
target="_blank"
rel="noopener noreferrer"
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>
)}
{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>
)}
</Card.CardContent>
)}
</Card.Card>
</Card.CardContent>
)}
</Card.AnimatedCard>
<div className="pt-5" />
</div>
))}
</div>
</ScrollArea>
);
}