diff --git a/apps/web/src/providers/router/__tests__/__snapshots__/RouterProvider.test.tsx.snap b/apps/web/src/providers/router/__tests__/__snapshots__/RouterProvider.test.tsx.snap index 35c327b..1f803a4 100644 --- a/apps/web/src/providers/router/__tests__/__snapshots__/RouterProvider.test.tsx.snap +++ b/apps/web/src/providers/router/__tests__/__snapshots__/RouterProvider.test.tsx.snap @@ -446,132 +446,136 @@ exports[`RouterProvider should render router provider correctly 2`] = ` class="css-1wym9nu" >
- -
- - - - -
-
-

- AudioMesh -

-
-
-
-
+
- Product -

+

+ AudioMesh +

+
- - - - -
-
+ - Menu - + +
+ +
diff --git a/apps/web/src/screens/DocsPage/DocsPage.tsx b/apps/web/src/screens/DocsPage/DocsPage.tsx index 1bd97d7..61fd329 100644 --- a/apps/web/src/screens/DocsPage/DocsPage.tsx +++ b/apps/web/src/screens/DocsPage/DocsPage.tsx @@ -12,6 +12,7 @@ import { import { MdPreview, getMdFileDataInString } from '@components/MdPreview'; import { TitleBoxContainer } from '@components'; import { Icon, IconType } from '@assets'; +import { useTranslation } from 'react-i18next'; import { DOCS_TOPICS } from './config'; interface HeadingItem { @@ -47,6 +48,7 @@ const parseHeadings = (markdown: string): HeadingItem[] => { }; const DocsPage = () => { + const { t } = useTranslation(); const { docId } = useParams<{ docId?: string }>(); const navigate = useNavigate(); const [mdContent, setMdContent] = React.useState(''); @@ -93,7 +95,7 @@ const DocsPage = () => { return ( { > {DOCS_TOPICS.map((topic) => ( - + { opacity={0.8} mb={3} > - {topic.title} + {t(topic.titleKey)} {topic.items.map((item) => { @@ -166,7 +168,7 @@ const DocsPage = () => { fontSize="sm" fontWeight={isActive ? 'semibold' : 'medium'} > - {item.title} + {t(item.titleKey)} ); @@ -196,11 +198,11 @@ const DocsPage = () => { - {activeDoc?.title || 'Select Topic'} + {activeDoc ? t(activeDoc.titleKey) : t('DocsPage.selectTopic')} - Switch + {t('DocsPage.switch')} @@ -212,7 +214,7 @@ const DocsPage = () => { maxW="400px" > {DOCS_TOPICS.map((topic) => ( - + { color="fg.muted" opacity={0.7} > - {topic.title} + {t(topic.titleKey)} {topic.items.map((item) => ( @@ -235,7 +237,7 @@ const DocsPage = () => { activeDoc?.id === item.id ? 'bold' : 'normal' } > - {item.title} + {t(item.titleKey)} ))} @@ -252,7 +254,7 @@ const DocsPage = () => { minH="300px" width="full" aria-busy="true" - aria-label="Loading documentation" + aria-label={t('DocsPage.loadingDocumentation')} > @@ -288,7 +290,7 @@ const DocsPage = () => { color="fg.muted" opacity={0.8} > - In this Page + {t('DocsPage.inThisPage')} {headings.map((heading) => ( diff --git a/apps/web/src/screens/DocsPage/__tests__/__snapshots__/DocsPage.test.tsx.snap b/apps/web/src/screens/DocsPage/__tests__/__snapshots__/DocsPage.test.tsx.snap index 2d304e0..6a68c1a 100644 --- a/apps/web/src/screens/DocsPage/__tests__/__snapshots__/DocsPage.test.tsx.snap +++ b/apps/web/src/screens/DocsPage/__tests__/__snapshots__/DocsPage.test.tsx.snap @@ -117,7 +117,7 @@ exports[`DocsPage should render the documentation layout with sidebar and TOC 1`

- API Gateway Reference + API Reference

diff --git a/apps/web/src/screens/DocsPage/config.ts b/apps/web/src/screens/DocsPage/config.ts index 8fad2d2..23da3be 100644 --- a/apps/web/src/screens/DocsPage/config.ts +++ b/apps/web/src/screens/DocsPage/config.ts @@ -1,78 +1,79 @@ export interface DocItem { id: string; - title: string; + titleKey: string; filePath: string; } export interface DocTopic { - title: string; + titleKey: string; items: DocItem[]; } export const DOCS_TOPICS: DocTopic[] = [ { - title: 'Getting Started', + titleKey: 'DocsPage.gettingStarted', items: [ { id: 'introduction', - title: 'Introduction', + titleKey: 'NavigationBar.introduction', filePath: 'data/product/introduction', }, { id: 'quickstart', - title: 'Quick Start', + titleKey: 'NavigationBar.quickstart', filePath: 'data/product/quickstart', }, { id: 'setup-guide', - title: 'Setup Guide', + titleKey: 'NavigationBar.setup-guide', filePath: 'data/product/telegram-credentials', }, ], }, { - title: 'Architecture', + titleKey: 'DocsPage.architecture', items: [ { id: 'system-design', - title: 'System Design', + titleKey: 'NavigationBar.system-design', filePath: 'data/product/system-design', }, { id: 'mtproto', - title: 'MTProto Protocol', + titleKey: 'NavigationBar.mtproto', filePath: 'data/product/mtproto', }, { id: 'direct-storage', - title: 'Direct Storage', + titleKey: 'NavigationBar.direct-storage', filePath: 'data/product/direct-storage', }, ], }, { - title: 'Reference', + titleKey: 'DocsPage.reference', items: [ { id: 'api-reference', - title: 'API Gateway Reference', + titleKey: 'NavigationBar.api-reference', filePath: 'data/product/api-reference', }, ], }, { - title: 'Version History', + titleKey: 'NavigationBar.version-history', items: [ { id: 'version-history-desktop', - title: 'Desktop', + titleKey: 'NavigationBar.version-history-desktop', filePath: 'data/product/version-history-desktop', }, { id: 'version-history-mobile', - title: 'Mobile', + titleKey: 'NavigationBar.version-history-mobile', filePath: 'data/product/version-history-mobile', }, ], }, ]; + diff --git a/apps/web/src/screens/Host/CopyrightFooter/CopyrightFooter.tsx b/apps/web/src/screens/Host/CopyrightFooter/CopyrightFooter.tsx index a97a0a2..e414e68 100644 --- a/apps/web/src/screens/Host/CopyrightFooter/CopyrightFooter.tsx +++ b/apps/web/src/screens/Host/CopyrightFooter/CopyrightFooter.tsx @@ -53,7 +53,7 @@ const CopyrightFooter = () => {
- AudioMesh + {t('Title')} ) => ( @@ -29,21 +28,28 @@ const GitHubIcon = (props: React.SVGProps) => ( const NavigationBar = () => { const { t } = useTranslation(); - const padding = usePaddingForScreen(); + const { pathname } = useLocation(); + const isDocsPage = pathname.startsWith('/docs'); return ( - + {/* Logo */} { > - {name} + {t(`NavigationBar.${name.toLowerCase()}`)} @@ -101,7 +107,7 @@ const NavigationBar = () => { { py={2} size="sm" > - Menu + {t('NavigationBar.menu')} @@ -156,7 +162,7 @@ const NavigationBar = () => { }} > - {name} + {t(`NavigationBar.${name.toLowerCase()}`)} @@ -167,7 +173,8 @@ const NavigationBar = () => {
- ); +
+); }; export default NavigationBar; diff --git a/apps/web/src/screens/Host/NavigationBar/__tests__/__snapshots__/NavigationBar.test.tsx.snap b/apps/web/src/screens/Host/NavigationBar/__tests__/__snapshots__/NavigationBar.test.tsx.snap index 5c7caee..32e8e2d 100644 --- a/apps/web/src/screens/Host/NavigationBar/__tests__/__snapshots__/NavigationBar.test.tsx.snap +++ b/apps/web/src/screens/Host/NavigationBar/__tests__/__snapshots__/NavigationBar.test.tsx.snap @@ -3,132 +3,136 @@ exports[`NavigationBar should render standard navigation bar correctly 1`] = `
diff --git a/apps/web/src/screens/Host/__tests__/__snapshots__/Host.test.tsx.snap b/apps/web/src/screens/Host/__tests__/__snapshots__/Host.test.tsx.snap index 9170648..7f88830 100644 --- a/apps/web/src/screens/Host/__tests__/__snapshots__/Host.test.tsx.snap +++ b/apps/web/src/screens/Host/__tests__/__snapshots__/Host.test.tsx.snap @@ -6,132 +6,136 @@ exports[`Host should render correctly 1`] = ` class="css-1wym9nu" > diff --git a/apps/web/src/screens/MdPage/MdPage.tsx b/apps/web/src/screens/MdPage/MdPage.tsx index b71d5fc..a93e6b7 100644 --- a/apps/web/src/screens/MdPage/MdPage.tsx +++ b/apps/web/src/screens/MdPage/MdPage.tsx @@ -2,9 +2,11 @@ import { useEffect, useState } from 'react'; import { useLocation, Navigate } from 'react-router-dom'; import { Box, Container, Spinner } from '@chakra-ui/react'; import { MdPreview, getMdFileDataInString } from '@components/MdPreview'; +import { useTranslation } from 'react-i18next'; import { MD_PAGE_CONFIG } from './const'; const MdPage = () => { + const { t } = useTranslation(); const location = useLocation(); const [mdContent, setMdContent] = useState(''); const [isLoading, setIsLoading] = useState(true); @@ -48,7 +50,7 @@ const MdPage = () => { minH="300px" width="full" aria-busy="true" - aria-label="Loading document" + aria-label={t('MdPage.loadingDocument')} > diff --git a/apps/web/src/screens/landingPage/__tests__/__snapshots__/LandingPage.test.tsx.snap b/apps/web/src/screens/landingPage/__tests__/__snapshots__/LandingPage.test.tsx.snap index b24595f..fab38bd 100644 --- a/apps/web/src/screens/landingPage/__tests__/__snapshots__/LandingPage.test.tsx.snap +++ b/apps/web/src/screens/landingPage/__tests__/__snapshots__/LandingPage.test.tsx.snap @@ -13,13 +13,22 @@ exports[`LandingPage should render correctly for guest user 1`] = `
+
+ +
-

