diff --git a/.github/workflows/cd_deploy.yml b/.github/workflows/cd_deploy.yml index 1a2824b..1d2cea1 100644 --- a/.github/workflows/cd_deploy.yml +++ b/.github/workflows/cd_deploy.yml @@ -56,14 +56,14 @@ jobs: WORKER_D1: ${{ secrets.WORKER_D1 }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - - name: Deploy — Worker - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.PAT }} - publish_dir: ./server - publish_branch: 'server' - allow_empty_commit: false - enable_jekyll: true + # - name: Deploy — Worker + # uses: peaceiris/actions-gh-pages@v4 + # with: + # github_token: ${{ secrets.PAT }} + # publish_dir: ./server + # publish_branch: 'server' + # allow_empty_commit: false + # enable_jekyll: true - name: Deploy — Website uses: peaceiris/actions-gh-pages@v4 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..f7c34e1 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,86 @@ +# Desenvolvimento do site + +Este repositório contém o **código-fonte** do site oficial da **JSConf Brasil** ([jsconf.com.br](https://jsconf.com.br)): páginas em **Docusaurus**, conteúdo multilíngue e um **Worker** (Cloudflare) para formulários e API. Aqui você encontra como rodar o projeto localmente, traduzir textos e validar o build antes de abrir um PR. + +--- + +## Primeiros passos + +```sh +npm ci +npm start # Inicia o website e o servidor localmente (pt-BR) +``` + +--- + +### Idiomas + +```sh +npm run start:en # Inglês +npm run start:es # Espanhol +``` + +> [!TIP] +> +> Use o componente `` ao invés de `` para visualizar as imagens corretamente em todos os idiomas durante o desenvolvimento. +> +> - O componente `` utiliza `loading="lazy"` e `decoding="async"` por padrão. + +--- + +### Traduções (i18n) + +Use `` para conteúdo JSX e `text()` para atributos HTML (`aria-label`, `alt`, `placeholder`, etc.): + +```tsx +

+ +

