diff --git a/src/components/AnimatedBackground.tsx b/src/components/AnimatedBackground.tsx
index 13a2608..e121d2a 100644
--- a/src/components/AnimatedBackground.tsx
+++ b/src/components/AnimatedBackground.tsx
@@ -1,85 +1,51 @@
'use client'
-import React, { useEffect, useMemo, useRef } from 'react'
+import dynamic from 'next/dynamic'
-const AnimatedBackground = () => {
- const particles = useMemo(() => Array.from({ length: 36 }, (_, i) => ({ left: (i * 47) % 100, top: (i * 71) % 100, delay: (i % 9) * 0.38, depth: 0.55 + (i % 5) * 0.12 })), [])
- const blobRefs = useRef<(HTMLDivElement | null)[]>([])
-
- useEffect(() => {
- let requestId: number
-
- const handleScroll = () => {
- const scroll = window.pageYOffset
-
- blobRefs.current.forEach((blob, index) => {
- if (!blob) return
-
- const xOffset =
- Math.sin(scroll / 120 + index * 0.6) * 100
-
- const yOffset =
- Math.cos(scroll / 120 + index * 0.6) * 35
-
- blob.style.transform = `translate(${xOffset}px, ${yOffset}px)`
- blob.style.transition = 'transform 1.2s ease-out'
- })
-
- requestId = requestAnimationFrame(handleScroll)
- }
-
- window.addEventListener('scroll', handleScroll)
- handleScroll()
-
- return () => {
- window.removeEventListener('scroll', handleScroll)
- cancelAnimationFrame(requestId)
- }
- }, [])
+const BackgroundScene = dynamic(
+ () => import('@/components/three/BackgroundScene'),
+ { ssr: false, loading: () => null },
+)
+export default function AnimatedBackground() {
return (
-
-
- {/* top left */}
-
{
- blobRefs.current[0] = ref
- }}
- className="absolute top-10 left-10 w-40 h-40 md:w-56 md:h-56 rounded-full bg-white blur-[90px] opacity-30"
- />
-
- {/* top right */}
-
{
- blobRefs.current[1] = ref
- }}
- className="absolute top-10 right-10 w-40 h-40 md:w-56 md:h-56 rounded-full bg-zinc-300 blur-[100px] opacity-25"
- />
-
- {/* bottom left */}
-
{
- blobRefs.current[2] = ref
- }}
- className="absolute bottom-10 left-10 w-44 h-44 md:w-60 md:h-60 rounded-full bg-zinc-400 blur-[110px] opacity-30"
- />
+
+
- {/* bottom right */}
-
{
- blobRefs.current[3] = ref
- }}
- className="absolute bottom-10 right-10 w-40 h-40 md:w-56 md:h-56 rounded-full bg-white blur-[100px] opacity-20"
- />
+
+
-
{particles.map((particle, index) => )}
-
-
- {/* GRID */}
-
+
+
+
)
}
-
-export default AnimatedBackground
diff --git a/src/components/three/BackgroundScene.tsx b/src/components/three/BackgroundScene.tsx
new file mode 100644
index 0000000..f82c44a
--- /dev/null
+++ b/src/components/three/BackgroundScene.tsx
@@ -0,0 +1,295 @@
+'use client'
+
+import { useEffect, useMemo, useRef } from 'react'
+import type { MutableRefObject } from 'react'
+import { Canvas, useFrame } from '@react-three/fiber'
+import * as THREE from 'three'
+
+type SceneSignals = {
+ pointer: MutableRefObject
+ scroll: MutableRefObject
+}
+
+type ParticleLayerProps = SceneSignals & {
+ color: string
+ count: number
+ depth: number
+ seed: number
+ size: number
+ speed: number
+}
+
+function seededUnit(index: number, seed: number) {
+ const value = Math.sin((index + 1) * 12.9898 + seed * 78.233) * 43758.5453
+ return value - Math.floor(value)
+}
+
+function createParticlePositions(count: number, depth: number, seed: number) {
+ const positions = new Float32Array(count * 3)
+
+ for (let index = 0; index < count; index += 1) {
+ const offset = index * 3
+ positions[offset] = (seededUnit(index * 3, seed) - 0.5) * 22
+ positions[offset + 1] = (seededUnit(index * 3 + 1, seed) - 0.5) * 14
+ positions[offset + 2] = -seededUnit(index * 3 + 2, seed) * depth
+ }
+
+ return positions
+}
+
+function ParticleLayer({
+ color,
+ count,
+ depth,
+ pointer,
+ scroll,
+ seed,
+ size,
+ speed,
+}: ParticleLayerProps) {
+ const particles = useRef(null)
+ const positions = useMemo(
+ () => createParticlePositions(count, depth, seed),
+ [count, depth, seed],
+ )
+
+ useFrame((_, delta) => {
+ if (!particles.current) return
+
+ particles.current.rotation.y += delta * speed
+ particles.current.rotation.x = THREE.MathUtils.damp(
+ particles.current.rotation.x,
+ pointer.current.y * 0.08,
+ 3,
+ delta,
+ )
+ particles.current.position.x = THREE.MathUtils.damp(
+ particles.current.position.x,
+ pointer.current.x * 0.42,
+ 3,
+ delta,
+ )
+ particles.current.position.y = THREE.MathUtils.damp(
+ particles.current.position.y,
+ pointer.current.y * 0.28 + (scroll.current % 5) * 0.12,
+ 3,
+ delta,
+ )
+ })
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+function OrbitalSculpture({ pointer, scroll }: SceneSignals) {
+ const rig = useRef(null)
+ const core = useRef(null)
+ const halo = useRef(null)
+
+ useFrame(({ clock }, delta) => {
+ const elapsed = clock.getElapsedTime()
+
+ if (rig.current) {
+ rig.current.rotation.x = THREE.MathUtils.damp(
+ rig.current.rotation.x,
+ pointer.current.y * 0.22 - 0.16,
+ 3.2,
+ delta,
+ )
+ rig.current.rotation.y = THREE.MathUtils.damp(
+ rig.current.rotation.y,
+ pointer.current.x * 0.3 + elapsed * 0.025,
+ 3.2,
+ delta,
+ )
+ rig.current.position.y = THREE.MathUtils.damp(
+ rig.current.position.y,
+ Math.sin(elapsed * 0.35) * 0.18 - (scroll.current % 4) * 0.08,
+ 2.5,
+ delta,
+ )
+ }
+
+ if (core.current) {
+ core.current.rotation.x += delta * 0.08
+ core.current.rotation.y += delta * 0.12
+ }
+
+ if (halo.current) {
+ halo.current.rotation.z -= delta * 0.035
+ halo.current.rotation.x = Math.sin(elapsed * 0.18) * 0.16
+ }
+ })
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function FloatingGeometry({ pointer }: Pick) {
+ const object = useRef(null)
+
+ useFrame(({ clock }, delta) => {
+ if (!object.current) return
+
+ const elapsed = clock.getElapsedTime()
+ object.current.rotation.x += delta * 0.045
+ object.current.rotation.y -= delta * 0.07
+ object.current.position.x = THREE.MathUtils.damp(
+ object.current.position.x,
+ -4.7 + pointer.current.x * 0.24,
+ 2.8,
+ delta,
+ )
+ object.current.position.y = -2.5 + Math.sin(elapsed * 0.42) * 0.22
+ })
+
+ return (
+
+
+
+
+ )
+}
+
+function Scene({ pointer, scroll }: SceneSignals) {
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+}
+
+export default function BackgroundScene() {
+ const pointer = useRef(new THREE.Vector2())
+ const scroll = useRef(0)
+
+ useEffect(() => {
+ const handlePointerMove = (event: PointerEvent) => {
+ pointer.current.set(
+ (event.clientX / window.innerWidth) * 2 - 1,
+ -(event.clientY / window.innerHeight) * 2 + 1,
+ )
+ }
+ const handlePointerLeave = () => pointer.current.set(0, 0)
+ const handleScroll = () => {
+ scroll.current = window.scrollY / Math.max(window.innerHeight, 1)
+ }
+
+ window.addEventListener('pointermove', handlePointerMove, { passive: true })
+ window.addEventListener('pointerleave', handlePointerLeave)
+ window.addEventListener('scroll', handleScroll, { passive: true })
+ handleScroll()
+
+ return () => {
+ window.removeEventListener('pointermove', handlePointerMove)
+ window.removeEventListener('pointerleave', handlePointerLeave)
+ window.removeEventListener('scroll', handleScroll)
+ }
+ }, [])
+
+ return (
+
+ )
+}