{
  "name": "tv",
  "title": "Tv",
  "type": "registry:component",
  "description": "A flat-screen TV on a stand, a low media console, or a wall. Switched on, it shows a seeded picture (sky, sun, hills) drawn from flat shapes.",
  "dependencies": [
    "@react-three/rapier@^2.2.0",
    "@runek/core@^0.13.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "Tv.tsx",
      "content": "import { CuboidCollider, RigidBody } from '@react-three/rapier'\nimport { pick, range, rng, sub, useWorld, type Vec3, type WorldComponentProps } from '@runek/core'\nimport { useMemo } from 'react'\nimport { Color } from 'three'\n\n/** On a center `stand`, on a low media `console`, or flat on a `wall`. */\nexport type TvMount = 'stand' | 'console' | 'wall'\n\nexport interface TvProps extends WorldComponentProps {\n  /** Screen width, in units. */\n  width?: number\n  /** `stand` and `console` are seeded when unset; `wall` must be asked for. On a wall the origin\n   *  is the middle of the screen with its back against the wall (local -Z). */\n  mount?: TvMount\n  /** Showing a seeded picture (a sky, a sun, hills) in this tint, or dark glass when off. */\n  on?: boolean\n  /** Picture tint. Seeded when unset. */\n  screen?: string\n  /** Bezel and stand; defaults to the palette's `metal`. */\n  color?: string\n  /** The console's wood; defaults to the palette's `woodDark`. */\n  consoleColor?: string\n}\n\ninterface Blob {\n  pos: Vec3\n  r: number\n  color: string\n}\n\nconst TINTS = ['#6fa3d8', '#e3a46a', '#8fc6a8', '#b49be0', '#e07f7f']\nconst OFF = '#101214'\n\n/**\n * A flat-screen television on a stand, on a low media console, or on a wall. Switched on, its\n * screen glows with a seeded picture drawn from a few flat shapes (no textures), so a lounge has\n * something to look at. One cuboid collider; standing mounts sit on the ground.\n */\nexport function Tv({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  seed = 1,\n  width = 1.2,\n  mount,\n  on = true,\n  screen,\n  color,\n  consoleColor,\n}: TvProps) {\n  const { unit, palette } = useWorld()\n  const frame = color ?? palette.metal\n  const W = width * unit\n  const Hs = (W * 9) / 16\n  const bezel = 0.015 * unit\n  const thick = 0.04 * unit\n\n  const look = useMemo(() => {\n    const r = rng(sub(seed, 0))\n    const rolled = { mount: pick<TvMount>(r, ['stand', 'console']), tint: pick(r, TINTS) }\n    const base = new Color(screen ?? rolled.tint)\n    const shade = (k: number, hue = 0) =>\n      base.clone().offsetHSL(hue, 0, 0).multiplyScalar(k).getStyle()\n    const horizon = range(r, -0.15, 0.1) * Hs\n    const blobs: Blob[] = [\n      // the sun, then hills (circles mostly below the horizon) in two depths\n      {\n        pos: [range(r, -0.3, 0.3) * W, horizon + range(r, 0.12, 0.28) * Hs, 0.0006],\n        r: 0.07 * W,\n        color: '#fff3c4',\n      },\n    ]\n    for (let i = 0; i < 4; i++) {\n      const rad = range(r, 0.12, 0.22) * W\n      blobs.push({\n        pos: [range(r, -0.5, 0.5) * W, horizon - rad * range(r, 0.8, 0.95), 0.001 + i * 0.0002],\n        r: rad,\n        color: shade(i < 2 ? 0.55 : 0.4, 0.25),\n      })\n    }\n    return {\n      mount: mount ?? rolled.mount,\n      sky: shade(1.15),\n      ground: shade(0.35, 0.25),\n      horizon,\n      blobs,\n    }\n  }, [seed, mount, screen, W, Hs])\n\n  const wall = look.mount === 'wall'\n  const consoleH = 0.45 * unit\n  const standLift = 0.08 * unit\n  // Screen center: on a wall it is the origin; standing, it rises above its base.\n  const cy = wall\n    ? 0\n    : (look.mount === 'console' ? consoleH + standLift : 0.75 * unit) + Hs / 2 + bezel\n  const cz = wall ? thick / 2 + 0.01 * unit : 0\n  const top = cy + Hs / 2 + bezel\n\n  return (\n    <RigidBody type=\"fixed\" colliders={false} position={position} rotation={rotation}>\n      {wall ? (\n        <CuboidCollider args={[W / 2, Hs / 2, thick / 2]} position={[0, 0, cz]} />\n      ) : look.mount === 'console' ? (\n        <CuboidCollider\n          args={[W * 0.62, consoleH / 2, 0.22 * unit]}\n          position={[0, consoleH / 2, 0]}\n        />\n      ) : (\n        <CuboidCollider args={[0.25 * unit, top / 2, 0.15 * unit]} position={[0, top / 2, 0]} />\n      )}\n\n      <mesh position={[0, cy, cz]} castShadow>\n        <boxGeometry args={[W + bezel * 2, Hs + bezel * 2, thick]} />\n        <meshStandardMaterial color={frame} roughness={0.4} metalness={0.3} />\n      </mesh>\n      <group position={[0, cy, cz + thick / 2 + 0.0005 * unit]}>\n        {on ? (\n          <>\n            <mesh position={[0, (Hs / 2 + look.horizon) / 2, 0]}>\n              <planeGeometry args={[W, Hs / 2 - look.horizon]} />\n              <meshBasicMaterial color={look.sky} toneMapped={false} />\n            </mesh>\n            <mesh position={[0, (look.horizon - Hs / 2) / 2, 0]}>\n              <planeGeometry args={[W, Hs / 2 + look.horizon]} />\n              <meshBasicMaterial color={look.ground} toneMapped={false} />\n            </mesh>\n            {/* shapes are clipped to the screen by keeping them inside it: circles are scaled\n                down where they would spill over the edge */}\n            {look.blobs.map((b) => (\n              <mesh\n                key={`${b.pos[0].toFixed(4)}:${b.pos[1].toFixed(4)}`}\n                position={[\n                  Math.max(-W / 2 + b.r, Math.min(W / 2 - b.r, b.pos[0])),\n                  Math.max(-Hs / 2 + Math.min(b.r, Hs / 2), b.pos[1]),\n                  b.pos[2] * unit,\n                ]}\n              >\n                <circleGeometry args={[Math.min(b.r, Hs / 2), 24]} />\n                <meshBasicMaterial color={b.color} toneMapped={false} />\n              </mesh>\n            ))}\n          </>\n        ) : (\n          <mesh>\n            <planeGeometry args={[W, Hs]} />\n            <meshStandardMaterial color={OFF} roughness={0.12} metalness={0.4} />\n          </mesh>\n        )}\n      </group>\n\n      {look.mount === 'stand' && (\n        <>\n          <mesh position={[0, (cy - Hs / 2) / 2, -thick * 0.2]} castShadow>\n            <boxGeometry args={[0.07 * unit, cy - Hs / 2, 0.04 * unit]} />\n            <meshStandardMaterial color={frame} roughness={0.5} metalness={0.3} />\n          </mesh>\n          <mesh position={[0, 0.01 * unit, 0]} castShadow receiveShadow>\n            <boxGeometry args={[0.45 * unit, 0.02 * unit, 0.26 * unit]} />\n            <meshStandardMaterial color={frame} roughness={0.5} metalness={0.3} />\n          </mesh>\n        </>\n      )}\n      {look.mount === 'console' && (\n        <>\n          <mesh position={[0, consoleH + standLift / 2, 0]} castShadow>\n            <boxGeometry args={[0.3 * unit, standLift, 0.18 * unit]} />\n            <meshStandardMaterial color={frame} roughness={0.5} metalness={0.3} />\n          </mesh>\n          <mesh position={[0, consoleH / 2 + 0.04 * unit, 0]} castShadow receiveShadow>\n            <boxGeometry args={[W * 1.24, consoleH - 0.08 * unit, 0.44 * unit]} />\n            <meshStandardMaterial color={consoleColor ?? palette.woodDark} roughness={0.7} />\n          </mesh>\n          {[-1, 1].map((s) => (\n            <mesh key={s} position={[s * W * 0.31, consoleH / 2 + 0.04 * unit, 0.222 * unit]}>\n              <planeGeometry args={[W * 0.58, consoleH - 0.14 * unit]} />\n              <meshStandardMaterial color={consoleColor ?? palette.wood} roughness={0.6} />\n            </mesh>\n          ))}\n          {[-1, 1].flatMap((sx) =>\n            [-1, 1].map((sz) => (\n              <mesh key={`${sx}${sz}`} position={[sx * W * 0.58, 0.02 * unit, sz * 0.18 * unit]}>\n                <boxGeometry args={[0.04 * unit, 0.04 * unit, 0.04 * unit]} />\n                <meshStandardMaterial color={frame} roughness={0.5} />\n              </mesh>\n            )),\n          )}\n        </>\n      )}\n    </RigidBody>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
