GSAP optimisation

This commit is contained in:
Debosmit Majumder
2026-04-28 14:30:03 +05:30
parent 486cc2d5db
commit 90125fde21
9 changed files with 160 additions and 295 deletions

View File

@@ -1,25 +1,28 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "@autumn/website",
"dependencies": {
"@gsap/react": "^2.1.2",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^16.2.4",
"@types/react": "^19.2.14",
"@vercel/analytics": "^2.0.1",
"framer-motion": "^12.38.0",
"gray-matter": "^4.0.3",
"gsap": "^3.15.0",
"lottie-react": "^2.4.1",
"lottie-web": "^5.13.0",
"matter-js": "^0.20.0",
"motion": "^12.38.0",
"next": "16.2.4",
"next-mdx-remote": "^6.0.0",
"ngrok": "^5.0.0-beta.2",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-syntax-highlighter": "^16.1.1",
"remark-frontmatter": "^5.0.0",
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.2",
@@ -159,6 +162,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@mdx-js/loader": ["@mdx-js/loader@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "source-map": "^0.7.0" }, "peerDependencies": { "webpack": ">=5" }, "optionalPeers": ["webpack"] }, "sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ=="],
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
@@ -169,6 +174,8 @@
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.1", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA=="],
"@next/mdx": ["@next/mdx@16.2.4", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-e/3bgla+/oF3vDlndI0eFPa0bnP47HPVA0InsAJi7Jr3DwV8WpEGuOcm/3PdI5/93FfNiBhMVeVHZpm1sFlmJw=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ=="],
@@ -809,6 +816,8 @@
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
"mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="],
"mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
@@ -831,6 +840,8 @@
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
"micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="],
"micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="],
"micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="],
@@ -907,8 +918,6 @@
"next": ["next@16.2.4", "", { "dependencies": { "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.4", "@next/swc-darwin-x64": "16.2.4", "@next/swc-linux-arm64-gnu": "16.2.4", "@next/swc-linux-arm64-musl": "16.2.4", "@next/swc-linux-x64-gnu": "16.2.4", "@next/swc-linux-x64-musl": "16.2.4", "@next/swc-win32-arm64-msvc": "16.2.4", "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": "dist/bin/next" }, "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q=="],
"next-mdx-remote": ["next-mdx-remote@6.0.0", "", { "dependencies": { "@babel/code-frame": "^7.23.5", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", "unist-util-remove": "^4.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.1", "vfile-matter": "^5.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-cJEpEZlgD6xGjB4jL8BnI8FaYdN9BzZM4NwadPe1YQr7pqoWjg9EBCMv3nXBkuHqMRfv2y33SzUsuyNh9LFAQQ=="],
"ngrok": ["ngrok@5.0.0-beta.2", "", { "dependencies": { "extract-zip": "^2.0.1", "got": "^11.8.5", "lodash.clonedeep": "^4.5.0", "uuid": "^7.0.0 || ^8.0.0", "yaml": "^2.2.2" }, "optionalDependencies": { "hpagent": "^0.1.2" }, "bin": "bin/ngrok" }, "sha512-UzsyGiJ4yTTQLCQD11k1DQaMwq2/SsztBg2b34zAqcyjS25qjDpogMKPaCKHwe/APRTHeel3iDXcVctk5CNaCQ=="],
"node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
@@ -1005,6 +1014,8 @@
"rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="],
"remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="],
"remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="],
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
@@ -1137,8 +1148,6 @@
"unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="],
"unist-util-remove": ["unist-util-remove@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
@@ -1157,8 +1166,6 @@
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-matter": ["vfile-matter@5.0.1", "", { "dependencies": { "vfile": "^6.0.0", "yaml": "^2.0.0" } }, "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -1217,6 +1224,10 @@
"is-bun-module/semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"mdast-util-frontmatter/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"micromark-extension-frontmatter/fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="],
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],

View File

