Add raven animation feature with SVG assets and Three.js integration

- Introduced a new SVG asset for the raven silhouette.
- Updated CSS to position and style the raven animation layer.
- Implemented `RavenBurstCanvas` and `RavenFlock` components for rendering animated ravens using Three.js.
- Enhanced `GateTransitProvider` to manage raven animation state and integrate with existing transition logic.
- Added `ravenFx` type and default values for controlling raven animation properties.
- Adjusted transform origins and z-index for improved visual alignment during transitions.
This commit is contained in:
Redsandy
2026-08-12 12:41:12 +03:00
parent 59d4a691d6
commit dfe445349b
7 changed files with 380 additions and 30 deletions

170
src/vfx/RavenFlock.tsx Normal file
View File

@@ -0,0 +1,170 @@
import { useLayoutEffect, useMemo, useRef } from 'react'
import { useFrame, useLoader, useThree } from '@react-three/fiber'
import * as THREE from 'three'
import type { MutableRefObject } from 'react'
import type { RavenFx } from './ravenFx'
const COUNT = 18
const APEX_Z = -2.8
const CONE_SLOPE = 0.42
const AXIS_SPEED = 5.2
const CAM_Z = 5.2
type Bird = {
active: boolean
born: boolean
depth: number
theta: number
phase: number
flapSpeed: number
size: number
flip: number
delay: number
speed: number
slope: number
}
function createBirds(): Bird[] {
const birds: Bird[] = []
for (let i = 0; i < COUNT; i++) {
const wave = Math.floor(i / 3)
const theta = (i / COUNT) * Math.PI * 2 + (Math.random() - 0.5) * 0.25
birds.push({
active: false,
born: false,
depth: 0,
theta,
phase: Math.random() * Math.PI * 2,
flapSpeed: 10 + Math.random() * 6,
size: 0.2 + Math.random() * 0.28,
flip: i % 2 === 0 ? -1 : 1,
delay: 0.03 * wave + Math.random() * 0.04,
speed: AXIS_SPEED * (0.85 + Math.random() * 0.35),
slope: CONE_SLOPE * (0.9 + Math.random() * 0.25),
})
}
return birds
}
/** Viewport CSS px → world on the apex plane. Uses the raven canvas rect for correct NDC. */
function screenToApexWorld(
sx: number,
sy: number,
camera: THREE.PerspectiveCamera,
glDom: HTMLCanvasElement | null,
out: THREE.Vector3,
) {
const canvasRect = glDom?.getBoundingClientRect()
const left = canvasRect?.left ?? 0
const top = canvasRect?.top ?? 0
const viewW = canvasRect?.width || window.innerWidth
const viewH = canvasRect?.height || window.innerHeight
const ndcX = ((sx - left) / viewW) * 2 - 1
const ndcY = -((sy - top) / viewH) * 2 + 1
const dist = Math.abs(CAM_Z - APEX_Z)
const vFov = THREE.MathUtils.degToRad(camera.fov)
const halfH = Math.tan(vFov / 2) * dist
const halfW = halfH * (viewW / viewH)
out.set(ndcX * halfW, ndcY * halfH, APEX_Z)
}
type RavenFlockProps = {
fx: MutableRefObject<RavenFx>
}
export function RavenFlock({ fx }: RavenFlockProps) {
const meshRef = useRef<THREE.InstancedMesh>(null)
const birds = useMemo(() => createBirds(), [])
const dummy = useMemo(() => new THREE.Object3D(), [])
const apex = useMemo(() => new THREE.Vector3(0, 0, APEX_Z), [])
const texture = useLoader(THREE.TextureLoader, '/raven-silhouette.svg')
const gl = useThree((s) => s.gl)
useLayoutEffect(() => {
texture.colorSpace = THREE.SRGBColorSpace
texture.anisotropy = 4
texture.needsUpdate = true
}, [texture])
useFrame((state, delta) => {
const mesh = meshRef.current
if (!mesh) return
const cam = state.camera as THREE.PerspectiveCamera
// Fixed camera — do NOT recenter on apex (that pinned birds to screen center)
cam.position.set(0, 0, CAM_Z)
cam.lookAt(0, 0, 0)
cam.updateMatrixWorld()
const { burst, opacity, apexSx, apexSy } = fx.current
if (Number.isFinite(apexSx) && Number.isFinite(apexSy)) {
screenToApexWorld(apexSx, apexSy, cam, gl.domElement, apex)
}
const mat = mesh.material as THREE.MeshBasicMaterial
mat.opacity = opacity
const dt = Math.min(delta, 0.05)
const t = state.clock.elapsedTime
for (let i = 0; i < COUNT; i++) {
const bird = birds[i]
if (!bird.born && burst > bird.delay) {
bird.born = true
bird.active = true
bird.depth = 0.02 + Math.random() * 0.06
}
if (!bird.active) {
dummy.position.set(0, -40, 0)
dummy.scale.set(0, 0, 0)
dummy.updateMatrix()
mesh.setMatrixAt(i, dummy.matrix)
continue
}
bird.depth += bird.speed * dt
// Cone tip at portal; base // screen (XY)
const radius = bird.depth * bird.slope
const x = apex.x + Math.cos(bird.theta) * radius
const y = apex.y + Math.sin(bird.theta) * radius
const z = apex.z + bird.depth
const flap = 1 + Math.sin(t * bird.flapSpeed + bird.phase) * 0.18
const grow = THREE.MathUtils.smoothstep(bird.depth, 0.15, 3.2)
const s = bird.size * flap * (0.15 + grow * 1.55)
dummy.position.set(x, y, z)
dummy.quaternion.copy(cam.quaternion)
dummy.rotateY(bird.flip < 0 ? Math.PI : 0)
dummy.rotateZ(Math.sin(t * bird.flapSpeed + bird.phase) * 0.16)
dummy.scale.set(s * 1.45, s, 1)
dummy.updateMatrix()
mesh.setMatrixAt(i, dummy.matrix)
if (bird.depth > 7.5) {
bird.active = false
}
}
mesh.instanceMatrix.needsUpdate = true
})
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]} frustumCulled={false}>
<planeGeometry args={[1, 0.55]} />
<meshBasicMaterial
map={texture}
color="#0a0b08"
transparent
depthWrite={false}
side={THREE.DoubleSide}
toneMapped={false}
/>
</instancedMesh>
)
}