diff --git a/public/r/Ballpit-JS-CSS.json b/public/r/Ballpit-JS-CSS.json index fcadf27a..2e93d52b 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 { useEffect, useRef } from 'react';\nimport {\n Vector3 as a,\n MeshPhysicalMaterial as c,\n InstancedMesh as d,\n Timer 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.#c.update();\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.reset();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\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.#c.dispose();\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 updateConfig(newProps) {\n if (newProps.count !== undefined && newProps.count !== s.config.count) {\n initialize({ ...s.config, ...newProps });\n } else {\n Object.assign(s.config, newProps);\n if (newProps.colors) {\n s.setColors(s.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n s.physics.setSizes();\n }\n }\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 const isFirstRender = useRef(true);\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 spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\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 Timer 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 // Bind once: `.bind()` returns a new function on every call, so binding again\n // in the teardown would hand removeEventListener a function that was never\n // registered, leaving the listener attached for the lifetime of the page.\n #boundResize = this.#f.bind(this);\n #boundVisibilityChange = this.#v.bind(this);\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.#boundResize);\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.#boundVisibilityChange);\n }\n #y() {\n window.removeEventListener('resize', this.#boundResize);\n this.#r?.disconnect();\n this.#o?.disconnect();\n document.removeEventListener('visibilitychange', this.#boundVisibilityChange);\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.#c.update();\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.reset();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\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.#c.dispose();\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 updateConfig(newProps) {\n if (newProps.count !== undefined && newProps.count !== s.config.count) {\n initialize({ ...s.config, ...newProps });\n } else {\n Object.assign(s.config, newProps);\n if (newProps.colors) {\n s.setColors(s.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n s.physics.setSizes();\n }\n }\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 const isFirstRender = useRef(true);\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 spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\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 894d7e5a..78291514 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 { useEffect, useRef } from 'react';\nimport {\n Vector3 as a,\n MeshPhysicalMaterial as c,\n InstancedMesh as d,\n Timer 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.#c.update();\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.reset();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\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.#c.dispose();\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 updateConfig(newProps) {\n if (newProps.count !== undefined && newProps.count !== s.config.count) {\n initialize({ ...s.config, ...newProps });\n } else {\n Object.assign(s.config, newProps);\n if (newProps.colors) {\n s.setColors(s.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n s.physics.setSizes();\n }\n }\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 const isFirstRender = useRef(true);\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 spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\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 Timer 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 // Bind once: `.bind()` returns a new function on every call, so binding again\n // in the teardown would hand removeEventListener a function that was never\n // registered, leaving the listener attached for the lifetime of the page.\n #boundResize = this.#f.bind(this);\n #boundVisibilityChange = this.#v.bind(this);\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.#boundResize);\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.#boundVisibilityChange);\n }\n #y() {\n window.removeEventListener('resize', this.#boundResize);\n this.#r?.disconnect();\n this.#o?.disconnect();\n document.removeEventListener('visibilitychange', this.#boundVisibilityChange);\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.#c.update();\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.reset();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\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.#c.dispose();\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 updateConfig(newProps) {\n if (newProps.count !== undefined && newProps.count !== s.config.count) {\n initialize({ ...s.config, ...newProps });\n } else {\n Object.assign(s.config, newProps);\n if (newProps.colors) {\n s.setColors(s.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n s.physics.setSizes();\n }\n }\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 const isFirstRender = useRef(true);\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 spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\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 e839042c..91e5864f 100644 --- a/public/r/Ballpit-TS-CSS.json +++ b/public/r/Ballpit-TS-CSS.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Ballpit/Ballpit.tsx", - "content": "import { gsap } from 'gsap';\nimport { Observer } from 'gsap/Observer';\nimport React, { useEffect, useRef } from 'react';\nimport {\n ACESFilmicToneMapping,\n AmbientLight,\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 Timer,\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 #timer: Timer = new Timer();\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.#timer.update();\n this.#animationState.delta = this.#timer.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.#timer.reset();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\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.#timer.dispose();\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 updateConfig: (newProps: { [key: string]: any }) => 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 updateConfig(newProps: { [key: string]: any }) {\n if (newProps.count !== undefined && newProps.count !== spheres.config.count) {\n initialize({ ...spheres.config, ...newProps });\n } else {\n Object.assign(spheres.config, newProps);\n if (newProps.colors) {\n spheres.setColors(spheres.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n spheres.physics.setSizes();\n }\n }\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 const isFirstRender = useRef(true);\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 spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\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 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 Timer,\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 #timer: Timer = new Timer();\n #animationState = { elapsed: 0, delta: 0 };\n #isAnimating: boolean = false;\n #isVisible: boolean = false;\n // Bind once: `.bind()` returns a new function on every call, so binding again\n // in the teardown would hand removeEventListener a function that was never\n // registered, leaving the listener attached for the lifetime of the page.\n #boundResize = this.#onResize.bind(this);\n #boundVisibilityChange = this.#onVisibilityChange.bind(this);\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.#boundResize);\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.#boundVisibilityChange);\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.#timer.update();\n this.#animationState.delta = this.#timer.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.#timer.reset();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\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.#timer.dispose();\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.#boundResize);\n this.#resizeObserver?.disconnect();\n this.#intersectionObserver?.disconnect();\n document.removeEventListener('visibilitychange', this.#boundVisibilityChange);\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 updateConfig: (newProps: { [key: string]: any }) => 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 updateConfig(newProps: { [key: string]: any }) {\n if (newProps.count !== undefined && newProps.count !== spheres.config.count) {\n initialize({ ...spheres.config, ...newProps });\n } else {\n Object.assign(spheres.config, newProps);\n if (newProps.colors) {\n spheres.setColors(spheres.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n spheres.physics.setSizes();\n }\n }\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 const isFirstRender = useRef(true);\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 spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\n\n return ;\n};\n\nexport default Ballpit;\n" } ], "registryDependencies": [], diff --git a/public/r/Ballpit-TS-TW.json b/public/r/Ballpit-TS-TW.json index d52651c2..1a9129ff 100644 --- a/public/r/Ballpit-TS-TW.json +++ b/public/r/Ballpit-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Ballpit/Ballpit.tsx", - "content": "import { gsap } from 'gsap';\nimport { Observer } from 'gsap/Observer';\nimport React, { useEffect, useRef } from 'react';\nimport {\n ACESFilmicToneMapping,\n AmbientLight,\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 Timer,\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 #timer: Timer = new Timer();\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.#timer.update();\n this.#animationState.delta = this.#timer.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.#timer.reset();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\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.#timer.dispose();\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 updateConfig: (newProps: { [key: string]: any }) => 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 updateConfig(newProps: { [key: string]: any }) {\n if (newProps.count !== undefined && newProps.count !== spheres.config.count) {\n initialize({ ...spheres.config, ...newProps });\n } else {\n Object.assign(spheres.config, newProps);\n if (newProps.colors) {\n spheres.setColors(spheres.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n spheres.physics.setSizes();\n }\n }\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 const isFirstRender = useRef(true);\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 spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\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 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 Timer,\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 #timer: Timer = new Timer();\n #animationState = { elapsed: 0, delta: 0 };\n #isAnimating: boolean = false;\n #isVisible: boolean = false;\n // Bind once: `.bind()` returns a new function on every call, so binding again\n // in the teardown would hand removeEventListener a function that was never\n // registered, leaving the listener attached for the lifetime of the page.\n #boundResize = this.#onResize.bind(this);\n #boundVisibilityChange = this.#onVisibilityChange.bind(this);\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.#boundResize);\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.#boundVisibilityChange);\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.#timer.update();\n this.#animationState.delta = this.#timer.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.#timer.reset();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\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.#timer.dispose();\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.#boundResize);\n this.#resizeObserver?.disconnect();\n this.#intersectionObserver?.disconnect();\n document.removeEventListener('visibilitychange', this.#boundVisibilityChange);\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 updateConfig: (newProps: { [key: string]: any }) => 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 updateConfig(newProps: { [key: string]: any }) {\n if (newProps.count !== undefined && newProps.count !== spheres.config.count) {\n initialize({ ...spheres.config, ...newProps });\n } else {\n Object.assign(spheres.config, newProps);\n if (newProps.colors) {\n spheres.setColors(spheres.config.colors);\n }\n if (newProps.minSize !== undefined || newProps.maxSize !== undefined || newProps.size0 !== undefined) {\n spheres.physics.setSizes();\n }\n }\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 const isFirstRender = useRef(true);\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 spheresInstanceRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false;\n return;\n }\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.updateConfig({ followCursor, ...props });\n }\n }, [props, followCursor]);\n\n return ;\n};\n\nexport default Ballpit;\n" } ], "registryDependencies": [], diff --git a/src/content/Backgrounds/Ballpit/Ballpit.jsx b/src/content/Backgrounds/Ballpit/Ballpit.jsx index e9ee9e24..cb06451f 100644 --- a/src/content/Backgrounds/Ballpit/Ballpit.jsx +++ b/src/content/Backgrounds/Ballpit/Ballpit.jsx @@ -42,6 +42,11 @@ class x { onAfterResize = () => {}; #s = false; #n = false; + // Bind once: `.bind()` returns a new function on every call, so binding again + // in the teardown would hand removeEventListener a function that was never + // registered, leaving the listener attached for the lifetime of the page. + #boundResize = this.#f.bind(this); + #boundVisibilityChange = this.#v.bind(this); isDisposed = false; #o; #r; @@ -83,7 +88,7 @@ class x { } #g() { if (!(this.#e.size instanceof Object)) { - window.addEventListener('resize', this.#f.bind(this)); + window.addEventListener('resize', this.#boundResize); if (this.#e.size === 'parent' && this.canvas.parentNode) { this.#r = new ResizeObserver(this.#f.bind(this)); this.#r.observe(this.canvas.parentNode); @@ -95,13 +100,13 @@ class x { threshold: 0 }); this.#o.observe(this.canvas); - document.addEventListener('visibilitychange', this.#v.bind(this)); + document.addEventListener('visibilitychange', this.#boundVisibilityChange); } #y() { - window.removeEventListener('resize', this.#f.bind(this)); + window.removeEventListener('resize', this.#boundResize); this.#r?.disconnect(); this.#o?.disconnect(); - document.removeEventListener('visibilitychange', this.#v.bind(this)); + document.removeEventListener('visibilitychange', this.#boundVisibilityChange); } #u(e) { this.#s = e[0].isIntersecting; diff --git a/src/tailwind/Backgrounds/Ballpit/Ballpit.jsx b/src/tailwind/Backgrounds/Ballpit/Ballpit.jsx index 7b932685..9ff03114 100644 --- a/src/tailwind/Backgrounds/Ballpit/Ballpit.jsx +++ b/src/tailwind/Backgrounds/Ballpit/Ballpit.jsx @@ -42,6 +42,11 @@ class x { onAfterResize = () => {}; #s = false; #n = false; + // Bind once: `.bind()` returns a new function on every call, so binding again + // in the teardown would hand removeEventListener a function that was never + // registered, leaving the listener attached for the lifetime of the page. + #boundResize = this.#f.bind(this); + #boundVisibilityChange = this.#v.bind(this); isDisposed = false; #o; #r; @@ -83,7 +88,7 @@ class x { } #g() { if (!(this.#e.size instanceof Object)) { - window.addEventListener('resize', this.#f.bind(this)); + window.addEventListener('resize', this.#boundResize); if (this.#e.size === 'parent' && this.canvas.parentNode) { this.#r = new ResizeObserver(this.#f.bind(this)); this.#r.observe(this.canvas.parentNode); @@ -95,13 +100,13 @@ class x { threshold: 0 }); this.#o.observe(this.canvas); - document.addEventListener('visibilitychange', this.#v.bind(this)); + document.addEventListener('visibilitychange', this.#boundVisibilityChange); } #y() { - window.removeEventListener('resize', this.#f.bind(this)); + window.removeEventListener('resize', this.#boundResize); this.#r?.disconnect(); this.#o?.disconnect(); - document.removeEventListener('visibilitychange', this.#v.bind(this)); + document.removeEventListener('visibilitychange', this.#boundVisibilityChange); } #u(e) { this.#s = e[0].isIntersecting; diff --git a/src/ts-default/Backgrounds/Ballpit/Ballpit.tsx b/src/ts-default/Backgrounds/Ballpit/Ballpit.tsx index 3f9700f6..52a82d48 100644 --- a/src/ts-default/Backgrounds/Ballpit/Ballpit.tsx +++ b/src/ts-default/Backgrounds/Ballpit/Ballpit.tsx @@ -55,6 +55,11 @@ class X { #animationState = { elapsed: 0, delta: 0 }; #isAnimating: boolean = false; #isVisible: boolean = false; + // Bind once: `.bind()` returns a new function on every call, so binding again + // in the teardown would hand removeEventListener a function that was never + // registered, leaving the listener attached for the lifetime of the page. + #boundResize = this.#onResize.bind(this); + #boundVisibilityChange = this.#onVisibilityChange.bind(this); canvas!: HTMLCanvasElement; camera!: PerspectiveCamera; @@ -123,7 +128,7 @@ class X { #initObservers() { if (!(this.#config.size instanceof Object)) { - window.addEventListener('resize', this.#onResize.bind(this)); + window.addEventListener('resize', this.#boundResize); if (this.#config.size === 'parent' && this.canvas.parentNode) { this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this)); this.#resizeObserver.observe(this.canvas.parentNode as Element); @@ -135,7 +140,7 @@ class X { threshold: 0 }); this.#intersectionObserver.observe(this.canvas); - document.addEventListener('visibilitychange', this.#onVisibilityChange.bind(this)); + document.addEventListener('visibilitychange', this.#boundVisibilityChange); } #onResize() { @@ -283,10 +288,10 @@ class X { } #onResizeCleanup() { - window.removeEventListener('resize', this.#onResize.bind(this)); + window.removeEventListener('resize', this.#boundResize); this.#resizeObserver?.disconnect(); this.#intersectionObserver?.disconnect(); - document.removeEventListener('visibilitychange', this.#onVisibilityChange.bind(this)); + document.removeEventListener('visibilitychange', this.#boundVisibilityChange); } } diff --git a/src/ts-tailwind/Backgrounds/Ballpit/Ballpit.tsx b/src/ts-tailwind/Backgrounds/Ballpit/Ballpit.tsx index 05eb478e..30078ad1 100644 --- a/src/ts-tailwind/Backgrounds/Ballpit/Ballpit.tsx +++ b/src/ts-tailwind/Backgrounds/Ballpit/Ballpit.tsx @@ -55,6 +55,11 @@ class X { #animationState = { elapsed: 0, delta: 0 }; #isAnimating: boolean = false; #isVisible: boolean = false; + // Bind once: `.bind()` returns a new function on every call, so binding again + // in the teardown would hand removeEventListener a function that was never + // registered, leaving the listener attached for the lifetime of the page. + #boundResize = this.#onResize.bind(this); + #boundVisibilityChange = this.#onVisibilityChange.bind(this); canvas!: HTMLCanvasElement; camera!: PerspectiveCamera; @@ -123,7 +128,7 @@ class X { #initObservers() { if (!(this.#config.size instanceof Object)) { - window.addEventListener('resize', this.#onResize.bind(this)); + window.addEventListener('resize', this.#boundResize); if (this.#config.size === 'parent' && this.canvas.parentNode) { this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this)); this.#resizeObserver.observe(this.canvas.parentNode as Element); @@ -135,7 +140,7 @@ class X { threshold: 0 }); this.#intersectionObserver.observe(this.canvas); - document.addEventListener('visibilitychange', this.#onVisibilityChange.bind(this)); + document.addEventListener('visibilitychange', this.#boundVisibilityChange); } #onResize() { @@ -283,10 +288,10 @@ class X { } #onResizeCleanup() { - window.removeEventListener('resize', this.#onResize.bind(this)); + window.removeEventListener('resize', this.#boundResize); this.#resizeObserver?.disconnect(); this.#intersectionObserver?.disconnect(); - document.removeEventListener('visibilitychange', this.#onVisibilityChange.bind(this)); + document.removeEventListener('visibilitychange', this.#boundVisibilityChange); } }