@@ -1,14 +1,22 @@
"use client";
import gsap from "gsap";
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
import type { PixelHoverHandle, PixelIconComponent } from "@/lib/types";
import { getGsap } from "@/lib/lazyGsap";
type GSAPTimeline = {
play: () => GSAPTimeline;
reverse: () => GSAPTimeline;
kill: () => void;
// biome-ignore lint/suspicious/noExplicitAny: structural shim for GSAP's overloaded .to() signature
to: (...args: any[]) => GSAPTimeline;
};
export const DashboardIconPixel = forwardRef<
PixelHoverHandle,
{ Icon: PixelIconComponent }
>(function DashboardIconPixel({ Icon }, ref) {
const iconRef = useRef<SVGSVGElement | null>(null);
const tlRef = useRef<gsap.core.Timeline | null>(null);
const tlRef = useRef<GSAPTimeline | null>(null);
useImperativeHandle(ref, () => ({
restart: () => tlRef.current?.play(),
@@ -16,47 +24,40 @@ export const DashboardIconPixel = forwardRef<
}));
useEffect(() => {
const pixelEls =
iconRef.current?.querySelectorAll<SVGGraphicsElement>(".icon-pixel-path");
if (!pixelEls?.length) return;
const el = iconRef.current;
if (!el) return;
let cancelled = false;
// Sort pixels by visual position: bottom-left → top-right diagonal
const pixels = Array.from(pixelEls).sort((a, b) => {
const aBox = a.getBBox();
const bBox = b.getBBox();
return (
aBox.x +
aBox.width / 2 -
(aBox.y + aBox.height / 2) -
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
);
});
getGsap().then((gsap) => {
if (cancelled) return;
const pixelEls = el.querySelectorAll<SVGGraphicsElement>(".icon-pixel-path");
if (!pixelEls.length) return;
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "left bottom",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current
.to(pixels, {
opacity: 1,
scale: 1.15,
fill: "#FFFFFF",
duration: 0.2,
stagger: 0.01,
ease: "power2.out",
})
.to(pixels, {
scale: 1,
duration: 0.01,
ease: "back.out(3)",
// Sort pixels by visual position: bottom-left → top-right diagonal
const pixels = Array.from(pixelEls).sort((a, b) => {
const aBox = a.getBBox();
const bBox = b.getBBox();
return (
aBox.x + aBox.width / 2 - (aBox.y + aBox.height / 2) -
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
);
});
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "left bottom",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current!
.to(pixels, { opacity: 1, scale: 1.15, fill: "#FFFFFF", duration: 0.2, stagger: 0.01, ease: "power2.out" })
.to(pixels, { scale: 1, duration: 0.01, ease: "back.out(3)" });
});
return () => {
cancelled = true;
tlRef.current?.kill();
};
}, []);

View File

@@ -1,5 +1,5 @@
"use client";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
import { motion, useMotionValue, useSpring, useTransform } from "motion/react";
import { useEffect, useRef, useState } from "react";
import type { LayoutProps } from "@/lib/types";
import AnimatedFooterImage from "./animated-footer-image";

View File

@@ -1,5 +1,5 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { AnimatedPlusMinus, faqData } from "@/app/constant";
import { cn } from "@/lib/utils";

View File

@@ -1,13 +1,12 @@
"use client";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { motion } from "motion/react";
import dynamic from "next/dynamic";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { CTALines, IconCTADocs, IconCTAStart } from "@/app/constant";
import { getGsap } from "@/lib/lazyGsap";
// `AutumnConfig` pulls in `react-syntax-highlighter` + `highlight.js` (~100KB
// gzipped + meaningful parse cost on mobile). It only renders on `xl+`
@@ -88,85 +87,44 @@ export default function Hero() {
};
}, []);
useGSAP(
() => {
gsap.set(".hero-root", { opacity: 0 });
useEffect(() => {
const container = containerRef.current;
if (!container) return;
gsap.set(".hero-bg", {
opacity: 0,
filter: "blur(6px) brightness(1)",
scale: 0.97,
transformOrigin: "center top",
});
let ctx: { revert: () => void } | null = null;
let cancelled = false;
gsap.set(".hero-reveal", {
opacity: 0,
y: 25,
filter: "blur(12px)",
scale: 0.96,
transformOrigin: "center bottom",
});
getGsap().then((gsap) => {
if (cancelled) return;
ctx = gsap.context(() => {
gsap.set(".hero-reveal", {
opacity: 0,
y: 25,
scale: 0.96,
transformOrigin: "center bottom",
});
gsap.set(".hero-cta", { opacity: 0, scale: 0.95 });
gsap.set(".hero-cta", { opacity: 0, scale: 0.95 });
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
const tl = gsap.timeline({
defaults: { overwrite: "auto" },
});
// hero-bg is intentionally NOT hidden — it is the LCP element and
// must be visible from first paint. The brightness flash still runs.
tl.to(".hero-bg", { filter: "brightness(1.6)", duration: 0.125, ease: "power2.in" })
.to(".hero-bg", { filter: "brightness(1)", duration: 0.125, ease: "power2.out" })
.to(".hero-reveal", { opacity: 1, y: 0, scale: 1, duration: 1.1, stagger: 0.1, ease: "power3.out" }, "-=0.2")
.to(".hero-cta", { opacity: 1, scale: 1, duration: 0.3, stagger: 0.06, ease: "back.out(1.5)" }, "-=0.1");
}, container);
});
tl.to(".hero-root", { opacity: 1, duration: 0.3, ease: "none" })
.to(".hero-bg", {
opacity: 1,
filter: "blur(0px) brightness(1)",
scale: 1,
duration: 0.4,
ease: "power2.out",
})
.to(".hero-bg", {
filter: "blur(0px) brightness(1.6)",
duration: 0.125,
ease: "power2.in",
})
.to(".hero-bg", {
filter: "blur(0px) brightness(1)",
duration: 0.125,
ease: "power2.out",
})
.to(
".hero-reveal",
{
opacity: 1,
y: 0,
filter: "blur(0px)",
scale: 1,
duration: 1.1,
stagger: 0.1,
ease: "power3.out",
},
"-=0.2",
)
.to(
".hero-cta",
{
opacity: 1,
scale: 1,
duration: 0.3,
stagger: 0.06,
ease: "back.out(1.5)",
},
"-=0.1",
);
},
{ scope: containerRef },
);
return () => {
cancelled = true;
ctx?.revert();
};
}, []);
return (
<div ref={containerRef}>
<div className="relative hero-root opacity-0 flex flex-col items-stretch pb-0 md:pb-12 mb-0 bg-[#0F0F0F]">
<div className="relative hero-root flex flex-col items-stretch pb-0 md:pb-12 mb-0 bg-[#0F0F0F]">
<div className="flex justify-between">
<div className="flex flex-col gap-6 px-4 xl:px-22.75 py-8 bg-[#0F0F0F] mt-26">
<h4 className="hero-reveal relative uppercase font-mono tracking-[-2%] text-[12px] md:text-sm leading-sm text-white md:text-[#FFFFFF99] bg-[#2c2c2d] w-fit p-2 min-h-[30px] md:min-h-[36px] flex items-center">
@@ -305,6 +263,7 @@ export default function Hero() {
aria-hidden="true"
fill
priority
fetchPriority="high"
sizes="100vw"
className="hero-bg absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 pointer-events-none select-none"
/>

View File

@@ -1,7 +1,5 @@
"use client";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
@@ -29,6 +27,7 @@ import type {
PixelIconComponent,
} from "@/lib/types";
import { cn } from "@/lib/utils";
import { getGsap } from "@/lib/lazyGsap";
import { DashboardIconPixel } from "./dashboard-icon-pixel";
const NAV_LINKS = [
@@ -42,10 +41,18 @@ const NAV_LINKS = [
},
];
type GSAPTimeline = {
play: () => GSAPTimeline;
reverse: () => GSAPTimeline;
kill: () => void;
// biome-ignore lint/suspicious/noExplicitAny: structural shim for GSAP's overloaded .to() signature
to: (...args: any[]) => GSAPTimeline;
};
const NavIconPixel = forwardRef<PixelHoverHandle, { Icon: PixelIconComponent }>(
function NavIconPixel({ Icon }, ref) {
const iconRef = useRef<SVGSVGElement | null>(null);
const tlRef = useRef<gsap.core.Timeline | null>(null);
const tlRef = useRef<GSAPTimeline | null>(null);
useImperativeHandle(ref, () => ({
restart: () => tlRef.current?.play(),
@@ -53,46 +60,39 @@ const NavIconPixel = forwardRef<PixelHoverHandle, { Icon: PixelIconComponent }>(
}));
useEffect(() => {
const pixelEls =
iconRef.current?.querySelectorAll<SVGPathElement>(".icon-pixel-path");
if (!pixelEls?.length) return;
const el = iconRef.current;
if (!el) return;
let cancelled = false;
const pixels = Array.from(pixelEls).sort((a, b) => {
const aBox = a.getBBox();
const bBox = b.getBBox();
return (
aBox.x +
aBox.width / 2 -
(aBox.y + aBox.height / 2) -
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
);
});
getGsap().then((gsap) => {
if (cancelled) return;
const pixelEls = el.querySelectorAll<SVGPathElement>(".icon-pixel-path");
if (!pixelEls.length) return;
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "left bottom",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current
.to(pixels, {
opacity: 1,
scale: 1.15,
fill: "#FFFFFF",
duration: 0.01,
stagger: 0.025,
ease: "power2.out",
})
.to(pixels, {
scale: 1,
duration: 0.01,
ease: "back.out(3)",
const pixels = Array.from(pixelEls).sort((a, b) => {
const aBox = a.getBBox();
const bBox = b.getBBox();
return (
aBox.x + aBox.width / 2 - (aBox.y + aBox.height / 2) -
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
);
});
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "left bottom",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current!
.to(pixels, { opacity: 1, scale: 1.15, fill: "#FFFFFF", duration: 0.01, stagger: 0.025, ease: "power2.out" })
.to(pixels, { scale: 1, duration: 0.01, ease: "back.out(3)" });
});
return () => {
cancelled = true;
tlRef.current?.kill();
};
}, []);
@@ -189,137 +189,21 @@ export default function Navbar({
};
}, [menuOpen]);
useGSAP(
() => {
if (!animateIntro) return;
// Skip the entrance animation on non-desktop viewports, or when
// hydration landed well after first paint. On mobile the GSAP chunk
// can arrive many seconds after the server-rendered navbar has
// already painted; hiding it with `gsap.set({ opacity: 0 })` at
// that point and re-animating it in is a visible flash that's much
// worse than no animation.
if (
window.matchMedia("(max-width: 1023px)").matches ||
performance.now() > 300
) {
return;
}
gsap.set(".nav-root", { opacity: 0 });
gsap.set(".nav-logo", {
opacity: 0,
filter: "blur(6px) brightness(1)",
scale: 0.92,
transformOrigin: "left center",
});
gsap.set(".nav-link", { opacity: 0, y: -8 });
gsap.set(".nav-dashboard", { opacity: 0, scale: 0.95 });
const navMobileRef = useRef<HTMLDivElement | null>(null);
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
tl.to(".nav-root", { opacity: 1, duration: 0.3, ease: "none" })
.to(".nav-logo", {
opacity: 1,
filter: "blur(0px) brightness(1)",
scale: 1,
duration: 0.7,
ease: "power2.out",
})
.to(".nav-logo", {
filter: "blur(0px) brightness(1.6)",
duration: 0.225,
ease: "power2.in",
})
.to(".nav-logo", {
filter: "blur(0px) brightness(1)",
duration: 0.125,
ease: "power2.out",
})
.to(
".nav-link",
{
opacity: 1,
y: 0,
duration: 0.25,
stagger: 0.06,
ease: "power2.out",
},
"-=0.05",
)
.to(
".nav-dashboard",
{
opacity: 1,
scale: 1,
duration: 0.3,
ease: "back.out(1.5)",
},
"-=0.1",
);
},
{ scope: containerRef, dependencies: [animateIntro] },
);
const mobileTl = useRef<gsap.core.Timeline | null>(null);
useGSAP(
() => {
gsap.set(".nav-mobile", {
opacity: 0,
clipPath: "inset(0% 0 100% 0)",
pointerEvents: "none",
});
gsap.set(
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
{
opacity: 0,
filter: "blur(8px)",
scale: 0.95,
},
);
mobileTl.current = gsap.timeline({
paused: true,
defaults: { overwrite: "auto" },
});
mobileTl.current
.to(".nav-mobile", {
opacity: 1,
y: 0,
pointerEvents: "auto",
clipPath: "inset(0% 0 0% 0)",
duration: 0.2,
ease: "power3.inOut",
})
.to(
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
{
opacity: 1,
filter: "blur(0px)",
scale: 1,
duration: 0.15,
stagger: 0.01,
ease: "power2.out",
},
"-=0.1",
);
},
{ scope: containerRef },
);
useGSAP(
() => {
if (mobileTl.current) {
if (menuOpen) {
mobileTl.current.timeScale(1).play();
} else {
mobileTl.current.timeScale(1.8).reverse();
}
}
},
{ scope: containerRef, dependencies: [menuOpen] },
);
useEffect(() => {
const el = navMobileRef.current;
if (!el) return;
if (menuOpen) {
el.style.opacity = "1";
el.style.pointerEvents = "auto";
el.style.clipPath = "inset(0% 0 0% 0)";
} else {
el.style.opacity = "0";
el.style.pointerEvents = "none";
el.style.clipPath = "inset(0% 0 100% 0)";
}
}, [menuOpen]);
return (
<div
@@ -404,8 +288,9 @@ export default function Navbar({
</nav>
</div>
<div
ref={navMobileRef}
className={cn(
"fixed nav-mobile inset-x-0 z-40 flex h-[calc(100dvh-58px)] flex-col overflow-x-hidden overflow-y-auto bg-[#000000] px-4 pb-8 font-mono uppercase transition-all duration-300 md:px-(--page-pad) lg:top-5",
"fixed inset-x-0 z-40 flex h-[calc(100dvh-58px)] flex-col overflow-x-hidden overflow-y-auto bg-[#000000] px-4 pb-8 font-mono uppercase md:px-(--page-pad) lg:top-5",
scrolled && !recoilHidden
? "top-[58px] sm:top-[60px]"
: "top-[66px] sm:top-[62px]",
@@ -414,6 +299,7 @@ export default function Navbar({
opacity: 0,
pointerEvents: "none",
clipPath: "inset(0% 0 100% 0)",
transition: "opacity 0.2s ease, clip-path 0.2s cubic-bezier(0.87, 0, 0.13, 1)",
}}
>
{/* Nav items */}

View File

@@ -0,0 +1,10 @@
// Loads GSAP exactly once — first call triggers the dynamic import,
// subsequent calls reuse the same promise.
type Gsap = (typeof import("gsap"))["default"];
let promise: Promise<Gsap> | null = null;
export const getGsap = (): Promise<Gsap> => {
if (!promise) promise = import("gsap").then((m) => m.default);
return promise;
};

View File

@@ -7,13 +7,13 @@ const nextConfig = {
pageExtensions: ["ts", "tsx", "md", "mdx"],
allowedDevOrigins: ["*.ngrok-free.dev"],
experimental: {
optimizePackageImports: ["framer-motion", "motion", "gsap", "@gsap/react"],
optimizePackageImports: ["motion", "gsap", "@gsap/react"],
optimizeCss: true,
},
images: {
// Serve AVIF to supporting browsers (better compression than WebP),
// falling back to WebP. Next.js negotiates via Accept header automatically.
// formats: ["image/avif", "image/webp"],
formats: ["image/avif", "image/webp"],
},
async headers() {
if (!isProd) return [];

View File

@@ -16,11 +16,9 @@
"@next/mdx": "^16.2.4",
"@types/react": "^19.2.14",
"@vercel/analytics": "^2.0.1",
"framer-motion": "^12.38.0",
"gray-matter": "^4.0.3",
"gsap": "^3.15.0",
"lottie-react": "^2.4.1",
"lottie-web": "^5.13.0",
"matter-js": "^0.20.0",
"motion": "^12.38.0",
"next": "16.2.4",