{
  "name": "screen",
  "title": "Screen",
  "type": "registry:component",
  "description": "A display of any size (a phone, a monitor, a TV, a digital frame, a billboard) on a wall, a stand, a desk foot, or in a frame. Shows a seeded picture or desktop, a text banner, animated static, or a color, or a texture the app hands it.",
  "dependencies": [
    "@react-three/fiber@^9.6.1",
    "@react-three/rapier@^2.2.0",
    "@runek/core@^0.15.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [
    "sign"
  ],
  "files": [
    {
      "path": "Screen.tsx",
      "content": "import { useFrame } from '@react-three/fiber'\nimport { CuboidCollider, RigidBody } from '@react-three/rapier'\nimport { pick, range, rng, sub, useWorld, type Vec3, type WorldComponentProps } from '@runek/core'\nimport { useMemo, useState } from 'react'\nimport { Color, ShaderMaterial, type Texture } from 'three'\nimport { Sign } from './Sign'\n\n/** Flat on a `wall`, on two floor `stand` posts, on a `desk` foot, or in a wooden picture `frame`\n *  (a digital picture frame, on a wall). */\nexport type ScreenMount = 'wall' | 'stand' | 'desk' | 'frame'\n/** What the screen shows when no `texture` is given. */\nexport type ScreenContent = 'picture' | 'windows' | 'banner' | 'static' | 'color'\n\nexport interface ScreenProps extends WorldComponentProps {\n  /** The lit area, [width, height] in units: a phone, a TV, a billboard. */\n  size?: [number, number]\n  /** Where it hangs or stands; `wall` by default. */\n  mount?: ScreenMount\n  /** A seeded `picture` (sky, sun, hills) or desktop `windows`, a `banner` of `text`, animated\n   *  `static`, or a plain `color`. Seeded between picture and windows when unset. */\n  content?: ScreenContent\n  /** The banner's words. */\n  text?: string\n  /** Background glow, and the tint of the picture and windows. Seeded when unset. */\n  color?: string\n  /** Banner text color. */\n  textColor?: string\n  /** Lit; off, dark glass. */\n  on?: boolean\n  /** Bezel and stand; defaults to the palette's `metal` (`wood` for a frame). */\n  frameColor?: string\n  /** JSX only: a texture the app owns (a `VideoTexture`, a `CanvasTexture`, a render target's\n   *  texture) drawn in place of `content`. The screen loads nothing itself. */\n  texture?: Texture\n}\n\ninterface Shape {\n  pos: Vec3\n  size: [number, number]\n  color: string\n  round?: boolean\n}\n\nconst TINTS = ['#6fa3d8', '#e3a46a', '#8fc6a8', '#b49be0', '#e07f7f', '#7fb8c9']\nconst OFF = '#101214'\n\nconst NOISE_VERTEX = /* glsl */ `\nvarying vec2 vUv;\nvoid main() {\n  vUv = uv;\n  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}`\nconst NOISE_FRAGMENT = /* glsl */ `\nuniform float uTime;\nuniform vec2 uCells;\nvarying vec2 vUv;\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }\nvoid main() {\n  vec2 cell = floor(vUv * uCells);\n  float n = hash(cell + floor(uTime * 24.0) * vec2(1.7, 9.2));\n  float band = 0.85 + 0.15 * sin(vUv.y * 40.0 + uTime * 6.0);\n  gl_FragColor = vec4(vec3(n * band), 1.0);\n}`\n\n/** Flat shapes for a picture or a desktop, in screen-local meters, kept inside the screen. */\nfunction compose(kind: 'picture' | 'windows', seed: number, tint: string, W: number, H: number) {\n  const r = rng(sub(seed, 1))\n  const base = new Color(tint)\n  const shade = (k: number, hue = 0) =>\n    base.clone().offsetHSL(hue, 0, 0).multiplyScalar(k).getStyle()\n  const out: Shape[] = []\n  if (kind === 'picture') {\n    const horizon = range(r, -0.15, 0.1) * H\n    out.push({ pos: [0, (H / 2 + horizon) / 2, 0], size: [W, H / 2 - horizon], color: shade(1.15) })\n    out.push({\n      pos: [0, (horizon - H / 2) / 2, 0],\n      size: [W, H / 2 + horizon],\n      color: shade(0.35, 0.25),\n    })\n    const m = Math.min(W, H)\n    const sunR = 0.12 * m\n    out.push({\n      pos: [\n        range(r, -0.3, 0.3) * W,\n        Math.min(H / 2 - sunR, horizon + range(r, 0.12, 0.28) * H),\n        0.0006,\n      ],\n      size: [sunR, sunR],\n      color: '#fff3c4',\n      round: true,\n    })\n    for (let i = 0; i < 4; i++) {\n      const rad = Math.min(range(r, 0.2, 0.38) * m, H / 2)\n      const x = range(r, -0.5, 0.5) * W\n      out.push({\n        pos: [\n          Math.max(-W / 2 + rad, Math.min(W / 2 - rad, x)),\n          Math.max(-H / 2 + rad, horizon - rad * range(r, 0.8, 0.95)),\n          0.001 + i * 0.0002,\n        ],\n        size: [rad, rad],\n        color: shade(i < 2 ? 0.55 : 0.4, 0.25),\n        round: true,\n      })\n    }\n    return out\n  }\n  out.push({ pos: [0, 0, 0], size: [W, H], color: tint })\n  const count = 1 + Math.floor(r() * 3)\n  for (let i = 0; i < count; i++) {\n    const w = range(r, 0.35, 0.7) * W\n    const h = range(r, 0.4, 0.75) * H\n    const x = range(r, -W / 2 + w / 2 + 0.02 * W, W / 2 - w / 2 - 0.02 * W)\n    const y = range(r, -H / 2 + h / 2 + 0.04 * H, H / 2 - h / 2 - 0.04 * H)\n    const z = 0.0006 * (i + 1)\n    out.push({ pos: [x, y, z], size: [w, h], color: shade(1.35) })\n    out.push({ pos: [x, y + h / 2 - 0.05 * h, z + 0.0002], size: [w, 0.1 * h], color: shade(0.75) })\n    const lines = 2 + Math.floor(r() * 4)\n    for (let l = 0; l < lines; l++) {\n      const lw = range(r, 0.3, 0.85) * w * 0.85\n      out.push({\n        pos: [x - w * 0.42 + lw / 2, y + h * 0.28 - l * h * 0.14, z + 0.0002],\n        size: [lw, 0.045 * h],\n        color: shade(0.6),\n      })\n    }\n  }\n  return out\n}\n\n/**\n * A display of any size: a phone, a computer screen, a TV, a digital picture frame, or a large\n * digital banner. It shows a seeded picture or desktop, a banner of text, animated static, or a\n * plain color, all geometry, and an app can hand it a live `texture` (video, canvas, a render of\n * another camera) to show instead; the screen itself never loads anything. On a `wall` or in a\n * `frame` its origin is the middle of the screen with its back to the wall (local -Z); on a\n * `stand` or a `desk` it is the foot. One cuboid collider.\n */\nexport function Screen({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  seed = 1,\n  size = [1.6, 0.9],\n  mount = 'wall',\n  content,\n  text = 'Runek',\n  color,\n  textColor = '#ffffff',\n  on = true,\n  frameColor,\n  texture,\n}: ScreenProps) {\n  const { unit, palette } = useWorld()\n  const W = size[0] * unit\n  const H = size[1] * unit\n  const framed = mount === 'frame'\n  const frame = frameColor ?? (framed ? palette.wood : palette.metal)\n  const m = Math.min(W, H)\n  const bezel = framed ? Math.max(0.03 * unit, m * 0.08) : Math.max(0.006 * unit, m * 0.015)\n  const thick = framed ? 0.03 * unit : Math.max(0.012 * unit, m * 0.03)\n\n  const look = useMemo(() => {\n    const r = rng(sub(seed, 0))\n    const kind = content ?? pick<ScreenContent>(r, ['picture', 'windows'])\n    const tint = color ?? pick(r, TINTS)\n    const shapes = kind === 'picture' || kind === 'windows' ? compose(kind, seed, tint, W, H) : []\n    return { kind, tint, shapes }\n  }, [seed, content, color, W, H])\n\n  const noise = useMemo(\n    () =>\n      new ShaderMaterial({\n        vertexShader: NOISE_VERTEX,\n        fragmentShader: NOISE_FRAGMENT,\n        uniforms: { uTime: { value: 0 }, uCells: { value: [160, 160 * (H / W)] } },\n        toneMapped: false,\n      }),\n    [W, H],\n  )\n  useFrame(({ clock }) => {\n    if (look.kind === 'static' && on && !texture) noise.uniforms.uTime.value = clock.elapsedTime\n  })\n\n  // the banner is one line, laid out at full height and then shrunk to fit the width\n  const [fit, setFit] = useState(1)\n  const standing = mount === 'stand' || mount === 'desk'\n  const lift = mount === 'stand' ? 0.8 * unit : 0.1 * unit + H * 0.1\n  const cy = standing ? lift + bezel + H / 2 : 0\n  const cz = standing ? 0 : thick / 2 + 0.005 * unit\n  const top = cy + H / 2 + bezel\n  const glass = 0.0005 * unit + thick / 2\n\n  return (\n    <RigidBody type=\"fixed\" colliders={false} position={position} rotation={rotation}>\n      {standing ? (\n        <CuboidCollider\n          args={[W / 2 + bezel, top / 2, Math.max(thick, 0.08 * unit)]}\n          position={[0, top / 2, 0]}\n        />\n      ) : (\n        <CuboidCollider args={[W / 2 + bezel, H / 2 + bezel, thick / 2]} position={[0, 0, cz]} />\n      )}\n\n      <mesh position={[0, cy, cz]} castShadow>\n        <boxGeometry args={[W + bezel * 2, H + bezel * 2, thick]} />\n        <meshStandardMaterial\n          color={frame}\n          roughness={framed ? 0.6 : 0.4}\n          metalness={framed ? 0 : 0.3}\n        />\n      </mesh>\n      {framed && (\n        <mesh position={[0, cy, cz + thick / 2 + 0.0003 * unit]}>\n          <planeGeometry args={[W + bezel * 0.6, H + bezel * 0.6]} />\n          <meshStandardMaterial color=\"#f2efe8\" roughness={0.9} />\n        </mesh>\n      )}\n\n      <group position={[0, cy, cz + glass + (framed ? 0.0005 * unit : 0)]}>\n        {!on ? (\n          <mesh>\n            <planeGeometry args={[W, H]} />\n            <meshStandardMaterial color={OFF} roughness={0.12} metalness={0.4} />\n          </mesh>\n        ) : texture ? (\n          <mesh>\n            <planeGeometry args={[W, H]} />\n            <meshBasicMaterial map={texture} toneMapped={false} />\n          </mesh>\n        ) : look.kind === 'static' ? (\n          <mesh material={noise}>\n            <planeGeometry args={[W, H]} />\n          </mesh>\n        ) : look.kind === 'color' || look.kind === 'banner' ? (\n          <>\n            <mesh>\n              <planeGeometry args={[W, H]} />\n              <meshBasicMaterial color={look.tint} toneMapped={false} />\n            </mesh>\n            {look.kind === 'banner' && text && (\n              <group position={[0, 0, 0.001 * unit]} scale={fit}>\n                <Sign\n                  size={(H * 0.42) / unit}\n                  color={textColor}\n                  onBounds={(w, h) => w > 0 && setFit(Math.min(1, (W * 0.9) / w, (H * 0.8) / h))}\n                >\n                  {text}\n                </Sign>\n              </group>\n            )}\n          </>\n        ) : (\n          look.shapes.map((s) => (\n            <mesh\n              key={`${s.pos[0].toFixed(4)}:${s.pos[1].toFixed(4)}:${s.pos[2]}`}\n              position={[s.pos[0], s.pos[1], s.pos[2] * unit]}\n            >\n              {s.round ? (\n                <circleGeometry args={[s.size[0], 28]} />\n              ) : (\n                <planeGeometry args={s.size} />\n              )}\n              <meshBasicMaterial color={s.color} toneMapped={false} />\n            </mesh>\n          ))\n        )}\n      </group>\n\n      {mount === 'stand' &&\n        [-1, 1].map((s) => (\n          <group key={s} position={[s * W * 0.35, 0, 0]}>\n            <mesh position={[0, (cy - H / 2) / 2, -thick]} castShadow>\n              <boxGeometry args={[0.06 * unit, cy - H / 2, 0.06 * unit]} />\n              <meshStandardMaterial color={frame} roughness={0.5} metalness={0.3} />\n            </mesh>\n            <mesh position={[0, 0.015 * unit, -thick]} castShadow receiveShadow>\n              <boxGeometry args={[0.12 * unit, 0.03 * unit, 0.4 * unit]} />\n              <meshStandardMaterial color={frame} roughness={0.5} metalness={0.3} />\n            </mesh>\n          </group>\n        ))}\n      {mount === 'desk' && (\n        <>\n          <mesh position={[0, (lift + H * 0.3) / 2, -thick]} castShadow>\n            <boxGeometry args={[0.05 * unit, lift + H * 0.3, 0.025 * unit]} />\n            <meshStandardMaterial color={frame} roughness={0.5} metalness={0.3} />\n          </mesh>\n          <mesh position={[0, 0.006 * unit, -thick]} castShadow receiveShadow>\n            <cylinderGeometry args={[0.11 * unit, 0.12 * unit, 0.012 * unit, 24]} />\n            <meshStandardMaterial color={frame} roughness={0.5} metalness={0.3} />\n          </mesh>\n        </>\n      )}\n    </RigidBody>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
