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
+
+```
+
+`` 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
-
-```
+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;