{
  "name": "poster",
  "title": "Poster",
  "type": "registry:component",
  "description": "A wall print of seeded flat art (shapes, color bands, a retro sunset, mountain peaks) with an optional caption and a slim or wide frame.",
  "dependencies": [
    "@runek/core@^0.15.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [
    "sign"
  ],
  "files": [
    {
      "path": "Poster.tsx",
      "content": "import { pick, range, rng, sub, useWorld, type WorldComponentProps } from '@runek/core'\nimport { useMemo } from 'react'\nimport type { Texture } from 'three'\nimport { Sign } from './Sign'\n\n/** The print: overlapping `shapes`, colored `bands`, a retro `sunset` over the sea, or `peaks`. */\nexport type PosterArt = 'shapes' | 'bands' | 'sunset' | 'peaks'\n/** No frame, a slim one, or a wide one with a mat. */\nexport type PosterFrame = 'none' | 'thin' | 'wide'\n\nexport interface PosterProps extends WorldComponentProps {\n  /** The print, [width, height] in units. */\n  size?: [number, number]\n  /** Seeded when unset. */\n  art?: PosterArt\n  /** Seeded when unset. */\n  frame?: PosterFrame\n  /** A caption printed under the art. */\n  title?: string\n  /** The frame; defaults to the palette's `woodDark`. */\n  frameColor?: string\n  /** The paper behind the art and the caption. */\n  paper?: string\n  /** The caption's ink. */\n  ink?: string\n  /** JSX only: a texture the app owns, printed in place of the seeded art. The poster loads\n   *  nothing itself. */\n  texture?: Texture\n}\n\ntype Kind = 'rect' | 'circle' | 'tri'\ninterface Shape {\n  kind: Kind\n  x: number\n  y: number\n  w: number\n  h: number\n  color: string\n}\n\nconst ARTS: PosterArt[] = ['shapes', 'bands', 'sunset', 'peaks']\nconst FRAMES: PosterFrame[] = ['none', 'thin', 'thin', 'wide']\nconst PALETTES = [\n  ['#e4572e', '#f3a712', '#29335c', '#a8c686', '#f0ead6'],\n  ['#264653', '#2a9d8f', '#e9c46a', '#f4a261', '#e76f51'],\n  ['#1d3557', '#457b9d', '#a8dadc', '#f1faee', '#e63946'],\n  ['#3d405b', '#81b29a', '#f2cc8f', '#e07a5f', '#f4f1de'],\n  ['#2b2d42', '#8d99ae', '#edf2f4', '#ef233c', '#d90429'],\n]\n\n/** Shapes in art-local meters (centered, W by H), each inside the art area. */\nfunction paint(art: PosterArt, seed: number, W: number, H: number): Shape[] {\n  const r = rng(sub(seed, 1))\n  const colors = pick(r, PALETTES)\n  const c = (i: number) => colors[i % colors.length]\n  const out: Shape[] = []\n  const m = Math.min(W, H)\n  if (art === 'bands') {\n    const n = 4 + Math.floor(r() * 5)\n    let y = -H / 2\n    const weights = Array.from({ length: n }, () => range(r, 0.5, 1.5))\n    const total = weights.reduce((a, b) => a + b, 0)\n    weights.forEach((wt, i) => {\n      const h = (wt / total) * H\n      out.push({ kind: 'rect', x: 0, y: y + h / 2, w: W, h, color: c(i) })\n      y += h\n    })\n    return out\n  }\n  if (art === 'sunset') {\n    const horizon = range(r, -0.2, 0.05) * H\n    out.push({\n      kind: 'rect',\n      x: 0,\n      y: (H / 2 + horizon) / 2,\n      w: W,\n      h: H / 2 - horizon,\n      color: c(4),\n    })\n    const sun = range(r, 0.28, 0.4) * m\n    out.push({ kind: 'circle', x: 0, y: horizon, w: sun, h: sun, color: c(0) })\n    // the sea covers the sun's lower half; stripes cut across what shows above it\n    out.push({\n      kind: 'rect',\n      x: 0,\n      y: (horizon - H / 2) / 2,\n      w: W,\n      h: H / 2 + horizon,\n      color: c(2),\n    })\n    for (let i = 0; i < 4; i++) {\n      out.push({\n        kind: 'rect',\n        x: 0,\n        y: horizon + sun * (0.15 + i * 0.18),\n        w: sun * 2.1,\n        h: sun * 0.05 * (1 + i * 0.4),\n        color: c(4),\n      })\n    }\n    for (let i = 0; i < 3; i++) {\n      out.push({\n        kind: 'rect',\n        x: range(r, -0.1, 0.1) * W,\n        y: horizon - (i + 1) * 0.08 * H,\n        w: range(r, 0.2, 0.6) * W,\n        h: 0.008 * H,\n        color: c(1),\n      })\n    }\n    return out\n  }\n  if (art === 'peaks') {\n    out.push({ kind: 'rect', x: 0, y: 0, w: W, h: H, color: c(4) })\n    out.push({\n      kind: 'circle',\n      x: range(r, -0.3, 0.3) * W,\n      y: range(r, 0.15, 0.3) * H,\n      w: 0.1 * m,\n      h: 0.1 * m,\n      color: c(1),\n    })\n    for (let i = 0; i < 4; i++) {\n      const w = range(r, 0.45, 0.8) * W\n      const h = range(r, 0.35, 0.65) * H * (1 - i * 0.12)\n      // a triangle's centroid sits a third of its height above the base\n      out.push({\n        kind: 'tri',\n        x: range(r, -W / 2 + w / 2, W / 2 - w / 2),\n        y: -H / 2 + h / 3,\n        w,\n        h,\n        color: c(i % 2 ? 3 : 0),\n      })\n    }\n    return out\n  }\n  out.push({ kind: 'rect', x: 0, y: 0, w: W, h: H, color: c(4) })\n  const n = 4 + Math.floor(r() * 4)\n  for (let i = 0; i < n; i++) {\n    const kind = pick<Kind>(r, ['rect', 'circle', 'circle', 'tri'])\n    const s = range(r, 0.15, 0.4) * m\n    const w = kind === 'rect' ? s * range(r, 0.6, 2) : s\n    const h = kind === 'rect' ? s * range(r, 0.6, 2) : s\n    const hw = kind === 'circle' ? s : Math.min(w, W) / 2\n    // how far the shape reaches below and above its origin\n    const below = kind === 'circle' ? s : kind === 'tri' ? h / 3 : h / 2\n    const above = kind === 'circle' ? s : kind === 'tri' ? (h * 2) / 3 : h / 2\n    out.push({\n      kind,\n      x: range(r, -W / 2 + hw, W / 2 - hw),\n      y: range(r, -H / 2 + below, H / 2 - above),\n      w: Math.min(w, W),\n      h: Math.min(h, H),\n      color: c(i % 4),\n    })\n  }\n  return out\n}\n\n/**\n * A print for a wall: seeded flat art (overlapping shapes, color bands, a retro sunset, or\n * mountain peaks) on paper, with an optional caption and a slim or wide frame. Its origin is the\n * middle of the print with its back to the wall (local -Z). An app can print its own `texture`\n * instead; the poster loads nothing itself. Decorative: no collider.\n */\nexport function Poster({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  seed = 1,\n  size = [0.5, 0.7],\n  art,\n  frame,\n  title,\n  frameColor,\n  paper = '#f4f1ea',\n  ink = '#222222',\n  texture,\n}: PosterProps) {\n  const { unit, palette } = useWorld()\n  const W = size[0] * unit\n  const H = size[1] * unit\n  const m = Math.min(W, H)\n  const margin = 0.06 * m\n  const caption = title ? 0.14 * H : 0\n  const artW = W - margin * 2\n  const artH = H - margin * 2 - caption\n\n  const look = useMemo(() => {\n    const r = rng(sub(seed, 0))\n    const rolled = { art: pick(r, ARTS), frame: pick(r, FRAMES) }\n    const kind = art ?? rolled.art\n    return { art: kind, frame: frame ?? rolled.frame, shapes: paint(kind, seed, artW, artH) }\n  }, [seed, art, frame, artW, artH])\n\n  const edge = look.frame === 'wide' ? 0.06 * unit : look.frame === 'thin' ? 0.02 * unit : 0\n  const depth = look.frame === 'none' ? 0.002 * unit : 0.025 * unit\n  const face = depth + 0.0005 * unit\n  const artY = caption / 2\n  const wood = frameColor ?? palette.woodDark\n  const layer = 0.0003 * unit\n\n  return (\n    <group position={position} rotation={rotation}>\n      {look.frame !== 'none' && (\n        <mesh position={[0, 0, depth / 2]} castShadow>\n          <boxGeometry args={[W + edge * 2, H + edge * 2, depth]} />\n          <meshStandardMaterial color={wood} roughness={0.6} />\n        </mesh>\n      )}\n      <mesh position={[0, 0, face]}>\n        <planeGeometry args={[W, H]} />\n        <meshStandardMaterial color={paper} roughness={0.9} />\n      </mesh>\n      <group position={[0, artY, face + layer]}>\n        {texture ? (\n          <mesh>\n            <planeGeometry args={[artW, artH]} />\n            <meshStandardMaterial map={texture} roughness={0.8} />\n          </mesh>\n        ) : (\n          look.shapes.map((s, i) => (\n            <mesh\n              key={i}\n              position={[s.x, s.y, i * layer]}\n              rotation={s.kind === 'tri' ? [0, 0, Math.PI / 2] : [0, 0, 0]}\n              scale={s.kind === 'tri' ? [s.h / 1.5, s.w / Math.sqrt(3), 1] : [1, 1, 1]}\n            >\n              {s.kind === 'rect' ? (\n                <planeGeometry args={[s.w, s.h]} />\n              ) : s.kind === 'circle' ? (\n                <circleGeometry args={[s.w, 32]} />\n              ) : (\n                <circleGeometry args={[1, 3]} />\n              )}\n              <meshStandardMaterial color={s.color} roughness={0.85} />\n            </mesh>\n          ))\n        )}\n      </group>\n      {title && (\n        <Sign\n          position={[0, -H / 2 + margin + caption / 2, face + layer]}\n          size={(caption * 0.45) / unit}\n          maxWidth={artW / unit}\n          color={ink}\n          variant=\"body\"\n        >\n          {title}\n        </Sign>\n      )}\n    </group>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
