component
Flag
A cloth flag on a pole; the cloth ripples per-frame, pinned at the luff. Thin pole collider.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add flag
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 { Flag } from './runek/Flag'
<Flag position={[0, 0, 0]} />
Or as a node in a world file (worlds as data):
{ "type": "Flag", "props": { "position": [0, 0, 0] } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
poleHeight |
number |
6 |
Pole height, in units. |
fly |
number |
2.4 |
Flag width away from the pole (the fly), in units. |
drop |
number |
1.5 |
Flag height (the drop), in units. |
waveSpeed |
number |
3 |
Ripple speed. |
waveAmplitude |
number |
0.22 |
Ripple depth as a fraction of the fly. |
color |
color |
Cloth color; defaults to the world palette’s fabric. |
|
poleColor |
color |
Pole color; defaults to the palette’s wood. |
|
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 |
Accepted for the component contract; this component has no seeded variation yet. |
Registry manifest
registry manifest /r/components/flag.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add flag fetches.
Browse the whole catalog in the gallery →.
Source (Flag.tsx)
import { useFrame } from '@react-three/fiber'
import { CylinderCollider, RigidBody } from '@react-three/rapier'
import { useWorld, type WorldComponentProps } from '@runek/core'
import { useMemo } from 'react'
import { DoubleSide, PlaneGeometry } from 'three'
export interface FlagProps extends WorldComponentProps {
/** Pole height, in units. */
poleHeight?: number
/** Flag width away from the pole (the fly), in units. */
fly?: number
/** Flag height (the drop), in units. */
drop?: number
/** Ripple speed. */
waveSpeed?: number
/** Ripple depth as a fraction of the fly. */
waveAmplitude?: number
/** Cloth color; defaults to the world palette's `fabric`. */
color?: string
/** Pole color; defaults to the palette's `wood`. */
poleColor?: string
}
/** A cloth flag on a pole. The cloth is a segmented plane rippled per-frame and pinned at the
* luff (pole side), so the wave grows toward the fly. The pole carries a thin collider; the
* cloth and finial are decorative. */
export function Flag({
position = [0, 0, 0],
rotation = [0, 0, 0],
poleHeight = 6,
fly = 2.4,
drop = 1.5,
waveSpeed = 3,
waveAmplitude = 0.22,
color,
poleColor,
}: FlagProps) {
const { unit, palette } = useWorld()
const cloth = color ?? palette.fabric
const pole = poleColor ?? palette.wood
const U = unit
const poleH = poleHeight * U
const flyW = fly * U
const dropH = drop * U
const poleR = 0.07 * U
const geo = useMemo(() => new PlaneGeometry(flyW, dropH, 18, 10), [flyW, dropH])
useFrame((state) => {
const t = state.clock.elapsedTime * waveSpeed
const p = geo.attributes.position
for (let i = 0; i < p.count; i++) {
const u = (p.getX(i) + flyW / 2) / flyW // 0 at luff, 1 at fly
const v = (p.getY(i) + dropH / 2) / dropH
const ripple = Math.sin(u * 11 - t) * 0.7 + Math.sin(u * 6 + v * 4 - t * 0.8) * 0.3
p.setZ(i, ripple * waveAmplitude * flyW * u)
}
p.needsUpdate = true
geo.computeVertexNormals()
})
return (
<RigidBody type="fixed" colliders={false} position={position} rotation={rotation}>
<CylinderCollider args={[poleH / 2, poleR * 1.5]} position={[0, poleH / 2, 0]} />
{/* pole */}
<mesh position={[0, poleH / 2, 0]} castShadow>
<cylinderGeometry args={[poleR, poleR, poleH, 8]} />
<meshStandardMaterial color={pole} roughness={0.7} />
</mesh>
{/* finial */}
<mesh position={[0, poleH + 0.06 * U, 0]} castShadow>
<sphereGeometry args={[0.12 * U, 10, 8]} />
<meshStandardMaterial color={pole} metalness={0.2} roughness={0.5} />
</mesh>
{/* cloth */}
<mesh
geometry={geo}
position={[poleR + flyW / 2, poleH - dropH / 2 - 0.25 * U, 0]}
castShadow
>
<meshStandardMaterial color={cloth} side={DoubleSide} roughness={0.9} />
</mesh>
</RigidBody>
)
}