component
TikiTorch
A bamboo tiki torch with a woven head and a flickering flame and light, for a beach path or a garden. Can burn only after dusk.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add tikitorch
Pulls @react-three/fiber@^9.6.1, @react-three/rapier@^2.2.0, @runek/core@^0.15.0, three@^0.184.0.
Use it
import { TikiTorch } from './runek/TikiTorch'
<TikiTorch position={[0, 0, 0]} seed={1} />
Or as a node in a world file (worlds as data):
{ "type": "TikiTorch", "props": { "position": [0, 0, 0], "seed": 1 } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
height |
number |
1.7 |
Height to the top of the head, in units. |
color |
color |
Bamboo pole and joints. | |
headColor |
color |
Woven head and bindings. Defaults to the palette’s bark. |
|
flameColor |
color |
"#ff7a1a" |
The outer flame. |
intensity |
number |
6 |
Light strength; it flickers around this. |
on |
boolean |
true |
Burning, or cold. |
nightOnly |
boolean |
false |
Burn only while the world’s sun is down. |
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. |
seed |
number |
1 |
Seed for the deterministic variation: same seed, same result. |
Registry manifest
registry manifest /r/components/tikitorch.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add tikitorch fetches.
Browse the whole catalog in the gallery →.
Source (TikiTorch.tsx)
import { useFrame } from '@react-three/fiber'
import { CuboidCollider, RigidBody } from '@react-three/rapier'
import { range, rng, sub, useSun, useWorld, type WorldComponentProps } from '@runek/core'
import { useMemo, useRef } from 'react'
import type { Group, PointLight } from 'three'
export interface TikiTorchProps extends WorldComponentProps {
/** Height to the top of the head, in units. */
height?: number
/** Bamboo pole and joints. */
color?: string
/** Woven head and bindings. Defaults to the palette's `bark`. */
headColor?: string
/** The outer flame. */
flameColor?: string
/** Light strength; it flickers around this. */
intensity?: number
/** Burning, or cold. */
on?: boolean
/** Burn only while the world's sun is down. */
nightOnly?: boolean
}
/**
* A bamboo tiki torch: a jointed pole with a seeded lean, a woven head bound in cord, and an
* animated flame with a warm flickering light, for a beachfront path or a garden at dusk.
* `nightOnly` lights it only after sunset. One thin cuboid collider.
*/
export function TikiTorch({
position = [0, 0, 0],
rotation = [0, 0, 0],
seed = 1,
height = 1.7,
color,
headColor,
flameColor = '#ff7a1a',
intensity = 6,
on = true,
nightOnly = false,
}: TikiTorchProps) {
const { unit, palette } = useWorld()
const sun = useSun()
const lit = on && (!nightOnly || !sun.day)
const pole = color ?? '#b89a5e'
const head = headColor ?? palette.bark
const H = height * unit
const flameRef = useRef<Group>(null)
const lightRef = useRef<PointLight>(null)
const look = useMemo(() => {
const r = rng(sub(seed, 0))
const headH = range(r, 0.22, 0.3) * unit
const poleH = H - headH
const joints = Math.max(2, Math.round(poleH / (range(r, 0.35, 0.5) * unit)))
return {
lean: [range(r, -0.04, 0.04), 0, range(r, -0.04, 0.04)] as [number, number, number],
headH,
headR: range(r, 0.07, 0.09) * unit,
flare: range(r, 1.1, 1.35),
poleH,
joints: Array.from({ length: joints - 1 }, (_, i) => ((i + 1) / joints) * poleH),
bands: 2 + Math.floor(r() * 2),
phase: r() * 10,
}
}, [seed, H, unit])
useFrame(({ clock }) => {
if (!lit) return
const t = clock.elapsedTime + look.phase
const n = Math.sin(t * 11) * 0.5 + Math.sin(t * 19 + 1) * 0.3 + Math.sin(t * 7 + 2) * 0.2
if (flameRef.current) {
flameRef.current.scale.set(1 - n * 0.08, 1 + n * 0.22, 1 - n * 0.08)
flameRef.current.rotation.z = n * 0.08
}
if (lightRef.current) lightRef.current.intensity = intensity * (1 + n * 0.25)
})
const poleR = 0.022 * unit
const { headH, headR, poleH } = look
return (
<RigidBody type="fixed" colliders={false} position={position} rotation={rotation}>
<CuboidCollider args={[poleR * 2, H / 2, poleR * 2]} position={[0, H / 2, 0]} />
<group rotation={look.lean}>
<mesh position={[0, poleH / 2, 0]} castShadow>
<cylinderGeometry args={[poleR, poleR * 1.15, poleH, 10]} />
<meshStandardMaterial color={pole} roughness={0.7} />
</mesh>
{look.joints.map((y) => (
<mesh key={y} position={[0, y, 0]}>
<torusGeometry args={[poleR * 1.05, poleR * 0.25, 6, 12]} />
<meshStandardMaterial color={pole} roughness={0.8} />
</mesh>
))}
{/* the woven head flares from the pole to its rim, bound with cord */}
<mesh position={[0, poleH + headH / 2, 0]} castShadow>
<cylinderGeometry args={[headR * look.flare, headR * 0.55, headH, 10, 1, true]} />
<meshStandardMaterial color={head} roughness={1} side={2} flatShading />
</mesh>
{Array.from({ length: look.bands }, (_, i) => {
const k = (i + 0.5) / look.bands
return (
<mesh key={k} position={[0, poleH + headH * k, 0]} rotation={[Math.PI / 2, 0, 0]}>
<torusGeometry
args={[headR * (0.55 + (look.flare - 0.55) * k) * 1.02, 0.006 * unit, 5, 14]}
/>
<meshStandardMaterial color={pole} roughness={0.9} />
</mesh>
)
})}
<mesh position={[0, poleH + headH * 0.92, 0]}>
<cylinderGeometry
args={[headR * look.flare * 0.92, headR * look.flare * 0.92, 0.01 * unit, 10]}
/>
<meshStandardMaterial color="#1a1410" roughness={1} />
</mesh>
{lit && (
<>
<group ref={flameRef} position={[0, poleH + headH, 0]}>
<mesh position={[0, headR * 1.3, 0]}>
<coneGeometry args={[headR * 0.9, headR * 2.8, 9]} />
<meshBasicMaterial
color={flameColor}
transparent
opacity={0.85}
toneMapped={false}
/>
</mesh>
<mesh position={[0, headR * 0.9, 0]}>
<coneGeometry args={[headR * 0.5, headR * 1.8, 9]} />
<meshBasicMaterial color="#ffd84d" transparent opacity={0.9} toneMapped={false} />
</mesh>
</group>
<pointLight
ref={lightRef}
position={[0, poleH + headH + headR * 2, 0]}
color="#ff9a3c"
intensity={intensity}
distance={8 * unit}
decay={2}
/>
</>
)}
</group>
</RigidBody>
)
}
TikiTorch.groundSitting = true