diff --git a/public/r/ASCIIText-JS-CSS.json b/public/r/ASCIIText-JS-CSS.json
index 9e177db5..0588c795 100644
--- a/public/r/ASCIIText-JS-CSS.json
+++ b/public/r/ASCIIText-JS-CSS.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "ASCIIText/ASCIIText.jsx",
- "content": "// Component ported and enhanced from https://codepen.io/JuanFuentes/pen/eYEeoyE\n\nimport { useRef, useEffect } from 'react';\nimport * as THREE from 'three';\n\nconst vertexShader = `\nvarying vec2 vUv;\nuniform float uTime;\nuniform float mouse;\nuniform float uEnableWaves;\n\nvoid main() {\n vUv = uv;\n float time = uTime * 5.;\n\n float waveFactor = uEnableWaves;\n\n vec3 transformed = position;\n\n transformed.x += sin(time + position.y) * 0.5 * waveFactor;\n transformed.y += cos(time + position.z) * 0.15 * waveFactor;\n transformed.z += sin(time + position.x) * waveFactor;\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);\n}\n`;\n\nconst fragmentShader = `\nvarying vec2 vUv;\nuniform float mouse;\nuniform float uTime;\nuniform sampler2D uTexture;\n\nvoid main() {\n float time = uTime;\n vec2 pos = vUv;\n \n float move = sin(time + mouse) * 0.01;\n float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;\n float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;\n float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;\n float a = texture2D(uTexture, pos).a;\n gl_FragColor = vec4(r, g, b, a);\n}\n`;\n\nMath.map = function (n, start, stop, start2, stop2) {\n return ((n - start) / (stop - start)) * (stop2 - start2) + start2;\n};\n\nconst PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1;\n\nclass AsciiFilter {\n constructor(renderer, { fontSize, fontFamily, charset, invert } = {}) {\n this.renderer = renderer;\n this.domElement = document.createElement('div');\n this.domElement.style.position = 'absolute';\n this.domElement.style.top = '0';\n this.domElement.style.left = '0';\n this.domElement.style.width = '100%';\n this.domElement.style.height = '100%';\n\n this.pre = document.createElement('pre');\n this.domElement.appendChild(this.pre);\n\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.domElement.appendChild(this.canvas);\n\n this.deg = 0;\n this.invert = invert ?? true;\n this.fontSize = fontSize ?? 12;\n this.fontFamily = fontFamily ?? \"'Courier New', monospace\";\n this.charset = charset ?? ' .\\'`^\",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';\n\n this.context.webkitImageSmoothingEnabled = false;\n this.context.mozImageSmoothingEnabled = false;\n this.context.msImageSmoothingEnabled = false;\n this.context.imageSmoothingEnabled = false;\n\n this.onMouseMove = this.onMouseMove.bind(this);\n document.addEventListener('mousemove', this.onMouseMove);\n }\n\n setSize(width, height) {\n this.width = width;\n this.height = height;\n this.renderer.setSize(width, height);\n this.reset();\n\n this.center = { x: width / 2, y: height / 2 };\n this.mouse = { x: this.center.x, y: this.center.y };\n }\n\n reset() {\n this.context.font = `${this.fontSize}px ${this.fontFamily}`;\n const charWidth = this.context.measureText('A').width;\n\n this.cols = Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize)));\n this.rows = Math.floor(this.height / this.fontSize);\n\n this.canvas.width = this.cols;\n this.canvas.height = this.rows;\n this.pre.style.fontFamily = this.fontFamily;\n this.pre.style.fontSize = `${this.fontSize}px`;\n this.pre.style.margin = '0';\n this.pre.style.padding = '0';\n this.pre.style.lineHeight = '1em';\n this.pre.style.position = 'absolute';\n this.pre.style.left = '0';\n this.pre.style.top = '0';\n this.pre.style.zIndex = '9';\n this.pre.style.backgroundAttachment = 'fixed';\n this.pre.style.mixBlendMode = 'difference';\n }\n\n render(scene, camera) {\n this.renderer.render(scene, camera);\n\n const w = this.canvas.width;\n const h = this.canvas.height;\n this.context.clearRect(0, 0, w, h);\n if (this.context && w && h) {\n this.context.drawImage(this.renderer.domElement, 0, 0, w, h);\n }\n\n this.asciify(this.context, w, h);\n this.hue();\n }\n\n onMouseMove(e) {\n this.mouse = { x: e.clientX * PX_RATIO, y: e.clientY * PX_RATIO };\n }\n\n get dx() {\n return this.mouse.x - this.center.x;\n }\n\n get dy() {\n return this.mouse.y - this.center.y;\n }\n\n hue() {\n const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI;\n this.deg += (deg - this.deg) * 0.075;\n this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)`;\n }\n\n asciify(ctx, w, h) {\n if (w && h) {\n const imgData = ctx.getImageData(0, 0, w, h).data;\n let str = '';\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = x * 4 + y * 4 * w;\n const [r, g, b, a] = [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];\n\n if (a === 0) {\n str += ' ';\n continue;\n }\n\n let gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;\n let idx = Math.floor((1 - gray) * (this.charset.length - 1));\n if (this.invert) idx = this.charset.length - idx - 1;\n str += this.charset[idx];\n }\n str += '\\n';\n }\n this.pre.innerHTML = str;\n }\n }\n\n dispose() {\n document.removeEventListener('mousemove', this.onMouseMove);\n }\n}\n\nclass CanvasTxt {\n constructor(txt, { fontSize = 200, fontFamily = 'Arial', color = '#fdf9f3' } = {}) {\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.txt = txt;\n this.fontSize = fontSize;\n this.fontFamily = fontFamily;\n this.color = color;\n\n this.font = `600 ${this.fontSize}px ${this.fontFamily}`;\n }\n\n resize() {\n this.context.font = this.font;\n const metrics = this.context.measureText(this.txt);\n\n const textWidth = Math.ceil(metrics.width) + 20;\n const textHeight = Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20;\n\n this.canvas.width = textWidth;\n this.canvas.height = textHeight;\n }\n\n render() {\n this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n this.context.fillStyle = this.color;\n this.context.font = this.font;\n\n const metrics = this.context.measureText(this.txt);\n const yPos = 10 + metrics.actualBoundingBoxAscent;\n\n this.context.fillText(this.txt, 10, yPos);\n }\n\n get width() {\n return this.canvas.width;\n }\n\n get height() {\n return this.canvas.height;\n }\n\n get texture() {\n return this.canvas;\n }\n}\n\nclass CanvAscii {\n constructor(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\n containerElem,\n width,\n height\n ) {\n this.textString = text;\n this.asciiFontSize = asciiFontSize;\n this.textFontSize = textFontSize;\n this.textColor = textColor;\n this.planeBaseHeight = planeBaseHeight;\n this.container = containerElem;\n this.width = width;\n this.height = height;\n this.enableWaves = enableWaves;\n\n this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);\n this.camera.position.z = 30;\n\n this.scene = new THREE.Scene();\n this.mouse = { x: this.width / 2, y: this.height / 2 };\n\n this.onMouseMove = this.onMouseMove.bind(this);\n }\n\n async init() {\n try {\n await document.fonts.load('600 200px \"IBM Plex Mono\"');\n await document.fonts.load('500 12px \"IBM Plex Mono\"');\n } catch (e) {\n // Font loading failed, continue with fallback\n }\n await document.fonts.ready;\n\n this.setMesh();\n this.setRenderer();\n }\n\n setMesh() {\n this.textCanvas = new CanvasTxt(this.textString, {\n fontSize: this.textFontSize,\n fontFamily: 'IBM Plex Mono',\n color: this.textColor\n });\n this.textCanvas.resize();\n this.textCanvas.render();\n\n this.texture = new THREE.CanvasTexture(this.textCanvas.texture);\n this.texture.minFilter = THREE.NearestFilter;\n\n const textAspect = this.textCanvas.width / this.textCanvas.height;\n const baseH = this.planeBaseHeight;\n const planeW = baseH * textAspect;\n const planeH = baseH;\n\n this.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36);\n this.material = new THREE.ShaderMaterial({\n vertexShader,\n fragmentShader,\n transparent: true,\n uniforms: {\n uTime: { value: 0 },\n mouse: { value: 1.0 },\n uTexture: { value: this.texture },\n uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }\n }\n });\n\n this.mesh = new THREE.Mesh(this.geometry, this.material);\n this.scene.add(this.mesh);\n }\n\n setRenderer() {\n this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });\n this.renderer.setPixelRatio(1);\n this.renderer.setClearColor(0x000000, 0);\n\n this.filter = new AsciiFilter(this.renderer, {\n fontFamily: 'IBM Plex Mono',\n fontSize: this.asciiFontSize,\n invert: true\n });\n\n this.container.appendChild(this.filter.domElement);\n this.setSize(this.width, this.height);\n\n this.container.addEventListener('mousemove', this.onMouseMove);\n this.container.addEventListener('touchmove', this.onMouseMove);\n }\n\n setSize(w, h) {\n this.width = w;\n this.height = h;\n\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n\n this.filter.setSize(w, h);\n\n this.center = { x: w / 2, y: h / 2 };\n }\n\n load() {\n this.animate();\n }\n\n onMouseMove(evt) {\n const e = evt.touches ? evt.touches[0] : evt;\n const bounds = this.container.getBoundingClientRect();\n const x = e.clientX - bounds.left;\n const y = e.clientY - bounds.top;\n this.mouse = { x, y };\n }\n\n animate() {\n const animateFrame = () => {\n this.animationFrameId = requestAnimationFrame(animateFrame);\n this.render();\n };\n animateFrame();\n }\n\n render() {\n const time = new Date().getTime() * 0.001;\n\n this.textCanvas.render();\n this.texture.needsUpdate = true;\n\n this.mesh.material.uniforms.uTime.value = Math.sin(time);\n\n this.updateRotation();\n this.filter.render(this.scene, this.camera);\n }\n\n updateRotation() {\n const x = Math.map(this.mouse.y, 0, this.height, 0.5, -0.5);\n const y = Math.map(this.mouse.x, 0, this.width, -0.5, 0.5);\n\n this.mesh.rotation.x += (x - this.mesh.rotation.x) * 0.05;\n this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05;\n }\n\n clear() {\n this.scene.traverse(obj => {\n if (obj.isMesh && typeof obj.material === 'object' && obj.material !== null) {\n Object.keys(obj.material).forEach(key => {\n const matProp = obj.material[key];\n if (matProp !== null && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n obj.material.dispose();\n obj.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n cancelAnimationFrame(this.animationFrameId);\n if (this.filter) {\n this.filter.dispose();\n if (this.filter.domElement.parentNode) {\n this.container.removeChild(this.filter.domElement);\n }\n }\n this.container.removeEventListener('mousemove', this.onMouseMove);\n this.container.removeEventListener('touchmove', this.onMouseMove);\n this.clear();\n if (this.renderer) {\n this.renderer.dispose();\n }\n }\n}\n\nexport default function ASCIIText({\n text = 'David!',\n asciiFontSize = 8,\n textFontSize = 200,\n textColor = '#fdf9f3',\n planeBaseHeight = 8,\n enableWaves = true\n}) {\n const containerRef = useRef(null);\n const asciiRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n let cancelled = false;\n let observer = null;\n let ro = null;\n\n const createAndInit = async (container, w, h) => {\n const instance = new CanvAscii(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\n container,\n w,\n h\n );\n await instance.init();\n return instance;\n };\n\n const setup = async () => {\n const { width, height } = containerRef.current.getBoundingClientRect();\n\n if (width === 0 || height === 0) {\n observer = new IntersectionObserver(\n async ([entry]) => {\n if (cancelled) return;\n if (entry.isIntersecting && entry.boundingClientRect.width > 0 && entry.boundingClientRect.height > 0) {\n const { width: w, height: h } = entry.boundingClientRect;\n observer.disconnect();\n observer = null;\n\n if (!cancelled) {\n asciiRef.current = await createAndInit(containerRef.current, w, h);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n }\n }\n }\n },\n { threshold: 0.1 }\n );\n observer.observe(containerRef.current);\n return;\n }\n\n asciiRef.current = await createAndInit(containerRef.current, width, height);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n\n ro = new ResizeObserver(entries => {\n if (!entries[0] || !asciiRef.current) return;\n const { width: w, height: h } = entries[0].contentRect;\n if (w > 0 && h > 0) {\n asciiRef.current.setSize(w, h);\n }\n });\n ro.observe(containerRef.current);\n }\n };\n\n setup();\n\n return () => {\n cancelled = true;\n if (observer) observer.disconnect();\n if (ro) ro.disconnect();\n if (asciiRef.current) {\n asciiRef.current.dispose();\n asciiRef.current = null;\n }\n };\n }, [text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves]);\n\n return (\n
\n \n
\n );\n}\n"
+ "content": "// Component ported and enhanced from https://codepen.io/JuanFuentes/pen/eYEeoyE\n\nimport { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nconst vertexShader = `\nvarying vec2 vUv;\nuniform float uTime;\nuniform float mouse;\nuniform float uEnableWaves;\n\nvoid main() {\n vUv = uv;\n float time = uTime * 5.;\n\n float waveFactor = uEnableWaves;\n\n vec3 transformed = position;\n\n transformed.x += sin(time + position.y) * 0.5 * waveFactor;\n transformed.y += cos(time + position.z) * 0.15 * waveFactor;\n transformed.z += sin(time + position.x) * waveFactor;\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);\n}\n`;\n\nconst fragmentShader = `\nvarying vec2 vUv;\nuniform float mouse;\nuniform float uTime;\nuniform sampler2D uTexture;\n\nvoid main() {\n float time = uTime;\n vec2 pos = vUv;\n \n float move = sin(time + mouse) * 0.01;\n float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;\n float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;\n float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;\n float a = texture2D(uTexture, pos).a;\n gl_FragColor = vec4(r, g, b, a);\n}\n`;\n\nMath.map = function (n, start, stop, start2, stop2) {\n return ((n - start) / (stop - start)) * (stop2 - start2) + start2;\n};\n\nconst PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1;\n\nclass AsciiFilter {\n constructor(renderer, { fontSize, fontFamily, charset, invert } = {}) {\n this.renderer = renderer;\n this.domElement = document.createElement('div');\n this.domElement.style.position = 'absolute';\n this.domElement.style.top = '0';\n this.domElement.style.left = '0';\n this.domElement.style.width = '100%';\n this.domElement.style.height = '100%';\n\n this.pre = document.createElement('pre');\n this.domElement.appendChild(this.pre);\n\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.domElement.appendChild(this.canvas);\n\n this.deg = 0;\n this.invert = invert ?? true;\n this.fontSize = fontSize ?? 12;\n this.fontFamily = fontFamily ?? \"'Courier New', monospace\";\n this.charset = charset ?? ' .\\'`^\",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';\n\n this.context.webkitImageSmoothingEnabled = false;\n this.context.mozImageSmoothingEnabled = false;\n this.context.msImageSmoothingEnabled = false;\n this.context.imageSmoothingEnabled = false;\n\n this.onMouseMove = this.onMouseMove.bind(this);\n document.addEventListener('mousemove', this.onMouseMove);\n }\n\n setSize(width, height) {\n this.width = width;\n this.height = height;\n this.renderer.setSize(width, height);\n this.reset();\n\n this.center = { x: width / 2, y: height / 2 };\n this.mouse = { x: this.center.x, y: this.center.y };\n }\n\n reset() {\n this.context.font = `${this.fontSize}px ${this.fontFamily}`;\n const charWidth = this.context.measureText('A').width;\n\n this.cols = Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize)));\n this.rows = Math.floor(this.height / this.fontSize);\n\n this.canvas.width = this.cols;\n this.canvas.height = this.rows;\n this.pre.style.fontFamily = this.fontFamily;\n this.pre.style.fontSize = `${this.fontSize}px`;\n this.pre.style.margin = '0';\n this.pre.style.padding = '0';\n this.pre.style.lineHeight = '1em';\n this.pre.style.position = 'absolute';\n this.pre.style.left = '0';\n this.pre.style.top = '0';\n this.pre.style.zIndex = '9';\n this.pre.style.backgroundAttachment = 'fixed';\n this.pre.style.mixBlendMode = 'difference';\n }\n\n render(scene, camera) {\n this.renderer.render(scene, camera);\n\n const w = this.canvas.width;\n const h = this.canvas.height;\n this.context.clearRect(0, 0, w, h);\n if (this.context && w && h) {\n this.context.drawImage(this.renderer.domElement, 0, 0, w, h);\n }\n\n this.asciify(this.context, w, h);\n this.hue();\n }\n\n onMouseMove(e) {\n this.mouse = { x: e.clientX * PX_RATIO, y: e.clientY * PX_RATIO };\n }\n\n get dx() {\n return this.mouse.x - this.center.x;\n }\n\n get dy() {\n return this.mouse.y - this.center.y;\n }\n\n hue() {\n const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI;\n this.deg += (deg - this.deg) * 0.075;\n this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)`;\n }\n\n asciify(ctx, w, h) {\n if (w && h) {\n const imgData = ctx.getImageData(0, 0, w, h).data;\n let str = '';\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = x * 4 + y * 4 * w;\n const [r, g, b, a] = [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];\n\n if (a === 0) {\n str += ' ';\n continue;\n }\n\n let gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;\n let idx = Math.floor((1 - gray) * (this.charset.length - 1));\n if (this.invert) idx = this.charset.length - idx - 1;\n str += this.charset[idx];\n }\n str += '\\n';\n }\n this.pre.innerHTML = str;\n }\n }\n\n dispose() {\n document.removeEventListener('mousemove', this.onMouseMove);\n }\n}\n\nclass CanvasTxt {\n constructor(txt, { fontSize = 200, fontFamily = 'Arial', color = '#fdf9f3' } = {}) {\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.txt = txt;\n this.fontSize = fontSize;\n this.fontFamily = fontFamily;\n this.color = color;\n\n this.font = `600 ${this.fontSize}px ${this.fontFamily}`;\n }\n\n resize() {\n this.context.font = this.font;\n const metrics = this.context.measureText(this.txt);\n\n const textWidth = Math.ceil(metrics.width) + 20;\n const textHeight = Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20;\n\n this.canvas.width = textWidth;\n this.canvas.height = textHeight;\n }\n\n render() {\n this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n this.context.fillStyle = this.color;\n this.context.font = this.font;\n\n const metrics = this.context.measureText(this.txt);\n const yPos = 10 + metrics.actualBoundingBoxAscent;\n\n this.context.fillText(this.txt, 10, yPos);\n }\n\n get width() {\n return this.canvas.width;\n }\n\n get height() {\n return this.canvas.height;\n }\n\n get texture() {\n return this.canvas;\n }\n}\n\nclass CanvAscii {\n constructor(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\n containerElem,\n width,\n height\n ) {\n this.textString = text;\n this.asciiFontSize = asciiFontSize;\n this.textFontSize = textFontSize;\n this.textColor = textColor;\n this.planeBaseHeight = planeBaseHeight;\n this.container = containerElem;\n this.width = width;\n this.height = height;\n this.enableWaves = enableWaves;\n\n this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);\n this.camera.position.z = 30;\n\n this.scene = new THREE.Scene();\n this.mouse = { x: this.width / 2, y: this.height / 2 };\n\n this.onMouseMove = this.onMouseMove.bind(this);\n }\n\n async init() {\n try {\n await document.fonts.load('600 200px \"IBM Plex Mono\"');\n await document.fonts.load('500 12px \"IBM Plex Mono\"');\n } catch (e) {\n // Font loading failed, continue with fallback\n }\n await document.fonts.ready;\n\n this.setMesh();\n this.setRenderer();\n }\n\n setMesh() {\n this.textCanvas = new CanvasTxt(this.textString, {\n fontSize: this.textFontSize,\n fontFamily: 'IBM Plex Mono',\n color: this.textColor\n });\n this.textCanvas.resize();\n this.textCanvas.render();\n\n this.texture = new THREE.CanvasTexture(this.textCanvas.texture);\n this.texture.minFilter = THREE.NearestFilter;\n\n const textAspect = this.textCanvas.width / this.textCanvas.height;\n const baseH = this.planeBaseHeight;\n const planeW = baseH * textAspect;\n const planeH = baseH;\n\n this.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36);\n this.material = new THREE.ShaderMaterial({\n vertexShader,\n fragmentShader,\n transparent: true,\n uniforms: {\n uTime: { value: 0 },\n mouse: { value: 1.0 },\n uTexture: { value: this.texture },\n uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }\n }\n });\n\n this.mesh = new THREE.Mesh(this.geometry, this.material);\n this.scene.add(this.mesh);\n }\n\n setRenderer() {\n this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });\n this.renderer.setPixelRatio(1);\n this.renderer.setClearColor(0x000000, 0);\n\n this.filter = new AsciiFilter(this.renderer, {\n fontFamily: 'IBM Plex Mono',\n fontSize: this.asciiFontSize,\n invert: true\n });\n\n this.container.appendChild(this.filter.domElement);\n this.setSize(this.width, this.height);\n\n this.container.addEventListener('mousemove', this.onMouseMove);\n this.container.addEventListener('touchmove', this.onMouseMove);\n }\n\n setSize(w, h) {\n this.width = w;\n this.height = h;\n\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n\n this.filter.setSize(w, h);\n\n this.center = { x: w / 2, y: h / 2 };\n }\n\n load() {\n this.animate();\n }\n\n onMouseMove(evt) {\n const e = evt.touches ? evt.touches[0] : evt;\n const bounds = this.container.getBoundingClientRect();\n const x = e.clientX - bounds.left;\n const y = e.clientY - bounds.top;\n this.mouse = { x, y };\n }\n\n animate() {\n const animateFrame = () => {\n this.animationFrameId = requestAnimationFrame(animateFrame);\n this.render();\n };\n animateFrame();\n }\n\n render() {\n const time = new Date().getTime() * 0.001;\n\n this.textCanvas.render();\n this.texture.needsUpdate = true;\n\n this.mesh.material.uniforms.uTime.value = Math.sin(time);\n\n this.updateRotation();\n this.filter.render(this.scene, this.camera);\n }\n\n updateRotation() {\n const x = Math.map(this.mouse.y, 0, this.height, 0.5, -0.5);\n const y = Math.map(this.mouse.x, 0, this.width, -0.5, 0.5);\n\n this.mesh.rotation.x += (x - this.mesh.rotation.x) * 0.05;\n this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05;\n }\n\n clear() {\n this.scene.traverse(obj => {\n if (obj.isMesh && typeof obj.material === 'object' && obj.material !== null) {\n Object.keys(obj.material).forEach(key => {\n const matProp = obj.material[key];\n if (matProp !== null && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n obj.material.dispose();\n obj.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n cancelAnimationFrame(this.animationFrameId);\n if (this.filter) {\n this.filter.dispose();\n if (this.filter.domElement.parentNode) {\n this.container.removeChild(this.filter.domElement);\n }\n }\n this.container.removeEventListener('mousemove', this.onMouseMove);\n this.container.removeEventListener('touchmove', this.onMouseMove);\n this.clear();\n if (this.renderer) {\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n }\n }\n}\n\nexport default function ASCIIText({\n text = 'David!',\n asciiFontSize = 8,\n textFontSize = 200,\n textColor = '#fdf9f3',\n planeBaseHeight = 8,\n enableWaves = true\n}) {\n const containerRef = useRef(null);\n const asciiRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n let cancelled = false;\n let observer = null;\n let ro = null;\n\n const createAndInit = async (container, w, h) => {\n const instance = new CanvAscii(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\n container,\n w,\n h\n );\n await instance.init();\n return instance;\n };\n\n const setup = async () => {\n const { width, height } = containerRef.current.getBoundingClientRect();\n\n if (width === 0 || height === 0) {\n observer = new IntersectionObserver(\n async ([entry]) => {\n if (cancelled) return;\n if (entry.isIntersecting && entry.boundingClientRect.width > 0 && entry.boundingClientRect.height > 0) {\n const { width: w, height: h } = entry.boundingClientRect;\n observer.disconnect();\n observer = null;\n\n if (!cancelled) {\n asciiRef.current = await createAndInit(containerRef.current, w, h);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n }\n }\n }\n },\n { threshold: 0.1 }\n );\n observer.observe(containerRef.current);\n return;\n }\n\n asciiRef.current = await createAndInit(containerRef.current, width, height);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n\n ro = new ResizeObserver(entries => {\n if (!entries[0] || !asciiRef.current) return;\n const { width: w, height: h } = entries[0].contentRect;\n if (w > 0 && h > 0) {\n asciiRef.current.setSize(w, h);\n }\n });\n ro.observe(containerRef.current);\n }\n };\n\n setup();\n\n return () => {\n cancelled = true;\n if (observer) observer.disconnect();\n if (ro) ro.disconnect();\n if (asciiRef.current) {\n asciiRef.current.dispose();\n asciiRef.current = null;\n }\n };\n }, [text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves]);\n\n return (\n \n \n
\n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/ASCIIText-JS-TW.json b/public/r/ASCIIText-JS-TW.json
index 3d58d43e..d34e9d39 100644
--- a/public/r/ASCIIText-JS-TW.json
+++ b/public/r/ASCIIText-JS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "ASCIIText/ASCIIText.jsx",
- "content": "// Component ported and enhanced from https://codepen.io/JuanFuentes/pen/eYEeoyE\n\nimport { useRef, useEffect } from 'react';\nimport * as THREE from 'three';\n\nconst vertexShader = `\nvarying vec2 vUv;\nuniform float uTime;\nuniform float mouse;\nuniform float uEnableWaves;\n\nvoid main() {\n vUv = uv;\n float time = uTime * 5.;\n\n float waveFactor = uEnableWaves;\n\n vec3 transformed = position;\n\n transformed.x += sin(time + position.y) * 0.5 * waveFactor;\n transformed.y += cos(time + position.z) * 0.15 * waveFactor;\n transformed.z += sin(time + position.x) * waveFactor;\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);\n}\n`;\n\nconst fragmentShader = `\nvarying vec2 vUv;\nuniform float mouse;\nuniform float uTime;\nuniform sampler2D uTexture;\n\nvoid main() {\n float time = uTime;\n vec2 pos = vUv;\n \n float move = sin(time + mouse) * 0.01;\n float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;\n float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;\n float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;\n float a = texture2D(uTexture, pos).a;\n gl_FragColor = vec4(r, g, b, a);\n}\n`;\n\nMath.map = function (n, start, stop, start2, stop2) {\n return ((n - start) / (stop - start)) * (stop2 - start2) + start2;\n};\n\nconst PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1;\n\nclass AsciiFilter {\n constructor(renderer, { fontSize, fontFamily, charset, invert } = {}) {\n this.renderer = renderer;\n this.domElement = document.createElement('div');\n this.domElement.style.position = 'absolute';\n this.domElement.style.top = '0';\n this.domElement.style.left = '0';\n this.domElement.style.width = '100%';\n this.domElement.style.height = '100%';\n\n this.pre = document.createElement('pre');\n this.domElement.appendChild(this.pre);\n\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.domElement.appendChild(this.canvas);\n\n this.deg = 0;\n this.invert = invert ?? true;\n this.fontSize = fontSize ?? 12;\n this.fontFamily = fontFamily ?? \"'Courier New', monospace\";\n this.charset = charset ?? ' .\\'`^\",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';\n\n this.context.webkitImageSmoothingEnabled = false;\n this.context.mozImageSmoothingEnabled = false;\n this.context.msImageSmoothingEnabled = false;\n this.context.imageSmoothingEnabled = false;\n\n this.onMouseMove = this.onMouseMove.bind(this);\n document.addEventListener('mousemove', this.onMouseMove);\n }\n\n setSize(width, height) {\n this.width = width;\n this.height = height;\n this.renderer.setSize(width, height);\n this.reset();\n\n this.center = { x: width / 2, y: height / 2 };\n this.mouse = { x: this.center.x, y: this.center.y };\n }\n\n reset() {\n this.context.font = `${this.fontSize}px ${this.fontFamily}`;\n const charWidth = this.context.measureText('A').width;\n\n this.cols = Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize)));\n this.rows = Math.floor(this.height / this.fontSize);\n\n this.canvas.width = this.cols;\n this.canvas.height = this.rows;\n this.pre.style.fontFamily = this.fontFamily;\n this.pre.style.fontSize = `${this.fontSize}px`;\n this.pre.style.margin = '0';\n this.pre.style.padding = '0';\n this.pre.style.lineHeight = '1em';\n this.pre.style.position = 'absolute';\n this.pre.style.left = '0';\n this.pre.style.top = '0';\n this.pre.style.zIndex = '9';\n this.pre.style.backgroundAttachment = 'fixed';\n this.pre.style.mixBlendMode = 'difference';\n }\n\n render(scene, camera) {\n this.renderer.render(scene, camera);\n\n const w = this.canvas.width;\n const h = this.canvas.height;\n this.context.clearRect(0, 0, w, h);\n if (this.context && w && h) {\n this.context.drawImage(this.renderer.domElement, 0, 0, w, h);\n }\n\n this.asciify(this.context, w, h);\n this.hue();\n }\n\n onMouseMove(e) {\n this.mouse = { x: e.clientX * PX_RATIO, y: e.clientY * PX_RATIO };\n }\n\n get dx() {\n return this.mouse.x - this.center.x;\n }\n\n get dy() {\n return this.mouse.y - this.center.y;\n }\n\n hue() {\n const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI;\n this.deg += (deg - this.deg) * 0.075;\n this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)`;\n }\n\n asciify(ctx, w, h) {\n if (w && h) {\n const imgData = ctx.getImageData(0, 0, w, h).data;\n let str = '';\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = x * 4 + y * 4 * w;\n const [r, g, b, a] = [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];\n\n if (a === 0) {\n str += ' ';\n continue;\n }\n\n let gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;\n let idx = Math.floor((1 - gray) * (this.charset.length - 1));\n if (this.invert) idx = this.charset.length - idx - 1;\n str += this.charset[idx];\n }\n str += '\\n';\n }\n this.pre.innerHTML = str;\n }\n }\n\n dispose() {\n document.removeEventListener('mousemove', this.onMouseMove);\n }\n}\n\nclass CanvasTxt {\n constructor(txt, { fontSize = 200, fontFamily = 'Arial', color = '#fdf9f3' } = {}) {\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.txt = txt;\n this.fontSize = fontSize;\n this.fontFamily = fontFamily;\n this.color = color;\n\n this.font = `600 ${this.fontSize}px ${this.fontFamily}`;\n }\n\n resize() {\n this.context.font = this.font;\n const metrics = this.context.measureText(this.txt);\n\n const textWidth = Math.ceil(metrics.width) + 20;\n const textHeight = Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20;\n\n this.canvas.width = textWidth;\n this.canvas.height = textHeight;\n }\n\n render() {\n this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n this.context.fillStyle = this.color;\n this.context.font = this.font;\n\n const metrics = this.context.measureText(this.txt);\n const yPos = 10 + metrics.actualBoundingBoxAscent;\n\n this.context.fillText(this.txt, 10, yPos);\n }\n\n get width() {\n return this.canvas.width;\n }\n\n get height() {\n return this.canvas.height;\n }\n\n get texture() {\n return this.canvas;\n }\n}\n\nclass CanvAscii {\n constructor(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\n containerElem,\n width,\n height\n ) {\n this.textString = text;\n this.asciiFontSize = asciiFontSize;\n this.textFontSize = textFontSize;\n this.textColor = textColor;\n this.planeBaseHeight = planeBaseHeight;\n this.container = containerElem;\n this.width = width;\n this.height = height;\n this.enableWaves = enableWaves;\n\n this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);\n this.camera.position.z = 30;\n\n this.scene = new THREE.Scene();\n this.mouse = { x: this.width / 2, y: this.height / 2 };\n\n this.onMouseMove = this.onMouseMove.bind(this);\n }\n\n async init() {\n try {\n await document.fonts.load('600 200px \"IBM Plex Mono\"');\n await document.fonts.load('500 12px \"IBM Plex Mono\"');\n } catch (e) {}\n await document.fonts.ready;\n this.setMesh();\n this.setRenderer();\n }\n\n setMesh() {\n this.textCanvas = new CanvasTxt(this.textString, {\n fontSize: this.textFontSize,\n fontFamily: 'IBM Plex Mono',\n color: this.textColor\n });\n this.textCanvas.resize();\n this.textCanvas.render();\n\n this.texture = new THREE.CanvasTexture(this.textCanvas.texture);\n this.texture.minFilter = THREE.NearestFilter;\n\n const textAspect = this.textCanvas.width / this.textCanvas.height;\n const baseH = this.planeBaseHeight;\n const planeW = baseH * textAspect;\n const planeH = baseH;\n\n this.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36);\n this.material = new THREE.ShaderMaterial({\n vertexShader,\n fragmentShader,\n transparent: true,\n uniforms: {\n uTime: { value: 0 },\n mouse: { value: 1.0 },\n uTexture: { value: this.texture },\n uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }\n }\n });\n\n this.mesh = new THREE.Mesh(this.geometry, this.material);\n this.scene.add(this.mesh);\n }\n\n setRenderer() {\n this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });\n this.renderer.setPixelRatio(1);\n this.renderer.setClearColor(0x000000, 0);\n\n this.filter = new AsciiFilter(this.renderer, {\n fontFamily: 'IBM Plex Mono',\n fontSize: this.asciiFontSize,\n invert: true\n });\n\n this.container.appendChild(this.filter.domElement);\n this.setSize(this.width, this.height);\n\n this.container.addEventListener('mousemove', this.onMouseMove);\n this.container.addEventListener('touchmove', this.onMouseMove);\n }\n\n setSize(w, h) {\n this.width = w;\n this.height = h;\n\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n\n this.filter.setSize(w, h);\n\n this.center = { x: w / 2, y: h / 2 };\n }\n\n load() {\n this.animate();\n }\n\n onMouseMove(evt) {\n const e = evt.touches ? evt.touches[0] : evt;\n const bounds = this.container.getBoundingClientRect();\n const x = e.clientX - bounds.left;\n const y = e.clientY - bounds.top;\n this.mouse = { x, y };\n }\n\n animate() {\n const animateFrame = () => {\n this.animationFrameId = requestAnimationFrame(animateFrame);\n this.render();\n };\n animateFrame();\n }\n\n render() {\n const time = new Date().getTime() * 0.001;\n\n this.textCanvas.render();\n this.texture.needsUpdate = true;\n\n this.mesh.material.uniforms.uTime.value = Math.sin(time);\n\n this.updateRotation();\n this.filter.render(this.scene, this.camera);\n }\n\n updateRotation() {\n const x = Math.map(this.mouse.y, 0, this.height, 0.5, -0.5);\n const y = Math.map(this.mouse.x, 0, this.width, -0.5, 0.5);\n\n this.mesh.rotation.x += (x - this.mesh.rotation.x) * 0.05;\n this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05;\n }\n\n clear() {\n this.scene.traverse(obj => {\n if (obj.isMesh && typeof obj.material === 'object' && obj.material !== null) {\n Object.keys(obj.material).forEach(key => {\n const matProp = obj.material[key];\n if (matProp !== null && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n obj.material.dispose();\n obj.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n cancelAnimationFrame(this.animationFrameId);\n if (this.filter) {\n this.filter.dispose();\n if (this.filter.domElement.parentNode) {\n this.container.removeChild(this.filter.domElement);\n }\n }\n this.container.removeEventListener('mousemove', this.onMouseMove);\n this.container.removeEventListener('touchmove', this.onMouseMove);\n this.clear();\n if (this.renderer) {\n this.renderer.dispose();\n }\n }\n}\n\nexport default function ASCIIText({\n text = 'David!',\n asciiFontSize = 8,\n textFontSize = 200,\n textColor = '#fdf9f3',\n planeBaseHeight = 8,\n enableWaves = true\n}) {\n const containerRef = useRef(null);\n const asciiRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n let cancelled = false;\n let observer = null;\n let ro = null;\n\n const createAndInit = async (container, w, h) => {\n const instance = new CanvAscii(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\n container,\n w,\n h\n );\n await instance.init();\n return instance;\n };\n\n const setup = async () => {\n const { width, height } = containerRef.current.getBoundingClientRect();\n\n if (width === 0 || height === 0) {\n observer = new IntersectionObserver(\n async ([entry]) => {\n if (cancelled) return;\n if (entry.isIntersecting && entry.boundingClientRect.width > 0 && entry.boundingClientRect.height > 0) {\n const { width: w, height: h } = entry.boundingClientRect;\n observer.disconnect();\n observer = null;\n\n if (!cancelled) {\n asciiRef.current = await createAndInit(containerRef.current, w, h);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n }\n }\n }\n },\n { threshold: 0.1 }\n );\n observer.observe(containerRef.current);\n return;\n }\n\n asciiRef.current = await createAndInit(containerRef.current, width, height);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n\n ro = new ResizeObserver(entries => {\n if (!entries[0] || !asciiRef.current) return;\n const { width: w, height: h } = entries[0].contentRect;\n if (w > 0 && h > 0) {\n asciiRef.current.setSize(w, h);\n }\n });\n ro.observe(containerRef.current);\n }\n };\n\n setup();\n\n return () => {\n cancelled = true;\n if (observer) observer.disconnect();\n if (ro) ro.disconnect();\n if (asciiRef.current) {\n asciiRef.current.dispose();\n asciiRef.current = null;\n }\n };\n }, [text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves]);\n\n return (\n \n \n
\n );\n}\n"
+ "content": "// Component ported and enhanced from https://codepen.io/JuanFuentes/pen/eYEeoyE\n\nimport { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nconst vertexShader = `\nvarying vec2 vUv;\nuniform float uTime;\nuniform float mouse;\nuniform float uEnableWaves;\n\nvoid main() {\n vUv = uv;\n float time = uTime * 5.;\n\n float waveFactor = uEnableWaves;\n\n vec3 transformed = position;\n\n transformed.x += sin(time + position.y) * 0.5 * waveFactor;\n transformed.y += cos(time + position.z) * 0.15 * waveFactor;\n transformed.z += sin(time + position.x) * waveFactor;\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);\n}\n`;\n\nconst fragmentShader = `\nvarying vec2 vUv;\nuniform float mouse;\nuniform float uTime;\nuniform sampler2D uTexture;\n\nvoid main() {\n float time = uTime;\n vec2 pos = vUv;\n \n float move = sin(time + mouse) * 0.01;\n float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;\n float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;\n float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;\n float a = texture2D(uTexture, pos).a;\n gl_FragColor = vec4(r, g, b, a);\n}\n`;\n\nMath.map = function (n, start, stop, start2, stop2) {\n return ((n - start) / (stop - start)) * (stop2 - start2) + start2;\n};\n\nconst PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1;\n\nclass AsciiFilter {\n constructor(renderer, { fontSize, fontFamily, charset, invert } = {}) {\n this.renderer = renderer;\n this.domElement = document.createElement('div');\n this.domElement.style.position = 'absolute';\n this.domElement.style.top = '0';\n this.domElement.style.left = '0';\n this.domElement.style.width = '100%';\n this.domElement.style.height = '100%';\n\n this.pre = document.createElement('pre');\n this.domElement.appendChild(this.pre);\n\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.domElement.appendChild(this.canvas);\n\n this.deg = 0;\n this.invert = invert ?? true;\n this.fontSize = fontSize ?? 12;\n this.fontFamily = fontFamily ?? \"'Courier New', monospace\";\n this.charset = charset ?? ' .\\'`^\",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';\n\n this.context.webkitImageSmoothingEnabled = false;\n this.context.mozImageSmoothingEnabled = false;\n this.context.msImageSmoothingEnabled = false;\n this.context.imageSmoothingEnabled = false;\n\n this.onMouseMove = this.onMouseMove.bind(this);\n document.addEventListener('mousemove', this.onMouseMove);\n }\n\n setSize(width, height) {\n this.width = width;\n this.height = height;\n this.renderer.setSize(width, height);\n this.reset();\n\n this.center = { x: width / 2, y: height / 2 };\n this.mouse = { x: this.center.x, y: this.center.y };\n }\n\n reset() {\n this.context.font = `${this.fontSize}px ${this.fontFamily}`;\n const charWidth = this.context.measureText('A').width;\n\n this.cols = Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize)));\n this.rows = Math.floor(this.height / this.fontSize);\n\n this.canvas.width = this.cols;\n this.canvas.height = this.rows;\n this.pre.style.fontFamily = this.fontFamily;\n this.pre.style.fontSize = `${this.fontSize}px`;\n this.pre.style.margin = '0';\n this.pre.style.padding = '0';\n this.pre.style.lineHeight = '1em';\n this.pre.style.position = 'absolute';\n this.pre.style.left = '0';\n this.pre.style.top = '0';\n this.pre.style.zIndex = '9';\n this.pre.style.backgroundAttachment = 'fixed';\n this.pre.style.mixBlendMode = 'difference';\n }\n\n render(scene, camera) {\n this.renderer.render(scene, camera);\n\n const w = this.canvas.width;\n const h = this.canvas.height;\n this.context.clearRect(0, 0, w, h);\n if (this.context && w && h) {\n this.context.drawImage(this.renderer.domElement, 0, 0, w, h);\n }\n\n this.asciify(this.context, w, h);\n this.hue();\n }\n\n onMouseMove(e) {\n this.mouse = { x: e.clientX * PX_RATIO, y: e.clientY * PX_RATIO };\n }\n\n get dx() {\n return this.mouse.x - this.center.x;\n }\n\n get dy() {\n return this.mouse.y - this.center.y;\n }\n\n hue() {\n const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI;\n this.deg += (deg - this.deg) * 0.075;\n this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)`;\n }\n\n asciify(ctx, w, h) {\n if (w && h) {\n const imgData = ctx.getImageData(0, 0, w, h).data;\n let str = '';\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = x * 4 + y * 4 * w;\n const [r, g, b, a] = [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];\n\n if (a === 0) {\n str += ' ';\n continue;\n }\n\n let gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;\n let idx = Math.floor((1 - gray) * (this.charset.length - 1));\n if (this.invert) idx = this.charset.length - idx - 1;\n str += this.charset[idx];\n }\n str += '\\n';\n }\n this.pre.innerHTML = str;\n }\n }\n\n dispose() {\n document.removeEventListener('mousemove', this.onMouseMove);\n }\n}\n\nclass CanvasTxt {\n constructor(txt, { fontSize = 200, fontFamily = 'Arial', color = '#fdf9f3' } = {}) {\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.txt = txt;\n this.fontSize = fontSize;\n this.fontFamily = fontFamily;\n this.color = color;\n\n this.font = `600 ${this.fontSize}px ${this.fontFamily}`;\n }\n\n resize() {\n this.context.font = this.font;\n const metrics = this.context.measureText(this.txt);\n\n const textWidth = Math.ceil(metrics.width) + 20;\n const textHeight = Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20;\n\n this.canvas.width = textWidth;\n this.canvas.height = textHeight;\n }\n\n render() {\n this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n this.context.fillStyle = this.color;\n this.context.font = this.font;\n\n const metrics = this.context.measureText(this.txt);\n const yPos = 10 + metrics.actualBoundingBoxAscent;\n\n this.context.fillText(this.txt, 10, yPos);\n }\n\n get width() {\n return this.canvas.width;\n }\n\n get height() {\n return this.canvas.height;\n }\n\n get texture() {\n return this.canvas;\n }\n}\n\nclass CanvAscii {\n constructor(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\n containerElem,\n width,\n height\n ) {\n this.textString = text;\n this.asciiFontSize = asciiFontSize;\n this.textFontSize = textFontSize;\n this.textColor = textColor;\n this.planeBaseHeight = planeBaseHeight;\n this.container = containerElem;\n this.width = width;\n this.height = height;\n this.enableWaves = enableWaves;\n\n this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);\n this.camera.position.z = 30;\n\n this.scene = new THREE.Scene();\n this.mouse = { x: this.width / 2, y: this.height / 2 };\n\n this.onMouseMove = this.onMouseMove.bind(this);\n }\n\n async init() {\n try {\n await document.fonts.load('600 200px \"IBM Plex Mono\"');\n await document.fonts.load('500 12px \"IBM Plex Mono\"');\n } catch (e) {}\n await document.fonts.ready;\n this.setMesh();\n this.setRenderer();\n }\n\n setMesh() {\n this.textCanvas = new CanvasTxt(this.textString, {\n fontSize: this.textFontSize,\n fontFamily: 'IBM Plex Mono',\n color: this.textColor\n });\n this.textCanvas.resize();\n this.textCanvas.render();\n\n this.texture = new THREE.CanvasTexture(this.textCanvas.texture);\n this.texture.minFilter = THREE.NearestFilter;\n\n const textAspect = this.textCanvas.width / this.textCanvas.height;\n const baseH = this.planeBaseHeight;\n const planeW = baseH * textAspect;\n const planeH = baseH;\n\n this.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36);\n this.material = new THREE.ShaderMaterial({\n vertexShader,\n fragmentShader,\n transparent: true,\n uniforms: {\n uTime: { value: 0 },\n mouse: { value: 1.0 },\n uTexture: { value: this.texture },\n uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }\n }\n });\n\n this.mesh = new THREE.Mesh(this.geometry, this.material);\n this.scene.add(this.mesh);\n }\n\n setRenderer() {\n this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });\n this.renderer.setPixelRatio(1);\n this.renderer.setClearColor(0x000000, 0);\n\n this.filter = new AsciiFilter(this.renderer, {\n fontFamily: 'IBM Plex Mono',\n fontSize: this.asciiFontSize,\n invert: true\n });\n\n this.container.appendChild(this.filter.domElement);\n this.setSize(this.width, this.height);\n\n this.container.addEventListener('mousemove', this.onMouseMove);\n this.container.addEventListener('touchmove', this.onMouseMove);\n }\n\n setSize(w, h) {\n this.width = w;\n this.height = h;\n\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n\n this.filter.setSize(w, h);\n\n this.center = { x: w / 2, y: h / 2 };\n }\n\n load() {\n this.animate();\n }\n\n onMouseMove(evt) {\n const e = evt.touches ? evt.touches[0] : evt;\n const bounds = this.container.getBoundingClientRect();\n const x = e.clientX - bounds.left;\n const y = e.clientY - bounds.top;\n this.mouse = { x, y };\n }\n\n animate() {\n const animateFrame = () => {\n this.animationFrameId = requestAnimationFrame(animateFrame);\n this.render();\n };\n animateFrame();\n }\n\n render() {\n const time = new Date().getTime() * 0.001;\n\n this.textCanvas.render();\n this.texture.needsUpdate = true;\n\n this.mesh.material.uniforms.uTime.value = Math.sin(time);\n\n this.updateRotation();\n this.filter.render(this.scene, this.camera);\n }\n\n updateRotation() {\n const x = Math.map(this.mouse.y, 0, this.height, 0.5, -0.5);\n const y = Math.map(this.mouse.x, 0, this.width, -0.5, 0.5);\n\n this.mesh.rotation.x += (x - this.mesh.rotation.x) * 0.05;\n this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05;\n }\n\n clear() {\n this.scene.traverse(obj => {\n if (obj.isMesh && typeof obj.material === 'object' && obj.material !== null) {\n Object.keys(obj.material).forEach(key => {\n const matProp = obj.material[key];\n if (matProp !== null && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n obj.material.dispose();\n obj.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n cancelAnimationFrame(this.animationFrameId);\n if (this.filter) {\n this.filter.dispose();\n if (this.filter.domElement.parentNode) {\n this.container.removeChild(this.filter.domElement);\n }\n }\n this.container.removeEventListener('mousemove', this.onMouseMove);\n this.container.removeEventListener('touchmove', this.onMouseMove);\n this.clear();\n if (this.renderer) {\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n }\n }\n}\n\nexport default function ASCIIText({\n text = 'David!',\n asciiFontSize = 8,\n textFontSize = 200,\n textColor = '#fdf9f3',\n planeBaseHeight = 8,\n enableWaves = true\n}) {\n const containerRef = useRef(null);\n const asciiRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n let cancelled = false;\n let observer = null;\n let ro = null;\n\n const createAndInit = async (container, w, h) => {\n const instance = new CanvAscii(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\n container,\n w,\n h\n );\n await instance.init();\n return instance;\n };\n\n const setup = async () => {\n const { width, height } = containerRef.current.getBoundingClientRect();\n\n if (width === 0 || height === 0) {\n observer = new IntersectionObserver(\n async ([entry]) => {\n if (cancelled) return;\n if (entry.isIntersecting && entry.boundingClientRect.width > 0 && entry.boundingClientRect.height > 0) {\n const { width: w, height: h } = entry.boundingClientRect;\n observer.disconnect();\n observer = null;\n\n if (!cancelled) {\n asciiRef.current = await createAndInit(containerRef.current, w, h);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n }\n }\n }\n },\n { threshold: 0.1 }\n );\n observer.observe(containerRef.current);\n return;\n }\n\n asciiRef.current = await createAndInit(containerRef.current, width, height);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n\n ro = new ResizeObserver(entries => {\n if (!entries[0] || !asciiRef.current) return;\n const { width: w, height: h } = entries[0].contentRect;\n if (w > 0 && h > 0) {\n asciiRef.current.setSize(w, h);\n }\n });\n ro.observe(containerRef.current);\n }\n };\n\n setup();\n\n return () => {\n cancelled = true;\n if (observer) observer.disconnect();\n if (ro) ro.disconnect();\n if (asciiRef.current) {\n asciiRef.current.dispose();\n asciiRef.current = null;\n }\n };\n }, [text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves]);\n\n return (\n \n \n
\n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/ASCIIText-TS-CSS.json b/public/r/ASCIIText-TS-CSS.json
index 019375bc..c298787b 100644
--- a/public/r/ASCIIText-TS-CSS.json
+++ b/public/r/ASCIIText-TS-CSS.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "ASCIIText/ASCIIText.tsx",
- "content": "// Component ported and enhanced from https://codepen.io/JuanFuentes/pen/eYEeoyE\n\nimport { useRef, useEffect } from 'react';\nimport * as THREE from 'three';\n\nconst vertexShader = `\nvarying vec2 vUv;\nuniform float uTime;\nuniform float mouse;\nuniform float uEnableWaves;\n\nvoid main() {\n vUv = uv;\n float time = uTime * 5.;\n\n float waveFactor = uEnableWaves;\n\n vec3 transformed = position;\n\n transformed.x += sin(time + position.y) * 0.5 * waveFactor;\n transformed.y += cos(time + position.z) * 0.15 * waveFactor;\n transformed.z += sin(time + position.x) * waveFactor;\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);\n}\n`;\n\nconst fragmentShader = `\nvarying vec2 vUv;\nuniform float mouse;\nuniform float uTime;\nuniform sampler2D uTexture;\n\nvoid main() {\n float time = uTime;\n vec2 pos = vUv;\n \n float move = sin(time + mouse) * 0.01;\n float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;\n float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;\n float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;\n float a = texture2D(uTexture, pos).a;\n gl_FragColor = vec4(r, g, b, a);\n}\n`;\n\nfunction map(n: number, start: number, stop: number, start2: number, stop2: number) {\n return ((n - start) / (stop - start)) * (stop2 - start2) + start2;\n}\n\nconst PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1;\n\ninterface AsciiFilterOptions {\n fontSize?: number;\n fontFamily?: string;\n charset?: string;\n invert?: boolean;\n}\n\nclass AsciiFilter {\n renderer!: THREE.WebGLRenderer;\n domElement: HTMLDivElement;\n pre: HTMLPreElement;\n canvas: HTMLCanvasElement;\n context: CanvasRenderingContext2D | null;\n deg: number;\n invert: boolean;\n fontSize: number;\n fontFamily: string;\n charset: string;\n width: number = 0;\n height: number = 0;\n center: { x: number; y: number } = { x: 0, y: 0 };\n mouse: { x: number; y: number } = { x: 0, y: 0 };\n cols: number = 0;\n rows: number = 0;\n\n constructor(renderer: THREE.WebGLRenderer, { fontSize, fontFamily, charset, invert }: AsciiFilterOptions = {}) {\n this.renderer = renderer;\n this.domElement = document.createElement('div');\n this.domElement.style.position = 'absolute';\n this.domElement.style.top = '0';\n this.domElement.style.left = '0';\n this.domElement.style.width = '100%';\n this.domElement.style.height = '100%';\n\n this.pre = document.createElement('pre');\n this.domElement.appendChild(this.pre);\n\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.domElement.appendChild(this.canvas);\n\n this.deg = 0;\n this.invert = invert ?? true;\n this.fontSize = fontSize ?? 12;\n this.fontFamily = fontFamily ?? \"'Courier New', monospace\";\n this.charset = charset ?? ' .\\'`^\",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';\n\n if (this.context) {\n this.context.imageSmoothingEnabled = false;\n this.context.imageSmoothingEnabled = false;\n }\n\n this.onMouseMove = this.onMouseMove.bind(this);\n document.addEventListener('mousemove', this.onMouseMove);\n }\n\n setSize(width: number, height: number) {\n this.width = width;\n this.height = height;\n this.renderer.setSize(width, height);\n this.reset();\n\n this.center = { x: width / 2, y: height / 2 };\n this.mouse = { x: this.center.x, y: this.center.y };\n }\n\n reset() {\n if (this.context) {\n this.context.font = `${this.fontSize}px ${this.fontFamily}`;\n const charWidth = this.context.measureText('A').width;\n\n this.cols = Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize)));\n this.rows = Math.floor(this.height / this.fontSize);\n\n this.canvas.width = this.cols;\n this.canvas.height = this.rows;\n this.pre.style.fontFamily = this.fontFamily;\n this.pre.style.fontSize = `${this.fontSize}px`;\n this.pre.style.margin = '0';\n this.pre.style.padding = '0';\n this.pre.style.lineHeight = '1em';\n this.pre.style.position = 'absolute';\n this.pre.style.left = '50%';\n this.pre.style.top = '50%';\n this.pre.style.transform = 'translate(-50%, -50%)';\n this.pre.style.zIndex = '9';\n this.pre.style.backgroundAttachment = 'fixed';\n this.pre.style.mixBlendMode = 'difference';\n }\n }\n\n render(scene: THREE.Scene, camera: THREE.Camera) {\n this.renderer.render(scene, camera);\n\n const w = this.canvas.width;\n const h = this.canvas.height;\n if (this.context) {\n this.context.clearRect(0, 0, w, h);\n this.context.drawImage(this.renderer.domElement, 0, 0, w, h);\n this.asciify(this.context, w, h);\n this.hue();\n }\n }\n\n onMouseMove(e: MouseEvent) {\n this.mouse = { x: e.clientX * PX_RATIO, y: e.clientY * PX_RATIO };\n }\n\n get dx() {\n return this.mouse.x - this.center.x;\n }\n\n get dy() {\n return this.mouse.y - this.center.y;\n }\n\n hue() {\n const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI;\n this.deg += (deg - this.deg) * 0.075;\n this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)`;\n }\n\n asciify(ctx: CanvasRenderingContext2D, w: number, h: number) {\n const imgData = ctx.getImageData(0, 0, w, h).data;\n let str = '';\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = x * 4 + y * 4 * w;\n const [r, g, b, a] = [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];\n\n if (a === 0) {\n str += ' ';\n continue;\n }\n\n let gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;\n let idx = Math.floor((1 - gray) * (this.charset.length - 1));\n if (this.invert) idx = this.charset.length - idx - 1;\n str += this.charset[idx];\n }\n str += '\\n';\n }\n this.pre.innerHTML = str;\n }\n\n dispose() {\n document.removeEventListener('mousemove', this.onMouseMove);\n }\n}\n\ninterface CanvasTxtOptions {\n fontSize?: number;\n fontFamily?: string;\n color?: string;\n}\n\nclass CanvasTxt {\n canvas: HTMLCanvasElement;\n context: CanvasRenderingContext2D | null;\n txt: string;\n fontSize: number;\n fontFamily: string;\n color: string;\n font: string;\n\n constructor(txt: string, { fontSize = 200, fontFamily = 'Arial', color = '#fdf9f3' }: CanvasTxtOptions = {}) {\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.txt = txt;\n this.fontSize = fontSize;\n this.fontFamily = fontFamily;\n this.color = color;\n\n this.font = `600 ${this.fontSize}px ${this.fontFamily}`;\n }\n\n resize() {\n if (this.context) {\n this.context.font = this.font;\n const metrics = this.context.measureText(this.txt);\n\n const textWidth = Math.ceil(metrics.width) + 20;\n const textHeight = Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20;\n\n this.canvas.width = textWidth;\n this.canvas.height = textHeight;\n }\n }\n\n render() {\n if (this.context) {\n this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n this.context.fillStyle = this.color;\n this.context.font = this.font;\n\n const metrics = this.context.measureText(this.txt);\n const yPos = 10 + metrics.actualBoundingBoxAscent;\n\n this.context.fillText(this.txt, 10, yPos);\n }\n }\n\n get width() {\n return this.canvas.width;\n }\n\n get height() {\n return this.canvas.height;\n }\n\n get texture() {\n return this.canvas;\n }\n}\n\ninterface CanvAsciiOptions {\n text: string;\n asciiFontSize: number;\n textFontSize: number;\n textColor: string;\n planeBaseHeight: number;\n enableWaves: boolean;\n}\n\nclass CanvAscii {\n textString: string;\n asciiFontSize: number;\n textFontSize: number;\n textColor: string;\n planeBaseHeight: number;\n container: HTMLElement;\n width: number;\n height: number;\n enableWaves: boolean;\n camera: THREE.PerspectiveCamera;\n scene: THREE.Scene;\n mouse: { x: number; y: number };\n textCanvas!: CanvasTxt;\n texture!: THREE.CanvasTexture;\n geometry: THREE.PlaneGeometry | undefined;\n material: THREE.ShaderMaterial | undefined;\n mesh!: THREE.Mesh;\n renderer!: THREE.WebGLRenderer;\n filter!: AsciiFilter;\n center: { x: number; y: number } = { x: 0, y: 0 };\n animationFrameId: number = 0;\n\n constructor(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves }: CanvAsciiOptions,\n containerElem: HTMLElement,\n width: number,\n height: number\n ) {\n this.textString = text;\n this.asciiFontSize = asciiFontSize;\n this.textFontSize = textFontSize;\n this.textColor = textColor;\n this.planeBaseHeight = planeBaseHeight;\n this.container = containerElem;\n this.width = width;\n this.height = height;\n this.enableWaves = enableWaves;\n\n this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);\n this.camera.position.z = 30;\n\n this.scene = new THREE.Scene();\n this.mouse = { x: this.width / 2, y: this.height / 2 };\n\n this.onMouseMove = this.onMouseMove.bind(this);\n }\n\n async init() {\n try {\n await document.fonts.load('600 200px \"IBM Plex Mono\"');\n await document.fonts.load('500 12px \"IBM Plex Mono\"');\n } catch (e) {}\n await document.fonts.ready;\n this.setMesh();\n this.setRenderer();\n }\n\n setMesh() {\n this.textCanvas = new CanvasTxt(this.textString, {\n fontSize: this.textFontSize,\n fontFamily: 'IBM Plex Mono',\n color: this.textColor\n });\n this.textCanvas.resize();\n this.textCanvas.render();\n\n this.texture = new THREE.CanvasTexture(this.textCanvas.texture);\n this.texture.minFilter = THREE.NearestFilter;\n\n const textAspect = this.textCanvas.width / this.textCanvas.height;\n const baseH = this.planeBaseHeight;\n const planeW = baseH * textAspect;\n const planeH = baseH;\n\n this.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36);\n this.material = new THREE.ShaderMaterial({\n vertexShader,\n fragmentShader,\n transparent: true,\n uniforms: {\n uTime: { value: 0 },\n mouse: { value: 1.0 },\n uTexture: { value: this.texture },\n uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }\n }\n });\n\n this.mesh = new THREE.Mesh(this.geometry, this.material);\n this.scene.add(this.mesh);\n }\n\n setRenderer() {\n this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });\n this.renderer.setPixelRatio(1);\n this.renderer.setClearColor(0x000000, 0);\n\n this.filter = new AsciiFilter(this.renderer, {\n fontFamily: 'IBM Plex Mono',\n fontSize: this.asciiFontSize,\n invert: true\n });\n\n this.container.appendChild(this.filter.domElement);\n this.setSize(this.width, this.height);\n\n this.container.addEventListener('mousemove', this.onMouseMove);\n this.container.addEventListener('touchmove', this.onMouseMove);\n }\n\n setSize(w: number, h: number) {\n this.width = w;\n this.height = h;\n\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n\n this.filter.setSize(w, h);\n\n this.center = { x: w / 2, y: h / 2 };\n }\n\n load() {\n this.animate();\n }\n\n onMouseMove(evt: MouseEvent | TouchEvent) {\n const e = (evt as TouchEvent).touches ? (evt as TouchEvent).touches[0] : (evt as MouseEvent);\n const bounds = this.container.getBoundingClientRect();\n const x = e.clientX - bounds.left;\n const y = e.clientY - bounds.top;\n this.mouse = { x, y };\n }\n\n animate() {\n const animateFrame = () => {\n this.animationFrameId = requestAnimationFrame(animateFrame);\n this.render();\n };\n animateFrame();\n }\n\n render() {\n const time = new Date().getTime() * 0.001;\n\n this.textCanvas.render();\n this.texture.needsUpdate = true;\n\n (this.mesh.material as THREE.ShaderMaterial).uniforms.uTime.value = Math.sin(time);\n\n this.updateRotation();\n this.filter.render(this.scene, this.camera);\n }\n\n updateRotation() {\n const x = map(this.mouse.y, 0, this.height, 0.5, -0.5);\n const y = map(this.mouse.x, 0, this.width, -0.5, 0.5);\n\n this.mesh.rotation.x += (x - this.mesh.rotation.x) * 0.05;\n this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05;\n }\n\n clear() {\n this.scene.traverse(object => {\n const obj = object as unknown as THREE.Mesh;\n if (!obj.isMesh) return;\n [obj.material].flat().forEach(material => {\n material.dispose();\n Object.keys(material).forEach(key => {\n const matProp = material[key as keyof typeof material];\n if (matProp && typeof matProp === 'object' && 'dispose' in matProp && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n });\n obj.geometry.dispose();\n });\n this.scene.clear();\n }\n\n dispose() {\n cancelAnimationFrame(this.animationFrameId);\n if (this.filter) {\n this.filter.dispose();\n if (this.filter.domElement.parentNode) {\n this.container.removeChild(this.filter.domElement);\n }\n }\n this.container.removeEventListener('mousemove', this.onMouseMove);\n this.container.removeEventListener('touchmove', this.onMouseMove);\n this.clear();\n if (this.renderer) {\n this.renderer.dispose();\n }\n }\n}\n\ninterface ASCIITextProps {\n text?: string;\n asciiFontSize?: number;\n textFontSize?: number;\n textColor?: string;\n planeBaseHeight?: number;\n enableWaves?: boolean;\n}\n\nexport default function ASCIIText({\n text = 'David!',\n asciiFontSize = 8,\n textFontSize = 200,\n textColor = '#fdf9f3',\n planeBaseHeight = 8,\n enableWaves = true\n}: ASCIITextProps) {\n const containerRef = useRef(null);\n const asciiRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n let cancelled = false;\n let observer: IntersectionObserver | null = null;\n let ro: ResizeObserver | null = null;\n\n const createAndInit = async (container: HTMLDivElement, w: number, h: number) => {\n const instance = new CanvAscii(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\n container,\n w,\n h\n );\n await instance.init();\n return instance;\n };\n\n const setup = async () => {\n const { width, height } = containerRef.current!.getBoundingClientRect();\n\n if (width === 0 || height === 0) {\n observer = new IntersectionObserver(\n async ([entry]) => {\n if (cancelled) return;\n if (entry.isIntersecting && entry.boundingClientRect.width > 0 && entry.boundingClientRect.height > 0) {\n const { width: w, height: h } = entry.boundingClientRect;\n observer?.disconnect();\n observer = null;\n\n if (!cancelled) {\n asciiRef.current = await createAndInit(containerRef.current!, w, h);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n }\n }\n }\n },\n { threshold: 0.1 }\n );\n observer.observe(containerRef.current!);\n return;\n }\n\n asciiRef.current = await createAndInit(containerRef.current!, width, height);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n\n ro = new ResizeObserver(entries => {\n if (!entries[0] || !asciiRef.current) return;\n const { width: w, height: h } = entries[0].contentRect;\n if (w > 0 && h > 0) {\n asciiRef.current.setSize(w, h);\n }\n });\n ro.observe(containerRef.current!);\n }\n };\n\n setup();\n\n return () => {\n cancelled = true;\n if (observer) observer.disconnect();\n if (ro) ro.disconnect();\n if (asciiRef.current) {\n asciiRef.current.dispose();\n asciiRef.current = null;\n }\n };\n }, [text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves]);\n\n return (\n \n \n
\n );\n}\n"
+ "content": "// Component ported and enhanced from https://codepen.io/JuanFuentes/pen/eYEeoyE\n\nimport { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nconst vertexShader = `\nvarying vec2 vUv;\nuniform float uTime;\nuniform float mouse;\nuniform float uEnableWaves;\n\nvoid main() {\n vUv = uv;\n float time = uTime * 5.;\n\n float waveFactor = uEnableWaves;\n\n vec3 transformed = position;\n\n transformed.x += sin(time + position.y) * 0.5 * waveFactor;\n transformed.y += cos(time + position.z) * 0.15 * waveFactor;\n transformed.z += sin(time + position.x) * waveFactor;\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);\n}\n`;\n\nconst fragmentShader = `\nvarying vec2 vUv;\nuniform float mouse;\nuniform float uTime;\nuniform sampler2D uTexture;\n\nvoid main() {\n float time = uTime;\n vec2 pos = vUv;\n \n float move = sin(time + mouse) * 0.01;\n float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;\n float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;\n float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;\n float a = texture2D(uTexture, pos).a;\n gl_FragColor = vec4(r, g, b, a);\n}\n`;\n\nfunction map(n: number, start: number, stop: number, start2: number, stop2: number) {\n return ((n - start) / (stop - start)) * (stop2 - start2) + start2;\n}\n\nconst PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1;\n\ninterface AsciiFilterOptions {\n fontSize?: number;\n fontFamily?: string;\n charset?: string;\n invert?: boolean;\n}\n\nclass AsciiFilter {\n renderer!: THREE.WebGLRenderer;\n domElement: HTMLDivElement;\n pre: HTMLPreElement;\n canvas: HTMLCanvasElement;\n context: CanvasRenderingContext2D | null;\n deg: number;\n invert: boolean;\n fontSize: number;\n fontFamily: string;\n charset: string;\n width: number = 0;\n height: number = 0;\n center: { x: number; y: number } = { x: 0, y: 0 };\n mouse: { x: number; y: number } = { x: 0, y: 0 };\n cols: number = 0;\n rows: number = 0;\n\n constructor(renderer: THREE.WebGLRenderer, { fontSize, fontFamily, charset, invert }: AsciiFilterOptions = {}) {\n this.renderer = renderer;\n this.domElement = document.createElement('div');\n this.domElement.style.position = 'absolute';\n this.domElement.style.top = '0';\n this.domElement.style.left = '0';\n this.domElement.style.width = '100%';\n this.domElement.style.height = '100%';\n\n this.pre = document.createElement('pre');\n this.domElement.appendChild(this.pre);\n\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.domElement.appendChild(this.canvas);\n\n this.deg = 0;\n this.invert = invert ?? true;\n this.fontSize = fontSize ?? 12;\n this.fontFamily = fontFamily ?? \"'Courier New', monospace\";\n this.charset = charset ?? ' .\\'`^\",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';\n\n if (this.context) {\n this.context.imageSmoothingEnabled = false;\n this.context.imageSmoothingEnabled = false;\n }\n\n this.onMouseMove = this.onMouseMove.bind(this);\n document.addEventListener('mousemove', this.onMouseMove);\n }\n\n setSize(width: number, height: number) {\n this.width = width;\n this.height = height;\n this.renderer.setSize(width, height);\n this.reset();\n\n this.center = { x: width / 2, y: height / 2 };\n this.mouse = { x: this.center.x, y: this.center.y };\n }\n\n reset() {\n if (this.context) {\n this.context.font = `${this.fontSize}px ${this.fontFamily}`;\n const charWidth = this.context.measureText('A').width;\n\n this.cols = Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize)));\n this.rows = Math.floor(this.height / this.fontSize);\n\n this.canvas.width = this.cols;\n this.canvas.height = this.rows;\n this.pre.style.fontFamily = this.fontFamily;\n this.pre.style.fontSize = `${this.fontSize}px`;\n this.pre.style.margin = '0';\n this.pre.style.padding = '0';\n this.pre.style.lineHeight = '1em';\n this.pre.style.position = 'absolute';\n this.pre.style.left = '50%';\n this.pre.style.top = '50%';\n this.pre.style.transform = 'translate(-50%, -50%)';\n this.pre.style.zIndex = '9';\n this.pre.style.backgroundAttachment = 'fixed';\n this.pre.style.mixBlendMode = 'difference';\n }\n }\n\n render(scene: THREE.Scene, camera: THREE.Camera) {\n this.renderer.render(scene, camera);\n\n const w = this.canvas.width;\n const h = this.canvas.height;\n if (this.context) {\n this.context.clearRect(0, 0, w, h);\n this.context.drawImage(this.renderer.domElement, 0, 0, w, h);\n this.asciify(this.context, w, h);\n this.hue();\n }\n }\n\n onMouseMove(e: MouseEvent) {\n this.mouse = { x: e.clientX * PX_RATIO, y: e.clientY * PX_RATIO };\n }\n\n get dx() {\n return this.mouse.x - this.center.x;\n }\n\n get dy() {\n return this.mouse.y - this.center.y;\n }\n\n hue() {\n const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI;\n this.deg += (deg - this.deg) * 0.075;\n this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)`;\n }\n\n asciify(ctx: CanvasRenderingContext2D, w: number, h: number) {\n const imgData = ctx.getImageData(0, 0, w, h).data;\n let str = '';\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = x * 4 + y * 4 * w;\n const [r, g, b, a] = [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];\n\n if (a === 0) {\n str += ' ';\n continue;\n }\n\n let gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;\n let idx = Math.floor((1 - gray) * (this.charset.length - 1));\n if (this.invert) idx = this.charset.length - idx - 1;\n str += this.charset[idx];\n }\n str += '\\n';\n }\n this.pre.innerHTML = str;\n }\n\n dispose() {\n document.removeEventListener('mousemove', this.onMouseMove);\n }\n}\n\ninterface CanvasTxtOptions {\n fontSize?: number;\n fontFamily?: string;\n color?: string;\n}\n\nclass CanvasTxt {\n canvas: HTMLCanvasElement;\n context: CanvasRenderingContext2D | null;\n txt: string;\n fontSize: number;\n fontFamily: string;\n color: string;\n font: string;\n\n constructor(txt: string, { fontSize = 200, fontFamily = 'Arial', color = '#fdf9f3' }: CanvasTxtOptions = {}) {\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.txt = txt;\n this.fontSize = fontSize;\n this.fontFamily = fontFamily;\n this.color = color;\n\n this.font = `600 ${this.fontSize}px ${this.fontFamily}`;\n }\n\n resize() {\n if (this.context) {\n this.context.font = this.font;\n const metrics = this.context.measureText(this.txt);\n\n const textWidth = Math.ceil(metrics.width) + 20;\n const textHeight = Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20;\n\n this.canvas.width = textWidth;\n this.canvas.height = textHeight;\n }\n }\n\n render() {\n if (this.context) {\n this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n this.context.fillStyle = this.color;\n this.context.font = this.font;\n\n const metrics = this.context.measureText(this.txt);\n const yPos = 10 + metrics.actualBoundingBoxAscent;\n\n this.context.fillText(this.txt, 10, yPos);\n }\n }\n\n get width() {\n return this.canvas.width;\n }\n\n get height() {\n return this.canvas.height;\n }\n\n get texture() {\n return this.canvas;\n }\n}\n\ninterface CanvAsciiOptions {\n text: string;\n asciiFontSize: number;\n textFontSize: number;\n textColor: string;\n planeBaseHeight: number;\n enableWaves: boolean;\n}\n\nclass CanvAscii {\n textString: string;\n asciiFontSize: number;\n textFontSize: number;\n textColor: string;\n planeBaseHeight: number;\n container: HTMLElement;\n width: number;\n height: number;\n enableWaves: boolean;\n camera: THREE.PerspectiveCamera;\n scene: THREE.Scene;\n mouse: { x: number; y: number };\n textCanvas!: CanvasTxt;\n texture!: THREE.CanvasTexture;\n geometry: THREE.PlaneGeometry | undefined;\n material: THREE.ShaderMaterial | undefined;\n mesh!: THREE.Mesh;\n renderer!: THREE.WebGLRenderer;\n filter!: AsciiFilter;\n center: { x: number; y: number } = { x: 0, y: 0 };\n animationFrameId: number = 0;\n\n constructor(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves }: CanvAsciiOptions,\n containerElem: HTMLElement,\n width: number,\n height: number\n ) {\n this.textString = text;\n this.asciiFontSize = asciiFontSize;\n this.textFontSize = textFontSize;\n this.textColor = textColor;\n this.planeBaseHeight = planeBaseHeight;\n this.container = containerElem;\n this.width = width;\n this.height = height;\n this.enableWaves = enableWaves;\n\n this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);\n this.camera.position.z = 30;\n\n this.scene = new THREE.Scene();\n this.mouse = { x: this.width / 2, y: this.height / 2 };\n\n this.onMouseMove = this.onMouseMove.bind(this);\n }\n\n async init() {\n try {\n await document.fonts.load('600 200px \"IBM Plex Mono\"');\n await document.fonts.load('500 12px \"IBM Plex Mono\"');\n } catch (e) {}\n await document.fonts.ready;\n this.setMesh();\n this.setRenderer();\n }\n\n setMesh() {\n this.textCanvas = new CanvasTxt(this.textString, {\n fontSize: this.textFontSize,\n fontFamily: 'IBM Plex Mono',\n color: this.textColor\n });\n this.textCanvas.resize();\n this.textCanvas.render();\n\n this.texture = new THREE.CanvasTexture(this.textCanvas.texture);\n this.texture.minFilter = THREE.NearestFilter;\n\n const textAspect = this.textCanvas.width / this.textCanvas.height;\n const baseH = this.planeBaseHeight;\n const planeW = baseH * textAspect;\n const planeH = baseH;\n\n this.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36);\n this.material = new THREE.ShaderMaterial({\n vertexShader,\n fragmentShader,\n transparent: true,\n uniforms: {\n uTime: { value: 0 },\n mouse: { value: 1.0 },\n uTexture: { value: this.texture },\n uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }\n }\n });\n\n this.mesh = new THREE.Mesh(this.geometry, this.material);\n this.scene.add(this.mesh);\n }\n\n setRenderer() {\n this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });\n this.renderer.setPixelRatio(1);\n this.renderer.setClearColor(0x000000, 0);\n\n this.filter = new AsciiFilter(this.renderer, {\n fontFamily: 'IBM Plex Mono',\n fontSize: this.asciiFontSize,\n invert: true\n });\n\n this.container.appendChild(this.filter.domElement);\n this.setSize(this.width, this.height);\n\n this.container.addEventListener('mousemove', this.onMouseMove);\n this.container.addEventListener('touchmove', this.onMouseMove);\n }\n\n setSize(w: number, h: number) {\n this.width = w;\n this.height = h;\n\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n\n this.filter.setSize(w, h);\n\n this.center = { x: w / 2, y: h / 2 };\n }\n\n load() {\n this.animate();\n }\n\n onMouseMove(evt: MouseEvent | TouchEvent) {\n const e = (evt as TouchEvent).touches ? (evt as TouchEvent).touches[0] : (evt as MouseEvent);\n const bounds = this.container.getBoundingClientRect();\n const x = e.clientX - bounds.left;\n const y = e.clientY - bounds.top;\n this.mouse = { x, y };\n }\n\n animate() {\n const animateFrame = () => {\n this.animationFrameId = requestAnimationFrame(animateFrame);\n this.render();\n };\n animateFrame();\n }\n\n render() {\n const time = new Date().getTime() * 0.001;\n\n this.textCanvas.render();\n this.texture.needsUpdate = true;\n\n (this.mesh.material as THREE.ShaderMaterial).uniforms.uTime.value = Math.sin(time);\n\n this.updateRotation();\n this.filter.render(this.scene, this.camera);\n }\n\n updateRotation() {\n const x = map(this.mouse.y, 0, this.height, 0.5, -0.5);\n const y = map(this.mouse.x, 0, this.width, -0.5, 0.5);\n\n this.mesh.rotation.x += (x - this.mesh.rotation.x) * 0.05;\n this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05;\n }\n\n clear() {\n this.scene.traverse(object => {\n const obj = object as unknown as THREE.Mesh;\n if (!obj.isMesh) return;\n [obj.material].flat().forEach(material => {\n material.dispose();\n Object.keys(material).forEach(key => {\n const matProp = material[key as keyof typeof material];\n if (matProp && typeof matProp === 'object' && 'dispose' in matProp && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n });\n obj.geometry.dispose();\n });\n this.scene.clear();\n }\n\n dispose() {\n cancelAnimationFrame(this.animationFrameId);\n if (this.filter) {\n this.filter.dispose();\n if (this.filter.domElement.parentNode) {\n this.container.removeChild(this.filter.domElement);\n }\n }\n this.container.removeEventListener('mousemove', this.onMouseMove);\n this.container.removeEventListener('touchmove', this.onMouseMove);\n this.clear();\n if (this.renderer) {\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n }\n }\n}\n\ninterface ASCIITextProps {\n text?: string;\n asciiFontSize?: number;\n textFontSize?: number;\n textColor?: string;\n planeBaseHeight?: number;\n enableWaves?: boolean;\n}\n\nexport default function ASCIIText({\n text = 'David!',\n asciiFontSize = 8,\n textFontSize = 200,\n textColor = '#fdf9f3',\n planeBaseHeight = 8,\n enableWaves = true\n}: ASCIITextProps) {\n const containerRef = useRef(null);\n const asciiRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n let cancelled = false;\n let observer: IntersectionObserver | null = null;\n let ro: ResizeObserver | null = null;\n\n const createAndInit = async (container: HTMLDivElement, w: number, h: number) => {\n const instance = new CanvAscii(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\n container,\n w,\n h\n );\n await instance.init();\n return instance;\n };\n\n const setup = async () => {\n const { width, height } = containerRef.current!.getBoundingClientRect();\n\n if (width === 0 || height === 0) {\n observer = new IntersectionObserver(\n async ([entry]) => {\n if (cancelled) return;\n if (entry.isIntersecting && entry.boundingClientRect.width > 0 && entry.boundingClientRect.height > 0) {\n const { width: w, height: h } = entry.boundingClientRect;\n observer?.disconnect();\n observer = null;\n\n if (!cancelled) {\n asciiRef.current = await createAndInit(containerRef.current!, w, h);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n }\n }\n }\n },\n { threshold: 0.1 }\n );\n observer.observe(containerRef.current!);\n return;\n }\n\n asciiRef.current = await createAndInit(containerRef.current!, width, height);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n\n ro = new ResizeObserver(entries => {\n if (!entries[0] || !asciiRef.current) return;\n const { width: w, height: h } = entries[0].contentRect;\n if (w > 0 && h > 0) {\n asciiRef.current.setSize(w, h);\n }\n });\n ro.observe(containerRef.current!);\n }\n };\n\n setup();\n\n return () => {\n cancelled = true;\n if (observer) observer.disconnect();\n if (ro) ro.disconnect();\n if (asciiRef.current) {\n asciiRef.current.dispose();\n asciiRef.current = null;\n }\n };\n }, [text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves]);\n\n return (\n \n \n
\n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/ASCIIText-TS-TW.json b/public/r/ASCIIText-TS-TW.json
index fa7a1f9f..bb16ee55 100644
--- a/public/r/ASCIIText-TS-TW.json
+++ b/public/r/ASCIIText-TS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "ASCIIText/ASCIIText.tsx",
- "content": "// Component ported and enhanced from https://codepen.io/JuanFuentes/pen/eYEeoyE\n\nimport { useRef, useEffect } from 'react';\nimport * as THREE from 'three';\n\nconst vertexShader = `\nvarying vec2 vUv;\nuniform float uTime;\nuniform float mouse;\nuniform float uEnableWaves;\n\nvoid main() {\n vUv = uv;\n float time = uTime * 5.;\n\n float waveFactor = uEnableWaves;\n\n vec3 transformed = position;\n\n transformed.x += sin(time + position.y) * 0.5 * waveFactor;\n transformed.y += cos(time + position.z) * 0.15 * waveFactor;\n transformed.z += sin(time + position.x) * waveFactor;\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);\n}\n`;\n\nconst fragmentShader = `\nvarying vec2 vUv;\nuniform float mouse;\nuniform float uTime;\nuniform sampler2D uTexture;\n\nvoid main() {\n float time = uTime;\n vec2 pos = vUv;\n \n float move = sin(time + mouse) * 0.01;\n float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;\n float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;\n float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;\n float a = texture2D(uTexture, pos).a;\n gl_FragColor = vec4(r, g, b, a);\n}\n`;\n\nfunction map(n: number, start: number, stop: number, start2: number, stop2: number) {\n return ((n - start) / (stop - start)) * (stop2 - start2) + start2;\n}\n\nconst PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1;\n\ninterface AsciiFilterOptions {\n fontSize?: number;\n fontFamily?: string;\n charset?: string;\n invert?: boolean;\n}\n\nclass AsciiFilter {\n renderer: THREE.WebGLRenderer;\n domElement: HTMLDivElement;\n pre: HTMLPreElement;\n canvas: HTMLCanvasElement;\n context: CanvasRenderingContext2D | null;\n deg: number;\n invert: boolean;\n fontSize: number;\n fontFamily: string;\n charset: string;\n width: number = 0;\n height: number = 0;\n center: { x: number; y: number } = { x: 0, y: 0 };\n mouse: { x: number; y: number } = { x: 0, y: 0 };\n cols: number = 0;\n rows: number = 0;\n\n constructor(renderer: THREE.WebGLRenderer, { fontSize, fontFamily, charset, invert }: AsciiFilterOptions = {}) {\n this.renderer = renderer;\n this.domElement = document.createElement('div');\n this.domElement.style.position = 'absolute';\n this.domElement.style.top = '0';\n this.domElement.style.left = '0';\n this.domElement.style.width = '100%';\n this.domElement.style.height = '100%';\n\n this.pre = document.createElement('pre');\n this.domElement.appendChild(this.pre);\n\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.domElement.appendChild(this.canvas);\n\n this.deg = 0;\n this.invert = invert ?? true;\n this.fontSize = fontSize ?? 12;\n this.fontFamily = fontFamily ?? \"'Courier New', monospace\";\n this.charset = charset ?? ' .\\'`^\",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';\n\n if (this.context) {\n this.context.imageSmoothingEnabled = false;\n this.context.imageSmoothingEnabled = false;\n }\n\n this.onMouseMove = this.onMouseMove.bind(this);\n document.addEventListener('mousemove', this.onMouseMove);\n }\n\n setSize(width: number, height: number) {\n this.width = width;\n this.height = height;\n this.renderer.setSize(width, height);\n this.reset();\n\n this.center = { x: width / 2, y: height / 2 };\n this.mouse = { x: this.center.x, y: this.center.y };\n }\n\n reset() {\n if (this.context) {\n this.context.font = `${this.fontSize}px ${this.fontFamily}`;\n const charWidth = this.context.measureText('A').width;\n\n this.cols = Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize)));\n this.rows = Math.floor(this.height / this.fontSize);\n\n this.canvas.width = this.cols;\n this.canvas.height = this.rows;\n this.pre.style.fontFamily = this.fontFamily;\n this.pre.style.fontSize = `${this.fontSize}px`;\n this.pre.style.margin = '0';\n this.pre.style.padding = '0';\n this.pre.style.lineHeight = '1em';\n this.pre.style.position = 'absolute';\n this.pre.style.left = '50%';\n this.pre.style.top = '50%';\n this.pre.style.transform = 'translate(-50%, -50%)';\n this.pre.style.zIndex = '9';\n this.pre.style.backgroundAttachment = 'fixed';\n this.pre.style.mixBlendMode = 'difference';\n }\n }\n\n render(scene: THREE.Scene, camera: THREE.Camera) {\n this.renderer.render(scene, camera);\n\n const w = this.canvas.width;\n const h = this.canvas.height;\n if (this.context) {\n this.context.clearRect(0, 0, w, h);\n if (this.context && w && h) {\n this.context.drawImage(this.renderer.domElement, 0, 0, w, h);\n }\n\n this.asciify(this.context, w, h);\n this.hue();\n }\n }\n\n onMouseMove(e: MouseEvent) {\n this.mouse = { x: e.clientX * PX_RATIO, y: e.clientY * PX_RATIO };\n }\n\n get dx() {\n return this.mouse.x - this.center.x;\n }\n\n get dy() {\n return this.mouse.y - this.center.y;\n }\n\n hue() {\n const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI;\n this.deg += (deg - this.deg) * 0.075;\n this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)`;\n }\n\n asciify(ctx: CanvasRenderingContext2D, w: number, h: number) {\n if (w && h) {\n const imgData = ctx.getImageData(0, 0, w, h).data;\n let str = '';\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = x * 4 + y * 4 * w;\n const [r, g, b, a] = [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];\n\n if (a === 0) {\n str += ' ';\n continue;\n }\n\n let gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;\n let idx = Math.floor((1 - gray) * (this.charset.length - 1));\n if (this.invert) idx = this.charset.length - idx - 1;\n str += this.charset[idx];\n }\n str += '\\n';\n }\n this.pre.innerHTML = str;\n }\n }\n\n dispose() {\n document.removeEventListener('mousemove', this.onMouseMove);\n }\n}\n\ninterface CanvasTxtOptions {\n fontSize?: number;\n fontFamily?: string;\n color?: string;\n}\n\nclass CanvasTxt {\n canvas: HTMLCanvasElement;\n context: CanvasRenderingContext2D | null;\n txt: string;\n fontSize: number;\n fontFamily: string;\n color: string;\n font: string;\n\n constructor(txt: string, { fontSize = 200, fontFamily = 'Arial', color = '#fdf9f3' }: CanvasTxtOptions = {}) {\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.txt = txt;\n this.fontSize = fontSize;\n this.fontFamily = fontFamily;\n this.color = color;\n\n this.font = `600 ${this.fontSize}px ${this.fontFamily}`;\n }\n\n resize() {\n if (this.context) {\n this.context.font = this.font;\n const metrics = this.context.measureText(this.txt);\n\n const textWidth = Math.ceil(metrics.width) + 20;\n const textHeight = Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20;\n\n this.canvas.width = textWidth;\n this.canvas.height = textHeight;\n }\n }\n\n render() {\n if (this.context) {\n this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n this.context.fillStyle = this.color;\n this.context.font = this.font;\n\n const metrics = this.context.measureText(this.txt);\n const yPos = 10 + metrics.actualBoundingBoxAscent;\n\n this.context.fillText(this.txt, 10, yPos);\n }\n }\n\n get width() {\n return this.canvas.width;\n }\n\n get height() {\n return this.canvas.height;\n }\n\n get texture() {\n return this.canvas;\n }\n}\n\ninterface CanvAsciiOptions {\n text: string;\n asciiFontSize: number;\n textFontSize: number;\n textColor: string;\n planeBaseHeight: number;\n enableWaves: boolean;\n}\n\nclass CanvAscii {\n textString: string;\n asciiFontSize: number;\n textFontSize: number;\n textColor: string;\n planeBaseHeight: number;\n container: HTMLElement;\n width: number;\n height: number;\n enableWaves: boolean;\n camera: THREE.PerspectiveCamera;\n scene: THREE.Scene;\n mouse: { x: number; y: number };\n textCanvas!: CanvasTxt;\n texture!: THREE.CanvasTexture;\n geometry!: THREE.PlaneGeometry;\n material!: THREE.ShaderMaterial;\n mesh!: THREE.Mesh;\n renderer!: THREE.WebGLRenderer;\n filter!: AsciiFilter;\n center!: { x: number; y: number };\n animationFrameId: number = 0;\n\n constructor(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves }: CanvAsciiOptions,\n containerElem: HTMLElement,\n width: number,\n height: number\n ) {\n this.textString = text;\n this.asciiFontSize = asciiFontSize;\n this.textFontSize = textFontSize;\n this.textColor = textColor;\n this.planeBaseHeight = planeBaseHeight;\n this.container = containerElem;\n this.width = width;\n this.height = height;\n this.enableWaves = enableWaves;\n\n this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);\n this.camera.position.z = 30;\n\n this.scene = new THREE.Scene();\n this.mouse = { x: this.width / 2, y: this.height / 2 };\n\n this.onMouseMove = this.onMouseMove.bind(this);\n }\n\n async init() {\n try {\n await document.fonts.load('600 200px \"IBM Plex Mono\"');\n await document.fonts.load('500 12px \"IBM Plex Mono\"');\n } catch (e) {}\n await document.fonts.ready;\n this.setMesh();\n this.setRenderer();\n }\n\n setMesh() {\n this.textCanvas = new CanvasTxt(this.textString, {\n fontSize: this.textFontSize,\n fontFamily: 'IBM Plex Mono',\n color: this.textColor\n });\n this.textCanvas.resize();\n this.textCanvas.render();\n\n this.texture = new THREE.CanvasTexture(this.textCanvas.texture);\n this.texture.minFilter = THREE.NearestFilter;\n\n const textAspect = this.textCanvas.width / this.textCanvas.height;\n const baseH = this.planeBaseHeight;\n const planeW = baseH * textAspect;\n const planeH = baseH;\n\n this.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36);\n this.material = new THREE.ShaderMaterial({\n vertexShader,\n fragmentShader,\n transparent: true,\n uniforms: {\n uTime: { value: 0 },\n mouse: { value: 1.0 },\n uTexture: { value: this.texture },\n uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }\n }\n });\n\n this.mesh = new THREE.Mesh(this.geometry, this.material);\n this.scene.add(this.mesh);\n }\n\n setRenderer() {\n this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });\n this.renderer.setPixelRatio(1);\n this.renderer.setClearColor(0x000000, 0);\n\n this.filter = new AsciiFilter(this.renderer, {\n fontFamily: 'IBM Plex Mono',\n fontSize: this.asciiFontSize,\n invert: true\n });\n\n this.container.appendChild(this.filter.domElement);\n this.setSize(this.width, this.height);\n\n this.container.addEventListener('mousemove', this.onMouseMove);\n this.container.addEventListener('touchmove', this.onMouseMove);\n }\n\n setSize(w: number, h: number) {\n this.width = w;\n this.height = h;\n\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n\n this.filter.setSize(w, h);\n\n this.center = { x: w / 2, y: h / 2 };\n }\n\n load() {\n this.animate();\n }\n\n onMouseMove(evt: MouseEvent | TouchEvent) {\n const e = (evt as TouchEvent).touches ? (evt as TouchEvent).touches[0] : (evt as MouseEvent);\n const bounds = this.container.getBoundingClientRect();\n const x = e.clientX - bounds.left;\n const y = e.clientY - bounds.top;\n this.mouse = { x, y };\n }\n\n animate() {\n const animateFrame = () => {\n this.animationFrameId = requestAnimationFrame(animateFrame);\n this.render();\n };\n animateFrame();\n }\n\n render() {\n const time = new Date().getTime() * 0.001;\n\n this.textCanvas.render();\n this.texture.needsUpdate = true;\n\n (this.mesh.material as THREE.ShaderMaterial).uniforms.uTime.value = Math.sin(time);\n\n this.updateRotation();\n this.filter.render(this.scene, this.camera);\n }\n\n updateRotation() {\n const x = map(this.mouse.y, 0, this.height, 0.5, -0.5);\n const y = map(this.mouse.x, 0, this.width, -0.5, 0.5);\n\n this.mesh.rotation.x += (x - this.mesh.rotation.x) * 0.05;\n this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05;\n }\n\n clear() {\n this.scene.traverse(object => {\n const obj = object as unknown as THREE.Mesh;\n if (!obj.isMesh) return;\n [obj.material].flat().forEach(material => {\n material.dispose();\n Object.keys(material).forEach(key => {\n const matProp = material[key as keyof typeof material];\n if (matProp && typeof matProp === 'object' && 'dispose' in matProp && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n });\n obj.geometry.dispose();\n });\n this.scene.clear();\n }\n\n dispose() {\n cancelAnimationFrame(this.animationFrameId);\n if (this.filter) {\n this.filter.dispose();\n if (this.filter.domElement.parentNode) {\n this.container.removeChild(this.filter.domElement);\n }\n }\n this.container.removeEventListener('mousemove', this.onMouseMove);\n this.container.removeEventListener('touchmove', this.onMouseMove);\n this.clear();\n if (this.renderer) {\n this.renderer.dispose();\n }\n }\n}\n\ninterface ASCIITextProps {\n text?: string;\n asciiFontSize?: number;\n textFontSize?: number;\n textColor?: string;\n planeBaseHeight?: number;\n enableWaves?: boolean;\n}\n\nexport default function ASCIIText({\n text = 'David!',\n asciiFontSize = 8,\n textFontSize = 200,\n textColor = '#fdf9f3',\n planeBaseHeight = 8,\n enableWaves = true\n}: ASCIITextProps) {\n const containerRef = useRef(null);\n const asciiRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n let cancelled = false;\n let observer: IntersectionObserver | null = null;\n let ro: ResizeObserver | null = null;\n\n const createAndInit = async (container: HTMLDivElement, w: number, h: number) => {\n const instance = new CanvAscii(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\n container,\n w,\n h\n );\n await instance.init();\n return instance;\n };\n\n const setup = async () => {\n const { width, height } = containerRef.current!.getBoundingClientRect();\n\n if (width === 0 || height === 0) {\n observer = new IntersectionObserver(\n async ([entry]) => {\n if (cancelled) return;\n if (entry.isIntersecting && entry.boundingClientRect.width > 0 && entry.boundingClientRect.height > 0) {\n const { width: w, height: h } = entry.boundingClientRect;\n observer?.disconnect();\n observer = null;\n\n if (!cancelled) {\n asciiRef.current = await createAndInit(containerRef.current!, w, h);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n }\n }\n }\n },\n { threshold: 0.1 }\n );\n observer.observe(containerRef.current!);\n return;\n }\n\n asciiRef.current = await createAndInit(containerRef.current!, width, height);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n\n ro = new ResizeObserver(entries => {\n if (!entries[0] || !asciiRef.current) return;\n const { width: w, height: h } = entries[0].contentRect;\n if (w > 0 && h > 0) {\n asciiRef.current.setSize(w, h);\n }\n });\n ro.observe(containerRef.current!);\n }\n };\n\n setup();\n\n return () => {\n cancelled = true;\n if (observer) observer.disconnect();\n if (ro) ro.disconnect();\n if (asciiRef.current) {\n asciiRef.current.dispose();\n asciiRef.current = null;\n }\n };\n }, [text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves]);\n\n return (\n \n \n
\n );\n}\n"
+ "content": "// Component ported and enhanced from https://codepen.io/JuanFuentes/pen/eYEeoyE\n\nimport { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nconst vertexShader = `\nvarying vec2 vUv;\nuniform float uTime;\nuniform float mouse;\nuniform float uEnableWaves;\n\nvoid main() {\n vUv = uv;\n float time = uTime * 5.;\n\n float waveFactor = uEnableWaves;\n\n vec3 transformed = position;\n\n transformed.x += sin(time + position.y) * 0.5 * waveFactor;\n transformed.y += cos(time + position.z) * 0.15 * waveFactor;\n transformed.z += sin(time + position.x) * waveFactor;\n\n gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);\n}\n`;\n\nconst fragmentShader = `\nvarying vec2 vUv;\nuniform float mouse;\nuniform float uTime;\nuniform sampler2D uTexture;\n\nvoid main() {\n float time = uTime;\n vec2 pos = vUv;\n \n float move = sin(time + mouse) * 0.01;\n float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;\n float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;\n float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;\n float a = texture2D(uTexture, pos).a;\n gl_FragColor = vec4(r, g, b, a);\n}\n`;\n\nfunction map(n: number, start: number, stop: number, start2: number, stop2: number) {\n return ((n - start) / (stop - start)) * (stop2 - start2) + start2;\n}\n\nconst PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1;\n\ninterface AsciiFilterOptions {\n fontSize?: number;\n fontFamily?: string;\n charset?: string;\n invert?: boolean;\n}\n\nclass AsciiFilter {\n renderer: THREE.WebGLRenderer;\n domElement: HTMLDivElement;\n pre: HTMLPreElement;\n canvas: HTMLCanvasElement;\n context: CanvasRenderingContext2D | null;\n deg: number;\n invert: boolean;\n fontSize: number;\n fontFamily: string;\n charset: string;\n width: number = 0;\n height: number = 0;\n center: { x: number; y: number } = { x: 0, y: 0 };\n mouse: { x: number; y: number } = { x: 0, y: 0 };\n cols: number = 0;\n rows: number = 0;\n\n constructor(renderer: THREE.WebGLRenderer, { fontSize, fontFamily, charset, invert }: AsciiFilterOptions = {}) {\n this.renderer = renderer;\n this.domElement = document.createElement('div');\n this.domElement.style.position = 'absolute';\n this.domElement.style.top = '0';\n this.domElement.style.left = '0';\n this.domElement.style.width = '100%';\n this.domElement.style.height = '100%';\n\n this.pre = document.createElement('pre');\n this.domElement.appendChild(this.pre);\n\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.domElement.appendChild(this.canvas);\n\n this.deg = 0;\n this.invert = invert ?? true;\n this.fontSize = fontSize ?? 12;\n this.fontFamily = fontFamily ?? \"'Courier New', monospace\";\n this.charset = charset ?? ' .\\'`^\",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';\n\n if (this.context) {\n this.context.imageSmoothingEnabled = false;\n this.context.imageSmoothingEnabled = false;\n }\n\n this.onMouseMove = this.onMouseMove.bind(this);\n document.addEventListener('mousemove', this.onMouseMove);\n }\n\n setSize(width: number, height: number) {\n this.width = width;\n this.height = height;\n this.renderer.setSize(width, height);\n this.reset();\n\n this.center = { x: width / 2, y: height / 2 };\n this.mouse = { x: this.center.x, y: this.center.y };\n }\n\n reset() {\n if (this.context) {\n this.context.font = `${this.fontSize}px ${this.fontFamily}`;\n const charWidth = this.context.measureText('A').width;\n\n this.cols = Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize)));\n this.rows = Math.floor(this.height / this.fontSize);\n\n this.canvas.width = this.cols;\n this.canvas.height = this.rows;\n this.pre.style.fontFamily = this.fontFamily;\n this.pre.style.fontSize = `${this.fontSize}px`;\n this.pre.style.margin = '0';\n this.pre.style.padding = '0';\n this.pre.style.lineHeight = '1em';\n this.pre.style.position = 'absolute';\n this.pre.style.left = '50%';\n this.pre.style.top = '50%';\n this.pre.style.transform = 'translate(-50%, -50%)';\n this.pre.style.zIndex = '9';\n this.pre.style.backgroundAttachment = 'fixed';\n this.pre.style.mixBlendMode = 'difference';\n }\n }\n\n render(scene: THREE.Scene, camera: THREE.Camera) {\n this.renderer.render(scene, camera);\n\n const w = this.canvas.width;\n const h = this.canvas.height;\n if (this.context) {\n this.context.clearRect(0, 0, w, h);\n if (this.context && w && h) {\n this.context.drawImage(this.renderer.domElement, 0, 0, w, h);\n }\n\n this.asciify(this.context, w, h);\n this.hue();\n }\n }\n\n onMouseMove(e: MouseEvent) {\n this.mouse = { x: e.clientX * PX_RATIO, y: e.clientY * PX_RATIO };\n }\n\n get dx() {\n return this.mouse.x - this.center.x;\n }\n\n get dy() {\n return this.mouse.y - this.center.y;\n }\n\n hue() {\n const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI;\n this.deg += (deg - this.deg) * 0.075;\n this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)`;\n }\n\n asciify(ctx: CanvasRenderingContext2D, w: number, h: number) {\n if (w && h) {\n const imgData = ctx.getImageData(0, 0, w, h).data;\n let str = '';\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = x * 4 + y * 4 * w;\n const [r, g, b, a] = [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];\n\n if (a === 0) {\n str += ' ';\n continue;\n }\n\n let gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;\n let idx = Math.floor((1 - gray) * (this.charset.length - 1));\n if (this.invert) idx = this.charset.length - idx - 1;\n str += this.charset[idx];\n }\n str += '\\n';\n }\n this.pre.innerHTML = str;\n }\n }\n\n dispose() {\n document.removeEventListener('mousemove', this.onMouseMove);\n }\n}\n\ninterface CanvasTxtOptions {\n fontSize?: number;\n fontFamily?: string;\n color?: string;\n}\n\nclass CanvasTxt {\n canvas: HTMLCanvasElement;\n context: CanvasRenderingContext2D | null;\n txt: string;\n fontSize: number;\n fontFamily: string;\n color: string;\n font: string;\n\n constructor(txt: string, { fontSize = 200, fontFamily = 'Arial', color = '#fdf9f3' }: CanvasTxtOptions = {}) {\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d');\n this.txt = txt;\n this.fontSize = fontSize;\n this.fontFamily = fontFamily;\n this.color = color;\n\n this.font = `600 ${this.fontSize}px ${this.fontFamily}`;\n }\n\n resize() {\n if (this.context) {\n this.context.font = this.font;\n const metrics = this.context.measureText(this.txt);\n\n const textWidth = Math.ceil(metrics.width) + 20;\n const textHeight = Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20;\n\n this.canvas.width = textWidth;\n this.canvas.height = textHeight;\n }\n }\n\n render() {\n if (this.context) {\n this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n this.context.fillStyle = this.color;\n this.context.font = this.font;\n\n const metrics = this.context.measureText(this.txt);\n const yPos = 10 + metrics.actualBoundingBoxAscent;\n\n this.context.fillText(this.txt, 10, yPos);\n }\n }\n\n get width() {\n return this.canvas.width;\n }\n\n get height() {\n return this.canvas.height;\n }\n\n get texture() {\n return this.canvas;\n }\n}\n\ninterface CanvAsciiOptions {\n text: string;\n asciiFontSize: number;\n textFontSize: number;\n textColor: string;\n planeBaseHeight: number;\n enableWaves: boolean;\n}\n\nclass CanvAscii {\n textString: string;\n asciiFontSize: number;\n textFontSize: number;\n textColor: string;\n planeBaseHeight: number;\n container: HTMLElement;\n width: number;\n height: number;\n enableWaves: boolean;\n camera: THREE.PerspectiveCamera;\n scene: THREE.Scene;\n mouse: { x: number; y: number };\n textCanvas!: CanvasTxt;\n texture!: THREE.CanvasTexture;\n geometry!: THREE.PlaneGeometry;\n material!: THREE.ShaderMaterial;\n mesh!: THREE.Mesh;\n renderer!: THREE.WebGLRenderer;\n filter!: AsciiFilter;\n center!: { x: number; y: number };\n animationFrameId: number = 0;\n\n constructor(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves }: CanvAsciiOptions,\n containerElem: HTMLElement,\n width: number,\n height: number\n ) {\n this.textString = text;\n this.asciiFontSize = asciiFontSize;\n this.textFontSize = textFontSize;\n this.textColor = textColor;\n this.planeBaseHeight = planeBaseHeight;\n this.container = containerElem;\n this.width = width;\n this.height = height;\n this.enableWaves = enableWaves;\n\n this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);\n this.camera.position.z = 30;\n\n this.scene = new THREE.Scene();\n this.mouse = { x: this.width / 2, y: this.height / 2 };\n\n this.onMouseMove = this.onMouseMove.bind(this);\n }\n\n async init() {\n try {\n await document.fonts.load('600 200px \"IBM Plex Mono\"');\n await document.fonts.load('500 12px \"IBM Plex Mono\"');\n } catch (e) {}\n await document.fonts.ready;\n this.setMesh();\n this.setRenderer();\n }\n\n setMesh() {\n this.textCanvas = new CanvasTxt(this.textString, {\n fontSize: this.textFontSize,\n fontFamily: 'IBM Plex Mono',\n color: this.textColor\n });\n this.textCanvas.resize();\n this.textCanvas.render();\n\n this.texture = new THREE.CanvasTexture(this.textCanvas.texture);\n this.texture.minFilter = THREE.NearestFilter;\n\n const textAspect = this.textCanvas.width / this.textCanvas.height;\n const baseH = this.planeBaseHeight;\n const planeW = baseH * textAspect;\n const planeH = baseH;\n\n this.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36);\n this.material = new THREE.ShaderMaterial({\n vertexShader,\n fragmentShader,\n transparent: true,\n uniforms: {\n uTime: { value: 0 },\n mouse: { value: 1.0 },\n uTexture: { value: this.texture },\n uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }\n }\n });\n\n this.mesh = new THREE.Mesh(this.geometry, this.material);\n this.scene.add(this.mesh);\n }\n\n setRenderer() {\n this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });\n this.renderer.setPixelRatio(1);\n this.renderer.setClearColor(0x000000, 0);\n\n this.filter = new AsciiFilter(this.renderer, {\n fontFamily: 'IBM Plex Mono',\n fontSize: this.asciiFontSize,\n invert: true\n });\n\n this.container.appendChild(this.filter.domElement);\n this.setSize(this.width, this.height);\n\n this.container.addEventListener('mousemove', this.onMouseMove);\n this.container.addEventListener('touchmove', this.onMouseMove);\n }\n\n setSize(w: number, h: number) {\n this.width = w;\n this.height = h;\n\n this.camera.aspect = w / h;\n this.camera.updateProjectionMatrix();\n\n this.filter.setSize(w, h);\n\n this.center = { x: w / 2, y: h / 2 };\n }\n\n load() {\n this.animate();\n }\n\n onMouseMove(evt: MouseEvent | TouchEvent) {\n const e = (evt as TouchEvent).touches ? (evt as TouchEvent).touches[0] : (evt as MouseEvent);\n const bounds = this.container.getBoundingClientRect();\n const x = e.clientX - bounds.left;\n const y = e.clientY - bounds.top;\n this.mouse = { x, y };\n }\n\n animate() {\n const animateFrame = () => {\n this.animationFrameId = requestAnimationFrame(animateFrame);\n this.render();\n };\n animateFrame();\n }\n\n render() {\n const time = new Date().getTime() * 0.001;\n\n this.textCanvas.render();\n this.texture.needsUpdate = true;\n\n (this.mesh.material as THREE.ShaderMaterial).uniforms.uTime.value = Math.sin(time);\n\n this.updateRotation();\n this.filter.render(this.scene, this.camera);\n }\n\n updateRotation() {\n const x = map(this.mouse.y, 0, this.height, 0.5, -0.5);\n const y = map(this.mouse.x, 0, this.width, -0.5, 0.5);\n\n this.mesh.rotation.x += (x - this.mesh.rotation.x) * 0.05;\n this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05;\n }\n\n clear() {\n this.scene.traverse(object => {\n const obj = object as unknown as THREE.Mesh;\n if (!obj.isMesh) return;\n [obj.material].flat().forEach(material => {\n material.dispose();\n Object.keys(material).forEach(key => {\n const matProp = material[key as keyof typeof material];\n if (matProp && typeof matProp === 'object' && 'dispose' in matProp && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n });\n obj.geometry.dispose();\n });\n this.scene.clear();\n }\n\n dispose() {\n cancelAnimationFrame(this.animationFrameId);\n if (this.filter) {\n this.filter.dispose();\n if (this.filter.domElement.parentNode) {\n this.container.removeChild(this.filter.domElement);\n }\n }\n this.container.removeEventListener('mousemove', this.onMouseMove);\n this.container.removeEventListener('touchmove', this.onMouseMove);\n this.clear();\n if (this.renderer) {\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n }\n }\n}\n\ninterface ASCIITextProps {\n text?: string;\n asciiFontSize?: number;\n textFontSize?: number;\n textColor?: string;\n planeBaseHeight?: number;\n enableWaves?: boolean;\n}\n\nexport default function ASCIIText({\n text = 'David!',\n asciiFontSize = 8,\n textFontSize = 200,\n textColor = '#fdf9f3',\n planeBaseHeight = 8,\n enableWaves = true\n}: ASCIITextProps) {\n const containerRef = useRef(null);\n const asciiRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n let cancelled = false;\n let observer: IntersectionObserver | null = null;\n let ro: ResizeObserver | null = null;\n\n const createAndInit = async (container: HTMLDivElement, w: number, h: number) => {\n const instance = new CanvAscii(\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\n container,\n w,\n h\n );\n await instance.init();\n return instance;\n };\n\n const setup = async () => {\n const { width, height } = containerRef.current!.getBoundingClientRect();\n\n if (width === 0 || height === 0) {\n observer = new IntersectionObserver(\n async ([entry]) => {\n if (cancelled) return;\n if (entry.isIntersecting && entry.boundingClientRect.width > 0 && entry.boundingClientRect.height > 0) {\n const { width: w, height: h } = entry.boundingClientRect;\n observer?.disconnect();\n observer = null;\n\n if (!cancelled) {\n asciiRef.current = await createAndInit(containerRef.current!, w, h);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n }\n }\n }\n },\n { threshold: 0.1 }\n );\n observer.observe(containerRef.current!);\n return;\n }\n\n asciiRef.current = await createAndInit(containerRef.current!, width, height);\n if (!cancelled && asciiRef.current) {\n asciiRef.current.load();\n\n ro = new ResizeObserver(entries => {\n if (!entries[0] || !asciiRef.current) return;\n const { width: w, height: h } = entries[0].contentRect;\n if (w > 0 && h > 0) {\n asciiRef.current.setSize(w, h);\n }\n });\n ro.observe(containerRef.current!);\n }\n };\n\n setup();\n\n return () => {\n cancelled = true;\n if (observer) observer.disconnect();\n if (ro) ro.disconnect();\n if (asciiRef.current) {\n asciiRef.current.dispose();\n asciiRef.current = null;\n }\n };\n }, [text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves]);\n\n return (\n \n \n
\n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/Ballpit-JS-CSS.json b/public/r/Ballpit-JS-CSS.json
index fa0c174c..6779a2e1 100644
--- a/public/r/Ballpit-JS-CSS.json
+++ b/public/r/Ballpit-JS-CSS.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "Ballpit/Ballpit.jsx",
- "content": "import { useRef, useEffect } from 'react';\nimport {\n Clock as e,\n PerspectiveCamera as t,\n Scene as i,\n WebGLRenderer as s,\n SRGBColorSpace as n,\n MathUtils as o,\n Vector2 as r,\n Vector3 as a,\n MeshPhysicalMaterial as c,\n ShaderChunk as h,\n Color as l,\n Object3D as m,\n InstancedMesh as d,\n PMREMGenerator as p,\n SphereGeometry as g,\n AmbientLight as f,\n PointLight as u,\n ACESFilmicToneMapping as v,\n Raycaster as y,\n Plane as w\n} from 'three';\nimport { RoomEnvironment as z } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\nclass x {\n #e;\n canvas;\n camera;\n cameraMinAspect;\n cameraMaxAspect;\n cameraFov;\n maxPixelRatio;\n minPixelRatio;\n scene;\n renderer;\n #t;\n size = { width: 0, height: 0, wWidth: 0, wHeight: 0, ratio: 0, pixelRatio: 0 };\n render = this.#i;\n onBeforeRender = () => {};\n onAfterRender = () => {};\n onAfterResize = () => {};\n #s = false;\n #n = false;\n isDisposed = false;\n #o;\n #r;\n #a;\n #c = new e();\n #h = { elapsed: 0, delta: 0 };\n #l;\n constructor(e) {\n this.#e = { ...e };\n this.#m();\n this.#d();\n this.#p();\n this.resize();\n this.#g();\n }\n #m() {\n this.camera = new t();\n this.cameraFov = this.camera.fov;\n }\n #d() {\n this.scene = new i();\n }\n #p() {\n if (this.#e.canvas) {\n this.canvas = this.#e.canvas;\n } else if (this.#e.id) {\n this.canvas = document.getElementById(this.#e.id);\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas.style.display = 'block';\n const e = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#e.rendererOptions ?? {})\n };\n this.renderer = new s(e);\n this.renderer.outputColorSpace = n;\n }\n #g() {\n if (!(this.#e.size instanceof Object)) {\n window.addEventListener('resize', this.#f.bind(this));\n if (this.#e.size === 'parent' && this.canvas.parentNode) {\n this.#r = new ResizeObserver(this.#f.bind(this));\n this.#r.observe(this.canvas.parentNode);\n }\n }\n this.#o = new IntersectionObserver(this.#u.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#o.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#v.bind(this));\n }\n #y() {\n window.removeEventListener('resize', this.#f.bind(this));\n this.#r?.disconnect();\n this.#o?.disconnect();\n document.removeEventListener('visibilitychange', this.#v.bind(this));\n }\n #u(e) {\n this.#s = e[0].isIntersecting;\n this.#s ? this.#w() : this.#z();\n }\n #v() {\n if (this.#s) {\n document.hidden ? this.#z() : this.#w();\n }\n }\n #f() {\n if (this.#a) clearTimeout(this.#a);\n this.#a = setTimeout(this.resize.bind(this), 100);\n }\n resize() {\n let e, t;\n if (this.#e.size instanceof Object) {\n e = this.#e.size.width;\n t = this.#e.size.height;\n } else if (this.#e.size === 'parent' && this.canvas.parentNode) {\n e = this.canvas.parentNode.offsetWidth;\n t = this.canvas.parentNode.offsetHeight;\n } else {\n e = window.innerWidth;\n t = window.innerHeight;\n }\n this.size.width = e;\n this.size.height = t;\n this.size.ratio = e / t;\n this.#x();\n this.#b();\n this.onAfterResize(this.size);\n }\n #x() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#A(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#A(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n #A(e) {\n const t = Math.tan(o.degToRad(this.cameraFov / 2)) / (this.camera.aspect / e);\n this.camera.fov = 2 * o.radToDeg(Math.atan(t));\n }\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const e = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(e / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if (this.camera.isOrthographicCamera) {\n this.size.wHeight = this.camera.top - this.camera.bottom;\n this.size.wWidth = this.camera.right - this.camera.left;\n }\n }\n #b() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#t?.setSize(this.size.width, this.size.height);\n let e = window.devicePixelRatio;\n if (this.maxPixelRatio && e > this.maxPixelRatio) {\n e = this.maxPixelRatio;\n } else if (this.minPixelRatio && e < this.minPixelRatio) {\n e = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(e);\n this.size.pixelRatio = e;\n }\n get postprocessing() {\n return this.#t;\n }\n set postprocessing(e) {\n this.#t = e;\n this.render = e.render.bind(e);\n }\n #w() {\n if (this.#n) return;\n const animate = () => {\n this.#l = requestAnimationFrame(animate);\n this.#h.delta = this.#c.getDelta();\n this.#h.elapsed += this.#h.delta;\n this.onBeforeRender(this.#h);\n this.render();\n this.onAfterRender(this.#h);\n };\n this.#n = true;\n this.#c.start();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\n this.#c.stop();\n }\n }\n #i() {\n this.renderer.render(this.scene, this.camera);\n }\n clear() {\n this.scene.traverse(e => {\n if (e.isMesh && typeof e.material === 'object' && e.material !== null) {\n Object.keys(e.material).forEach(t => {\n const i = e.material[t];\n if (i !== null && typeof i === 'object' && typeof i.dispose === 'function') {\n i.dispose();\n }\n });\n e.material.dispose();\n e.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n dispose() {\n this.#y();\n this.#z();\n this.clear();\n this.#t?.dispose();\n this.renderer.dispose();\n this.isDisposed = true;\n }\n}\n\nconst b = new Map(),\n A = new r();\nlet R = false;\nfunction S(e) {\n const t = {\n position: new r(),\n nPosition: new r(),\n hover: false,\n touching: false,\n onEnter() {},\n onMove() {},\n onClick() {},\n onLeave() {},\n ...e\n };\n (function (e, t) {\n if (!b.has(e)) {\n b.set(e, t);\n if (!R) {\n document.body.addEventListener('pointermove', M);\n document.body.addEventListener('pointerleave', L);\n document.body.addEventListener('click', C);\n\n document.body.addEventListener('touchstart', TouchStart, { passive: false });\n document.body.addEventListener('touchmove', TouchMove, { passive: false });\n document.body.addEventListener('touchend', TouchEnd, { passive: false });\n document.body.addEventListener('touchcancel', TouchEnd, { passive: false });\n\n R = true;\n }\n }\n })(e.domElement, t);\n t.dispose = () => {\n const t = e.domElement;\n b.delete(t);\n if (b.size === 0) {\n document.body.removeEventListener('pointermove', M);\n document.body.removeEventListener('pointerleave', L);\n document.body.removeEventListener('click', C);\n\n document.body.removeEventListener('touchstart', TouchStart);\n document.body.removeEventListener('touchmove', TouchMove);\n document.body.removeEventListener('touchend', TouchEnd);\n document.body.removeEventListener('touchcancel', TouchEnd);\n\n R = false;\n }\n };\n return t;\n}\n\nfunction M(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n processInteraction();\n}\n\nfunction processInteraction() {\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n if (D(i)) {\n P(t, i);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && !t.touching) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction C(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n P(t, i);\n if (D(i)) t.onClick(t);\n }\n}\n\nfunction L() {\n for (const t of b.values()) {\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction TouchStart(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n if (D(rect)) {\n t.touching = true;\n P(t, rect);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchMove(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n P(t, rect);\n\n if (D(rect)) {\n if (!t.hover) {\n t.hover = true;\n t.touching = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && t.touching) {\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchEnd() {\n for (const [, t] of b) {\n if (t.touching) {\n t.touching = false;\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n }\n}\n\nfunction P(e, t) {\n const { position: i, nPosition: s } = e;\n i.x = A.x - t.left;\n i.y = A.y - t.top;\n s.x = (i.x / t.width) * 2 - 1;\n s.y = (-i.y / t.height) * 2 + 1;\n}\nfunction D(e) {\n const { x: t, y: i } = A;\n const { left: s, top: n, width: o, height: r } = e;\n return t >= s && t <= s + o && i >= n && i <= n + r;\n}\n\nconst { randFloat: k, randFloatSpread: E } = o;\nconst F = new a();\nconst I = new a();\nconst O = new a();\nconst V = new a();\nconst B = new a();\nconst N = new a();\nconst _ = new a();\nconst j = new a();\nconst H = new a();\nconst T = new a();\n\nclass W {\n constructor(e) {\n this.config = e;\n this.positionData = new Float32Array(3 * e.count).fill(0);\n this.velocityData = new Float32Array(3 * e.count).fill(0);\n this.sizeData = new Float32Array(e.count).fill(1);\n this.center = new a();\n this.#R();\n this.setSizes();\n }\n #R() {\n const { config: e, positionData: t } = this;\n this.center.toArray(t, 0);\n for (let i = 1; i < e.count; i++) {\n const s = 3 * i;\n t[s] = E(2 * e.maxX);\n t[s + 1] = E(2 * e.maxY);\n t[s + 2] = E(2 * e.maxZ);\n }\n }\n setSizes() {\n const { config: e, sizeData: t } = this;\n t[0] = e.size0;\n for (let i = 1; i < e.count; i++) {\n t[i] = k(e.minSize, e.maxSize);\n }\n }\n update(e) {\n const { config: t, center: i, positionData: s, sizeData: n, velocityData: o } = this;\n let r = 0;\n if (t.controlSphere0) {\n r = 1;\n F.fromArray(s, 0);\n F.lerp(i, 0.1).toArray(s, 0);\n V.set(0, 0, 0).toArray(o, 0);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n B.y -= e.delta * t.gravity * n[idx];\n B.multiplyScalar(t.friction);\n B.clampLength(0, t.maxVelocity);\n I.add(B);\n I.toArray(s, base);\n B.toArray(o, base);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n const radius = n[idx];\n for (let jdx = idx + 1; jdx < t.count; jdx++) {\n const otherBase = 3 * jdx;\n O.fromArray(s, otherBase);\n N.fromArray(o, otherBase);\n const otherRadius = n[jdx];\n _.copy(O).sub(I);\n const dist = _.length();\n const sumRadius = radius + otherRadius;\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n j.copy(_)\n .normalize()\n .multiplyScalar(0.5 * overlap);\n H.copy(j).multiplyScalar(Math.max(B.length(), 1));\n T.copy(j).multiplyScalar(Math.max(N.length(), 1));\n I.sub(j);\n B.sub(H);\n I.toArray(s, base);\n B.toArray(o, base);\n O.add(j);\n N.add(T);\n O.toArray(s, otherBase);\n N.toArray(o, otherBase);\n }\n }\n if (t.controlSphere0) {\n _.copy(F).sub(I);\n const dist = _.length();\n const sumRadius0 = radius + n[0];\n if (dist < sumRadius0) {\n const diff = sumRadius0 - dist;\n j.copy(_.normalize()).multiplyScalar(diff);\n H.copy(j).multiplyScalar(Math.max(B.length(), 2));\n I.sub(j);\n B.sub(H);\n }\n }\n if (Math.abs(I.x) + radius > t.maxX) {\n I.x = Math.sign(I.x) * (t.maxX - radius);\n B.x = -B.x * t.wallBounce;\n }\n if (t.gravity === 0) {\n if (Math.abs(I.y) + radius > t.maxY) {\n I.y = Math.sign(I.y) * (t.maxY - radius);\n B.y = -B.y * t.wallBounce;\n }\n } else if (I.y - radius < -t.maxY) {\n I.y = -t.maxY + radius;\n B.y = -B.y * t.wallBounce;\n }\n const maxBoundary = Math.max(t.maxZ, t.maxSize);\n if (Math.abs(I.z) + radius > maxBoundary) {\n I.z = Math.sign(I.z) * (t.maxZ - radius);\n B.z = -B.z * t.wallBounce;\n }\n I.toArray(s, base);\n B.toArray(o, base);\n }\n }\n}\n\nclass Y extends c {\n constructor(e) {\n super(e);\n this.uniforms = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n this.defines.USE_UV = '';\n this.onBeforeCompile = e => {\n Object.assign(e.uniforms, this.uniforms);\n e.fragmentShader =\n '\\n uniform float thicknessPower;\\n uniform float thicknessScale;\\n uniform float thicknessDistortion;\\n uniform float thicknessAmbient;\\n uniform float thicknessAttenuation;\\n ' +\n e.fragmentShader;\n e.fragmentShader = e.fragmentShader.replace(\n 'void main() {',\n '\\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\\n #ifdef USE_COLOR\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\\n #else\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\\n #endif\\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\\n }\\n\\n void main() {\\n '\n );\n const t = h.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n '\\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\\n '\n );\n e.fragmentShader = e.fragmentShader.replace('#include ', t);\n if (this.onBeforeCompile2) this.onBeforeCompile2(e);\n };\n }\n}\n\nconst X = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 16777215,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new m();\n\nclass Z extends d {\n constructor(e, t = {}) {\n const i = { ...X, ...t };\n const s = new z();\n const n = new p(e, 0.04).fromScene(s).texture;\n const o = new g();\n const r = new Y({ envMap: n, ...i.materialParams });\n r.envMapRotation.x = -Math.PI / 2;\n super(o, r, i.count);\n this.config = i;\n this.physics = new W(i);\n this.#S();\n this.setColors(i.colors);\n }\n #S() {\n this.ambientLight = new f(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new u(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n setColors(e) {\n if (Array.isArray(e) && e.length > 1) {\n const t = (function (e) {\n let t, i;\n function setColors(e) {\n t = e;\n i = [];\n t.forEach(col => {\n i.push(new l(col));\n });\n }\n setColors(e);\n return {\n setColors,\n getColorAt: function (ratio, out = new l()) {\n const scaled = Math.max(0, Math.min(1, ratio)) * (t.length - 1);\n const idx = Math.floor(scaled);\n const start = i[idx];\n if (idx >= t.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = i[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(e);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, t.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light.color.copy(t.getColorAt(idx / this.count));\n }\n }\n this.instanceColor.needsUpdate = true;\n }\n }\n update(e) {\n this.physics.update(e);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\nfunction createBallpit(e, t = {}) {\n const i = new x({\n canvas: e,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let s;\n i.renderer.toneMapping = v;\n i.camera.position.set(0, 0, 20);\n i.camera.lookAt(0, 0, 0);\n i.cameraMaxAspect = 1.5;\n i.resize();\n initialize(t);\n const n = new y();\n const o = new w(new a(0, 0, 1), 0);\n const r = new a();\n let c = false;\n\n e.style.touchAction = 'none';\n e.style.userSelect = 'none';\n e.style.webkitUserSelect = 'none';\n\n const h = S({\n domElement: e,\n onMove() {\n n.setFromCamera(h.nPosition, i.camera);\n i.camera.getWorldDirection(o.normal);\n n.ray.intersectPlane(o, r);\n s.physics.center.copy(r);\n s.config.controlSphere0 = true;\n },\n onLeave() {\n s.config.controlSphere0 = false;\n }\n });\n function initialize(e) {\n if (s) {\n i.clear();\n i.scene.remove(s);\n }\n s = new Z(i.renderer, e);\n i.scene.add(s);\n }\n i.onBeforeRender = e => {\n if (!c) s.update(e);\n };\n i.onAfterResize = e => {\n s.config.maxX = e.wWidth / 2;\n s.config.maxY = e.wHeight / 2;\n };\n return {\n three: i,\n get spheres() {\n return s;\n },\n setCount(e) {\n initialize({ ...s.config, count: e });\n },\n togglePause() {\n c = !c;\n },\n dispose() {\n h.dispose();\n i.dispose();\n }\n };\n}\n\nconst Ballpit = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, { followCursor, ...props });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n"
+ "content": "import { useEffect, useRef } from 'react';\nimport {\n Vector3 as a,\n MeshPhysicalMaterial as c,\n InstancedMesh as d,\n Clock as e,\n AmbientLight as f,\n SphereGeometry as g,\n ShaderChunk as h,\n Scene as i,\n Color as l,\n Object3D as m,\n SRGBColorSpace as n,\n MathUtils as o,\n PMREMGenerator as p,\n Vector2 as r,\n WebGLRenderer as s,\n PerspectiveCamera as t,\n PointLight as u,\n ACESFilmicToneMapping as v,\n Plane as w,\n Raycaster as y\n} from 'three';\nimport { RoomEnvironment as z } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\nclass x {\n #e;\n canvas;\n camera;\n cameraMinAspect;\n cameraMaxAspect;\n cameraFov;\n maxPixelRatio;\n minPixelRatio;\n scene;\n renderer;\n #t;\n size = { width: 0, height: 0, wWidth: 0, wHeight: 0, ratio: 0, pixelRatio: 0 };\n render = this.#i;\n onBeforeRender = () => {};\n onAfterRender = () => {};\n onAfterResize = () => {};\n #s = false;\n #n = false;\n isDisposed = false;\n #o;\n #r;\n #a;\n #c = new e();\n #h = { elapsed: 0, delta: 0 };\n #l;\n constructor(e) {\n this.#e = { ...e };\n this.#m();\n this.#d();\n this.#p();\n this.resize();\n this.#g();\n }\n #m() {\n this.camera = new t();\n this.cameraFov = this.camera.fov;\n }\n #d() {\n this.scene = new i();\n }\n #p() {\n if (this.#e.canvas) {\n this.canvas = this.#e.canvas;\n } else if (this.#e.id) {\n this.canvas = document.getElementById(this.#e.id);\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas.style.display = 'block';\n const e = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#e.rendererOptions ?? {})\n };\n this.renderer = new s(e);\n this.renderer.outputColorSpace = n;\n }\n #g() {\n if (!(this.#e.size instanceof Object)) {\n window.addEventListener('resize', this.#f.bind(this));\n if (this.#e.size === 'parent' && this.canvas.parentNode) {\n this.#r = new ResizeObserver(this.#f.bind(this));\n this.#r.observe(this.canvas.parentNode);\n }\n }\n this.#o = new IntersectionObserver(this.#u.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#o.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#v.bind(this));\n }\n #y() {\n window.removeEventListener('resize', this.#f.bind(this));\n this.#r?.disconnect();\n this.#o?.disconnect();\n document.removeEventListener('visibilitychange', this.#v.bind(this));\n }\n #u(e) {\n this.#s = e[0].isIntersecting;\n this.#s ? this.#w() : this.#z();\n }\n #v() {\n if (this.#s) {\n document.hidden ? this.#z() : this.#w();\n }\n }\n #f() {\n if (this.#a) clearTimeout(this.#a);\n this.#a = setTimeout(this.resize.bind(this), 100);\n }\n resize() {\n let e, t;\n if (this.#e.size instanceof Object) {\n e = this.#e.size.width;\n t = this.#e.size.height;\n } else if (this.#e.size === 'parent' && this.canvas.parentNode) {\n e = this.canvas.parentNode.offsetWidth;\n t = this.canvas.parentNode.offsetHeight;\n } else {\n e = window.innerWidth;\n t = window.innerHeight;\n }\n this.size.width = e;\n this.size.height = t;\n this.size.ratio = e / t;\n this.#x();\n this.#b();\n this.onAfterResize(this.size);\n }\n #x() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#A(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#A(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n #A(e) {\n const t = Math.tan(o.degToRad(this.cameraFov / 2)) / (this.camera.aspect / e);\n this.camera.fov = 2 * o.radToDeg(Math.atan(t));\n }\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const e = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(e / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if (this.camera.isOrthographicCamera) {\n this.size.wHeight = this.camera.top - this.camera.bottom;\n this.size.wWidth = this.camera.right - this.camera.left;\n }\n }\n #b() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#t?.setSize(this.size.width, this.size.height);\n let e = window.devicePixelRatio;\n if (this.maxPixelRatio && e > this.maxPixelRatio) {\n e = this.maxPixelRatio;\n } else if (this.minPixelRatio && e < this.minPixelRatio) {\n e = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(e);\n this.size.pixelRatio = e;\n }\n get postprocessing() {\n return this.#t;\n }\n set postprocessing(e) {\n this.#t = e;\n this.render = e.render.bind(e);\n }\n #w() {\n if (this.#n) return;\n const animate = () => {\n this.#l = requestAnimationFrame(animate);\n this.#h.delta = this.#c.getDelta();\n this.#h.elapsed += this.#h.delta;\n this.onBeforeRender(this.#h);\n this.render();\n this.onAfterRender(this.#h);\n };\n this.#n = true;\n this.#c.start();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\n this.#c.stop();\n }\n }\n #i() {\n this.renderer.render(this.scene, this.camera);\n }\n clear() {\n this.scene.traverse(e => {\n if (e.isMesh && typeof e.material === 'object' && e.material !== null) {\n Object.keys(e.material).forEach(t => {\n const i = e.material[t];\n if (i !== null && typeof i === 'object' && typeof i.dispose === 'function') {\n i.dispose();\n }\n });\n e.material.dispose();\n e.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n dispose() {\n this.#y();\n this.#z();\n this.clear();\n this.#t?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n}\n\nconst b = new Map(),\n A = new r();\nlet R = false;\nfunction S(e) {\n const t = {\n position: new r(),\n nPosition: new r(),\n hover: false,\n touching: false,\n onEnter() {},\n onMove() {},\n onClick() {},\n onLeave() {},\n ...e\n };\n (function (e, t) {\n if (!b.has(e)) {\n b.set(e, t);\n if (!R) {\n document.body.addEventListener('pointermove', M);\n document.body.addEventListener('pointerleave', L);\n document.body.addEventListener('click', C);\n\n document.body.addEventListener('touchstart', TouchStart, { passive: false });\n document.body.addEventListener('touchmove', TouchMove, { passive: false });\n document.body.addEventListener('touchend', TouchEnd, { passive: false });\n document.body.addEventListener('touchcancel', TouchEnd, { passive: false });\n\n R = true;\n }\n }\n })(e.domElement, t);\n t.dispose = () => {\n const t = e.domElement;\n b.delete(t);\n if (b.size === 0) {\n document.body.removeEventListener('pointermove', M);\n document.body.removeEventListener('pointerleave', L);\n document.body.removeEventListener('click', C);\n\n document.body.removeEventListener('touchstart', TouchStart);\n document.body.removeEventListener('touchmove', TouchMove);\n document.body.removeEventListener('touchend', TouchEnd);\n document.body.removeEventListener('touchcancel', TouchEnd);\n\n R = false;\n }\n };\n return t;\n}\n\nfunction M(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n processInteraction();\n}\n\nfunction processInteraction() {\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n if (D(i)) {\n P(t, i);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && !t.touching) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction C(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n P(t, i);\n if (D(i)) t.onClick(t);\n }\n}\n\nfunction L() {\n for (const t of b.values()) {\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction TouchStart(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n if (D(rect)) {\n t.touching = true;\n P(t, rect);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchMove(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n P(t, rect);\n\n if (D(rect)) {\n if (!t.hover) {\n t.hover = true;\n t.touching = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && t.touching) {\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchEnd() {\n for (const [, t] of b) {\n if (t.touching) {\n t.touching = false;\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n }\n}\n\nfunction P(e, t) {\n const { position: i, nPosition: s } = e;\n i.x = A.x - t.left;\n i.y = A.y - t.top;\n s.x = (i.x / t.width) * 2 - 1;\n s.y = (-i.y / t.height) * 2 + 1;\n}\nfunction D(e) {\n const { x: t, y: i } = A;\n const { left: s, top: n, width: o, height: r } = e;\n return t >= s && t <= s + o && i >= n && i <= n + r;\n}\n\nconst { randFloat: k, randFloatSpread: E } = o;\nconst F = new a();\nconst I = new a();\nconst O = new a();\nconst V = new a();\nconst B = new a();\nconst N = new a();\nconst _ = new a();\nconst j = new a();\nconst H = new a();\nconst T = new a();\n\nclass W {\n constructor(e) {\n this.config = e;\n this.positionData = new Float32Array(3 * e.count).fill(0);\n this.velocityData = new Float32Array(3 * e.count).fill(0);\n this.sizeData = new Float32Array(e.count).fill(1);\n this.center = new a();\n this.#R();\n this.setSizes();\n }\n #R() {\n const { config: e, positionData: t } = this;\n this.center.toArray(t, 0);\n for (let i = 1; i < e.count; i++) {\n const s = 3 * i;\n t[s] = E(2 * e.maxX);\n t[s + 1] = E(2 * e.maxY);\n t[s + 2] = E(2 * e.maxZ);\n }\n }\n setSizes() {\n const { config: e, sizeData: t } = this;\n t[0] = e.size0;\n for (let i = 1; i < e.count; i++) {\n t[i] = k(e.minSize, e.maxSize);\n }\n }\n update(e) {\n const { config: t, center: i, positionData: s, sizeData: n, velocityData: o } = this;\n let r = 0;\n if (t.controlSphere0) {\n r = 1;\n F.fromArray(s, 0);\n F.lerp(i, 0.1).toArray(s, 0);\n V.set(0, 0, 0).toArray(o, 0);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n B.y -= e.delta * t.gravity * n[idx];\n B.multiplyScalar(t.friction);\n B.clampLength(0, t.maxVelocity);\n I.add(B);\n I.toArray(s, base);\n B.toArray(o, base);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n const radius = n[idx];\n for (let jdx = idx + 1; jdx < t.count; jdx++) {\n const otherBase = 3 * jdx;\n O.fromArray(s, otherBase);\n N.fromArray(o, otherBase);\n const otherRadius = n[jdx];\n _.copy(O).sub(I);\n const dist = _.length();\n const sumRadius = radius + otherRadius;\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n j.copy(_)\n .normalize()\n .multiplyScalar(0.5 * overlap);\n H.copy(j).multiplyScalar(Math.max(B.length(), 1));\n T.copy(j).multiplyScalar(Math.max(N.length(), 1));\n I.sub(j);\n B.sub(H);\n I.toArray(s, base);\n B.toArray(o, base);\n O.add(j);\n N.add(T);\n O.toArray(s, otherBase);\n N.toArray(o, otherBase);\n }\n }\n if (t.controlSphere0) {\n _.copy(F).sub(I);\n const dist = _.length();\n const sumRadius0 = radius + n[0];\n if (dist < sumRadius0) {\n const diff = sumRadius0 - dist;\n j.copy(_.normalize()).multiplyScalar(diff);\n H.copy(j).multiplyScalar(Math.max(B.length(), 2));\n I.sub(j);\n B.sub(H);\n }\n }\n if (Math.abs(I.x) + radius > t.maxX) {\n I.x = Math.sign(I.x) * (t.maxX - radius);\n B.x = -B.x * t.wallBounce;\n }\n if (t.gravity === 0) {\n if (Math.abs(I.y) + radius > t.maxY) {\n I.y = Math.sign(I.y) * (t.maxY - radius);\n B.y = -B.y * t.wallBounce;\n }\n } else if (I.y - radius < -t.maxY) {\n I.y = -t.maxY + radius;\n B.y = -B.y * t.wallBounce;\n }\n const maxBoundary = Math.max(t.maxZ, t.maxSize);\n if (Math.abs(I.z) + radius > maxBoundary) {\n I.z = Math.sign(I.z) * (t.maxZ - radius);\n B.z = -B.z * t.wallBounce;\n }\n I.toArray(s, base);\n B.toArray(o, base);\n }\n }\n}\n\nclass Y extends c {\n constructor(e) {\n super(e);\n this.uniforms = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n this.defines.USE_UV = '';\n this.onBeforeCompile = e => {\n Object.assign(e.uniforms, this.uniforms);\n e.fragmentShader =\n '\\n uniform float thicknessPower;\\n uniform float thicknessScale;\\n uniform float thicknessDistortion;\\n uniform float thicknessAmbient;\\n uniform float thicknessAttenuation;\\n ' +\n e.fragmentShader;\n e.fragmentShader = e.fragmentShader.replace(\n 'void main() {',\n '\\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\\n #ifdef USE_COLOR\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\\n #else\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\\n #endif\\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\\n }\\n\\n void main() {\\n '\n );\n const t = h.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n '\\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\\n '\n );\n e.fragmentShader = e.fragmentShader.replace('#include ', t);\n if (this.onBeforeCompile2) this.onBeforeCompile2(e);\n };\n }\n}\n\nconst X = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 16777215,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new m();\n\nclass Z extends d {\n constructor(e, t = {}) {\n const i = { ...X, ...t };\n const s = new z();\n const n = new p(e, 0.04).fromScene(s).texture;\n const o = new g();\n const r = new Y({ envMap: n, ...i.materialParams });\n r.envMapRotation.x = -Math.PI / 2;\n super(o, r, i.count);\n this.config = i;\n this.physics = new W(i);\n this.#S();\n this.setColors(i.colors);\n }\n #S() {\n this.ambientLight = new f(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new u(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n setColors(e) {\n if (Array.isArray(e) && e.length > 1) {\n const t = (function (e) {\n let t, i;\n function setColors(e) {\n t = e;\n i = [];\n t.forEach(col => {\n i.push(new l(col));\n });\n }\n setColors(e);\n return {\n setColors,\n getColorAt: function (ratio, out = new l()) {\n const scaled = Math.max(0, Math.min(1, ratio)) * (t.length - 1);\n const idx = Math.floor(scaled);\n const start = i[idx];\n if (idx >= t.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = i[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(e);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, t.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light.color.copy(t.getColorAt(idx / this.count));\n }\n }\n this.instanceColor.needsUpdate = true;\n }\n }\n update(e) {\n this.physics.update(e);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\nfunction createBallpit(e, t = {}) {\n const i = new x({\n canvas: e,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let s;\n i.renderer.toneMapping = v;\n i.camera.position.set(0, 0, 20);\n i.camera.lookAt(0, 0, 0);\n i.cameraMaxAspect = 1.5;\n i.resize();\n initialize(t);\n const n = new y();\n const o = new w(new a(0, 0, 1), 0);\n const r = new a();\n let c = false;\n\n e.style.touchAction = 'none';\n e.style.userSelect = 'none';\n e.style.webkitUserSelect = 'none';\n\n const h = S({\n domElement: e,\n onMove() {\n n.setFromCamera(h.nPosition, i.camera);\n i.camera.getWorldDirection(o.normal);\n n.ray.intersectPlane(o, r);\n s.physics.center.copy(r);\n s.config.controlSphere0 = true;\n },\n onLeave() {\n s.config.controlSphere0 = false;\n }\n });\n function initialize(e) {\n if (s) {\n i.clear();\n i.scene.remove(s);\n }\n s = new Z(i.renderer, e);\n i.scene.add(s);\n }\n i.onBeforeRender = e => {\n if (!c) s.update(e);\n };\n i.onAfterResize = e => {\n s.config.maxX = e.wWidth / 2;\n s.config.maxY = e.wHeight / 2;\n };\n return {\n three: i,\n get spheres() {\n return s;\n },\n setCount(e) {\n initialize({ ...s.config, count: e });\n },\n togglePause() {\n c = !c;\n },\n dispose() {\n h.dispose();\n i.dispose();\n }\n };\n}\n\nconst Ballpit = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, { followCursor, ...props });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/Ballpit-JS-TW.json b/public/r/Ballpit-JS-TW.json
index eda060c6..28fc5164 100644
--- a/public/r/Ballpit-JS-TW.json
+++ b/public/r/Ballpit-JS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "Ballpit/Ballpit.jsx",
- "content": "import { useRef, useEffect } from 'react';\nimport {\n Clock as e,\n PerspectiveCamera as t,\n Scene as i,\n WebGLRenderer as s,\n SRGBColorSpace as n,\n MathUtils as o,\n Vector2 as r,\n Vector3 as a,\n MeshPhysicalMaterial as c,\n ShaderChunk as h,\n Color as l,\n Object3D as m,\n InstancedMesh as d,\n PMREMGenerator as p,\n SphereGeometry as g,\n AmbientLight as f,\n PointLight as u,\n ACESFilmicToneMapping as v,\n Raycaster as y,\n Plane as w\n} from 'three';\nimport { RoomEnvironment as z } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\nclass x {\n #e;\n canvas;\n camera;\n cameraMinAspect;\n cameraMaxAspect;\n cameraFov;\n maxPixelRatio;\n minPixelRatio;\n scene;\n renderer;\n #t;\n size = { width: 0, height: 0, wWidth: 0, wHeight: 0, ratio: 0, pixelRatio: 0 };\n render = this.#i;\n onBeforeRender = () => {};\n onAfterRender = () => {};\n onAfterResize = () => {};\n #s = false;\n #n = false;\n isDisposed = false;\n #o;\n #r;\n #a;\n #c = new e();\n #h = { elapsed: 0, delta: 0 };\n #l;\n constructor(e) {\n this.#e = { ...e };\n this.#m();\n this.#d();\n this.#p();\n this.resize();\n this.#g();\n }\n #m() {\n this.camera = new t();\n this.cameraFov = this.camera.fov;\n }\n #d() {\n this.scene = new i();\n }\n #p() {\n if (this.#e.canvas) {\n this.canvas = this.#e.canvas;\n } else if (this.#e.id) {\n this.canvas = document.getElementById(this.#e.id);\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas.style.display = 'block';\n const e = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#e.rendererOptions ?? {})\n };\n this.renderer = new s(e);\n this.renderer.outputColorSpace = n;\n }\n #g() {\n if (!(this.#e.size instanceof Object)) {\n window.addEventListener('resize', this.#f.bind(this));\n if (this.#e.size === 'parent' && this.canvas.parentNode) {\n this.#r = new ResizeObserver(this.#f.bind(this));\n this.#r.observe(this.canvas.parentNode);\n }\n }\n this.#o = new IntersectionObserver(this.#u.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#o.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#v.bind(this));\n }\n #y() {\n window.removeEventListener('resize', this.#f.bind(this));\n this.#r?.disconnect();\n this.#o?.disconnect();\n document.removeEventListener('visibilitychange', this.#v.bind(this));\n }\n #u(e) {\n this.#s = e[0].isIntersecting;\n this.#s ? this.#w() : this.#z();\n }\n #v() {\n if (this.#s) {\n document.hidden ? this.#z() : this.#w();\n }\n }\n #f() {\n if (this.#a) clearTimeout(this.#a);\n this.#a = setTimeout(this.resize.bind(this), 100);\n }\n resize() {\n let e, t;\n if (this.#e.size instanceof Object) {\n e = this.#e.size.width;\n t = this.#e.size.height;\n } else if (this.#e.size === 'parent' && this.canvas.parentNode) {\n e = this.canvas.parentNode.offsetWidth;\n t = this.canvas.parentNode.offsetHeight;\n } else {\n e = window.innerWidth;\n t = window.innerHeight;\n }\n this.size.width = e;\n this.size.height = t;\n this.size.ratio = e / t;\n this.#x();\n this.#b();\n this.onAfterResize(this.size);\n }\n #x() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#A(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#A(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n #A(e) {\n const t = Math.tan(o.degToRad(this.cameraFov / 2)) / (this.camera.aspect / e);\n this.camera.fov = 2 * o.radToDeg(Math.atan(t));\n }\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const e = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(e / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if (this.camera.isOrthographicCamera) {\n this.size.wHeight = this.camera.top - this.camera.bottom;\n this.size.wWidth = this.camera.right - this.camera.left;\n }\n }\n #b() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#t?.setSize(this.size.width, this.size.height);\n let e = window.devicePixelRatio;\n if (this.maxPixelRatio && e > this.maxPixelRatio) {\n e = this.maxPixelRatio;\n } else if (this.minPixelRatio && e < this.minPixelRatio) {\n e = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(e);\n this.size.pixelRatio = e;\n }\n get postprocessing() {\n return this.#t;\n }\n set postprocessing(e) {\n this.#t = e;\n this.render = e.render.bind(e);\n }\n #w() {\n if (this.#n) return;\n const animate = () => {\n this.#l = requestAnimationFrame(animate);\n this.#h.delta = this.#c.getDelta();\n this.#h.elapsed += this.#h.delta;\n this.onBeforeRender(this.#h);\n this.render();\n this.onAfterRender(this.#h);\n };\n this.#n = true;\n this.#c.start();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\n this.#c.stop();\n }\n }\n #i() {\n this.renderer.render(this.scene, this.camera);\n }\n clear() {\n this.scene.traverse(e => {\n if (e.isMesh && typeof e.material === 'object' && e.material !== null) {\n Object.keys(e.material).forEach(t => {\n const i = e.material[t];\n if (i !== null && typeof i === 'object' && typeof i.dispose === 'function') {\n i.dispose();\n }\n });\n e.material.dispose();\n e.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n dispose() {\n this.#y();\n this.#z();\n this.clear();\n this.#t?.dispose();\n this.renderer.dispose();\n this.isDisposed = true;\n }\n}\n\nconst b = new Map(),\n A = new r();\nlet R = false;\nfunction S(e) {\n const t = {\n position: new r(),\n nPosition: new r(),\n hover: false,\n touching: false,\n onEnter() {},\n onMove() {},\n onClick() {},\n onLeave() {},\n ...e\n };\n (function (e, t) {\n if (!b.has(e)) {\n b.set(e, t);\n if (!R) {\n document.body.addEventListener('pointermove', M);\n document.body.addEventListener('pointerleave', L);\n document.body.addEventListener('click', C);\n\n document.body.addEventListener('touchstart', TouchStart, { passive: false });\n document.body.addEventListener('touchmove', TouchMove, { passive: false });\n document.body.addEventListener('touchend', TouchEnd, { passive: false });\n document.body.addEventListener('touchcancel', TouchEnd, { passive: false });\n\n R = true;\n }\n }\n })(e.domElement, t);\n t.dispose = () => {\n const t = e.domElement;\n b.delete(t);\n if (b.size === 0) {\n document.body.removeEventListener('pointermove', M);\n document.body.removeEventListener('pointerleave', L);\n document.body.removeEventListener('click', C);\n\n document.body.removeEventListener('touchstart', TouchStart);\n document.body.removeEventListener('touchmove', TouchMove);\n document.body.removeEventListener('touchend', TouchEnd);\n document.body.removeEventListener('touchcancel', TouchEnd);\n\n R = false;\n }\n };\n return t;\n}\n\nfunction M(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n processInteraction();\n}\n\nfunction processInteraction() {\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n if (D(i)) {\n P(t, i);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && !t.touching) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction C(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n P(t, i);\n if (D(i)) t.onClick(t);\n }\n}\n\nfunction L() {\n for (const t of b.values()) {\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction TouchStart(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n if (D(rect)) {\n t.touching = true;\n P(t, rect);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchMove(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n P(t, rect);\n\n if (D(rect)) {\n if (!t.hover) {\n t.hover = true;\n t.touching = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && t.touching) {\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchEnd() {\n for (const [, t] of b) {\n if (t.touching) {\n t.touching = false;\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n }\n}\n\nfunction P(e, t) {\n const { position: i, nPosition: s } = e;\n i.x = A.x - t.left;\n i.y = A.y - t.top;\n s.x = (i.x / t.width) * 2 - 1;\n s.y = (-i.y / t.height) * 2 + 1;\n}\nfunction D(e) {\n const { x: t, y: i } = A;\n const { left: s, top: n, width: o, height: r } = e;\n return t >= s && t <= s + o && i >= n && i <= n + r;\n}\n\nconst { randFloat: k, randFloatSpread: E } = o;\nconst F = new a();\nconst I = new a();\nconst O = new a();\nconst V = new a();\nconst B = new a();\nconst N = new a();\nconst _ = new a();\nconst j = new a();\nconst H = new a();\nconst T = new a();\n\nclass W {\n constructor(e) {\n this.config = e;\n this.positionData = new Float32Array(3 * e.count).fill(0);\n this.velocityData = new Float32Array(3 * e.count).fill(0);\n this.sizeData = new Float32Array(e.count).fill(1);\n this.center = new a();\n this.#R();\n this.setSizes();\n }\n #R() {\n const { config: e, positionData: t } = this;\n this.center.toArray(t, 0);\n for (let i = 1; i < e.count; i++) {\n const s = 3 * i;\n t[s] = E(2 * e.maxX);\n t[s + 1] = E(2 * e.maxY);\n t[s + 2] = E(2 * e.maxZ);\n }\n }\n setSizes() {\n const { config: e, sizeData: t } = this;\n t[0] = e.size0;\n for (let i = 1; i < e.count; i++) {\n t[i] = k(e.minSize, e.maxSize);\n }\n }\n update(e) {\n const { config: t, center: i, positionData: s, sizeData: n, velocityData: o } = this;\n let r = 0;\n if (t.controlSphere0) {\n r = 1;\n F.fromArray(s, 0);\n F.lerp(i, 0.1).toArray(s, 0);\n V.set(0, 0, 0).toArray(o, 0);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n B.y -= e.delta * t.gravity * n[idx];\n B.multiplyScalar(t.friction);\n B.clampLength(0, t.maxVelocity);\n I.add(B);\n I.toArray(s, base);\n B.toArray(o, base);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n const radius = n[idx];\n for (let jdx = idx + 1; jdx < t.count; jdx++) {\n const otherBase = 3 * jdx;\n O.fromArray(s, otherBase);\n N.fromArray(o, otherBase);\n const otherRadius = n[jdx];\n _.copy(O).sub(I);\n const dist = _.length();\n const sumRadius = radius + otherRadius;\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n j.copy(_)\n .normalize()\n .multiplyScalar(0.5 * overlap);\n H.copy(j).multiplyScalar(Math.max(B.length(), 1));\n T.copy(j).multiplyScalar(Math.max(N.length(), 1));\n I.sub(j);\n B.sub(H);\n I.toArray(s, base);\n B.toArray(o, base);\n O.add(j);\n N.add(T);\n O.toArray(s, otherBase);\n N.toArray(o, otherBase);\n }\n }\n if (t.controlSphere0) {\n _.copy(F).sub(I);\n const dist = _.length();\n const sumRadius0 = radius + n[0];\n if (dist < sumRadius0) {\n const diff = sumRadius0 - dist;\n j.copy(_.normalize()).multiplyScalar(diff);\n H.copy(j).multiplyScalar(Math.max(B.length(), 2));\n I.sub(j);\n B.sub(H);\n }\n }\n if (Math.abs(I.x) + radius > t.maxX) {\n I.x = Math.sign(I.x) * (t.maxX - radius);\n B.x = -B.x * t.wallBounce;\n }\n if (t.gravity === 0) {\n if (Math.abs(I.y) + radius > t.maxY) {\n I.y = Math.sign(I.y) * (t.maxY - radius);\n B.y = -B.y * t.wallBounce;\n }\n } else if (I.y - radius < -t.maxY) {\n I.y = -t.maxY + radius;\n B.y = -B.y * t.wallBounce;\n }\n const maxBoundary = Math.max(t.maxZ, t.maxSize);\n if (Math.abs(I.z) + radius > maxBoundary) {\n I.z = Math.sign(I.z) * (t.maxZ - radius);\n B.z = -B.z * t.wallBounce;\n }\n I.toArray(s, base);\n B.toArray(o, base);\n }\n }\n}\n\nclass Y extends c {\n constructor(e) {\n super(e);\n this.uniforms = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n this.defines.USE_UV = '';\n this.onBeforeCompile = e => {\n Object.assign(e.uniforms, this.uniforms);\n e.fragmentShader =\n '\\n uniform float thicknessPower;\\n uniform float thicknessScale;\\n uniform float thicknessDistortion;\\n uniform float thicknessAmbient;\\n uniform float thicknessAttenuation;\\n ' +\n e.fragmentShader;\n e.fragmentShader = e.fragmentShader.replace(\n 'void main() {',\n '\\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\\n #ifdef USE_COLOR\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\\n #else\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\\n #endif\\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\\n }\\n\\n void main() {\\n '\n );\n const t = h.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n '\\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\\n '\n );\n e.fragmentShader = e.fragmentShader.replace('#include ', t);\n if (this.onBeforeCompile2) this.onBeforeCompile2(e);\n };\n }\n}\n\nconst X = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 16777215,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new m();\n\nclass Z extends d {\n constructor(e, t = {}) {\n const i = { ...X, ...t };\n const s = new z();\n const n = new p(e, 0.04).fromScene(s).texture;\n const o = new g();\n const r = new Y({ envMap: n, ...i.materialParams });\n r.envMapRotation.x = -Math.PI / 2;\n super(o, r, i.count);\n this.config = i;\n this.physics = new W(i);\n this.#S();\n this.setColors(i.colors);\n }\n #S() {\n this.ambientLight = new f(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new u(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n setColors(e) {\n if (Array.isArray(e) && e.length > 1) {\n const t = (function (e) {\n let t, i;\n function setColors(e) {\n t = e;\n i = [];\n t.forEach(col => {\n i.push(new l(col));\n });\n }\n setColors(e);\n return {\n setColors,\n getColorAt: function (ratio, out = new l()) {\n const scaled = Math.max(0, Math.min(1, ratio)) * (t.length - 1);\n const idx = Math.floor(scaled);\n const start = i[idx];\n if (idx >= t.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = i[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(e);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, t.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light.color.copy(t.getColorAt(idx / this.count));\n }\n }\n this.instanceColor.needsUpdate = true;\n }\n }\n update(e) {\n this.physics.update(e);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\nfunction createBallpit(e, t = {}) {\n const i = new x({\n canvas: e,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let s;\n i.renderer.toneMapping = v;\n i.camera.position.set(0, 0, 20);\n i.camera.lookAt(0, 0, 0);\n i.cameraMaxAspect = 1.5;\n i.resize();\n initialize(t);\n const n = new y();\n const o = new w(new a(0, 0, 1), 0);\n const r = new a();\n let c = false;\n\n e.style.touchAction = 'none';\n e.style.userSelect = 'none';\n e.style.webkitUserSelect = 'none';\n\n const h = S({\n domElement: e,\n onMove() {\n n.setFromCamera(h.nPosition, i.camera);\n i.camera.getWorldDirection(o.normal);\n n.ray.intersectPlane(o, r);\n s.physics.center.copy(r);\n s.config.controlSphere0 = true;\n },\n onLeave() {\n s.config.controlSphere0 = false;\n }\n });\n function initialize(e) {\n if (s) {\n i.clear();\n i.scene.remove(s);\n }\n s = new Z(i.renderer, e);\n i.scene.add(s);\n }\n i.onBeforeRender = e => {\n if (!c) s.update(e);\n };\n i.onAfterResize = e => {\n s.config.maxX = e.wWidth / 2;\n s.config.maxY = e.wHeight / 2;\n };\n return {\n three: i,\n get spheres() {\n return s;\n },\n setCount(e) {\n initialize({ ...s.config, count: e });\n },\n togglePause() {\n c = !c;\n },\n dispose() {\n h.dispose();\n i.dispose();\n }\n };\n}\n\nconst Ballpit = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, { followCursor, ...props });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n"
+ "content": "import { useEffect, useRef } from 'react';\nimport {\n Vector3 as a,\n MeshPhysicalMaterial as c,\n InstancedMesh as d,\n Clock as e,\n AmbientLight as f,\n SphereGeometry as g,\n ShaderChunk as h,\n Scene as i,\n Color as l,\n Object3D as m,\n SRGBColorSpace as n,\n MathUtils as o,\n PMREMGenerator as p,\n Vector2 as r,\n WebGLRenderer as s,\n PerspectiveCamera as t,\n PointLight as u,\n ACESFilmicToneMapping as v,\n Plane as w,\n Raycaster as y\n} from 'three';\nimport { RoomEnvironment as z } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\nclass x {\n #e;\n canvas;\n camera;\n cameraMinAspect;\n cameraMaxAspect;\n cameraFov;\n maxPixelRatio;\n minPixelRatio;\n scene;\n renderer;\n #t;\n size = { width: 0, height: 0, wWidth: 0, wHeight: 0, ratio: 0, pixelRatio: 0 };\n render = this.#i;\n onBeforeRender = () => {};\n onAfterRender = () => {};\n onAfterResize = () => {};\n #s = false;\n #n = false;\n isDisposed = false;\n #o;\n #r;\n #a;\n #c = new e();\n #h = { elapsed: 0, delta: 0 };\n #l;\n constructor(e) {\n this.#e = { ...e };\n this.#m();\n this.#d();\n this.#p();\n this.resize();\n this.#g();\n }\n #m() {\n this.camera = new t();\n this.cameraFov = this.camera.fov;\n }\n #d() {\n this.scene = new i();\n }\n #p() {\n if (this.#e.canvas) {\n this.canvas = this.#e.canvas;\n } else if (this.#e.id) {\n this.canvas = document.getElementById(this.#e.id);\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas.style.display = 'block';\n const e = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#e.rendererOptions ?? {})\n };\n this.renderer = new s(e);\n this.renderer.outputColorSpace = n;\n }\n #g() {\n if (!(this.#e.size instanceof Object)) {\n window.addEventListener('resize', this.#f.bind(this));\n if (this.#e.size === 'parent' && this.canvas.parentNode) {\n this.#r = new ResizeObserver(this.#f.bind(this));\n this.#r.observe(this.canvas.parentNode);\n }\n }\n this.#o = new IntersectionObserver(this.#u.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#o.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#v.bind(this));\n }\n #y() {\n window.removeEventListener('resize', this.#f.bind(this));\n this.#r?.disconnect();\n this.#o?.disconnect();\n document.removeEventListener('visibilitychange', this.#v.bind(this));\n }\n #u(e) {\n this.#s = e[0].isIntersecting;\n this.#s ? this.#w() : this.#z();\n }\n #v() {\n if (this.#s) {\n document.hidden ? this.#z() : this.#w();\n }\n }\n #f() {\n if (this.#a) clearTimeout(this.#a);\n this.#a = setTimeout(this.resize.bind(this), 100);\n }\n resize() {\n let e, t;\n if (this.#e.size instanceof Object) {\n e = this.#e.size.width;\n t = this.#e.size.height;\n } else if (this.#e.size === 'parent' && this.canvas.parentNode) {\n e = this.canvas.parentNode.offsetWidth;\n t = this.canvas.parentNode.offsetHeight;\n } else {\n e = window.innerWidth;\n t = window.innerHeight;\n }\n this.size.width = e;\n this.size.height = t;\n this.size.ratio = e / t;\n this.#x();\n this.#b();\n this.onAfterResize(this.size);\n }\n #x() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#A(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#A(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n #A(e) {\n const t = Math.tan(o.degToRad(this.cameraFov / 2)) / (this.camera.aspect / e);\n this.camera.fov = 2 * o.radToDeg(Math.atan(t));\n }\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const e = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(e / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if (this.camera.isOrthographicCamera) {\n this.size.wHeight = this.camera.top - this.camera.bottom;\n this.size.wWidth = this.camera.right - this.camera.left;\n }\n }\n #b() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#t?.setSize(this.size.width, this.size.height);\n let e = window.devicePixelRatio;\n if (this.maxPixelRatio && e > this.maxPixelRatio) {\n e = this.maxPixelRatio;\n } else if (this.minPixelRatio && e < this.minPixelRatio) {\n e = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(e);\n this.size.pixelRatio = e;\n }\n get postprocessing() {\n return this.#t;\n }\n set postprocessing(e) {\n this.#t = e;\n this.render = e.render.bind(e);\n }\n #w() {\n if (this.#n) return;\n const animate = () => {\n this.#l = requestAnimationFrame(animate);\n this.#h.delta = this.#c.getDelta();\n this.#h.elapsed += this.#h.delta;\n this.onBeforeRender(this.#h);\n this.render();\n this.onAfterRender(this.#h);\n };\n this.#n = true;\n this.#c.start();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\n this.#c.stop();\n }\n }\n #i() {\n this.renderer.render(this.scene, this.camera);\n }\n clear() {\n this.scene.traverse(e => {\n if (e.isMesh && typeof e.material === 'object' && e.material !== null) {\n Object.keys(e.material).forEach(t => {\n const i = e.material[t];\n if (i !== null && typeof i === 'object' && typeof i.dispose === 'function') {\n i.dispose();\n }\n });\n e.material.dispose();\n e.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n dispose() {\n this.#y();\n this.#z();\n this.clear();\n this.#t?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n}\n\nconst b = new Map(),\n A = new r();\nlet R = false;\nfunction S(e) {\n const t = {\n position: new r(),\n nPosition: new r(),\n hover: false,\n touching: false,\n onEnter() {},\n onMove() {},\n onClick() {},\n onLeave() {},\n ...e\n };\n (function (e, t) {\n if (!b.has(e)) {\n b.set(e, t);\n if (!R) {\n document.body.addEventListener('pointermove', M);\n document.body.addEventListener('pointerleave', L);\n document.body.addEventListener('click', C);\n\n document.body.addEventListener('touchstart', TouchStart, { passive: false });\n document.body.addEventListener('touchmove', TouchMove, { passive: false });\n document.body.addEventListener('touchend', TouchEnd, { passive: false });\n document.body.addEventListener('touchcancel', TouchEnd, { passive: false });\n\n R = true;\n }\n }\n })(e.domElement, t);\n t.dispose = () => {\n const t = e.domElement;\n b.delete(t);\n if (b.size === 0) {\n document.body.removeEventListener('pointermove', M);\n document.body.removeEventListener('pointerleave', L);\n document.body.removeEventListener('click', C);\n\n document.body.removeEventListener('touchstart', TouchStart);\n document.body.removeEventListener('touchmove', TouchMove);\n document.body.removeEventListener('touchend', TouchEnd);\n document.body.removeEventListener('touchcancel', TouchEnd);\n\n R = false;\n }\n };\n return t;\n}\n\nfunction M(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n processInteraction();\n}\n\nfunction processInteraction() {\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n if (D(i)) {\n P(t, i);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && !t.touching) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction C(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n P(t, i);\n if (D(i)) t.onClick(t);\n }\n}\n\nfunction L() {\n for (const t of b.values()) {\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction TouchStart(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n if (D(rect)) {\n t.touching = true;\n P(t, rect);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchMove(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n P(t, rect);\n\n if (D(rect)) {\n if (!t.hover) {\n t.hover = true;\n t.touching = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && t.touching) {\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchEnd() {\n for (const [, t] of b) {\n if (t.touching) {\n t.touching = false;\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n }\n}\n\nfunction P(e, t) {\n const { position: i, nPosition: s } = e;\n i.x = A.x - t.left;\n i.y = A.y - t.top;\n s.x = (i.x / t.width) * 2 - 1;\n s.y = (-i.y / t.height) * 2 + 1;\n}\nfunction D(e) {\n const { x: t, y: i } = A;\n const { left: s, top: n, width: o, height: r } = e;\n return t >= s && t <= s + o && i >= n && i <= n + r;\n}\n\nconst { randFloat: k, randFloatSpread: E } = o;\nconst F = new a();\nconst I = new a();\nconst O = new a();\nconst V = new a();\nconst B = new a();\nconst N = new a();\nconst _ = new a();\nconst j = new a();\nconst H = new a();\nconst T = new a();\n\nclass W {\n constructor(e) {\n this.config = e;\n this.positionData = new Float32Array(3 * e.count).fill(0);\n this.velocityData = new Float32Array(3 * e.count).fill(0);\n this.sizeData = new Float32Array(e.count).fill(1);\n this.center = new a();\n this.#R();\n this.setSizes();\n }\n #R() {\n const { config: e, positionData: t } = this;\n this.center.toArray(t, 0);\n for (let i = 1; i < e.count; i++) {\n const s = 3 * i;\n t[s] = E(2 * e.maxX);\n t[s + 1] = E(2 * e.maxY);\n t[s + 2] = E(2 * e.maxZ);\n }\n }\n setSizes() {\n const { config: e, sizeData: t } = this;\n t[0] = e.size0;\n for (let i = 1; i < e.count; i++) {\n t[i] = k(e.minSize, e.maxSize);\n }\n }\n update(e) {\n const { config: t, center: i, positionData: s, sizeData: n, velocityData: o } = this;\n let r = 0;\n if (t.controlSphere0) {\n r = 1;\n F.fromArray(s, 0);\n F.lerp(i, 0.1).toArray(s, 0);\n V.set(0, 0, 0).toArray(o, 0);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n B.y -= e.delta * t.gravity * n[idx];\n B.multiplyScalar(t.friction);\n B.clampLength(0, t.maxVelocity);\n I.add(B);\n I.toArray(s, base);\n B.toArray(o, base);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n const radius = n[idx];\n for (let jdx = idx + 1; jdx < t.count; jdx++) {\n const otherBase = 3 * jdx;\n O.fromArray(s, otherBase);\n N.fromArray(o, otherBase);\n const otherRadius = n[jdx];\n _.copy(O).sub(I);\n const dist = _.length();\n const sumRadius = radius + otherRadius;\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n j.copy(_)\n .normalize()\n .multiplyScalar(0.5 * overlap);\n H.copy(j).multiplyScalar(Math.max(B.length(), 1));\n T.copy(j).multiplyScalar(Math.max(N.length(), 1));\n I.sub(j);\n B.sub(H);\n I.toArray(s, base);\n B.toArray(o, base);\n O.add(j);\n N.add(T);\n O.toArray(s, otherBase);\n N.toArray(o, otherBase);\n }\n }\n if (t.controlSphere0) {\n _.copy(F).sub(I);\n const dist = _.length();\n const sumRadius0 = radius + n[0];\n if (dist < sumRadius0) {\n const diff = sumRadius0 - dist;\n j.copy(_.normalize()).multiplyScalar(diff);\n H.copy(j).multiplyScalar(Math.max(B.length(), 2));\n I.sub(j);\n B.sub(H);\n }\n }\n if (Math.abs(I.x) + radius > t.maxX) {\n I.x = Math.sign(I.x) * (t.maxX - radius);\n B.x = -B.x * t.wallBounce;\n }\n if (t.gravity === 0) {\n if (Math.abs(I.y) + radius > t.maxY) {\n I.y = Math.sign(I.y) * (t.maxY - radius);\n B.y = -B.y * t.wallBounce;\n }\n } else if (I.y - radius < -t.maxY) {\n I.y = -t.maxY + radius;\n B.y = -B.y * t.wallBounce;\n }\n const maxBoundary = Math.max(t.maxZ, t.maxSize);\n if (Math.abs(I.z) + radius > maxBoundary) {\n I.z = Math.sign(I.z) * (t.maxZ - radius);\n B.z = -B.z * t.wallBounce;\n }\n I.toArray(s, base);\n B.toArray(o, base);\n }\n }\n}\n\nclass Y extends c {\n constructor(e) {\n super(e);\n this.uniforms = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n this.defines.USE_UV = '';\n this.onBeforeCompile = e => {\n Object.assign(e.uniforms, this.uniforms);\n e.fragmentShader =\n '\\n uniform float thicknessPower;\\n uniform float thicknessScale;\\n uniform float thicknessDistortion;\\n uniform float thicknessAmbient;\\n uniform float thicknessAttenuation;\\n ' +\n e.fragmentShader;\n e.fragmentShader = e.fragmentShader.replace(\n 'void main() {',\n '\\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\\n #ifdef USE_COLOR\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\\n #else\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\\n #endif\\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\\n }\\n\\n void main() {\\n '\n );\n const t = h.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n '\\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\\n '\n );\n e.fragmentShader = e.fragmentShader.replace('#include ', t);\n if (this.onBeforeCompile2) this.onBeforeCompile2(e);\n };\n }\n}\n\nconst X = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 16777215,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new m();\n\nclass Z extends d {\n constructor(e, t = {}) {\n const i = { ...X, ...t };\n const s = new z();\n const n = new p(e, 0.04).fromScene(s).texture;\n const o = new g();\n const r = new Y({ envMap: n, ...i.materialParams });\n r.envMapRotation.x = -Math.PI / 2;\n super(o, r, i.count);\n this.config = i;\n this.physics = new W(i);\n this.#S();\n this.setColors(i.colors);\n }\n #S() {\n this.ambientLight = new f(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new u(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n setColors(e) {\n if (Array.isArray(e) && e.length > 1) {\n const t = (function (e) {\n let t, i;\n function setColors(e) {\n t = e;\n i = [];\n t.forEach(col => {\n i.push(new l(col));\n });\n }\n setColors(e);\n return {\n setColors,\n getColorAt: function (ratio, out = new l()) {\n const scaled = Math.max(0, Math.min(1, ratio)) * (t.length - 1);\n const idx = Math.floor(scaled);\n const start = i[idx];\n if (idx >= t.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = i[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(e);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, t.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light.color.copy(t.getColorAt(idx / this.count));\n }\n }\n this.instanceColor.needsUpdate = true;\n }\n }\n update(e) {\n this.physics.update(e);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\nfunction createBallpit(e, t = {}) {\n const i = new x({\n canvas: e,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let s;\n i.renderer.toneMapping = v;\n i.camera.position.set(0, 0, 20);\n i.camera.lookAt(0, 0, 0);\n i.cameraMaxAspect = 1.5;\n i.resize();\n initialize(t);\n const n = new y();\n const o = new w(new a(0, 0, 1), 0);\n const r = new a();\n let c = false;\n\n e.style.touchAction = 'none';\n e.style.userSelect = 'none';\n e.style.webkitUserSelect = 'none';\n\n const h = S({\n domElement: e,\n onMove() {\n n.setFromCamera(h.nPosition, i.camera);\n i.camera.getWorldDirection(o.normal);\n n.ray.intersectPlane(o, r);\n s.physics.center.copy(r);\n s.config.controlSphere0 = true;\n },\n onLeave() {\n s.config.controlSphere0 = false;\n }\n });\n function initialize(e) {\n if (s) {\n i.clear();\n i.scene.remove(s);\n }\n s = new Z(i.renderer, e);\n i.scene.add(s);\n }\n i.onBeforeRender = e => {\n if (!c) s.update(e);\n };\n i.onAfterResize = e => {\n s.config.maxX = e.wWidth / 2;\n s.config.maxY = e.wHeight / 2;\n };\n return {\n three: i,\n get spheres() {\n return s;\n },\n setCount(e) {\n initialize({ ...s.config, count: e });\n },\n togglePause() {\n c = !c;\n },\n dispose() {\n h.dispose();\n i.dispose();\n }\n };\n}\n\nconst Ballpit = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, { followCursor, ...props });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/Ballpit-TS-CSS.json b/public/r/Ballpit-TS-CSS.json
index 49685ea7..e57686b6 100644
--- a/public/r/Ballpit-TS-CSS.json
+++ b/public/r/Ballpit-TS-CSS.json
@@ -8,12 +8,12 @@
{
"type": "registry:component",
"path": "Ballpit/Ballpit.tsx",
- "content": "import React, { useRef, useEffect } from 'react';\nimport {\n Clock,\n PerspectiveCamera,\n Scene,\n WebGLRenderer,\n WebGLRendererParameters,\n SRGBColorSpace,\n MathUtils,\n Vector2,\n Vector3,\n MeshPhysicalMaterial,\n ShaderChunk,\n Color,\n Object3D,\n InstancedMesh,\n PMREMGenerator,\n SphereGeometry,\n AmbientLight,\n PointLight,\n ACESFilmicToneMapping,\n Raycaster,\n Plane\n} from 'three';\nimport { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';\nimport { Observer } from 'gsap/Observer';\nimport { gsap } from 'gsap';\n\ngsap.registerPlugin(Observer);\n\ninterface XConfig {\n canvas?: HTMLCanvasElement;\n id?: string;\n rendererOptions?: Partial;\n size?: 'parent' | { width: number; height: number };\n}\n\ninterface SizeData {\n width: number;\n height: number;\n wWidth: number;\n wHeight: number;\n ratio: number;\n pixelRatio: number;\n}\n\nclass X {\n #config: XConfig;\n #postprocessing: any;\n #resizeObserver?: ResizeObserver;\n #intersectionObserver?: IntersectionObserver;\n #resizeTimer?: number;\n #animationFrameId: number = 0;\n #clock: Clock = new Clock();\n #animationState = { elapsed: 0, delta: 0 };\n #isAnimating: boolean = false;\n #isVisible: boolean = false;\n\n canvas!: HTMLCanvasElement;\n camera!: PerspectiveCamera;\n cameraMinAspect?: number;\n cameraMaxAspect?: number;\n cameraFov!: number;\n maxPixelRatio?: number;\n minPixelRatio?: number;\n scene!: Scene;\n renderer!: WebGLRenderer;\n size: SizeData = {\n width: 0,\n height: 0,\n wWidth: 0,\n wHeight: 0,\n ratio: 0,\n pixelRatio: 0\n };\n\n render: () => void = this.#render.bind(this);\n onBeforeRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterResize: (size: SizeData) => void = () => {};\n isDisposed: boolean = false;\n\n constructor(config: XConfig) {\n this.#config = { ...config };\n this.#initCamera();\n this.#initScene();\n this.#initRenderer();\n this.resize();\n this.#initObservers();\n }\n\n #initCamera() {\n this.camera = new PerspectiveCamera();\n this.cameraFov = this.camera.fov;\n }\n\n #initScene() {\n this.scene = new Scene();\n }\n\n #initRenderer() {\n if (this.#config.canvas) {\n this.canvas = this.#config.canvas;\n } else if (this.#config.id) {\n const elem = document.getElementById(this.#config.id);\n if (elem instanceof HTMLCanvasElement) {\n this.canvas = elem;\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas!.style.display = 'block';\n const rendererOptions: WebGLRendererParameters = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#config.rendererOptions ?? {})\n };\n this.renderer = new WebGLRenderer(rendererOptions);\n this.renderer.outputColorSpace = SRGBColorSpace;\n }\n\n #initObservers() {\n if (!(this.#config.size instanceof Object)) {\n window.addEventListener('resize', this.#onResize.bind(this));\n if (this.#config.size === 'parent' && this.canvas.parentNode) {\n this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));\n this.#resizeObserver.observe(this.canvas.parentNode as Element);\n }\n }\n this.#intersectionObserver = new IntersectionObserver(this.#onIntersection.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#intersectionObserver.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n\n #onResize() {\n if (this.#resizeTimer) clearTimeout(this.#resizeTimer);\n this.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);\n }\n\n resize() {\n let w: number, h: number;\n if (this.#config.size instanceof Object) {\n w = this.#config.size.width;\n h = this.#config.size.height;\n } else if (this.#config.size === 'parent' && this.canvas.parentNode) {\n w = (this.canvas.parentNode as HTMLElement).offsetWidth;\n h = (this.canvas.parentNode as HTMLElement).offsetHeight;\n } else {\n w = window.innerWidth;\n h = window.innerHeight;\n }\n this.size.width = w;\n this.size.height = h;\n this.size.ratio = w / h;\n this.#updateCamera();\n this.#updateRenderer();\n this.onAfterResize(this.size);\n }\n\n #updateCamera() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#adjustFov(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#adjustFov(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n\n #adjustFov(aspect: number) {\n const tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));\n const newTan = tanFov / (this.camera.aspect / aspect);\n this.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));\n }\n\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const fovRad = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(fovRad / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if ((this.camera as any).isOrthographicCamera) {\n const cam = this.camera as any;\n this.size.wHeight = cam.top - cam.bottom;\n this.size.wWidth = cam.right - cam.left;\n }\n }\n\n #updateRenderer() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#postprocessing?.setSize(this.size.width, this.size.height);\n let pr = window.devicePixelRatio;\n if (this.maxPixelRatio && pr > this.maxPixelRatio) {\n pr = this.maxPixelRatio;\n } else if (this.minPixelRatio && pr < this.minPixelRatio) {\n pr = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(pr);\n this.size.pixelRatio = pr;\n }\n\n get postprocessing() {\n return this.#postprocessing;\n }\n set postprocessing(value: any) {\n this.#postprocessing = value;\n this.render = value.render.bind(value);\n }\n\n #onIntersection(entries: IntersectionObserverEntry[]) {\n this.#isAnimating = entries[0].isIntersecting;\n this.#isAnimating ? this.#startAnimation() : this.#stopAnimation();\n }\n\n #onVisibilityChange() {\n if (this.#isAnimating) {\n document.hidden ? this.#stopAnimation() : this.#startAnimation();\n }\n }\n\n #startAnimation() {\n if (this.#isVisible) return;\n const animateFrame = () => {\n this.#animationFrameId = requestAnimationFrame(animateFrame);\n this.#animationState.delta = this.#clock.getDelta();\n this.#animationState.elapsed += this.#animationState.delta;\n this.onBeforeRender(this.#animationState);\n this.render();\n this.onAfterRender(this.#animationState);\n };\n this.#isVisible = true;\n this.#clock.start();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\n this.#clock.stop();\n }\n }\n\n #render() {\n this.renderer.render(this.scene, this.camera);\n }\n\n clear() {\n this.scene.traverse(obj => {\n if ((obj as any).isMesh && typeof (obj as any).material === 'object' && (obj as any).material !== null) {\n Object.keys((obj as any).material).forEach(key => {\n const matProp = (obj as any).material[key];\n if (matProp && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n (obj as any).material.dispose();\n (obj as any).geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n this.#onResizeCleanup();\n this.#stopAnimation();\n this.clear();\n this.#postprocessing?.dispose();\n this.renderer.dispose();\n this.isDisposed = true;\n }\n\n #onResizeCleanup() {\n window.removeEventListener('resize', this.#onResize.bind(this));\n this.#resizeObserver?.disconnect();\n this.#intersectionObserver?.disconnect();\n document.removeEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n}\n\ninterface WConfig {\n count: number;\n maxX: number;\n maxY: number;\n maxZ: number;\n maxSize: number;\n minSize: number;\n size0: number;\n gravity: number;\n friction: number;\n wallBounce: number;\n maxVelocity: number;\n controlSphere0?: boolean;\n followCursor?: boolean;\n}\n\nclass W {\n config: WConfig;\n positionData: Float32Array;\n velocityData: Float32Array;\n sizeData: Float32Array;\n center: Vector3 = new Vector3();\n\n constructor(config: WConfig) {\n this.config = config;\n this.positionData = new Float32Array(3 * config.count).fill(0);\n this.velocityData = new Float32Array(3 * config.count).fill(0);\n this.sizeData = new Float32Array(config.count).fill(1);\n this.center = new Vector3();\n this.#initializePositions();\n this.setSizes();\n }\n\n #initializePositions() {\n const { config, positionData } = this;\n this.center.toArray(positionData, 0);\n for (let i = 1; i < config.count; i++) {\n const idx = 3 * i;\n positionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);\n positionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);\n positionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);\n }\n }\n\n setSizes() {\n const { config, sizeData } = this;\n sizeData[0] = config.size0;\n for (let i = 1; i < config.count; i++) {\n sizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);\n }\n }\n\n update(deltaInfo: { delta: number }) {\n const { config, center, positionData, sizeData, velocityData } = this;\n let startIdx = 0;\n if (config.controlSphere0) {\n startIdx = 1;\n const firstVec = new Vector3().fromArray(positionData, 0);\n firstVec.lerp(center, 0.1).toArray(positionData, 0);\n new Vector3(0, 0, 0).toArray(velocityData, 0);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n vel.y -= deltaInfo.delta * config.gravity * sizeData[idx];\n vel.multiplyScalar(config.friction);\n vel.clampLength(0, config.maxVelocity);\n pos.add(vel);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n const radius = sizeData[idx];\n for (let jdx = idx + 1; jdx < config.count; jdx++) {\n const otherBase = 3 * jdx;\n const otherPos = new Vector3().fromArray(positionData, otherBase);\n const otherVel = new Vector3().fromArray(velocityData, otherBase);\n const diff = new Vector3().copy(otherPos).sub(pos);\n const dist = diff.length();\n const sumRadius = radius + sizeData[jdx];\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n const correction = diff.normalize().multiplyScalar(0.5 * overlap);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 1));\n pos.sub(correction);\n vel.sub(velCorrection);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n otherPos.add(correction);\n otherVel.add(correction.clone().multiplyScalar(Math.max(otherVel.length(), 1)));\n otherPos.toArray(positionData, otherBase);\n otherVel.toArray(velocityData, otherBase);\n }\n }\n if (config.controlSphere0) {\n const diff = new Vector3().copy(new Vector3().fromArray(positionData, 0)).sub(pos);\n const d = diff.length();\n const sumRadius0 = radius + sizeData[0];\n if (d < sumRadius0) {\n const correction = diff.normalize().multiplyScalar(sumRadius0 - d);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 2));\n pos.sub(correction);\n vel.sub(velCorrection);\n }\n }\n if (Math.abs(pos.x) + radius > config.maxX) {\n pos.x = Math.sign(pos.x) * (config.maxX - radius);\n vel.x = -vel.x * config.wallBounce;\n }\n if (config.gravity === 0) {\n if (Math.abs(pos.y) + radius > config.maxY) {\n pos.y = Math.sign(pos.y) * (config.maxY - radius);\n vel.y = -vel.y * config.wallBounce;\n }\n } else if (pos.y - radius < -config.maxY) {\n pos.y = -config.maxY + radius;\n vel.y = -vel.y * config.wallBounce;\n }\n const maxBoundary = Math.max(config.maxZ, config.maxSize);\n if (Math.abs(pos.z) + radius > maxBoundary) {\n pos.z = Math.sign(pos.z) * (config.maxZ - radius);\n vel.z = -vel.z * config.wallBounce;\n }\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n }\n}\n\nclass Y extends MeshPhysicalMaterial {\n uniforms: { [key: string]: { value: any } } = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n\n constructor(params: any) {\n super(params);\n this.defines = { USE_UV: '' };\n this.onBeforeCompile = shader => {\n Object.assign(shader.uniforms, this.uniforms);\n shader.fragmentShader =\n `\n uniform float thicknessPower;\n uniform float thicknessScale;\n uniform float thicknessDistortion;\n uniform float thicknessAmbient;\n uniform float thicknessAttenuation;\n ` + shader.fragmentShader;\n shader.fragmentShader = shader.fragmentShader.replace(\n 'void main() {',\n `\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\n #ifdef USE_COLOR\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\n #else\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\n #endif\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\n }\n\n void main() {\n `\n );\n const lightsChunk = ShaderChunk.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n `\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\n `\n );\n shader.fragmentShader = shader.fragmentShader.replace('#include ', lightsChunk);\n if (this.onBeforeCompile2) this.onBeforeCompile2(shader);\n };\n }\n onBeforeCompile2?: (shader: any) => void;\n}\n\nconst XConfig = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 0xffffff,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new Object3D();\n\nlet globalPointerActive = false;\nconst pointerPosition = new Vector2();\n\ninterface PointerData {\n position: Vector2;\n nPosition: Vector2;\n hover: boolean;\n touching: boolean;\n onEnter: (data: PointerData) => void;\n onMove: (data: PointerData) => void;\n onClick: (data: PointerData) => void;\n onLeave: (data: PointerData) => void;\n dispose?: () => void;\n}\n\nconst pointerMap = new Map();\n\nfunction createPointerData(options: Partial & { domElement: HTMLElement }): PointerData {\n const defaultData: PointerData = {\n position: new Vector2(),\n nPosition: new Vector2(),\n hover: false,\n touching: false,\n onEnter: () => {},\n onMove: () => {},\n onClick: () => {},\n onLeave: () => {},\n ...options\n };\n if (!pointerMap.has(options.domElement)) {\n pointerMap.set(options.domElement, defaultData);\n if (!globalPointerActive) {\n document.body.addEventListener('pointermove', onPointerMove as EventListener);\n document.body.addEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.addEventListener('click', onPointerClick as EventListener);\n\n document.body.addEventListener('touchstart', onTouchStart as EventListener, { passive: false });\n document.body.addEventListener('touchmove', onTouchMove as EventListener, { passive: false });\n document.body.addEventListener('touchend', onTouchEnd as EventListener, { passive: false });\n document.body.addEventListener('touchcancel', onTouchEnd as EventListener, { passive: false });\n globalPointerActive = true;\n }\n }\n defaultData.dispose = () => {\n pointerMap.delete(options.domElement);\n if (pointerMap.size === 0) {\n document.body.removeEventListener('pointermove', onPointerMove as EventListener);\n document.body.removeEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.removeEventListener('click', onPointerClick as EventListener);\n\n document.body.removeEventListener('touchstart', onTouchStart as EventListener);\n document.body.removeEventListener('touchmove', onTouchMove as EventListener);\n document.body.removeEventListener('touchend', onTouchEnd as EventListener);\n document.body.removeEventListener('touchcancel', onTouchEnd as EventListener);\n globalPointerActive = false;\n }\n };\n return defaultData;\n}\n\nfunction onPointerMove(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n processPointerInteraction();\n}\n\nfunction processPointerInteraction() {\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && !data.touching) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction onTouchStart(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n data.touching = true;\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchMove(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) {\n if (!data.hover) {\n data.hover = true;\n data.touching = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && data.touching) {\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchEnd() {\n for (const [, data] of pointerMap) {\n if (data.touching) {\n data.touching = false;\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n }\n}\n\nfunction onPointerClick(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) data.onClick(data);\n }\n}\n\nfunction onPointerLeave() {\n for (const data of pointerMap.values()) {\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction updatePointerData(data: PointerData, rect: DOMRect) {\n data.position.set(pointerPosition.x - rect.left, pointerPosition.y - rect.top);\n data.nPosition.set((data.position.x / rect.width) * 2 - 1, (-data.position.y / rect.height) * 2 + 1);\n}\n\nfunction isInside(rect: DOMRect) {\n return (\n pointerPosition.x >= rect.left &&\n pointerPosition.x <= rect.left + rect.width &&\n pointerPosition.y >= rect.top &&\n pointerPosition.y <= rect.top + rect.height\n );\n}\n\nconst { randFloat, randFloatSpread } = MathUtils;\nconst F = new Vector3();\nconst I = new Vector3();\nconst O = new Vector3();\nconst V = new Vector3();\nconst B = new Vector3();\nconst N = new Vector3();\nconst _ = new Vector3();\nconst j = new Vector3();\nconst H = new Vector3();\nconst T = new Vector3();\n\nclass Z extends InstancedMesh {\n config: typeof XConfig;\n physics: W;\n ambientLight: AmbientLight | undefined;\n light: PointLight | undefined;\n\n constructor(renderer: WebGLRenderer, params: Partial = {}) {\n const config = { ...XConfig, ...params };\n const roomEnv = new RoomEnvironment();\n const pmrem = new PMREMGenerator(renderer);\n const envTexture = pmrem.fromScene(roomEnv).texture;\n const geometry = new SphereGeometry();\n const material = new Y({ envMap: envTexture, ...config.materialParams });\n material.envMapRotation.x = -Math.PI / 2;\n super(geometry, material, config.count);\n this.config = config;\n this.physics = new W(config);\n this.#setupLights();\n this.setColors(config.colors);\n }\n\n #setupLights() {\n this.ambientLight = new AmbientLight(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new PointLight(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n\n setColors(colors: number[]) {\n if (Array.isArray(colors) && colors.length > 1) {\n const colorUtils = (function (colorsArr: number[]) {\n let baseColors: number[] = colorsArr;\n let colorObjects: Color[] = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n return {\n setColors: (cols: number[]) => {\n baseColors = cols;\n colorObjects = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n },\n getColorAt: (ratio: number, out: Color = new Color()) => {\n const clamped = Math.max(0, Math.min(1, ratio));\n const scaled = clamped * (baseColors.length - 1);\n const idx = Math.floor(scaled);\n const start = colorObjects[idx];\n if (idx >= baseColors.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = colorObjects[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(colors);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, colorUtils.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light!.color.copy(colorUtils.getColorAt(idx / this.count));\n }\n }\n\n if (!this.instanceColor) return;\n this.instanceColor.needsUpdate = true;\n }\n }\n\n update(deltaInfo: { delta: number }) {\n this.physics.update(deltaInfo);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light!.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\ninterface CreateBallpitReturn {\n three: X;\n spheres: Z;\n setCount: (count: number) => void;\n togglePause: () => void;\n dispose: () => void;\n}\n\nfunction createBallpit(canvas: HTMLCanvasElement, config: any = {}): CreateBallpitReturn {\n const threeInstance = new X({\n canvas,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let spheres: Z;\n threeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n threeInstance.camera.position.set(0, 0, 20);\n threeInstance.camera.lookAt(0, 0, 0);\n threeInstance.cameraMaxAspect = 1.5;\n threeInstance.resize();\n initialize(config);\n const raycaster = new Raycaster();\n const plane = new Plane(new Vector3(0, 0, 1), 0);\n const intersectionPoint = new Vector3();\n let isPaused = false;\n\n canvas.style.touchAction = 'none';\n canvas.style.userSelect = 'none';\n (canvas.style as any).webkitUserSelect = 'none';\n\n const pointerData = createPointerData({\n domElement: canvas,\n onMove() {\n raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n threeInstance.camera.getWorldDirection(plane.normal);\n raycaster.ray.intersectPlane(plane, intersectionPoint);\n spheres.physics.center.copy(intersectionPoint);\n spheres.config.controlSphere0 = true;\n },\n onLeave() {\n spheres.config.controlSphere0 = false;\n }\n });\n function initialize(cfg: any) {\n if (spheres) {\n threeInstance.clear();\n threeInstance.scene.remove(spheres);\n }\n spheres = new Z(threeInstance.renderer, cfg);\n threeInstance.scene.add(spheres);\n }\n threeInstance.onBeforeRender = deltaInfo => {\n if (!isPaused) spheres.update(deltaInfo);\n };\n threeInstance.onAfterResize = size => {\n spheres.config.maxX = size.wWidth / 2;\n spheres.config.maxY = size.wHeight / 2;\n };\n return {\n three: threeInstance,\n get spheres() {\n return spheres;\n },\n setCount(count: number) {\n initialize({ ...spheres.config, count });\n },\n togglePause() {\n isPaused = !isPaused;\n },\n dispose() {\n pointerData.dispose?.();\n threeInstance.dispose();\n }\n };\n}\n\ninterface BallpitProps {\n className?: string;\n followCursor?: boolean;\n [key: string]: any;\n}\n\nconst Ballpit: React.FC = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, {\n followCursor,\n ...props\n });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n"
+ "content": "import { gsap } from 'gsap';\nimport { Observer } from 'gsap/Observer';\nimport React, { useEffect, useRef } from 'react';\nimport {\n ACESFilmicToneMapping,\n AmbientLight,\n Clock,\n Color,\n InstancedMesh,\n MathUtils,\n MeshPhysicalMaterial,\n Object3D,\n PerspectiveCamera,\n Plane,\n PMREMGenerator,\n PointLight,\n Raycaster,\n Scene,\n ShaderChunk,\n SphereGeometry,\n SRGBColorSpace,\n Vector2,\n Vector3,\n WebGLRenderer,\n WebGLRendererParameters\n} from 'three';\nimport { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\ngsap.registerPlugin(Observer);\n\ninterface XConfig {\n canvas?: HTMLCanvasElement;\n id?: string;\n rendererOptions?: Partial;\n size?: 'parent' | { width: number; height: number };\n}\n\ninterface SizeData {\n width: number;\n height: number;\n wWidth: number;\n wHeight: number;\n ratio: number;\n pixelRatio: number;\n}\n\nclass X {\n #config: XConfig;\n #postprocessing: any;\n #resizeObserver?: ResizeObserver;\n #intersectionObserver?: IntersectionObserver;\n #resizeTimer?: number;\n #animationFrameId: number = 0;\n #clock: Clock = new Clock();\n #animationState = { elapsed: 0, delta: 0 };\n #isAnimating: boolean = false;\n #isVisible: boolean = false;\n\n canvas!: HTMLCanvasElement;\n camera!: PerspectiveCamera;\n cameraMinAspect?: number;\n cameraMaxAspect?: number;\n cameraFov!: number;\n maxPixelRatio?: number;\n minPixelRatio?: number;\n scene!: Scene;\n renderer!: WebGLRenderer;\n size: SizeData = {\n width: 0,\n height: 0,\n wWidth: 0,\n wHeight: 0,\n ratio: 0,\n pixelRatio: 0\n };\n\n render: () => void = this.#render.bind(this);\n onBeforeRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterResize: (size: SizeData) => void = () => {};\n isDisposed: boolean = false;\n\n constructor(config: XConfig) {\n this.#config = { ...config };\n this.#initCamera();\n this.#initScene();\n this.#initRenderer();\n this.resize();\n this.#initObservers();\n }\n\n #initCamera() {\n this.camera = new PerspectiveCamera();\n this.cameraFov = this.camera.fov;\n }\n\n #initScene() {\n this.scene = new Scene();\n }\n\n #initRenderer() {\n if (this.#config.canvas) {\n this.canvas = this.#config.canvas;\n } else if (this.#config.id) {\n const elem = document.getElementById(this.#config.id);\n if (elem instanceof HTMLCanvasElement) {\n this.canvas = elem;\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas!.style.display = 'block';\n const rendererOptions: WebGLRendererParameters = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#config.rendererOptions ?? {})\n };\n this.renderer = new WebGLRenderer(rendererOptions);\n this.renderer.outputColorSpace = SRGBColorSpace;\n }\n\n #initObservers() {\n if (!(this.#config.size instanceof Object)) {\n window.addEventListener('resize', this.#onResize.bind(this));\n if (this.#config.size === 'parent' && this.canvas.parentNode) {\n this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));\n this.#resizeObserver.observe(this.canvas.parentNode as Element);\n }\n }\n this.#intersectionObserver = new IntersectionObserver(this.#onIntersection.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#intersectionObserver.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n\n #onResize() {\n if (this.#resizeTimer) clearTimeout(this.#resizeTimer);\n this.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);\n }\n\n resize() {\n let w: number, h: number;\n if (this.#config.size instanceof Object) {\n w = this.#config.size.width;\n h = this.#config.size.height;\n } else if (this.#config.size === 'parent' && this.canvas.parentNode) {\n w = (this.canvas.parentNode as HTMLElement).offsetWidth;\n h = (this.canvas.parentNode as HTMLElement).offsetHeight;\n } else {\n w = window.innerWidth;\n h = window.innerHeight;\n }\n this.size.width = w;\n this.size.height = h;\n this.size.ratio = w / h;\n this.#updateCamera();\n this.#updateRenderer();\n this.onAfterResize(this.size);\n }\n\n #updateCamera() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#adjustFov(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#adjustFov(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n\n #adjustFov(aspect: number) {\n const tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));\n const newTan = tanFov / (this.camera.aspect / aspect);\n this.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));\n }\n\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const fovRad = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(fovRad / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if ((this.camera as any).isOrthographicCamera) {\n const cam = this.camera as any;\n this.size.wHeight = cam.top - cam.bottom;\n this.size.wWidth = cam.right - cam.left;\n }\n }\n\n #updateRenderer() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#postprocessing?.setSize(this.size.width, this.size.height);\n let pr = window.devicePixelRatio;\n if (this.maxPixelRatio && pr > this.maxPixelRatio) {\n pr = this.maxPixelRatio;\n } else if (this.minPixelRatio && pr < this.minPixelRatio) {\n pr = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(pr);\n this.size.pixelRatio = pr;\n }\n\n get postprocessing() {\n return this.#postprocessing;\n }\n set postprocessing(value: any) {\n this.#postprocessing = value;\n this.render = value.render.bind(value);\n }\n\n #onIntersection(entries: IntersectionObserverEntry[]) {\n this.#isAnimating = entries[0].isIntersecting;\n this.#isAnimating ? this.#startAnimation() : this.#stopAnimation();\n }\n\n #onVisibilityChange() {\n if (this.#isAnimating) {\n document.hidden ? this.#stopAnimation() : this.#startAnimation();\n }\n }\n\n #startAnimation() {\n if (this.#isVisible) return;\n const animateFrame = () => {\n this.#animationFrameId = requestAnimationFrame(animateFrame);\n this.#animationState.delta = this.#clock.getDelta();\n this.#animationState.elapsed += this.#animationState.delta;\n this.onBeforeRender(this.#animationState);\n this.render();\n this.onAfterRender(this.#animationState);\n };\n this.#isVisible = true;\n this.#clock.start();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\n this.#clock.stop();\n }\n }\n\n #render() {\n this.renderer.render(this.scene, this.camera);\n }\n\n clear() {\n this.scene.traverse(obj => {\n if ((obj as any).isMesh && typeof (obj as any).material === 'object' && (obj as any).material !== null) {\n Object.keys((obj as any).material).forEach(key => {\n const matProp = (obj as any).material[key];\n if (matProp && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n (obj as any).material.dispose();\n (obj as any).geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n this.#onResizeCleanup();\n this.#stopAnimation();\n this.clear();\n this.#postprocessing?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n\n #onResizeCleanup() {\n window.removeEventListener('resize', this.#onResize.bind(this));\n this.#resizeObserver?.disconnect();\n this.#intersectionObserver?.disconnect();\n document.removeEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n}\n\ninterface WConfig {\n count: number;\n maxX: number;\n maxY: number;\n maxZ: number;\n maxSize: number;\n minSize: number;\n size0: number;\n gravity: number;\n friction: number;\n wallBounce: number;\n maxVelocity: number;\n controlSphere0?: boolean;\n followCursor?: boolean;\n}\n\nclass W {\n config: WConfig;\n positionData: Float32Array;\n velocityData: Float32Array;\n sizeData: Float32Array;\n center: Vector3 = new Vector3();\n\n constructor(config: WConfig) {\n this.config = config;\n this.positionData = new Float32Array(3 * config.count).fill(0);\n this.velocityData = new Float32Array(3 * config.count).fill(0);\n this.sizeData = new Float32Array(config.count).fill(1);\n this.center = new Vector3();\n this.#initializePositions();\n this.setSizes();\n }\n\n #initializePositions() {\n const { config, positionData } = this;\n this.center.toArray(positionData, 0);\n for (let i = 1; i < config.count; i++) {\n const idx = 3 * i;\n positionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);\n positionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);\n positionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);\n }\n }\n\n setSizes() {\n const { config, sizeData } = this;\n sizeData[0] = config.size0;\n for (let i = 1; i < config.count; i++) {\n sizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);\n }\n }\n\n update(deltaInfo: { delta: number }) {\n const { config, center, positionData, sizeData, velocityData } = this;\n let startIdx = 0;\n if (config.controlSphere0) {\n startIdx = 1;\n const firstVec = new Vector3().fromArray(positionData, 0);\n firstVec.lerp(center, 0.1).toArray(positionData, 0);\n new Vector3(0, 0, 0).toArray(velocityData, 0);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n vel.y -= deltaInfo.delta * config.gravity * sizeData[idx];\n vel.multiplyScalar(config.friction);\n vel.clampLength(0, config.maxVelocity);\n pos.add(vel);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n const radius = sizeData[idx];\n for (let jdx = idx + 1; jdx < config.count; jdx++) {\n const otherBase = 3 * jdx;\n const otherPos = new Vector3().fromArray(positionData, otherBase);\n const otherVel = new Vector3().fromArray(velocityData, otherBase);\n const diff = new Vector3().copy(otherPos).sub(pos);\n const dist = diff.length();\n const sumRadius = radius + sizeData[jdx];\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n const correction = diff.normalize().multiplyScalar(0.5 * overlap);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 1));\n pos.sub(correction);\n vel.sub(velCorrection);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n otherPos.add(correction);\n otherVel.add(correction.clone().multiplyScalar(Math.max(otherVel.length(), 1)));\n otherPos.toArray(positionData, otherBase);\n otherVel.toArray(velocityData, otherBase);\n }\n }\n if (config.controlSphere0) {\n const diff = new Vector3().copy(new Vector3().fromArray(positionData, 0)).sub(pos);\n const d = diff.length();\n const sumRadius0 = radius + sizeData[0];\n if (d < sumRadius0) {\n const correction = diff.normalize().multiplyScalar(sumRadius0 - d);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 2));\n pos.sub(correction);\n vel.sub(velCorrection);\n }\n }\n if (Math.abs(pos.x) + radius > config.maxX) {\n pos.x = Math.sign(pos.x) * (config.maxX - radius);\n vel.x = -vel.x * config.wallBounce;\n }\n if (config.gravity === 0) {\n if (Math.abs(pos.y) + radius > config.maxY) {\n pos.y = Math.sign(pos.y) * (config.maxY - radius);\n vel.y = -vel.y * config.wallBounce;\n }\n } else if (pos.y - radius < -config.maxY) {\n pos.y = -config.maxY + radius;\n vel.y = -vel.y * config.wallBounce;\n }\n const maxBoundary = Math.max(config.maxZ, config.maxSize);\n if (Math.abs(pos.z) + radius > maxBoundary) {\n pos.z = Math.sign(pos.z) * (config.maxZ - radius);\n vel.z = -vel.z * config.wallBounce;\n }\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n }\n}\n\nclass Y extends MeshPhysicalMaterial {\n uniforms: { [key: string]: { value: any } } = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n defines: { USE_UV: string };\n\n constructor(params: any) {\n super(params);\n this.defines = { USE_UV: '' };\n this.onBeforeCompile = shader => {\n Object.assign(shader.uniforms, this.uniforms);\n shader.fragmentShader =\n `\n uniform float thicknessPower;\n uniform float thicknessScale;\n uniform float thicknessDistortion;\n uniform float thicknessAmbient;\n uniform float thicknessAttenuation;\n ` + shader.fragmentShader;\n shader.fragmentShader = shader.fragmentShader.replace(\n 'void main() {',\n `\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\n #ifdef USE_COLOR\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\n #else\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\n #endif\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\n }\n\n void main() {\n `\n );\n const lightsChunk = ShaderChunk.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n `\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\n `\n );\n shader.fragmentShader = shader.fragmentShader.replace('#include ', lightsChunk);\n if (this.onBeforeCompile2) this.onBeforeCompile2(shader);\n };\n }\n onBeforeCompile2?: (shader: any) => void;\n}\n\nconst XConfig = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 0xffffff,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new Object3D();\n\nlet globalPointerActive = false;\nconst pointerPosition = new Vector2();\n\ninterface PointerData {\n position: Vector2;\n nPosition: Vector2;\n hover: boolean;\n touching: boolean;\n onEnter: (data: PointerData) => void;\n onMove: (data: PointerData) => void;\n onClick: (data: PointerData) => void;\n onLeave: (data: PointerData) => void;\n dispose?: () => void;\n}\n\nconst pointerMap = new Map();\n\nfunction createPointerData(options: Partial & { domElement: HTMLElement }): PointerData {\n const defaultData: PointerData = {\n position: new Vector2(),\n nPosition: new Vector2(),\n hover: false,\n touching: false,\n onEnter: () => {},\n onMove: () => {},\n onClick: () => {},\n onLeave: () => {},\n ...options\n };\n if (!pointerMap.has(options.domElement)) {\n pointerMap.set(options.domElement, defaultData);\n if (!globalPointerActive) {\n document.body.addEventListener('pointermove', onPointerMove as EventListener);\n document.body.addEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.addEventListener('click', onPointerClick as EventListener);\n\n document.body.addEventListener('touchstart', onTouchStart as EventListener, { passive: false });\n document.body.addEventListener('touchmove', onTouchMove as EventListener, { passive: false });\n document.body.addEventListener('touchend', onTouchEnd as EventListener, { passive: false });\n document.body.addEventListener('touchcancel', onTouchEnd as EventListener, { passive: false });\n globalPointerActive = true;\n }\n }\n defaultData.dispose = () => {\n pointerMap.delete(options.domElement);\n if (pointerMap.size === 0) {\n document.body.removeEventListener('pointermove', onPointerMove as EventListener);\n document.body.removeEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.removeEventListener('click', onPointerClick as EventListener);\n\n document.body.removeEventListener('touchstart', onTouchStart as EventListener);\n document.body.removeEventListener('touchmove', onTouchMove as EventListener);\n document.body.removeEventListener('touchend', onTouchEnd as EventListener);\n document.body.removeEventListener('touchcancel', onTouchEnd as EventListener);\n globalPointerActive = false;\n }\n };\n return defaultData;\n}\n\nfunction onPointerMove(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n processPointerInteraction();\n}\n\nfunction processPointerInteraction() {\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && !data.touching) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction onTouchStart(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n data.touching = true;\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchMove(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) {\n if (!data.hover) {\n data.hover = true;\n data.touching = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && data.touching) {\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchEnd() {\n for (const [, data] of pointerMap) {\n if (data.touching) {\n data.touching = false;\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n }\n}\n\nfunction onPointerClick(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) data.onClick(data);\n }\n}\n\nfunction onPointerLeave() {\n for (const data of pointerMap.values()) {\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction updatePointerData(data: PointerData, rect: DOMRect) {\n data.position.set(pointerPosition.x - rect.left, pointerPosition.y - rect.top);\n data.nPosition.set((data.position.x / rect.width) * 2 - 1, (-data.position.y / rect.height) * 2 + 1);\n}\n\nfunction isInside(rect: DOMRect) {\n return (\n pointerPosition.x >= rect.left &&\n pointerPosition.x <= rect.left + rect.width &&\n pointerPosition.y >= rect.top &&\n pointerPosition.y <= rect.top + rect.height\n );\n}\n\nconst { randFloat, randFloatSpread } = MathUtils;\nconst F = new Vector3();\nconst I = new Vector3();\nconst O = new Vector3();\nconst V = new Vector3();\nconst B = new Vector3();\nconst N = new Vector3();\nconst _ = new Vector3();\nconst j = new Vector3();\nconst H = new Vector3();\nconst T = new Vector3();\n\nclass Z extends InstancedMesh {\n config: typeof XConfig;\n physics: W;\n ambientLight: AmbientLight | undefined;\n light: PointLight | undefined;\n\n constructor(renderer: WebGLRenderer, params: Partial = {}) {\n const config = { ...XConfig, ...params };\n const roomEnv = new RoomEnvironment();\n const pmrem = new PMREMGenerator(renderer);\n const envTexture = pmrem.fromScene(roomEnv).texture;\n const geometry = new SphereGeometry();\n const material = new Y({ envMap: envTexture, ...config.materialParams });\n material.envMapRotation.x = -Math.PI / 2;\n super(geometry, material, config.count);\n this.config = config;\n this.physics = new W(config);\n this.#setupLights();\n this.setColors(config.colors);\n }\n\n #setupLights() {\n this.ambientLight = new AmbientLight(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new PointLight(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n\n setColors(colors: number[]) {\n if (Array.isArray(colors) && colors.length > 1) {\n const colorUtils = (function (colorsArr: number[]) {\n let baseColors: number[] = colorsArr;\n let colorObjects: Color[] = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n return {\n setColors: (cols: number[]) => {\n baseColors = cols;\n colorObjects = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n },\n getColorAt: (ratio: number, out: Color = new Color()) => {\n const clamped = Math.max(0, Math.min(1, ratio));\n const scaled = clamped * (baseColors.length - 1);\n const idx = Math.floor(scaled);\n const start = colorObjects[idx];\n if (idx >= baseColors.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = colorObjects[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(colors);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, colorUtils.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light!.color.copy(colorUtils.getColorAt(idx / this.count));\n }\n }\n\n if (!this.instanceColor) return;\n this.instanceColor.needsUpdate = true;\n }\n }\n\n update(deltaInfo: { delta: number }) {\n this.physics.update(deltaInfo);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light!.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\ninterface CreateBallpitReturn {\n three: X;\n spheres: Z;\n setCount: (count: number) => void;\n togglePause: () => void;\n dispose: () => void;\n}\n\nfunction createBallpit(canvas: HTMLCanvasElement, config: any = {}): CreateBallpitReturn {\n const threeInstance = new X({\n canvas,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let spheres: Z;\n threeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n threeInstance.camera.position.set(0, 0, 20);\n threeInstance.camera.lookAt(0, 0, 0);\n threeInstance.cameraMaxAspect = 1.5;\n threeInstance.resize();\n initialize(config);\n const raycaster = new Raycaster();\n const plane = new Plane(new Vector3(0, 0, 1), 0);\n const intersectionPoint = new Vector3();\n let isPaused = false;\n\n canvas.style.touchAction = 'none';\n canvas.style.userSelect = 'none';\n (canvas.style as any).webkitUserSelect = 'none';\n\n const pointerData = createPointerData({\n domElement: canvas,\n onMove() {\n raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n threeInstance.camera.getWorldDirection(plane.normal);\n raycaster.ray.intersectPlane(plane, intersectionPoint);\n spheres.physics.center.copy(intersectionPoint);\n spheres.config.controlSphere0 = true;\n },\n onLeave() {\n spheres.config.controlSphere0 = false;\n }\n });\n function initialize(cfg: any) {\n if (spheres) {\n threeInstance.clear();\n threeInstance.scene.remove(spheres);\n }\n spheres = new Z(threeInstance.renderer, cfg);\n threeInstance.scene.add(spheres);\n }\n threeInstance.onBeforeRender = deltaInfo => {\n if (!isPaused) spheres.update(deltaInfo);\n };\n threeInstance.onAfterResize = size => {\n spheres.config.maxX = size.wWidth / 2;\n spheres.config.maxY = size.wHeight / 2;\n };\n return {\n three: threeInstance,\n get spheres() {\n return spheres;\n },\n setCount(count: number) {\n initialize({ ...spheres.config, count });\n },\n togglePause() {\n isPaused = !isPaused;\n },\n dispose() {\n pointerData.dispose?.();\n threeInstance.dispose();\n }\n };\n}\n\ninterface BallpitProps {\n className?: string;\n followCursor?: boolean;\n [key: string]: any;\n}\n\nconst Ballpit: React.FC = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, {\n followCursor,\n ...props\n });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n"
}
],
"registryDependencies": [],
"dependencies": [
- "three@^0.167.1",
- "gsap@^3.13.0"
+ "gsap@^3.13.0",
+ "three@^0.167.1"
]
}
\ No newline at end of file
diff --git a/public/r/Ballpit-TS-TW.json b/public/r/Ballpit-TS-TW.json
index acc2a2bb..cf9b1360 100644
--- a/public/r/Ballpit-TS-TW.json
+++ b/public/r/Ballpit-TS-TW.json
@@ -8,12 +8,12 @@
{
"type": "registry:component",
"path": "Ballpit/Ballpit.tsx",
- "content": "import React, { useRef, useEffect } from 'react';\nimport {\n Clock,\n PerspectiveCamera,\n Scene,\n WebGLRenderer,\n WebGLRendererParameters,\n SRGBColorSpace,\n MathUtils,\n Vector2,\n Vector3,\n MeshPhysicalMaterial,\n ShaderChunk,\n Color,\n Object3D,\n InstancedMesh,\n PMREMGenerator,\n SphereGeometry,\n AmbientLight,\n PointLight,\n ACESFilmicToneMapping,\n Raycaster,\n Plane\n} from 'three';\nimport { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';\nimport { Observer } from 'gsap/Observer';\nimport { gsap } from 'gsap';\n\ngsap.registerPlugin(Observer);\n\ninterface XConfig {\n canvas?: HTMLCanvasElement;\n id?: string;\n rendererOptions?: Partial;\n size?: 'parent' | { width: number; height: number };\n}\n\ninterface SizeData {\n width: number;\n height: number;\n wWidth: number;\n wHeight: number;\n ratio: number;\n pixelRatio: number;\n}\n\nclass X {\n #config: XConfig;\n #postprocessing: any;\n #resizeObserver?: ResizeObserver;\n #intersectionObserver?: IntersectionObserver;\n #resizeTimer?: number;\n #animationFrameId: number = 0;\n #clock: Clock = new Clock();\n #animationState = { elapsed: 0, delta: 0 };\n #isAnimating: boolean = false;\n #isVisible: boolean = false;\n\n canvas!: HTMLCanvasElement;\n camera!: PerspectiveCamera;\n cameraMinAspect?: number;\n cameraMaxAspect?: number;\n cameraFov!: number;\n maxPixelRatio?: number;\n minPixelRatio?: number;\n scene!: Scene;\n renderer!: WebGLRenderer;\n size: SizeData = {\n width: 0,\n height: 0,\n wWidth: 0,\n wHeight: 0,\n ratio: 0,\n pixelRatio: 0\n };\n\n render: () => void = this.#render.bind(this);\n onBeforeRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterResize: (size: SizeData) => void = () => {};\n isDisposed: boolean = false;\n\n constructor(config: XConfig) {\n this.#config = { ...config };\n this.#initCamera();\n this.#initScene();\n this.#initRenderer();\n this.resize();\n this.#initObservers();\n }\n\n #initCamera() {\n this.camera = new PerspectiveCamera();\n this.cameraFov = this.camera.fov;\n }\n\n #initScene() {\n this.scene = new Scene();\n }\n\n #initRenderer() {\n if (this.#config.canvas) {\n this.canvas = this.#config.canvas;\n } else if (this.#config.id) {\n const elem = document.getElementById(this.#config.id);\n if (elem instanceof HTMLCanvasElement) {\n this.canvas = elem;\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas!.style.display = 'block';\n const rendererOptions: WebGLRendererParameters = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#config.rendererOptions ?? {})\n };\n this.renderer = new WebGLRenderer(rendererOptions);\n this.renderer.outputColorSpace = SRGBColorSpace;\n }\n\n #initObservers() {\n if (!(this.#config.size instanceof Object)) {\n window.addEventListener('resize', this.#onResize.bind(this));\n if (this.#config.size === 'parent' && this.canvas.parentNode) {\n this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));\n this.#resizeObserver.observe(this.canvas.parentNode as Element);\n }\n }\n this.#intersectionObserver = new IntersectionObserver(this.#onIntersection.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#intersectionObserver.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n\n #onResize() {\n if (this.#resizeTimer) clearTimeout(this.#resizeTimer);\n this.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);\n }\n\n resize() {\n let w: number, h: number;\n if (this.#config.size instanceof Object) {\n w = this.#config.size.width;\n h = this.#config.size.height;\n } else if (this.#config.size === 'parent' && this.canvas.parentNode) {\n w = (this.canvas.parentNode as HTMLElement).offsetWidth;\n h = (this.canvas.parentNode as HTMLElement).offsetHeight;\n } else {\n w = window.innerWidth;\n h = window.innerHeight;\n }\n this.size.width = w;\n this.size.height = h;\n this.size.ratio = w / h;\n this.#updateCamera();\n this.#updateRenderer();\n this.onAfterResize(this.size);\n }\n\n #updateCamera() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#adjustFov(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#adjustFov(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n\n #adjustFov(aspect: number) {\n const tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));\n const newTan = tanFov / (this.camera.aspect / aspect);\n this.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));\n }\n\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const fovRad = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(fovRad / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if ((this.camera as any).isOrthographicCamera) {\n const cam = this.camera as any;\n this.size.wHeight = cam.top - cam.bottom;\n this.size.wWidth = cam.right - cam.left;\n }\n }\n\n #updateRenderer() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#postprocessing?.setSize(this.size.width, this.size.height);\n let pr = window.devicePixelRatio;\n if (this.maxPixelRatio && pr > this.maxPixelRatio) {\n pr = this.maxPixelRatio;\n } else if (this.minPixelRatio && pr < this.minPixelRatio) {\n pr = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(pr);\n this.size.pixelRatio = pr;\n }\n\n get postprocessing() {\n return this.#postprocessing;\n }\n set postprocessing(value: any) {\n this.#postprocessing = value;\n this.render = value.render.bind(value);\n }\n\n #onIntersection(entries: IntersectionObserverEntry[]) {\n this.#isAnimating = entries[0].isIntersecting;\n this.#isAnimating ? this.#startAnimation() : this.#stopAnimation();\n }\n\n #onVisibilityChange() {\n if (this.#isAnimating) {\n document.hidden ? this.#stopAnimation() : this.#startAnimation();\n }\n }\n\n #startAnimation() {\n if (this.#isVisible) return;\n const animateFrame = () => {\n this.#animationFrameId = requestAnimationFrame(animateFrame);\n this.#animationState.delta = this.#clock.getDelta();\n this.#animationState.elapsed += this.#animationState.delta;\n this.onBeforeRender(this.#animationState);\n this.render();\n this.onAfterRender(this.#animationState);\n };\n this.#isVisible = true;\n this.#clock.start();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\n this.#clock.stop();\n }\n }\n\n #render() {\n this.renderer.render(this.scene, this.camera);\n }\n\n clear() {\n this.scene.traverse(obj => {\n if ((obj as any).isMesh && typeof (obj as any).material === 'object' && (obj as any).material !== null) {\n Object.keys((obj as any).material).forEach(key => {\n const matProp = (obj as any).material[key];\n if (matProp && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n (obj as any).material.dispose();\n (obj as any).geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n this.#onResizeCleanup();\n this.#stopAnimation();\n this.clear();\n this.#postprocessing?.dispose();\n this.renderer.dispose();\n this.isDisposed = true;\n }\n\n #onResizeCleanup() {\n window.removeEventListener('resize', this.#onResize.bind(this));\n this.#resizeObserver?.disconnect();\n this.#intersectionObserver?.disconnect();\n document.removeEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n}\n\ninterface WConfig {\n count: number;\n maxX: number;\n maxY: number;\n maxZ: number;\n maxSize: number;\n minSize: number;\n size0: number;\n gravity: number;\n friction: number;\n wallBounce: number;\n maxVelocity: number;\n controlSphere0?: boolean;\n followCursor?: boolean;\n}\n\nclass W {\n config: WConfig;\n positionData: Float32Array;\n velocityData: Float32Array;\n sizeData: Float32Array;\n center: Vector3 = new Vector3();\n\n constructor(config: WConfig) {\n this.config = config;\n this.positionData = new Float32Array(3 * config.count).fill(0);\n this.velocityData = new Float32Array(3 * config.count).fill(0);\n this.sizeData = new Float32Array(config.count).fill(1);\n this.center = new Vector3();\n this.#initializePositions();\n this.setSizes();\n }\n\n #initializePositions() {\n const { config, positionData } = this;\n this.center.toArray(positionData, 0);\n for (let i = 1; i < config.count; i++) {\n const idx = 3 * i;\n positionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);\n positionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);\n positionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);\n }\n }\n\n setSizes() {\n const { config, sizeData } = this;\n sizeData[0] = config.size0;\n for (let i = 1; i < config.count; i++) {\n sizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);\n }\n }\n\n update(deltaInfo: { delta: number }) {\n const { config, center, positionData, sizeData, velocityData } = this;\n let startIdx = 0;\n if (config.controlSphere0) {\n startIdx = 1;\n const firstVec = new Vector3().fromArray(positionData, 0);\n firstVec.lerp(center, 0.1).toArray(positionData, 0);\n new Vector3(0, 0, 0).toArray(velocityData, 0);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n vel.y -= deltaInfo.delta * config.gravity * sizeData[idx];\n vel.multiplyScalar(config.friction);\n vel.clampLength(0, config.maxVelocity);\n pos.add(vel);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n const radius = sizeData[idx];\n for (let jdx = idx + 1; jdx < config.count; jdx++) {\n const otherBase = 3 * jdx;\n const otherPos = new Vector3().fromArray(positionData, otherBase);\n const otherVel = new Vector3().fromArray(velocityData, otherBase);\n const diff = new Vector3().copy(otherPos).sub(pos);\n const dist = diff.length();\n const sumRadius = radius + sizeData[jdx];\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n const correction = diff.normalize().multiplyScalar(0.5 * overlap);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 1));\n pos.sub(correction);\n vel.sub(velCorrection);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n otherPos.add(correction);\n otherVel.add(correction.clone().multiplyScalar(Math.max(otherVel.length(), 1)));\n otherPos.toArray(positionData, otherBase);\n otherVel.toArray(velocityData, otherBase);\n }\n }\n if (config.controlSphere0) {\n const diff = new Vector3().copy(new Vector3().fromArray(positionData, 0)).sub(pos);\n const d = diff.length();\n const sumRadius0 = radius + sizeData[0];\n if (d < sumRadius0) {\n const correction = diff.normalize().multiplyScalar(sumRadius0 - d);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 2));\n pos.sub(correction);\n vel.sub(velCorrection);\n }\n }\n if (Math.abs(pos.x) + radius > config.maxX) {\n pos.x = Math.sign(pos.x) * (config.maxX - radius);\n vel.x = -vel.x * config.wallBounce;\n }\n if (config.gravity === 0) {\n if (Math.abs(pos.y) + radius > config.maxY) {\n pos.y = Math.sign(pos.y) * (config.maxY - radius);\n vel.y = -vel.y * config.wallBounce;\n }\n } else if (pos.y - radius < -config.maxY) {\n pos.y = -config.maxY + radius;\n vel.y = -vel.y * config.wallBounce;\n }\n const maxBoundary = Math.max(config.maxZ, config.maxSize);\n if (Math.abs(pos.z) + radius > maxBoundary) {\n pos.z = Math.sign(pos.z) * (config.maxZ - radius);\n vel.z = -vel.z * config.wallBounce;\n }\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n }\n}\n\nclass Y extends MeshPhysicalMaterial {\n uniforms: { [key: string]: { value: any } } = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n\n constructor(params: any) {\n super(params);\n this.defines = { USE_UV: '' };\n this.onBeforeCompile = shader => {\n Object.assign(shader.uniforms, this.uniforms);\n shader.fragmentShader =\n `\n uniform float thicknessPower;\n uniform float thicknessScale;\n uniform float thicknessDistortion;\n uniform float thicknessAmbient;\n uniform float thicknessAttenuation;\n ` + shader.fragmentShader;\n shader.fragmentShader = shader.fragmentShader.replace(\n 'void main() {',\n `\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\n #ifdef USE_COLOR\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\n #else\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\n #endif\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\n }\n\n void main() {\n `\n );\n const lightsChunk = ShaderChunk.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n `\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\n `\n );\n shader.fragmentShader = shader.fragmentShader.replace('#include ', lightsChunk);\n if (this.onBeforeCompile2) this.onBeforeCompile2(shader);\n };\n }\n onBeforeCompile2?: (shader: any) => void;\n}\n\nconst XConfig = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 0xffffff,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new Object3D();\n\nlet globalPointerActive = false;\nconst pointerPosition = new Vector2();\n\ninterface PointerData {\n position: Vector2;\n nPosition: Vector2;\n hover: boolean;\n touching: boolean;\n onEnter: (data: PointerData) => void;\n onMove: (data: PointerData) => void;\n onClick: (data: PointerData) => void;\n onLeave: (data: PointerData) => void;\n dispose?: () => void;\n}\n\nconst pointerMap = new Map();\n\nfunction createPointerData(options: Partial & { domElement: HTMLElement }): PointerData {\n const defaultData: PointerData = {\n position: new Vector2(),\n nPosition: new Vector2(),\n hover: false,\n touching: false,\n onEnter: () => {},\n onMove: () => {},\n onClick: () => {},\n onLeave: () => {},\n ...options\n };\n if (!pointerMap.has(options.domElement)) {\n pointerMap.set(options.domElement, defaultData);\n if (!globalPointerActive) {\n document.body.addEventListener('pointermove', onPointerMove as EventListener);\n document.body.addEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.addEventListener('click', onPointerClick as EventListener);\n\n document.body.addEventListener('touchstart', onTouchStart as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchmove', onTouchMove as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchend', onTouchEnd as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchcancel', onTouchEnd as EventListener, {\n passive: false\n });\n globalPointerActive = true;\n }\n }\n defaultData.dispose = () => {\n pointerMap.delete(options.domElement);\n if (pointerMap.size === 0) {\n document.body.removeEventListener('pointermove', onPointerMove as EventListener);\n document.body.removeEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.removeEventListener('click', onPointerClick as EventListener);\n\n document.body.removeEventListener('touchstart', onTouchStart as EventListener);\n document.body.removeEventListener('touchmove', onTouchMove as EventListener);\n document.body.removeEventListener('touchend', onTouchEnd as EventListener);\n document.body.removeEventListener('touchcancel', onTouchEnd as EventListener);\n globalPointerActive = false;\n }\n };\n return defaultData;\n}\n\nfunction onPointerMove(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n processPointerInteraction();\n}\n\nfunction processPointerInteraction() {\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && !data.touching) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction onTouchStart(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n data.touching = true;\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchMove(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) {\n if (!data.hover) {\n data.hover = true;\n data.touching = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && data.touching) {\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchEnd() {\n for (const [, data] of pointerMap) {\n if (data.touching) {\n data.touching = false;\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n }\n}\n\nfunction onPointerClick(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) data.onClick(data);\n }\n}\n\nfunction onPointerLeave() {\n for (const data of pointerMap.values()) {\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction updatePointerData(data: PointerData, rect: DOMRect) {\n data.position.set(pointerPosition.x - rect.left, pointerPosition.y - rect.top);\n data.nPosition.set((data.position.x / rect.width) * 2 - 1, (-data.position.y / rect.height) * 2 + 1);\n}\n\nfunction isInside(rect: DOMRect) {\n return (\n pointerPosition.x >= rect.left &&\n pointerPosition.x <= rect.left + rect.width &&\n pointerPosition.y >= rect.top &&\n pointerPosition.y <= rect.top + rect.height\n );\n}\n\nclass Z extends InstancedMesh {\n config: typeof XConfig;\n physics: W;\n ambientLight: AmbientLight | undefined;\n light: PointLight | undefined;\n\n constructor(renderer: WebGLRenderer, params: Partial = {}) {\n const config = { ...XConfig, ...params };\n const roomEnv = new RoomEnvironment();\n const pmrem = new PMREMGenerator(renderer);\n const envTexture = pmrem.fromScene(roomEnv).texture;\n const geometry = new SphereGeometry();\n const material = new Y({ envMap: envTexture, ...config.materialParams });\n material.envMapRotation.x = -Math.PI / 2;\n super(geometry, material, config.count);\n this.config = config;\n this.physics = new W(config);\n this.#setupLights();\n this.setColors(config.colors);\n }\n\n #setupLights() {\n this.ambientLight = new AmbientLight(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new PointLight(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n\n setColors(colors: number[]) {\n if (Array.isArray(colors) && colors.length > 1) {\n const colorUtils = (function (colorsArr: number[]) {\n let baseColors: number[] = colorsArr;\n let colorObjects: Color[] = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n return {\n setColors: (cols: number[]) => {\n baseColors = cols;\n colorObjects = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n },\n getColorAt: (ratio: number, out: Color = new Color()) => {\n const clamped = Math.max(0, Math.min(1, ratio));\n const scaled = clamped * (baseColors.length - 1);\n const idx = Math.floor(scaled);\n const start = colorObjects[idx];\n if (idx >= baseColors.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = colorObjects[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(colors);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, colorUtils.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light!.color.copy(colorUtils.getColorAt(idx / this.count));\n }\n }\n\n if (!this.instanceColor) return;\n this.instanceColor.needsUpdate = true;\n }\n }\n\n update(deltaInfo: { delta: number }) {\n this.physics.update(deltaInfo);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light!.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\ninterface CreateBallpitReturn {\n three: X;\n spheres: Z;\n setCount: (count: number) => void;\n togglePause: () => void;\n dispose: () => void;\n}\n\nfunction createBallpit(canvas: HTMLCanvasElement, config: any = {}): CreateBallpitReturn {\n const threeInstance = new X({\n canvas,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let spheres: Z;\n threeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n threeInstance.camera.position.set(0, 0, 20);\n threeInstance.camera.lookAt(0, 0, 0);\n threeInstance.cameraMaxAspect = 1.5;\n threeInstance.resize();\n initialize(config);\n const raycaster = new Raycaster();\n const plane = new Plane(new Vector3(0, 0, 1), 0);\n const intersectionPoint = new Vector3();\n let isPaused = false;\n\n canvas.style.touchAction = 'none';\n canvas.style.userSelect = 'none';\n (canvas.style as any).webkitUserSelect = 'none';\n\n const pointerData = createPointerData({\n domElement: canvas,\n onMove() {\n raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n threeInstance.camera.getWorldDirection(plane.normal);\n raycaster.ray.intersectPlane(plane, intersectionPoint);\n spheres.physics.center.copy(intersectionPoint);\n spheres.config.controlSphere0 = true;\n },\n onLeave() {\n spheres.config.controlSphere0 = false;\n }\n });\n function initialize(cfg: any) {\n if (spheres) {\n threeInstance.clear();\n threeInstance.scene.remove(spheres);\n }\n spheres = new Z(threeInstance.renderer, cfg);\n threeInstance.scene.add(spheres);\n }\n threeInstance.onBeforeRender = deltaInfo => {\n if (!isPaused) spheres.update(deltaInfo);\n };\n threeInstance.onAfterResize = size => {\n spheres.config.maxX = size.wWidth / 2;\n spheres.config.maxY = size.wHeight / 2;\n };\n return {\n three: threeInstance,\n get spheres() {\n return spheres;\n },\n setCount(count: number) {\n initialize({ ...spheres.config, count });\n },\n togglePause() {\n isPaused = !isPaused;\n },\n dispose() {\n pointerData.dispose?.();\n threeInstance.dispose();\n }\n };\n}\n\ninterface BallpitProps {\n className?: string;\n followCursor?: boolean;\n [key: string]: any;\n}\n\nconst Ballpit: React.FC = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, {\n followCursor,\n ...props\n });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n"
+ "content": "import { gsap } from 'gsap';\nimport { Observer } from 'gsap/Observer';\nimport React, { useEffect, useRef } from 'react';\nimport {\n ACESFilmicToneMapping,\n AmbientLight,\n Clock,\n Color,\n InstancedMesh,\n MathUtils,\n MeshPhysicalMaterial,\n Object3D,\n PerspectiveCamera,\n Plane,\n PMREMGenerator,\n PointLight,\n Raycaster,\n Scene,\n ShaderChunk,\n SphereGeometry,\n SRGBColorSpace,\n Vector2,\n Vector3,\n WebGLRenderer,\n WebGLRendererParameters\n} from 'three';\nimport { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\ngsap.registerPlugin(Observer);\n\ninterface XConfig {\n canvas?: HTMLCanvasElement;\n id?: string;\n rendererOptions?: Partial;\n size?: 'parent' | { width: number; height: number };\n}\n\ninterface SizeData {\n width: number;\n height: number;\n wWidth: number;\n wHeight: number;\n ratio: number;\n pixelRatio: number;\n}\n\nclass X {\n #config: XConfig;\n #postprocessing: any;\n #resizeObserver?: ResizeObserver;\n #intersectionObserver?: IntersectionObserver;\n #resizeTimer?: number;\n #animationFrameId: number = 0;\n #clock: Clock = new Clock();\n #animationState = { elapsed: 0, delta: 0 };\n #isAnimating: boolean = false;\n #isVisible: boolean = false;\n\n canvas!: HTMLCanvasElement;\n camera!: PerspectiveCamera;\n cameraMinAspect?: number;\n cameraMaxAspect?: number;\n cameraFov!: number;\n maxPixelRatio?: number;\n minPixelRatio?: number;\n scene!: Scene;\n renderer!: WebGLRenderer;\n size: SizeData = {\n width: 0,\n height: 0,\n wWidth: 0,\n wHeight: 0,\n ratio: 0,\n pixelRatio: 0\n };\n\n render: () => void = this.#render.bind(this);\n onBeforeRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterResize: (size: SizeData) => void = () => {};\n isDisposed: boolean = false;\n\n constructor(config: XConfig) {\n this.#config = { ...config };\n this.#initCamera();\n this.#initScene();\n this.#initRenderer();\n this.resize();\n this.#initObservers();\n }\n\n #initCamera() {\n this.camera = new PerspectiveCamera();\n this.cameraFov = this.camera.fov;\n }\n\n #initScene() {\n this.scene = new Scene();\n }\n\n #initRenderer() {\n if (this.#config.canvas) {\n this.canvas = this.#config.canvas;\n } else if (this.#config.id) {\n const elem = document.getElementById(this.#config.id);\n if (elem instanceof HTMLCanvasElement) {\n this.canvas = elem;\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas!.style.display = 'block';\n const rendererOptions: WebGLRendererParameters = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#config.rendererOptions ?? {})\n };\n this.renderer = new WebGLRenderer(rendererOptions);\n this.renderer.outputColorSpace = SRGBColorSpace;\n }\n\n #initObservers() {\n if (!(this.#config.size instanceof Object)) {\n window.addEventListener('resize', this.#onResize.bind(this));\n if (this.#config.size === 'parent' && this.canvas.parentNode) {\n this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));\n this.#resizeObserver.observe(this.canvas.parentNode as Element);\n }\n }\n this.#intersectionObserver = new IntersectionObserver(this.#onIntersection.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#intersectionObserver.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n\n #onResize() {\n if (this.#resizeTimer) clearTimeout(this.#resizeTimer);\n this.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);\n }\n\n resize() {\n let w: number, h: number;\n if (this.#config.size instanceof Object) {\n w = this.#config.size.width;\n h = this.#config.size.height;\n } else if (this.#config.size === 'parent' && this.canvas.parentNode) {\n w = (this.canvas.parentNode as HTMLElement).offsetWidth;\n h = (this.canvas.parentNode as HTMLElement).offsetHeight;\n } else {\n w = window.innerWidth;\n h = window.innerHeight;\n }\n this.size.width = w;\n this.size.height = h;\n this.size.ratio = w / h;\n this.#updateCamera();\n this.#updateRenderer();\n this.onAfterResize(this.size);\n }\n\n #updateCamera() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#adjustFov(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#adjustFov(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n\n #adjustFov(aspect: number) {\n const tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));\n const newTan = tanFov / (this.camera.aspect / aspect);\n this.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));\n }\n\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const fovRad = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(fovRad / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if ((this.camera as any).isOrthographicCamera) {\n const cam = this.camera as any;\n this.size.wHeight = cam.top - cam.bottom;\n this.size.wWidth = cam.right - cam.left;\n }\n }\n\n #updateRenderer() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#postprocessing?.setSize(this.size.width, this.size.height);\n let pr = window.devicePixelRatio;\n if (this.maxPixelRatio && pr > this.maxPixelRatio) {\n pr = this.maxPixelRatio;\n } else if (this.minPixelRatio && pr < this.minPixelRatio) {\n pr = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(pr);\n this.size.pixelRatio = pr;\n }\n\n get postprocessing() {\n return this.#postprocessing;\n }\n set postprocessing(value: any) {\n this.#postprocessing = value;\n this.render = value.render.bind(value);\n }\n\n #onIntersection(entries: IntersectionObserverEntry[]) {\n this.#isAnimating = entries[0].isIntersecting;\n this.#isAnimating ? this.#startAnimation() : this.#stopAnimation();\n }\n\n #onVisibilityChange() {\n if (this.#isAnimating) {\n document.hidden ? this.#stopAnimation() : this.#startAnimation();\n }\n }\n\n #startAnimation() {\n if (this.#isVisible) return;\n const animateFrame = () => {\n this.#animationFrameId = requestAnimationFrame(animateFrame);\n this.#animationState.delta = this.#clock.getDelta();\n this.#animationState.elapsed += this.#animationState.delta;\n this.onBeforeRender(this.#animationState);\n this.render();\n this.onAfterRender(this.#animationState);\n };\n this.#isVisible = true;\n this.#clock.start();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\n this.#clock.stop();\n }\n }\n\n #render() {\n this.renderer.render(this.scene, this.camera);\n }\n\n clear() {\n this.scene.traverse(obj => {\n if ((obj as any).isMesh && typeof (obj as any).material === 'object' && (obj as any).material !== null) {\n Object.keys((obj as any).material).forEach(key => {\n const matProp = (obj as any).material[key];\n if (matProp && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n (obj as any).material.dispose();\n (obj as any).geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n this.#onResizeCleanup();\n this.#stopAnimation();\n this.clear();\n this.#postprocessing?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n\n #onResizeCleanup() {\n window.removeEventListener('resize', this.#onResize.bind(this));\n this.#resizeObserver?.disconnect();\n this.#intersectionObserver?.disconnect();\n document.removeEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n}\n\ninterface WConfig {\n count: number;\n maxX: number;\n maxY: number;\n maxZ: number;\n maxSize: number;\n minSize: number;\n size0: number;\n gravity: number;\n friction: number;\n wallBounce: number;\n maxVelocity: number;\n controlSphere0?: boolean;\n followCursor?: boolean;\n}\n\nclass W {\n config: WConfig;\n positionData: Float32Array;\n velocityData: Float32Array;\n sizeData: Float32Array;\n center: Vector3 = new Vector3();\n\n constructor(config: WConfig) {\n this.config = config;\n this.positionData = new Float32Array(3 * config.count).fill(0);\n this.velocityData = new Float32Array(3 * config.count).fill(0);\n this.sizeData = new Float32Array(config.count).fill(1);\n this.center = new Vector3();\n this.#initializePositions();\n this.setSizes();\n }\n\n #initializePositions() {\n const { config, positionData } = this;\n this.center.toArray(positionData, 0);\n for (let i = 1; i < config.count; i++) {\n const idx = 3 * i;\n positionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);\n positionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);\n positionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);\n }\n }\n\n setSizes() {\n const { config, sizeData } = this;\n sizeData[0] = config.size0;\n for (let i = 1; i < config.count; i++) {\n sizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);\n }\n }\n\n update(deltaInfo: { delta: number }) {\n const { config, center, positionData, sizeData, velocityData } = this;\n let startIdx = 0;\n if (config.controlSphere0) {\n startIdx = 1;\n const firstVec = new Vector3().fromArray(positionData, 0);\n firstVec.lerp(center, 0.1).toArray(positionData, 0);\n new Vector3(0, 0, 0).toArray(velocityData, 0);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n vel.y -= deltaInfo.delta * config.gravity * sizeData[idx];\n vel.multiplyScalar(config.friction);\n vel.clampLength(0, config.maxVelocity);\n pos.add(vel);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n const radius = sizeData[idx];\n for (let jdx = idx + 1; jdx < config.count; jdx++) {\n const otherBase = 3 * jdx;\n const otherPos = new Vector3().fromArray(positionData, otherBase);\n const otherVel = new Vector3().fromArray(velocityData, otherBase);\n const diff = new Vector3().copy(otherPos).sub(pos);\n const dist = diff.length();\n const sumRadius = radius + sizeData[jdx];\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n const correction = diff.normalize().multiplyScalar(0.5 * overlap);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 1));\n pos.sub(correction);\n vel.sub(velCorrection);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n otherPos.add(correction);\n otherVel.add(correction.clone().multiplyScalar(Math.max(otherVel.length(), 1)));\n otherPos.toArray(positionData, otherBase);\n otherVel.toArray(velocityData, otherBase);\n }\n }\n if (config.controlSphere0) {\n const diff = new Vector3().copy(new Vector3().fromArray(positionData, 0)).sub(pos);\n const d = diff.length();\n const sumRadius0 = radius + sizeData[0];\n if (d < sumRadius0) {\n const correction = diff.normalize().multiplyScalar(sumRadius0 - d);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 2));\n pos.sub(correction);\n vel.sub(velCorrection);\n }\n }\n if (Math.abs(pos.x) + radius > config.maxX) {\n pos.x = Math.sign(pos.x) * (config.maxX - radius);\n vel.x = -vel.x * config.wallBounce;\n }\n if (config.gravity === 0) {\n if (Math.abs(pos.y) + radius > config.maxY) {\n pos.y = Math.sign(pos.y) * (config.maxY - radius);\n vel.y = -vel.y * config.wallBounce;\n }\n } else if (pos.y - radius < -config.maxY) {\n pos.y = -config.maxY + radius;\n vel.y = -vel.y * config.wallBounce;\n }\n const maxBoundary = Math.max(config.maxZ, config.maxSize);\n if (Math.abs(pos.z) + radius > maxBoundary) {\n pos.z = Math.sign(pos.z) * (config.maxZ - radius);\n vel.z = -vel.z * config.wallBounce;\n }\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n }\n}\n\nclass Y extends MeshPhysicalMaterial {\n uniforms: { [key: string]: { value: any } } = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n defines: { USE_UV: string; };\n\n constructor(params: any) {\n super(params);\n this.defines = { USE_UV: '' };\n this.onBeforeCompile = shader => {\n Object.assign(shader.uniforms, this.uniforms);\n shader.fragmentShader =\n `\n uniform float thicknessPower;\n uniform float thicknessScale;\n uniform float thicknessDistortion;\n uniform float thicknessAmbient;\n uniform float thicknessAttenuation;\n ` + shader.fragmentShader;\n shader.fragmentShader = shader.fragmentShader.replace(\n 'void main() {',\n `\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\n #ifdef USE_COLOR\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\n #else\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\n #endif\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\n }\n\n void main() {\n `\n );\n const lightsChunk = ShaderChunk.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n `\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\n `\n );\n shader.fragmentShader = shader.fragmentShader.replace('#include ', lightsChunk);\n if (this.onBeforeCompile2) this.onBeforeCompile2(shader);\n };\n }\n onBeforeCompile2?: (shader: any) => void;\n}\n\nconst XConfig = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 0xffffff,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new Object3D();\n\nlet globalPointerActive = false;\nconst pointerPosition = new Vector2();\n\ninterface PointerData {\n position: Vector2;\n nPosition: Vector2;\n hover: boolean;\n touching: boolean;\n onEnter: (data: PointerData) => void;\n onMove: (data: PointerData) => void;\n onClick: (data: PointerData) => void;\n onLeave: (data: PointerData) => void;\n dispose?: () => void;\n}\n\nconst pointerMap = new Map();\n\nfunction createPointerData(options: Partial & { domElement: HTMLElement }): PointerData {\n const defaultData: PointerData = {\n position: new Vector2(),\n nPosition: new Vector2(),\n hover: false,\n touching: false,\n onEnter: () => {},\n onMove: () => {},\n onClick: () => {},\n onLeave: () => {},\n ...options\n };\n if (!pointerMap.has(options.domElement)) {\n pointerMap.set(options.domElement, defaultData);\n if (!globalPointerActive) {\n document.body.addEventListener('pointermove', onPointerMove as EventListener);\n document.body.addEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.addEventListener('click', onPointerClick as EventListener);\n\n document.body.addEventListener('touchstart', onTouchStart as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchmove', onTouchMove as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchend', onTouchEnd as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchcancel', onTouchEnd as EventListener, {\n passive: false\n });\n globalPointerActive = true;\n }\n }\n defaultData.dispose = () => {\n pointerMap.delete(options.domElement);\n if (pointerMap.size === 0) {\n document.body.removeEventListener('pointermove', onPointerMove as EventListener);\n document.body.removeEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.removeEventListener('click', onPointerClick as EventListener);\n\n document.body.removeEventListener('touchstart', onTouchStart as EventListener);\n document.body.removeEventListener('touchmove', onTouchMove as EventListener);\n document.body.removeEventListener('touchend', onTouchEnd as EventListener);\n document.body.removeEventListener('touchcancel', onTouchEnd as EventListener);\n globalPointerActive = false;\n }\n };\n return defaultData;\n}\n\nfunction onPointerMove(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n processPointerInteraction();\n}\n\nfunction processPointerInteraction() {\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && !data.touching) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction onTouchStart(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n data.touching = true;\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchMove(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) {\n if (!data.hover) {\n data.hover = true;\n data.touching = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && data.touching) {\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchEnd() {\n for (const [, data] of pointerMap) {\n if (data.touching) {\n data.touching = false;\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n }\n}\n\nfunction onPointerClick(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) data.onClick(data);\n }\n}\n\nfunction onPointerLeave() {\n for (const data of pointerMap.values()) {\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction updatePointerData(data: PointerData, rect: DOMRect) {\n data.position.set(pointerPosition.x - rect.left, pointerPosition.y - rect.top);\n data.nPosition.set((data.position.x / rect.width) * 2 - 1, (-data.position.y / rect.height) * 2 + 1);\n}\n\nfunction isInside(rect: DOMRect) {\n return (\n pointerPosition.x >= rect.left &&\n pointerPosition.x <= rect.left + rect.width &&\n pointerPosition.y >= rect.top &&\n pointerPosition.y <= rect.top + rect.height\n );\n}\n\nclass Z extends InstancedMesh {\n config: typeof XConfig;\n physics: W;\n ambientLight: AmbientLight | undefined;\n light: PointLight | undefined;\n\n constructor(renderer: WebGLRenderer, params: Partial = {}) {\n const config = { ...XConfig, ...params };\n const roomEnv = new RoomEnvironment();\n const pmrem = new PMREMGenerator(renderer);\n const envTexture = pmrem.fromScene(roomEnv).texture;\n const geometry = new SphereGeometry();\n const material = new Y({ envMap: envTexture, ...config.materialParams });\n material.envMapRotation.x = -Math.PI / 2;\n super(geometry, material, config.count);\n this.config = config;\n this.physics = new W(config);\n this.#setupLights();\n this.setColors(config.colors);\n }\n\n #setupLights() {\n this.ambientLight = new AmbientLight(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new PointLight(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n\n setColors(colors: number[]) {\n if (Array.isArray(colors) && colors.length > 1) {\n const colorUtils = (function (colorsArr: number[]) {\n let baseColors: number[] = colorsArr;\n let colorObjects: Color[] = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n return {\n setColors: (cols: number[]) => {\n baseColors = cols;\n colorObjects = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n },\n getColorAt: (ratio: number, out: Color = new Color()) => {\n const clamped = Math.max(0, Math.min(1, ratio));\n const scaled = clamped * (baseColors.length - 1);\n const idx = Math.floor(scaled);\n const start = colorObjects[idx];\n if (idx >= baseColors.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = colorObjects[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(colors);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, colorUtils.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light!.color.copy(colorUtils.getColorAt(idx / this.count));\n }\n }\n\n if (!this.instanceColor) return;\n this.instanceColor.needsUpdate = true;\n }\n }\n\n update(deltaInfo: { delta: number }) {\n this.physics.update(deltaInfo);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light!.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\ninterface CreateBallpitReturn {\n three: X;\n spheres: Z;\n setCount: (count: number) => void;\n togglePause: () => void;\n dispose: () => void;\n}\n\nfunction createBallpit(canvas: HTMLCanvasElement, config: any = {}): CreateBallpitReturn {\n const threeInstance = new X({\n canvas,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let spheres: Z;\n threeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n threeInstance.camera.position.set(0, 0, 20);\n threeInstance.camera.lookAt(0, 0, 0);\n threeInstance.cameraMaxAspect = 1.5;\n threeInstance.resize();\n initialize(config);\n const raycaster = new Raycaster();\n const plane = new Plane(new Vector3(0, 0, 1), 0);\n const intersectionPoint = new Vector3();\n let isPaused = false;\n\n canvas.style.touchAction = 'none';\n canvas.style.userSelect = 'none';\n (canvas.style as any).webkitUserSelect = 'none';\n\n const pointerData = createPointerData({\n domElement: canvas,\n onMove() {\n raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n threeInstance.camera.getWorldDirection(plane.normal);\n raycaster.ray.intersectPlane(plane, intersectionPoint);\n spheres.physics.center.copy(intersectionPoint);\n spheres.config.controlSphere0 = true;\n },\n onLeave() {\n spheres.config.controlSphere0 = false;\n }\n });\n function initialize(cfg: any) {\n if (spheres) {\n threeInstance.clear();\n threeInstance.scene.remove(spheres);\n }\n spheres = new Z(threeInstance.renderer, cfg);\n threeInstance.scene.add(spheres);\n }\n threeInstance.onBeforeRender = deltaInfo => {\n if (!isPaused) spheres.update(deltaInfo);\n };\n threeInstance.onAfterResize = size => {\n spheres.config.maxX = size.wWidth / 2;\n spheres.config.maxY = size.wHeight / 2;\n };\n return {\n three: threeInstance,\n get spheres() {\n return spheres;\n },\n setCount(count: number) {\n initialize({ ...spheres.config, count });\n },\n togglePause() {\n isPaused = !isPaused;\n },\n dispose() {\n pointerData.dispose?.();\n threeInstance.dispose();\n }\n };\n}\n\ninterface BallpitProps {\n className?: string;\n followCursor?: boolean;\n [key: string]: any;\n}\n\nconst Ballpit: React.FC = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, {\n followCursor,\n ...props\n });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n"
}
],
"registryDependencies": [],
"dependencies": [
- "three@^0.167.1",
- "gsap@^3.13.0"
+ "gsap@^3.13.0",
+ "three@^0.167.1"
]
}
\ No newline at end of file
diff --git a/public/r/ColorBends-JS-CSS.json b/public/r/ColorBends-JS-CSS.json
index 53d1cbcf..8ed5dace 100644
--- a/public/r/ColorBends-JS-CSS.json
+++ b/public/r/ColorBends-JS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "ColorBends/ColorBends.jsx",
- "content": "import { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './ColorBends.css';\n\nconst MAX_COLORS = 8;\n\nconst frag = `\n#define MAX_COLORS ${MAX_COLORS}\nuniform vec2 uCanvas;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec2 uRot;\nuniform int uColorCount;\nuniform vec3 uColors[MAX_COLORS];\nuniform int uTransparent;\nuniform float uScale;\nuniform float uFrequency;\nuniform float uWarpStrength;\nuniform vec2 uPointer; // in NDC [-1,1]\nuniform float uMouseInfluence;\nuniform float uParallax;\nuniform float uNoise;\nvarying vec2 vUv;\n\nvoid main() {\n float t = uTime * uSpeed;\n vec2 p = vUv * 2.0 - 1.0;\n p += uPointer * uParallax * 0.1;\n vec2 rp = vec2(p.x * uRot.x - p.y * uRot.y, p.x * uRot.y + p.y * uRot.x);\n vec2 q = vec2(rp.x * (uCanvas.x / uCanvas.y), rp.y);\n q /= max(uScale, 0.0001);\n q /= 0.5 + 0.2 * dot(q, q);\n q += 0.2 * cos(t) - 7.56;\n vec2 toward = (uPointer - rp);\n q += toward * uMouseInfluence * 0.2;\n\n vec3 col = vec3(0.0);\n float a = 1.0;\n\n if (uColorCount > 0) {\n vec2 s = q;\n vec3 sumCol = vec3(0.0);\n float cover = 0.0;\n for (int i = 0; i < MAX_COLORS; ++i) {\n if (i >= uColorCount) break;\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3); // strong response across 0..1\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0); // allow >1 to amplify displacement\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float m = mix(m0, m1, kMix);\n float w = 1.0 - exp(-6.0 / exp(6.0 * m));\n sumCol += uColors[i] * w;\n cover = max(cover, w);\n }\n col = clamp(sumCol, 0.0, 1.0);\n a = uTransparent > 0 ? cover : 1.0;\n } else {\n vec2 s = q;\n for (int k = 0; k < 3; ++k) {\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3);\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0);\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float m = mix(m0, m1, kMix);\n col[k] = 1.0 - exp(-6.0 / exp(6.0 * m));\n }\n a = uTransparent > 0 ? max(max(col.r, col.g), col.b) : 1.0;\n }\n\n if (uNoise > 0.0001) {\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(uTime), vec2(12.9898, 78.233))) * 43758.5453123);\n col += (n - 0.5) * uNoise;\n col = clamp(col, 0.0, 1.0);\n }\n\n vec3 rgb = (uTransparent > 0) ? col * a : col;\n gl_FragColor = vec4(rgb, a);\n}\n`;\n\nconst vert = `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nexport default function ColorBends({\n className,\n style,\n rotation = 45,\n speed = 0.2,\n colors = [],\n transparent = true,\n autoRotate = 0,\n scale = 1,\n frequency = 1,\n warpStrength = 1,\n mouseInfluence = 1,\n parallax = 0.5,\n noise = 0.1\n}) {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const rafRef = useRef(null);\n const materialRef = useRef(null);\n const resizeObserverRef = useRef(null);\n const rotationRef = useRef(rotation);\n const autoRotateRef = useRef(autoRotate);\n const pointerTargetRef = useRef(new THREE.Vector2(0, 0));\n const pointerCurrentRef = useRef(new THREE.Vector2(0, 0));\n const pointerSmoothRef = useRef(8);\n\n useEffect(() => {\n const container = containerRef.current;\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0));\n const material = new THREE.ShaderMaterial({\n vertexShader: vert,\n fragmentShader: frag,\n uniforms: {\n uCanvas: { value: new THREE.Vector2(1, 1) },\n uTime: { value: 0 },\n uSpeed: { value: speed },\n uRot: { value: new THREE.Vector2(1, 0) },\n uColorCount: { value: 0 },\n uColors: { value: uColorsArray },\n uTransparent: { value: transparent ? 1 : 0 },\n uScale: { value: scale },\n uFrequency: { value: frequency },\n uWarpStrength: { value: warpStrength },\n uPointer: { value: new THREE.Vector2(0, 0) },\n uMouseInfluence: { value: mouseInfluence },\n uParallax: { value: parallax },\n uNoise: { value: noise }\n },\n premultipliedAlpha: true,\n transparent: true\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n const renderer = new THREE.WebGLRenderer({\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n });\n rendererRef.current = renderer;\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setClearColor(0x000000, transparent ? 0 : 1);\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.display = 'block';\n container.appendChild(renderer.domElement);\n\n const clock = new THREE.Clock();\n\n const handleResize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h, false);\n material.uniforms.uCanvas.value.set(w, h);\n };\n\n handleResize();\n\n if ('ResizeObserver' in window) {\n const ro = new ResizeObserver(handleResize);\n ro.observe(container);\n resizeObserverRef.current = ro;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const loop = () => {\n const dt = clock.getDelta();\n const elapsed = clock.elapsedTime;\n material.uniforms.uTime.value = elapsed;\n\n const deg = (rotationRef.current % 360) + autoRotateRef.current * elapsed;\n const rad = (deg * Math.PI) / 180;\n const c = Math.cos(rad);\n const s = Math.sin(rad);\n material.uniforms.uRot.value.set(c, s);\n\n const cur = pointerCurrentRef.current;\n const tgt = pointerTargetRef.current;\n const amt = Math.min(1, dt * pointerSmoothRef.current);\n cur.lerp(tgt, amt);\n material.uniforms.uPointer.value.copy(cur);\n renderer.render(scene, camera);\n rafRef.current = requestAnimationFrame(loop);\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n if (resizeObserverRef.current) resizeObserverRef.current.disconnect();\n else window.removeEventListener('resize', handleResize);\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n if (renderer.domElement && renderer.domElement.parentElement === container) {\n container.removeChild(renderer.domElement);\n }\n };\n }, [frequency, mouseInfluence, noise, parallax, scale, speed, transparent, warpStrength]);\n\n useEffect(() => {\n const material = materialRef.current;\n const renderer = rendererRef.current;\n if (!material) return;\n\n rotationRef.current = rotation;\n autoRotateRef.current = autoRotate;\n material.uniforms.uSpeed.value = speed;\n material.uniforms.uScale.value = scale;\n material.uniforms.uFrequency.value = frequency;\n material.uniforms.uWarpStrength.value = warpStrength;\n material.uniforms.uMouseInfluence.value = mouseInfluence;\n material.uniforms.uParallax.value = parallax;\n material.uniforms.uNoise.value = noise;\n\n const toVec3 = hex => {\n const h = hex.replace('#', '').trim();\n const v =\n h.length === 3\n ? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)]\n : [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255);\n };\n\n const arr = (colors || []).filter(Boolean).slice(0, MAX_COLORS).map(toVec3);\n for (let i = 0; i < MAX_COLORS; i++) {\n const vec = material.uniforms.uColors.value[i];\n if (i < arr.length) vec.copy(arr[i]);\n else vec.set(0, 0, 0);\n }\n material.uniforms.uColorCount.value = arr.length;\n\n material.uniforms.uTransparent.value = transparent ? 1 : 0;\n if (renderer) renderer.setClearColor(0x000000, transparent ? 0 : 1);\n }, [\n rotation,\n autoRotate,\n speed,\n scale,\n frequency,\n warpStrength,\n mouseInfluence,\n parallax,\n noise,\n colors,\n transparent\n ]);\n\n useEffect(() => {\n const material = materialRef.current;\n const container = containerRef.current;\n if (!material || !container) return;\n\n const handlePointerMove = e => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / (rect.width || 1)) * 2 - 1;\n const y = -(((e.clientY - rect.top) / (rect.height || 1)) * 2 - 1);\n pointerTargetRef.current.set(x, y);\n };\n\n container.addEventListener('pointermove', handlePointerMove);\n return () => {\n container.removeEventListener('pointermove', handlePointerMove);\n };\n }, []);\n\n return ;\n}\n"
+ "content": "import { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './ColorBends.css';\n\nconst MAX_COLORS = 8;\n\nconst frag = `\n#define MAX_COLORS ${MAX_COLORS}\nuniform vec2 uCanvas;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec2 uRot;\nuniform int uColorCount;\nuniform vec3 uColors[MAX_COLORS];\nuniform int uTransparent;\nuniform float uScale;\nuniform float uFrequency;\nuniform float uWarpStrength;\nuniform vec2 uPointer; // in NDC [-1,1]\nuniform float uMouseInfluence;\nuniform float uParallax;\nuniform float uNoise;\nvarying vec2 vUv;\n\nvoid main() {\n float t = uTime * uSpeed;\n vec2 p = vUv * 2.0 - 1.0;\n p += uPointer * uParallax * 0.1;\n vec2 rp = vec2(p.x * uRot.x - p.y * uRot.y, p.x * uRot.y + p.y * uRot.x);\n vec2 q = vec2(rp.x * (uCanvas.x / uCanvas.y), rp.y);\n q /= max(uScale, 0.0001);\n q /= 0.5 + 0.2 * dot(q, q);\n q += 0.2 * cos(t) - 7.56;\n vec2 toward = (uPointer - rp);\n q += toward * uMouseInfluence * 0.2;\n\n vec3 col = vec3(0.0);\n float a = 1.0;\n\n if (uColorCount > 0) {\n vec2 s = q;\n vec3 sumCol = vec3(0.0);\n float cover = 0.0;\n for (int i = 0; i < MAX_COLORS; ++i) {\n if (i >= uColorCount) break;\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3); // strong response across 0..1\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0); // allow >1 to amplify displacement\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float m = mix(m0, m1, kMix);\n float w = 1.0 - exp(-6.0 / exp(6.0 * m));\n sumCol += uColors[i] * w;\n cover = max(cover, w);\n }\n col = clamp(sumCol, 0.0, 1.0);\n a = uTransparent > 0 ? cover : 1.0;\n } else {\n vec2 s = q;\n for (int k = 0; k < 3; ++k) {\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3);\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0);\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float m = mix(m0, m1, kMix);\n col[k] = 1.0 - exp(-6.0 / exp(6.0 * m));\n }\n a = uTransparent > 0 ? max(max(col.r, col.g), col.b) : 1.0;\n }\n\n if (uNoise > 0.0001) {\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(uTime), vec2(12.9898, 78.233))) * 43758.5453123);\n col += (n - 0.5) * uNoise;\n col = clamp(col, 0.0, 1.0);\n }\n\n vec3 rgb = (uTransparent > 0) ? col * a : col;\n gl_FragColor = vec4(rgb, a);\n}\n`;\n\nconst vert = `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nexport default function ColorBends({\n className,\n style,\n rotation = 45,\n speed = 0.2,\n colors = [],\n transparent = true,\n autoRotate = 0,\n scale = 1,\n frequency = 1,\n warpStrength = 1,\n mouseInfluence = 1,\n parallax = 0.5,\n noise = 0.1\n}) {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const rafRef = useRef(null);\n const materialRef = useRef(null);\n const resizeObserverRef = useRef(null);\n const rotationRef = useRef(rotation);\n const autoRotateRef = useRef(autoRotate);\n const pointerTargetRef = useRef(new THREE.Vector2(0, 0));\n const pointerCurrentRef = useRef(new THREE.Vector2(0, 0));\n const pointerSmoothRef = useRef(8);\n\n useEffect(() => {\n const container = containerRef.current;\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0));\n const material = new THREE.ShaderMaterial({\n vertexShader: vert,\n fragmentShader: frag,\n uniforms: {\n uCanvas: { value: new THREE.Vector2(1, 1) },\n uTime: { value: 0 },\n uSpeed: { value: speed },\n uRot: { value: new THREE.Vector2(1, 0) },\n uColorCount: { value: 0 },\n uColors: { value: uColorsArray },\n uTransparent: { value: transparent ? 1 : 0 },\n uScale: { value: scale },\n uFrequency: { value: frequency },\n uWarpStrength: { value: warpStrength },\n uPointer: { value: new THREE.Vector2(0, 0) },\n uMouseInfluence: { value: mouseInfluence },\n uParallax: { value: parallax },\n uNoise: { value: noise }\n },\n premultipliedAlpha: true,\n transparent: true\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n const renderer = new THREE.WebGLRenderer({\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n });\n rendererRef.current = renderer;\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setClearColor(0x000000, transparent ? 0 : 1);\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.display = 'block';\n container.appendChild(renderer.domElement);\n\n const clock = new THREE.Clock();\n\n const handleResize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h, false);\n material.uniforms.uCanvas.value.set(w, h);\n };\n\n handleResize();\n\n if ('ResizeObserver' in window) {\n const ro = new ResizeObserver(handleResize);\n ro.observe(container);\n resizeObserverRef.current = ro;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const loop = () => {\n const dt = clock.getDelta();\n const elapsed = clock.elapsedTime;\n material.uniforms.uTime.value = elapsed;\n\n const deg = (rotationRef.current % 360) + autoRotateRef.current * elapsed;\n const rad = (deg * Math.PI) / 180;\n const c = Math.cos(rad);\n const s = Math.sin(rad);\n material.uniforms.uRot.value.set(c, s);\n\n const cur = pointerCurrentRef.current;\n const tgt = pointerTargetRef.current;\n const amt = Math.min(1, dt * pointerSmoothRef.current);\n cur.lerp(tgt, amt);\n material.uniforms.uPointer.value.copy(cur);\n renderer.render(scene, camera);\n rafRef.current = requestAnimationFrame(loop);\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n if (resizeObserverRef.current) resizeObserverRef.current.disconnect();\n else window.removeEventListener('resize', handleResize);\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (renderer.domElement && renderer.domElement.parentElement === container) {\n container.removeChild(renderer.domElement);\n }\n };\n }, [frequency, mouseInfluence, noise, parallax, scale, speed, transparent, warpStrength]);\n\n useEffect(() => {\n const material = materialRef.current;\n const renderer = rendererRef.current;\n if (!material) return;\n\n rotationRef.current = rotation;\n autoRotateRef.current = autoRotate;\n material.uniforms.uSpeed.value = speed;\n material.uniforms.uScale.value = scale;\n material.uniforms.uFrequency.value = frequency;\n material.uniforms.uWarpStrength.value = warpStrength;\n material.uniforms.uMouseInfluence.value = mouseInfluence;\n material.uniforms.uParallax.value = parallax;\n material.uniforms.uNoise.value = noise;\n\n const toVec3 = hex => {\n const h = hex.replace('#', '').trim();\n const v =\n h.length === 3\n ? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)]\n : [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255);\n };\n\n const arr = (colors || []).filter(Boolean).slice(0, MAX_COLORS).map(toVec3);\n for (let i = 0; i < MAX_COLORS; i++) {\n const vec = material.uniforms.uColors.value[i];\n if (i < arr.length) vec.copy(arr[i]);\n else vec.set(0, 0, 0);\n }\n material.uniforms.uColorCount.value = arr.length;\n\n material.uniforms.uTransparent.value = transparent ? 1 : 0;\n if (renderer) renderer.setClearColor(0x000000, transparent ? 0 : 1);\n }, [\n rotation,\n autoRotate,\n speed,\n scale,\n frequency,\n warpStrength,\n mouseInfluence,\n parallax,\n noise,\n colors,\n transparent\n ]);\n\n useEffect(() => {\n const material = materialRef.current;\n const container = containerRef.current;\n if (!material || !container) return;\n\n const handlePointerMove = e => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / (rect.width || 1)) * 2 - 1;\n const y = -(((e.clientY - rect.top) / (rect.height || 1)) * 2 - 1);\n pointerTargetRef.current.set(x, y);\n };\n\n container.addEventListener('pointermove', handlePointerMove);\n return () => {\n container.removeEventListener('pointermove', handlePointerMove);\n };\n }, []);\n\n return ;\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/ColorBends-JS-TW.json b/public/r/ColorBends-JS-TW.json
index 5d537f8c..2af6396d 100644
--- a/public/r/ColorBends-JS-TW.json
+++ b/public/r/ColorBends-JS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "ColorBends/ColorBends.jsx",
- "content": "import { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nconst MAX_COLORS = 8;\n\nconst frag = `\n#define MAX_COLORS ${MAX_COLORS}\nuniform vec2 uCanvas;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec2 uRot;\nuniform int uColorCount;\nuniform vec3 uColors[MAX_COLORS];\nuniform int uTransparent;\nuniform float uScale;\nuniform float uFrequency;\nuniform float uWarpStrength;\nuniform vec2 uPointer; // in NDC [-1,1]\nuniform float uMouseInfluence;\nuniform float uParallax;\nuniform float uNoise;\nvarying vec2 vUv;\n\nvoid main() {\n float t = uTime * uSpeed;\n vec2 p = vUv * 2.0 - 1.0;\n p += uPointer * uParallax * 0.1;\n vec2 rp = vec2(p.x * uRot.x - p.y * uRot.y, p.x * uRot.y + p.y * uRot.x);\n vec2 q = vec2(rp.x * (uCanvas.x / uCanvas.y), rp.y);\n q /= max(uScale, 0.0001);\n q /= 0.5 + 0.2 * dot(q, q);\n q += 0.2 * cos(t) - 7.56;\n vec2 toward = (uPointer - rp);\n q += toward * uMouseInfluence * 0.2;\n\n vec3 col = vec3(0.0);\n float a = 1.0;\n\n if (uColorCount > 0) {\n vec2 s = q;\n vec3 sumCol = vec3(0.0);\n float cover = 0.0;\n for (int i = 0; i < MAX_COLORS; ++i) {\n if (i >= uColorCount) break;\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3); // strong response across 0..1\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0); // allow >1 to amplify displacement\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float m = mix(m0, m1, kMix);\n float w = 1.0 - exp(-6.0 / exp(6.0 * m));\n sumCol += uColors[i] * w;\n cover = max(cover, w);\n }\n col = clamp(sumCol, 0.0, 1.0);\n a = uTransparent > 0 ? cover : 1.0;\n } else {\n vec2 s = q;\n for (int k = 0; k < 3; ++k) {\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3);\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0);\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float m = mix(m0, m1, kMix);\n col[k] = 1.0 - exp(-6.0 / exp(6.0 * m));\n }\n a = uTransparent > 0 ? max(max(col.r, col.g), col.b) : 1.0;\n }\n\n if (uNoise > 0.0001) {\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(uTime), vec2(12.9898, 78.233))) * 43758.5453123);\n col += (n - 0.5) * uNoise;\n col = clamp(col, 0.0, 1.0);\n }\n\n vec3 rgb = (uTransparent > 0) ? col * a : col;\n gl_FragColor = vec4(rgb, a);\n}\n`;\n\nconst vert = `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nexport default function ColorBends({\n className,\n style,\n rotation = 45,\n speed = 0.2,\n colors = [],\n transparent = true,\n autoRotate = 0,\n scale = 1,\n frequency = 1,\n warpStrength = 1,\n mouseInfluence = 1,\n parallax = 0.5,\n noise = 0.1\n}) {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const rafRef = useRef(null);\n const materialRef = useRef(null);\n const resizeObserverRef = useRef(null);\n const rotationRef = useRef(rotation);\n const autoRotateRef = useRef(autoRotate);\n const pointerTargetRef = useRef(new THREE.Vector2(0, 0));\n const pointerCurrentRef = useRef(new THREE.Vector2(0, 0));\n const pointerSmoothRef = useRef(8);\n\n useEffect(() => {\n const container = containerRef.current;\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0));\n const material = new THREE.ShaderMaterial({\n vertexShader: vert,\n fragmentShader: frag,\n uniforms: {\n uCanvas: { value: new THREE.Vector2(1, 1) },\n uTime: { value: 0 },\n uSpeed: { value: speed },\n uRot: { value: new THREE.Vector2(1, 0) },\n uColorCount: { value: 0 },\n uColors: { value: uColorsArray },\n uTransparent: { value: transparent ? 1 : 0 },\n uScale: { value: scale },\n uFrequency: { value: frequency },\n uWarpStrength: { value: warpStrength },\n uPointer: { value: new THREE.Vector2(0, 0) },\n uMouseInfluence: { value: mouseInfluence },\n uParallax: { value: parallax },\n uNoise: { value: noise }\n },\n premultipliedAlpha: true,\n transparent: true\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n const renderer = new THREE.WebGLRenderer({\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n });\n rendererRef.current = renderer;\n // Three r152+ uses outputColorSpace and SRGBColorSpace\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setClearColor(0x000000, transparent ? 0 : 1);\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.display = 'block';\n container.appendChild(renderer.domElement);\n\n const clock = new THREE.Clock();\n\n const handleResize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h, false);\n material.uniforms.uCanvas.value.set(w, h);\n };\n\n handleResize();\n\n if ('ResizeObserver' in window) {\n const ro = new ResizeObserver(handleResize);\n ro.observe(container);\n resizeObserverRef.current = ro;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const loop = () => {\n const dt = clock.getDelta();\n const elapsed = clock.elapsedTime;\n material.uniforms.uTime.value = elapsed;\n\n const deg = (rotationRef.current % 360) + autoRotateRef.current * elapsed;\n const rad = (deg * Math.PI) / 180;\n const c = Math.cos(rad);\n const s = Math.sin(rad);\n material.uniforms.uRot.value.set(c, s);\n\n const cur = pointerCurrentRef.current;\n const tgt = pointerTargetRef.current;\n const amt = Math.min(1, dt * pointerSmoothRef.current);\n cur.lerp(tgt, amt);\n material.uniforms.uPointer.value.copy(cur);\n renderer.render(scene, camera);\n rafRef.current = requestAnimationFrame(loop);\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n if (resizeObserverRef.current) resizeObserverRef.current.disconnect();\n else window.removeEventListener('resize', handleResize);\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n if (renderer.domElement && renderer.domElement.parentElement === container) {\n container.removeChild(renderer.domElement);\n }\n };\n }, [frequency, mouseInfluence, noise, parallax, scale, speed, transparent, warpStrength]);\n\n useEffect(() => {\n const material = materialRef.current;\n const renderer = rendererRef.current;\n if (!material) return;\n\n rotationRef.current = rotation;\n autoRotateRef.current = autoRotate;\n material.uniforms.uSpeed.value = speed;\n material.uniforms.uScale.value = scale;\n material.uniforms.uFrequency.value = frequency;\n material.uniforms.uWarpStrength.value = warpStrength;\n material.uniforms.uMouseInfluence.value = mouseInfluence;\n material.uniforms.uParallax.value = parallax;\n material.uniforms.uNoise.value = noise;\n\n const toVec3 = hex => {\n const h = hex.replace('#', '').trim();\n const v =\n h.length === 3\n ? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)]\n : [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255);\n };\n\n const arr = (colors || []).filter(Boolean).slice(0, MAX_COLORS).map(toVec3);\n for (let i = 0; i < MAX_COLORS; i++) {\n const vec = material.uniforms.uColors.value[i];\n if (i < arr.length) vec.copy(arr[i]);\n else vec.set(0, 0, 0);\n }\n material.uniforms.uColorCount.value = arr.length;\n\n material.uniforms.uTransparent.value = transparent ? 1 : 0;\n if (renderer) renderer.setClearColor(0x000000, transparent ? 0 : 1);\n }, [\n rotation,\n autoRotate,\n speed,\n scale,\n frequency,\n warpStrength,\n mouseInfluence,\n parallax,\n noise,\n colors,\n transparent\n ]);\n\n useEffect(() => {\n const material = materialRef.current;\n const container = containerRef.current;\n if (!material || !container) return;\n\n const handlePointerMove = e => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / (rect.width || 1)) * 2 - 1;\n const y = -(((e.clientY - rect.top) / (rect.height || 1)) * 2 - 1);\n pointerTargetRef.current.set(x, y);\n };\n\n container.addEventListener('pointermove', handlePointerMove);\n return () => {\n container.removeEventListener('pointermove', handlePointerMove);\n };\n }, []);\n\n return ;\n}\n"
+ "content": "import { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nconst MAX_COLORS = 8;\n\nconst frag = `\n#define MAX_COLORS ${MAX_COLORS}\nuniform vec2 uCanvas;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec2 uRot;\nuniform int uColorCount;\nuniform vec3 uColors[MAX_COLORS];\nuniform int uTransparent;\nuniform float uScale;\nuniform float uFrequency;\nuniform float uWarpStrength;\nuniform vec2 uPointer; // in NDC [-1,1]\nuniform float uMouseInfluence;\nuniform float uParallax;\nuniform float uNoise;\nvarying vec2 vUv;\n\nvoid main() {\n float t = uTime * uSpeed;\n vec2 p = vUv * 2.0 - 1.0;\n p += uPointer * uParallax * 0.1;\n vec2 rp = vec2(p.x * uRot.x - p.y * uRot.y, p.x * uRot.y + p.y * uRot.x);\n vec2 q = vec2(rp.x * (uCanvas.x / uCanvas.y), rp.y);\n q /= max(uScale, 0.0001);\n q /= 0.5 + 0.2 * dot(q, q);\n q += 0.2 * cos(t) - 7.56;\n vec2 toward = (uPointer - rp);\n q += toward * uMouseInfluence * 0.2;\n\n vec3 col = vec3(0.0);\n float a = 1.0;\n\n if (uColorCount > 0) {\n vec2 s = q;\n vec3 sumCol = vec3(0.0);\n float cover = 0.0;\n for (int i = 0; i < MAX_COLORS; ++i) {\n if (i >= uColorCount) break;\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3); // strong response across 0..1\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0); // allow >1 to amplify displacement\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float m = mix(m0, m1, kMix);\n float w = 1.0 - exp(-6.0 / exp(6.0 * m));\n sumCol += uColors[i] * w;\n cover = max(cover, w);\n }\n col = clamp(sumCol, 0.0, 1.0);\n a = uTransparent > 0 ? cover : 1.0;\n } else {\n vec2 s = q;\n for (int k = 0; k < 3; ++k) {\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3);\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0);\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float m = mix(m0, m1, kMix);\n col[k] = 1.0 - exp(-6.0 / exp(6.0 * m));\n }\n a = uTransparent > 0 ? max(max(col.r, col.g), col.b) : 1.0;\n }\n\n if (uNoise > 0.0001) {\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(uTime), vec2(12.9898, 78.233))) * 43758.5453123);\n col += (n - 0.5) * uNoise;\n col = clamp(col, 0.0, 1.0);\n }\n\n vec3 rgb = (uTransparent > 0) ? col * a : col;\n gl_FragColor = vec4(rgb, a);\n}\n`;\n\nconst vert = `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nexport default function ColorBends({\n className,\n style,\n rotation = 45,\n speed = 0.2,\n colors = [],\n transparent = true,\n autoRotate = 0,\n scale = 1,\n frequency = 1,\n warpStrength = 1,\n mouseInfluence = 1,\n parallax = 0.5,\n noise = 0.1\n}) {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const rafRef = useRef(null);\n const materialRef = useRef(null);\n const resizeObserverRef = useRef(null);\n const rotationRef = useRef(rotation);\n const autoRotateRef = useRef(autoRotate);\n const pointerTargetRef = useRef(new THREE.Vector2(0, 0));\n const pointerCurrentRef = useRef(new THREE.Vector2(0, 0));\n const pointerSmoothRef = useRef(8);\n\n useEffect(() => {\n const container = containerRef.current;\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0));\n const material = new THREE.ShaderMaterial({\n vertexShader: vert,\n fragmentShader: frag,\n uniforms: {\n uCanvas: { value: new THREE.Vector2(1, 1) },\n uTime: { value: 0 },\n uSpeed: { value: speed },\n uRot: { value: new THREE.Vector2(1, 0) },\n uColorCount: { value: 0 },\n uColors: { value: uColorsArray },\n uTransparent: { value: transparent ? 1 : 0 },\n uScale: { value: scale },\n uFrequency: { value: frequency },\n uWarpStrength: { value: warpStrength },\n uPointer: { value: new THREE.Vector2(0, 0) },\n uMouseInfluence: { value: mouseInfluence },\n uParallax: { value: parallax },\n uNoise: { value: noise }\n },\n premultipliedAlpha: true,\n transparent: true\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n const renderer = new THREE.WebGLRenderer({\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n });\n rendererRef.current = renderer;\n // Three r152+ uses outputColorSpace and SRGBColorSpace\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setClearColor(0x000000, transparent ? 0 : 1);\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.display = 'block';\n container.appendChild(renderer.domElement);\n\n const clock = new THREE.Clock();\n\n const handleResize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h, false);\n material.uniforms.uCanvas.value.set(w, h);\n };\n\n handleResize();\n\n if ('ResizeObserver' in window) {\n const ro = new ResizeObserver(handleResize);\n ro.observe(container);\n resizeObserverRef.current = ro;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const loop = () => {\n const dt = clock.getDelta();\n const elapsed = clock.elapsedTime;\n material.uniforms.uTime.value = elapsed;\n\n const deg = (rotationRef.current % 360) + autoRotateRef.current * elapsed;\n const rad = (deg * Math.PI) / 180;\n const c = Math.cos(rad);\n const s = Math.sin(rad);\n material.uniforms.uRot.value.set(c, s);\n\n const cur = pointerCurrentRef.current;\n const tgt = pointerTargetRef.current;\n const amt = Math.min(1, dt * pointerSmoothRef.current);\n cur.lerp(tgt, amt);\n material.uniforms.uPointer.value.copy(cur);\n renderer.render(scene, camera);\n rafRef.current = requestAnimationFrame(loop);\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n if (resizeObserverRef.current) resizeObserverRef.current.disconnect();\n else window.removeEventListener('resize', handleResize);\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (renderer.domElement && renderer.domElement.parentElement === container) {\n container.removeChild(renderer.domElement);\n }\n };\n }, [frequency, mouseInfluence, noise, parallax, scale, speed, transparent, warpStrength]);\n\n useEffect(() => {\n const material = materialRef.current;\n const renderer = rendererRef.current;\n if (!material) return;\n\n rotationRef.current = rotation;\n autoRotateRef.current = autoRotate;\n material.uniforms.uSpeed.value = speed;\n material.uniforms.uScale.value = scale;\n material.uniforms.uFrequency.value = frequency;\n material.uniforms.uWarpStrength.value = warpStrength;\n material.uniforms.uMouseInfluence.value = mouseInfluence;\n material.uniforms.uParallax.value = parallax;\n material.uniforms.uNoise.value = noise;\n\n const toVec3 = hex => {\n const h = hex.replace('#', '').trim();\n const v =\n h.length === 3\n ? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)]\n : [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255);\n };\n\n const arr = (colors || []).filter(Boolean).slice(0, MAX_COLORS).map(toVec3);\n for (let i = 0; i < MAX_COLORS; i++) {\n const vec = material.uniforms.uColors.value[i];\n if (i < arr.length) vec.copy(arr[i]);\n else vec.set(0, 0, 0);\n }\n material.uniforms.uColorCount.value = arr.length;\n\n material.uniforms.uTransparent.value = transparent ? 1 : 0;\n if (renderer) renderer.setClearColor(0x000000, transparent ? 0 : 1);\n }, [\n rotation,\n autoRotate,\n speed,\n scale,\n frequency,\n warpStrength,\n mouseInfluence,\n parallax,\n noise,\n colors,\n transparent\n ]);\n\n useEffect(() => {\n const material = materialRef.current;\n const container = containerRef.current;\n if (!material || !container) return;\n\n const handlePointerMove = e => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / (rect.width || 1)) * 2 - 1;\n const y = -(((e.clientY - rect.top) / (rect.height || 1)) * 2 - 1);\n pointerTargetRef.current.set(x, y);\n };\n\n container.addEventListener('pointermove', handlePointerMove);\n return () => {\n container.removeEventListener('pointermove', handlePointerMove);\n };\n }, []);\n\n return ;\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/ColorBends-TS-CSS.json b/public/r/ColorBends-TS-CSS.json
index 22511f44..bdbd60d9 100644
--- a/public/r/ColorBends-TS-CSS.json
+++ b/public/r/ColorBends-TS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "ColorBends/ColorBends.tsx",
- "content": "import React, { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './ColorBends.css';\n\ntype ColorBendsProps = {\n className?: string;\n style?: React.CSSProperties;\n rotation?: number;\n speed?: number;\n colors?: string[];\n transparent?: boolean;\n autoRotate?: number;\n scale?: number;\n frequency?: number;\n warpStrength?: number;\n mouseInfluence?: number;\n parallax?: number;\n noise?: number;\n};\n\nconst MAX_COLORS = 8 as const;\n\nconst frag = `\n#define MAX_COLORS ${MAX_COLORS}\nuniform vec2 uCanvas;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec2 uRot;\nuniform int uColorCount;\nuniform vec3 uColors[MAX_COLORS];\nuniform int uTransparent;\nuniform float uScale;\nuniform float uFrequency;\nuniform float uWarpStrength;\nuniform vec2 uPointer; // in NDC [-1,1]\nuniform float uMouseInfluence;\nuniform float uParallax;\nuniform float uNoise;\nvarying vec2 vUv;\n\nvoid main() {\n float t = uTime * uSpeed;\n vec2 p = vUv * 2.0 - 1.0;\n p += uPointer * uParallax * 0.1;\n vec2 rp = vec2(p.x * uRot.x - p.y * uRot.y, p.x * uRot.y + p.y * uRot.x);\n vec2 q = vec2(rp.x * (uCanvas.x / uCanvas.y), rp.y);\n q /= max(uScale, 0.0001);\n q /= 0.5 + 0.2 * dot(q, q);\n q += 0.2 * cos(t) - 7.56;\n vec2 toward = (uPointer - rp);\n q += toward * uMouseInfluence * 0.2;\n\n vec3 col = vec3(0.0);\n float a = 1.0;\n\n if (uColorCount > 0) {\n vec2 s = q;\n vec3 sumCol = vec3(0.0);\n float cover = 0.0;\n for (int i = 0; i < MAX_COLORS; ++i) {\n if (i >= uColorCount) break;\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3); // strong response across 0..1\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0); // allow >1 to amplify displacement\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float m = mix(m0, m1, kMix);\n float w = 1.0 - exp(-6.0 / exp(6.0 * m));\n sumCol += uColors[i] * w;\n cover = max(cover, w);\n }\n col = clamp(sumCol, 0.0, 1.0);\n a = uTransparent > 0 ? cover : 1.0;\n } else {\n vec2 s = q;\n for (int k = 0; k < 3; ++k) {\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3);\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0);\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float m = mix(m0, m1, kMix);\n col[k] = 1.0 - exp(-6.0 / exp(6.0 * m));\n }\n a = uTransparent > 0 ? max(max(col.r, col.g), col.b) : 1.0;\n }\n\n if (uNoise > 0.0001) {\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(uTime), vec2(12.9898, 78.233))) * 43758.5453123);\n col += (n - 0.5) * uNoise;\n col = clamp(col, 0.0, 1.0);\n }\n\n vec3 rgb = (uTransparent > 0) ? col * a : col;\n gl_FragColor = vec4(rgb, a);\n}\n`;\n\nconst vert = `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nexport default function ColorBends({\n className,\n style,\n rotation = 45,\n speed = 0.2,\n colors = [],\n transparent = true,\n autoRotate = 0,\n scale = 1,\n frequency = 1,\n warpStrength = 1,\n mouseInfluence = 1,\n parallax = 0.5,\n noise = 0.1\n}: ColorBendsProps) {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const rafRef = useRef(null);\n const materialRef = useRef(null);\n const resizeObserverRef = useRef(null);\n const rotationRef = useRef(rotation);\n const autoRotateRef = useRef(autoRotate);\n const pointerTargetRef = useRef(new THREE.Vector2(0, 0));\n const pointerCurrentRef = useRef(new THREE.Vector2(0, 0));\n const pointerSmoothRef = useRef(8);\n\n useEffect(() => {\n const container = containerRef.current!;\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0));\n const material = new THREE.ShaderMaterial({\n vertexShader: vert,\n fragmentShader: frag,\n uniforms: {\n uCanvas: { value: new THREE.Vector2(1, 1) },\n uTime: { value: 0 },\n uSpeed: { value: speed },\n uRot: { value: new THREE.Vector2(1, 0) },\n uColorCount: { value: 0 },\n uColors: { value: uColorsArray },\n uTransparent: { value: transparent ? 1 : 0 },\n uScale: { value: scale },\n uFrequency: { value: frequency },\n uWarpStrength: { value: warpStrength },\n uPointer: { value: new THREE.Vector2(0, 0) },\n uMouseInfluence: { value: mouseInfluence },\n uParallax: { value: parallax },\n uNoise: { value: noise }\n },\n premultipliedAlpha: true,\n transparent: true\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n const renderer = new THREE.WebGLRenderer({\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n });\n rendererRef.current = renderer;\n (renderer as any).outputColorSpace = (THREE as any).SRGBColorSpace;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setClearColor(0x000000, transparent ? 0 : 1);\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.display = 'block';\n container.appendChild(renderer.domElement);\n\n const clock = new THREE.Clock();\n\n const handleResize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h, false);\n (material.uniforms.uCanvas.value as THREE.Vector2).set(w, h);\n };\n\n handleResize();\n\n if ('ResizeObserver' in window) {\n const ro = new ResizeObserver(handleResize);\n ro.observe(container);\n resizeObserverRef.current = ro;\n } else {\n (window as Window).addEventListener('resize', handleResize);\n }\n\n const loop = () => {\n const dt = clock.getDelta();\n const elapsed = clock.elapsedTime;\n material.uniforms.uTime.value = elapsed;\n\n const deg = (rotationRef.current % 360) + autoRotateRef.current * elapsed;\n const rad = (deg * Math.PI) / 180;\n const c = Math.cos(rad);\n const s = Math.sin(rad);\n (material.uniforms.uRot.value as THREE.Vector2).set(c, s);\n\n const cur = pointerCurrentRef.current;\n const tgt = pointerTargetRef.current;\n const amt = Math.min(1, dt * pointerSmoothRef.current);\n cur.lerp(tgt, amt);\n (material.uniforms.uPointer.value as THREE.Vector2).copy(cur);\n renderer.render(scene, camera);\n rafRef.current = requestAnimationFrame(loop);\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n if (resizeObserverRef.current) resizeObserverRef.current.disconnect();\n else (window as Window).removeEventListener('resize', handleResize);\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n if (renderer.domElement && renderer.domElement.parentElement === container) {\n container.removeChild(renderer.domElement);\n }\n };\n }, []);\n\n useEffect(() => {\n const material = materialRef.current;\n const renderer = rendererRef.current;\n if (!material) return;\n\n rotationRef.current = rotation;\n autoRotateRef.current = autoRotate;\n material.uniforms.uSpeed.value = speed;\n material.uniforms.uScale.value = scale;\n material.uniforms.uFrequency.value = frequency;\n material.uniforms.uWarpStrength.value = warpStrength;\n material.uniforms.uMouseInfluence.value = mouseInfluence;\n material.uniforms.uParallax.value = parallax;\n material.uniforms.uNoise.value = noise;\n\n const toVec3 = (hex: string) => {\n const h = hex.replace('#', '').trim();\n const v =\n h.length === 3\n ? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)]\n : [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255);\n };\n\n const arr = (colors || []).filter(Boolean).slice(0, MAX_COLORS).map(toVec3);\n for (let i = 0; i < MAX_COLORS; i++) {\n const vec = (material.uniforms.uColors.value as THREE.Vector3[])[i];\n if (i < arr.length) vec.copy(arr[i]);\n else vec.set(0, 0, 0);\n }\n material.uniforms.uColorCount.value = arr.length;\n\n material.uniforms.uTransparent.value = transparent ? 1 : 0;\n if (renderer) renderer.setClearColor(0x000000, transparent ? 0 : 1);\n }, [\n rotation,\n autoRotate,\n speed,\n scale,\n frequency,\n warpStrength,\n mouseInfluence,\n parallax,\n noise,\n colors,\n transparent\n ]);\n\n useEffect(() => {\n const material = materialRef.current;\n const container = containerRef.current;\n if (!material || !container) return;\n\n const handlePointerMove = (e: PointerEvent) => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / (rect.width || 1)) * 2 - 1;\n const y = -(((e.clientY - rect.top) / (rect.height || 1)) * 2 - 1);\n pointerTargetRef.current.set(x, y);\n };\n\n container.addEventListener('pointermove', handlePointerMove);\n return () => {\n container.removeEventListener('pointermove', handlePointerMove);\n };\n }, []);\n\n return ;\n}\n"
+ "content": "import React, { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './ColorBends.css';\n\ntype ColorBendsProps = {\n className?: string;\n style?: React.CSSProperties;\n rotation?: number;\n speed?: number;\n colors?: string[];\n transparent?: boolean;\n autoRotate?: number;\n scale?: number;\n frequency?: number;\n warpStrength?: number;\n mouseInfluence?: number;\n parallax?: number;\n noise?: number;\n};\n\nconst MAX_COLORS = 8 as const;\n\nconst frag = `\n#define MAX_COLORS ${MAX_COLORS}\nuniform vec2 uCanvas;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec2 uRot;\nuniform int uColorCount;\nuniform vec3 uColors[MAX_COLORS];\nuniform int uTransparent;\nuniform float uScale;\nuniform float uFrequency;\nuniform float uWarpStrength;\nuniform vec2 uPointer; // in NDC [-1,1]\nuniform float uMouseInfluence;\nuniform float uParallax;\nuniform float uNoise;\nvarying vec2 vUv;\n\nvoid main() {\n float t = uTime * uSpeed;\n vec2 p = vUv * 2.0 - 1.0;\n p += uPointer * uParallax * 0.1;\n vec2 rp = vec2(p.x * uRot.x - p.y * uRot.y, p.x * uRot.y + p.y * uRot.x);\n vec2 q = vec2(rp.x * (uCanvas.x / uCanvas.y), rp.y);\n q /= max(uScale, 0.0001);\n q /= 0.5 + 0.2 * dot(q, q);\n q += 0.2 * cos(t) - 7.56;\n vec2 toward = (uPointer - rp);\n q += toward * uMouseInfluence * 0.2;\n\n vec3 col = vec3(0.0);\n float a = 1.0;\n\n if (uColorCount > 0) {\n vec2 s = q;\n vec3 sumCol = vec3(0.0);\n float cover = 0.0;\n for (int i = 0; i < MAX_COLORS; ++i) {\n if (i >= uColorCount) break;\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3); // strong response across 0..1\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0); // allow >1 to amplify displacement\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float m = mix(m0, m1, kMix);\n float w = 1.0 - exp(-6.0 / exp(6.0 * m));\n sumCol += uColors[i] * w;\n cover = max(cover, w);\n }\n col = clamp(sumCol, 0.0, 1.0);\n a = uTransparent > 0 ? cover : 1.0;\n } else {\n vec2 s = q;\n for (int k = 0; k < 3; ++k) {\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3);\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0);\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float m = mix(m0, m1, kMix);\n col[k] = 1.0 - exp(-6.0 / exp(6.0 * m));\n }\n a = uTransparent > 0 ? max(max(col.r, col.g), col.b) : 1.0;\n }\n\n if (uNoise > 0.0001) {\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(uTime), vec2(12.9898, 78.233))) * 43758.5453123);\n col += (n - 0.5) * uNoise;\n col = clamp(col, 0.0, 1.0);\n }\n\n vec3 rgb = (uTransparent > 0) ? col * a : col;\n gl_FragColor = vec4(rgb, a);\n}\n`;\n\nconst vert = `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nexport default function ColorBends({\n className,\n style,\n rotation = 45,\n speed = 0.2,\n colors = [],\n transparent = true,\n autoRotate = 0,\n scale = 1,\n frequency = 1,\n warpStrength = 1,\n mouseInfluence = 1,\n parallax = 0.5,\n noise = 0.1\n}: ColorBendsProps) {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const rafRef = useRef(null);\n const materialRef = useRef(null);\n const resizeObserverRef = useRef(null);\n const rotationRef = useRef(rotation);\n const autoRotateRef = useRef(autoRotate);\n const pointerTargetRef = useRef(new THREE.Vector2(0, 0));\n const pointerCurrentRef = useRef(new THREE.Vector2(0, 0));\n const pointerSmoothRef = useRef(8);\n\n useEffect(() => {\n const container = containerRef.current!;\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0));\n const material = new THREE.ShaderMaterial({\n vertexShader: vert,\n fragmentShader: frag,\n uniforms: {\n uCanvas: { value: new THREE.Vector2(1, 1) },\n uTime: { value: 0 },\n uSpeed: { value: speed },\n uRot: { value: new THREE.Vector2(1, 0) },\n uColorCount: { value: 0 },\n uColors: { value: uColorsArray },\n uTransparent: { value: transparent ? 1 : 0 },\n uScale: { value: scale },\n uFrequency: { value: frequency },\n uWarpStrength: { value: warpStrength },\n uPointer: { value: new THREE.Vector2(0, 0) },\n uMouseInfluence: { value: mouseInfluence },\n uParallax: { value: parallax },\n uNoise: { value: noise }\n },\n premultipliedAlpha: true,\n transparent: true\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n const renderer = new THREE.WebGLRenderer({\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n });\n rendererRef.current = renderer;\n (renderer as any).outputColorSpace = (THREE as any).SRGBColorSpace;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setClearColor(0x000000, transparent ? 0 : 1);\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.display = 'block';\n container.appendChild(renderer.domElement);\n\n const clock = new THREE.Clock();\n\n const handleResize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h, false);\n (material.uniforms.uCanvas.value as THREE.Vector2).set(w, h);\n };\n\n handleResize();\n\n if ('ResizeObserver' in window) {\n const ro = new ResizeObserver(handleResize);\n ro.observe(container);\n resizeObserverRef.current = ro;\n } else {\n (window as Window).addEventListener('resize', handleResize);\n }\n\n const loop = () => {\n const dt = clock.getDelta();\n const elapsed = clock.elapsedTime;\n material.uniforms.uTime.value = elapsed;\n\n const deg = (rotationRef.current % 360) + autoRotateRef.current * elapsed;\n const rad = (deg * Math.PI) / 180;\n const c = Math.cos(rad);\n const s = Math.sin(rad);\n (material.uniforms.uRot.value as THREE.Vector2).set(c, s);\n\n const cur = pointerCurrentRef.current;\n const tgt = pointerTargetRef.current;\n const amt = Math.min(1, dt * pointerSmoothRef.current);\n cur.lerp(tgt, amt);\n (material.uniforms.uPointer.value as THREE.Vector2).copy(cur);\n renderer.render(scene, camera);\n rafRef.current = requestAnimationFrame(loop);\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n if (resizeObserverRef.current) resizeObserverRef.current.disconnect();\n else (window as Window).removeEventListener('resize', handleResize);\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (renderer.domElement && renderer.domElement.parentElement === container) {\n container.removeChild(renderer.domElement);\n }\n };\n }, []);\n\n useEffect(() => {\n const material = materialRef.current;\n const renderer = rendererRef.current;\n if (!material) return;\n\n rotationRef.current = rotation;\n autoRotateRef.current = autoRotate;\n material.uniforms.uSpeed.value = speed;\n material.uniforms.uScale.value = scale;\n material.uniforms.uFrequency.value = frequency;\n material.uniforms.uWarpStrength.value = warpStrength;\n material.uniforms.uMouseInfluence.value = mouseInfluence;\n material.uniforms.uParallax.value = parallax;\n material.uniforms.uNoise.value = noise;\n\n const toVec3 = (hex: string) => {\n const h = hex.replace('#', '').trim();\n const v =\n h.length === 3\n ? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)]\n : [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255);\n };\n\n const arr = (colors || []).filter(Boolean).slice(0, MAX_COLORS).map(toVec3);\n for (let i = 0; i < MAX_COLORS; i++) {\n const vec = (material.uniforms.uColors.value as THREE.Vector3[])[i];\n if (i < arr.length) vec.copy(arr[i]);\n else vec.set(0, 0, 0);\n }\n material.uniforms.uColorCount.value = arr.length;\n\n material.uniforms.uTransparent.value = transparent ? 1 : 0;\n if (renderer) renderer.setClearColor(0x000000, transparent ? 0 : 1);\n }, [\n rotation,\n autoRotate,\n speed,\n scale,\n frequency,\n warpStrength,\n mouseInfluence,\n parallax,\n noise,\n colors,\n transparent\n ]);\n\n useEffect(() => {\n const material = materialRef.current;\n const container = containerRef.current;\n if (!material || !container) return;\n\n const handlePointerMove = (e: PointerEvent) => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / (rect.width || 1)) * 2 - 1;\n const y = -(((e.clientY - rect.top) / (rect.height || 1)) * 2 - 1);\n pointerTargetRef.current.set(x, y);\n };\n\n container.addEventListener('pointermove', handlePointerMove);\n return () => {\n container.removeEventListener('pointermove', handlePointerMove);\n };\n }, []);\n\n return ;\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/ColorBends-TS-TW.json b/public/r/ColorBends-TS-TW.json
index 42901580..56bc833c 100644
--- a/public/r/ColorBends-TS-TW.json
+++ b/public/r/ColorBends-TS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "ColorBends/ColorBends.tsx",
- "content": "import React, { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\ntype ColorBendsProps = {\n className?: string;\n style?: React.CSSProperties;\n rotation?: number;\n speed?: number;\n colors?: string[];\n transparent?: boolean;\n autoRotate?: number;\n scale?: number;\n frequency?: number;\n warpStrength?: number;\n mouseInfluence?: number;\n parallax?: number;\n noise?: number;\n};\n\nconst MAX_COLORS = 8 as const;\n\nconst frag = `\n#define MAX_COLORS ${MAX_COLORS}\nuniform vec2 uCanvas;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec2 uRot;\nuniform int uColorCount;\nuniform vec3 uColors[MAX_COLORS];\nuniform int uTransparent;\nuniform float uScale;\nuniform float uFrequency;\nuniform float uWarpStrength;\nuniform vec2 uPointer; // in NDC [-1,1]\nuniform float uMouseInfluence;\nuniform float uParallax;\nuniform float uNoise;\nvarying vec2 vUv;\n\nvoid main() {\n float t = uTime * uSpeed;\n vec2 p = vUv * 2.0 - 1.0;\n p += uPointer * uParallax * 0.1;\n vec2 rp = vec2(p.x * uRot.x - p.y * uRot.y, p.x * uRot.y + p.y * uRot.x);\n vec2 q = vec2(rp.x * (uCanvas.x / uCanvas.y), rp.y);\n q /= max(uScale, 0.0001);\n q /= 0.5 + 0.2 * dot(q, q);\n q += 0.2 * cos(t) - 7.56;\n vec2 toward = (uPointer - rp);\n q += toward * uMouseInfluence * 0.2;\n\n vec3 col = vec3(0.0);\n float a = 1.0;\n\n if (uColorCount > 0) {\n vec2 s = q;\n vec3 sumCol = vec3(0.0);\n float cover = 0.0;\n for (int i = 0; i < MAX_COLORS; ++i) {\n if (i >= uColorCount) break;\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3); // strong response across 0..1\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0); // allow >1 to amplify displacement\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float m = mix(m0, m1, kMix);\n float w = 1.0 - exp(-6.0 / exp(6.0 * m));\n sumCol += uColors[i] * w;\n cover = max(cover, w);\n }\n col = clamp(sumCol, 0.0, 1.0);\n a = uTransparent > 0 ? cover : 1.0;\n } else {\n vec2 s = q;\n for (int k = 0; k < 3; ++k) {\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3);\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0);\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float m = mix(m0, m1, kMix);\n col[k] = 1.0 - exp(-6.0 / exp(6.0 * m));\n }\n a = uTransparent > 0 ? max(max(col.r, col.g), col.b) : 1.0;\n }\n\n if (uNoise > 0.0001) {\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(uTime), vec2(12.9898, 78.233))) * 43758.5453123);\n col += (n - 0.5) * uNoise;\n col = clamp(col, 0.0, 1.0);\n }\n\n vec3 rgb = (uTransparent > 0) ? col * a : col;\n gl_FragColor = vec4(rgb, a);\n}\n`;\n\nconst vert = `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nexport default function ColorBends({\n className,\n style,\n rotation = 45,\n speed = 0.2,\n colors = [],\n transparent = true,\n autoRotate = 0,\n scale = 1,\n frequency = 1,\n warpStrength = 1,\n mouseInfluence = 1,\n parallax = 0.5,\n noise = 0.1\n}: ColorBendsProps) {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const rafRef = useRef(null);\n const materialRef = useRef(null);\n const resizeObserverRef = useRef(null);\n const rotationRef = useRef(rotation);\n const autoRotateRef = useRef(autoRotate);\n const pointerTargetRef = useRef(new THREE.Vector2(0, 0));\n const pointerCurrentRef = useRef(new THREE.Vector2(0, 0));\n const pointerSmoothRef = useRef(8);\n\n useEffect(() => {\n const container = containerRef.current!;\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0));\n const material = new THREE.ShaderMaterial({\n vertexShader: vert,\n fragmentShader: frag,\n uniforms: {\n uCanvas: { value: new THREE.Vector2(1, 1) },\n uTime: { value: 0 },\n uSpeed: { value: speed },\n uRot: { value: new THREE.Vector2(1, 0) },\n uColorCount: { value: 0 },\n uColors: { value: uColorsArray },\n uTransparent: { value: transparent ? 1 : 0 },\n uScale: { value: scale },\n uFrequency: { value: frequency },\n uWarpStrength: { value: warpStrength },\n uPointer: { value: new THREE.Vector2(0, 0) },\n uMouseInfluence: { value: mouseInfluence },\n uParallax: { value: parallax },\n uNoise: { value: noise }\n },\n premultipliedAlpha: true,\n transparent: true\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n const renderer = new THREE.WebGLRenderer({\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n });\n rendererRef.current = renderer;\n (renderer as any).outputColorSpace = (THREE as any).SRGBColorSpace;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setClearColor(0x000000, transparent ? 0 : 1);\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.display = 'block';\n container.appendChild(renderer.domElement);\n\n const clock = new THREE.Clock();\n\n const handleResize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h, false);\n (material.uniforms.uCanvas.value as THREE.Vector2).set(w, h);\n };\n\n handleResize();\n\n if ('ResizeObserver' in window) {\n const ro = new ResizeObserver(handleResize);\n ro.observe(container);\n resizeObserverRef.current = ro;\n } else {\n (window as Window).addEventListener('resize', handleResize);\n }\n\n const loop = () => {\n const dt = clock.getDelta();\n const elapsed = clock.elapsedTime;\n material.uniforms.uTime.value = elapsed;\n\n const deg = (rotationRef.current % 360) + autoRotateRef.current * elapsed;\n const rad = (deg * Math.PI) / 180;\n const c = Math.cos(rad);\n const s = Math.sin(rad);\n (material.uniforms.uRot.value as THREE.Vector2).set(c, s);\n\n const cur = pointerCurrentRef.current;\n const tgt = pointerTargetRef.current;\n const amt = Math.min(1, dt * pointerSmoothRef.current);\n cur.lerp(tgt, amt);\n (material.uniforms.uPointer.value as THREE.Vector2).copy(cur);\n renderer.render(scene, camera);\n rafRef.current = requestAnimationFrame(loop);\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n if (resizeObserverRef.current) resizeObserverRef.current.disconnect();\n else (window as Window).removeEventListener('resize', handleResize);\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n if (renderer.domElement && renderer.domElement.parentElement === container) {\n container.removeChild(renderer.domElement);\n }\n };\n }, []);\n\n useEffect(() => {\n const material = materialRef.current;\n const renderer = rendererRef.current;\n if (!material) return;\n\n rotationRef.current = rotation;\n autoRotateRef.current = autoRotate;\n material.uniforms.uSpeed.value = speed;\n material.uniforms.uScale.value = scale;\n material.uniforms.uFrequency.value = frequency;\n material.uniforms.uWarpStrength.value = warpStrength;\n material.uniforms.uMouseInfluence.value = mouseInfluence;\n material.uniforms.uParallax.value = parallax;\n material.uniforms.uNoise.value = noise;\n\n const toVec3 = (hex: string) => {\n const h = hex.replace('#', '').trim();\n const v =\n h.length === 3\n ? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)]\n : [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255);\n };\n\n const arr = (colors || []).filter(Boolean).slice(0, MAX_COLORS).map(toVec3);\n for (let i = 0; i < MAX_COLORS; i++) {\n const vec = (material.uniforms.uColors.value as THREE.Vector3[])[i];\n if (i < arr.length) vec.copy(arr[i]);\n else vec.set(0, 0, 0);\n }\n material.uniforms.uColorCount.value = arr.length;\n\n material.uniforms.uTransparent.value = transparent ? 1 : 0;\n if (renderer) renderer.setClearColor(0x000000, transparent ? 0 : 1);\n }, [\n rotation,\n autoRotate,\n speed,\n scale,\n frequency,\n warpStrength,\n mouseInfluence,\n parallax,\n noise,\n colors,\n transparent\n ]);\n\n useEffect(() => {\n const material = materialRef.current;\n const container = containerRef.current;\n if (!material || !container) return;\n\n const handlePointerMove = (e: PointerEvent) => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / (rect.width || 1)) * 2 - 1;\n const y = -(((e.clientY - rect.top) / (rect.height || 1)) * 2 - 1);\n pointerTargetRef.current.set(x, y);\n };\n\n container.addEventListener('pointermove', handlePointerMove);\n return () => {\n container.removeEventListener('pointermove', handlePointerMove);\n };\n }, []);\n\n return ;\n}\n"
+ "content": "import React, { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\ntype ColorBendsProps = {\n className?: string;\n style?: React.CSSProperties;\n rotation?: number;\n speed?: number;\n colors?: string[];\n transparent?: boolean;\n autoRotate?: number;\n scale?: number;\n frequency?: number;\n warpStrength?: number;\n mouseInfluence?: number;\n parallax?: number;\n noise?: number;\n};\n\nconst MAX_COLORS = 8 as const;\n\nconst frag = `\n#define MAX_COLORS ${MAX_COLORS}\nuniform vec2 uCanvas;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec2 uRot;\nuniform int uColorCount;\nuniform vec3 uColors[MAX_COLORS];\nuniform int uTransparent;\nuniform float uScale;\nuniform float uFrequency;\nuniform float uWarpStrength;\nuniform vec2 uPointer; // in NDC [-1,1]\nuniform float uMouseInfluence;\nuniform float uParallax;\nuniform float uNoise;\nvarying vec2 vUv;\n\nvoid main() {\n float t = uTime * uSpeed;\n vec2 p = vUv * 2.0 - 1.0;\n p += uPointer * uParallax * 0.1;\n vec2 rp = vec2(p.x * uRot.x - p.y * uRot.y, p.x * uRot.y + p.y * uRot.x);\n vec2 q = vec2(rp.x * (uCanvas.x / uCanvas.y), rp.y);\n q /= max(uScale, 0.0001);\n q /= 0.5 + 0.2 * dot(q, q);\n q += 0.2 * cos(t) - 7.56;\n vec2 toward = (uPointer - rp);\n q += toward * uMouseInfluence * 0.2;\n\n vec3 col = vec3(0.0);\n float a = 1.0;\n\n if (uColorCount > 0) {\n vec2 s = q;\n vec3 sumCol = vec3(0.0);\n float cover = 0.0;\n for (int i = 0; i < MAX_COLORS; ++i) {\n if (i >= uColorCount) break;\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3); // strong response across 0..1\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0); // allow >1 to amplify displacement\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(i)) / 4.0);\n float m = mix(m0, m1, kMix);\n float w = 1.0 - exp(-6.0 / exp(6.0 * m));\n sumCol += uColors[i] * w;\n cover = max(cover, w);\n }\n col = clamp(sumCol, 0.0, 1.0);\n a = uTransparent > 0 ? cover : 1.0;\n } else {\n vec2 s = q;\n for (int k = 0; k < 3; ++k) {\n s -= 0.01;\n vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));\n float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float kBelow = clamp(uWarpStrength, 0.0, 1.0);\n float kMix = pow(kBelow, 0.3);\n float gain = 1.0 + max(uWarpStrength - 1.0, 0.0);\n vec2 disp = (r - s) * kBelow;\n vec2 warped = s + disp * gain;\n float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(k)) / 4.0);\n float m = mix(m0, m1, kMix);\n col[k] = 1.0 - exp(-6.0 / exp(6.0 * m));\n }\n a = uTransparent > 0 ? max(max(col.r, col.g), col.b) : 1.0;\n }\n\n if (uNoise > 0.0001) {\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(uTime), vec2(12.9898, 78.233))) * 43758.5453123);\n col += (n - 0.5) * uNoise;\n col = clamp(col, 0.0, 1.0);\n }\n\n vec3 rgb = (uTransparent > 0) ? col * a : col;\n gl_FragColor = vec4(rgb, a);\n}\n`;\n\nconst vert = `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nexport default function ColorBends({\n className,\n style,\n rotation = 45,\n speed = 0.2,\n colors = [],\n transparent = true,\n autoRotate = 0,\n scale = 1,\n frequency = 1,\n warpStrength = 1,\n mouseInfluence = 1,\n parallax = 0.5,\n noise = 0.1\n}: ColorBendsProps) {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const rafRef = useRef(null);\n const materialRef = useRef(null);\n const resizeObserverRef = useRef(null);\n const rotationRef = useRef(rotation);\n const autoRotateRef = useRef(autoRotate);\n const pointerTargetRef = useRef(new THREE.Vector2(0, 0));\n const pointerCurrentRef = useRef(new THREE.Vector2(0, 0));\n const pointerSmoothRef = useRef(8);\n\n useEffect(() => {\n const container = containerRef.current!;\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0));\n const material = new THREE.ShaderMaterial({\n vertexShader: vert,\n fragmentShader: frag,\n uniforms: {\n uCanvas: { value: new THREE.Vector2(1, 1) },\n uTime: { value: 0 },\n uSpeed: { value: speed },\n uRot: { value: new THREE.Vector2(1, 0) },\n uColorCount: { value: 0 },\n uColors: { value: uColorsArray },\n uTransparent: { value: transparent ? 1 : 0 },\n uScale: { value: scale },\n uFrequency: { value: frequency },\n uWarpStrength: { value: warpStrength },\n uPointer: { value: new THREE.Vector2(0, 0) },\n uMouseInfluence: { value: mouseInfluence },\n uParallax: { value: parallax },\n uNoise: { value: noise }\n },\n premultipliedAlpha: true,\n transparent: true\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n const renderer = new THREE.WebGLRenderer({\n antialias: false,\n powerPreference: 'high-performance',\n alpha: true\n });\n rendererRef.current = renderer;\n (renderer as any).outputColorSpace = (THREE as any).SRGBColorSpace;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setClearColor(0x000000, transparent ? 0 : 1);\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.display = 'block';\n container.appendChild(renderer.domElement);\n\n const clock = new THREE.Clock();\n\n const handleResize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h, false);\n (material.uniforms.uCanvas.value as THREE.Vector2).set(w, h);\n };\n\n handleResize();\n\n if ('ResizeObserver' in window) {\n const ro = new ResizeObserver(handleResize);\n ro.observe(container);\n resizeObserverRef.current = ro;\n } else {\n (window as Window).addEventListener('resize', handleResize);\n }\n\n const loop = () => {\n const dt = clock.getDelta();\n const elapsed = clock.elapsedTime;\n material.uniforms.uTime.value = elapsed;\n\n const deg = (rotationRef.current % 360) + autoRotateRef.current * elapsed;\n const rad = (deg * Math.PI) / 180;\n const c = Math.cos(rad);\n const s = Math.sin(rad);\n (material.uniforms.uRot.value as THREE.Vector2).set(c, s);\n\n const cur = pointerCurrentRef.current;\n const tgt = pointerTargetRef.current;\n const amt = Math.min(1, dt * pointerSmoothRef.current);\n cur.lerp(tgt, amt);\n (material.uniforms.uPointer.value as THREE.Vector2).copy(cur);\n renderer.render(scene, camera);\n rafRef.current = requestAnimationFrame(loop);\n };\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n if (resizeObserverRef.current) resizeObserverRef.current.disconnect();\n else (window as Window).removeEventListener('resize', handleResize);\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (renderer.domElement && renderer.domElement.parentElement === container) {\n container.removeChild(renderer.domElement);\n }\n };\n }, []);\n\n useEffect(() => {\n const material = materialRef.current;\n const renderer = rendererRef.current;\n if (!material) return;\n\n rotationRef.current = rotation;\n autoRotateRef.current = autoRotate;\n material.uniforms.uSpeed.value = speed;\n material.uniforms.uScale.value = scale;\n material.uniforms.uFrequency.value = frequency;\n material.uniforms.uWarpStrength.value = warpStrength;\n material.uniforms.uMouseInfluence.value = mouseInfluence;\n material.uniforms.uParallax.value = parallax;\n material.uniforms.uNoise.value = noise;\n\n const toVec3 = (hex: string) => {\n const h = hex.replace('#', '').trim();\n const v =\n h.length === 3\n ? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)]\n : [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];\n return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255);\n };\n\n const arr = (colors || []).filter(Boolean).slice(0, MAX_COLORS).map(toVec3);\n for (let i = 0; i < MAX_COLORS; i++) {\n const vec = (material.uniforms.uColors.value as THREE.Vector3[])[i];\n if (i < arr.length) vec.copy(arr[i]);\n else vec.set(0, 0, 0);\n }\n material.uniforms.uColorCount.value = arr.length;\n\n material.uniforms.uTransparent.value = transparent ? 1 : 0;\n if (renderer) renderer.setClearColor(0x000000, transparent ? 0 : 1);\n }, [\n rotation,\n autoRotate,\n speed,\n scale,\n frequency,\n warpStrength,\n mouseInfluence,\n parallax,\n noise,\n colors,\n transparent\n ]);\n\n useEffect(() => {\n const material = materialRef.current;\n const container = containerRef.current;\n if (!material || !container) return;\n\n const handlePointerMove = (e: PointerEvent) => {\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / (rect.width || 1)) * 2 - 1;\n const y = -(((e.clientY - rect.top) / (rect.height || 1)) * 2 - 1);\n pointerTargetRef.current.set(x, y);\n };\n\n container.addEventListener('pointermove', handlePointerMove);\n return () => {\n container.removeEventListener('pointermove', handlePointerMove);\n };\n }, []);\n\n return ;\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/FloatingLines-JS-CSS.json b/public/r/FloatingLines-JS-CSS.json
index d797a956..59415351 100644
--- a/public/r/FloatingLines-JS-CSS.json
+++ b/public/r/FloatingLines-JS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "FloatingLines/FloatingLines.jsx",
- "content": "import { useEffect, useRef } from 'react';\nimport {\n Scene,\n OrthographicCamera,\n WebGLRenderer,\n PlaneGeometry,\n Mesh,\n ShaderMaterial,\n Vector3,\n Vector2,\n Clock\n} from 'three';\n\nimport './FloatingLines.css';\n\nconst vertexShader = `\nprecision highp float;\n\nvoid main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float animationSpeed;\n\nuniform bool enableTop;\nuniform bool enableMiddle;\nuniform bool enableBottom;\n\nuniform int topLineCount;\nuniform int middleLineCount;\nuniform int bottomLineCount;\n\nuniform float topLineDistance;\nuniform float middleLineDistance;\nuniform float bottomLineDistance;\n\nuniform vec3 topWavePosition;\nuniform vec3 middleWavePosition;\nuniform vec3 bottomWavePosition;\n\nuniform vec2 iMouse;\nuniform bool interactive;\nuniform float bendRadius;\nuniform float bendStrength;\nuniform float bendInfluence;\n\nuniform bool parallax;\nuniform float parallaxStrength;\nuniform vec2 parallaxOffset;\n\nuniform vec3 lineGradient[8];\nuniform int lineGradientCount;\n\nconst vec3 BLACK = vec3(0.0);\nconst vec3 PINK = vec3(233.0, 71.0, 245.0) / 255.0;\nconst vec3 BLUE = vec3(47.0, 75.0, 162.0) / 255.0;\n\nmat2 rotate(float r) {\n return mat2(cos(r), sin(r), -sin(r), cos(r));\n}\n\nvec3 background_color(vec2 uv) {\n vec3 col = vec3(0.0);\n\n float y = sin(uv.x - 0.2) * 0.3 - 0.1;\n float m = uv.y - y;\n\n col += mix(BLUE, BLACK, smoothstep(0.0, 1.0, abs(m)));\n col += mix(PINK, BLACK, smoothstep(0.0, 1.0, abs(m - 0.8)));\n return col * 0.5;\n}\n\nvec3 getLineColor(float t, vec3 baseColor) {\n if (lineGradientCount <= 0) {\n return baseColor;\n }\n\n vec3 gradientColor;\n \n if (lineGradientCount == 1) {\n gradientColor = lineGradient[0];\n } else {\n float clampedT = clamp(t, 0.0, 0.9999);\n float scaled = clampedT * float(lineGradientCount - 1);\n int idx = int(floor(scaled));\n float f = fract(scaled);\n int idx2 = min(idx + 1, lineGradientCount - 1);\n\n vec3 c1 = lineGradient[idx];\n vec3 c2 = lineGradient[idx2];\n \n gradientColor = mix(c1, c2, f);\n }\n \n return gradientColor * 0.5;\n}\n\n float wave(vec2 uv, float offset, vec2 screenUv, vec2 mouseUv, bool shouldBend) {\n float time = iTime * animationSpeed;\n\n float x_offset = offset;\n float x_movement = time * 0.1;\n float amp = sin(offset + time * 0.2) * 0.3;\n float y = sin(uv.x + x_offset + x_movement) * amp;\n\n if (shouldBend) {\n vec2 d = screenUv - mouseUv;\n float influence = exp(-dot(d, d) * bendRadius); // radial falloff around cursor\n float bendOffset = (mouseUv.y - screenUv.y) * influence * bendStrength * bendInfluence;\n y += bendOffset;\n }\n\n float m = uv.y - y;\n return 0.0175 / max(abs(m) + 0.01, 1e-3) + 0.01;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n baseUv.y *= -1.0;\n \n if (parallax) {\n baseUv += parallaxOffset;\n }\n\n vec3 col = vec3(0.0);\n\n vec3 b = lineGradientCount > 0 ? vec3(0.0) : background_color(baseUv);\n\n vec2 mouseUv = vec2(0.0);\n if (interactive) {\n mouseUv = (2.0 * iMouse - iResolution.xy) / iResolution.y;\n mouseUv.y *= -1.0;\n }\n \n if (enableBottom) {\n for (int i = 0; i < bottomLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(bottomLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = bottomWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(bottomLineDistance * fi + bottomWavePosition.x, bottomWavePosition.y),\n 1.5 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.2;\n }\n }\n\n if (enableMiddle) {\n for (int i = 0; i < middleLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(middleLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = middleWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(middleLineDistance * fi + middleWavePosition.x, middleWavePosition.y),\n 2.0 + 0.15 * fi,\n baseUv,\n mouseUv,\n interactive\n );\n }\n }\n\n if (enableTop) {\n for (int i = 0; i < topLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(topLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = topWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n ruv.x *= -1.0;\n col += lineCol * wave(\n ruv + vec2(topLineDistance * fi + topWavePosition.x, topWavePosition.y),\n 1.0 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.1;\n }\n }\n\n fragColor = vec4(col, 1.0);\n}\n\nvoid main() {\n vec4 color = vec4(0.0);\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}\n`;\n\nconst MAX_GRADIENT_STOPS = 8;\n\nfunction hexToVec3(hex) {\n let value = hex.trim();\n\n if (value.startsWith('#')) {\n value = value.slice(1);\n }\n\n let r = 255;\n let g = 255;\n let b = 255;\n\n if (value.length === 3) {\n r = parseInt(value[0] + value[0], 16);\n g = parseInt(value[1] + value[1], 16);\n b = parseInt(value[2] + value[2], 16);\n } else if (value.length === 6) {\n r = parseInt(value.slice(0, 2), 16);\n g = parseInt(value.slice(2, 4), 16);\n b = parseInt(value.slice(4, 6), 16);\n }\n\n return new Vector3(r / 255, g / 255, b / 255);\n}\n\nexport default function FloatingLines({\n linesGradient,\n enabledWaves = ['top', 'middle', 'bottom'],\n lineCount = [6],\n lineDistance = [5],\n topWavePosition,\n middleWavePosition,\n bottomWavePosition = { x: 2.0, y: -0.7, rotate: -1 },\n animationSpeed = 1,\n interactive = true,\n bendRadius = 5.0,\n bendStrength = -0.5,\n mouseDamping = 0.05,\n parallax = true,\n parallaxStrength = 0.2,\n mixBlendMode = 'screen'\n}) {\n const containerRef = useRef(null);\n const targetMouseRef = useRef(new Vector2(-1000, -1000));\n const currentMouseRef = useRef(new Vector2(-1000, -1000));\n const targetInfluenceRef = useRef(0);\n const currentInfluenceRef = useRef(0);\n const targetParallaxRef = useRef(new Vector2(0, 0));\n const currentParallaxRef = useRef(new Vector2(0, 0));\n\n const getLineCount = waveType => {\n if (typeof lineCount === 'number') return lineCount;\n if (!enabledWaves.includes(waveType)) return 0;\n const index = enabledWaves.indexOf(waveType);\n return lineCount[index] ?? 6;\n };\n\n const getLineDistance = waveType => {\n if (typeof lineDistance === 'number') return lineDistance;\n if (!enabledWaves.includes(waveType)) return 0.1;\n const index = enabledWaves.indexOf(waveType);\n return lineDistance[index] ?? 0.1;\n };\n\n const topLineCount = enabledWaves.includes('top') ? getLineCount('top') : 0;\n const middleLineCount = enabledWaves.includes('middle') ? getLineCount('middle') : 0;\n const bottomLineCount = enabledWaves.includes('bottom') ? getLineCount('bottom') : 0;\n\n const topLineDistance = enabledWaves.includes('top') ? getLineDistance('top') * 0.01 : 0.01;\n const middleLineDistance = enabledWaves.includes('middle') ? getLineDistance('middle') * 0.01 : 0.01;\n const bottomLineDistance = enabledWaves.includes('bottom') ? getLineDistance('bottom') * 0.01 : 0.01;\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const scene = new Scene();\n\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n camera.position.z = 1;\n\n const renderer = new WebGLRenderer({ antialias: true, alpha: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n containerRef.current.appendChild(renderer.domElement);\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: new Vector3(1, 1, 1) },\n animationSpeed: { value: animationSpeed },\n\n enableTop: { value: enabledWaves.includes('top') },\n enableMiddle: { value: enabledWaves.includes('middle') },\n enableBottom: { value: enabledWaves.includes('bottom') },\n\n topLineCount: { value: topLineCount },\n middleLineCount: { value: middleLineCount },\n bottomLineCount: { value: bottomLineCount },\n\n topLineDistance: { value: topLineDistance },\n middleLineDistance: { value: middleLineDistance },\n bottomLineDistance: { value: bottomLineDistance },\n\n topWavePosition: {\n value: new Vector3(topWavePosition?.x ?? 10.0, topWavePosition?.y ?? 0.5, topWavePosition?.rotate ?? -0.4)\n },\n middleWavePosition: {\n value: new Vector3(\n middleWavePosition?.x ?? 5.0,\n middleWavePosition?.y ?? 0.0,\n middleWavePosition?.rotate ?? 0.2\n )\n },\n bottomWavePosition: {\n value: new Vector3(\n bottomWavePosition?.x ?? 2.0,\n bottomWavePosition?.y ?? -0.7,\n bottomWavePosition?.rotate ?? 0.4\n )\n },\n\n iMouse: { value: new Vector2(-1000, -1000) },\n interactive: { value: interactive },\n bendRadius: { value: bendRadius },\n bendStrength: { value: bendStrength },\n bendInfluence: { value: 0 },\n\n parallax: { value: parallax },\n parallaxStrength: { value: parallaxStrength },\n parallaxOffset: { value: new Vector2(0, 0) },\n\n lineGradient: {\n value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1))\n },\n lineGradientCount: { value: 0 }\n };\n\n if (linesGradient && linesGradient.length > 0) {\n const stops = linesGradient.slice(0, MAX_GRADIENT_STOPS);\n uniforms.lineGradientCount.value = stops.length;\n\n stops.forEach((hex, i) => {\n const color = hexToVec3(hex);\n uniforms.lineGradient.value[i].set(color.x, color.y, color.z);\n });\n }\n\n const material = new ShaderMaterial({\n uniforms,\n vertexShader,\n fragmentShader\n });\n\n const geometry = new PlaneGeometry(2, 2);\n const mesh = new Mesh(geometry, material);\n scene.add(mesh);\n\n const clock = new Clock();\n\n const setSize = () => {\n const el = containerRef.current;\n const width = el.clientWidth || 1;\n const height = el.clientHeight || 1;\n\n renderer.setSize(width, height, false);\n\n const canvasWidth = renderer.domElement.width;\n const canvasHeight = renderer.domElement.height;\n uniforms.iResolution.value.set(canvasWidth, canvasHeight, 1);\n };\n\n setSize();\n\n const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(setSize) : null;\n\n if (ro && containerRef.current) {\n ro.observe(containerRef.current);\n }\n\n const handlePointerMove = event => {\n const rect = renderer.domElement.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n const dpr = renderer.getPixelRatio();\n\n targetMouseRef.current.set(x * dpr, (rect.height - y) * dpr);\n targetInfluenceRef.current = 1.0;\n\n if (parallax) {\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const offsetX = (x - centerX) / rect.width;\n const offsetY = -(y - centerY) / rect.height;\n targetParallaxRef.current.set(offsetX * parallaxStrength, offsetY * parallaxStrength);\n }\n };\n\n const handlePointerLeave = () => {\n targetInfluenceRef.current = 0.0;\n };\n\n if (interactive) {\n renderer.domElement.addEventListener('pointermove', handlePointerMove);\n renderer.domElement.addEventListener('pointerleave', handlePointerLeave);\n }\n\n let raf = 0;\n const renderLoop = () => {\n uniforms.iTime.value = clock.getElapsedTime();\n\n if (interactive) {\n currentMouseRef.current.lerp(targetMouseRef.current, mouseDamping);\n uniforms.iMouse.value.copy(currentMouseRef.current);\n\n currentInfluenceRef.current += (targetInfluenceRef.current - currentInfluenceRef.current) * mouseDamping;\n uniforms.bendInfluence.value = currentInfluenceRef.current;\n }\n\n if (parallax) {\n currentParallaxRef.current.lerp(targetParallaxRef.current, mouseDamping);\n uniforms.parallaxOffset.value.copy(currentParallaxRef.current);\n }\n\n renderer.render(scene, camera);\n raf = requestAnimationFrame(renderLoop);\n };\n renderLoop();\n\n return () => {\n cancelAnimationFrame(raf);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n if (ro && containerRef.current) {\n ro.disconnect();\n }\n\n if (interactive) {\n renderer.domElement.removeEventListener('pointermove', handlePointerMove);\n renderer.domElement.removeEventListener('pointerleave', handlePointerLeave);\n }\n\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n if (renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n linesGradient,\n enabledWaves,\n lineCount,\n lineDistance,\n topWavePosition,\n middleWavePosition,\n bottomWavePosition,\n animationSpeed,\n interactive,\n bendRadius,\n bendStrength,\n mouseDamping,\n parallax,\n parallaxStrength\n ]);\n\n return (\n \n );\n}\n"
+ "content": "import { useEffect, useRef } from 'react';\nimport {\n Clock,\n Mesh,\n OrthographicCamera,\n PlaneGeometry,\n Scene,\n ShaderMaterial,\n Vector2,\n Vector3,\n WebGLRenderer\n} from 'three';\n\nimport './FloatingLines.css';\n\nconst vertexShader = `\nprecision highp float;\n\nvoid main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float animationSpeed;\n\nuniform bool enableTop;\nuniform bool enableMiddle;\nuniform bool enableBottom;\n\nuniform int topLineCount;\nuniform int middleLineCount;\nuniform int bottomLineCount;\n\nuniform float topLineDistance;\nuniform float middleLineDistance;\nuniform float bottomLineDistance;\n\nuniform vec3 topWavePosition;\nuniform vec3 middleWavePosition;\nuniform vec3 bottomWavePosition;\n\nuniform vec2 iMouse;\nuniform bool interactive;\nuniform float bendRadius;\nuniform float bendStrength;\nuniform float bendInfluence;\n\nuniform bool parallax;\nuniform float parallaxStrength;\nuniform vec2 parallaxOffset;\n\nuniform vec3 lineGradient[8];\nuniform int lineGradientCount;\n\nconst vec3 BLACK = vec3(0.0);\nconst vec3 PINK = vec3(233.0, 71.0, 245.0) / 255.0;\nconst vec3 BLUE = vec3(47.0, 75.0, 162.0) / 255.0;\n\nmat2 rotate(float r) {\n return mat2(cos(r), sin(r), -sin(r), cos(r));\n}\n\nvec3 background_color(vec2 uv) {\n vec3 col = vec3(0.0);\n\n float y = sin(uv.x - 0.2) * 0.3 - 0.1;\n float m = uv.y - y;\n\n col += mix(BLUE, BLACK, smoothstep(0.0, 1.0, abs(m)));\n col += mix(PINK, BLACK, smoothstep(0.0, 1.0, abs(m - 0.8)));\n return col * 0.5;\n}\n\nvec3 getLineColor(float t, vec3 baseColor) {\n if (lineGradientCount <= 0) {\n return baseColor;\n }\n\n vec3 gradientColor;\n \n if (lineGradientCount == 1) {\n gradientColor = lineGradient[0];\n } else {\n float clampedT = clamp(t, 0.0, 0.9999);\n float scaled = clampedT * float(lineGradientCount - 1);\n int idx = int(floor(scaled));\n float f = fract(scaled);\n int idx2 = min(idx + 1, lineGradientCount - 1);\n\n vec3 c1 = lineGradient[idx];\n vec3 c2 = lineGradient[idx2];\n \n gradientColor = mix(c1, c2, f);\n }\n \n return gradientColor * 0.5;\n}\n\n float wave(vec2 uv, float offset, vec2 screenUv, vec2 mouseUv, bool shouldBend) {\n float time = iTime * animationSpeed;\n\n float x_offset = offset;\n float x_movement = time * 0.1;\n float amp = sin(offset + time * 0.2) * 0.3;\n float y = sin(uv.x + x_offset + x_movement) * amp;\n\n if (shouldBend) {\n vec2 d = screenUv - mouseUv;\n float influence = exp(-dot(d, d) * bendRadius); // radial falloff around cursor\n float bendOffset = (mouseUv.y - screenUv.y) * influence * bendStrength * bendInfluence;\n y += bendOffset;\n }\n\n float m = uv.y - y;\n return 0.0175 / max(abs(m) + 0.01, 1e-3) + 0.01;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n baseUv.y *= -1.0;\n \n if (parallax) {\n baseUv += parallaxOffset;\n }\n\n vec3 col = vec3(0.0);\n\n vec3 b = lineGradientCount > 0 ? vec3(0.0) : background_color(baseUv);\n\n vec2 mouseUv = vec2(0.0);\n if (interactive) {\n mouseUv = (2.0 * iMouse - iResolution.xy) / iResolution.y;\n mouseUv.y *= -1.0;\n }\n \n if (enableBottom) {\n for (int i = 0; i < bottomLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(bottomLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = bottomWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(bottomLineDistance * fi + bottomWavePosition.x, bottomWavePosition.y),\n 1.5 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.2;\n }\n }\n\n if (enableMiddle) {\n for (int i = 0; i < middleLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(middleLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = middleWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(middleLineDistance * fi + middleWavePosition.x, middleWavePosition.y),\n 2.0 + 0.15 * fi,\n baseUv,\n mouseUv,\n interactive\n );\n }\n }\n\n if (enableTop) {\n for (int i = 0; i < topLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(topLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = topWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n ruv.x *= -1.0;\n col += lineCol * wave(\n ruv + vec2(topLineDistance * fi + topWavePosition.x, topWavePosition.y),\n 1.0 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.1;\n }\n }\n\n fragColor = vec4(col, 1.0);\n}\n\nvoid main() {\n vec4 color = vec4(0.0);\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}\n`;\n\nconst MAX_GRADIENT_STOPS = 8;\n\nfunction hexToVec3(hex) {\n let value = hex.trim();\n\n if (value.startsWith('#')) {\n value = value.slice(1);\n }\n\n let r = 255;\n let g = 255;\n let b = 255;\n\n if (value.length === 3) {\n r = parseInt(value[0] + value[0], 16);\n g = parseInt(value[1] + value[1], 16);\n b = parseInt(value[2] + value[2], 16);\n } else if (value.length === 6) {\n r = parseInt(value.slice(0, 2), 16);\n g = parseInt(value.slice(2, 4), 16);\n b = parseInt(value.slice(4, 6), 16);\n }\n\n return new Vector3(r / 255, g / 255, b / 255);\n}\n\nexport default function FloatingLines({\n linesGradient,\n enabledWaves = ['top', 'middle', 'bottom'],\n lineCount = [6],\n lineDistance = [5],\n topWavePosition,\n middleWavePosition,\n bottomWavePosition = { x: 2.0, y: -0.7, rotate: -1 },\n animationSpeed = 1,\n interactive = true,\n bendRadius = 5.0,\n bendStrength = -0.5,\n mouseDamping = 0.05,\n parallax = true,\n parallaxStrength = 0.2,\n mixBlendMode = 'screen'\n}) {\n const containerRef = useRef(null);\n const targetMouseRef = useRef(new Vector2(-1000, -1000));\n const currentMouseRef = useRef(new Vector2(-1000, -1000));\n const targetInfluenceRef = useRef(0);\n const currentInfluenceRef = useRef(0);\n const targetParallaxRef = useRef(new Vector2(0, 0));\n const currentParallaxRef = useRef(new Vector2(0, 0));\n\n const getLineCount = waveType => {\n if (typeof lineCount === 'number') return lineCount;\n if (!enabledWaves.includes(waveType)) return 0;\n const index = enabledWaves.indexOf(waveType);\n return lineCount[index] ?? 6;\n };\n\n const getLineDistance = waveType => {\n if (typeof lineDistance === 'number') return lineDistance;\n if (!enabledWaves.includes(waveType)) return 0.1;\n const index = enabledWaves.indexOf(waveType);\n return lineDistance[index] ?? 0.1;\n };\n\n const topLineCount = enabledWaves.includes('top') ? getLineCount('top') : 0;\n const middleLineCount = enabledWaves.includes('middle') ? getLineCount('middle') : 0;\n const bottomLineCount = enabledWaves.includes('bottom') ? getLineCount('bottom') : 0;\n\n const topLineDistance = enabledWaves.includes('top') ? getLineDistance('top') * 0.01 : 0.01;\n const middleLineDistance = enabledWaves.includes('middle') ? getLineDistance('middle') * 0.01 : 0.01;\n const bottomLineDistance = enabledWaves.includes('bottom') ? getLineDistance('bottom') * 0.01 : 0.01;\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const scene = new Scene();\n\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n camera.position.z = 1;\n\n const renderer = new WebGLRenderer({ antialias: true, alpha: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n containerRef.current.appendChild(renderer.domElement);\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: new Vector3(1, 1, 1) },\n animationSpeed: { value: animationSpeed },\n\n enableTop: { value: enabledWaves.includes('top') },\n enableMiddle: { value: enabledWaves.includes('middle') },\n enableBottom: { value: enabledWaves.includes('bottom') },\n\n topLineCount: { value: topLineCount },\n middleLineCount: { value: middleLineCount },\n bottomLineCount: { value: bottomLineCount },\n\n topLineDistance: { value: topLineDistance },\n middleLineDistance: { value: middleLineDistance },\n bottomLineDistance: { value: bottomLineDistance },\n\n topWavePosition: {\n value: new Vector3(topWavePosition?.x ?? 10.0, topWavePosition?.y ?? 0.5, topWavePosition?.rotate ?? -0.4)\n },\n middleWavePosition: {\n value: new Vector3(\n middleWavePosition?.x ?? 5.0,\n middleWavePosition?.y ?? 0.0,\n middleWavePosition?.rotate ?? 0.2\n )\n },\n bottomWavePosition: {\n value: new Vector3(\n bottomWavePosition?.x ?? 2.0,\n bottomWavePosition?.y ?? -0.7,\n bottomWavePosition?.rotate ?? 0.4\n )\n },\n\n iMouse: { value: new Vector2(-1000, -1000) },\n interactive: { value: interactive },\n bendRadius: { value: bendRadius },\n bendStrength: { value: bendStrength },\n bendInfluence: { value: 0 },\n\n parallax: { value: parallax },\n parallaxStrength: { value: parallaxStrength },\n parallaxOffset: { value: new Vector2(0, 0) },\n\n lineGradient: {\n value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1))\n },\n lineGradientCount: { value: 0 }\n };\n\n if (linesGradient && linesGradient.length > 0) {\n const stops = linesGradient.slice(0, MAX_GRADIENT_STOPS);\n uniforms.lineGradientCount.value = stops.length;\n\n stops.forEach((hex, i) => {\n const color = hexToVec3(hex);\n uniforms.lineGradient.value[i].set(color.x, color.y, color.z);\n });\n }\n\n const material = new ShaderMaterial({\n uniforms,\n vertexShader,\n fragmentShader\n });\n\n const geometry = new PlaneGeometry(2, 2);\n const mesh = new Mesh(geometry, material);\n scene.add(mesh);\n\n const clock = new Clock();\n\n const setSize = () => {\n const el = containerRef.current;\n const width = el.clientWidth || 1;\n const height = el.clientHeight || 1;\n\n renderer.setSize(width, height, false);\n\n const canvasWidth = renderer.domElement.width;\n const canvasHeight = renderer.domElement.height;\n uniforms.iResolution.value.set(canvasWidth, canvasHeight, 1);\n };\n\n setSize();\n\n const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(setSize) : null;\n\n if (ro && containerRef.current) {\n ro.observe(containerRef.current);\n }\n\n const handlePointerMove = event => {\n const rect = renderer.domElement.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n const dpr = renderer.getPixelRatio();\n\n targetMouseRef.current.set(x * dpr, (rect.height - y) * dpr);\n targetInfluenceRef.current = 1.0;\n\n if (parallax) {\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const offsetX = (x - centerX) / rect.width;\n const offsetY = -(y - centerY) / rect.height;\n targetParallaxRef.current.set(offsetX * parallaxStrength, offsetY * parallaxStrength);\n }\n };\n\n const handlePointerLeave = () => {\n targetInfluenceRef.current = 0.0;\n };\n\n if (interactive) {\n renderer.domElement.addEventListener('pointermove', handlePointerMove);\n renderer.domElement.addEventListener('pointerleave', handlePointerLeave);\n }\n\n let raf = 0;\n const renderLoop = () => {\n uniforms.iTime.value = clock.getElapsedTime();\n\n if (interactive) {\n currentMouseRef.current.lerp(targetMouseRef.current, mouseDamping);\n uniforms.iMouse.value.copy(currentMouseRef.current);\n\n currentInfluenceRef.current += (targetInfluenceRef.current - currentInfluenceRef.current) * mouseDamping;\n uniforms.bendInfluence.value = currentInfluenceRef.current;\n }\n\n if (parallax) {\n currentParallaxRef.current.lerp(targetParallaxRef.current, mouseDamping);\n uniforms.parallaxOffset.value.copy(currentParallaxRef.current);\n }\n\n renderer.render(scene, camera);\n raf = requestAnimationFrame(renderLoop);\n };\n renderLoop();\n\n return () => {\n cancelAnimationFrame(raf);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n if (ro && containerRef.current) {\n ro.disconnect();\n }\n\n if (interactive) {\n renderer.domElement.removeEventListener('pointermove', handlePointerMove);\n renderer.domElement.removeEventListener('pointerleave', handlePointerLeave);\n }\n\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n linesGradient,\n enabledWaves,\n lineCount,\n lineDistance,\n topWavePosition,\n middleWavePosition,\n bottomWavePosition,\n animationSpeed,\n interactive,\n bendRadius,\n bendStrength,\n mouseDamping,\n parallax,\n parallaxStrength\n ]);\n\n return (\n \n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/FloatingLines-JS-TW.json b/public/r/FloatingLines-JS-TW.json
index b355e4cd..99a9657d 100644
--- a/public/r/FloatingLines-JS-TW.json
+++ b/public/r/FloatingLines-JS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "FloatingLines/FloatingLines.jsx",
- "content": "import { useEffect, useRef } from 'react';\nimport {\n Scene,\n OrthographicCamera,\n WebGLRenderer,\n PlaneGeometry,\n Mesh,\n ShaderMaterial,\n Vector3,\n Vector2,\n Clock\n} from 'three';\n\nconst vertexShader = `\nprecision highp float;\n\nvoid main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float animationSpeed;\n\nuniform bool enableTop;\nuniform bool enableMiddle;\nuniform bool enableBottom;\n\nuniform int topLineCount;\nuniform int middleLineCount;\nuniform int bottomLineCount;\n\nuniform float topLineDistance;\nuniform float middleLineDistance;\nuniform float bottomLineDistance;\n\nuniform vec3 topWavePosition;\nuniform vec3 middleWavePosition;\nuniform vec3 bottomWavePosition;\n\nuniform vec2 iMouse;\nuniform bool interactive;\nuniform float bendRadius;\nuniform float bendStrength;\nuniform float bendInfluence;\n\nuniform bool parallax;\nuniform float parallaxStrength;\nuniform vec2 parallaxOffset;\n\nuniform vec3 lineGradient[8];\nuniform int lineGradientCount;\n\nconst vec3 BLACK = vec3(0.0);\nconst vec3 PINK = vec3(233.0, 71.0, 245.0) / 255.0;\nconst vec3 BLUE = vec3(47.0, 75.0, 162.0) / 255.0;\n\nmat2 rotate(float r) {\n return mat2(cos(r), sin(r), -sin(r), cos(r));\n}\n\nvec3 background_color(vec2 uv) {\n vec3 col = vec3(0.0);\n\n float y = sin(uv.x - 0.2) * 0.3 - 0.1;\n float m = uv.y - y;\n\n col += mix(BLUE, BLACK, smoothstep(0.0, 1.0, abs(m)));\n col += mix(PINK, BLACK, smoothstep(0.0, 1.0, abs(m - 0.8)));\n return col * 0.5;\n}\n\nvec3 getLineColor(float t, vec3 baseColor) {\n if (lineGradientCount <= 0) {\n return baseColor;\n }\n\n vec3 gradientColor;\n \n if (lineGradientCount == 1) {\n gradientColor = lineGradient[0];\n } else {\n float clampedT = clamp(t, 0.0, 0.9999);\n float scaled = clampedT * float(lineGradientCount - 1);\n int idx = int(floor(scaled));\n float f = fract(scaled);\n int idx2 = min(idx + 1, lineGradientCount - 1);\n\n vec3 c1 = lineGradient[idx];\n vec3 c2 = lineGradient[idx2];\n \n gradientColor = mix(c1, c2, f);\n }\n \n return gradientColor * 0.5;\n}\n\n float wave(vec2 uv, float offset, vec2 screenUv, vec2 mouseUv, bool shouldBend) {\n float time = iTime * animationSpeed;\n\n float x_offset = offset;\n float x_movement = time * 0.1;\n float amp = sin(offset + time * 0.2) * 0.3;\n float y = sin(uv.x + x_offset + x_movement) * amp;\n\n if (shouldBend) {\n vec2 d = screenUv - mouseUv;\n float influence = exp(-dot(d, d) * bendRadius); // radial falloff around cursor\n float bendOffset = (mouseUv.y - screenUv.y) * influence * bendStrength * bendInfluence;\n y += bendOffset;\n }\n\n float m = uv.y - y;\n return 0.0175 / max(abs(m) + 0.01, 1e-3) + 0.01;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n baseUv.y *= -1.0;\n \n if (parallax) {\n baseUv += parallaxOffset;\n }\n\n vec3 col = vec3(0.0);\n\n vec3 b = lineGradientCount > 0 ? vec3(0.0) : background_color(baseUv);\n\n vec2 mouseUv = vec2(0.0);\n if (interactive) {\n mouseUv = (2.0 * iMouse - iResolution.xy) / iResolution.y;\n mouseUv.y *= -1.0;\n }\n \n if (enableBottom) {\n for (int i = 0; i < bottomLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(bottomLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = bottomWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(bottomLineDistance * fi + bottomWavePosition.x, bottomWavePosition.y),\n 1.5 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.2;\n }\n }\n\n if (enableMiddle) {\n for (int i = 0; i < middleLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(middleLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = middleWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(middleLineDistance * fi + middleWavePosition.x, middleWavePosition.y),\n 2.0 + 0.15 * fi,\n baseUv,\n mouseUv,\n interactive\n );\n }\n }\n\n if (enableTop) {\n for (int i = 0; i < topLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(topLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = topWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n ruv.x *= -1.0;\n col += lineCol * wave(\n ruv + vec2(topLineDistance * fi + topWavePosition.x, topWavePosition.y),\n 1.0 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.1;\n }\n }\n\n fragColor = vec4(col, 1.0);\n}\n\nvoid main() {\n vec4 color = vec4(0.0);\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}\n`;\n\nconst MAX_GRADIENT_STOPS = 8;\n\nfunction hexToVec3(hex) {\n let value = hex.trim();\n\n if (value.startsWith('#')) {\n value = value.slice(1);\n }\n\n let r = 255;\n let g = 255;\n let b = 255;\n\n if (value.length === 3) {\n r = parseInt(value[0] + value[0], 16);\n g = parseInt(value[1] + value[1], 16);\n b = parseInt(value[2] + value[2], 16);\n } else if (value.length === 6) {\n r = parseInt(value.slice(0, 2), 16);\n g = parseInt(value.slice(2, 4), 16);\n b = parseInt(value.slice(4, 6), 16);\n }\n\n return new Vector3(r / 255, g / 255, b / 255);\n}\n\nexport default function FloatingLines({\n linesGradient,\n enabledWaves = ['top', 'middle', 'bottom'],\n lineCount = [6],\n lineDistance = [5],\n topWavePosition,\n middleWavePosition,\n bottomWavePosition = { x: 2.0, y: -0.7, rotate: -1 },\n animationSpeed = 1,\n interactive = true,\n bendRadius = 5.0,\n bendStrength = -0.5,\n mouseDamping = 0.05,\n parallax = true,\n parallaxStrength = 0.2,\n mixBlendMode = 'screen'\n}) {\n const containerRef = useRef(null);\n const targetMouseRef = useRef(new Vector2(-1000, -1000));\n const currentMouseRef = useRef(new Vector2(-1000, -1000));\n const targetInfluenceRef = useRef(0);\n const currentInfluenceRef = useRef(0);\n const targetParallaxRef = useRef(new Vector2(0, 0));\n const currentParallaxRef = useRef(new Vector2(0, 0));\n\n const getLineCount = waveType => {\n if (typeof lineCount === 'number') return lineCount;\n if (!enabledWaves.includes(waveType)) return 0;\n const index = enabledWaves.indexOf(waveType);\n return lineCount[index] ?? 6;\n };\n\n const getLineDistance = waveType => {\n if (typeof lineDistance === 'number') return lineDistance;\n if (!enabledWaves.includes(waveType)) return 0.1;\n const index = enabledWaves.indexOf(waveType);\n return lineDistance[index] ?? 0.1;\n };\n\n const topLineCount = enabledWaves.includes('top') ? getLineCount('top') : 0;\n const middleLineCount = enabledWaves.includes('middle') ? getLineCount('middle') : 0;\n const bottomLineCount = enabledWaves.includes('bottom') ? getLineCount('bottom') : 0;\n\n const topLineDistance = enabledWaves.includes('top') ? getLineDistance('top') * 0.01 : 0.01;\n const middleLineDistance = enabledWaves.includes('middle') ? getLineDistance('middle') * 0.01 : 0.01;\n const bottomLineDistance = enabledWaves.includes('bottom') ? getLineDistance('bottom') * 0.01 : 0.01;\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const scene = new Scene();\n\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n camera.position.z = 1;\n\n const renderer = new WebGLRenderer({ antialias: true, alpha: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n containerRef.current.appendChild(renderer.domElement);\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: new Vector3(1, 1, 1) },\n animationSpeed: { value: animationSpeed },\n\n enableTop: { value: enabledWaves.includes('top') },\n enableMiddle: { value: enabledWaves.includes('middle') },\n enableBottom: { value: enabledWaves.includes('bottom') },\n\n topLineCount: { value: topLineCount },\n middleLineCount: { value: middleLineCount },\n bottomLineCount: { value: bottomLineCount },\n\n topLineDistance: { value: topLineDistance },\n middleLineDistance: { value: middleLineDistance },\n bottomLineDistance: { value: bottomLineDistance },\n\n topWavePosition: {\n value: new Vector3(topWavePosition?.x ?? 10.0, topWavePosition?.y ?? 0.5, topWavePosition?.rotate ?? -0.4)\n },\n middleWavePosition: {\n value: new Vector3(\n middleWavePosition?.x ?? 5.0,\n middleWavePosition?.y ?? 0.0,\n middleWavePosition?.rotate ?? 0.2\n )\n },\n bottomWavePosition: {\n value: new Vector3(\n bottomWavePosition?.x ?? 2.0,\n bottomWavePosition?.y ?? -0.7,\n bottomWavePosition?.rotate ?? 0.4\n )\n },\n\n iMouse: { value: new Vector2(-1000, -1000) },\n interactive: { value: interactive },\n bendRadius: { value: bendRadius },\n bendStrength: { value: bendStrength },\n bendInfluence: { value: 0 },\n\n parallax: { value: parallax },\n parallaxStrength: { value: parallaxStrength },\n parallaxOffset: { value: new Vector2(0, 0) },\n\n lineGradient: {\n value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1))\n },\n lineGradientCount: { value: 0 }\n };\n\n if (linesGradient && linesGradient.length > 0) {\n const stops = linesGradient.slice(0, MAX_GRADIENT_STOPS);\n uniforms.lineGradientCount.value = stops.length;\n\n stops.forEach((hex, i) => {\n const color = hexToVec3(hex);\n uniforms.lineGradient.value[i].set(color.x, color.y, color.z);\n });\n }\n\n const material = new ShaderMaterial({\n uniforms,\n vertexShader,\n fragmentShader\n });\n\n const geometry = new PlaneGeometry(2, 2);\n const mesh = new Mesh(geometry, material);\n scene.add(mesh);\n\n const clock = new Clock();\n\n const setSize = () => {\n const el = containerRef.current;\n const width = el.clientWidth || 1;\n const height = el.clientHeight || 1;\n\n renderer.setSize(width, height, false);\n\n const canvasWidth = renderer.domElement.width;\n const canvasHeight = renderer.domElement.height;\n uniforms.iResolution.value.set(canvasWidth, canvasHeight, 1);\n };\n\n setSize();\n\n const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(setSize) : null;\n\n if (ro && containerRef.current) {\n ro.observe(containerRef.current);\n }\n\n const handlePointerMove = event => {\n const rect = renderer.domElement.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n const dpr = renderer.getPixelRatio();\n\n targetMouseRef.current.set(x * dpr, (rect.height - y) * dpr);\n targetInfluenceRef.current = 1.0;\n\n if (parallax) {\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const offsetX = (x - centerX) / rect.width;\n const offsetY = -(y - centerY) / rect.height;\n targetParallaxRef.current.set(offsetX * parallaxStrength, offsetY * parallaxStrength);\n }\n };\n\n const handlePointerLeave = () => {\n targetInfluenceRef.current = 0.0;\n };\n\n if (interactive) {\n renderer.domElement.addEventListener('pointermove', handlePointerMove);\n renderer.domElement.addEventListener('pointerleave', handlePointerLeave);\n }\n\n let raf = 0;\n const renderLoop = () => {\n uniforms.iTime.value = clock.getElapsedTime();\n\n if (interactive) {\n currentMouseRef.current.lerp(targetMouseRef.current, mouseDamping);\n uniforms.iMouse.value.copy(currentMouseRef.current);\n\n currentInfluenceRef.current += (targetInfluenceRef.current - currentInfluenceRef.current) * mouseDamping;\n uniforms.bendInfluence.value = currentInfluenceRef.current;\n }\n\n if (parallax) {\n currentParallaxRef.current.lerp(targetParallaxRef.current, mouseDamping);\n uniforms.parallaxOffset.value.copy(currentParallaxRef.current);\n }\n\n renderer.render(scene, camera);\n raf = requestAnimationFrame(renderLoop);\n };\n renderLoop();\n\n return () => {\n cancelAnimationFrame(raf);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n if (ro && containerRef.current) {\n ro.disconnect();\n }\n\n if (interactive) {\n renderer.domElement.removeEventListener('pointermove', handlePointerMove);\n renderer.domElement.removeEventListener('pointerleave', handlePointerLeave);\n }\n\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n if (renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n linesGradient,\n enabledWaves,\n lineCount,\n lineDistance,\n topWavePosition,\n middleWavePosition,\n bottomWavePosition,\n animationSpeed,\n interactive,\n bendRadius,\n bendStrength,\n mouseDamping,\n parallax,\n parallaxStrength\n ]);\n\n return (\n \n );\n}\n"
+ "content": "import { useEffect, useRef } from 'react';\nimport {\n Clock,\n Mesh,\n OrthographicCamera,\n PlaneGeometry,\n Scene,\n ShaderMaterial,\n Vector2,\n Vector3,\n WebGLRenderer\n} from 'three';\n\nconst vertexShader = `\nprecision highp float;\n\nvoid main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float animationSpeed;\n\nuniform bool enableTop;\nuniform bool enableMiddle;\nuniform bool enableBottom;\n\nuniform int topLineCount;\nuniform int middleLineCount;\nuniform int bottomLineCount;\n\nuniform float topLineDistance;\nuniform float middleLineDistance;\nuniform float bottomLineDistance;\n\nuniform vec3 topWavePosition;\nuniform vec3 middleWavePosition;\nuniform vec3 bottomWavePosition;\n\nuniform vec2 iMouse;\nuniform bool interactive;\nuniform float bendRadius;\nuniform float bendStrength;\nuniform float bendInfluence;\n\nuniform bool parallax;\nuniform float parallaxStrength;\nuniform vec2 parallaxOffset;\n\nuniform vec3 lineGradient[8];\nuniform int lineGradientCount;\n\nconst vec3 BLACK = vec3(0.0);\nconst vec3 PINK = vec3(233.0, 71.0, 245.0) / 255.0;\nconst vec3 BLUE = vec3(47.0, 75.0, 162.0) / 255.0;\n\nmat2 rotate(float r) {\n return mat2(cos(r), sin(r), -sin(r), cos(r));\n}\n\nvec3 background_color(vec2 uv) {\n vec3 col = vec3(0.0);\n\n float y = sin(uv.x - 0.2) * 0.3 - 0.1;\n float m = uv.y - y;\n\n col += mix(BLUE, BLACK, smoothstep(0.0, 1.0, abs(m)));\n col += mix(PINK, BLACK, smoothstep(0.0, 1.0, abs(m - 0.8)));\n return col * 0.5;\n}\n\nvec3 getLineColor(float t, vec3 baseColor) {\n if (lineGradientCount <= 0) {\n return baseColor;\n }\n\n vec3 gradientColor;\n \n if (lineGradientCount == 1) {\n gradientColor = lineGradient[0];\n } else {\n float clampedT = clamp(t, 0.0, 0.9999);\n float scaled = clampedT * float(lineGradientCount - 1);\n int idx = int(floor(scaled));\n float f = fract(scaled);\n int idx2 = min(idx + 1, lineGradientCount - 1);\n\n vec3 c1 = lineGradient[idx];\n vec3 c2 = lineGradient[idx2];\n \n gradientColor = mix(c1, c2, f);\n }\n \n return gradientColor * 0.5;\n}\n\n float wave(vec2 uv, float offset, vec2 screenUv, vec2 mouseUv, bool shouldBend) {\n float time = iTime * animationSpeed;\n\n float x_offset = offset;\n float x_movement = time * 0.1;\n float amp = sin(offset + time * 0.2) * 0.3;\n float y = sin(uv.x + x_offset + x_movement) * amp;\n\n if (shouldBend) {\n vec2 d = screenUv - mouseUv;\n float influence = exp(-dot(d, d) * bendRadius); // radial falloff around cursor\n float bendOffset = (mouseUv.y - screenUv.y) * influence * bendStrength * bendInfluence;\n y += bendOffset;\n }\n\n float m = uv.y - y;\n return 0.0175 / max(abs(m) + 0.01, 1e-3) + 0.01;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n baseUv.y *= -1.0;\n \n if (parallax) {\n baseUv += parallaxOffset;\n }\n\n vec3 col = vec3(0.0);\n\n vec3 b = lineGradientCount > 0 ? vec3(0.0) : background_color(baseUv);\n\n vec2 mouseUv = vec2(0.0);\n if (interactive) {\n mouseUv = (2.0 * iMouse - iResolution.xy) / iResolution.y;\n mouseUv.y *= -1.0;\n }\n \n if (enableBottom) {\n for (int i = 0; i < bottomLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(bottomLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = bottomWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(bottomLineDistance * fi + bottomWavePosition.x, bottomWavePosition.y),\n 1.5 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.2;\n }\n }\n\n if (enableMiddle) {\n for (int i = 0; i < middleLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(middleLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = middleWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(middleLineDistance * fi + middleWavePosition.x, middleWavePosition.y),\n 2.0 + 0.15 * fi,\n baseUv,\n mouseUv,\n interactive\n );\n }\n }\n\n if (enableTop) {\n for (int i = 0; i < topLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(topLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = topWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n ruv.x *= -1.0;\n col += lineCol * wave(\n ruv + vec2(topLineDistance * fi + topWavePosition.x, topWavePosition.y),\n 1.0 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.1;\n }\n }\n\n fragColor = vec4(col, 1.0);\n}\n\nvoid main() {\n vec4 color = vec4(0.0);\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}\n`;\n\nconst MAX_GRADIENT_STOPS = 8;\n\nfunction hexToVec3(hex) {\n let value = hex.trim();\n\n if (value.startsWith('#')) {\n value = value.slice(1);\n }\n\n let r = 255;\n let g = 255;\n let b = 255;\n\n if (value.length === 3) {\n r = parseInt(value[0] + value[0], 16);\n g = parseInt(value[1] + value[1], 16);\n b = parseInt(value[2] + value[2], 16);\n } else if (value.length === 6) {\n r = parseInt(value.slice(0, 2), 16);\n g = parseInt(value.slice(2, 4), 16);\n b = parseInt(value.slice(4, 6), 16);\n }\n\n return new Vector3(r / 255, g / 255, b / 255);\n}\n\nexport default function FloatingLines({\n linesGradient,\n enabledWaves = ['top', 'middle', 'bottom'],\n lineCount = [6],\n lineDistance = [5],\n topWavePosition,\n middleWavePosition,\n bottomWavePosition = { x: 2.0, y: -0.7, rotate: -1 },\n animationSpeed = 1,\n interactive = true,\n bendRadius = 5.0,\n bendStrength = -0.5,\n mouseDamping = 0.05,\n parallax = true,\n parallaxStrength = 0.2,\n mixBlendMode = 'screen'\n}) {\n const containerRef = useRef(null);\n const targetMouseRef = useRef(new Vector2(-1000, -1000));\n const currentMouseRef = useRef(new Vector2(-1000, -1000));\n const targetInfluenceRef = useRef(0);\n const currentInfluenceRef = useRef(0);\n const targetParallaxRef = useRef(new Vector2(0, 0));\n const currentParallaxRef = useRef(new Vector2(0, 0));\n\n const getLineCount = waveType => {\n if (typeof lineCount === 'number') return lineCount;\n if (!enabledWaves.includes(waveType)) return 0;\n const index = enabledWaves.indexOf(waveType);\n return lineCount[index] ?? 6;\n };\n\n const getLineDistance = waveType => {\n if (typeof lineDistance === 'number') return lineDistance;\n if (!enabledWaves.includes(waveType)) return 0.1;\n const index = enabledWaves.indexOf(waveType);\n return lineDistance[index] ?? 0.1;\n };\n\n const topLineCount = enabledWaves.includes('top') ? getLineCount('top') : 0;\n const middleLineCount = enabledWaves.includes('middle') ? getLineCount('middle') : 0;\n const bottomLineCount = enabledWaves.includes('bottom') ? getLineCount('bottom') : 0;\n\n const topLineDistance = enabledWaves.includes('top') ? getLineDistance('top') * 0.01 : 0.01;\n const middleLineDistance = enabledWaves.includes('middle') ? getLineDistance('middle') * 0.01 : 0.01;\n const bottomLineDistance = enabledWaves.includes('bottom') ? getLineDistance('bottom') * 0.01 : 0.01;\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const scene = new Scene();\n\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n camera.position.z = 1;\n\n const renderer = new WebGLRenderer({ antialias: true, alpha: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n containerRef.current.appendChild(renderer.domElement);\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: new Vector3(1, 1, 1) },\n animationSpeed: { value: animationSpeed },\n\n enableTop: { value: enabledWaves.includes('top') },\n enableMiddle: { value: enabledWaves.includes('middle') },\n enableBottom: { value: enabledWaves.includes('bottom') },\n\n topLineCount: { value: topLineCount },\n middleLineCount: { value: middleLineCount },\n bottomLineCount: { value: bottomLineCount },\n\n topLineDistance: { value: topLineDistance },\n middleLineDistance: { value: middleLineDistance },\n bottomLineDistance: { value: bottomLineDistance },\n\n topWavePosition: {\n value: new Vector3(topWavePosition?.x ?? 10.0, topWavePosition?.y ?? 0.5, topWavePosition?.rotate ?? -0.4)\n },\n middleWavePosition: {\n value: new Vector3(\n middleWavePosition?.x ?? 5.0,\n middleWavePosition?.y ?? 0.0,\n middleWavePosition?.rotate ?? 0.2\n )\n },\n bottomWavePosition: {\n value: new Vector3(\n bottomWavePosition?.x ?? 2.0,\n bottomWavePosition?.y ?? -0.7,\n bottomWavePosition?.rotate ?? 0.4\n )\n },\n\n iMouse: { value: new Vector2(-1000, -1000) },\n interactive: { value: interactive },\n bendRadius: { value: bendRadius },\n bendStrength: { value: bendStrength },\n bendInfluence: { value: 0 },\n\n parallax: { value: parallax },\n parallaxStrength: { value: parallaxStrength },\n parallaxOffset: { value: new Vector2(0, 0) },\n\n lineGradient: {\n value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1))\n },\n lineGradientCount: { value: 0 }\n };\n\n if (linesGradient && linesGradient.length > 0) {\n const stops = linesGradient.slice(0, MAX_GRADIENT_STOPS);\n uniforms.lineGradientCount.value = stops.length;\n\n stops.forEach((hex, i) => {\n const color = hexToVec3(hex);\n uniforms.lineGradient.value[i].set(color.x, color.y, color.z);\n });\n }\n\n const material = new ShaderMaterial({\n uniforms,\n vertexShader,\n fragmentShader\n });\n\n const geometry = new PlaneGeometry(2, 2);\n const mesh = new Mesh(geometry, material);\n scene.add(mesh);\n\n const clock = new Clock();\n\n const setSize = () => {\n const el = containerRef.current;\n const width = el.clientWidth || 1;\n const height = el.clientHeight || 1;\n\n renderer.setSize(width, height, false);\n\n const canvasWidth = renderer.domElement.width;\n const canvasHeight = renderer.domElement.height;\n uniforms.iResolution.value.set(canvasWidth, canvasHeight, 1);\n };\n\n setSize();\n\n const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(setSize) : null;\n\n if (ro && containerRef.current) {\n ro.observe(containerRef.current);\n }\n\n const handlePointerMove = event => {\n const rect = renderer.domElement.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n const dpr = renderer.getPixelRatio();\n\n targetMouseRef.current.set(x * dpr, (rect.height - y) * dpr);\n targetInfluenceRef.current = 1.0;\n\n if (parallax) {\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const offsetX = (x - centerX) / rect.width;\n const offsetY = -(y - centerY) / rect.height;\n targetParallaxRef.current.set(offsetX * parallaxStrength, offsetY * parallaxStrength);\n }\n };\n\n const handlePointerLeave = () => {\n targetInfluenceRef.current = 0.0;\n };\n\n if (interactive) {\n renderer.domElement.addEventListener('pointermove', handlePointerMove);\n renderer.domElement.addEventListener('pointerleave', handlePointerLeave);\n }\n\n let raf = 0;\n const renderLoop = () => {\n uniforms.iTime.value = clock.getElapsedTime();\n\n if (interactive) {\n currentMouseRef.current.lerp(targetMouseRef.current, mouseDamping);\n uniforms.iMouse.value.copy(currentMouseRef.current);\n\n currentInfluenceRef.current += (targetInfluenceRef.current - currentInfluenceRef.current) * mouseDamping;\n uniforms.bendInfluence.value = currentInfluenceRef.current;\n }\n\n if (parallax) {\n currentParallaxRef.current.lerp(targetParallaxRef.current, mouseDamping);\n uniforms.parallaxOffset.value.copy(currentParallaxRef.current);\n }\n\n renderer.render(scene, camera);\n raf = requestAnimationFrame(renderLoop);\n };\n renderLoop();\n\n return () => {\n cancelAnimationFrame(raf);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n if (ro && containerRef.current) {\n ro.disconnect();\n }\n\n if (interactive) {\n renderer.domElement.removeEventListener('pointermove', handlePointerMove);\n renderer.domElement.removeEventListener('pointerleave', handlePointerLeave);\n }\n\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n linesGradient,\n enabledWaves,\n lineCount,\n lineDistance,\n topWavePosition,\n middleWavePosition,\n bottomWavePosition,\n animationSpeed,\n interactive,\n bendRadius,\n bendStrength,\n mouseDamping,\n parallax,\n parallaxStrength\n ]);\n\n return (\n \n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/FloatingLines-TS-CSS.json b/public/r/FloatingLines-TS-CSS.json
index b2e64328..f9d82360 100644
--- a/public/r/FloatingLines-TS-CSS.json
+++ b/public/r/FloatingLines-TS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "FloatingLines/FloatingLines.tsx",
- "content": "import { useEffect, useRef } from 'react';\nimport {\n Scene,\n OrthographicCamera,\n WebGLRenderer,\n PlaneGeometry,\n Mesh,\n ShaderMaterial,\n Vector3,\n Vector2,\n Clock\n} from 'three';\n\nimport './FloatingLines.css';\n\nconst vertexShader = `\nprecision highp float;\n\nvoid main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float animationSpeed;\n\nuniform bool enableTop;\nuniform bool enableMiddle;\nuniform bool enableBottom;\n\nuniform int topLineCount;\nuniform int middleLineCount;\nuniform int bottomLineCount;\n\nuniform float topLineDistance;\nuniform float middleLineDistance;\nuniform float bottomLineDistance;\n\nuniform vec3 topWavePosition;\nuniform vec3 middleWavePosition;\nuniform vec3 bottomWavePosition;\n\nuniform vec2 iMouse;\nuniform bool interactive;\nuniform float bendRadius;\nuniform float bendStrength;\nuniform float bendInfluence;\n\nuniform bool parallax;\nuniform float parallaxStrength;\nuniform vec2 parallaxOffset;\n\nuniform vec3 lineGradient[8];\nuniform int lineGradientCount;\n\nconst vec3 BLACK = vec3(0.0);\nconst vec3 PINK = vec3(233.0, 71.0, 245.0) / 255.0;\nconst vec3 BLUE = vec3(47.0, 75.0, 162.0) / 255.0;\n\nmat2 rotate(float r) {\n return mat2(cos(r), sin(r), -sin(r), cos(r));\n}\n\nvec3 background_color(vec2 uv) {\n vec3 col = vec3(0.0);\n\n float y = sin(uv.x - 0.2) * 0.3 - 0.1;\n float m = uv.y - y;\n\n col += mix(BLUE, BLACK, smoothstep(0.0, 1.0, abs(m)));\n col += mix(PINK, BLACK, smoothstep(0.0, 1.0, abs(m - 0.8)));\n return col * 0.5;\n}\n\nvec3 getLineColor(float t, vec3 baseColor) {\n if (lineGradientCount <= 0) {\n return baseColor;\n }\n\n vec3 gradientColor;\n \n if (lineGradientCount == 1) {\n gradientColor = lineGradient[0];\n } else {\n float clampedT = clamp(t, 0.0, 0.9999);\n float scaled = clampedT * float(lineGradientCount - 1);\n int idx = int(floor(scaled));\n float f = fract(scaled);\n int idx2 = min(idx + 1, lineGradientCount - 1);\n\n vec3 c1 = lineGradient[idx];\n vec3 c2 = lineGradient[idx2];\n \n gradientColor = mix(c1, c2, f);\n }\n \n return gradientColor * 0.5;\n}\n\n float wave(vec2 uv, float offset, vec2 screenUv, vec2 mouseUv, bool shouldBend) {\n float time = iTime * animationSpeed;\n\n float x_offset = offset;\n float x_movement = time * 0.1;\n float amp = sin(offset + time * 0.2) * 0.3;\n float y = sin(uv.x + x_offset + x_movement) * amp;\n\n if (shouldBend) {\n vec2 d = screenUv - mouseUv;\n float influence = exp(-dot(d, d) * bendRadius); // radial falloff around cursor\n float bendOffset = (mouseUv.y - screenUv.y) * influence * bendStrength * bendInfluence;\n y += bendOffset;\n }\n\n float m = uv.y - y;\n return 0.0175 / max(abs(m) + 0.01, 1e-3) + 0.01;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n baseUv.y *= -1.0;\n \n if (parallax) {\n baseUv += parallaxOffset;\n }\n\n vec3 col = vec3(0.0);\n\n vec3 b = lineGradientCount > 0 ? vec3(0.0) : background_color(baseUv);\n\n vec2 mouseUv = vec2(0.0);\n if (interactive) {\n mouseUv = (2.0 * iMouse - iResolution.xy) / iResolution.y;\n mouseUv.y *= -1.0;\n }\n \n if (enableBottom) {\n for (int i = 0; i < bottomLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(bottomLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = bottomWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(bottomLineDistance * fi + bottomWavePosition.x, bottomWavePosition.y),\n 1.5 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.2;\n }\n }\n\n if (enableMiddle) {\n for (int i = 0; i < middleLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(middleLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = middleWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(middleLineDistance * fi + middleWavePosition.x, middleWavePosition.y),\n 2.0 + 0.15 * fi,\n baseUv,\n mouseUv,\n interactive\n );\n }\n }\n\n if (enableTop) {\n for (int i = 0; i < topLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(topLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = topWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n ruv.x *= -1.0;\n col += lineCol * wave(\n ruv + vec2(topLineDistance * fi + topWavePosition.x, topWavePosition.y),\n 1.0 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.1;\n }\n }\n\n fragColor = vec4(col, 1.0);\n}\n\nvoid main() {\n vec4 color = vec4(0.0);\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}\n`;\n\nconst MAX_GRADIENT_STOPS = 8;\n\ntype WavePosition = {\n x: number;\n y: number;\n rotate: number;\n};\n\ntype FloatingLinesProps = {\n linesGradient?: string[];\n enabledWaves?: Array<'top' | 'middle' | 'bottom'>;\n lineCount?: number | number[];\n lineDistance?: number | number[];\n topWavePosition?: WavePosition;\n middleWavePosition?: WavePosition;\n bottomWavePosition?: WavePosition;\n animationSpeed?: number;\n interactive?: boolean;\n bendRadius?: number;\n bendStrength?: number;\n mouseDamping?: number;\n parallax?: boolean;\n parallaxStrength?: number;\n mixBlendMode?: React.CSSProperties['mixBlendMode'];\n};\n\nfunction hexToVec3(hex: string): Vector3 {\n let value = hex.trim();\n\n if (value.startsWith('#')) {\n value = value.slice(1);\n }\n\n let r = 255;\n let g = 255;\n let b = 255;\n\n if (value.length === 3) {\n r = parseInt(value[0] + value[0], 16);\n g = parseInt(value[1] + value[1], 16);\n b = parseInt(value[2] + value[2], 16);\n } else if (value.length === 6) {\n r = parseInt(value.slice(0, 2), 16);\n g = parseInt(value.slice(2, 4), 16);\n b = parseInt(value.slice(4, 6), 16);\n }\n\n return new Vector3(r / 255, g / 255, b / 255);\n}\n\nexport default function FloatingLines({\n linesGradient,\n enabledWaves = ['top', 'middle', 'bottom'],\n lineCount = [6],\n lineDistance = [5],\n topWavePosition,\n middleWavePosition,\n bottomWavePosition = { x: 2.0, y: -0.7, rotate: -1 },\n animationSpeed = 1,\n interactive = true,\n bendRadius = 5.0,\n bendStrength = -0.5,\n mouseDamping = 0.05,\n parallax = true,\n parallaxStrength = 0.2,\n mixBlendMode = 'screen'\n}: FloatingLinesProps) {\n const containerRef = useRef(null);\n const targetMouseRef = useRef(new Vector2(-1000, -1000));\n const currentMouseRef = useRef(new Vector2(-1000, -1000));\n const targetInfluenceRef = useRef(0);\n const currentInfluenceRef = useRef(0);\n const targetParallaxRef = useRef(new Vector2(0, 0));\n const currentParallaxRef = useRef(new Vector2(0, 0));\n\n const getLineCount = (waveType: 'top' | 'middle' | 'bottom'): number => {\n if (typeof lineCount === 'number') return lineCount;\n if (!enabledWaves.includes(waveType)) return 0;\n const index = enabledWaves.indexOf(waveType);\n return lineCount[index] ?? 6;\n };\n\n const getLineDistance = (waveType: 'top' | 'middle' | 'bottom'): number => {\n if (typeof lineDistance === 'number') return lineDistance;\n if (!enabledWaves.includes(waveType)) return 0.1;\n const index = enabledWaves.indexOf(waveType);\n return lineDistance[index] ?? 0.1;\n };\n\n const topLineCount = enabledWaves.includes('top') ? getLineCount('top') : 0;\n const middleLineCount = enabledWaves.includes('middle') ? getLineCount('middle') : 0;\n const bottomLineCount = enabledWaves.includes('bottom') ? getLineCount('bottom') : 0;\n\n const topLineDistance = enabledWaves.includes('top') ? getLineDistance('top') * 0.01 : 0.01;\n const middleLineDistance = enabledWaves.includes('middle') ? getLineDistance('middle') * 0.01 : 0.01;\n const bottomLineDistance = enabledWaves.includes('bottom') ? getLineDistance('bottom') * 0.01 : 0.01;\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const scene = new Scene();\n\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n camera.position.z = 1;\n\n const renderer = new WebGLRenderer({ antialias: true, alpha: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n containerRef.current.appendChild(renderer.domElement);\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: new Vector3(1, 1, 1) },\n animationSpeed: { value: animationSpeed },\n\n enableTop: { value: enabledWaves.includes('top') },\n enableMiddle: { value: enabledWaves.includes('middle') },\n enableBottom: { value: enabledWaves.includes('bottom') },\n\n topLineCount: { value: topLineCount },\n middleLineCount: { value: middleLineCount },\n bottomLineCount: { value: bottomLineCount },\n\n topLineDistance: { value: topLineDistance },\n middleLineDistance: { value: middleLineDistance },\n bottomLineDistance: { value: bottomLineDistance },\n\n topWavePosition: {\n value: new Vector3(topWavePosition?.x ?? 10.0, topWavePosition?.y ?? 0.5, topWavePosition?.rotate ?? -0.4)\n },\n middleWavePosition: {\n value: new Vector3(\n middleWavePosition?.x ?? 5.0,\n middleWavePosition?.y ?? 0.0,\n middleWavePosition?.rotate ?? 0.2\n )\n },\n bottomWavePosition: {\n value: new Vector3(\n bottomWavePosition?.x ?? 2.0,\n bottomWavePosition?.y ?? -0.7,\n bottomWavePosition?.rotate ?? 0.4\n )\n },\n\n iMouse: { value: new Vector2(-1000, -1000) },\n interactive: { value: interactive },\n bendRadius: { value: bendRadius },\n bendStrength: { value: bendStrength },\n bendInfluence: { value: 0 },\n\n parallax: { value: parallax },\n parallaxStrength: { value: parallaxStrength },\n parallaxOffset: { value: new Vector2(0, 0) },\n\n lineGradient: {\n value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1))\n },\n lineGradientCount: { value: 0 }\n };\n\n if (linesGradient && linesGradient.length > 0) {\n const stops = linesGradient.slice(0, MAX_GRADIENT_STOPS);\n uniforms.lineGradientCount.value = stops.length;\n\n stops.forEach((hex, i) => {\n const color = hexToVec3(hex);\n uniforms.lineGradient.value[i].set(color.x, color.y, color.z);\n });\n }\n\n const material = new ShaderMaterial({\n uniforms,\n vertexShader,\n fragmentShader\n });\n\n const geometry = new PlaneGeometry(2, 2);\n const mesh = new Mesh(geometry, material);\n scene.add(mesh);\n\n const clock = new Clock();\n\n const setSize = () => {\n const el = containerRef.current!;\n const width = el.clientWidth || 1;\n const height = el.clientHeight || 1;\n\n renderer.setSize(width, height, false);\n\n const canvasWidth = renderer.domElement.width;\n const canvasHeight = renderer.domElement.height;\n uniforms.iResolution.value.set(canvasWidth, canvasHeight, 1);\n };\n\n setSize();\n\n const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(setSize) : null;\n\n if (ro && containerRef.current) {\n ro.observe(containerRef.current);\n }\n\n const handlePointerMove = (event: PointerEvent) => {\n const rect = renderer.domElement.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n const dpr = renderer.getPixelRatio();\n\n targetMouseRef.current.set(x * dpr, (rect.height - y) * dpr);\n targetInfluenceRef.current = 1.0;\n\n if (parallax) {\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const offsetX = (x - centerX) / rect.width;\n const offsetY = -(y - centerY) / rect.height;\n targetParallaxRef.current.set(offsetX * parallaxStrength, offsetY * parallaxStrength);\n }\n };\n\n const handlePointerLeave = () => {\n targetInfluenceRef.current = 0.0;\n };\n\n if (interactive) {\n renderer.domElement.addEventListener('pointermove', handlePointerMove);\n renderer.domElement.addEventListener('pointerleave', handlePointerLeave);\n }\n\n let raf = 0;\n const renderLoop = () => {\n uniforms.iTime.value = clock.getElapsedTime();\n\n if (interactive) {\n currentMouseRef.current.lerp(targetMouseRef.current, mouseDamping);\n uniforms.iMouse.value.copy(currentMouseRef.current);\n\n currentInfluenceRef.current += (targetInfluenceRef.current - currentInfluenceRef.current) * mouseDamping;\n uniforms.bendInfluence.value = currentInfluenceRef.current;\n }\n\n if (parallax) {\n currentParallaxRef.current.lerp(targetParallaxRef.current, mouseDamping);\n uniforms.parallaxOffset.value.copy(currentParallaxRef.current);\n }\n\n renderer.render(scene, camera);\n raf = requestAnimationFrame(renderLoop);\n };\n renderLoop();\n\n return () => {\n cancelAnimationFrame(raf);\n if (ro && containerRef.current) {\n ro.disconnect();\n }\n\n if (interactive) {\n renderer.domElement.removeEventListener('pointermove', handlePointerMove);\n renderer.domElement.removeEventListener('pointerleave', handlePointerLeave);\n }\n\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n if (renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n };\n }, [\n linesGradient,\n enabledWaves,\n lineCount,\n lineDistance,\n topWavePosition,\n middleWavePosition,\n bottomWavePosition,\n animationSpeed,\n interactive,\n bendRadius,\n bendStrength,\n mouseDamping,\n parallax,\n parallaxStrength\n ]);\n\n return (\n \n );\n}\n"
+ "content": "import { useEffect, useRef } from 'react';\nimport {\n Clock,\n Mesh,\n OrthographicCamera,\n PlaneGeometry,\n Scene,\n ShaderMaterial,\n Vector2,\n Vector3,\n WebGLRenderer\n} from 'three';\n\nimport './FloatingLines.css';\n\nconst vertexShader = `\nprecision highp float;\n\nvoid main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float animationSpeed;\n\nuniform bool enableTop;\nuniform bool enableMiddle;\nuniform bool enableBottom;\n\nuniform int topLineCount;\nuniform int middleLineCount;\nuniform int bottomLineCount;\n\nuniform float topLineDistance;\nuniform float middleLineDistance;\nuniform float bottomLineDistance;\n\nuniform vec3 topWavePosition;\nuniform vec3 middleWavePosition;\nuniform vec3 bottomWavePosition;\n\nuniform vec2 iMouse;\nuniform bool interactive;\nuniform float bendRadius;\nuniform float bendStrength;\nuniform float bendInfluence;\n\nuniform bool parallax;\nuniform float parallaxStrength;\nuniform vec2 parallaxOffset;\n\nuniform vec3 lineGradient[8];\nuniform int lineGradientCount;\n\nconst vec3 BLACK = vec3(0.0);\nconst vec3 PINK = vec3(233.0, 71.0, 245.0) / 255.0;\nconst vec3 BLUE = vec3(47.0, 75.0, 162.0) / 255.0;\n\nmat2 rotate(float r) {\n return mat2(cos(r), sin(r), -sin(r), cos(r));\n}\n\nvec3 background_color(vec2 uv) {\n vec3 col = vec3(0.0);\n\n float y = sin(uv.x - 0.2) * 0.3 - 0.1;\n float m = uv.y - y;\n\n col += mix(BLUE, BLACK, smoothstep(0.0, 1.0, abs(m)));\n col += mix(PINK, BLACK, smoothstep(0.0, 1.0, abs(m - 0.8)));\n return col * 0.5;\n}\n\nvec3 getLineColor(float t, vec3 baseColor) {\n if (lineGradientCount <= 0) {\n return baseColor;\n }\n\n vec3 gradientColor;\n \n if (lineGradientCount == 1) {\n gradientColor = lineGradient[0];\n } else {\n float clampedT = clamp(t, 0.0, 0.9999);\n float scaled = clampedT * float(lineGradientCount - 1);\n int idx = int(floor(scaled));\n float f = fract(scaled);\n int idx2 = min(idx + 1, lineGradientCount - 1);\n\n vec3 c1 = lineGradient[idx];\n vec3 c2 = lineGradient[idx2];\n \n gradientColor = mix(c1, c2, f);\n }\n \n return gradientColor * 0.5;\n}\n\n float wave(vec2 uv, float offset, vec2 screenUv, vec2 mouseUv, bool shouldBend) {\n float time = iTime * animationSpeed;\n\n float x_offset = offset;\n float x_movement = time * 0.1;\n float amp = sin(offset + time * 0.2) * 0.3;\n float y = sin(uv.x + x_offset + x_movement) * amp;\n\n if (shouldBend) {\n vec2 d = screenUv - mouseUv;\n float influence = exp(-dot(d, d) * bendRadius); // radial falloff around cursor\n float bendOffset = (mouseUv.y - screenUv.y) * influence * bendStrength * bendInfluence;\n y += bendOffset;\n }\n\n float m = uv.y - y;\n return 0.0175 / max(abs(m) + 0.01, 1e-3) + 0.01;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n baseUv.y *= -1.0;\n \n if (parallax) {\n baseUv += parallaxOffset;\n }\n\n vec3 col = vec3(0.0);\n\n vec3 b = lineGradientCount > 0 ? vec3(0.0) : background_color(baseUv);\n\n vec2 mouseUv = vec2(0.0);\n if (interactive) {\n mouseUv = (2.0 * iMouse - iResolution.xy) / iResolution.y;\n mouseUv.y *= -1.0;\n }\n \n if (enableBottom) {\n for (int i = 0; i < bottomLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(bottomLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = bottomWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(bottomLineDistance * fi + bottomWavePosition.x, bottomWavePosition.y),\n 1.5 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.2;\n }\n }\n\n if (enableMiddle) {\n for (int i = 0; i < middleLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(middleLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = middleWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(middleLineDistance * fi + middleWavePosition.x, middleWavePosition.y),\n 2.0 + 0.15 * fi,\n baseUv,\n mouseUv,\n interactive\n );\n }\n }\n\n if (enableTop) {\n for (int i = 0; i < topLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(topLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = topWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n ruv.x *= -1.0;\n col += lineCol * wave(\n ruv + vec2(topLineDistance * fi + topWavePosition.x, topWavePosition.y),\n 1.0 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.1;\n }\n }\n\n fragColor = vec4(col, 1.0);\n}\n\nvoid main() {\n vec4 color = vec4(0.0);\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}\n`;\n\nconst MAX_GRADIENT_STOPS = 8;\n\ntype WavePosition = {\n x: number;\n y: number;\n rotate: number;\n};\n\ntype FloatingLinesProps = {\n linesGradient?: string[];\n enabledWaves?: Array<'top' | 'middle' | 'bottom'>;\n lineCount?: number | number[];\n lineDistance?: number | number[];\n topWavePosition?: WavePosition;\n middleWavePosition?: WavePosition;\n bottomWavePosition?: WavePosition;\n animationSpeed?: number;\n interactive?: boolean;\n bendRadius?: number;\n bendStrength?: number;\n mouseDamping?: number;\n parallax?: boolean;\n parallaxStrength?: number;\n mixBlendMode?: React.CSSProperties['mixBlendMode'];\n};\n\nfunction hexToVec3(hex: string): Vector3 {\n let value = hex.trim();\n\n if (value.startsWith('#')) {\n value = value.slice(1);\n }\n\n let r = 255;\n let g = 255;\n let b = 255;\n\n if (value.length === 3) {\n r = parseInt(value[0] + value[0], 16);\n g = parseInt(value[1] + value[1], 16);\n b = parseInt(value[2] + value[2], 16);\n } else if (value.length === 6) {\n r = parseInt(value.slice(0, 2), 16);\n g = parseInt(value.slice(2, 4), 16);\n b = parseInt(value.slice(4, 6), 16);\n }\n\n return new Vector3(r / 255, g / 255, b / 255);\n}\n\nexport default function FloatingLines({\n linesGradient,\n enabledWaves = ['top', 'middle', 'bottom'],\n lineCount = [6],\n lineDistance = [5],\n topWavePosition,\n middleWavePosition,\n bottomWavePosition = { x: 2.0, y: -0.7, rotate: -1 },\n animationSpeed = 1,\n interactive = true,\n bendRadius = 5.0,\n bendStrength = -0.5,\n mouseDamping = 0.05,\n parallax = true,\n parallaxStrength = 0.2,\n mixBlendMode = 'screen'\n}: FloatingLinesProps) {\n const containerRef = useRef(null);\n const targetMouseRef = useRef(new Vector2(-1000, -1000));\n const currentMouseRef = useRef(new Vector2(-1000, -1000));\n const targetInfluenceRef = useRef(0);\n const currentInfluenceRef = useRef(0);\n const targetParallaxRef = useRef(new Vector2(0, 0));\n const currentParallaxRef = useRef(new Vector2(0, 0));\n\n const getLineCount = (waveType: 'top' | 'middle' | 'bottom'): number => {\n if (typeof lineCount === 'number') return lineCount;\n if (!enabledWaves.includes(waveType)) return 0;\n const index = enabledWaves.indexOf(waveType);\n return lineCount[index] ?? 6;\n };\n\n const getLineDistance = (waveType: 'top' | 'middle' | 'bottom'): number => {\n if (typeof lineDistance === 'number') return lineDistance;\n if (!enabledWaves.includes(waveType)) return 0.1;\n const index = enabledWaves.indexOf(waveType);\n return lineDistance[index] ?? 0.1;\n };\n\n const topLineCount = enabledWaves.includes('top') ? getLineCount('top') : 0;\n const middleLineCount = enabledWaves.includes('middle') ? getLineCount('middle') : 0;\n const bottomLineCount = enabledWaves.includes('bottom') ? getLineCount('bottom') : 0;\n\n const topLineDistance = enabledWaves.includes('top') ? getLineDistance('top') * 0.01 : 0.01;\n const middleLineDistance = enabledWaves.includes('middle') ? getLineDistance('middle') * 0.01 : 0.01;\n const bottomLineDistance = enabledWaves.includes('bottom') ? getLineDistance('bottom') * 0.01 : 0.01;\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const scene = new Scene();\n\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n camera.position.z = 1;\n\n const renderer = new WebGLRenderer({ antialias: true, alpha: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n containerRef.current.appendChild(renderer.domElement);\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: new Vector3(1, 1, 1) },\n animationSpeed: { value: animationSpeed },\n\n enableTop: { value: enabledWaves.includes('top') },\n enableMiddle: { value: enabledWaves.includes('middle') },\n enableBottom: { value: enabledWaves.includes('bottom') },\n\n topLineCount: { value: topLineCount },\n middleLineCount: { value: middleLineCount },\n bottomLineCount: { value: bottomLineCount },\n\n topLineDistance: { value: topLineDistance },\n middleLineDistance: { value: middleLineDistance },\n bottomLineDistance: { value: bottomLineDistance },\n\n topWavePosition: {\n value: new Vector3(topWavePosition?.x ?? 10.0, topWavePosition?.y ?? 0.5, topWavePosition?.rotate ?? -0.4)\n },\n middleWavePosition: {\n value: new Vector3(\n middleWavePosition?.x ?? 5.0,\n middleWavePosition?.y ?? 0.0,\n middleWavePosition?.rotate ?? 0.2\n )\n },\n bottomWavePosition: {\n value: new Vector3(\n bottomWavePosition?.x ?? 2.0,\n bottomWavePosition?.y ?? -0.7,\n bottomWavePosition?.rotate ?? 0.4\n )\n },\n\n iMouse: { value: new Vector2(-1000, -1000) },\n interactive: { value: interactive },\n bendRadius: { value: bendRadius },\n bendStrength: { value: bendStrength },\n bendInfluence: { value: 0 },\n\n parallax: { value: parallax },\n parallaxStrength: { value: parallaxStrength },\n parallaxOffset: { value: new Vector2(0, 0) },\n\n lineGradient: {\n value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1))\n },\n lineGradientCount: { value: 0 }\n };\n\n if (linesGradient && linesGradient.length > 0) {\n const stops = linesGradient.slice(0, MAX_GRADIENT_STOPS);\n uniforms.lineGradientCount.value = stops.length;\n\n stops.forEach((hex, i) => {\n const color = hexToVec3(hex);\n uniforms.lineGradient.value[i].set(color.x, color.y, color.z);\n });\n }\n\n const material = new ShaderMaterial({\n uniforms,\n vertexShader,\n fragmentShader\n });\n\n const geometry = new PlaneGeometry(2, 2);\n const mesh = new Mesh(geometry, material);\n scene.add(mesh);\n\n const clock = new Clock();\n\n const setSize = () => {\n const el = containerRef.current!;\n const width = el.clientWidth || 1;\n const height = el.clientHeight || 1;\n\n renderer.setSize(width, height, false);\n\n const canvasWidth = renderer.domElement.width;\n const canvasHeight = renderer.domElement.height;\n uniforms.iResolution.value.set(canvasWidth, canvasHeight, 1);\n };\n\n setSize();\n\n const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(setSize) : null;\n\n if (ro && containerRef.current) {\n ro.observe(containerRef.current);\n }\n\n const handlePointerMove = (event: PointerEvent) => {\n const rect = renderer.domElement.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n const dpr = renderer.getPixelRatio();\n\n targetMouseRef.current.set(x * dpr, (rect.height - y) * dpr);\n targetInfluenceRef.current = 1.0;\n\n if (parallax) {\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const offsetX = (x - centerX) / rect.width;\n const offsetY = -(y - centerY) / rect.height;\n targetParallaxRef.current.set(offsetX * parallaxStrength, offsetY * parallaxStrength);\n }\n };\n\n const handlePointerLeave = () => {\n targetInfluenceRef.current = 0.0;\n };\n\n if (interactive) {\n renderer.domElement.addEventListener('pointermove', handlePointerMove);\n renderer.domElement.addEventListener('pointerleave', handlePointerLeave);\n }\n\n let raf = 0;\n const renderLoop = () => {\n uniforms.iTime.value = clock.getElapsedTime();\n\n if (interactive) {\n currentMouseRef.current.lerp(targetMouseRef.current, mouseDamping);\n uniforms.iMouse.value.copy(currentMouseRef.current);\n\n currentInfluenceRef.current += (targetInfluenceRef.current - currentInfluenceRef.current) * mouseDamping;\n uniforms.bendInfluence.value = currentInfluenceRef.current;\n }\n\n if (parallax) {\n currentParallaxRef.current.lerp(targetParallaxRef.current, mouseDamping);\n uniforms.parallaxOffset.value.copy(currentParallaxRef.current);\n }\n\n renderer.render(scene, camera);\n raf = requestAnimationFrame(renderLoop);\n };\n renderLoop();\n\n return () => {\n cancelAnimationFrame(raf);\n if (ro && containerRef.current) {\n ro.disconnect();\n }\n\n if (interactive) {\n renderer.domElement.removeEventListener('pointermove', handlePointerMove);\n renderer.domElement.removeEventListener('pointerleave', handlePointerLeave);\n }\n\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n };\n }, [\n linesGradient,\n enabledWaves,\n lineCount,\n lineDistance,\n topWavePosition,\n middleWavePosition,\n bottomWavePosition,\n animationSpeed,\n interactive,\n bendRadius,\n bendStrength,\n mouseDamping,\n parallax,\n parallaxStrength\n ]);\n\n return (\n \n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/FloatingLines-TS-TW.json b/public/r/FloatingLines-TS-TW.json
index 7b41fe06..05700a8d 100644
--- a/public/r/FloatingLines-TS-TW.json
+++ b/public/r/FloatingLines-TS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "FloatingLines/FloatingLines.tsx",
- "content": "import { useEffect, useRef } from 'react';\nimport {\n Scene,\n OrthographicCamera,\n WebGLRenderer,\n PlaneGeometry,\n Mesh,\n ShaderMaterial,\n Vector3,\n Vector2,\n Clock\n} from 'three';\n\nconst vertexShader = `\nprecision highp float;\n\nvoid main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float animationSpeed;\n\nuniform bool enableTop;\nuniform bool enableMiddle;\nuniform bool enableBottom;\n\nuniform int topLineCount;\nuniform int middleLineCount;\nuniform int bottomLineCount;\n\nuniform float topLineDistance;\nuniform float middleLineDistance;\nuniform float bottomLineDistance;\n\nuniform vec3 topWavePosition;\nuniform vec3 middleWavePosition;\nuniform vec3 bottomWavePosition;\n\nuniform vec2 iMouse;\nuniform bool interactive;\nuniform float bendRadius;\nuniform float bendStrength;\nuniform float bendInfluence;\n\nuniform bool parallax;\nuniform float parallaxStrength;\nuniform vec2 parallaxOffset;\n\nuniform vec3 lineGradient[8];\nuniform int lineGradientCount;\n\nconst vec3 BLACK = vec3(0.0);\nconst vec3 PINK = vec3(233.0, 71.0, 245.0) / 255.0;\nconst vec3 BLUE = vec3(47.0, 75.0, 162.0) / 255.0;\n\nmat2 rotate(float r) {\n return mat2(cos(r), sin(r), -sin(r), cos(r));\n}\n\nvec3 background_color(vec2 uv) {\n vec3 col = vec3(0.0);\n\n float y = sin(uv.x - 0.2) * 0.3 - 0.1;\n float m = uv.y - y;\n\n col += mix(BLUE, BLACK, smoothstep(0.0, 1.0, abs(m)));\n col += mix(PINK, BLACK, smoothstep(0.0, 1.0, abs(m - 0.8)));\n return col * 0.5;\n}\n\nvec3 getLineColor(float t, vec3 baseColor) {\n if (lineGradientCount <= 0) {\n return baseColor;\n }\n\n vec3 gradientColor;\n \n if (lineGradientCount == 1) {\n gradientColor = lineGradient[0];\n } else {\n float clampedT = clamp(t, 0.0, 0.9999);\n float scaled = clampedT * float(lineGradientCount - 1);\n int idx = int(floor(scaled));\n float f = fract(scaled);\n int idx2 = min(idx + 1, lineGradientCount - 1);\n\n vec3 c1 = lineGradient[idx];\n vec3 c2 = lineGradient[idx2];\n \n gradientColor = mix(c1, c2, f);\n }\n \n return gradientColor * 0.5;\n}\n\n float wave(vec2 uv, float offset, vec2 screenUv, vec2 mouseUv, bool shouldBend) {\n float time = iTime * animationSpeed;\n\n float x_offset = offset;\n float x_movement = time * 0.1;\n float amp = sin(offset + time * 0.2) * 0.3;\n float y = sin(uv.x + x_offset + x_movement) * amp;\n\n if (shouldBend) {\n vec2 d = screenUv - mouseUv;\n float influence = exp(-dot(d, d) * bendRadius); // radial falloff around cursor\n float bendOffset = (mouseUv.y - screenUv.y) * influence * bendStrength * bendInfluence;\n y += bendOffset;\n }\n\n float m = uv.y - y;\n return 0.0175 / max(abs(m) + 0.01, 1e-3) + 0.01;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n baseUv.y *= -1.0;\n \n if (parallax) {\n baseUv += parallaxOffset;\n }\n\n vec3 col = vec3(0.0);\n\n vec3 b = lineGradientCount > 0 ? vec3(0.0) : background_color(baseUv);\n\n vec2 mouseUv = vec2(0.0);\n if (interactive) {\n mouseUv = (2.0 * iMouse - iResolution.xy) / iResolution.y;\n mouseUv.y *= -1.0;\n }\n \n if (enableBottom) {\n for (int i = 0; i < bottomLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(bottomLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = bottomWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(bottomLineDistance * fi + bottomWavePosition.x, bottomWavePosition.y),\n 1.5 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.2;\n }\n }\n\n if (enableMiddle) {\n for (int i = 0; i < middleLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(middleLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = middleWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(middleLineDistance * fi + middleWavePosition.x, middleWavePosition.y),\n 2.0 + 0.15 * fi,\n baseUv,\n mouseUv,\n interactive\n );\n }\n }\n\n if (enableTop) {\n for (int i = 0; i < topLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(topLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = topWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n ruv.x *= -1.0;\n col += lineCol * wave(\n ruv + vec2(topLineDistance * fi + topWavePosition.x, topWavePosition.y),\n 1.0 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.1;\n }\n }\n\n fragColor = vec4(col, 1.0);\n}\n\nvoid main() {\n vec4 color = vec4(0.0);\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}\n`;\n\nconst MAX_GRADIENT_STOPS = 8;\n\ntype WavePosition = {\n x: number;\n y: number;\n rotate: number;\n};\n\ntype FloatingLinesProps = {\n linesGradient?: string[];\n enabledWaves?: Array<'top' | 'middle' | 'bottom'>;\n lineCount?: number | number[];\n lineDistance?: number | number[];\n topWavePosition?: WavePosition;\n middleWavePosition?: WavePosition;\n bottomWavePosition?: WavePosition;\n animationSpeed?: number;\n interactive?: boolean;\n bendRadius?: number;\n bendStrength?: number;\n mouseDamping?: number;\n parallax?: boolean;\n parallaxStrength?: number;\n mixBlendMode?: React.CSSProperties['mixBlendMode'];\n};\n\nfunction hexToVec3(hex: string): Vector3 {\n let value = hex.trim();\n\n if (value.startsWith('#')) {\n value = value.slice(1);\n }\n\n let r = 255;\n let g = 255;\n let b = 255;\n\n if (value.length === 3) {\n r = parseInt(value[0] + value[0], 16);\n g = parseInt(value[1] + value[1], 16);\n b = parseInt(value[2] + value[2], 16);\n } else if (value.length === 6) {\n r = parseInt(value.slice(0, 2), 16);\n g = parseInt(value.slice(2, 4), 16);\n b = parseInt(value.slice(4, 6), 16);\n }\n\n return new Vector3(r / 255, g / 255, b / 255);\n}\n\nexport default function FloatingLines({\n linesGradient,\n enabledWaves = ['top', 'middle', 'bottom'],\n lineCount = [6],\n lineDistance = [5],\n topWavePosition,\n middleWavePosition,\n bottomWavePosition = { x: 2.0, y: -0.7, rotate: -1 },\n animationSpeed = 1,\n interactive = true,\n bendRadius = 5.0,\n bendStrength = -0.5,\n mouseDamping = 0.05,\n parallax = true,\n parallaxStrength = 0.2,\n mixBlendMode = 'screen'\n}: FloatingLinesProps) {\n const containerRef = useRef(null);\n const targetMouseRef = useRef(new Vector2(-1000, -1000));\n const currentMouseRef = useRef(new Vector2(-1000, -1000));\n const targetInfluenceRef = useRef(0);\n const currentInfluenceRef = useRef(0);\n const targetParallaxRef = useRef(new Vector2(0, 0));\n const currentParallaxRef = useRef(new Vector2(0, 0));\n\n const getLineCount = (waveType: 'top' | 'middle' | 'bottom'): number => {\n if (typeof lineCount === 'number') return lineCount;\n if (!enabledWaves.includes(waveType)) return 0;\n const index = enabledWaves.indexOf(waveType);\n return lineCount[index] ?? 6;\n };\n\n const getLineDistance = (waveType: 'top' | 'middle' | 'bottom'): number => {\n if (typeof lineDistance === 'number') return lineDistance;\n if (!enabledWaves.includes(waveType)) return 0.1;\n const index = enabledWaves.indexOf(waveType);\n return lineDistance[index] ?? 0.1;\n };\n\n const topLineCount = enabledWaves.includes('top') ? getLineCount('top') : 0;\n const middleLineCount = enabledWaves.includes('middle') ? getLineCount('middle') : 0;\n const bottomLineCount = enabledWaves.includes('bottom') ? getLineCount('bottom') : 0;\n\n const topLineDistance = enabledWaves.includes('top') ? getLineDistance('top') * 0.01 : 0.01;\n const middleLineDistance = enabledWaves.includes('middle') ? getLineDistance('middle') * 0.01 : 0.01;\n const bottomLineDistance = enabledWaves.includes('bottom') ? getLineDistance('bottom') * 0.01 : 0.01;\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const scene = new Scene();\n\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n camera.position.z = 1;\n\n const renderer = new WebGLRenderer({ antialias: true, alpha: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n containerRef.current.appendChild(renderer.domElement);\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: new Vector3(1, 1, 1) },\n animationSpeed: { value: animationSpeed },\n\n enableTop: { value: enabledWaves.includes('top') },\n enableMiddle: { value: enabledWaves.includes('middle') },\n enableBottom: { value: enabledWaves.includes('bottom') },\n\n topLineCount: { value: topLineCount },\n middleLineCount: { value: middleLineCount },\n bottomLineCount: { value: bottomLineCount },\n\n topLineDistance: { value: topLineDistance },\n middleLineDistance: { value: middleLineDistance },\n bottomLineDistance: { value: bottomLineDistance },\n\n topWavePosition: {\n value: new Vector3(topWavePosition?.x ?? 10.0, topWavePosition?.y ?? 0.5, topWavePosition?.rotate ?? -0.4)\n },\n middleWavePosition: {\n value: new Vector3(\n middleWavePosition?.x ?? 5.0,\n middleWavePosition?.y ?? 0.0,\n middleWavePosition?.rotate ?? 0.2\n )\n },\n bottomWavePosition: {\n value: new Vector3(\n bottomWavePosition?.x ?? 2.0,\n bottomWavePosition?.y ?? -0.7,\n bottomWavePosition?.rotate ?? 0.4\n )\n },\n\n iMouse: { value: new Vector2(-1000, -1000) },\n interactive: { value: interactive },\n bendRadius: { value: bendRadius },\n bendStrength: { value: bendStrength },\n bendInfluence: { value: 0 },\n\n parallax: { value: parallax },\n parallaxStrength: { value: parallaxStrength },\n parallaxOffset: { value: new Vector2(0, 0) },\n\n lineGradient: {\n value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1))\n },\n lineGradientCount: { value: 0 }\n };\n\n if (linesGradient && linesGradient.length > 0) {\n const stops = linesGradient.slice(0, MAX_GRADIENT_STOPS);\n uniforms.lineGradientCount.value = stops.length;\n\n stops.forEach((hex, i) => {\n const color = hexToVec3(hex);\n uniforms.lineGradient.value[i].set(color.x, color.y, color.z);\n });\n }\n\n const material = new ShaderMaterial({\n uniforms,\n vertexShader,\n fragmentShader\n });\n\n const geometry = new PlaneGeometry(2, 2);\n const mesh = new Mesh(geometry, material);\n scene.add(mesh);\n\n const clock = new Clock();\n\n const setSize = () => {\n const el = containerRef.current!;\n const width = el.clientWidth || 1;\n const height = el.clientHeight || 1;\n\n renderer.setSize(width, height, false);\n\n const canvasWidth = renderer.domElement.width;\n const canvasHeight = renderer.domElement.height;\n uniforms.iResolution.value.set(canvasWidth, canvasHeight, 1);\n };\n\n setSize();\n\n const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(setSize) : null;\n\n if (ro && containerRef.current) {\n ro.observe(containerRef.current);\n }\n\n const handlePointerMove = (event: PointerEvent) => {\n const rect = renderer.domElement.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n const dpr = renderer.getPixelRatio();\n\n targetMouseRef.current.set(x * dpr, (rect.height - y) * dpr);\n targetInfluenceRef.current = 1.0;\n\n if (parallax) {\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const offsetX = (x - centerX) / rect.width;\n const offsetY = -(y - centerY) / rect.height;\n targetParallaxRef.current.set(offsetX * parallaxStrength, offsetY * parallaxStrength);\n }\n };\n\n const handlePointerLeave = () => {\n targetInfluenceRef.current = 0.0;\n };\n\n if (interactive) {\n renderer.domElement.addEventListener('pointermove', handlePointerMove);\n renderer.domElement.addEventListener('pointerleave', handlePointerLeave);\n }\n\n let raf = 0;\n const renderLoop = () => {\n uniforms.iTime.value = clock.getElapsedTime();\n\n if (interactive) {\n currentMouseRef.current.lerp(targetMouseRef.current, mouseDamping);\n uniforms.iMouse.value.copy(currentMouseRef.current);\n\n currentInfluenceRef.current += (targetInfluenceRef.current - currentInfluenceRef.current) * mouseDamping;\n uniforms.bendInfluence.value = currentInfluenceRef.current;\n }\n\n if (parallax) {\n currentParallaxRef.current.lerp(targetParallaxRef.current, mouseDamping);\n uniforms.parallaxOffset.value.copy(currentParallaxRef.current);\n }\n\n renderer.render(scene, camera);\n raf = requestAnimationFrame(renderLoop);\n };\n renderLoop();\n\n return () => {\n cancelAnimationFrame(raf);\n if (ro && containerRef.current) {\n ro.disconnect();\n }\n\n if (interactive) {\n renderer.domElement.removeEventListener('pointermove', handlePointerMove);\n renderer.domElement.removeEventListener('pointerleave', handlePointerLeave);\n }\n\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n if (renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n };\n }, [\n linesGradient,\n enabledWaves,\n lineCount,\n lineDistance,\n topWavePosition,\n middleWavePosition,\n bottomWavePosition,\n animationSpeed,\n interactive,\n bendRadius,\n bendStrength,\n mouseDamping,\n parallax,\n parallaxStrength\n ]);\n\n return (\n \n );\n}\n"
+ "content": "import { useEffect, useRef } from 'react';\nimport {\n Clock,\n Mesh,\n OrthographicCamera,\n PlaneGeometry,\n Scene,\n ShaderMaterial,\n Vector2,\n Vector3,\n WebGLRenderer\n} from 'three';\n\nconst vertexShader = `\nprecision highp float;\n\nvoid main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float animationSpeed;\n\nuniform bool enableTop;\nuniform bool enableMiddle;\nuniform bool enableBottom;\n\nuniform int topLineCount;\nuniform int middleLineCount;\nuniform int bottomLineCount;\n\nuniform float topLineDistance;\nuniform float middleLineDistance;\nuniform float bottomLineDistance;\n\nuniform vec3 topWavePosition;\nuniform vec3 middleWavePosition;\nuniform vec3 bottomWavePosition;\n\nuniform vec2 iMouse;\nuniform bool interactive;\nuniform float bendRadius;\nuniform float bendStrength;\nuniform float bendInfluence;\n\nuniform bool parallax;\nuniform float parallaxStrength;\nuniform vec2 parallaxOffset;\n\nuniform vec3 lineGradient[8];\nuniform int lineGradientCount;\n\nconst vec3 BLACK = vec3(0.0);\nconst vec3 PINK = vec3(233.0, 71.0, 245.0) / 255.0;\nconst vec3 BLUE = vec3(47.0, 75.0, 162.0) / 255.0;\n\nmat2 rotate(float r) {\n return mat2(cos(r), sin(r), -sin(r), cos(r));\n}\n\nvec3 background_color(vec2 uv) {\n vec3 col = vec3(0.0);\n\n float y = sin(uv.x - 0.2) * 0.3 - 0.1;\n float m = uv.y - y;\n\n col += mix(BLUE, BLACK, smoothstep(0.0, 1.0, abs(m)));\n col += mix(PINK, BLACK, smoothstep(0.0, 1.0, abs(m - 0.8)));\n return col * 0.5;\n}\n\nvec3 getLineColor(float t, vec3 baseColor) {\n if (lineGradientCount <= 0) {\n return baseColor;\n }\n\n vec3 gradientColor;\n \n if (lineGradientCount == 1) {\n gradientColor = lineGradient[0];\n } else {\n float clampedT = clamp(t, 0.0, 0.9999);\n float scaled = clampedT * float(lineGradientCount - 1);\n int idx = int(floor(scaled));\n float f = fract(scaled);\n int idx2 = min(idx + 1, lineGradientCount - 1);\n\n vec3 c1 = lineGradient[idx];\n vec3 c2 = lineGradient[idx2];\n \n gradientColor = mix(c1, c2, f);\n }\n \n return gradientColor * 0.5;\n}\n\n float wave(vec2 uv, float offset, vec2 screenUv, vec2 mouseUv, bool shouldBend) {\n float time = iTime * animationSpeed;\n\n float x_offset = offset;\n float x_movement = time * 0.1;\n float amp = sin(offset + time * 0.2) * 0.3;\n float y = sin(uv.x + x_offset + x_movement) * amp;\n\n if (shouldBend) {\n vec2 d = screenUv - mouseUv;\n float influence = exp(-dot(d, d) * bendRadius); // radial falloff around cursor\n float bendOffset = (mouseUv.y - screenUv.y) * influence * bendStrength * bendInfluence;\n y += bendOffset;\n }\n\n float m = uv.y - y;\n return 0.0175 / max(abs(m) + 0.01, 1e-3) + 0.01;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n baseUv.y *= -1.0;\n \n if (parallax) {\n baseUv += parallaxOffset;\n }\n\n vec3 col = vec3(0.0);\n\n vec3 b = lineGradientCount > 0 ? vec3(0.0) : background_color(baseUv);\n\n vec2 mouseUv = vec2(0.0);\n if (interactive) {\n mouseUv = (2.0 * iMouse - iResolution.xy) / iResolution.y;\n mouseUv.y *= -1.0;\n }\n \n if (enableBottom) {\n for (int i = 0; i < bottomLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(bottomLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = bottomWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(bottomLineDistance * fi + bottomWavePosition.x, bottomWavePosition.y),\n 1.5 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.2;\n }\n }\n\n if (enableMiddle) {\n for (int i = 0; i < middleLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(middleLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = middleWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(middleLineDistance * fi + middleWavePosition.x, middleWavePosition.y),\n 2.0 + 0.15 * fi,\n baseUv,\n mouseUv,\n interactive\n );\n }\n }\n\n if (enableTop) {\n for (int i = 0; i < topLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(topLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n \n float angle = topWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n ruv.x *= -1.0;\n col += lineCol * wave(\n ruv + vec2(topLineDistance * fi + topWavePosition.x, topWavePosition.y),\n 1.0 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.1;\n }\n }\n\n fragColor = vec4(col, 1.0);\n}\n\nvoid main() {\n vec4 color = vec4(0.0);\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}\n`;\n\nconst MAX_GRADIENT_STOPS = 8;\n\ntype WavePosition = {\n x: number;\n y: number;\n rotate: number;\n};\n\ntype FloatingLinesProps = {\n linesGradient?: string[];\n enabledWaves?: Array<'top' | 'middle' | 'bottom'>;\n lineCount?: number | number[];\n lineDistance?: number | number[];\n topWavePosition?: WavePosition;\n middleWavePosition?: WavePosition;\n bottomWavePosition?: WavePosition;\n animationSpeed?: number;\n interactive?: boolean;\n bendRadius?: number;\n bendStrength?: number;\n mouseDamping?: number;\n parallax?: boolean;\n parallaxStrength?: number;\n mixBlendMode?: React.CSSProperties['mixBlendMode'];\n};\n\nfunction hexToVec3(hex: string): Vector3 {\n let value = hex.trim();\n\n if (value.startsWith('#')) {\n value = value.slice(1);\n }\n\n let r = 255;\n let g = 255;\n let b = 255;\n\n if (value.length === 3) {\n r = parseInt(value[0] + value[0], 16);\n g = parseInt(value[1] + value[1], 16);\n b = parseInt(value[2] + value[2], 16);\n } else if (value.length === 6) {\n r = parseInt(value.slice(0, 2), 16);\n g = parseInt(value.slice(2, 4), 16);\n b = parseInt(value.slice(4, 6), 16);\n }\n\n return new Vector3(r / 255, g / 255, b / 255);\n}\n\nexport default function FloatingLines({\n linesGradient,\n enabledWaves = ['top', 'middle', 'bottom'],\n lineCount = [6],\n lineDistance = [5],\n topWavePosition,\n middleWavePosition,\n bottomWavePosition = { x: 2.0, y: -0.7, rotate: -1 },\n animationSpeed = 1,\n interactive = true,\n bendRadius = 5.0,\n bendStrength = -0.5,\n mouseDamping = 0.05,\n parallax = true,\n parallaxStrength = 0.2,\n mixBlendMode = 'screen'\n}: FloatingLinesProps) {\n const containerRef = useRef(null);\n const targetMouseRef = useRef(new Vector2(-1000, -1000));\n const currentMouseRef = useRef(new Vector2(-1000, -1000));\n const targetInfluenceRef = useRef(0);\n const currentInfluenceRef = useRef(0);\n const targetParallaxRef = useRef(new Vector2(0, 0));\n const currentParallaxRef = useRef(new Vector2(0, 0));\n\n const getLineCount = (waveType: 'top' | 'middle' | 'bottom'): number => {\n if (typeof lineCount === 'number') return lineCount;\n if (!enabledWaves.includes(waveType)) return 0;\n const index = enabledWaves.indexOf(waveType);\n return lineCount[index] ?? 6;\n };\n\n const getLineDistance = (waveType: 'top' | 'middle' | 'bottom'): number => {\n if (typeof lineDistance === 'number') return lineDistance;\n if (!enabledWaves.includes(waveType)) return 0.1;\n const index = enabledWaves.indexOf(waveType);\n return lineDistance[index] ?? 0.1;\n };\n\n const topLineCount = enabledWaves.includes('top') ? getLineCount('top') : 0;\n const middleLineCount = enabledWaves.includes('middle') ? getLineCount('middle') : 0;\n const bottomLineCount = enabledWaves.includes('bottom') ? getLineCount('bottom') : 0;\n\n const topLineDistance = enabledWaves.includes('top') ? getLineDistance('top') * 0.01 : 0.01;\n const middleLineDistance = enabledWaves.includes('middle') ? getLineDistance('middle') * 0.01 : 0.01;\n const bottomLineDistance = enabledWaves.includes('bottom') ? getLineDistance('bottom') * 0.01 : 0.01;\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const scene = new Scene();\n\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n camera.position.z = 1;\n\n const renderer = new WebGLRenderer({ antialias: true, alpha: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n containerRef.current.appendChild(renderer.domElement);\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: new Vector3(1, 1, 1) },\n animationSpeed: { value: animationSpeed },\n\n enableTop: { value: enabledWaves.includes('top') },\n enableMiddle: { value: enabledWaves.includes('middle') },\n enableBottom: { value: enabledWaves.includes('bottom') },\n\n topLineCount: { value: topLineCount },\n middleLineCount: { value: middleLineCount },\n bottomLineCount: { value: bottomLineCount },\n\n topLineDistance: { value: topLineDistance },\n middleLineDistance: { value: middleLineDistance },\n bottomLineDistance: { value: bottomLineDistance },\n\n topWavePosition: {\n value: new Vector3(topWavePosition?.x ?? 10.0, topWavePosition?.y ?? 0.5, topWavePosition?.rotate ?? -0.4)\n },\n middleWavePosition: {\n value: new Vector3(\n middleWavePosition?.x ?? 5.0,\n middleWavePosition?.y ?? 0.0,\n middleWavePosition?.rotate ?? 0.2\n )\n },\n bottomWavePosition: {\n value: new Vector3(\n bottomWavePosition?.x ?? 2.0,\n bottomWavePosition?.y ?? -0.7,\n bottomWavePosition?.rotate ?? 0.4\n )\n },\n\n iMouse: { value: new Vector2(-1000, -1000) },\n interactive: { value: interactive },\n bendRadius: { value: bendRadius },\n bendStrength: { value: bendStrength },\n bendInfluence: { value: 0 },\n\n parallax: { value: parallax },\n parallaxStrength: { value: parallaxStrength },\n parallaxOffset: { value: new Vector2(0, 0) },\n\n lineGradient: {\n value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1))\n },\n lineGradientCount: { value: 0 }\n };\n\n if (linesGradient && linesGradient.length > 0) {\n const stops = linesGradient.slice(0, MAX_GRADIENT_STOPS);\n uniforms.lineGradientCount.value = stops.length;\n\n stops.forEach((hex, i) => {\n const color = hexToVec3(hex);\n uniforms.lineGradient.value[i].set(color.x, color.y, color.z);\n });\n }\n\n const material = new ShaderMaterial({\n uniforms,\n vertexShader,\n fragmentShader\n });\n\n const geometry = new PlaneGeometry(2, 2);\n const mesh = new Mesh(geometry, material);\n scene.add(mesh);\n\n const clock = new Clock();\n\n const setSize = () => {\n const el = containerRef.current!;\n const width = el.clientWidth || 1;\n const height = el.clientHeight || 1;\n\n renderer.setSize(width, height, false);\n\n const canvasWidth = renderer.domElement.width;\n const canvasHeight = renderer.domElement.height;\n uniforms.iResolution.value.set(canvasWidth, canvasHeight, 1);\n };\n\n setSize();\n\n const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(setSize) : null;\n\n if (ro && containerRef.current) {\n ro.observe(containerRef.current);\n }\n\n const handlePointerMove = (event: PointerEvent) => {\n const rect = renderer.domElement.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n const dpr = renderer.getPixelRatio();\n\n targetMouseRef.current.set(x * dpr, (rect.height - y) * dpr);\n targetInfluenceRef.current = 1.0;\n\n if (parallax) {\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const offsetX = (x - centerX) / rect.width;\n const offsetY = -(y - centerY) / rect.height;\n targetParallaxRef.current.set(offsetX * parallaxStrength, offsetY * parallaxStrength);\n }\n };\n\n const handlePointerLeave = () => {\n targetInfluenceRef.current = 0.0;\n };\n\n if (interactive) {\n renderer.domElement.addEventListener('pointermove', handlePointerMove);\n renderer.domElement.addEventListener('pointerleave', handlePointerLeave);\n }\n\n let raf = 0;\n const renderLoop = () => {\n uniforms.iTime.value = clock.getElapsedTime();\n\n if (interactive) {\n currentMouseRef.current.lerp(targetMouseRef.current, mouseDamping);\n uniforms.iMouse.value.copy(currentMouseRef.current);\n\n currentInfluenceRef.current += (targetInfluenceRef.current - currentInfluenceRef.current) * mouseDamping;\n uniforms.bendInfluence.value = currentInfluenceRef.current;\n }\n\n if (parallax) {\n currentParallaxRef.current.lerp(targetParallaxRef.current, mouseDamping);\n uniforms.parallaxOffset.value.copy(currentParallaxRef.current);\n }\n\n renderer.render(scene, camera);\n raf = requestAnimationFrame(renderLoop);\n };\n renderLoop();\n\n return () => {\n cancelAnimationFrame(raf);\n if (ro && containerRef.current) {\n ro.disconnect();\n }\n\n if (interactive) {\n renderer.domElement.removeEventListener('pointermove', handlePointerMove);\n renderer.domElement.removeEventListener('pointerleave', handlePointerLeave);\n }\n\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n if (renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n };\n }, [\n linesGradient,\n enabledWaves,\n lineCount,\n lineDistance,\n topWavePosition,\n middleWavePosition,\n bottomWavePosition,\n animationSpeed,\n interactive,\n bendRadius,\n bendStrength,\n mouseDamping,\n parallax,\n parallaxStrength\n ]);\n\n return (\n \n );\n}\n"
}
],
"registryDependencies": [],
diff --git a/public/r/GhostCursor-JS-CSS.json b/public/r/GhostCursor-JS-CSS.json
index 0abf0643..54f1b703 100644
--- a/public/r/GhostCursor-JS-CSS.json
+++ b/public/r/GhostCursor-JS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "GhostCursor/GhostCursor.jsx",
- "content": "import { useEffect, useMemo, useRef } from 'react';\nimport * as THREE from 'three';\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';\nimport { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';\nimport './GhostCursor.css';\n\nconst GhostCursor = ({\n className,\n style,\n trailLength = 50,\n inertia = 0.5,\n grainIntensity = 0.05,\n bloomStrength = 0.1,\n bloomRadius = 1.0,\n bloomThreshold = 0.025,\n\n brightness = 1,\n color = '#B19EEF',\n mixBlendMode = 'screen',\n edgeIntensity = 0,\n\n maxDevicePixelRatio = 0.5,\n targetPixels,\n\n fadeDelayMs,\n fadeDurationMs,\n zIndex = 10\n}) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const composerRef = useRef(null);\n const materialRef = useRef(null);\n const bloomPassRef = useRef(null);\n const filmPassRef = useRef(null);\n\n const trailBufRef = useRef([]);\n const headRef = useRef(0);\n\n const rafRef = useRef(null);\n const resizeObsRef = useRef(null);\n const currentMouseRef = useRef(new THREE.Vector2(0.5, 0.5));\n const velocityRef = useRef(new THREE.Vector2(0, 0));\n const fadeOpacityRef = useRef(1.0);\n const lastMoveTimeRef = useRef(typeof performance !== 'undefined' ? performance.now() : Date.now());\n const pointerActiveRef = useRef(false);\n const runningRef = useRef(false);\n\n const isTouch = useMemo(\n () => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0),\n []\n );\n\n const pixelBudget = targetPixels ?? (isTouch ? 0.9e6 : 1.3e6);\n const fadeDelay = fadeDelayMs ?? (isTouch ? 500 : 1000);\n const fadeDuration = fadeDurationMs ?? (isTouch ? 1000 : 1500);\n\n const baseVertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n uniform float iTime;\n uniform vec3 iResolution;\n uniform vec2 iMouse;\n uniform vec2 iPrevMouse[MAX_TRAIL_LENGTH];\n uniform float iOpacity;\n uniform float iScale;\n uniform vec3 iBaseColor;\n uniform float iBrightness;\n uniform float iEdgeIntensity;\n varying vec2 vUv;\n\n float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))) * 43758.5453123); }\n float noise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n f *= f * (3. - 2. * f);\n return mix(mix(hash(i + vec2(0.,0.)), hash(i + vec2(1.,0.)), f.x),\n mix(hash(i + vec2(0.,1.)), hash(i + vec2(1.,1.)), f.x), f.y);\n }\n float fbm(vec2 p){\n float v = 0.0;\n float a = 0.5;\n mat2 m = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n for(int i=0;i<5;i++){\n v += a * noise(p);\n p = m * p * 2.0;\n a *= 0.5;\n }\n return v;\n }\n vec3 tint1(vec3 base){ return mix(base, vec3(1.0), 0.15); }\n vec3 tint2(vec3 base){ return mix(base, vec3(0.8, 0.9, 1.0), 0.25); }\n\n vec4 blob(vec2 p, vec2 mousePos, float intensity, float activity) {\n vec2 q = vec2(fbm(p * iScale + iTime * 0.1), fbm(p * iScale + vec2(5.2,1.3) + iTime * 0.1));\n vec2 r = vec2(fbm(p * iScale + q * 1.5 + iTime * 0.15), fbm(p * iScale + q * 1.5 + vec2(8.3,2.8) + iTime * 0.15));\n\n float smoke = fbm(p * iScale + r * 0.8);\n float radius = 0.5 + 0.3 * (1.0 / iScale);\n float distFactor = 1.0 - smoothstep(0.0, radius * activity, length(p - mousePos));\n float alpha = pow(smoke, 2.5) * distFactor;\n\n vec3 c1 = tint1(iBaseColor);\n vec3 c2 = tint2(iBaseColor);\n vec3 color = mix(c1, c2, sin(iTime * 0.5) * 0.5 + 0.5);\n\n return vec4(color * alpha * intensity, alpha * intensity);\n }\n\n void main() {\n vec2 uv = (gl_FragCoord.xy / iResolution.xy * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n vec2 mouse = (iMouse * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n\n vec3 colorAcc = vec3(0.0);\n float alphaAcc = 0.0;\n\n vec4 b = blob(uv, mouse, 1.0, iOpacity);\n colorAcc += b.rgb;\n alphaAcc += b.a;\n\n for (int i = 0; i < MAX_TRAIL_LENGTH; i++) {\n vec2 pm = (iPrevMouse[i] * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n float t = 1.0 - float(i) / float(MAX_TRAIL_LENGTH);\n t = pow(t, 2.0);\n if (t > 0.01) {\n vec4 bt = blob(uv, pm, t * 0.8, iOpacity);\n colorAcc += bt.rgb;\n alphaAcc += bt.a;\n }\n }\n\n colorAcc *= iBrightness;\n\n vec2 uv01 = gl_FragCoord.xy / iResolution.xy;\n float edgeDist = min(min(uv01.x, 1.0 - uv01.x), min(uv01.y, 1.0 - uv01.y));\n float distFromEdge = clamp(edgeDist * 2.0, 0.0, 1.0);\n float k = clamp(iEdgeIntensity, 0.0, 1.0);\n float edgeMask = mix(1.0 - k, 1.0, distFromEdge);\n\n float outAlpha = clamp(alphaAcc * iOpacity * edgeMask, 0.0, 1.0);\n gl_FragColor = vec4(colorAcc, outAlpha);\n }\n `;\n\n const FilmGrainShader = useMemo(() => {\n return {\n uniforms: {\n tDiffuse: { value: null },\n iTime: { value: 0 },\n intensity: { value: grainIntensity }\n },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float iTime;\n uniform float intensity;\n varying vec2 vUv;\n\n float hash1(float n){ return fract(sin(n)*43758.5453); }\n\n void main(){\n vec4 color = texture2D(tDiffuse, vUv);\n float n = hash1(vUv.x*1000.0 + vUv.y*2000.0 + iTime) * 2.0 - 1.0;\n color.rgb += n * intensity * color.rgb;\n gl_FragColor = color;\n }\n `\n };\n }, [grainIntensity]);\n\n const UnpremultiplyPass = useMemo(\n () =>\n new ShaderPass({\n uniforms: { tDiffuse: { value: null } },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n varying vec2 vUv;\n void main(){\n vec4 c = texture2D(tDiffuse, vUv);\n float a = max(c.a, 1e-5);\n vec3 straight = c.rgb / a;\n gl_FragColor = vec4(clamp(straight, 0.0, 1.0), c.a);\n }\n `\n }),\n []\n );\n\n function calculateScale(el) {\n const r = el.getBoundingClientRect();\n const base = 600;\n const current = Math.min(Math.max(1, r.width), Math.max(1, r.height));\n return Math.max(0.5, Math.min(2.0, current / base));\n }\n\n useEffect(() => {\n const host = containerRef.current;\n const parent = host?.parentElement;\n if (!host || !parent) return;\n\n const prevParentPos = parent.style.position;\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = 'relative';\n }\n\n const renderer = new THREE.WebGLRenderer({\n antialias: !isTouch,\n alpha: true,\n depth: false,\n stencil: false,\n powerPreference: isTouch ? 'low-power' : 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false\n });\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n renderer.domElement.style.pointerEvents = 'none';\n if (mixBlendMode) {\n renderer.domElement.style.mixBlendMode = String(mixBlendMode);\n } else {\n renderer.domElement.style.removeProperty('mix-blend-mode');\n }\n\n host.appendChild(renderer.domElement);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geom = new THREE.PlaneGeometry(2, 2);\n\n const maxTrail = Math.max(1, Math.floor(trailLength));\n trailBufRef.current = Array.from({ length: maxTrail }, () => new THREE.Vector2(0.5, 0.5));\n headRef.current = 0;\n\n const baseColor = new THREE.Color(color);\n\n const material = new THREE.ShaderMaterial({\n defines: { MAX_TRAIL_LENGTH: maxTrail },\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new THREE.Vector3(1, 1, 1) },\n iMouse: { value: new THREE.Vector2(0.5, 0.5) },\n iPrevMouse: { value: trailBufRef.current.map(v => v.clone()) },\n iOpacity: { value: 1.0 },\n iScale: { value: 1.0 },\n iBaseColor: { value: new THREE.Vector3(baseColor.r, baseColor.g, baseColor.b) },\n iBrightness: { value: brightness },\n iEdgeIntensity: { value: edgeIntensity }\n },\n vertexShader: baseVertexShader,\n fragmentShader,\n transparent: true,\n depthTest: false,\n depthWrite: false\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geom, material);\n scene.add(mesh);\n\n const composer = new EffectComposer(renderer);\n composerRef.current = composer;\n\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloomPass = new UnrealBloomPass(new THREE.Vector2(1, 1), bloomStrength, bloomRadius, bloomThreshold);\n bloomPassRef.current = bloomPass;\n composer.addPass(bloomPass);\n\n const filmPass = new ShaderPass(FilmGrainShader);\n filmPassRef.current = filmPass;\n composer.addPass(filmPass);\n\n composer.addPass(UnpremultiplyPass);\n\n const resize = () => {\n const rect = host.getBoundingClientRect();\n const cssW = Math.max(1, Math.floor(rect.width));\n const cssH = Math.max(1, Math.floor(rect.height));\n\n const currentDPR = Math.min(\n typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n maxDevicePixelRatio\n );\n const need = cssW * cssH * currentDPR * currentDPR;\n const scale = need <= pixelBudget ? 1 : Math.max(0.5, Math.min(1, Math.sqrt(pixelBudget / Math.max(1, need))));\n const pixelRatio = currentDPR * scale;\n\n renderer.setPixelRatio(pixelRatio);\n renderer.setSize(cssW, cssH, false);\n\n composer.setPixelRatio?.(pixelRatio);\n composer.setSize(cssW, cssH);\n\n const wpx = Math.max(1, Math.floor(cssW * pixelRatio));\n const hpx = Math.max(1, Math.floor(cssH * pixelRatio));\n material.uniforms.iResolution.value.set(wpx, hpx, 1);\n material.uniforms.iScale.value = calculateScale(host);\n bloomPass.setSize(wpx, hpx);\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n resizeObsRef.current = ro;\n ro.observe(parent);\n ro.observe(host);\n\n const start = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const animate = () => {\n const now = performance.now();\n const t = (now - start) / 1000;\n\n const mat = materialRef.current;\n const comp = composerRef.current;\n\n if (pointerActiveRef.current) {\n velocityRef.current.set(\n currentMouseRef.current.x - mat.uniforms.iMouse.value.x,\n currentMouseRef.current.y - mat.uniforms.iMouse.value.y\n );\n mat.uniforms.iMouse.value.copy(currentMouseRef.current);\n fadeOpacityRef.current = 1.0;\n } else {\n velocityRef.current.multiplyScalar(inertia);\n if (velocityRef.current.lengthSq() > 1e-6) {\n mat.uniforms.iMouse.value.add(velocityRef.current);\n }\n const dt = now - lastMoveTimeRef.current;\n if (dt > fadeDelay) {\n const k = Math.min(1, (dt - fadeDelay) / fadeDuration);\n fadeOpacityRef.current = Math.max(0, 1 - k);\n }\n }\n\n const N = trailBufRef.current.length;\n headRef.current = (headRef.current + 1) % N;\n trailBufRef.current[headRef.current].copy(mat.uniforms.iMouse.value);\n const arr = mat.uniforms.iPrevMouse.value;\n for (let i = 0; i < N; i++) {\n const srcIdx = (headRef.current - i + N) % N;\n arr[i].copy(trailBufRef.current[srcIdx]);\n }\n\n mat.uniforms.iOpacity.value = fadeOpacityRef.current;\n mat.uniforms.iTime.value = t;\n\n if (filmPassRef.current?.uniforms?.iTime) {\n filmPassRef.current.uniforms.iTime.value = t;\n }\n\n comp.render();\n\n if (!pointerActiveRef.current && fadeOpacityRef.current <= 0.001) {\n runningRef.current = false;\n rafRef.current = null;\n return;\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n const ensureLoop = () => {\n if (!runningRef.current) {\n runningRef.current = true;\n rafRef.current = requestAnimationFrame(animate);\n }\n };\n\n const onPointerMove = e => {\n const rect = parent.getBoundingClientRect();\n const x = THREE.MathUtils.clamp((e.clientX - rect.left) / Math.max(1, rect.width), 0, 1);\n const y = THREE.MathUtils.clamp(1 - (e.clientY - rect.top) / Math.max(1, rect.height), 0, 1);\n currentMouseRef.current.set(x, y);\n pointerActiveRef.current = true;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n const onPointerEnter = () => {\n pointerActiveRef.current = true;\n ensureLoop();\n };\n const onPointerLeave = () => {\n pointerActiveRef.current = false;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n\n parent.addEventListener('pointermove', onPointerMove, { passive: true });\n parent.addEventListener('pointerenter', onPointerEnter, { passive: true });\n parent.addEventListener('pointerleave', onPointerLeave, { passive: true });\n\n ensureLoop();\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n runningRef.current = false;\n rafRef.current = null;\n\n parent.removeEventListener('pointermove', onPointerMove);\n parent.removeEventListener('pointerenter', onPointerEnter);\n parent.removeEventListener('pointerleave', onPointerLeave);\n resizeObsRef.current?.disconnect();\n\n scene.clear();\n geom.dispose();\n material.dispose();\n composer.dispose();\n renderer.dispose();\n\n if (renderer.domElement && renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = prevParentPos;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n trailLength,\n inertia,\n grainIntensity,\n bloomStrength,\n bloomRadius,\n bloomThreshold,\n pixelBudget,\n fadeDelay,\n fadeDuration,\n isTouch,\n color,\n brightness,\n mixBlendMode,\n edgeIntensity\n ]);\n\n useEffect(() => {\n if (materialRef.current) {\n const c = new THREE.Color(color);\n materialRef.current.uniforms.iBaseColor.value.set(c.r, c.g, c.b);\n }\n }, [color]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iBrightness.value = brightness;\n }\n }, [brightness]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iEdgeIntensity.value = edgeIntensity;\n }\n }, [edgeIntensity]);\n\n useEffect(() => {\n if (filmPassRef.current?.uniforms?.intensity) {\n filmPassRef.current.uniforms.intensity.value = grainIntensity;\n }\n }, [grainIntensity]);\n\n useEffect(() => {\n const el = rendererRef.current?.domElement;\n if (!el) return;\n if (mixBlendMode) {\n el.style.mixBlendMode = String(mixBlendMode);\n } else {\n el.style.removeProperty('mix-blend-mode');\n }\n }, [mixBlendMode]);\n\n const mergedStyle = useMemo(() => ({ zIndex, ...style }), [zIndex, style]);\n\n return ;\n};\n\nexport default GhostCursor;\n"
+ "content": "import { useEffect, useMemo, useRef } from 'react';\nimport * as THREE from 'three';\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';\nimport { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';\nimport './GhostCursor.css';\n\nconst GhostCursor = ({\n className,\n style,\n trailLength = 50,\n inertia = 0.5,\n grainIntensity = 0.05,\n bloomStrength = 0.1,\n bloomRadius = 1.0,\n bloomThreshold = 0.025,\n\n brightness = 1,\n color = '#B19EEF',\n mixBlendMode = 'screen',\n edgeIntensity = 0,\n\n maxDevicePixelRatio = 0.5,\n targetPixels,\n\n fadeDelayMs,\n fadeDurationMs,\n zIndex = 10\n}) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const composerRef = useRef(null);\n const materialRef = useRef(null);\n const bloomPassRef = useRef(null);\n const filmPassRef = useRef(null);\n\n const trailBufRef = useRef([]);\n const headRef = useRef(0);\n\n const rafRef = useRef(null);\n const resizeObsRef = useRef(null);\n const currentMouseRef = useRef(new THREE.Vector2(0.5, 0.5));\n const velocityRef = useRef(new THREE.Vector2(0, 0));\n const fadeOpacityRef = useRef(1.0);\n const lastMoveTimeRef = useRef(typeof performance !== 'undefined' ? performance.now() : Date.now());\n const pointerActiveRef = useRef(false);\n const runningRef = useRef(false);\n\n const isTouch = useMemo(\n () => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0),\n []\n );\n\n const pixelBudget = targetPixels ?? (isTouch ? 0.9e6 : 1.3e6);\n const fadeDelay = fadeDelayMs ?? (isTouch ? 500 : 1000);\n const fadeDuration = fadeDurationMs ?? (isTouch ? 1000 : 1500);\n\n const baseVertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n uniform float iTime;\n uniform vec3 iResolution;\n uniform vec2 iMouse;\n uniform vec2 iPrevMouse[MAX_TRAIL_LENGTH];\n uniform float iOpacity;\n uniform float iScale;\n uniform vec3 iBaseColor;\n uniform float iBrightness;\n uniform float iEdgeIntensity;\n varying vec2 vUv;\n\n float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))) * 43758.5453123); }\n float noise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n f *= f * (3. - 2. * f);\n return mix(mix(hash(i + vec2(0.,0.)), hash(i + vec2(1.,0.)), f.x),\n mix(hash(i + vec2(0.,1.)), hash(i + vec2(1.,1.)), f.x), f.y);\n }\n float fbm(vec2 p){\n float v = 0.0;\n float a = 0.5;\n mat2 m = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n for(int i=0;i<5;i++){\n v += a * noise(p);\n p = m * p * 2.0;\n a *= 0.5;\n }\n return v;\n }\n vec3 tint1(vec3 base){ return mix(base, vec3(1.0), 0.15); }\n vec3 tint2(vec3 base){ return mix(base, vec3(0.8, 0.9, 1.0), 0.25); }\n\n vec4 blob(vec2 p, vec2 mousePos, float intensity, float activity) {\n vec2 q = vec2(fbm(p * iScale + iTime * 0.1), fbm(p * iScale + vec2(5.2,1.3) + iTime * 0.1));\n vec2 r = vec2(fbm(p * iScale + q * 1.5 + iTime * 0.15), fbm(p * iScale + q * 1.5 + vec2(8.3,2.8) + iTime * 0.15));\n\n float smoke = fbm(p * iScale + r * 0.8);\n float radius = 0.5 + 0.3 * (1.0 / iScale);\n float distFactor = 1.0 - smoothstep(0.0, radius * activity, length(p - mousePos));\n float alpha = pow(smoke, 2.5) * distFactor;\n\n vec3 c1 = tint1(iBaseColor);\n vec3 c2 = tint2(iBaseColor);\n vec3 color = mix(c1, c2, sin(iTime * 0.5) * 0.5 + 0.5);\n\n return vec4(color * alpha * intensity, alpha * intensity);\n }\n\n void main() {\n vec2 uv = (gl_FragCoord.xy / iResolution.xy * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n vec2 mouse = (iMouse * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n\n vec3 colorAcc = vec3(0.0);\n float alphaAcc = 0.0;\n\n vec4 b = blob(uv, mouse, 1.0, iOpacity);\n colorAcc += b.rgb;\n alphaAcc += b.a;\n\n for (int i = 0; i < MAX_TRAIL_LENGTH; i++) {\n vec2 pm = (iPrevMouse[i] * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n float t = 1.0 - float(i) / float(MAX_TRAIL_LENGTH);\n t = pow(t, 2.0);\n if (t > 0.01) {\n vec4 bt = blob(uv, pm, t * 0.8, iOpacity);\n colorAcc += bt.rgb;\n alphaAcc += bt.a;\n }\n }\n\n colorAcc *= iBrightness;\n\n vec2 uv01 = gl_FragCoord.xy / iResolution.xy;\n float edgeDist = min(min(uv01.x, 1.0 - uv01.x), min(uv01.y, 1.0 - uv01.y));\n float distFromEdge = clamp(edgeDist * 2.0, 0.0, 1.0);\n float k = clamp(iEdgeIntensity, 0.0, 1.0);\n float edgeMask = mix(1.0 - k, 1.0, distFromEdge);\n\n float outAlpha = clamp(alphaAcc * iOpacity * edgeMask, 0.0, 1.0);\n gl_FragColor = vec4(colorAcc, outAlpha);\n }\n `;\n\n const FilmGrainShader = useMemo(() => {\n return {\n uniforms: {\n tDiffuse: { value: null },\n iTime: { value: 0 },\n intensity: { value: grainIntensity }\n },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float iTime;\n uniform float intensity;\n varying vec2 vUv;\n\n float hash1(float n){ return fract(sin(n)*43758.5453); }\n\n void main(){\n vec4 color = texture2D(tDiffuse, vUv);\n float n = hash1(vUv.x*1000.0 + vUv.y*2000.0 + iTime) * 2.0 - 1.0;\n color.rgb += n * intensity * color.rgb;\n gl_FragColor = color;\n }\n `\n };\n }, [grainIntensity]);\n\n const UnpremultiplyPass = useMemo(\n () =>\n new ShaderPass({\n uniforms: { tDiffuse: { value: null } },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n varying vec2 vUv;\n void main(){\n vec4 c = texture2D(tDiffuse, vUv);\n float a = max(c.a, 1e-5);\n vec3 straight = c.rgb / a;\n gl_FragColor = vec4(clamp(straight, 0.0, 1.0), c.a);\n }\n `\n }),\n []\n );\n\n function calculateScale(el) {\n const r = el.getBoundingClientRect();\n const base = 600;\n const current = Math.min(Math.max(1, r.width), Math.max(1, r.height));\n return Math.max(0.5, Math.min(2.0, current / base));\n }\n\n useEffect(() => {\n const host = containerRef.current;\n const parent = host?.parentElement;\n if (!host || !parent) return;\n\n const prevParentPos = parent.style.position;\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = 'relative';\n }\n\n const renderer = new THREE.WebGLRenderer({\n antialias: !isTouch,\n alpha: true,\n depth: false,\n stencil: false,\n powerPreference: isTouch ? 'low-power' : 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false\n });\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n renderer.domElement.style.pointerEvents = 'none';\n if (mixBlendMode) {\n renderer.domElement.style.mixBlendMode = String(mixBlendMode);\n } else {\n renderer.domElement.style.removeProperty('mix-blend-mode');\n }\n\n host.appendChild(renderer.domElement);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geom = new THREE.PlaneGeometry(2, 2);\n\n const maxTrail = Math.max(1, Math.floor(trailLength));\n trailBufRef.current = Array.from({ length: maxTrail }, () => new THREE.Vector2(0.5, 0.5));\n headRef.current = 0;\n\n const baseColor = new THREE.Color(color);\n\n const material = new THREE.ShaderMaterial({\n defines: { MAX_TRAIL_LENGTH: maxTrail },\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new THREE.Vector3(1, 1, 1) },\n iMouse: { value: new THREE.Vector2(0.5, 0.5) },\n iPrevMouse: { value: trailBufRef.current.map(v => v.clone()) },\n iOpacity: { value: 1.0 },\n iScale: { value: 1.0 },\n iBaseColor: { value: new THREE.Vector3(baseColor.r, baseColor.g, baseColor.b) },\n iBrightness: { value: brightness },\n iEdgeIntensity: { value: edgeIntensity }\n },\n vertexShader: baseVertexShader,\n fragmentShader,\n transparent: true,\n depthTest: false,\n depthWrite: false\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geom, material);\n scene.add(mesh);\n\n const composer = new EffectComposer(renderer);\n composerRef.current = composer;\n\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloomPass = new UnrealBloomPass(new THREE.Vector2(1, 1), bloomStrength, bloomRadius, bloomThreshold);\n bloomPassRef.current = bloomPass;\n composer.addPass(bloomPass);\n\n const filmPass = new ShaderPass(FilmGrainShader);\n filmPassRef.current = filmPass;\n composer.addPass(filmPass);\n\n composer.addPass(UnpremultiplyPass);\n\n const resize = () => {\n const rect = host.getBoundingClientRect();\n const cssW = Math.max(1, Math.floor(rect.width));\n const cssH = Math.max(1, Math.floor(rect.height));\n\n const currentDPR = Math.min(\n typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n maxDevicePixelRatio\n );\n const need = cssW * cssH * currentDPR * currentDPR;\n const scale = need <= pixelBudget ? 1 : Math.max(0.5, Math.min(1, Math.sqrt(pixelBudget / Math.max(1, need))));\n const pixelRatio = currentDPR * scale;\n\n renderer.setPixelRatio(pixelRatio);\n renderer.setSize(cssW, cssH, false);\n\n composer.setPixelRatio?.(pixelRatio);\n composer.setSize(cssW, cssH);\n\n const wpx = Math.max(1, Math.floor(cssW * pixelRatio));\n const hpx = Math.max(1, Math.floor(cssH * pixelRatio));\n material.uniforms.iResolution.value.set(wpx, hpx, 1);\n material.uniforms.iScale.value = calculateScale(host);\n bloomPass.setSize(wpx, hpx);\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n resizeObsRef.current = ro;\n ro.observe(parent);\n ro.observe(host);\n\n const start = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const animate = () => {\n const now = performance.now();\n const t = (now - start) / 1000;\n\n const mat = materialRef.current;\n const comp = composerRef.current;\n\n if (pointerActiveRef.current) {\n velocityRef.current.set(\n currentMouseRef.current.x - mat.uniforms.iMouse.value.x,\n currentMouseRef.current.y - mat.uniforms.iMouse.value.y\n );\n mat.uniforms.iMouse.value.copy(currentMouseRef.current);\n fadeOpacityRef.current = 1.0;\n } else {\n velocityRef.current.multiplyScalar(inertia);\n if (velocityRef.current.lengthSq() > 1e-6) {\n mat.uniforms.iMouse.value.add(velocityRef.current);\n }\n const dt = now - lastMoveTimeRef.current;\n if (dt > fadeDelay) {\n const k = Math.min(1, (dt - fadeDelay) / fadeDuration);\n fadeOpacityRef.current = Math.max(0, 1 - k);\n }\n }\n\n const N = trailBufRef.current.length;\n headRef.current = (headRef.current + 1) % N;\n trailBufRef.current[headRef.current].copy(mat.uniforms.iMouse.value);\n const arr = mat.uniforms.iPrevMouse.value;\n for (let i = 0; i < N; i++) {\n const srcIdx = (headRef.current - i + N) % N;\n arr[i].copy(trailBufRef.current[srcIdx]);\n }\n\n mat.uniforms.iOpacity.value = fadeOpacityRef.current;\n mat.uniforms.iTime.value = t;\n\n if (filmPassRef.current?.uniforms?.iTime) {\n filmPassRef.current.uniforms.iTime.value = t;\n }\n\n comp.render();\n\n if (!pointerActiveRef.current && fadeOpacityRef.current <= 0.001) {\n runningRef.current = false;\n rafRef.current = null;\n return;\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n const ensureLoop = () => {\n if (!runningRef.current) {\n runningRef.current = true;\n rafRef.current = requestAnimationFrame(animate);\n }\n };\n\n const onPointerMove = e => {\n const rect = parent.getBoundingClientRect();\n const x = THREE.MathUtils.clamp((e.clientX - rect.left) / Math.max(1, rect.width), 0, 1);\n const y = THREE.MathUtils.clamp(1 - (e.clientY - rect.top) / Math.max(1, rect.height), 0, 1);\n currentMouseRef.current.set(x, y);\n pointerActiveRef.current = true;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n const onPointerEnter = () => {\n pointerActiveRef.current = true;\n ensureLoop();\n };\n const onPointerLeave = () => {\n pointerActiveRef.current = false;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n\n parent.addEventListener('pointermove', onPointerMove, { passive: true });\n parent.addEventListener('pointerenter', onPointerEnter, { passive: true });\n parent.addEventListener('pointerleave', onPointerLeave, { passive: true });\n\n ensureLoop();\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n runningRef.current = false;\n rafRef.current = null;\n\n parent.removeEventListener('pointermove', onPointerMove);\n parent.removeEventListener('pointerenter', onPointerEnter);\n parent.removeEventListener('pointerleave', onPointerLeave);\n resizeObsRef.current?.disconnect();\n\n scene.clear();\n geom.dispose();\n material.dispose();\n composer.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n\n if (renderer.domElement && renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = prevParentPos;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n trailLength,\n inertia,\n grainIntensity,\n bloomStrength,\n bloomRadius,\n bloomThreshold,\n pixelBudget,\n fadeDelay,\n fadeDuration,\n isTouch,\n color,\n brightness,\n mixBlendMode,\n edgeIntensity\n ]);\n\n useEffect(() => {\n if (materialRef.current) {\n const c = new THREE.Color(color);\n materialRef.current.uniforms.iBaseColor.value.set(c.r, c.g, c.b);\n }\n }, [color]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iBrightness.value = brightness;\n }\n }, [brightness]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iEdgeIntensity.value = edgeIntensity;\n }\n }, [edgeIntensity]);\n\n useEffect(() => {\n if (filmPassRef.current?.uniforms?.intensity) {\n filmPassRef.current.uniforms.intensity.value = grainIntensity;\n }\n }, [grainIntensity]);\n\n useEffect(() => {\n const el = rendererRef.current?.domElement;\n if (!el) return;\n if (mixBlendMode) {\n el.style.mixBlendMode = String(mixBlendMode);\n } else {\n el.style.removeProperty('mix-blend-mode');\n }\n }, [mixBlendMode]);\n\n const mergedStyle = useMemo(() => ({ zIndex, ...style }), [zIndex, style]);\n\n return ;\n};\n\nexport default GhostCursor;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/GhostCursor-JS-TW.json b/public/r/GhostCursor-JS-TW.json
index 31523a1e..c32ea90e 100644
--- a/public/r/GhostCursor-JS-TW.json
+++ b/public/r/GhostCursor-JS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "GhostCursor/GhostCursor.jsx",
- "content": "import { useEffect, useMemo, useRef } from 'react';\nimport * as THREE from 'three';\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';\nimport { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';\n\nconst GhostCursor = ({\n className,\n style,\n trailLength = 50,\n inertia = 0.5,\n grainIntensity = 0.05,\n bloomStrength = 0.1,\n bloomRadius = 1.0,\n bloomThreshold = 0.025,\n\n brightness = 1,\n color = '#B19EEF',\n mixBlendMode = 'screen',\n edgeIntensity = 0,\n\n maxDevicePixelRatio = 0.5,\n targetPixels,\n\n fadeDelayMs,\n fadeDurationMs,\n zIndex = 10\n}) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const composerRef = useRef(null);\n const materialRef = useRef(null);\n const bloomPassRef = useRef(null);\n const filmPassRef = useRef(null);\n\n const trailBufRef = useRef([]);\n const headRef = useRef(0);\n\n const rafRef = useRef(null);\n const resizeObsRef = useRef(null);\n const currentMouseRef = useRef(new THREE.Vector2(0.5, 0.5));\n const velocityRef = useRef(new THREE.Vector2(0, 0));\n const fadeOpacityRef = useRef(1.0);\n const lastMoveTimeRef = useRef(typeof performance !== 'undefined' ? performance.now() : Date.now());\n const pointerActiveRef = useRef(false);\n const runningRef = useRef(false);\n\n const isTouch = useMemo(\n () => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0),\n []\n );\n\n const pixelBudget = targetPixels ?? (isTouch ? 0.9e6 : 1.3e6);\n const fadeDelay = fadeDelayMs ?? (isTouch ? 500 : 1000);\n const fadeDuration = fadeDurationMs ?? (isTouch ? 1000 : 1500);\n\n const baseVertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n uniform float iTime;\n uniform vec3 iResolution;\n uniform vec2 iMouse;\n uniform vec2 iPrevMouse[MAX_TRAIL_LENGTH];\n uniform float iOpacity;\n uniform float iScale;\n uniform vec3 iBaseColor;\n uniform float iBrightness;\n uniform float iEdgeIntensity;\n varying vec2 vUv;\n\n float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))) * 43758.5453123); }\n float noise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n f *= f * (3. - 2. * f);\n return mix(mix(hash(i + vec2(0.,0.)), hash(i + vec2(1.,0.)), f.x),\n mix(hash(i + vec2(0.,1.)), hash(i + vec2(1.,1.)), f.x), f.y);\n }\n float fbm(vec2 p){\n float v = 0.0;\n float a = 0.5;\n mat2 m = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n for(int i=0;i<5;i++){\n v += a * noise(p);\n p = m * p * 2.0;\n a *= 0.5;\n }\n return v;\n }\n vec3 tint1(vec3 base){ return mix(base, vec3(1.0), 0.15); }\n vec3 tint2(vec3 base){ return mix(base, vec3(0.8, 0.9, 1.0), 0.25); }\n\n vec4 blob(vec2 p, vec2 mousePos, float intensity, float activity) {\n vec2 q = vec2(fbm(p * iScale + iTime * 0.1), fbm(p * iScale + vec2(5.2,1.3) + iTime * 0.1));\n vec2 r = vec2(fbm(p * iScale + q * 1.5 + iTime * 0.15), fbm(p * iScale + q * 1.5 + vec2(8.3,2.8) + iTime * 0.15));\n\n float smoke = fbm(p * iScale + r * 0.8);\n float radius = 0.5 + 0.3 * (1.0 / iScale);\n float distFactor = 1.0 - smoothstep(0.0, radius * activity, length(p - mousePos));\n float alpha = pow(smoke, 2.5) * distFactor;\n\n vec3 c1 = tint1(iBaseColor);\n vec3 c2 = tint2(iBaseColor);\n vec3 color = mix(c1, c2, sin(iTime * 0.5) * 0.5 + 0.5);\n\n return vec4(color * alpha * intensity, alpha * intensity);\n }\n\n void main() {\n vec2 uv = (gl_FragCoord.xy / iResolution.xy * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n vec2 mouse = (iMouse * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n\n vec3 colorAcc = vec3(0.0);\n float alphaAcc = 0.0;\n\n vec4 b = blob(uv, mouse, 1.0, iOpacity);\n colorAcc += b.rgb;\n alphaAcc += b.a;\n\n for (int i = 0; i < MAX_TRAIL_LENGTH; i++) {\n vec2 pm = (iPrevMouse[i] * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n float t = 1.0 - float(i) / float(MAX_TRAIL_LENGTH);\n t = pow(t, 2.0);\n if (t > 0.01) {\n vec4 bt = blob(uv, pm, t * 0.8, iOpacity);\n colorAcc += bt.rgb;\n alphaAcc += bt.a;\n }\n }\n\n colorAcc *= iBrightness;\n\n vec2 uv01 = gl_FragCoord.xy / iResolution.xy;\n float edgeDist = min(min(uv01.x, 1.0 - uv01.x), min(uv01.y, 1.0 - uv01.y));\n float distFromEdge = clamp(edgeDist * 2.0, 0.0, 1.0);\n float k = clamp(iEdgeIntensity, 0.0, 1.0);\n float edgeMask = mix(1.0 - k, 1.0, distFromEdge);\n\n float outAlpha = clamp(alphaAcc * iOpacity * edgeMask, 0.0, 1.0);\n gl_FragColor = vec4(colorAcc, outAlpha);\n }\n `;\n\n const FilmGrainShader = useMemo(() => {\n return {\n uniforms: {\n tDiffuse: { value: null },\n iTime: { value: 0 },\n intensity: { value: grainIntensity }\n },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float iTime;\n uniform float intensity;\n varying vec2 vUv;\n\n float hash1(float n){ return fract(sin(n)*43758.5453); }\n\n void main(){\n vec4 color = texture2D(tDiffuse, vUv);\n float n = hash1(vUv.x*1000.0 + vUv.y*2000.0 + iTime) * 2.0 - 1.0;\n color.rgb += n * intensity * color.rgb;\n gl_FragColor = color;\n }\n `\n };\n }, [grainIntensity]);\n\n const UnpremultiplyPass = useMemo(\n () =>\n new ShaderPass({\n uniforms: { tDiffuse: { value: null } },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n varying vec2 vUv;\n void main(){\n vec4 c = texture2D(tDiffuse, vUv);\n float a = max(c.a, 1e-5);\n vec3 straight = c.rgb / a;\n gl_FragColor = vec4(clamp(straight, 0.0, 1.0), c.a);\n }\n `\n }),\n []\n );\n\n function calculateScale(el) {\n const r = el.getBoundingClientRect();\n const base = 600;\n const current = Math.min(Math.max(1, r.width), Math.max(1, r.height));\n return Math.max(0.5, Math.min(2.0, current / base));\n }\n\n useEffect(() => {\n const host = containerRef.current;\n const parent = host?.parentElement;\n if (!host || !parent) return;\n\n const prevParentPos = parent.style.position;\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = 'relative';\n }\n\n const renderer = new THREE.WebGLRenderer({\n antialias: !isTouch,\n alpha: true,\n depth: false,\n stencil: false,\n powerPreference: isTouch ? 'low-power' : 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false\n });\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n renderer.domElement.style.pointerEvents = 'none';\n if (mixBlendMode) {\n renderer.domElement.style.mixBlendMode = String(mixBlendMode);\n } else {\n renderer.domElement.style.removeProperty('mix-blend-mode');\n }\n\n renderer.domElement.style.display = 'block';\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.background = 'transparent';\n\n host.appendChild(renderer.domElement);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geom = new THREE.PlaneGeometry(2, 2);\n\n const maxTrail = Math.max(1, Math.floor(trailLength));\n trailBufRef.current = Array.from({ length: maxTrail }, () => new THREE.Vector2(0.5, 0.5));\n headRef.current = 0;\n\n const baseColor = new THREE.Color(color);\n\n const material = new THREE.ShaderMaterial({\n defines: { MAX_TRAIL_LENGTH: maxTrail },\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new THREE.Vector3(1, 1, 1) },\n iMouse: { value: new THREE.Vector2(0.5, 0.5) },\n iPrevMouse: { value: trailBufRef.current.map(v => v.clone()) },\n iOpacity: { value: 1.0 },\n iScale: { value: 1.0 },\n iBaseColor: { value: new THREE.Vector3(baseColor.r, baseColor.g, baseColor.b) },\n iBrightness: { value: brightness },\n iEdgeIntensity: { value: edgeIntensity }\n },\n vertexShader: baseVertexShader,\n fragmentShader,\n transparent: true,\n depthTest: false,\n depthWrite: false\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geom, material);\n scene.add(mesh);\n\n const composer = new EffectComposer(renderer);\n composerRef.current = composer;\n\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloomPass = new UnrealBloomPass(new THREE.Vector2(1, 1), bloomStrength, bloomRadius, bloomThreshold);\n bloomPassRef.current = bloomPass;\n composer.addPass(bloomPass);\n\n const filmPass = new ShaderPass(FilmGrainShader);\n filmPassRef.current = filmPass;\n composer.addPass(filmPass);\n\n composer.addPass(UnpremultiplyPass);\n\n const resize = () => {\n const rect = host.getBoundingClientRect();\n const cssW = Math.max(1, Math.floor(rect.width));\n const cssH = Math.max(1, Math.floor(rect.height));\n\n const currentDPR = Math.min(\n typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n maxDevicePixelRatio\n );\n const need = cssW * cssH * currentDPR * currentDPR;\n const scale = need <= pixelBudget ? 1 : Math.max(0.5, Math.min(1, Math.sqrt(pixelBudget / Math.max(1, need))));\n const pixelRatio = currentDPR * scale;\n\n renderer.setPixelRatio(pixelRatio);\n renderer.setSize(cssW, cssH, false);\n\n composer.setPixelRatio?.(pixelRatio);\n composer.setSize(cssW, cssH);\n\n const wpx = Math.max(1, Math.floor(cssW * pixelRatio));\n const hpx = Math.max(1, Math.floor(cssH * pixelRatio));\n material.uniforms.iResolution.value.set(wpx, hpx, 1);\n material.uniforms.iScale.value = calculateScale(host);\n bloomPass.setSize(wpx, hpx);\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n resizeObsRef.current = ro;\n ro.observe(parent);\n ro.observe(host);\n\n const start = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const animate = () => {\n const now = performance.now();\n const t = (now - start) / 1000;\n\n const mat = materialRef.current;\n const comp = composerRef.current;\n\n if (pointerActiveRef.current) {\n velocityRef.current.set(\n currentMouseRef.current.x - mat.uniforms.iMouse.value.x,\n currentMouseRef.current.y - mat.uniforms.iMouse.value.y\n );\n mat.uniforms.iMouse.value.copy(currentMouseRef.current);\n fadeOpacityRef.current = 1.0;\n } else {\n velocityRef.current.multiplyScalar(inertia);\n if (velocityRef.current.lengthSq() > 1e-6) {\n mat.uniforms.iMouse.value.add(velocityRef.current);\n }\n const dt = now - lastMoveTimeRef.current;\n if (dt > fadeDelay) {\n const k = Math.min(1, (dt - fadeDelay) / fadeDuration);\n fadeOpacityRef.current = Math.max(0, 1 - k);\n }\n }\n\n const N = trailBufRef.current.length;\n headRef.current = (headRef.current + 1) % N;\n trailBufRef.current[headRef.current].copy(mat.uniforms.iMouse.value);\n const arr = mat.uniforms.iPrevMouse.value;\n for (let i = 0; i < N; i++) {\n const srcIdx = (headRef.current - i + N) % N;\n arr[i].copy(trailBufRef.current[srcIdx]);\n }\n\n mat.uniforms.iOpacity.value = fadeOpacityRef.current;\n mat.uniforms.iTime.value = t;\n\n if (filmPassRef.current?.uniforms?.iTime) {\n filmPassRef.current.uniforms.iTime.value = t;\n }\n\n comp.render();\n\n if (!pointerActiveRef.current && fadeOpacityRef.current <= 0.001) {\n runningRef.current = false;\n rafRef.current = null;\n return;\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n const ensureLoop = () => {\n if (!runningRef.current) {\n runningRef.current = true;\n rafRef.current = requestAnimationFrame(animate);\n }\n };\n\n const onPointerMove = e => {\n const rect = parent.getBoundingClientRect();\n const x = THREE.MathUtils.clamp((e.clientX - rect.left) / Math.max(1, rect.width), 0, 1);\n const y = THREE.MathUtils.clamp(1 - (e.clientY - rect.top) / Math.max(1, rect.height), 0, 1);\n currentMouseRef.current.set(x, y);\n pointerActiveRef.current = true;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n const onPointerEnter = () => {\n pointerActiveRef.current = true;\n ensureLoop();\n };\n const onPointerLeave = () => {\n pointerActiveRef.current = false;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n\n parent.addEventListener('pointermove', onPointerMove, { passive: true });\n parent.addEventListener('pointerenter', onPointerEnter, { passive: true });\n parent.addEventListener('pointerleave', onPointerLeave, { passive: true });\n\n ensureLoop();\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n runningRef.current = false;\n rafRef.current = null;\n\n parent.removeEventListener('pointermove', onPointerMove);\n parent.removeEventListener('pointerenter', onPointerEnter);\n parent.removeEventListener('pointerleave', onPointerLeave);\n resizeObsRef.current?.disconnect();\n\n scene.clear();\n geom.dispose();\n material.dispose();\n composer.dispose();\n renderer.dispose();\n\n if (renderer.domElement && renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = prevParentPos;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n trailLength,\n inertia,\n grainIntensity,\n bloomStrength,\n bloomRadius,\n bloomThreshold,\n pixelBudget,\n fadeDelay,\n fadeDuration,\n isTouch,\n color,\n brightness,\n mixBlendMode,\n edgeIntensity\n ]);\n\n useEffect(() => {\n if (materialRef.current) {\n const c = new THREE.Color(color);\n materialRef.current.uniforms.iBaseColor.value.set(c.r, c.g, c.b);\n }\n }, [color]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iBrightness.value = brightness;\n }\n }, [brightness]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iEdgeIntensity.value = edgeIntensity;\n }\n }, [edgeIntensity]);\n\n useEffect(() => {\n if (filmPassRef.current?.uniforms?.intensity) {\n filmPassRef.current.uniforms.intensity.value = grainIntensity;\n }\n }, [grainIntensity]);\n\n useEffect(() => {\n const el = rendererRef.current?.domElement;\n if (!el) return;\n if (mixBlendMode) {\n el.style.mixBlendMode = String(mixBlendMode);\n } else {\n el.style.removeProperty('mix-blend-mode');\n }\n }, [mixBlendMode]);\n\n const mergedStyle = useMemo(() => ({ zIndex, ...style }), [zIndex, style]);\n\n return ;\n};\n\nexport default GhostCursor;\n"
+ "content": "import { useEffect, useMemo, useRef } from 'react';\nimport * as THREE from 'three';\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';\nimport { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';\n\nconst GhostCursor = ({\n className,\n style,\n trailLength = 50,\n inertia = 0.5,\n grainIntensity = 0.05,\n bloomStrength = 0.1,\n bloomRadius = 1.0,\n bloomThreshold = 0.025,\n\n brightness = 1,\n color = '#B19EEF',\n mixBlendMode = 'screen',\n edgeIntensity = 0,\n\n maxDevicePixelRatio = 0.5,\n targetPixels,\n\n fadeDelayMs,\n fadeDurationMs,\n zIndex = 10\n}) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const composerRef = useRef(null);\n const materialRef = useRef(null);\n const bloomPassRef = useRef(null);\n const filmPassRef = useRef(null);\n\n const trailBufRef = useRef([]);\n const headRef = useRef(0);\n\n const rafRef = useRef(null);\n const resizeObsRef = useRef(null);\n const currentMouseRef = useRef(new THREE.Vector2(0.5, 0.5));\n const velocityRef = useRef(new THREE.Vector2(0, 0));\n const fadeOpacityRef = useRef(1.0);\n const lastMoveTimeRef = useRef(typeof performance !== 'undefined' ? performance.now() : Date.now());\n const pointerActiveRef = useRef(false);\n const runningRef = useRef(false);\n\n const isTouch = useMemo(\n () => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0),\n []\n );\n\n const pixelBudget = targetPixels ?? (isTouch ? 0.9e6 : 1.3e6);\n const fadeDelay = fadeDelayMs ?? (isTouch ? 500 : 1000);\n const fadeDuration = fadeDurationMs ?? (isTouch ? 1000 : 1500);\n\n const baseVertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n uniform float iTime;\n uniform vec3 iResolution;\n uniform vec2 iMouse;\n uniform vec2 iPrevMouse[MAX_TRAIL_LENGTH];\n uniform float iOpacity;\n uniform float iScale;\n uniform vec3 iBaseColor;\n uniform float iBrightness;\n uniform float iEdgeIntensity;\n varying vec2 vUv;\n\n float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))) * 43758.5453123); }\n float noise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n f *= f * (3. - 2. * f);\n return mix(mix(hash(i + vec2(0.,0.)), hash(i + vec2(1.,0.)), f.x),\n mix(hash(i + vec2(0.,1.)), hash(i + vec2(1.,1.)), f.x), f.y);\n }\n float fbm(vec2 p){\n float v = 0.0;\n float a = 0.5;\n mat2 m = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n for(int i=0;i<5;i++){\n v += a * noise(p);\n p = m * p * 2.0;\n a *= 0.5;\n }\n return v;\n }\n vec3 tint1(vec3 base){ return mix(base, vec3(1.0), 0.15); }\n vec3 tint2(vec3 base){ return mix(base, vec3(0.8, 0.9, 1.0), 0.25); }\n\n vec4 blob(vec2 p, vec2 mousePos, float intensity, float activity) {\n vec2 q = vec2(fbm(p * iScale + iTime * 0.1), fbm(p * iScale + vec2(5.2,1.3) + iTime * 0.1));\n vec2 r = vec2(fbm(p * iScale + q * 1.5 + iTime * 0.15), fbm(p * iScale + q * 1.5 + vec2(8.3,2.8) + iTime * 0.15));\n\n float smoke = fbm(p * iScale + r * 0.8);\n float radius = 0.5 + 0.3 * (1.0 / iScale);\n float distFactor = 1.0 - smoothstep(0.0, radius * activity, length(p - mousePos));\n float alpha = pow(smoke, 2.5) * distFactor;\n\n vec3 c1 = tint1(iBaseColor);\n vec3 c2 = tint2(iBaseColor);\n vec3 color = mix(c1, c2, sin(iTime * 0.5) * 0.5 + 0.5);\n\n return vec4(color * alpha * intensity, alpha * intensity);\n }\n\n void main() {\n vec2 uv = (gl_FragCoord.xy / iResolution.xy * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n vec2 mouse = (iMouse * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n\n vec3 colorAcc = vec3(0.0);\n float alphaAcc = 0.0;\n\n vec4 b = blob(uv, mouse, 1.0, iOpacity);\n colorAcc += b.rgb;\n alphaAcc += b.a;\n\n for (int i = 0; i < MAX_TRAIL_LENGTH; i++) {\n vec2 pm = (iPrevMouse[i] * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n float t = 1.0 - float(i) / float(MAX_TRAIL_LENGTH);\n t = pow(t, 2.0);\n if (t > 0.01) {\n vec4 bt = blob(uv, pm, t * 0.8, iOpacity);\n colorAcc += bt.rgb;\n alphaAcc += bt.a;\n }\n }\n\n colorAcc *= iBrightness;\n\n vec2 uv01 = gl_FragCoord.xy / iResolution.xy;\n float edgeDist = min(min(uv01.x, 1.0 - uv01.x), min(uv01.y, 1.0 - uv01.y));\n float distFromEdge = clamp(edgeDist * 2.0, 0.0, 1.0);\n float k = clamp(iEdgeIntensity, 0.0, 1.0);\n float edgeMask = mix(1.0 - k, 1.0, distFromEdge);\n\n float outAlpha = clamp(alphaAcc * iOpacity * edgeMask, 0.0, 1.0);\n gl_FragColor = vec4(colorAcc, outAlpha);\n }\n `;\n\n const FilmGrainShader = useMemo(() => {\n return {\n uniforms: {\n tDiffuse: { value: null },\n iTime: { value: 0 },\n intensity: { value: grainIntensity }\n },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float iTime;\n uniform float intensity;\n varying vec2 vUv;\n\n float hash1(float n){ return fract(sin(n)*43758.5453); }\n\n void main(){\n vec4 color = texture2D(tDiffuse, vUv);\n float n = hash1(vUv.x*1000.0 + vUv.y*2000.0 + iTime) * 2.0 - 1.0;\n color.rgb += n * intensity * color.rgb;\n gl_FragColor = color;\n }\n `\n };\n }, [grainIntensity]);\n\n const UnpremultiplyPass = useMemo(\n () =>\n new ShaderPass({\n uniforms: { tDiffuse: { value: null } },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n varying vec2 vUv;\n void main(){\n vec4 c = texture2D(tDiffuse, vUv);\n float a = max(c.a, 1e-5);\n vec3 straight = c.rgb / a;\n gl_FragColor = vec4(clamp(straight, 0.0, 1.0), c.a);\n }\n `\n }),\n []\n );\n\n function calculateScale(el) {\n const r = el.getBoundingClientRect();\n const base = 600;\n const current = Math.min(Math.max(1, r.width), Math.max(1, r.height));\n return Math.max(0.5, Math.min(2.0, current / base));\n }\n\n useEffect(() => {\n const host = containerRef.current;\n const parent = host?.parentElement;\n if (!host || !parent) return;\n\n const prevParentPos = parent.style.position;\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = 'relative';\n }\n\n const renderer = new THREE.WebGLRenderer({\n antialias: !isTouch,\n alpha: true,\n depth: false,\n stencil: false,\n powerPreference: isTouch ? 'low-power' : 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false\n });\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n renderer.domElement.style.pointerEvents = 'none';\n if (mixBlendMode) {\n renderer.domElement.style.mixBlendMode = String(mixBlendMode);\n } else {\n renderer.domElement.style.removeProperty('mix-blend-mode');\n }\n\n renderer.domElement.style.display = 'block';\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.background = 'transparent';\n\n host.appendChild(renderer.domElement);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geom = new THREE.PlaneGeometry(2, 2);\n\n const maxTrail = Math.max(1, Math.floor(trailLength));\n trailBufRef.current = Array.from({ length: maxTrail }, () => new THREE.Vector2(0.5, 0.5));\n headRef.current = 0;\n\n const baseColor = new THREE.Color(color);\n\n const material = new THREE.ShaderMaterial({\n defines: { MAX_TRAIL_LENGTH: maxTrail },\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new THREE.Vector3(1, 1, 1) },\n iMouse: { value: new THREE.Vector2(0.5, 0.5) },\n iPrevMouse: { value: trailBufRef.current.map(v => v.clone()) },\n iOpacity: { value: 1.0 },\n iScale: { value: 1.0 },\n iBaseColor: { value: new THREE.Vector3(baseColor.r, baseColor.g, baseColor.b) },\n iBrightness: { value: brightness },\n iEdgeIntensity: { value: edgeIntensity }\n },\n vertexShader: baseVertexShader,\n fragmentShader,\n transparent: true,\n depthTest: false,\n depthWrite: false\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geom, material);\n scene.add(mesh);\n\n const composer = new EffectComposer(renderer);\n composerRef.current = composer;\n\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloomPass = new UnrealBloomPass(new THREE.Vector2(1, 1), bloomStrength, bloomRadius, bloomThreshold);\n bloomPassRef.current = bloomPass;\n composer.addPass(bloomPass);\n\n const filmPass = new ShaderPass(FilmGrainShader);\n filmPassRef.current = filmPass;\n composer.addPass(filmPass);\n\n composer.addPass(UnpremultiplyPass);\n\n const resize = () => {\n const rect = host.getBoundingClientRect();\n const cssW = Math.max(1, Math.floor(rect.width));\n const cssH = Math.max(1, Math.floor(rect.height));\n\n const currentDPR = Math.min(\n typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n maxDevicePixelRatio\n );\n const need = cssW * cssH * currentDPR * currentDPR;\n const scale = need <= pixelBudget ? 1 : Math.max(0.5, Math.min(1, Math.sqrt(pixelBudget / Math.max(1, need))));\n const pixelRatio = currentDPR * scale;\n\n renderer.setPixelRatio(pixelRatio);\n renderer.setSize(cssW, cssH, false);\n\n composer.setPixelRatio?.(pixelRatio);\n composer.setSize(cssW, cssH);\n\n const wpx = Math.max(1, Math.floor(cssW * pixelRatio));\n const hpx = Math.max(1, Math.floor(cssH * pixelRatio));\n material.uniforms.iResolution.value.set(wpx, hpx, 1);\n material.uniforms.iScale.value = calculateScale(host);\n bloomPass.setSize(wpx, hpx);\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n resizeObsRef.current = ro;\n ro.observe(parent);\n ro.observe(host);\n\n const start = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const animate = () => {\n const now = performance.now();\n const t = (now - start) / 1000;\n\n const mat = materialRef.current;\n const comp = composerRef.current;\n\n if (pointerActiveRef.current) {\n velocityRef.current.set(\n currentMouseRef.current.x - mat.uniforms.iMouse.value.x,\n currentMouseRef.current.y - mat.uniforms.iMouse.value.y\n );\n mat.uniforms.iMouse.value.copy(currentMouseRef.current);\n fadeOpacityRef.current = 1.0;\n } else {\n velocityRef.current.multiplyScalar(inertia);\n if (velocityRef.current.lengthSq() > 1e-6) {\n mat.uniforms.iMouse.value.add(velocityRef.current);\n }\n const dt = now - lastMoveTimeRef.current;\n if (dt > fadeDelay) {\n const k = Math.min(1, (dt - fadeDelay) / fadeDuration);\n fadeOpacityRef.current = Math.max(0, 1 - k);\n }\n }\n\n const N = trailBufRef.current.length;\n headRef.current = (headRef.current + 1) % N;\n trailBufRef.current[headRef.current].copy(mat.uniforms.iMouse.value);\n const arr = mat.uniforms.iPrevMouse.value;\n for (let i = 0; i < N; i++) {\n const srcIdx = (headRef.current - i + N) % N;\n arr[i].copy(trailBufRef.current[srcIdx]);\n }\n\n mat.uniforms.iOpacity.value = fadeOpacityRef.current;\n mat.uniforms.iTime.value = t;\n\n if (filmPassRef.current?.uniforms?.iTime) {\n filmPassRef.current.uniforms.iTime.value = t;\n }\n\n comp.render();\n\n if (!pointerActiveRef.current && fadeOpacityRef.current <= 0.001) {\n runningRef.current = false;\n rafRef.current = null;\n return;\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n const ensureLoop = () => {\n if (!runningRef.current) {\n runningRef.current = true;\n rafRef.current = requestAnimationFrame(animate);\n }\n };\n\n const onPointerMove = e => {\n const rect = parent.getBoundingClientRect();\n const x = THREE.MathUtils.clamp((e.clientX - rect.left) / Math.max(1, rect.width), 0, 1);\n const y = THREE.MathUtils.clamp(1 - (e.clientY - rect.top) / Math.max(1, rect.height), 0, 1);\n currentMouseRef.current.set(x, y);\n pointerActiveRef.current = true;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n const onPointerEnter = () => {\n pointerActiveRef.current = true;\n ensureLoop();\n };\n const onPointerLeave = () => {\n pointerActiveRef.current = false;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n\n parent.addEventListener('pointermove', onPointerMove, { passive: true });\n parent.addEventListener('pointerenter', onPointerEnter, { passive: true });\n parent.addEventListener('pointerleave', onPointerLeave, { passive: true });\n\n ensureLoop();\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n runningRef.current = false;\n rafRef.current = null;\n\n parent.removeEventListener('pointermove', onPointerMove);\n parent.removeEventListener('pointerenter', onPointerEnter);\n parent.removeEventListener('pointerleave', onPointerLeave);\n resizeObsRef.current?.disconnect();\n\n scene.clear();\n geom.dispose();\n material.dispose();\n composer.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n\n if (renderer.domElement && renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = prevParentPos;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n trailLength,\n inertia,\n grainIntensity,\n bloomStrength,\n bloomRadius,\n bloomThreshold,\n pixelBudget,\n fadeDelay,\n fadeDuration,\n isTouch,\n color,\n brightness,\n mixBlendMode,\n edgeIntensity\n ]);\n\n useEffect(() => {\n if (materialRef.current) {\n const c = new THREE.Color(color);\n materialRef.current.uniforms.iBaseColor.value.set(c.r, c.g, c.b);\n }\n }, [color]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iBrightness.value = brightness;\n }\n }, [brightness]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iEdgeIntensity.value = edgeIntensity;\n }\n }, [edgeIntensity]);\n\n useEffect(() => {\n if (filmPassRef.current?.uniforms?.intensity) {\n filmPassRef.current.uniforms.intensity.value = grainIntensity;\n }\n }, [grainIntensity]);\n\n useEffect(() => {\n const el = rendererRef.current?.domElement;\n if (!el) return;\n if (mixBlendMode) {\n el.style.mixBlendMode = String(mixBlendMode);\n } else {\n el.style.removeProperty('mix-blend-mode');\n }\n }, [mixBlendMode]);\n\n const mergedStyle = useMemo(() => ({ zIndex, ...style }), [zIndex, style]);\n\n return ;\n};\n\nexport default GhostCursor;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/GhostCursor-TS-CSS.json b/public/r/GhostCursor-TS-CSS.json
index a62cb7e4..5ba7a8e3 100644
--- a/public/r/GhostCursor-TS-CSS.json
+++ b/public/r/GhostCursor-TS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "GhostCursor/GhostCursor.tsx",
- "content": "import React, { useEffect, useMemo, useRef } from 'react';\nimport * as THREE from 'three';\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';\nimport { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';\nimport './GhostCursor.css';\n\ntype GhostCursorProps = {\n className?: string;\n style?: React.CSSProperties;\n\n trailLength?: number;\n inertia?: number;\n grainIntensity?: number;\n bloomStrength?: number;\n bloomRadius?: number;\n bloomThreshold?: number;\n\n brightness?: number;\n color?: string;\n mixBlendMode?: React.CSSProperties['mixBlendMode'];\n edgeIntensity?: number;\n\n maxDevicePixelRatio?: number;\n targetPixels?: number;\n fadeDelayMs?: number;\n fadeDurationMs?: number;\n zIndex?: number;\n};\n\nconst GhostCursor: React.FC = ({\n className,\n style,\n trailLength = 50,\n inertia = 0.5,\n grainIntensity = 0.05,\n bloomStrength = 0.1,\n bloomRadius = 1.0,\n bloomThreshold = 0.025,\n\n brightness = 1,\n color = '#B19EEF',\n mixBlendMode = 'screen',\n edgeIntensity = 0,\n\n maxDevicePixelRatio = 0.5,\n targetPixels,\n\n fadeDelayMs,\n fadeDurationMs,\n zIndex = 10\n}) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const composerRef = useRef(null);\n const materialRef = useRef(null);\n const bloomPassRef = useRef(null);\n const filmPassRef = useRef(null);\n\n // Trail circular buffer\n const trailBufRef = useRef([]);\n const headRef = useRef(0);\n\n const rafRef = useRef(null);\n const resizeObsRef = useRef(null);\n const currentMouseRef = useRef(new THREE.Vector2(0.5, 0.5));\n const velocityRef = useRef(new THREE.Vector2(0, 0));\n const fadeOpacityRef = useRef(1.0);\n const lastMoveTimeRef = useRef(typeof performance !== 'undefined' ? performance.now() : Date.now());\n const pointerActiveRef = useRef(false);\n const runningRef = useRef(false);\n\n const isTouch = useMemo(\n () => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0),\n []\n );\n\n const pixelBudget = targetPixels ?? (isTouch ? 0.9e6 : 1.3e6);\n const fadeDelay = fadeDelayMs ?? (isTouch ? 500 : 1000);\n const fadeDuration = fadeDurationMs ?? (isTouch ? 1000 : 1500);\n\n const baseVertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n uniform float iTime;\n uniform vec3 iResolution;\n uniform vec2 iMouse;\n uniform vec2 iPrevMouse[MAX_TRAIL_LENGTH];\n uniform float iOpacity;\n uniform float iScale;\n uniform vec3 iBaseColor;\n uniform float iBrightness;\n uniform float iEdgeIntensity;\n varying vec2 vUv;\n\n float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))) * 43758.5453123); }\n float noise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n f *= f * (3. - 2. * f);\n return mix(mix(hash(i + vec2(0.,0.)), hash(i + vec2(1.,0.)), f.x),\n mix(hash(i + vec2(0.,1.)), hash(i + vec2(1.,1.)), f.x), f.y);\n }\n float fbm(vec2 p){\n float v = 0.0;\n float a = 0.5;\n mat2 m = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n for(int i=0;i<5;i++){\n v += a * noise(p);\n p = m * p * 2.0;\n a *= 0.5;\n }\n return v;\n }\n vec3 tint1(vec3 base){ return mix(base, vec3(1.0), 0.15); }\n vec3 tint2(vec3 base){ return mix(base, vec3(0.8, 0.9, 1.0), 0.25); }\n\n vec4 blob(vec2 p, vec2 mousePos, float intensity, float activity) {\n vec2 q = vec2(fbm(p * iScale + iTime * 0.1), fbm(p * iScale + vec2(5.2,1.3) + iTime * 0.1));\n vec2 r = vec2(fbm(p * iScale + q * 1.5 + iTime * 0.15), fbm(p * iScale + q * 1.5 + vec2(8.3,2.8) + iTime * 0.15));\n\n float smoke = fbm(p * iScale + r * 0.8);\n float radius = 0.5 + 0.3 * (1.0 / iScale);\n float distFactor = 1.0 - smoothstep(0.0, radius * activity, length(p - mousePos));\n float alpha = pow(smoke, 2.5) * distFactor;\n\n vec3 c1 = tint1(iBaseColor);\n vec3 c2 = tint2(iBaseColor);\n vec3 color = mix(c1, c2, sin(iTime * 0.5) * 0.5 + 0.5);\n\n return vec4(color * alpha * intensity, alpha * intensity);\n }\n\n void main() {\n vec2 uv = (gl_FragCoord.xy / iResolution.xy * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n vec2 mouse = (iMouse * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n\n vec3 colorAcc = vec3(0.0);\n float alphaAcc = 0.0;\n\n vec4 b = blob(uv, mouse, 1.0, iOpacity);\n colorAcc += b.rgb;\n alphaAcc += b.a;\n\n for (int i = 0; i < MAX_TRAIL_LENGTH; i++) {\n vec2 pm = (iPrevMouse[i] * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n float t = 1.0 - float(i) / float(MAX_TRAIL_LENGTH);\n t = pow(t, 2.0);\n if (t > 0.01) {\n vec4 bt = blob(uv, pm, t * 0.8, iOpacity);\n colorAcc += bt.rgb;\n alphaAcc += bt.a;\n }\n }\n\n colorAcc *= iBrightness;\n\n vec2 uv01 = gl_FragCoord.xy / iResolution.xy;\n float edgeDist = min(min(uv01.x, 1.0 - uv01.x), min(uv01.y, 1.0 - uv01.y));\n float distFromEdge = clamp(edgeDist * 2.0, 0.0, 1.0);\n float k = clamp(iEdgeIntensity, 0.0, 1.0);\n float edgeMask = mix(1.0 - k, 1.0, distFromEdge);\n\n float outAlpha = clamp(alphaAcc * iOpacity * edgeMask, 0.0, 1.0);\n gl_FragColor = vec4(colorAcc, outAlpha);\n }\n `;\n\n const FilmGrainShader = useMemo(() => {\n return {\n uniforms: {\n tDiffuse: { value: null },\n iTime: { value: 0 },\n intensity: { value: grainIntensity }\n },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float iTime;\n uniform float intensity;\n varying vec2 vUv;\n\n float hash1(float n){ return fract(sin(n)*43758.5453); }\n\n void main(){\n vec4 color = texture2D(tDiffuse, vUv);\n float n = hash1(vUv.x*1000.0 + vUv.y*2000.0 + iTime) * 2.0 - 1.0;\n color.rgb += n * intensity * color.rgb;\n gl_FragColor = color;\n }\n `\n };\n }, [grainIntensity]);\n\n const UnpremultiplyPass = useMemo(\n () =>\n new ShaderPass({\n uniforms: { tDiffuse: { value: null } },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n varying vec2 vUv;\n void main(){\n vec4 c = texture2D(tDiffuse, vUv);\n float a = max(c.a, 1e-5);\n vec3 straight = c.rgb / a;\n gl_FragColor = vec4(clamp(straight, 0.0, 1.0), c.a);\n }\n `\n }),\n []\n );\n\n function calculateScale(el: HTMLElement) {\n const r = el.getBoundingClientRect();\n const base = 600;\n const current = Math.min(Math.max(1, r.width), Math.max(1, r.height));\n return Math.max(0.5, Math.min(2.0, current / base));\n }\n\n useEffect(() => {\n const host = containerRef.current;\n const parent = host?.parentElement;\n if (!host || !parent) return;\n\n const prevParentPos = parent.style.position;\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = 'relative';\n }\n\n const renderer = new THREE.WebGLRenderer({\n antialias: !isTouch,\n alpha: true,\n depth: false,\n stencil: false,\n powerPreference: isTouch ? 'low-power' : 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false\n });\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n renderer.domElement.style.pointerEvents = 'none';\n if (mixBlendMode) {\n renderer.domElement.style.mixBlendMode = String(mixBlendMode);\n } else {\n renderer.domElement.style.removeProperty('mix-blend-mode');\n }\n\n host.appendChild(renderer.domElement);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geom = new THREE.PlaneGeometry(2, 2);\n\n const maxTrail = Math.max(1, Math.floor(trailLength));\n trailBufRef.current = Array.from({ length: maxTrail }, () => new THREE.Vector2(0.5, 0.5));\n headRef.current = 0;\n\n const baseColor = new THREE.Color(color);\n\n const material = new THREE.ShaderMaterial({\n defines: { MAX_TRAIL_LENGTH: maxTrail },\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new THREE.Vector3(1, 1, 1) },\n iMouse: { value: new THREE.Vector2(0.5, 0.5) },\n iPrevMouse: { value: trailBufRef.current.map(v => v.clone()) },\n iOpacity: { value: 1.0 },\n iScale: { value: 1.0 },\n iBaseColor: { value: new THREE.Vector3(baseColor.r, baseColor.g, baseColor.b) },\n iBrightness: { value: brightness },\n iEdgeIntensity: { value: edgeIntensity }\n },\n vertexShader: baseVertexShader,\n fragmentShader,\n transparent: true,\n depthTest: false,\n depthWrite: false\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geom, material);\n scene.add(mesh);\n\n const composer = new EffectComposer(renderer);\n composerRef.current = composer;\n\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloomPass = new UnrealBloomPass(new THREE.Vector2(1, 1), bloomStrength, bloomRadius, bloomThreshold);\n bloomPassRef.current = bloomPass;\n composer.addPass(bloomPass);\n\n const filmPass = new ShaderPass(FilmGrainShader as any);\n filmPassRef.current = filmPass;\n composer.addPass(filmPass);\n\n composer.addPass(UnpremultiplyPass);\n\n const resize = () => {\n const rect = host.getBoundingClientRect();\n const cssW = Math.max(1, Math.floor(rect.width));\n const cssH = Math.max(1, Math.floor(rect.height));\n\n const currentDPR = Math.min(\n typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n maxDevicePixelRatio\n );\n const need = cssW * cssH * currentDPR * currentDPR;\n const scale = need <= pixelBudget ? 1 : Math.max(0.5, Math.min(1, Math.sqrt(pixelBudget / Math.max(1, need))));\n const pixelRatio = currentDPR * scale;\n\n renderer.setPixelRatio(pixelRatio);\n renderer.setSize(cssW, cssH, false);\n\n composer.setPixelRatio?.(pixelRatio);\n composer.setSize(cssW, cssH);\n\n const wpx = Math.max(1, Math.floor(cssW * pixelRatio));\n const hpx = Math.max(1, Math.floor(cssH * pixelRatio));\n material.uniforms.iResolution.value.set(wpx, hpx, 1);\n material.uniforms.iScale.value = calculateScale(host);\n bloomPass.setSize(wpx, hpx);\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n resizeObsRef.current = ro;\n ro.observe(parent);\n ro.observe(host);\n\n const start = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const animate = () => {\n const now = performance.now();\n const t = (now - start) / 1000;\n\n const mat = materialRef.current!;\n const comp = composerRef.current!;\n\n if (pointerActiveRef.current) {\n velocityRef.current.set(\n currentMouseRef.current.x - mat.uniforms.iMouse.value.x,\n currentMouseRef.current.y - mat.uniforms.iMouse.value.y\n );\n mat.uniforms.iMouse.value.copy(currentMouseRef.current);\n fadeOpacityRef.current = 1.0;\n } else {\n velocityRef.current.multiplyScalar(inertia);\n if (velocityRef.current.lengthSq() > 1e-6) {\n mat.uniforms.iMouse.value.add(velocityRef.current);\n }\n const dt = now - lastMoveTimeRef.current;\n if (dt > fadeDelay) {\n const k = Math.min(1, (dt - fadeDelay) / fadeDuration);\n fadeOpacityRef.current = Math.max(0, 1 - k);\n }\n }\n\n const N = trailBufRef.current.length;\n headRef.current = (headRef.current + 1) % N;\n trailBufRef.current[headRef.current].copy(mat.uniforms.iMouse.value);\n const arr = mat.uniforms.iPrevMouse.value as THREE.Vector2[];\n for (let i = 0; i < N; i++) {\n const srcIdx = (headRef.current - i + N) % N;\n arr[i].copy(trailBufRef.current[srcIdx]);\n }\n\n mat.uniforms.iOpacity.value = fadeOpacityRef.current;\n mat.uniforms.iTime.value = t;\n\n if (filmPassRef.current?.uniforms?.iTime) {\n filmPassRef.current.uniforms.iTime.value = t;\n }\n\n comp.render();\n\n if (!pointerActiveRef.current && fadeOpacityRef.current <= 0.001) {\n runningRef.current = false;\n rafRef.current = null;\n return;\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n const ensureLoop = () => {\n if (!runningRef.current) {\n runningRef.current = true;\n rafRef.current = requestAnimationFrame(animate);\n }\n };\n\n const onPointerMove = (e: PointerEvent) => {\n const rect = parent.getBoundingClientRect();\n const x = THREE.MathUtils.clamp((e.clientX - rect.left) / Math.max(1, rect.width), 0, 1);\n const y = THREE.MathUtils.clamp(1 - (e.clientY - rect.top) / Math.max(1, rect.height), 0, 1);\n currentMouseRef.current.set(x, y);\n pointerActiveRef.current = true;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n const onPointerEnter = () => {\n pointerActiveRef.current = true;\n ensureLoop();\n };\n const onPointerLeave = () => {\n pointerActiveRef.current = false;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n\n parent.addEventListener('pointermove', onPointerMove, { passive: true });\n parent.addEventListener('pointerenter', onPointerEnter, { passive: true });\n parent.addEventListener('pointerleave', onPointerLeave, { passive: true });\n\n ensureLoop();\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n runningRef.current = false;\n rafRef.current = null;\n\n parent.removeEventListener('pointermove', onPointerMove);\n parent.removeEventListener('pointerenter', onPointerEnter);\n parent.removeEventListener('pointerleave', onPointerLeave);\n resizeObsRef.current?.disconnect();\n\n scene.clear();\n geom.dispose();\n material.dispose();\n composer.dispose();\n renderer.dispose();\n\n if (renderer.domElement && renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = prevParentPos;\n }\n };\n }, [\n trailLength,\n inertia,\n grainIntensity,\n bloomStrength,\n bloomRadius,\n bloomThreshold,\n pixelBudget,\n fadeDelay,\n fadeDuration,\n isTouch,\n color,\n brightness,\n mixBlendMode,\n edgeIntensity\n ]);\n\n useEffect(() => {\n if (materialRef.current) {\n const c = new THREE.Color(color);\n (materialRef.current.uniforms.iBaseColor.value as THREE.Vector3).set(c.r, c.g, c.b);\n }\n }, [color]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iBrightness.value = brightness;\n }\n }, [brightness]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iEdgeIntensity.value = edgeIntensity;\n }\n }, [edgeIntensity]);\n\n useEffect(() => {\n if (filmPassRef.current?.uniforms?.intensity) {\n filmPassRef.current.uniforms.intensity.value = grainIntensity;\n }\n }, [grainIntensity]);\n\n useEffect(() => {\n const el = rendererRef.current?.domElement;\n if (!el) return;\n if (mixBlendMode) {\n el.style.mixBlendMode = String(mixBlendMode);\n } else {\n el.style.removeProperty('mix-blend-mode');\n }\n }, [mixBlendMode]);\n\n const mergedStyle = useMemo(() => ({ zIndex, ...style }), [zIndex, style]);\n\n return ;\n};\n\nexport default GhostCursor;\n"
+ "content": "import React, { useEffect, useMemo, useRef } from 'react';\nimport * as THREE from 'three';\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';\nimport { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';\nimport './GhostCursor.css';\n\ntype GhostCursorProps = {\n className?: string;\n style?: React.CSSProperties;\n\n trailLength?: number;\n inertia?: number;\n grainIntensity?: number;\n bloomStrength?: number;\n bloomRadius?: number;\n bloomThreshold?: number;\n\n brightness?: number;\n color?: string;\n mixBlendMode?: React.CSSProperties['mixBlendMode'];\n edgeIntensity?: number;\n\n maxDevicePixelRatio?: number;\n targetPixels?: number;\n fadeDelayMs?: number;\n fadeDurationMs?: number;\n zIndex?: number;\n};\n\nconst GhostCursor: React.FC = ({\n className,\n style,\n trailLength = 50,\n inertia = 0.5,\n grainIntensity = 0.05,\n bloomStrength = 0.1,\n bloomRadius = 1.0,\n bloomThreshold = 0.025,\n\n brightness = 1,\n color = '#B19EEF',\n mixBlendMode = 'screen',\n edgeIntensity = 0,\n\n maxDevicePixelRatio = 0.5,\n targetPixels,\n\n fadeDelayMs,\n fadeDurationMs,\n zIndex = 10\n}) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const composerRef = useRef(null);\n const materialRef = useRef(null);\n const bloomPassRef = useRef(null);\n const filmPassRef = useRef(null);\n\n // Trail circular buffer\n const trailBufRef = useRef([]);\n const headRef = useRef(0);\n\n const rafRef = useRef(null);\n const resizeObsRef = useRef(null);\n const currentMouseRef = useRef(new THREE.Vector2(0.5, 0.5));\n const velocityRef = useRef(new THREE.Vector2(0, 0));\n const fadeOpacityRef = useRef(1.0);\n const lastMoveTimeRef = useRef(typeof performance !== 'undefined' ? performance.now() : Date.now());\n const pointerActiveRef = useRef(false);\n const runningRef = useRef(false);\n\n const isTouch = useMemo(\n () => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0),\n []\n );\n\n const pixelBudget = targetPixels ?? (isTouch ? 0.9e6 : 1.3e6);\n const fadeDelay = fadeDelayMs ?? (isTouch ? 500 : 1000);\n const fadeDuration = fadeDurationMs ?? (isTouch ? 1000 : 1500);\n\n const baseVertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n uniform float iTime;\n uniform vec3 iResolution;\n uniform vec2 iMouse;\n uniform vec2 iPrevMouse[MAX_TRAIL_LENGTH];\n uniform float iOpacity;\n uniform float iScale;\n uniform vec3 iBaseColor;\n uniform float iBrightness;\n uniform float iEdgeIntensity;\n varying vec2 vUv;\n\n float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))) * 43758.5453123); }\n float noise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n f *= f * (3. - 2. * f);\n return mix(mix(hash(i + vec2(0.,0.)), hash(i + vec2(1.,0.)), f.x),\n mix(hash(i + vec2(0.,1.)), hash(i + vec2(1.,1.)), f.x), f.y);\n }\n float fbm(vec2 p){\n float v = 0.0;\n float a = 0.5;\n mat2 m = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n for(int i=0;i<5;i++){\n v += a * noise(p);\n p = m * p * 2.0;\n a *= 0.5;\n }\n return v;\n }\n vec3 tint1(vec3 base){ return mix(base, vec3(1.0), 0.15); }\n vec3 tint2(vec3 base){ return mix(base, vec3(0.8, 0.9, 1.0), 0.25); }\n\n vec4 blob(vec2 p, vec2 mousePos, float intensity, float activity) {\n vec2 q = vec2(fbm(p * iScale + iTime * 0.1), fbm(p * iScale + vec2(5.2,1.3) + iTime * 0.1));\n vec2 r = vec2(fbm(p * iScale + q * 1.5 + iTime * 0.15), fbm(p * iScale + q * 1.5 + vec2(8.3,2.8) + iTime * 0.15));\n\n float smoke = fbm(p * iScale + r * 0.8);\n float radius = 0.5 + 0.3 * (1.0 / iScale);\n float distFactor = 1.0 - smoothstep(0.0, radius * activity, length(p - mousePos));\n float alpha = pow(smoke, 2.5) * distFactor;\n\n vec3 c1 = tint1(iBaseColor);\n vec3 c2 = tint2(iBaseColor);\n vec3 color = mix(c1, c2, sin(iTime * 0.5) * 0.5 + 0.5);\n\n return vec4(color * alpha * intensity, alpha * intensity);\n }\n\n void main() {\n vec2 uv = (gl_FragCoord.xy / iResolution.xy * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n vec2 mouse = (iMouse * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n\n vec3 colorAcc = vec3(0.0);\n float alphaAcc = 0.0;\n\n vec4 b = blob(uv, mouse, 1.0, iOpacity);\n colorAcc += b.rgb;\n alphaAcc += b.a;\n\n for (int i = 0; i < MAX_TRAIL_LENGTH; i++) {\n vec2 pm = (iPrevMouse[i] * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n float t = 1.0 - float(i) / float(MAX_TRAIL_LENGTH);\n t = pow(t, 2.0);\n if (t > 0.01) {\n vec4 bt = blob(uv, pm, t * 0.8, iOpacity);\n colorAcc += bt.rgb;\n alphaAcc += bt.a;\n }\n }\n\n colorAcc *= iBrightness;\n\n vec2 uv01 = gl_FragCoord.xy / iResolution.xy;\n float edgeDist = min(min(uv01.x, 1.0 - uv01.x), min(uv01.y, 1.0 - uv01.y));\n float distFromEdge = clamp(edgeDist * 2.0, 0.0, 1.0);\n float k = clamp(iEdgeIntensity, 0.0, 1.0);\n float edgeMask = mix(1.0 - k, 1.0, distFromEdge);\n\n float outAlpha = clamp(alphaAcc * iOpacity * edgeMask, 0.0, 1.0);\n gl_FragColor = vec4(colorAcc, outAlpha);\n }\n `;\n\n const FilmGrainShader = useMemo(() => {\n return {\n uniforms: {\n tDiffuse: { value: null },\n iTime: { value: 0 },\n intensity: { value: grainIntensity }\n },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float iTime;\n uniform float intensity;\n varying vec2 vUv;\n\n float hash1(float n){ return fract(sin(n)*43758.5453); }\n\n void main(){\n vec4 color = texture2D(tDiffuse, vUv);\n float n = hash1(vUv.x*1000.0 + vUv.y*2000.0 + iTime) * 2.0 - 1.0;\n color.rgb += n * intensity * color.rgb;\n gl_FragColor = color;\n }\n `\n };\n }, [grainIntensity]);\n\n const UnpremultiplyPass = useMemo(\n () =>\n new ShaderPass({\n uniforms: { tDiffuse: { value: null } },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n varying vec2 vUv;\n void main(){\n vec4 c = texture2D(tDiffuse, vUv);\n float a = max(c.a, 1e-5);\n vec3 straight = c.rgb / a;\n gl_FragColor = vec4(clamp(straight, 0.0, 1.0), c.a);\n }\n `\n }),\n []\n );\n\n function calculateScale(el: HTMLElement) {\n const r = el.getBoundingClientRect();\n const base = 600;\n const current = Math.min(Math.max(1, r.width), Math.max(1, r.height));\n return Math.max(0.5, Math.min(2.0, current / base));\n }\n\n useEffect(() => {\n const host = containerRef.current;\n const parent = host?.parentElement;\n if (!host || !parent) return;\n\n const prevParentPos = parent.style.position;\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = 'relative';\n }\n\n const renderer = new THREE.WebGLRenderer({\n antialias: !isTouch,\n alpha: true,\n depth: false,\n stencil: false,\n powerPreference: isTouch ? 'low-power' : 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false\n });\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n renderer.domElement.style.pointerEvents = 'none';\n if (mixBlendMode) {\n renderer.domElement.style.mixBlendMode = String(mixBlendMode);\n } else {\n renderer.domElement.style.removeProperty('mix-blend-mode');\n }\n\n host.appendChild(renderer.domElement);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geom = new THREE.PlaneGeometry(2, 2);\n\n const maxTrail = Math.max(1, Math.floor(trailLength));\n trailBufRef.current = Array.from({ length: maxTrail }, () => new THREE.Vector2(0.5, 0.5));\n headRef.current = 0;\n\n const baseColor = new THREE.Color(color);\n\n const material = new THREE.ShaderMaterial({\n defines: { MAX_TRAIL_LENGTH: maxTrail },\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new THREE.Vector3(1, 1, 1) },\n iMouse: { value: new THREE.Vector2(0.5, 0.5) },\n iPrevMouse: { value: trailBufRef.current.map(v => v.clone()) },\n iOpacity: { value: 1.0 },\n iScale: { value: 1.0 },\n iBaseColor: { value: new THREE.Vector3(baseColor.r, baseColor.g, baseColor.b) },\n iBrightness: { value: brightness },\n iEdgeIntensity: { value: edgeIntensity }\n },\n vertexShader: baseVertexShader,\n fragmentShader,\n transparent: true,\n depthTest: false,\n depthWrite: false\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geom, material);\n scene.add(mesh);\n\n const composer = new EffectComposer(renderer);\n composerRef.current = composer;\n\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloomPass = new UnrealBloomPass(new THREE.Vector2(1, 1), bloomStrength, bloomRadius, bloomThreshold);\n bloomPassRef.current = bloomPass;\n composer.addPass(bloomPass);\n\n const filmPass = new ShaderPass(FilmGrainShader as any);\n filmPassRef.current = filmPass;\n composer.addPass(filmPass);\n\n composer.addPass(UnpremultiplyPass);\n\n const resize = () => {\n const rect = host.getBoundingClientRect();\n const cssW = Math.max(1, Math.floor(rect.width));\n const cssH = Math.max(1, Math.floor(rect.height));\n\n const currentDPR = Math.min(\n typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n maxDevicePixelRatio\n );\n const need = cssW * cssH * currentDPR * currentDPR;\n const scale = need <= pixelBudget ? 1 : Math.max(0.5, Math.min(1, Math.sqrt(pixelBudget / Math.max(1, need))));\n const pixelRatio = currentDPR * scale;\n\n renderer.setPixelRatio(pixelRatio);\n renderer.setSize(cssW, cssH, false);\n\n composer.setPixelRatio?.(pixelRatio);\n composer.setSize(cssW, cssH);\n\n const wpx = Math.max(1, Math.floor(cssW * pixelRatio));\n const hpx = Math.max(1, Math.floor(cssH * pixelRatio));\n material.uniforms.iResolution.value.set(wpx, hpx, 1);\n material.uniforms.iScale.value = calculateScale(host);\n bloomPass.setSize(wpx, hpx);\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n resizeObsRef.current = ro;\n ro.observe(parent);\n ro.observe(host);\n\n const start = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const animate = () => {\n const now = performance.now();\n const t = (now - start) / 1000;\n\n const mat = materialRef.current!;\n const comp = composerRef.current!;\n\n if (pointerActiveRef.current) {\n velocityRef.current.set(\n currentMouseRef.current.x - mat.uniforms.iMouse.value.x,\n currentMouseRef.current.y - mat.uniforms.iMouse.value.y\n );\n mat.uniforms.iMouse.value.copy(currentMouseRef.current);\n fadeOpacityRef.current = 1.0;\n } else {\n velocityRef.current.multiplyScalar(inertia);\n if (velocityRef.current.lengthSq() > 1e-6) {\n mat.uniforms.iMouse.value.add(velocityRef.current);\n }\n const dt = now - lastMoveTimeRef.current;\n if (dt > fadeDelay) {\n const k = Math.min(1, (dt - fadeDelay) / fadeDuration);\n fadeOpacityRef.current = Math.max(0, 1 - k);\n }\n }\n\n const N = trailBufRef.current.length;\n headRef.current = (headRef.current + 1) % N;\n trailBufRef.current[headRef.current].copy(mat.uniforms.iMouse.value);\n const arr = mat.uniforms.iPrevMouse.value as THREE.Vector2[];\n for (let i = 0; i < N; i++) {\n const srcIdx = (headRef.current - i + N) % N;\n arr[i].copy(trailBufRef.current[srcIdx]);\n }\n\n mat.uniforms.iOpacity.value = fadeOpacityRef.current;\n mat.uniforms.iTime.value = t;\n\n if (filmPassRef.current?.uniforms?.iTime) {\n filmPassRef.current.uniforms.iTime.value = t;\n }\n\n comp.render();\n\n if (!pointerActiveRef.current && fadeOpacityRef.current <= 0.001) {\n runningRef.current = false;\n rafRef.current = null;\n return;\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n const ensureLoop = () => {\n if (!runningRef.current) {\n runningRef.current = true;\n rafRef.current = requestAnimationFrame(animate);\n }\n };\n\n const onPointerMove = (e: PointerEvent) => {\n const rect = parent.getBoundingClientRect();\n const x = THREE.MathUtils.clamp((e.clientX - rect.left) / Math.max(1, rect.width), 0, 1);\n const y = THREE.MathUtils.clamp(1 - (e.clientY - rect.top) / Math.max(1, rect.height), 0, 1);\n currentMouseRef.current.set(x, y);\n pointerActiveRef.current = true;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n const onPointerEnter = () => {\n pointerActiveRef.current = true;\n ensureLoop();\n };\n const onPointerLeave = () => {\n pointerActiveRef.current = false;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n\n parent.addEventListener('pointermove', onPointerMove, { passive: true });\n parent.addEventListener('pointerenter', onPointerEnter, { passive: true });\n parent.addEventListener('pointerleave', onPointerLeave, { passive: true });\n\n ensureLoop();\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n runningRef.current = false;\n rafRef.current = null;\n\n parent.removeEventListener('pointermove', onPointerMove);\n parent.removeEventListener('pointerenter', onPointerEnter);\n parent.removeEventListener('pointerleave', onPointerLeave);\n resizeObsRef.current?.disconnect();\n\n scene.clear();\n geom.dispose();\n material.dispose();\n composer.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n\n if (renderer.domElement && renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = prevParentPos;\n }\n };\n }, [\n trailLength,\n inertia,\n grainIntensity,\n bloomStrength,\n bloomRadius,\n bloomThreshold,\n pixelBudget,\n fadeDelay,\n fadeDuration,\n isTouch,\n color,\n brightness,\n mixBlendMode,\n edgeIntensity\n ]);\n\n useEffect(() => {\n if (materialRef.current) {\n const c = new THREE.Color(color);\n (materialRef.current.uniforms.iBaseColor.value as THREE.Vector3).set(c.r, c.g, c.b);\n }\n }, [color]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iBrightness.value = brightness;\n }\n }, [brightness]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iEdgeIntensity.value = edgeIntensity;\n }\n }, [edgeIntensity]);\n\n useEffect(() => {\n if (filmPassRef.current?.uniforms?.intensity) {\n filmPassRef.current.uniforms.intensity.value = grainIntensity;\n }\n }, [grainIntensity]);\n\n useEffect(() => {\n const el = rendererRef.current?.domElement;\n if (!el) return;\n if (mixBlendMode) {\n el.style.mixBlendMode = String(mixBlendMode);\n } else {\n el.style.removeProperty('mix-blend-mode');\n }\n }, [mixBlendMode]);\n\n const mergedStyle = useMemo(() => ({ zIndex, ...style }), [zIndex, style]);\n\n return ;\n};\n\nexport default GhostCursor;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/GhostCursor-TS-TW.json b/public/r/GhostCursor-TS-TW.json
index cf7088c8..eec85cd9 100644
--- a/public/r/GhostCursor-TS-TW.json
+++ b/public/r/GhostCursor-TS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "GhostCursor/GhostCursor.tsx",
- "content": "import React, { useEffect, useMemo, useRef } from 'react';\nimport * as THREE from 'three';\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';\nimport { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';\n\ntype GhostCursorProps = {\n className?: string;\n style?: React.CSSProperties;\n\n trailLength?: number;\n inertia?: number;\n grainIntensity?: number;\n bloomStrength?: number;\n bloomRadius?: number;\n bloomThreshold?: number;\n\n brightness?: number;\n color?: string;\n mixBlendMode?: React.CSSProperties['mixBlendMode'];\n edgeIntensity?: number;\n\n maxDevicePixelRatio?: number;\n targetPixels?: number;\n fadeDelayMs?: number;\n fadeDurationMs?: number;\n zIndex?: number;\n};\n\nconst GhostCursor: React.FC = ({\n className,\n style,\n trailLength = 50,\n inertia = 0.5,\n grainIntensity = 0.05,\n bloomStrength = 0.1,\n bloomRadius = 1.0,\n bloomThreshold = 0.025,\n\n brightness = 1,\n color = '#B19EEF',\n mixBlendMode = 'screen',\n edgeIntensity = 0,\n\n maxDevicePixelRatio = 0.5,\n targetPixels,\n\n fadeDelayMs,\n fadeDurationMs,\n zIndex = 10\n}) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const composerRef = useRef(null);\n const materialRef = useRef(null);\n const bloomPassRef = useRef(null);\n const filmPassRef = useRef(null);\n\n // Trail circular buffer\n const trailBufRef = useRef([]);\n const headRef = useRef(0);\n\n const rafRef = useRef(null);\n const resizeObsRef = useRef(null);\n const currentMouseRef = useRef(new THREE.Vector2(0.5, 0.5));\n const velocityRef = useRef(new THREE.Vector2(0, 0));\n const fadeOpacityRef = useRef(1.0);\n const lastMoveTimeRef = useRef(typeof performance !== 'undefined' ? performance.now() : Date.now());\n const pointerActiveRef = useRef(false);\n const runningRef = useRef(false);\n\n const isTouch = useMemo(\n () => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0),\n []\n );\n\n const pixelBudget = targetPixels ?? (isTouch ? 0.9e6 : 1.3e6);\n const fadeDelay = fadeDelayMs ?? (isTouch ? 500 : 1000);\n const fadeDuration = fadeDurationMs ?? (isTouch ? 1000 : 1500);\n\n const baseVertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n uniform float iTime;\n uniform vec3 iResolution;\n uniform vec2 iMouse;\n uniform vec2 iPrevMouse[MAX_TRAIL_LENGTH];\n uniform float iOpacity;\n uniform float iScale;\n uniform vec3 iBaseColor;\n uniform float iBrightness;\n uniform float iEdgeIntensity;\n varying vec2 vUv;\n\n float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))) * 43758.5453123); }\n float noise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n f *= f * (3. - 2. * f);\n return mix(mix(hash(i + vec2(0.,0.)), hash(i + vec2(1.,0.)), f.x),\n mix(hash(i + vec2(0.,1.)), hash(i + vec2(1.,1.)), f.x), f.y);\n }\n float fbm(vec2 p){\n float v = 0.0;\n float a = 0.5;\n mat2 m = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n for(int i=0;i<5;i++){\n v += a * noise(p);\n p = m * p * 2.0;\n a *= 0.5;\n }\n return v;\n }\n vec3 tint1(vec3 base){ return mix(base, vec3(1.0), 0.15); }\n vec3 tint2(vec3 base){ return mix(base, vec3(0.8, 0.9, 1.0), 0.25); }\n\n vec4 blob(vec2 p, vec2 mousePos, float intensity, float activity) {\n vec2 q = vec2(fbm(p * iScale + iTime * 0.1), fbm(p * iScale + vec2(5.2,1.3) + iTime * 0.1));\n vec2 r = vec2(fbm(p * iScale + q * 1.5 + iTime * 0.15), fbm(p * iScale + q * 1.5 + vec2(8.3,2.8) + iTime * 0.15));\n\n float smoke = fbm(p * iScale + r * 0.8);\n float radius = 0.5 + 0.3 * (1.0 / iScale);\n float distFactor = 1.0 - smoothstep(0.0, radius * activity, length(p - mousePos));\n float alpha = pow(smoke, 2.5) * distFactor;\n\n vec3 c1 = tint1(iBaseColor);\n vec3 c2 = tint2(iBaseColor);\n vec3 color = mix(c1, c2, sin(iTime * 0.5) * 0.5 + 0.5);\n\n return vec4(color * alpha * intensity, alpha * intensity);\n }\n\n void main() {\n vec2 uv = (gl_FragCoord.xy / iResolution.xy * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n vec2 mouse = (iMouse * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n\n vec3 colorAcc = vec3(0.0);\n float alphaAcc = 0.0;\n\n vec4 b = blob(uv, mouse, 1.0, iOpacity);\n colorAcc += b.rgb;\n alphaAcc += b.a;\n\n for (int i = 0; i < MAX_TRAIL_LENGTH; i++) {\n vec2 pm = (iPrevMouse[i] * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n float t = 1.0 - float(i) / float(MAX_TRAIL_LENGTH);\n t = pow(t, 2.0);\n if (t > 0.01) {\n vec4 bt = blob(uv, pm, t * 0.8, iOpacity);\n colorAcc += bt.rgb;\n alphaAcc += bt.a;\n }\n }\n\n colorAcc *= iBrightness;\n\n vec2 uv01 = gl_FragCoord.xy / iResolution.xy;\n float edgeDist = min(min(uv01.x, 1.0 - uv01.x), min(uv01.y, 1.0 - uv01.y));\n float distFromEdge = clamp(edgeDist * 2.0, 0.0, 1.0);\n float k = clamp(iEdgeIntensity, 0.0, 1.0);\n float edgeMask = mix(1.0 - k, 1.0, distFromEdge);\n\n float outAlpha = clamp(alphaAcc * iOpacity * edgeMask, 0.0, 1.0);\n gl_FragColor = vec4(colorAcc, outAlpha);\n }\n `;\n\n const FilmGrainShader = useMemo(() => {\n return {\n uniforms: {\n tDiffuse: { value: null },\n iTime: { value: 0 },\n intensity: { value: grainIntensity }\n },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float iTime;\n uniform float intensity;\n varying vec2 vUv;\n\n float hash1(float n){ return fract(sin(n)*43758.5453); }\n\n void main(){\n vec4 color = texture2D(tDiffuse, vUv);\n float n = hash1(vUv.x*1000.0 + vUv.y*2000.0 + iTime) * 2.0 - 1.0;\n color.rgb += n * intensity * color.rgb;\n gl_FragColor = color;\n }\n `\n };\n }, [grainIntensity]);\n\n const UnpremultiplyPass = useMemo(\n () =>\n new ShaderPass({\n uniforms: { tDiffuse: { value: null } },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n varying vec2 vUv;\n void main(){\n vec4 c = texture2D(tDiffuse, vUv);\n float a = max(c.a, 1e-5);\n vec3 straight = c.rgb / a;\n gl_FragColor = vec4(clamp(straight, 0.0, 1.0), c.a);\n }\n `\n }),\n []\n );\n\n function calculateScale(el: HTMLElement) {\n const r = el.getBoundingClientRect();\n const base = 600;\n const current = Math.min(Math.max(1, r.width), Math.max(1, r.height));\n return Math.max(0.5, Math.min(2.0, current / base));\n }\n\n useEffect(() => {\n const host = containerRef.current;\n const parent = host?.parentElement;\n if (!host || !parent) return;\n\n const prevParentPos = parent.style.position;\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = 'relative';\n }\n\n const renderer = new THREE.WebGLRenderer({\n antialias: !isTouch,\n alpha: true,\n depth: false,\n stencil: false,\n powerPreference: isTouch ? 'low-power' : 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false\n });\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n renderer.domElement.style.pointerEvents = 'none';\n if (mixBlendMode) {\n renderer.domElement.style.mixBlendMode = String(mixBlendMode);\n } else {\n renderer.domElement.style.removeProperty('mix-blend-mode');\n }\n\n renderer.domElement.style.display = 'block';\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.background = 'transparent';\n\n host.appendChild(renderer.domElement);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geom = new THREE.PlaneGeometry(2, 2);\n\n const maxTrail = Math.max(1, Math.floor(trailLength));\n trailBufRef.current = Array.from({ length: maxTrail }, () => new THREE.Vector2(0.5, 0.5));\n headRef.current = 0;\n\n const baseColor = new THREE.Color(color);\n\n const material = new THREE.ShaderMaterial({\n defines: { MAX_TRAIL_LENGTH: maxTrail },\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new THREE.Vector3(1, 1, 1) },\n iMouse: { value: new THREE.Vector2(0.5, 0.5) },\n iPrevMouse: { value: trailBufRef.current.map(v => v.clone()) },\n iOpacity: { value: 1.0 },\n iScale: { value: 1.0 },\n iBaseColor: { value: new THREE.Vector3(baseColor.r, baseColor.g, baseColor.b) },\n iBrightness: { value: brightness },\n iEdgeIntensity: { value: edgeIntensity }\n },\n vertexShader: baseVertexShader,\n fragmentShader,\n transparent: true,\n depthTest: false,\n depthWrite: false\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geom, material);\n scene.add(mesh);\n\n const composer = new EffectComposer(renderer);\n composerRef.current = composer;\n\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloomPass = new UnrealBloomPass(new THREE.Vector2(1, 1), bloomStrength, bloomRadius, bloomThreshold);\n bloomPassRef.current = bloomPass;\n composer.addPass(bloomPass);\n\n const filmPass = new ShaderPass(FilmGrainShader as any);\n filmPassRef.current = filmPass;\n composer.addPass(filmPass);\n\n composer.addPass(UnpremultiplyPass);\n\n const resize = () => {\n const rect = host.getBoundingClientRect();\n const cssW = Math.max(1, Math.floor(rect.width));\n const cssH = Math.max(1, Math.floor(rect.height));\n\n const currentDPR = Math.min(\n typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n maxDevicePixelRatio\n );\n const need = cssW * cssH * currentDPR * currentDPR;\n const scale = need <= pixelBudget ? 1 : Math.max(0.5, Math.min(1, Math.sqrt(pixelBudget / Math.max(1, need))));\n const pixelRatio = currentDPR * scale;\n\n renderer.setPixelRatio(pixelRatio);\n renderer.setSize(cssW, cssH, false);\n\n composer.setPixelRatio?.(pixelRatio);\n composer.setSize(cssW, cssH);\n\n const wpx = Math.max(1, Math.floor(cssW * pixelRatio));\n const hpx = Math.max(1, Math.floor(cssH * pixelRatio));\n material.uniforms.iResolution.value.set(wpx, hpx, 1);\n material.uniforms.iScale.value = calculateScale(host);\n bloomPass.setSize(wpx, hpx);\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n resizeObsRef.current = ro;\n ro.observe(parent);\n ro.observe(host);\n\n const start = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const animate = () => {\n const now = performance.now();\n const t = (now - start) / 1000;\n\n const mat = materialRef.current!;\n const comp = composerRef.current!;\n\n if (pointerActiveRef.current) {\n velocityRef.current.set(\n currentMouseRef.current.x - mat.uniforms.iMouse.value.x,\n currentMouseRef.current.y - mat.uniforms.iMouse.value.y\n );\n mat.uniforms.iMouse.value.copy(currentMouseRef.current);\n fadeOpacityRef.current = 1.0;\n } else {\n velocityRef.current.multiplyScalar(inertia);\n if (velocityRef.current.lengthSq() > 1e-6) {\n mat.uniforms.iMouse.value.add(velocityRef.current);\n }\n const dt = now - lastMoveTimeRef.current;\n if (dt > fadeDelay) {\n const k = Math.min(1, (dt - fadeDelay) / fadeDuration);\n fadeOpacityRef.current = Math.max(0, 1 - k);\n }\n }\n\n const N = trailBufRef.current.length;\n headRef.current = (headRef.current + 1) % N;\n trailBufRef.current[headRef.current].copy(mat.uniforms.iMouse.value);\n const arr = mat.uniforms.iPrevMouse.value as THREE.Vector2[];\n for (let i = 0; i < N; i++) {\n const srcIdx = (headRef.current - i + N) % N;\n arr[i].copy(trailBufRef.current[srcIdx]);\n }\n\n mat.uniforms.iOpacity.value = fadeOpacityRef.current;\n mat.uniforms.iTime.value = t;\n\n if (filmPassRef.current?.uniforms?.iTime) {\n filmPassRef.current.uniforms.iTime.value = t;\n }\n\n comp.render();\n\n if (!pointerActiveRef.current && fadeOpacityRef.current <= 0.001) {\n runningRef.current = false;\n rafRef.current = null;\n return;\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n const ensureLoop = () => {\n if (!runningRef.current) {\n runningRef.current = true;\n rafRef.current = requestAnimationFrame(animate);\n }\n };\n\n const onPointerMove = (e: PointerEvent) => {\n const rect = parent.getBoundingClientRect();\n const x = THREE.MathUtils.clamp((e.clientX - rect.left) / Math.max(1, rect.width), 0, 1);\n const y = THREE.MathUtils.clamp(1 - (e.clientY - rect.top) / Math.max(1, rect.height), 0, 1);\n currentMouseRef.current.set(x, y);\n pointerActiveRef.current = true;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n const onPointerEnter = () => {\n pointerActiveRef.current = true;\n ensureLoop();\n };\n const onPointerLeave = () => {\n pointerActiveRef.current = false;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n\n parent.addEventListener('pointermove', onPointerMove, { passive: true });\n parent.addEventListener('pointerenter', onPointerEnter, { passive: true });\n parent.addEventListener('pointerleave', onPointerLeave, { passive: true });\n\n ensureLoop();\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n runningRef.current = false;\n rafRef.current = null;\n\n parent.removeEventListener('pointermove', onPointerMove);\n parent.removeEventListener('pointerenter', onPointerEnter);\n parent.removeEventListener('pointerleave', onPointerLeave);\n resizeObsRef.current?.disconnect();\n\n scene.clear();\n geom.dispose();\n material.dispose();\n composer.dispose();\n renderer.dispose();\n\n if (renderer.domElement && renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = prevParentPos;\n }\n };\n }, [\n trailLength,\n inertia,\n grainIntensity,\n bloomStrength,\n bloomRadius,\n bloomThreshold,\n pixelBudget,\n fadeDelay,\n fadeDuration,\n isTouch,\n color,\n brightness,\n mixBlendMode,\n edgeIntensity\n ]);\n\n useEffect(() => {\n if (materialRef.current) {\n const c = new THREE.Color(color);\n (materialRef.current.uniforms.iBaseColor.value as THREE.Vector3).set(c.r, c.g, c.b);\n }\n }, [color]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iBrightness.value = brightness;\n }\n }, [brightness]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iEdgeIntensity.value = edgeIntensity;\n }\n }, [edgeIntensity]);\n\n useEffect(() => {\n if (filmPassRef.current?.uniforms?.intensity) {\n filmPassRef.current.uniforms.intensity.value = grainIntensity;\n }\n }, [grainIntensity]);\n\n useEffect(() => {\n const el = rendererRef.current?.domElement;\n if (!el) return;\n if (mixBlendMode) {\n el.style.mixBlendMode = String(mixBlendMode);\n } else {\n el.style.removeProperty('mix-blend-mode');\n }\n }, [mixBlendMode]);\n\n const mergedStyle = useMemo(() => ({ zIndex, ...style }), [zIndex, style]);\n\n return (\n \n );\n};\n\nexport default GhostCursor;\n"
+ "content": "import React, { useEffect, useMemo, useRef } from 'react';\nimport * as THREE from 'three';\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';\nimport { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';\n\ntype GhostCursorProps = {\n className?: string;\n style?: React.CSSProperties;\n\n trailLength?: number;\n inertia?: number;\n grainIntensity?: number;\n bloomStrength?: number;\n bloomRadius?: number;\n bloomThreshold?: number;\n\n brightness?: number;\n color?: string;\n mixBlendMode?: React.CSSProperties['mixBlendMode'];\n edgeIntensity?: number;\n\n maxDevicePixelRatio?: number;\n targetPixels?: number;\n fadeDelayMs?: number;\n fadeDurationMs?: number;\n zIndex?: number;\n};\n\nconst GhostCursor: React.FC = ({\n className,\n style,\n trailLength = 50,\n inertia = 0.5,\n grainIntensity = 0.05,\n bloomStrength = 0.1,\n bloomRadius = 1.0,\n bloomThreshold = 0.025,\n\n brightness = 1,\n color = '#B19EEF',\n mixBlendMode = 'screen',\n edgeIntensity = 0,\n\n maxDevicePixelRatio = 0.5,\n targetPixels,\n\n fadeDelayMs,\n fadeDurationMs,\n zIndex = 10\n}) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const composerRef = useRef(null);\n const materialRef = useRef(null);\n const bloomPassRef = useRef(null);\n const filmPassRef = useRef(null);\n\n // Trail circular buffer\n const trailBufRef = useRef([]);\n const headRef = useRef(0);\n\n const rafRef = useRef(null);\n const resizeObsRef = useRef(null);\n const currentMouseRef = useRef(new THREE.Vector2(0.5, 0.5));\n const velocityRef = useRef(new THREE.Vector2(0, 0));\n const fadeOpacityRef = useRef(1.0);\n const lastMoveTimeRef = useRef(typeof performance !== 'undefined' ? performance.now() : Date.now());\n const pointerActiveRef = useRef(false);\n const runningRef = useRef(false);\n\n const isTouch = useMemo(\n () => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0),\n []\n );\n\n const pixelBudget = targetPixels ?? (isTouch ? 0.9e6 : 1.3e6);\n const fadeDelay = fadeDelayMs ?? (isTouch ? 500 : 1000);\n const fadeDuration = fadeDurationMs ?? (isTouch ? 1000 : 1500);\n\n const baseVertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n uniform float iTime;\n uniform vec3 iResolution;\n uniform vec2 iMouse;\n uniform vec2 iPrevMouse[MAX_TRAIL_LENGTH];\n uniform float iOpacity;\n uniform float iScale;\n uniform vec3 iBaseColor;\n uniform float iBrightness;\n uniform float iEdgeIntensity;\n varying vec2 vUv;\n\n float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))) * 43758.5453123); }\n float noise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n f *= f * (3. - 2. * f);\n return mix(mix(hash(i + vec2(0.,0.)), hash(i + vec2(1.,0.)), f.x),\n mix(hash(i + vec2(0.,1.)), hash(i + vec2(1.,1.)), f.x), f.y);\n }\n float fbm(vec2 p){\n float v = 0.0;\n float a = 0.5;\n mat2 m = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n for(int i=0;i<5;i++){\n v += a * noise(p);\n p = m * p * 2.0;\n a *= 0.5;\n }\n return v;\n }\n vec3 tint1(vec3 base){ return mix(base, vec3(1.0), 0.15); }\n vec3 tint2(vec3 base){ return mix(base, vec3(0.8, 0.9, 1.0), 0.25); }\n\n vec4 blob(vec2 p, vec2 mousePos, float intensity, float activity) {\n vec2 q = vec2(fbm(p * iScale + iTime * 0.1), fbm(p * iScale + vec2(5.2,1.3) + iTime * 0.1));\n vec2 r = vec2(fbm(p * iScale + q * 1.5 + iTime * 0.15), fbm(p * iScale + q * 1.5 + vec2(8.3,2.8) + iTime * 0.15));\n\n float smoke = fbm(p * iScale + r * 0.8);\n float radius = 0.5 + 0.3 * (1.0 / iScale);\n float distFactor = 1.0 - smoothstep(0.0, radius * activity, length(p - mousePos));\n float alpha = pow(smoke, 2.5) * distFactor;\n\n vec3 c1 = tint1(iBaseColor);\n vec3 c2 = tint2(iBaseColor);\n vec3 color = mix(c1, c2, sin(iTime * 0.5) * 0.5 + 0.5);\n\n return vec4(color * alpha * intensity, alpha * intensity);\n }\n\n void main() {\n vec2 uv = (gl_FragCoord.xy / iResolution.xy * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n vec2 mouse = (iMouse * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n\n vec3 colorAcc = vec3(0.0);\n float alphaAcc = 0.0;\n\n vec4 b = blob(uv, mouse, 1.0, iOpacity);\n colorAcc += b.rgb;\n alphaAcc += b.a;\n\n for (int i = 0; i < MAX_TRAIL_LENGTH; i++) {\n vec2 pm = (iPrevMouse[i] * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n float t = 1.0 - float(i) / float(MAX_TRAIL_LENGTH);\n t = pow(t, 2.0);\n if (t > 0.01) {\n vec4 bt = blob(uv, pm, t * 0.8, iOpacity);\n colorAcc += bt.rgb;\n alphaAcc += bt.a;\n }\n }\n\n colorAcc *= iBrightness;\n\n vec2 uv01 = gl_FragCoord.xy / iResolution.xy;\n float edgeDist = min(min(uv01.x, 1.0 - uv01.x), min(uv01.y, 1.0 - uv01.y));\n float distFromEdge = clamp(edgeDist * 2.0, 0.0, 1.0);\n float k = clamp(iEdgeIntensity, 0.0, 1.0);\n float edgeMask = mix(1.0 - k, 1.0, distFromEdge);\n\n float outAlpha = clamp(alphaAcc * iOpacity * edgeMask, 0.0, 1.0);\n gl_FragColor = vec4(colorAcc, outAlpha);\n }\n `;\n\n const FilmGrainShader = useMemo(() => {\n return {\n uniforms: {\n tDiffuse: { value: null },\n iTime: { value: 0 },\n intensity: { value: grainIntensity }\n },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float iTime;\n uniform float intensity;\n varying vec2 vUv;\n\n float hash1(float n){ return fract(sin(n)*43758.5453); }\n\n void main(){\n vec4 color = texture2D(tDiffuse, vUv);\n float n = hash1(vUv.x*1000.0 + vUv.y*2000.0 + iTime) * 2.0 - 1.0;\n color.rgb += n * intensity * color.rgb;\n gl_FragColor = color;\n }\n `\n };\n }, [grainIntensity]);\n\n const UnpremultiplyPass = useMemo(\n () =>\n new ShaderPass({\n uniforms: { tDiffuse: { value: null } },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n varying vec2 vUv;\n void main(){\n vec4 c = texture2D(tDiffuse, vUv);\n float a = max(c.a, 1e-5);\n vec3 straight = c.rgb / a;\n gl_FragColor = vec4(clamp(straight, 0.0, 1.0), c.a);\n }\n `\n }),\n []\n );\n\n function calculateScale(el: HTMLElement) {\n const r = el.getBoundingClientRect();\n const base = 600;\n const current = Math.min(Math.max(1, r.width), Math.max(1, r.height));\n return Math.max(0.5, Math.min(2.0, current / base));\n }\n\n useEffect(() => {\n const host = containerRef.current;\n const parent = host?.parentElement;\n if (!host || !parent) return;\n\n const prevParentPos = parent.style.position;\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = 'relative';\n }\n\n const renderer = new THREE.WebGLRenderer({\n antialias: !isTouch,\n alpha: true,\n depth: false,\n stencil: false,\n powerPreference: isTouch ? 'low-power' : 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false\n });\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n renderer.domElement.style.pointerEvents = 'none';\n if (mixBlendMode) {\n renderer.domElement.style.mixBlendMode = String(mixBlendMode);\n } else {\n renderer.domElement.style.removeProperty('mix-blend-mode');\n }\n\n renderer.domElement.style.display = 'block';\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.background = 'transparent';\n\n host.appendChild(renderer.domElement);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geom = new THREE.PlaneGeometry(2, 2);\n\n const maxTrail = Math.max(1, Math.floor(trailLength));\n trailBufRef.current = Array.from({ length: maxTrail }, () => new THREE.Vector2(0.5, 0.5));\n headRef.current = 0;\n\n const baseColor = new THREE.Color(color);\n\n const material = new THREE.ShaderMaterial({\n defines: { MAX_TRAIL_LENGTH: maxTrail },\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new THREE.Vector3(1, 1, 1) },\n iMouse: { value: new THREE.Vector2(0.5, 0.5) },\n iPrevMouse: { value: trailBufRef.current.map(v => v.clone()) },\n iOpacity: { value: 1.0 },\n iScale: { value: 1.0 },\n iBaseColor: { value: new THREE.Vector3(baseColor.r, baseColor.g, baseColor.b) },\n iBrightness: { value: brightness },\n iEdgeIntensity: { value: edgeIntensity }\n },\n vertexShader: baseVertexShader,\n fragmentShader,\n transparent: true,\n depthTest: false,\n depthWrite: false\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geom, material);\n scene.add(mesh);\n\n const composer = new EffectComposer(renderer);\n composerRef.current = composer;\n\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloomPass = new UnrealBloomPass(new THREE.Vector2(1, 1), bloomStrength, bloomRadius, bloomThreshold);\n bloomPassRef.current = bloomPass;\n composer.addPass(bloomPass);\n\n const filmPass = new ShaderPass(FilmGrainShader as any);\n filmPassRef.current = filmPass;\n composer.addPass(filmPass);\n\n composer.addPass(UnpremultiplyPass);\n\n const resize = () => {\n const rect = host.getBoundingClientRect();\n const cssW = Math.max(1, Math.floor(rect.width));\n const cssH = Math.max(1, Math.floor(rect.height));\n\n const currentDPR = Math.min(\n typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n maxDevicePixelRatio\n );\n const need = cssW * cssH * currentDPR * currentDPR;\n const scale = need <= pixelBudget ? 1 : Math.max(0.5, Math.min(1, Math.sqrt(pixelBudget / Math.max(1, need))));\n const pixelRatio = currentDPR * scale;\n\n renderer.setPixelRatio(pixelRatio);\n renderer.setSize(cssW, cssH, false);\n\n composer.setPixelRatio?.(pixelRatio);\n composer.setSize(cssW, cssH);\n\n const wpx = Math.max(1, Math.floor(cssW * pixelRatio));\n const hpx = Math.max(1, Math.floor(cssH * pixelRatio));\n material.uniforms.iResolution.value.set(wpx, hpx, 1);\n material.uniforms.iScale.value = calculateScale(host);\n bloomPass.setSize(wpx, hpx);\n };\n\n resize();\n const ro = new ResizeObserver(resize);\n resizeObsRef.current = ro;\n ro.observe(parent);\n ro.observe(host);\n\n const start = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const animate = () => {\n const now = performance.now();\n const t = (now - start) / 1000;\n\n const mat = materialRef.current!;\n const comp = composerRef.current!;\n\n if (pointerActiveRef.current) {\n velocityRef.current.set(\n currentMouseRef.current.x - mat.uniforms.iMouse.value.x,\n currentMouseRef.current.y - mat.uniforms.iMouse.value.y\n );\n mat.uniforms.iMouse.value.copy(currentMouseRef.current);\n fadeOpacityRef.current = 1.0;\n } else {\n velocityRef.current.multiplyScalar(inertia);\n if (velocityRef.current.lengthSq() > 1e-6) {\n mat.uniforms.iMouse.value.add(velocityRef.current);\n }\n const dt = now - lastMoveTimeRef.current;\n if (dt > fadeDelay) {\n const k = Math.min(1, (dt - fadeDelay) / fadeDuration);\n fadeOpacityRef.current = Math.max(0, 1 - k);\n }\n }\n\n const N = trailBufRef.current.length;\n headRef.current = (headRef.current + 1) % N;\n trailBufRef.current[headRef.current].copy(mat.uniforms.iMouse.value);\n const arr = mat.uniforms.iPrevMouse.value as THREE.Vector2[];\n for (let i = 0; i < N; i++) {\n const srcIdx = (headRef.current - i + N) % N;\n arr[i].copy(trailBufRef.current[srcIdx]);\n }\n\n mat.uniforms.iOpacity.value = fadeOpacityRef.current;\n mat.uniforms.iTime.value = t;\n\n if (filmPassRef.current?.uniforms?.iTime) {\n filmPassRef.current.uniforms.iTime.value = t;\n }\n\n comp.render();\n\n if (!pointerActiveRef.current && fadeOpacityRef.current <= 0.001) {\n runningRef.current = false;\n rafRef.current = null;\n return;\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n const ensureLoop = () => {\n if (!runningRef.current) {\n runningRef.current = true;\n rafRef.current = requestAnimationFrame(animate);\n }\n };\n\n const onPointerMove = (e: PointerEvent) => {\n const rect = parent.getBoundingClientRect();\n const x = THREE.MathUtils.clamp((e.clientX - rect.left) / Math.max(1, rect.width), 0, 1);\n const y = THREE.MathUtils.clamp(1 - (e.clientY - rect.top) / Math.max(1, rect.height), 0, 1);\n currentMouseRef.current.set(x, y);\n pointerActiveRef.current = true;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n const onPointerEnter = () => {\n pointerActiveRef.current = true;\n ensureLoop();\n };\n const onPointerLeave = () => {\n pointerActiveRef.current = false;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n\n parent.addEventListener('pointermove', onPointerMove, { passive: true });\n parent.addEventListener('pointerenter', onPointerEnter, { passive: true });\n parent.addEventListener('pointerleave', onPointerLeave, { passive: true });\n\n ensureLoop();\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n runningRef.current = false;\n rafRef.current = null;\n\n parent.removeEventListener('pointermove', onPointerMove);\n parent.removeEventListener('pointerenter', onPointerEnter);\n parent.removeEventListener('pointerleave', onPointerLeave);\n resizeObsRef.current?.disconnect();\n\n scene.clear();\n geom.dispose();\n material.dispose();\n composer.dispose();\n renderer.dispose();\n renderer.forceContextLoss();\n\n if (renderer.domElement && renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = prevParentPos;\n }\n };\n }, [\n trailLength,\n inertia,\n grainIntensity,\n bloomStrength,\n bloomRadius,\n bloomThreshold,\n pixelBudget,\n fadeDelay,\n fadeDuration,\n isTouch,\n color,\n brightness,\n mixBlendMode,\n edgeIntensity\n ]);\n\n useEffect(() => {\n if (materialRef.current) {\n const c = new THREE.Color(color);\n (materialRef.current.uniforms.iBaseColor.value as THREE.Vector3).set(c.r, c.g, c.b);\n }\n }, [color]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iBrightness.value = brightness;\n }\n }, [brightness]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iEdgeIntensity.value = edgeIntensity;\n }\n }, [edgeIntensity]);\n\n useEffect(() => {\n if (filmPassRef.current?.uniforms?.intensity) {\n filmPassRef.current.uniforms.intensity.value = grainIntensity;\n }\n }, [grainIntensity]);\n\n useEffect(() => {\n const el = rendererRef.current?.domElement;\n if (!el) return;\n if (mixBlendMode) {\n el.style.mixBlendMode = String(mixBlendMode);\n } else {\n el.style.removeProperty('mix-blend-mode');\n }\n }, [mixBlendMode]);\n\n const mergedStyle = useMemo(() => ({ zIndex, ...style }), [zIndex, style]);\n\n return (\n \n );\n};\n\nexport default GhostCursor;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/GridDistortion-JS-CSS.json b/public/r/GridDistortion-JS-CSS.json
index dfe3f1dc..0794611c 100644
--- a/public/r/GridDistortion-JS-CSS.json
+++ b/public/r/GridDistortion-JS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "GridDistortion/GridDistortion.jsx",
- "content": "import { useRef, useEffect } from 'react';\nimport * as THREE from 'three';\nimport './GridDistortion.css';\n\nconst vertexShader = `\nuniform float time;\nvarying vec2 vUv;\nvarying vec3 vPosition;\n\nvoid main() {\n vUv = uv;\n vPosition = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}`;\n\nconst fragmentShader = `\nuniform sampler2D uDataTexture;\nuniform sampler2D uTexture;\nuniform vec4 resolution;\nvarying vec2 vUv;\n\nvoid main() {\n vec2 uv = vUv;\n vec4 offset = texture2D(uDataTexture, vUv);\n gl_FragColor = texture2D(uTexture, uv - 0.02 * offset.rg);\n}`;\n\nconst GridDistortion = ({ grid = 15, mouse = 0.1, strength = 0.15, relaxation = 0.9, imageSrc, className = '' }) => {\n const containerRef = useRef(null);\n const sceneRef = useRef(null);\n const rendererRef = useRef(null);\n const cameraRef = useRef(null);\n const planeRef = useRef(null);\n const imageAspectRef = useRef(1);\n const animationIdRef = useRef(null);\n const resizeObserverRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n\n const scene = new THREE.Scene();\n sceneRef.current = scene;\n\n const renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: true,\n powerPreference: 'high-performance'\n });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n container.innerHTML = '';\n container.appendChild(renderer.domElement);\n\n const camera = new THREE.OrthographicCamera(0, 0, 0, 0, -1000, 1000);\n camera.position.z = 2;\n cameraRef.current = camera;\n\n const uniforms = {\n time: { value: 0 },\n resolution: { value: new THREE.Vector4() },\n uTexture: { value: null },\n uDataTexture: { value: null }\n };\n\n const textureLoader = new THREE.TextureLoader();\n textureLoader.load(imageSrc, texture => {\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.wrapS = THREE.ClampToEdgeWrapping;\n texture.wrapT = THREE.ClampToEdgeWrapping;\n imageAspectRef.current = texture.image.width / texture.image.height;\n uniforms.uTexture.value = texture;\n handleResize();\n });\n\n const size = grid;\n const data = new Float32Array(4 * size * size);\n for (let i = 0; i < size * size; i++) {\n data[i * 4] = Math.random() * 255 - 125;\n data[i * 4 + 1] = Math.random() * 255 - 125;\n }\n\n const dataTexture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType);\n dataTexture.needsUpdate = true;\n uniforms.uDataTexture.value = dataTexture;\n\n const material = new THREE.ShaderMaterial({\n side: THREE.DoubleSide,\n uniforms,\n vertexShader,\n fragmentShader,\n transparent: true\n });\n\n const geometry = new THREE.PlaneGeometry(1, 1, size - 1, size - 1);\n const plane = new THREE.Mesh(geometry, material);\n planeRef.current = plane;\n scene.add(plane);\n\n const handleResize = () => {\n if (!container || !renderer || !camera) return;\n\n const rect = container.getBoundingClientRect();\n const width = rect.width;\n const height = rect.height;\n\n if (width === 0 || height === 0) return;\n\n const containerAspect = width / height;\n\n renderer.setSize(width, height);\n\n if (plane) {\n plane.scale.set(containerAspect, 1, 1);\n }\n\n const frustumHeight = 1;\n const frustumWidth = frustumHeight * containerAspect;\n camera.left = -frustumWidth / 2;\n camera.right = frustumWidth / 2;\n camera.top = frustumHeight / 2;\n camera.bottom = -frustumHeight / 2;\n camera.updateProjectionMatrix();\n\n uniforms.resolution.value.set(width, height, 1, 1);\n };\n\n if (window.ResizeObserver) {\n const resizeObserver = new ResizeObserver(() => {\n handleResize();\n });\n resizeObserver.observe(container);\n resizeObserverRef.current = resizeObserver;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const mouseState = {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n };\n\n const handleMouseMove = e => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1 - (e.clientY - rect.top) / rect.height;\n mouseState.vX = x - mouseState.prevX;\n mouseState.vY = y - mouseState.prevY;\n Object.assign(mouseState, { x, y, prevX: x, prevY: y });\n };\n\n const handleMouseLeave = () => {\n if (dataTexture) {\n dataTexture.needsUpdate = true;\n }\n Object.assign(mouseState, {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n });\n };\n\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n handleResize();\n\n const animate = () => {\n animationIdRef.current = requestAnimationFrame(animate);\n\n if (!renderer || !scene || !camera) return;\n\n uniforms.time.value += 0.05;\n\n const data = dataTexture.image.data;\n for (let i = 0; i < size * size; i++) {\n data[i * 4] *= relaxation;\n data[i * 4 + 1] *= relaxation;\n }\n\n const gridMouseX = size * mouseState.x;\n const gridMouseY = size * mouseState.y;\n const maxDist = size * mouse;\n\n for (let i = 0; i < size; i++) {\n for (let j = 0; j < size; j++) {\n const distSq = Math.pow(gridMouseX - i, 2) + Math.pow(gridMouseY - j, 2);\n if (distSq < maxDist * maxDist) {\n const index = 4 * (i + size * j);\n const power = Math.min(maxDist / Math.sqrt(distSq), 10);\n data[index] += strength * 100 * mouseState.vX * power;\n data[index + 1] -= strength * 100 * mouseState.vY * power;\n }\n }\n }\n\n dataTexture.needsUpdate = true;\n renderer.render(scene, camera);\n };\n\n animate();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n } else {\n window.removeEventListener('resize', handleResize);\n }\n\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n\n if (renderer) {\n renderer.dispose();\n if (container.contains(renderer.domElement)) {\n container.removeChild(renderer.domElement);\n }\n }\n\n if (geometry) geometry.dispose();\n if (material) material.dispose();\n if (dataTexture) dataTexture.dispose();\n if (uniforms.uTexture.value) uniforms.uTexture.value.dispose();\n\n sceneRef.current = null;\n rendererRef.current = null;\n cameraRef.current = null;\n planeRef.current = null;\n };\n }, [grid, mouse, strength, relaxation, imageSrc]);\n\n return (\n \n );\n};\n\nexport default GridDistortion;\n"
+ "content": "import { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './GridDistortion.css';\n\nconst vertexShader = `\nuniform float time;\nvarying vec2 vUv;\nvarying vec3 vPosition;\n\nvoid main() {\n vUv = uv;\n vPosition = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}`;\n\nconst fragmentShader = `\nuniform sampler2D uDataTexture;\nuniform sampler2D uTexture;\nuniform vec4 resolution;\nvarying vec2 vUv;\n\nvoid main() {\n vec2 uv = vUv;\n vec4 offset = texture2D(uDataTexture, vUv);\n gl_FragColor = texture2D(uTexture, uv - 0.02 * offset.rg);\n}`;\n\nconst GridDistortion = ({ grid = 15, mouse = 0.1, strength = 0.15, relaxation = 0.9, imageSrc, className = '' }) => {\n const containerRef = useRef(null);\n const sceneRef = useRef(null);\n const rendererRef = useRef(null);\n const cameraRef = useRef(null);\n const planeRef = useRef(null);\n const imageAspectRef = useRef(1);\n const animationIdRef = useRef(null);\n const resizeObserverRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n\n const scene = new THREE.Scene();\n sceneRef.current = scene;\n\n const renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: true,\n powerPreference: 'high-performance'\n });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n container.innerHTML = '';\n container.appendChild(renderer.domElement);\n\n const camera = new THREE.OrthographicCamera(0, 0, 0, 0, -1000, 1000);\n camera.position.z = 2;\n cameraRef.current = camera;\n\n const uniforms = {\n time: { value: 0 },\n resolution: { value: new THREE.Vector4() },\n uTexture: { value: null },\n uDataTexture: { value: null }\n };\n\n const textureLoader = new THREE.TextureLoader();\n textureLoader.load(imageSrc, texture => {\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.wrapS = THREE.ClampToEdgeWrapping;\n texture.wrapT = THREE.ClampToEdgeWrapping;\n imageAspectRef.current = texture.image.width / texture.image.height;\n uniforms.uTexture.value = texture;\n handleResize();\n });\n\n const size = grid;\n const data = new Float32Array(4 * size * size);\n for (let i = 0; i < size * size; i++) {\n data[i * 4] = Math.random() * 255 - 125;\n data[i * 4 + 1] = Math.random() * 255 - 125;\n }\n\n const dataTexture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType);\n dataTexture.needsUpdate = true;\n uniforms.uDataTexture.value = dataTexture;\n\n const material = new THREE.ShaderMaterial({\n side: THREE.DoubleSide,\n uniforms,\n vertexShader,\n fragmentShader,\n transparent: true\n });\n\n const geometry = new THREE.PlaneGeometry(1, 1, size - 1, size - 1);\n const plane = new THREE.Mesh(geometry, material);\n planeRef.current = plane;\n scene.add(plane);\n\n const handleResize = () => {\n if (!container || !renderer || !camera) return;\n\n const rect = container.getBoundingClientRect();\n const width = rect.width;\n const height = rect.height;\n\n if (width === 0 || height === 0) return;\n\n const containerAspect = width / height;\n\n renderer.setSize(width, height);\n\n if (plane) {\n plane.scale.set(containerAspect, 1, 1);\n }\n\n const frustumHeight = 1;\n const frustumWidth = frustumHeight * containerAspect;\n camera.left = -frustumWidth / 2;\n camera.right = frustumWidth / 2;\n camera.top = frustumHeight / 2;\n camera.bottom = -frustumHeight / 2;\n camera.updateProjectionMatrix();\n\n uniforms.resolution.value.set(width, height, 1, 1);\n };\n\n if (window.ResizeObserver) {\n const resizeObserver = new ResizeObserver(() => {\n handleResize();\n });\n resizeObserver.observe(container);\n resizeObserverRef.current = resizeObserver;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const mouseState = {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n };\n\n const handleMouseMove = e => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1 - (e.clientY - rect.top) / rect.height;\n mouseState.vX = x - mouseState.prevX;\n mouseState.vY = y - mouseState.prevY;\n Object.assign(mouseState, { x, y, prevX: x, prevY: y });\n };\n\n const handleMouseLeave = () => {\n if (dataTexture) {\n dataTexture.needsUpdate = true;\n }\n Object.assign(mouseState, {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n });\n };\n\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n handleResize();\n\n const animate = () => {\n animationIdRef.current = requestAnimationFrame(animate);\n\n if (!renderer || !scene || !camera) return;\n\n uniforms.time.value += 0.05;\n\n const data = dataTexture.image.data;\n for (let i = 0; i < size * size; i++) {\n data[i * 4] *= relaxation;\n data[i * 4 + 1] *= relaxation;\n }\n\n const gridMouseX = size * mouseState.x;\n const gridMouseY = size * mouseState.y;\n const maxDist = size * mouse;\n\n for (let i = 0; i < size; i++) {\n for (let j = 0; j < size; j++) {\n const distSq = Math.pow(gridMouseX - i, 2) + Math.pow(gridMouseY - j, 2);\n if (distSq < maxDist * maxDist) {\n const index = 4 * (i + size * j);\n const power = Math.min(maxDist / Math.sqrt(distSq), 10);\n data[index] += strength * 100 * mouseState.vX * power;\n data[index + 1] -= strength * 100 * mouseState.vY * power;\n }\n }\n }\n\n dataTexture.needsUpdate = true;\n renderer.render(scene, camera);\n };\n\n animate();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n } else {\n window.removeEventListener('resize', handleResize);\n }\n\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n\n if (renderer) {\n renderer.dispose();\n renderer.forceContextLoss();\n if (container.contains(renderer.domElement)) {\n container.removeChild(renderer.domElement);\n }\n }\n\n if (geometry) geometry.dispose();\n if (material) material.dispose();\n if (dataTexture) dataTexture.dispose();\n if (uniforms.uTexture.value) uniforms.uTexture.value.dispose();\n\n sceneRef.current = null;\n rendererRef.current = null;\n cameraRef.current = null;\n planeRef.current = null;\n };\n }, [grid, mouse, strength, relaxation, imageSrc]);\n\n return (\n \n );\n};\n\nexport default GridDistortion;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/GridDistortion-JS-TW.json b/public/r/GridDistortion-JS-TW.json
index 74cc6236..902b15b9 100644
--- a/public/r/GridDistortion-JS-TW.json
+++ b/public/r/GridDistortion-JS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "GridDistortion/GridDistortion.jsx",
- "content": "import { useRef, useEffect } from 'react';\nimport * as THREE from 'three';\n\nconst vertexShader = `\nuniform float time;\nvarying vec2 vUv;\nvarying vec3 vPosition;\n\nvoid main() {\n vUv = uv;\n vPosition = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}`;\n\nconst fragmentShader = `\nuniform sampler2D uDataTexture;\nuniform sampler2D uTexture;\nuniform vec4 resolution;\nvarying vec2 vUv;\n\nvoid main() {\n vec2 uv = vUv;\n vec4 offset = texture2D(uDataTexture, vUv);\n gl_FragColor = texture2D(uTexture, uv - 0.02 * offset.rg);\n}`;\n\nconst GridDistortion = ({ grid = 15, mouse = 0.1, strength = 0.15, relaxation = 0.9, imageSrc, className = '' }) => {\n const containerRef = useRef(null);\n const sceneRef = useRef(null);\n const rendererRef = useRef(null);\n const cameraRef = useRef(null);\n const planeRef = useRef(null);\n const imageAspectRef = useRef(1);\n const animationIdRef = useRef(null);\n const resizeObserverRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n\n const scene = new THREE.Scene();\n sceneRef.current = scene;\n\n const renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: true,\n powerPreference: 'high-performance'\n });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n container.innerHTML = '';\n container.appendChild(renderer.domElement);\n\n const camera = new THREE.OrthographicCamera(0, 0, 0, 0, -1000, 1000);\n camera.position.z = 2;\n cameraRef.current = camera;\n\n const uniforms = {\n time: { value: 0 },\n resolution: { value: new THREE.Vector4() },\n uTexture: { value: null },\n uDataTexture: { value: null }\n };\n\n const textureLoader = new THREE.TextureLoader();\n textureLoader.load(imageSrc, texture => {\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.wrapS = THREE.ClampToEdgeWrapping;\n texture.wrapT = THREE.ClampToEdgeWrapping;\n imageAspectRef.current = texture.image.width / texture.image.height;\n uniforms.uTexture.value = texture;\n handleResize();\n });\n\n const size = grid;\n const data = new Float32Array(4 * size * size);\n for (let i = 0; i < size * size; i++) {\n data[i * 4] = Math.random() * 255 - 125;\n data[i * 4 + 1] = Math.random() * 255 - 125;\n }\n\n const dataTexture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType);\n dataTexture.needsUpdate = true;\n uniforms.uDataTexture.value = dataTexture;\n\n const material = new THREE.ShaderMaterial({\n side: THREE.DoubleSide,\n uniforms,\n vertexShader,\n fragmentShader,\n transparent: true\n });\n\n const geometry = new THREE.PlaneGeometry(1, 1, size - 1, size - 1);\n const plane = new THREE.Mesh(geometry, material);\n planeRef.current = plane;\n scene.add(plane);\n\n const handleResize = () => {\n if (!container || !renderer || !camera) return;\n\n const rect = container.getBoundingClientRect();\n const width = rect.width;\n const height = rect.height;\n\n if (width === 0 || height === 0) return;\n\n const containerAspect = width / height;\n\n renderer.setSize(width, height);\n\n if (plane) {\n plane.scale.set(containerAspect, 1, 1);\n }\n\n const frustumHeight = 1;\n const frustumWidth = frustumHeight * containerAspect;\n camera.left = -frustumWidth / 2;\n camera.right = frustumWidth / 2;\n camera.top = frustumHeight / 2;\n camera.bottom = -frustumHeight / 2;\n camera.updateProjectionMatrix();\n\n uniforms.resolution.value.set(width, height, 1, 1);\n };\n\n if (window.ResizeObserver) {\n const resizeObserver = new ResizeObserver(() => {\n handleResize();\n });\n resizeObserver.observe(container);\n resizeObserverRef.current = resizeObserver;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const mouseState = {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n };\n\n const handleMouseMove = e => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1 - (e.clientY - rect.top) / rect.height;\n mouseState.vX = x - mouseState.prevX;\n mouseState.vY = y - mouseState.prevY;\n Object.assign(mouseState, { x, y, prevX: x, prevY: y });\n };\n\n const handleMouseLeave = () => {\n if (dataTexture) {\n dataTexture.needsUpdate = true;\n }\n Object.assign(mouseState, {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n });\n };\n\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n handleResize();\n\n const animate = () => {\n animationIdRef.current = requestAnimationFrame(animate);\n\n if (!renderer || !scene || !camera) return;\n\n uniforms.time.value += 0.05;\n\n const data = dataTexture.image.data;\n for (let i = 0; i < size * size; i++) {\n data[i * 4] *= relaxation;\n data[i * 4 + 1] *= relaxation;\n }\n\n const gridMouseX = size * mouseState.x;\n const gridMouseY = size * mouseState.y;\n const maxDist = size * mouse;\n\n for (let i = 0; i < size; i++) {\n for (let j = 0; j < size; j++) {\n const distSq = Math.pow(gridMouseX - i, 2) + Math.pow(gridMouseY - j, 2);\n if (distSq < maxDist * maxDist) {\n const index = 4 * (i + size * j);\n const power = Math.min(maxDist / Math.sqrt(distSq), 10);\n data[index] += strength * 100 * mouseState.vX * power;\n data[index + 1] -= strength * 100 * mouseState.vY * power;\n }\n }\n }\n\n dataTexture.needsUpdate = true;\n renderer.render(scene, camera);\n };\n\n animate();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n } else {\n window.removeEventListener('resize', handleResize);\n }\n\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n\n if (renderer) {\n renderer.dispose();\n if (container.contains(renderer.domElement)) {\n container.removeChild(renderer.domElement);\n }\n }\n\n if (geometry) geometry.dispose();\n if (material) material.dispose();\n if (dataTexture) dataTexture.dispose();\n if (uniforms.uTexture.value) uniforms.uTexture.value.dispose();\n\n sceneRef.current = null;\n rendererRef.current = null;\n cameraRef.current = null;\n planeRef.current = null;\n };\n }, [grid, mouse, strength, relaxation, imageSrc]);\n\n return (\n \n );\n};\n\nexport default GridDistortion;\n"
+ "content": "import { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nconst vertexShader = `\nuniform float time;\nvarying vec2 vUv;\nvarying vec3 vPosition;\n\nvoid main() {\n vUv = uv;\n vPosition = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}`;\n\nconst fragmentShader = `\nuniform sampler2D uDataTexture;\nuniform sampler2D uTexture;\nuniform vec4 resolution;\nvarying vec2 vUv;\n\nvoid main() {\n vec2 uv = vUv;\n vec4 offset = texture2D(uDataTexture, vUv);\n gl_FragColor = texture2D(uTexture, uv - 0.02 * offset.rg);\n}`;\n\nconst GridDistortion = ({ grid = 15, mouse = 0.1, strength = 0.15, relaxation = 0.9, imageSrc, className = '' }) => {\n const containerRef = useRef(null);\n const sceneRef = useRef(null);\n const rendererRef = useRef(null);\n const cameraRef = useRef(null);\n const planeRef = useRef(null);\n const imageAspectRef = useRef(1);\n const animationIdRef = useRef(null);\n const resizeObserverRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n\n const scene = new THREE.Scene();\n sceneRef.current = scene;\n\n const renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: true,\n powerPreference: 'high-performance'\n });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n container.innerHTML = '';\n container.appendChild(renderer.domElement);\n\n const camera = new THREE.OrthographicCamera(0, 0, 0, 0, -1000, 1000);\n camera.position.z = 2;\n cameraRef.current = camera;\n\n const uniforms = {\n time: { value: 0 },\n resolution: { value: new THREE.Vector4() },\n uTexture: { value: null },\n uDataTexture: { value: null }\n };\n\n const textureLoader = new THREE.TextureLoader();\n textureLoader.load(imageSrc, texture => {\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.wrapS = THREE.ClampToEdgeWrapping;\n texture.wrapT = THREE.ClampToEdgeWrapping;\n imageAspectRef.current = texture.image.width / texture.image.height;\n uniforms.uTexture.value = texture;\n handleResize();\n });\n\n const size = grid;\n const data = new Float32Array(4 * size * size);\n for (let i = 0; i < size * size; i++) {\n data[i * 4] = Math.random() * 255 - 125;\n data[i * 4 + 1] = Math.random() * 255 - 125;\n }\n\n const dataTexture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType);\n dataTexture.needsUpdate = true;\n uniforms.uDataTexture.value = dataTexture;\n\n const material = new THREE.ShaderMaterial({\n side: THREE.DoubleSide,\n uniforms,\n vertexShader,\n fragmentShader,\n transparent: true\n });\n\n const geometry = new THREE.PlaneGeometry(1, 1, size - 1, size - 1);\n const plane = new THREE.Mesh(geometry, material);\n planeRef.current = plane;\n scene.add(plane);\n\n const handleResize = () => {\n if (!container || !renderer || !camera) return;\n\n const rect = container.getBoundingClientRect();\n const width = rect.width;\n const height = rect.height;\n\n if (width === 0 || height === 0) return;\n\n const containerAspect = width / height;\n\n renderer.setSize(width, height);\n\n if (plane) {\n plane.scale.set(containerAspect, 1, 1);\n }\n\n const frustumHeight = 1;\n const frustumWidth = frustumHeight * containerAspect;\n camera.left = -frustumWidth / 2;\n camera.right = frustumWidth / 2;\n camera.top = frustumHeight / 2;\n camera.bottom = -frustumHeight / 2;\n camera.updateProjectionMatrix();\n\n uniforms.resolution.value.set(width, height, 1, 1);\n };\n\n if (window.ResizeObserver) {\n const resizeObserver = new ResizeObserver(() => {\n handleResize();\n });\n resizeObserver.observe(container);\n resizeObserverRef.current = resizeObserver;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const mouseState = {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n };\n\n const handleMouseMove = e => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1 - (e.clientY - rect.top) / rect.height;\n mouseState.vX = x - mouseState.prevX;\n mouseState.vY = y - mouseState.prevY;\n Object.assign(mouseState, { x, y, prevX: x, prevY: y });\n };\n\n const handleMouseLeave = () => {\n if (dataTexture) {\n dataTexture.needsUpdate = true;\n }\n Object.assign(mouseState, {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n });\n };\n\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n handleResize();\n\n const animate = () => {\n animationIdRef.current = requestAnimationFrame(animate);\n\n if (!renderer || !scene || !camera) return;\n\n uniforms.time.value += 0.05;\n\n const data = dataTexture.image.data;\n for (let i = 0; i < size * size; i++) {\n data[i * 4] *= relaxation;\n data[i * 4 + 1] *= relaxation;\n }\n\n const gridMouseX = size * mouseState.x;\n const gridMouseY = size * mouseState.y;\n const maxDist = size * mouse;\n\n for (let i = 0; i < size; i++) {\n for (let j = 0; j < size; j++) {\n const distSq = Math.pow(gridMouseX - i, 2) + Math.pow(gridMouseY - j, 2);\n if (distSq < maxDist * maxDist) {\n const index = 4 * (i + size * j);\n const power = Math.min(maxDist / Math.sqrt(distSq), 10);\n data[index] += strength * 100 * mouseState.vX * power;\n data[index + 1] -= strength * 100 * mouseState.vY * power;\n }\n }\n }\n\n dataTexture.needsUpdate = true;\n renderer.render(scene, camera);\n };\n\n animate();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n } else {\n window.removeEventListener('resize', handleResize);\n }\n\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n\n if (renderer) {\n renderer.dispose();\n renderer.forceContextLoss();\n if (container.contains(renderer.domElement)) {\n container.removeChild(renderer.domElement);\n }\n }\n\n if (geometry) geometry.dispose();\n if (material) material.dispose();\n if (dataTexture) dataTexture.dispose();\n if (uniforms.uTexture.value) uniforms.uTexture.value.dispose();\n\n sceneRef.current = null;\n rendererRef.current = null;\n cameraRef.current = null;\n planeRef.current = null;\n };\n }, [grid, mouse, strength, relaxation, imageSrc]);\n\n return (\n \n );\n};\n\nexport default GridDistortion;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/GridDistortion-TS-CSS.json b/public/r/GridDistortion-TS-CSS.json
index d45a2867..0600d95f 100644
--- a/public/r/GridDistortion-TS-CSS.json
+++ b/public/r/GridDistortion-TS-CSS.json
@@ -13,7 +13,7 @@
{
"type": "registry:component",
"path": "GridDistortion/GridDistortion.tsx",
- "content": "import React, { useRef, useEffect } from 'react';\nimport * as THREE from 'three';\nimport './GridDistortion.css';\n\ninterface GridDistortionProps {\n grid?: number;\n mouse?: number;\n strength?: number;\n relaxation?: number;\n imageSrc: string;\n className?: string;\n}\n\nconst vertexShader = `\nuniform float time;\nvarying vec2 vUv;\nvarying vec3 vPosition;\n\nvoid main() {\n vUv = uv;\n vPosition = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nuniform sampler2D uDataTexture;\nuniform sampler2D uTexture;\nuniform vec4 resolution;\nvarying vec2 vUv;\n\nvoid main() {\n vec2 uv = vUv;\n vec4 offset = texture2D(uDataTexture, vUv);\n gl_FragColor = texture2D(uTexture, uv - 0.02 * offset.rg);\n}\n`;\n\nconst GridDistortion: React.FC = ({\n grid = 15,\n mouse = 0.1,\n strength = 0.15,\n relaxation = 0.9,\n imageSrc,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const sceneRef = useRef(null);\n const rendererRef = useRef(null);\n const cameraRef = useRef(null);\n const planeRef = useRef(null);\n const imageAspectRef = useRef(1);\n const animationIdRef = useRef(null);\n const resizeObserverRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n\n const scene = new THREE.Scene();\n sceneRef.current = scene;\n\n const renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: true,\n powerPreference: 'high-performance'\n });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n container.innerHTML = '';\n container.appendChild(renderer.domElement);\n\n const camera = new THREE.OrthographicCamera(0, 0, 0, 0, -1000, 1000);\n camera.position.z = 2;\n cameraRef.current = camera;\n\n const uniforms = {\n time: { value: 0 },\n resolution: { value: new THREE.Vector4() },\n uTexture: { value: null as THREE.Texture | null },\n uDataTexture: { value: null as THREE.DataTexture | null }\n };\n\n const textureLoader = new THREE.TextureLoader();\n textureLoader.load(imageSrc, texture => {\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.wrapS = THREE.ClampToEdgeWrapping;\n texture.wrapT = THREE.ClampToEdgeWrapping;\n imageAspectRef.current = texture.image.width / texture.image.height;\n uniforms.uTexture.value = texture;\n handleResize();\n });\n\n const size = grid;\n const data = new Float32Array(4 * size * size);\n for (let i = 0; i < size * size; i++) {\n data[i * 4] = Math.random() * 255 - 125;\n data[i * 4 + 1] = Math.random() * 255 - 125;\n }\n\n const dataTexture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType);\n dataTexture.needsUpdate = true;\n uniforms.uDataTexture.value = dataTexture;\n\n const material = new THREE.ShaderMaterial({\n side: THREE.DoubleSide,\n uniforms,\n vertexShader,\n fragmentShader,\n transparent: true\n });\n\n const geometry = new THREE.PlaneGeometry(1, 1, size - 1, size - 1);\n const plane = new THREE.Mesh(geometry, material);\n planeRef.current = plane;\n scene.add(plane);\n\n const handleResize = () => {\n if (!container || !renderer || !camera) return;\n\n const rect = container.getBoundingClientRect();\n const width = rect.width;\n const height = rect.height;\n\n if (width === 0 || height === 0) return;\n\n const containerAspect = width / height;\n\n renderer.setSize(width, height);\n\n if (plane) {\n plane.scale.set(containerAspect, 1, 1);\n }\n\n const frustumHeight = 1;\n const frustumWidth = frustumHeight * containerAspect;\n camera.left = -frustumWidth / 2;\n camera.right = frustumWidth / 2;\n camera.top = frustumHeight / 2;\n camera.bottom = -frustumHeight / 2;\n camera.updateProjectionMatrix();\n\n uniforms.resolution.value.set(width, height, 1, 1);\n };\n\n if (window.ResizeObserver) {\n const resizeObserver = new ResizeObserver(() => {\n handleResize();\n });\n resizeObserver.observe(container);\n resizeObserverRef.current = resizeObserver;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const mouseState = {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n };\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1 - (e.clientY - rect.top) / rect.height;\n mouseState.vX = x - mouseState.prevX;\n mouseState.vY = y - mouseState.prevY;\n Object.assign(mouseState, { x, y, prevX: x, prevY: y });\n };\n\n const handleMouseLeave = () => {\n if (dataTexture) {\n dataTexture.needsUpdate = true;\n }\n Object.assign(mouseState, {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n });\n };\n\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n handleResize();\n\n const animate = () => {\n animationIdRef.current = requestAnimationFrame(animate);\n\n if (!renderer || !scene || !camera) return;\n\n uniforms.time.value += 0.05;\n\n if (!(dataTexture.image.data instanceof Float32Array)) {\n console.error('dataTexture.image.data is not a Float32Array');\n return;\n }\n const data: Float32Array = dataTexture.image.data;\n for (let i = 0; i < size * size; i++) {\n data[i * 4] *= relaxation;\n data[i * 4 + 1] *= relaxation;\n }\n\n const gridMouseX = size * mouseState.x;\n const gridMouseY = size * mouseState.y;\n const maxDist = size * mouse;\n\n for (let i = 0; i < size; i++) {\n for (let j = 0; j < size; j++) {\n const distSq = Math.pow(gridMouseX - i, 2) + Math.pow(gridMouseY - j, 2);\n if (distSq < maxDist * maxDist) {\n const index = 4 * (i + size * j);\n const power = Math.min(maxDist / Math.sqrt(distSq), 10);\n data[index] += strength * 100 * mouseState.vX * power;\n data[index + 1] -= strength * 100 * mouseState.vY * power;\n }\n }\n }\n\n dataTexture.needsUpdate = true;\n renderer.render(scene, camera);\n };\n\n animate();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n } else {\n window.removeEventListener('resize', handleResize);\n }\n\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n\n if (renderer) {\n renderer.dispose();\n if (container.contains(renderer.domElement)) {\n container.removeChild(renderer.domElement);\n }\n }\n\n if (geometry) geometry.dispose();\n if (material) material.dispose();\n if (dataTexture) dataTexture.dispose();\n if (uniforms.uTexture.value) uniforms.uTexture.value.dispose();\n\n sceneRef.current = null;\n rendererRef.current = null;\n cameraRef.current = null;\n planeRef.current = null;\n };\n }, [grid, mouse, strength, relaxation, imageSrc]);\n\n return (\n \n );\n};\n\nexport default GridDistortion;\n"
+ "content": "import React, { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport './GridDistortion.css';\n\ninterface GridDistortionProps {\n grid?: number;\n mouse?: number;\n strength?: number;\n relaxation?: number;\n imageSrc: string;\n className?: string;\n}\n\nconst vertexShader = `\nuniform float time;\nvarying vec2 vUv;\nvarying vec3 vPosition;\n\nvoid main() {\n vUv = uv;\n vPosition = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nuniform sampler2D uDataTexture;\nuniform sampler2D uTexture;\nuniform vec4 resolution;\nvarying vec2 vUv;\n\nvoid main() {\n vec2 uv = vUv;\n vec4 offset = texture2D(uDataTexture, vUv);\n gl_FragColor = texture2D(uTexture, uv - 0.02 * offset.rg);\n}\n`;\n\nconst GridDistortion: React.FC = ({\n grid = 15,\n mouse = 0.1,\n strength = 0.15,\n relaxation = 0.9,\n imageSrc,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const sceneRef = useRef(null);\n const rendererRef = useRef(null);\n const cameraRef = useRef(null);\n const planeRef = useRef(null);\n const imageAspectRef = useRef(1);\n const animationIdRef = useRef(null);\n const resizeObserverRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n\n const scene = new THREE.Scene();\n sceneRef.current = scene;\n\n const renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: true,\n powerPreference: 'high-performance'\n });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n container.innerHTML = '';\n container.appendChild(renderer.domElement);\n\n const camera = new THREE.OrthographicCamera(0, 0, 0, 0, -1000, 1000);\n camera.position.z = 2;\n cameraRef.current = camera;\n\n const uniforms = {\n time: { value: 0 },\n resolution: { value: new THREE.Vector4() },\n uTexture: { value: null as THREE.Texture | null },\n uDataTexture: { value: null as THREE.DataTexture | null }\n };\n\n const textureLoader = new THREE.TextureLoader();\n textureLoader.load(imageSrc, texture => {\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.wrapS = THREE.ClampToEdgeWrapping;\n texture.wrapT = THREE.ClampToEdgeWrapping;\n imageAspectRef.current = texture.image.width / texture.image.height;\n uniforms.uTexture.value = texture;\n handleResize();\n });\n\n const size = grid;\n const data = new Float32Array(4 * size * size);\n for (let i = 0; i < size * size; i++) {\n data[i * 4] = Math.random() * 255 - 125;\n data[i * 4 + 1] = Math.random() * 255 - 125;\n }\n\n const dataTexture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType);\n dataTexture.needsUpdate = true;\n uniforms.uDataTexture.value = dataTexture;\n\n const material = new THREE.ShaderMaterial({\n side: THREE.DoubleSide,\n uniforms,\n vertexShader,\n fragmentShader,\n transparent: true\n });\n\n const geometry = new THREE.PlaneGeometry(1, 1, size - 1, size - 1);\n const plane = new THREE.Mesh(geometry, material);\n planeRef.current = plane;\n scene.add(plane);\n\n const handleResize = () => {\n if (!container || !renderer || !camera) return;\n\n const rect = container.getBoundingClientRect();\n const width = rect.width;\n const height = rect.height;\n\n if (width === 0 || height === 0) return;\n\n const containerAspect = width / height;\n\n renderer.setSize(width, height);\n\n if (plane) {\n plane.scale.set(containerAspect, 1, 1);\n }\n\n const frustumHeight = 1;\n const frustumWidth = frustumHeight * containerAspect;\n camera.left = -frustumWidth / 2;\n camera.right = frustumWidth / 2;\n camera.top = frustumHeight / 2;\n camera.bottom = -frustumHeight / 2;\n camera.updateProjectionMatrix();\n\n uniforms.resolution.value.set(width, height, 1, 1);\n };\n\n if (window.ResizeObserver) {\n const resizeObserver = new ResizeObserver(() => {\n handleResize();\n });\n resizeObserver.observe(container);\n resizeObserverRef.current = resizeObserver;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const mouseState = {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n };\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1 - (e.clientY - rect.top) / rect.height;\n mouseState.vX = x - mouseState.prevX;\n mouseState.vY = y - mouseState.prevY;\n Object.assign(mouseState, { x, y, prevX: x, prevY: y });\n };\n\n const handleMouseLeave = () => {\n if (dataTexture) {\n dataTexture.needsUpdate = true;\n }\n Object.assign(mouseState, {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n });\n };\n\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n handleResize();\n\n const animate = () => {\n animationIdRef.current = requestAnimationFrame(animate);\n\n if (!renderer || !scene || !camera) return;\n\n uniforms.time.value += 0.05;\n\n if (!(dataTexture.image.data instanceof Float32Array)) {\n console.error('dataTexture.image.data is not a Float32Array');\n return;\n }\n const data: Float32Array = dataTexture.image.data;\n for (let i = 0; i < size * size; i++) {\n data[i * 4] *= relaxation;\n data[i * 4 + 1] *= relaxation;\n }\n\n const gridMouseX = size * mouseState.x;\n const gridMouseY = size * mouseState.y;\n const maxDist = size * mouse;\n\n for (let i = 0; i < size; i++) {\n for (let j = 0; j < size; j++) {\n const distSq = Math.pow(gridMouseX - i, 2) + Math.pow(gridMouseY - j, 2);\n if (distSq < maxDist * maxDist) {\n const index = 4 * (i + size * j);\n const power = Math.min(maxDist / Math.sqrt(distSq), 10);\n data[index] += strength * 100 * mouseState.vX * power;\n data[index + 1] -= strength * 100 * mouseState.vY * power;\n }\n }\n }\n\n dataTexture.needsUpdate = true;\n renderer.render(scene, camera);\n };\n\n animate();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n } else {\n window.removeEventListener('resize', handleResize);\n }\n\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n\n if (renderer) {\n renderer.dispose();\n renderer.forceContextLoss();\n if (container.contains(renderer.domElement)) {\n container.removeChild(renderer.domElement);\n }\n }\n\n if (geometry) geometry.dispose();\n if (material) material.dispose();\n if (dataTexture) dataTexture.dispose();\n if (uniforms.uTexture.value) uniforms.uTexture.value.dispose();\n\n sceneRef.current = null;\n rendererRef.current = null;\n cameraRef.current = null;\n planeRef.current = null;\n };\n }, [grid, mouse, strength, relaxation, imageSrc]);\n\n return (\n \n );\n};\n\nexport default GridDistortion;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/GridDistortion-TS-TW.json b/public/r/GridDistortion-TS-TW.json
index 4fb2181e..5c66be76 100644
--- a/public/r/GridDistortion-TS-TW.json
+++ b/public/r/GridDistortion-TS-TW.json
@@ -8,7 +8,7 @@
{
"type": "registry:component",
"path": "GridDistortion/GridDistortion.tsx",
- "content": "import React, { useRef, useEffect } from 'react';\nimport * as THREE from 'three';\n\ninterface GridDistortionProps {\n grid?: number;\n mouse?: number;\n strength?: number;\n relaxation?: number;\n imageSrc: string;\n className?: string;\n}\n\nconst vertexShader = `\nuniform float time;\nvarying vec2 vUv;\nvarying vec3 vPosition;\n\nvoid main() {\n vUv = uv;\n vPosition = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nuniform sampler2D uDataTexture;\nuniform sampler2D uTexture;\nuniform vec4 resolution;\nvarying vec2 vUv;\n\nvoid main() {\n vec2 uv = vUv;\n vec4 offset = texture2D(uDataTexture, vUv);\n gl_FragColor = texture2D(uTexture, uv - 0.02 * offset.rg);\n}\n`;\n\nconst GridDistortion: React.FC = ({\n grid = 15,\n mouse = 0.1,\n strength = 0.15,\n relaxation = 0.9,\n imageSrc,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const sceneRef = useRef(null);\n const rendererRef = useRef(null);\n const cameraRef = useRef(null);\n const planeRef = useRef(null);\n const imageAspectRef = useRef(1);\n const animationIdRef = useRef(null);\n const resizeObserverRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n\n const scene = new THREE.Scene();\n sceneRef.current = scene;\n\n const renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: true,\n powerPreference: 'high-performance'\n });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n container.innerHTML = '';\n container.appendChild(renderer.domElement);\n\n const camera = new THREE.OrthographicCamera(0, 0, 0, 0, -1000, 1000);\n camera.position.z = 2;\n cameraRef.current = camera;\n\n const uniforms = {\n time: { value: 0 },\n resolution: { value: new THREE.Vector4() },\n uTexture: { value: null as THREE.Texture | null },\n uDataTexture: { value: null as THREE.DataTexture | null }\n };\n\n const textureLoader = new THREE.TextureLoader();\n textureLoader.load(imageSrc, texture => {\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.wrapS = THREE.ClampToEdgeWrapping;\n texture.wrapT = THREE.ClampToEdgeWrapping;\n imageAspectRef.current = texture.image.width / texture.image.height;\n uniforms.uTexture.value = texture;\n handleResize();\n });\n\n const size = grid;\n const data = new Float32Array(4 * size * size);\n for (let i = 0; i < size * size; i++) {\n data[i * 4] = Math.random() * 255 - 125;\n data[i * 4 + 1] = Math.random() * 255 - 125;\n }\n\n const dataTexture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType);\n dataTexture.needsUpdate = true;\n uniforms.uDataTexture.value = dataTexture;\n\n const material = new THREE.ShaderMaterial({\n side: THREE.DoubleSide,\n uniforms,\n vertexShader,\n fragmentShader,\n transparent: true\n });\n\n const geometry = new THREE.PlaneGeometry(1, 1, size - 1, size - 1);\n const plane = new THREE.Mesh(geometry, material);\n planeRef.current = plane;\n scene.add(plane);\n\n const handleResize = () => {\n if (!container || !renderer || !camera) return;\n\n const rect = container.getBoundingClientRect();\n const width = rect.width;\n const height = rect.height;\n\n if (width === 0 || height === 0) return;\n\n const containerAspect = width / height;\n\n renderer.setSize(width, height);\n\n if (plane) {\n plane.scale.set(containerAspect, 1, 1);\n }\n\n const frustumHeight = 1;\n const frustumWidth = frustumHeight * containerAspect;\n camera.left = -frustumWidth / 2;\n camera.right = frustumWidth / 2;\n camera.top = frustumHeight / 2;\n camera.bottom = -frustumHeight / 2;\n camera.updateProjectionMatrix();\n\n uniforms.resolution.value.set(width, height, 1, 1);\n };\n\n if (window.ResizeObserver) {\n const resizeObserver = new ResizeObserver(() => {\n handleResize();\n });\n resizeObserver.observe(container);\n resizeObserverRef.current = resizeObserver;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const mouseState = {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n };\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1 - (e.clientY - rect.top) / rect.height;\n mouseState.vX = x - mouseState.prevX;\n mouseState.vY = y - mouseState.prevY;\n Object.assign(mouseState, { x, y, prevX: x, prevY: y });\n };\n\n const handleMouseLeave = () => {\n if (dataTexture) {\n dataTexture.needsUpdate = true;\n }\n Object.assign(mouseState, {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n });\n };\n\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n handleResize();\n\n const animate = () => {\n animationIdRef.current = requestAnimationFrame(animate);\n\n if (!renderer || !scene || !camera) return;\n\n uniforms.time.value += 0.05;\n\n if (!(dataTexture.image.data instanceof Float32Array)) {\n console.error('dataTexture.image.data is not a Float32Array');\n return;\n }\n const data: Float32Array = dataTexture.image.data;\n for (let i = 0; i < size * size; i++) {\n data[i * 4] *= relaxation;\n data[i * 4 + 1] *= relaxation;\n }\n\n const gridMouseX = size * mouseState.x;\n const gridMouseY = size * mouseState.y;\n const maxDist = size * mouse;\n\n for (let i = 0; i < size; i++) {\n for (let j = 0; j < size; j++) {\n const distSq = Math.pow(gridMouseX - i, 2) + Math.pow(gridMouseY - j, 2);\n if (distSq < maxDist * maxDist) {\n const index = 4 * (i + size * j);\n const power = Math.min(maxDist / Math.sqrt(distSq), 10);\n data[index] += strength * 100 * mouseState.vX * power;\n data[index + 1] -= strength * 100 * mouseState.vY * power;\n }\n }\n }\n\n dataTexture.needsUpdate = true;\n renderer.render(scene, camera);\n };\n\n animate();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n } else {\n window.removeEventListener('resize', handleResize);\n }\n\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n\n if (renderer) {\n renderer.dispose();\n if (container.contains(renderer.domElement)) {\n container.removeChild(renderer.domElement);\n }\n }\n\n if (geometry) geometry.dispose();\n if (material) material.dispose();\n if (dataTexture) dataTexture.dispose();\n if (uniforms.uTexture.value) uniforms.uTexture.value.dispose();\n\n sceneRef.current = null;\n rendererRef.current = null;\n cameraRef.current = null;\n planeRef.current = null;\n };\n }, [grid, mouse, strength, relaxation, imageSrc]);\n\n return (\n \n );\n};\n\nexport default GridDistortion;\n"
+ "content": "import React, { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\ninterface GridDistortionProps {\n grid?: number;\n mouse?: number;\n strength?: number;\n relaxation?: number;\n imageSrc: string;\n className?: string;\n}\n\nconst vertexShader = `\nuniform float time;\nvarying vec2 vUv;\nvarying vec3 vPosition;\n\nvoid main() {\n vUv = uv;\n vPosition = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nuniform sampler2D uDataTexture;\nuniform sampler2D uTexture;\nuniform vec4 resolution;\nvarying vec2 vUv;\n\nvoid main() {\n vec2 uv = vUv;\n vec4 offset = texture2D(uDataTexture, vUv);\n gl_FragColor = texture2D(uTexture, uv - 0.02 * offset.rg);\n}\n`;\n\nconst GridDistortion: React.FC = ({\n grid = 15,\n mouse = 0.1,\n strength = 0.15,\n relaxation = 0.9,\n imageSrc,\n className = ''\n}) => {\n const containerRef = useRef(null);\n const sceneRef = useRef(null);\n const rendererRef = useRef(null);\n const cameraRef = useRef(null);\n const planeRef = useRef(null);\n const imageAspectRef = useRef(1);\n const animationIdRef = useRef(null);\n const resizeObserverRef = useRef(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const container = containerRef.current;\n\n const scene = new THREE.Scene();\n sceneRef.current = scene;\n\n const renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: true,\n powerPreference: 'high-performance'\n });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n container.innerHTML = '';\n container.appendChild(renderer.domElement);\n\n const camera = new THREE.OrthographicCamera(0, 0, 0, 0, -1000, 1000);\n camera.position.z = 2;\n cameraRef.current = camera;\n\n const uniforms = {\n time: { value: 0 },\n resolution: { value: new THREE.Vector4() },\n uTexture: { value: null as THREE.Texture | null },\n uDataTexture: { value: null as THREE.DataTexture | null }\n };\n\n const textureLoader = new THREE.TextureLoader();\n textureLoader.load(imageSrc, texture => {\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.wrapS = THREE.ClampToEdgeWrapping;\n texture.wrapT = THREE.ClampToEdgeWrapping;\n imageAspectRef.current = texture.image.width / texture.image.height;\n uniforms.uTexture.value = texture;\n handleResize();\n });\n\n const size = grid;\n const data = new Float32Array(4 * size * size);\n for (let i = 0; i < size * size; i++) {\n data[i * 4] = Math.random() * 255 - 125;\n data[i * 4 + 1] = Math.random() * 255 - 125;\n }\n\n const dataTexture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType);\n dataTexture.needsUpdate = true;\n uniforms.uDataTexture.value = dataTexture;\n\n const material = new THREE.ShaderMaterial({\n side: THREE.DoubleSide,\n uniforms,\n vertexShader,\n fragmentShader,\n transparent: true\n });\n\n const geometry = new THREE.PlaneGeometry(1, 1, size - 1, size - 1);\n const plane = new THREE.Mesh(geometry, material);\n planeRef.current = plane;\n scene.add(plane);\n\n const handleResize = () => {\n if (!container || !renderer || !camera) return;\n\n const rect = container.getBoundingClientRect();\n const width = rect.width;\n const height = rect.height;\n\n if (width === 0 || height === 0) return;\n\n const containerAspect = width / height;\n\n renderer.setSize(width, height);\n\n if (plane) {\n plane.scale.set(containerAspect, 1, 1);\n }\n\n const frustumHeight = 1;\n const frustumWidth = frustumHeight * containerAspect;\n camera.left = -frustumWidth / 2;\n camera.right = frustumWidth / 2;\n camera.top = frustumHeight / 2;\n camera.bottom = -frustumHeight / 2;\n camera.updateProjectionMatrix();\n\n uniforms.resolution.value.set(width, height, 1, 1);\n };\n\n if (window.ResizeObserver) {\n const resizeObserver = new ResizeObserver(() => {\n handleResize();\n });\n resizeObserver.observe(container);\n resizeObserverRef.current = resizeObserver;\n } else {\n window.addEventListener('resize', handleResize);\n }\n\n const mouseState = {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n };\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1 - (e.clientY - rect.top) / rect.height;\n mouseState.vX = x - mouseState.prevX;\n mouseState.vY = y - mouseState.prevY;\n Object.assign(mouseState, { x, y, prevX: x, prevY: y });\n };\n\n const handleMouseLeave = () => {\n if (dataTexture) {\n dataTexture.needsUpdate = true;\n }\n Object.assign(mouseState, {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n });\n };\n\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n\n handleResize();\n\n const animate = () => {\n animationIdRef.current = requestAnimationFrame(animate);\n\n if (!renderer || !scene || !camera) return;\n\n uniforms.time.value += 0.05;\n\n if (!(dataTexture.image.data instanceof Float32Array)) {\n console.error('dataTexture.image.data is not a Float32Array');\n return;\n }\n const data: Float32Array = dataTexture.image.data;\n for (let i = 0; i < size * size; i++) {\n data[i * 4] *= relaxation;\n data[i * 4 + 1] *= relaxation;\n }\n\n const gridMouseX = size * mouseState.x;\n const gridMouseY = size * mouseState.y;\n const maxDist = size * mouse;\n\n for (let i = 0; i < size; i++) {\n for (let j = 0; j < size; j++) {\n const distSq = Math.pow(gridMouseX - i, 2) + Math.pow(gridMouseY - j, 2);\n if (distSq < maxDist * maxDist) {\n const index = 4 * (i + size * j);\n const power = Math.min(maxDist / Math.sqrt(distSq), 10);\n data[index] += strength * 100 * mouseState.vX * power;\n data[index + 1] -= strength * 100 * mouseState.vY * power;\n }\n }\n }\n\n dataTexture.needsUpdate = true;\n renderer.render(scene, camera);\n };\n\n animate();\n\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n }\n\n if (resizeObserverRef.current) {\n resizeObserverRef.current.disconnect();\n } else {\n window.removeEventListener('resize', handleResize);\n }\n\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n\n if (renderer) {\n renderer.dispose();\n renderer.forceContextLoss();\n if (container.contains(renderer.domElement)) {\n container.removeChild(renderer.domElement);\n }\n }\n\n if (geometry) geometry.dispose();\n if (material) material.dispose();\n if (dataTexture) dataTexture.dispose();\n if (uniforms.uTexture.value) uniforms.uTexture.value.dispose();\n\n sceneRef.current = null;\n rendererRef.current = null;\n cameraRef.current = null;\n planeRef.current = null;\n };\n }, [grid, mouse, strength, relaxation, imageSrc]);\n\n return (\n \n );\n};\n\nexport default GridDistortion;\n"
}
],
"registryDependencies": [],
diff --git a/public/r/GridScan-JS-CSS.json b/public/r/GridScan-JS-CSS.json
index 7a5f3fab..b177a2a9 100644
--- a/public/r/GridScan-JS-CSS.json
+++ b/public/r/GridScan-JS-CSS.json
@@ -13,13 +13,13 @@
{
"type": "registry:component",
"path": "GridScan/GridScan.jsx",
- "content": "import { useEffect, useRef, useState } from 'react';\nimport { EffectComposer, RenderPass, EffectPass, BloomEffect, ChromaticAberrationEffect } from 'postprocessing';\nimport * as THREE from 'three';\nimport * as faceapi from 'face-api.js';\nimport './GridScan.css';\n\nconst vert = `\nvarying vec2 vUv;\nvoid main(){\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`;\n\nconst frag = `\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec2 uSkew;\nuniform float uTilt;\nuniform float uYaw;\nuniform float uLineThickness;\nuniform vec3 uLinesColor;\nuniform vec3 uScanColor;\nuniform float uGridScale;\nuniform float uLineStyle;\nuniform float uLineJitter;\nuniform float uScanOpacity;\nuniform float uScanDirection;\nuniform float uNoise;\nuniform float uBloomOpacity;\nuniform float uScanGlow;\nuniform float uScanSoftness;\nuniform float uPhaseTaper;\nuniform float uScanDuration;\nuniform float uScanDelay;\nvarying vec2 vUv;\n\nuniform float uScanStarts[8];\nuniform float uScanCount;\n\nconst int MAX_SCANS = 8;\n\nfloat smoother01(float a, float b, float x){\n float t = clamp((x - a) / max(1e-5, (b - a)), 0.0, 1.0);\n return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord)\n{\n vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n\n vec3 ro = vec3(0.0);\n vec3 rd = normalize(vec3(p, 2.0));\n\n float cR = cos(uTilt), sR = sin(uTilt);\n rd.xy = mat2(cR, -sR, sR, cR) * rd.xy;\n\n float cY = cos(uYaw), sY = sin(uYaw);\n rd.xz = mat2(cY, -sY, sY, cY) * rd.xz;\n\n vec2 skew = clamp(uSkew, vec2(-0.7), vec2(0.7));\n rd.xy += skew * rd.z;\n\n vec3 color = vec3(0.0);\n float minT = 1e20;\n float gridScale = max(1e-5, uGridScale);\n float fadeStrength = 2.0;\n vec2 gridUV = vec2(0.0);\n\n float hitIsY = 1.0;\n for (int i = 0; i < 4; i++)\n {\n float isY = float(i < 2);\n float pos = mix(-0.2, 0.2, float(i)) * isY + mix(-0.5, 0.5, float(i - 2)) * (1.0 - isY);\n float num = pos - (isY * ro.y + (1.0 - isY) * ro.x);\n float den = isY * rd.y + (1.0 - isY) * rd.x;\n float t = num / den;\n vec3 h = ro + rd * t;\n\n float depthBoost = smoothstep(0.0, 3.0, h.z);\n h.xy += skew * 0.15 * depthBoost;\n\n bool use = t > 0.0 && t < minT;\n gridUV = use ? mix(h.zy, h.xz, isY) / gridScale : gridUV;\n minT = use ? t : minT;\n hitIsY = use ? isY : hitIsY;\n }\n\n vec3 hit = ro + rd * minT;\n float dist = length(hit - ro);\n\n float jitterAmt = clamp(uLineJitter, 0.0, 1.0);\n if (jitterAmt > 0.0) {\n vec2 j = vec2(\n sin(gridUV.y * 2.7 + iTime * 1.8),\n cos(gridUV.x * 2.3 - iTime * 1.6)\n ) * (0.15 * jitterAmt);\n gridUV += j;\n }\n float fx = fract(gridUV.x);\n float fy = fract(gridUV.y);\n float ax = min(fx, 1.0 - fx);\n float ay = min(fy, 1.0 - fy);\n float wx = fwidth(gridUV.x);\n float wy = fwidth(gridUV.y);\n float halfPx = max(0.0, uLineThickness) * 0.5;\n\n float tx = halfPx * wx;\n float ty = halfPx * wy;\n\n float aax = wx;\n float aay = wy;\n\n float lineX = 1.0 - smoothstep(tx, tx + aax, ax);\n float lineY = 1.0 - smoothstep(ty, ty + aay, ay);\n if (uLineStyle > 0.5) {\n float dashRepeat = 4.0;\n float dashDuty = 0.5;\n float vy = fract(gridUV.y * dashRepeat);\n float vx = fract(gridUV.x * dashRepeat);\n float dashMaskY = step(vy, dashDuty);\n float dashMaskX = step(vx, dashDuty);\n if (uLineStyle < 1.5) {\n lineX *= dashMaskY;\n lineY *= dashMaskX;\n } else {\n float dotRepeat = 6.0;\n float dotWidth = 0.18;\n float cy = abs(fract(gridUV.y * dotRepeat) - 0.5);\n float cx = abs(fract(gridUV.x * dotRepeat) - 0.5);\n float dotMaskY = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.y * dotRepeat), cy);\n float dotMaskX = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.x * dotRepeat), cx);\n lineX *= dotMaskY;\n lineY *= dotMaskX;\n }\n }\n float primaryMask = max(lineX, lineY);\n\n vec2 gridUV2 = (hitIsY > 0.5 ? hit.xz : hit.zy) / gridScale;\n if (jitterAmt > 0.0) {\n vec2 j2 = vec2(\n cos(gridUV2.y * 2.1 - iTime * 1.4),\n sin(gridUV2.x * 2.5 + iTime * 1.7)\n ) * (0.15 * jitterAmt);\n gridUV2 += j2;\n }\n float fx2 = fract(gridUV2.x);\n float fy2 = fract(gridUV2.y);\n float ax2 = min(fx2, 1.0 - fx2);\n float ay2 = min(fy2, 1.0 - fy2);\n float wx2 = fwidth(gridUV2.x);\n float wy2 = fwidth(gridUV2.y);\n float tx2 = halfPx * wx2;\n float ty2 = halfPx * wy2;\n float aax2 = wx2;\n float aay2 = wy2;\n float lineX2 = 1.0 - smoothstep(tx2, tx2 + aax2, ax2);\n float lineY2 = 1.0 - smoothstep(ty2, ty2 + aay2, ay2);\n if (uLineStyle > 0.5) {\n float dashRepeat2 = 4.0;\n float dashDuty2 = 0.5;\n float vy2m = fract(gridUV2.y * dashRepeat2);\n float vx2m = fract(gridUV2.x * dashRepeat2);\n float dashMaskY2 = step(vy2m, dashDuty2);\n float dashMaskX2 = step(vx2m, dashDuty2);\n if (uLineStyle < 1.5) {\n lineX2 *= dashMaskY2;\n lineY2 *= dashMaskX2;\n } else {\n float dotRepeat2 = 6.0;\n float dotWidth2 = 0.18;\n float cy2 = abs(fract(gridUV2.y * dotRepeat2) - 0.5);\n float cx2 = abs(fract(gridUV2.x * dotRepeat2) - 0.5);\n float dotMaskY2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.y * dotRepeat2), cy2);\n float dotMaskX2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.x * dotRepeat2), cx2);\n lineX2 *= dotMaskY2;\n lineY2 *= dotMaskX2;\n }\n }\n float altMask = max(lineX2, lineY2);\n\n float edgeDistX = min(abs(hit.x - (-0.5)), abs(hit.x - 0.5));\n float edgeDistY = min(abs(hit.y - (-0.2)), abs(hit.y - 0.2));\n float edgeDist = mix(edgeDistY, edgeDistX, hitIsY);\n float edgeGate = 1.0 - smoothstep(gridScale * 0.5, gridScale * 2.0, edgeDist);\n altMask *= edgeGate;\n\n float lineMask = max(primaryMask, altMask);\n\n float fade = exp(-dist * fadeStrength);\n\n float dur = max(0.05, uScanDuration);\n float del = max(0.0, uScanDelay);\n float scanZMax = 2.0;\n float widthScale = max(0.1, uScanGlow);\n float sigma = max(0.001, 0.18 * widthScale * uScanSoftness);\n float sigmaA = sigma * 2.0;\n\n float combinedPulse = 0.0;\n float combinedAura = 0.0;\n\n float cycle = dur + del;\n float tCycle = mod(iTime, cycle);\n float scanPhase = clamp((tCycle - del) / dur, 0.0, 1.0);\n float phase = scanPhase;\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phase = 1.0 - phase;\n } else if (uScanDirection > 1.5) {\n float t2 = mod(max(0.0, iTime - del), 2.0 * dur);\n phase = (t2 < dur) ? (t2 / dur) : (1.0 - (t2 - dur) / dur);\n }\n float scanZ = phase * scanZMax;\n float dz = abs(hit.z - scanZ);\n float lineBand = exp(-0.5 * (dz * dz) / (sigma * sigma));\n float taper = clamp(uPhaseTaper, 0.0, 0.49);\n float headW = taper;\n float tailW = taper;\n float headFade = smoother01(0.0, headW, phase);\n float tailFade = 1.0 - smoother01(1.0 - tailW, 1.0, phase);\n float phaseWindow = headFade * tailFade;\n float pulseBase = lineBand * phaseWindow;\n combinedPulse += pulseBase * clamp(uScanOpacity, 0.0, 1.0);\n float auraBand = exp(-0.5 * (dz * dz) / (sigmaA * sigmaA));\n combinedAura += (auraBand * 0.25) * phaseWindow * clamp(uScanOpacity, 0.0, 1.0);\n\n for (int i = 0; i < MAX_SCANS; i++) {\n if (float(i) >= uScanCount) break;\n float tActiveI = iTime - uScanStarts[i];\n float phaseI = clamp(tActiveI / dur, 0.0, 1.0);\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phaseI = 1.0 - phaseI;\n } else if (uScanDirection > 1.5) {\n phaseI = (phaseI < 0.5) ? (phaseI * 2.0) : (1.0 - (phaseI - 0.5) * 2.0);\n }\n float scanZI = phaseI * scanZMax;\n float dzI = abs(hit.z - scanZI);\n float lineBandI = exp(-0.5 * (dzI * dzI) / (sigma * sigma));\n float headFadeI = smoother01(0.0, headW, phaseI);\n float tailFadeI = 1.0 - smoother01(1.0 - tailW, 1.0, phaseI);\n float phaseWindowI = headFadeI * tailFadeI;\n combinedPulse += lineBandI * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n float auraBandI = exp(-0.5 * (dzI * dzI) / (sigmaA * sigmaA));\n combinedAura += (auraBandI * 0.25) * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n }\n\n float lineVis = lineMask;\n vec3 gridCol = uLinesColor * lineVis * fade;\n vec3 scanCol = uScanColor * combinedPulse;\n vec3 scanAura = uScanColor * combinedAura;\n\n color = gridCol + scanCol + scanAura;\n\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(iTime * 123.4), vec2(12.9898,78.233))) * 43758.5453123);\n color += (n - 0.5) * uNoise;\n color = clamp(color, 0.0, 1.0);\n float alpha = clamp(max(lineVis, combinedPulse), 0.0, 1.0);\n float gx = 1.0 - smoothstep(tx * 2.0, tx * 2.0 + aax * 2.0, ax);\n float gy = 1.0 - smoothstep(ty * 2.0, ty * 2.0 + aay * 2.0, ay);\n float halo = max(gx, gy) * fade;\n alpha = max(alpha, halo * clamp(uBloomOpacity, 0.0, 1.0));\n fragColor = vec4(color, alpha);\n}\n\nvoid main(){\n vec4 c;\n mainImage(c, vUv * iResolution.xy);\n gl_FragColor = c;\n}\n`;\n\nexport const GridScan = ({\n enableWebcam = false,\n showPreview = false,\n modelsPath = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights',\n sensitivity = 0.55,\n lineThickness = 1,\n linesColor = '#392e4e',\n scanColor = '#FF9FFC',\n scanOpacity = 0.4,\n gridScale = 0.1,\n lineStyle = 'solid',\n lineJitter = 0.1,\n scanDirection = 'pingpong',\n enablePost = true,\n bloomIntensity = 0,\n bloomThreshold = 0,\n bloomSmoothing = 0,\n chromaticAberration = 0.002,\n noiseIntensity = 0.01,\n scanGlow = 0.5,\n scanSoftness = 2,\n scanPhaseTaper = 0.9,\n scanDuration = 2.0,\n scanDelay = 2.0,\n enableGyro = false,\n scanOnClick = false,\n snapBackDelay = 250,\n className,\n style\n}) => {\n const containerRef = useRef(null);\n const videoRef = useRef(null);\n\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const composerRef = useRef(null);\n const bloomRef = useRef(null);\n const chromaRef = useRef(null);\n const rafRef = useRef(null);\n\n const [modelsReady, setModelsReady] = useState(false);\n const [uiFaceActive, setUiFaceActive] = useState(false);\n\n const lookTarget = useRef(new THREE.Vector2(0, 0));\n const tiltTarget = useRef(0);\n const yawTarget = useRef(0);\n\n const lookCurrent = useRef(new THREE.Vector2(0, 0));\n const lookVel = useRef(new THREE.Vector2(0, 0));\n const tiltCurrent = useRef(0);\n const tiltVel = useRef(0);\n const yawCurrent = useRef(0);\n const yawVel = useRef(0);\n\n const MAX_SCANS = 8;\n const scanStartsRef = useRef([]);\n\n const pushScan = t => {\n const arr = scanStartsRef.current.slice();\n if (arr.length >= MAX_SCANS) arr.shift();\n arr.push(t);\n scanStartsRef.current = arr;\n if (materialRef.current) {\n const u = materialRef.current.uniforms;\n const buf = new Array(MAX_SCANS).fill(0);\n for (let i = 0; i < arr.length && i < MAX_SCANS; i++) buf[i] = arr[i];\n u.uScanStarts.value = buf;\n u.uScanCount.value = arr.length;\n }\n };\n\n const bufX = useRef([]);\n const bufY = useRef([]);\n const bufT = useRef([]);\n const bufYaw = useRef([]);\n\n const s = THREE.MathUtils.clamp(sensitivity, 0, 1);\n const skewScale = THREE.MathUtils.lerp(0.06, 0.2, s);\n const tiltScale = THREE.MathUtils.lerp(0.12, 0.3, s);\n const yawScale = THREE.MathUtils.lerp(0.1, 0.28, s);\n const depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s);\n const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s);\n const maxSpeed = Infinity;\n\n const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n let leaveTimer = null;\n const onMove = e => {\n if (uiFaceActive) return;\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n const rect = el.getBoundingClientRect();\n const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n lookTarget.current.set(nx, ny);\n };\n const onClick = async () => {\n const nowSec = performance.now() / 1000;\n if (scanOnClick) pushScan(nowSec);\n if (\n enableGyro &&\n typeof window !== 'undefined' &&\n window.DeviceOrientationEvent &&\n DeviceOrientationEvent.requestPermission\n ) {\n try {\n await DeviceOrientationEvent.requestPermission();\n } catch {\n // noop\n }\n }\n };\n const onEnter = () => {\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n };\n const onLeave = () => {\n if (uiFaceActive) return;\n if (leaveTimer) clearTimeout(leaveTimer);\n leaveTimer = window.setTimeout(\n () => {\n lookTarget.current.set(0, 0);\n tiltTarget.current = 0;\n yawTarget.current = 0;\n },\n Math.max(0, snapBackDelay || 0)\n );\n };\n el.addEventListener('mousemove', onMove);\n el.addEventListener('mouseenter', onEnter);\n if (scanOnClick) el.addEventListener('click', onClick);\n el.addEventListener('mouseleave', onLeave);\n return () => {\n el.removeEventListener('mousemove', onMove);\n el.removeEventListener('mouseenter', onEnter);\n el.removeEventListener('mouseleave', onLeave);\n if (scanOnClick) el.removeEventListener('click', onClick);\n if (leaveTimer) clearTimeout(leaveTimer);\n };\n }, [uiFaceActive, snapBackDelay, scanOnClick, enableGyro]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n rendererRef.current = renderer;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setSize(container.clientWidth, container.clientHeight);\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.toneMapping = THREE.NoToneMapping;\n renderer.autoClear = false;\n renderer.setClearColor(0x000000, 0);\n container.appendChild(renderer.domElement);\n\n const uniforms = {\n iResolution: {\n value: new THREE.Vector3(container.clientWidth, container.clientHeight, renderer.getPixelRatio())\n },\n iTime: { value: 0 },\n uSkew: { value: new THREE.Vector2(0, 0) },\n uTilt: { value: 0 },\n uYaw: { value: 0 },\n uLineThickness: { value: lineThickness },\n uLinesColor: { value: srgbColor(linesColor) },\n uScanColor: { value: srgbColor(scanColor) },\n uGridScale: { value: gridScale },\n uLineStyle: { value: lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0 },\n uLineJitter: { value: Math.max(0, Math.min(1, lineJitter || 0)) },\n uScanOpacity: { value: scanOpacity },\n uNoise: { value: noiseIntensity },\n uBloomOpacity: { value: bloomIntensity },\n uScanGlow: { value: scanGlow },\n uScanSoftness: { value: scanSoftness },\n uPhaseTaper: { value: scanPhaseTaper },\n uScanDuration: { value: scanDuration },\n uScanDelay: { value: scanDelay },\n uScanDirection: { value: scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0 },\n uScanStarts: { value: new Array(MAX_SCANS).fill(0) },\n uScanCount: { value: 0 }\n };\n\n const material = new THREE.ShaderMaterial({\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.current = material;\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);\n scene.add(quad);\n\n let composer = null;\n if (enablePost) {\n composer = new EffectComposer(renderer);\n composerRef.current = composer;\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloom = new BloomEffect({\n intensity: 1.0,\n luminanceThreshold: bloomThreshold,\n luminanceSmoothing: bloomSmoothing\n });\n bloom.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current = bloom;\n\n const chroma = new ChromaticAberrationEffect({\n offset: new THREE.Vector2(chromaticAberration, chromaticAberration),\n radialModulation: true,\n modulationOffset: 0.0\n });\n chromaRef.current = chroma;\n\n const effectPass = new EffectPass(camera, bloom, chroma);\n effectPass.renderToScreen = true;\n composer.addPass(effectPass);\n }\n\n const onResize = () => {\n renderer.setSize(container.clientWidth, container.clientHeight);\n material.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio());\n if (composerRef.current) composerRef.current.setSize(container.clientWidth, container.clientHeight);\n };\n window.addEventListener('resize', onResize);\n\n let last = performance.now();\n const tick = () => {\n const now = performance.now();\n const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));\n last = now;\n\n lookCurrent.current.copy(\n smoothDampVec2(lookCurrent.current, lookTarget.current, lookVel.current, smoothTime, maxSpeed, dt)\n );\n\n const tiltSm = smoothDampFloat(\n tiltCurrent.current,\n tiltTarget.current,\n { v: tiltVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n tiltCurrent.current = tiltSm.value;\n tiltVel.current = tiltSm.v;\n\n const yawSm = smoothDampFloat(\n yawCurrent.current,\n yawTarget.current,\n { v: yawVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n yawCurrent.current = yawSm.value;\n yawVel.current = yawSm.v;\n\n const skew = new THREE.Vector2(lookCurrent.current.x * skewScale, -lookCurrent.current.y * yBoost * skewScale);\n material.uniforms.uSkew.value.set(skew.x, skew.y);\n material.uniforms.uTilt.value = tiltCurrent.current * tiltScale;\n material.uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent.current * yawScale, -0.6, 0.6);\n\n material.uniforms.iTime.value = now / 1000;\n renderer.clear(true, true, true);\n if (composerRef.current) {\n composerRef.current.render(dt);\n } else {\n renderer.render(scene, camera);\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n rafRef.current = requestAnimationFrame(tick);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n window.removeEventListener('resize', onResize);\n material.dispose();\n quad.geometry.dispose();\n\n if (composerRef.current) {\n composerRef.current.dispose();\n composerRef.current = null;\n }\n renderer.dispose();\n container.removeChild(renderer.domElement);\n };\n }, [\n sensitivity,\n lineThickness,\n linesColor,\n scanColor,\n scanOpacity,\n gridScale,\n lineStyle,\n lineJitter,\n scanDirection,\n enablePost,\n noiseIntensity,\n bloomIntensity,\n scanGlow,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n smoothTime,\n maxSpeed,\n skewScale,\n yBoost,\n tiltScale,\n yawScale\n ]);\n\n useEffect(() => {\n const m = materialRef.current;\n if (m) {\n const u = m.uniforms;\n u.uLineThickness.value = lineThickness;\n u.uLinesColor.value.copy(srgbColor(linesColor));\n u.uScanColor.value.copy(srgbColor(scanColor));\n u.uGridScale.value = gridScale;\n u.uLineStyle.value = lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0;\n u.uLineJitter.value = Math.max(0, Math.min(1, lineJitter || 0));\n u.uBloomOpacity.value = Math.max(0, bloomIntensity);\n u.uNoise.value = Math.max(0, noiseIntensity);\n u.uScanGlow.value = scanGlow;\n u.uScanOpacity.value = Math.max(0, Math.min(1, scanOpacity));\n u.uScanDirection.value = scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0;\n u.uScanSoftness.value = scanSoftness;\n u.uPhaseTaper.value = scanPhaseTaper;\n u.uScanDuration.value = Math.max(0.05, scanDuration);\n u.uScanDelay.value = Math.max(0.0, scanDelay);\n }\n if (bloomRef.current) {\n bloomRef.current.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current.luminanceMaterial.threshold = bloomThreshold;\n bloomRef.current.luminanceMaterial.smoothing = bloomSmoothing;\n }\n if (chromaRef.current) {\n chromaRef.current.offset.set(chromaticAberration, chromaticAberration);\n }\n }, [\n lineThickness,\n linesColor,\n scanColor,\n gridScale,\n lineStyle,\n lineJitter,\n bloomIntensity,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n noiseIntensity,\n scanGlow,\n scanOpacity,\n scanDirection,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay\n ]);\n\n useEffect(() => {\n if (!enableGyro) return;\n const handler = e => {\n if (uiFaceActive) return;\n const gamma = e.gamma ?? 0;\n const beta = e.beta ?? 0;\n const nx = THREE.MathUtils.clamp(gamma / 45, -1, 1);\n const ny = THREE.MathUtils.clamp(-beta / 30, -1, 1);\n lookTarget.current.set(nx, ny);\n tiltTarget.current = THREE.MathUtils.degToRad(gamma) * 0.4;\n };\n window.addEventListener('deviceorientation', handler);\n return () => {\n window.removeEventListener('deviceorientation', handler);\n };\n }, [enableGyro, uiFaceActive]);\n\n useEffect(() => {\n let canceled = false;\n const load = async () => {\n try {\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath),\n faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelsPath)\n ]);\n if (!canceled) setModelsReady(true);\n } catch {\n if (!canceled) setModelsReady(false);\n }\n };\n load();\n return () => {\n canceled = true;\n };\n }, [modelsPath]);\n\n useEffect(() => {\n let stop = false;\n let lastDetect = 0;\n const video = videoRef.current;\n\n const start = async () => {\n if (!enableWebcam || !modelsReady) return;\n if (!video) return;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } },\n audio: false\n });\n video.srcObject = stream;\n await video.play();\n } catch {\n return;\n }\n\n const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });\n\n const detect = async ts => {\n if (stop) return;\n\n if (ts - lastDetect >= 33) {\n lastDetect = ts;\n try {\n const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true);\n if (res && res.detection) {\n const det = res.detection;\n const box = det.box;\n const vw = video.videoWidth || 1;\n const vh = video.videoHeight || 1;\n\n const cx = box.x + box.width * 0.5;\n const cy = box.y + box.height * 0.5;\n const nx = (cx / vw) * 2 - 1;\n const ny = (cy / vh) * 2 - 1;\n medianPush(bufX.current, nx, 5);\n medianPush(bufY.current, ny, 5);\n const nxm = median(bufX.current);\n const nym = median(bufY.current);\n\n const look = new THREE.Vector2(Math.tanh(nxm), Math.tanh(nym));\n\n const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh));\n const depthScale = 1 + depthResponse * (faceSize - 0.25);\n lookTarget.current.copy(look.multiplyScalar(depthScale));\n\n const leftEye = res.landmarks.getLeftEye();\n const rightEye = res.landmarks.getRightEye();\n const lc = centroid(leftEye);\n const rc = centroid(rightEye);\n const tilt = Math.atan2(rc.y - lc.y, rc.x - lc.x);\n medianPush(bufT.current, tilt, 5);\n tiltTarget.current = median(bufT.current);\n\n const nose = res.landmarks.getNose();\n const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)];\n const jaw = res.landmarks.getJawOutline();\n const leftCheek = jaw[3] || jaw[2];\n const rightCheek = jaw[13] || jaw[14];\n const dL = dist2(tip, leftCheek);\n const dR = dist2(tip, rightCheek);\n const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6;\n let yawSignal = THREE.MathUtils.clamp((dR - dL) / (eyeDist * 1.6), -1, 1);\n yawSignal = Math.tanh(yawSignal);\n medianPush(bufYaw.current, yawSignal, 5);\n yawTarget.current = median(bufYaw.current);\n\n setUiFaceActive(true);\n } else {\n setUiFaceActive(false);\n }\n } catch {\n setUiFaceActive(false);\n }\n }\n\n if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {\n video.requestVideoFrameCallback(() => detect(performance.now()));\n } else {\n requestAnimationFrame(detect);\n }\n };\n\n requestAnimationFrame(detect);\n };\n\n start();\n\n return () => {\n stop = true;\n if (video) {\n const stream = video.srcObject;\n if (stream) stream.getTracks().forEach(t => t.stop());\n video.pause();\n video.srcObject = null;\n }\n };\n }, [enableWebcam, modelsReady, depthResponse]);\n\n return (\n \n {showPreview && (\n
\n
\n
\n {enableWebcam\n ? modelsReady\n ? uiFaceActive\n ? 'Face: tracking'\n : 'Face: searching'\n : 'Loading models'\n : 'Webcam disabled'}\n
\n
\n )}\n
\n );\n};\n\nfunction srgbColor(hex) {\n const c = new THREE.Color(hex);\n return c.convertSRGBToLinear();\n}\n\nfunction smoothDampVec2(current, target, currentVelocity, smoothTime, maxSpeed, deltaTime) {\n const out = current.clone();\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current.clone().sub(target);\n const originalTo = target.clone();\n\n const maxChange = maxSpeed * smoothTime;\n if (change.length() > maxChange) change.setLength(maxChange);\n\n target = current.clone().sub(change);\n const temp = currentVelocity.clone().addScaledVector(change, omega).multiplyScalar(deltaTime);\n currentVelocity.sub(temp.clone().multiplyScalar(omega));\n currentVelocity.multiplyScalar(exp);\n\n out.copy(target.clone().add(change.add(temp).multiplyScalar(exp)));\n\n const origMinusCurrent = originalTo.clone().sub(current);\n const outMinusOrig = out.clone().sub(originalTo);\n if (origMinusCurrent.dot(outMinusOrig) > 0) {\n out.copy(originalTo);\n currentVelocity.set(0, 0);\n }\n return out;\n}\n\nfunction smoothDampFloat(current, target, velRef, smoothTime, maxSpeed, deltaTime) {\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current - target;\n const originalTo = target;\n\n const maxChange = maxSpeed * smoothTime;\n change = Math.sign(change) * Math.min(Math.abs(change), maxChange);\n\n target = current - change;\n const temp = (velRef.v + omega * change) * deltaTime;\n velRef.v = (velRef.v - omega * temp) * exp;\n\n let out = target + (change + temp) * exp;\n\n const origMinusCurrent = originalTo - current;\n const outMinusOrig = out - originalTo;\n if (origMinusCurrent * outMinusOrig > 0) {\n out = originalTo;\n velRef.v = 0;\n }\n return { value: out, v: velRef.v };\n}\n\nfunction medianPush(buf, v, maxLen) {\n buf.push(v);\n if (buf.length > maxLen) buf.shift();\n}\n\nfunction median(buf) {\n if (buf.length === 0) return 0;\n const a = [...buf].sort((x, y) => x - y);\n const mid = Math.floor(a.length / 2);\n return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) * 0.5;\n}\n\nfunction centroid(points) {\n let x = 0,\n y = 0;\n const n = points.length || 1;\n for (const p of points) {\n x += p.x;\n y += p.y;\n }\n return { x: x / n, y: y / n };\n}\n\nfunction dist2(a, b) {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n"
+ "content": "import * as faceapi from 'face-api.js';\nimport { BloomEffect, ChromaticAberrationEffect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';\nimport { useEffect, useRef, useState } from 'react';\nimport * as THREE from 'three';\nimport './GridScan.css';\n\nconst vert = `\nvarying vec2 vUv;\nvoid main(){\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`;\n\nconst frag = `\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec2 uSkew;\nuniform float uTilt;\nuniform float uYaw;\nuniform float uLineThickness;\nuniform vec3 uLinesColor;\nuniform vec3 uScanColor;\nuniform float uGridScale;\nuniform float uLineStyle;\nuniform float uLineJitter;\nuniform float uScanOpacity;\nuniform float uScanDirection;\nuniform float uNoise;\nuniform float uBloomOpacity;\nuniform float uScanGlow;\nuniform float uScanSoftness;\nuniform float uPhaseTaper;\nuniform float uScanDuration;\nuniform float uScanDelay;\nvarying vec2 vUv;\n\nuniform float uScanStarts[8];\nuniform float uScanCount;\n\nconst int MAX_SCANS = 8;\n\nfloat smoother01(float a, float b, float x){\n float t = clamp((x - a) / max(1e-5, (b - a)), 0.0, 1.0);\n return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord)\n{\n vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n\n vec3 ro = vec3(0.0);\n vec3 rd = normalize(vec3(p, 2.0));\n\n float cR = cos(uTilt), sR = sin(uTilt);\n rd.xy = mat2(cR, -sR, sR, cR) * rd.xy;\n\n float cY = cos(uYaw), sY = sin(uYaw);\n rd.xz = mat2(cY, -sY, sY, cY) * rd.xz;\n\n vec2 skew = clamp(uSkew, vec2(-0.7), vec2(0.7));\n rd.xy += skew * rd.z;\n\n vec3 color = vec3(0.0);\n float minT = 1e20;\n float gridScale = max(1e-5, uGridScale);\n float fadeStrength = 2.0;\n vec2 gridUV = vec2(0.0);\n\n float hitIsY = 1.0;\n for (int i = 0; i < 4; i++)\n {\n float isY = float(i < 2);\n float pos = mix(-0.2, 0.2, float(i)) * isY + mix(-0.5, 0.5, float(i - 2)) * (1.0 - isY);\n float num = pos - (isY * ro.y + (1.0 - isY) * ro.x);\n float den = isY * rd.y + (1.0 - isY) * rd.x;\n float t = num / den;\n vec3 h = ro + rd * t;\n\n float depthBoost = smoothstep(0.0, 3.0, h.z);\n h.xy += skew * 0.15 * depthBoost;\n\n bool use = t > 0.0 && t < minT;\n gridUV = use ? mix(h.zy, h.xz, isY) / gridScale : gridUV;\n minT = use ? t : minT;\n hitIsY = use ? isY : hitIsY;\n }\n\n vec3 hit = ro + rd * minT;\n float dist = length(hit - ro);\n\n float jitterAmt = clamp(uLineJitter, 0.0, 1.0);\n if (jitterAmt > 0.0) {\n vec2 j = vec2(\n sin(gridUV.y * 2.7 + iTime * 1.8),\n cos(gridUV.x * 2.3 - iTime * 1.6)\n ) * (0.15 * jitterAmt);\n gridUV += j;\n }\n float fx = fract(gridUV.x);\n float fy = fract(gridUV.y);\n float ax = min(fx, 1.0 - fx);\n float ay = min(fy, 1.0 - fy);\n float wx = fwidth(gridUV.x);\n float wy = fwidth(gridUV.y);\n float halfPx = max(0.0, uLineThickness) * 0.5;\n\n float tx = halfPx * wx;\n float ty = halfPx * wy;\n\n float aax = wx;\n float aay = wy;\n\n float lineX = 1.0 - smoothstep(tx, tx + aax, ax);\n float lineY = 1.0 - smoothstep(ty, ty + aay, ay);\n if (uLineStyle > 0.5) {\n float dashRepeat = 4.0;\n float dashDuty = 0.5;\n float vy = fract(gridUV.y * dashRepeat);\n float vx = fract(gridUV.x * dashRepeat);\n float dashMaskY = step(vy, dashDuty);\n float dashMaskX = step(vx, dashDuty);\n if (uLineStyle < 1.5) {\n lineX *= dashMaskY;\n lineY *= dashMaskX;\n } else {\n float dotRepeat = 6.0;\n float dotWidth = 0.18;\n float cy = abs(fract(gridUV.y * dotRepeat) - 0.5);\n float cx = abs(fract(gridUV.x * dotRepeat) - 0.5);\n float dotMaskY = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.y * dotRepeat), cy);\n float dotMaskX = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.x * dotRepeat), cx);\n lineX *= dotMaskY;\n lineY *= dotMaskX;\n }\n }\n float primaryMask = max(lineX, lineY);\n\n vec2 gridUV2 = (hitIsY > 0.5 ? hit.xz : hit.zy) / gridScale;\n if (jitterAmt > 0.0) {\n vec2 j2 = vec2(\n cos(gridUV2.y * 2.1 - iTime * 1.4),\n sin(gridUV2.x * 2.5 + iTime * 1.7)\n ) * (0.15 * jitterAmt);\n gridUV2 += j2;\n }\n float fx2 = fract(gridUV2.x);\n float fy2 = fract(gridUV2.y);\n float ax2 = min(fx2, 1.0 - fx2);\n float ay2 = min(fy2, 1.0 - fy2);\n float wx2 = fwidth(gridUV2.x);\n float wy2 = fwidth(gridUV2.y);\n float tx2 = halfPx * wx2;\n float ty2 = halfPx * wy2;\n float aax2 = wx2;\n float aay2 = wy2;\n float lineX2 = 1.0 - smoothstep(tx2, tx2 + aax2, ax2);\n float lineY2 = 1.0 - smoothstep(ty2, ty2 + aay2, ay2);\n if (uLineStyle > 0.5) {\n float dashRepeat2 = 4.0;\n float dashDuty2 = 0.5;\n float vy2m = fract(gridUV2.y * dashRepeat2);\n float vx2m = fract(gridUV2.x * dashRepeat2);\n float dashMaskY2 = step(vy2m, dashDuty2);\n float dashMaskX2 = step(vx2m, dashDuty2);\n if (uLineStyle < 1.5) {\n lineX2 *= dashMaskY2;\n lineY2 *= dashMaskX2;\n } else {\n float dotRepeat2 = 6.0;\n float dotWidth2 = 0.18;\n float cy2 = abs(fract(gridUV2.y * dotRepeat2) - 0.5);\n float cx2 = abs(fract(gridUV2.x * dotRepeat2) - 0.5);\n float dotMaskY2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.y * dotRepeat2), cy2);\n float dotMaskX2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.x * dotRepeat2), cx2);\n lineX2 *= dotMaskY2;\n lineY2 *= dotMaskX2;\n }\n }\n float altMask = max(lineX2, lineY2);\n\n float edgeDistX = min(abs(hit.x - (-0.5)), abs(hit.x - 0.5));\n float edgeDistY = min(abs(hit.y - (-0.2)), abs(hit.y - 0.2));\n float edgeDist = mix(edgeDistY, edgeDistX, hitIsY);\n float edgeGate = 1.0 - smoothstep(gridScale * 0.5, gridScale * 2.0, edgeDist);\n altMask *= edgeGate;\n\n float lineMask = max(primaryMask, altMask);\n\n float fade = exp(-dist * fadeStrength);\n\n float dur = max(0.05, uScanDuration);\n float del = max(0.0, uScanDelay);\n float scanZMax = 2.0;\n float widthScale = max(0.1, uScanGlow);\n float sigma = max(0.001, 0.18 * widthScale * uScanSoftness);\n float sigmaA = sigma * 2.0;\n\n float combinedPulse = 0.0;\n float combinedAura = 0.0;\n\n float cycle = dur + del;\n float tCycle = mod(iTime, cycle);\n float scanPhase = clamp((tCycle - del) / dur, 0.0, 1.0);\n float phase = scanPhase;\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phase = 1.0 - phase;\n } else if (uScanDirection > 1.5) {\n float t2 = mod(max(0.0, iTime - del), 2.0 * dur);\n phase = (t2 < dur) ? (t2 / dur) : (1.0 - (t2 - dur) / dur);\n }\n float scanZ = phase * scanZMax;\n float dz = abs(hit.z - scanZ);\n float lineBand = exp(-0.5 * (dz * dz) / (sigma * sigma));\n float taper = clamp(uPhaseTaper, 0.0, 0.49);\n float headW = taper;\n float tailW = taper;\n float headFade = smoother01(0.0, headW, phase);\n float tailFade = 1.0 - smoother01(1.0 - tailW, 1.0, phase);\n float phaseWindow = headFade * tailFade;\n float pulseBase = lineBand * phaseWindow;\n combinedPulse += pulseBase * clamp(uScanOpacity, 0.0, 1.0);\n float auraBand = exp(-0.5 * (dz * dz) / (sigmaA * sigmaA));\n combinedAura += (auraBand * 0.25) * phaseWindow * clamp(uScanOpacity, 0.0, 1.0);\n\n for (int i = 0; i < MAX_SCANS; i++) {\n if (float(i) >= uScanCount) break;\n float tActiveI = iTime - uScanStarts[i];\n float phaseI = clamp(tActiveI / dur, 0.0, 1.0);\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phaseI = 1.0 - phaseI;\n } else if (uScanDirection > 1.5) {\n phaseI = (phaseI < 0.5) ? (phaseI * 2.0) : (1.0 - (phaseI - 0.5) * 2.0);\n }\n float scanZI = phaseI * scanZMax;\n float dzI = abs(hit.z - scanZI);\n float lineBandI = exp(-0.5 * (dzI * dzI) / (sigma * sigma));\n float headFadeI = smoother01(0.0, headW, phaseI);\n float tailFadeI = 1.0 - smoother01(1.0 - tailW, 1.0, phaseI);\n float phaseWindowI = headFadeI * tailFadeI;\n combinedPulse += lineBandI * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n float auraBandI = exp(-0.5 * (dzI * dzI) / (sigmaA * sigmaA));\n combinedAura += (auraBandI * 0.25) * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n }\n\n float lineVis = lineMask;\n vec3 gridCol = uLinesColor * lineVis * fade;\n vec3 scanCol = uScanColor * combinedPulse;\n vec3 scanAura = uScanColor * combinedAura;\n\n color = gridCol + scanCol + scanAura;\n\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(iTime * 123.4), vec2(12.9898,78.233))) * 43758.5453123);\n color += (n - 0.5) * uNoise;\n color = clamp(color, 0.0, 1.0);\n float alpha = clamp(max(lineVis, combinedPulse), 0.0, 1.0);\n float gx = 1.0 - smoothstep(tx * 2.0, tx * 2.0 + aax * 2.0, ax);\n float gy = 1.0 - smoothstep(ty * 2.0, ty * 2.0 + aay * 2.0, ay);\n float halo = max(gx, gy) * fade;\n alpha = max(alpha, halo * clamp(uBloomOpacity, 0.0, 1.0));\n fragColor = vec4(color, alpha);\n}\n\nvoid main(){\n vec4 c;\n mainImage(c, vUv * iResolution.xy);\n gl_FragColor = c;\n}\n`;\n\nexport const GridScan = ({\n enableWebcam = false,\n showPreview = false,\n modelsPath = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights',\n sensitivity = 0.55,\n lineThickness = 1,\n linesColor = '#392e4e',\n scanColor = '#FF9FFC',\n scanOpacity = 0.4,\n gridScale = 0.1,\n lineStyle = 'solid',\n lineJitter = 0.1,\n scanDirection = 'pingpong',\n enablePost = true,\n bloomIntensity = 0,\n bloomThreshold = 0,\n bloomSmoothing = 0,\n chromaticAberration = 0.002,\n noiseIntensity = 0.01,\n scanGlow = 0.5,\n scanSoftness = 2,\n scanPhaseTaper = 0.9,\n scanDuration = 2.0,\n scanDelay = 2.0,\n enableGyro = false,\n scanOnClick = false,\n snapBackDelay = 250,\n className,\n style\n}) => {\n const containerRef = useRef(null);\n const videoRef = useRef(null);\n\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const composerRef = useRef(null);\n const bloomRef = useRef(null);\n const chromaRef = useRef(null);\n const rafRef = useRef(null);\n\n const [modelsReady, setModelsReady] = useState(false);\n const [uiFaceActive, setUiFaceActive] = useState(false);\n\n const lookTarget = useRef(new THREE.Vector2(0, 0));\n const tiltTarget = useRef(0);\n const yawTarget = useRef(0);\n\n const lookCurrent = useRef(new THREE.Vector2(0, 0));\n const lookVel = useRef(new THREE.Vector2(0, 0));\n const tiltCurrent = useRef(0);\n const tiltVel = useRef(0);\n const yawCurrent = useRef(0);\n const yawVel = useRef(0);\n\n const MAX_SCANS = 8;\n const scanStartsRef = useRef([]);\n\n const pushScan = t => {\n const arr = scanStartsRef.current.slice();\n if (arr.length >= MAX_SCANS) arr.shift();\n arr.push(t);\n scanStartsRef.current = arr;\n if (materialRef.current) {\n const u = materialRef.current.uniforms;\n const buf = new Array(MAX_SCANS).fill(0);\n for (let i = 0; i < arr.length && i < MAX_SCANS; i++) buf[i] = arr[i];\n u.uScanStarts.value = buf;\n u.uScanCount.value = arr.length;\n }\n };\n\n const bufX = useRef([]);\n const bufY = useRef([]);\n const bufT = useRef([]);\n const bufYaw = useRef([]);\n\n const s = THREE.MathUtils.clamp(sensitivity, 0, 1);\n const skewScale = THREE.MathUtils.lerp(0.06, 0.2, s);\n const tiltScale = THREE.MathUtils.lerp(0.12, 0.3, s);\n const yawScale = THREE.MathUtils.lerp(0.1, 0.28, s);\n const depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s);\n const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s);\n const maxSpeed = Infinity;\n\n const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n let leaveTimer = null;\n const onMove = e => {\n if (uiFaceActive) return;\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n const rect = el.getBoundingClientRect();\n const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n lookTarget.current.set(nx, ny);\n };\n const onClick = async () => {\n const nowSec = performance.now() / 1000;\n if (scanOnClick) pushScan(nowSec);\n if (\n enableGyro &&\n typeof window !== 'undefined' &&\n window.DeviceOrientationEvent &&\n DeviceOrientationEvent.requestPermission\n ) {\n try {\n await DeviceOrientationEvent.requestPermission();\n } catch {\n // noop\n }\n }\n };\n const onEnter = () => {\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n };\n const onLeave = () => {\n if (uiFaceActive) return;\n if (leaveTimer) clearTimeout(leaveTimer);\n leaveTimer = window.setTimeout(\n () => {\n lookTarget.current.set(0, 0);\n tiltTarget.current = 0;\n yawTarget.current = 0;\n },\n Math.max(0, snapBackDelay || 0)\n );\n };\n el.addEventListener('mousemove', onMove);\n el.addEventListener('mouseenter', onEnter);\n if (scanOnClick) el.addEventListener('click', onClick);\n el.addEventListener('mouseleave', onLeave);\n return () => {\n el.removeEventListener('mousemove', onMove);\n el.removeEventListener('mouseenter', onEnter);\n el.removeEventListener('mouseleave', onLeave);\n if (scanOnClick) el.removeEventListener('click', onClick);\n if (leaveTimer) clearTimeout(leaveTimer);\n };\n }, [uiFaceActive, snapBackDelay, scanOnClick, enableGyro]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n rendererRef.current = renderer;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setSize(container.clientWidth, container.clientHeight);\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.toneMapping = THREE.NoToneMapping;\n renderer.autoClear = false;\n renderer.setClearColor(0x000000, 0);\n container.appendChild(renderer.domElement);\n\n const uniforms = {\n iResolution: {\n value: new THREE.Vector3(container.clientWidth, container.clientHeight, renderer.getPixelRatio())\n },\n iTime: { value: 0 },\n uSkew: { value: new THREE.Vector2(0, 0) },\n uTilt: { value: 0 },\n uYaw: { value: 0 },\n uLineThickness: { value: lineThickness },\n uLinesColor: { value: srgbColor(linesColor) },\n uScanColor: { value: srgbColor(scanColor) },\n uGridScale: { value: gridScale },\n uLineStyle: { value: lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0 },\n uLineJitter: { value: Math.max(0, Math.min(1, lineJitter || 0)) },\n uScanOpacity: { value: scanOpacity },\n uNoise: { value: noiseIntensity },\n uBloomOpacity: { value: bloomIntensity },\n uScanGlow: { value: scanGlow },\n uScanSoftness: { value: scanSoftness },\n uPhaseTaper: { value: scanPhaseTaper },\n uScanDuration: { value: scanDuration },\n uScanDelay: { value: scanDelay },\n uScanDirection: { value: scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0 },\n uScanStarts: { value: new Array(MAX_SCANS).fill(0) },\n uScanCount: { value: 0 }\n };\n\n const material = new THREE.ShaderMaterial({\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.current = material;\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);\n scene.add(quad);\n\n let composer = null;\n if (enablePost) {\n composer = new EffectComposer(renderer);\n composerRef.current = composer;\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloom = new BloomEffect({\n intensity: 1.0,\n luminanceThreshold: bloomThreshold,\n luminanceSmoothing: bloomSmoothing\n });\n bloom.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current = bloom;\n\n const chroma = new ChromaticAberrationEffect({\n offset: new THREE.Vector2(chromaticAberration, chromaticAberration),\n radialModulation: true,\n modulationOffset: 0.0\n });\n chromaRef.current = chroma;\n\n const effectPass = new EffectPass(camera, bloom, chroma);\n effectPass.renderToScreen = true;\n composer.addPass(effectPass);\n }\n\n const onResize = () => {\n renderer.setSize(container.clientWidth, container.clientHeight);\n material.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio());\n if (composerRef.current) composerRef.current.setSize(container.clientWidth, container.clientHeight);\n };\n window.addEventListener('resize', onResize);\n\n let last = performance.now();\n const tick = () => {\n const now = performance.now();\n const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));\n last = now;\n\n lookCurrent.current.copy(\n smoothDampVec2(lookCurrent.current, lookTarget.current, lookVel.current, smoothTime, maxSpeed, dt)\n );\n\n const tiltSm = smoothDampFloat(\n tiltCurrent.current,\n tiltTarget.current,\n { v: tiltVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n tiltCurrent.current = tiltSm.value;\n tiltVel.current = tiltSm.v;\n\n const yawSm = smoothDampFloat(\n yawCurrent.current,\n yawTarget.current,\n { v: yawVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n yawCurrent.current = yawSm.value;\n yawVel.current = yawSm.v;\n\n const skew = new THREE.Vector2(lookCurrent.current.x * skewScale, -lookCurrent.current.y * yBoost * skewScale);\n material.uniforms.uSkew.value.set(skew.x, skew.y);\n material.uniforms.uTilt.value = tiltCurrent.current * tiltScale;\n material.uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent.current * yawScale, -0.6, 0.6);\n\n material.uniforms.iTime.value = now / 1000;\n renderer.clear(true, true, true);\n if (composerRef.current) {\n composerRef.current.render(dt);\n } else {\n renderer.render(scene, camera);\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n rafRef.current = requestAnimationFrame(tick);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n window.removeEventListener('resize', onResize);\n material.dispose();\n quad.geometry.dispose();\n\n if (composerRef.current) {\n composerRef.current.dispose();\n composerRef.current = null;\n }\n renderer.dispose();\n renderer.forceContextLoss();\n container.removeChild(renderer.domElement);\n };\n }, [\n sensitivity,\n lineThickness,\n linesColor,\n scanColor,\n scanOpacity,\n gridScale,\n lineStyle,\n lineJitter,\n scanDirection,\n enablePost,\n noiseIntensity,\n bloomIntensity,\n scanGlow,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n smoothTime,\n maxSpeed,\n skewScale,\n yBoost,\n tiltScale,\n yawScale\n ]);\n\n useEffect(() => {\n const m = materialRef.current;\n if (m) {\n const u = m.uniforms;\n u.uLineThickness.value = lineThickness;\n u.uLinesColor.value.copy(srgbColor(linesColor));\n u.uScanColor.value.copy(srgbColor(scanColor));\n u.uGridScale.value = gridScale;\n u.uLineStyle.value = lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0;\n u.uLineJitter.value = Math.max(0, Math.min(1, lineJitter || 0));\n u.uBloomOpacity.value = Math.max(0, bloomIntensity);\n u.uNoise.value = Math.max(0, noiseIntensity);\n u.uScanGlow.value = scanGlow;\n u.uScanOpacity.value = Math.max(0, Math.min(1, scanOpacity));\n u.uScanDirection.value = scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0;\n u.uScanSoftness.value = scanSoftness;\n u.uPhaseTaper.value = scanPhaseTaper;\n u.uScanDuration.value = Math.max(0.05, scanDuration);\n u.uScanDelay.value = Math.max(0.0, scanDelay);\n }\n if (bloomRef.current) {\n bloomRef.current.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current.luminanceMaterial.threshold = bloomThreshold;\n bloomRef.current.luminanceMaterial.smoothing = bloomSmoothing;\n }\n if (chromaRef.current) {\n chromaRef.current.offset.set(chromaticAberration, chromaticAberration);\n }\n }, [\n lineThickness,\n linesColor,\n scanColor,\n gridScale,\n lineStyle,\n lineJitter,\n bloomIntensity,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n noiseIntensity,\n scanGlow,\n scanOpacity,\n scanDirection,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay\n ]);\n\n useEffect(() => {\n if (!enableGyro) return;\n const handler = e => {\n if (uiFaceActive) return;\n const gamma = e.gamma ?? 0;\n const beta = e.beta ?? 0;\n const nx = THREE.MathUtils.clamp(gamma / 45, -1, 1);\n const ny = THREE.MathUtils.clamp(-beta / 30, -1, 1);\n lookTarget.current.set(nx, ny);\n tiltTarget.current = THREE.MathUtils.degToRad(gamma) * 0.4;\n };\n window.addEventListener('deviceorientation', handler);\n return () => {\n window.removeEventListener('deviceorientation', handler);\n };\n }, [enableGyro, uiFaceActive]);\n\n useEffect(() => {\n let canceled = false;\n const load = async () => {\n try {\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath),\n faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelsPath)\n ]);\n if (!canceled) setModelsReady(true);\n } catch {\n if (!canceled) setModelsReady(false);\n }\n };\n load();\n return () => {\n canceled = true;\n };\n }, [modelsPath]);\n\n useEffect(() => {\n let stop = false;\n let lastDetect = 0;\n const video = videoRef.current;\n\n const start = async () => {\n if (!enableWebcam || !modelsReady) return;\n if (!video) return;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } },\n audio: false\n });\n video.srcObject = stream;\n await video.play();\n } catch {\n return;\n }\n\n const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });\n\n const detect = async ts => {\n if (stop) return;\n\n if (ts - lastDetect >= 33) {\n lastDetect = ts;\n try {\n const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true);\n if (res && res.detection) {\n const det = res.detection;\n const box = det.box;\n const vw = video.videoWidth || 1;\n const vh = video.videoHeight || 1;\n\n const cx = box.x + box.width * 0.5;\n const cy = box.y + box.height * 0.5;\n const nx = (cx / vw) * 2 - 1;\n const ny = (cy / vh) * 2 - 1;\n medianPush(bufX.current, nx, 5);\n medianPush(bufY.current, ny, 5);\n const nxm = median(bufX.current);\n const nym = median(bufY.current);\n\n const look = new THREE.Vector2(Math.tanh(nxm), Math.tanh(nym));\n\n const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh));\n const depthScale = 1 + depthResponse * (faceSize - 0.25);\n lookTarget.current.copy(look.multiplyScalar(depthScale));\n\n const leftEye = res.landmarks.getLeftEye();\n const rightEye = res.landmarks.getRightEye();\n const lc = centroid(leftEye);\n const rc = centroid(rightEye);\n const tilt = Math.atan2(rc.y - lc.y, rc.x - lc.x);\n medianPush(bufT.current, tilt, 5);\n tiltTarget.current = median(bufT.current);\n\n const nose = res.landmarks.getNose();\n const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)];\n const jaw = res.landmarks.getJawOutline();\n const leftCheek = jaw[3] || jaw[2];\n const rightCheek = jaw[13] || jaw[14];\n const dL = dist2(tip, leftCheek);\n const dR = dist2(tip, rightCheek);\n const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6;\n let yawSignal = THREE.MathUtils.clamp((dR - dL) / (eyeDist * 1.6), -1, 1);\n yawSignal = Math.tanh(yawSignal);\n medianPush(bufYaw.current, yawSignal, 5);\n yawTarget.current = median(bufYaw.current);\n\n setUiFaceActive(true);\n } else {\n setUiFaceActive(false);\n }\n } catch {\n setUiFaceActive(false);\n }\n }\n\n if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {\n video.requestVideoFrameCallback(() => detect(performance.now()));\n } else {\n requestAnimationFrame(detect);\n }\n };\n\n requestAnimationFrame(detect);\n };\n\n start();\n\n return () => {\n stop = true;\n if (video) {\n const stream = video.srcObject;\n if (stream) stream.getTracks().forEach(t => t.stop());\n video.pause();\n video.srcObject = null;\n }\n };\n }, [enableWebcam, modelsReady, depthResponse]);\n\n return (\n \n {showPreview && (\n
\n
\n
\n {enableWebcam\n ? modelsReady\n ? uiFaceActive\n ? 'Face: tracking'\n : 'Face: searching'\n : 'Loading models'\n : 'Webcam disabled'}\n
\n
\n )}\n
\n );\n};\n\nfunction srgbColor(hex) {\n const c = new THREE.Color(hex);\n return c.convertSRGBToLinear();\n}\n\nfunction smoothDampVec2(current, target, currentVelocity, smoothTime, maxSpeed, deltaTime) {\n const out = current.clone();\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current.clone().sub(target);\n const originalTo = target.clone();\n\n const maxChange = maxSpeed * smoothTime;\n if (change.length() > maxChange) change.setLength(maxChange);\n\n target = current.clone().sub(change);\n const temp = currentVelocity.clone().addScaledVector(change, omega).multiplyScalar(deltaTime);\n currentVelocity.sub(temp.clone().multiplyScalar(omega));\n currentVelocity.multiplyScalar(exp);\n\n out.copy(target.clone().add(change.add(temp).multiplyScalar(exp)));\n\n const origMinusCurrent = originalTo.clone().sub(current);\n const outMinusOrig = out.clone().sub(originalTo);\n if (origMinusCurrent.dot(outMinusOrig) > 0) {\n out.copy(originalTo);\n currentVelocity.set(0, 0);\n }\n return out;\n}\n\nfunction smoothDampFloat(current, target, velRef, smoothTime, maxSpeed, deltaTime) {\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current - target;\n const originalTo = target;\n\n const maxChange = maxSpeed * smoothTime;\n change = Math.sign(change) * Math.min(Math.abs(change), maxChange);\n\n target = current - change;\n const temp = (velRef.v + omega * change) * deltaTime;\n velRef.v = (velRef.v - omega * temp) * exp;\n\n let out = target + (change + temp) * exp;\n\n const origMinusCurrent = originalTo - current;\n const outMinusOrig = out - originalTo;\n if (origMinusCurrent * outMinusOrig > 0) {\n out = originalTo;\n velRef.v = 0;\n }\n return { value: out, v: velRef.v };\n}\n\nfunction medianPush(buf, v, maxLen) {\n buf.push(v);\n if (buf.length > maxLen) buf.shift();\n}\n\nfunction median(buf) {\n if (buf.length === 0) return 0;\n const a = [...buf].sort((x, y) => x - y);\n const mid = Math.floor(a.length / 2);\n return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) * 0.5;\n}\n\nfunction centroid(points) {\n let x = 0,\n y = 0;\n const n = points.length || 1;\n for (const p of points) {\n x += p.x;\n y += p.y;\n }\n return { x: x / n, y: y / n };\n}\n\nfunction dist2(a, b) {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n"
}
],
"registryDependencies": [],
"dependencies": [
+ "face-api.js@^0.22.2",
"postprocessing@^6.36.0",
- "three@^0.167.1",
- "face-api.js@^0.22.2"
+ "three@^0.167.1"
]
}
\ No newline at end of file
diff --git a/public/r/GridScan-JS-TW.json b/public/r/GridScan-JS-TW.json
index 780735b8..0ec42e21 100644
--- a/public/r/GridScan-JS-TW.json
+++ b/public/r/GridScan-JS-TW.json
@@ -8,13 +8,13 @@
{
"type": "registry:component",
"path": "GridScan/GridScan.jsx",
- "content": "import { useEffect, useRef, useState } from 'react';\nimport { EffectComposer, RenderPass, EffectPass, BloomEffect, ChromaticAberrationEffect } from 'postprocessing';\nimport * as THREE from 'three';\nimport * as faceapi from 'face-api.js';\n\nconst vert = `\nvarying vec2 vUv;\nvoid main(){\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`;\n\nconst frag = `\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec2 uSkew;\nuniform float uTilt;\nuniform float uYaw;\nuniform float uLineThickness;\nuniform vec3 uLinesColor;\nuniform vec3 uScanColor;\nuniform float uGridScale;\nuniform float uLineStyle;\nuniform float uLineJitter;\nuniform float uScanOpacity;\nuniform float uScanDirection;\nuniform float uNoise;\nuniform float uBloomOpacity;\nuniform float uScanGlow;\nuniform float uScanSoftness;\nuniform float uPhaseTaper;\nuniform float uScanDuration;\nuniform float uScanDelay;\nvarying vec2 vUv;\n\nuniform float uScanStarts[8];\nuniform float uScanCount;\n\nconst int MAX_SCANS = 8;\n\nfloat smoother01(float a, float b, float x){\n float t = clamp((x - a) / max(1e-5, (b - a)), 0.0, 1.0);\n return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord)\n{\n vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n\n vec3 ro = vec3(0.0);\n vec3 rd = normalize(vec3(p, 2.0));\n\n float cR = cos(uTilt), sR = sin(uTilt);\n rd.xy = mat2(cR, -sR, sR, cR) * rd.xy;\n\n float cY = cos(uYaw), sY = sin(uYaw);\n rd.xz = mat2(cY, -sY, sY, cY) * rd.xz;\n\n vec2 skew = clamp(uSkew, vec2(-0.7), vec2(0.7));\n rd.xy += skew * rd.z;\n\n vec3 color = vec3(0.0);\n float minT = 1e20;\n float gridScale = max(1e-5, uGridScale);\n float fadeStrength = 2.0;\n vec2 gridUV = vec2(0.0);\n\n float hitIsY = 1.0;\n for (int i = 0; i < 4; i++)\n {\n float isY = float(i < 2);\n float pos = mix(-0.2, 0.2, float(i)) * isY + mix(-0.5, 0.5, float(i - 2)) * (1.0 - isY);\n float num = pos - (isY * ro.y + (1.0 - isY) * ro.x);\n float den = isY * rd.y + (1.0 - isY) * rd.x;\n float t = num / den;\n vec3 h = ro + rd * t;\n\n float depthBoost = smoothstep(0.0, 3.0, h.z);\n h.xy += skew * 0.15 * depthBoost;\n\n bool use = t > 0.0 && t < minT;\n gridUV = use ? mix(h.zy, h.xz, isY) / gridScale : gridUV;\n minT = use ? t : minT;\n hitIsY = use ? isY : hitIsY;\n }\n\n vec3 hit = ro + rd * minT;\n float dist = length(hit - ro);\n\n float jitterAmt = clamp(uLineJitter, 0.0, 1.0);\n if (jitterAmt > 0.0) {\n vec2 j = vec2(\n sin(gridUV.y * 2.7 + iTime * 1.8),\n cos(gridUV.x * 2.3 - iTime * 1.6)\n ) * (0.15 * jitterAmt);\n gridUV += j;\n }\n float fx = fract(gridUV.x);\n float fy = fract(gridUV.y);\n float ax = min(fx, 1.0 - fx);\n float ay = min(fy, 1.0 - fy);\n float wx = fwidth(gridUV.x);\n float wy = fwidth(gridUV.y);\n float halfPx = max(0.0, uLineThickness) * 0.5;\n\n float tx = halfPx * wx;\n float ty = halfPx * wy;\n\n float aax = wx;\n float aay = wy;\n\n float lineX = 1.0 - smoothstep(tx, tx + aax, ax);\n float lineY = 1.0 - smoothstep(ty, ty + aay, ay);\n if (uLineStyle > 0.5) {\n float dashRepeat = 4.0;\n float dashDuty = 0.5;\n float vy = fract(gridUV.y * dashRepeat);\n float vx = fract(gridUV.x * dashRepeat);\n float dashMaskY = step(vy, dashDuty);\n float dashMaskX = step(vx, dashDuty);\n if (uLineStyle < 1.5) {\n lineX *= dashMaskY;\n lineY *= dashMaskX;\n } else {\n float dotRepeat = 6.0;\n float dotWidth = 0.18;\n float cy = abs(fract(gridUV.y * dotRepeat) - 0.5);\n float cx = abs(fract(gridUV.x * dotRepeat) - 0.5);\n float dotMaskY = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.y * dotRepeat), cy);\n float dotMaskX = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.x * dotRepeat), cx);\n lineX *= dotMaskY;\n lineY *= dotMaskX;\n }\n }\n float primaryMask = max(lineX, lineY);\n\n vec2 gridUV2 = (hitIsY > 0.5 ? hit.xz : hit.zy) / gridScale;\n if (jitterAmt > 0.0) {\n vec2 j2 = vec2(\n cos(gridUV2.y * 2.1 - iTime * 1.4),\n sin(gridUV2.x * 2.5 + iTime * 1.7)\n ) * (0.15 * jitterAmt);\n gridUV2 += j2;\n }\n float fx2 = fract(gridUV2.x);\n float fy2 = fract(gridUV2.y);\n float ax2 = min(fx2, 1.0 - fx2);\n float ay2 = min(fy2, 1.0 - fy2);\n float wx2 = fwidth(gridUV2.x);\n float wy2 = fwidth(gridUV2.y);\n float tx2 = halfPx * wx2;\n float ty2 = halfPx * wy2;\n float aax2 = wx2;\n float aay2 = wy2;\n float lineX2 = 1.0 - smoothstep(tx2, tx2 + aax2, ax2);\n float lineY2 = 1.0 - smoothstep(ty2, ty2 + aay2, ay2);\n if (uLineStyle > 0.5) {\n float dashRepeat2 = 4.0;\n float dashDuty2 = 0.5;\n float vy2m = fract(gridUV2.y * dashRepeat2);\n float vx2m = fract(gridUV2.x * dashRepeat2);\n float dashMaskY2 = step(vy2m, dashDuty2);\n float dashMaskX2 = step(vx2m, dashDuty2);\n if (uLineStyle < 1.5) {\n lineX2 *= dashMaskY2;\n lineY2 *= dashMaskX2;\n } else {\n float dotRepeat2 = 6.0;\n float dotWidth2 = 0.18;\n float cy2 = abs(fract(gridUV2.y * dotRepeat2) - 0.5);\n float cx2 = abs(fract(gridUV2.x * dotRepeat2) - 0.5);\n float dotMaskY2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.y * dotRepeat2), cy2);\n float dotMaskX2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.x * dotRepeat2), cx2);\n lineX2 *= dotMaskY2;\n lineY2 *= dotMaskX2;\n }\n }\n float altMask = max(lineX2, lineY2);\n\n float edgeDistX = min(abs(hit.x - (-0.5)), abs(hit.x - 0.5));\n float edgeDistY = min(abs(hit.y - (-0.2)), abs(hit.y - 0.2));\n float edgeDist = mix(edgeDistY, edgeDistX, hitIsY);\n float edgeGate = 1.0 - smoothstep(gridScale * 0.5, gridScale * 2.0, edgeDist);\n altMask *= edgeGate;\n\n float lineMask = max(primaryMask, altMask);\n\n float fade = exp(-dist * fadeStrength);\n\n float dur = max(0.05, uScanDuration);\n float del = max(0.0, uScanDelay);\n float scanZMax = 2.0;\n float widthScale = max(0.1, uScanGlow);\n float sigma = max(0.001, 0.18 * widthScale * uScanSoftness);\n float sigmaA = sigma * 2.0;\n\n float combinedPulse = 0.0;\n float combinedAura = 0.0;\n\n float cycle = dur + del;\n float tCycle = mod(iTime, cycle);\n float scanPhase = clamp((tCycle - del) / dur, 0.0, 1.0);\n float phase = scanPhase;\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phase = 1.0 - phase;\n } else if (uScanDirection > 1.5) {\n float t2 = mod(max(0.0, iTime - del), 2.0 * dur);\n phase = (t2 < dur) ? (t2 / dur) : (1.0 - (t2 - dur) / dur);\n }\n float scanZ = phase * scanZMax;\n float dz = abs(hit.z - scanZ);\n float lineBand = exp(-0.5 * (dz * dz) / (sigma * sigma));\n float taper = clamp(uPhaseTaper, 0.0, 0.49);\n float headW = taper;\n float tailW = taper;\n float headFade = smoother01(0.0, headW, phase);\n float tailFade = 1.0 - smoother01(1.0 - tailW, 1.0, phase);\n float phaseWindow = headFade * tailFade;\n float pulseBase = lineBand * phaseWindow;\n combinedPulse += pulseBase * clamp(uScanOpacity, 0.0, 1.0);\n float auraBand = exp(-0.5 * (dz * dz) / (sigmaA * sigmaA));\n combinedAura += (auraBand * 0.25) * phaseWindow * clamp(uScanOpacity, 0.0, 1.0);\n\n for (int i = 0; i < MAX_SCANS; i++) {\n if (float(i) >= uScanCount) break;\n float tActiveI = iTime - uScanStarts[i];\n float phaseI = clamp(tActiveI / dur, 0.0, 1.0);\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phaseI = 1.0 - phaseI;\n } else if (uScanDirection > 1.5) {\n phaseI = (phaseI < 0.5) ? (phaseI * 2.0) : (1.0 - (phaseI - 0.5) * 2.0);\n }\n float scanZI = phaseI * scanZMax;\n float dzI = abs(hit.z - scanZI);\n float lineBandI = exp(-0.5 * (dzI * dzI) / (sigma * sigma));\n float headFadeI = smoother01(0.0, headW, phaseI);\n float tailFadeI = 1.0 - smoother01(1.0 - tailW, 1.0, phaseI);\n float phaseWindowI = headFadeI * tailFadeI;\n combinedPulse += lineBandI * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n float auraBandI = exp(-0.5 * (dzI * dzI) / (sigmaA * sigmaA));\n combinedAura += (auraBandI * 0.25) * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n }\n\n float lineVis = lineMask;\n vec3 gridCol = uLinesColor * lineVis * fade;\n vec3 scanCol = uScanColor * combinedPulse;\n vec3 scanAura = uScanColor * combinedAura;\n\n color = gridCol + scanCol + scanAura;\n\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(iTime * 123.4), vec2(12.9898,78.233))) * 43758.5453123);\n color += (n - 0.5) * uNoise;\n color = clamp(color, 0.0, 1.0);\n float alpha = clamp(max(lineVis, combinedPulse), 0.0, 1.0);\n float gx = 1.0 - smoothstep(tx * 2.0, tx * 2.0 + aax * 2.0, ax);\n float gy = 1.0 - smoothstep(ty * 2.0, ty * 2.0 + aay * 2.0, ay);\n float halo = max(gx, gy) * fade;\n alpha = max(alpha, halo * clamp(uBloomOpacity, 0.0, 1.0));\n fragColor = vec4(color, alpha);\n}\n\nvoid main(){\n vec4 c;\n mainImage(c, vUv * iResolution.xy);\n gl_FragColor = c;\n}\n`;\n\nexport const GridScan = ({\n enableWebcam = false,\n showPreview = false,\n modelsPath = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights',\n sensitivity = 0.55,\n lineThickness = 1,\n linesColor = '#392e4e',\n scanColor = '#FF9FFC',\n scanOpacity = 0.4,\n gridScale = 0.1,\n lineStyle = 'solid',\n lineJitter = 0.1,\n scanDirection = 'pingpong',\n enablePost = true,\n bloomIntensity = 0,\n bloomThreshold = 0,\n bloomSmoothing = 0,\n chromaticAberration = 0.002,\n noiseIntensity = 0.01,\n scanGlow = 0.5,\n scanSoftness = 2,\n scanPhaseTaper = 0.9,\n scanDuration = 2.0,\n scanDelay = 2.0,\n enableGyro = false,\n scanOnClick = false,\n snapBackDelay = 250,\n className,\n style\n}) => {\n const containerRef = useRef(null);\n const videoRef = useRef(null);\n\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const composerRef = useRef(null);\n const bloomRef = useRef(null);\n const chromaRef = useRef(null);\n const rafRef = useRef(null);\n\n const [modelsReady, setModelsReady] = useState(false);\n const [uiFaceActive, setUiFaceActive] = useState(false);\n\n const lookTarget = useRef(new THREE.Vector2(0, 0));\n const tiltTarget = useRef(0);\n const yawTarget = useRef(0);\n\n const lookCurrent = useRef(new THREE.Vector2(0, 0));\n const lookVel = useRef(new THREE.Vector2(0, 0));\n const tiltCurrent = useRef(0);\n const tiltVel = useRef(0);\n const yawCurrent = useRef(0);\n const yawVel = useRef(0);\n\n const MAX_SCANS = 8;\n const scanStartsRef = useRef([]);\n\n const pushScan = t => {\n const arr = scanStartsRef.current.slice();\n if (arr.length >= MAX_SCANS) arr.shift();\n arr.push(t);\n scanStartsRef.current = arr;\n if (materialRef.current) {\n const u = materialRef.current.uniforms;\n const buf = new Array(MAX_SCANS).fill(0);\n for (let i = 0; i < arr.length && i < MAX_SCANS; i++) buf[i] = arr[i];\n u.uScanStarts.value = buf;\n u.uScanCount.value = arr.length;\n }\n };\n\n const bufX = useRef([]);\n const bufY = useRef([]);\n const bufT = useRef([]);\n const bufYaw = useRef([]);\n\n const s = THREE.MathUtils.clamp(sensitivity, 0, 1);\n const skewScale = THREE.MathUtils.lerp(0.06, 0.2, s);\n const tiltScale = THREE.MathUtils.lerp(0.12, 0.3, s);\n const yawScale = THREE.MathUtils.lerp(0.1, 0.28, s);\n const depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s);\n const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s);\n const maxSpeed = Infinity;\n\n const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n let leaveTimer = null;\n const onMove = e => {\n if (uiFaceActive) return;\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n const rect = el.getBoundingClientRect();\n const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n lookTarget.current.set(nx, ny);\n };\n const onClick = async () => {\n const nowSec = performance.now() / 1000;\n if (scanOnClick) pushScan(nowSec);\n if (\n enableGyro &&\n typeof window !== 'undefined' &&\n window.DeviceOrientationEvent &&\n DeviceOrientationEvent.requestPermission\n ) {\n try {\n await DeviceOrientationEvent.requestPermission();\n } catch {\n // noop\n }\n }\n };\n const onEnter = () => {\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n };\n const onLeave = () => {\n if (uiFaceActive) return;\n if (leaveTimer) clearTimeout(leaveTimer);\n leaveTimer = window.setTimeout(\n () => {\n lookTarget.current.set(0, 0);\n tiltTarget.current = 0;\n yawTarget.current = 0;\n },\n Math.max(0, snapBackDelay || 0)\n );\n };\n el.addEventListener('mousemove', onMove);\n el.addEventListener('mouseenter', onEnter);\n if (scanOnClick) el.addEventListener('click', onClick);\n el.addEventListener('mouseleave', onLeave);\n return () => {\n el.removeEventListener('mousemove', onMove);\n el.removeEventListener('mouseenter', onEnter);\n el.removeEventListener('mouseleave', onLeave);\n if (scanOnClick) el.removeEventListener('click', onClick);\n if (leaveTimer) clearTimeout(leaveTimer);\n };\n }, [uiFaceActive, snapBackDelay, scanOnClick, enableGyro]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n rendererRef.current = renderer;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setSize(container.clientWidth, container.clientHeight);\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.toneMapping = THREE.NoToneMapping;\n renderer.autoClear = false;\n renderer.setClearColor(0x000000, 0);\n container.appendChild(renderer.domElement);\n\n const uniforms = {\n iResolution: {\n value: new THREE.Vector3(container.clientWidth, container.clientHeight, renderer.getPixelRatio())\n },\n iTime: { value: 0 },\n uSkew: { value: new THREE.Vector2(0, 0) },\n uTilt: { value: 0 },\n uYaw: { value: 0 },\n uLineThickness: { value: lineThickness },\n uLinesColor: { value: srgbColor(linesColor) },\n uScanColor: { value: srgbColor(scanColor) },\n uGridScale: { value: gridScale },\n uLineStyle: { value: lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0 },\n uLineJitter: { value: Math.max(0, Math.min(1, lineJitter || 0)) },\n uScanOpacity: { value: scanOpacity },\n uNoise: { value: noiseIntensity },\n uBloomOpacity: { value: bloomIntensity },\n uScanGlow: { value: scanGlow },\n uScanSoftness: { value: scanSoftness },\n uPhaseTaper: { value: scanPhaseTaper },\n uScanDuration: { value: scanDuration },\n uScanDelay: { value: scanDelay },\n uScanDirection: { value: scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0 },\n uScanStarts: { value: new Array(MAX_SCANS).fill(0) },\n uScanCount: { value: 0 }\n };\n\n const material = new THREE.ShaderMaterial({\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.current = material;\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);\n scene.add(quad);\n\n let composer = null;\n if (enablePost) {\n composer = new EffectComposer(renderer);\n composerRef.current = composer;\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloom = new BloomEffect({\n intensity: 1.0,\n luminanceThreshold: bloomThreshold,\n luminanceSmoothing: bloomSmoothing\n });\n bloom.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current = bloom;\n\n const chroma = new ChromaticAberrationEffect({\n offset: new THREE.Vector2(chromaticAberration, chromaticAberration),\n radialModulation: true,\n modulationOffset: 0.0\n });\n chromaRef.current = chroma;\n\n const effectPass = new EffectPass(camera, bloom, chroma);\n effectPass.renderToScreen = true;\n composer.addPass(effectPass);\n }\n\n const onResize = () => {\n renderer.setSize(container.clientWidth, container.clientHeight);\n material.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio());\n if (composerRef.current) composerRef.current.setSize(container.clientWidth, container.clientHeight);\n };\n window.addEventListener('resize', onResize);\n\n let last = performance.now();\n const tick = () => {\n const now = performance.now();\n const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));\n last = now;\n\n lookCurrent.current.copy(\n smoothDampVec2(lookCurrent.current, lookTarget.current, lookVel.current, smoothTime, maxSpeed, dt)\n );\n\n const tiltSm = smoothDampFloat(\n tiltCurrent.current,\n tiltTarget.current,\n { v: tiltVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n tiltCurrent.current = tiltSm.value;\n tiltVel.current = tiltSm.v;\n\n const yawSm = smoothDampFloat(\n yawCurrent.current,\n yawTarget.current,\n { v: yawVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n yawCurrent.current = yawSm.value;\n yawVel.current = yawSm.v;\n\n const skew = new THREE.Vector2(lookCurrent.current.x * skewScale, -lookCurrent.current.y * yBoost * skewScale);\n material.uniforms.uSkew.value.set(skew.x, skew.y);\n material.uniforms.uTilt.value = tiltCurrent.current * tiltScale;\n material.uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent.current * yawScale, -0.6, 0.6);\n\n material.uniforms.iTime.value = now / 1000;\n renderer.clear(true, true, true);\n if (composerRef.current) {\n composerRef.current.render(dt);\n } else {\n renderer.render(scene, camera);\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n rafRef.current = requestAnimationFrame(tick);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n window.removeEventListener('resize', onResize);\n material.dispose();\n quad.geometry.dispose();\n\n if (composerRef.current) {\n composerRef.current.dispose();\n composerRef.current = null;\n }\n renderer.dispose();\n container.removeChild(renderer.domElement);\n };\n }, [\n sensitivity,\n lineThickness,\n linesColor,\n scanColor,\n scanOpacity,\n gridScale,\n lineStyle,\n lineJitter,\n scanDirection,\n enablePost,\n noiseIntensity,\n bloomIntensity,\n scanGlow,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n smoothTime,\n maxSpeed,\n skewScale,\n yBoost,\n tiltScale,\n yawScale\n ]);\n\n useEffect(() => {\n const m = materialRef.current;\n if (m) {\n const u = m.uniforms;\n u.uLineThickness.value = lineThickness;\n u.uLinesColor.value.copy(srgbColor(linesColor));\n u.uScanColor.value.copy(srgbColor(scanColor));\n u.uGridScale.value = gridScale;\n u.uLineStyle.value = lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0;\n u.uLineJitter.value = Math.max(0, Math.min(1, lineJitter || 0));\n u.uBloomOpacity.value = Math.max(0, bloomIntensity);\n u.uNoise.value = Math.max(0, noiseIntensity);\n u.uScanGlow.value = scanGlow;\n u.uScanOpacity.value = Math.max(0, Math.min(1, scanOpacity));\n u.uScanDirection.value = scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0;\n u.uScanSoftness.value = scanSoftness;\n u.uPhaseTaper.value = scanPhaseTaper;\n u.uScanDuration.value = Math.max(0.05, scanDuration);\n u.uScanDelay.value = Math.max(0.0, scanDelay);\n }\n if (bloomRef.current) {\n bloomRef.current.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current.luminanceMaterial.threshold = bloomThreshold;\n bloomRef.current.luminanceMaterial.smoothing = bloomSmoothing;\n }\n if (chromaRef.current) {\n chromaRef.current.offset.set(chromaticAberration, chromaticAberration);\n }\n }, [\n lineThickness,\n linesColor,\n scanColor,\n gridScale,\n lineStyle,\n lineJitter,\n bloomIntensity,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n noiseIntensity,\n scanGlow,\n scanOpacity,\n scanDirection,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay\n ]);\n\n useEffect(() => {\n if (!enableGyro) return;\n const handler = e => {\n if (uiFaceActive) return;\n const gamma = e.gamma ?? 0;\n const beta = e.beta ?? 0;\n const nx = THREE.MathUtils.clamp(gamma / 45, -1, 1);\n const ny = THREE.MathUtils.clamp(-beta / 30, -1, 1);\n lookTarget.current.set(nx, ny);\n tiltTarget.current = THREE.MathUtils.degToRad(gamma) * 0.4;\n };\n window.addEventListener('deviceorientation', handler);\n return () => {\n window.removeEventListener('deviceorientation', handler);\n };\n }, [enableGyro, uiFaceActive]);\n\n useEffect(() => {\n let canceled = false;\n const load = async () => {\n try {\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath),\n faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelsPath)\n ]);\n if (!canceled) setModelsReady(true);\n } catch {\n if (!canceled) setModelsReady(false);\n }\n };\n load();\n return () => {\n canceled = true;\n };\n }, [modelsPath]);\n\n useEffect(() => {\n let stop = false;\n let lastDetect = 0;\n const video = videoRef.current;\n\n const start = async () => {\n if (!enableWebcam || !modelsReady) return;\n if (!video) return;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } },\n audio: false\n });\n video.srcObject = stream;\n await video.play();\n } catch {\n return;\n }\n\n const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });\n\n const detect = async ts => {\n if (stop) return;\n\n if (ts - lastDetect >= 33) {\n lastDetect = ts;\n try {\n const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true);\n if (res && res.detection) {\n const det = res.detection;\n const box = det.box;\n const vw = video.videoWidth || 1;\n const vh = video.videoHeight || 1;\n\n const cx = box.x + box.width * 0.5;\n const cy = box.y + box.height * 0.5;\n const nx = (cx / vw) * 2 - 1;\n const ny = (cy / vh) * 2 - 1;\n medianPush(bufX.current, nx, 5);\n medianPush(bufY.current, ny, 5);\n const nxm = median(bufX.current);\n const nym = median(bufY.current);\n\n const look = new THREE.Vector2(Math.tanh(nxm), Math.tanh(nym));\n\n const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh));\n const depthScale = 1 + depthResponse * (faceSize - 0.25);\n lookTarget.current.copy(look.multiplyScalar(depthScale));\n\n const leftEye = res.landmarks.getLeftEye();\n const rightEye = res.landmarks.getRightEye();\n const lc = centroid(leftEye);\n const rc = centroid(rightEye);\n const tilt = Math.atan2(rc.y - lc.y, rc.x - lc.x);\n medianPush(bufT.current, tilt, 5);\n tiltTarget.current = median(bufT.current);\n\n const nose = res.landmarks.getNose();\n const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)];\n const jaw = res.landmarks.getJawOutline();\n const leftCheek = jaw[3] || jaw[2];\n const rightCheek = jaw[13] || jaw[14];\n const dL = dist2(tip, leftCheek);\n const dR = dist2(tip, rightCheek);\n const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6;\n let yawSignal = THREE.MathUtils.clamp((dR - dL) / (eyeDist * 1.6), -1, 1);\n yawSignal = Math.tanh(yawSignal);\n medianPush(bufYaw.current, yawSignal, 5);\n yawTarget.current = median(bufYaw.current);\n\n setUiFaceActive(true);\n } else {\n setUiFaceActive(false);\n }\n } catch {\n setUiFaceActive(false);\n }\n }\n\n if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {\n video.requestVideoFrameCallback(() => detect(performance.now()));\n } else {\n requestAnimationFrame(detect);\n }\n };\n\n requestAnimationFrame(detect);\n };\n\n start();\n\n return () => {\n stop = true;\n if (video) {\n const stream = video.srcObject;\n if (stream) stream.getTracks().forEach(t => t.stop());\n video.pause();\n video.srcObject = null;\n }\n };\n }, [enableWebcam, modelsReady, depthResponse]);\n\n return (\n \n {showPreview && (\n
\n
\n
\n {enableWebcam\n ? modelsReady\n ? uiFaceActive\n ? 'Face: tracking'\n : 'Face: searching'\n : 'Loading models'\n : 'Webcam disabled'}\n
\n
\n )}\n
\n );\n};\n\nfunction srgbColor(hex) {\n const c = new THREE.Color(hex);\n return c.convertSRGBToLinear();\n}\n\nfunction smoothDampVec2(current, target, currentVelocity, smoothTime, maxSpeed, deltaTime) {\n const out = current.clone();\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current.clone().sub(target);\n const originalTo = target.clone();\n\n const maxChange = maxSpeed * smoothTime;\n if (change.length() > maxChange) change.setLength(maxChange);\n\n target = current.clone().sub(change);\n const temp = currentVelocity.clone().addScaledVector(change, omega).multiplyScalar(deltaTime);\n currentVelocity.sub(temp.clone().multiplyScalar(omega));\n currentVelocity.multiplyScalar(exp);\n\n out.copy(target.clone().add(change.add(temp).multiplyScalar(exp)));\n\n const origMinusCurrent = originalTo.clone().sub(current);\n const outMinusOrig = out.clone().sub(originalTo);\n if (origMinusCurrent.dot(outMinusOrig) > 0) {\n out.copy(originalTo);\n currentVelocity.set(0, 0);\n }\n return out;\n}\n\nfunction smoothDampFloat(current, target, velRef, smoothTime, maxSpeed, deltaTime) {\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current - target;\n const originalTo = target;\n\n const maxChange = maxSpeed * smoothTime;\n change = Math.sign(change) * Math.min(Math.abs(change), maxChange);\n\n target = current - change;\n const temp = (velRef.v + omega * change) * deltaTime;\n velRef.v = (velRef.v - omega * temp) * exp;\n\n let out = target + (change + temp) * exp;\n\n const origMinusCurrent = originalTo - current;\n const outMinusOrig = out - originalTo;\n if (origMinusCurrent * outMinusOrig > 0) {\n out = originalTo;\n velRef.v = 0;\n }\n return { value: out, v: velRef.v };\n}\n\nfunction medianPush(buf, v, maxLen) {\n buf.push(v);\n if (buf.length > maxLen) buf.shift();\n}\n\nfunction median(buf) {\n if (buf.length === 0) return 0;\n const a = [...buf].sort((x, y) => x - y);\n const mid = Math.floor(a.length / 2);\n return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) * 0.5;\n}\n\nfunction centroid(points) {\n let x = 0,\n y = 0;\n const n = points.length || 1;\n for (const p of points) {\n x += p.x;\n y += p.y;\n }\n return { x: x / n, y: y / n };\n}\n\nfunction dist2(a, b) {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n"
+ "content": "import * as faceapi from 'face-api.js';\nimport { BloomEffect, ChromaticAberrationEffect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';\nimport { useEffect, useRef, useState } from 'react';\nimport * as THREE from 'three';\n\nconst vert = `\nvarying vec2 vUv;\nvoid main(){\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`;\n\nconst frag = `\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec2 uSkew;\nuniform float uTilt;\nuniform float uYaw;\nuniform float uLineThickness;\nuniform vec3 uLinesColor;\nuniform vec3 uScanColor;\nuniform float uGridScale;\nuniform float uLineStyle;\nuniform float uLineJitter;\nuniform float uScanOpacity;\nuniform float uScanDirection;\nuniform float uNoise;\nuniform float uBloomOpacity;\nuniform float uScanGlow;\nuniform float uScanSoftness;\nuniform float uPhaseTaper;\nuniform float uScanDuration;\nuniform float uScanDelay;\nvarying vec2 vUv;\n\nuniform float uScanStarts[8];\nuniform float uScanCount;\n\nconst int MAX_SCANS = 8;\n\nfloat smoother01(float a, float b, float x){\n float t = clamp((x - a) / max(1e-5, (b - a)), 0.0, 1.0);\n return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord)\n{\n vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n\n vec3 ro = vec3(0.0);\n vec3 rd = normalize(vec3(p, 2.0));\n\n float cR = cos(uTilt), sR = sin(uTilt);\n rd.xy = mat2(cR, -sR, sR, cR) * rd.xy;\n\n float cY = cos(uYaw), sY = sin(uYaw);\n rd.xz = mat2(cY, -sY, sY, cY) * rd.xz;\n\n vec2 skew = clamp(uSkew, vec2(-0.7), vec2(0.7));\n rd.xy += skew * rd.z;\n\n vec3 color = vec3(0.0);\n float minT = 1e20;\n float gridScale = max(1e-5, uGridScale);\n float fadeStrength = 2.0;\n vec2 gridUV = vec2(0.0);\n\n float hitIsY = 1.0;\n for (int i = 0; i < 4; i++)\n {\n float isY = float(i < 2);\n float pos = mix(-0.2, 0.2, float(i)) * isY + mix(-0.5, 0.5, float(i - 2)) * (1.0 - isY);\n float num = pos - (isY * ro.y + (1.0 - isY) * ro.x);\n float den = isY * rd.y + (1.0 - isY) * rd.x;\n float t = num / den;\n vec3 h = ro + rd * t;\n\n float depthBoost = smoothstep(0.0, 3.0, h.z);\n h.xy += skew * 0.15 * depthBoost;\n\n bool use = t > 0.0 && t < minT;\n gridUV = use ? mix(h.zy, h.xz, isY) / gridScale : gridUV;\n minT = use ? t : minT;\n hitIsY = use ? isY : hitIsY;\n }\n\n vec3 hit = ro + rd * minT;\n float dist = length(hit - ro);\n\n float jitterAmt = clamp(uLineJitter, 0.0, 1.0);\n if (jitterAmt > 0.0) {\n vec2 j = vec2(\n sin(gridUV.y * 2.7 + iTime * 1.8),\n cos(gridUV.x * 2.3 - iTime * 1.6)\n ) * (0.15 * jitterAmt);\n gridUV += j;\n }\n float fx = fract(gridUV.x);\n float fy = fract(gridUV.y);\n float ax = min(fx, 1.0 - fx);\n float ay = min(fy, 1.0 - fy);\n float wx = fwidth(gridUV.x);\n float wy = fwidth(gridUV.y);\n float halfPx = max(0.0, uLineThickness) * 0.5;\n\n float tx = halfPx * wx;\n float ty = halfPx * wy;\n\n float aax = wx;\n float aay = wy;\n\n float lineX = 1.0 - smoothstep(tx, tx + aax, ax);\n float lineY = 1.0 - smoothstep(ty, ty + aay, ay);\n if (uLineStyle > 0.5) {\n float dashRepeat = 4.0;\n float dashDuty = 0.5;\n float vy = fract(gridUV.y * dashRepeat);\n float vx = fract(gridUV.x * dashRepeat);\n float dashMaskY = step(vy, dashDuty);\n float dashMaskX = step(vx, dashDuty);\n if (uLineStyle < 1.5) {\n lineX *= dashMaskY;\n lineY *= dashMaskX;\n } else {\n float dotRepeat = 6.0;\n float dotWidth = 0.18;\n float cy = abs(fract(gridUV.y * dotRepeat) - 0.5);\n float cx = abs(fract(gridUV.x * dotRepeat) - 0.5);\n float dotMaskY = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.y * dotRepeat), cy);\n float dotMaskX = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.x * dotRepeat), cx);\n lineX *= dotMaskY;\n lineY *= dotMaskX;\n }\n }\n float primaryMask = max(lineX, lineY);\n\n vec2 gridUV2 = (hitIsY > 0.5 ? hit.xz : hit.zy) / gridScale;\n if (jitterAmt > 0.0) {\n vec2 j2 = vec2(\n cos(gridUV2.y * 2.1 - iTime * 1.4),\n sin(gridUV2.x * 2.5 + iTime * 1.7)\n ) * (0.15 * jitterAmt);\n gridUV2 += j2;\n }\n float fx2 = fract(gridUV2.x);\n float fy2 = fract(gridUV2.y);\n float ax2 = min(fx2, 1.0 - fx2);\n float ay2 = min(fy2, 1.0 - fy2);\n float wx2 = fwidth(gridUV2.x);\n float wy2 = fwidth(gridUV2.y);\n float tx2 = halfPx * wx2;\n float ty2 = halfPx * wy2;\n float aax2 = wx2;\n float aay2 = wy2;\n float lineX2 = 1.0 - smoothstep(tx2, tx2 + aax2, ax2);\n float lineY2 = 1.0 - smoothstep(ty2, ty2 + aay2, ay2);\n if (uLineStyle > 0.5) {\n float dashRepeat2 = 4.0;\n float dashDuty2 = 0.5;\n float vy2m = fract(gridUV2.y * dashRepeat2);\n float vx2m = fract(gridUV2.x * dashRepeat2);\n float dashMaskY2 = step(vy2m, dashDuty2);\n float dashMaskX2 = step(vx2m, dashDuty2);\n if (uLineStyle < 1.5) {\n lineX2 *= dashMaskY2;\n lineY2 *= dashMaskX2;\n } else {\n float dotRepeat2 = 6.0;\n float dotWidth2 = 0.18;\n float cy2 = abs(fract(gridUV2.y * dotRepeat2) - 0.5);\n float cx2 = abs(fract(gridUV2.x * dotRepeat2) - 0.5);\n float dotMaskY2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.y * dotRepeat2), cy2);\n float dotMaskX2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.x * dotRepeat2), cx2);\n lineX2 *= dotMaskY2;\n lineY2 *= dotMaskX2;\n }\n }\n float altMask = max(lineX2, lineY2);\n\n float edgeDistX = min(abs(hit.x - (-0.5)), abs(hit.x - 0.5));\n float edgeDistY = min(abs(hit.y - (-0.2)), abs(hit.y - 0.2));\n float edgeDist = mix(edgeDistY, edgeDistX, hitIsY);\n float edgeGate = 1.0 - smoothstep(gridScale * 0.5, gridScale * 2.0, edgeDist);\n altMask *= edgeGate;\n\n float lineMask = max(primaryMask, altMask);\n\n float fade = exp(-dist * fadeStrength);\n\n float dur = max(0.05, uScanDuration);\n float del = max(0.0, uScanDelay);\n float scanZMax = 2.0;\n float widthScale = max(0.1, uScanGlow);\n float sigma = max(0.001, 0.18 * widthScale * uScanSoftness);\n float sigmaA = sigma * 2.0;\n\n float combinedPulse = 0.0;\n float combinedAura = 0.0;\n\n float cycle = dur + del;\n float tCycle = mod(iTime, cycle);\n float scanPhase = clamp((tCycle - del) / dur, 0.0, 1.0);\n float phase = scanPhase;\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phase = 1.0 - phase;\n } else if (uScanDirection > 1.5) {\n float t2 = mod(max(0.0, iTime - del), 2.0 * dur);\n phase = (t2 < dur) ? (t2 / dur) : (1.0 - (t2 - dur) / dur);\n }\n float scanZ = phase * scanZMax;\n float dz = abs(hit.z - scanZ);\n float lineBand = exp(-0.5 * (dz * dz) / (sigma * sigma));\n float taper = clamp(uPhaseTaper, 0.0, 0.49);\n float headW = taper;\n float tailW = taper;\n float headFade = smoother01(0.0, headW, phase);\n float tailFade = 1.0 - smoother01(1.0 - tailW, 1.0, phase);\n float phaseWindow = headFade * tailFade;\n float pulseBase = lineBand * phaseWindow;\n combinedPulse += pulseBase * clamp(uScanOpacity, 0.0, 1.0);\n float auraBand = exp(-0.5 * (dz * dz) / (sigmaA * sigmaA));\n combinedAura += (auraBand * 0.25) * phaseWindow * clamp(uScanOpacity, 0.0, 1.0);\n\n for (int i = 0; i < MAX_SCANS; i++) {\n if (float(i) >= uScanCount) break;\n float tActiveI = iTime - uScanStarts[i];\n float phaseI = clamp(tActiveI / dur, 0.0, 1.0);\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phaseI = 1.0 - phaseI;\n } else if (uScanDirection > 1.5) {\n phaseI = (phaseI < 0.5) ? (phaseI * 2.0) : (1.0 - (phaseI - 0.5) * 2.0);\n }\n float scanZI = phaseI * scanZMax;\n float dzI = abs(hit.z - scanZI);\n float lineBandI = exp(-0.5 * (dzI * dzI) / (sigma * sigma));\n float headFadeI = smoother01(0.0, headW, phaseI);\n float tailFadeI = 1.0 - smoother01(1.0 - tailW, 1.0, phaseI);\n float phaseWindowI = headFadeI * tailFadeI;\n combinedPulse += lineBandI * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n float auraBandI = exp(-0.5 * (dzI * dzI) / (sigmaA * sigmaA));\n combinedAura += (auraBandI * 0.25) * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n }\n\n float lineVis = lineMask;\n vec3 gridCol = uLinesColor * lineVis * fade;\n vec3 scanCol = uScanColor * combinedPulse;\n vec3 scanAura = uScanColor * combinedAura;\n\n color = gridCol + scanCol + scanAura;\n\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(iTime * 123.4), vec2(12.9898,78.233))) * 43758.5453123);\n color += (n - 0.5) * uNoise;\n color = clamp(color, 0.0, 1.0);\n float alpha = clamp(max(lineVis, combinedPulse), 0.0, 1.0);\n float gx = 1.0 - smoothstep(tx * 2.0, tx * 2.0 + aax * 2.0, ax);\n float gy = 1.0 - smoothstep(ty * 2.0, ty * 2.0 + aay * 2.0, ay);\n float halo = max(gx, gy) * fade;\n alpha = max(alpha, halo * clamp(uBloomOpacity, 0.0, 1.0));\n fragColor = vec4(color, alpha);\n}\n\nvoid main(){\n vec4 c;\n mainImage(c, vUv * iResolution.xy);\n gl_FragColor = c;\n}\n`;\n\nexport const GridScan = ({\n enableWebcam = false,\n showPreview = false,\n modelsPath = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights',\n sensitivity = 0.55,\n lineThickness = 1,\n linesColor = '#392e4e',\n scanColor = '#FF9FFC',\n scanOpacity = 0.4,\n gridScale = 0.1,\n lineStyle = 'solid',\n lineJitter = 0.1,\n scanDirection = 'pingpong',\n enablePost = true,\n bloomIntensity = 0,\n bloomThreshold = 0,\n bloomSmoothing = 0,\n chromaticAberration = 0.002,\n noiseIntensity = 0.01,\n scanGlow = 0.5,\n scanSoftness = 2,\n scanPhaseTaper = 0.9,\n scanDuration = 2.0,\n scanDelay = 2.0,\n enableGyro = false,\n scanOnClick = false,\n snapBackDelay = 250,\n className,\n style\n}) => {\n const containerRef = useRef(null);\n const videoRef = useRef(null);\n\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const composerRef = useRef(null);\n const bloomRef = useRef(null);\n const chromaRef = useRef(null);\n const rafRef = useRef(null);\n\n const [modelsReady, setModelsReady] = useState(false);\n const [uiFaceActive, setUiFaceActive] = useState(false);\n\n const lookTarget = useRef(new THREE.Vector2(0, 0));\n const tiltTarget = useRef(0);\n const yawTarget = useRef(0);\n\n const lookCurrent = useRef(new THREE.Vector2(0, 0));\n const lookVel = useRef(new THREE.Vector2(0, 0));\n const tiltCurrent = useRef(0);\n const tiltVel = useRef(0);\n const yawCurrent = useRef(0);\n const yawVel = useRef(0);\n\n const MAX_SCANS = 8;\n const scanStartsRef = useRef([]);\n\n const pushScan = t => {\n const arr = scanStartsRef.current.slice();\n if (arr.length >= MAX_SCANS) arr.shift();\n arr.push(t);\n scanStartsRef.current = arr;\n if (materialRef.current) {\n const u = materialRef.current.uniforms;\n const buf = new Array(MAX_SCANS).fill(0);\n for (let i = 0; i < arr.length && i < MAX_SCANS; i++) buf[i] = arr[i];\n u.uScanStarts.value = buf;\n u.uScanCount.value = arr.length;\n }\n };\n\n const bufX = useRef([]);\n const bufY = useRef([]);\n const bufT = useRef([]);\n const bufYaw = useRef([]);\n\n const s = THREE.MathUtils.clamp(sensitivity, 0, 1);\n const skewScale = THREE.MathUtils.lerp(0.06, 0.2, s);\n const tiltScale = THREE.MathUtils.lerp(0.12, 0.3, s);\n const yawScale = THREE.MathUtils.lerp(0.1, 0.28, s);\n const depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s);\n const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s);\n const maxSpeed = Infinity;\n\n const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n let leaveTimer = null;\n const onMove = e => {\n if (uiFaceActive) return;\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n const rect = el.getBoundingClientRect();\n const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n lookTarget.current.set(nx, ny);\n };\n const onClick = async () => {\n const nowSec = performance.now() / 1000;\n if (scanOnClick) pushScan(nowSec);\n if (\n enableGyro &&\n typeof window !== 'undefined' &&\n window.DeviceOrientationEvent &&\n DeviceOrientationEvent.requestPermission\n ) {\n try {\n await DeviceOrientationEvent.requestPermission();\n } catch {\n // noop\n }\n }\n };\n const onEnter = () => {\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n };\n const onLeave = () => {\n if (uiFaceActive) return;\n if (leaveTimer) clearTimeout(leaveTimer);\n leaveTimer = window.setTimeout(\n () => {\n lookTarget.current.set(0, 0);\n tiltTarget.current = 0;\n yawTarget.current = 0;\n },\n Math.max(0, snapBackDelay || 0)\n );\n };\n el.addEventListener('mousemove', onMove);\n el.addEventListener('mouseenter', onEnter);\n if (scanOnClick) el.addEventListener('click', onClick);\n el.addEventListener('mouseleave', onLeave);\n return () => {\n el.removeEventListener('mousemove', onMove);\n el.removeEventListener('mouseenter', onEnter);\n el.removeEventListener('mouseleave', onLeave);\n if (scanOnClick) el.removeEventListener('click', onClick);\n if (leaveTimer) clearTimeout(leaveTimer);\n };\n }, [uiFaceActive, snapBackDelay, scanOnClick, enableGyro]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n rendererRef.current = renderer;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setSize(container.clientWidth, container.clientHeight);\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.toneMapping = THREE.NoToneMapping;\n renderer.autoClear = false;\n renderer.setClearColor(0x000000, 0);\n container.appendChild(renderer.domElement);\n\n const uniforms = {\n iResolution: {\n value: new THREE.Vector3(container.clientWidth, container.clientHeight, renderer.getPixelRatio())\n },\n iTime: { value: 0 },\n uSkew: { value: new THREE.Vector2(0, 0) },\n uTilt: { value: 0 },\n uYaw: { value: 0 },\n uLineThickness: { value: lineThickness },\n uLinesColor: { value: srgbColor(linesColor) },\n uScanColor: { value: srgbColor(scanColor) },\n uGridScale: { value: gridScale },\n uLineStyle: { value: lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0 },\n uLineJitter: { value: Math.max(0, Math.min(1, lineJitter || 0)) },\n uScanOpacity: { value: scanOpacity },\n uNoise: { value: noiseIntensity },\n uBloomOpacity: { value: bloomIntensity },\n uScanGlow: { value: scanGlow },\n uScanSoftness: { value: scanSoftness },\n uPhaseTaper: { value: scanPhaseTaper },\n uScanDuration: { value: scanDuration },\n uScanDelay: { value: scanDelay },\n uScanDirection: { value: scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0 },\n uScanStarts: { value: new Array(MAX_SCANS).fill(0) },\n uScanCount: { value: 0 }\n };\n\n const material = new THREE.ShaderMaterial({\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.current = material;\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);\n scene.add(quad);\n\n let composer = null;\n if (enablePost) {\n composer = new EffectComposer(renderer);\n composerRef.current = composer;\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloom = new BloomEffect({\n intensity: 1.0,\n luminanceThreshold: bloomThreshold,\n luminanceSmoothing: bloomSmoothing\n });\n bloom.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current = bloom;\n\n const chroma = new ChromaticAberrationEffect({\n offset: new THREE.Vector2(chromaticAberration, chromaticAberration),\n radialModulation: true,\n modulationOffset: 0.0\n });\n chromaRef.current = chroma;\n\n const effectPass = new EffectPass(camera, bloom, chroma);\n effectPass.renderToScreen = true;\n composer.addPass(effectPass);\n }\n\n const onResize = () => {\n renderer.setSize(container.clientWidth, container.clientHeight);\n material.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio());\n if (composerRef.current) composerRef.current.setSize(container.clientWidth, container.clientHeight);\n };\n window.addEventListener('resize', onResize);\n\n let last = performance.now();\n const tick = () => {\n const now = performance.now();\n const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));\n last = now;\n\n lookCurrent.current.copy(\n smoothDampVec2(lookCurrent.current, lookTarget.current, lookVel.current, smoothTime, maxSpeed, dt)\n );\n\n const tiltSm = smoothDampFloat(\n tiltCurrent.current,\n tiltTarget.current,\n { v: tiltVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n tiltCurrent.current = tiltSm.value;\n tiltVel.current = tiltSm.v;\n\n const yawSm = smoothDampFloat(\n yawCurrent.current,\n yawTarget.current,\n { v: yawVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n yawCurrent.current = yawSm.value;\n yawVel.current = yawSm.v;\n\n const skew = new THREE.Vector2(lookCurrent.current.x * skewScale, -lookCurrent.current.y * yBoost * skewScale);\n material.uniforms.uSkew.value.set(skew.x, skew.y);\n material.uniforms.uTilt.value = tiltCurrent.current * tiltScale;\n material.uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent.current * yawScale, -0.6, 0.6);\n\n material.uniforms.iTime.value = now / 1000;\n renderer.clear(true, true, true);\n if (composerRef.current) {\n composerRef.current.render(dt);\n } else {\n renderer.render(scene, camera);\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n rafRef.current = requestAnimationFrame(tick);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n window.removeEventListener('resize', onResize);\n material.dispose();\n quad.geometry.dispose();\n\n if (composerRef.current) {\n composerRef.current.dispose();\n composerRef.current = null;\n }\n renderer.dispose();\n renderer.forceContextLoss();\n container.removeChild(renderer.domElement);\n };\n }, [\n sensitivity,\n lineThickness,\n linesColor,\n scanColor,\n scanOpacity,\n gridScale,\n lineStyle,\n lineJitter,\n scanDirection,\n enablePost,\n noiseIntensity,\n bloomIntensity,\n scanGlow,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n smoothTime,\n maxSpeed,\n skewScale,\n yBoost,\n tiltScale,\n yawScale\n ]);\n\n useEffect(() => {\n const m = materialRef.current;\n if (m) {\n const u = m.uniforms;\n u.uLineThickness.value = lineThickness;\n u.uLinesColor.value.copy(srgbColor(linesColor));\n u.uScanColor.value.copy(srgbColor(scanColor));\n u.uGridScale.value = gridScale;\n u.uLineStyle.value = lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0;\n u.uLineJitter.value = Math.max(0, Math.min(1, lineJitter || 0));\n u.uBloomOpacity.value = Math.max(0, bloomIntensity);\n u.uNoise.value = Math.max(0, noiseIntensity);\n u.uScanGlow.value = scanGlow;\n u.uScanOpacity.value = Math.max(0, Math.min(1, scanOpacity));\n u.uScanDirection.value = scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0;\n u.uScanSoftness.value = scanSoftness;\n u.uPhaseTaper.value = scanPhaseTaper;\n u.uScanDuration.value = Math.max(0.05, scanDuration);\n u.uScanDelay.value = Math.max(0.0, scanDelay);\n }\n if (bloomRef.current) {\n bloomRef.current.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current.luminanceMaterial.threshold = bloomThreshold;\n bloomRef.current.luminanceMaterial.smoothing = bloomSmoothing;\n }\n if (chromaRef.current) {\n chromaRef.current.offset.set(chromaticAberration, chromaticAberration);\n }\n }, [\n lineThickness,\n linesColor,\n scanColor,\n gridScale,\n lineStyle,\n lineJitter,\n bloomIntensity,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n noiseIntensity,\n scanGlow,\n scanOpacity,\n scanDirection,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay\n ]);\n\n useEffect(() => {\n if (!enableGyro) return;\n const handler = e => {\n if (uiFaceActive) return;\n const gamma = e.gamma ?? 0;\n const beta = e.beta ?? 0;\n const nx = THREE.MathUtils.clamp(gamma / 45, -1, 1);\n const ny = THREE.MathUtils.clamp(-beta / 30, -1, 1);\n lookTarget.current.set(nx, ny);\n tiltTarget.current = THREE.MathUtils.degToRad(gamma) * 0.4;\n };\n window.addEventListener('deviceorientation', handler);\n return () => {\n window.removeEventListener('deviceorientation', handler);\n };\n }, [enableGyro, uiFaceActive]);\n\n useEffect(() => {\n let canceled = false;\n const load = async () => {\n try {\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath),\n faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelsPath)\n ]);\n if (!canceled) setModelsReady(true);\n } catch {\n if (!canceled) setModelsReady(false);\n }\n };\n load();\n return () => {\n canceled = true;\n };\n }, [modelsPath]);\n\n useEffect(() => {\n let stop = false;\n let lastDetect = 0;\n const video = videoRef.current;\n\n const start = async () => {\n if (!enableWebcam || !modelsReady) return;\n if (!video) return;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } },\n audio: false\n });\n video.srcObject = stream;\n await video.play();\n } catch {\n return;\n }\n\n const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });\n\n const detect = async ts => {\n if (stop) return;\n\n if (ts - lastDetect >= 33) {\n lastDetect = ts;\n try {\n const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true);\n if (res && res.detection) {\n const det = res.detection;\n const box = det.box;\n const vw = video.videoWidth || 1;\n const vh = video.videoHeight || 1;\n\n const cx = box.x + box.width * 0.5;\n const cy = box.y + box.height * 0.5;\n const nx = (cx / vw) * 2 - 1;\n const ny = (cy / vh) * 2 - 1;\n medianPush(bufX.current, nx, 5);\n medianPush(bufY.current, ny, 5);\n const nxm = median(bufX.current);\n const nym = median(bufY.current);\n\n const look = new THREE.Vector2(Math.tanh(nxm), Math.tanh(nym));\n\n const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh));\n const depthScale = 1 + depthResponse * (faceSize - 0.25);\n lookTarget.current.copy(look.multiplyScalar(depthScale));\n\n const leftEye = res.landmarks.getLeftEye();\n const rightEye = res.landmarks.getRightEye();\n const lc = centroid(leftEye);\n const rc = centroid(rightEye);\n const tilt = Math.atan2(rc.y - lc.y, rc.x - lc.x);\n medianPush(bufT.current, tilt, 5);\n tiltTarget.current = median(bufT.current);\n\n const nose = res.landmarks.getNose();\n const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)];\n const jaw = res.landmarks.getJawOutline();\n const leftCheek = jaw[3] || jaw[2];\n const rightCheek = jaw[13] || jaw[14];\n const dL = dist2(tip, leftCheek);\n const dR = dist2(tip, rightCheek);\n const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6;\n let yawSignal = THREE.MathUtils.clamp((dR - dL) / (eyeDist * 1.6), -1, 1);\n yawSignal = Math.tanh(yawSignal);\n medianPush(bufYaw.current, yawSignal, 5);\n yawTarget.current = median(bufYaw.current);\n\n setUiFaceActive(true);\n } else {\n setUiFaceActive(false);\n }\n } catch {\n setUiFaceActive(false);\n }\n }\n\n if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {\n video.requestVideoFrameCallback(() => detect(performance.now()));\n } else {\n requestAnimationFrame(detect);\n }\n };\n\n requestAnimationFrame(detect);\n };\n\n start();\n\n return () => {\n stop = true;\n if (video) {\n const stream = video.srcObject;\n if (stream) stream.getTracks().forEach(t => t.stop());\n video.pause();\n video.srcObject = null;\n }\n };\n }, [enableWebcam, modelsReady, depthResponse]);\n\n return (\n \n {showPreview && (\n
\n
\n
\n {enableWebcam\n ? modelsReady\n ? uiFaceActive\n ? 'Face: tracking'\n : 'Face: searching'\n : 'Loading models'\n : 'Webcam disabled'}\n
\n
\n )}\n
\n );\n};\n\nfunction srgbColor(hex) {\n const c = new THREE.Color(hex);\n return c.convertSRGBToLinear();\n}\n\nfunction smoothDampVec2(current, target, currentVelocity, smoothTime, maxSpeed, deltaTime) {\n const out = current.clone();\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current.clone().sub(target);\n const originalTo = target.clone();\n\n const maxChange = maxSpeed * smoothTime;\n if (change.length() > maxChange) change.setLength(maxChange);\n\n target = current.clone().sub(change);\n const temp = currentVelocity.clone().addScaledVector(change, omega).multiplyScalar(deltaTime);\n currentVelocity.sub(temp.clone().multiplyScalar(omega));\n currentVelocity.multiplyScalar(exp);\n\n out.copy(target.clone().add(change.add(temp).multiplyScalar(exp)));\n\n const origMinusCurrent = originalTo.clone().sub(current);\n const outMinusOrig = out.clone().sub(originalTo);\n if (origMinusCurrent.dot(outMinusOrig) > 0) {\n out.copy(originalTo);\n currentVelocity.set(0, 0);\n }\n return out;\n}\n\nfunction smoothDampFloat(current, target, velRef, smoothTime, maxSpeed, deltaTime) {\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current - target;\n const originalTo = target;\n\n const maxChange = maxSpeed * smoothTime;\n change = Math.sign(change) * Math.min(Math.abs(change), maxChange);\n\n target = current - change;\n const temp = (velRef.v + omega * change) * deltaTime;\n velRef.v = (velRef.v - omega * temp) * exp;\n\n let out = target + (change + temp) * exp;\n\n const origMinusCurrent = originalTo - current;\n const outMinusOrig = out - originalTo;\n if (origMinusCurrent * outMinusOrig > 0) {\n out = originalTo;\n velRef.v = 0;\n }\n return { value: out, v: velRef.v };\n}\n\nfunction medianPush(buf, v, maxLen) {\n buf.push(v);\n if (buf.length > maxLen) buf.shift();\n}\n\nfunction median(buf) {\n if (buf.length === 0) return 0;\n const a = [...buf].sort((x, y) => x - y);\n const mid = Math.floor(a.length / 2);\n return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) * 0.5;\n}\n\nfunction centroid(points) {\n let x = 0,\n y = 0;\n const n = points.length || 1;\n for (const p of points) {\n x += p.x;\n y += p.y;\n }\n return { x: x / n, y: y / n };\n}\n\nfunction dist2(a, b) {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n"
}
],
"registryDependencies": [],
"dependencies": [
+ "face-api.js@^0.22.2",
"postprocessing@^6.36.0",
- "three@^0.167.1",
- "face-api.js@^0.22.2"
+ "three@^0.167.1"
]
}
\ No newline at end of file
diff --git a/public/r/GridScan-TS-CSS.json b/public/r/GridScan-TS-CSS.json
index 3ea8b703..2260e6df 100644
--- a/public/r/GridScan-TS-CSS.json
+++ b/public/r/GridScan-TS-CSS.json
@@ -13,13 +13,13 @@
{
"type": "registry:component",
"path": "GridScan/GridScan.tsx",
- "content": "import React, { useEffect, useRef, useState } from 'react';\nimport { EffectComposer, RenderPass, EffectPass, BloomEffect, ChromaticAberrationEffect } from 'postprocessing';\nimport * as THREE from 'three';\nimport * as faceapi from 'face-api.js';\nimport './GridScan.css';\n\ntype GridScanProps = {\n enableWebcam?: boolean;\n showPreview?: boolean;\n modelsPath?: string;\n sensitivity?: number;\n\n lineThickness?: number;\n linesColor?: string;\n\n gridScale?: number;\n lineStyle?: 'solid' | 'dashed' | 'dotted';\n lineJitter?: number;\n\n enablePost?: boolean;\n bloomIntensity?: number;\n bloomThreshold?: number;\n bloomSmoothing?: number;\n chromaticAberration?: number;\n noiseIntensity?: number;\n\n scanColor?: string;\n scanOpacity?: number;\n scanDirection?: 'forward' | 'backward' | 'pingpong';\n scanSoftness?: number;\n scanGlow?: number;\n scanPhaseTaper?: number;\n scanDuration?: number;\n scanDelay?: number;\n enableGyro?: boolean;\n scanOnClick?: boolean;\n snapBackDelay?: number;\n className?: string;\n style?: React.CSSProperties;\n};\n\nconst vert = `\nvarying vec2 vUv;\nvoid main(){\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`;\n\nconst frag = `\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec2 uSkew;\nuniform float uTilt;\nuniform float uYaw;\nuniform float uLineThickness;\nuniform vec3 uLinesColor;\nuniform vec3 uScanColor;\nuniform float uGridScale;\nuniform float uLineStyle;\nuniform float uLineJitter;\nuniform float uScanOpacity;\nuniform float uScanDirection;\nuniform float uNoise;\nuniform float uBloomOpacity;\nuniform float uScanGlow;\nuniform float uScanSoftness;\nuniform float uPhaseTaper;\nuniform float uScanDuration;\nuniform float uScanDelay;\nvarying vec2 vUv;\n\nuniform float uScanStarts[8];\nuniform float uScanCount;\n\nconst int MAX_SCANS = 8;\n\nfloat smoother01(float a, float b, float x){\n float t = clamp((x - a) / max(1e-5, (b - a)), 0.0, 1.0);\n return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord)\n{\n vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n\n vec3 ro = vec3(0.0);\n vec3 rd = normalize(vec3(p, 2.0));\n\n float cR = cos(uTilt), sR = sin(uTilt);\n rd.xy = mat2(cR, -sR, sR, cR) * rd.xy;\n\n float cY = cos(uYaw), sY = sin(uYaw);\n rd.xz = mat2(cY, -sY, sY, cY) * rd.xz;\n\n vec2 skew = clamp(uSkew, vec2(-0.7), vec2(0.7));\n rd.xy += skew * rd.z;\n\n vec3 color = vec3(0.0);\n float minT = 1e20;\n float gridScale = max(1e-5, uGridScale);\n float fadeStrength = 2.0;\n vec2 gridUV = vec2(0.0);\n\n float hitIsY = 1.0;\n for (int i = 0; i < 4; i++)\n {\n float isY = float(i < 2);\n float pos = mix(-0.2, 0.2, float(i)) * isY + mix(-0.5, 0.5, float(i - 2)) * (1.0 - isY);\n float num = pos - (isY * ro.y + (1.0 - isY) * ro.x);\n float den = isY * rd.y + (1.0 - isY) * rd.x;\n float t = num / den;\n vec3 h = ro + rd * t;\n\n float depthBoost = smoothstep(0.0, 3.0, h.z);\n h.xy += skew * 0.15 * depthBoost;\n\n bool use = t > 0.0 && t < minT;\n gridUV = use ? mix(h.zy, h.xz, isY) / gridScale : gridUV;\n minT = use ? t : minT;\n hitIsY = use ? isY : hitIsY;\n }\n\n vec3 hit = ro + rd * minT;\n float dist = length(hit - ro);\n\n float jitterAmt = clamp(uLineJitter, 0.0, 1.0);\n if (jitterAmt > 0.0) {\n vec2 j = vec2(\n sin(gridUV.y * 2.7 + iTime * 1.8),\n cos(gridUV.x * 2.3 - iTime * 1.6)\n ) * (0.15 * jitterAmt);\n gridUV += j;\n }\n float fx = fract(gridUV.x);\n float fy = fract(gridUV.y);\n float ax = min(fx, 1.0 - fx);\n float ay = min(fy, 1.0 - fy);\n float wx = fwidth(gridUV.x);\n float wy = fwidth(gridUV.y);\n float halfPx = max(0.0, uLineThickness) * 0.5;\n\n float tx = halfPx * wx;\n float ty = halfPx * wy;\n\n float aax = wx;\n float aay = wy;\n\n float lineX = 1.0 - smoothstep(tx, tx + aax, ax);\n float lineY = 1.0 - smoothstep(ty, ty + aay, ay);\n if (uLineStyle > 0.5) {\n float dashRepeat = 4.0;\n float dashDuty = 0.5;\n float vy = fract(gridUV.y * dashRepeat);\n float vx = fract(gridUV.x * dashRepeat);\n float dashMaskY = step(vy, dashDuty);\n float dashMaskX = step(vx, dashDuty);\n if (uLineStyle < 1.5) {\n lineX *= dashMaskY;\n lineY *= dashMaskX;\n } else {\n float dotRepeat = 6.0;\n float dotWidth = 0.18;\n float cy = abs(fract(gridUV.y * dotRepeat) - 0.5);\n float cx = abs(fract(gridUV.x * dotRepeat) - 0.5);\n float dotMaskY = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.y * dotRepeat), cy);\n float dotMaskX = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.x * dotRepeat), cx);\n lineX *= dotMaskY;\n lineY *= dotMaskX;\n }\n }\n float primaryMask = max(lineX, lineY);\n\n vec2 gridUV2 = (hitIsY > 0.5 ? hit.xz : hit.zy) / gridScale;\n if (jitterAmt > 0.0) {\n vec2 j2 = vec2(\n cos(gridUV2.y * 2.1 - iTime * 1.4),\n sin(gridUV2.x * 2.5 + iTime * 1.7)\n ) * (0.15 * jitterAmt);\n gridUV2 += j2;\n }\n float fx2 = fract(gridUV2.x);\n float fy2 = fract(gridUV2.y);\n float ax2 = min(fx2, 1.0 - fx2);\n float ay2 = min(fy2, 1.0 - fy2);\n float wx2 = fwidth(gridUV2.x);\n float wy2 = fwidth(gridUV2.y);\n float tx2 = halfPx * wx2;\n float ty2 = halfPx * wy2;\n float aax2 = wx2;\n float aay2 = wy2;\n float lineX2 = 1.0 - smoothstep(tx2, tx2 + aax2, ax2);\n float lineY2 = 1.0 - smoothstep(ty2, ty2 + aay2, ay2);\n if (uLineStyle > 0.5) {\n float dashRepeat2 = 4.0;\n float dashDuty2 = 0.5;\n float vy2m = fract(gridUV2.y * dashRepeat2);\n float vx2m = fract(gridUV2.x * dashRepeat2);\n float dashMaskY2 = step(vy2m, dashDuty2);\n float dashMaskX2 = step(vx2m, dashDuty2);\n if (uLineStyle < 1.5) {\n lineX2 *= dashMaskY2;\n lineY2 *= dashMaskX2;\n } else {\n float dotRepeat2 = 6.0;\n float dotWidth2 = 0.18;\n float cy2 = abs(fract(gridUV2.y * dotRepeat2) - 0.5);\n float cx2 = abs(fract(gridUV2.x * dotRepeat2) - 0.5);\n float dotMaskY2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.y * dotRepeat2), cy2);\n float dotMaskX2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.x * dotRepeat2), cx2);\n lineX2 *= dotMaskY2;\n lineY2 *= dotMaskX2;\n }\n }\n float altMask = max(lineX2, lineY2);\n\n float edgeDistX = min(abs(hit.x - (-0.5)), abs(hit.x - 0.5));\n float edgeDistY = min(abs(hit.y - (-0.2)), abs(hit.y - 0.2));\n float edgeDist = mix(edgeDistY, edgeDistX, hitIsY);\n float edgeGate = 1.0 - smoothstep(gridScale * 0.5, gridScale * 2.0, edgeDist);\n altMask *= edgeGate;\n\n float lineMask = max(primaryMask, altMask);\n\n float fade = exp(-dist * fadeStrength);\n\n float dur = max(0.05, uScanDuration);\n float del = max(0.0, uScanDelay);\n float scanZMax = 2.0;\n float widthScale = max(0.1, uScanGlow);\n float sigma = max(0.001, 0.18 * widthScale * uScanSoftness);\n float sigmaA = sigma * 2.0;\n\n float combinedPulse = 0.0;\n float combinedAura = 0.0;\n\n float cycle = dur + del;\n float tCycle = mod(iTime, cycle);\n float scanPhase = clamp((tCycle - del) / dur, 0.0, 1.0);\n float phase = scanPhase;\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phase = 1.0 - phase;\n } else if (uScanDirection > 1.5) {\n float t2 = mod(max(0.0, iTime - del), 2.0 * dur);\n phase = (t2 < dur) ? (t2 / dur) : (1.0 - (t2 - dur) / dur);\n }\n float scanZ = phase * scanZMax;\n float dz = abs(hit.z - scanZ);\n float lineBand = exp(-0.5 * (dz * dz) / (sigma * sigma));\n float taper = clamp(uPhaseTaper, 0.0, 0.49);\n float headW = taper;\n float tailW = taper;\n float headFade = smoother01(0.0, headW, phase);\n float tailFade = 1.0 - smoother01(1.0 - tailW, 1.0, phase);\n float phaseWindow = headFade * tailFade;\n float pulseBase = lineBand * phaseWindow;\n combinedPulse += pulseBase * clamp(uScanOpacity, 0.0, 1.0);\n float auraBand = exp(-0.5 * (dz * dz) / (sigmaA * sigmaA));\n combinedAura += (auraBand * 0.25) * phaseWindow * clamp(uScanOpacity, 0.0, 1.0);\n\n for (int i = 0; i < MAX_SCANS; i++) {\n if (float(i) >= uScanCount) break;\n float tActiveI = iTime - uScanStarts[i];\n float phaseI = clamp(tActiveI / dur, 0.0, 1.0);\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phaseI = 1.0 - phaseI;\n } else if (uScanDirection > 1.5) {\n phaseI = (phaseI < 0.5) ? (phaseI * 2.0) : (1.0 - (phaseI - 0.5) * 2.0);\n }\n float scanZI = phaseI * scanZMax;\n float dzI = abs(hit.z - scanZI);\n float lineBandI = exp(-0.5 * (dzI * dzI) / (sigma * sigma));\n float headFadeI = smoother01(0.0, headW, phaseI);\n float tailFadeI = 1.0 - smoother01(1.0 - tailW, 1.0, phaseI);\n float phaseWindowI = headFadeI * tailFadeI;\n combinedPulse += lineBandI * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n float auraBandI = exp(-0.5 * (dzI * dzI) / (sigmaA * sigmaA));\n combinedAura += (auraBandI * 0.25) * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n }\n\n float lineVis = lineMask;\n vec3 gridCol = uLinesColor * lineVis * fade;\n vec3 scanCol = uScanColor * combinedPulse;\n vec3 scanAura = uScanColor * combinedAura;\n\n color = gridCol + scanCol + scanAura;\n\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(iTime * 123.4), vec2(12.9898,78.233))) * 43758.5453123);\n color += (n - 0.5) * uNoise;\n color = clamp(color, 0.0, 1.0);\n float alpha = clamp(max(lineVis, combinedPulse), 0.0, 1.0);\n float gx = 1.0 - smoothstep(tx * 2.0, tx * 2.0 + aax * 2.0, ax);\n float gy = 1.0 - smoothstep(ty * 2.0, ty * 2.0 + aay * 2.0, ay);\n float halo = max(gx, gy) * fade;\n alpha = max(alpha, halo * clamp(uBloomOpacity, 0.0, 1.0));\n fragColor = vec4(color, alpha);\n}\n\nvoid main(){\n vec4 c;\n mainImage(c, vUv * iResolution.xy);\n gl_FragColor = c;\n}\n`;\n\nexport const GridScan: React.FC = ({\n enableWebcam = false,\n showPreview = false,\n modelsPath = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights',\n sensitivity = 0.55,\n lineThickness = 1,\n linesColor = '#392e4e',\n scanColor = '#FF9FFC',\n scanOpacity = 0.4,\n gridScale = 0.1,\n lineStyle = 'solid',\n lineJitter = 0.1,\n scanDirection = 'pingpong',\n enablePost = true,\n bloomIntensity = 0,\n bloomThreshold = 0,\n bloomSmoothing = 0,\n chromaticAberration = 0.002,\n noiseIntensity = 0.01,\n scanGlow = 0.5,\n scanSoftness = 2,\n scanPhaseTaper = 0.9,\n scanDuration = 2.0,\n scanDelay = 2.0,\n enableGyro = false,\n scanOnClick = false,\n snapBackDelay = 250,\n className,\n style\n}) => {\n const containerRef = useRef(null);\n const videoRef = useRef(null);\n\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const composerRef = useRef(null);\n const bloomRef = useRef(null);\n const chromaRef = useRef(null);\n const rafRef = useRef(null);\n\n const [modelsReady, setModelsReady] = useState(false);\n const [uiFaceActive, setUiFaceActive] = useState(false);\n\n const lookTarget = useRef(new THREE.Vector2(0, 0));\n const tiltTarget = useRef(0);\n const yawTarget = useRef(0);\n\n const lookCurrent = useRef(new THREE.Vector2(0, 0));\n const lookVel = useRef(new THREE.Vector2(0, 0));\n const tiltCurrent = useRef(0);\n const tiltVel = useRef(0);\n const yawCurrent = useRef(0);\n const yawVel = useRef(0);\n\n const MAX_SCANS = 8;\n const scanStartsRef = useRef([]);\n\n const pushScan = (t: number) => {\n const arr = scanStartsRef.current.slice();\n if (arr.length >= MAX_SCANS) arr.shift();\n arr.push(t);\n scanStartsRef.current = arr;\n if (materialRef.current) {\n const u = materialRef.current.uniforms;\n const buf = new Array(MAX_SCANS).fill(0);\n for (let i = 0; i < arr.length && i < MAX_SCANS; i++) buf[i] = arr[i];\n u.uScanStarts.value = buf;\n u.uScanCount.value = arr.length;\n }\n };\n\n const bufX = useRef([]);\n const bufY = useRef([]);\n const bufT = useRef([]);\n const bufYaw = useRef([]);\n\n const s = THREE.MathUtils.clamp(sensitivity, 0, 1);\n const skewScale = THREE.MathUtils.lerp(0.06, 0.2, s);\n const tiltScale = THREE.MathUtils.lerp(0.12, 0.3, s);\n const yawScale = THREE.MathUtils.lerp(0.1, 0.28, s);\n const depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s);\n const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s);\n const maxSpeed = Infinity;\n\n const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n let leaveTimer: number | null = null;\n const onMove = (e: MouseEvent) => {\n if (uiFaceActive) return;\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n const rect = el.getBoundingClientRect();\n const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n lookTarget.current.set(nx, ny);\n };\n const onClick = async () => {\n const nowSec = performance.now() / 1000;\n if (scanOnClick) pushScan(nowSec);\n if (\n enableGyro &&\n typeof window !== 'undefined' &&\n (window as any).DeviceOrientationEvent &&\n (DeviceOrientationEvent as any).requestPermission\n ) {\n try {\n await (DeviceOrientationEvent as any).requestPermission();\n } catch {}\n }\n };\n const onEnter = () => {\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n };\n const onLeave = () => {\n if (uiFaceActive) return;\n if (leaveTimer) clearTimeout(leaveTimer);\n leaveTimer = window.setTimeout(\n () => {\n lookTarget.current.set(0, 0);\n tiltTarget.current = 0;\n yawTarget.current = 0;\n },\n Math.max(0, snapBackDelay || 0)\n );\n };\n el.addEventListener('mousemove', onMove);\n el.addEventListener('mouseenter', onEnter);\n if (scanOnClick) el.addEventListener('click', onClick);\n el.addEventListener('mouseleave', onLeave);\n return () => {\n el.removeEventListener('mousemove', onMove);\n el.removeEventListener('mouseenter', onEnter);\n el.removeEventListener('mouseleave', onLeave);\n if (scanOnClick) el.removeEventListener('click', onClick);\n if (leaveTimer) clearTimeout(leaveTimer);\n };\n }, [uiFaceActive, snapBackDelay, scanOnClick, enableGyro]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n rendererRef.current = renderer;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setSize(container.clientWidth, container.clientHeight);\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.toneMapping = THREE.NoToneMapping;\n renderer.autoClear = false;\n renderer.setClearColor(0x000000, 0);\n container.appendChild(renderer.domElement);\n\n const uniforms = {\n iResolution: {\n value: new THREE.Vector3(container.clientWidth, container.clientHeight, renderer.getPixelRatio())\n },\n iTime: { value: 0 },\n uSkew: { value: new THREE.Vector2(0, 0) },\n uTilt: { value: 0 },\n uYaw: { value: 0 },\n uLineThickness: { value: lineThickness },\n uLinesColor: { value: srgbColor(linesColor) },\n uScanColor: { value: srgbColor(scanColor) },\n uGridScale: { value: gridScale },\n uLineStyle: { value: lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0 },\n uLineJitter: { value: Math.max(0, Math.min(1, lineJitter || 0)) },\n uScanOpacity: { value: scanOpacity },\n uNoise: { value: noiseIntensity },\n uBloomOpacity: { value: bloomIntensity },\n uScanGlow: { value: scanGlow },\n uScanSoftness: { value: scanSoftness },\n uPhaseTaper: { value: scanPhaseTaper },\n uScanDuration: { value: scanDuration },\n uScanDelay: { value: scanDelay },\n uScanDirection: { value: scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0 },\n uScanStarts: { value: new Array(MAX_SCANS).fill(0) },\n uScanCount: { value: 0 }\n };\n\n const material = new THREE.ShaderMaterial({\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.current = material;\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);\n scene.add(quad);\n\n let composer: EffectComposer | null = null;\n if (enablePost) {\n composer = new EffectComposer(renderer);\n composerRef.current = composer;\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloom = new BloomEffect({\n intensity: 1.0,\n luminanceThreshold: bloomThreshold,\n luminanceSmoothing: bloomSmoothing\n });\n bloom.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current = bloom;\n\n const chroma = new ChromaticAberrationEffect({\n offset: new THREE.Vector2(chromaticAberration, chromaticAberration),\n radialModulation: true,\n modulationOffset: 0.0\n });\n chromaRef.current = chroma;\n\n const effectPass = new EffectPass(camera, bloom, chroma);\n effectPass.renderToScreen = true;\n composer.addPass(effectPass);\n }\n\n const onResize = () => {\n renderer.setSize(container.clientWidth, container.clientHeight);\n material.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio());\n if (composerRef.current) composerRef.current.setSize(container.clientWidth, container.clientHeight);\n };\n window.addEventListener('resize', onResize);\n\n let last = performance.now();\n const tick = () => {\n const now = performance.now();\n const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));\n last = now;\n\n lookCurrent.current.copy(\n smoothDampVec2(lookCurrent.current, lookTarget.current, lookVel.current, smoothTime, maxSpeed, dt)\n );\n\n const tiltSm = smoothDampFloat(\n tiltCurrent.current,\n tiltTarget.current,\n { v: tiltVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n tiltCurrent.current = tiltSm.value;\n tiltVel.current = tiltSm.v;\n\n const yawSm = smoothDampFloat(\n yawCurrent.current,\n yawTarget.current,\n { v: yawVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n yawCurrent.current = yawSm.value;\n yawVel.current = yawSm.v;\n\n const skew = new THREE.Vector2(lookCurrent.current.x * skewScale, -lookCurrent.current.y * yBoost * skewScale);\n material.uniforms.uSkew.value.set(skew.x, skew.y);\n material.uniforms.uTilt.value = tiltCurrent.current * tiltScale;\n material.uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent.current * yawScale, -0.6, 0.6);\n\n material.uniforms.iTime.value = now / 1000;\n renderer.clear(true, true, true);\n if (composerRef.current) {\n composerRef.current.render(dt);\n } else {\n renderer.render(scene, camera);\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n rafRef.current = requestAnimationFrame(tick);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n window.removeEventListener('resize', onResize);\n material.dispose();\n (quad.geometry as THREE.BufferGeometry).dispose();\n if (composerRef.current) {\n composerRef.current.dispose();\n composerRef.current = null;\n }\n renderer.dispose();\n container.removeChild(renderer.domElement);\n };\n }, [\n sensitivity,\n lineThickness,\n linesColor,\n scanColor,\n scanOpacity,\n gridScale,\n lineStyle,\n lineJitter,\n scanDirection,\n enablePost\n ]);\n\n useEffect(() => {\n const m = materialRef.current;\n if (m) {\n const u = m.uniforms;\n u.uLineThickness.value = lineThickness;\n (u.uLinesColor.value as THREE.Color).copy(srgbColor(linesColor));\n (u.uScanColor.value as THREE.Color).copy(srgbColor(scanColor));\n u.uGridScale.value = gridScale;\n u.uLineStyle.value = lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0;\n u.uLineJitter.value = Math.max(0, Math.min(1, lineJitter || 0));\n u.uBloomOpacity.value = Math.max(0, bloomIntensity);\n u.uNoise.value = Math.max(0, noiseIntensity);\n u.uScanGlow.value = scanGlow;\n u.uScanOpacity.value = Math.max(0, Math.min(1, scanOpacity));\n u.uScanDirection.value = scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0;\n u.uScanSoftness.value = scanSoftness;\n u.uPhaseTaper.value = scanPhaseTaper;\n u.uScanDuration.value = Math.max(0.05, scanDuration);\n u.uScanDelay.value = Math.max(0.0, scanDelay);\n }\n if (bloomRef.current) {\n bloomRef.current.blendMode.opacity.value = Math.max(0, bloomIntensity);\n (bloomRef.current as any).luminanceMaterial.threshold = bloomThreshold;\n (bloomRef.current as any).luminanceMaterial.smoothing = bloomSmoothing;\n }\n if (chromaRef.current) {\n chromaRef.current.offset.set(chromaticAberration, chromaticAberration);\n }\n }, [\n lineThickness,\n linesColor,\n scanColor,\n gridScale,\n lineStyle,\n lineJitter,\n bloomIntensity,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n noiseIntensity,\n scanGlow,\n scanOpacity,\n scanDirection,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay\n ]);\n\n useEffect(() => {\n if (!enableGyro) return;\n const handler = (e: DeviceOrientationEvent) => {\n if (uiFaceActive) return;\n const gamma = e.gamma ?? 0;\n const beta = e.beta ?? 0;\n const nx = THREE.MathUtils.clamp(gamma / 45, -1, 1);\n const ny = THREE.MathUtils.clamp(-beta / 30, -1, 1);\n lookTarget.current.set(nx, ny);\n tiltTarget.current = THREE.MathUtils.degToRad(gamma) * 0.4;\n };\n window.addEventListener('deviceorientation', handler);\n return () => {\n window.removeEventListener('deviceorientation', handler);\n };\n }, [enableGyro, uiFaceActive]);\n\n useEffect(() => {\n let canceled = false;\n const load = async () => {\n try {\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath),\n faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelsPath)\n ]);\n if (!canceled) setModelsReady(true);\n } catch {\n if (!canceled) setModelsReady(false);\n }\n };\n load();\n return () => {\n canceled = true;\n };\n }, [modelsPath]);\n\n useEffect(() => {\n let stop = false;\n let lastDetect = 0;\n\n const start = async () => {\n if (!enableWebcam || !modelsReady) return;\n const video = videoRef.current;\n if (!video) return;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } },\n audio: false\n });\n video.srcObject = stream;\n await video.play();\n } catch {\n return;\n }\n\n const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });\n\n const detect = async (ts: number) => {\n if (stop) return;\n\n if (ts - lastDetect >= 33) {\n lastDetect = ts;\n try {\n const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true);\n if (res && res.detection) {\n const det = res.detection;\n const box = det.box;\n const vw = video.videoWidth || 1;\n const vh = video.videoHeight || 1;\n\n const cx = box.x + box.width * 0.5;\n const cy = box.y + box.height * 0.5;\n const nx = (cx / vw) * 2 - 1;\n const ny = (cy / vh) * 2 - 1;\n medianPush(bufX.current, nx, 5);\n medianPush(bufY.current, ny, 5);\n const nxm = median(bufX.current);\n const nym = median(bufY.current);\n\n const look = new THREE.Vector2(Math.tanh(nxm), Math.tanh(nym));\n\n const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh));\n const depthScale = 1 + depthResponse * (faceSize - 0.25);\n lookTarget.current.copy(look.multiplyScalar(depthScale));\n\n const leftEye = res.landmarks.getLeftEye();\n const rightEye = res.landmarks.getRightEye();\n const lc = centroid(leftEye);\n const rc = centroid(rightEye);\n const tilt = Math.atan2(rc.y - lc.y, rc.x - lc.x);\n medianPush(bufT.current, tilt, 5);\n tiltTarget.current = median(bufT.current);\n\n const nose = res.landmarks.getNose();\n const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)];\n const jaw = res.landmarks.getJawOutline();\n const leftCheek = jaw[3] || jaw[2];\n const rightCheek = jaw[13] || jaw[14];\n const dL = dist2(tip, leftCheek);\n const dR = dist2(tip, rightCheek);\n const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6;\n let yawSignal = THREE.MathUtils.clamp((dR - dL) / (eyeDist * 1.6), -1, 1);\n yawSignal = Math.tanh(yawSignal);\n medianPush(bufYaw.current, yawSignal, 5);\n yawTarget.current = median(bufYaw.current);\n\n setUiFaceActive(true);\n } else {\n setUiFaceActive(false);\n }\n } catch {\n setUiFaceActive(false);\n }\n }\n\n if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {\n (video as any).requestVideoFrameCallback(() => detect(performance.now()));\n } else {\n requestAnimationFrame(detect);\n }\n };\n\n requestAnimationFrame(detect);\n };\n\n start();\n\n return () => {\n stop = true;\n const video = videoRef.current;\n if (video) {\n const stream = video.srcObject as MediaStream | null;\n if (stream) stream.getTracks().forEach(t => t.stop());\n video.pause();\n video.srcObject = null;\n }\n };\n }, [enableWebcam, modelsReady, depthResponse]);\n\n return (\n \n {showPreview && (\n
\n
\n
\n {enableWebcam\n ? modelsReady\n ? uiFaceActive\n ? 'Face: tracking'\n : 'Face: searching'\n : 'Loading models'\n : 'Webcam disabled'}\n
\n
\n )}\n
\n );\n};\n\nfunction srgbColor(hex: string) {\n const c = new THREE.Color(hex);\n return c.convertSRGBToLinear();\n}\n\nfunction smoothDampVec2(\n current: THREE.Vector2,\n target: THREE.Vector2,\n currentVelocity: THREE.Vector2,\n smoothTime: number,\n maxSpeed: number,\n deltaTime: number\n): THREE.Vector2 {\n const out = current.clone();\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current.clone().sub(target);\n const originalTo = target.clone();\n\n const maxChange = maxSpeed * smoothTime;\n if (change.length() > maxChange) change.setLength(maxChange);\n\n target = current.clone().sub(change);\n const temp = currentVelocity.clone().addScaledVector(change, omega).multiplyScalar(deltaTime);\n currentVelocity.sub(temp.clone().multiplyScalar(omega));\n currentVelocity.multiplyScalar(exp);\n\n out.copy(target.clone().add(change.add(temp).multiplyScalar(exp)));\n\n const origMinusCurrent = originalTo.clone().sub(current);\n const outMinusOrig = out.clone().sub(originalTo);\n if (origMinusCurrent.dot(outMinusOrig) > 0) {\n out.copy(originalTo);\n currentVelocity.set(0, 0);\n }\n return out;\n}\n\nfunction smoothDampFloat(\n current: number,\n target: number,\n velRef: { v: number },\n smoothTime: number,\n maxSpeed: number,\n deltaTime: number\n): { value: number; v: number } {\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current - target;\n const originalTo = target;\n\n const maxChange = maxSpeed * smoothTime;\n change = Math.sign(change) * Math.min(Math.abs(change), maxChange);\n\n target = current - change;\n const temp = (velRef.v + omega * change) * deltaTime;\n velRef.v = (velRef.v - omega * temp) * exp;\n\n let out = target + (change + temp) * exp;\n\n const origMinusCurrent = originalTo - current;\n const outMinusOrig = out - originalTo;\n if (origMinusCurrent * outMinusOrig > 0) {\n out = originalTo;\n velRef.v = 0;\n }\n return { value: out, v: velRef.v };\n}\n\nfunction medianPush(buf: number[], v: number, maxLen: number) {\n buf.push(v);\n if (buf.length > maxLen) buf.shift();\n}\n\nfunction median(buf: number[]) {\n if (buf.length === 0) return 0;\n const a = [...buf].sort((x, y) => x - y);\n const mid = Math.floor(a.length / 2);\n return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) * 0.5;\n}\n\nfunction centroid(points: { x: number; y: number }[]) {\n let x = 0,\n y = 0;\n const n = points.length || 1;\n for (const p of points) {\n x += p.x;\n y += p.y;\n }\n return { x: x / n, y: y / n };\n}\n\nfunction dist2(a: { x: number; y: number }, b: { x: number; y: number }) {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n"
+ "content": "import * as faceapi from 'face-api.js';\nimport { BloomEffect, ChromaticAberrationEffect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';\nimport React, { useEffect, useRef, useState } from 'react';\nimport * as THREE from 'three';\nimport './GridScan.css';\n\ntype GridScanProps = {\n enableWebcam?: boolean;\n showPreview?: boolean;\n modelsPath?: string;\n sensitivity?: number;\n\n lineThickness?: number;\n linesColor?: string;\n\n gridScale?: number;\n lineStyle?: 'solid' | 'dashed' | 'dotted';\n lineJitter?: number;\n\n enablePost?: boolean;\n bloomIntensity?: number;\n bloomThreshold?: number;\n bloomSmoothing?: number;\n chromaticAberration?: number;\n noiseIntensity?: number;\n\n scanColor?: string;\n scanOpacity?: number;\n scanDirection?: 'forward' | 'backward' | 'pingpong';\n scanSoftness?: number;\n scanGlow?: number;\n scanPhaseTaper?: number;\n scanDuration?: number;\n scanDelay?: number;\n enableGyro?: boolean;\n scanOnClick?: boolean;\n snapBackDelay?: number;\n className?: string;\n style?: React.CSSProperties;\n};\n\nconst vert = `\nvarying vec2 vUv;\nvoid main(){\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`;\n\nconst frag = `\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec2 uSkew;\nuniform float uTilt;\nuniform float uYaw;\nuniform float uLineThickness;\nuniform vec3 uLinesColor;\nuniform vec3 uScanColor;\nuniform float uGridScale;\nuniform float uLineStyle;\nuniform float uLineJitter;\nuniform float uScanOpacity;\nuniform float uScanDirection;\nuniform float uNoise;\nuniform float uBloomOpacity;\nuniform float uScanGlow;\nuniform float uScanSoftness;\nuniform float uPhaseTaper;\nuniform float uScanDuration;\nuniform float uScanDelay;\nvarying vec2 vUv;\n\nuniform float uScanStarts[8];\nuniform float uScanCount;\n\nconst int MAX_SCANS = 8;\n\nfloat smoother01(float a, float b, float x){\n float t = clamp((x - a) / max(1e-5, (b - a)), 0.0, 1.0);\n return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord)\n{\n vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n\n vec3 ro = vec3(0.0);\n vec3 rd = normalize(vec3(p, 2.0));\n\n float cR = cos(uTilt), sR = sin(uTilt);\n rd.xy = mat2(cR, -sR, sR, cR) * rd.xy;\n\n float cY = cos(uYaw), sY = sin(uYaw);\n rd.xz = mat2(cY, -sY, sY, cY) * rd.xz;\n\n vec2 skew = clamp(uSkew, vec2(-0.7), vec2(0.7));\n rd.xy += skew * rd.z;\n\n vec3 color = vec3(0.0);\n float minT = 1e20;\n float gridScale = max(1e-5, uGridScale);\n float fadeStrength = 2.0;\n vec2 gridUV = vec2(0.0);\n\n float hitIsY = 1.0;\n for (int i = 0; i < 4; i++)\n {\n float isY = float(i < 2);\n float pos = mix(-0.2, 0.2, float(i)) * isY + mix(-0.5, 0.5, float(i - 2)) * (1.0 - isY);\n float num = pos - (isY * ro.y + (1.0 - isY) * ro.x);\n float den = isY * rd.y + (1.0 - isY) * rd.x;\n float t = num / den;\n vec3 h = ro + rd * t;\n\n float depthBoost = smoothstep(0.0, 3.0, h.z);\n h.xy += skew * 0.15 * depthBoost;\n\n bool use = t > 0.0 && t < minT;\n gridUV = use ? mix(h.zy, h.xz, isY) / gridScale : gridUV;\n minT = use ? t : minT;\n hitIsY = use ? isY : hitIsY;\n }\n\n vec3 hit = ro + rd * minT;\n float dist = length(hit - ro);\n\n float jitterAmt = clamp(uLineJitter, 0.0, 1.0);\n if (jitterAmt > 0.0) {\n vec2 j = vec2(\n sin(gridUV.y * 2.7 + iTime * 1.8),\n cos(gridUV.x * 2.3 - iTime * 1.6)\n ) * (0.15 * jitterAmt);\n gridUV += j;\n }\n float fx = fract(gridUV.x);\n float fy = fract(gridUV.y);\n float ax = min(fx, 1.0 - fx);\n float ay = min(fy, 1.0 - fy);\n float wx = fwidth(gridUV.x);\n float wy = fwidth(gridUV.y);\n float halfPx = max(0.0, uLineThickness) * 0.5;\n\n float tx = halfPx * wx;\n float ty = halfPx * wy;\n\n float aax = wx;\n float aay = wy;\n\n float lineX = 1.0 - smoothstep(tx, tx + aax, ax);\n float lineY = 1.0 - smoothstep(ty, ty + aay, ay);\n if (uLineStyle > 0.5) {\n float dashRepeat = 4.0;\n float dashDuty = 0.5;\n float vy = fract(gridUV.y * dashRepeat);\n float vx = fract(gridUV.x * dashRepeat);\n float dashMaskY = step(vy, dashDuty);\n float dashMaskX = step(vx, dashDuty);\n if (uLineStyle < 1.5) {\n lineX *= dashMaskY;\n lineY *= dashMaskX;\n } else {\n float dotRepeat = 6.0;\n float dotWidth = 0.18;\n float cy = abs(fract(gridUV.y * dotRepeat) - 0.5);\n float cx = abs(fract(gridUV.x * dotRepeat) - 0.5);\n float dotMaskY = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.y * dotRepeat), cy);\n float dotMaskX = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.x * dotRepeat), cx);\n lineX *= dotMaskY;\n lineY *= dotMaskX;\n }\n }\n float primaryMask = max(lineX, lineY);\n\n vec2 gridUV2 = (hitIsY > 0.5 ? hit.xz : hit.zy) / gridScale;\n if (jitterAmt > 0.0) {\n vec2 j2 = vec2(\n cos(gridUV2.y * 2.1 - iTime * 1.4),\n sin(gridUV2.x * 2.5 + iTime * 1.7)\n ) * (0.15 * jitterAmt);\n gridUV2 += j2;\n }\n float fx2 = fract(gridUV2.x);\n float fy2 = fract(gridUV2.y);\n float ax2 = min(fx2, 1.0 - fx2);\n float ay2 = min(fy2, 1.0 - fy2);\n float wx2 = fwidth(gridUV2.x);\n float wy2 = fwidth(gridUV2.y);\n float tx2 = halfPx * wx2;\n float ty2 = halfPx * wy2;\n float aax2 = wx2;\n float aay2 = wy2;\n float lineX2 = 1.0 - smoothstep(tx2, tx2 + aax2, ax2);\n float lineY2 = 1.0 - smoothstep(ty2, ty2 + aay2, ay2);\n if (uLineStyle > 0.5) {\n float dashRepeat2 = 4.0;\n float dashDuty2 = 0.5;\n float vy2m = fract(gridUV2.y * dashRepeat2);\n float vx2m = fract(gridUV2.x * dashRepeat2);\n float dashMaskY2 = step(vy2m, dashDuty2);\n float dashMaskX2 = step(vx2m, dashDuty2);\n if (uLineStyle < 1.5) {\n lineX2 *= dashMaskY2;\n lineY2 *= dashMaskX2;\n } else {\n float dotRepeat2 = 6.0;\n float dotWidth2 = 0.18;\n float cy2 = abs(fract(gridUV2.y * dotRepeat2) - 0.5);\n float cx2 = abs(fract(gridUV2.x * dotRepeat2) - 0.5);\n float dotMaskY2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.y * dotRepeat2), cy2);\n float dotMaskX2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.x * dotRepeat2), cx2);\n lineX2 *= dotMaskY2;\n lineY2 *= dotMaskX2;\n }\n }\n float altMask = max(lineX2, lineY2);\n\n float edgeDistX = min(abs(hit.x - (-0.5)), abs(hit.x - 0.5));\n float edgeDistY = min(abs(hit.y - (-0.2)), abs(hit.y - 0.2));\n float edgeDist = mix(edgeDistY, edgeDistX, hitIsY);\n float edgeGate = 1.0 - smoothstep(gridScale * 0.5, gridScale * 2.0, edgeDist);\n altMask *= edgeGate;\n\n float lineMask = max(primaryMask, altMask);\n\n float fade = exp(-dist * fadeStrength);\n\n float dur = max(0.05, uScanDuration);\n float del = max(0.0, uScanDelay);\n float scanZMax = 2.0;\n float widthScale = max(0.1, uScanGlow);\n float sigma = max(0.001, 0.18 * widthScale * uScanSoftness);\n float sigmaA = sigma * 2.0;\n\n float combinedPulse = 0.0;\n float combinedAura = 0.0;\n\n float cycle = dur + del;\n float tCycle = mod(iTime, cycle);\n float scanPhase = clamp((tCycle - del) / dur, 0.0, 1.0);\n float phase = scanPhase;\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phase = 1.0 - phase;\n } else if (uScanDirection > 1.5) {\n float t2 = mod(max(0.0, iTime - del), 2.0 * dur);\n phase = (t2 < dur) ? (t2 / dur) : (1.0 - (t2 - dur) / dur);\n }\n float scanZ = phase * scanZMax;\n float dz = abs(hit.z - scanZ);\n float lineBand = exp(-0.5 * (dz * dz) / (sigma * sigma));\n float taper = clamp(uPhaseTaper, 0.0, 0.49);\n float headW = taper;\n float tailW = taper;\n float headFade = smoother01(0.0, headW, phase);\n float tailFade = 1.0 - smoother01(1.0 - tailW, 1.0, phase);\n float phaseWindow = headFade * tailFade;\n float pulseBase = lineBand * phaseWindow;\n combinedPulse += pulseBase * clamp(uScanOpacity, 0.0, 1.0);\n float auraBand = exp(-0.5 * (dz * dz) / (sigmaA * sigmaA));\n combinedAura += (auraBand * 0.25) * phaseWindow * clamp(uScanOpacity, 0.0, 1.0);\n\n for (int i = 0; i < MAX_SCANS; i++) {\n if (float(i) >= uScanCount) break;\n float tActiveI = iTime - uScanStarts[i];\n float phaseI = clamp(tActiveI / dur, 0.0, 1.0);\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phaseI = 1.0 - phaseI;\n } else if (uScanDirection > 1.5) {\n phaseI = (phaseI < 0.5) ? (phaseI * 2.0) : (1.0 - (phaseI - 0.5) * 2.0);\n }\n float scanZI = phaseI * scanZMax;\n float dzI = abs(hit.z - scanZI);\n float lineBandI = exp(-0.5 * (dzI * dzI) / (sigma * sigma));\n float headFadeI = smoother01(0.0, headW, phaseI);\n float tailFadeI = 1.0 - smoother01(1.0 - tailW, 1.0, phaseI);\n float phaseWindowI = headFadeI * tailFadeI;\n combinedPulse += lineBandI * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n float auraBandI = exp(-0.5 * (dzI * dzI) / (sigmaA * sigmaA));\n combinedAura += (auraBandI * 0.25) * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n }\n\n float lineVis = lineMask;\n vec3 gridCol = uLinesColor * lineVis * fade;\n vec3 scanCol = uScanColor * combinedPulse;\n vec3 scanAura = uScanColor * combinedAura;\n\n color = gridCol + scanCol + scanAura;\n\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(iTime * 123.4), vec2(12.9898,78.233))) * 43758.5453123);\n color += (n - 0.5) * uNoise;\n color = clamp(color, 0.0, 1.0);\n float alpha = clamp(max(lineVis, combinedPulse), 0.0, 1.0);\n float gx = 1.0 - smoothstep(tx * 2.0, tx * 2.0 + aax * 2.0, ax);\n float gy = 1.0 - smoothstep(ty * 2.0, ty * 2.0 + aay * 2.0, ay);\n float halo = max(gx, gy) * fade;\n alpha = max(alpha, halo * clamp(uBloomOpacity, 0.0, 1.0));\n fragColor = vec4(color, alpha);\n}\n\nvoid main(){\n vec4 c;\n mainImage(c, vUv * iResolution.xy);\n gl_FragColor = c;\n}\n`;\n\nexport const GridScan: React.FC = ({\n enableWebcam = false,\n showPreview = false,\n modelsPath = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights',\n sensitivity = 0.55,\n lineThickness = 1,\n linesColor = '#392e4e',\n scanColor = '#FF9FFC',\n scanOpacity = 0.4,\n gridScale = 0.1,\n lineStyle = 'solid',\n lineJitter = 0.1,\n scanDirection = 'pingpong',\n enablePost = true,\n bloomIntensity = 0,\n bloomThreshold = 0,\n bloomSmoothing = 0,\n chromaticAberration = 0.002,\n noiseIntensity = 0.01,\n scanGlow = 0.5,\n scanSoftness = 2,\n scanPhaseTaper = 0.9,\n scanDuration = 2.0,\n scanDelay = 2.0,\n enableGyro = false,\n scanOnClick = false,\n snapBackDelay = 250,\n className,\n style\n}) => {\n const containerRef = useRef(null);\n const videoRef = useRef(null);\n\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const composerRef = useRef(null);\n const bloomRef = useRef(null);\n const chromaRef = useRef(null);\n const rafRef = useRef(null);\n\n const [modelsReady, setModelsReady] = useState(false);\n const [uiFaceActive, setUiFaceActive] = useState(false);\n\n const lookTarget = useRef(new THREE.Vector2(0, 0));\n const tiltTarget = useRef(0);\n const yawTarget = useRef(0);\n\n const lookCurrent = useRef(new THREE.Vector2(0, 0));\n const lookVel = useRef(new THREE.Vector2(0, 0));\n const tiltCurrent = useRef(0);\n const tiltVel = useRef(0);\n const yawCurrent = useRef(0);\n const yawVel = useRef(0);\n\n const MAX_SCANS = 8;\n const scanStartsRef = useRef([]);\n\n const pushScan = (t: number) => {\n const arr = scanStartsRef.current.slice();\n if (arr.length >= MAX_SCANS) arr.shift();\n arr.push(t);\n scanStartsRef.current = arr;\n if (materialRef.current) {\n const u = materialRef.current.uniforms;\n const buf = new Array(MAX_SCANS).fill(0);\n for (let i = 0; i < arr.length && i < MAX_SCANS; i++) buf[i] = arr[i];\n u.uScanStarts.value = buf;\n u.uScanCount.value = arr.length;\n }\n };\n\n const bufX = useRef([]);\n const bufY = useRef([]);\n const bufT = useRef([]);\n const bufYaw = useRef([]);\n\n const s = THREE.MathUtils.clamp(sensitivity, 0, 1);\n const skewScale = THREE.MathUtils.lerp(0.06, 0.2, s);\n const tiltScale = THREE.MathUtils.lerp(0.12, 0.3, s);\n const yawScale = THREE.MathUtils.lerp(0.1, 0.28, s);\n const depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s);\n const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s);\n const maxSpeed = Infinity;\n\n const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n let leaveTimer: number | null = null;\n const onMove = (e: MouseEvent) => {\n if (uiFaceActive) return;\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n const rect = el.getBoundingClientRect();\n const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n lookTarget.current.set(nx, ny);\n };\n const onClick = async () => {\n const nowSec = performance.now() / 1000;\n if (scanOnClick) pushScan(nowSec);\n if (\n enableGyro &&\n typeof window !== 'undefined' &&\n (window as any).DeviceOrientationEvent &&\n (DeviceOrientationEvent as any).requestPermission\n ) {\n try {\n await (DeviceOrientationEvent as any).requestPermission();\n } catch {}\n }\n };\n const onEnter = () => {\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n };\n const onLeave = () => {\n if (uiFaceActive) return;\n if (leaveTimer) clearTimeout(leaveTimer);\n leaveTimer = window.setTimeout(\n () => {\n lookTarget.current.set(0, 0);\n tiltTarget.current = 0;\n yawTarget.current = 0;\n },\n Math.max(0, snapBackDelay || 0)\n );\n };\n el.addEventListener('mousemove', onMove);\n el.addEventListener('mouseenter', onEnter);\n if (scanOnClick) el.addEventListener('click', onClick);\n el.addEventListener('mouseleave', onLeave);\n return () => {\n el.removeEventListener('mousemove', onMove);\n el.removeEventListener('mouseenter', onEnter);\n el.removeEventListener('mouseleave', onLeave);\n if (scanOnClick) el.removeEventListener('click', onClick);\n if (leaveTimer) clearTimeout(leaveTimer);\n };\n }, [uiFaceActive, snapBackDelay, scanOnClick, enableGyro]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n rendererRef.current = renderer;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setSize(container.clientWidth, container.clientHeight);\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.toneMapping = THREE.NoToneMapping;\n renderer.autoClear = false;\n renderer.setClearColor(0x000000, 0);\n container.appendChild(renderer.domElement);\n\n const uniforms = {\n iResolution: {\n value: new THREE.Vector3(container.clientWidth, container.clientHeight, renderer.getPixelRatio())\n },\n iTime: { value: 0 },\n uSkew: { value: new THREE.Vector2(0, 0) },\n uTilt: { value: 0 },\n uYaw: { value: 0 },\n uLineThickness: { value: lineThickness },\n uLinesColor: { value: srgbColor(linesColor) },\n uScanColor: { value: srgbColor(scanColor) },\n uGridScale: { value: gridScale },\n uLineStyle: { value: lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0 },\n uLineJitter: { value: Math.max(0, Math.min(1, lineJitter || 0)) },\n uScanOpacity: { value: scanOpacity },\n uNoise: { value: noiseIntensity },\n uBloomOpacity: { value: bloomIntensity },\n uScanGlow: { value: scanGlow },\n uScanSoftness: { value: scanSoftness },\n uPhaseTaper: { value: scanPhaseTaper },\n uScanDuration: { value: scanDuration },\n uScanDelay: { value: scanDelay },\n uScanDirection: { value: scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0 },\n uScanStarts: { value: new Array(MAX_SCANS).fill(0) },\n uScanCount: { value: 0 }\n };\n\n const material = new THREE.ShaderMaterial({\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.current = material;\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);\n scene.add(quad);\n\n let composer: EffectComposer | null = null;\n if (enablePost) {\n composer = new EffectComposer(renderer);\n composerRef.current = composer;\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloom = new BloomEffect({\n intensity: 1.0,\n luminanceThreshold: bloomThreshold,\n luminanceSmoothing: bloomSmoothing\n });\n bloom.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current = bloom;\n\n const chroma = new ChromaticAberrationEffect({\n offset: new THREE.Vector2(chromaticAberration, chromaticAberration),\n radialModulation: true,\n modulationOffset: 0.0\n });\n chromaRef.current = chroma;\n\n const effectPass = new EffectPass(camera, bloom, chroma);\n effectPass.renderToScreen = true;\n composer.addPass(effectPass);\n }\n\n const onResize = () => {\n renderer.setSize(container.clientWidth, container.clientHeight);\n material.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio());\n if (composerRef.current) composerRef.current.setSize(container.clientWidth, container.clientHeight);\n };\n window.addEventListener('resize', onResize);\n\n let last = performance.now();\n const tick = () => {\n const now = performance.now();\n const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));\n last = now;\n\n lookCurrent.current.copy(\n smoothDampVec2(lookCurrent.current, lookTarget.current, lookVel.current, smoothTime, maxSpeed, dt)\n );\n\n const tiltSm = smoothDampFloat(\n tiltCurrent.current,\n tiltTarget.current,\n { v: tiltVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n tiltCurrent.current = tiltSm.value;\n tiltVel.current = tiltSm.v;\n\n const yawSm = smoothDampFloat(\n yawCurrent.current,\n yawTarget.current,\n { v: yawVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n yawCurrent.current = yawSm.value;\n yawVel.current = yawSm.v;\n\n const skew = new THREE.Vector2(lookCurrent.current.x * skewScale, -lookCurrent.current.y * yBoost * skewScale);\n material.uniforms.uSkew.value.set(skew.x, skew.y);\n material.uniforms.uTilt.value = tiltCurrent.current * tiltScale;\n material.uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent.current * yawScale, -0.6, 0.6);\n\n material.uniforms.iTime.value = now / 1000;\n renderer.clear(true, true, true);\n if (composerRef.current) {\n composerRef.current.render(dt);\n } else {\n renderer.render(scene, camera);\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n rafRef.current = requestAnimationFrame(tick);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n window.removeEventListener('resize', onResize);\n material.dispose();\n (quad.geometry as THREE.BufferGeometry).dispose();\n if (composerRef.current) {\n composerRef.current.dispose();\n composerRef.current = null;\n }\n renderer.dispose();\n renderer.forceContextLoss();\n container.removeChild(renderer.domElement);\n };\n }, [\n sensitivity,\n lineThickness,\n linesColor,\n scanColor,\n scanOpacity,\n gridScale,\n lineStyle,\n lineJitter,\n scanDirection,\n enablePost\n ]);\n\n useEffect(() => {\n const m = materialRef.current;\n if (m) {\n const u = m.uniforms;\n u.uLineThickness.value = lineThickness;\n (u.uLinesColor.value as THREE.Color).copy(srgbColor(linesColor));\n (u.uScanColor.value as THREE.Color).copy(srgbColor(scanColor));\n u.uGridScale.value = gridScale;\n u.uLineStyle.value = lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0;\n u.uLineJitter.value = Math.max(0, Math.min(1, lineJitter || 0));\n u.uBloomOpacity.value = Math.max(0, bloomIntensity);\n u.uNoise.value = Math.max(0, noiseIntensity);\n u.uScanGlow.value = scanGlow;\n u.uScanOpacity.value = Math.max(0, Math.min(1, scanOpacity));\n u.uScanDirection.value = scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0;\n u.uScanSoftness.value = scanSoftness;\n u.uPhaseTaper.value = scanPhaseTaper;\n u.uScanDuration.value = Math.max(0.05, scanDuration);\n u.uScanDelay.value = Math.max(0.0, scanDelay);\n }\n if (bloomRef.current) {\n bloomRef.current.blendMode.opacity.value = Math.max(0, bloomIntensity);\n (bloomRef.current as any).luminanceMaterial.threshold = bloomThreshold;\n (bloomRef.current as any).luminanceMaterial.smoothing = bloomSmoothing;\n }\n if (chromaRef.current) {\n chromaRef.current.offset.set(chromaticAberration, chromaticAberration);\n }\n }, [\n lineThickness,\n linesColor,\n scanColor,\n gridScale,\n lineStyle,\n lineJitter,\n bloomIntensity,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n noiseIntensity,\n scanGlow,\n scanOpacity,\n scanDirection,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay\n ]);\n\n useEffect(() => {\n if (!enableGyro) return;\n const handler = (e: DeviceOrientationEvent) => {\n if (uiFaceActive) return;\n const gamma = e.gamma ?? 0;\n const beta = e.beta ?? 0;\n const nx = THREE.MathUtils.clamp(gamma / 45, -1, 1);\n const ny = THREE.MathUtils.clamp(-beta / 30, -1, 1);\n lookTarget.current.set(nx, ny);\n tiltTarget.current = THREE.MathUtils.degToRad(gamma) * 0.4;\n };\n window.addEventListener('deviceorientation', handler);\n return () => {\n window.removeEventListener('deviceorientation', handler);\n };\n }, [enableGyro, uiFaceActive]);\n\n useEffect(() => {\n let canceled = false;\n const load = async () => {\n try {\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath),\n faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelsPath)\n ]);\n if (!canceled) setModelsReady(true);\n } catch {\n if (!canceled) setModelsReady(false);\n }\n };\n load();\n return () => {\n canceled = true;\n };\n }, [modelsPath]);\n\n useEffect(() => {\n let stop = false;\n let lastDetect = 0;\n\n const start = async () => {\n if (!enableWebcam || !modelsReady) return;\n const video = videoRef.current;\n if (!video) return;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } },\n audio: false\n });\n video.srcObject = stream;\n await video.play();\n } catch {\n return;\n }\n\n const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });\n\n const detect = async (ts: number) => {\n if (stop) return;\n\n if (ts - lastDetect >= 33) {\n lastDetect = ts;\n try {\n const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true);\n if (res && res.detection) {\n const det = res.detection;\n const box = det.box;\n const vw = video.videoWidth || 1;\n const vh = video.videoHeight || 1;\n\n const cx = box.x + box.width * 0.5;\n const cy = box.y + box.height * 0.5;\n const nx = (cx / vw) * 2 - 1;\n const ny = (cy / vh) * 2 - 1;\n medianPush(bufX.current, nx, 5);\n medianPush(bufY.current, ny, 5);\n const nxm = median(bufX.current);\n const nym = median(bufY.current);\n\n const look = new THREE.Vector2(Math.tanh(nxm), Math.tanh(nym));\n\n const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh));\n const depthScale = 1 + depthResponse * (faceSize - 0.25);\n lookTarget.current.copy(look.multiplyScalar(depthScale));\n\n const leftEye = res.landmarks.getLeftEye();\n const rightEye = res.landmarks.getRightEye();\n const lc = centroid(leftEye);\n const rc = centroid(rightEye);\n const tilt = Math.atan2(rc.y - lc.y, rc.x - lc.x);\n medianPush(bufT.current, tilt, 5);\n tiltTarget.current = median(bufT.current);\n\n const nose = res.landmarks.getNose();\n const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)];\n const jaw = res.landmarks.getJawOutline();\n const leftCheek = jaw[3] || jaw[2];\n const rightCheek = jaw[13] || jaw[14];\n const dL = dist2(tip, leftCheek);\n const dR = dist2(tip, rightCheek);\n const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6;\n let yawSignal = THREE.MathUtils.clamp((dR - dL) / (eyeDist * 1.6), -1, 1);\n yawSignal = Math.tanh(yawSignal);\n medianPush(bufYaw.current, yawSignal, 5);\n yawTarget.current = median(bufYaw.current);\n\n setUiFaceActive(true);\n } else {\n setUiFaceActive(false);\n }\n } catch {\n setUiFaceActive(false);\n }\n }\n\n if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {\n (video as any).requestVideoFrameCallback(() => detect(performance.now()));\n } else {\n requestAnimationFrame(detect);\n }\n };\n\n requestAnimationFrame(detect);\n };\n\n start();\n\n return () => {\n stop = true;\n const video = videoRef.current;\n if (video) {\n const stream = video.srcObject as MediaStream | null;\n if (stream) stream.getTracks().forEach(t => t.stop());\n video.pause();\n video.srcObject = null;\n }\n };\n }, [enableWebcam, modelsReady, depthResponse]);\n\n return (\n \n {showPreview && (\n
\n
\n
\n {enableWebcam\n ? modelsReady\n ? uiFaceActive\n ? 'Face: tracking'\n : 'Face: searching'\n : 'Loading models'\n : 'Webcam disabled'}\n
\n
\n )}\n
\n );\n};\n\nfunction srgbColor(hex: string) {\n const c = new THREE.Color(hex);\n return c.convertSRGBToLinear();\n}\n\nfunction smoothDampVec2(\n current: THREE.Vector2,\n target: THREE.Vector2,\n currentVelocity: THREE.Vector2,\n smoothTime: number,\n maxSpeed: number,\n deltaTime: number\n): THREE.Vector2 {\n const out = current.clone();\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current.clone().sub(target);\n const originalTo = target.clone();\n\n const maxChange = maxSpeed * smoothTime;\n if (change.length() > maxChange) change.setLength(maxChange);\n\n target = current.clone().sub(change);\n const temp = currentVelocity.clone().addScaledVector(change, omega).multiplyScalar(deltaTime);\n currentVelocity.sub(temp.clone().multiplyScalar(omega));\n currentVelocity.multiplyScalar(exp);\n\n out.copy(target.clone().add(change.add(temp).multiplyScalar(exp)));\n\n const origMinusCurrent = originalTo.clone().sub(current);\n const outMinusOrig = out.clone().sub(originalTo);\n if (origMinusCurrent.dot(outMinusOrig) > 0) {\n out.copy(originalTo);\n currentVelocity.set(0, 0);\n }\n return out;\n}\n\nfunction smoothDampFloat(\n current: number,\n target: number,\n velRef: { v: number },\n smoothTime: number,\n maxSpeed: number,\n deltaTime: number\n): { value: number; v: number } {\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current - target;\n const originalTo = target;\n\n const maxChange = maxSpeed * smoothTime;\n change = Math.sign(change) * Math.min(Math.abs(change), maxChange);\n\n target = current - change;\n const temp = (velRef.v + omega * change) * deltaTime;\n velRef.v = (velRef.v - omega * temp) * exp;\n\n let out = target + (change + temp) * exp;\n\n const origMinusCurrent = originalTo - current;\n const outMinusOrig = out - originalTo;\n if (origMinusCurrent * outMinusOrig > 0) {\n out = originalTo;\n velRef.v = 0;\n }\n return { value: out, v: velRef.v };\n}\n\nfunction medianPush(buf: number[], v: number, maxLen: number) {\n buf.push(v);\n if (buf.length > maxLen) buf.shift();\n}\n\nfunction median(buf: number[]) {\n if (buf.length === 0) return 0;\n const a = [...buf].sort((x, y) => x - y);\n const mid = Math.floor(a.length / 2);\n return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) * 0.5;\n}\n\nfunction centroid(points: { x: number; y: number }[]) {\n let x = 0,\n y = 0;\n const n = points.length || 1;\n for (const p of points) {\n x += p.x;\n y += p.y;\n }\n return { x: x / n, y: y / n };\n}\n\nfunction dist2(a: { x: number; y: number }, b: { x: number; y: number }) {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n"
}
],
"registryDependencies": [],
"dependencies": [
+ "face-api.js@^0.22.2",
"postprocessing@^6.36.0",
- "three@^0.167.1",
- "face-api.js@^0.22.2"
+ "three@^0.167.1"
]
}
\ No newline at end of file
diff --git a/public/r/GridScan-TS-TW.json b/public/r/GridScan-TS-TW.json
index 666dfca4..61539c0b 100644
--- a/public/r/GridScan-TS-TW.json
+++ b/public/r/GridScan-TS-TW.json
@@ -8,13 +8,13 @@
{
"type": "registry:component",
"path": "GridScan/GridScan.tsx",
- "content": "import React, { useEffect, useRef, useState } from 'react';\nimport { EffectComposer, RenderPass, EffectPass, BloomEffect, ChromaticAberrationEffect } from 'postprocessing';\nimport * as THREE from 'three';\nimport * as faceapi from 'face-api.js';\n\ntype GridScanProps = {\n enableWebcam?: boolean;\n showPreview?: boolean;\n modelsPath?: string;\n sensitivity?: number;\n\n lineThickness?: number;\n linesColor?: string;\n\n gridScale?: number;\n lineStyle?: 'solid' | 'dashed' | 'dotted';\n lineJitter?: number;\n\n enablePost?: boolean;\n bloomIntensity?: number;\n bloomThreshold?: number;\n bloomSmoothing?: number;\n chromaticAberration?: number;\n noiseIntensity?: number;\n\n scanColor?: string;\n scanOpacity?: number;\n scanDirection?: 'forward' | 'backward' | 'pingpong';\n scanSoftness?: number;\n scanGlow?: number;\n scanPhaseTaper?: number;\n scanDuration?: number;\n scanDelay?: number;\n enableGyro?: boolean;\n scanOnClick?: boolean;\n snapBackDelay?: number;\n className?: string;\n style?: React.CSSProperties;\n};\n\nconst vert = `\nvarying vec2 vUv;\nvoid main(){\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`;\n\nconst frag = `\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec2 uSkew;\nuniform float uTilt;\nuniform float uYaw;\nuniform float uLineThickness;\nuniform vec3 uLinesColor;\nuniform vec3 uScanColor;\nuniform float uGridScale;\nuniform float uLineStyle;\nuniform float uLineJitter;\nuniform float uScanOpacity;\nuniform float uScanDirection;\nuniform float uNoise;\nuniform float uBloomOpacity;\nuniform float uScanGlow;\nuniform float uScanSoftness;\nuniform float uPhaseTaper;\nuniform float uScanDuration;\nuniform float uScanDelay;\nvarying vec2 vUv;\n\nuniform float uScanStarts[8];\nuniform float uScanCount;\n\nconst int MAX_SCANS = 8;\n\nfloat smoother01(float a, float b, float x){\n float t = clamp((x - a) / max(1e-5, (b - a)), 0.0, 1.0);\n return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord)\n{\n vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n\n vec3 ro = vec3(0.0);\n vec3 rd = normalize(vec3(p, 2.0));\n\n float cR = cos(uTilt), sR = sin(uTilt);\n rd.xy = mat2(cR, -sR, sR, cR) * rd.xy;\n\n float cY = cos(uYaw), sY = sin(uYaw);\n rd.xz = mat2(cY, -sY, sY, cY) * rd.xz;\n\n vec2 skew = clamp(uSkew, vec2(-0.7), vec2(0.7));\n rd.xy += skew * rd.z;\n\n vec3 color = vec3(0.0);\n float minT = 1e20;\n float gridScale = max(1e-5, uGridScale);\n float fadeStrength = 2.0;\n vec2 gridUV = vec2(0.0);\n\n float hitIsY = 1.0;\n for (int i = 0; i < 4; i++)\n {\n float isY = float(i < 2);\n float pos = mix(-0.2, 0.2, float(i)) * isY + mix(-0.5, 0.5, float(i - 2)) * (1.0 - isY);\n float num = pos - (isY * ro.y + (1.0 - isY) * ro.x);\n float den = isY * rd.y + (1.0 - isY) * rd.x;\n float t = num / den;\n vec3 h = ro + rd * t;\n\n float depthBoost = smoothstep(0.0, 3.0, h.z);\n h.xy += skew * 0.15 * depthBoost;\n\n bool use = t > 0.0 && t < minT;\n gridUV = use ? mix(h.zy, h.xz, isY) / gridScale : gridUV;\n minT = use ? t : minT;\n hitIsY = use ? isY : hitIsY;\n }\n\n vec3 hit = ro + rd * minT;\n float dist = length(hit - ro);\n\n float jitterAmt = clamp(uLineJitter, 0.0, 1.0);\n if (jitterAmt > 0.0) {\n vec2 j = vec2(\n sin(gridUV.y * 2.7 + iTime * 1.8),\n cos(gridUV.x * 2.3 - iTime * 1.6)\n ) * (0.15 * jitterAmt);\n gridUV += j;\n }\n float fx = fract(gridUV.x);\n float fy = fract(gridUV.y);\n float ax = min(fx, 1.0 - fx);\n float ay = min(fy, 1.0 - fy);\n float wx = fwidth(gridUV.x);\n float wy = fwidth(gridUV.y);\n float halfPx = max(0.0, uLineThickness) * 0.5;\n\n float tx = halfPx * wx;\n float ty = halfPx * wy;\n\n float aax = wx;\n float aay = wy;\n\n float lineX = 1.0 - smoothstep(tx, tx + aax, ax);\n float lineY = 1.0 - smoothstep(ty, ty + aay, ay);\n if (uLineStyle > 0.5) {\n float dashRepeat = 4.0;\n float dashDuty = 0.5;\n float vy = fract(gridUV.y * dashRepeat);\n float vx = fract(gridUV.x * dashRepeat);\n float dashMaskY = step(vy, dashDuty);\n float dashMaskX = step(vx, dashDuty);\n if (uLineStyle < 1.5) {\n lineX *= dashMaskY;\n lineY *= dashMaskX;\n } else {\n float dotRepeat = 6.0;\n float dotWidth = 0.18;\n float cy = abs(fract(gridUV.y * dotRepeat) - 0.5);\n float cx = abs(fract(gridUV.x * dotRepeat) - 0.5);\n float dotMaskY = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.y * dotRepeat), cy);\n float dotMaskX = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.x * dotRepeat), cx);\n lineX *= dotMaskY;\n lineY *= dotMaskX;\n }\n }\n float primaryMask = max(lineX, lineY);\n\n vec2 gridUV2 = (hitIsY > 0.5 ? hit.xz : hit.zy) / gridScale;\n if (jitterAmt > 0.0) {\n vec2 j2 = vec2(\n cos(gridUV2.y * 2.1 - iTime * 1.4),\n sin(gridUV2.x * 2.5 + iTime * 1.7)\n ) * (0.15 * jitterAmt);\n gridUV2 += j2;\n }\n float fx2 = fract(gridUV2.x);\n float fy2 = fract(gridUV2.y);\n float ax2 = min(fx2, 1.0 - fx2);\n float ay2 = min(fy2, 1.0 - fy2);\n float wx2 = fwidth(gridUV2.x);\n float wy2 = fwidth(gridUV2.y);\n float tx2 = halfPx * wx2;\n float ty2 = halfPx * wy2;\n float aax2 = wx2;\n float aay2 = wy2;\n float lineX2 = 1.0 - smoothstep(tx2, tx2 + aax2, ax2);\n float lineY2 = 1.0 - smoothstep(ty2, ty2 + aay2, ay2);\n if (uLineStyle > 0.5) {\n float dashRepeat2 = 4.0;\n float dashDuty2 = 0.5;\n float vy2m = fract(gridUV2.y * dashRepeat2);\n float vx2m = fract(gridUV2.x * dashRepeat2);\n float dashMaskY2 = step(vy2m, dashDuty2);\n float dashMaskX2 = step(vx2m, dashDuty2);\n if (uLineStyle < 1.5) {\n lineX2 *= dashMaskY2;\n lineY2 *= dashMaskX2;\n } else {\n float dotRepeat2 = 6.0;\n float dotWidth2 = 0.18;\n float cy2 = abs(fract(gridUV2.y * dotRepeat2) - 0.5);\n float cx2 = abs(fract(gridUV2.x * dotRepeat2) - 0.5);\n float dotMaskY2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.y * dotRepeat2), cy2);\n float dotMaskX2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.x * dotRepeat2), cx2);\n lineX2 *= dotMaskY2;\n lineY2 *= dotMaskX2;\n }\n }\n float altMask = max(lineX2, lineY2);\n\n float edgeDistX = min(abs(hit.x - (-0.5)), abs(hit.x - 0.5));\n float edgeDistY = min(abs(hit.y - (-0.2)), abs(hit.y - 0.2));\n float edgeDist = mix(edgeDistY, edgeDistX, hitIsY);\n float edgeGate = 1.0 - smoothstep(gridScale * 0.5, gridScale * 2.0, edgeDist);\n altMask *= edgeGate;\n\n float lineMask = max(primaryMask, altMask);\n\n float fade = exp(-dist * fadeStrength);\n\n float dur = max(0.05, uScanDuration);\n float del = max(0.0, uScanDelay);\n float scanZMax = 2.0;\n float widthScale = max(0.1, uScanGlow);\n float sigma = max(0.001, 0.18 * widthScale * uScanSoftness);\n float sigmaA = sigma * 2.0;\n\n float combinedPulse = 0.0;\n float combinedAura = 0.0;\n\n float cycle = dur + del;\n float tCycle = mod(iTime, cycle);\n float scanPhase = clamp((tCycle - del) / dur, 0.0, 1.0);\n float phase = scanPhase;\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phase = 1.0 - phase;\n } else if (uScanDirection > 1.5) {\n float t2 = mod(max(0.0, iTime - del), 2.0 * dur);\n phase = (t2 < dur) ? (t2 / dur) : (1.0 - (t2 - dur) / dur);\n }\n float scanZ = phase * scanZMax;\n float dz = abs(hit.z - scanZ);\n float lineBand = exp(-0.5 * (dz * dz) / (sigma * sigma));\n float taper = clamp(uPhaseTaper, 0.0, 0.49);\n float headW = taper;\n float tailW = taper;\n float headFade = smoother01(0.0, headW, phase);\n float tailFade = 1.0 - smoother01(1.0 - tailW, 1.0, phase);\n float phaseWindow = headFade * tailFade;\n float pulseBase = lineBand * phaseWindow;\n combinedPulse += pulseBase * clamp(uScanOpacity, 0.0, 1.0);\n float auraBand = exp(-0.5 * (dz * dz) / (sigmaA * sigmaA));\n combinedAura += (auraBand * 0.25) * phaseWindow * clamp(uScanOpacity, 0.0, 1.0);\n\n for (int i = 0; i < MAX_SCANS; i++) {\n if (float(i) >= uScanCount) break;\n float tActiveI = iTime - uScanStarts[i];\n float phaseI = clamp(tActiveI / dur, 0.0, 1.0);\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phaseI = 1.0 - phaseI;\n } else if (uScanDirection > 1.5) {\n phaseI = (phaseI < 0.5) ? (phaseI * 2.0) : (1.0 - (phaseI - 0.5) * 2.0);\n }\n float scanZI = phaseI * scanZMax;\n float dzI = abs(hit.z - scanZI);\n float lineBandI = exp(-0.5 * (dzI * dzI) / (sigma * sigma));\n float headFadeI = smoother01(0.0, headW, phaseI);\n float tailFadeI = 1.0 - smoother01(1.0 - tailW, 1.0, phaseI);\n float phaseWindowI = headFadeI * tailFadeI;\n combinedPulse += lineBandI * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n float auraBandI = exp(-0.5 * (dzI * dzI) / (sigmaA * sigmaA));\n combinedAura += (auraBandI * 0.25) * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n }\n\n float lineVis = lineMask;\n vec3 gridCol = uLinesColor * lineVis * fade;\n vec3 scanCol = uScanColor * combinedPulse;\n vec3 scanAura = uScanColor * combinedAura;\n\n color = gridCol + scanCol + scanAura;\n\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(iTime * 123.4), vec2(12.9898,78.233))) * 43758.5453123);\n color += (n - 0.5) * uNoise;\n color = clamp(color, 0.0, 1.0);\n float alpha = clamp(max(lineVis, combinedPulse), 0.0, 1.0);\n float gx = 1.0 - smoothstep(tx * 2.0, tx * 2.0 + aax * 2.0, ax);\n float gy = 1.0 - smoothstep(ty * 2.0, ty * 2.0 + aay * 2.0, ay);\n float halo = max(gx, gy) * fade;\n alpha = max(alpha, halo * clamp(uBloomOpacity, 0.0, 1.0));\n fragColor = vec4(color, alpha);\n}\n\nvoid main(){\n vec4 c;\n mainImage(c, vUv * iResolution.xy);\n gl_FragColor = c;\n}\n`;\n\nexport const GridScan: React.FC = ({\n enableWebcam = false,\n showPreview = false,\n modelsPath = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights',\n sensitivity = 0.55,\n lineThickness = 1,\n linesColor = '#392e4e',\n scanColor = '#FF9FFC',\n scanOpacity = 0.4,\n gridScale = 0.1,\n lineStyle = 'solid',\n lineJitter = 0.1,\n scanDirection = 'pingpong',\n enablePost = true,\n bloomIntensity = 0,\n bloomThreshold = 0,\n bloomSmoothing = 0,\n chromaticAberration = 0.002,\n noiseIntensity = 0.01,\n scanGlow = 0.5,\n scanSoftness = 2,\n scanPhaseTaper = 0.9,\n scanDuration = 2.0,\n scanDelay = 2.0,\n enableGyro = false,\n scanOnClick = false,\n snapBackDelay = 250,\n className,\n style\n}) => {\n const containerRef = useRef(null);\n const videoRef = useRef(null);\n\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const composerRef = useRef(null);\n const bloomRef = useRef(null);\n const chromaRef = useRef(null);\n const rafRef = useRef(null);\n\n const [modelsReady, setModelsReady] = useState(false);\n const [uiFaceActive, setUiFaceActive] = useState(false);\n\n const lookTarget = useRef(new THREE.Vector2(0, 0));\n const tiltTarget = useRef(0);\n const yawTarget = useRef(0);\n\n const lookCurrent = useRef(new THREE.Vector2(0, 0));\n const lookVel = useRef(new THREE.Vector2(0, 0));\n const tiltCurrent = useRef(0);\n const tiltVel = useRef(0);\n const yawCurrent = useRef(0);\n const yawVel = useRef(0);\n\n const MAX_SCANS = 8;\n const scanStartsRef = useRef([]);\n\n const pushScan = (t: number) => {\n const arr = scanStartsRef.current.slice();\n if (arr.length >= MAX_SCANS) arr.shift();\n arr.push(t);\n scanStartsRef.current = arr;\n if (materialRef.current) {\n const u = materialRef.current.uniforms;\n const buf = new Array(MAX_SCANS).fill(0);\n for (let i = 0; i < arr.length && i < MAX_SCANS; i++) buf[i] = arr[i];\n u.uScanStarts.value = buf;\n u.uScanCount.value = arr.length;\n }\n };\n\n const bufX = useRef([]);\n const bufY = useRef([]);\n const bufT = useRef([]);\n const bufYaw = useRef([]);\n\n const s = THREE.MathUtils.clamp(sensitivity, 0, 1);\n const skewScale = THREE.MathUtils.lerp(0.06, 0.2, s);\n const tiltScale = THREE.MathUtils.lerp(0.12, 0.3, s);\n const yawScale = THREE.MathUtils.lerp(0.1, 0.28, s);\n const depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s);\n const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s);\n const maxSpeed = Infinity;\n\n const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n let leaveTimer: number | null = null;\n const onMove = (e: MouseEvent) => {\n if (uiFaceActive) return;\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n const rect = el.getBoundingClientRect();\n const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n lookTarget.current.set(nx, ny);\n };\n const onClick = async () => {\n const nowSec = performance.now() / 1000;\n if (scanOnClick) pushScan(nowSec);\n if (\n enableGyro &&\n typeof window !== 'undefined' &&\n (window as any).DeviceOrientationEvent &&\n (DeviceOrientationEvent as any).requestPermission\n ) {\n try {\n await (DeviceOrientationEvent as any).requestPermission();\n } catch {}\n }\n };\n const onEnter = () => {\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n };\n const onLeave = () => {\n if (uiFaceActive) return;\n if (leaveTimer) clearTimeout(leaveTimer);\n leaveTimer = window.setTimeout(\n () => {\n lookTarget.current.set(0, 0);\n tiltTarget.current = 0;\n yawTarget.current = 0;\n },\n Math.max(0, snapBackDelay || 0)\n );\n };\n el.addEventListener('mousemove', onMove);\n el.addEventListener('mouseenter', onEnter);\n if (scanOnClick) el.addEventListener('click', onClick);\n el.addEventListener('mouseleave', onLeave);\n return () => {\n el.removeEventListener('mousemove', onMove);\n el.removeEventListener('mouseenter', onEnter);\n el.removeEventListener('mouseleave', onLeave);\n if (scanOnClick) el.removeEventListener('click', onClick);\n if (leaveTimer) clearTimeout(leaveTimer);\n };\n }, [uiFaceActive, snapBackDelay, scanOnClick, enableGyro]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n rendererRef.current = renderer;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setSize(container.clientWidth, container.clientHeight);\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.toneMapping = THREE.NoToneMapping;\n renderer.autoClear = false;\n renderer.setClearColor(0x000000, 0);\n container.appendChild(renderer.domElement);\n\n const uniforms = {\n iResolution: {\n value: new THREE.Vector3(container.clientWidth, container.clientHeight, renderer.getPixelRatio())\n },\n iTime: { value: 0 },\n uSkew: { value: new THREE.Vector2(0, 0) },\n uTilt: { value: 0 },\n uYaw: { value: 0 },\n uLineThickness: { value: lineThickness },\n uLinesColor: { value: srgbColor(linesColor) },\n uScanColor: { value: srgbColor(scanColor) },\n uGridScale: { value: gridScale },\n uLineStyle: { value: lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0 },\n uLineJitter: { value: Math.max(0, Math.min(1, lineJitter || 0)) },\n uScanOpacity: { value: scanOpacity },\n uNoise: { value: noiseIntensity },\n uBloomOpacity: { value: bloomIntensity },\n uScanGlow: { value: scanGlow },\n uScanSoftness: { value: scanSoftness },\n uPhaseTaper: { value: scanPhaseTaper },\n uScanDuration: { value: scanDuration },\n uScanDelay: { value: scanDelay },\n uScanDirection: { value: scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0 },\n uScanStarts: { value: new Array(MAX_SCANS).fill(0) },\n uScanCount: { value: 0 }\n };\n\n const material = new THREE.ShaderMaterial({\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.current = material;\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);\n scene.add(quad);\n\n let composer: EffectComposer | null = null;\n if (enablePost) {\n composer = new EffectComposer(renderer);\n composerRef.current = composer;\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloom = new BloomEffect({\n intensity: 1.0,\n luminanceThreshold: bloomThreshold,\n luminanceSmoothing: bloomSmoothing\n });\n bloom.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current = bloom;\n\n const chroma = new ChromaticAberrationEffect({\n offset: new THREE.Vector2(chromaticAberration, chromaticAberration),\n radialModulation: true,\n modulationOffset: 0.0\n });\n chromaRef.current = chroma;\n\n const effectPass = new EffectPass(camera, bloom, chroma);\n effectPass.renderToScreen = true;\n composer.addPass(effectPass);\n }\n\n const onResize = () => {\n renderer.setSize(container.clientWidth, container.clientHeight);\n material.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio());\n if (composerRef.current) composerRef.current.setSize(container.clientWidth, container.clientHeight);\n };\n window.addEventListener('resize', onResize);\n\n let last = performance.now();\n const tick = () => {\n const now = performance.now();\n const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));\n last = now;\n\n lookCurrent.current.copy(\n smoothDampVec2(lookCurrent.current, lookTarget.current, lookVel.current, smoothTime, maxSpeed, dt)\n );\n\n const tiltSm = smoothDampFloat(\n tiltCurrent.current,\n tiltTarget.current,\n { v: tiltVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n tiltCurrent.current = tiltSm.value;\n tiltVel.current = tiltSm.v;\n\n const yawSm = smoothDampFloat(\n yawCurrent.current,\n yawTarget.current,\n { v: yawVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n yawCurrent.current = yawSm.value;\n yawVel.current = yawSm.v;\n\n const skew = new THREE.Vector2(lookCurrent.current.x * skewScale, -lookCurrent.current.y * yBoost * skewScale);\n material.uniforms.uSkew.value.set(skew.x, skew.y);\n material.uniforms.uTilt.value = tiltCurrent.current * tiltScale;\n material.uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent.current * yawScale, -0.6, 0.6);\n\n material.uniforms.iTime.value = now / 1000;\n renderer.clear(true, true, true);\n if (composerRef.current) {\n composerRef.current.render(dt);\n } else {\n renderer.render(scene, camera);\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n rafRef.current = requestAnimationFrame(tick);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n window.removeEventListener('resize', onResize);\n material.dispose();\n (quad.geometry as THREE.BufferGeometry).dispose();\n if (composerRef.current) {\n composerRef.current.dispose();\n composerRef.current = null;\n }\n renderer.dispose();\n container.removeChild(renderer.domElement);\n };\n }, [\n sensitivity,\n lineThickness,\n linesColor,\n scanColor,\n scanOpacity,\n gridScale,\n lineStyle,\n lineJitter,\n scanDirection,\n enablePost\n ]);\n\n useEffect(() => {\n const m = materialRef.current;\n if (m) {\n const u = m.uniforms;\n u.uLineThickness.value = lineThickness;\n (u.uLinesColor.value as THREE.Color).copy(srgbColor(linesColor));\n (u.uScanColor.value as THREE.Color).copy(srgbColor(scanColor));\n u.uGridScale.value = gridScale;\n u.uLineStyle.value = lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0;\n u.uLineJitter.value = Math.max(0, Math.min(1, lineJitter || 0));\n u.uBloomOpacity.value = Math.max(0, bloomIntensity);\n u.uNoise.value = Math.max(0, noiseIntensity);\n u.uScanGlow.value = scanGlow;\n u.uScanOpacity.value = Math.max(0, Math.min(1, scanOpacity));\n u.uScanDirection.value = scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0;\n u.uScanSoftness.value = scanSoftness;\n u.uPhaseTaper.value = scanPhaseTaper;\n u.uScanDuration.value = Math.max(0.05, scanDuration);\n u.uScanDelay.value = Math.max(0.0, scanDelay);\n }\n if (bloomRef.current) {\n bloomRef.current.blendMode.opacity.value = Math.max(0, bloomIntensity);\n (bloomRef.current as any).luminanceMaterial.threshold = bloomThreshold;\n (bloomRef.current as any).luminanceMaterial.smoothing = bloomSmoothing;\n }\n if (chromaRef.current) {\n chromaRef.current.offset.set(chromaticAberration, chromaticAberration);\n }\n }, [\n lineThickness,\n linesColor,\n scanColor,\n gridScale,\n lineStyle,\n lineJitter,\n bloomIntensity,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n noiseIntensity,\n scanGlow,\n scanOpacity,\n scanDirection,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay\n ]);\n\n useEffect(() => {\n if (!enableGyro) return;\n const handler = (e: DeviceOrientationEvent) => {\n if (uiFaceActive) return;\n const gamma = e.gamma ?? 0;\n const beta = e.beta ?? 0;\n const nx = THREE.MathUtils.clamp(gamma / 45, -1, 1);\n const ny = THREE.MathUtils.clamp(-beta / 30, -1, 1);\n lookTarget.current.set(nx, ny);\n tiltTarget.current = THREE.MathUtils.degToRad(gamma) * 0.4;\n };\n window.addEventListener('deviceorientation', handler);\n return () => {\n window.removeEventListener('deviceorientation', handler);\n };\n }, [enableGyro, uiFaceActive]);\n\n useEffect(() => {\n let canceled = false;\n const load = async () => {\n try {\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath),\n faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelsPath)\n ]);\n if (!canceled) setModelsReady(true);\n } catch {\n if (!canceled) setModelsReady(false);\n }\n };\n load();\n return () => {\n canceled = true;\n };\n }, [modelsPath]);\n\n useEffect(() => {\n let stop = false;\n let lastDetect = 0;\n\n const start = async () => {\n if (!enableWebcam || !modelsReady) return;\n const video = videoRef.current;\n if (!video) return;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } },\n audio: false\n });\n video.srcObject = stream;\n await video.play();\n } catch {\n return;\n }\n\n const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });\n\n const detect = async (ts: number) => {\n if (stop) return;\n\n if (ts - lastDetect >= 33) {\n lastDetect = ts;\n try {\n const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true);\n if (res && res.detection) {\n const det = res.detection;\n const box = det.box;\n const vw = video.videoWidth || 1;\n const vh = video.videoHeight || 1;\n\n const cx = box.x + box.width * 0.5;\n const cy = box.y + box.height * 0.5;\n const nx = (cx / vw) * 2 - 1;\n const ny = (cy / vh) * 2 - 1;\n medianPush(bufX.current, nx, 5);\n medianPush(bufY.current, ny, 5);\n const nxm = median(bufX.current);\n const nym = median(bufY.current);\n\n const look = new THREE.Vector2(Math.tanh(nxm), Math.tanh(nym));\n\n const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh));\n const depthScale = 1 + depthResponse * (faceSize - 0.25);\n lookTarget.current.copy(look.multiplyScalar(depthScale));\n\n const leftEye = res.landmarks.getLeftEye();\n const rightEye = res.landmarks.getRightEye();\n const lc = centroid(leftEye);\n const rc = centroid(rightEye);\n const tilt = Math.atan2(rc.y - lc.y, rc.x - lc.x);\n medianPush(bufT.current, tilt, 5);\n tiltTarget.current = median(bufT.current);\n\n const nose = res.landmarks.getNose();\n const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)];\n const jaw = res.landmarks.getJawOutline();\n const leftCheek = jaw[3] || jaw[2];\n const rightCheek = jaw[13] || jaw[14];\n const dL = dist2(tip, leftCheek);\n const dR = dist2(tip, rightCheek);\n const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6;\n let yawSignal = THREE.MathUtils.clamp((dR - dL) / (eyeDist * 1.6), -1, 1);\n yawSignal = Math.tanh(yawSignal);\n medianPush(bufYaw.current, yawSignal, 5);\n yawTarget.current = median(bufYaw.current);\n\n setUiFaceActive(true);\n } else {\n setUiFaceActive(false);\n }\n } catch {\n setUiFaceActive(false);\n }\n }\n\n if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {\n (video as any).requestVideoFrameCallback(() => detect(performance.now()));\n } else {\n requestAnimationFrame(detect);\n }\n };\n\n requestAnimationFrame(detect);\n };\n\n start();\n\n return () => {\n stop = true;\n const video = videoRef.current;\n if (video) {\n const stream = video.srcObject as MediaStream | null;\n if (stream) stream.getTracks().forEach(t => t.stop());\n video.pause();\n video.srcObject = null;\n }\n };\n }, [enableWebcam, modelsReady, depthResponse]);\n\n return (\n \n {showPreview && (\n
\n
\n
\n {enableWebcam\n ? modelsReady\n ? uiFaceActive\n ? 'Face: tracking'\n : 'Face: searching'\n : 'Loading models'\n : 'Webcam disabled'}\n
\n
\n )}\n
\n );\n};\n\nfunction srgbColor(hex: string) {\n const c = new THREE.Color(hex);\n return c.convertSRGBToLinear();\n}\n\nfunction smoothDampVec2(\n current: THREE.Vector2,\n target: THREE.Vector2,\n currentVelocity: THREE.Vector2,\n smoothTime: number,\n maxSpeed: number,\n deltaTime: number\n): THREE.Vector2 {\n const out = current.clone();\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current.clone().sub(target);\n const originalTo = target.clone();\n\n const maxChange = maxSpeed * smoothTime;\n if (change.length() > maxChange) change.setLength(maxChange);\n\n target = current.clone().sub(change);\n const temp = currentVelocity.clone().addScaledVector(change, omega).multiplyScalar(deltaTime);\n currentVelocity.sub(temp.clone().multiplyScalar(omega));\n currentVelocity.multiplyScalar(exp);\n\n out.copy(target.clone().add(change.add(temp).multiplyScalar(exp)));\n\n const origMinusCurrent = originalTo.clone().sub(current);\n const outMinusOrig = out.clone().sub(originalTo);\n if (origMinusCurrent.dot(outMinusOrig) > 0) {\n out.copy(originalTo);\n currentVelocity.set(0, 0);\n }\n return out;\n}\n\nfunction smoothDampFloat(\n current: number,\n target: number,\n velRef: { v: number },\n smoothTime: number,\n maxSpeed: number,\n deltaTime: number\n): { value: number; v: number } {\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current - target;\n const originalTo = target;\n\n const maxChange = maxSpeed * smoothTime;\n change = Math.sign(change) * Math.min(Math.abs(change), maxChange);\n\n target = current - change;\n const temp = (velRef.v + omega * change) * deltaTime;\n velRef.v = (velRef.v - omega * temp) * exp;\n\n let out = target + (change + temp) * exp;\n\n const origMinusCurrent = originalTo - current;\n const outMinusOrig = out - originalTo;\n if (origMinusCurrent * outMinusOrig > 0) {\n out = originalTo;\n velRef.v = 0;\n }\n return { value: out, v: velRef.v };\n}\n\nfunction medianPush(buf: number[], v: number, maxLen: number) {\n buf.push(v);\n if (buf.length > maxLen) buf.shift();\n}\n\nfunction median(buf: number[]) {\n if (buf.length === 0) return 0;\n const a = [...buf].sort((x, y) => x - y);\n const mid = Math.floor(a.length / 2);\n return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) * 0.5;\n}\n\nfunction centroid(points: { x: number; y: number }[]) {\n let x = 0,\n y = 0;\n const n = points.length || 1;\n for (const p of points) {\n x += p.x;\n y += p.y;\n }\n return { x: x / n, y: y / n };\n}\n\nfunction dist2(a: { x: number; y: number }, b: { x: number; y: number }) {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n"
+ "content": "import * as faceapi from 'face-api.js';\nimport { BloomEffect, ChromaticAberrationEffect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';\nimport React, { useEffect, useRef, useState } from 'react';\nimport * as THREE from 'three';\n\ntype GridScanProps = {\n enableWebcam?: boolean;\n showPreview?: boolean;\n modelsPath?: string;\n sensitivity?: number;\n\n lineThickness?: number;\n linesColor?: string;\n\n gridScale?: number;\n lineStyle?: 'solid' | 'dashed' | 'dotted';\n lineJitter?: number;\n\n enablePost?: boolean;\n bloomIntensity?: number;\n bloomThreshold?: number;\n bloomSmoothing?: number;\n chromaticAberration?: number;\n noiseIntensity?: number;\n\n scanColor?: string;\n scanOpacity?: number;\n scanDirection?: 'forward' | 'backward' | 'pingpong';\n scanSoftness?: number;\n scanGlow?: number;\n scanPhaseTaper?: number;\n scanDuration?: number;\n scanDelay?: number;\n enableGyro?: boolean;\n scanOnClick?: boolean;\n snapBackDelay?: number;\n className?: string;\n style?: React.CSSProperties;\n};\n\nconst vert = `\nvarying vec2 vUv;\nvoid main(){\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`;\n\nconst frag = `\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec2 uSkew;\nuniform float uTilt;\nuniform float uYaw;\nuniform float uLineThickness;\nuniform vec3 uLinesColor;\nuniform vec3 uScanColor;\nuniform float uGridScale;\nuniform float uLineStyle;\nuniform float uLineJitter;\nuniform float uScanOpacity;\nuniform float uScanDirection;\nuniform float uNoise;\nuniform float uBloomOpacity;\nuniform float uScanGlow;\nuniform float uScanSoftness;\nuniform float uPhaseTaper;\nuniform float uScanDuration;\nuniform float uScanDelay;\nvarying vec2 vUv;\n\nuniform float uScanStarts[8];\nuniform float uScanCount;\n\nconst int MAX_SCANS = 8;\n\nfloat smoother01(float a, float b, float x){\n float t = clamp((x - a) / max(1e-5, (b - a)), 0.0, 1.0);\n return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord)\n{\n vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n\n vec3 ro = vec3(0.0);\n vec3 rd = normalize(vec3(p, 2.0));\n\n float cR = cos(uTilt), sR = sin(uTilt);\n rd.xy = mat2(cR, -sR, sR, cR) * rd.xy;\n\n float cY = cos(uYaw), sY = sin(uYaw);\n rd.xz = mat2(cY, -sY, sY, cY) * rd.xz;\n\n vec2 skew = clamp(uSkew, vec2(-0.7), vec2(0.7));\n rd.xy += skew * rd.z;\n\n vec3 color = vec3(0.0);\n float minT = 1e20;\n float gridScale = max(1e-5, uGridScale);\n float fadeStrength = 2.0;\n vec2 gridUV = vec2(0.0);\n\n float hitIsY = 1.0;\n for (int i = 0; i < 4; i++)\n {\n float isY = float(i < 2);\n float pos = mix(-0.2, 0.2, float(i)) * isY + mix(-0.5, 0.5, float(i - 2)) * (1.0 - isY);\n float num = pos - (isY * ro.y + (1.0 - isY) * ro.x);\n float den = isY * rd.y + (1.0 - isY) * rd.x;\n float t = num / den;\n vec3 h = ro + rd * t;\n\n float depthBoost = smoothstep(0.0, 3.0, h.z);\n h.xy += skew * 0.15 * depthBoost;\n\n bool use = t > 0.0 && t < minT;\n gridUV = use ? mix(h.zy, h.xz, isY) / gridScale : gridUV;\n minT = use ? t : minT;\n hitIsY = use ? isY : hitIsY;\n }\n\n vec3 hit = ro + rd * minT;\n float dist = length(hit - ro);\n\n float jitterAmt = clamp(uLineJitter, 0.0, 1.0);\n if (jitterAmt > 0.0) {\n vec2 j = vec2(\n sin(gridUV.y * 2.7 + iTime * 1.8),\n cos(gridUV.x * 2.3 - iTime * 1.6)\n ) * (0.15 * jitterAmt);\n gridUV += j;\n }\n float fx = fract(gridUV.x);\n float fy = fract(gridUV.y);\n float ax = min(fx, 1.0 - fx);\n float ay = min(fy, 1.0 - fy);\n float wx = fwidth(gridUV.x);\n float wy = fwidth(gridUV.y);\n float halfPx = max(0.0, uLineThickness) * 0.5;\n\n float tx = halfPx * wx;\n float ty = halfPx * wy;\n\n float aax = wx;\n float aay = wy;\n\n float lineX = 1.0 - smoothstep(tx, tx + aax, ax);\n float lineY = 1.0 - smoothstep(ty, ty + aay, ay);\n if (uLineStyle > 0.5) {\n float dashRepeat = 4.0;\n float dashDuty = 0.5;\n float vy = fract(gridUV.y * dashRepeat);\n float vx = fract(gridUV.x * dashRepeat);\n float dashMaskY = step(vy, dashDuty);\n float dashMaskX = step(vx, dashDuty);\n if (uLineStyle < 1.5) {\n lineX *= dashMaskY;\n lineY *= dashMaskX;\n } else {\n float dotRepeat = 6.0;\n float dotWidth = 0.18;\n float cy = abs(fract(gridUV.y * dotRepeat) - 0.5);\n float cx = abs(fract(gridUV.x * dotRepeat) - 0.5);\n float dotMaskY = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.y * dotRepeat), cy);\n float dotMaskX = 1.0 - smoothstep(dotWidth, dotWidth + fwidth(gridUV.x * dotRepeat), cx);\n lineX *= dotMaskY;\n lineY *= dotMaskX;\n }\n }\n float primaryMask = max(lineX, lineY);\n\n vec2 gridUV2 = (hitIsY > 0.5 ? hit.xz : hit.zy) / gridScale;\n if (jitterAmt > 0.0) {\n vec2 j2 = vec2(\n cos(gridUV2.y * 2.1 - iTime * 1.4),\n sin(gridUV2.x * 2.5 + iTime * 1.7)\n ) * (0.15 * jitterAmt);\n gridUV2 += j2;\n }\n float fx2 = fract(gridUV2.x);\n float fy2 = fract(gridUV2.y);\n float ax2 = min(fx2, 1.0 - fx2);\n float ay2 = min(fy2, 1.0 - fy2);\n float wx2 = fwidth(gridUV2.x);\n float wy2 = fwidth(gridUV2.y);\n float tx2 = halfPx * wx2;\n float ty2 = halfPx * wy2;\n float aax2 = wx2;\n float aay2 = wy2;\n float lineX2 = 1.0 - smoothstep(tx2, tx2 + aax2, ax2);\n float lineY2 = 1.0 - smoothstep(ty2, ty2 + aay2, ay2);\n if (uLineStyle > 0.5) {\n float dashRepeat2 = 4.0;\n float dashDuty2 = 0.5;\n float vy2m = fract(gridUV2.y * dashRepeat2);\n float vx2m = fract(gridUV2.x * dashRepeat2);\n float dashMaskY2 = step(vy2m, dashDuty2);\n float dashMaskX2 = step(vx2m, dashDuty2);\n if (uLineStyle < 1.5) {\n lineX2 *= dashMaskY2;\n lineY2 *= dashMaskX2;\n } else {\n float dotRepeat2 = 6.0;\n float dotWidth2 = 0.18;\n float cy2 = abs(fract(gridUV2.y * dotRepeat2) - 0.5);\n float cx2 = abs(fract(gridUV2.x * dotRepeat2) - 0.5);\n float dotMaskY2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.y * dotRepeat2), cy2);\n float dotMaskX2 = 1.0 - smoothstep(dotWidth2, dotWidth2 + fwidth(gridUV2.x * dotRepeat2), cx2);\n lineX2 *= dotMaskY2;\n lineY2 *= dotMaskX2;\n }\n }\n float altMask = max(lineX2, lineY2);\n\n float edgeDistX = min(abs(hit.x - (-0.5)), abs(hit.x - 0.5));\n float edgeDistY = min(abs(hit.y - (-0.2)), abs(hit.y - 0.2));\n float edgeDist = mix(edgeDistY, edgeDistX, hitIsY);\n float edgeGate = 1.0 - smoothstep(gridScale * 0.5, gridScale * 2.0, edgeDist);\n altMask *= edgeGate;\n\n float lineMask = max(primaryMask, altMask);\n\n float fade = exp(-dist * fadeStrength);\n\n float dur = max(0.05, uScanDuration);\n float del = max(0.0, uScanDelay);\n float scanZMax = 2.0;\n float widthScale = max(0.1, uScanGlow);\n float sigma = max(0.001, 0.18 * widthScale * uScanSoftness);\n float sigmaA = sigma * 2.0;\n\n float combinedPulse = 0.0;\n float combinedAura = 0.0;\n\n float cycle = dur + del;\n float tCycle = mod(iTime, cycle);\n float scanPhase = clamp((tCycle - del) / dur, 0.0, 1.0);\n float phase = scanPhase;\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phase = 1.0 - phase;\n } else if (uScanDirection > 1.5) {\n float t2 = mod(max(0.0, iTime - del), 2.0 * dur);\n phase = (t2 < dur) ? (t2 / dur) : (1.0 - (t2 - dur) / dur);\n }\n float scanZ = phase * scanZMax;\n float dz = abs(hit.z - scanZ);\n float lineBand = exp(-0.5 * (dz * dz) / (sigma * sigma));\n float taper = clamp(uPhaseTaper, 0.0, 0.49);\n float headW = taper;\n float tailW = taper;\n float headFade = smoother01(0.0, headW, phase);\n float tailFade = 1.0 - smoother01(1.0 - tailW, 1.0, phase);\n float phaseWindow = headFade * tailFade;\n float pulseBase = lineBand * phaseWindow;\n combinedPulse += pulseBase * clamp(uScanOpacity, 0.0, 1.0);\n float auraBand = exp(-0.5 * (dz * dz) / (sigmaA * sigmaA));\n combinedAura += (auraBand * 0.25) * phaseWindow * clamp(uScanOpacity, 0.0, 1.0);\n\n for (int i = 0; i < MAX_SCANS; i++) {\n if (float(i) >= uScanCount) break;\n float tActiveI = iTime - uScanStarts[i];\n float phaseI = clamp(tActiveI / dur, 0.0, 1.0);\n if (uScanDirection > 0.5 && uScanDirection < 1.5) {\n phaseI = 1.0 - phaseI;\n } else if (uScanDirection > 1.5) {\n phaseI = (phaseI < 0.5) ? (phaseI * 2.0) : (1.0 - (phaseI - 0.5) * 2.0);\n }\n float scanZI = phaseI * scanZMax;\n float dzI = abs(hit.z - scanZI);\n float lineBandI = exp(-0.5 * (dzI * dzI) / (sigma * sigma));\n float headFadeI = smoother01(0.0, headW, phaseI);\n float tailFadeI = 1.0 - smoother01(1.0 - tailW, 1.0, phaseI);\n float phaseWindowI = headFadeI * tailFadeI;\n combinedPulse += lineBandI * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n float auraBandI = exp(-0.5 * (dzI * dzI) / (sigmaA * sigmaA));\n combinedAura += (auraBandI * 0.25) * phaseWindowI * clamp(uScanOpacity, 0.0, 1.0);\n }\n\n float lineVis = lineMask;\n vec3 gridCol = uLinesColor * lineVis * fade;\n vec3 scanCol = uScanColor * combinedPulse;\n vec3 scanAura = uScanColor * combinedAura;\n\n color = gridCol + scanCol + scanAura;\n\n float n = fract(sin(dot(gl_FragCoord.xy + vec2(iTime * 123.4), vec2(12.9898,78.233))) * 43758.5453123);\n color += (n - 0.5) * uNoise;\n color = clamp(color, 0.0, 1.0);\n float alpha = clamp(max(lineVis, combinedPulse), 0.0, 1.0);\n float gx = 1.0 - smoothstep(tx * 2.0, tx * 2.0 + aax * 2.0, ax);\n float gy = 1.0 - smoothstep(ty * 2.0, ty * 2.0 + aay * 2.0, ay);\n float halo = max(gx, gy) * fade;\n alpha = max(alpha, halo * clamp(uBloomOpacity, 0.0, 1.0));\n fragColor = vec4(color, alpha);\n}\n\nvoid main(){\n vec4 c;\n mainImage(c, vUv * iResolution.xy);\n gl_FragColor = c;\n}\n`;\n\nexport const GridScan: React.FC = ({\n enableWebcam = false,\n showPreview = false,\n modelsPath = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights',\n sensitivity = 0.55,\n lineThickness = 1,\n linesColor = '#392e4e',\n scanColor = '#FF9FFC',\n scanOpacity = 0.4,\n gridScale = 0.1,\n lineStyle = 'solid',\n lineJitter = 0.1,\n scanDirection = 'pingpong',\n enablePost = true,\n bloomIntensity = 0,\n bloomThreshold = 0,\n bloomSmoothing = 0,\n chromaticAberration = 0.002,\n noiseIntensity = 0.01,\n scanGlow = 0.5,\n scanSoftness = 2,\n scanPhaseTaper = 0.9,\n scanDuration = 2.0,\n scanDelay = 2.0,\n enableGyro = false,\n scanOnClick = false,\n snapBackDelay = 250,\n className,\n style\n}) => {\n const containerRef = useRef(null);\n const videoRef = useRef(null);\n\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const composerRef = useRef(null);\n const bloomRef = useRef(null);\n const chromaRef = useRef(null);\n const rafRef = useRef(null);\n\n const [modelsReady, setModelsReady] = useState(false);\n const [uiFaceActive, setUiFaceActive] = useState(false);\n\n const lookTarget = useRef(new THREE.Vector2(0, 0));\n const tiltTarget = useRef(0);\n const yawTarget = useRef(0);\n\n const lookCurrent = useRef(new THREE.Vector2(0, 0));\n const lookVel = useRef(new THREE.Vector2(0, 0));\n const tiltCurrent = useRef(0);\n const tiltVel = useRef(0);\n const yawCurrent = useRef(0);\n const yawVel = useRef(0);\n\n const MAX_SCANS = 8;\n const scanStartsRef = useRef([]);\n\n const pushScan = (t: number) => {\n const arr = scanStartsRef.current.slice();\n if (arr.length >= MAX_SCANS) arr.shift();\n arr.push(t);\n scanStartsRef.current = arr;\n if (materialRef.current) {\n const u = materialRef.current.uniforms;\n const buf = new Array(MAX_SCANS).fill(0);\n for (let i = 0; i < arr.length && i < MAX_SCANS; i++) buf[i] = arr[i];\n u.uScanStarts.value = buf;\n u.uScanCount.value = arr.length;\n }\n };\n\n const bufX = useRef([]);\n const bufY = useRef([]);\n const bufT = useRef([]);\n const bufYaw = useRef([]);\n\n const s = THREE.MathUtils.clamp(sensitivity, 0, 1);\n const skewScale = THREE.MathUtils.lerp(0.06, 0.2, s);\n const tiltScale = THREE.MathUtils.lerp(0.12, 0.3, s);\n const yawScale = THREE.MathUtils.lerp(0.1, 0.28, s);\n const depthResponse = THREE.MathUtils.lerp(0.25, 0.45, s);\n const smoothTime = THREE.MathUtils.lerp(0.45, 0.12, s);\n const maxSpeed = Infinity;\n\n const yBoost = THREE.MathUtils.lerp(1.2, 1.6, s);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n let leaveTimer: number | null = null;\n const onMove = (e: MouseEvent) => {\n if (uiFaceActive) return;\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n const rect = el.getBoundingClientRect();\n const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n const ny = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n lookTarget.current.set(nx, ny);\n };\n const onClick = async () => {\n const nowSec = performance.now() / 1000;\n if (scanOnClick) pushScan(nowSec);\n if (\n enableGyro &&\n typeof window !== 'undefined' &&\n (window as any).DeviceOrientationEvent &&\n (DeviceOrientationEvent as any).requestPermission\n ) {\n try {\n await (DeviceOrientationEvent as any).requestPermission();\n } catch {}\n }\n };\n const onEnter = () => {\n if (leaveTimer) {\n clearTimeout(leaveTimer);\n leaveTimer = null;\n }\n };\n const onLeave = () => {\n if (uiFaceActive) return;\n if (leaveTimer) clearTimeout(leaveTimer);\n leaveTimer = window.setTimeout(\n () => {\n lookTarget.current.set(0, 0);\n tiltTarget.current = 0;\n yawTarget.current = 0;\n },\n Math.max(0, snapBackDelay || 0)\n );\n };\n el.addEventListener('mousemove', onMove);\n el.addEventListener('mouseenter', onEnter);\n if (scanOnClick) el.addEventListener('click', onClick);\n el.addEventListener('mouseleave', onLeave);\n return () => {\n el.removeEventListener('mousemove', onMove);\n el.removeEventListener('mouseenter', onEnter);\n el.removeEventListener('mouseleave', onLeave);\n if (scanOnClick) el.removeEventListener('click', onClick);\n if (leaveTimer) clearTimeout(leaveTimer);\n };\n }, [uiFaceActive, snapBackDelay, scanOnClick, enableGyro]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n rendererRef.current = renderer;\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.setSize(container.clientWidth, container.clientHeight);\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.toneMapping = THREE.NoToneMapping;\n renderer.autoClear = false;\n renderer.setClearColor(0x000000, 0);\n container.appendChild(renderer.domElement);\n\n const uniforms = {\n iResolution: {\n value: new THREE.Vector3(container.clientWidth, container.clientHeight, renderer.getPixelRatio())\n },\n iTime: { value: 0 },\n uSkew: { value: new THREE.Vector2(0, 0) },\n uTilt: { value: 0 },\n uYaw: { value: 0 },\n uLineThickness: { value: lineThickness },\n uLinesColor: { value: srgbColor(linesColor) },\n uScanColor: { value: srgbColor(scanColor) },\n uGridScale: { value: gridScale },\n uLineStyle: { value: lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0 },\n uLineJitter: { value: Math.max(0, Math.min(1, lineJitter || 0)) },\n uScanOpacity: { value: scanOpacity },\n uNoise: { value: noiseIntensity },\n uBloomOpacity: { value: bloomIntensity },\n uScanGlow: { value: scanGlow },\n uScanSoftness: { value: scanSoftness },\n uPhaseTaper: { value: scanPhaseTaper },\n uScanDuration: { value: scanDuration },\n uScanDelay: { value: scanDelay },\n uScanDirection: { value: scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0 },\n uScanStarts: { value: new Array(MAX_SCANS).fill(0) },\n uScanCount: { value: 0 }\n };\n\n const material = new THREE.ShaderMaterial({\n uniforms,\n vertexShader: vert,\n fragmentShader: frag,\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.current = material;\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);\n scene.add(quad);\n\n let composer: EffectComposer | null = null;\n if (enablePost) {\n composer = new EffectComposer(renderer);\n composerRef.current = composer;\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloom = new BloomEffect({\n intensity: 1.0,\n luminanceThreshold: bloomThreshold,\n luminanceSmoothing: bloomSmoothing\n });\n bloom.blendMode.opacity.value = Math.max(0, bloomIntensity);\n bloomRef.current = bloom;\n\n const chroma = new ChromaticAberrationEffect({\n offset: new THREE.Vector2(chromaticAberration, chromaticAberration),\n radialModulation: true,\n modulationOffset: 0.0\n });\n chromaRef.current = chroma;\n\n const effectPass = new EffectPass(camera, bloom, chroma);\n effectPass.renderToScreen = true;\n composer.addPass(effectPass);\n }\n\n const onResize = () => {\n renderer.setSize(container.clientWidth, container.clientHeight);\n material.uniforms.iResolution.value.set(container.clientWidth, container.clientHeight, renderer.getPixelRatio());\n if (composerRef.current) composerRef.current.setSize(container.clientWidth, container.clientHeight);\n };\n window.addEventListener('resize', onResize);\n\n let last = performance.now();\n const tick = () => {\n const now = performance.now();\n const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));\n last = now;\n\n lookCurrent.current.copy(\n smoothDampVec2(lookCurrent.current, lookTarget.current, lookVel.current, smoothTime, maxSpeed, dt)\n );\n\n const tiltSm = smoothDampFloat(\n tiltCurrent.current,\n tiltTarget.current,\n { v: tiltVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n tiltCurrent.current = tiltSm.value;\n tiltVel.current = tiltSm.v;\n\n const yawSm = smoothDampFloat(\n yawCurrent.current,\n yawTarget.current,\n { v: yawVel.current },\n smoothTime,\n maxSpeed,\n dt\n );\n yawCurrent.current = yawSm.value;\n yawVel.current = yawSm.v;\n\n const skew = new THREE.Vector2(lookCurrent.current.x * skewScale, -lookCurrent.current.y * yBoost * skewScale);\n material.uniforms.uSkew.value.set(skew.x, skew.y);\n material.uniforms.uTilt.value = tiltCurrent.current * tiltScale;\n material.uniforms.uYaw.value = THREE.MathUtils.clamp(yawCurrent.current * yawScale, -0.6, 0.6);\n\n material.uniforms.iTime.value = now / 1000;\n renderer.clear(true, true, true);\n if (composerRef.current) {\n composerRef.current.render(dt);\n } else {\n renderer.render(scene, camera);\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n rafRef.current = requestAnimationFrame(tick);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n window.removeEventListener('resize', onResize);\n material.dispose();\n (quad.geometry as THREE.BufferGeometry).dispose();\n if (composerRef.current) {\n composerRef.current.dispose();\n composerRef.current = null;\n }\n renderer.dispose();\n renderer.forceContextLoss();\n container.removeChild(renderer.domElement);\n };\n }, [\n sensitivity,\n lineThickness,\n linesColor,\n scanColor,\n scanOpacity,\n gridScale,\n lineStyle,\n lineJitter,\n scanDirection,\n enablePost\n ]);\n\n useEffect(() => {\n const m = materialRef.current;\n if (m) {\n const u = m.uniforms;\n u.uLineThickness.value = lineThickness;\n (u.uLinesColor.value as THREE.Color).copy(srgbColor(linesColor));\n (u.uScanColor.value as THREE.Color).copy(srgbColor(scanColor));\n u.uGridScale.value = gridScale;\n u.uLineStyle.value = lineStyle === 'dashed' ? 1 : lineStyle === 'dotted' ? 2 : 0;\n u.uLineJitter.value = Math.max(0, Math.min(1, lineJitter || 0));\n u.uBloomOpacity.value = Math.max(0, bloomIntensity);\n u.uNoise.value = Math.max(0, noiseIntensity);\n u.uScanGlow.value = scanGlow;\n u.uScanOpacity.value = Math.max(0, Math.min(1, scanOpacity));\n u.uScanDirection.value = scanDirection === 'backward' ? 1 : scanDirection === 'pingpong' ? 2 : 0;\n u.uScanSoftness.value = scanSoftness;\n u.uPhaseTaper.value = scanPhaseTaper;\n u.uScanDuration.value = Math.max(0.05, scanDuration);\n u.uScanDelay.value = Math.max(0.0, scanDelay);\n }\n if (bloomRef.current) {\n bloomRef.current.blendMode.opacity.value = Math.max(0, bloomIntensity);\n (bloomRef.current as any).luminanceMaterial.threshold = bloomThreshold;\n (bloomRef.current as any).luminanceMaterial.smoothing = bloomSmoothing;\n }\n if (chromaRef.current) {\n chromaRef.current.offset.set(chromaticAberration, chromaticAberration);\n }\n }, [\n lineThickness,\n linesColor,\n scanColor,\n gridScale,\n lineStyle,\n lineJitter,\n bloomIntensity,\n bloomThreshold,\n bloomSmoothing,\n chromaticAberration,\n noiseIntensity,\n scanGlow,\n scanOpacity,\n scanDirection,\n scanSoftness,\n scanPhaseTaper,\n scanDuration,\n scanDelay\n ]);\n\n useEffect(() => {\n if (!enableGyro) return;\n const handler = (e: DeviceOrientationEvent) => {\n if (uiFaceActive) return;\n const gamma = e.gamma ?? 0;\n const beta = e.beta ?? 0;\n const nx = THREE.MathUtils.clamp(gamma / 45, -1, 1);\n const ny = THREE.MathUtils.clamp(-beta / 30, -1, 1);\n lookTarget.current.set(nx, ny);\n tiltTarget.current = THREE.MathUtils.degToRad(gamma) * 0.4;\n };\n window.addEventListener('deviceorientation', handler);\n return () => {\n window.removeEventListener('deviceorientation', handler);\n };\n }, [enableGyro, uiFaceActive]);\n\n useEffect(() => {\n let canceled = false;\n const load = async () => {\n try {\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(modelsPath),\n faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelsPath)\n ]);\n if (!canceled) setModelsReady(true);\n } catch {\n if (!canceled) setModelsReady(false);\n }\n };\n load();\n return () => {\n canceled = true;\n };\n }, [modelsPath]);\n\n useEffect(() => {\n let stop = false;\n let lastDetect = 0;\n\n const start = async () => {\n if (!enableWebcam || !modelsReady) return;\n const video = videoRef.current;\n if (!video) return;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } },\n audio: false\n });\n video.srcObject = stream;\n await video.play();\n } catch {\n return;\n }\n\n const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });\n\n const detect = async (ts: number) => {\n if (stop) return;\n\n if (ts - lastDetect >= 33) {\n lastDetect = ts;\n try {\n const res = await faceapi.detectSingleFace(video, opts).withFaceLandmarks(true);\n if (res && res.detection) {\n const det = res.detection;\n const box = det.box;\n const vw = video.videoWidth || 1;\n const vh = video.videoHeight || 1;\n\n const cx = box.x + box.width * 0.5;\n const cy = box.y + box.height * 0.5;\n const nx = (cx / vw) * 2 - 1;\n const ny = (cy / vh) * 2 - 1;\n medianPush(bufX.current, nx, 5);\n medianPush(bufY.current, ny, 5);\n const nxm = median(bufX.current);\n const nym = median(bufY.current);\n\n const look = new THREE.Vector2(Math.tanh(nxm), Math.tanh(nym));\n\n const faceSize = Math.min(1, Math.hypot(box.width / vw, box.height / vh));\n const depthScale = 1 + depthResponse * (faceSize - 0.25);\n lookTarget.current.copy(look.multiplyScalar(depthScale));\n\n const leftEye = res.landmarks.getLeftEye();\n const rightEye = res.landmarks.getRightEye();\n const lc = centroid(leftEye);\n const rc = centroid(rightEye);\n const tilt = Math.atan2(rc.y - lc.y, rc.x - lc.x);\n medianPush(bufT.current, tilt, 5);\n tiltTarget.current = median(bufT.current);\n\n const nose = res.landmarks.getNose();\n const tip = nose[nose.length - 1] || nose[Math.floor(nose.length / 2)];\n const jaw = res.landmarks.getJawOutline();\n const leftCheek = jaw[3] || jaw[2];\n const rightCheek = jaw[13] || jaw[14];\n const dL = dist2(tip, leftCheek);\n const dR = dist2(tip, rightCheek);\n const eyeDist = Math.hypot(rc.x - lc.x, rc.y - lc.y) + 1e-6;\n let yawSignal = THREE.MathUtils.clamp((dR - dL) / (eyeDist * 1.6), -1, 1);\n yawSignal = Math.tanh(yawSignal);\n medianPush(bufYaw.current, yawSignal, 5);\n yawTarget.current = median(bufYaw.current);\n\n setUiFaceActive(true);\n } else {\n setUiFaceActive(false);\n }\n } catch {\n setUiFaceActive(false);\n }\n }\n\n if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {\n (video as any).requestVideoFrameCallback(() => detect(performance.now()));\n } else {\n requestAnimationFrame(detect);\n }\n };\n\n requestAnimationFrame(detect);\n };\n\n start();\n\n return () => {\n stop = true;\n const video = videoRef.current;\n if (video) {\n const stream = video.srcObject as MediaStream | null;\n if (stream) stream.getTracks().forEach(t => t.stop());\n video.pause();\n video.srcObject = null;\n }\n };\n }, [enableWebcam, modelsReady, depthResponse]);\n\n return (\n \n {showPreview && (\n
\n
\n
\n {enableWebcam\n ? modelsReady\n ? uiFaceActive\n ? 'Face: tracking'\n : 'Face: searching'\n : 'Loading models'\n : 'Webcam disabled'}\n
\n
\n )}\n
\n );\n};\n\nfunction srgbColor(hex: string) {\n const c = new THREE.Color(hex);\n return c.convertSRGBToLinear();\n}\n\nfunction smoothDampVec2(\n current: THREE.Vector2,\n target: THREE.Vector2,\n currentVelocity: THREE.Vector2,\n smoothTime: number,\n maxSpeed: number,\n deltaTime: number\n): THREE.Vector2 {\n const out = current.clone();\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current.clone().sub(target);\n const originalTo = target.clone();\n\n const maxChange = maxSpeed * smoothTime;\n if (change.length() > maxChange) change.setLength(maxChange);\n\n target = current.clone().sub(change);\n const temp = currentVelocity.clone().addScaledVector(change, omega).multiplyScalar(deltaTime);\n currentVelocity.sub(temp.clone().multiplyScalar(omega));\n currentVelocity.multiplyScalar(exp);\n\n out.copy(target.clone().add(change.add(temp).multiplyScalar(exp)));\n\n const origMinusCurrent = originalTo.clone().sub(current);\n const outMinusOrig = out.clone().sub(originalTo);\n if (origMinusCurrent.dot(outMinusOrig) > 0) {\n out.copy(originalTo);\n currentVelocity.set(0, 0);\n }\n return out;\n}\n\nfunction smoothDampFloat(\n current: number,\n target: number,\n velRef: { v: number },\n smoothTime: number,\n maxSpeed: number,\n deltaTime: number\n): { value: number; v: number } {\n smoothTime = Math.max(0.0001, smoothTime);\n const omega = 2 / smoothTime;\n const x = omega * deltaTime;\n const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);\n\n let change = current - target;\n const originalTo = target;\n\n const maxChange = maxSpeed * smoothTime;\n change = Math.sign(change) * Math.min(Math.abs(change), maxChange);\n\n target = current - change;\n const temp = (velRef.v + omega * change) * deltaTime;\n velRef.v = (velRef.v - omega * temp) * exp;\n\n let out = target + (change + temp) * exp;\n\n const origMinusCurrent = originalTo - current;\n const outMinusOrig = out - originalTo;\n if (origMinusCurrent * outMinusOrig > 0) {\n out = originalTo;\n velRef.v = 0;\n }\n return { value: out, v: velRef.v };\n}\n\nfunction medianPush(buf: number[], v: number, maxLen: number) {\n buf.push(v);\n if (buf.length > maxLen) buf.shift();\n}\n\nfunction median(buf: number[]) {\n if (buf.length === 0) return 0;\n const a = [...buf].sort((x, y) => x - y);\n const mid = Math.floor(a.length / 2);\n return a.length % 2 ? a[mid] : (a[mid - 1] + a[mid]) * 0.5;\n}\n\nfunction centroid(points: { x: number; y: number }[]) {\n let x = 0,\n y = 0;\n const n = points.length || 1;\n for (const p of points) {\n x += p.x;\n y += p.y;\n }\n return { x: x / n, y: y / n };\n}\n\nfunction dist2(a: { x: number; y: number }, b: { x: number; y: number }) {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n"
}
],
"registryDependencies": [],
"dependencies": [
+ "face-api.js@^0.22.2",
"postprocessing@^6.36.0",
- "three@^0.167.1",
- "face-api.js@^0.22.2"
+ "three@^0.167.1"
]
}
\ No newline at end of file
diff --git a/public/r/Hyperspeed-JS-CSS.json b/public/r/Hyperspeed-JS-CSS.json
index b27f4990..da2fc870 100644
--- a/public/r/Hyperspeed-JS-CSS.json
+++ b/public/r/Hyperspeed-JS-CSS.json
@@ -18,12 +18,12 @@
{
"type": "registry:component",
"path": "Hyperspeed/Hyperspeed.jsx",
- "content": "import { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\nimport { BloomEffect, EffectComposer, EffectPass, RenderPass, SMAAEffect, SMAAPreset } from 'postprocessing';\n\nimport './Hyperspeed.css';\n\nconst DEFAULT_EFFECT_OPTIONS = {\n onSpeedUp: () => {},\n onSlowDown: () => {},\n distortion: 'turbulentDistortion',\n length: 400,\n roadWidth: 10,\n islandWidth: 2,\n lanesPerRoad: 4,\n fov: 90,\n fovSpeedUp: 150,\n speedUp: 2,\n carLightsFade: 0.4,\n totalSideLightSticks: 20,\n lightPairsPerRoadWay: 40,\n shoulderLinesWidthPercentage: 0.05,\n brokenLinesWidthPercentage: 0.1,\n brokenLinesLengthPercentage: 0.5,\n lightStickWidth: [0.12, 0.5],\n lightStickHeight: [1.3, 1.7],\n movingAwaySpeed: [60, 80],\n movingCloserSpeed: [-120, -160],\n carLightsLength: [400 * 0.03, 400 * 0.2],\n carLightsRadius: [0.05, 0.14],\n carWidthPercentage: [0.3, 0.5],\n carShiftX: [-0.8, 0.8],\n carFloorSeparation: [0, 5],\n colors: {\n roadColor: 0x080808,\n islandColor: 0x0a0a0a,\n background: 0x000000,\n shoulderLines: 0xffffff,\n brokenLines: 0xffffff,\n leftCars: [0xd856bf, 0x6750a2, 0xc247ac],\n rightCars: [0x03b3c3, 0x0e5ea5, 0x324555],\n sticks: 0x03b3c3\n }\n};\n\nconst Hyperspeed = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => {\n const hyperspeed = useRef(null);\n const appRef = useRef(null);\n\n useEffect(() => {\n if (appRef.current) {\n appRef.current.dispose();\n const container = document.getElementById('lights');\n if (container) {\n while (container.firstChild) {\n container.removeChild(container.firstChild);\n }\n }\n }\n const mountainUniforms = {\n uFreq: { value: new THREE.Vector3(3, 6, 10) },\n uAmp: { value: new THREE.Vector3(30, 30, 20) }\n };\n\n const xyUniforms = {\n uFreq: { value: new THREE.Vector2(5, 2) },\n uAmp: { value: new THREE.Vector2(25, 15) }\n };\n\n const LongRaceUniforms = {\n uFreq: { value: new THREE.Vector2(2, 3) },\n uAmp: { value: new THREE.Vector2(35, 10) }\n };\n\n const turbulentUniforms = {\n uFreq: { value: new THREE.Vector4(4, 8, 8, 1) },\n uAmp: { value: new THREE.Vector4(25, 5, 10, 10) }\n };\n\n const deepUniforms = {\n uFreq: { value: new THREE.Vector2(4, 8) },\n uAmp: { value: new THREE.Vector2(10, 20) },\n uPowY: { value: new THREE.Vector2(20, 2) }\n };\n\n let nsin = val => Math.sin(val) * 0.5 + 0.5;\n\n const distortions = {\n mountainDistortion: {\n uniforms: mountainUniforms,\n getDistortion: `\n uniform vec3 uAmp;\n uniform vec3 uFreq;\n #define PI 3.14159265358979\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n nsin(progress * PI * uFreq.y + uTime) * uAmp.y - nsin(movementProgressFix * PI * uFreq.y + uTime) * uAmp.y,\n nsin(progress * PI * uFreq.z + uTime) * uAmp.z - nsin(movementProgressFix * PI * uFreq.z + uTime) * uAmp.z\n );\n }\n `,\n getJS: (progress, time) => {\n let movementProgressFix = 0.02;\n let uFreq = mountainUniforms.uFreq.value;\n let uAmp = mountainUniforms.uAmp.value;\n let distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n nsin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n nsin(movementProgressFix * Math.PI * uFreq.y + time) * uAmp.y,\n nsin(progress * Math.PI * uFreq.z + time) * uAmp.z -\n nsin(movementProgressFix * Math.PI * uFreq.z + time) * uAmp.z\n );\n let lookAtAmp = new THREE.Vector3(2, 2, 2);\n let lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n xyDistortion: {\n uniforms: xyUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float movementProgressFix = 0.02;\n return vec3( \n cos(progress * PI * uFreq.x + uTime) * uAmp.x - cos(movementProgressFix * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + PI/2. + uTime) * uAmp.y - sin(movementProgressFix * PI * uFreq.y + PI/2. + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n let movementProgressFix = 0.02;\n let uFreq = xyUniforms.uFreq.value;\n let uAmp = xyUniforms.uAmp.value;\n let distortion = new THREE.Vector3(\n Math.cos(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.cos(movementProgressFix * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y -\n Math.sin(movementProgressFix * Math.PI * uFreq.y + time + Math.PI / 2) * uAmp.y,\n 0\n );\n let lookAtAmp = new THREE.Vector3(2, 0.4, 1);\n let lookAtOffset = new THREE.Vector3(0, 0, -3);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n LongRaceDistortion: {\n uniforms: LongRaceUniforms,\n getDistortion: `\n uniform vec2 uFreq;\n uniform vec2 uAmp;\n #define PI 3.14159265358979\n vec3 getDistortion(float progress){\n float camProgress = 0.0125;\n return vec3( \n sin(progress * PI * uFreq.x + uTime) * uAmp.x - sin(camProgress * PI * uFreq.x + uTime) * uAmp.x,\n sin(progress * PI * uFreq.y + uTime) * uAmp.y - sin(camProgress * PI * uFreq.y + uTime) * uAmp.y,\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n let camProgress = 0.0125;\n let uFreq = LongRaceUniforms.uFreq.value;\n let uAmp = LongRaceUniforms.uAmp.value;\n let distortion = new THREE.Vector3(\n Math.sin(progress * Math.PI * uFreq.x + time) * uAmp.x -\n Math.sin(camProgress * Math.PI * uFreq.x + time) * uAmp.x,\n Math.sin(progress * Math.PI * uFreq.y + time) * uAmp.y -\n Math.sin(camProgress * Math.PI * uFreq.y + time) * uAmp.y,\n 0\n );\n let lookAtAmp = new THREE.Vector3(1, 1, 0);\n let lookAtOffset = new THREE.Vector3(0, 0, -5);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortion: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r + uTime) * uAmp.r +\n pow(cos(PI * progress * uFreq.g + uTime * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b + uTime) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a + uTime / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.0125),\n getDistortionY(progress) - getDistortionY(0.0125),\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n const uFreq = turbulentUniforms.uFreq.value;\n const uAmp = turbulentUniforms.uAmp.value;\n\n const getX = p =>\n Math.cos(Math.PI * p * uFreq.x + time) * uAmp.x +\n Math.pow(Math.cos(Math.PI * p * uFreq.y + time * (uFreq.y / uFreq.x)), 2) * uAmp.y;\n\n const getY = p =>\n -nsin(Math.PI * p * uFreq.z + time) * uAmp.z -\n Math.pow(nsin(Math.PI * p * uFreq.w + time / (uFreq.z / uFreq.w)), 5) * uAmp.w;\n\n let distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.007),\n getY(progress) - getY(progress + 0.007),\n 0\n );\n let lookAtAmp = new THREE.Vector3(-2, -5, 0);\n let lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n },\n turbulentDistortionStill: {\n uniforms: turbulentUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n cos(PI * progress * uFreq.r) * uAmp.r +\n pow(cos(PI * progress * uFreq.g * (uFreq.g / uFreq.r)), 2. ) * uAmp.g\n );\n }\n float getDistortionY(float progress){\n return (\n -nsin(PI * progress * uFreq.b) * uAmp.b +\n -pow(nsin(PI * progress * uFreq.a / (uFreq.b / uFreq.a)), 5.) * uAmp.a\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `\n },\n deepDistortionStill: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x) * uAmp.x * 2.\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.05),\n 0.\n );\n }\n `\n },\n deepDistortion: {\n uniforms: deepUniforms,\n getDistortion: `\n uniform vec4 uFreq;\n uniform vec4 uAmp;\n uniform vec2 uPowY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n #define PI 3.14159265358979\n float getDistortionX(float progress){\n return (\n sin(progress * PI * uFreq.x + uTime) * uAmp.x\n );\n }\n float getDistortionY(float progress){\n return (\n pow(abs(progress * uPowY.x), uPowY.y) + sin(progress * PI * uFreq.y + uTime) * uAmp.y\n );\n }\n vec3 getDistortion(float progress){\n return vec3(\n getDistortionX(progress) - getDistortionX(0.02),\n getDistortionY(progress) - getDistortionY(0.02),\n 0.\n );\n }\n `,\n getJS: (progress, time) => {\n const uFreq = deepUniforms.uFreq.value;\n const uAmp = deepUniforms.uAmp.value;\n const uPowY = deepUniforms.uPowY.value;\n\n const getX = p => Math.sin(p * Math.PI * uFreq.x + time) * uAmp.x;\n const getY = p => Math.pow(p * uPowY.x, uPowY.y) + Math.sin(p * Math.PI * uFreq.y + time) * uAmp.y;\n\n let distortion = new THREE.Vector3(\n getX(progress) - getX(progress + 0.01),\n getY(progress) - getY(progress + 0.01),\n 0\n );\n let lookAtAmp = new THREE.Vector3(-2, -4, 0);\n let lookAtOffset = new THREE.Vector3(0, 0, -10);\n return distortion.multiply(lookAtAmp).add(lookAtOffset);\n }\n }\n };\n\n class App {\n constructor(container, options = {}) {\n this.options = options;\n if (this.options.distortion == null) {\n this.options.distortion = {\n uniforms: distortion_uniforms,\n getDistortion: distortion_vertex\n };\n }\n this.container = container;\n this.renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: true\n });\n this.renderer.setSize(container.offsetWidth, container.offsetHeight, false);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n this.composer = new EffectComposer(this.renderer);\n container.append(this.renderer.domElement);\n\n this.camera = new THREE.PerspectiveCamera(\n options.fov,\n container.offsetWidth / container.offsetHeight,\n 0.1,\n 10000\n );\n this.camera.position.z = -5;\n this.camera.position.y = 8;\n this.camera.position.x = 0;\n this.scene = new THREE.Scene();\n this.scene.background = null;\n\n let fog = new THREE.Fog(options.colors.background, options.length * 0.2, options.length * 500);\n this.scene.fog = fog;\n this.fogUniforms = {\n fogColor: { value: fog.color },\n fogNear: { value: fog.near },\n fogFar: { value: fog.far }\n };\n this.clock = new THREE.Clock();\n this.assets = {};\n this.disposed = false;\n\n this.road = new Road(this, options);\n this.leftCarLights = new CarLights(\n this,\n options,\n options.colors.leftCars,\n options.movingAwaySpeed,\n new THREE.Vector2(0, 1 - options.carLightsFade)\n );\n this.rightCarLights = new CarLights(\n this,\n options,\n options.colors.rightCars,\n options.movingCloserSpeed,\n new THREE.Vector2(1, 0 + options.carLightsFade)\n );\n this.leftSticks = new LightsSticks(this, options);\n\n this.fovTarget = options.fov;\n this.speedUpTarget = 0;\n this.speedUp = 0;\n this.timeOffset = 0;\n\n this.tick = this.tick.bind(this);\n this.init = this.init.bind(this);\n this.setSize = this.setSize.bind(this);\n this.onMouseDown = this.onMouseDown.bind(this);\n this.onMouseUp = this.onMouseUp.bind(this);\n\n this.onTouchStart = this.onTouchStart.bind(this);\n this.onTouchEnd = this.onTouchEnd.bind(this);\n this.onContextMenu = this.onContextMenu.bind(this);\n\n window.addEventListener('resize', this.onWindowResize.bind(this));\n }\n\n onWindowResize() {\n const width = this.container.offsetWidth;\n const height = this.container.offsetHeight;\n\n this.renderer.setSize(width, height);\n this.camera.aspect = width / height;\n this.camera.updateProjectionMatrix();\n this.composer.setSize(width, height);\n }\n\n initPasses() {\n this.renderPass = new RenderPass(this.scene, this.camera);\n this.bloomPass = new EffectPass(\n this.camera,\n new BloomEffect({\n luminanceThreshold: 0.2,\n luminanceSmoothing: 0,\n resolutionScale: 1\n })\n );\n\n const smaaPass = new EffectPass(\n this.camera,\n new SMAAEffect({\n preset: SMAAPreset.MEDIUM,\n searchImage: SMAAEffect.searchImageDataURL,\n areaImage: SMAAEffect.areaImageDataURL\n })\n );\n this.renderPass.renderToScreen = false;\n this.bloomPass.renderToScreen = false;\n smaaPass.renderToScreen = true;\n this.composer.addPass(this.renderPass);\n this.composer.addPass(this.bloomPass);\n this.composer.addPass(smaaPass);\n }\n\n loadAssets() {\n const assets = this.assets;\n return new Promise(resolve => {\n const manager = new THREE.LoadingManager(resolve);\n\n const searchImage = new Image();\n const areaImage = new Image();\n assets.smaa = {};\n searchImage.addEventListener('load', function () {\n assets.smaa.search = this;\n manager.itemEnd('smaa-search');\n });\n\n areaImage.addEventListener('load', function () {\n assets.smaa.area = this;\n manager.itemEnd('smaa-area');\n });\n manager.itemStart('smaa-search');\n manager.itemStart('smaa-area');\n\n searchImage.src = SMAAEffect.searchImageDataURL;\n areaImage.src = SMAAEffect.areaImageDataURL;\n });\n }\n\n init() {\n this.initPasses();\n const options = this.options;\n this.road.init();\n this.leftCarLights.init();\n\n this.leftCarLights.mesh.position.setX(-options.roadWidth / 2 - options.islandWidth / 2);\n this.rightCarLights.init();\n this.rightCarLights.mesh.position.setX(options.roadWidth / 2 + options.islandWidth / 2);\n this.leftSticks.init();\n this.leftSticks.mesh.position.setX(-(options.roadWidth + options.islandWidth / 2));\n\n this.container.addEventListener('mousedown', this.onMouseDown);\n this.container.addEventListener('mouseup', this.onMouseUp);\n this.container.addEventListener('mouseout', this.onMouseUp);\n\n this.container.addEventListener('touchstart', this.onTouchStart, { passive: true });\n this.container.addEventListener('touchend', this.onTouchEnd, { passive: true });\n this.container.addEventListener('touchcancel', this.onTouchEnd, { passive: true });\n\n this.container.addEventListener('contextmenu', this.onContextMenu);\n\n this.tick();\n }\n\n onMouseDown(ev) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onMouseUp(ev) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onTouchStart(ev) {\n if (this.options.onSpeedUp) this.options.onSpeedUp(ev);\n this.fovTarget = this.options.fovSpeedUp;\n this.speedUpTarget = this.options.speedUp;\n }\n\n onTouchEnd(ev) {\n if (this.options.onSlowDown) this.options.onSlowDown(ev);\n this.fovTarget = this.options.fov;\n this.speedUpTarget = 0;\n }\n\n onContextMenu(ev) {\n ev.preventDefault();\n }\n\n update(delta) {\n let lerpPercentage = Math.exp(-(-60 * Math.log2(1 - 0.1)) * delta);\n this.speedUp += lerp(this.speedUp, this.speedUpTarget, lerpPercentage, 0.00001);\n this.timeOffset += this.speedUp * delta;\n\n let time = this.clock.elapsedTime + this.timeOffset;\n\n this.rightCarLights.update(time);\n this.leftCarLights.update(time);\n this.leftSticks.update(time);\n this.road.update(time);\n\n let updateCamera = false;\n let fovChange = lerp(this.camera.fov, this.fovTarget, lerpPercentage);\n if (fovChange !== 0) {\n this.camera.fov += fovChange * delta * 6;\n updateCamera = true;\n }\n\n if (this.options.distortion.getJS) {\n const distortion = this.options.distortion.getJS(0.025, time);\n\n this.camera.lookAt(\n new THREE.Vector3(\n this.camera.position.x + distortion.x,\n this.camera.position.y + distortion.y,\n this.camera.position.z + distortion.z\n )\n );\n updateCamera = true;\n }\n if (updateCamera) {\n this.camera.updateProjectionMatrix();\n }\n\n }\n\n render(delta) {\n this.composer.render(delta);\n }\n\n dispose() {\n this.disposed = true;\n\n if (this.renderer) {\n this.renderer.dispose();\n }\n if (this.composer) {\n this.composer.dispose();\n }\n if (this.scene) {\n this.scene.clear();\n }\n\n window.removeEventListener('resize', this.onWindowResize.bind(this));\n if (this.container) {\n this.container.removeEventListener('mousedown', this.onMouseDown);\n this.container.removeEventListener('mouseup', this.onMouseUp);\n this.container.removeEventListener('mouseout', this.onMouseUp);\n\n this.container.removeEventListener('touchstart', this.onTouchStart);\n this.container.removeEventListener('touchend', this.onTouchEnd);\n this.container.removeEventListener('touchcancel', this.onTouchEnd);\n this.container.removeEventListener('contextmenu', this.onContextMenu);\n }\n }\n\n setSize(width, height, updateStyles) {\n this.composer.setSize(width, height, updateStyles);\n }\n\n tick() {\n if (this.disposed || !this) return;\n if (resizeRendererToDisplaySize(this.renderer, this.setSize)) {\n const canvas = this.renderer.domElement;\n this.camera.aspect = canvas.clientWidth / canvas.clientHeight;\n this.camera.updateProjectionMatrix();\n }\n const delta = this.clock.getDelta();\n this.render(delta);\n this.update(delta);\n requestAnimationFrame(this.tick);\n }\n }\n\n const distortion_uniforms = {\n uDistortionX: { value: new THREE.Vector2(80, 3) },\n uDistortionY: { value: new THREE.Vector2(-40, 2.5) }\n };\n\n const distortion_vertex = `\n #define PI 3.14159265358979\n uniform vec2 uDistortionX;\n uniform vec2 uDistortionY;\n float nsin(float val){\n return sin(val) * 0.5 + 0.5;\n }\n vec3 getDistortion(float progress){\n progress = clamp(progress, 0., 1.);\n float xAmp = uDistortionX.r;\n float xFreq = uDistortionX.g;\n float yAmp = uDistortionY.r;\n float yFreq = uDistortionY.g;\n return vec3( \n xAmp * nsin(progress * PI * xFreq - PI / 2.),\n yAmp * nsin(progress * PI * yFreq - PI / 2.),\n 0.\n );\n }\n `;\n\n const random = base => {\n if (Array.isArray(base)) return Math.random() * (base[1] - base[0]) + base[0];\n return Math.random() * base;\n };\n\n const pickRandom = arr => {\n if (Array.isArray(arr)) return arr[Math.floor(Math.random() * arr.length)];\n return arr;\n };\n\n function lerp(current, target, speed = 0.1, limit = 0.001) {\n let change = (target - current) * speed;\n if (Math.abs(change) < limit) {\n change = target - current;\n }\n return change;\n }\n\n class CarLights {\n constructor(webgl, options, colors, speed, fade) {\n this.webgl = webgl;\n this.options = options;\n this.colors = colors;\n this.speed = speed;\n this.fade = fade;\n }\n\n init() {\n const options = this.options;\n let curve = new THREE.LineCurve3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1));\n let geometry = new THREE.TubeGeometry(curve, 40, 1, 8, false);\n\n let instanced = new THREE.InstancedBufferGeometry().copy(geometry);\n instanced.instanceCount = options.lightPairsPerRoadWay * 2;\n\n let laneWidth = options.roadWidth / options.lanesPerRoad;\n\n let aOffset = [];\n let aMetrics = [];\n let aColor = [];\n\n let colors = this.colors;\n if (Array.isArray(colors)) {\n colors = colors.map(c => new THREE.Color(c));\n } else {\n colors = new THREE.Color(colors);\n }\n\n for (let i = 0; i < options.lightPairsPerRoadWay; i++) {\n let radius = random(options.carLightsRadius);\n let length = random(options.carLightsLength);\n let speed = random(this.speed);\n\n let carLane = i % options.lanesPerRoad;\n let laneX = carLane * laneWidth - options.roadWidth / 2 + laneWidth / 2;\n\n let carWidth = random(options.carWidthPercentage) * laneWidth;\n let carShiftX = random(options.carShiftX) * laneWidth;\n laneX += carShiftX;\n\n let offsetY = random(options.carFloorSeparation) + radius * 1.3;\n\n let offsetZ = -random(options.length);\n\n aOffset.push(laneX - carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aOffset.push(laneX + carWidth / 2);\n aOffset.push(offsetY);\n aOffset.push(offsetZ);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(speed);\n\n aMetrics.push(radius);\n aMetrics.push(length);\n aMetrics.push(speed);\n\n let color = pickRandom(colors);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 3, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n\n let material = new THREE.ShaderMaterial({\n fragmentShader: carLightsFragment,\n vertexShader: carLightsVertex,\n transparent: true,\n uniforms: Object.assign(\n {\n uTime: { value: 0 },\n uTravelLength: { value: options.length },\n uFade: { value: this.fade }\n },\n this.webgl.fogUniforms,\n options.distortion.uniforms\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n options.distortion.getDistortion\n );\n };\n\n let mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n\n const carLightsFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n varying vec2 vUv; \n uniform vec2 uFade;\n void main() {\n vec3 color = vec3(vColor);\n float alpha = smoothstep(uFade.x, uFade.y, vUv.x);\n gl_FragColor = vec4(color, alpha);\n if (gl_FragColor.a < 0.0001) discard;\n ${THREE.ShaderChunk['fog_fragment']}\n }\n `;\n\n const carLightsVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute vec3 aOffset;\n attribute vec3 aMetrics;\n attribute vec3 aColor;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec2 vUv; \n varying vec3 vColor; \n #include \n void main() {\n vec3 transformed = position.xyz;\n float radius = aMetrics.r;\n float myLength = aMetrics.g;\n float speed = aMetrics.b;\n\n transformed.xy *= radius;\n transformed.z *= myLength;\n\n transformed.z += myLength - mod(uTime * speed + aOffset.z, uTravelLength);\n transformed.xy += aOffset.xy;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vUv = uv;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n `;\n\n class LightsSticks {\n constructor(webgl, options) {\n this.webgl = webgl;\n this.options = options;\n }\n\n init() {\n const options = this.options;\n const geometry = new THREE.PlaneGeometry(1, 1);\n let instanced = new THREE.InstancedBufferGeometry().copy(geometry);\n let totalSticks = options.totalSideLightSticks;\n instanced.instanceCount = totalSticks;\n\n let stickoffset = options.length / (totalSticks - 1);\n const aOffset = [];\n const aColor = [];\n const aMetrics = [];\n\n let colors = options.colors.sticks;\n if (Array.isArray(colors)) {\n colors = colors.map(c => new THREE.Color(c));\n } else {\n colors = new THREE.Color(colors);\n }\n\n for (let i = 0; i < totalSticks; i++) {\n let width = random(options.lightStickWidth);\n let height = random(options.lightStickHeight);\n aOffset.push((i - 1) * stickoffset * 2 + stickoffset * Math.random());\n\n let color = pickRandom(colors);\n aColor.push(color.r);\n aColor.push(color.g);\n aColor.push(color.b);\n\n aMetrics.push(width);\n aMetrics.push(height);\n }\n\n instanced.setAttribute('aOffset', new THREE.InstancedBufferAttribute(new Float32Array(aOffset), 1, false));\n instanced.setAttribute('aColor', new THREE.InstancedBufferAttribute(new Float32Array(aColor), 3, false));\n instanced.setAttribute('aMetrics', new THREE.InstancedBufferAttribute(new Float32Array(aMetrics), 2, false));\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: sideSticksFragment,\n vertexShader: sideSticksVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(\n {\n uTravelLength: { value: options.length },\n uTime: { value: 0 }\n },\n this.webgl.fogUniforms,\n options.distortion.uniforms\n )\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n options.distortion.getDistortion\n );\n };\n\n const mesh = new THREE.Mesh(instanced, material);\n mesh.frustumCulled = false;\n this.webgl.scene.add(mesh);\n this.mesh = mesh;\n }\n\n update(time) {\n this.mesh.material.uniforms.uTime.value = time;\n }\n }\n\n const sideSticksVertex = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_vertex']}\n attribute float aOffset;\n attribute vec3 aColor;\n attribute vec2 aMetrics;\n uniform float uTravelLength;\n uniform float uTime;\n varying vec3 vColor;\n mat4 rotationY( in float angle ) {\n return mat4(\tcos(angle),\t\t0,\t\tsin(angle),\t0,\n 0,\t\t1.0,\t\t\t 0,\t0,\n -sin(angle),\t0,\t\tcos(angle),\t0,\n 0, \t\t0,\t\t\t\t0,\t1);\n }\n #include \n void main(){\n vec3 transformed = position.xyz;\n float width = aMetrics.x;\n float height = aMetrics.y;\n\n transformed.xy *= vec2(width, height);\n float time = mod(uTime * 60. * 2. + aOffset, uTravelLength);\n\n transformed = (rotationY(3.14/2.) * vec4(transformed,1.)).xyz;\n\n transformed.z += - uTravelLength + time;\n\n float progress = abs(transformed.z / uTravelLength);\n transformed.xyz += getDistortion(progress);\n\n transformed.y += height / 2.;\n transformed.x += -width / 2.;\n vec4 mvPosition = modelViewMatrix * vec4(transformed, 1.);\n gl_Position = projectionMatrix * mvPosition;\n vColor = aColor;\n ${THREE.ShaderChunk['fog_vertex']}\n }\n `;\n\n const sideSticksFragment = `\n #define USE_FOG;\n ${THREE.ShaderChunk['fog_pars_fragment']}\n varying vec3 vColor;\n void main(){\n vec3 color = vec3(vColor);\n gl_FragColor = vec4(color,1.);\n ${THREE.ShaderChunk['fog_fragment']}\n }\n `;\n\n class Road {\n constructor(webgl, options) {\n this.webgl = webgl;\n this.options = options;\n this.uTime = { value: 0 };\n }\n\n createPlane(side, width, isRoad) {\n const options = this.options;\n let segments = 100;\n const geometry = new THREE.PlaneGeometry(\n isRoad ? options.roadWidth : options.islandWidth,\n options.length,\n 20,\n segments\n );\n let uniforms = {\n uTravelLength: { value: options.length },\n uColor: { value: new THREE.Color(isRoad ? options.colors.roadColor : options.colors.islandColor) },\n uTime: this.uTime\n };\n\n if (isRoad) {\n uniforms = Object.assign(uniforms, {\n uLanes: { value: options.lanesPerRoad },\n uBrokenLinesColor: { value: new THREE.Color(options.colors.brokenLines) },\n uShoulderLinesColor: { value: new THREE.Color(options.colors.shoulderLines) },\n uShoulderLinesWidthPercentage: { value: options.shoulderLinesWidthPercentage },\n uBrokenLinesLengthPercentage: { value: options.brokenLinesLengthPercentage },\n uBrokenLinesWidthPercentage: { value: options.brokenLinesWidthPercentage }\n });\n }\n\n const material = new THREE.ShaderMaterial({\n fragmentShader: isRoad ? roadFragment : islandFragment,\n vertexShader: roadVertex,\n side: THREE.DoubleSide,\n uniforms: Object.assign(uniforms, this.webgl.fogUniforms, options.distortion.uniforms)\n });\n\n material.onBeforeCompile = shader => {\n shader.vertexShader = shader.vertexShader.replace(\n '#include ',\n options.distortion.getDistortion\n );\n };\n\n const mesh = new THREE.Mesh(geometry, material);\n mesh.rotation.x = -Math.PI / 2;\n mesh.position.z = -options.length / 2;\n mesh.position.x += (this.options.islandWidth / 2 + options.roadWidth / 2) * side;\n this.webgl.scene.add(mesh);\n\n return mesh;\n }\n\n init() {\n this.leftRoadWay = this.createPlane(-1, this.options.roadWidth, true);\n this.rightRoadWay = this.createPlane(1, this.options.roadWidth, true);\n this.island = this.createPlane(0, this.options.islandWidth, false);\n }\n\n update(time) {\n this.uTime.value = time;\n }\n }\n\n const roadBaseFragment = `\n #define USE_FOG;\n varying vec2 vUv; \n uniform vec3 uColor;\n uniform float uTime;\n #include