component

Plant

Potted plant: a tapered planter and a seeded cluster of foliage.

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

Tune every prop in the workshop →

Add it

npx @runek/cli add plant

Pulls @react-three/rapier@^2.2.0, @runek/core@^0.15.0, three@^0.184.0.

Use it

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

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

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

{ "type": "Plant", "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.
height number 0.7 Height, in units.
potColor color palette wood Planter color; defaults to the world palette’s wood slot.
foliageColor color palette foliage Foliage color; defaults to the world palette’s foliage slot.
seed number 1 Seed for the deterministic variation: same seed, same result.

Registry manifest

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

Browse the whole catalog in the gallery →.

Source (Plant.tsx)
import { CuboidCollider, RigidBody } from '@react-three/rapier'
import { rng, useWorld, type Vec3 } from '@runek/core'
import { useMemo } from 'react'
import * as THREE from 'three'

export interface PlantProps {
  position?: Vec3
  rotation?: Vec3
  height?: number
  /** Planter color; defaults to the world palette's `wood` slot. */
  potColor?: string
  /** Foliage color; defaults to the world palette's `foliage` slot. */
  foliageColor?: string
  seed?: number
}

interface Leaf {
  pos: Vec3
  r: number
  color: string
}

/** A potted plant: a tapered planter, soil, and a seeded cluster of foliage. Indoor
 *  life. The planter owns a small collider. */
export function Plant({
  position = [0, 0, 0],
  rotation = [0, 0, 0],
  height = 0.7,
  potColor,
  foliageColor,
  seed = 1,
}: PlantProps) {
  const { unit, palette } = useWorld()
  const pot = potColor ?? palette.wood
  const foliage = foliageColor ?? palette.foliage
  const H = height * unit
  const potH = H * 0.4
  const potR = 0.18 * unit

  const leaves = useMemo<Leaf[]>(() => {
    const next = rng(seed)
    const base = new THREE.Color(foliage)
    const tint = new THREE.Color()
    return Array.from({ length: 6 }, () => {
      const a = next() * Math.PI * 2
      const rad = next() * potR * 0.8
      return {
        pos: [Math.cos(a) * rad, potH + 0.1 * unit + next() * H * 0.4, Math.sin(a) * rad] as Vec3,
        r: (0.1 + next() * 0.1) * unit,
        color: tint
          .copy(base)
          .multiplyScalar(0.8 + next() * 0.4)
          .getStyle(),
      }
    })
  }, [potR, potH, H, foliage, seed, unit])

  return (
    <RigidBody type="fixed" colliders={false} position={position} rotation={rotation}>
      <CuboidCollider args={[potR, potH / 2, potR]} position={[0, potH / 2, 0]} />
      <mesh castShadow receiveShadow position={[0, potH / 2, 0]}>
        <cylinderGeometry args={[potR, potR * 0.78, potH, 16]} />
        <meshStandardMaterial color={pot} roughness={0.9} />
      </mesh>
      <mesh position={[0, potH - 0.01 * unit, 0]} rotation={[-Math.PI / 2, 0, 0]}>
        <circleGeometry args={[potR * 0.92, 16]} />
        <meshStandardMaterial color="#3a2a1a" roughness={1} />
      </mesh>
      {leaves.map((l) => (
        <mesh
          key={`leaf-${l.pos[0].toFixed(3)}:${l.pos[1].toFixed(3)}`}
          castShadow
          position={l.pos}
        >
          <icosahedronGeometry args={[l.r, 0]} />
          <meshStandardMaterial color={l.color} flatShading roughness={0.9} />
        </mesh>
      ))}
    </RigidBody>
  )
}

Plant.groundSitting = true