component

DeskLamp

A banker's desk lamp: a brass base and stem under a cased glass shade with a pull chain, pooling light on the desk. Can light only after dusk.

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

Tune every prop in the workshop →

Add it

npx @runek/cli add desklamp

Pulls @runek/core@^0.15.0.

Use it

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

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

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

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

Props

Prop Type Default Description
width number 0.3 Shade width, in units.
shadeColor color The glass shade. Seeded (mostly library green) when unset.
color color Base, stem, and harp; defaults to the palette’s accent (brass).
lightColor color "#ffe2b0" The light and the glowing bulb.
intensity number 3 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 1 Seed for the deterministic variation: same seed, same result.

Registry manifest

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

Browse the whole catalog in the gallery →.

Source (DeskLamp.tsx)
import { pick, rng, sub, useSun, useWorld, type WorldComponentProps } from '@runek/core'
import { useMemo } from 'react'

export interface DeskLampProps extends WorldComponentProps {
  /** Shade width, in units. */
  width?: number
  /** The glass shade. Seeded (mostly library green) when unset. */
  shadeColor?: string
  /** Base, stem, and harp; 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
}

const SHADES = ['#1f6b47', '#1f6b47', '#1f6b47', '#2b4f8a', '#8a2b2b', '#b8862f']

/**
 * A banker's desk lamp: a brass base and stem holding a cased glass shade over the work, with a
 * pull chain. Its origin is the foot, so it sits on a `Desk` or `Table` top, and its small light
 * pools on the desk below. `nightOnly` lights it with the world's dusk. Decorative: no collider.
 */
export function DeskLamp({
  position = [0, 0, 0],
  rotation = [0, 0, 0],
  seed = 1,
  width = 0.3,
  shadeColor,
  color,
  lightColor = '#ffe2b0',
  intensity = 3,
  on = true,
  nightOnly = false,
}: DeskLampProps) {
  const { unit, palette } = useWorld()
  const sun = useSun()
  const lit = on && (!nightOnly || !sun.day)
  const brass = color ?? palette.accent
  const W = width * unit
  const shadeR = W * 0.3
  const stemH = W * 1.05

  const shade = useMemo(() => shadeColor ?? pick(rng(sub(seed, 0)), SHADES), [seed, shadeColor])

  const metal = <meshStandardMaterial color={brass} metalness={0.75} roughness={0.3} />
  const baseH = 0.025 * unit

  return (
    <group position={position} rotation={rotation}>
      {/* an oval base: a cylinder squashed front to back */}
      <mesh position={[0, baseH / 2, 0]} scale={[1, 1, 0.62]} castShadow receiveShadow>
        <cylinderGeometry args={[W * 0.36, W * 0.4, baseH, 28]} />
        {metal}
      </mesh>
      <mesh position={[0, stemH / 2, -W * 0.06]} castShadow>
        <cylinderGeometry args={[0.008 * unit, 0.01 * unit, stemH, 10]} />
        {metal}
      </mesh>
      {/* the harp: a bar across under the shade, a short arm forward to it */}
      <mesh position={[0, stemH, 0]} rotation={[Math.PI / 2, 0, 0]}>
        <cylinderGeometry args={[0.006 * unit, 0.006 * unit, W * 0.14, 8]} />
        {metal}
      </mesh>
      <group position={[0, stemH + shadeR * 0.2, W * 0.08]} rotation={[-0.18, 0, 0]}>
        {/* cased glass: a half tube lying along X, opening downward, with end caps */}
        <mesh rotation={[0, 0, Math.PI / 2]} castShadow>
          <cylinderGeometry args={[shadeR, shadeR, W, 24, 1, true, 0, Math.PI]} />
          <meshStandardMaterial
            color={shade}
            roughness={0.15}
            metalness={0.1}
            emissive={shade}
            emissiveIntensity={lit ? 0.25 : 0}
            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={[shadeR, 24, 0, Math.PI]} />
            {metal}
          </mesh>
        ))}
        <mesh position={[0, -shadeR * 0.35, 0]} rotation={[0, 0, Math.PI / 2]}>
          <cylinderGeometry args={[0.012 * unit, 0.012 * unit, W * 0.7, 10]} />
          <meshStandardMaterial
            color="#fff6e0"
            emissive={lightColor}
            emissiveIntensity={lit ? 2 : 0}
            toneMapped={!lit}
          />
        </mesh>
        <mesh position={[W * 0.2, -shadeR * 0.6, shadeR * 0.95]}>
          <cylinderGeometry args={[0.002 * unit, 0.002 * unit, shadeR * 1.2, 4]} />
          {metal}
        </mesh>
        <mesh position={[W * 0.2, -shadeR * 1.22, shadeR * 0.95]}>
          <sphereGeometry args={[0.007 * unit, 8, 6]} />
          {metal}
        </mesh>
        {lit && (
          <pointLight
            position={[0, -shadeR * 0.8, 0]}
            color={lightColor}
            intensity={intensity}
            distance={3 * unit}
            decay={2}
          />
        )}
      </group>
    </group>
  )
}