{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ice-logo",
  "type": "registry:lib",
  "title": "Ice Logo",
  "description": "Any SVG logo cut from ice: it crumbles into splinters, chips and powder under the cursor, then pieces itself back together.",
  "dependencies": [
    "d3-delaunay@^6.0.6",
    "polygon-clipping@^0.15.7",
    "three@0.185.1"
  ],
  "files": [
    {
      "path": "registry/ice-logo/default-mark.ts",
      "type": "registry:file",
      "target": "lib/vanilla-motion/ice-logo/default-mark.ts",
      "content": "// Neutral default logo for the public effect: a thick hexagonal ring, like an ice crystal.\n// Client logos are passed in as `svg` (see ../demos/).\nexport const DEFAULT_MARK_SVG =\n  `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\"><path fill-rule=\"evenodd\" d=\"M16 1L28.99 8.5L28.99 23.5L16 31L3.01 23.5L3.01 8.5ZM9.072 12L9.072 20L16 24L22.928 20L22.928 12L16 8Z\"/></svg>`;\n"
    },
    {
      "path": "registry/ice-logo/ice-logo.ts",
      "type": "registry:file",
      "target": "lib/vanilla-motion/ice-logo/ice-logo.ts",
      "content": "import { Delaunay } from \"d3-delaunay\";\nimport polygonClipping from \"polygon-clipping\";\nimport * as THREE from \"three\";\nimport { RoomEnvironment } from \"three/examples/jsm/environments/RoomEnvironment.js\";\nimport { SVGLoader } from \"three/examples/jsm/loaders/SVGLoader.js\";\n\nimport { defaults, LOGO_UNITS, REBUILD_KEYS, type IceLogoParams } from \"./params\";\nimport { DEFAULT_MARK_SVG } from \"./default-mark\";\n\nexport type { IceLogoParams } from \"./params\";\nexport { defaults } from \"./params\";\n\nexport type IceLogoOptions = Partial<IceLogoParams> & {\n  /** SVG markup of the logo. Filled shapes become the ice; holes stay holes. */\n  svg?: string;\n};\n\n/** The public surface wrappers use (Toolcraft canvas, embed). */\nexport interface IceLogoHandle {\n  setParams(next: Partial<IceLogoParams>): void;\n  setLogo(svg: string): void;\n  resize(width: number, height: number): void;\n  shatterAll(): void;\n  restoreNow(): void;\n  dispose(): void;\n}\n\n/** Start the effect on a canvas. Same as `new IceLogo(…)`, typed as the narrow handle. */\nexport function createIceLogo(canvas: HTMLCanvasElement, options: IceLogoOptions = {}): IceLogoHandle {\n  return new IceLogo(canvas, options);\n}\n\n/**\n * Fill an element with the effect: adds a canvas, keeps it sized to the element, and removes it\n * on dispose. Used by the embed page and the embed script; same options as the class.\n */\nexport function mount(container: HTMLElement, options: IceLogoOptions = {}): IceLogoHandle {\n  const canvas = document.createElement(\"canvas\");\n  canvas.style.cssText = \"display:block;width:100%;height:100%;touch-action:none\";\n  container.appendChild(canvas);\n  const fx = new IceLogo(canvas, options);\n  const ro = new ResizeObserver(([entry]) => fx.resize(entry.contentRect.width, entry.contentRect.height));\n  ro.observe(container);\n  return {\n    setParams: (next) => fx.setParams(next),\n    setLogo: (svg) => fx.setLogo(svg),\n    resize: (w, h) => fx.resize(w, h),\n    shatterAll: () => fx.shatterAll(),\n    restoreNow: () => fx.restoreNow(),\n    dispose: () => { ro.disconnect(); fx.dispose(); canvas.remove(); },\n  };\n}\n\n/**\n * Embed auto-init hook (scripts/build-embeds.mjs): options the markup carries beyond the\n * data-* params. An inline <svg> inside the element becomes the logo and is removed.\n */\nexport function optionsFromElement(el: HTMLElement): IceLogoOptions {\n  const svg = el.querySelector(\"svg\");\n  if (!svg) return {};\n  svg.remove();\n  return { svg: svg.outerHTML };\n}\n\ntype Ring = [number, number][];\ntype Polygon = Ring[];\ntype MultiPolygon = Polygon[];\n\ntype Shard = {\n  mesh: THREE.Mesh;\n  restQ: THREE.Quaternion;\n  cell: number;\n  rest: THREE.Vector3;\n  v: THREE.Vector3;\n  w: THREE.Vector3;\n  state: 0 | 1 | 2; // rest, flying, returning\n  t: number;\n};\ntype Cell = { shards: Shard[]; broken: boolean; open: boolean };\n\nconst ringArea = (r: Ring) => { let a = 0; for (let i = 0; i < r.length - 1; i++) a += r[i][0] * r[i + 1][1] - r[i + 1][0] * r[i][1]; return a / 2; };\nconst ringPerimeter = (r: Ring) => { let p = 0; for (let i = 0; i < r.length - 1; i++) p += Math.hypot(r[i + 1][0] - r[i][0], r[i + 1][1] - r[i][1]); return p; };\nfunction ringCentroid(r: Ring): [number, number] {\n  let a = 0, cx = 0, cy = 0;\n  for (let i = 0; i < r.length - 1; i++) {\n    const [x0, y0] = r[i], [x1, y1] = r[i + 1], f = x0 * y1 - x1 * y0;\n    a += f; cx += (x0 + x1) * f; cy += (y0 + y1) * f;\n  }\n  a *= 0.5;\n  return [cx / (6 * a), cy / (6 * a)];\n}\nfunction mulberry32(seed: number) {\n  let a = seed;\n  return () => {\n    a |= 0; a = (a + 0x6d2b79f5) | 0;\n    let t = Math.imul(a ^ (a >>> 15), 1 | a);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nfunction makeFrostTexture() {\n  const s = 512, c = document.createElement(\"canvas\"); c.width = c.height = s;\n  const g = c.getContext(\"2d\")!;\n  g.fillStyle = \"rgb(190,190,190)\"; g.fillRect(0, 0, s, s);\n  for (let i = 0; i < 3200; i++) {\n    const x = Math.random() * s, y = Math.random() * s, r = Math.random() * 12 + 1;\n    const v = Math.random() < 0.5 ? 255 : 0;\n    const gr = g.createRadialGradient(x, y, 0, x, y, r);\n    gr.addColorStop(0, `rgba(${v},${v},${v},${Math.random() * 0.3})`);\n    gr.addColorStop(1, `rgba(${v},${v},${v},0)`);\n    g.fillStyle = gr; g.fillRect(x - r, y - r, r * 2, r * 2);\n  }\n  g.lineWidth = 1;\n  for (let i = 0; i < 110; i++) {\n    g.strokeStyle = `rgba(255,255,255,${Math.random() * 0.4})`;\n    g.beginPath(); let x = Math.random() * s, y = Math.random() * s; g.moveTo(x, y);\n    for (let k = 0; k < 6; k++) { x += (Math.random() - 0.5) * 60; y += (Math.random() - 0.5) * 60; g.lineTo(x, y); }\n    g.stroke();\n  }\n  const t = new THREE.CanvasTexture(c);\n  t.wrapS = t.wrapT = THREE.RepeatWrapping; t.repeat.set(1 / 12, 1 / 12);\n  return t;\n}\n\n// Frost inside and on the ice: patchy, not even. Big cloudy patches leave clear areas between\n// them; fine grain, bubbles and bright hairline cracks sit on top.\nfunction makeCloudTexture() {\n  const s = 1024, c = document.createElement(\"canvas\"); c.width = c.height = s;\n  const g = c.getContext(\"2d\")!;\n  g.fillStyle = \"#000\"; g.fillRect(0, 0, s, s);\n  const blob = (x: number, y: number, r: number, a: number) => {\n    for (const ox of [-s, 0, s]) for (const oy of [-s, 0, s]) { // wrap so the tile has no seams\n      const gr = g.createRadialGradient(x + ox, y + oy, 0, x + ox, y + oy, r);\n      gr.addColorStop(0, `rgba(255,255,255,${a})`); gr.addColorStop(1, \"rgba(255,255,255,0)\");\n      g.fillStyle = gr; g.fillRect(x + ox - r, y + oy - r, r * 2, r * 2);\n    }\n  };\n  for (let i = 0; i < 46; i++) blob(Math.random() * s, Math.random() * s, 70 + Math.random() * 170, 0.12 + Math.random() * 0.32);\n  for (let i = 0; i < 260; i++) blob(Math.random() * s, Math.random() * s, 8 + Math.random() * 30, 0.1 + Math.random() * 0.3);\n  for (let i = 0; i < 9000; i++) { // grain\n    g.fillStyle = `rgba(255,255,255,${0.15 + Math.random() * 0.55})`;\n    const r = 0.5 + Math.pow(Math.random(), 3) * 2.4;\n    g.beginPath(); g.arc(Math.random() * s, Math.random() * s, r, 0, Math.PI * 2); g.fill();\n  }\n  for (let i = 0; i < 420; i++) { // bubbles: bright ring, clear middle\n    const x = Math.random() * s, y = Math.random() * s, r = 1.5 + Math.random() * 5;\n    g.strokeStyle = `rgba(255,255,255,${0.35 + Math.random() * 0.6})`; g.lineWidth = 1 + Math.random();\n    g.beginPath(); g.arc(x, y, r, 0, Math.PI * 2); g.stroke();\n  }\n  for (let i = 0; i < 70; i++) { // hairline cracks\n    g.strokeStyle = `rgba(255,255,255,${0.25 + Math.random() * 0.6})`; g.lineWidth = 1 + Math.random() * 2;\n    g.beginPath(); let x = Math.random() * s, y = Math.random() * s; g.moveTo(x, y);\n    const a = Math.random() * Math.PI * 2;\n    for (let k = 0; k < 4; k++) { x += Math.cos(a + (Math.random() - 0.5) * 0.9) * (25 + Math.random() * 60); y += Math.sin(a + (Math.random() - 0.5) * 0.9) * (25 + Math.random() * 60); g.lineTo(x, y); }\n    g.stroke();\n  }\n  const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace;\n  t.wrapS = t.wrapT = THREE.RepeatWrapping; t.repeat.set(1 / 20, 1 / 20);\n  t.anisotropy = 4;\n  return t;\n}\n\n// frost on a broken chunk: fine white grain, a few cloudy patches and hairline cracks on clear ice\nfunction makeSpeckleTexture() {\n  const s = 256, c = document.createElement(\"canvas\"); c.width = c.height = s;\n  const g = c.getContext(\"2d\")!;\n  g.fillStyle = \"#000\"; g.fillRect(0, 0, s, s);\n  for (let i = 0; i < 26; i++) {\n    const x = Math.random() * s, y = Math.random() * s, r = 10 + Math.random() * 34;\n    const gr = g.createRadialGradient(x, y, 0, x, y, r);\n    gr.addColorStop(0, `rgba(255,255,255,${0.15 + Math.random() * 0.35})`); gr.addColorStop(1, \"rgba(255,255,255,0)\");\n    g.fillStyle = gr; g.fillRect(x - r, y - r, r * 2, r * 2);\n  }\n  for (let i = 0; i < 2600; i++) {\n    g.fillStyle = `rgba(255,255,255,${0.25 + Math.random() * 0.75})`;\n    const r = 0.4 + Math.pow(Math.random(), 3) * 2.2;\n    g.beginPath(); g.arc(Math.random() * s, Math.random() * s, r, 0, Math.PI * 2); g.fill();\n  }\n  for (let i = 0; i < 22; i++) {\n    g.strokeStyle = `rgba(255,255,255,${0.3 + Math.random() * 0.6})`; g.lineWidth = 0.6 + Math.random() * 1.2;\n    g.beginPath(); const x = Math.random() * s, y = Math.random() * s, a = Math.random() * Math.PI, l = 15 + Math.random() * 60;\n    g.moveTo(x, y); g.lineTo(x + Math.cos(a) * l, y + Math.sin(a) * l); g.stroke();\n  }\n  const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace;\n  t.wrapS = t.wrapT = THREE.RepeatWrapping; t.repeat.set(2, 2);\n  return t;\n}\n\nconst DUST = 6000;\nconst CHIPS = 1200;\nconst LOGO_WORLD_W = 4;\n\n/**\n * A logo cut from ice that crumbles under the cursor and pieces itself back together.\n * Plain class: no Toolcraft, no React. `new IceLogo(canvas, { svg })` starts its own render\n * loop and pointer handling; call `resize(w, h)` when the canvas size changes and `dispose()`.\n */\nexport class IceLogo implements IceLogoHandle {\n  /** Same as meta.json \"version\"; bump both when the class or params change. */\n  static readonly version = \"1.0.0\";\n  private P: IceLogoParams;\n  private renderer: THREE.WebGLRenderer;\n  private scene = new THREE.Scene();\n  private camera = new THREE.PerspectiveCamera(30, 16 / 9, 0.1, 100);\n  private pivot = new THREE.Group();\n  private group = new THREE.Group();\n  private hitPlane: THREE.Mesh;\n  private lights: THREE.Light[] = [];\n\n  private frost = makeFrostTexture();\n  private cloud = makeCloudTexture();\n  private speckle = makeSpeckleTexture();\n  private clearMat: THREE.MeshPhysicalMaterial;\n  private intactMat: THREE.MeshPhysicalMaterial;\n  private chipMat: THREE.MeshPhysicalMaterial;\n  private cellUniforms = { uCells: { value: null as THREE.DataTexture | null }, uCount: { value: 0 }, uMetric: { value: new THREE.Matrix3() }, uRim: { value: 0.5 } };\n  private cellTex: THREE.DataTexture | null = null;\n\n  private logo: MultiPolygon = [];\n  private W = 1; private H = 1;\n  private shards: Shard[] = [];\n  private cells: Cell[] = [];\n  private neighbors: number[][] = [];\n  private intact: THREE.Mesh | null = null;\n\n  private dPos = new Float32Array(DUST * 3); private dVel = new Float32Array(DUST * 3);\n  private dAlpha = new Float32Array(DUST); private dSize = new Float32Array(DUST);\n  private dLife = new Float32Array(DUST); private dMax = new Float32Array(DUST);\n  private dGeo = new THREE.BufferGeometry(); private dMat: THREE.ShaderMaterial; private dHead = 0;\n\n  private lumps = [0, 1, 2, 3, 4, 5, 6, 7].map((i) => IceLogo.makeLump(i));\n  private chipMesh: THREE.InstancedMesh;\n  private chips = Array.from({ length: CHIPS }, () => ({ p: new THREE.Vector3(), v: new THREE.Vector3(), q: new THREE.Quaternion(), w: new THREE.Vector3(), s: new THREE.Vector3(), life: 0 }));\n  private chipHead = 0;\n\n  private ray = new THREE.Raycaster(); private ndc = new THREE.Vector2(); private tilt = new THREE.Vector2();\n  private pointerVel = new THREE.Vector2(); private prevLocal: THREE.Vector3 | null = null; private prevT = 0;\n  private lastHit: THREE.Vector3 | null = null; private lastImpactT = -1e9; private lastUserInput = -1e9;\n  private demo = { next: 0, t0: 0, dur: 0, from: new THREE.Vector3(), to: new THREE.Vector3(), active: false, count: 0 };\n  private last = performance.now() / 1000;\n  private tmpQ = new THREE.Quaternion(); private tmpV = new THREE.Vector3(); private tmpS = new THREE.Vector3();\n  private idQ = new THREE.Quaternion(); private chipM = new THREE.Matrix4(); private zeroM = new THREE.Matrix4().makeScale(0, 0, 0);\n  private keyDir = new THREE.Vector3(-3, 5, 6).normalize();\n  private disposed = false;\n\n  constructor(private canvas: HTMLCanvasElement, options: IceLogoOptions = {}) {\n    const { svg = DEFAULT_MARK_SVG, ...params } = options;\n    this.P = { ...defaults, ...params };\n    this.renderer = new THREE.WebGLRenderer({ antialias: true, canvas });\n    this.renderer.toneMapping = THREE.ACESFilmicToneMapping;\n    this.renderer.toneMappingExposure = 1.05;\n    this.scene.background = new THREE.Color(0x0c0c0c);\n    const pmrem = new THREE.PMREMGenerator(this.renderer);\n    this.scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;\n    pmrem.dispose();\n    this.camera.position.set(0, 0, 13);\n\n    const key = new THREE.DirectionalLight(0xffffff, 2.2); key.position.set(-3, 5, 6);\n    const rim = new THREE.DirectionalLight(0xdfe8ff, 3.2); rim.position.set(4, -2, -5);\n    const fill = new THREE.DirectionalLight(0xffffff, 0.6); fill.position.set(5, 2, 4);\n    this.lights = [key, rim, fill];\n    this.scene.add(key, rim, fill);\n\n    // soft glow behind the logo so the ice has something to bend\n    const bc = document.createElement(\"canvas\"); bc.width = bc.height = 512;\n    const bg = bc.getContext(\"2d\")!;\n    const grd = bg.createRadialGradient(256, 256, 10, 256, 256, 256);\n    grd.addColorStop(0, \"#2a2a2a\"); grd.addColorStop(1, \"#0c0c0c\");\n    bg.fillStyle = grd; bg.fillRect(0, 0, 512, 512);\n    const bt = new THREE.CanvasTexture(bc); bt.colorSpace = THREE.SRGBColorSpace;\n    const backdrop = new THREE.Mesh(new THREE.PlaneGeometry(40, 40), new THREE.MeshBasicMaterial({ map: bt }));\n    backdrop.position.z = -6; this.scene.add(backdrop);\n\n    this.clearMat = this.makeIce(true);\n    this.intactMat = this.makeIce(false);\n    this.chipMat = this.makeIce(true);\n    for (const m of [this.clearMat, this.chipMat]) { m.emissiveMap = this.speckle; m.bumpMap = this.speckle; m.roughnessMap = null; }\n    // (the whole logo stays single-sided: drawing its inside walls too halves the frame rate)\n    this.intactMat.onBeforeCompile = (sh) => {\n      Object.assign(sh.uniforms, this.cellUniforms);\n      sh.vertexShader = sh.vertexShader\n        .replace(\"#include <common>\", \"#include <common>\\nvarying vec3 vLocal;\")\n        .replace(\"#include <begin_vertex>\", \"#include <begin_vertex>\\nvLocal = position;\");\n      sh.fragmentShader = sh.fragmentShader\n        .replace(\"#include <common>\", \"#include <common>\\nvarying vec3 vLocal; uniform highp sampler2D uCells; uniform int uCount; uniform mat3 uMetric; uniform float uRim;\")\n        .replace(\"void main() {\", `void main() {\n          float best = 1e9; float open = 0.;\n          vec2 pw = vLocal.xy + vec2(sin(vLocal.y * 2.9 + vLocal.z * 1.7) + .6 * sin(vLocal.y * 7.1 + vLocal.x * 3.3), sin(vLocal.x * 3.1 - vLocal.z * 2.1) + .6 * sin(vLocal.x * 6.7 - vLocal.y * 2.7)) * .28;\n          vec2 q = (uMetric * vec3(pw, 1.)).xy;\n          for (int i = 0; i < 1024; i++) {\n            if (i >= uCount) break;\n            vec4 c = texelFetch(uCells, ivec2(i, 0), 0);\n            vec2 d = q - c.xy; float dd = dot(d, d);\n            if (dd < best) { best = dd; open = c.z; }\n          }\n          if (open > .5) discard;`)\n        // frost gathers on the rounded edges: white where the surface turns away from the eye, clear face-on\n        .replace(\"#include <emissivemap_fragment>\", `#include <emissivemap_fragment>\n          float rimF = pow(1. - abs(dot(normalize(vNormal), normalize(vViewPosition))), 2.2);\n          totalEmissiveRadiance += vec3(rimF * uRim);`);\n    };\n\n    this.scene.add(this.pivot); this.pivot.add(this.group);\n    this.hitPlane = new THREE.Mesh(new THREE.PlaneGeometry(400, 400), new THREE.MeshBasicMaterial({ visible: false }));\n    this.group.add(this.hitPlane);\n\n    // dust\n    this.dGeo.setAttribute(\"position\", new THREE.BufferAttribute(this.dPos, 3).setUsage(THREE.DynamicDrawUsage));\n    this.dGeo.setAttribute(\"aAlpha\", new THREE.BufferAttribute(this.dAlpha, 1).setUsage(THREE.DynamicDrawUsage));\n    this.dGeo.setAttribute(\"aSize\", new THREE.BufferAttribute(this.dSize, 1));\n    this.dMat = new THREE.ShaderMaterial({\n      transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,\n      uniforms: { uScale: { value: 1 } },\n      vertexShader: `attribute float aAlpha; attribute float aSize; uniform float uScale; varying float vA;\n        void main(){ vec4 mv = modelViewMatrix * vec4(position,1.); gl_Position = projectionMatrix * mv;\n          gl_PointSize = aSize * uScale / -mv.z; vA = aAlpha; }`,\n      fragmentShader: `varying float vA;\n        void main(){ float d = length(gl_PointCoord - .5); if (d > .5) discard;\n          float a = smoothstep(.5, .05, d) * vA; gl_FragColor = vec4(vec3(.92,.95,1.) * a, a); }`,\n    });\n    const dust = new THREE.Points(this.dGeo, this.dMat); dust.frustumCulled = false; this.group.add(dust);\n\n    // chips: small lumps of crushed ice\n    const chipGeo = this.lumps[0];\n    this.chipMesh = new THREE.InstancedMesh(chipGeo, this.chipMat, CHIPS);\n    this.chipMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); this.chipMesh.frustumCulled = false;\n    for (let i = 0; i < CHIPS; i++) this.chipMesh.setMatrixAt(i, this.zeroM);\n    this.group.add(this.chipMesh);\n\n    this.parseLogo(svg);\n    this.group.scale.setScalar(LOGO_WORLD_W / LOGO_UNITS);\n    this.build();\n    this.applyMaterial();\n\n    canvas.addEventListener(\"pointermove\", this.onMove);\n    canvas.addEventListener(\"pointerleave\", this.onLeave);\n    this.renderer.setAnimationLoop(this.frame);\n  }\n\n  /** A jagged chunk of broken ice: few, flat, sharp-edged facets (not a rounded, melted lump). */\n  private static makeLump(index: number) {\n    const g = index % 3 === 0 ? new THREE.OctahedronGeometry(1, 0)\n      : index % 3 === 1 ? new THREE.DodecahedronGeometry(1, 0) : new THREE.IcosahedronGeometry(1, 0);\n    const pos = g.attributes.position, seen = new Map<string, number>();\n    for (let i = 0; i < pos.count; i++) {\n      const k = `${pos.getX(i).toFixed(3)},${pos.getY(i).toFixed(3)},${pos.getZ(i).toFixed(3)}`;\n      if (!seen.has(k)) seen.set(k, 0.4 + Math.random() * 1.0);\n      const f = seen.get(k)!; pos.setXYZ(i, pos.getX(i) * f, pos.getY(i) * f, pos.getZ(i) * f);\n    }\n    g.computeVertexNormals(); // not merged, so every facet stays flat and its edges stay sharp\n    return g;\n  }\n\n  private makeIce(flat: boolean) {\n    return new THREE.MeshPhysicalMaterial({\n      color: 0xffffff, metalness: 0, attenuationColor: new THREE.Color(0xf2f6ff), attenuationDistance: 14,\n      clearcoat: 1, clearcoatRoughness: 0.08, roughnessMap: this.frost, bumpMap: this.frost,\n      emissive: 0xffffff, emissiveMap: this.cloud, specularIntensity: 1, flatShading: flat,\n    });\n  }\n\n  private applyMaterial() {\n    const P = this.P;\n    for (const m of [this.intactMat]) {\n      m.transmission = P.transmission; m.roughness = P.roughness; m.thickness = P.thickness; m.ior = P.ior;\n      m.bumpScale = P.frost; m.emissiveIntensity = P.milk; m.envMapIntensity = P.light;\n    }\n    // Freshly broken ice is clear and glossy inside; only the outer skin is frosted.\n    const f = P.shardFrost;\n    for (const m of [this.clearMat, this.chipMat]) {\n      m.transmission = 1 - 0.6 * f; m.roughness = 0.03 + 0.3 * f; m.thickness = 1.5; m.ior = P.ior;\n      m.bumpScale = P.frost * 0.5; m.emissiveIntensity = 0.5 + f * 2.5; m.envMapIntensity = P.light * 1.6;\n    }\n    const base = [2.2, 3.2, 0.6];\n    this.lights.forEach((l, i) => { l.intensity = base[i] * (P.light / 2.6); });\n  }\n\n  setParams(next: Partial<IceLogoParams>) {\n    const prev = this.P;\n    this.P = { ...prev, ...next };\n    this.applyMaterial();\n    if (REBUILD_KEYS.some((k) => prev[k] !== this.P[k])) this.build();\n  }\n\n  /** Swap the logo for another SVG and rebuild. */\n  setLogo(svg: string) {\n    this.parseLogo(svg);\n    this.build();\n  }\n\n  resize(width: number, height: number) {\n    if (width <= 0 || height <= 0) return;\n    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n    this.renderer.setSize(width, height, false);\n    this.camera.aspect = width / height;\n    const fit = LOGO_WORLD_W / (0.5 * this.camera.aspect) / (2 * Math.tan(THREE.MathUtils.degToRad(15)));\n    this.camera.position.z = Math.max(13, fit);\n    this.camera.updateProjectionMatrix();\n    this.dMat.uniforms.uScale.value = this.renderer.getPixelRatio() * height * 0.5 * this.camera.projectionMatrix.elements[5];\n  }\n\n  shatterAll() { this.impact(new THREE.Vector3(0, 0, 0), Math.max(this.W, this.H), this.P.force * 1.3, new THREE.Vector2(), false); }\n  restoreNow() { for (const s of this.shards) if (s.state === 1) s.t = Math.random() * 0.25; }\n\n  dispose() {\n    this.disposed = true;\n    this.renderer.setAnimationLoop(null);\n    this.canvas.removeEventListener(\"pointermove\", this.onMove);\n    this.canvas.removeEventListener(\"pointerleave\", this.onLeave);\n    this.intact?.geometry.dispose();\n    this.dGeo.dispose(); this.dMat.dispose();\n    for (const m of [this.clearMat, this.intactMat, this.chipMat]) m.dispose();\n    for (const g of this.lumps) g.dispose();\n    this.frost.dispose(); this.cloud.dispose(); this.speckle.dispose(); this.cellTex?.dispose();\n    this.renderer.dispose();\n  }\n\n  // ---------------------------------------------------------------- geometry\n  private parseLogo(svgMarkup: string) {\n    const svg = new SVGLoader().parse(svgMarkup);\n    const raw: Polygon[] = [];\n    for (const path of svg.paths) for (const shape of SVGLoader.createShapes(path)) {\n      const { shape: outer, holes } = shape.extractPoints(16);\n      if (outer.length < 3) continue;\n      const ring = (pts: THREE.Vector2[]): Ring => { const a = pts.map((p) => [p.x, -p.y] as [number, number]); a.push([a[0][0], a[0][1]]); return a; };\n      raw.push([ring(outer), ...holes.filter((h) => h.length >= 3).map(ring)]);\n    }\n    if (!raw.length) throw new Error(\"IceLogo: the SVG has no filled shapes to build the logo from.\");\n    let logo = polygonClipping.union(raw[0], ...raw.slice(1)) as MultiPolygon;\n    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n    for (const poly of logo) for (const r of poly) for (const [x, y] of r) {\n      minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y);\n    }\n    // Centre it and scale its larger side to LOGO_UNITS, so every value means the same for any SVG.\n    const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2, k = LOGO_UNITS / Math.max(maxX - minX, maxY - minY);\n    logo = logo.map((poly) => poly.map((r) => r.map(([x, y]) => [(x - cx) * k, (y - cy) * k] as [number, number])));\n    this.logo = logo; this.W = (maxX - minX) * k; this.H = (maxY - minY) * k;\n  }\n\n  private insideLogo(x: number, y: number) {\n    let inside = false;\n    for (const poly of this.logo) for (const r of poly) {\n      for (let i = 0, j = r.length - 1; i < r.length; j = i++) {\n        const [xi, yi] = r[i], [xj, yj] = r[j];\n        if ((yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside;\n      }\n    }\n    return inside;\n  }\n\n  private build() {\n    const P = this.P;\n    for (const s of this.shards) this.group.remove(s.mesh);\n    if (this.intact) { this.group.remove(this.intact); this.intact.geometry.dispose(); }\n    this.shards = []; this.cells = [];\n    const rng = mulberry32(7);\n    const minX = -this.W / 2, minY = -this.H / 2;\n    const count = Math.max(8, Math.min(1000, Math.round(P.cells)));\n    const pts: [number, number][] = []; let guard = 0;\n    const centers: [number, number][] = [];\n    while (centers.length < 16 && guard++ < 200000) {\n      const x = minX + rng() * this.W, y = minY + rng() * this.H;\n      if (this.insideLogo(x, y)) centers.push([x, y]);\n    }\n    while (pts.length < count && guard++ < 200000) {\n      let x: number, y: number;\n      if (rng() < 0.5) { x = minX + rng() * this.W; y = minY + rng() * this.H; } else {\n        const c = centers[Math.floor(rng() * centers.length)], a = rng() * Math.PI * 2, r = Math.pow(rng(), 1.6) * this.W * 0.09;\n        x = c[0] + Math.cos(a) * r; y = c[1] + Math.sin(a) * r;\n      }\n      if (this.insideLogo(x, y)) pts.push([x, y]);\n    }\n    // Stretched Voronoi: cells become long splinters that run along the strokes of the mark,\n    // the way ice splits, instead of round pebbles. q = S * R(-theta) * p.\n    const theta = -Math.PI / 4, kS = Math.max(1, P.splinter), ct = Math.cos(theta), st = Math.sin(theta);\n    const toQ = ([x, y]: [number, number]): [number, number] => [ct * x + st * y, (-st * x + ct * y) * kS];\n    const toP = ([u, v]: [number, number]): [number, number] => [ct * u - st * (v / kS), st * u + ct * (v / kS)];\n    this.cellUniforms.uMetric.value.set(ct, st, 0, -st * kS, ct * kS, 0, 0, 0, 1);\n    const qPts = pts.map(toQ);\n    const corners = ([[minX - 1, minY - 1], [-minX + 1, minY - 1], [-minX + 1, -minY + 1], [minX - 1, -minY + 1]] as [number, number][]).map(toQ);\n    const del = Delaunay.from(qPts);\n    const vor = del.voronoi([\n      Math.min(...corners.map((c) => c[0])), Math.min(...corners.map((c) => c[1])),\n      Math.max(...corners.map((c) => c[0])), Math.max(...corners.map((c) => c[1])),\n    ]);\n    this.neighbors = pts.map((_, i) => [...del.neighbors(i)]);\n    const total = P.depth + 2 * P.bevel; // full thickness of the whole logo\n    const layers = Math.max(1, Math.min(4, Math.round(P.layers)));\n    for (let i = 0; i < pts.length; i++) {\n      this.cells.push({ shards: [], broken: false, open: false });\n      const cellQ = vor.cellPolygon(i); if (!cellQ) continue;\n      const cell = (cellQ as Ring).map(toP);\n      const pieces = polygonClipping.intersection([cell], this.logo) as MultiPolygon;\n      for (const poly of pieces) {\n        // cut the cell through its thickness at jittered heights so pieces are lumps, not slabs\n        const cuts = [0];\n        for (let l = 1; l < layers; l++) cuts.push((l + (rng() - 0.5) * 0.5) / layers);\n        cuts.push(1);\n        for (let l = 0; l < layers; l++) this.addShard(poly, i, -total / 2 + cuts[l] * total, -total / 2 + cuts[l + 1] * total);\n      }\n    }\n    const data = new Float32Array(pts.length * 4);\n    qPts.forEach(([x, y], i) => { data[i * 4] = x; data[i * 4 + 1] = y; });\n    this.cellTex?.dispose();\n    this.cellTex = new THREE.DataTexture(data, pts.length, 1, THREE.RGBAFormat, THREE.FloatType);\n    this.cellTex.minFilter = this.cellTex.magFilter = THREE.NearestFilter; this.cellTex.needsUpdate = true;\n    this.cellUniforms.uCells.value = this.cellTex; this.cellUniforms.uCount.value = pts.length;\n\n    const shapes = this.logo.map((poly) => {\n      const s = new THREE.Shape(poly[0].slice(0, -1).map(([x, y]) => new THREE.Vector2(x, y)));\n      for (const h of poly.slice(1)) s.holes.push(new THREE.Path(h.slice(0, -1).map(([x, y]) => new THREE.Vector2(x, y))));\n      return s;\n    });\n    const g = new THREE.ExtrudeGeometry(shapes, {\n      depth: P.depth, curveSegments: 24,\n      bevelEnabled: P.bevel > 0, bevelThickness: P.bevel, bevelSize: P.bevel, bevelOffset: -P.bevel, bevelSegments: 8,\n    });\n    g.translate(0, 0, -P.depth / 2);\n    this.intact = new THREE.Mesh(g, this.intactMat);\n    this.group.add(this.intact);\n  }\n\n  private addShard(poly: Polygon, cellIndex: number, z0: number, z1: number) {\n    const outer = poly[0], area = Math.abs(ringArea(outer));\n    if (area < 0.03) return;\n    const [cx, cy] = ringCentroid(outer);\n    // The loose piece for this part of the cell is a nugget of crushed ice, about the size of the cell.\n    const size = Math.min(Math.sqrt(area) * 0.7, (z1 - z0) * 0.65) * (0.45 + Math.pow(Math.random(), 1.5) * 1.1);\n    const mesh = new THREE.Mesh(this.lumps[Math.floor(Math.random() * this.lumps.length)], this.clearMat);\n    mesh.scale.set(size * (0.9 + Math.random() * 0.7), size * (0.7 + Math.random() * 0.6), size * (0.3 + Math.random() * 0.45)); // chunky plates\n    const restQ = new THREE.Quaternion().random();\n    const cz = (z0 + z1) / 2 + (Math.random() - 0.5) * (z1 - z0) * 0.3;\n    mesh.position.set(cx, cy, cz); mesh.quaternion.copy(restQ); mesh.visible = false;\n    this.group.add(mesh);\n    const s: Shard = { mesh, restQ, cell: cellIndex, rest: new THREE.Vector3(cx, cy, cz), v: new THREE.Vector3(), w: new THREE.Vector3(), state: 0, t: 0 };\n    this.shards.push(s); this.cells[cellIndex].shards.push(s);\n  }\n\n  private syncCells() {\n    if (!this.cellTex) return;\n    for (const c of this.cells) c.broken = c.shards.some((s) => s.state !== 0);\n    let changed = false;\n    const data = this.cellTex.image.data as Float32Array;\n    this.cells.forEach((c, i) => {\n      const open = c.broken;\n      if (open !== c.open) { c.open = open; changed = true; data[i * 4 + 2] = open ? 1 : 0; }\n      for (const s of c.shards) s.mesh.visible = open;\n    });\n    if (changed) this.cellTex.needsUpdate = true;\n  }\n\n  // ---------------------------------------------------------------- break / restore\n  private impact(p: THREE.Vector3, R: number, F: number, pv: THREE.Vector2, limited = true) {\n    const P = this.P; let n = 0;\n    // Toughness: the reach is measured in 3D from the touched point, so a touch chips the\n    // surface instead of punching through, and only `maxBroken` % of the logo can be loose at once.\n    let budget = Infinity;\n    if (limited) {\n      let loose = 0; for (const s of this.shards) if (s.state !== 0) loose++;\n      budget = Math.floor((P.maxBroken / 100) * this.shards.length) - loose;\n      if (budget <= 0) return;\n    }\n    const hits: { s: Shard; d: number }[] = [];\n    for (const s of this.shards) {\n      if (s.state === 1) continue;\n      // depth counts for little, so a touch breaks through the thickness and leaves a real hole\n      const d = limited\n        ? Math.hypot(s.mesh.position.x - p.x, s.mesh.position.y - p.y, (s.mesh.position.z - p.z) * 0.22)\n        : Math.hypot(s.mesh.position.x - p.x, s.mesh.position.y - p.y);\n      if (d <= R) hits.push({ s, d });\n    }\n    hits.sort((a, b) => a.d - b.d);\n    for (const { s, d } of hits) {\n      if (n >= budget) break;\n      const dx = s.mesh.position.x - p.x, dy = s.mesh.position.y - p.y;\n      const k = Math.pow(1 - d / R, 0.6);\n      let nx = dx, ny = dy;\n      const d2 = Math.hypot(dx, dy);\n      if (d2 < 1e-3) { const a = Math.random() * Math.PI * 2; nx = Math.cos(a); ny = Math.sin(a); } else { nx /= d2; ny /= d2; }\n      const fling = Math.random() < 0.25 ? 1.8 + Math.random() * 2 : 0.25 + Math.random() * 0.9; // a quarter are thrown further\n      const sp = F * k * fling;\n      s.v.set(nx * sp, ny * sp, F * k * (Math.random() - 0.4) * 1.1 + Math.sign(s.rest.z) * F * 0.25 * Math.random());\n      s.v.x += THREE.MathUtils.clamp(pv.x * 0.1, -14, 14) * k;\n      s.v.y += THREE.MathUtils.clamp(pv.y * 0.1, -14, 14) * k;\n      s.w.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).multiplyScalar(2 * P.spin * (0.4 + k));\n      s.state = 1;\n      s.t = P.restoreDelay + (d / R) * 0.2 + Math.random() * P.restoreStagger;\n      n++;\n    }\n    if (n) { this.spawnDust(p, R, n); this.spawnChips(p, R, n); }\n  }\n\n  private stepShards(dt: number) {\n    const P = this.P, k = P.stiffness, c = 2 * Math.sqrt(k) * P.damping, rotRate = Math.sqrt(k) * 1.1;\n    for (const s of this.shards) {\n      if (s.state === 0) continue;\n      const m = s.mesh;\n      if (s.state === 1) {\n        s.v.y -= P.gravity * dt;\n        s.v.multiplyScalar(Math.exp(-P.drag * dt));\n        m.position.addScaledVector(s.v, dt);\n        const ang = s.w.length();\n        if (ang > 1e-5) { this.tmpQ.setFromAxisAngle(this.tmpV.copy(s.w).divideScalar(ang), ang * dt); m.quaternion.premultiply(this.tmpQ); }\n        s.w.multiplyScalar(Math.exp(-0.6 * dt));\n        if ((s.t -= dt) <= 0) s.state = 2;\n      } else {\n        this.tmpV.subVectors(s.rest, m.position).multiplyScalar(k).addScaledVector(s.v, -c);\n        s.v.addScaledVector(this.tmpV, dt);\n        m.position.addScaledVector(s.v, dt);\n        m.quaternion.slerp(s.restQ, 1 - Math.exp(-rotRate * dt));\n        if (m.position.distanceToSquared(s.rest) < 2.5e-5 && s.v.lengthSq() < 2.5e-3 && m.quaternion.angleTo(s.restQ) < 0.01) {\n          m.position.copy(s.rest); m.quaternion.copy(s.restQ); s.v.set(0, 0, 0); s.w.set(0, 0, 0); s.state = 0;\n        }\n      }\n    }\n  }\n\n  private spawnDust(p: THREE.Vector3, R: number, n: number) {\n    const P = this.P, count = Math.min(1200, Math.round(P.dust * (0.25 + n * 0.06)));\n    for (let k = 0; k < count; k++) {\n      const i = this.dHead; this.dHead = (this.dHead + 1) % DUST;\n      const a = Math.random() * Math.PI * 2, r = Math.sqrt(Math.random()) * R * 0.8;\n      this.dPos[i * 3] = p.x + Math.cos(a) * r; this.dPos[i * 3 + 1] = p.y + Math.sin(a) * r; this.dPos[i * 3 + 2] = (Math.random() - 0.5) * P.depth;\n      const sp = 2 + Math.pow(Math.random(), 1.5) * P.force * 2.8;\n      this.dVel[i * 3] = Math.cos(a) * sp; this.dVel[i * 3 + 1] = Math.sin(a) * sp; this.dVel[i * 3 + 2] = (Math.random() - 0.3) * P.force;\n      this.dMax[i] = this.dLife[i] = 0.8 + Math.random() * 2.2;\n      this.dSize[i] = 0.006 + Math.pow(Math.random(), 3) * 0.05;\n    }\n    this.dGeo.attributes.aSize.needsUpdate = true;\n  }\n\n  private updateDust(dt: number) {\n    const g = this.P.gravity * 0.5, drag = Math.exp(-1.5 * dt);\n    for (let i = 0; i < DUST; i++) {\n      if (this.dLife[i] <= 0) { this.dAlpha[i] = 0; continue; }\n      this.dLife[i] -= dt;\n      this.dVel[i * 3] *= drag; this.dVel[i * 3 + 1] = this.dVel[i * 3 + 1] * drag - g * dt; this.dVel[i * 3 + 2] *= drag;\n      this.dPos[i * 3] += this.dVel[i * 3] * dt; this.dPos[i * 3 + 1] += this.dVel[i * 3 + 1] * dt; this.dPos[i * 3 + 2] += this.dVel[i * 3 + 2] * dt;\n      this.dAlpha[i] = Math.min(1, (Math.max(this.dLife[i], 0) / this.dMax[i]) * 2.5) * 0.9;\n    }\n    this.dGeo.attributes.position.needsUpdate = true;\n    this.dGeo.attributes.aAlpha.needsUpdate = true;\n  }\n\n  private spawnChips(p: THREE.Vector3, R: number, n: number) {\n    const P = this.P, count = Math.min(260, Math.round(P.chips * n * 0.35));\n    for (let k = 0; k < count; k++) {\n      const c = this.chips[this.chipHead]; this.chipHead = (this.chipHead + 1) % CHIPS;\n      const a = Math.random() * Math.PI * 2, r = Math.sqrt(Math.random()) * R * 0.8;\n      c.p.set(p.x + Math.cos(a) * r, p.y + Math.sin(a) * r, (Math.random() - 0.5) * P.depth);\n      const sp = (0.2 + Math.pow(Math.random(), 1.4) * 1.7) * P.force;\n      c.v.set(Math.cos(a) * sp, Math.sin(a) * sp, (Math.random() - 0.3) * P.force);\n      c.q.random(); c.w.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).multiplyScalar(14);\n      const sz = 0.05 + Math.pow(Math.random(), 2.5) * 0.34;\n      c.s.set(sz * (0.7 + Math.random() * 0.9), sz * (0.5 + Math.random() * 0.7), sz * (0.25 + Math.random() * 0.45));\n      c.life = 1.2 + Math.random() * 2.2;\n    }\n  }\n\n  private updateChips(dt: number) {\n    const P = this.P;\n    for (let i = 0; i < CHIPS; i++) {\n      const c = this.chips[i];\n      if (c.life <= 0) continue;\n      c.life -= dt;\n      if (c.life <= 0) { this.chipMesh.setMatrixAt(i, this.zeroM); continue; }\n      c.v.multiplyScalar(Math.exp(-P.drag * 0.65 * dt)); c.v.y -= P.gravity * 1.2 * dt;\n      c.p.addScaledVector(c.v, dt);\n      const ang = c.w.length();\n      this.tmpQ.setFromAxisAngle(this.tmpV.copy(c.w).divideScalar(ang), ang * dt); c.q.premultiply(this.tmpQ);\n      c.w.multiplyScalar(Math.exp(-0.8 * dt));\n      this.chipM.compose(c.p, c.q, this.tmpS.copy(c.s).multiplyScalar(Math.min(1, c.life * 2.5)));\n      this.chipMesh.setMatrixAt(i, this.chipM);\n    }\n    this.chipMesh.instanceMatrix.needsUpdate = true;\n  }\n\n  // ---------------------------------------------------------------- pointer + autoBreak\n  private localPoint(ndc: THREE.Vector2) {\n    this.ray.setFromCamera(ndc, this.camera);\n    const h = this.intact ? this.ray.intersectObject(this.intact)[0] : undefined;\n    return h ? this.group.worldToLocal(h.point.clone()) : null;\n  }\n  private moveTo(p: THREE.Vector3, t: number) {\n    if (this.prevLocal) { const dt = Math.max(t - this.prevT, 1 / 120); this.pointerVel.set((p.x - this.prevLocal.x) / dt, (p.y - this.prevLocal.y) / dt); }\n    this.prevLocal = p.clone(); this.prevT = t;\n    if (t - this.lastImpactT < this.P.cooldown) return;\n    if (!this.lastHit || this.lastHit.distanceTo(p) > this.P.reach * 0.5) { this.impact(p, this.P.reach, this.P.force, this.pointerVel); this.lastHit = p.clone(); this.lastImpactT = t; }\n  }\n  private setNdc(e: PointerEvent) {\n    const r = this.canvas.getBoundingClientRect();\n    this.ndc.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);\n    this.tilt.copy(this.ndc);\n  }\n  private onMove = (e: PointerEvent) => {\n    this.setNdc(e); this.lastUserInput = performance.now() / 1000;\n    const p = this.localPoint(this.ndc);\n    if (p) this.moveTo(p, this.lastUserInput);\n    else { this.prevLocal = null; this.lastHit = null; } // off the logo: nothing breaks\n  };\n  private onLeave = () => { this.prevLocal = null; this.lastHit = null; this.tilt.set(0, 0); };\n\n  private updateDemo(t: number) {\n    const d = this.demo;\n    if (!this.P.autoBreak || t - this.lastUserInput < 3) { d.active = false; return; }\n    if (!d.active && t > d.next) {\n      d.count++;\n      if (d.count % 5 === 0) { this.shatterAll(); d.next = t + 5; return; }\n      const y0 = (Math.random() - 0.5) * this.H * 0.8, y1 = (Math.random() - 0.5) * this.H * 0.8, dir = Math.random() < 0.5 ? 1 : -1;\n      const zf = this.P.depth / 2 + this.P.bevel;\n      d.from.set(-dir * this.W * 0.7, y0, zf); d.to.set(dir * this.W * (0.1 + Math.random() * 0.6), y1, zf);\n      d.t0 = t; d.dur = 1.1 + Math.random() * 0.7; d.active = true; this.prevLocal = null; this.lastHit = null;\n    }\n    if (d.active) {\n      const u = Math.min((t - d.t0) / d.dur, 1);\n      this.moveTo(this.tmpV.lerpVectors(d.from, d.to, u).clone(), t);\n      if (u >= 1) { d.active = false; d.next = t + 3.4; this.prevLocal = null; this.lastHit = null; }\n    }\n  }\n\n  private frame = () => {\n    if (this.disposed) return;\n    const t = performance.now() / 1000;\n    const dt = Math.min(t - this.last, 1 / 30); this.last = t;\n    this.updateDemo(t);\n    const sub = Math.max(1, Math.ceil(dt / (1 / 120)));\n    for (let i = 0; i < sub; i++) this.stepShards(dt / sub);\n    this.updateChips(dt); this.updateDust(dt); this.syncCells();\n\n    // slow tumble, as in the reference, plus a small lean toward the cursor\n    const P = this.P, ts = t * P.swaySpeed;\n    const ry = Math.sin(ts) * P.sway + this.tilt.x * P.follow;\n    const rx = Math.sin(ts * 0.73 + 1.3) * P.sway * 0.45 - this.tilt.y * P.follow;\n    const rz = Math.sin(ts * 0.41 + 0.6) * P.sway * 0.18;\n    const ease = 1 - Math.exp(-3 * dt);\n    this.pivot.rotation.y += (ry - this.pivot.rotation.y) * ease;\n    this.pivot.rotation.x += (rx - this.pivot.rotation.x) * ease;\n    this.pivot.rotation.z += (rz - this.pivot.rotation.z) * ease;\n\n    // The face frosts white when it turns toward the key light (top left) and clears when it\n    // turns away, so the ice changes between cloudy and see-through as the cursor moves.\n    const facing = this.tmpV.set(0, 0, 1).applyEuler(this.pivot.rotation).dot(this.keyDir);\n    const lit = THREE.MathUtils.smoothstep(facing, 0.5, 0.9);\n    const shift = P.frostShift, m = this.intactMat;\n    m.emissiveIntensity = P.milk * (1 + shift * (lit * 0.9 - 0.55));\n    this.cellUniforms.uRim.value = P.rim * (1 + shift * (lit * 0.8 - 0.4));\n    m.transmission = THREE.MathUtils.clamp(P.transmission + shift * (0.6 - lit) * 0.16, 0, 1);\n    m.roughness = THREE.MathUtils.clamp(P.roughness + shift * (lit - 0.5) * 0.3, 0, 1);\n\n    this.renderer.render(this.scene, this.camera);\n  };\n}\n"
    },
    {
      "path": "registry/ice-logo/params.ts",
      "type": "registry:file",
      "target": "lib/vanilla-motion/ice-logo/params.ts",
      "content": "// The one definition of every Ice Logo value. The class merges options over `defaults`,\n// ice-logo-controls.ts builds the Toolcraft panel from the same keys, and the embed reads\n// the same keys as kebab-case data attributes (data-max-broken, data-auto-break…).\n//\n// Lengths (depth, bevel, reach…) are in \"logo units\": every SVG is scaled so its larger side\n// is LOGO_UNITS long before anything is built, so the values work for any logo.\n\nexport type IceLogoParams = {\n  // shape (rebuilds the geometry)\n  cells: number;\n  layers: number;\n  depth: number;\n  bevel: number;\n  splinter: number;\n  // break\n  reach: number;\n  force: number;\n  spin: number;\n  gravity: number;\n  drag: number;\n  dust: number;\n  chips: number;\n  maxBroken: number;\n  cooldown: number;\n  // restore\n  restoreDelay: number;\n  restoreStagger: number;\n  stiffness: number;\n  damping: number;\n  // ice\n  transmission: number;\n  roughness: number;\n  thickness: number;\n  ior: number;\n  frost: number;\n  milk: number;\n  light: number;\n  shardFrost: number;\n  frostShift: number;\n  rim: number;\n  // motion\n  sway: number;\n  swaySpeed: number;\n  follow: number;\n  autoBreak: boolean;\n};\n\n/** Tuned on the Hola mark (Hola repo, 418bf86). */\nexport const defaults: IceLogoParams = {\n  cells: 520, layers: 3, depth: 9, bevel: 1.2, splinter: 1,\n  reach: 3, maxBroken: 30, cooldown: 0.12, force: 12, spin: 5, gravity: 2.2, drag: 2, dust: 700, chips: 10,\n  restoreDelay: 1.2, restoreStagger: 1, stiffness: 45, damping: 0.85,\n  transmission: 0.96, roughness: 0.26, thickness: 7, ior: 1.31, frost: 2.4, milk: 0.75, rim: 0.55, light: 2.6, shardFrost: 0.2, frostShift: 0.85,\n  sway: 0, swaySpeed: 0.35, follow: 0.45, autoBreak: false,\n};\n\n/** Changing any of these rebuilds the geometry. */\nexport const REBUILD_KEYS: readonly (keyof IceLogoParams)[] = [\"cells\", \"layers\", \"depth\", \"bevel\", \"splinter\"];\n\n/**\n * Size of the logo's larger side after normalising, in logo units. 28.5 is about the Hola mark's\n * height (its larger side), so the tuned values above keep their look.\n */\nexport const LOGO_UNITS = 28.5;\n"
    }
  ],
  "meta": {
    "version": "1.0.0",
    "entry": "ice-logo.ts",
    "docs": "https://vanilla-motion.com/ice-logo/skill.md"
  }
}
