{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "orb", "dependencies": [ "@react-three/drei", "@react-three/fiber", "three", "@types/three" ], "files": [ { "path": "components/ui/orb.tsx", "content": "\"use client\"\n\nimport { useEffect, useMemo, useRef } from \"react\"\nimport { useTexture } from \"@react-three/drei\"\nimport { Canvas, useFrame, useThree } from \"@react-three/fiber\"\nimport * as THREE from \"three\"\n\nexport type AgentState = null | \"thinking\" | \"listening\" | \"talking\"\n\ntype OrbProps = {\n colors?: [string, string]\n colorsRef?: React.RefObject<[string, string]>\n resizeDebounce?: number\n seed?: number\n agentState?: AgentState\n volumeMode?: \"auto\" | \"manual\"\n manualInput?: number\n manualOutput?: number\n inputVolumeRef?: React.RefObject\n outputVolumeRef?: React.RefObject\n getInputVolume?: () => number\n getOutputVolume?: () => number\n className?: string\n}\n\nexport function Orb({\n colors = [\"#CADCFC\", \"#A0B9D1\"],\n colorsRef,\n resizeDebounce = 100,\n seed,\n agentState = null,\n volumeMode = \"auto\",\n manualInput,\n manualOutput,\n inputVolumeRef,\n outputVolumeRef,\n getInputVolume,\n getOutputVolume,\n className,\n}: OrbProps) {\n return (\n
\n \n \n \n
\n )\n}\n\nfunction Scene({\n colors,\n colorsRef,\n seed,\n agentState,\n volumeMode,\n manualInput,\n manualOutput,\n inputVolumeRef,\n outputVolumeRef,\n getInputVolume,\n getOutputVolume,\n}: {\n colors: [string, string]\n colorsRef?: React.RefObject<[string, string]>\n seed?: number\n agentState: AgentState\n volumeMode: \"auto\" | \"manual\"\n manualInput?: number\n manualOutput?: number\n inputVolumeRef?: React.RefObject\n outputVolumeRef?: React.RefObject\n getInputVolume?: () => number\n getOutputVolume?: () => number\n}) {\n const { gl } = useThree()\n const circleRef =\n useRef>(null)\n const initialColorsRef = useRef<[string, string]>(colors)\n const targetColor1Ref = useRef(new THREE.Color(colors[0]))\n const targetColor2Ref = useRef(new THREE.Color(colors[1]))\n const animSpeedRef = useRef(0.1)\n const perlinNoiseTexture = useTexture(\n \"https://storage.googleapis.com/eleven-public-cdn/images/perlin-noise.png\"\n )\n\n const agentRef = useRef(agentState)\n const modeRef = useRef<\"auto\" | \"manual\">(volumeMode)\n const manualInRef = useRef(manualInput ?? 0)\n const manualOutRef = useRef(manualOutput ?? 0)\n const curInRef = useRef(0)\n const curOutRef = useRef(0)\n\n useEffect(() => {\n agentRef.current = agentState\n }, [agentState])\n\n useEffect(() => {\n modeRef.current = volumeMode\n }, [volumeMode])\n\n useEffect(() => {\n manualInRef.current = clamp01(\n manualInput ?? inputVolumeRef?.current ?? getInputVolume?.() ?? 0\n )\n }, [manualInput, inputVolumeRef, getInputVolume])\n\n useEffect(() => {\n manualOutRef.current = clamp01(\n manualOutput ?? outputVolumeRef?.current ?? getOutputVolume?.() ?? 0\n )\n }, [manualOutput, outputVolumeRef, getOutputVolume])\n\n const random = useMemo(\n () => splitmix32(seed ?? Math.floor(Math.random() * 2 ** 32)),\n [seed]\n )\n const offsets = useMemo(\n () =>\n new Float32Array(Array.from({ length: 7 }, () => random() * Math.PI * 2)),\n [random]\n )\n\n useEffect(() => {\n targetColor1Ref.current = new THREE.Color(colors[0])\n targetColor2Ref.current = new THREE.Color(colors[1])\n }, [colors])\n\n useEffect(() => {\n const apply = () => {\n if (!circleRef.current) return\n const isDark = document.documentElement.classList.contains(\"dark\")\n circleRef.current.material.uniforms.uInverted.value = isDark ? 1 : 0\n }\n\n apply()\n\n const observer = new MutationObserver(apply)\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n })\n return () => observer.disconnect()\n }, [])\n\n useFrame((_, delta: number) => {\n const mat = circleRef.current?.material\n if (!mat) return\n const live = colorsRef?.current\n if (live) {\n if (live[0]) targetColor1Ref.current.set(live[0])\n if (live[1]) targetColor2Ref.current.set(live[1])\n }\n const u = mat.uniforms\n u.uTime.value += delta * 0.5\n\n if (u.uOpacity.value < 1) {\n u.uOpacity.value = Math.min(1, u.uOpacity.value + delta * 2)\n }\n\n let targetIn = 0\n let targetOut = 0.3\n if (modeRef.current === \"manual\") {\n targetIn = clamp01(\n manualInput ?? inputVolumeRef?.current ?? getInputVolume?.() ?? 0\n )\n targetOut = clamp01(\n manualOutput ?? outputVolumeRef?.current ?? getOutputVolume?.() ?? 0\n )\n } else {\n const t = u.uTime.value * 2\n if (agentRef.current === null) {\n targetIn = 0\n targetOut = 0.3\n } else if (agentRef.current === \"listening\") {\n targetIn = clamp01(0.55 + Math.sin(t * 3.2) * 0.35)\n targetOut = 0.45\n } else if (agentRef.current === \"talking\") {\n targetIn = clamp01(0.65 + Math.sin(t * 4.8) * 0.22)\n targetOut = clamp01(0.75 + Math.sin(t * 3.6) * 0.22)\n } else {\n const base = 0.38 + 0.07 * Math.sin(t * 0.7)\n const wander = 0.05 * Math.sin(t * 2.1) * Math.sin(t * 0.37 + 1.2)\n targetIn = clamp01(base + wander)\n targetOut = clamp01(0.48 + 0.12 * Math.sin(t * 1.05 + 0.6))\n }\n }\n\n curInRef.current += (targetIn - curInRef.current) * 0.2\n curOutRef.current += (targetOut - curOutRef.current) * 0.2\n\n const targetSpeed = 0.1 + (1 - Math.pow(curOutRef.current - 1, 2)) * 0.9\n animSpeedRef.current += (targetSpeed - animSpeedRef.current) * 0.12\n\n u.uAnimation.value += delta * animSpeedRef.current\n u.uInputVolume.value = curInRef.current\n u.uOutputVolume.value = curOutRef.current\n u.uColor1.value.lerp(targetColor1Ref.current, 0.08)\n u.uColor2.value.lerp(targetColor2Ref.current, 0.08)\n })\n\n useEffect(() => {\n const canvas = gl.domElement\n const onContextLost = (event: Event) => {\n event.preventDefault()\n setTimeout(() => {\n gl.forceContextRestore()\n }, 1)\n }\n canvas.addEventListener(\"webglcontextlost\", onContextLost, false)\n return () =>\n canvas.removeEventListener(\"webglcontextlost\", onContextLost, false)\n }, [gl])\n\n const uniforms = useMemo(() => {\n perlinNoiseTexture.wrapS = THREE.RepeatWrapping\n perlinNoiseTexture.wrapT = THREE.RepeatWrapping\n const isDark =\n typeof document !== \"undefined\" &&\n document.documentElement.classList.contains(\"dark\")\n return {\n uColor1: new THREE.Uniform(new THREE.Color(initialColorsRef.current[0])),\n uColor2: new THREE.Uniform(new THREE.Color(initialColorsRef.current[1])),\n uOffsets: { value: offsets },\n uPerlinTexture: new THREE.Uniform(perlinNoiseTexture),\n uTime: new THREE.Uniform(0),\n uAnimation: new THREE.Uniform(0.1),\n uInverted: new THREE.Uniform(isDark ? 1 : 0),\n uInputVolume: new THREE.Uniform(0),\n uOutputVolume: new THREE.Uniform(0),\n uOpacity: new THREE.Uniform(0),\n }\n }, [perlinNoiseTexture, offsets])\n\n return (\n \n \n \n \n )\n}\n\nfunction splitmix32(a: number) {\n return function () {\n a |= 0\n a = (a + 0x9e3779b9) | 0\n let t = a ^ (a >>> 16)\n t = Math.imul(t, 0x21f0aaad)\n t = t ^ (t >>> 15)\n t = Math.imul(t, 0x735a2d97)\n return ((t = t ^ (t >>> 15)) >>> 0) / 4294967296\n }\n}\n\nfunction clamp01(n: number) {\n if (!Number.isFinite(n)) return 0\n return Math.min(1, Math.max(0, n))\n}\nconst vertexShader = /* glsl */ `\nuniform float uTime;\nuniform sampler2D uPerlinTexture;\nvarying vec2 vUv;\n\nvoid main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`\n\nconst fragmentShader = /* glsl */ `\nuniform float uTime;\nuniform float uAnimation;\nuniform float uInverted;\nuniform float uOffsets[7];\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform float uInputVolume;\nuniform float uOutputVolume;\nuniform float uOpacity;\nuniform sampler2D uPerlinTexture;\nvarying vec2 vUv;\n\nconst float PI = 3.14159265358979323846;\n\n// Draw a single oval with soft edges and calculate its gradient color\nbool drawOval(vec2 polarUv, vec2 polarCenter, float a, float b, bool reverseGradient, float softness, out vec4 color) {\n vec2 p = polarUv - polarCenter;\n float oval = (p.x * p.x) / (a * a) + (p.y * p.y) / (b * b);\n\n float edge = smoothstep(1.0, 1.0 - softness, oval);\n\n if (edge > 0.0) {\n float gradient = reverseGradient ? (1.0 - (p.x / a + 1.0) / 2.0) : ((p.x / a + 1.0) / 2.0);\n // Flatten gradient toward middle value for more uniform appearance\n gradient = mix(0.5, gradient, 0.1);\n color = vec4(vec3(gradient), 0.85 * edge);\n return true;\n }\n return false;\n}\n\n// Map grayscale value to a 4-color ramp (color1, color2, color3, color4)\nvec3 colorRamp(float grayscale, vec3 color1, vec3 color2, vec3 color3, vec3 color4) {\n if (grayscale < 0.33) {\n return mix(color1, color2, grayscale * 3.0);\n } else if (grayscale < 0.66) {\n return mix(color2, color3, (grayscale - 0.33) * 3.0);\n } else {\n return mix(color3, color4, (grayscale - 0.66) * 3.0);\n }\n}\n\nvec2 hash2(vec2 p) {\n return fract(sin(vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)))) * 43758.5453);\n}\n\n// 2D noise for the ring\nfloat noise2D(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n \n vec2 u = f * f * (3.0 - 2.0 * f);\n float n = mix(\n mix(dot(hash2(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0)),\n dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)), u.x),\n mix(dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)),\n dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)), u.x),\n u.y\n );\n\n return 0.5 + 0.5 * n;\n}\n\nfloat sharpRing(vec3 decomposed, float time) {\n float ringStart = 1.0;\n float ringWidth = 0.3;\n float noiseScale = 5.0;\n\n float noise = mix(\n noise2D(vec2(decomposed.x, time) * noiseScale),\n noise2D(vec2(decomposed.y, time) * noiseScale),\n decomposed.z\n );\n\n noise = (noise - 0.5) * 2.5;\n\n return ringStart + noise * ringWidth * 1.5;\n}\n\nfloat smoothRing(vec3 decomposed, float time) {\n float ringStart = 0.9;\n float ringWidth = 0.2;\n float noiseScale = 6.0;\n\n float noise = mix(\n noise2D(vec2(decomposed.x, time) * noiseScale),\n noise2D(vec2(decomposed.y, time) * noiseScale),\n decomposed.z\n );\n\n noise = (noise - 0.5) * 5.0;\n\n return ringStart + noise * ringWidth;\n}\n\nfloat flow(vec3 decomposed, float time) {\n return mix(\n texture(uPerlinTexture, vec2(time, decomposed.x / 2.0)).r,\n texture(uPerlinTexture, vec2(time, decomposed.y / 2.0)).r,\n decomposed.z\n );\n}\n\nvoid main() {\n // Normalize vUv to be centered around (0.0, 0.0)\n vec2 uv = vUv * 2.0 - 1.0;\n\n // Convert uv to polar coordinates\n float radius = length(uv);\n float theta = atan(uv.y, uv.x);\n if (theta < 0.0) theta += 2.0 * PI; // Normalize theta to [0, 2*PI]\n\n // Decomposed angle is used for sampling noise textures without seams:\n // float noise = mix(sample(decomposed.x), sample(decomposed.y), decomposed.z);\n vec3 decomposed = vec3(\n // angle in the range [0, 1]\n theta / (2.0 * PI),\n // angle offset by 180 degrees in the range [1, 2]\n mod(theta / (2.0 * PI) + 0.5, 1.0) + 1.0,\n // mixing factor between two noises\n abs(theta / PI - 1.0)\n );\n\n // Add noise to the angle for a flow-like distortion (reduced for flatter look)\n float noise = flow(decomposed, radius * 0.03 - uAnimation * 0.2) - 0.5;\n theta += noise * mix(0.08, 0.25, uOutputVolume);\n\n // Initialize the base color to white\n vec4 color = vec4(1.0, 1.0, 1.0, 1.0);\n\n // Original parameters for the ovals in polar coordinates\n float originalCenters[7] = float[7](0.0, 0.5 * PI, 1.0 * PI, 1.5 * PI, 2.0 * PI, 2.5 * PI, 3.0 * PI);\n\n // Parameters for the animated centers in polar coordinates\n float centers[7];\n for (int i = 0; i < 7; i++) {\n centers[i] = originalCenters[i] + 0.5 * sin(uTime / 20.0 + uOffsets[i]);\n }\n\n float a, b;\n vec4 ovalColor;\n\n // Check if the pixel is inside any of the ovals\n for (int i = 0; i < 7; i++) {\n float noise = texture(uPerlinTexture, vec2(mod(centers[i] + uTime * 0.05, 1.0), 0.5)).r;\n a = 0.5 + noise * 0.3; // Increased for more coverage\n b = noise * mix(3.5, 2.5, uInputVolume); // Increased height for fuller appearance\n bool reverseGradient = (i % 2 == 1); // Reverse gradient for every second oval\n\n // Calculate the distance in polar coordinates\n float distTheta = min(\n abs(theta - centers[i]),\n min(\n abs(theta + 2.0 * PI - centers[i]),\n abs(theta - 2.0 * PI - centers[i])\n )\n );\n float distRadius = radius;\n\n float softness = 0.6; // Increased softness for flatter, less pronounced edges\n\n // Check if the pixel is inside the oval in polar coordinates\n if (drawOval(vec2(distTheta, distRadius), vec2(0.0, 0.0), a, b, reverseGradient, softness, ovalColor)) {\n // Blend the oval color with the existing color\n color.rgb = mix(color.rgb, ovalColor.rgb, ovalColor.a);\n color.a = max(color.a, ovalColor.a); // Max alpha\n }\n }\n \n // Calculate both noisy rings\n float ringRadius1 = sharpRing(decomposed, uTime * 0.1);\n float ringRadius2 = smoothRing(decomposed, uTime * 0.1);\n \n // Adjust rings based on input volume (reduced for flatter appearance)\n float inputRadius1 = radius + uInputVolume * 0.2;\n float inputRadius2 = radius + uInputVolume * 0.15;\n float opacity1 = mix(0.2, 0.6, uInputVolume);\n float opacity2 = mix(0.15, 0.45, uInputVolume);\n\n // Blend both rings\n float ringAlpha1 = (inputRadius2 >= ringRadius1) ? opacity1 : 0.0;\n float ringAlpha2 = smoothstep(ringRadius2 - 0.05, ringRadius2 + 0.05, inputRadius1) * opacity2;\n \n float totalRingAlpha = max(ringAlpha1, ringAlpha2);\n \n // Apply screen blend mode for combined rings\n vec3 ringColor = vec3(1.0); // White ring color\n color.rgb = 1.0 - (1.0 - color.rgb) * (1.0 - ringColor * totalRingAlpha);\n\n // Define colours to ramp against greyscale (could increase the amount of colours in the ramp)\n vec3 color1 = vec3(0.0, 0.0, 0.0); // Black\n vec3 color2 = uColor1; // Darker Color\n vec3 color3 = uColor2; // Lighter Color\n vec3 color4 = vec3(1.0, 1.0, 1.0); // White\n\n // Convert grayscale color to the color ramp\n float luminance = mix(color.r, 1.0 - color.r, uInverted);\n color.rgb = colorRamp(luminance, color1, color2, color3, color4); // Apply the color ramp\n\n // Apply fade-in opacity\n color.a *= uOpacity;\n\n gl_FragColor = color;\n}\n`\n", "type": "registry:ui" } ], "type": "registry:ui" }