+``` + +```tsx +{text({ +``` + +`` e `text()` são abstrações do [``](https://docusaurus.io/docs/docusaurus-core#translate) do **Docusaurus**: + +- IDs tipados com autocomplete a partir de `i18n/pt-BR/code.json` +- Fallback automático do idioma principal (`pt-BR`) — não é necessário passar `children` + +Para adicionar um novo texto: + +1. Crie a chave em `i18n/pt-BR/code.json` +2. Use `` ou `text({ id: '...' })` no componente +3. Os tipos são inferidos automaticamente usando `i18n/pt-BR/code.json` como fonte de verdade + +--- + +### Formatação / Linting + +```sh +npm run lint:fix +``` + +--- + +## Compilação + +```sh +npm run build # Compila o website e o worker +``` + +--- + +## Testes + +```sh +npm test # Testes unitários +``` + +```sh +npm run typecheck # Verificação de tipos TypeScript +``` + +```sh +npm run lint # Verificação de linting +``` diff --git a/README.md b/README.md index 0235c6c..8f30b63 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,21 @@ -# JSConf Brasil 2026 🐢✨ +# JSConf Brasil 2026 -## Desenvolvimento +A **JSConf Brasil** é a conferência brasileira dentro da família **JSConf**, dedicada à comunidade **JavaScript** e ao ecossistema em torno da linguagem — do front ao back, ferramentas, performance e as pessoas que constroem produtos com tecnologia web. -```sh -npm ci -npm start # Inicia o website e o servidor localmente (pt-BR) -``` +## Propósito ---- - -### Idiomas - -```sh -npm run start:en # Inglês -npm run start:es # Espanhol -``` - -> [!TIP] -> -> Use o componente `` ao invés de `` para visualizar as imagens corretamente em todos os idiomas durante o desenvolvimento. -> -> - O componente `` utiliza `loading="lazy"` e `decoding="async"` por padrão. - ---- - -### Traduções (i18n) - -Use `` para conteúdo JSX e `text()` para atributos HTML ( `aria-label` , `alt` , `placeholder` , etc.): - -```tsx -

- -

-``` - -```tsx -{text({ -``` +O evento existe para **reunir quem desenvolve com JS**, **dividir conhecimento de alto nível** e **fortalecer redes** entre profissionais, estudantes e comunidades de todo o país. Queremos um espaço acolhedor, com palestras que inspiram e práticas que refletem o estado da arte, sempre com olhar para o contexto brasileiro. -`` e `text()` são abstrações do [ `` ](https://docusaurus.io/docs/docusaurus-core#translate) do **Docusaurus**: +## JSConf BR e a NodeBR -- IDs tipados com autocomplete a partir de `i18n/pt-BR/code.json` -- Fallback automático do idioma principal (`pt-BR`) — não é necessário passar `children` +A **JSConf Brasil** é um **braço da [NodeBR](https://nodebr.org)** — a associação que apoia e articula a comunidade **Node.js** e JavaScript no Brasil. Essa ligação traduz o compromisso com **código aberto**, **educação** e **representação** da comunidade técnica nacional em eventos de referência internacional, alinhados aos valores que a NodeBR defende no país. -Para adicionar um novo texto: +## Site e repositório -1. Crie a chave em `i18n/pt-BR/code.json` -2. Use `` ou `text({ id: '...' })` no componente -3. Os tipos são inferidos automaticamente usando `i18n/pt-BR/code.json` como fonte de verdade +O site público está em **[jsconf.com.br](https://jsconf.com.br)**. Este repositório é o código do site e dos serviços que o sustentam (por exemplo, formulários e integrações). ---- - -### Formatação / Linting - -```sh -npm run lint:fix -``` - ---- - -## Compilação - -```sh -npm run build # Compila o website -``` +Quer contribuir com código, traduções ou correções? Veja **[DEVELOPMENT.md](./DEVELOPMENT.md)**. --- -## Testes - -```sh -npm test # Testes unitários -``` - -```sh -npm run typecheck # Verificação de tipos TypeScript -``` - -```sh -npm run lint # Verificação de linting -``` +Licença: **AGPL-3.0-only** — ver [LICENSE](./LICENSE). diff --git a/i18n/en-US/code.json b/i18n/en-US/code.json index 2a2b546..3b53049 100644 --- a/i18n/en-US/code.json +++ b/i18n/en-US/code.json @@ -167,12 +167,6 @@ "team.ana.position": { "message": "Senior Software Engineer" }, - "team.weslley.bio": { - "message": "Weslley Araújo is a developer with over 11 years of experience, MySQL2 maintainer and creator of Poku, a high-performance test runner that challenges the programming language itself." - }, - "team.weslley.position": { - "message": "Principal Developer" - }, "team.lojhan.bio": { "message": "Tech Lead & Full Stack Developer. Helping teams build scalable applications with modern technologies. Specialized in frontend, backend and architecture design." }, diff --git a/i18n/es-419/code.json b/i18n/es-419/code.json index af19f06..7ec79d6 100644 --- a/i18n/es-419/code.json +++ b/i18n/es-419/code.json @@ -167,12 +167,6 @@ "team.ana.position": { "message": "Senior Software Engineer" }, - "team.weslley.bio": { - "message": "Weslley Araújo es un desarrollador con más de 11 años de experiencia, mantenedor de MySQL2 y creador de Poku, un test runner de alto rendimiento que desafía al propio lenguaje de programación." - }, - "team.weslley.position": { - "message": "Principal Developer" - }, "team.lojhan.bio": { "message": "Tech Lead & Full Stack Developer. Ayudando a equipos a construir aplicaciones escalables con tecnologías modernas. Especializado en frontend, backend y diseño de arquitectura." }, diff --git a/i18n/pt-BR/code.json b/i18n/pt-BR/code.json index fb181e4..bd77e87 100644 --- a/i18n/pt-BR/code.json +++ b/i18n/pt-BR/code.json @@ -167,12 +167,6 @@ "team.ana.position": { "message": "Senior Software Engineer" }, - "team.weslley.bio": { - "message": "Weslley Araújo é um desenvolvedor com mais de 11 anos de experiência, mantenedor do MySQL2 e criador do Poku, um test runner de alta performance que desafia a própria linguagem de programação." - }, - "team.weslley.position": { - "message": "Principal Developer" - }, "team.lojhan.bio": { "message": "Tech Lead & Full Stack Developer. Ajudando equipes a construir aplicações escaláveis com tecnologias modernas. Especializado em frontend, backend e design de arquitetura." }, diff --git a/package-lock.json b/package-lock.json index 5dbf850..62cac20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1339,9 +1339,9 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", + "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", "dev": true, "license": "MIT", "dependencies": { @@ -5096,9 +5096,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5116,9 +5113,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5136,9 +5130,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5156,9 +5147,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5176,9 +5164,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5196,9 +5181,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5216,9 +5198,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5236,9 +5215,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5256,9 +5232,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5282,9 +5255,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5308,9 +5278,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5334,9 +5301,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5360,9 +5324,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5386,9 +5347,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5412,9 +5370,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5438,9 +5393,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -6301,9 +6253,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6325,9 +6274,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6349,9 +6295,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6373,9 +6316,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6397,9 +6337,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6421,9 +6358,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7028,9 +6962,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7045,9 +6976,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7062,9 +6990,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7079,9 +7004,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7096,9 +7018,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7113,9 +7032,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7130,9 +7046,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7147,9 +7060,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7164,9 +7074,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7181,9 +7088,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7198,9 +7102,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7215,9 +7116,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7232,9 +7130,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7815,9 +7710,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7835,9 +7727,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7855,9 +7744,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7875,9 +7761,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7917,6 +7800,70 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", @@ -14107,9 +14054,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -14131,9 +14075,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -14155,9 +14096,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -14179,9 +14117,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/src/website/assets/img/team/wells.webp b/src/website/assets/img/team/wells.webp deleted file mode 100644 index 64bc10e..0000000 Binary files a/src/website/assets/img/team/wells.webp and /dev/null differ diff --git a/src/website/components/home/Team.tsx b/src/website/components/home/Team.tsx index e9fb922..8224428 100644 --- a/src/website/components/home/Team.tsx +++ b/src/website/components/home/Team.tsx @@ -91,20 +91,6 @@ export const Team = () => {
- -).map((d) => ({ cx: d.cx, cy: d.cy, r: d.r })); - -const BASE_COLOR = `rgba(${INITIAL_COLOR.red},${INITIAL_COLOR.green},${INITIAL_COLOR.blue},${INITIAL_COLOR.alpha})`; -const HIGH_COLOR = `rgba(${FINAL_COLOR.red},${FINAL_COLOR.green},${FINAL_COLOR.blue},${FINAL_COLOR.alpha})`; - -// 100, 200, 300, 400 dots per overlay -const overlaySubsets: Dot[][] = Array.from( - { length: OVERLAY_COUNT }, - (_, i) => { - const count = Math.round(((i + 1) / OVERLAY_COUNT) * OVERLAY_MAX_DOTS); - return [...allDots].sort(() => Math.random() - 0.5).slice(0, count); - } -); - -const offscreen = - typeof document !== 'undefined' ? document.createElement('canvas') : null; - -function toDataURL( - dots: Dot[], - color: string, - renderWidth: number, - renderHeight: number -): string { - if (!offscreen) return ''; - offscreen.width = renderWidth; - offscreen.height = renderHeight; - const ctx = offscreen.getContext('2d'); - if (!ctx) return ''; - ctx.clearRect(0, 0, renderWidth, renderHeight); - const scale = renderWidth / VIEWBOX_WIDTH; - ctx.fillStyle = color; - for (const d of dots) { - const size = Math.max(1, Math.round(d.r * 2 * scale)); - ctx.fillRect( - Math.round(d.cx * scale - size / 2), - Math.round(d.cy * scale - size / 2), - size, - size - ); - } - return offscreen.toDataURL(); -} - -type BRProps = { className?: string; style?: React.CSSProperties }; - -const BRCanvas = ({ className, style }: BRProps) => { - const containerRef = useRef(null); - const baseRef = useRef(null); - const overlayRefs = useRef<(HTMLImageElement | null)[]>( - Array(OVERLAY_COUNT).fill(null) - ); - - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - let rendered = false; - - const repaint = () => { - const containerWidth = container.offsetWidth; - const containerHeight = container.offsetHeight; - if (!containerWidth || !containerHeight) return; - - const renderHeight = Math.round( - containerHeight * (RENDER_WIDTH / containerWidth) - ); - - if (baseRef.current) - baseRef.current.src = toDataURL( - allDots, - BASE_COLOR, - RENDER_WIDTH, - renderHeight - ); - - overlayRefs.current.forEach((el, i) => { - if (el) - el.src = toDataURL( - overlaySubsets[i]!, - HIGH_COLOR, - RENDER_WIDTH, - renderHeight - ); - }); - - rendered = true; - }; - - repaint(); - - const ro = new ResizeObserver(() => { - // Skip re-render on tiny fluctuations after first paint - if (rendered) repaint(); - }); - ro.observe(container); - return () => ro.disconnect(); - }, []); +type CanvasProps = { + className?: string; + style?: React.CSSProperties; +}; - const layerStyle: React.CSSProperties = { - position: 'absolute', - inset: 0, - width: '100%', - height: '100%', - imageRendering: 'pixelated', - }; +const Canvas = ({ className, style }: CanvasProps) => { + const { canvasRef } = useBR(); return ( -
- - {Array.from({ length: OVERLAY_COUNT }, (_, i) => ( - { - overlayRefs.current[i] = el; - }} - style={{ - ...layerStyle, - // translateZ forces independent compositor layer so opacity animation - // doesn't trigger Layerize on the parent subtree every frame - transform: 'translateZ(0)', - animation: `br-cycle ${CYCLE_S}s steps(${CYCLE_S * 30}) ${-(CYCLE_S / OVERLAY_COUNT) * i}s infinite`, - willChange: 'opacity', - }} - /> - ))} -
+ style={{ + width: '100%', + height: '100%', + ...style, + }} + /> ); }; -export const BR = memo(BRCanvas); +export const BR = memo(Canvas); diff --git a/src/website/components/shared/Page.tsx b/src/website/components/shared/Page.tsx index f733651..133d927 100644 --- a/src/website/components/shared/Page.tsx +++ b/src/website/components/shared/Page.tsx @@ -1,6 +1,7 @@ import '@site/src/website/scss/pages/root.scss'; import Head from '@docusaurus/Head'; import Layout from '@theme/Layout'; +import { useBackground } from '../../hooks/Background/useBackground'; type PageProps = { children: React.ReactNode; @@ -9,12 +10,21 @@ type PageProps = { }; export const Page = ({ title, description, children }: PageProps) => { + const { canvasRef } = useBackground({ + intensity: 0.0025, + zoomSpeed: 0.0005, + starColor: '#073f2950', + }); + return ( -
{children}
+
+ + {children} +
); }; diff --git a/src/website/hooks/BR/animation.ts b/src/website/hooks/BR/animation.ts new file mode 100644 index 0000000..62152ea --- /dev/null +++ b/src/website/hooks/BR/animation.ts @@ -0,0 +1,116 @@ +import type { + AnimationRefs, + AnimationState, +} from '@site/src/website/hooks/BR/types'; +import { draw } from '@site/src/website/hooks/BR/canvas'; +import { + FINAL_COLOR, + INITIAL_COLOR, +} from '@site/src/website/hooks/BR/definitions'; +import { + initializeResizeObserver, + stopAnimationFrame, +} from '@site/src/website/hooks/shared/animation'; + +const easeIn = (progress: number): number => progress * progress; + +const lerp = (start: number, end: number, progress: number): number => + start + (end - start) * progress; + +export const getColor = (progress: number): string => { + const easedProgress = easeIn(Math.min(1, Math.max(0, progress))); + const red = Math.round( + lerp(INITIAL_COLOR.red, FINAL_COLOR.red, easedProgress) + ); + const green = Math.round( + lerp(INITIAL_COLOR.green, FINAL_COLOR.green, easedProgress) + ); + const blue = Math.round( + lerp(INITIAL_COLOR.blue, FINAL_COLOR.blue, easedProgress) + ); + const alpha = lerp(INITIAL_COLOR.alpha, FINAL_COLOR.alpha, easedProgress); + + return `rgba(${red},${green},${blue},${alpha})`; +}; + +const createAnimationLoop = ( + canvas: HTMLCanvasElement, + animation: AnimationRefs, + state: AnimationState +) => { + const animate = (timestamp: number) => { + const context = canvas.getContext('2d'); + if (!context) return; + + animation.startTime ??= timestamp; + + const shouldRenderFrame = timestamp - animation.lastFrameTime >= 16; + if (shouldRenderFrame) { + const elapsed = + timestamp - animation.startTime + animation.elapsedBeforePause; + draw(context, canvas, state, elapsed); + animation.lastFrameTime = timestamp; + } + + animation.animationFrameId = requestAnimationFrame(animate); + }; + + return animate; +}; + +const resetPauseState = (animation: AnimationRefs): void => { + if (animation.pauseTime === null) return; + + animation.startTime = null; + animation.pauseTime = null; +}; + +const savePauseState = (animation: AnimationRefs): void => { + const canSavePauseState = + animation.startTime !== null && animation.pauseTime === null; + if (!canSavePauseState) return; + + animation.elapsedBeforePause += performance.now() - animation.startTime!; + animation.pauseTime = performance.now(); + animation.startTime = null; +}; + +const startAnimationLoop = ( + canvas: HTMLCanvasElement, + animation: AnimationRefs, + state: AnimationState, + handleResize: () => void +): void => { + const animate = createAnimationLoop(canvas, animation, state); + + stopAnimationFrame(animation); + requestAnimationFrame(() => { + handleResize(); + animation.animationFrameId = requestAnimationFrame(animate); + }); +}; + +export const handleVisibilityOn = ( + canvas: HTMLCanvasElement, + animation: AnimationRefs, + state: AnimationState, + handleResize: () => void +): void => { + resetPauseState(animation); + initializeResizeObserver(canvas, animation, handleResize); + startAnimationLoop(canvas, animation, state, handleResize); +}; + +export const handleVisibilityOff = (animation: AnimationRefs): void => { + savePauseState(animation); + stopAnimationFrame(animation); +}; + +export const createElapsedTimeGetter = + (animation: AnimationRefs) => (): number => { + if (animation.startTime === null) return 0; + + return ( + performance.now() - animation.startTime + animation.elapsedBeforePause + ); + }; diff --git a/src/website/hooks/BR/canvas.ts b/src/website/hooks/BR/canvas.ts new file mode 100644 index 0000000..fec8c66 --- /dev/null +++ b/src/website/hooks/BR/canvas.ts @@ -0,0 +1,82 @@ +import type { AnimationState, Dot } from '@site/src/website/hooks/BR/types'; +import { getColor } from '@site/src/website/hooks/BR/animation'; +import { VIEWBOX_WIDTH } from '@site/src/website/hooks/BR/definitions'; +import { + calculateDotProgress, + processRemovals, +} from '@site/src/website/hooks/BR/helpers'; + +const drawSquare = ( + context: CanvasRenderingContext2D, + dot: Dot, + progress: number, + scale: number +): void => { + const color = getColor(progress); + const size = dot.radius * 2 * scale; + const x = dot.centerX * scale - size / 2; + const y = dot.centerY * scale - size / 2; + const borderRadius = size * 0.2; + + context.fillStyle = color; + context.beginPath(); + context.roundRect(x, y, size, size, borderRadius); + context.fill(); +}; + +export const draw = ( + context: CanvasRenderingContext2D, + canvas: HTMLCanvasElement, + state: AnimationState, + elapsed: number +): void => { + if (canvas.width === 0 || canvas.height === 0) return; + + const visibleIndices: number[] = []; + const scale = canvas.width / VIEWBOX_WIDTH; + + let visibleCount = 0; + + context.clearRect(0, 0, canvas.width, canvas.height); + + for (let dotIndex = 0; dotIndex < state.dots.length; dotIndex++) { + const dotState = state.dots[dotIndex]; + if (!dotState) continue; + + const result = calculateDotProgress(dotState, elapsed); + + dotState.visible = result.visible; + dotState.disappearAt = result.disappearAt; + + if (result.progress > 0) { + visibleCount++; + + const isStableVisible = result.visible && result.disappearAt === null; + if (isStableVisible) { + visibleIndices.push(dotIndex); + } + } + + drawSquare(context, dotState.dot, result.progress, scale); + } + + processRemovals(state, visibleIndices, visibleCount, elapsed); +}; + +export const updateCanvasSize = ( + canvas: HTMLCanvasElement, + state: AnimationState, + getElapsed: () => number +): void => { + const rect = canvas.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) return; + + const devicePixelRatio = window.devicePixelRatio || 1; + canvas.width = rect.width * devicePixelRatio; + canvas.height = rect.height * devicePixelRatio; + + const context = canvas.getContext('2d'); + if (context) { + draw(context, canvas, state, getElapsed()); + } +}; diff --git a/src/website/hooks/BR/definitions.ts b/src/website/hooks/BR/definitions.ts index 5407c8f..b4a1b9b 100644 --- a/src/website/hooks/BR/definitions.ts +++ b/src/website/hooks/BR/definitions.ts @@ -1,6 +1,6 @@ export const ANIMATION_DURATION = 250; export const DELAY_PER_DOT = 25; -export const VISIBLE_THRESHOLD = 200; +export const VISIBLE_THRESHOLD = 1000; export const VIEWBOX_WIDTH = 1086; export const INITIAL_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.025 }; export const FINAL_COLOR = { red: 102, green: 255, blue: 0, alpha: 0.25 }; diff --git a/src/website/hooks/BR/helpers.ts b/src/website/hooks/BR/helpers.ts new file mode 100644 index 0000000..69e29e9 --- /dev/null +++ b/src/website/hooks/BR/helpers.ts @@ -0,0 +1,168 @@ +import type { + AnimationState, + Dot, + DotProgressResult, + DotState, + RawDotData, +} from '@site/src/website/hooks/BR/types'; +import { + ANIMATION_DURATION, + DELAY_PER_DOT, + VISIBLE_THRESHOLD, +} from '@site/src/website/hooks/BR/definitions'; +import dots from '@site/src/website/hooks/BR/dots.json'; + +const shuffleArray = (array: T[]): T[] => { + const result = [...array]; + + for (let currentIndex = result.length - 1; currentIndex > 0; currentIndex--) { + const randomIndex = Math.floor(Math.random() * (currentIndex + 1)); + + const currentValue = result[currentIndex]; + const randomValue = result[randomIndex]; + + if (currentValue !== undefined && randomValue !== undefined) { + result[currentIndex] = randomValue; + result[randomIndex] = currentValue; + } + } + + return result; +}; + +const mapRawDotToDot = (rawDot: RawDotData): Dot => ({ + centerX: rawDot.cx, + centerY: rawDot.cy, + radius: rawDot.r, +}); + +const createHiddenProgress = (): DotProgressResult => ({ + progress: 0, + visible: false, + disappearAt: null, +}); + +const createVisibleProgress = (): DotProgressResult => ({ + progress: 1, + visible: true, + disappearAt: null, +}); + +const calculateDisappearingProgress = ( + elapsed: number, + disappearAt: number, + visible: boolean +): DotProgressResult | null => { + if (disappearAt === null) return null; + + const timeSinceDisappear = elapsed - disappearAt; + const hasFinishedDisappearing = timeSinceDisappear >= ANIMATION_DURATION; + + if (hasFinishedDisappearing) return createHiddenProgress(); + + return { + progress: 1 - timeSinceDisappear / ANIMATION_DURATION, + visible, + disappearAt, + }; +}; + +const calculateAppearingProgress = ( + elapsed: number, + appearAt: number +): DotProgressResult => { + const timeSinceAppear = elapsed - appearAt; + + const hasNotStartedAppearing = timeSinceAppear <= 0; + if (hasNotStartedAppearing) return createHiddenProgress(); + + const hasFinishedAppearing = timeSinceAppear >= ANIMATION_DURATION; + if (hasFinishedAppearing) return createVisibleProgress(); + + return { + progress: timeSinceAppear / ANIMATION_DURATION, + visible: false, + disappearAt: null, + }; +}; + +export const calculateDotProgress = ( + dotState: DotState, + elapsed: number +): DotProgressResult => { + const { visible, appearAt, disappearAt } = dotState; + + const disappearingProgress = calculateDisappearingProgress( + elapsed, + disappearAt!, + visible + ); + if (disappearingProgress) return disappearingProgress; + + if (visible) return createVisibleProgress(); + + return calculateAppearingProgress(elapsed, appearAt); +}; + +const scheduleDotsForRemoval = ( + state: AnimationState, + visibleIndices: number[], + elapsed: number +): void => { + state.nextRemovalAt ??= elapsed; + + while (state.nextRemovalAt <= elapsed && visibleIndices.length > 0) { + const randomIndex = Math.floor(Math.random() * visibleIndices.length); + const dotIndex = visibleIndices.splice(randomIndex, 1)[0]; + const dot = dotIndex !== undefined ? state.dots[dotIndex] : undefined; + + if (dot) { + dot.disappearAt = state.nextRemovalAt; + dot.appearAt = state.nextDelay; + } + + state.nextDelay += DELAY_PER_DOT; + state.nextRemovalAt += DELAY_PER_DOT; + } +}; + +const resetRemovalSchedule = (state: AnimationState): void => { + state.nextRemovalAt = null; +}; + +export const processRemovals = ( + state: AnimationState, + visibleIndices: number[], + visibleCount: number, + elapsed: number +): void => { + const shouldScheduleRemovals = + visibleCount >= VISIBLE_THRESHOLD && visibleIndices.length > 0; + const shouldResetSchedule = visibleCount < VISIBLE_THRESHOLD; + + if (shouldScheduleRemovals) { + scheduleDotsForRemoval(state, visibleIndices, elapsed); + return; + } + + if (shouldResetSchedule) { + resetRemovalSchedule(state); + } +}; + +export const createInitialState = (): AnimationState => { + const indices = shuffleArray( + Array.from({ length: dots.length }, (_, index) => index) + ); + + return { + dots: (dots as RawDotData[]).map((rawDot, index) => ({ + dot: mapRawDotToDot(rawDot), + visible: false, + appearAt: (indices[index] ?? index) * DELAY_PER_DOT, + disappearAt: null, + })), + nextDelay: dots.length * DELAY_PER_DOT, + nextRemovalAt: null, + }; +}; diff --git a/src/website/hooks/BR/types.ts b/src/website/hooks/BR/types.ts new file mode 100644 index 0000000..686e93b --- /dev/null +++ b/src/website/hooks/BR/types.ts @@ -0,0 +1,39 @@ +export type Dot = { + centerX: number; + centerY: number; + radius: number; +}; + +export type DotState = { + dot: Dot; + visible: boolean; + appearAt: number; + disappearAt: number | null; +}; + +export type AnimationState = { + dots: DotState[]; + nextDelay: number; + nextRemovalAt: number | null; +}; + +export type AnimationRefs = { + animationFrameId: number; + lastFrameTime: number; + elapsedBeforePause: number; + pauseTime: number | null; + startTime: number | null; + resizeObserver: ResizeObserver | null; +}; + +export type RawDotData = { + cx: number; + cy: number; + r: number; +}; + +export type DotProgressResult = { + progress: number; + visible: boolean; + disappearAt: number | null; +}; diff --git a/src/website/hooks/BR/useBR.tsx b/src/website/hooks/BR/useBR.tsx new file mode 100644 index 0000000..a3f79e6 --- /dev/null +++ b/src/website/hooks/BR/useBR.tsx @@ -0,0 +1,54 @@ +import { useRef } from 'react'; +import { + createElapsedTimeGetter, + handleVisibilityOff, + handleVisibilityOn, +} from '@site/src/website/hooks/BR/animation'; +import { updateCanvasSize } from '@site/src/website/hooks/BR/canvas'; +import { createInitialState } from '@site/src/website/hooks/BR/helpers'; +import { + AnimationRefs, + AnimationState, +} from '@site/src/website/hooks/BR/types'; +import { cleanupAnimation } from '@site/src/website/hooks/shared/animation'; +import { useVisibility } from '@site/src/website/hooks/useVisibility'; + +export const useBR = () => { + const canvasRef = useRef(null); + const stateRef = useRef(null); + const animationRef = useRef({ + animationFrameId: 0, + lastFrameTime: 0, + elapsedBeforePause: 0, + pauseTime: null, + startTime: null, + resizeObserver: null, + }); + + useVisibility( + canvasRef, + ({ isFullyVisible }, canvas) => { + const animation = animationRef.current; + + stateRef.current ??= createInitialState(); + const state = stateRef.current; + + const getElapsed = createElapsedTimeGetter(animation); + const handleResize = () => updateCanvasSize(canvas, state, getElapsed); + + const visibilityHandlers: Record void> = { + visible: () => + handleVisibilityOn(canvas, animation, state, handleResize), + hidden: () => handleVisibilityOff(animation), + }; + + const visibilityState = isFullyVisible ? 'visible' : 'hidden'; + visibilityHandlers[visibilityState]?.(); + }, + { + onReset: () => cleanupAnimation(animationRef.current), + } + ); + + return { canvasRef }; +}; diff --git a/src/website/hooks/Background/animation.ts b/src/website/hooks/Background/animation.ts new file mode 100644 index 0000000..7be71ae --- /dev/null +++ b/src/website/hooks/Background/animation.ts @@ -0,0 +1,113 @@ +import type { + AnimationRefs, + AnimationState, +} from '@site/src/website/hooks/Background/types'; +import { + isStarOutOfBounds, + recycleStar, +} from '@site/src/website/hooks/Background/stars'; +import { stopAnimationFrame } from '@site/src/website/hooks/shared/animation'; + +const updateVelocity = (state: AnimationState): void => { + const { velocity, config } = state; + const { friction, responsiveness } = config; + + velocity.targetX *= friction; + velocity.targetY *= friction; + + velocity.x += (velocity.targetX - velocity.x) * responsiveness; + velocity.y += (velocity.targetY - velocity.y) * responsiveness; +}; + +const updateStars = (state: AnimationState): void => { + const { stars, velocity, dimensions, config } = state; + const { width, height } = dimensions; + const { overflowThreshold } = config; + + const centerX = width / 2; + const centerY = height / 2; + + for (const star of stars) { + star.x += velocity.x * star.z; + star.y += velocity.y * star.z; + + star.x += (star.x - centerX) * velocity.zoom * star.z; + star.y += (star.y - centerY) * velocity.zoom * star.z; + star.z += velocity.zoom; + + if (isStarOutOfBounds(star, width, height, overflowThreshold)) + recycleStar(star, state); + } +}; + +const renderStars = ( + context: CanvasRenderingContext2D, + state: AnimationState +): void => { + const { stars, velocity, dimensions, config } = state; + const { scale } = dimensions; + const { starSize, starColor } = config; + + context.lineCap = 'round'; + context.strokeStyle = starColor; + + for (const star of stars) { + context.beginPath(); + + context.lineWidth = starSize * star.z * scale; + context.globalAlpha = 0.5 + 0.5 * Math.random(); + + context.moveTo(star.x, star.y); + + let tailX = velocity.x * 2; + let tailY = velocity.y * 2; + + if (Math.abs(tailX) < 0.1) tailX = 0.5; + if (Math.abs(tailY) < 0.1) tailY = 0.5; + + context.lineTo(star.x + tailX, star.y + tailY); + context.stroke(); + } +}; + +const clearCanvas = ( + context: CanvasRenderingContext2D, + width: number, + height: number +): void => { + context.clearRect(0, 0, width, height); +}; + +const createAnimationLoop = ( + canvas: HTMLCanvasElement, + state: AnimationState, + refs: AnimationRefs +) => { + const animate = (): void => { + const context = canvas.getContext('2d'); + if (!context) return; + + const { width, height } = state.dimensions; + + clearCanvas(context, width, height); + updateVelocity(state); + updateStars(state); + renderStars(context, state); + + refs.animationFrameId = requestAnimationFrame(animate); + }; + + return animate; +}; + +export const startAnimation = ( + canvas: HTMLCanvasElement, + state: AnimationState, + refs: AnimationRefs +): void => { + stopAnimationFrame(refs); + + const animate = createAnimationLoop(canvas, state, refs); + + refs.animationFrameId = requestAnimationFrame(animate); +}; diff --git a/src/website/hooks/Background/definitions.ts b/src/website/hooks/Background/definitions.ts new file mode 100644 index 0000000..f3e8197 --- /dev/null +++ b/src/website/hooks/Background/definitions.ts @@ -0,0 +1,16 @@ +import type { BackgroundConfig } from '@site/src/website/hooks/Background/types'; + +export const DEFAULT_CONFIG: BackgroundConfig = { + starColor: '#ffffff', + starSize: 3, + starMinScale: 0.2, + starCount: 0, + overflowThreshold: 50, + intensity: 0.15, + zoomSpeed: 0.0005, + friction: 0.96, + responsiveness: 0.8, +}; + +export const calculateStarCount = (width: number, height: number): number => + Math.floor((width + height) / 8); diff --git a/src/website/hooks/Background/stars.ts b/src/website/hooks/Background/stars.ts new file mode 100644 index 0000000..81bcb67 --- /dev/null +++ b/src/website/hooks/Background/stars.ts @@ -0,0 +1,91 @@ +import type { + AnimationState, + RecycleDirection, + Star, +} from '@site/src/website/hooks/Background/types'; + +export const createStar = (minScale: number): Star => ({ + x: 0, + y: 0, + z: minScale + Math.random() * (1 - minScale), +}); + +export const createStars = (count: number, minScale: number): Star[] => + Array.from({ length: count }, () => createStar(minScale)); + +export const placeStar = (star: Star, width: number, height: number): void => { + star.x = Math.random() * width; + star.y = Math.random() * height; +}; + +export const placeAllStars = ( + stars: Star[], + width: number, + height: number +): void => { + for (const star of stars) placeStar(star, width, height); +}; + +const determineRecycleDirection = ( + velocityX: number, + velocityY: number +): RecycleDirection => { + const absVelocityX = Math.abs(velocityX); + const absVelocityY = Math.abs(velocityY); + + const hasSignificantVelocity = absVelocityX > 1 || absVelocityY > 1; + if (!hasSignificantVelocity) return 'center'; + + const isHorizontalDominant = + absVelocityX > absVelocityY + ? Math.random() < absVelocityX / (absVelocityX + absVelocityY) + : Math.random() >= absVelocityY / (absVelocityX + absVelocityY); + + if (isHorizontalDominant) return velocityX > 0 ? 'left' : 'right'; + return velocityY > 0 ? 'top' : 'bottom'; +}; + +export const recycleStar = (star: Star, state: AnimationState): void => { + const { velocity, dimensions, config } = state; + const { width, height } = dimensions; + const { overflowThreshold, starMinScale } = config; + + const direction = determineRecycleDirection(velocity.x, velocity.y); + + star.z = starMinScale + Math.random() * (1 - starMinScale); + + switch (direction) { + case 'center': + star.z = 0.1; + star.x = Math.random() * width; + star.y = Math.random() * height; + break; + case 'left': + star.x = -overflowThreshold; + star.y = Math.random() * height; + break; + case 'right': + star.x = width + overflowThreshold; + star.y = Math.random() * height; + break; + case 'top': + star.x = Math.random() * width; + star.y = -overflowThreshold; + break; + case 'bottom': + star.x = Math.random() * width; + star.y = height + overflowThreshold; + break; + } +}; + +export const isStarOutOfBounds = ( + star: Star, + width: number, + height: number, + threshold: number +): boolean => + star.x < -threshold || + star.x > width + threshold || + star.y < -threshold || + star.y > height + threshold; diff --git a/src/website/hooks/Background/types.ts b/src/website/hooks/Background/types.ts new file mode 100644 index 0000000..f0620e6 --- /dev/null +++ b/src/website/hooks/Background/types.ts @@ -0,0 +1,54 @@ +export type Star = { + x: number; + y: number; + z: number; +}; + +export type RecycleDirection = 'center' | 'left' | 'right' | 'top' | 'bottom'; + +export type Velocity = { + x: number; + y: number; + targetX: number; + targetY: number; + zoom: number; +}; + +export type Pointer = { + x: number | null; + y: number | null; + isTouch: boolean; +}; + +export type CanvasDimensions = { + width: number; + height: number; + scale: number; +}; + +export type BackgroundConfig = { + starColor: string; + starSize: number; + starMinScale: number; + starCount: number; + overflowThreshold: number; + intensity: number; + zoomSpeed: number; + friction: number; + responsiveness: number; +}; + +export type AnimationState = { + stars: Star[]; + velocity: Velocity; + pointer: Pointer; + dimensions: CanvasDimensions; + config: BackgroundConfig; +}; + +export type AnimationRefs = { + animationFrameId: number; + resizeObserver: ResizeObserver | null; +}; + +export type UseBackgroundOptions = Partial; diff --git a/src/website/hooks/Background/useBackground.tsx b/src/website/hooks/Background/useBackground.tsx new file mode 100644 index 0000000..4d74c34 --- /dev/null +++ b/src/website/hooks/Background/useBackground.tsx @@ -0,0 +1,123 @@ +import type { + AnimationRefs, + AnimationState, + Pointer, + UseBackgroundOptions, +} from '@site/src/website/hooks/Background/types'; +import { useCallback, useEffect, useRef } from 'react'; +import { startAnimation } from '@site/src/website/hooks/Background/animation'; +import { + calculateStarCount, + DEFAULT_CONFIG, +} from '@site/src/website/hooks/Background/definitions'; +import { + createStars, + placeAllStars, +} from '@site/src/website/hooks/Background/stars'; +import { cleanupAnimation } from '@site/src/website/hooks/shared/animation'; + +const createInitialState = (options: UseBackgroundOptions): AnimationState => { + const config = { ...DEFAULT_CONFIG, ...options }; + + return { + stars: [], + velocity: { x: 0, y: 0, targetX: 0, targetY: 0, zoom: config.zoomSpeed }, + pointer: { x: null, y: null, isTouch: false }, + dimensions: { width: 0, height: 0, scale: 1 }, + config, + }; +}; + +const updatePointerVelocity = ( + state: AnimationState, + clientX: number, + clientY: number +): void => { + const { pointer, velocity, dimensions, config } = state; + const { intensity } = config; + + if (pointer.x !== null && pointer.y !== null) { + const deltaX = clientX - pointer.x; + const deltaY = clientY - pointer.y; + + velocity.targetX += deltaX * intensity * dimensions.scale; + velocity.targetY += deltaY * intensity * dimensions.scale; + } + + pointer.x = clientX; + pointer.y = clientY; +}; + +const resetPointer = (pointer: Pointer): void => { + pointer.x = null; + pointer.y = null; +}; + +export const useBackground = ( + options: UseBackgroundOptions = Object.create(null) +) => { + const canvasRef = useRef(null); + const stateRef = useRef(createInitialState(options)); + const refsRef = useRef({ + animationFrameId: 0, + resizeObserver: null, + }); + + const handleResize = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const state = stateRef.current; + const scale = window.devicePixelRatio || 1; + const width = canvas.offsetWidth * scale; + const height = canvas.offsetHeight * scale; + + canvas.width = width; + canvas.height = height; + + state.dimensions = { width, height, scale }; + + if (state.stars.length === 0) { + const count = state.config.starCount || calculateStarCount(width, height); + state.stars = createStars(count, state.config.starMinScale); + } + + placeAllStars(state.stars, width, height); + }, []); + + const handleMouseMove = useCallback((event: MouseEvent) => { + const state = stateRef.current; + state.pointer.isTouch = false; + updatePointerVelocity(state, event.clientX, event.clientY); + }, []); + + const handlePointerLeave = useCallback(() => { + resetPointer(stateRef.current.pointer); + }, []); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const state = stateRef.current; + const refs = refsRef.current; + + handleResize(); + + refs.resizeObserver = new ResizeObserver(handleResize); + refs.resizeObserver.observe(canvas); + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseleave', handlePointerLeave); + + startAnimation(canvas, state, refs); + + return () => { + cleanupAnimation(refs); + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseleave', handlePointerLeave); + }; + }, [handleResize, handleMouseMove, handlePointerLeave]); + + return { canvasRef }; +}; diff --git a/src/website/hooks/shared/animation.ts b/src/website/hooks/shared/animation.ts index cd3ae1d..92f826a 100644 --- a/src/website/hooks/shared/animation.ts +++ b/src/website/hooks/shared/animation.ts @@ -1,13 +1,12 @@ +import type { AnimationRefs } from '../Background/types'; + export const stopAnimationFrame = (refs: { animationFrameId: number; }): void => { cancelAnimationFrame(refs.animationFrameId); }; -export const cleanupAnimation = (refs: { - animationFrameId: number; - resizeObserver: ResizeObserver | null; -}): void => { +export const cleanupAnimation = (refs: AnimationRefs): void => { stopAnimationFrame(refs); refs.resizeObserver?.disconnect(); refs.resizeObserver = null; diff --git a/src/website/hooks/useScroll.tsx b/src/website/hooks/useScroll.tsx index 31ba30c..f8fbeb2 100644 --- a/src/website/hooks/useScroll.tsx +++ b/src/website/hooks/useScroll.tsx @@ -1,8 +1,3 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) https://awesomeyou.io and contributors. All rights reserved. - * Licensed under the GNU Affero General Public License v3.0. See https://github.com/wellwelwel/awesomeyou/blob/main/LICENSE for license information. - *--------------------------------------------------------------------------------------------*/ - import type { RefObject } from 'react'; import { useEffect } from 'react'; diff --git a/src/website/hooks/useScrollSpy.tsx b/src/website/hooks/useScrollSpy.tsx index 8d93852..b9b516f 100644 --- a/src/website/hooks/useScrollSpy.tsx +++ b/src/website/hooks/useScrollSpy.tsx @@ -9,14 +9,11 @@ type Options = { rootMargin?: string; }; -const DEFAULT_THRESHOLD: number | number[] = [0.1, 0.5]; -const DEFAULT_ROOT_MARGIN = '-120px 0px 0px 0px'; - export const useScrollSpy = ( sections: T, options?: Options ): string | null => { - const { threshold = DEFAULT_THRESHOLD, rootMargin = DEFAULT_ROOT_MARGIN } = + const { threshold = [0.1, 0.5], rootMargin = '-120px 0px 0px 0px' } = options ?? Object.create(null); const [activeId, setActiveId] = useState(null); diff --git a/src/website/scss/global/_animations.scss b/src/website/scss/global/_animations.scss index 59f9411..20843c0 100644 --- a/src/website/scss/global/_animations.scss +++ b/src/website/scss/global/_animations.scss @@ -10,12 +10,14 @@ @keyframes fadeUp { 0% { - transform: translateY(4rem); + position: relative; + top: 4rem; opacity: 0; } 100% { - transform: translateY(0); + position: relative; + top: 0; opacity: 1; } } @@ -61,24 +63,31 @@ } } -@keyframes breathe { +@keyframes morph { 0%, 100% { - rotate: 0deg; + border-radius: 50% 53% 52% 51% / 51% 53% 50% 52%; } - 50% { - rotate: 10deg; + 20% { + border-radius: 52% 50% 51% 53% / 53% 50% 52% 51%; + } + 40% { + border-radius: 51% 52% 53% 50% / 50% 52% 51% 53%; + } + 60% { + border-radius: 53% 51% 50% 52% / 52% 51% 53% 50%; + } + 80% { + border-radius: 50% 53% 52% 51% / 53% 50% 51% 52%; } } -@keyframes br-cycle { +@keyframes breathe { 0%, 100% { - opacity: 0; + rotate: 0deg; } - - 15%, - 25% { - opacity: 1; + 50% { + rotate: 10deg; } } diff --git a/src/website/scss/global/_root.scss b/src/website/scss/global/_root.scss index f133d3d..2a35fb3 100644 --- a/src/website/scss/global/_root.scss +++ b/src/website/scss/global/_root.scss @@ -33,6 +33,14 @@ body { @include load-css('../pages/partial/footer'); } + canvas.bg { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + z-index: -5; + } + .page-content { margin-top: 12.5rem; margin-bottom: 5rem; diff --git a/src/website/scss/pages/_gallery.scss b/src/website/scss/pages/_gallery.scss index 3a63b59..bd6420a 100644 --- a/src/website/scss/pages/_gallery.scss +++ b/src/website/scss/pages/_gallery.scss @@ -71,7 +71,9 @@ width: 3rem; height: 3rem; stroke: var(--ifm-color-primary-darker); - transition: transform 0.4s var(--ease-bounce-strong); + transition: + transform 0.4s var(--ease-bounce-strong), + stroke 0.3s ease; } } diff --git a/src/website/scss/pages/_waitlist.scss b/src/website/scss/pages/_waitlist.scss index 2911c07..51d7ac2 100644 --- a/src/website/scss/pages/_waitlist.scss +++ b/src/website/scss/pages/_waitlist.scss @@ -17,9 +17,12 @@ width: 8rem; height: 8rem; overflow: hidden; - will-change: transform; - animation: breathe 25s var(--ease-back-out-soft) alternate infinite; - transition: transform 0.4s ease; + animation: + morph 12.5s var(--ease-bounce-strong) alternate infinite, + breathe 25s var(--ease-back-out-soft) alternate infinite; + transition: + border-radius 0.4s ease, + rotate 0.4s ease; svg { display: block;