{"$schema":"https://ui.shadcn.com/schema/registry.json","author":"Kaiyu Hsu <uicapsule@kyh.io>","categories":["effects","geometric"],"dependencies":["three"],"description":"Noise-displaced particle sphere with additive-blended dots and custom vertex shader","devDependencies":["@types/three"],"files":[{"content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport * as THREE from \"three\";\n\nconst webGlNoise = `\nvec3 mod289(vec3 x) {\n  return x - floor(x * (1.0 / 289.0)) * 289.0;\n}\n\nvec4 mod289(vec4 x) {\n  return x - floor(x * (1.0 / 289.0)) * 289.0;\n}\n\nvec4 permute(vec4 x) {\n  return mod289(((x*34.0)+10.0)*x);\n}\n\nvec4 taylorInvSqrt(vec4 r)\n{\n  return 1.79284291400159 - 0.85373472095314 * r;\n}\n\n  float snoise(vec3 v)\n  {\n  const vec2  C = vec2(1.0/6.0, 1.0/3.0) ;\n  const vec4  D = vec4(0.0, 0.5, 1.0, 2.0);\n\n  // First corner\n  vec3 i  = floor(v + dot(v, C.yyy) );\n  vec3 x0 =   v - i + dot(i, C.xxx) ;\n\n  // Other corners\n  vec3 g = step(x0.yzx, x0.xyz);\n  vec3 l = 1.0 - g;\n  vec3 i1 = min( g.xyz, l.zxy );\n  vec3 i2 = max( g.xyz, l.zxy );\n\n  //   x0 = x0 - 0.0 + 0.0 * C.xxx;\n  //   x1 = x0 - i1  + 1.0 * C.xxx;\n  //   x2 = x0 - i2  + 2.0 * C.xxx;\n  //   x3 = x0 - 1.0 + 3.0 * C.xxx;\n  vec3 x1 = x0 - i1 + C.xxx;\n  vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y\n  vec3 x3 = x0 - D.yyy;      // -1.0+3.0*C.x = -0.5 = -D.y\n\n  // Permutations\n  i = mod289(i);\n  vec4 p = permute( permute( permute(\n              i.z + vec4(0.0, i1.z, i2.z, 1.0 ))\n            + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))\n            + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));\n\n  // Gradients: 7x7 points over a square, mapped onto an octahedron.\n  // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)\n  float n_ = 0.142857142857; // 1.0/7.0\n  vec3  ns = n_ * D.wyz - D.xzx;\n\n  vec4 j = p - 49.0 * floor(p * ns.z * ns.z);  //  mod(p,7*7)\n\n  vec4 x_ = floor(j * ns.z);\n  vec4 y_ = floor(j - 7.0 * x_ );    // mod(j,N)\n\n  vec4 x = x_ *ns.x + ns.yyyy;\n  vec4 y = y_ *ns.x + ns.yyyy;\n  vec4 h = 1.0 - abs(x) - abs(y);\n\n  vec4 b0 = vec4( x.xy, y.xy );\n  vec4 b1 = vec4( x.zw, y.zw );\n\n  //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;\n  //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;\n  vec4 s0 = floor(b0)*2.0 + 1.0;\n  vec4 s1 = floor(b1)*2.0 + 1.0;\n  vec4 sh = -step(h, vec4(0.0));\n\n  vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;\n  vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;\n\n  vec3 p0 = vec3(a0.xy,h.x);\n  vec3 p1 = vec3(a0.zw,h.y);\n  vec3 p2 = vec3(a1.xy,h.z);\n  vec3 p3 = vec3(a1.zw,h.w);\n\n  //Normalise gradients\n  vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));\n  p0 *= norm.x;\n  p1 *= norm.y;\n  p2 *= norm.z;\n  p3 *= norm.w;\n\n  // Mix final noise value\n  vec4 m = max(0.5 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);\n  m = m * m;\n  return 105.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),\n                                dot(p2,x2), dot(p3,x3) ) );\n}\n`;\n\nconst createDotTexture = (size = 32, color = \"#FFFFFF\"): THREE.CanvasTexture => {\n  const radius = size * 0.5;\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = size;\n  canvas.height = size;\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) {\n    throw new Error(\"2D canvas context not available\");\n  }\n\n  const circle = new Path2D();\n  circle.arc(radius, radius, radius, 0, 2 * Math.PI);\n  ctx.fillStyle = color;\n  // eslint-disable-next-line unicorn/no-array-fill-with-reference-type -- Canvas fills a path, not an array.\n  ctx.fill(circle);\n\n  return new THREE.CanvasTexture(canvas);\n};\n\n/**\n * Injects the noise-displacement vertex shader into a PointsMaterial.\n * Returns a setter for the `time` uniform — the compiled shader is only available\n * once WebGL first compiles the program, so the setter is a no-op until then.\n */\nconst setupPointsShader = (\n  material: THREE.PointsMaterial,\n  opts: { radius: number; particleSizeMin: number; particleSizeMax: number },\n): ((time: number) => void) => {\n  const { radius, particleSizeMin, particleSizeMax } = opts;\n  let compiled: THREE.WebGLProgramParametersWithUniforms | null = null;\n\n  material.onBeforeCompile = (shader: THREE.WebGLProgramParametersWithUniforms) => {\n    shader.uniforms.time = { value: 0 };\n    shader.uniforms.radius = { value: radius };\n    shader.uniforms.particleSizeMin = { value: particleSizeMin };\n    shader.uniforms.particleSizeMax = { value: particleSizeMax };\n    shader.vertexShader = `${\n      webGlNoise\n    }\\nuniform float time;\\nuniform float radius;\\nuniform float particleSizeMin;\\nuniform float particleSizeMax;\\n${\n      shader.vertexShader\n    }`;\n    shader.vertexShader = shader.vertexShader.replace(\n      \"#include <begin_vertex>\",\n      `\n          vec3 p = position;\n          float n = snoise( vec3( p.x*.6 + time*0.2, p.y*0.4 + time*0.3, p.z*.2 + time*0.2) );\n          p += n *0.4;\n\n          float l = radius / length(p);\n          p *= l;\n          float s = mix(particleSizeMin, particleSizeMax, n);\n          vec3 transformed = vec3( p.x, p.y, p.z );\n        `,\n    );\n    shader.vertexShader = shader.vertexShader.replace(\"gl_PointSize = size;\", \"gl_PointSize = s;\");\n\n    compiled = shader;\n  };\n\n  return (time: number) => {\n    const timeUniform = compiled?.uniforms.time;\n    if (timeUniform) {\n      timeUniform.value = time;\n    }\n  };\n};\n\nexport const ParticleOrb = () => {\n  const rootRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const container = rootRef.current;\n    if (!container) {\n      return;\n    }\n\n    const scene = new THREE.Scene();\n\n    const getSize = () => ({\n      height: container.clientHeight,\n      width: container.clientWidth,\n    });\n\n    const { width: initialWidth, height: initialHeight } = getSize();\n\n    const camera = new THREE.PerspectiveCamera(75, initialWidth / initialHeight, 0.1, 1000);\n    camera.position.z = 3;\n\n    const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });\n    renderer.setPixelRatio(window.devicePixelRatio);\n    renderer.setSize(initialWidth, initialHeight);\n    container.append(renderer.domElement);\n\n    const radius = 1.5;\n    const detail = 40;\n    const particleSizeMin = 0.01;\n    const particleSizeMax = 0.08;\n\n    const geometry = new THREE.IcosahedronGeometry(1, detail);\n    const texture = createDotTexture(32, \"#FFFFFF\");\n    const material = new THREE.PointsMaterial({\n      blending: THREE.AdditiveBlending,\n      color: 0x10_1a_88,\n      depthTest: false,\n      map: texture,\n    });\n\n    const setShaderTime = setupPointsShader(material, {\n      particleSizeMax,\n      particleSizeMin,\n      radius,\n    });\n\n    const points = new THREE.Points(geometry, material);\n    scene.add(points);\n\n    let animationFrameId = 0;\n    const animate = (timeMs: number) => {\n      const time = timeMs * 0.001;\n      points.rotation.set(0, time * 0.2, 0);\n      setShaderTime(time);\n      renderer.render(scene, camera);\n      animationFrameId = requestAnimationFrame(animate);\n    };\n    animationFrameId = requestAnimationFrame(animate);\n\n    const resizeObserver = new ResizeObserver(() => {\n      const { width, height } = getSize();\n      camera.aspect = width / height;\n      camera.updateProjectionMatrix();\n      renderer.setSize(width, height);\n    });\n    resizeObserver.observe(container);\n\n    return () => {\n      cancelAnimationFrame(animationFrameId);\n      resizeObserver.disconnect();\n      scene.remove(points);\n      geometry.dispose();\n      material.dispose();\n      texture.dispose();\n      renderer.dispose();\n      if (renderer.domElement.parentElement === container) {\n        renderer.domElement.remove();\n      }\n    };\n  }, []);\n\n  return (\n    <div\n      ref={rootRef}\n      className=\"h-full w-full bg-[radial-gradient(circle_farthest-corner,_#060a33,_#000000)]\"\n    />\n  );\n};\n","path":"/particle-orb.tsx","target":"uicapsule/particle-orb/particle-orb.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { ParticleOrb } from \"./particle-orb\";\n\nconst Preview = () => (\n  <div className=\"h-screen w-full\">\n    <ParticleOrb />\n  </div>\n);\n\nexport default Preview;\n","path":"/preview.tsx","target":"uicapsule/particle-orb/preview.tsx","type":"registry:file"}],"homepage":"https://www.uicapsule.com/ui/particle-orb","name":"particle-orb","registryDependencies":[],"title":"Particle Orb","type":"registry:block"}