From ca2a31270b0fec912acebafcf5bd9b020ee36f89 Mon Sep 17 00:00:00 2001 From: v4ltages Date: Sat, 2 May 2026 20:39:23 +0300 Subject: [PATCH] Animated logo --- src/app/globals.css | 274 +++++++++++++------------ src/components/AnimatedTipilanLogo.tsx | 76 ++++--- src/components/HeroSection.tsx | 28 ++- 3 files changed, 207 insertions(+), 171 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 9dc9a20..8c05cb9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,170 +1,174 @@ -@import 'tailwindcss'; +@import "tailwindcss"; @import "tw-animate-css"; @custom-variant dark (&:is(.dark *)); @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); - --color-card: var(--card); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } @theme { - --breakpoint-xs: 30rem; + --breakpoint-xs: 30rem; } body { - font-family: "Work Sans", Arial, Helvetica, sans-serif; - max-width: 100vw; - padding: 0; - margin: 0; + font-family: "Work Sans", Arial, Helvetica, sans-serif; + max-width: 100vw; + padding: 0; + margin: 0; } :root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } .dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); } @layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } } .material-symbols-outlined { - font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 24; + font-variation-settings: + "FILL" 1, + "wght" 700, + "GRAD" 0, + "opsz" 24; } .tipilan-logo-letter { - animation: tipilan-logo-letter-in 720ms cubic-bezier(0.16, 1, 0.3, 1) both; - animation-delay: var(--tipilan-logo-letter-delay, 0ms); - opacity: 0; - transform: translate3d(0, 100%, 0); - will-change: opacity, transform; + animation: tipilan-logo-letter-in 720ms cubic-bezier(0.16, 1, 0.3, 1) both; + animation-delay: var(--tipilan-logo-letter-delay, 0ms); + opacity: 0; + transform: translate3d(0, 100%, 0); + will-change: opacity, transform; } @keyframes tipilan-logo-letter-in { - 0% { - opacity: 0; - transform: translate3d(0, 100%, 0); - } + 0% { + opacity: 0; + transform: translate3d(0, 100%, 0); + } - 70% { - opacity: 1; - } + 70% { + opacity: 1; + } - 100% { - opacity: 1; - transform: translate3d(0, 0, 0); - } + 100% { + opacity: 1; + transform: translate3d(0, 0, 0); + } } @media (prefers-reduced-motion: reduce) { - .tipilan-logo-letter { - animation: none; - opacity: 1; - transform: none; - will-change: auto; - } + .tipilan-logo-letter { + animation: none; + opacity: 1; + transform: none; + will-change: auto; + } } diff --git a/src/components/AnimatedTipilanLogo.tsx b/src/components/AnimatedTipilanLogo.tsx index de91ffb..43a4660 100644 --- a/src/components/AnimatedTipilanLogo.tsx +++ b/src/components/AnimatedTipilanLogo.tsx @@ -1,8 +1,8 @@ "use client"; import Image from "next/image"; -import type { AnimationEvent, CSSProperties } from "react"; -import { useEffect, useState } from "react"; +import type { CSSProperties } from "react"; +import { useEffect, useRef, useState } from "react"; const LOGO_WIDTH = 2092; const LOGO_HEIGHT = 300; @@ -18,24 +18,53 @@ const logoLetters = [ ] as const; export default function AnimatedTipilanLogo() { - const [isAnimationComplete, setIsAnimationComplete] = useState(false); + const [isReducedMotion, setIsReducedMotion] = useState(false); + const [loadedCount, setLoadedCount] = useState(0); + const [shouldAnimate, setShouldAnimate] = useState(false); + const loadedLetterIndicesRef = useRef>(new Set()); useEffect(() => { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { - setIsAnimationComplete(true); + setIsReducedMotion(true); } }, []); - if (isAnimationComplete) { + useEffect(() => { + if (loadedCount >= logoLetters.length) { + const rafId = window.requestAnimationFrame(() => { + setShouldAnimate(true); + }); + + return () => window.cancelAnimationFrame(rafId); + } + + return undefined; + }, [loadedCount]); + + const handleLetterLoad = (index: number) => { + if (loadedLetterIndicesRef.current.has(index)) { + return; + } + + loadedLetterIndicesRef.current.add(index); + setLoadedCount(loadedLetterIndicesRef.current.size); + }; + + if (isReducedMotion) { return ( - TipiLAN Logo +
+ TipiLAN Logo +
); } @@ -47,8 +76,6 @@ export default function AnimatedTipilanLogo() { style={{ aspectRatio: `${LOGO_WIDTH} / ${LOGO_HEIGHT}` }} > {logoLetters.map((letter, index) => { - const isLastLetter = index === logoLetters.length - 1; - return ( ) => { - if (event.animationName === "tipilan-logo-letter-in") { - setIsAnimationComplete(true); - } - } - : undefined - } + className={`absolute top-0 h-full object-fill ${shouldAnimate ? "tipilan-logo-letter" : ""}`} + onLoad={() => handleLetterLoad(index)} + onError={() => handleLetterLoad(index)} style={ { left: `${(letter.x / LOGO_WIDTH) * 100}%`, width: `${(letter.width / LOGO_WIDTH) * 100}%`, zIndex: logoLetters.length - index, "--tipilan-logo-letter-delay": `${index * 90}ms`, + ...(shouldAnimate + ? {} + : { + opacity: 0, + transform: "translate3d(0, 100%, 0)", + }), } as CSSProperties } /> diff --git a/src/components/HeroSection.tsx b/src/components/HeroSection.tsx index e6254f7..f5efe5d 100644 --- a/src/components/HeroSection.tsx +++ b/src/components/HeroSection.tsx @@ -8,7 +8,7 @@ export default function HeroSection() { const t = useTranslations("home"); return ( -
+
{/* Background image */} {/* Content */} -
+
{/* Left: logo + info + CTA */} -
+
-
+

{t("hero.date")}

@@ -35,23 +35,25 @@ export default function HeroSection() {
{t("hero.buyTicket")}
{/* Right: prize pool + award */} -
-
-

+

+
+

{t("hero.prizePool")}

10 000€

-
+
TalTech student award -

- {t("hero.awardPrefix")} {t("hero.awardHighlight")} {t("hero.awardSuffix")} +

+ {t("hero.awardPrefix")}{" "} + {t("hero.awardHighlight")}{" "} + {t("hero.awardSuffix")}