component

Birds

A loose flock circling overhead, each bird flapping on its own seeded orbit.

⌖ Explore the library in 3D → · View as Markdown

Tune every prop in the workshop →

Add it

npx @runek/cli add birds

Pulls @react-three/fiber@^9.6.1, @runek/core@^0.15.0, three@^0.184.0.

Use it

import { Birds } from './runek/Birds'

<Birds position={[0, 0, 0]} seed={1} />

Or as a node in a world file (worlds as data):

{ "type": "Birds", "props": { "position": [0, 0, 0], "seed": 1 } }

Props

Prop Type Default Description
position [x, y, z] [0, 0, 0] Position [x, y, z] in units (1 unit = 1 m, Y-up).
rotation [x, y, z] [0, 0, 0] Euler rotation [x, y, z] in radians.
count number 10
area number 20 Orbit spread, in units.
height number 12 Height, in units.
speed number 1
color color "#2a2a30"
seed number 1 Seed for the deterministic variation: same seed, same result.

Registry manifest

registry manifest /r/components/birds.json Self-contained JSON: inlined source plus resolved dependencies, exactly what runek add birds fetches.

Browse the whole catalog in the gallery →.

Source (Birds.tsx)
import { useFrame } from '@react-three/fiber'
import { rng, useWorld, type Vec3 } from '@runek/core'
import { useMemo, useRef } from 'react'
import type { Group } from 'three'

export interface BirdsProps {
  position?: Vec3
  rotation?: Vec3
  count?: number
  /** Orbit spread, in units. */
  area?: number
  height?: number
  speed?: number
  color?: string
  seed?: number
}

interface BirdSpec {
  rx: number
  rz: number
  cx: number
  cz: number
  phase: number
  y: number
  flap: number
  dir: number
}

/** A loose flock circling overhead, each bird flapping on its own seeded orbit.
 *  Motion and life for the sky. Decorative (no collider). */
export function Birds({
  position = [0, 0, 0],
  rotation = [0, 0, 0],
  count = 10,
  area = 20,
  height = 12,
  speed = 1,
  color = '#2a2a30',
  seed = 1,
}: BirdsProps) {
  const { unit } = useWorld()
  const A = area * unit
  const hy = height * unit
  const bodyRefs = useRef<(Group | null)[]>([])
  const leftRefs = useRef<(Group | null)[]>([])
  const rightRefs = useRef<(Group | null)[]>([])

  const birds = useMemo<BirdSpec[]>(() => {
    const next = rng(seed)
    return Array.from({ length: count }, () => ({
      rx: A * (0.3 + next() * 0.5),
      rz: A * (0.3 + next() * 0.5),
      cx: (next() - 0.5) * A * 0.4,
      cz: (next() - 0.5) * A * 0.4,
      phase: next() * Math.PI * 2,
      y: hy * (0.8 + next() * 0.4),
      flap: 7 + next() * 4,
      dir: next() < 0.5 ? 1 : -1,
    }))
  }, [A, hy, count, seed])

  useFrame(({ clock }) => {
    const t = clock.elapsedTime * speed
    birds.forEach((b, i) => {
      const g = bodyRefs.current[i]
      if (!g) return
      const ang = b.phase + b.dir * t * 0.3
      g.position.set(
        b.cx + Math.cos(ang) * b.rx,
        b.y + Math.sin(t * 0.8 + b.phase) * 0.4 * unit,
        b.cz + Math.sin(ang) * b.rz,
      )
      const vx = -Math.sin(ang) * b.rx * b.dir
      const vz = Math.cos(ang) * b.rz * b.dir
      g.rotation.y = Math.atan2(vx, vz)
      const flap = Math.sin(t * b.flap) * 0.6
      const l = leftRefs.current[i]
      const r = rightRefs.current[i]
      if (l) l.rotation.z = 0.2 + flap
      if (r) r.rotation.z = -0.2 - flap
    })
  })

  const wingL = 0.35 * unit
  return (
    <group position={position} rotation={rotation}>
      {birds.map((b, i) => (
        <group
          key={`bird-${b.phase.toFixed(4)}`}
          ref={(el) => {
            bodyRefs.current[i] = el
          }}
        >
          <mesh castShadow>
            <sphereGeometry args={[0.06 * unit, 6, 6]} />
            <meshStandardMaterial color={color} />
          </mesh>
          <group
            ref={(el) => {
              leftRefs.current[i] = el
            }}
          >
            <mesh position={[wingL / 2, 0, 0]} castShadow>
              <boxGeometry args={[wingL, 0.01 * unit, 0.12 * unit]} />
              <meshStandardMaterial color={color} />
            </mesh>
          </group>
          <group
            ref={(el) => {
              rightRefs.current[i] = el
            }}
          >
            <mesh position={[-wingL / 2, 0, 0]} castShadow>
              <boxGeometry args={[wingL, 0.01 * unit, 0.12 * unit]} />
              <meshStandardMaterial color={color} />
            </mesh>
          </group>
        </group>
      ))}
    </group>
  )
}