component
PictureLight
A brass picture light on one or two arms that washes the face below it: over a picture on a wall, or on the top of a bookshelf. Can light only after dusk.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add picturelight
Pulls @runek/core@^0.15.0, three@^0.184.0.
Use it
import { PictureLight } from './runek/PictureLight'
<PictureLight position={[0, 0, 0]} />
Or as a node in a world file (worlds as data):
{ "type": "PictureLight", "props": { "position": [0, 0, 0] } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
width |
number |
0.5 |
Length of the hood, in units. |
reach |
number |
0.16 |
How far the hood stands out from the mount, in units. |
fall |
number |
1.2 |
How far below the mount the light falls, in units: the picture’s or the shelf’s height. |
color |
color |
Hood, arms, and back plate; defaults to the palette’s accent (brass). |
|
lightColor |
color |
"#ffe6c0" |
The light and the glowing bulb. |
intensity |
number |
6 |
Light strength. |
on |
boolean |
true |
Switched on. |
nightOnly |
boolean |
false |
Light 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 |
Accepted for the component contract; this component has no seeded variation yet. |
Registry manifest
registry manifest /r/components/picturelight.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add picturelight fetches.
Browse the whole catalog in the gallery →.
Source (PictureLight.tsx)
import { useSun, useWorld, type WorldComponentProps } from '@runek/core'
import { useMemo } from 'react'
import { Object3D } from 'three'
export interface PictureLightProps extends WorldComponentProps {
/** Length of the hood, in units. */
width?: number
/** How far the hood stands out from the mount, in units. */
reach?: number
/** How far below the mount the light falls, in units: the picture's or the shelf's height. */
fall?: number
/** Hood, arms, and back plate; defaults to the palette's `accent` (brass). */
color?: string
/** The light and the glowing bulb. */
lightColor?: string
/** Light strength. */
intensity?: number
/** Switched on. */
on?: boolean
/** Light only while the world's sun is down. */
nightOnly?: boolean
}
/**
* A picture light: a brass hood on one or two arms (two past 0.6 m), washing the face below it.
* Its origin is the mount point on a vertical face, which it stands out from along local +Z: fix
* it over a picture on a `Wall`, or on the top front edge of a `Bookshelf` to light the books.
* Accepts `seed` for the contract; it has no seeded variation. Decorative: no collider.
*/
export function PictureLight({
position = [0, 0, 0],
rotation = [0, 0, 0],
width = 0.5,
reach = 0.16,
fall = 1.2,
color,
lightColor = '#ffe6c0',
intensity = 6,
on = true,
nightOnly = false,
}: PictureLightProps) {
const { unit, palette } = useWorld()
const sun = useSun()
const lit = on && (!nightOnly || !sun.day)
const brass = color ?? palette.accent
const W = width * unit
const Z = reach * unit
const rise = 0.06 * unit
const hoodR = 0.035 * unit
const arms = width > 0.6 ? [-W * 0.3, W * 0.3] : [0]
const armLen = Math.hypot(Z, rise)
const armTilt = Math.atan2(Z, rise)
const target = useMemo(() => new Object3D(), [])
const metal = <meshStandardMaterial color={brass} metalness={0.75} roughness={0.3} />
return (
<group position={position} rotation={rotation}>
{arms.map((x) => (
<group key={x} position={[x, 0, 0]}>
<mesh position={[0, 0, 0.004 * unit]}>
<boxGeometry args={[0.05 * unit, 0.07 * unit, 0.008 * unit]} />
{metal}
</mesh>
<mesh position={[0, rise / 2, Z / 2]} rotation={[armTilt, 0, 0]}>
<cylinderGeometry args={[0.005 * unit, 0.005 * unit, armLen, 8]} />
{metal}
</mesh>
</group>
))}
{/* the hood: a half tube along X, opening down and back toward the face it lights */}
<group position={[0, rise, Z]} rotation={[-0.5, 0, 0]}>
<mesh rotation={[0, 0, Math.PI / 2]} castShadow>
<cylinderGeometry args={[hoodR, hoodR, W, 18, 1, true, 0, Math.PI]} />
<meshStandardMaterial color={brass} metalness={0.75} roughness={0.3} side={2} />
</mesh>
{[-1, 1].map((s) => (
<mesh key={s} position={[(s * W) / 2, 0, 0]} rotation={[0, (s * Math.PI) / 2, 0]}>
<circleGeometry args={[hoodR, 18, 0, Math.PI]} />
{metal}
</mesh>
))}
<mesh position={[0, -hoodR * 0.3, 0]} rotation={[0, 0, Math.PI / 2]}>
<cylinderGeometry args={[0.008 * unit, 0.008 * unit, W * 0.9, 8]} />
<meshStandardMaterial
color="#fff6e0"
emissive={lightColor}
emissiveIntensity={lit ? 2 : 0}
toneMapped={!lit}
/>
</mesh>
</group>
<primitive object={target} position={[0, -fall * unit * 0.6, 0]} />
{lit && (
<spotLight
position={[0, rise - hoodR, Z]}
target={target}
color={lightColor}
intensity={intensity}
distance={(fall + 1) * unit}
angle={0.9}
penumbra={0.8}
decay={2}
/>
)}
</group>
)
}