- Mesh Routing Console -

-
-
-
-
+
+

+ Mesh Routing Console +

+
+ class="chakra-stack css-141cw8v" + > +
+
+
+
-
-
-
- - - - - - - -
- - - + + -

- HOST -

-
-
+ + + + + +

+ HOST +

+
- - - - - -
-
+ + + + + +
-
-
+ class="chakra-stack css-14ad6p5" + > +
+
+
+
+

+ MacBook Pro +

-

- MacBook Pro -

-
-
- - - - -
-
+ + + + +
-
-
+ class="chakra-stack css-14ad6p5" + > +
+
+
+
+

+ iPad Pro +

-

- iPad Pro -

-
-
- - - - -
-
+ + + + +
-
-
+ class="chakra-stack css-14ad6p5" + > +
+
+
+
+

+ Galaxy S24 +

-

- Galaxy S24 -

-
-
- - - - - - -
-
+ + + + + + +
-
-
+ class="chakra-stack css-14ad6p5" + > +
+
+
+
+

+ Mesh Speaker +

-

- Mesh Speaker -

-
-
- - - - -
-
+ + + + +
-
-
+ class="chakra-stack css-14ad6p5" + > +
+
+
+
+

+ iPhone 15 +

-

- iPhone 15 -

-
-
+
- - - - + + - - + + + - - + - - + - - - - - - + /> + + + + + + +
+ +
+
- + +
@@ -939,7 +1070,7 @@ exports[`LandingPage should render correctly for guest user 1`] = ` in="SourceGraphic" in2="map" result="dispGreen" - scale="15" + scale="12" xChannelSelector="R" yChannelSelector="G" /> @@ -957,7 +1088,7 @@ exports[`LandingPage should render correctly for guest user 1`] = ` in="SourceGraphic" in2="map" result="dispBlue" - scale="15" + scale="12" xChannelSelector="R" yChannelSelector="G" /> @@ -992,146 +1123,28 @@ exports[`LandingPage should render correctly for guest user 1`] = `
- -
-
-
-
- - - - - - - - - - - - - - - - -
-
-

- > - [SYS] AudioMesh Routing Daemon initialised on 127.0.0.1:3000 -

-

- > - [NET] Dual-protocol synchronization running: Wi-Fi + Bluetooth -

-

- > - [SYS] Ready for connection discovery... -

+

+ > + [SYS] AudioMesh Routing Daemon initialised on 127.0.0.1:3000 +

+

+ > + [NET] Dual-protocol synchronization running: Wi-Fi + Bluetooth +

+

+ > + [SYS] Ready for connection discovery... +

+
@@ -1707,10 +1720,20 @@ exports[`LandingPage should render correctly for guest user 1`] = `
+
+ WebGL not supported +
+
+
@@ -1785,7 +1808,7 @@ exports[`LandingPage should render correctly for guest user 1`] = ` in="SourceGraphic" in2="map" result="dispGreen" - scale="-180" + scale="20" xChannelSelector="R" yChannelSelector="G" /> @@ -1803,7 +1826,7 @@ exports[`LandingPage should render correctly for guest user 1`] = ` in="SourceGraphic" in2="map" result="dispBlue" - scale="-160" + scale="20" xChannelSelector="R" yChannelSelector="G" /> @@ -1883,7 +1906,7 @@ exports[`LandingPage should render correctly for guest user 1`] = `
@@ -1937,7 +1960,7 @@ exports[`LandingPage should render correctly for guest user 1`] = ` in="SourceGraphic" in2="map" result="dispGreen" - scale="-180" + scale="20" xChannelSelector="R" yChannelSelector="G" /> @@ -1955,7 +1978,7 @@ exports[`LandingPage should render correctly for guest user 1`] = ` in="SourceGraphic" in2="map" result="dispBlue" - scale="-160" + scale="20" xChannelSelector="R" yChannelSelector="G" /> @@ -2026,7 +2049,7 @@ exports[`LandingPage should render correctly for guest user 1`] = `
@@ -2080,7 +2103,7 @@ exports[`LandingPage should render correctly for guest user 1`] = ` in="SourceGraphic" in2="map" result="dispGreen" - scale="-180" + scale="20" xChannelSelector="R" yChannelSelector="G" /> @@ -2098,7 +2121,7 @@ exports[`LandingPage should render correctly for guest user 1`] = ` in="SourceGraphic" in2="map" result="dispBlue" - scale="-160" + scale="20" xChannelSelector="R" yChannelSelector="G" /> @@ -2174,7 +2197,7 @@ exports[`LandingPage should render correctly for guest user 1`] = `
@@ -2228,7 +2251,7 @@ exports[`LandingPage should render correctly for guest user 1`] = ` in="SourceGraphic" in2="map" result="dispGreen" - scale="-180" + scale="20" xChannelSelector="R" yChannelSelector="G" /> @@ -2246,7 +2269,7 @@ exports[`LandingPage should render correctly for guest user 1`] = ` in="SourceGraphic" in2="map" result="dispBlue" - scale="-160" + scale="20" xChannelSelector="R" yChannelSelector="G" /> diff --git a/apps/web/src/screens/landingPage/components/FeaturesSection.tsx b/apps/web/src/screens/landingPage/components/FeaturesSection.tsx index b786f99..29951b0 100644 --- a/apps/web/src/screens/landingPage/components/FeaturesSection.tsx +++ b/apps/web/src/screens/landingPage/components/FeaturesSection.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import { Box, VStack, Heading, Text, SimpleGrid, Flex } from '@chakra-ui/react'; +import { Box, VStack, Heading, Text, SimpleGrid, Flex, useToken } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import { Icon, IconType } from '@assets'; -import { GlassBox } from '@components'; +import { GlassBox, LightPillar } from '@components'; const featureCards = [ { @@ -33,16 +33,45 @@ const featureCards = [ export const FeaturesSection = () => { const { t } = useTranslation(); + const [topColor, bottomColor] = useToken('colors', ['pillar.top', 'pillar.bottom']); return ( - + {/* Light Pillar background decoration */} + + + + + { - + {featureCards.map(({ icon, titleKey, descKey, className }) => ( { justifyContent="center" width="100%" minHeight="90vh" - className={styles.techGrid} py={{ base: 10, md: 14 }} px={{ base: 6, md: 16 }} position="relative" @@ -38,6 +37,26 @@ export const HeroSection = ({ children }: HeroSectionProps) => { 'radial(circle at 50% -20%, rgba(139, 92, 246, 0.15), bg.default 80%)', }} > + {/* Dynamic Grid Animation Background */} + + + + {/* Immersive moving glows */}
@@ -51,6 +70,7 @@ export const HeroSection = ({ children }: HeroSectionProps) => { alignItems="center" justifyContent="space-between" zIndex={2} + pointerEvents="none" > {/* LEFT: Copy content */} { gap={6} flex={1.1} maxW={{ base: 'xl', lg: 'none' }} + pointerEvents="auto" > @@ -197,7 +218,9 @@ export const HeroSection = ({ children }: HeroSectionProps) => { {/* RIGHT: Diagnostics Terminal */} - {children} + + {children} + {/* Scroll indicator */} diff --git a/apps/web/src/screens/landingPage/components/MeshVisualizer.tsx b/apps/web/src/screens/landingPage/components/MeshVisualizer.tsx index 7f7488f..6d89e7a 100644 --- a/apps/web/src/screens/landingPage/components/MeshVisualizer.tsx +++ b/apps/web/src/screens/landingPage/components/MeshVisualizer.tsx @@ -374,7 +374,7 @@ export const MeshVisualizer = ({ > - HOST + {t('LandingPage.host')} diff --git a/shared/common/package.json b/shared/common/package.json index dfd396f..d3ea1f0 100644 --- a/shared/common/package.json +++ b/shared/common/package.json @@ -16,13 +16,15 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet-async": "^2.0.5", - "react-i18next": "^14.1.1" + "react-i18next": "^14.1.1", + "three": "^0.184.0" }, "devDependencies": { "@tanstack/react-query": "^5.51.23", "@types/node": "^16.18.91", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/three": "^0.184.1", "react-router-dom": "^7.15.0", "typescript": "^6.0.3" } diff --git a/shared/common/src/components/GlassBox/GlassBox.tsx b/shared/common/src/components/GlassBox/GlassBox.tsx index 07a9346..d68f36f 100644 --- a/shared/common/src/components/GlassBox/GlassBox.tsx +++ b/shared/common/src/components/GlassBox/GlassBox.tsx @@ -93,8 +93,44 @@ const GlassSurface: React.FC = ({ const greenChannelRef = useRef(null); const blueChannelRef = useRef(null); const gaussianBlurRef = useRef(null); + const globalIsDark = useDarkMode(); + const [localIsDark, setLocalIsDark] = useState(false); - const isDarkMode = useDarkMode(); + useEffect(() => { + if (typeof window === 'undefined') return; + + const checkDark = () => { + if (!containerRef.current) return; + const closestDark = containerRef.current.closest('.dark'); + setLocalIsDark(!!closestDark); + }; + + checkDark(); + + const observer = new MutationObserver(() => { + checkDark(); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'], + }); + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class'], + }); + + if (containerRef.current?.parentElement) { + observer.observe(containerRef.current.parentElement, { + attributes: true, + attributeFilter: ['class'], + }); + } + + return () => observer.disconnect(); + }, [globalIsDark]); + + const isDarkMode = localIsDark || globalIsDark; const generateDisplacementMap = () => { const rect = containerRef.current?.getBoundingClientRect(); diff --git a/shared/common/src/components/GridAnimation/GridAnimation.tsx b/shared/common/src/components/GridAnimation/GridAnimation.tsx new file mode 100644 index 0000000..e017257 --- /dev/null +++ b/shared/common/src/components/GridAnimation/GridAnimation.tsx @@ -0,0 +1,406 @@ +import React, { useRef, useEffect } from 'react'; +import { GridAnimationProps, GridOffset } from './types'; +import { useTheme } from 'next-themes'; + + +const GridAnimation = ({ + direction = 'right', + speed = 1, + borderColor = '#999', + squareSize = 30, + hoverFillColor = '#222', + shape = 'square', + hoverTrailAmount = 2 +}: GridAnimationProps) => { + const { resolvedTheme } = useTheme(); + const isDark = resolvedTheme === 'dark'; + const canvasRef = useRef(null); + const requestRef = useRef(null); + const numSquaresX = useRef(0); + const numSquaresY = useRef(0); + const gridOffset = useRef({ x: 0, y: 0 }); + const hoveredSquareRef = useRef(null); + const trailCells = useRef([]); + const cellOpacities = useRef>(new Map()); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + + const isHex = shape === 'hexagon'; + const isTri = shape === 'triangle'; + const hexHoriz = squareSize * 1.5; + const hexVert = squareSize * Math.sqrt(3); + + const resizeCanvas = () => { + canvas.width = canvas.offsetWidth; + canvas.height = canvas.offsetHeight; + numSquaresX.current = Math.ceil(canvas.width / squareSize) + 1; + numSquaresY.current = Math.ceil(canvas.height / squareSize) + 1; + }; + + window.addEventListener('resize', resizeCanvas); + resizeCanvas(); + + const drawHex = (cx: number, cy: number, size: number) => { + if (!ctx) return; + ctx.beginPath(); + for (let i = 0; i < 6; i++) { + const angle = (Math.PI / 3) * i; + const vx = cx + size * Math.cos(angle); + const vy = cy + size * Math.sin(angle); + if (i === 0) ctx.moveTo(vx, vy); + else ctx.lineTo(vx, vy); + } + ctx.closePath(); + }; + + const drawCircle = (cx: number, cy: number, size: number) => { + if (!ctx) return; + ctx.beginPath(); + ctx.arc(cx, cy, size / 2, 0, Math.PI * 2); + ctx.closePath(); + }; + + const drawTriangle = (cx: number, cy: number, size: number, flip: boolean) => { + if (!ctx) return; + ctx.beginPath(); + if (flip) { + ctx.moveTo(cx, cy + size / 2); + ctx.lineTo(cx + size / 2, cy - size / 2); + ctx.lineTo(cx - size / 2, cy - size / 2); + } else { + ctx.moveTo(cx, cy - size / 2); + ctx.lineTo(cx + size / 2, cy + size / 2); + ctx.lineTo(cx - size / 2, cy + size / 2); + } + ctx.closePath(); + }; + + const drawGrid = () => { + if (!ctx) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (isHex) { + const colShift = Math.floor(gridOffset.current.x / hexHoriz); + const offsetX = ((gridOffset.current.x % hexHoriz) + hexHoriz) % hexHoriz; + const offsetY = ((gridOffset.current.y % hexVert) + hexVert) % hexVert; + + const cols = Math.ceil(canvas.width / hexHoriz) + 3; + const rows = Math.ceil(canvas.height / hexVert) + 3; + + for (let col = -2; col < cols; col++) { + for (let row = -2; row < rows; row++) { + const cx = col * hexHoriz + offsetX; + const cy = row * hexVert + ((col + colShift) % 2 !== 0 ? hexVert / 2 : 0) + offsetY; + + const cellKey = `${col},${row}`; + const alpha = cellOpacities.current.get(cellKey); + if (alpha) { + ctx.globalAlpha = alpha; + drawHex(cx, cy, squareSize); + ctx.fillStyle = hoverFillColor; + ctx.fill(); + ctx.globalAlpha = 1; + } + + drawHex(cx, cy, squareSize); + ctx.strokeStyle = borderColor; + ctx.stroke(); + } + } + } else if (isTri) { + const halfW = squareSize / 2; + const colShift = Math.floor(gridOffset.current.x / halfW); + const rowShift = Math.floor(gridOffset.current.y / squareSize); + const offsetX = ((gridOffset.current.x % halfW) + halfW) % halfW; + const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize; + + const cols = Math.ceil(canvas.width / halfW) + 4; + const rows = Math.ceil(canvas.height / squareSize) + 4; + + for (let col = -2; col < cols; col++) { + for (let row = -2; row < rows; row++) { + const cx = col * halfW + offsetX; + const cy = row * squareSize + squareSize / 2 + offsetY; + const flip = ((col + colShift + row + rowShift) % 2 + 2) % 2 !== 0; + + const cellKey = `${col},${row}`; + const alpha = cellOpacities.current.get(cellKey); + if (alpha) { + ctx.globalAlpha = alpha; + drawTriangle(cx, cy, squareSize, flip); + ctx.fillStyle = hoverFillColor; + ctx.fill(); + ctx.globalAlpha = 1; + } + + drawTriangle(cx, cy, squareSize, flip); + ctx.strokeStyle = borderColor; + ctx.stroke(); + } + } + } else if (shape === 'circle') { + const offsetX = ((gridOffset.current.x % squareSize) + squareSize) % squareSize; + const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize; + + const cols = Math.ceil(canvas.width / squareSize) + 3; + const rows = Math.ceil(canvas.height / squareSize) + 3; + + for (let col = -2; col < cols; col++) { + for (let row = -2; row < rows; row++) { + const cx = col * squareSize + squareSize / 2 + offsetX; + const cy = row * squareSize + squareSize / 2 + offsetY; + + const cellKey = `${col},${row}`; + const alpha = cellOpacities.current.get(cellKey); + if (alpha) { + ctx.globalAlpha = alpha; + drawCircle(cx, cy, squareSize); + ctx.fillStyle = hoverFillColor; + ctx.fill(); + ctx.globalAlpha = 1; + } + + drawCircle(cx, cy, squareSize); + ctx.strokeStyle = borderColor; + ctx.stroke(); + } + } + } else { + const offsetX = ((gridOffset.current.x % squareSize) + squareSize) % squareSize; + const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize; + + const cols = Math.ceil(canvas.width / squareSize) + 3; + const rows = Math.ceil(canvas.height / squareSize) + 3; + + for (let col = -2; col < cols; col++) { + for (let row = -2; row < rows; row++) { + const sx = col * squareSize + offsetX; + const sy = row * squareSize + offsetY; + + const cellKey = `${col},${row}`; + const alpha = cellOpacities.current.get(cellKey); + if (alpha) { + ctx.globalAlpha = alpha; + ctx.fillStyle = hoverFillColor; + ctx.fillRect(sx, sy, squareSize, squareSize); + ctx.globalAlpha = 1; + } + + ctx.strokeStyle = borderColor; + ctx.strokeRect(sx, sy, squareSize, squareSize); + } + } + } + + const gradient = ctx.createRadialGradient( + canvas.width / 2, + canvas.height / 2, + 0, + canvas.width / 2, + canvas.height / 2, + Math.sqrt(canvas.width ** 2 + canvas.height ** 2) / 2 + ); + gradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); + gradient.addColorStop(1, isDark ? '#120F17' : '#f8f9fa'); + + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }; + + const updateAnimation = () => { + const effectiveSpeed = Math.max(speed, 0.1); + const wrapX = isHex ? hexHoriz * 2 : squareSize; + const wrapY = isHex ? hexVert : isTri ? squareSize * 2 : squareSize; + + switch (direction) { + case 'right': + gridOffset.current.x = (gridOffset.current.x - effectiveSpeed + wrapX) % wrapX; + break; + case 'left': + gridOffset.current.x = (gridOffset.current.x + effectiveSpeed + wrapX) % wrapX; + break; + case 'up': + gridOffset.current.y = (gridOffset.current.y + effectiveSpeed + wrapY) % wrapY; + break; + case 'down': + gridOffset.current.y = (gridOffset.current.y - effectiveSpeed + wrapY) % wrapY; + break; + case 'diagonal': + gridOffset.current.x = (gridOffset.current.x - effectiveSpeed + wrapX) % wrapX; + gridOffset.current.y = (gridOffset.current.y - effectiveSpeed + wrapY) % wrapY; + break; + default: + break; + } + + updateCellOpacities(); + drawGrid(); + requestRef.current = requestAnimationFrame(updateAnimation); + }; + + const updateCellOpacities = () => { + const targets = new Map(); + + if (hoveredSquareRef.current) { + targets.set(`${hoveredSquareRef.current.x},${hoveredSquareRef.current.y}`, 1); + } + + if (hoverTrailAmount > 0) { + for (let i = 0; i < trailCells.current.length; i++) { + const t = trailCells.current[i]; + const key = `${t.x},${t.y}`; + if (!targets.has(key)) { + targets.set(key, (trailCells.current.length - i) / (trailCells.current.length + 1)); + } + } + } + + for (const [key] of targets) { + if (!cellOpacities.current.has(key)) { + cellOpacities.current.set(key, 0); + } + } + + for (const [key, opacity] of cellOpacities.current) { + const target = targets.get(key) || 0; + const next = opacity + (target - opacity) * 0.15; + if (next < 0.005) { + cellOpacities.current.delete(key); + } else { + cellOpacities.current.set(key, next); + } + } + }; + + const handleMouseMove = (event: MouseEvent) => { + const rect = canvas.getBoundingClientRect(); + const mouseX = event.clientX - rect.left; + const mouseY = event.clientY - rect.top; + + if (isHex) { + const colShift = Math.floor(gridOffset.current.x / hexHoriz); + const offsetX = ((gridOffset.current.x % hexHoriz) + hexHoriz) % hexHoriz; + const offsetY = ((gridOffset.current.y % hexVert) + hexVert) % hexVert; + const adjustedX = mouseX - offsetX; + const adjustedY = mouseY - offsetY; + + const col = Math.round(adjustedX / hexHoriz); + const rowOffset = (col + colShift) % 2 !== 0 ? hexVert / 2 : 0; + const row = Math.round((adjustedY - rowOffset) / hexVert); + + if ( + !hoveredSquareRef.current || + hoveredSquareRef.current.x !== col || + hoveredSquareRef.current.y !== row + ) { + if (hoveredSquareRef.current && hoverTrailAmount > 0) { + trailCells.current.unshift({ ...hoveredSquareRef.current }); + if (trailCells.current.length > hoverTrailAmount) trailCells.current.length = hoverTrailAmount; + } + hoveredSquareRef.current = { x: col, y: row }; + } + } else if (isTri) { + const halfW = squareSize / 2; + const offsetX = ((gridOffset.current.x % halfW) + halfW) % halfW; + const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize; + + const adjustedX = mouseX - offsetX; + const adjustedY = mouseY - offsetY; + + const col = Math.round(adjustedX / halfW); + const row = Math.floor(adjustedY / squareSize); + + if ( + !hoveredSquareRef.current || + hoveredSquareRef.current.x !== col || + hoveredSquareRef.current.y !== row + ) { + if (hoveredSquareRef.current && hoverTrailAmount > 0) { + trailCells.current.unshift({ ...hoveredSquareRef.current }); + if (trailCells.current.length > hoverTrailAmount) trailCells.current.length = hoverTrailAmount; + } + hoveredSquareRef.current = { x: col, y: row }; + } + } else if (shape === 'circle') { + const offsetX = ((gridOffset.current.x % squareSize) + squareSize) % squareSize; + const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize; + + const adjustedX = mouseX - offsetX; + const adjustedY = mouseY - offsetY; + + const col = Math.round(adjustedX / squareSize); + const row = Math.round(adjustedY / squareSize); + + if ( + !hoveredSquareRef.current || + hoveredSquareRef.current.x !== col || + hoveredSquareRef.current.y !== row + ) { + if (hoveredSquareRef.current && hoverTrailAmount > 0) { + trailCells.current.unshift({ ...hoveredSquareRef.current }); + if (trailCells.current.length > hoverTrailAmount) trailCells.current.length = hoverTrailAmount; + } + hoveredSquareRef.current = { x: col, y: row }; + } + } else { + const offsetX = ((gridOffset.current.x % squareSize) + squareSize) % squareSize; + const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize; + + const adjustedX = mouseX - offsetX; + const adjustedY = mouseY - offsetY; + + const col = Math.floor(adjustedX / squareSize); + const row = Math.floor(adjustedY / squareSize); + + if ( + !hoveredSquareRef.current || + hoveredSquareRef.current.x !== col || + hoveredSquareRef.current.y !== row + ) { + if (hoveredSquareRef.current && hoverTrailAmount > 0) { + trailCells.current.unshift({ ...hoveredSquareRef.current }); + if (trailCells.current.length > hoverTrailAmount) trailCells.current.length = hoverTrailAmount; + } + hoveredSquareRef.current = { x: col, y: row }; + } + } + }; + + const handleMouseLeave = () => { + if (hoveredSquareRef.current && hoverTrailAmount > 0) { + trailCells.current.unshift({ ...hoveredSquareRef.current }); + if (trailCells.current.length > hoverTrailAmount) trailCells.current.length = hoverTrailAmount; + } + hoveredSquareRef.current = null; + }; + + canvas.addEventListener('mousemove', handleMouseMove); + canvas.addEventListener('mouseleave', handleMouseLeave); + requestRef.current = requestAnimationFrame(updateAnimation); + + return () => { + window.removeEventListener('resize', resizeCanvas); + if (requestRef.current) cancelAnimationFrame(requestRef.current); + canvas.removeEventListener('mousemove', handleMouseMove); + canvas.removeEventListener('mouseleave', handleMouseLeave); + }; + }, [direction, speed, borderColor, hoverFillColor, squareSize, shape, hoverTrailAmount, isDark]); + + return ( + + ); +}; + +export default GridAnimation; diff --git a/shared/common/src/components/GridAnimation/types.tsx b/shared/common/src/components/GridAnimation/types.tsx new file mode 100644 index 0000000..e7ed003 --- /dev/null +++ b/shared/common/src/components/GridAnimation/types.tsx @@ -0,0 +1,16 @@ +export type CanvasStrokeStyle = string | CanvasGradient | CanvasPattern; + +export interface GridOffset { + x: number; + y: number; +} + +export interface GridAnimationProps { + direction?: 'diagonal' | 'up' | 'right' | 'down' | 'left'; + speed?: number; + borderColor?: CanvasStrokeStyle; + squareSize?: number; + hoverFillColor?: CanvasStrokeStyle; + shape?: 'square' | 'hexagon' | 'circle' | 'triangle'; + hoverTrailAmount?: number; +} diff --git a/shared/common/src/components/Pillar/Pillar.tsx b/shared/common/src/components/Pillar/Pillar.tsx new file mode 100644 index 0000000..6fa7828 --- /dev/null +++ b/shared/common/src/components/Pillar/Pillar.tsx @@ -0,0 +1,481 @@ +import React, { useRef, useEffect, useState } from 'react'; +import * as THREE from 'three'; +import { LightPillarProps } from './types'; + +const parseColor = (hex: string): THREE.Vector3 => { + let cleanHex = hex; + if (hex.startsWith('#')) { + if (hex.length === 9) { + cleanHex = hex.slice(0, 7); + } else if (hex.length === 5) { + cleanHex = hex.slice(0, 4); + } + } + const color = new THREE.Color(cleanHex); + return new THREE.Vector3(color.r, color.g, color.b); +}; + + +const LightPillar: React.FC = ({ + topColor = '#5227FF', + bottomColor = '#FF9FFC', + intensity = 1.0, + rotationSpeed = 0.3, + interactive = false, + className = '', + glowAmount = 0.005, + pillarWidth = 3.0, + pillarHeight = 0.4, + noiseIntensity = 0.5, + mixBlendMode = 'screen', + pillarRotation = 0, + quality = 'high' +}) => { + const containerRef = useRef(null); + const rafRef = useRef(null); + const rendererRef = useRef(null); + const materialRef = useRef(null); + const sceneRef = useRef(null); + const cameraRef = useRef(null); + const geometryRef = useRef(null); + const mouseRef = useRef(new THREE.Vector2(0, 0)); + const timeRef = useRef(0); + const rotationSpeedRef = useRef(rotationSpeed); + const [webGLSupported, setWebGLSupported] = useState(true); + + // Check WebGL support + useEffect(() => { + const canvas = document.createElement('canvas'); + const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); + if (!gl) { + setWebGLSupported(false); + } + }, []); + + useEffect(() => { + if (!containerRef.current || !webGLSupported) return; + + const container = containerRef.current; + const width = container.clientWidth || 300; + const height = container.clientHeight || 300; + console.log('LightPillar client dimensions (initial):', width, height); + + + + const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + const isLowEndDevice = isMobile || (navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4); + + let effectiveQuality = quality; + if (isLowEndDevice && quality === 'high') effectiveQuality = 'medium'; + if (isMobile && quality !== 'low') effectiveQuality = 'low'; + + const qualitySettings = { + low: { iterations: 24, waveIterations: 1, pixelRatio: 0.5, precision: 'mediump', stepMultiplier: 1.5 }, + medium: { iterations: 40, waveIterations: 2, pixelRatio: 0.65, precision: 'mediump', stepMultiplier: 1.2 }, + high: { + iterations: 80, + waveIterations: 4, + pixelRatio: Math.min(window.devicePixelRatio, 2), + precision: 'highp', + stepMultiplier: 1.0 + } + }; + + const settings = qualitySettings[effectiveQuality] || qualitySettings.medium; + + // Scene setup + const scene = new THREE.Scene(); + sceneRef.current = scene; + const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); + cameraRef.current = camera; + + let renderer: THREE.WebGLRenderer; + try { + renderer = new THREE.WebGLRenderer({ + antialias: false, + alpha: true, + powerPreference: effectiveQuality === 'low' ? 'low-power' : 'high-performance', + precision: settings.precision, + stencil: false, + depth: false + }); + } catch (error) { + console.error('Failed to create WebGL renderer:', error); + setWebGLSupported(false); + return; + } + + renderer.setSize(width, height); + renderer.setPixelRatio(settings.pixelRatio); + container.appendChild(renderer.domElement); + rendererRef.current = renderer; + + // Shader material + const vertexShader = ` + varying vec2 vUv; + void main() { + vUv = uv; + gl_Position = vec4(position, 1.0); + } + `; + + const fragmentShader = ` + uniform float uTime; + uniform vec2 uResolution; + uniform vec2 uMouse; + uniform vec3 uTopColor; + uniform vec3 uBottomColor; + uniform float uIntensity; + uniform bool uInteractive; + uniform float uGlowAmount; + uniform float uPillarWidth; + uniform float uPillarHeight; + uniform float uNoiseIntensity; + uniform float uPillarRotation; + uniform float uRotCos; + uniform float uRotSin; + uniform float uPillarRotCos; + uniform float uPillarRotSin; + uniform float uWaveSin[4]; + uniform float uWaveCos[4]; + varying vec2 vUv; + + const float PI = 3.141592653589793; + const float EPSILON = 0.001; + const float E = 2.71828182845904523536; + + float noise(vec2 coord) { + vec2 r = (E * sin(E * coord)); + return fract(r.x * r.y * (1.0 + coord.x)); + } + + void main() { + vec2 fragCoord = vUv * uResolution; + vec2 uv = (fragCoord * 2.0 - uResolution) / uResolution.y; + + // Apply 2D rotation to UV coordinates using pre-computed values + uv = vec2( + uv.x * uPillarRotCos - uv.y * uPillarRotSin, + uv.x * uPillarRotSin + uv.y * uPillarRotCos + ); + + vec3 origin = vec3(0.0, 0.0, -10.0); + vec3 direction = normalize(vec3(uv, 1.0)); + + float maxDepth = 50.0; + float depth = 0.1; + + // Use pre-computed rotation values (or mouse-based) + float rotCos = uRotCos; + float rotSin = uRotSin; + if(uInteractive && length(uMouse) > 0.0) { + float mouseAngle = uMouse.x * PI * 2.0; + rotCos = cos(mouseAngle); + rotSin = sin(mouseAngle); + } + + vec3 color = vec3(0.0); + + const int ITERATIONS = ${settings.iterations}; + const int WAVE_ITERATIONS = ${settings.waveIterations}; + const float STEP_MULT = ${settings.stepMultiplier.toFixed(1)}; + + for(int i = 0; i < ITERATIONS; i++) { + vec3 pos = origin + direction * depth; + + // Inline rotation: pos.xz *= rotMat + float newX = pos.x * rotCos - pos.z * rotSin; + float newZ = pos.x * rotSin + pos.z * rotCos; + pos.x = newX; + pos.z = newZ; + + // Apply vertical scaling and wave deformation + vec3 deformed = pos; + deformed.y *= uPillarHeight; + deformed = deformed + vec3(0.0, uTime, 0.0); + + // Inlined wave deformation + float frequency = 1.0; + float amplitude = 1.0; + for(int j = 0; j < WAVE_ITERATIONS; j++) { + // Inline rotation: deformed.xz *= rot(0.4) using pre-computed + float wx = deformed.x * uWaveCos[j] - deformed.z * uWaveSin[j]; + float wz = deformed.x * uWaveSin[j] + deformed.z * uWaveCos[j]; + deformed.x = wx; + deformed.z = wz; + + float phase = uTime * float(j) * 2.0; + vec3 oscillation = cos(deformed.zxy * frequency - phase); + deformed += oscillation * amplitude; + frequency *= 2.0; + amplitude *= 0.5; + } + + // Calculate distance field using cosine pattern + vec2 cosinePair = cos(deformed.xz); + float fieldDistance = length(cosinePair) - 0.2; + + // Radial boundary constraint (inlined blendMax) + float radialBound = length(pos.xz) - uPillarWidth; + float k = 4.0; + float h = max(k - abs(-radialBound - (-fieldDistance)), 0.0); + fieldDistance = -(min(-radialBound, -fieldDistance) - h * h * 0.25 / k); + + fieldDistance = abs(fieldDistance) * 0.15 + 0.01; + + vec3 gradient = mix(uBottomColor, uTopColor, smoothstep(15.0, -15.0, pos.y)); + color += gradient / fieldDistance; + + if(fieldDistance < EPSILON || depth > maxDepth) break; + depth += fieldDistance * STEP_MULT; + } + + // Normalize by pillar width to maintain consistent glow regardless of size + float widthNormalization = uPillarWidth / 3.0; + color = tanh(color * uGlowAmount / widthNormalization); + + // Add noise postprocessing + float rnd = noise(gl_FragCoord.xy); + color -= rnd / 15.0 * uNoiseIntensity; + + vec3 finalColor = color * uIntensity; + float alpha = max(max(finalColor.r, finalColor.g), finalColor.b); + gl_FragColor = vec4(finalColor, clamp(alpha, 0.0, 1.0)); + } + `; + + // Pre-compute wave rotation values + const waveAngle = 0.4; + const waveSinValues = new Float32Array(4); + const waveCosValues = new Float32Array(4); + for (let i = 0; i < 4; i++) { + waveSinValues[i] = Math.sin(waveAngle); + waveCosValues[i] = Math.cos(waveAngle); + } + + // Pre-compute pillar rotation + const pillarRotRad = (pillarRotation * Math.PI) / 180.0; + const pillarRotCos = Math.cos(pillarRotRad); + const pillarRotSin = Math.sin(pillarRotRad); + + const material = new THREE.ShaderMaterial({ + vertexShader, + fragmentShader, + uniforms: { + uTime: { value: 0 }, + uResolution: { value: new THREE.Vector2(width, height) }, + uMouse: { value: mouseRef.current }, + uTopColor: { value: parseColor(topColor) }, + uBottomColor: { value: parseColor(bottomColor) }, + uIntensity: { value: intensity }, + uInteractive: { value: interactive }, + uGlowAmount: { value: glowAmount }, + uPillarWidth: { value: pillarWidth }, + uPillarHeight: { value: pillarHeight }, + uNoiseIntensity: { value: noiseIntensity }, + uPillarRotation: { value: pillarRotation }, + uRotCos: { value: 1.0 }, + uRotSin: { value: 0.0 }, + uPillarRotCos: { value: pillarRotCos }, + uPillarRotSin: { value: pillarRotSin }, + uWaveSin: { value: waveSinValues }, + uWaveCos: { value: waveCosValues } + }, + transparent: true, + depthWrite: false, + depthTest: false + }); + materialRef.current = material; + + const geometry = new THREE.PlaneGeometry(2, 2); + geometryRef.current = geometry; + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // Mouse interaction - throttled for performance + let mouseMoveTimeout: number | null = null; + const handleMouseMove = (event: MouseEvent) => { + if (!interactive) return; + + if (mouseMoveTimeout) return; + + mouseMoveTimeout = window.setTimeout(() => { + mouseMoveTimeout = null; + }, 16); // ~60fps throttle + + const rect = container.getBoundingClientRect(); + const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + mouseRef.current.set(x, y); + }; + + if (interactive) { + container.addEventListener('mousemove', handleMouseMove, { passive: true }); + } + + // Animation loop with fixed timestep + let lastTime = performance.now(); + const targetFPS = effectiveQuality === 'low' ? 30 : 60; + const frameTime = 1000 / targetFPS; + + const animate = (currentTime: number) => { + if (!materialRef.current || !rendererRef.current || !sceneRef.current || !cameraRef.current) return; + + const deltaTime = currentTime - lastTime; + + if (deltaTime >= frameTime) { + timeRef.current += 0.016 * rotationSpeedRef.current; + materialRef.current.uniforms.uTime.value = timeRef.current; + + // Pre-compute rotation on CPU + const rotAngle = timeRef.current * 0.3; + materialRef.current.uniforms.uRotCos.value = Math.cos(rotAngle); + materialRef.current.uniforms.uRotSin.value = Math.sin(rotAngle); + + rendererRef.current.render(sceneRef.current, cameraRef.current); + lastTime = currentTime - (deltaTime % frameTime); + } + + rafRef.current = requestAnimationFrame(animate); + }; + rafRef.current = requestAnimationFrame(animate); + + // Handle resize using ResizeObserver + const resizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + const w = entry.contentRect.width; + const h = entry.contentRect.height; + if (w > 0 && h > 0) { + if (rendererRef.current && materialRef.current) { + rendererRef.current.setSize(w, h); + materialRef.current.uniforms.uResolution.value.set(w, h); + } + } + } + }); + resizeObserver.observe(container); + + // Cleanup + return () => { + resizeObserver.disconnect(); + if (interactive) { + container.removeEventListener('mousemove', handleMouseMove); + } + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + if (rendererRef.current) { + rendererRef.current.dispose(); + rendererRef.current.forceContextLoss(); + if (container.contains(rendererRef.current.domElement)) { + container.removeChild(rendererRef.current.domElement); + } + } + if (materialRef.current) { + materialRef.current.dispose(); + } + if (geometryRef.current) { + geometryRef.current.dispose(); + } + + rendererRef.current = null; + materialRef.current = null; + sceneRef.current = null; + cameraRef.current = null; + geometryRef.current = null; + rafRef.current = null; + }; + }, [webGLSupported, quality]); + + useEffect(() => { + rotationSpeedRef.current = rotationSpeed; + }, [rotationSpeed]); + + useEffect(() => { + if (!materialRef.current) return; + materialRef.current.uniforms.uTopColor.value = parseColor(topColor); + }, [topColor]); + + useEffect(() => { + if (!materialRef.current) return; + materialRef.current.uniforms.uBottomColor.value = parseColor(bottomColor); + }, [bottomColor]); + + useEffect(() => { + if (!materialRef.current) return; + materialRef.current.uniforms.uIntensity.value = intensity; + }, [intensity]); + + useEffect(() => { + if (!materialRef.current) return; + materialRef.current.uniforms.uInteractive.value = interactive; + }, [interactive]); + + useEffect(() => { + if (!materialRef.current) return; + materialRef.current.uniforms.uGlowAmount.value = glowAmount; + }, [glowAmount]); + + useEffect(() => { + if (!materialRef.current) return; + materialRef.current.uniforms.uPillarWidth.value = pillarWidth; + }, [pillarWidth]); + + useEffect(() => { + if (!materialRef.current) return; + materialRef.current.uniforms.uPillarHeight.value = pillarHeight; + }, [pillarHeight]); + + useEffect(() => { + if (!materialRef.current) return; + materialRef.current.uniforms.uNoiseIntensity.value = noiseIntensity; + }, [noiseIntensity]); + + useEffect(() => { + if (!materialRef.current) return; + const pillarRotRad = (pillarRotation * Math.PI) / 180; + materialRef.current.uniforms.uPillarRotCos.value = Math.cos(pillarRotRad); + materialRef.current.uniforms.uPillarRotSin.value = Math.sin(pillarRotRad); + }, [pillarRotation]); + + if (!webGLSupported) { + return ( +
+ WebGL not supported +
+ ); + } + + return ( +
+ ); +}; + +export default LightPillar; diff --git a/shared/common/src/components/Pillar/types.ts b/shared/common/src/components/Pillar/types.ts new file mode 100644 index 0000000..ce5012e --- /dev/null +++ b/shared/common/src/components/Pillar/types.ts @@ -0,0 +1,15 @@ +export interface LightPillarProps { + topColor?: string; + bottomColor?: string; + intensity?: number; + rotationSpeed?: number; + interactive?: boolean; + className?: string; + glowAmount?: number; + pillarWidth?: number; + pillarHeight?: number; + noiseIntensity?: number; + mixBlendMode?: React.CSSProperties['mixBlendMode']; + pillarRotation?: number; + quality?: 'low' | 'medium' | 'high'; +} diff --git a/shared/common/src/components/index.ts b/shared/common/src/components/index.ts index d48e6c0..244c124 100644 --- a/shared/common/src/components/index.ts +++ b/shared/common/src/components/index.ts @@ -6,3 +6,8 @@ export * from './MdPreview'; export * from './LinkButton'; export * from './ElectricBorder'; export * from './GlassBox'; +export { default as LightPillar } from './Pillar/Pillar'; +export * from './Pillar/types'; +export { default as GridAnimation } from './GridAnimation/GridAnimation'; +export * from './GridAnimation/types'; + diff --git a/shared/common/src/components/theme/theme.ts b/shared/common/src/components/theme/theme.ts index 4d690b9..1c1f04f 100644 --- a/shared/common/src/components/theme/theme.ts +++ b/shared/common/src/components/theme/theme.ts @@ -228,6 +228,12 @@ const customConfig = defineConfig({ 'border.muted': { value: { _light: '#e3e3e3', _dark: '#282a2c' }, }, + 'pillar.top': { + value: { _light: '#450470ff', _dark: '#7000b9ff' }, + }, + 'pillar.bottom': { + value: { _light: '#036943ff', _dark: '#00d184ff' }, + }, // Terminal log line colors — adapt to light/dark mode // Dark: bright neon for readability on dark bg // Light: deeper saturated shades for readability on white bg diff --git a/shared/common/src/localization/locales/en/common.json b/shared/common/src/localization/locales/en/common.json index a838855..ce7e0d8 100644 --- a/shared/common/src/localization/locales/en/common.json +++ b/shared/common/src/localization/locales/en/common.json @@ -1,3 +1,22 @@ { - "Title": "AudioMesh" + "Title": "AudioMesh", + "DocsPage": { + "switch": "Switch", + "inThisPage": "In this Page", + "loadingDocumentation": "Loading documentation", + "gettingStarted": "Getting Started", + "architecture": "Architecture", + "reference": "Reference", + "selectTopic": "Select Topic" + }, + "NavigationBar": { + "githubRepository": "GitHub Repository", + "menu": "Menu" + }, + "MdPage": { + "loadingDocument": "Loading document" + }, + "LandingPage": { + "host": "HOST" + } } diff --git a/shared/common/src/localization/locales/es/common.json b/shared/common/src/localization/locales/es/common.json index a838855..47907fb 100644 --- a/shared/common/src/localization/locales/es/common.json +++ b/shared/common/src/localization/locales/es/common.json @@ -1,3 +1,22 @@ { - "Title": "AudioMesh" + "Title": "AudioMesh", + "DocsPage": { + "switch": "Cambiar", + "inThisPage": "En esta página", + "loadingDocumentation": "Cargando documentación...", + "gettingStarted": "Primeros pasos", + "architecture": "Arquitectura", + "reference": "Referencia", + "selectTopic": "Seleccionar tema" + }, + "NavigationBar": { + "githubRepository": "Repositorio de GitHub", + "menu": "Menú" + }, + "MdPage": { + "loadingDocument": "Cargando documento..." + }, + "LandingPage": { + "host": "HOST" + } } diff --git a/shared/common/src/localization/locales/hi/common.json b/shared/common/src/localization/locales/hi/common.json index 0f437c7..07a4aba 100644 --- a/shared/common/src/localization/locales/hi/common.json +++ b/shared/common/src/localization/locales/hi/common.json @@ -1,3 +1,22 @@ { - "Title": "ऑडियोमेश" + "Title": "ऑडियोमेश", + "DocsPage": { + "switch": "बदलें", + "inThisPage": "इस पृष्ठ में", + "loadingDocumentation": "दस्तावेज़ीकरण लोड हो रहा है...", + "gettingStarted": "शुरू करना", + "architecture": "आर्किटेक्चर", + "reference": "संदर्भ", + "selectTopic": "विषय चुनें" + }, + "NavigationBar": { + "githubRepository": "गिटहब रिपोजिटरी", + "menu": "मेन्यू" + }, + "MdPage": { + "loadingDocument": "दस्तावेज़ लोड हो रहा है..." + }, + "LandingPage": { + "host": "होस्ट" + } } diff --git a/shared/common/src/localization/locales/ja/common.json b/shared/common/src/localization/locales/ja/common.json index a838855..4a09063 100644 --- a/shared/common/src/localization/locales/ja/common.json +++ b/shared/common/src/localization/locales/ja/common.json @@ -1,3 +1,22 @@ { - "Title": "AudioMesh" + "Title": "AudioMesh", + "DocsPage": { + "switch": "切り替え", + "inThisPage": "目次", + "loadingDocumentation": "ドキュメントを読み込み中...", + "gettingStarted": "はじめに", + "architecture": "アーキテクチャ", + "reference": "リファレンス", + "selectTopic": "トピックを選択" + }, + "NavigationBar": { + "githubRepository": "GitHubリポジトリ", + "menu": "メニュー" + }, + "MdPage": { + "loadingDocument": "ドキュメントを読み込み中..." + }, + "LandingPage": { + "host": "ホスト" + } } diff --git a/shared/common/src/localization/locales/ru/common.json b/shared/common/src/localization/locales/ru/common.json index a838855..8f7c725 100644 --- a/shared/common/src/localization/locales/ru/common.json +++ b/shared/common/src/localization/locales/ru/common.json @@ -1,3 +1,22 @@ { - "Title": "AudioMesh" + "Title": "AudioMesh", + "DocsPage": { + "switch": "Переключить", + "inThisPage": "На этой странице", + "loadingDocumentation": "Загрузка документации...", + "gettingStarted": "Начало работы", + "architecture": "Архитектура", + "reference": "Справочник", + "selectTopic": "Выбрать тему" + }, + "NavigationBar": { + "githubRepository": "Репозиторий GitHub", + "menu": "Меню" + }, + "MdPage": { + "loadingDocument": "Загрузка документа..." + }, + "LandingPage": { + "host": "ХОСТ" + } } diff --git a/shared/common/src/localization/locales/zh/common.json b/shared/common/src/localization/locales/zh/common.json index a838855..657a0d9 100644 --- a/shared/common/src/localization/locales/zh/common.json +++ b/shared/common/src/localization/locales/zh/common.json @@ -1,3 +1,22 @@ { - "Title": "AudioMesh" + "Title": "AudioMesh", + "DocsPage": { + "switch": "切换", + "inThisPage": "本页目录", + "loadingDocumentation": "正在加载文档...", + "gettingStarted": "开始使用", + "architecture": "系统架构", + "reference": "参考指南", + "selectTopic": "选择主题" + }, + "NavigationBar": { + "githubRepository": "GitHub 仓库", + "menu": "菜单" + }, + "MdPage": { + "loadingDocument": "正在加载文档..." + }, + "LandingPage": { + "host": "主控" + } } diff --git a/yarn.lock b/yarn.lock index 6de0920..c54267e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1986,6 +1986,13 @@ __metadata: languageName: node linkType: hard +"@dimforge/rapier3d-compat@npm:~0.12.0": + version: 0.12.0 + resolution: "@dimforge/rapier3d-compat@npm:0.12.0" + checksum: 10c0/c66c24f90649c0fc870679c12e7fec1a111080d44450169b57561f957d7b6b284ad8a3ceeba95533e213176ea171351acebd3dd43885fafb33f18bfbd9d507db + languageName: node + linkType: hard + "@electron/asar@npm:3.4.1, @electron/asar@npm:^3.3.1": version: 3.4.1 resolution: "@electron/asar@npm:3.4.1" @@ -3759,6 +3766,13 @@ __metadata: languageName: node linkType: hard +"@tweenjs/tween.js@npm:~23.1.3": + version: 23.1.3 + resolution: "@tweenjs/tween.js@npm:23.1.3" + checksum: 10c0/811b30f5f0e7409fb41833401c501c2d6f600eb5f43039dd9067a7f70aff6dad5f5ce1233848e13f0b33a269a160d9c133f344d986cbff4f1f6b72ddecd06c89 + languageName: node + linkType: hard + "@types/aria-query@npm:^5.0.1": version: 5.0.4 resolution: "@types/aria-query@npm:5.0.4" @@ -4632,6 +4646,13 @@ __metadata: languageName: node linkType: hard +"@types/stats.js@npm:*": + version: 0.17.4 + resolution: "@types/stats.js@npm:0.17.4" + checksum: 10c0/4fe0429998519f0476f03a25b4900b4d4a1474606478657271e40a884f7936ba902ea564b1c95cfd33a8e84af46cef6e1e98bb23e86fd3b6676cd5b974987151 + languageName: node + linkType: hard + "@types/testing-library__jest-dom@npm:^5.9.1": version: 5.14.9 resolution: "@types/testing-library__jest-dom@npm:5.14.9" @@ -4641,6 +4662,20 @@ __metadata: languageName: node linkType: hard +"@types/three@npm:^0.184.1": + version: 0.184.1 + resolution: "@types/three@npm:0.184.1" + dependencies: + "@dimforge/rapier3d-compat": "npm:~0.12.0" + "@tweenjs/tween.js": "npm:~23.1.3" + "@types/stats.js": "npm:*" + "@types/webxr": "npm:>=0.5.17" + fflate: "npm:~0.8.2" + meshoptimizer: "npm:~1.1.1" + checksum: 10c0/de5d9d1c9c2ff9642ced8d3567293a2a0b1c4550decea9073fd766a469ab72e0502014caf499d1db98ab0036ee28060e47b19acccd0ca96438883bbe1210c37c + languageName: node + linkType: hard + "@types/tough-cookie@npm:*": version: 4.0.5 resolution: "@types/tough-cookie@npm:4.0.5" @@ -4676,6 +4711,13 @@ __metadata: languageName: node linkType: hard +"@types/webxr@npm:>=0.5.17": + version: 0.5.24 + resolution: "@types/webxr@npm:0.5.24" + checksum: 10c0/ff59ffd390d06ca3f89ab2531d583ac10bc5e2ab82e5a01ecc40fbc365224a3375e7caa5b14649cf6141db21fb024940da7ad2bd8faa3cd497a6665257cb53b5 + languageName: node + linkType: hard + "@types/ws@npm:^8.5.5": version: 8.18.1 resolution: "@types/ws@npm:8.18.1" @@ -6761,6 +6803,7 @@ __metadata: "@types/node": "npm:^16.18.91" "@types/react": "npm:^19.2.14" "@types/react-dom": "npm:^19.2.3" + "@types/three": "npm:^0.184.1" "@uiw/react-markdown-preview": "npm:^5.1.3" i18next: "npm:^23.11.2" i18next-browser-languagedetector: "npm:^7.2.1" @@ -6772,6 +6815,7 @@ __metadata: react-helmet-async: "npm:^2.0.5" react-i18next: "npm:^14.1.1" react-router-dom: "npm:^7.15.0" + three: "npm:^0.184.0" typescript: "npm:^6.0.3" languageName: unknown linkType: soft @@ -10849,6 +10893,13 @@ __metadata: languageName: node linkType: hard +"fflate@npm:~0.8.2": + version: 0.8.3 + resolution: "fflate@npm:0.8.3" + checksum: 10c0/eab181ca37f5348ae76d4b6f840e0026e30220e33153289ac942222d8b9638237d486507dbcc09878d724095bd354993a2ee48bbee99c8f2c6440d4448719aa7 + languageName: node + linkType: hard + "figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -15036,6 +15087,13 @@ __metadata: languageName: node linkType: hard +"meshoptimizer@npm:~1.1.1": + version: 1.1.1 + resolution: "meshoptimizer@npm:1.1.1" + checksum: 10c0/aacd816cbe206e49f16c88ed8b13deafec40cdb8b844080b6d614b4d945dfc7242d5dccfbc1269375a5d4dce2f354261c107d3ce6e564961bd9a220e4616dafb + languageName: node + linkType: hard + "methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" @@ -19804,6 +19862,13 @@ __metadata: languageName: node linkType: hard +"three@npm:^0.184.0": + version: 0.184.0 + resolution: "three@npm:0.184.0" + checksum: 10c0/6af4a36dd97e7082c8daca57af162a36df0ca18337a805913bdb6787dd5b2626face8b51d361a0cb2f767e18fdb1ad7e73ab945ae32647741af9278311f5369d + languageName: node + linkType: hard + "throat@npm:^6.0.1": version: 6.0.2 resolution: "throat@npm:6.0.2"