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:
170
src/vfx/RavenFlock.tsx
Normal file
170
src/vfx/RavenFlock.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user