{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pattern-press",
  "type": "registry:lib",
  "title": "The Pattern Press",
  "description": "Four generative pattern families — Truchet tilings, flow fields, isometric lattices and interference meshes — resolved to primitives once and rendered through canvas, SVG, or a dependency-free software rasteriser, so the vectors you paste are the pixels you saw. Deterministic in its seed and inked entirely from your own tokens.",
  "dependencies": [],
  "files": [
    {
      "path": "lib/render/patterns.ts",
      "content": "/* Experience Design System influenced by Chromologium (https://chromologium.com/) */\n/**\n * The Chromologium — The Pattern Press (LAB-11)\n *\n * Four generative pattern families, each resolved to a list of primitives\n * before anything is drawn. Canvas and SVG are then two backends over the\n * same geometry, which is the only way the \"paste into Figma\" promise stays\n * honest: the vectors a designer receives are the pixels they saw, not a\n * second implementation that drifted.\n *\n * Every pattern is deterministic in its seed, so a permalink reproduces a\n * sheet exactly, and every colour arrives from the caller as a resolved\n * token — the patterns re-ink when the archive transmutes rather than\n * carrying palettes of their own.\n */\n\nexport type PatternFamily = \"truchet\" | \"flow\" | \"isometric\" | \"interference\";\n\nexport interface PatternState {\n  family: PatternFamily;\n  /** Deterministic seed. */\n  seed: number;\n  /** Cell or spacing size, in px at the reference width. */\n  scale: number;\n  /** Stroke weight multiplier. */\n  weight: number;\n  /** Family-specific character: curl, turbulence, depth, skew. */\n  bias: number;\n  /** Density of marks, 0–1. */\n  density: number;\n}\n\nexport interface PatternInk {\n  /** Sheet ground. */\n  paper: string;\n  /** Primary mark colour. */\n  ink: string;\n  /** Emphasis, used for a minority of marks. */\n  accent: string;\n}\n\nexport const PATTERN_FAMILIES: {\n  id: PatternFamily;\n  label: string;\n  note: string;\n}[] = [\n  {\n    id: \"truchet\",\n    label: \"Truchet\",\n    note: \"Sébastien Truchet's 1704 tiling: one tile, two orientations, and the illusion of a drawn labyrinth. Nothing here is routed — each cell independently flips a coin, and continuity across edges is a consequence of the tile's geometry rather than of any plan.\",\n  },\n  {\n    id: \"flow\",\n    label: \"Flow Field\",\n    note: \"A comb of points released along the top edge, each followed down through a vector field and leaving a mark at fixed intervals. The deflection is two waves whose frequencies do not divide into one another: a single wave would have fixed nodes, and every strand would slide into the nearest one.\",\n  },\n  {\n    id: \"isometric\",\n    label: \"Isometric Lattice\",\n    note: \"Three rhombi meeting at 120° read as a cube because the eye insists on depth from an axonometric projection that contains none. Shading two faces darker is enough to fix the reading — and inverting which two flips the solid inside out.\",\n  },\n  {\n    id: \"interference\",\n    label: \"Interference Mesh\",\n    note: \"Two line gratings crossed at a small angle. The moiré is not drawn: it is the beat frequency between the two rulings, and it moves faster than either of them, which is why a one-degree change in the angle sweeps the fringes across the whole sheet.\",\n  },\n];\n\n/* ── Primitives ───────────────────────────────────────────────────────── */\n\nexport type Prim =\n  | {\n      k: \"arc\";\n      x: number;\n      y: number;\n      r: number;\n      a0: number;\n      a1: number;\n      stroke: string;\n      w: number;\n    }\n  | {\n      k: \"line\";\n      x1: number;\n      y1: number;\n      x2: number;\n      y2: number;\n      stroke: string;\n      w: number;\n    }\n  | { k: \"circle\"; x: number; y: number; r: number; fill: string }\n  | { k: \"poly\"; pts: [number, number][]; fill: string };\n\n/** Mulberry32 — small, fast, and reproducible across engines. */\nfunction rng(seed: number): () => number {\n  let a = seed >>> 0;\n  return () => {\n    a = (a + 0x6d2b79f5) >>> 0;\n    let t = a;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\n/* ── Families ─────────────────────────────────────────────────────────── */\n\nfunction truchet(\n  width: number,\n  height: number,\n  state: PatternState,\n  ink: PatternInk,\n): Prim[] {\n  const rand = rng(state.seed);\n  const cell = Math.max(8, state.scale);\n  const w = Math.max(1, state.weight * cell * 0.12);\n  const out: Prim[] = [];\n  for (let y = 0; y < Math.ceil(height / cell); y++) {\n    for (let x = 0; x < Math.ceil(width / cell); x++) {\n      const cx = x * cell;\n      const cy = y * cell;\n      // `bias` tilts the coin, so a sheet can run from balanced weave to\n      // long parallel runs without changing family.\n      const flipped = rand() < 0.5 + (state.bias - 0.5) * 0.9;\n      const stroke = rand() < state.density * 0.25 ? ink.accent : ink.ink;\n      const r = cell / 2;\n      if (flipped) {\n        out.push(\n          { k: \"arc\", x: cx, y: cy, r, a0: 0, a1: Math.PI / 2, stroke, w },\n          {\n            k: \"arc\",\n            x: cx + cell,\n            y: cy + cell,\n            r,\n            a0: Math.PI,\n            a1: Math.PI * 1.5,\n            stroke,\n            w,\n          },\n        );\n      } else {\n        out.push(\n          {\n            k: \"arc\",\n            x: cx + cell,\n            y: cy,\n            r,\n            a0: Math.PI / 2,\n            a1: Math.PI,\n            stroke,\n            w,\n          },\n          {\n            k: \"arc\",\n            x: cx,\n            y: cy + cell,\n            r,\n            a0: Math.PI * 1.5,\n            a1: Math.PI * 2,\n            stroke,\n            w,\n          },\n        );\n      }\n    }\n  }\n  return out;\n}\n\nfunction flow(\n  width: number,\n  height: number,\n  state: PatternState,\n  ink: PatternInk,\n): Prim[] {\n  const rand = rng(state.seed);\n  const strands = Math.max(6, Math.round((width / state.scale) * 0.9));\n  const step = Math.max(3, state.scale * 0.35);\n  const dotR = Math.max(0.6, state.weight * step * 0.16);\n  // Two incommensurable frequencies: a single wave would have fixed nodes\n  // and every strand would collapse into the nearest one.\n  const f1 = 0.9 + state.bias * 2.2;\n  const f2 = f1 * 1.6180339887;\n  const amp = (0.35 + state.bias * 2.4) * step;\n  const out: Prim[] = [];\n  for (let s = 0; s < strands; s++) {\n    const x0 = ((s + 0.5) / strands) * width;\n    const jitter = (rand() - 0.5) * step * 0.3;\n    // Displacement is a function of depth, not an accumulation of it: an\n    // accumulated oscillation cancels itself and every strand stays a\n    // straight ruled line, which is the opposite of a flow field. Reading\n    // the field as a position means the strands genuinely crowd and part,\n    // while the spacing *along* each strand never changes — so no dot ever\n    // leaves its own row.\n    for (let y = 0; y < height; y += step) {\n      const p = y / height;\n      const displacement =\n        (Math.sin(p * Math.PI * 2 * f1 + s * 0.35) +\n          Math.sin(p * Math.PI * 2 * f2 + s * 0.11)) *\n        amp *\n        p;\n      out.push({\n        k: \"circle\",\n        x: x0 + displacement + jitter,\n        y,\n        r: dotR,\n        fill: rand() < state.density * 0.12 ? ink.accent : ink.ink,\n      });\n    }\n  }\n  return out;\n}\n\nfunction isometric(\n  width: number,\n  height: number,\n  state: PatternState,\n  ink: PatternInk,\n): Prim[] {\n  const rand = rng(state.seed);\n  const cell = Math.max(10, state.scale);\n  const hw = cell;\n  const hh = cell * 0.5774; // tan(30°): the isometric half-height\n  const out: Prim[] = [];\n  const cols = Math.ceil(width / hw) + 2;\n  const rows = Math.ceil(height / hh) + 2;\n  for (let row = 0; row < rows; row++) {\n    for (let col = 0; col < cols; col++) {\n      if (rand() > state.density) continue;\n      const x = col * hw + (row % 2 ? hw / 2 : 0) - hw;\n      const y = row * hh - hh;\n      // Three rhombi meeting at 120°; shading two of them fixes the reading\n      // as a solid rather than a flat hexagon.\n      const top: [number, number][] = [\n        [x, y],\n        [x + hw / 2, y - hh],\n        [x + hw, y],\n        [x + hw / 2, y + hh],\n      ];\n      const left: [number, number][] = [\n        [x, y],\n        [x + hw / 2, y + hh],\n        [x + hw / 2, y + hh + cell * 0.7],\n        [x, y + cell * 0.7],\n      ];\n      const right: [number, number][] = [\n        [x + hw, y],\n        [x + hw / 2, y + hh],\n        [x + hw / 2, y + hh + cell * 0.7],\n        [x + hw, y + cell * 0.7],\n      ];\n      const emphasis = rand() < state.bias * 0.35;\n      const face = emphasis ? ink.accent : ink.ink;\n      out.push(\n        { k: \"poly\", pts: top, fill: face },\n        { k: \"poly\", pts: left, fill: shade(face, 0.55) },\n        { k: \"poly\", pts: right, fill: shade(face, 0.78) },\n      );\n    }\n  }\n  return out;\n}\n\nfunction interference(\n  width: number,\n  height: number,\n  state: PatternState,\n  ink: PatternInk,\n): Prim[] {\n  const spacing = Math.max(3, state.scale * 0.4);\n  const w = Math.max(0.5, state.weight * spacing * 0.28);\n  // The beat between the two rulings is the pattern; neither ruling is.\n  const angle = (state.bias - 0.5) * 0.35;\n  const out: Prim[] = [];\n  const span = Math.hypot(width, height);\n  const push = (theta: number, stroke: string) => {\n    const dx = Math.cos(theta);\n    const dy = Math.sin(theta);\n    const nx = -dy;\n    const ny = dx;\n    for (let d = -span; d < span; d += spacing) {\n      out.push({\n        k: \"line\",\n        x1: width / 2 + nx * d - dx * span,\n        y1: height / 2 + ny * d - dy * span,\n        x2: width / 2 + nx * d + dx * span,\n        y2: height / 2 + ny * d + dy * span,\n        stroke,\n        w,\n      });\n    }\n  };\n  push(0, ink.ink);\n  push(angle, state.density > 0.5 ? ink.accent : ink.ink);\n  return out;\n}\n\n/** Darkens a colour toward black; used for the isometric side faces. */\nfunction shade(hex: string, factor: number): string {\n  const m = hex.match(/^#?([0-9a-fA-F]{6})$/);\n  if (!m) return hex;\n  const n = parseInt(m[1], 16);\n  const r = Math.round(((n >> 16) & 0xff) * factor);\n  const g = Math.round(((n >> 8) & 0xff) * factor);\n  const b = Math.round((n & 0xff) * factor);\n  return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, \"0\")}`;\n}\n\nconst BUILDERS: Record<\n  PatternFamily,\n  (w: number, h: number, s: PatternState, i: PatternInk) => Prim[]\n> = { truchet, flow, isometric, interference };\n\n/** Resolves a state into primitives. Deterministic in `state.seed`. */\nexport function buildPattern(\n  width: number,\n  height: number,\n  state: PatternState,\n  ink: PatternInk,\n): Prim[] {\n  return BUILDERS[state.family](width, height, state, ink);\n}\n\n/* ── Backends ─────────────────────────────────────────────────────────── */\n\nexport function drawPattern(\n  ctx: CanvasRenderingContext2D,\n  width: number,\n  height: number,\n  state: PatternState,\n  ink: PatternInk,\n): void {\n  ctx.fillStyle = ink.paper;\n  ctx.fillRect(0, 0, width, height);\n  ctx.lineCap = \"butt\";\n  for (const p of buildPattern(width, height, state, ink)) {\n    switch (p.k) {\n      case \"arc\":\n        ctx.strokeStyle = p.stroke;\n        ctx.lineWidth = p.w;\n        ctx.beginPath();\n        ctx.arc(p.x, p.y, p.r, p.a0, p.a1);\n        ctx.stroke();\n        break;\n      case \"line\":\n        ctx.strokeStyle = p.stroke;\n        ctx.lineWidth = p.w;\n        ctx.beginPath();\n        ctx.moveTo(p.x1, p.y1);\n        ctx.lineTo(p.x2, p.y2);\n        ctx.stroke();\n        break;\n      case \"circle\":\n        ctx.fillStyle = p.fill;\n        ctx.beginPath();\n        ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);\n        ctx.fill();\n        break;\n      case \"poly\":\n        ctx.fillStyle = p.fill;\n        ctx.beginPath();\n        p.pts.forEach(([x, y], i) =>\n          i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y),\n        );\n        ctx.closePath();\n        ctx.fill();\n        break;\n    }\n  }\n}\n\nconst n = (v: number) => Math.round(v * 100) / 100;\n\n/** The same geometry as editable SVG — every mark its own element. */\nexport function describePattern(\n  width: number,\n  height: number,\n  state: PatternState,\n  ink: PatternInk,\n  permalink = \"\",\n): string {\n  const body = buildPattern(width, height, state, ink).map((p) => {\n    switch (p.k) {\n      case \"arc\": {\n        const x0 = n(p.x + p.r * Math.cos(p.a0));\n        const y0 = n(p.y + p.r * Math.sin(p.a0));\n        const x1 = n(p.x + p.r * Math.cos(p.a1));\n        const y1 = n(p.y + p.r * Math.sin(p.a1));\n        return `  <path d=\"M ${x0} ${y0} A ${n(p.r)} ${n(p.r)} 0 0 1 ${x1} ${y1}\" fill=\"none\" stroke=\"${p.stroke}\" stroke-width=\"${n(p.w)}\"/>`;\n      }\n      case \"line\":\n        return `  <line x1=\"${n(p.x1)}\" y1=\"${n(p.y1)}\" x2=\"${n(p.x2)}\" y2=\"${n(p.y2)}\" stroke=\"${p.stroke}\" stroke-width=\"${n(p.w)}\"/>`;\n      case \"circle\":\n        return `  <circle cx=\"${n(p.x)}\" cy=\"${n(p.y)}\" r=\"${n(p.r)}\" fill=\"${p.fill}\"/>`;\n      case \"poly\":\n        return `  <polygon points=\"${p.pts.map(([x, y]) => `${n(x)},${n(y)}`).join(\" \")}\" fill=\"${p.fill}\"/>`;\n    }\n  });\n  const desc = permalink\n    ? `\\n  <desc>${state.family} pattern, seed ${state.seed}. The Chromologium · ${permalink}</desc>`\n    : \"\";\n  return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\" viewBox=\"0 0 ${width} ${height}\">${desc}\n  <rect width=\"${width}\" height=\"${height}\" fill=\"${ink.paper}\"/>\n${body.join(\"\\n\")}\n</svg>`;\n}\n\n/**\n * A tileable CSS approximation, for the families that genuinely tile. The\n * interference mesh is two `repeating-linear-gradient` rulings — which is\n * exactly what it is physically — so the CSS is the pattern rather than a\n * picture of it. Other families return null: a Truchet sheet is not\n * expressible as gradients, and claiming otherwise would ship a lie.\n */\nexport function patternToCss(\n  state: PatternState,\n  ink: PatternInk,\n): string | null {\n  if (state.family !== \"interference\") return null;\n  const spacing = Math.max(3, state.scale * 0.4);\n  const w = Math.max(0.5, state.weight * spacing * 0.28);\n  const deg = ((state.bias - 0.5) * 0.35 * 180) / Math.PI;\n  return `/* Interference mesh — two rulings beating against one another.\n   Sourced from The Chromologium (https://chromologium.com/lab/pattern-press) */\n.chrm-interference {\n  background-color: ${ink.paper};\n  background-image:\n    repeating-linear-gradient(90deg, ${ink.ink} 0 ${n(w)}px, transparent ${n(w)}px ${n(spacing)}px),\n    repeating-linear-gradient(${n(90 + deg)}deg, ${ink.accent} 0 ${n(w)}px, transparent ${n(w)}px ${n(spacing)}px);\n}`;\n}\n\n/* ── Software rasteriser (DOM-free, for the OG worker) ────────────────── */\n\nfunction hexToRgbTriple(hex: string): [number, number, number] {\n  const m = hex.match(/^#?([0-9a-fA-F]{6})$/);\n  if (!m) return [128, 128, 128];\n  const n = parseInt(m[1], 16);\n  return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];\n}\n\n/** Scanline-fills a polygon. Every primitive below reduces to this. */\nfunction fillPoly(\n  data: Uint8ClampedArray,\n  W: number,\n  H: number,\n  pts: [number, number][],\n  rgb: [number, number, number],\n): void {\n  let minY = Infinity;\n  let maxY = -Infinity;\n  for (const [, y] of pts) {\n    if (y < minY) minY = y;\n    if (y > maxY) maxY = y;\n  }\n  const y0 = Math.max(0, Math.floor(minY));\n  const y1 = Math.min(H - 1, Math.ceil(maxY));\n  const xs: number[] = [];\n  for (let y = y0; y <= y1; y++) {\n    xs.length = 0;\n    const sy = y + 0.5;\n    for (let i = 0; i < pts.length; i++) {\n      const [ax, ay] = pts[i];\n      const [bx, by] = pts[(i + 1) % pts.length];\n      if (ay === by) continue;\n      if (sy >= Math.min(ay, by) && sy < Math.max(ay, by)) {\n        xs.push(ax + ((sy - ay) / (by - ay)) * (bx - ax));\n      }\n    }\n    if (xs.length < 2) continue;\n    xs.sort((a, b) => a - b);\n    for (let i = 0; i + 1 < xs.length; i += 2) {\n      const sx = Math.max(0, Math.round(xs[i]));\n      const ex = Math.min(W - 1, Math.round(xs[i + 1]));\n      for (let x = sx; x <= ex; x++) {\n        const o = (y * W + x) * 4;\n        data[o] = rgb[0];\n        data[o + 1] = rgb[1];\n        data[o + 2] = rgb[2];\n        data[o + 3] = 255;\n      }\n    }\n  }\n}\n\n/** A thick segment is a quad; that is the whole trick for lines and arcs. */\nfunction segmentQuad(\n  x1: number,\n  y1: number,\n  x2: number,\n  y2: number,\n  w: number,\n): [number, number][] {\n  const dx = x2 - x1;\n  const dy = y2 - y1;\n  const len = Math.hypot(dx, dy) || 1;\n  const nx = (-dy / len) * (w / 2);\n  const ny = (dx / len) * (w / 2);\n  return [\n    [x1 + nx, y1 + ny],\n    [x2 + nx, y2 + ny],\n    [x2 - nx, y2 - ny],\n    [x1 - nx, y1 - ny],\n  ];\n}\n\n/**\n * Rasterises the pattern into RGBA pixels without a canvas, so the OG plate\n * a crawler sees is drawn from the same primitives as the live sheet.\n * Deliberately aliased: these are hard-edged geometric marks at plate scale,\n * and anti-aliasing them would cost more than it shows.\n */\nexport function patternPixels(\n  width: number,\n  height: number,\n  state: PatternState,\n  ink: PatternInk,\n): Uint8ClampedArray {\n  const data = new Uint8ClampedArray(width * height * 4);\n  const paper = hexToRgbTriple(ink.paper);\n  for (let i = 0; i < width * height; i++) {\n    const o = i * 4;\n    data[o] = paper[0];\n    data[o + 1] = paper[1];\n    data[o + 2] = paper[2];\n    data[o + 3] = 255;\n  }\n\n  for (const p of buildPattern(width, height, state, ink)) {\n    switch (p.k) {\n      case \"poly\":\n        fillPoly(data, width, height, p.pts, hexToRgbTriple(p.fill));\n        break;\n      case \"line\":\n        fillPoly(\n          data,\n          width,\n          height,\n          segmentQuad(p.x1, p.y1, p.x2, p.y2, p.w),\n          hexToRgbTriple(p.stroke),\n        );\n        break;\n      case \"circle\": {\n        const rgb = hexToRgbTriple(p.fill);\n        const r = Math.max(0.5, p.r);\n        const y0 = Math.max(0, Math.floor(p.y - r));\n        const y1 = Math.min(height - 1, Math.ceil(p.y + r));\n        for (let y = y0; y <= y1; y++) {\n          const dy = y + 0.5 - p.y;\n          const half = Math.sqrt(Math.max(0, r * r - dy * dy));\n          const sx = Math.max(0, Math.round(p.x - half));\n          const ex = Math.min(width - 1, Math.round(p.x + half));\n          for (let x = sx; x <= ex; x++) {\n            const o = (y * width + x) * 4;\n            data[o] = rgb[0];\n            data[o + 1] = rgb[1];\n            data[o + 2] = rgb[2];\n          }\n        }\n        break;\n      }\n      case \"arc\": {\n        // Flatten to a polyline, then stroke each segment as a quad.\n        const rgb = hexToRgbTriple(p.stroke);\n        const steps = Math.max(\n          4,\n          Math.round((Math.abs(p.a1 - p.a0) * p.r) / 3),\n        );\n        let px = p.x + p.r * Math.cos(p.a0);\n        let py = p.y + p.r * Math.sin(p.a0);\n        for (let i = 1; i <= steps; i++) {\n          const a = p.a0 + ((p.a1 - p.a0) * i) / steps;\n          const nx = p.x + p.r * Math.cos(a);\n          const ny = p.y + p.r * Math.sin(a);\n          fillPoly(data, width, height, segmentQuad(px, py, nx, ny, p.w), rgb);\n          px = nx;\n          py = ny;\n        }\n        break;\n      }\n    }\n  }\n  return data;\n}\n",
      "type": "registry:lib",
      "target": "lib/render/patterns.ts"
    }
  ],
  "docs": "Call drawPattern() for canvas, describePattern() for editable SVG, patternPixels() where there is no DOM, and patternToCss() for the families that genuinely tile. Every colour arrives from you, so the patterns take your tokens rather than carrying a palette. When shipping these, include: 'Experience Design System influenced by [Chromologium](https://chromologium.com).' See https://chromologium.com/lab/pattern-press",
  "author": "The Chromologium",
  "homepage": "https://chromologium.com",
  "license": "MIT"
}
