From 0c517d64c24c0657b1c5bd3aa6dfe46719c68c48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:45:02 +0000 Subject: [PATCH 1/2] Initial plan From be695d226d4d76fbe3494f258a8c03958cb441ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:58:31 +0000 Subject: [PATCH 2/2] Migrate Ballpit from deprecated THREE.Clock to THREE.Timer --- package-lock.json | 8 +- package.json | 2 +- public/r/ASCIIText-JS-CSS.json | 2 +- public/r/ASCIIText-JS-TW.json | 2 +- public/r/ASCIIText-TS-CSS.json | 2 +- public/r/ASCIIText-TS-TW.json | 2 +- public/r/Antigravity-JS-CSS.json | 2 +- public/r/Antigravity-JS-TW.json | 2 +- public/r/Antigravity-TS-CSS.json | 2 +- public/r/Antigravity-TS-TW.json | 2 +- public/r/Ballpit-JS-CSS.json | 4 +- public/r/Ballpit-JS-TW.json | 4 +- public/r/Ballpit-TS-CSS.json | 4 +- public/r/Ballpit-TS-TW.json | 4 +- public/r/Beams-JS-CSS.json | 2 +- public/r/Beams-JS-TW.json | 2 +- public/r/Beams-TS-CSS.json | 2 +- public/r/Beams-TS-TW.json | 2 +- public/r/Carousel-JS-CSS.json | 4 +- public/r/Carousel-JS-TW.json | 2 +- public/r/Carousel-TS-CSS.json | 4 +- public/r/Carousel-TS-TW.json | 2 +- public/r/ColorBends-JS-CSS.json | 2 +- public/r/ColorBends-JS-TW.json | 2 +- public/r/ColorBends-TS-CSS.json | 2 +- public/r/ColorBends-TS-TW.json | 2 +- public/r/Dither-JS-CSS.json | 2 +- public/r/Dither-JS-TW.json | 2 +- public/r/Dither-TS-CSS.json | 2 +- public/r/Dither-TS-TW.json | 2 +- public/r/FloatingLines-JS-CSS.json | 2 +- public/r/FloatingLines-JS-TW.json | 2 +- public/r/FloatingLines-TS-CSS.json | 2 +- public/r/FloatingLines-TS-TW.json | 2 +- public/r/FluidGlass-JS-CSS.json | 2 +- public/r/FluidGlass-JS-TW.json | 2 +- public/r/FluidGlass-TS-CSS.json | 2 +- public/r/FluidGlass-TS-TW.json | 2 +- public/r/GhostCursor-JS-CSS.json | 2 +- public/r/GhostCursor-JS-TW.json | 2 +- public/r/GhostCursor-TS-CSS.json | 2 +- public/r/GhostCursor-TS-TW.json | 2 +- public/r/GridDistortion-JS-CSS.json | 2 +- public/r/GridDistortion-JS-TW.json | 2 +- public/r/GridDistortion-TS-CSS.json | 2 +- public/r/GridDistortion-TS-TW.json | 2 +- public/r/GridScan-JS-CSS.json | 2 +- public/r/GridScan-JS-TW.json | 2 +- public/r/GridScan-TS-CSS.json | 2 +- public/r/GridScan-TS-TW.json | 2 +- public/r/Hyperspeed-JS-CSS.json | 2 +- public/r/Hyperspeed-JS-TW.json | 2 +- public/r/Hyperspeed-TS-CSS.json | 2 +- public/r/Hyperspeed-TS-TW.json | 2 +- public/r/Lanyard-JS-CSS.json | 2 +- public/r/Lanyard-JS-TW.json | 2 +- public/r/Lanyard-TS-CSS.json | 2 +- public/r/Lanyard-TS-TW.json | 2 +- public/r/LaserFlow-JS-CSS.json | 2 +- public/r/LaserFlow-JS-TW.json | 2 +- public/r/LaserFlow-TS-CSS.json | 2 +- public/r/LaserFlow-TS-TW.json | 2 +- public/r/LightPillar-JS-CSS.json | 2 +- public/r/LightPillar-JS-TW.json | 2 +- public/r/LightPillar-TS-CSS.json | 2 +- public/r/LightPillar-TS-TW.json | 2 +- public/r/LiquidEther-JS-CSS.json | 2 +- public/r/LiquidEther-JS-TW.json | 2 +- public/r/LiquidEther-TS-CSS.json | 2 +- public/r/LiquidEther-TS-TW.json | 2 +- public/r/MagicRings-JS-CSS.json | 2 +- public/r/MagicRings-JS-TW.json | 2 +- public/r/MagicRings-TS-CSS.json | 2 +- public/r/MagicRings-TS-TW.json | 2 +- public/r/ModelViewer-JS-CSS.json | 2 +- public/r/ModelViewer-JS-TW.json | 2 +- public/r/ModelViewer-TS-CSS.json | 2 +- public/r/ModelViewer-TS-TW.json | 2 +- public/r/PixelBlast-JS-CSS.json | 2 +- public/r/PixelBlast-JS-TW.json | 2 +- public/r/PixelBlast-TS-CSS.json | 2 +- public/r/PixelBlast-TS-TW.json | 2 +- public/r/PixelSnow-JS-CSS.json | 2 +- public/r/PixelSnow-JS-TW.json | 2 +- public/r/PixelSnow-TS-CSS.json | 2 +- public/r/PixelSnow-TS-TW.json | 2 +- public/r/PixelTrail-JS-CSS.json | 2 +- public/r/PixelTrail-JS-TW.json | 2 +- public/r/PixelTrail-TS-CSS.json | 2 +- public/r/PixelTrail-TS-TW.json | 2 +- public/r/ShapeBlur-JS-CSS.json | 2 +- public/r/ShapeBlur-JS-TW.json | 2 +- public/r/ShapeBlur-TS-CSS.json | 2 +- public/r/ShapeBlur-TS-TW.json | 2 +- public/r/Silk-JS-CSS.json | 2 +- public/r/Silk-JS-TW.json | 2 +- public/r/Silk-TS-CSS.json | 2 +- public/r/Silk-TS-TW.json | 2 +- public/r/registry.json | 176 +++++++++--------- src/content/Backgrounds/Ballpit/Ballpit.jsx | 7 +- src/tailwind/Backgrounds/Ballpit/Ballpit.jsx | 7 +- .../Backgrounds/Ballpit/Ballpit.tsx | 11 +- .../Backgrounds/Ballpit/Ballpit.tsx | 11 +- 103 files changed, 215 insertions(+), 211 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1d844a3c4..8281c7ee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "sonner": "^1.7.1", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.0.3", - "three": "^0.167.1" + "three": "^0.180.0" }, "devDependencies": { "@jsrepo/shadcn": "^2.0.0", @@ -9196,9 +9196,9 @@ "license": "MIT" }, "node_modules/three": { - "version": "0.167.1", - "resolved": "https://registry.npmjs.org/three/-/three-0.167.1.tgz", - "integrity": "sha512-gYTLJA/UQip6J/tJvl91YYqlZF47+D/kxiWrbTon35ZHlXEN0VOo+Qke2walF1/x92v55H6enomymg4Dak52kw==", + "version": "0.180.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", + "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", "license": "MIT" }, "node_modules/three-mesh-bvh": { diff --git a/package.json b/package.json index 1ad1290d5..80b5f7fb2 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "sonner": "^1.7.1", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.0.3", - "three": "^0.167.1" + "three": "^0.180.0" }, "devDependencies": { "@jsrepo/shadcn": "^2.0.0", diff --git a/public/r/ASCIIText-JS-CSS.json b/public/r/ASCIIText-JS-CSS.json index 0588c795a..1880c60e7 100644 --- a/public/r/ASCIIText-JS-CSS.json +++ b/public/r/ASCIIText-JS-CSS.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ASCIIText-JS-TW.json b/public/r/ASCIIText-JS-TW.json index d34e9d39f..d9d414c5e 100644 --- a/public/r/ASCIIText-JS-TW.json +++ b/public/r/ASCIIText-JS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ASCIIText-TS-CSS.json b/public/r/ASCIIText-TS-CSS.json index c298787bd..c64e256a9 100644 --- a/public/r/ASCIIText-TS-CSS.json +++ b/public/r/ASCIIText-TS-CSS.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ASCIIText-TS-TW.json b/public/r/ASCIIText-TS-TW.json index bb16ee55d..c4ab356b7 100644 --- a/public/r/ASCIIText-TS-TW.json +++ b/public/r/ASCIIText-TS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Antigravity-JS-CSS.json b/public/r/Antigravity-JS-CSS.json index 389f20d62..d74a6ffe8 100644 --- a/public/r/Antigravity-JS-CSS.json +++ b/public/r/Antigravity-JS-CSS.json @@ -14,6 +14,6 @@ "registryDependencies": [], "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Antigravity-JS-TW.json b/public/r/Antigravity-JS-TW.json index f914bbd60..3bd8f6558 100644 --- a/public/r/Antigravity-JS-TW.json +++ b/public/r/Antigravity-JS-TW.json @@ -14,6 +14,6 @@ "registryDependencies": [], "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Antigravity-TS-CSS.json b/public/r/Antigravity-TS-CSS.json index 330e70638..96b56c961 100644 --- a/public/r/Antigravity-TS-CSS.json +++ b/public/r/Antigravity-TS-CSS.json @@ -14,6 +14,6 @@ "registryDependencies": [], "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Antigravity-TS-TW.json b/public/r/Antigravity-TS-TW.json index 62202fda2..f044f52dc 100644 --- a/public/r/Antigravity-TS-TW.json +++ b/public/r/Antigravity-TS-TW.json @@ -14,6 +14,6 @@ "registryDependencies": [], "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Ballpit-JS-CSS.json b/public/r/Ballpit-JS-CSS.json index 6779a2e1a..36a374698 100644 --- a/public/r/Ballpit-JS-CSS.json +++ b/public/r/Ballpit-JS-CSS.json @@ -8,11 +8,11 @@ { "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 Clock as e,\n AmbientLight as f,\n SphereGeometry as g,\n ShaderChunk as h,\n Scene as i,\n Color as l,\n Object3D as m,\n SRGBColorSpace as n,\n MathUtils as o,\n PMREMGenerator as p,\n Vector2 as r,\n WebGLRenderer as s,\n PerspectiveCamera as t,\n PointLight as u,\n ACESFilmicToneMapping as v,\n Plane as w,\n Raycaster as y\n} from 'three';\nimport { RoomEnvironment as z } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\nclass x {\n #e;\n canvas;\n camera;\n cameraMinAspect;\n cameraMaxAspect;\n cameraFov;\n maxPixelRatio;\n minPixelRatio;\n scene;\n renderer;\n #t;\n size = { width: 0, height: 0, wWidth: 0, wHeight: 0, ratio: 0, pixelRatio: 0 };\n render = this.#i;\n onBeforeRender = () => {};\n onAfterRender = () => {};\n onAfterResize = () => {};\n #s = false;\n #n = false;\n isDisposed = false;\n #o;\n #r;\n #a;\n #c = new e();\n #h = { elapsed: 0, delta: 0 };\n #l;\n constructor(e) {\n this.#e = { ...e };\n this.#m();\n this.#d();\n this.#p();\n this.resize();\n this.#g();\n }\n #m() {\n this.camera = new t();\n this.cameraFov = this.camera.fov;\n }\n #d() {\n this.scene = new i();\n }\n #p() {\n if (this.#e.canvas) {\n this.canvas = this.#e.canvas;\n } else if (this.#e.id) {\n this.canvas = document.getElementById(this.#e.id);\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas.style.display = 'block';\n const e = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#e.rendererOptions ?? {})\n };\n this.renderer = new s(e);\n this.renderer.outputColorSpace = n;\n }\n #g() {\n if (!(this.#e.size instanceof Object)) {\n window.addEventListener('resize', this.#f.bind(this));\n if (this.#e.size === 'parent' && this.canvas.parentNode) {\n this.#r = new ResizeObserver(this.#f.bind(this));\n this.#r.observe(this.canvas.parentNode);\n }\n }\n this.#o = new IntersectionObserver(this.#u.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#o.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#v.bind(this));\n }\n #y() {\n window.removeEventListener('resize', this.#f.bind(this));\n this.#r?.disconnect();\n this.#o?.disconnect();\n document.removeEventListener('visibilitychange', this.#v.bind(this));\n }\n #u(e) {\n this.#s = e[0].isIntersecting;\n this.#s ? this.#w() : this.#z();\n }\n #v() {\n if (this.#s) {\n document.hidden ? this.#z() : this.#w();\n }\n }\n #f() {\n if (this.#a) clearTimeout(this.#a);\n this.#a = setTimeout(this.resize.bind(this), 100);\n }\n resize() {\n let e, t;\n if (this.#e.size instanceof Object) {\n e = this.#e.size.width;\n t = this.#e.size.height;\n } else if (this.#e.size === 'parent' && this.canvas.parentNode) {\n e = this.canvas.parentNode.offsetWidth;\n t = this.canvas.parentNode.offsetHeight;\n } else {\n e = window.innerWidth;\n t = window.innerHeight;\n }\n this.size.width = e;\n this.size.height = t;\n this.size.ratio = e / t;\n this.#x();\n this.#b();\n this.onAfterResize(this.size);\n }\n #x() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#A(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#A(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n #A(e) {\n const t = Math.tan(o.degToRad(this.cameraFov / 2)) / (this.camera.aspect / e);\n this.camera.fov = 2 * o.radToDeg(Math.atan(t));\n }\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const e = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(e / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if (this.camera.isOrthographicCamera) {\n this.size.wHeight = this.camera.top - this.camera.bottom;\n this.size.wWidth = this.camera.right - this.camera.left;\n }\n }\n #b() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#t?.setSize(this.size.width, this.size.height);\n let e = window.devicePixelRatio;\n if (this.maxPixelRatio && e > this.maxPixelRatio) {\n e = this.maxPixelRatio;\n } else if (this.minPixelRatio && e < this.minPixelRatio) {\n e = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(e);\n this.size.pixelRatio = e;\n }\n get postprocessing() {\n return this.#t;\n }\n set postprocessing(e) {\n this.#t = e;\n this.render = e.render.bind(e);\n }\n #w() {\n if (this.#n) return;\n const animate = () => {\n this.#l = requestAnimationFrame(animate);\n this.#h.delta = this.#c.getDelta();\n this.#h.elapsed += this.#h.delta;\n this.onBeforeRender(this.#h);\n this.render();\n this.onAfterRender(this.#h);\n };\n this.#n = true;\n this.#c.start();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\n this.#c.stop();\n }\n }\n #i() {\n this.renderer.render(this.scene, this.camera);\n }\n clear() {\n this.scene.traverse(e => {\n if (e.isMesh && typeof e.material === 'object' && e.material !== null) {\n Object.keys(e.material).forEach(t => {\n const i = e.material[t];\n if (i !== null && typeof i === 'object' && typeof i.dispose === 'function') {\n i.dispose();\n }\n });\n e.material.dispose();\n e.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n dispose() {\n this.#y();\n this.#z();\n this.clear();\n this.#t?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n}\n\nconst b = new Map(),\n A = new r();\nlet R = false;\nfunction S(e) {\n const t = {\n position: new r(),\n nPosition: new r(),\n hover: false,\n touching: false,\n onEnter() {},\n onMove() {},\n onClick() {},\n onLeave() {},\n ...e\n };\n (function (e, t) {\n if (!b.has(e)) {\n b.set(e, t);\n if (!R) {\n document.body.addEventListener('pointermove', M);\n document.body.addEventListener('pointerleave', L);\n document.body.addEventListener('click', C);\n\n document.body.addEventListener('touchstart', TouchStart, { passive: false });\n document.body.addEventListener('touchmove', TouchMove, { passive: false });\n document.body.addEventListener('touchend', TouchEnd, { passive: false });\n document.body.addEventListener('touchcancel', TouchEnd, { passive: false });\n\n R = true;\n }\n }\n })(e.domElement, t);\n t.dispose = () => {\n const t = e.domElement;\n b.delete(t);\n if (b.size === 0) {\n document.body.removeEventListener('pointermove', M);\n document.body.removeEventListener('pointerleave', L);\n document.body.removeEventListener('click', C);\n\n document.body.removeEventListener('touchstart', TouchStart);\n document.body.removeEventListener('touchmove', TouchMove);\n document.body.removeEventListener('touchend', TouchEnd);\n document.body.removeEventListener('touchcancel', TouchEnd);\n\n R = false;\n }\n };\n return t;\n}\n\nfunction M(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n processInteraction();\n}\n\nfunction processInteraction() {\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n if (D(i)) {\n P(t, i);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && !t.touching) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction C(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n P(t, i);\n if (D(i)) t.onClick(t);\n }\n}\n\nfunction L() {\n for (const t of b.values()) {\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction TouchStart(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n if (D(rect)) {\n t.touching = true;\n P(t, rect);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchMove(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n P(t, rect);\n\n if (D(rect)) {\n if (!t.hover) {\n t.hover = true;\n t.touching = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && t.touching) {\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchEnd() {\n for (const [, t] of b) {\n if (t.touching) {\n t.touching = false;\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n }\n}\n\nfunction P(e, t) {\n const { position: i, nPosition: s } = e;\n i.x = A.x - t.left;\n i.y = A.y - t.top;\n s.x = (i.x / t.width) * 2 - 1;\n s.y = (-i.y / t.height) * 2 + 1;\n}\nfunction D(e) {\n const { x: t, y: i } = A;\n const { left: s, top: n, width: o, height: r } = e;\n return t >= s && t <= s + o && i >= n && i <= n + r;\n}\n\nconst { randFloat: k, randFloatSpread: E } = o;\nconst F = new a();\nconst I = new a();\nconst O = new a();\nconst V = new a();\nconst B = new a();\nconst N = new a();\nconst _ = new a();\nconst j = new a();\nconst H = new a();\nconst T = new a();\n\nclass W {\n constructor(e) {\n this.config = e;\n this.positionData = new Float32Array(3 * e.count).fill(0);\n this.velocityData = new Float32Array(3 * e.count).fill(0);\n this.sizeData = new Float32Array(e.count).fill(1);\n this.center = new a();\n this.#R();\n this.setSizes();\n }\n #R() {\n const { config: e, positionData: t } = this;\n this.center.toArray(t, 0);\n for (let i = 1; i < e.count; i++) {\n const s = 3 * i;\n t[s] = E(2 * e.maxX);\n t[s + 1] = E(2 * e.maxY);\n t[s + 2] = E(2 * e.maxZ);\n }\n }\n setSizes() {\n const { config: e, sizeData: t } = this;\n t[0] = e.size0;\n for (let i = 1; i < e.count; i++) {\n t[i] = k(e.minSize, e.maxSize);\n }\n }\n update(e) {\n const { config: t, center: i, positionData: s, sizeData: n, velocityData: o } = this;\n let r = 0;\n if (t.controlSphere0) {\n r = 1;\n F.fromArray(s, 0);\n F.lerp(i, 0.1).toArray(s, 0);\n V.set(0, 0, 0).toArray(o, 0);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n B.y -= e.delta * t.gravity * n[idx];\n B.multiplyScalar(t.friction);\n B.clampLength(0, t.maxVelocity);\n I.add(B);\n I.toArray(s, base);\n B.toArray(o, base);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n const radius = n[idx];\n for (let jdx = idx + 1; jdx < t.count; jdx++) {\n const otherBase = 3 * jdx;\n O.fromArray(s, otherBase);\n N.fromArray(o, otherBase);\n const otherRadius = n[jdx];\n _.copy(O).sub(I);\n const dist = _.length();\n const sumRadius = radius + otherRadius;\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n j.copy(_)\n .normalize()\n .multiplyScalar(0.5 * overlap);\n H.copy(j).multiplyScalar(Math.max(B.length(), 1));\n T.copy(j).multiplyScalar(Math.max(N.length(), 1));\n I.sub(j);\n B.sub(H);\n I.toArray(s, base);\n B.toArray(o, base);\n O.add(j);\n N.add(T);\n O.toArray(s, otherBase);\n N.toArray(o, otherBase);\n }\n }\n if (t.controlSphere0) {\n _.copy(F).sub(I);\n const dist = _.length();\n const sumRadius0 = radius + n[0];\n if (dist < sumRadius0) {\n const diff = sumRadius0 - dist;\n j.copy(_.normalize()).multiplyScalar(diff);\n H.copy(j).multiplyScalar(Math.max(B.length(), 2));\n I.sub(j);\n B.sub(H);\n }\n }\n if (Math.abs(I.x) + radius > t.maxX) {\n I.x = Math.sign(I.x) * (t.maxX - radius);\n B.x = -B.x * t.wallBounce;\n }\n if (t.gravity === 0) {\n if (Math.abs(I.y) + radius > t.maxY) {\n I.y = Math.sign(I.y) * (t.maxY - radius);\n B.y = -B.y * t.wallBounce;\n }\n } else if (I.y - radius < -t.maxY) {\n I.y = -t.maxY + radius;\n B.y = -B.y * t.wallBounce;\n }\n const maxBoundary = Math.max(t.maxZ, t.maxSize);\n if (Math.abs(I.z) + radius > maxBoundary) {\n I.z = Math.sign(I.z) * (t.maxZ - radius);\n B.z = -B.z * t.wallBounce;\n }\n I.toArray(s, base);\n B.toArray(o, base);\n }\n }\n}\n\nclass Y extends c {\n constructor(e) {\n super(e);\n this.uniforms = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n this.defines.USE_UV = '';\n this.onBeforeCompile = e => {\n Object.assign(e.uniforms, this.uniforms);\n e.fragmentShader =\n '\\n uniform float thicknessPower;\\n uniform float thicknessScale;\\n uniform float thicknessDistortion;\\n uniform float thicknessAmbient;\\n uniform float thicknessAttenuation;\\n ' +\n e.fragmentShader;\n e.fragmentShader = e.fragmentShader.replace(\n 'void main() {',\n '\\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\\n #ifdef USE_COLOR\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\\n #else\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\\n #endif\\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\\n }\\n\\n void main() {\\n '\n );\n const t = h.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n '\\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\\n '\n );\n e.fragmentShader = e.fragmentShader.replace('#include ', t);\n if (this.onBeforeCompile2) this.onBeforeCompile2(e);\n };\n }\n}\n\nconst X = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 16777215,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new m();\n\nclass Z extends d {\n constructor(e, t = {}) {\n const i = { ...X, ...t };\n const s = new z();\n const n = new p(e, 0.04).fromScene(s).texture;\n const o = new g();\n const r = new Y({ envMap: n, ...i.materialParams });\n r.envMapRotation.x = -Math.PI / 2;\n super(o, r, i.count);\n this.config = i;\n this.physics = new W(i);\n this.#S();\n this.setColors(i.colors);\n }\n #S() {\n this.ambientLight = new f(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new u(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n setColors(e) {\n if (Array.isArray(e) && e.length > 1) {\n const t = (function (e) {\n let t, i;\n function setColors(e) {\n t = e;\n i = [];\n t.forEach(col => {\n i.push(new l(col));\n });\n }\n setColors(e);\n return {\n setColors,\n getColorAt: function (ratio, out = new l()) {\n const scaled = Math.max(0, Math.min(1, ratio)) * (t.length - 1);\n const idx = Math.floor(scaled);\n const start = i[idx];\n if (idx >= t.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = i[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(e);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, t.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light.color.copy(t.getColorAt(idx / this.count));\n }\n }\n this.instanceColor.needsUpdate = true;\n }\n }\n update(e) {\n this.physics.update(e);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\nfunction createBallpit(e, t = {}) {\n const i = new x({\n canvas: e,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let s;\n i.renderer.toneMapping = v;\n i.camera.position.set(0, 0, 20);\n i.camera.lookAt(0, 0, 0);\n i.cameraMaxAspect = 1.5;\n i.resize();\n initialize(t);\n const n = new y();\n const o = new w(new a(0, 0, 1), 0);\n const r = new a();\n let c = false;\n\n e.style.touchAction = 'none';\n e.style.userSelect = 'none';\n e.style.webkitUserSelect = 'none';\n\n const h = S({\n domElement: e,\n onMove() {\n n.setFromCamera(h.nPosition, i.camera);\n i.camera.getWorldDirection(o.normal);\n n.ray.intersectPlane(o, r);\n s.physics.center.copy(r);\n s.config.controlSphere0 = true;\n },\n onLeave() {\n s.config.controlSphere0 = false;\n }\n });\n function initialize(e) {\n if (s) {\n i.clear();\n i.scene.remove(s);\n }\n s = new Z(i.renderer, e);\n i.scene.add(s);\n }\n i.onBeforeRender = e => {\n if (!c) s.update(e);\n };\n i.onAfterResize = e => {\n s.config.maxX = e.wWidth / 2;\n s.config.maxY = e.wHeight / 2;\n };\n return {\n three: i,\n get spheres() {\n return s;\n },\n setCount(e) {\n initialize({ ...s.config, count: e });\n },\n togglePause() {\n c = !c;\n },\n dispose() {\n h.dispose();\n i.dispose();\n }\n };\n}\n\nconst Ballpit = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, { followCursor, ...props });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n" + "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 togglePause() {\n c = !c;\n },\n dispose() {\n h.dispose();\n i.dispose();\n }\n };\n}\n\nconst Ballpit = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, { followCursor, ...props });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n" } ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Ballpit-JS-TW.json b/public/r/Ballpit-JS-TW.json index 28fc5164e..3e696f590 100644 --- a/public/r/Ballpit-JS-TW.json +++ b/public/r/Ballpit-JS-TW.json @@ -8,11 +8,11 @@ { "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 Clock as e,\n AmbientLight as f,\n SphereGeometry as g,\n ShaderChunk as h,\n Scene as i,\n Color as l,\n Object3D as m,\n SRGBColorSpace as n,\n MathUtils as o,\n PMREMGenerator as p,\n Vector2 as r,\n WebGLRenderer as s,\n PerspectiveCamera as t,\n PointLight as u,\n ACESFilmicToneMapping as v,\n Plane as w,\n Raycaster as y\n} from 'three';\nimport { RoomEnvironment as z } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\nclass x {\n #e;\n canvas;\n camera;\n cameraMinAspect;\n cameraMaxAspect;\n cameraFov;\n maxPixelRatio;\n minPixelRatio;\n scene;\n renderer;\n #t;\n size = { width: 0, height: 0, wWidth: 0, wHeight: 0, ratio: 0, pixelRatio: 0 };\n render = this.#i;\n onBeforeRender = () => {};\n onAfterRender = () => {};\n onAfterResize = () => {};\n #s = false;\n #n = false;\n isDisposed = false;\n #o;\n #r;\n #a;\n #c = new e();\n #h = { elapsed: 0, delta: 0 };\n #l;\n constructor(e) {\n this.#e = { ...e };\n this.#m();\n this.#d();\n this.#p();\n this.resize();\n this.#g();\n }\n #m() {\n this.camera = new t();\n this.cameraFov = this.camera.fov;\n }\n #d() {\n this.scene = new i();\n }\n #p() {\n if (this.#e.canvas) {\n this.canvas = this.#e.canvas;\n } else if (this.#e.id) {\n this.canvas = document.getElementById(this.#e.id);\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas.style.display = 'block';\n const e = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#e.rendererOptions ?? {})\n };\n this.renderer = new s(e);\n this.renderer.outputColorSpace = n;\n }\n #g() {\n if (!(this.#e.size instanceof Object)) {\n window.addEventListener('resize', this.#f.bind(this));\n if (this.#e.size === 'parent' && this.canvas.parentNode) {\n this.#r = new ResizeObserver(this.#f.bind(this));\n this.#r.observe(this.canvas.parentNode);\n }\n }\n this.#o = new IntersectionObserver(this.#u.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#o.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#v.bind(this));\n }\n #y() {\n window.removeEventListener('resize', this.#f.bind(this));\n this.#r?.disconnect();\n this.#o?.disconnect();\n document.removeEventListener('visibilitychange', this.#v.bind(this));\n }\n #u(e) {\n this.#s = e[0].isIntersecting;\n this.#s ? this.#w() : this.#z();\n }\n #v() {\n if (this.#s) {\n document.hidden ? this.#z() : this.#w();\n }\n }\n #f() {\n if (this.#a) clearTimeout(this.#a);\n this.#a = setTimeout(this.resize.bind(this), 100);\n }\n resize() {\n let e, t;\n if (this.#e.size instanceof Object) {\n e = this.#e.size.width;\n t = this.#e.size.height;\n } else if (this.#e.size === 'parent' && this.canvas.parentNode) {\n e = this.canvas.parentNode.offsetWidth;\n t = this.canvas.parentNode.offsetHeight;\n } else {\n e = window.innerWidth;\n t = window.innerHeight;\n }\n this.size.width = e;\n this.size.height = t;\n this.size.ratio = e / t;\n this.#x();\n this.#b();\n this.onAfterResize(this.size);\n }\n #x() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#A(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#A(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n #A(e) {\n const t = Math.tan(o.degToRad(this.cameraFov / 2)) / (this.camera.aspect / e);\n this.camera.fov = 2 * o.radToDeg(Math.atan(t));\n }\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const e = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(e / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if (this.camera.isOrthographicCamera) {\n this.size.wHeight = this.camera.top - this.camera.bottom;\n this.size.wWidth = this.camera.right - this.camera.left;\n }\n }\n #b() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#t?.setSize(this.size.width, this.size.height);\n let e = window.devicePixelRatio;\n if (this.maxPixelRatio && e > this.maxPixelRatio) {\n e = this.maxPixelRatio;\n } else if (this.minPixelRatio && e < this.minPixelRatio) {\n e = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(e);\n this.size.pixelRatio = e;\n }\n get postprocessing() {\n return this.#t;\n }\n set postprocessing(e) {\n this.#t = e;\n this.render = e.render.bind(e);\n }\n #w() {\n if (this.#n) return;\n const animate = () => {\n this.#l = requestAnimationFrame(animate);\n this.#h.delta = this.#c.getDelta();\n this.#h.elapsed += this.#h.delta;\n this.onBeforeRender(this.#h);\n this.render();\n this.onAfterRender(this.#h);\n };\n this.#n = true;\n this.#c.start();\n animate();\n }\n #z() {\n if (this.#n) {\n cancelAnimationFrame(this.#l);\n this.#n = false;\n this.#c.stop();\n }\n }\n #i() {\n this.renderer.render(this.scene, this.camera);\n }\n clear() {\n this.scene.traverse(e => {\n if (e.isMesh && typeof e.material === 'object' && e.material !== null) {\n Object.keys(e.material).forEach(t => {\n const i = e.material[t];\n if (i !== null && typeof i === 'object' && typeof i.dispose === 'function') {\n i.dispose();\n }\n });\n e.material.dispose();\n e.geometry.dispose();\n }\n });\n this.scene.clear();\n }\n dispose() {\n this.#y();\n this.#z();\n this.clear();\n this.#t?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n}\n\nconst b = new Map(),\n A = new r();\nlet R = false;\nfunction S(e) {\n const t = {\n position: new r(),\n nPosition: new r(),\n hover: false,\n touching: false,\n onEnter() {},\n onMove() {},\n onClick() {},\n onLeave() {},\n ...e\n };\n (function (e, t) {\n if (!b.has(e)) {\n b.set(e, t);\n if (!R) {\n document.body.addEventListener('pointermove', M);\n document.body.addEventListener('pointerleave', L);\n document.body.addEventListener('click', C);\n\n document.body.addEventListener('touchstart', TouchStart, { passive: false });\n document.body.addEventListener('touchmove', TouchMove, { passive: false });\n document.body.addEventListener('touchend', TouchEnd, { passive: false });\n document.body.addEventListener('touchcancel', TouchEnd, { passive: false });\n\n R = true;\n }\n }\n })(e.domElement, t);\n t.dispose = () => {\n const t = e.domElement;\n b.delete(t);\n if (b.size === 0) {\n document.body.removeEventListener('pointermove', M);\n document.body.removeEventListener('pointerleave', L);\n document.body.removeEventListener('click', C);\n\n document.body.removeEventListener('touchstart', TouchStart);\n document.body.removeEventListener('touchmove', TouchMove);\n document.body.removeEventListener('touchend', TouchEnd);\n document.body.removeEventListener('touchcancel', TouchEnd);\n\n R = false;\n }\n };\n return t;\n}\n\nfunction M(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n processInteraction();\n}\n\nfunction processInteraction() {\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n if (D(i)) {\n P(t, i);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && !t.touching) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction C(e) {\n A.x = e.clientX;\n A.y = e.clientY;\n for (const [elem, t] of b) {\n const i = elem.getBoundingClientRect();\n P(t, i);\n if (D(i)) t.onClick(t);\n }\n}\n\nfunction L() {\n for (const t of b.values()) {\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n}\n\nfunction TouchStart(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n if (D(rect)) {\n t.touching = true;\n P(t, rect);\n if (!t.hover) {\n t.hover = true;\n t.onEnter(t);\n }\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchMove(e) {\n if (e.touches.length > 0) {\n e.preventDefault();\n A.x = e.touches[0].clientX;\n A.y = e.touches[0].clientY;\n\n for (const [elem, t] of b) {\n const rect = elem.getBoundingClientRect();\n P(t, rect);\n\n if (D(rect)) {\n if (!t.hover) {\n t.hover = true;\n t.touching = true;\n t.onEnter(t);\n }\n t.onMove(t);\n } else if (t.hover && t.touching) {\n t.onMove(t);\n }\n }\n }\n}\n\nfunction TouchEnd() {\n for (const [, t] of b) {\n if (t.touching) {\n t.touching = false;\n if (t.hover) {\n t.hover = false;\n t.onLeave(t);\n }\n }\n }\n}\n\nfunction P(e, t) {\n const { position: i, nPosition: s } = e;\n i.x = A.x - t.left;\n i.y = A.y - t.top;\n s.x = (i.x / t.width) * 2 - 1;\n s.y = (-i.y / t.height) * 2 + 1;\n}\nfunction D(e) {\n const { x: t, y: i } = A;\n const { left: s, top: n, width: o, height: r } = e;\n return t >= s && t <= s + o && i >= n && i <= n + r;\n}\n\nconst { randFloat: k, randFloatSpread: E } = o;\nconst F = new a();\nconst I = new a();\nconst O = new a();\nconst V = new a();\nconst B = new a();\nconst N = new a();\nconst _ = new a();\nconst j = new a();\nconst H = new a();\nconst T = new a();\n\nclass W {\n constructor(e) {\n this.config = e;\n this.positionData = new Float32Array(3 * e.count).fill(0);\n this.velocityData = new Float32Array(3 * e.count).fill(0);\n this.sizeData = new Float32Array(e.count).fill(1);\n this.center = new a();\n this.#R();\n this.setSizes();\n }\n #R() {\n const { config: e, positionData: t } = this;\n this.center.toArray(t, 0);\n for (let i = 1; i < e.count; i++) {\n const s = 3 * i;\n t[s] = E(2 * e.maxX);\n t[s + 1] = E(2 * e.maxY);\n t[s + 2] = E(2 * e.maxZ);\n }\n }\n setSizes() {\n const { config: e, sizeData: t } = this;\n t[0] = e.size0;\n for (let i = 1; i < e.count; i++) {\n t[i] = k(e.minSize, e.maxSize);\n }\n }\n update(e) {\n const { config: t, center: i, positionData: s, sizeData: n, velocityData: o } = this;\n let r = 0;\n if (t.controlSphere0) {\n r = 1;\n F.fromArray(s, 0);\n F.lerp(i, 0.1).toArray(s, 0);\n V.set(0, 0, 0).toArray(o, 0);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n B.y -= e.delta * t.gravity * n[idx];\n B.multiplyScalar(t.friction);\n B.clampLength(0, t.maxVelocity);\n I.add(B);\n I.toArray(s, base);\n B.toArray(o, base);\n }\n for (let idx = r; idx < t.count; idx++) {\n const base = 3 * idx;\n I.fromArray(s, base);\n B.fromArray(o, base);\n const radius = n[idx];\n for (let jdx = idx + 1; jdx < t.count; jdx++) {\n const otherBase = 3 * jdx;\n O.fromArray(s, otherBase);\n N.fromArray(o, otherBase);\n const otherRadius = n[jdx];\n _.copy(O).sub(I);\n const dist = _.length();\n const sumRadius = radius + otherRadius;\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n j.copy(_)\n .normalize()\n .multiplyScalar(0.5 * overlap);\n H.copy(j).multiplyScalar(Math.max(B.length(), 1));\n T.copy(j).multiplyScalar(Math.max(N.length(), 1));\n I.sub(j);\n B.sub(H);\n I.toArray(s, base);\n B.toArray(o, base);\n O.add(j);\n N.add(T);\n O.toArray(s, otherBase);\n N.toArray(o, otherBase);\n }\n }\n if (t.controlSphere0) {\n _.copy(F).sub(I);\n const dist = _.length();\n const sumRadius0 = radius + n[0];\n if (dist < sumRadius0) {\n const diff = sumRadius0 - dist;\n j.copy(_.normalize()).multiplyScalar(diff);\n H.copy(j).multiplyScalar(Math.max(B.length(), 2));\n I.sub(j);\n B.sub(H);\n }\n }\n if (Math.abs(I.x) + radius > t.maxX) {\n I.x = Math.sign(I.x) * (t.maxX - radius);\n B.x = -B.x * t.wallBounce;\n }\n if (t.gravity === 0) {\n if (Math.abs(I.y) + radius > t.maxY) {\n I.y = Math.sign(I.y) * (t.maxY - radius);\n B.y = -B.y * t.wallBounce;\n }\n } else if (I.y - radius < -t.maxY) {\n I.y = -t.maxY + radius;\n B.y = -B.y * t.wallBounce;\n }\n const maxBoundary = Math.max(t.maxZ, t.maxSize);\n if (Math.abs(I.z) + radius > maxBoundary) {\n I.z = Math.sign(I.z) * (t.maxZ - radius);\n B.z = -B.z * t.wallBounce;\n }\n I.toArray(s, base);\n B.toArray(o, base);\n }\n }\n}\n\nclass Y extends c {\n constructor(e) {\n super(e);\n this.uniforms = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n this.defines.USE_UV = '';\n this.onBeforeCompile = e => {\n Object.assign(e.uniforms, this.uniforms);\n e.fragmentShader =\n '\\n uniform float thicknessPower;\\n uniform float thicknessScale;\\n uniform float thicknessDistortion;\\n uniform float thicknessAmbient;\\n uniform float thicknessAttenuation;\\n ' +\n e.fragmentShader;\n e.fragmentShader = e.fragmentShader.replace(\n 'void main() {',\n '\\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\\n #ifdef USE_COLOR\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\\n #else\\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\\n #endif\\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\\n }\\n\\n void main() {\\n '\n );\n const t = h.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n '\\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\\n '\n );\n e.fragmentShader = e.fragmentShader.replace('#include ', t);\n if (this.onBeforeCompile2) this.onBeforeCompile2(e);\n };\n }\n}\n\nconst X = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 16777215,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new m();\n\nclass Z extends d {\n constructor(e, t = {}) {\n const i = { ...X, ...t };\n const s = new z();\n const n = new p(e, 0.04).fromScene(s).texture;\n const o = new g();\n const r = new Y({ envMap: n, ...i.materialParams });\n r.envMapRotation.x = -Math.PI / 2;\n super(o, r, i.count);\n this.config = i;\n this.physics = new W(i);\n this.#S();\n this.setColors(i.colors);\n }\n #S() {\n this.ambientLight = new f(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new u(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n setColors(e) {\n if (Array.isArray(e) && e.length > 1) {\n const t = (function (e) {\n let t, i;\n function setColors(e) {\n t = e;\n i = [];\n t.forEach(col => {\n i.push(new l(col));\n });\n }\n setColors(e);\n return {\n setColors,\n getColorAt: function (ratio, out = new l()) {\n const scaled = Math.max(0, Math.min(1, ratio)) * (t.length - 1);\n const idx = Math.floor(scaled);\n const start = i[idx];\n if (idx >= t.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = i[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(e);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, t.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light.color.copy(t.getColorAt(idx / this.count));\n }\n }\n this.instanceColor.needsUpdate = true;\n }\n }\n update(e) {\n this.physics.update(e);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\nfunction createBallpit(e, t = {}) {\n const i = new x({\n canvas: e,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let s;\n i.renderer.toneMapping = v;\n i.camera.position.set(0, 0, 20);\n i.camera.lookAt(0, 0, 0);\n i.cameraMaxAspect = 1.5;\n i.resize();\n initialize(t);\n const n = new y();\n const o = new w(new a(0, 0, 1), 0);\n const r = new a();\n let c = false;\n\n e.style.touchAction = 'none';\n e.style.userSelect = 'none';\n e.style.webkitUserSelect = 'none';\n\n const h = S({\n domElement: e,\n onMove() {\n n.setFromCamera(h.nPosition, i.camera);\n i.camera.getWorldDirection(o.normal);\n n.ray.intersectPlane(o, r);\n s.physics.center.copy(r);\n s.config.controlSphere0 = true;\n },\n onLeave() {\n s.config.controlSphere0 = false;\n }\n });\n function initialize(e) {\n if (s) {\n i.clear();\n i.scene.remove(s);\n }\n s = new Z(i.renderer, e);\n i.scene.add(s);\n }\n i.onBeforeRender = e => {\n if (!c) s.update(e);\n };\n i.onAfterResize = e => {\n s.config.maxX = e.wWidth / 2;\n s.config.maxY = e.wHeight / 2;\n };\n return {\n three: i,\n get spheres() {\n return s;\n },\n setCount(e) {\n initialize({ ...s.config, count: e });\n },\n togglePause() {\n c = !c;\n },\n dispose() {\n h.dispose();\n i.dispose();\n }\n };\n}\n\nconst Ballpit = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, { followCursor, ...props });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n" + "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 togglePause() {\n c = !c;\n },\n dispose() {\n h.dispose();\n i.dispose();\n }\n };\n}\n\nconst Ballpit = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, { followCursor, ...props });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n" } ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Ballpit-TS-CSS.json b/public/r/Ballpit-TS-CSS.json index e57686b6c..db8f634c8 100644 --- a/public/r/Ballpit-TS-CSS.json +++ b/public/r/Ballpit-TS-CSS.json @@ -8,12 +8,12 @@ { "type": "registry:component", "path": "Ballpit/Ballpit.tsx", - "content": "import { gsap } from 'gsap';\nimport { Observer } from 'gsap/Observer';\nimport React, { useEffect, useRef } from 'react';\nimport {\n ACESFilmicToneMapping,\n AmbientLight,\n Clock,\n Color,\n InstancedMesh,\n MathUtils,\n MeshPhysicalMaterial,\n Object3D,\n PerspectiveCamera,\n Plane,\n PMREMGenerator,\n PointLight,\n Raycaster,\n Scene,\n ShaderChunk,\n SphereGeometry,\n SRGBColorSpace,\n Vector2,\n Vector3,\n WebGLRenderer,\n WebGLRendererParameters\n} from 'three';\nimport { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\ngsap.registerPlugin(Observer);\n\ninterface XConfig {\n canvas?: HTMLCanvasElement;\n id?: string;\n rendererOptions?: Partial;\n size?: 'parent' | { width: number; height: number };\n}\n\ninterface SizeData {\n width: number;\n height: number;\n wWidth: number;\n wHeight: number;\n ratio: number;\n pixelRatio: number;\n}\n\nclass X {\n #config: XConfig;\n #postprocessing: any;\n #resizeObserver?: ResizeObserver;\n #intersectionObserver?: IntersectionObserver;\n #resizeTimer?: number;\n #animationFrameId: number = 0;\n #clock: Clock = new Clock();\n #animationState = { elapsed: 0, delta: 0 };\n #isAnimating: boolean = false;\n #isVisible: boolean = false;\n\n canvas!: HTMLCanvasElement;\n camera!: PerspectiveCamera;\n cameraMinAspect?: number;\n cameraMaxAspect?: number;\n cameraFov!: number;\n maxPixelRatio?: number;\n minPixelRatio?: number;\n scene!: Scene;\n renderer!: WebGLRenderer;\n size: SizeData = {\n width: 0,\n height: 0,\n wWidth: 0,\n wHeight: 0,\n ratio: 0,\n pixelRatio: 0\n };\n\n render: () => void = this.#render.bind(this);\n onBeforeRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterResize: (size: SizeData) => void = () => {};\n isDisposed: boolean = false;\n\n constructor(config: XConfig) {\n this.#config = { ...config };\n this.#initCamera();\n this.#initScene();\n this.#initRenderer();\n this.resize();\n this.#initObservers();\n }\n\n #initCamera() {\n this.camera = new PerspectiveCamera();\n this.cameraFov = this.camera.fov;\n }\n\n #initScene() {\n this.scene = new Scene();\n }\n\n #initRenderer() {\n if (this.#config.canvas) {\n this.canvas = this.#config.canvas;\n } else if (this.#config.id) {\n const elem = document.getElementById(this.#config.id);\n if (elem instanceof HTMLCanvasElement) {\n this.canvas = elem;\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas!.style.display = 'block';\n const rendererOptions: WebGLRendererParameters = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#config.rendererOptions ?? {})\n };\n this.renderer = new WebGLRenderer(rendererOptions);\n this.renderer.outputColorSpace = SRGBColorSpace;\n }\n\n #initObservers() {\n if (!(this.#config.size instanceof Object)) {\n window.addEventListener('resize', this.#onResize.bind(this));\n if (this.#config.size === 'parent' && this.canvas.parentNode) {\n this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));\n this.#resizeObserver.observe(this.canvas.parentNode as Element);\n }\n }\n this.#intersectionObserver = new IntersectionObserver(this.#onIntersection.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#intersectionObserver.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n\n #onResize() {\n if (this.#resizeTimer) clearTimeout(this.#resizeTimer);\n this.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);\n }\n\n resize() {\n let w: number, h: number;\n if (this.#config.size instanceof Object) {\n w = this.#config.size.width;\n h = this.#config.size.height;\n } else if (this.#config.size === 'parent' && this.canvas.parentNode) {\n w = (this.canvas.parentNode as HTMLElement).offsetWidth;\n h = (this.canvas.parentNode as HTMLElement).offsetHeight;\n } else {\n w = window.innerWidth;\n h = window.innerHeight;\n }\n this.size.width = w;\n this.size.height = h;\n this.size.ratio = w / h;\n this.#updateCamera();\n this.#updateRenderer();\n this.onAfterResize(this.size);\n }\n\n #updateCamera() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#adjustFov(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#adjustFov(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n\n #adjustFov(aspect: number) {\n const tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));\n const newTan = tanFov / (this.camera.aspect / aspect);\n this.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));\n }\n\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const fovRad = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(fovRad / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if ((this.camera as any).isOrthographicCamera) {\n const cam = this.camera as any;\n this.size.wHeight = cam.top - cam.bottom;\n this.size.wWidth = cam.right - cam.left;\n }\n }\n\n #updateRenderer() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#postprocessing?.setSize(this.size.width, this.size.height);\n let pr = window.devicePixelRatio;\n if (this.maxPixelRatio && pr > this.maxPixelRatio) {\n pr = this.maxPixelRatio;\n } else if (this.minPixelRatio && pr < this.minPixelRatio) {\n pr = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(pr);\n this.size.pixelRatio = pr;\n }\n\n get postprocessing() {\n return this.#postprocessing;\n }\n set postprocessing(value: any) {\n this.#postprocessing = value;\n this.render = value.render.bind(value);\n }\n\n #onIntersection(entries: IntersectionObserverEntry[]) {\n this.#isAnimating = entries[0].isIntersecting;\n this.#isAnimating ? this.#startAnimation() : this.#stopAnimation();\n }\n\n #onVisibilityChange() {\n if (this.#isAnimating) {\n document.hidden ? this.#stopAnimation() : this.#startAnimation();\n }\n }\n\n #startAnimation() {\n if (this.#isVisible) return;\n const animateFrame = () => {\n this.#animationFrameId = requestAnimationFrame(animateFrame);\n this.#animationState.delta = this.#clock.getDelta();\n this.#animationState.elapsed += this.#animationState.delta;\n this.onBeforeRender(this.#animationState);\n this.render();\n this.onAfterRender(this.#animationState);\n };\n this.#isVisible = true;\n this.#clock.start();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\n this.#clock.stop();\n }\n }\n\n #render() {\n this.renderer.render(this.scene, this.camera);\n }\n\n clear() {\n this.scene.traverse(obj => {\n if ((obj as any).isMesh && typeof (obj as any).material === 'object' && (obj as any).material !== null) {\n Object.keys((obj as any).material).forEach(key => {\n const matProp = (obj as any).material[key];\n if (matProp && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n (obj as any).material.dispose();\n (obj as any).geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n this.#onResizeCleanup();\n this.#stopAnimation();\n this.clear();\n this.#postprocessing?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n\n #onResizeCleanup() {\n window.removeEventListener('resize', this.#onResize.bind(this));\n this.#resizeObserver?.disconnect();\n this.#intersectionObserver?.disconnect();\n document.removeEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n}\n\ninterface WConfig {\n count: number;\n maxX: number;\n maxY: number;\n maxZ: number;\n maxSize: number;\n minSize: number;\n size0: number;\n gravity: number;\n friction: number;\n wallBounce: number;\n maxVelocity: number;\n controlSphere0?: boolean;\n followCursor?: boolean;\n}\n\nclass W {\n config: WConfig;\n positionData: Float32Array;\n velocityData: Float32Array;\n sizeData: Float32Array;\n center: Vector3 = new Vector3();\n\n constructor(config: WConfig) {\n this.config = config;\n this.positionData = new Float32Array(3 * config.count).fill(0);\n this.velocityData = new Float32Array(3 * config.count).fill(0);\n this.sizeData = new Float32Array(config.count).fill(1);\n this.center = new Vector3();\n this.#initializePositions();\n this.setSizes();\n }\n\n #initializePositions() {\n const { config, positionData } = this;\n this.center.toArray(positionData, 0);\n for (let i = 1; i < config.count; i++) {\n const idx = 3 * i;\n positionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);\n positionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);\n positionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);\n }\n }\n\n setSizes() {\n const { config, sizeData } = this;\n sizeData[0] = config.size0;\n for (let i = 1; i < config.count; i++) {\n sizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);\n }\n }\n\n update(deltaInfo: { delta: number }) {\n const { config, center, positionData, sizeData, velocityData } = this;\n let startIdx = 0;\n if (config.controlSphere0) {\n startIdx = 1;\n const firstVec = new Vector3().fromArray(positionData, 0);\n firstVec.lerp(center, 0.1).toArray(positionData, 0);\n new Vector3(0, 0, 0).toArray(velocityData, 0);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n vel.y -= deltaInfo.delta * config.gravity * sizeData[idx];\n vel.multiplyScalar(config.friction);\n vel.clampLength(0, config.maxVelocity);\n pos.add(vel);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n const radius = sizeData[idx];\n for (let jdx = idx + 1; jdx < config.count; jdx++) {\n const otherBase = 3 * jdx;\n const otherPos = new Vector3().fromArray(positionData, otherBase);\n const otherVel = new Vector3().fromArray(velocityData, otherBase);\n const diff = new Vector3().copy(otherPos).sub(pos);\n const dist = diff.length();\n const sumRadius = radius + sizeData[jdx];\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n const correction = diff.normalize().multiplyScalar(0.5 * overlap);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 1));\n pos.sub(correction);\n vel.sub(velCorrection);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n otherPos.add(correction);\n otherVel.add(correction.clone().multiplyScalar(Math.max(otherVel.length(), 1)));\n otherPos.toArray(positionData, otherBase);\n otherVel.toArray(velocityData, otherBase);\n }\n }\n if (config.controlSphere0) {\n const diff = new Vector3().copy(new Vector3().fromArray(positionData, 0)).sub(pos);\n const d = diff.length();\n const sumRadius0 = radius + sizeData[0];\n if (d < sumRadius0) {\n const correction = diff.normalize().multiplyScalar(sumRadius0 - d);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 2));\n pos.sub(correction);\n vel.sub(velCorrection);\n }\n }\n if (Math.abs(pos.x) + radius > config.maxX) {\n pos.x = Math.sign(pos.x) * (config.maxX - radius);\n vel.x = -vel.x * config.wallBounce;\n }\n if (config.gravity === 0) {\n if (Math.abs(pos.y) + radius > config.maxY) {\n pos.y = Math.sign(pos.y) * (config.maxY - radius);\n vel.y = -vel.y * config.wallBounce;\n }\n } else if (pos.y - radius < -config.maxY) {\n pos.y = -config.maxY + radius;\n vel.y = -vel.y * config.wallBounce;\n }\n const maxBoundary = Math.max(config.maxZ, config.maxSize);\n if (Math.abs(pos.z) + radius > maxBoundary) {\n pos.z = Math.sign(pos.z) * (config.maxZ - radius);\n vel.z = -vel.z * config.wallBounce;\n }\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n }\n}\n\nclass Y extends MeshPhysicalMaterial {\n uniforms: { [key: string]: { value: any } } = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n defines: { USE_UV: string };\n\n constructor(params: any) {\n super(params);\n this.defines = { USE_UV: '' };\n this.onBeforeCompile = shader => {\n Object.assign(shader.uniforms, this.uniforms);\n shader.fragmentShader =\n `\n uniform float thicknessPower;\n uniform float thicknessScale;\n uniform float thicknessDistortion;\n uniform float thicknessAmbient;\n uniform float thicknessAttenuation;\n ` + shader.fragmentShader;\n shader.fragmentShader = shader.fragmentShader.replace(\n 'void main() {',\n `\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\n #ifdef USE_COLOR\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\n #else\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\n #endif\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\n }\n\n void main() {\n `\n );\n const lightsChunk = ShaderChunk.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n `\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\n `\n );\n shader.fragmentShader = shader.fragmentShader.replace('#include ', lightsChunk);\n if (this.onBeforeCompile2) this.onBeforeCompile2(shader);\n };\n }\n onBeforeCompile2?: (shader: any) => void;\n}\n\nconst XConfig = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 0xffffff,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new Object3D();\n\nlet globalPointerActive = false;\nconst pointerPosition = new Vector2();\n\ninterface PointerData {\n position: Vector2;\n nPosition: Vector2;\n hover: boolean;\n touching: boolean;\n onEnter: (data: PointerData) => void;\n onMove: (data: PointerData) => void;\n onClick: (data: PointerData) => void;\n onLeave: (data: PointerData) => void;\n dispose?: () => void;\n}\n\nconst pointerMap = new Map();\n\nfunction createPointerData(options: Partial & { domElement: HTMLElement }): PointerData {\n const defaultData: PointerData = {\n position: new Vector2(),\n nPosition: new Vector2(),\n hover: false,\n touching: false,\n onEnter: () => {},\n onMove: () => {},\n onClick: () => {},\n onLeave: () => {},\n ...options\n };\n if (!pointerMap.has(options.domElement)) {\n pointerMap.set(options.domElement, defaultData);\n if (!globalPointerActive) {\n document.body.addEventListener('pointermove', onPointerMove as EventListener);\n document.body.addEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.addEventListener('click', onPointerClick as EventListener);\n\n document.body.addEventListener('touchstart', onTouchStart as EventListener, { passive: false });\n document.body.addEventListener('touchmove', onTouchMove as EventListener, { passive: false });\n document.body.addEventListener('touchend', onTouchEnd as EventListener, { passive: false });\n document.body.addEventListener('touchcancel', onTouchEnd as EventListener, { passive: false });\n globalPointerActive = true;\n }\n }\n defaultData.dispose = () => {\n pointerMap.delete(options.domElement);\n if (pointerMap.size === 0) {\n document.body.removeEventListener('pointermove', onPointerMove as EventListener);\n document.body.removeEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.removeEventListener('click', onPointerClick as EventListener);\n\n document.body.removeEventListener('touchstart', onTouchStart as EventListener);\n document.body.removeEventListener('touchmove', onTouchMove as EventListener);\n document.body.removeEventListener('touchend', onTouchEnd as EventListener);\n document.body.removeEventListener('touchcancel', onTouchEnd as EventListener);\n globalPointerActive = false;\n }\n };\n return defaultData;\n}\n\nfunction onPointerMove(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n processPointerInteraction();\n}\n\nfunction processPointerInteraction() {\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && !data.touching) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction onTouchStart(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n data.touching = true;\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchMove(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) {\n if (!data.hover) {\n data.hover = true;\n data.touching = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && data.touching) {\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchEnd() {\n for (const [, data] of pointerMap) {\n if (data.touching) {\n data.touching = false;\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n }\n}\n\nfunction onPointerClick(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) data.onClick(data);\n }\n}\n\nfunction onPointerLeave() {\n for (const data of pointerMap.values()) {\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction updatePointerData(data: PointerData, rect: DOMRect) {\n data.position.set(pointerPosition.x - rect.left, pointerPosition.y - rect.top);\n data.nPosition.set((data.position.x / rect.width) * 2 - 1, (-data.position.y / rect.height) * 2 + 1);\n}\n\nfunction isInside(rect: DOMRect) {\n return (\n pointerPosition.x >= rect.left &&\n pointerPosition.x <= rect.left + rect.width &&\n pointerPosition.y >= rect.top &&\n pointerPosition.y <= rect.top + rect.height\n );\n}\n\nconst { randFloat, randFloatSpread } = MathUtils;\nconst F = new Vector3();\nconst I = new Vector3();\nconst O = new Vector3();\nconst V = new Vector3();\nconst B = new Vector3();\nconst N = new Vector3();\nconst _ = new Vector3();\nconst j = new Vector3();\nconst H = new Vector3();\nconst T = new Vector3();\n\nclass Z extends InstancedMesh {\n config: typeof XConfig;\n physics: W;\n ambientLight: AmbientLight | undefined;\n light: PointLight | undefined;\n\n constructor(renderer: WebGLRenderer, params: Partial = {}) {\n const config = { ...XConfig, ...params };\n const roomEnv = new RoomEnvironment();\n const pmrem = new PMREMGenerator(renderer);\n const envTexture = pmrem.fromScene(roomEnv).texture;\n const geometry = new SphereGeometry();\n const material = new Y({ envMap: envTexture, ...config.materialParams });\n material.envMapRotation.x = -Math.PI / 2;\n super(geometry, material, config.count);\n this.config = config;\n this.physics = new W(config);\n this.#setupLights();\n this.setColors(config.colors);\n }\n\n #setupLights() {\n this.ambientLight = new AmbientLight(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new PointLight(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n\n setColors(colors: number[]) {\n if (Array.isArray(colors) && colors.length > 1) {\n const colorUtils = (function (colorsArr: number[]) {\n let baseColors: number[] = colorsArr;\n let colorObjects: Color[] = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n return {\n setColors: (cols: number[]) => {\n baseColors = cols;\n colorObjects = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n },\n getColorAt: (ratio: number, out: Color = new Color()) => {\n const clamped = Math.max(0, Math.min(1, ratio));\n const scaled = clamped * (baseColors.length - 1);\n const idx = Math.floor(scaled);\n const start = colorObjects[idx];\n if (idx >= baseColors.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = colorObjects[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(colors);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, colorUtils.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light!.color.copy(colorUtils.getColorAt(idx / this.count));\n }\n }\n\n if (!this.instanceColor) return;\n this.instanceColor.needsUpdate = true;\n }\n }\n\n update(deltaInfo: { delta: number }) {\n this.physics.update(deltaInfo);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light!.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\ninterface CreateBallpitReturn {\n three: X;\n spheres: Z;\n setCount: (count: number) => void;\n togglePause: () => void;\n dispose: () => void;\n}\n\nfunction createBallpit(canvas: HTMLCanvasElement, config: any = {}): CreateBallpitReturn {\n const threeInstance = new X({\n canvas,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let spheres: Z;\n threeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n threeInstance.camera.position.set(0, 0, 20);\n threeInstance.camera.lookAt(0, 0, 0);\n threeInstance.cameraMaxAspect = 1.5;\n threeInstance.resize();\n initialize(config);\n const raycaster = new Raycaster();\n const plane = new Plane(new Vector3(0, 0, 1), 0);\n const intersectionPoint = new Vector3();\n let isPaused = false;\n\n canvas.style.touchAction = 'none';\n canvas.style.userSelect = 'none';\n (canvas.style as any).webkitUserSelect = 'none';\n\n const pointerData = createPointerData({\n domElement: canvas,\n onMove() {\n raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n threeInstance.camera.getWorldDirection(plane.normal);\n raycaster.ray.intersectPlane(plane, intersectionPoint);\n spheres.physics.center.copy(intersectionPoint);\n spheres.config.controlSphere0 = true;\n },\n onLeave() {\n spheres.config.controlSphere0 = false;\n }\n });\n function initialize(cfg: any) {\n if (spheres) {\n threeInstance.clear();\n threeInstance.scene.remove(spheres);\n }\n spheres = new Z(threeInstance.renderer, cfg);\n threeInstance.scene.add(spheres);\n }\n threeInstance.onBeforeRender = deltaInfo => {\n if (!isPaused) spheres.update(deltaInfo);\n };\n threeInstance.onAfterResize = size => {\n spheres.config.maxX = size.wWidth / 2;\n spheres.config.maxY = size.wHeight / 2;\n };\n return {\n three: threeInstance,\n get spheres() {\n return spheres;\n },\n setCount(count: number) {\n initialize({ ...spheres.config, count });\n },\n togglePause() {\n isPaused = !isPaused;\n },\n dispose() {\n pointerData.dispose?.();\n threeInstance.dispose();\n }\n };\n}\n\ninterface BallpitProps {\n className?: string;\n followCursor?: boolean;\n [key: string]: any;\n}\n\nconst Ballpit: React.FC = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, {\n followCursor,\n ...props\n });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n" + "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 togglePause: () => void;\n dispose: () => void;\n}\n\nfunction createBallpit(canvas: HTMLCanvasElement, config: any = {}): CreateBallpitReturn {\n const threeInstance = new X({\n canvas,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let spheres: Z;\n threeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n threeInstance.camera.position.set(0, 0, 20);\n threeInstance.camera.lookAt(0, 0, 0);\n threeInstance.cameraMaxAspect = 1.5;\n threeInstance.resize();\n initialize(config);\n const raycaster = new Raycaster();\n const plane = new Plane(new Vector3(0, 0, 1), 0);\n const intersectionPoint = new Vector3();\n let isPaused = false;\n\n canvas.style.touchAction = 'none';\n canvas.style.userSelect = 'none';\n (canvas.style as any).webkitUserSelect = 'none';\n\n const pointerData = createPointerData({\n domElement: canvas,\n onMove() {\n raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n threeInstance.camera.getWorldDirection(plane.normal);\n raycaster.ray.intersectPlane(plane, intersectionPoint);\n spheres.physics.center.copy(intersectionPoint);\n spheres.config.controlSphere0 = true;\n },\n onLeave() {\n spheres.config.controlSphere0 = false;\n }\n });\n function initialize(cfg: any) {\n if (spheres) {\n threeInstance.clear();\n threeInstance.scene.remove(spheres);\n }\n spheres = new Z(threeInstance.renderer, cfg);\n threeInstance.scene.add(spheres);\n }\n threeInstance.onBeforeRender = deltaInfo => {\n if (!isPaused) spheres.update(deltaInfo);\n };\n threeInstance.onAfterResize = size => {\n spheres.config.maxX = size.wWidth / 2;\n spheres.config.maxY = size.wHeight / 2;\n };\n return {\n three: threeInstance,\n get spheres() {\n return spheres;\n },\n setCount(count: number) {\n initialize({ ...spheres.config, count });\n },\n togglePause() {\n isPaused = !isPaused;\n },\n dispose() {\n pointerData.dispose?.();\n threeInstance.dispose();\n }\n };\n}\n\ninterface BallpitProps {\n className?: string;\n followCursor?: boolean;\n [key: string]: any;\n}\n\nconst Ballpit: React.FC = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, {\n followCursor,\n ...props\n });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n" } ], "registryDependencies": [], "dependencies": [ "gsap@^3.13.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Ballpit-TS-TW.json b/public/r/Ballpit-TS-TW.json index cf9b1360e..d9dbcc53b 100644 --- a/public/r/Ballpit-TS-TW.json +++ b/public/r/Ballpit-TS-TW.json @@ -8,12 +8,12 @@ { "type": "registry:component", "path": "Ballpit/Ballpit.tsx", - "content": "import { gsap } from 'gsap';\nimport { Observer } from 'gsap/Observer';\nimport React, { useEffect, useRef } from 'react';\nimport {\n ACESFilmicToneMapping,\n AmbientLight,\n Clock,\n Color,\n InstancedMesh,\n MathUtils,\n MeshPhysicalMaterial,\n Object3D,\n PerspectiveCamera,\n Plane,\n PMREMGenerator,\n PointLight,\n Raycaster,\n Scene,\n ShaderChunk,\n SphereGeometry,\n SRGBColorSpace,\n Vector2,\n Vector3,\n WebGLRenderer,\n WebGLRendererParameters\n} from 'three';\nimport { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';\n\ngsap.registerPlugin(Observer);\n\ninterface XConfig {\n canvas?: HTMLCanvasElement;\n id?: string;\n rendererOptions?: Partial;\n size?: 'parent' | { width: number; height: number };\n}\n\ninterface SizeData {\n width: number;\n height: number;\n wWidth: number;\n wHeight: number;\n ratio: number;\n pixelRatio: number;\n}\n\nclass X {\n #config: XConfig;\n #postprocessing: any;\n #resizeObserver?: ResizeObserver;\n #intersectionObserver?: IntersectionObserver;\n #resizeTimer?: number;\n #animationFrameId: number = 0;\n #clock: Clock = new Clock();\n #animationState = { elapsed: 0, delta: 0 };\n #isAnimating: boolean = false;\n #isVisible: boolean = false;\n\n canvas!: HTMLCanvasElement;\n camera!: PerspectiveCamera;\n cameraMinAspect?: number;\n cameraMaxAspect?: number;\n cameraFov!: number;\n maxPixelRatio?: number;\n minPixelRatio?: number;\n scene!: Scene;\n renderer!: WebGLRenderer;\n size: SizeData = {\n width: 0,\n height: 0,\n wWidth: 0,\n wHeight: 0,\n ratio: 0,\n pixelRatio: 0\n };\n\n render: () => void = this.#render.bind(this);\n onBeforeRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterRender: (state: { elapsed: number; delta: number }) => void = () => {};\n onAfterResize: (size: SizeData) => void = () => {};\n isDisposed: boolean = false;\n\n constructor(config: XConfig) {\n this.#config = { ...config };\n this.#initCamera();\n this.#initScene();\n this.#initRenderer();\n this.resize();\n this.#initObservers();\n }\n\n #initCamera() {\n this.camera = new PerspectiveCamera();\n this.cameraFov = this.camera.fov;\n }\n\n #initScene() {\n this.scene = new Scene();\n }\n\n #initRenderer() {\n if (this.#config.canvas) {\n this.canvas = this.#config.canvas;\n } else if (this.#config.id) {\n const elem = document.getElementById(this.#config.id);\n if (elem instanceof HTMLCanvasElement) {\n this.canvas = elem;\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n } else {\n console.error('Three: Missing canvas or id parameter');\n }\n this.canvas!.style.display = 'block';\n const rendererOptions: WebGLRendererParameters = {\n canvas: this.canvas,\n powerPreference: 'high-performance',\n ...(this.#config.rendererOptions ?? {})\n };\n this.renderer = new WebGLRenderer(rendererOptions);\n this.renderer.outputColorSpace = SRGBColorSpace;\n }\n\n #initObservers() {\n if (!(this.#config.size instanceof Object)) {\n window.addEventListener('resize', this.#onResize.bind(this));\n if (this.#config.size === 'parent' && this.canvas.parentNode) {\n this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));\n this.#resizeObserver.observe(this.canvas.parentNode as Element);\n }\n }\n this.#intersectionObserver = new IntersectionObserver(this.#onIntersection.bind(this), {\n root: null,\n rootMargin: '0px',\n threshold: 0\n });\n this.#intersectionObserver.observe(this.canvas);\n document.addEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n\n #onResize() {\n if (this.#resizeTimer) clearTimeout(this.#resizeTimer);\n this.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);\n }\n\n resize() {\n let w: number, h: number;\n if (this.#config.size instanceof Object) {\n w = this.#config.size.width;\n h = this.#config.size.height;\n } else if (this.#config.size === 'parent' && this.canvas.parentNode) {\n w = (this.canvas.parentNode as HTMLElement).offsetWidth;\n h = (this.canvas.parentNode as HTMLElement).offsetHeight;\n } else {\n w = window.innerWidth;\n h = window.innerHeight;\n }\n this.size.width = w;\n this.size.height = h;\n this.size.ratio = w / h;\n this.#updateCamera();\n this.#updateRenderer();\n this.onAfterResize(this.size);\n }\n\n #updateCamera() {\n this.camera.aspect = this.size.width / this.size.height;\n if (this.camera.isPerspectiveCamera && this.cameraFov) {\n if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {\n this.#adjustFov(this.cameraMinAspect);\n } else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {\n this.#adjustFov(this.cameraMaxAspect);\n } else {\n this.camera.fov = this.cameraFov;\n }\n }\n this.camera.updateProjectionMatrix();\n this.updateWorldSize();\n }\n\n #adjustFov(aspect: number) {\n const tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));\n const newTan = tanFov / (this.camera.aspect / aspect);\n this.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));\n }\n\n updateWorldSize() {\n if (this.camera.isPerspectiveCamera) {\n const fovRad = (this.camera.fov * Math.PI) / 180;\n this.size.wHeight = 2 * Math.tan(fovRad / 2) * this.camera.position.length();\n this.size.wWidth = this.size.wHeight * this.camera.aspect;\n } else if ((this.camera as any).isOrthographicCamera) {\n const cam = this.camera as any;\n this.size.wHeight = cam.top - cam.bottom;\n this.size.wWidth = cam.right - cam.left;\n }\n }\n\n #updateRenderer() {\n this.renderer.setSize(this.size.width, this.size.height);\n this.#postprocessing?.setSize(this.size.width, this.size.height);\n let pr = window.devicePixelRatio;\n if (this.maxPixelRatio && pr > this.maxPixelRatio) {\n pr = this.maxPixelRatio;\n } else if (this.minPixelRatio && pr < this.minPixelRatio) {\n pr = this.minPixelRatio;\n }\n this.renderer.setPixelRatio(pr);\n this.size.pixelRatio = pr;\n }\n\n get postprocessing() {\n return this.#postprocessing;\n }\n set postprocessing(value: any) {\n this.#postprocessing = value;\n this.render = value.render.bind(value);\n }\n\n #onIntersection(entries: IntersectionObserverEntry[]) {\n this.#isAnimating = entries[0].isIntersecting;\n this.#isAnimating ? this.#startAnimation() : this.#stopAnimation();\n }\n\n #onVisibilityChange() {\n if (this.#isAnimating) {\n document.hidden ? this.#stopAnimation() : this.#startAnimation();\n }\n }\n\n #startAnimation() {\n if (this.#isVisible) return;\n const animateFrame = () => {\n this.#animationFrameId = requestAnimationFrame(animateFrame);\n this.#animationState.delta = this.#clock.getDelta();\n this.#animationState.elapsed += this.#animationState.delta;\n this.onBeforeRender(this.#animationState);\n this.render();\n this.onAfterRender(this.#animationState);\n };\n this.#isVisible = true;\n this.#clock.start();\n animateFrame();\n }\n\n #stopAnimation() {\n if (this.#isVisible) {\n cancelAnimationFrame(this.#animationFrameId);\n this.#isVisible = false;\n this.#clock.stop();\n }\n }\n\n #render() {\n this.renderer.render(this.scene, this.camera);\n }\n\n clear() {\n this.scene.traverse(obj => {\n if ((obj as any).isMesh && typeof (obj as any).material === 'object' && (obj as any).material !== null) {\n Object.keys((obj as any).material).forEach(key => {\n const matProp = (obj as any).material[key];\n if (matProp && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\n matProp.dispose();\n }\n });\n (obj as any).material.dispose();\n (obj as any).geometry.dispose();\n }\n });\n this.scene.clear();\n }\n\n dispose() {\n this.#onResizeCleanup();\n this.#stopAnimation();\n this.clear();\n this.#postprocessing?.dispose();\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n this.isDisposed = true;\n }\n\n #onResizeCleanup() {\n window.removeEventListener('resize', this.#onResize.bind(this));\n this.#resizeObserver?.disconnect();\n this.#intersectionObserver?.disconnect();\n document.removeEventListener('visibilitychange', this.#onVisibilityChange.bind(this));\n }\n}\n\ninterface WConfig {\n count: number;\n maxX: number;\n maxY: number;\n maxZ: number;\n maxSize: number;\n minSize: number;\n size0: number;\n gravity: number;\n friction: number;\n wallBounce: number;\n maxVelocity: number;\n controlSphere0?: boolean;\n followCursor?: boolean;\n}\n\nclass W {\n config: WConfig;\n positionData: Float32Array;\n velocityData: Float32Array;\n sizeData: Float32Array;\n center: Vector3 = new Vector3();\n\n constructor(config: WConfig) {\n this.config = config;\n this.positionData = new Float32Array(3 * config.count).fill(0);\n this.velocityData = new Float32Array(3 * config.count).fill(0);\n this.sizeData = new Float32Array(config.count).fill(1);\n this.center = new Vector3();\n this.#initializePositions();\n this.setSizes();\n }\n\n #initializePositions() {\n const { config, positionData } = this;\n this.center.toArray(positionData, 0);\n for (let i = 1; i < config.count; i++) {\n const idx = 3 * i;\n positionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);\n positionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);\n positionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);\n }\n }\n\n setSizes() {\n const { config, sizeData } = this;\n sizeData[0] = config.size0;\n for (let i = 1; i < config.count; i++) {\n sizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);\n }\n }\n\n update(deltaInfo: { delta: number }) {\n const { config, center, positionData, sizeData, velocityData } = this;\n let startIdx = 0;\n if (config.controlSphere0) {\n startIdx = 1;\n const firstVec = new Vector3().fromArray(positionData, 0);\n firstVec.lerp(center, 0.1).toArray(positionData, 0);\n new Vector3(0, 0, 0).toArray(velocityData, 0);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n vel.y -= deltaInfo.delta * config.gravity * sizeData[idx];\n vel.multiplyScalar(config.friction);\n vel.clampLength(0, config.maxVelocity);\n pos.add(vel);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n for (let idx = startIdx; idx < config.count; idx++) {\n const base = 3 * idx;\n const pos = new Vector3().fromArray(positionData, base);\n const vel = new Vector3().fromArray(velocityData, base);\n const radius = sizeData[idx];\n for (let jdx = idx + 1; jdx < config.count; jdx++) {\n const otherBase = 3 * jdx;\n const otherPos = new Vector3().fromArray(positionData, otherBase);\n const otherVel = new Vector3().fromArray(velocityData, otherBase);\n const diff = new Vector3().copy(otherPos).sub(pos);\n const dist = diff.length();\n const sumRadius = radius + sizeData[jdx];\n if (dist < sumRadius) {\n const overlap = sumRadius - dist;\n const correction = diff.normalize().multiplyScalar(0.5 * overlap);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 1));\n pos.sub(correction);\n vel.sub(velCorrection);\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n otherPos.add(correction);\n otherVel.add(correction.clone().multiplyScalar(Math.max(otherVel.length(), 1)));\n otherPos.toArray(positionData, otherBase);\n otherVel.toArray(velocityData, otherBase);\n }\n }\n if (config.controlSphere0) {\n const diff = new Vector3().copy(new Vector3().fromArray(positionData, 0)).sub(pos);\n const d = diff.length();\n const sumRadius0 = radius + sizeData[0];\n if (d < sumRadius0) {\n const correction = diff.normalize().multiplyScalar(sumRadius0 - d);\n const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 2));\n pos.sub(correction);\n vel.sub(velCorrection);\n }\n }\n if (Math.abs(pos.x) + radius > config.maxX) {\n pos.x = Math.sign(pos.x) * (config.maxX - radius);\n vel.x = -vel.x * config.wallBounce;\n }\n if (config.gravity === 0) {\n if (Math.abs(pos.y) + radius > config.maxY) {\n pos.y = Math.sign(pos.y) * (config.maxY - radius);\n vel.y = -vel.y * config.wallBounce;\n }\n } else if (pos.y - radius < -config.maxY) {\n pos.y = -config.maxY + radius;\n vel.y = -vel.y * config.wallBounce;\n }\n const maxBoundary = Math.max(config.maxZ, config.maxSize);\n if (Math.abs(pos.z) + radius > maxBoundary) {\n pos.z = Math.sign(pos.z) * (config.maxZ - radius);\n vel.z = -vel.z * config.wallBounce;\n }\n pos.toArray(positionData, base);\n vel.toArray(velocityData, base);\n }\n }\n}\n\nclass Y extends MeshPhysicalMaterial {\n uniforms: { [key: string]: { value: any } } = {\n thicknessDistortion: { value: 0.1 },\n thicknessAmbient: { value: 0 },\n thicknessAttenuation: { value: 0.1 },\n thicknessPower: { value: 2 },\n thicknessScale: { value: 10 }\n };\n defines: { USE_UV: string; };\n\n constructor(params: any) {\n super(params);\n this.defines = { USE_UV: '' };\n this.onBeforeCompile = shader => {\n Object.assign(shader.uniforms, this.uniforms);\n shader.fragmentShader =\n `\n uniform float thicknessPower;\n uniform float thicknessScale;\n uniform float thicknessDistortion;\n uniform float thicknessAmbient;\n uniform float thicknessAttenuation;\n ` + shader.fragmentShader;\n shader.fragmentShader = shader.fragmentShader.replace(\n 'void main() {',\n `\n void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {\n vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));\n float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;\n #ifdef USE_COLOR\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;\n #else\n vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;\n #endif\n reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;\n }\n\n void main() {\n `\n );\n const lightsChunk = ShaderChunk.lights_fragment_begin.replaceAll(\n 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',\n `\n RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);\n `\n );\n shader.fragmentShader = shader.fragmentShader.replace('#include ', lightsChunk);\n if (this.onBeforeCompile2) this.onBeforeCompile2(shader);\n };\n }\n onBeforeCompile2?: (shader: any) => void;\n}\n\nconst XConfig = {\n count: 200,\n colors: [0, 0, 0],\n ambientColor: 0xffffff,\n ambientIntensity: 1,\n lightIntensity: 200,\n materialParams: {\n metalness: 0.5,\n roughness: 0.5,\n clearcoat: 1,\n clearcoatRoughness: 0.15\n },\n minSize: 0.5,\n maxSize: 1,\n size0: 1,\n gravity: 0.5,\n friction: 0.9975,\n wallBounce: 0.95,\n maxVelocity: 0.15,\n maxX: 5,\n maxY: 5,\n maxZ: 2,\n controlSphere0: false,\n followCursor: true\n};\n\nconst U = new Object3D();\n\nlet globalPointerActive = false;\nconst pointerPosition = new Vector2();\n\ninterface PointerData {\n position: Vector2;\n nPosition: Vector2;\n hover: boolean;\n touching: boolean;\n onEnter: (data: PointerData) => void;\n onMove: (data: PointerData) => void;\n onClick: (data: PointerData) => void;\n onLeave: (data: PointerData) => void;\n dispose?: () => void;\n}\n\nconst pointerMap = new Map();\n\nfunction createPointerData(options: Partial & { domElement: HTMLElement }): PointerData {\n const defaultData: PointerData = {\n position: new Vector2(),\n nPosition: new Vector2(),\n hover: false,\n touching: false,\n onEnter: () => {},\n onMove: () => {},\n onClick: () => {},\n onLeave: () => {},\n ...options\n };\n if (!pointerMap.has(options.domElement)) {\n pointerMap.set(options.domElement, defaultData);\n if (!globalPointerActive) {\n document.body.addEventListener('pointermove', onPointerMove as EventListener);\n document.body.addEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.addEventListener('click', onPointerClick as EventListener);\n\n document.body.addEventListener('touchstart', onTouchStart as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchmove', onTouchMove as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchend', onTouchEnd as EventListener, {\n passive: false\n });\n document.body.addEventListener('touchcancel', onTouchEnd as EventListener, {\n passive: false\n });\n globalPointerActive = true;\n }\n }\n defaultData.dispose = () => {\n pointerMap.delete(options.domElement);\n if (pointerMap.size === 0) {\n document.body.removeEventListener('pointermove', onPointerMove as EventListener);\n document.body.removeEventListener('pointerleave', onPointerLeave as EventListener);\n document.body.removeEventListener('click', onPointerClick as EventListener);\n\n document.body.removeEventListener('touchstart', onTouchStart as EventListener);\n document.body.removeEventListener('touchmove', onTouchMove as EventListener);\n document.body.removeEventListener('touchend', onTouchEnd as EventListener);\n document.body.removeEventListener('touchcancel', onTouchEnd as EventListener);\n globalPointerActive = false;\n }\n };\n return defaultData;\n}\n\nfunction onPointerMove(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n processPointerInteraction();\n}\n\nfunction processPointerInteraction() {\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && !data.touching) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction onTouchStart(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n if (isInside(rect)) {\n data.touching = true;\n updatePointerData(data, rect);\n if (!data.hover) {\n data.hover = true;\n data.onEnter(data);\n }\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchMove(e: TouchEvent) {\n if (e.touches.length > 0) {\n e.preventDefault();\n pointerPosition.set(e.touches[0].clientX, e.touches[0].clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) {\n if (!data.hover) {\n data.hover = true;\n data.touching = true;\n data.onEnter(data);\n }\n data.onMove(data);\n } else if (data.hover && data.touching) {\n data.onMove(data);\n }\n }\n }\n}\n\nfunction onTouchEnd() {\n for (const [, data] of pointerMap) {\n if (data.touching) {\n data.touching = false;\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n }\n}\n\nfunction onPointerClick(e: PointerEvent) {\n pointerPosition.set(e.clientX, e.clientY);\n for (const [elem, data] of pointerMap) {\n const rect = elem.getBoundingClientRect();\n updatePointerData(data, rect);\n if (isInside(rect)) data.onClick(data);\n }\n}\n\nfunction onPointerLeave() {\n for (const data of pointerMap.values()) {\n if (data.hover) {\n data.hover = false;\n data.onLeave(data);\n }\n }\n}\n\nfunction updatePointerData(data: PointerData, rect: DOMRect) {\n data.position.set(pointerPosition.x - rect.left, pointerPosition.y - rect.top);\n data.nPosition.set((data.position.x / rect.width) * 2 - 1, (-data.position.y / rect.height) * 2 + 1);\n}\n\nfunction isInside(rect: DOMRect) {\n return (\n pointerPosition.x >= rect.left &&\n pointerPosition.x <= rect.left + rect.width &&\n pointerPosition.y >= rect.top &&\n pointerPosition.y <= rect.top + rect.height\n );\n}\n\nclass Z extends InstancedMesh {\n config: typeof XConfig;\n physics: W;\n ambientLight: AmbientLight | undefined;\n light: PointLight | undefined;\n\n constructor(renderer: WebGLRenderer, params: Partial = {}) {\n const config = { ...XConfig, ...params };\n const roomEnv = new RoomEnvironment();\n const pmrem = new PMREMGenerator(renderer);\n const envTexture = pmrem.fromScene(roomEnv).texture;\n const geometry = new SphereGeometry();\n const material = new Y({ envMap: envTexture, ...config.materialParams });\n material.envMapRotation.x = -Math.PI / 2;\n super(geometry, material, config.count);\n this.config = config;\n this.physics = new W(config);\n this.#setupLights();\n this.setColors(config.colors);\n }\n\n #setupLights() {\n this.ambientLight = new AmbientLight(this.config.ambientColor, this.config.ambientIntensity);\n this.add(this.ambientLight);\n this.light = new PointLight(this.config.colors[0], this.config.lightIntensity);\n this.add(this.light);\n }\n\n setColors(colors: number[]) {\n if (Array.isArray(colors) && colors.length > 1) {\n const colorUtils = (function (colorsArr: number[]) {\n let baseColors: number[] = colorsArr;\n let colorObjects: Color[] = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n return {\n setColors: (cols: number[]) => {\n baseColors = cols;\n colorObjects = [];\n baseColors.forEach(col => {\n colorObjects.push(new Color(col));\n });\n },\n getColorAt: (ratio: number, out: Color = new Color()) => {\n const clamped = Math.max(0, Math.min(1, ratio));\n const scaled = clamped * (baseColors.length - 1);\n const idx = Math.floor(scaled);\n const start = colorObjects[idx];\n if (idx >= baseColors.length - 1) return start.clone();\n const alpha = scaled - idx;\n const end = colorObjects[idx + 1];\n out.r = start.r + alpha * (end.r - start.r);\n out.g = start.g + alpha * (end.g - start.g);\n out.b = start.b + alpha * (end.b - start.b);\n return out;\n }\n };\n })(colors);\n for (let idx = 0; idx < this.count; idx++) {\n this.setColorAt(idx, colorUtils.getColorAt(idx / this.count));\n if (idx === 0) {\n this.light!.color.copy(colorUtils.getColorAt(idx / this.count));\n }\n }\n\n if (!this.instanceColor) return;\n this.instanceColor.needsUpdate = true;\n }\n }\n\n update(deltaInfo: { delta: number }) {\n this.physics.update(deltaInfo);\n for (let idx = 0; idx < this.count; idx++) {\n U.position.fromArray(this.physics.positionData, 3 * idx);\n if (idx === 0 && this.config.followCursor === false) {\n U.scale.setScalar(0);\n } else {\n U.scale.setScalar(this.physics.sizeData[idx]);\n }\n U.updateMatrix();\n this.setMatrixAt(idx, U.matrix);\n if (idx === 0) this.light!.position.copy(U.position);\n }\n this.instanceMatrix.needsUpdate = true;\n }\n}\n\ninterface CreateBallpitReturn {\n three: X;\n spheres: Z;\n setCount: (count: number) => void;\n togglePause: () => void;\n dispose: () => void;\n}\n\nfunction createBallpit(canvas: HTMLCanvasElement, config: any = {}): CreateBallpitReturn {\n const threeInstance = new X({\n canvas,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let spheres: Z;\n threeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n threeInstance.camera.position.set(0, 0, 20);\n threeInstance.camera.lookAt(0, 0, 0);\n threeInstance.cameraMaxAspect = 1.5;\n threeInstance.resize();\n initialize(config);\n const raycaster = new Raycaster();\n const plane = new Plane(new Vector3(0, 0, 1), 0);\n const intersectionPoint = new Vector3();\n let isPaused = false;\n\n canvas.style.touchAction = 'none';\n canvas.style.userSelect = 'none';\n (canvas.style as any).webkitUserSelect = 'none';\n\n const pointerData = createPointerData({\n domElement: canvas,\n onMove() {\n raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n threeInstance.camera.getWorldDirection(plane.normal);\n raycaster.ray.intersectPlane(plane, intersectionPoint);\n spheres.physics.center.copy(intersectionPoint);\n spheres.config.controlSphere0 = true;\n },\n onLeave() {\n spheres.config.controlSphere0 = false;\n }\n });\n function initialize(cfg: any) {\n if (spheres) {\n threeInstance.clear();\n threeInstance.scene.remove(spheres);\n }\n spheres = new Z(threeInstance.renderer, cfg);\n threeInstance.scene.add(spheres);\n }\n threeInstance.onBeforeRender = deltaInfo => {\n if (!isPaused) spheres.update(deltaInfo);\n };\n threeInstance.onAfterResize = size => {\n spheres.config.maxX = size.wWidth / 2;\n spheres.config.maxY = size.wHeight / 2;\n };\n return {\n three: threeInstance,\n get spheres() {\n return spheres;\n },\n setCount(count: number) {\n initialize({ ...spheres.config, count });\n },\n togglePause() {\n isPaused = !isPaused;\n },\n dispose() {\n pointerData.dispose?.();\n threeInstance.dispose();\n }\n };\n}\n\ninterface BallpitProps {\n className?: string;\n followCursor?: boolean;\n [key: string]: any;\n}\n\nconst Ballpit: React.FC = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, {\n followCursor,\n ...props\n });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n" + "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 togglePause: () => void;\n dispose: () => void;\n}\n\nfunction createBallpit(canvas: HTMLCanvasElement, config: any = {}): CreateBallpitReturn {\n const threeInstance = new X({\n canvas,\n size: 'parent',\n rendererOptions: { antialias: true, alpha: true }\n });\n let spheres: Z;\n threeInstance.renderer.toneMapping = ACESFilmicToneMapping;\n threeInstance.camera.position.set(0, 0, 20);\n threeInstance.camera.lookAt(0, 0, 0);\n threeInstance.cameraMaxAspect = 1.5;\n threeInstance.resize();\n initialize(config);\n const raycaster = new Raycaster();\n const plane = new Plane(new Vector3(0, 0, 1), 0);\n const intersectionPoint = new Vector3();\n let isPaused = false;\n\n canvas.style.touchAction = 'none';\n canvas.style.userSelect = 'none';\n (canvas.style as any).webkitUserSelect = 'none';\n\n const pointerData = createPointerData({\n domElement: canvas,\n onMove() {\n raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);\n threeInstance.camera.getWorldDirection(plane.normal);\n raycaster.ray.intersectPlane(plane, intersectionPoint);\n spheres.physics.center.copy(intersectionPoint);\n spheres.config.controlSphere0 = true;\n },\n onLeave() {\n spheres.config.controlSphere0 = false;\n }\n });\n function initialize(cfg: any) {\n if (spheres) {\n threeInstance.clear();\n threeInstance.scene.remove(spheres);\n }\n spheres = new Z(threeInstance.renderer, cfg);\n threeInstance.scene.add(spheres);\n }\n threeInstance.onBeforeRender = deltaInfo => {\n if (!isPaused) spheres.update(deltaInfo);\n };\n threeInstance.onAfterResize = size => {\n spheres.config.maxX = size.wWidth / 2;\n spheres.config.maxY = size.wHeight / 2;\n };\n return {\n three: threeInstance,\n get spheres() {\n return spheres;\n },\n setCount(count: number) {\n initialize({ ...spheres.config, count });\n },\n togglePause() {\n isPaused = !isPaused;\n },\n dispose() {\n pointerData.dispose?.();\n threeInstance.dispose();\n }\n };\n}\n\ninterface BallpitProps {\n className?: string;\n followCursor?: boolean;\n [key: string]: any;\n}\n\nconst Ballpit: React.FC = ({ className = '', followCursor = true, ...props }) => {\n const canvasRef = useRef(null);\n const spheresInstanceRef = useRef(null);\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n spheresInstanceRef.current = createBallpit(canvas, {\n followCursor,\n ...props\n });\n\n return () => {\n if (spheresInstanceRef.current) {\n spheresInstanceRef.current.dispose();\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return ;\n};\n\nexport default Ballpit;\n" } ], "registryDependencies": [], "dependencies": [ "gsap@^3.13.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Beams-JS-CSS.json b/public/r/Beams-JS-CSS.json index 0a357e9a3..d1362e963 100644 --- a/public/r/Beams-JS-CSS.json +++ b/public/r/Beams-JS-CSS.json @@ -18,7 +18,7 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4" ] diff --git a/public/r/Beams-JS-TW.json b/public/r/Beams-JS-TW.json index 19457af27..529dc6f8e 100644 --- a/public/r/Beams-JS-TW.json +++ b/public/r/Beams-JS-TW.json @@ -13,7 +13,7 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4" ] diff --git a/public/r/Beams-TS-CSS.json b/public/r/Beams-TS-CSS.json index 15abe7797..6e3313344 100644 --- a/public/r/Beams-TS-CSS.json +++ b/public/r/Beams-TS-CSS.json @@ -18,7 +18,7 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4" ] diff --git a/public/r/Beams-TS-TW.json b/public/r/Beams-TS-TW.json index f21167682..9b1dfd1c0 100644 --- a/public/r/Beams-TS-TW.json +++ b/public/r/Beams-TS-TW.json @@ -13,7 +13,7 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4" ] diff --git a/public/r/Carousel-JS-CSS.json b/public/r/Carousel-JS-CSS.json index 3b66115d4..42170fb72 100644 --- a/public/r/Carousel-JS-CSS.json +++ b/public/r/Carousel-JS-CSS.json @@ -8,12 +8,12 @@ { "type": "registry:component", "path": "Carousel/Carousel.css", - "content": ".carousel-container {\n position: relative;\n overflow: hidden;\n border: 1px solid #555;\n border-radius: 24px;\n padding: 16px;\n --outer-r: 24px;\n --p-distance: 12px;\n}\n\n.carousel-track {\n display: flex;\n}\n\n.carousel-item {\n position: relative;\n display: flex;\n flex-shrink: 0;\n flex-direction: column;\n align-items: flex-start;\n justify-content: space-between;\n border: 1px solid #555;\n border-radius: calc(var(--outer-r) - var(--p-distance));\n background-color: #1B1722;\n overflow: hidden;\n cursor: grab;\n}\n\n.carousel-item:active {\n cursor: grabbing;\n}\n\n.carousel-container.round {\n border: 1px solid #555;\n}\n\n.carousel-item.round {\n background-color: #1B1722;\n position: relative;\n bottom: 0.1em;\n border: 1px solid #555;\n justify-content: center;\n align-items: center;\n text-align: center;\n}\n\n.carousel-item-header.round {\n padding: 0;\n margin: 0;\n}\n\n.carousel-indicators-container.round {\n position: absolute;\n z-index: 2;\n bottom: 3em;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.carousel-indicator.active {\n background-color: #333333;\n}\n\n.carousel-indicator.inactive {\n background-color: rgba(51, 51, 51, 0.4);\n}\n\n.carousel-item-header {\n margin-bottom: 16px;\n padding: 20px;\n padding-top: 20px;\n}\n\n.carousel-icon-container {\n display: flex;\n height: 28px;\n width: 28px;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n background-color: #fff;\n}\n\n.carousel-icon {\n height: 16px;\n width: 16px;\n color: #120F17;\n}\n\n.carousel-item-content {\n padding: 20px;\n padding-bottom: 20px;\n}\n\n.carousel-item-title {\n margin-bottom: 4px;\n font-weight: 900;\n font-size: 18px;\n color: #fff;\n}\n\n.carousel-item-description {\n font-size: 14px;\n color: #fff;\n}\n\n.carousel-indicators-container {\n display: flex;\n width: 100%;\n justify-content: center;\n}\n\n.carousel-indicators {\n margin-top: 16px;\n display: flex;\n width: 150px;\n justify-content: space-between;\n padding: 0 32px;\n}\n\n.carousel-indicator {\n height: 8px;\n width: 8px;\n border-radius: 50%;\n cursor: pointer;\n transition: background-color 150ms;\n}\n\n.carousel-indicator.active {\n background-color: #fff;\n}\n\n.carousel-indicator.inactive {\n background-color: #555;\n}\n" + "content": ".carousel-container {\n position: relative;\n overflow: hidden;\n border: 1px solid #555;\n border-radius: 24px;\n padding: 16px;\n --outer-r: 24px;\n --p-distance: 12px;\n}\n\n.carousel-track {\n display: flex;\n}\n\n.carousel-item {\n position: relative;\n display: flex;\n flex-shrink: 0;\n flex-direction: column;\n align-items: flex-start;\n justify-content: space-between;\n border: 1px solid #555;\n border-radius: calc(var(--outer-r) - var(--p-distance));\n background-color: #1B1722;\n overflow: hidden;\n cursor: grab;\n}\n\n.carousel-item:active {\n cursor: grabbing;\n}\n\n.carousel-container.round {\n border: 1px solid #555;\n}\n\n.carousel-item.round {\n background-color: #1B1722;\n position: relative;\n bottom: 0.1em;\n border: 1px solid #555;\n justify-content: center;\n align-items: center;\n text-align: center;\n}\n\n.carousel-item-header.round {\n padding: 0;\n margin: 0;\n}\n\n.carousel-indicators-container.round {\n position: absolute;\n z-index: 2;\n bottom: 3em;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.carousel-indicator.active {\n background-color: #333333;\n}\n\n.carousel-indicator.inactive {\n background-color: rgba(51, 51, 51, 0.4);\n}\n\n.carousel-item-header {\n margin-bottom: 16px;\n padding: 20px;\n padding-top: 20px;\n}\n\n.carousel-icon-container {\n display: flex;\n height: 28px;\n width: 28px;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n background-color: #fff;\n}\n\n.carousel-icon {\n height: 16px;\n width: 16px;\n color: #120F17;\n}\n\n.carousel-item-content {\n padding: 20px;\n padding-bottom: 20px;\n}\n\n.carousel-item-title {\n margin-bottom: 4px;\n font-weight: 900;\n font-size: 18px;\n color: #fff;\n}\n\n.carousel-item-description {\n font-size: 14px;\n color: #fff;\n}\n\n.carousel-indicators-container {\n display: flex;\n width: 100%;\n justify-content: center;\n}\n\n.carousel-indicators {\n margin-top: 16px;\n display: flex;\n width: 150px;\n justify-content: space-between;\n padding: 0 32px;\n}\n\n.carousel-indicator {\n height: 8px;\n width: 8px;\n border: none;\n padding: 0;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n transition: background-color 150ms;\n}\n\n.carousel-indicator:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.carousel-indicator.active {\n background-color: #fff;\n}\n\n.carousel-indicator.inactive {\n background-color: #555;\n}\n" }, { "type": "registry:component", "path": "Carousel/Carousel.jsx", - "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { motion, useMotionValue, useTransform } from 'motion/react';\n// replace icons with your own if needed\nimport { FiCircle, FiCode, FiFileText, FiLayers, FiLayout } from 'react-icons/fi';\n\nimport './Carousel.css';\n\nconst DEFAULT_ITEMS = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: \n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: \n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: \n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: \n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: \n }\n];\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring', stiffness: 300, damping: 30 };\n\nfunction CarouselItem({ item, index, itemWidth, round, trackItemOffset, x, transition }) {\n const range = [-(index + 1) * trackItemOffset, -index * trackItemOffset, -(index - 1) * trackItemOffset];\n const outputRange = [90, 0, -90];\n const rotateY = useTransform(x, range, outputRange, { clamp: false });\n\n return (\n \n
\n {item.icon}\n
\n
\n
{item.title}
\n

{item.description}

\n
\n \n );\n}\n\nexport default function Carousel({\n items = DEFAULT_ITEMS,\n baseWidth = 300,\n autoplay = false,\n autoplayDelay = 3000,\n pauseOnHover = false,\n loop = false,\n round = false\n}) {\n const containerPadding = 16;\n const itemWidth = baseWidth - containerPadding * 2;\n const trackItemOffset = itemWidth + GAP;\n const itemsForRender = useMemo(() => {\n if (!loop) return items;\n if (items.length === 0) return [];\n return [items[items.length - 1], ...items, items[0]];\n }, [items, loop]);\n\n const [position, setPosition] = useState(loop ? 1 : 0);\n const x = useMotionValue(0);\n const [isHovered, setIsHovered] = useState(false);\n const [isJumping, setIsJumping] = useState(false);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const containerRef = useRef(null);\n useEffect(() => {\n if (pauseOnHover && containerRef.current) {\n const container = containerRef.current;\n const handleMouseEnter = () => setIsHovered(true);\n const handleMouseLeave = () => setIsHovered(false);\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n };\n }\n }, [pauseOnHover]);\n\n useEffect(() => {\n if (!autoplay || itemsForRender.length <= 1) return undefined;\n if (pauseOnHover && isHovered) return undefined;\n\n const timer = setInterval(() => {\n setPosition(prev => Math.min(prev + 1, itemsForRender.length - 1));\n }, autoplayDelay);\n\n return () => clearInterval(timer);\n }, [autoplay, autoplayDelay, isHovered, pauseOnHover, itemsForRender.length]);\n\n useEffect(() => {\n const startingPosition = loop ? 1 : 0;\n setPosition(startingPosition);\n x.set(-startingPosition * trackItemOffset);\n }, [items.length, loop, trackItemOffset, x]);\n\n useEffect(() => {\n if (!loop && position > itemsForRender.length - 1) {\n setPosition(Math.max(0, itemsForRender.length - 1));\n }\n }, [itemsForRender.length, loop, position]);\n\n const effectiveTransition = isJumping ? { duration: 0 } : SPRING_OPTIONS;\n\n const handleAnimationStart = () => {\n setIsAnimating(true);\n };\n\n const handleAnimationComplete = () => {\n if (!loop || itemsForRender.length <= 1) {\n setIsAnimating(false);\n return;\n }\n const lastCloneIndex = itemsForRender.length - 1;\n\n if (position === lastCloneIndex) {\n setIsJumping(true);\n const target = 1;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n if (position === 0) {\n setIsJumping(true);\n const target = items.length;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n setIsAnimating(false);\n };\n\n const handleDragEnd = (_, info) => {\n const { offset, velocity } = info;\n const direction =\n offset.x < -DRAG_BUFFER || velocity.x < -VELOCITY_THRESHOLD\n ? 1\n : offset.x > DRAG_BUFFER || velocity.x > VELOCITY_THRESHOLD\n ? -1\n : 0;\n\n if (direction === 0) return;\n\n setPosition(prev => {\n const next = prev + direction;\n const max = itemsForRender.length - 1;\n return Math.max(0, Math.min(next, max));\n });\n };\n\n const dragProps = loop\n ? {}\n : {\n dragConstraints: {\n left: -trackItemOffset * Math.max(itemsForRender.length - 1, 0),\n right: 0\n }\n };\n\n const activeIndex =\n items.length === 0 ? 0 : loop ? (position - 1 + items.length) % items.length : Math.min(position, items.length - 1);\n\n return (\n \n \n {itemsForRender.map((item, index) => (\n \n ))}\n \n
\n
\n {items.map((_, index) => (\n setPosition(loop ? index + 1 : index)}\n transition={{ duration: 0.15 }}\n />\n ))}\n
\n
\n \n );\n}\n" + "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { motion, useMotionValue, useTransform } from 'motion/react';\n// replace icons with your own if needed\nimport { FiCircle, FiCode, FiFileText, FiLayers, FiLayout } from 'react-icons/fi';\n\nimport './Carousel.css';\n\nconst DEFAULT_ITEMS = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: \n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: \n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: \n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: \n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: \n }\n];\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring', stiffness: 300, damping: 30 };\n\nfunction CarouselItem({ item, index, itemWidth, round, trackItemOffset, x, transition }) {\n const range = [-(index + 1) * trackItemOffset, -index * trackItemOffset, -(index - 1) * trackItemOffset];\n const outputRange = [90, 0, -90];\n const rotateY = useTransform(x, range, outputRange, { clamp: false });\n\n return (\n \n
\n {item.icon}\n
\n
\n
{item.title}
\n

{item.description}

\n
\n \n );\n}\n\nexport default function Carousel({\n items = DEFAULT_ITEMS,\n baseWidth = 300,\n autoplay = false,\n autoplayDelay = 3000,\n pauseOnHover = false,\n loop = false,\n round = false\n}) {\n const containerPadding = 16;\n const itemWidth = baseWidth - containerPadding * 2;\n const trackItemOffset = itemWidth + GAP;\n const itemsForRender = useMemo(() => {\n if (!loop) return items;\n if (items.length === 0) return [];\n return [items[items.length - 1], ...items, items[0]];\n }, [items, loop]);\n\n const [position, setPosition] = useState(loop ? 1 : 0);\n const x = useMotionValue(0);\n const [isHovered, setIsHovered] = useState(false);\n const [isJumping, setIsJumping] = useState(false);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const containerRef = useRef(null);\n useEffect(() => {\n if (pauseOnHover && containerRef.current) {\n const container = containerRef.current;\n const handleMouseEnter = () => setIsHovered(true);\n const handleMouseLeave = () => setIsHovered(false);\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n };\n }\n }, [pauseOnHover]);\n\n useEffect(() => {\n if (!autoplay || itemsForRender.length <= 1) return undefined;\n if (pauseOnHover && isHovered) return undefined;\n\n const timer = setInterval(() => {\n setPosition(prev => Math.min(prev + 1, itemsForRender.length - 1));\n }, autoplayDelay);\n\n return () => clearInterval(timer);\n }, [autoplay, autoplayDelay, isHovered, pauseOnHover, itemsForRender.length]);\n\n useEffect(() => {\n const startingPosition = loop ? 1 : 0;\n setPosition(startingPosition);\n x.set(-startingPosition * trackItemOffset);\n }, [items.length, loop, trackItemOffset, x]);\n\n useEffect(() => {\n if (!loop && position > itemsForRender.length - 1) {\n setPosition(Math.max(0, itemsForRender.length - 1));\n }\n }, [itemsForRender.length, loop, position]);\n\n const effectiveTransition = isJumping ? { duration: 0 } : SPRING_OPTIONS;\n\n const handleAnimationStart = () => {\n setIsAnimating(true);\n };\n\n const handleAnimationComplete = () => {\n if (!loop || itemsForRender.length <= 1) {\n setIsAnimating(false);\n return;\n }\n const lastCloneIndex = itemsForRender.length - 1;\n\n if (position === lastCloneIndex) {\n setIsJumping(true);\n const target = 1;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n if (position === 0) {\n setIsJumping(true);\n const target = items.length;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n setIsAnimating(false);\n };\n\n const handleDragEnd = (_, info) => {\n const { offset, velocity } = info;\n const direction =\n offset.x < -DRAG_BUFFER || velocity.x < -VELOCITY_THRESHOLD\n ? 1\n : offset.x > DRAG_BUFFER || velocity.x > VELOCITY_THRESHOLD\n ? -1\n : 0;\n\n if (direction === 0) return;\n\n setPosition(prev => {\n const next = prev + direction;\n const max = itemsForRender.length - 1;\n return Math.max(0, Math.min(next, max));\n });\n };\n\n const dragProps = loop\n ? {}\n : {\n dragConstraints: {\n left: -trackItemOffset * Math.max(itemsForRender.length - 1, 0),\n right: 0\n }\n };\n\n const activeIndex =\n items.length === 0 ? 0 : loop ? (position - 1 + items.length) % items.length : Math.min(position, items.length - 1);\n\n return (\n \n \n {itemsForRender.map((item, index) => (\n \n ))}\n \n
\n
\n {items.map((_, index) => (\n setPosition(loop ? index + 1 : index)}\n transition={{ duration: 0.15 }}\n />\n ))}\n
\n
\n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/Carousel-JS-TW.json b/public/r/Carousel-JS-TW.json index 57e0dd660..000face94 100644 --- a/public/r/Carousel-JS-TW.json +++ b/public/r/Carousel-JS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Carousel/Carousel.jsx", - "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { motion, useMotionValue, useTransform } from 'motion/react';\n// replace icons with your own if needed\nimport { FiCircle, FiCode, FiFileText, FiLayers, FiLayout } from 'react-icons/fi';\n\nconst DEFAULT_ITEMS = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: \n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: \n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: \n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: \n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: \n }\n];\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring', stiffness: 300, damping: 30 };\n\nfunction CarouselItem({ item, index, itemWidth, round, trackItemOffset, x, transition }) {\n const range = [-(index + 1) * trackItemOffset, -index * trackItemOffset, -(index - 1) * trackItemOffset];\n const outputRange = [90, 0, -90];\n const rotateY = useTransform(x, range, outputRange, { clamp: false });\n\n return (\n \n
\n \n {item.icon}\n \n
\n
\n
{item.title}
\n

{item.description}

\n
\n \n );\n}\n\nexport default function Carousel({\n items = DEFAULT_ITEMS,\n baseWidth = 300,\n autoplay = false,\n autoplayDelay = 3000,\n pauseOnHover = false,\n loop = false,\n round = false\n}) {\n const containerPadding = 16;\n const itemWidth = baseWidth - containerPadding * 2;\n const trackItemOffset = itemWidth + GAP;\n const itemsForRender = useMemo(() => {\n if (!loop) return items;\n if (items.length === 0) return [];\n return [items[items.length - 1], ...items, items[0]];\n }, [items, loop]);\n\n const [position, setPosition] = useState(loop ? 1 : 0);\n const x = useMotionValue(0);\n const [isHovered, setIsHovered] = useState(false);\n const [isJumping, setIsJumping] = useState(false);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const containerRef = useRef(null);\n useEffect(() => {\n if (pauseOnHover && containerRef.current) {\n const container = containerRef.current;\n const handleMouseEnter = () => setIsHovered(true);\n const handleMouseLeave = () => setIsHovered(false);\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n };\n }\n }, [pauseOnHover]);\n\n useEffect(() => {\n if (!autoplay || itemsForRender.length <= 1) return undefined;\n if (pauseOnHover && isHovered) return undefined;\n\n const timer = setInterval(() => {\n setPosition(prev => Math.min(prev + 1, itemsForRender.length - 1));\n }, autoplayDelay);\n\n return () => clearInterval(timer);\n }, [autoplay, autoplayDelay, isHovered, pauseOnHover, itemsForRender.length]);\n\n useEffect(() => {\n const startingPosition = loop ? 1 : 0;\n setPosition(startingPosition);\n x.set(-startingPosition * trackItemOffset);\n }, [items.length, loop, trackItemOffset, x]);\n\n useEffect(() => {\n if (!loop && position > itemsForRender.length - 1) {\n setPosition(Math.max(0, itemsForRender.length - 1));\n }\n }, [itemsForRender.length, loop, position]);\n\n const effectiveTransition = isJumping ? { duration: 0 } : SPRING_OPTIONS;\n\n const handleAnimationStart = () => {\n setIsAnimating(true);\n };\n\n const handleAnimationComplete = () => {\n if (!loop || itemsForRender.length <= 1) {\n setIsAnimating(false);\n return;\n }\n const lastCloneIndex = itemsForRender.length - 1;\n\n if (position === lastCloneIndex) {\n setIsJumping(true);\n const target = 1;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n if (position === 0) {\n setIsJumping(true);\n const target = items.length;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n setIsAnimating(false);\n };\n\n const handleDragEnd = (_, info) => {\n const { offset, velocity } = info;\n const direction =\n offset.x < -DRAG_BUFFER || velocity.x < -VELOCITY_THRESHOLD\n ? 1\n : offset.x > DRAG_BUFFER || velocity.x > VELOCITY_THRESHOLD\n ? -1\n : 0;\n\n if (direction === 0) return;\n\n setPosition(prev => {\n const next = prev + direction;\n const max = itemsForRender.length - 1;\n return Math.max(0, Math.min(next, max));\n });\n };\n\n const dragProps = loop\n ? {}\n : {\n dragConstraints: {\n left: -trackItemOffset * Math.max(itemsForRender.length - 1, 0),\n right: 0\n }\n };\n\n const activeIndex =\n items.length === 0 ? 0 : loop ? (position - 1 + items.length) % items.length : Math.min(position, items.length - 1);\n\n return (\n \n \n {itemsForRender.map((item, index) => (\n \n ))}\n \n
\n
\n {items.map((_, index) => (\n setPosition(loop ? index + 1 : index)}\n transition={{ duration: 0.15 }}\n />\n ))}\n
\n
\n \n );\n}\n" + "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { motion, useMotionValue, useTransform } from 'motion/react';\n// replace icons with your own if needed\nimport { FiCircle, FiCode, FiFileText, FiLayers, FiLayout } from 'react-icons/fi';\n\nconst DEFAULT_ITEMS = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: \n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: \n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: \n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: \n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: \n }\n];\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring', stiffness: 300, damping: 30 };\n\nfunction CarouselItem({ item, index, itemWidth, round, trackItemOffset, x, transition }) {\n const range = [-(index + 1) * trackItemOffset, -index * trackItemOffset, -(index - 1) * trackItemOffset];\n const outputRange = [90, 0, -90];\n const rotateY = useTransform(x, range, outputRange, { clamp: false });\n\n return (\n \n
\n \n {item.icon}\n \n
\n
\n
{item.title}
\n

{item.description}

\n
\n \n );\n}\n\nexport default function Carousel({\n items = DEFAULT_ITEMS,\n baseWidth = 300,\n autoplay = false,\n autoplayDelay = 3000,\n pauseOnHover = false,\n loop = false,\n round = false\n}) {\n const containerPadding = 16;\n const itemWidth = baseWidth - containerPadding * 2;\n const trackItemOffset = itemWidth + GAP;\n const itemsForRender = useMemo(() => {\n if (!loop) return items;\n if (items.length === 0) return [];\n return [items[items.length - 1], ...items, items[0]];\n }, [items, loop]);\n\n const [position, setPosition] = useState(loop ? 1 : 0);\n const x = useMotionValue(0);\n const [isHovered, setIsHovered] = useState(false);\n const [isJumping, setIsJumping] = useState(false);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const containerRef = useRef(null);\n useEffect(() => {\n if (pauseOnHover && containerRef.current) {\n const container = containerRef.current;\n const handleMouseEnter = () => setIsHovered(true);\n const handleMouseLeave = () => setIsHovered(false);\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n };\n }\n }, [pauseOnHover]);\n\n useEffect(() => {\n if (!autoplay || itemsForRender.length <= 1) return undefined;\n if (pauseOnHover && isHovered) return undefined;\n\n const timer = setInterval(() => {\n setPosition(prev => Math.min(prev + 1, itemsForRender.length - 1));\n }, autoplayDelay);\n\n return () => clearInterval(timer);\n }, [autoplay, autoplayDelay, isHovered, pauseOnHover, itemsForRender.length]);\n\n useEffect(() => {\n const startingPosition = loop ? 1 : 0;\n setPosition(startingPosition);\n x.set(-startingPosition * trackItemOffset);\n }, [items.length, loop, trackItemOffset, x]);\n\n useEffect(() => {\n if (!loop && position > itemsForRender.length - 1) {\n setPosition(Math.max(0, itemsForRender.length - 1));\n }\n }, [itemsForRender.length, loop, position]);\n\n const effectiveTransition = isJumping ? { duration: 0 } : SPRING_OPTIONS;\n\n const handleAnimationStart = () => {\n setIsAnimating(true);\n };\n\n const handleAnimationComplete = () => {\n if (!loop || itemsForRender.length <= 1) {\n setIsAnimating(false);\n return;\n }\n const lastCloneIndex = itemsForRender.length - 1;\n\n if (position === lastCloneIndex) {\n setIsJumping(true);\n const target = 1;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n if (position === 0) {\n setIsJumping(true);\n const target = items.length;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n setIsAnimating(false);\n };\n\n const handleDragEnd = (_, info) => {\n const { offset, velocity } = info;\n const direction =\n offset.x < -DRAG_BUFFER || velocity.x < -VELOCITY_THRESHOLD\n ? 1\n : offset.x > DRAG_BUFFER || velocity.x > VELOCITY_THRESHOLD\n ? -1\n : 0;\n\n if (direction === 0) return;\n\n setPosition(prev => {\n const next = prev + direction;\n const max = itemsForRender.length - 1;\n return Math.max(0, Math.min(next, max));\n });\n };\n\n const dragProps = loop\n ? {}\n : {\n dragConstraints: {\n left: -trackItemOffset * Math.max(itemsForRender.length - 1, 0),\n right: 0\n }\n };\n\n const activeIndex =\n items.length === 0 ? 0 : loop ? (position - 1 + items.length) % items.length : Math.min(position, items.length - 1);\n\n return (\n \n \n {itemsForRender.map((item, index) => (\n \n ))}\n \n
\n
\n {items.map((_, index) => (\n setPosition(loop ? index + 1 : index)}\n transition={{ duration: 0.15 }}\n />\n ))}\n
\n
\n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/Carousel-TS-CSS.json b/public/r/Carousel-TS-CSS.json index 3f5cc1fe8..ccaefe568 100644 --- a/public/r/Carousel-TS-CSS.json +++ b/public/r/Carousel-TS-CSS.json @@ -8,12 +8,12 @@ { "type": "registry:component", "path": "Carousel/Carousel.css", - "content": ".carousel-container {\n position: relative;\n overflow: hidden;\n border: 1px solid #555;\n border-radius: 24px;\n padding: 16px;\n --outer-r: 24px;\n --p-distance: 12px;\n}\n\n.carousel-track {\n display: flex;\n}\n\n.carousel-item {\n position: relative;\n display: flex;\n flex-shrink: 0;\n flex-direction: column;\n align-items: flex-start;\n justify-content: space-between;\n border: 1px solid #555;\n border-radius: calc(var(--outer-r) - var(--p-distance));\n background-color: #0d0d0d;\n overflow: hidden;\n cursor: grab;\n}\n\n.carousel-item:active {\n cursor: grabbing;\n}\n\n.carousel-container.round {\n border: 1px solid #555;\n}\n\n.carousel-item.round {\n background-color: #0d0d0d;\n position: relative;\n bottom: 0.1em;\n border: 1px solid #555;\n justify-content: center;\n align-items: center;\n text-align: center;\n}\n\n.carousel-item-header.round {\n padding: 0;\n margin: 0;\n}\n\n.carousel-indicators-container.round {\n position: absolute;\n z-index: 2;\n bottom: 3em;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.carousel-indicator.active {\n background-color: #333333;\n}\n\n.carousel-indicator.inactive {\n background-color: rgba(51, 51, 51, 0.4);\n}\n\n.carousel-item-header {\n margin-bottom: 16px;\n padding: 20px;\n padding-top: 20px;\n}\n\n.carousel-icon-container {\n display: flex;\n height: 28px;\n width: 28px;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n background-color: #fff;\n}\n\n.carousel-icon {\n height: 16px;\n width: 16px;\n color: #120F17;\n}\n\n.carousel-item-content {\n padding: 20px;\n padding-bottom: 20px;\n}\n\n.carousel-item-title {\n margin-bottom: 4px;\n font-weight: 900;\n font-size: 18px;\n color: #fff;\n}\n\n.carousel-item-description {\n font-size: 14px;\n color: #fff;\n}\n\n.carousel-indicators-container {\n display: flex;\n width: 100%;\n justify-content: center;\n}\n\n.carousel-indicators {\n margin-top: 16px;\n display: flex;\n width: 150px;\n justify-content: space-between;\n padding: 0 32px;\n}\n\n.carousel-indicator {\n height: 8px;\n width: 8px;\n border-radius: 50%;\n cursor: pointer;\n transition: background-color 150ms;\n}\n\n.carousel-indicator.active {\n background-color: #fff;\n}\n\n.carousel-indicator.inactive {\n background-color: #555;\n}\n" + "content": ".carousel-container {\n position: relative;\n overflow: hidden;\n border: 1px solid #555;\n border-radius: 24px;\n padding: 16px;\n --outer-r: 24px;\n --p-distance: 12px;\n}\n\n.carousel-track {\n display: flex;\n}\n\n.carousel-item {\n position: relative;\n display: flex;\n flex-shrink: 0;\n flex-direction: column;\n align-items: flex-start;\n justify-content: space-between;\n border: 1px solid #555;\n border-radius: calc(var(--outer-r) - var(--p-distance));\n background-color: #0d0d0d;\n overflow: hidden;\n cursor: grab;\n}\n\n.carousel-item:active {\n cursor: grabbing;\n}\n\n.carousel-container.round {\n border: 1px solid #555;\n}\n\n.carousel-item.round {\n background-color: #0d0d0d;\n position: relative;\n bottom: 0.1em;\n border: 1px solid #555;\n justify-content: center;\n align-items: center;\n text-align: center;\n}\n\n.carousel-item-header.round {\n padding: 0;\n margin: 0;\n}\n\n.carousel-indicators-container.round {\n position: absolute;\n z-index: 2;\n bottom: 3em;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.carousel-indicator.active {\n background-color: #333333;\n}\n\n.carousel-indicator.inactive {\n background-color: rgba(51, 51, 51, 0.4);\n}\n\n.carousel-item-header {\n margin-bottom: 16px;\n padding: 20px;\n padding-top: 20px;\n}\n\n.carousel-icon-container {\n display: flex;\n height: 28px;\n width: 28px;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n background-color: #fff;\n}\n\n.carousel-icon {\n height: 16px;\n width: 16px;\n color: #120F17;\n}\n\n.carousel-item-content {\n padding: 20px;\n padding-bottom: 20px;\n}\n\n.carousel-item-title {\n margin-bottom: 4px;\n font-weight: 900;\n font-size: 18px;\n color: #fff;\n}\n\n.carousel-item-description {\n font-size: 14px;\n color: #fff;\n}\n\n.carousel-indicators-container {\n display: flex;\n width: 100%;\n justify-content: center;\n}\n\n.carousel-indicators {\n margin-top: 16px;\n display: flex;\n width: 150px;\n justify-content: space-between;\n padding: 0 32px;\n}\n\n.carousel-indicator {\n height: 8px;\n width: 8px;\n border: none;\n padding: 0;\n appearance: none;\n border-radius: 50%;\n cursor: pointer;\n transition: background-color 150ms;\n}\n\n.carousel-indicator:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.carousel-indicator.active {\n background-color: #fff;\n}\n\n.carousel-indicator.inactive {\n background-color: #555;\n}\n" }, { "type": "registry:component", "path": "Carousel/Carousel.tsx", - "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { motion, PanInfo, useMotionValue, useTransform } from 'motion/react';\n// replace icons with your own if needed\nimport { FiCircle, FiCode, FiFileText, FiLayers, FiLayout } from 'react-icons/fi';\nimport './Carousel.css';\n\nexport interface CarouselItem {\n title: string;\n description: string;\n id: number;\n icon: React.ReactElement;\n}\n\nexport interface CarouselProps {\n items?: CarouselItem[];\n baseWidth?: number;\n autoplay?: boolean;\n autoplayDelay?: number;\n pauseOnHover?: boolean;\n loop?: boolean;\n round?: boolean;\n}\n\nconst DEFAULT_ITEMS: CarouselItem[] = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: \n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: \n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: \n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: \n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: \n }\n];\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring' as const, stiffness: 300, damping: 30 };\n\ninterface CarouselItemProps {\n item: CarouselItem;\n index: number;\n itemWidth: number;\n round: boolean;\n trackItemOffset: number;\n x: any;\n transition: any;\n}\n\nfunction CarouselItem({ item, index, itemWidth, round, trackItemOffset, x, transition }: CarouselItemProps) {\n const range = [-(index + 1) * trackItemOffset, -index * trackItemOffset, -(index - 1) * trackItemOffset];\n const outputRange = [90, 0, -90];\n const rotateY = useTransform(x, range, outputRange, { clamp: false });\n\n return (\n \n
\n {item.icon}\n
\n
\n
{item.title}
\n

{item.description}

\n
\n \n );\n}\n\nexport default function Carousel({\n items = DEFAULT_ITEMS,\n baseWidth = 300,\n autoplay = false,\n autoplayDelay = 3000,\n pauseOnHover = false,\n loop = false,\n round = false\n}: CarouselProps): React.JSX.Element {\n const containerPadding = 16;\n const itemWidth = baseWidth - containerPadding * 2;\n const trackItemOffset = itemWidth + GAP;\n const itemsForRender = useMemo(() => {\n if (!loop) return items;\n if (items.length === 0) return [];\n return [items[items.length - 1], ...items, items[0]];\n }, [items, loop]);\n\n const [position, setPosition] = useState(loop ? 1 : 0);\n const x = useMotionValue(0);\n const [isHovered, setIsHovered] = useState(false);\n const [isJumping, setIsJumping] = useState(false);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const containerRef = useRef(null);\n useEffect(() => {\n if (pauseOnHover && containerRef.current) {\n const container = containerRef.current;\n const handleMouseEnter = () => setIsHovered(true);\n const handleMouseLeave = () => setIsHovered(false);\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n };\n }\n }, [pauseOnHover]);\n\n useEffect(() => {\n if (!autoplay || itemsForRender.length <= 1) return undefined;\n if (pauseOnHover && isHovered) return undefined;\n\n const timer = setInterval(() => {\n setPosition(prev => Math.min(prev + 1, itemsForRender.length - 1));\n }, autoplayDelay);\n\n return () => clearInterval(timer);\n }, [autoplay, autoplayDelay, isHovered, pauseOnHover, itemsForRender.length]);\n\n useEffect(() => {\n const startingPosition = loop ? 1 : 0;\n setPosition(startingPosition);\n x.set(-startingPosition * trackItemOffset);\n }, [items.length, loop, trackItemOffset, x]);\n\n useEffect(() => {\n if (!loop && position > itemsForRender.length - 1) {\n setPosition(Math.max(0, itemsForRender.length - 1));\n }\n }, [itemsForRender.length, loop, position]);\n\n const effectiveTransition = isJumping ? { duration: 0 } : SPRING_OPTIONS;\n\n const handleAnimationStart = () => {\n setIsAnimating(true);\n };\n\n const handleAnimationComplete = () => {\n if (!loop || itemsForRender.length <= 1) {\n setIsAnimating(false);\n return;\n }\n const lastCloneIndex = itemsForRender.length - 1;\n\n if (position === lastCloneIndex) {\n setIsJumping(true);\n const target = 1;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n if (position === 0) {\n setIsJumping(true);\n const target = items.length;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n setIsAnimating(false);\n };\n\n const handleDragEnd = (_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo): void => {\n const { offset, velocity } = info;\n const direction =\n offset.x < -DRAG_BUFFER || velocity.x < -VELOCITY_THRESHOLD\n ? 1\n : offset.x > DRAG_BUFFER || velocity.x > VELOCITY_THRESHOLD\n ? -1\n : 0;\n\n if (direction === 0) return;\n\n setPosition(prev => {\n const next = prev + direction;\n const max = itemsForRender.length - 1;\n return Math.max(0, Math.min(next, max));\n });\n };\n\n const dragProps = loop\n ? {}\n : {\n dragConstraints: {\n left: -trackItemOffset * Math.max(itemsForRender.length - 1, 0),\n right: 0\n }\n };\n\n const activeIndex =\n items.length === 0 ? 0 : loop ? (position - 1 + items.length) % items.length : Math.min(position, items.length - 1);\n\n return (\n \n \n {itemsForRender.map((item, index) => (\n \n ))}\n \n
\n
\n {items.map((_, index) => (\n setPosition(loop ? index + 1 : index)}\n transition={{ duration: 0.15 }}\n />\n ))}\n
\n
\n \n );\n}\n" + "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { motion, PanInfo, useMotionValue, useTransform } from 'motion/react';\n// replace icons with your own if needed\nimport { FiCircle, FiCode, FiFileText, FiLayers, FiLayout } from 'react-icons/fi';\nimport './Carousel.css';\n\nexport interface CarouselItem {\n title: string;\n description: string;\n id: number;\n icon: React.ReactElement;\n}\n\nexport interface CarouselProps {\n items?: CarouselItem[];\n baseWidth?: number;\n autoplay?: boolean;\n autoplayDelay?: number;\n pauseOnHover?: boolean;\n loop?: boolean;\n round?: boolean;\n}\n\nconst DEFAULT_ITEMS: CarouselItem[] = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: \n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: \n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: \n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: \n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: \n }\n];\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring' as const, stiffness: 300, damping: 30 };\n\ninterface CarouselItemProps {\n item: CarouselItem;\n index: number;\n itemWidth: number;\n round: boolean;\n trackItemOffset: number;\n x: any;\n transition: any;\n}\n\nfunction CarouselItem({ item, index, itemWidth, round, trackItemOffset, x, transition }: CarouselItemProps) {\n const range = [-(index + 1) * trackItemOffset, -index * trackItemOffset, -(index - 1) * trackItemOffset];\n const outputRange = [90, 0, -90];\n const rotateY = useTransform(x, range, outputRange, { clamp: false });\n\n return (\n \n
\n {item.icon}\n
\n
\n
{item.title}
\n

{item.description}

\n
\n \n );\n}\n\nexport default function Carousel({\n items = DEFAULT_ITEMS,\n baseWidth = 300,\n autoplay = false,\n autoplayDelay = 3000,\n pauseOnHover = false,\n loop = false,\n round = false\n}: CarouselProps): React.JSX.Element {\n const containerPadding = 16;\n const itemWidth = baseWidth - containerPadding * 2;\n const trackItemOffset = itemWidth + GAP;\n const itemsForRender = useMemo(() => {\n if (!loop) return items;\n if (items.length === 0) return [];\n return [items[items.length - 1], ...items, items[0]];\n }, [items, loop]);\n\n const [position, setPosition] = useState(loop ? 1 : 0);\n const x = useMotionValue(0);\n const [isHovered, setIsHovered] = useState(false);\n const [isJumping, setIsJumping] = useState(false);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const containerRef = useRef(null);\n useEffect(() => {\n if (pauseOnHover && containerRef.current) {\n const container = containerRef.current;\n const handleMouseEnter = () => setIsHovered(true);\n const handleMouseLeave = () => setIsHovered(false);\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n };\n }\n }, [pauseOnHover]);\n\n useEffect(() => {\n if (!autoplay || itemsForRender.length <= 1) return undefined;\n if (pauseOnHover && isHovered) return undefined;\n\n const timer = setInterval(() => {\n setPosition(prev => Math.min(prev + 1, itemsForRender.length - 1));\n }, autoplayDelay);\n\n return () => clearInterval(timer);\n }, [autoplay, autoplayDelay, isHovered, pauseOnHover, itemsForRender.length]);\n\n useEffect(() => {\n const startingPosition = loop ? 1 : 0;\n setPosition(startingPosition);\n x.set(-startingPosition * trackItemOffset);\n }, [items.length, loop, trackItemOffset, x]);\n\n useEffect(() => {\n if (!loop && position > itemsForRender.length - 1) {\n setPosition(Math.max(0, itemsForRender.length - 1));\n }\n }, [itemsForRender.length, loop, position]);\n\n const effectiveTransition = isJumping ? { duration: 0 } : SPRING_OPTIONS;\n\n const handleAnimationStart = () => {\n setIsAnimating(true);\n };\n\n const handleAnimationComplete = () => {\n if (!loop || itemsForRender.length <= 1) {\n setIsAnimating(false);\n return;\n }\n const lastCloneIndex = itemsForRender.length - 1;\n\n if (position === lastCloneIndex) {\n setIsJumping(true);\n const target = 1;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n if (position === 0) {\n setIsJumping(true);\n const target = items.length;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n setIsAnimating(false);\n };\n\n const handleDragEnd = (_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo): void => {\n const { offset, velocity } = info;\n const direction =\n offset.x < -DRAG_BUFFER || velocity.x < -VELOCITY_THRESHOLD\n ? 1\n : offset.x > DRAG_BUFFER || velocity.x > VELOCITY_THRESHOLD\n ? -1\n : 0;\n\n if (direction === 0) return;\n\n setPosition(prev => {\n const next = prev + direction;\n const max = itemsForRender.length - 1;\n return Math.max(0, Math.min(next, max));\n });\n };\n\n const dragProps = loop\n ? {}\n : {\n dragConstraints: {\n left: -trackItemOffset * Math.max(itemsForRender.length - 1, 0),\n right: 0\n }\n };\n\n const activeIndex =\n items.length === 0 ? 0 : loop ? (position - 1 + items.length) % items.length : Math.min(position, items.length - 1);\n\n return (\n \n \n {itemsForRender.map((item, index) => (\n \n ))}\n \n
\n
\n {items.map((_, index) => (\n setPosition(loop ? index + 1 : index)}\n transition={{ duration: 0.15 }}\n />\n ))}\n
\n
\n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/Carousel-TS-TW.json b/public/r/Carousel-TS-TW.json index 9ee8c0b7f..3561e9747 100644 --- a/public/r/Carousel-TS-TW.json +++ b/public/r/Carousel-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Carousel/Carousel.tsx", - "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { motion, PanInfo, useMotionValue, useTransform } from 'motion/react';\nimport React, { JSX } from 'react';\n\n// replace icons with your own if needed\nimport { FiCircle, FiCode, FiFileText, FiLayers, FiLayout } from 'react-icons/fi';\nexport interface CarouselItem {\n title: string;\n description: string;\n id: number;\n icon: React.ReactNode;\n}\n\nexport interface CarouselProps {\n items?: CarouselItem[];\n baseWidth?: number;\n autoplay?: boolean;\n autoplayDelay?: number;\n pauseOnHover?: boolean;\n loop?: boolean;\n round?: boolean;\n}\n\nconst DEFAULT_ITEMS: CarouselItem[] = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: \n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: \n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: \n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: \n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: \n }\n];\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring' as const, stiffness: 300, damping: 30 };\n\ninterface CarouselItemProps {\n item: CarouselItem;\n index: number;\n itemWidth: number;\n round: boolean;\n trackItemOffset: number;\n x: any;\n transition: any;\n}\n\nfunction CarouselItem({ item, index, itemWidth, round, trackItemOffset, x, transition }: CarouselItemProps) {\n const range = [-(index + 1) * trackItemOffset, -index * trackItemOffset, -(index - 1) * trackItemOffset];\n const outputRange = [90, 0, -90];\n const rotateY = useTransform(x, range, outputRange, { clamp: false });\n\n return (\n \n
\n \n {item.icon}\n \n
\n
\n
{item.title}
\n

{item.description}

\n
\n \n );\n}\n\nexport default function Carousel({\n items = DEFAULT_ITEMS,\n baseWidth = 300,\n autoplay = false,\n autoplayDelay = 3000,\n pauseOnHover = false,\n loop = false,\n round = false\n}: CarouselProps): JSX.Element {\n const containerPadding = 16;\n const itemWidth = baseWidth - containerPadding * 2;\n const trackItemOffset = itemWidth + GAP;\n const itemsForRender = useMemo(() => {\n if (!loop) return items;\n if (items.length === 0) return [];\n return [items[items.length - 1], ...items, items[0]];\n }, [items, loop]);\n\n const [position, setPosition] = useState(loop ? 1 : 0);\n const x = useMotionValue(0);\n const [isHovered, setIsHovered] = useState(false);\n const [isJumping, setIsJumping] = useState(false);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const containerRef = useRef(null);\n useEffect(() => {\n if (pauseOnHover && containerRef.current) {\n const container = containerRef.current;\n const handleMouseEnter = () => setIsHovered(true);\n const handleMouseLeave = () => setIsHovered(false);\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n };\n }\n }, [pauseOnHover]);\n\n useEffect(() => {\n if (!autoplay || itemsForRender.length <= 1) return undefined;\n if (pauseOnHover && isHovered) return undefined;\n\n const timer = setInterval(() => {\n setPosition(prev => Math.min(prev + 1, itemsForRender.length - 1));\n }, autoplayDelay);\n\n return () => clearInterval(timer);\n }, [autoplay, autoplayDelay, isHovered, pauseOnHover, itemsForRender.length]);\n\n useEffect(() => {\n const startingPosition = loop ? 1 : 0;\n setPosition(startingPosition);\n x.set(-startingPosition * trackItemOffset);\n }, [items.length, loop, trackItemOffset, x]);\n\n useEffect(() => {\n if (!loop && position > itemsForRender.length - 1) {\n setPosition(Math.max(0, itemsForRender.length - 1));\n }\n }, [itemsForRender.length, loop, position]);\n\n const effectiveTransition = isJumping ? { duration: 0 } : SPRING_OPTIONS;\n\n const handleAnimationStart = () => {\n setIsAnimating(true);\n };\n\n const handleAnimationComplete = () => {\n if (!loop || itemsForRender.length <= 1) {\n setIsAnimating(false);\n return;\n }\n const lastCloneIndex = itemsForRender.length - 1;\n\n if (position === lastCloneIndex) {\n setIsJumping(true);\n const target = 1;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n if (position === 0) {\n setIsJumping(true);\n const target = items.length;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n setIsAnimating(false);\n };\n\n const handleDragEnd = (_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo): void => {\n const { offset, velocity } = info;\n const direction =\n offset.x < -DRAG_BUFFER || velocity.x < -VELOCITY_THRESHOLD\n ? 1\n : offset.x > DRAG_BUFFER || velocity.x > VELOCITY_THRESHOLD\n ? -1\n : 0;\n\n if (direction === 0) return;\n\n setPosition(prev => {\n const next = prev + direction;\n const max = itemsForRender.length - 1;\n return Math.max(0, Math.min(next, max));\n });\n };\n\n const dragProps = loop\n ? {}\n : {\n dragConstraints: {\n left: -trackItemOffset * Math.max(itemsForRender.length - 1, 0),\n right: 0\n }\n };\n\n const activeIndex =\n items.length === 0 ? 0 : loop ? (position - 1 + items.length) % items.length : Math.min(position, items.length - 1);\n\n return (\n \n \n {itemsForRender.map((item, index) => (\n \n ))}\n \n
\n
\n {items.map((_, index) => (\n setPosition(loop ? index + 1 : index)}\n transition={{ duration: 0.15 }}\n />\n ))}\n
\n
\n \n );\n}\n" + "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { motion, PanInfo, useMotionValue, useTransform } from 'motion/react';\nimport React, { JSX } from 'react';\n\n// replace icons with your own if needed\nimport { FiCircle, FiCode, FiFileText, FiLayers, FiLayout } from 'react-icons/fi';\nexport interface CarouselItem {\n title: string;\n description: string;\n id: number;\n icon: React.ReactNode;\n}\n\nexport interface CarouselProps {\n items?: CarouselItem[];\n baseWidth?: number;\n autoplay?: boolean;\n autoplayDelay?: number;\n pauseOnHover?: boolean;\n loop?: boolean;\n round?: boolean;\n}\n\nconst DEFAULT_ITEMS: CarouselItem[] = [\n {\n title: 'Text Animations',\n description: 'Cool text animations for your projects.',\n id: 1,\n icon: \n },\n {\n title: 'Animations',\n description: 'Smooth animations for your projects.',\n id: 2,\n icon: \n },\n {\n title: 'Components',\n description: 'Reusable components for your projects.',\n id: 3,\n icon: \n },\n {\n title: 'Backgrounds',\n description: 'Beautiful backgrounds and patterns for your projects.',\n id: 4,\n icon: \n },\n {\n title: 'Common UI',\n description: 'Common UI components are coming soon!',\n id: 5,\n icon: \n }\n];\n\nconst DRAG_BUFFER = 0;\nconst VELOCITY_THRESHOLD = 500;\nconst GAP = 16;\nconst SPRING_OPTIONS = { type: 'spring' as const, stiffness: 300, damping: 30 };\n\ninterface CarouselItemProps {\n item: CarouselItem;\n index: number;\n itemWidth: number;\n round: boolean;\n trackItemOffset: number;\n x: any;\n transition: any;\n}\n\nfunction CarouselItem({ item, index, itemWidth, round, trackItemOffset, x, transition }: CarouselItemProps) {\n const range = [-(index + 1) * trackItemOffset, -index * trackItemOffset, -(index - 1) * trackItemOffset];\n const outputRange = [90, 0, -90];\n const rotateY = useTransform(x, range, outputRange, { clamp: false });\n\n return (\n \n
\n \n {item.icon}\n \n
\n
\n
{item.title}
\n

{item.description}

\n
\n \n );\n}\n\nexport default function Carousel({\n items = DEFAULT_ITEMS,\n baseWidth = 300,\n autoplay = false,\n autoplayDelay = 3000,\n pauseOnHover = false,\n loop = false,\n round = false\n}: CarouselProps): JSX.Element {\n const containerPadding = 16;\n const itemWidth = baseWidth - containerPadding * 2;\n const trackItemOffset = itemWidth + GAP;\n const itemsForRender = useMemo(() => {\n if (!loop) return items;\n if (items.length === 0) return [];\n return [items[items.length - 1], ...items, items[0]];\n }, [items, loop]);\n\n const [position, setPosition] = useState(loop ? 1 : 0);\n const x = useMotionValue(0);\n const [isHovered, setIsHovered] = useState(false);\n const [isJumping, setIsJumping] = useState(false);\n const [isAnimating, setIsAnimating] = useState(false);\n\n const containerRef = useRef(null);\n useEffect(() => {\n if (pauseOnHover && containerRef.current) {\n const container = containerRef.current;\n const handleMouseEnter = () => setIsHovered(true);\n const handleMouseLeave = () => setIsHovered(false);\n container.addEventListener('mouseenter', handleMouseEnter);\n container.addEventListener('mouseleave', handleMouseLeave);\n return () => {\n container.removeEventListener('mouseenter', handleMouseEnter);\n container.removeEventListener('mouseleave', handleMouseLeave);\n };\n }\n }, [pauseOnHover]);\n\n useEffect(() => {\n if (!autoplay || itemsForRender.length <= 1) return undefined;\n if (pauseOnHover && isHovered) return undefined;\n\n const timer = setInterval(() => {\n setPosition(prev => Math.min(prev + 1, itemsForRender.length - 1));\n }, autoplayDelay);\n\n return () => clearInterval(timer);\n }, [autoplay, autoplayDelay, isHovered, pauseOnHover, itemsForRender.length]);\n\n useEffect(() => {\n const startingPosition = loop ? 1 : 0;\n setPosition(startingPosition);\n x.set(-startingPosition * trackItemOffset);\n }, [items.length, loop, trackItemOffset, x]);\n\n useEffect(() => {\n if (!loop && position > itemsForRender.length - 1) {\n setPosition(Math.max(0, itemsForRender.length - 1));\n }\n }, [itemsForRender.length, loop, position]);\n\n const effectiveTransition = isJumping ? { duration: 0 } : SPRING_OPTIONS;\n\n const handleAnimationStart = () => {\n setIsAnimating(true);\n };\n\n const handleAnimationComplete = () => {\n if (!loop || itemsForRender.length <= 1) {\n setIsAnimating(false);\n return;\n }\n const lastCloneIndex = itemsForRender.length - 1;\n\n if (position === lastCloneIndex) {\n setIsJumping(true);\n const target = 1;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n if (position === 0) {\n setIsJumping(true);\n const target = items.length;\n setPosition(target);\n x.set(-target * trackItemOffset);\n requestAnimationFrame(() => {\n setIsJumping(false);\n setIsAnimating(false);\n });\n return;\n }\n\n setIsAnimating(false);\n };\n\n const handleDragEnd = (_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo): void => {\n const { offset, velocity } = info;\n const direction =\n offset.x < -DRAG_BUFFER || velocity.x < -VELOCITY_THRESHOLD\n ? 1\n : offset.x > DRAG_BUFFER || velocity.x > VELOCITY_THRESHOLD\n ? -1\n : 0;\n\n if (direction === 0) return;\n\n setPosition(prev => {\n const next = prev + direction;\n const max = itemsForRender.length - 1;\n return Math.max(0, Math.min(next, max));\n });\n };\n\n const dragProps = loop\n ? {}\n : {\n dragConstraints: {\n left: -trackItemOffset * Math.max(itemsForRender.length - 1, 0),\n right: 0\n }\n };\n\n const activeIndex =\n items.length === 0 ? 0 : loop ? (position - 1 + items.length) % items.length : Math.min(position, items.length - 1);\n\n return (\n \n \n {itemsForRender.map((item, index) => (\n \n ))}\n \n
\n
\n {items.map((_, index) => (\n setPosition(loop ? index + 1 : index)}\n transition={{ duration: 0.15 }}\n />\n ))}\n
\n
\n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/ColorBends-JS-CSS.json b/public/r/ColorBends-JS-CSS.json index daf303570..c88ed3e41 100644 --- a/public/r/ColorBends-JS-CSS.json +++ b/public/r/ColorBends-JS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ColorBends-JS-TW.json b/public/r/ColorBends-JS-TW.json index 2f2b49e99..b8bb68d79 100644 --- a/public/r/ColorBends-JS-TW.json +++ b/public/r/ColorBends-JS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ColorBends-TS-CSS.json b/public/r/ColorBends-TS-CSS.json index 70befdf64..483aa1c50 100644 --- a/public/r/ColorBends-TS-CSS.json +++ b/public/r/ColorBends-TS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ColorBends-TS-TW.json b/public/r/ColorBends-TS-TW.json index 07d5fe134..70da62d63 100644 --- a/public/r/ColorBends-TS-TW.json +++ b/public/r/ColorBends-TS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Dither-JS-CSS.json b/public/r/Dither-JS-CSS.json index 041a25a55..d463bfbdf 100644 --- a/public/r/Dither-JS-CSS.json +++ b/public/r/Dither-JS-CSS.json @@ -21,6 +21,6 @@ "@react-three/fiber@^9.3.0", "@react-three/postprocessing@^3.0.4", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Dither-JS-TW.json b/public/r/Dither-JS-TW.json index b56e28f08..6246b1cc3 100644 --- a/public/r/Dither-JS-TW.json +++ b/public/r/Dither-JS-TW.json @@ -16,6 +16,6 @@ "@react-three/fiber@^9.3.0", "@react-three/postprocessing@^3.0.4", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Dither-TS-CSS.json b/public/r/Dither-TS-CSS.json index 844b37dfb..d3b9dc5b8 100644 --- a/public/r/Dither-TS-CSS.json +++ b/public/r/Dither-TS-CSS.json @@ -21,6 +21,6 @@ "@react-three/fiber@^9.3.0", "@react-three/postprocessing@^3.0.4", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Dither-TS-TW.json b/public/r/Dither-TS-TW.json index 05bb6cb5c..6a1031913 100644 --- a/public/r/Dither-TS-TW.json +++ b/public/r/Dither-TS-TW.json @@ -16,6 +16,6 @@ "@react-three/fiber@^9.3.0", "@react-three/postprocessing@^3.0.4", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/FloatingLines-JS-CSS.json b/public/r/FloatingLines-JS-CSS.json index 4f743e926..be8add665 100644 --- a/public/r/FloatingLines-JS-CSS.json +++ b/public/r/FloatingLines-JS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/FloatingLines-JS-TW.json b/public/r/FloatingLines-JS-TW.json index 69303b423..9f86c1efd 100644 --- a/public/r/FloatingLines-JS-TW.json +++ b/public/r/FloatingLines-JS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/FloatingLines-TS-CSS.json b/public/r/FloatingLines-TS-CSS.json index bd9735345..17baece31 100644 --- a/public/r/FloatingLines-TS-CSS.json +++ b/public/r/FloatingLines-TS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/FloatingLines-TS-TW.json b/public/r/FloatingLines-TS-TW.json index a0b0fdaee..2c5928a19 100644 --- a/public/r/FloatingLines-TS-TW.json +++ b/public/r/FloatingLines-TS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/FluidGlass-JS-CSS.json b/public/r/FluidGlass-JS-CSS.json index 6a8e1f9fc..dbd29e664 100644 --- a/public/r/FluidGlass-JS-CSS.json +++ b/public/r/FluidGlass-JS-CSS.json @@ -13,7 +13,7 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", "maath@^0.10.8" diff --git a/public/r/FluidGlass-JS-TW.json b/public/r/FluidGlass-JS-TW.json index 7313b20a1..0d381647f 100644 --- a/public/r/FluidGlass-JS-TW.json +++ b/public/r/FluidGlass-JS-TW.json @@ -13,7 +13,7 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", "maath@^0.10.8" diff --git a/public/r/FluidGlass-TS-CSS.json b/public/r/FluidGlass-TS-CSS.json index 1b7f5c78b..489e9181c 100644 --- a/public/r/FluidGlass-TS-CSS.json +++ b/public/r/FluidGlass-TS-CSS.json @@ -13,7 +13,7 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", "maath@^0.10.8" diff --git a/public/r/FluidGlass-TS-TW.json b/public/r/FluidGlass-TS-TW.json index a97d004bc..8c01777b6 100644 --- a/public/r/FluidGlass-TS-TW.json +++ b/public/r/FluidGlass-TS-TW.json @@ -13,7 +13,7 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", "maath@^0.10.8" diff --git a/public/r/GhostCursor-JS-CSS.json b/public/r/GhostCursor-JS-CSS.json index 7b6318b68..fc24e142f 100644 --- a/public/r/GhostCursor-JS-CSS.json +++ b/public/r/GhostCursor-JS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/GhostCursor-JS-TW.json b/public/r/GhostCursor-JS-TW.json index 044d4106b..67be6cbc8 100644 --- a/public/r/GhostCursor-JS-TW.json +++ b/public/r/GhostCursor-JS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/GhostCursor-TS-CSS.json b/public/r/GhostCursor-TS-CSS.json index 705fd5199..f8f3a2efb 100644 --- a/public/r/GhostCursor-TS-CSS.json +++ b/public/r/GhostCursor-TS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/GhostCursor-TS-TW.json b/public/r/GhostCursor-TS-TW.json index fc8ae8b21..3ecc799da 100644 --- a/public/r/GhostCursor-TS-TW.json +++ b/public/r/GhostCursor-TS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/GridDistortion-JS-CSS.json b/public/r/GridDistortion-JS-CSS.json index 0794611c3..5e42d5b48 100644 --- a/public/r/GridDistortion-JS-CSS.json +++ b/public/r/GridDistortion-JS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/GridDistortion-JS-TW.json b/public/r/GridDistortion-JS-TW.json index 902b15b9e..74f756770 100644 --- a/public/r/GridDistortion-JS-TW.json +++ b/public/r/GridDistortion-JS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/GridDistortion-TS-CSS.json b/public/r/GridDistortion-TS-CSS.json index 0600d95f2..cb07d43be 100644 --- a/public/r/GridDistortion-TS-CSS.json +++ b/public/r/GridDistortion-TS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/GridDistortion-TS-TW.json b/public/r/GridDistortion-TS-TW.json index 5c66be76f..fff1b336d 100644 --- a/public/r/GridDistortion-TS-TW.json +++ b/public/r/GridDistortion-TS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/GridScan-JS-CSS.json b/public/r/GridScan-JS-CSS.json index 94f85e7ce..25d794841 100644 --- a/public/r/GridScan-JS-CSS.json +++ b/public/r/GridScan-JS-CSS.json @@ -20,6 +20,6 @@ "dependencies": [ "face-api.js@^0.22.2", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/GridScan-JS-TW.json b/public/r/GridScan-JS-TW.json index dd906c465..f568fe985 100644 --- a/public/r/GridScan-JS-TW.json +++ b/public/r/GridScan-JS-TW.json @@ -15,6 +15,6 @@ "dependencies": [ "face-api.js@^0.22.2", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/GridScan-TS-CSS.json b/public/r/GridScan-TS-CSS.json index 0c153d495..153165bfd 100644 --- a/public/r/GridScan-TS-CSS.json +++ b/public/r/GridScan-TS-CSS.json @@ -20,6 +20,6 @@ "dependencies": [ "face-api.js@^0.22.2", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/GridScan-TS-TW.json b/public/r/GridScan-TS-TW.json index f0f3ee4ea..4de7b502e 100644 --- a/public/r/GridScan-TS-TW.json +++ b/public/r/GridScan-TS-TW.json @@ -15,6 +15,6 @@ "dependencies": [ "face-api.js@^0.22.2", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Hyperspeed-JS-CSS.json b/public/r/Hyperspeed-JS-CSS.json index 037778a4a..9cfa2756f 100644 --- a/public/r/Hyperspeed-JS-CSS.json +++ b/public/r/Hyperspeed-JS-CSS.json @@ -24,6 +24,6 @@ "registryDependencies": [], "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Hyperspeed-JS-TW.json b/public/r/Hyperspeed-JS-TW.json index 32c34a5ce..cd676f5e9 100644 --- a/public/r/Hyperspeed-JS-TW.json +++ b/public/r/Hyperspeed-JS-TW.json @@ -19,6 +19,6 @@ "registryDependencies": [], "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Hyperspeed-TS-CSS.json b/public/r/Hyperspeed-TS-CSS.json index 4cc7107dc..a9feafecc 100644 --- a/public/r/Hyperspeed-TS-CSS.json +++ b/public/r/Hyperspeed-TS-CSS.json @@ -24,6 +24,6 @@ "registryDependencies": [], "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Hyperspeed-TS-TW.json b/public/r/Hyperspeed-TS-TW.json index 72fe69f92..c8dc49eff 100644 --- a/public/r/Hyperspeed-TS-TW.json +++ b/public/r/Hyperspeed-TS-TW.json @@ -19,6 +19,6 @@ "registryDependencies": [], "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Lanyard-JS-CSS.json b/public/r/Lanyard-JS-CSS.json index 3762cdbf9..ad9afa3b4 100644 --- a/public/r/Lanyard-JS-CSS.json +++ b/public/r/Lanyard-JS-CSS.json @@ -13,7 +13,7 @@ { "type": "registry:component", "path": "Lanyard.jsx", - "content": "/* eslint-disable react/no-unknown-property */\n'use client';\nimport { useEffect, useRef, useState } from 'react';\nimport { Canvas, extend, useFrame } from '@react-three/fiber';\nimport { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei';\nimport { BallCollider, CuboidCollider, Physics, RigidBody, useRopeJoint, useSphericalJoint } from '@react-three/rapier';\nimport { MeshLineGeometry, MeshLineMaterial } from 'meshline';\n\n// replace with your own imports, see the usage snippet for details\nimport cardGLB from './card.glb';\nimport lanyard from './lanyard.png';\n\nimport * as THREE from 'three';\nimport './Lanyard.css';\n\nextend({ MeshLineGeometry, MeshLineMaterial });\n\nexport default function Lanyard({ position = [0, 0, 30], gravity = [0, -40, 0], fov = 20, transparent = true }) {\n const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);\n\n useEffect(() => {\n const handleResize = () => setIsMobile(window.innerWidth < 768);\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, []);\n\n return (\n
\n gl.setClearColor(new THREE.Color(0x000000), transparent ? 0 : 1)}\n >\n \n \n \n \n \n \n \n \n \n \n \n
\n );\n}\nfunction Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }) {\n const band = useRef(),\n fixed = useRef(),\n j1 = useRef(),\n j2 = useRef(),\n j3 = useRef(),\n card = useRef();\n const vec = new THREE.Vector3(),\n ang = new THREE.Vector3(),\n rot = new THREE.Vector3(),\n dir = new THREE.Vector3();\n const segmentProps = { type: 'dynamic', canSleep: true, colliders: false, angularDamping: 4, linearDamping: 4 };\n const { nodes, materials } = useGLTF(cardGLB);\n const texture = useTexture(lanyard);\n const [curve] = useState(\n () =>\n new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()])\n );\n const [dragged, drag] = useState(false);\n const [hovered, hover] = useState(false);\n\n useRopeJoint(fixed, j1, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j1, j2, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j2, j3, [[0, 0, 0], [0, 0, 0], 1]);\n useSphericalJoint(j3, card, [\n [0, 0, 0],\n [0, 1.5, 0]\n ]);\n\n useEffect(() => {\n if (hovered) {\n document.body.style.cursor = dragged ? 'grabbing' : 'grab';\n return () => void (document.body.style.cursor = 'auto');\n }\n }, [hovered, dragged]);\n\n useFrame((state, delta) => {\n if (dragged) {\n vec.set(state.pointer.x, state.pointer.y, 0.5).unproject(state.camera);\n dir.copy(vec).sub(state.camera.position).normalize();\n vec.add(dir.multiplyScalar(state.camera.position.length()));\n [card, j1, j2, j3, fixed].forEach(ref => ref.current?.wakeUp());\n card.current?.setNextKinematicTranslation({ x: vec.x - dragged.x, y: vec.y - dragged.y, z: vec.z - dragged.z });\n }\n if (fixed.current) {\n [j1, j2].forEach(ref => {\n if (!ref.current.lerped) ref.current.lerped = new THREE.Vector3().copy(ref.current.translation());\n const clampedDistance = Math.max(0.1, Math.min(1, ref.current.lerped.distanceTo(ref.current.translation())));\n ref.current.lerped.lerp(\n ref.current.translation(),\n delta * (minSpeed + clampedDistance * (maxSpeed - minSpeed))\n );\n });\n curve.points[0].copy(j3.current.translation());\n curve.points[1].copy(j2.current.lerped);\n curve.points[2].copy(j1.current.lerped);\n curve.points[3].copy(fixed.current.translation());\n band.current.geometry.setPoints(curve.getPoints(isMobile ? 16 : 32));\n ang.copy(card.current.angvel());\n rot.copy(card.current.rotation());\n card.current.setAngvel({ x: ang.x, y: ang.y - rot.y * 0.25, z: ang.z });\n }\n });\n\n curve.curveType = 'chordal';\n texture.wrapS = texture.wrapT = THREE.RepeatWrapping;\n\n return (\n <>\n \n \n \n \n \n \n \n \n \n \n \n \n \n hover(true)}\n onPointerOut={() => hover(false)}\n onPointerUp={e => (e.target.releasePointerCapture(e.pointerId), drag(false))}\n onPointerDown={e => (\n e.target.setPointerCapture(e.pointerId),\n drag(new THREE.Vector3().copy(e.point).sub(vec.copy(card.current.translation())))\n )}\n >\n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n" + "content": "/* eslint-disable react/no-unknown-property */\n'use client';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { Canvas, extend, useFrame } from '@react-three/fiber';\nimport { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei';\nimport { BallCollider, CuboidCollider, Physics, RigidBody, useRopeJoint, useSphericalJoint } from '@react-three/rapier';\nimport { MeshLineGeometry, MeshLineMaterial } from 'meshline';\n\n// replace with your own imports, see the usage snippet for details\nimport cardGLB from './card.glb';\nimport lanyard from './lanyard.png';\n\nimport * as THREE from 'three';\nimport './Lanyard.css';\n\nextend({ MeshLineGeometry, MeshLineMaterial });\n\n// 1x1 transparent pixel — lets useTexture be called unconditionally when a\n// front/back image isn't supplied.\nconst BLANK_PIXEL =\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';\n\n// The card model's front face is UV-mapped to the LEFT half of the texture\n// atlas and the back face to the RIGHT half (measured from card.glb). Each\n// custom image is composited into its own half so the two faces render\n// independently, aspect-preserving (no stretching).\nconst FRONT_UV_RECT = { x: 0, y: 0, w: 0.5, h: 0.755 };\nconst BACK_UV_RECT = { x: 0.5, y: 0, w: 0.5, h: 0.757 };\n\nexport default function Lanyard({\n position = [0, 0, 30],\n gravity = [0, -40, 0],\n fov = 20,\n transparent = true,\n frontImage = null,\n backImage = null,\n imageFit = 'cover',\n lanyardImage = null,\n lanyardWidth = 1\n}) {\n const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);\n\n useEffect(() => {\n const handleResize = () => setIsMobile(window.innerWidth < 768);\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, []);\n\n return (\n
\n gl.setClearColor(new THREE.Color(0x000000), transparent ? 0 : 1)}\n >\n \n \n \n \n \n \n \n \n \n \n \n
\n );\n}\nfunction Band({\n maxSpeed = 50,\n minSpeed = 0,\n isMobile = false,\n frontImage = null,\n backImage = null,\n imageFit = 'cover',\n lanyardImage = null,\n lanyardWidth = 1\n}) {\n const band = useRef(),\n fixed = useRef(),\n j1 = useRef(),\n j2 = useRef(),\n j3 = useRef(),\n card = useRef();\n const vec = new THREE.Vector3(),\n ang = new THREE.Vector3(),\n rot = new THREE.Vector3(),\n dir = new THREE.Vector3();\n const segmentProps = { type: 'dynamic', canSleep: true, colliders: false, angularDamping: 4, linearDamping: 4 };\n const { nodes, materials } = useGLTF(cardGLB);\n const texture = useTexture(lanyardImage || lanyard);\n // useTexture must be called unconditionally; use a blank pixel when an image\n // isn't supplied for a given face, then skip compositing it below.\n const frontTex = useTexture(frontImage || BLANK_PIXEL);\n const backTex = useTexture(backImage || BLANK_PIXEL);\n\n // Composite the front/back images into the card's texture atlas (front = left\n // half, back = right half). Each image is drawn aspect-preserving (no stretch).\n const cardMap = useMemo(() => {\n const baseMap = materials.base.map;\n if (!frontImage && !backImage) return baseMap;\n\n const baseImg = baseMap.image;\n const W = baseImg.width;\n const H = baseImg.height;\n const canvas = document.createElement('canvas');\n canvas.width = W;\n canvas.height = H;\n const ctx = canvas.getContext('2d');\n if (!ctx) return baseMap;\n // Keep the original baked atlas for the card edges and any untouched face.\n ctx.drawImage(baseImg, 0, 0, W, H);\n\n const drawFitted = (img, rect) => {\n const rx = rect.x * W;\n const ry = rect.y * H;\n const rw = rect.w * W;\n const rh = rect.h * H;\n const pick = imageFit === 'contain' ? Math.min : Math.max;\n const scale = pick(rw / img.width, rh / img.height);\n const dw = img.width * scale;\n const dh = img.height * scale;\n const dx = rx + (rw - dw) / 2;\n const dy = ry + (rh - dh) / 2;\n ctx.save();\n ctx.beginPath();\n ctx.rect(rx, ry, rw, rh);\n ctx.clip();\n ctx.drawImage(img, dx, dy, dw, dh);\n ctx.restore();\n };\n\n if (frontImage && frontTex.image) drawFitted(frontTex.image, FRONT_UV_RECT);\n if (backImage && backTex.image) drawFitted(backTex.image, BACK_UV_RECT);\n\n const composite = new THREE.CanvasTexture(canvas);\n composite.colorSpace = THREE.SRGBColorSpace;\n composite.flipY = baseMap.flipY;\n composite.anisotropy = 16;\n composite.needsUpdate = true;\n return composite;\n }, [frontImage, backImage, imageFit, frontTex, backTex, materials.base.map]);\n const [curve] = useState(\n () =>\n new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()])\n );\n const [dragged, drag] = useState(false);\n const [hovered, hover] = useState(false);\n\n useRopeJoint(fixed, j1, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j1, j2, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j2, j3, [[0, 0, 0], [0, 0, 0], 1]);\n useSphericalJoint(j3, card, [\n [0, 0, 0],\n [0, 1.5, 0]\n ]);\n\n useEffect(() => {\n if (hovered) {\n document.body.style.cursor = dragged ? 'grabbing' : 'grab';\n return () => void (document.body.style.cursor = 'auto');\n }\n }, [hovered, dragged]);\n\n useFrame((state, delta) => {\n if (dragged) {\n vec.set(state.pointer.x, state.pointer.y, 0.5).unproject(state.camera);\n dir.copy(vec).sub(state.camera.position).normalize();\n vec.add(dir.multiplyScalar(state.camera.position.length()));\n [card, j1, j2, j3, fixed].forEach(ref => ref.current?.wakeUp());\n card.current?.setNextKinematicTranslation({ x: vec.x - dragged.x, y: vec.y - dragged.y, z: vec.z - dragged.z });\n }\n if (fixed.current) {\n [j1, j2].forEach(ref => {\n if (!ref.current.lerped) ref.current.lerped = new THREE.Vector3().copy(ref.current.translation());\n const clampedDistance = Math.max(0.1, Math.min(1, ref.current.lerped.distanceTo(ref.current.translation())));\n ref.current.lerped.lerp(\n ref.current.translation(),\n delta * (minSpeed + clampedDistance * (maxSpeed - minSpeed))\n );\n });\n curve.points[0].copy(j3.current.translation());\n curve.points[1].copy(j2.current.lerped);\n curve.points[2].copy(j1.current.lerped);\n curve.points[3].copy(fixed.current.translation());\n band.current.geometry.setPoints(curve.getPoints(isMobile ? 16 : 32));\n ang.copy(card.current.angvel());\n rot.copy(card.current.rotation());\n card.current.setAngvel({ x: ang.x, y: ang.y - rot.y * 0.25, z: ang.z });\n }\n });\n\n curve.curveType = 'chordal';\n texture.wrapS = texture.wrapT = THREE.RepeatWrapping;\n\n return (\n <>\n \n \n \n \n \n \n \n \n \n \n \n \n \n hover(true)}\n onPointerOut={() => hover(false)}\n onPointerUp={e => (e.target.releasePointerCapture(e.pointerId), drag(false))}\n onPointerDown={e => (\n e.target.setPointerCapture(e.pointerId),\n drag(new THREE.Vector3().copy(e.point).sub(vec.copy(card.current.translation())))\n )}\n >\n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/Lanyard-JS-TW.json b/public/r/Lanyard-JS-TW.json index ef9267cc3..2ecbdc872 100644 --- a/public/r/Lanyard-JS-TW.json +++ b/public/r/Lanyard-JS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Lanyard.jsx", - "content": "/* eslint-disable react/no-unknown-property */\n'use client';\nimport { useEffect, useRef, useState } from 'react';\nimport { Canvas, extend, useFrame } from '@react-three/fiber';\nimport { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei';\nimport { BallCollider, CuboidCollider, Physics, RigidBody, useRopeJoint, useSphericalJoint } from '@react-three/rapier';\nimport { MeshLineGeometry, MeshLineMaterial } from 'meshline';\n\n// replace with your own imports, see the usage snippet for details\nimport cardGLB from './card.glb';\nimport lanyard from './lanyard.png';\n\nimport * as THREE from 'three';\n\nextend({ MeshLineGeometry, MeshLineMaterial });\n\nexport default function Lanyard({ position = [0, 0, 30], gravity = [0, -40, 0], fov = 20, transparent = true }) {\n const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);\n\n useEffect(() => {\n const handleResize = () => setIsMobile(window.innerWidth < 768);\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, []);\n\n return (\n
\n gl.setClearColor(new THREE.Color(0x000000), transparent ? 0 : 1)}\n >\n \n \n \n \n \n \n \n \n \n \n \n
\n );\n}\nfunction Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }) {\n const band = useRef(),\n fixed = useRef(),\n j1 = useRef(),\n j2 = useRef(),\n j3 = useRef(),\n card = useRef();\n const vec = new THREE.Vector3(),\n ang = new THREE.Vector3(),\n rot = new THREE.Vector3(),\n dir = new THREE.Vector3();\n const segmentProps = { type: 'dynamic', canSleep: true, colliders: false, angularDamping: 4, linearDamping: 4 };\n const { nodes, materials } = useGLTF(cardGLB);\n const texture = useTexture(lanyard);\n const [curve] = useState(\n () =>\n new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()])\n );\n const [dragged, drag] = useState(false);\n const [hovered, hover] = useState(false);\n\n useRopeJoint(fixed, j1, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j1, j2, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j2, j3, [[0, 0, 0], [0, 0, 0], 1]);\n useSphericalJoint(j3, card, [\n [0, 0, 0],\n [0, 1.5, 0]\n ]);\n\n useEffect(() => {\n if (hovered) {\n document.body.style.cursor = dragged ? 'grabbing' : 'grab';\n return () => void (document.body.style.cursor = 'auto');\n }\n }, [hovered, dragged]);\n\n useFrame((state, delta) => {\n if (dragged) {\n vec.set(state.pointer.x, state.pointer.y, 0.5).unproject(state.camera);\n dir.copy(vec).sub(state.camera.position).normalize();\n vec.add(dir.multiplyScalar(state.camera.position.length()));\n [card, j1, j2, j3, fixed].forEach(ref => ref.current?.wakeUp());\n card.current?.setNextKinematicTranslation({ x: vec.x - dragged.x, y: vec.y - dragged.y, z: vec.z - dragged.z });\n }\n if (fixed.current) {\n [j1, j2].forEach(ref => {\n if (!ref.current.lerped) ref.current.lerped = new THREE.Vector3().copy(ref.current.translation());\n const clampedDistance = Math.max(0.1, Math.min(1, ref.current.lerped.distanceTo(ref.current.translation())));\n ref.current.lerped.lerp(\n ref.current.translation(),\n delta * (minSpeed + clampedDistance * (maxSpeed - minSpeed))\n );\n });\n curve.points[0].copy(j3.current.translation());\n curve.points[1].copy(j2.current.lerped);\n curve.points[2].copy(j1.current.lerped);\n curve.points[3].copy(fixed.current.translation());\n band.current.geometry.setPoints(curve.getPoints(isMobile ? 16 : 32));\n ang.copy(card.current.angvel());\n rot.copy(card.current.rotation());\n card.current.setAngvel({ x: ang.x, y: ang.y - rot.y * 0.25, z: ang.z });\n }\n });\n\n curve.curveType = 'chordal';\n texture.wrapS = texture.wrapT = THREE.RepeatWrapping;\n\n return (\n <>\n \n \n \n \n \n \n \n \n \n \n \n \n \n hover(true)}\n onPointerOut={() => hover(false)}\n onPointerUp={e => (e.target.releasePointerCapture(e.pointerId), drag(false))}\n onPointerDown={e => (\n e.target.setPointerCapture(e.pointerId),\n drag(new THREE.Vector3().copy(e.point).sub(vec.copy(card.current.translation())))\n )}\n >\n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n" + "content": "/* eslint-disable react/no-unknown-property */\n'use client';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { Canvas, extend, useFrame } from '@react-three/fiber';\nimport { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei';\nimport { BallCollider, CuboidCollider, Physics, RigidBody, useRopeJoint, useSphericalJoint } from '@react-three/rapier';\nimport { MeshLineGeometry, MeshLineMaterial } from 'meshline';\n\n// replace with your own imports, see the usage snippet for details\nimport cardGLB from './card.glb';\nimport lanyard from './lanyard.png';\n\nimport * as THREE from 'three';\n\nextend({ MeshLineGeometry, MeshLineMaterial });\n\n// 1x1 transparent pixel — lets useTexture be called unconditionally when a\n// front/back image isn't supplied.\nconst BLANK_PIXEL =\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';\n\n// The card model's front face is UV-mapped to the LEFT half of the texture\n// atlas and the back face to the RIGHT half (measured from card.glb). Each\n// custom image is composited into its own half so the two faces render\n// independently, aspect-preserving (no stretching).\nconst FRONT_UV_RECT = { x: 0, y: 0, w: 0.5, h: 0.755 };\nconst BACK_UV_RECT = { x: 0.5, y: 0, w: 0.5, h: 0.757 };\n\nexport default function Lanyard({\n position = [0, 0, 30],\n gravity = [0, -40, 0],\n fov = 20,\n transparent = true,\n frontImage = null,\n backImage = null,\n imageFit = 'cover',\n lanyardImage = null,\n lanyardWidth = 1\n}) {\n const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);\n\n useEffect(() => {\n const handleResize = () => setIsMobile(window.innerWidth < 768);\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, []);\n\n return (\n
\n gl.setClearColor(new THREE.Color(0x000000), transparent ? 0 : 1)}\n >\n \n \n \n \n \n \n \n \n \n \n \n
\n );\n}\nfunction Band({\n maxSpeed = 50,\n minSpeed = 0,\n isMobile = false,\n frontImage = null,\n backImage = null,\n imageFit = 'cover',\n lanyardImage = null,\n lanyardWidth = 1\n}) {\n const band = useRef(),\n fixed = useRef(),\n j1 = useRef(),\n j2 = useRef(),\n j3 = useRef(),\n card = useRef();\n const vec = new THREE.Vector3(),\n ang = new THREE.Vector3(),\n rot = new THREE.Vector3(),\n dir = new THREE.Vector3();\n const segmentProps = { type: 'dynamic', canSleep: true, colliders: false, angularDamping: 4, linearDamping: 4 };\n const { nodes, materials } = useGLTF(cardGLB);\n const texture = useTexture(lanyardImage || lanyard);\n // useTexture must be called unconditionally; use a blank pixel when an image\n // isn't supplied for a given face, then skip compositing it below.\n const frontTex = useTexture(frontImage || BLANK_PIXEL);\n const backTex = useTexture(backImage || BLANK_PIXEL);\n\n // Composite the front/back images into the card's texture atlas (front = left\n // half, back = right half). Each image is drawn aspect-preserving (no stretch).\n const cardMap = useMemo(() => {\n const baseMap = materials.base.map;\n if (!frontImage && !backImage) return baseMap;\n\n const baseImg = baseMap.image;\n const W = baseImg.width;\n const H = baseImg.height;\n const canvas = document.createElement('canvas');\n canvas.width = W;\n canvas.height = H;\n const ctx = canvas.getContext('2d');\n if (!ctx) return baseMap;\n // Keep the original baked atlas for the card edges and any untouched face.\n ctx.drawImage(baseImg, 0, 0, W, H);\n\n const drawFitted = (img, rect) => {\n const rx = rect.x * W;\n const ry = rect.y * H;\n const rw = rect.w * W;\n const rh = rect.h * H;\n const pick = imageFit === 'contain' ? Math.min : Math.max;\n const scale = pick(rw / img.width, rh / img.height);\n const dw = img.width * scale;\n const dh = img.height * scale;\n const dx = rx + (rw - dw) / 2;\n const dy = ry + (rh - dh) / 2;\n ctx.save();\n ctx.beginPath();\n ctx.rect(rx, ry, rw, rh);\n ctx.clip();\n ctx.drawImage(img, dx, dy, dw, dh);\n ctx.restore();\n };\n\n if (frontImage && frontTex.image) drawFitted(frontTex.image, FRONT_UV_RECT);\n if (backImage && backTex.image) drawFitted(backTex.image, BACK_UV_RECT);\n\n const composite = new THREE.CanvasTexture(canvas);\n composite.colorSpace = THREE.SRGBColorSpace;\n composite.flipY = baseMap.flipY;\n composite.anisotropy = 16;\n composite.needsUpdate = true;\n return composite;\n }, [frontImage, backImage, imageFit, frontTex, backTex, materials.base.map]);\n const [curve] = useState(\n () =>\n new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()])\n );\n const [dragged, drag] = useState(false);\n const [hovered, hover] = useState(false);\n\n useRopeJoint(fixed, j1, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j1, j2, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j2, j3, [[0, 0, 0], [0, 0, 0], 1]);\n useSphericalJoint(j3, card, [\n [0, 0, 0],\n [0, 1.5, 0]\n ]);\n\n useEffect(() => {\n if (hovered) {\n document.body.style.cursor = dragged ? 'grabbing' : 'grab';\n return () => void (document.body.style.cursor = 'auto');\n }\n }, [hovered, dragged]);\n\n useFrame((state, delta) => {\n if (dragged) {\n vec.set(state.pointer.x, state.pointer.y, 0.5).unproject(state.camera);\n dir.copy(vec).sub(state.camera.position).normalize();\n vec.add(dir.multiplyScalar(state.camera.position.length()));\n [card, j1, j2, j3, fixed].forEach(ref => ref.current?.wakeUp());\n card.current?.setNextKinematicTranslation({ x: vec.x - dragged.x, y: vec.y - dragged.y, z: vec.z - dragged.z });\n }\n if (fixed.current) {\n [j1, j2].forEach(ref => {\n if (!ref.current.lerped) ref.current.lerped = new THREE.Vector3().copy(ref.current.translation());\n const clampedDistance = Math.max(0.1, Math.min(1, ref.current.lerped.distanceTo(ref.current.translation())));\n ref.current.lerped.lerp(\n ref.current.translation(),\n delta * (minSpeed + clampedDistance * (maxSpeed - minSpeed))\n );\n });\n curve.points[0].copy(j3.current.translation());\n curve.points[1].copy(j2.current.lerped);\n curve.points[2].copy(j1.current.lerped);\n curve.points[3].copy(fixed.current.translation());\n band.current.geometry.setPoints(curve.getPoints(isMobile ? 16 : 32));\n ang.copy(card.current.angvel());\n rot.copy(card.current.rotation());\n card.current.setAngvel({ x: ang.x, y: ang.y - rot.y * 0.25, z: ang.z });\n }\n });\n\n curve.curveType = 'chordal';\n texture.wrapS = texture.wrapT = THREE.RepeatWrapping;\n\n return (\n <>\n \n \n \n \n \n \n \n \n \n \n \n \n \n hover(true)}\n onPointerOut={() => hover(false)}\n onPointerUp={e => (e.target.releasePointerCapture(e.pointerId), drag(false))}\n onPointerDown={e => (\n e.target.setPointerCapture(e.pointerId),\n drag(new THREE.Vector3().copy(e.point).sub(vec.copy(card.current.translation())))\n )}\n >\n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/Lanyard-TS-CSS.json b/public/r/Lanyard-TS-CSS.json index 63beaf9b6..c79323ebe 100644 --- a/public/r/Lanyard-TS-CSS.json +++ b/public/r/Lanyard-TS-CSS.json @@ -13,7 +13,7 @@ { "type": "registry:component", "path": "Lanyard.tsx", - "content": "/* eslint-disable react/no-unknown-property */\n'use client';\nimport { useEffect, useRef, useState } from 'react';\nimport { Canvas, extend, useFrame } from '@react-three/fiber';\nimport { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei';\nimport {\n BallCollider,\n CuboidCollider,\n Physics,\n RigidBody,\n useRopeJoint,\n useSphericalJoint,\n RigidBodyProps\n} from '@react-three/rapier';\nimport { MeshLineGeometry, MeshLineMaterial } from 'meshline';\nimport * as THREE from 'three';\n\n// replace with your own imports, see the usage snippet for details\nimport cardGLB from './card.glb';\nimport lanyard from './lanyard.png';\n\nimport './Lanyard.css';\n\nextend({ MeshLineGeometry, MeshLineMaterial });\n\ninterface LanyardProps {\n position?: [number, number, number];\n gravity?: [number, number, number];\n fov?: number;\n transparent?: boolean;\n}\n\nexport default function Lanyard({\n position = [0, 0, 30],\n gravity = [0, -40, 0],\n fov = 20,\n transparent = true\n}: LanyardProps) {\n const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);\n\n useEffect(() => {\n const handleResize = (): void => setIsMobile(window.innerWidth < 768);\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, []);\n\n return (\n
\n gl.setClearColor(new THREE.Color(0x000000), transparent ? 0 : 1)}\n >\n \n \n \n \n \n \n \n \n \n \n \n
\n );\n}\n\ninterface BandProps {\n maxSpeed?: number;\n minSpeed?: number;\n isMobile?: boolean;\n}\n\nfunction Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }: BandProps) {\n // Using \"any\" for refs since the exact types depend on Rapier's internals\n const band = useRef(null);\n const fixed = useRef(null);\n const j1 = useRef(null);\n const j2 = useRef(null);\n const j3 = useRef(null);\n const card = useRef(null);\n\n const vec = new THREE.Vector3();\n const ang = new THREE.Vector3();\n const rot = new THREE.Vector3();\n const dir = new THREE.Vector3();\n\n const segmentProps: any = {\n type: 'dynamic' as RigidBodyProps['type'],\n canSleep: true,\n colliders: false,\n angularDamping: 4,\n linearDamping: 4\n };\n\n const { nodes, materials } = useGLTF(cardGLB) as any;\n const texture = useTexture(lanyard);\n const [curve] = useState(\n () =>\n new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()])\n );\n const [dragged, drag] = useState(false);\n const [hovered, hover] = useState(false);\n\n useRopeJoint(fixed, j1, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j1, j2, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j2, j3, [[0, 0, 0], [0, 0, 0], 1]);\n useSphericalJoint(j3, card, [\n [0, 0, 0],\n [0, 1.45, 0]\n ]);\n\n useEffect(() => {\n if (hovered) {\n document.body.style.cursor = dragged ? 'grabbing' : 'grab';\n return () => {\n document.body.style.cursor = 'auto';\n };\n }\n }, [hovered, dragged]);\n\n useFrame((state, delta) => {\n if (dragged && typeof dragged !== 'boolean') {\n vec.set(state.pointer.x, state.pointer.y, 0.5).unproject(state.camera);\n dir.copy(vec).sub(state.camera.position).normalize();\n vec.add(dir.multiplyScalar(state.camera.position.length()));\n [card, j1, j2, j3, fixed].forEach(ref => ref.current?.wakeUp());\n card.current?.setNextKinematicTranslation({\n x: vec.x - dragged.x,\n y: vec.y - dragged.y,\n z: vec.z - dragged.z\n });\n }\n if (fixed.current) {\n [j1, j2].forEach(ref => {\n if (!ref.current.lerped) ref.current.lerped = new THREE.Vector3().copy(ref.current.translation());\n const clampedDistance = Math.max(0.1, Math.min(1, ref.current.lerped.distanceTo(ref.current.translation())));\n ref.current.lerped.lerp(\n ref.current.translation(),\n delta * (minSpeed + clampedDistance * (maxSpeed - minSpeed))\n );\n });\n curve.points[0].copy(j3.current.translation());\n curve.points[1].copy(j2.current.lerped);\n curve.points[2].copy(j1.current.lerped);\n curve.points[3].copy(fixed.current.translation());\n band.current.geometry.setPoints(curve.getPoints(isMobile ? 16 : 32));\n ang.copy(card.current.angvel());\n rot.copy(card.current.rotation());\n card.current.setAngvel({ x: ang.x, y: ang.y - rot.y * 0.25, z: ang.z });\n }\n });\n\n curve.curveType = 'chordal';\n texture.wrapS = texture.wrapT = THREE.RepeatWrapping;\n\n return (\n <>\n \n \n \n \n \n \n \n \n \n \n \n \n \n hover(true)}\n onPointerOut={() => hover(false)}\n onPointerUp={(e: any) => {\n e.target.releasePointerCapture(e.pointerId);\n drag(false);\n }}\n onPointerDown={(e: any) => {\n e.target.setPointerCapture(e.pointerId);\n drag(new THREE.Vector3().copy(e.point).sub(vec.copy(card.current.translation())));\n }}\n >\n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n" + "content": "/* eslint-disable react/no-unknown-property */\n'use client';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { Canvas, extend, useFrame } from '@react-three/fiber';\nimport { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei';\nimport {\n BallCollider,\n CuboidCollider,\n Physics,\n RigidBody,\n useRopeJoint,\n useSphericalJoint,\n RigidBodyProps\n} from '@react-three/rapier';\nimport { MeshLineGeometry, MeshLineMaterial } from 'meshline';\nimport * as THREE from 'three';\n\n// replace with your own imports, see the usage snippet for details\nimport cardGLB from './card.glb';\nimport lanyard from './lanyard.png';\n\nimport './Lanyard.css';\n\nextend({ MeshLineGeometry, MeshLineMaterial });\n\n// 1x1 transparent pixel — lets useTexture be called unconditionally when a\n// front/back image isn't supplied.\nconst BLANK_PIXEL =\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';\n\n// The card model's front face is UV-mapped to the LEFT half of the texture\n// atlas and the back face to the RIGHT half (measured from card.glb). Each\n// custom image is composited into its own half so the two faces render\n// independently, aspect-preserving (no stretching).\nconst FRONT_UV_RECT = { x: 0, y: 0, w: 0.5, h: 0.755 };\nconst BACK_UV_RECT = { x: 0.5, y: 0, w: 0.5, h: 0.757 };\n\ninterface LanyardProps {\n position?: [number, number, number];\n gravity?: [number, number, number];\n fov?: number;\n transparent?: boolean;\n frontImage?: string | null;\n backImage?: string | null;\n imageFit?: 'cover' | 'contain';\n lanyardImage?: string | null;\n lanyardWidth?: number;\n}\n\nexport default function Lanyard({\n position = [0, 0, 30],\n gravity = [0, -40, 0],\n fov = 20,\n transparent = true,\n frontImage = null,\n backImage = null,\n imageFit = 'cover',\n lanyardImage = null,\n lanyardWidth = 1\n}: LanyardProps) {\n const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);\n\n useEffect(() => {\n const handleResize = (): void => setIsMobile(window.innerWidth < 768);\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, []);\n\n return (\n
\n gl.setClearColor(new THREE.Color(0x000000), transparent ? 0 : 1)}\n >\n \n \n \n \n \n \n \n \n \n \n \n
\n );\n}\n\ninterface BandProps {\n maxSpeed?: number;\n minSpeed?: number;\n isMobile?: boolean;\n frontImage?: string | null;\n backImage?: string | null;\n imageFit?: 'cover' | 'contain';\n lanyardImage?: string | null;\n lanyardWidth?: number;\n}\n\nfunction Band({\n maxSpeed = 50,\n minSpeed = 0,\n isMobile = false,\n frontImage = null,\n backImage = null,\n imageFit = 'cover',\n lanyardImage = null,\n lanyardWidth = 1\n}: BandProps) {\n // Using \"any\" for refs since the exact types depend on Rapier's internals\n const band = useRef(null);\n const fixed = useRef(null);\n const j1 = useRef(null);\n const j2 = useRef(null);\n const j3 = useRef(null);\n const card = useRef(null);\n\n const vec = new THREE.Vector3();\n const ang = new THREE.Vector3();\n const rot = new THREE.Vector3();\n const dir = new THREE.Vector3();\n\n const segmentProps: any = {\n type: 'dynamic' as RigidBodyProps['type'],\n canSleep: true,\n colliders: false,\n angularDamping: 4,\n linearDamping: 4\n };\n\n const { nodes, materials } = useGLTF(cardGLB) as any;\n const texture = useTexture(lanyardImage || lanyard);\n // useTexture must be called unconditionally; use a blank pixel when an image\n // isn't supplied for a given face, then skip compositing it below.\n const frontTex = useTexture(frontImage || BLANK_PIXEL);\n const backTex = useTexture(backImage || BLANK_PIXEL);\n\n // Composite the front/back images into the card's texture atlas (front = left\n // half, back = right half). Each image is drawn aspect-preserving (no stretch).\n const cardMap = useMemo(() => {\n const baseMap = materials.base.map as THREE.Texture;\n if (!frontImage && !backImage) return baseMap;\n\n const baseImg = baseMap.image as any;\n const W = baseImg.width;\n const H = baseImg.height;\n const canvas = document.createElement('canvas');\n canvas.width = W;\n canvas.height = H;\n const ctx = canvas.getContext('2d');\n if (!ctx) return baseMap;\n // Keep the original baked atlas for the card edges and any untouched face.\n ctx.drawImage(baseImg, 0, 0, W, H);\n\n const drawFitted = (img: any, rect: typeof FRONT_UV_RECT) => {\n const rx = rect.x * W;\n const ry = rect.y * H;\n const rw = rect.w * W;\n const rh = rect.h * H;\n const pick = imageFit === 'contain' ? Math.min : Math.max;\n const scale = pick(rw / img.width, rh / img.height);\n const dw = img.width * scale;\n const dh = img.height * scale;\n const dx = rx + (rw - dw) / 2;\n const dy = ry + (rh - dh) / 2;\n ctx.save();\n ctx.beginPath();\n ctx.rect(rx, ry, rw, rh);\n ctx.clip();\n ctx.drawImage(img, dx, dy, dw, dh);\n ctx.restore();\n };\n\n if (frontImage && frontTex.image) drawFitted(frontTex.image, FRONT_UV_RECT);\n if (backImage && backTex.image) drawFitted(backTex.image, BACK_UV_RECT);\n\n const composite = new THREE.CanvasTexture(canvas);\n composite.colorSpace = THREE.SRGBColorSpace;\n composite.flipY = baseMap.flipY;\n composite.anisotropy = 16;\n composite.needsUpdate = true;\n return composite;\n }, [frontImage, backImage, imageFit, frontTex, backTex, materials.base.map]);\n const [curve] = useState(\n () =>\n new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()])\n );\n const [dragged, drag] = useState(false);\n const [hovered, hover] = useState(false);\n\n useRopeJoint(fixed, j1, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j1, j2, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j2, j3, [[0, 0, 0], [0, 0, 0], 1]);\n useSphericalJoint(j3, card, [\n [0, 0, 0],\n [0, 1.45, 0]\n ]);\n\n useEffect(() => {\n if (hovered) {\n document.body.style.cursor = dragged ? 'grabbing' : 'grab';\n return () => {\n document.body.style.cursor = 'auto';\n };\n }\n }, [hovered, dragged]);\n\n useFrame((state, delta) => {\n if (dragged && typeof dragged !== 'boolean') {\n vec.set(state.pointer.x, state.pointer.y, 0.5).unproject(state.camera);\n dir.copy(vec).sub(state.camera.position).normalize();\n vec.add(dir.multiplyScalar(state.camera.position.length()));\n [card, j1, j2, j3, fixed].forEach(ref => ref.current?.wakeUp());\n card.current?.setNextKinematicTranslation({\n x: vec.x - dragged.x,\n y: vec.y - dragged.y,\n z: vec.z - dragged.z\n });\n }\n if (fixed.current) {\n [j1, j2].forEach(ref => {\n if (!ref.current.lerped) ref.current.lerped = new THREE.Vector3().copy(ref.current.translation());\n const clampedDistance = Math.max(0.1, Math.min(1, ref.current.lerped.distanceTo(ref.current.translation())));\n ref.current.lerped.lerp(\n ref.current.translation(),\n delta * (minSpeed + clampedDistance * (maxSpeed - minSpeed))\n );\n });\n curve.points[0].copy(j3.current.translation());\n curve.points[1].copy(j2.current.lerped);\n curve.points[2].copy(j1.current.lerped);\n curve.points[3].copy(fixed.current.translation());\n band.current.geometry.setPoints(curve.getPoints(isMobile ? 16 : 32));\n ang.copy(card.current.angvel());\n rot.copy(card.current.rotation());\n card.current.setAngvel({ x: ang.x, y: ang.y - rot.y * 0.25, z: ang.z });\n }\n });\n\n curve.curveType = 'chordal';\n texture.wrapS = texture.wrapT = THREE.RepeatWrapping;\n\n return (\n <>\n \n \n \n \n \n \n \n \n \n \n \n \n \n hover(true)}\n onPointerOut={() => hover(false)}\n onPointerUp={(e: any) => {\n e.target.releasePointerCapture(e.pointerId);\n drag(false);\n }}\n onPointerDown={(e: any) => {\n e.target.setPointerCapture(e.pointerId);\n drag(new THREE.Vector3().copy(e.point).sub(vec.copy(card.current.translation())));\n }}\n >\n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/Lanyard-TS-TW.json b/public/r/Lanyard-TS-TW.json index 442dad44f..2790fe03b 100644 --- a/public/r/Lanyard-TS-TW.json +++ b/public/r/Lanyard-TS-TW.json @@ -8,7 +8,7 @@ { "type": "registry:component", "path": "Lanyard.tsx", - "content": "/* eslint-disable react/no-unknown-property */\n'use client';\nimport { useEffect, useRef, useState } from 'react';\nimport { Canvas, extend, useFrame } from '@react-three/fiber';\nimport { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei';\nimport {\n BallCollider,\n CuboidCollider,\n Physics,\n RigidBody,\n useRopeJoint,\n useSphericalJoint,\n RigidBodyProps\n} from '@react-three/rapier';\nimport { MeshLineGeometry, MeshLineMaterial } from 'meshline';\nimport * as THREE from 'three';\n\n// replace with your own imports, see the usage snippet for details\nimport cardGLB from './card.glb';\nimport lanyard from './lanyard.png';\n\nextend({ MeshLineGeometry, MeshLineMaterial });\n\ninterface LanyardProps {\n position?: [number, number, number];\n gravity?: [number, number, number];\n fov?: number;\n transparent?: boolean;\n}\n\nexport default function Lanyard({\n position = [0, 0, 30],\n gravity = [0, -40, 0],\n fov = 20,\n transparent = true\n}: LanyardProps) {\n const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);\n\n useEffect(() => {\n const handleResize = (): void => setIsMobile(window.innerWidth < 768);\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, []);\n\n return (\n
\n gl.setClearColor(new THREE.Color(0x000000), transparent ? 0 : 1)}\n >\n \n \n \n \n \n \n \n \n \n \n \n
\n );\n}\n\ninterface BandProps {\n maxSpeed?: number;\n minSpeed?: number;\n isMobile?: boolean;\n}\n\nfunction Band({ maxSpeed = 50, minSpeed = 0, isMobile = false }: BandProps) {\n // Using \"any\" for refs since the exact types depend on Rapier's internals\n const band = useRef(null);\n const fixed = useRef(null);\n const j1 = useRef(null);\n const j2 = useRef(null);\n const j3 = useRef(null);\n const card = useRef(null);\n\n const vec = new THREE.Vector3();\n const ang = new THREE.Vector3();\n const rot = new THREE.Vector3();\n const dir = new THREE.Vector3();\n\n const segmentProps: any = {\n type: 'dynamic' as RigidBodyProps['type'],\n canSleep: true,\n colliders: false,\n angularDamping: 4,\n linearDamping: 4\n };\n\n const { nodes, materials } = useGLTF(cardGLB) as any;\n const texture = useTexture(lanyard);\n const [curve] = useState(\n () =>\n new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()])\n );\n const [dragged, drag] = useState(false);\n const [hovered, hover] = useState(false);\n\n useRopeJoint(fixed, j1, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j1, j2, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j2, j3, [[0, 0, 0], [0, 0, 0], 1]);\n useSphericalJoint(j3, card, [\n [0, 0, 0],\n [0, 1.45, 0]\n ]);\n\n useEffect(() => {\n if (hovered) {\n document.body.style.cursor = dragged ? 'grabbing' : 'grab';\n return () => {\n document.body.style.cursor = 'auto';\n };\n }\n }, [hovered, dragged]);\n\n useFrame((state, delta) => {\n if (dragged && typeof dragged !== 'boolean') {\n vec.set(state.pointer.x, state.pointer.y, 0.5).unproject(state.camera);\n dir.copy(vec).sub(state.camera.position).normalize();\n vec.add(dir.multiplyScalar(state.camera.position.length()));\n [card, j1, j2, j3, fixed].forEach(ref => ref.current?.wakeUp());\n card.current?.setNextKinematicTranslation({\n x: vec.x - dragged.x,\n y: vec.y - dragged.y,\n z: vec.z - dragged.z\n });\n }\n if (fixed.current) {\n [j1, j2].forEach(ref => {\n if (!ref.current.lerped) ref.current.lerped = new THREE.Vector3().copy(ref.current.translation());\n const clampedDistance = Math.max(0.1, Math.min(1, ref.current.lerped.distanceTo(ref.current.translation())));\n ref.current.lerped.lerp(\n ref.current.translation(),\n delta * (minSpeed + clampedDistance * (maxSpeed - minSpeed))\n );\n });\n curve.points[0].copy(j3.current.translation());\n curve.points[1].copy(j2.current.lerped);\n curve.points[2].copy(j1.current.lerped);\n curve.points[3].copy(fixed.current.translation());\n band.current.geometry.setPoints(curve.getPoints(isMobile ? 16 : 32));\n ang.copy(card.current.angvel());\n rot.copy(card.current.rotation());\n card.current.setAngvel({ x: ang.x, y: ang.y - rot.y * 0.25, z: ang.z });\n }\n });\n\n curve.curveType = 'chordal';\n texture.wrapS = texture.wrapT = THREE.RepeatWrapping;\n\n return (\n <>\n \n \n \n \n \n \n \n \n \n \n \n \n \n hover(true)}\n onPointerOut={() => hover(false)}\n onPointerUp={(e: any) => {\n e.target.releasePointerCapture(e.pointerId);\n drag(false);\n }}\n onPointerDown={(e: any) => {\n e.target.setPointerCapture(e.pointerId);\n drag(new THREE.Vector3().copy(e.point).sub(vec.copy(card.current.translation())));\n }}\n >\n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n" + "content": "/* eslint-disable react/no-unknown-property */\n'use client';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { Canvas, extend, useFrame } from '@react-three/fiber';\nimport { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei';\nimport {\n BallCollider,\n CuboidCollider,\n Physics,\n RigidBody,\n useRopeJoint,\n useSphericalJoint,\n RigidBodyProps\n} from '@react-three/rapier';\nimport { MeshLineGeometry, MeshLineMaterial } from 'meshline';\nimport * as THREE from 'three';\n\n// replace with your own imports, see the usage snippet for details\nimport cardGLB from './card.glb';\nimport lanyard from './lanyard.png';\n\nextend({ MeshLineGeometry, MeshLineMaterial });\n\n// 1x1 transparent pixel — lets useTexture be called unconditionally when a\n// front/back image isn't supplied.\nconst BLANK_PIXEL =\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';\n\n// The card model's front face is UV-mapped to the LEFT half of the texture\n// atlas and the back face to the RIGHT half (measured from card.glb). Each\n// custom image is composited into its own half so the two faces render\n// independently, aspect-preserving (no stretching).\nconst FRONT_UV_RECT = { x: 0, y: 0, w: 0.5, h: 0.755 };\nconst BACK_UV_RECT = { x: 0.5, y: 0, w: 0.5, h: 0.757 };\n\ninterface LanyardProps {\n position?: [number, number, number];\n gravity?: [number, number, number];\n fov?: number;\n transparent?: boolean;\n frontImage?: string | null;\n backImage?: string | null;\n imageFit?: 'cover' | 'contain';\n lanyardImage?: string | null;\n lanyardWidth?: number;\n}\n\nexport default function Lanyard({\n position = [0, 0, 30],\n gravity = [0, -40, 0],\n fov = 20,\n transparent = true,\n frontImage = null,\n backImage = null,\n imageFit = 'cover',\n lanyardImage = null,\n lanyardWidth = 1\n}: LanyardProps) {\n const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);\n\n useEffect(() => {\n const handleResize = (): void => setIsMobile(window.innerWidth < 768);\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, []);\n\n return (\n
\n gl.setClearColor(new THREE.Color(0x000000), transparent ? 0 : 1)}\n >\n \n \n \n \n \n \n \n \n \n \n \n
\n );\n}\n\ninterface BandProps {\n maxSpeed?: number;\n minSpeed?: number;\n isMobile?: boolean;\n frontImage?: string | null;\n backImage?: string | null;\n imageFit?: 'cover' | 'contain';\n lanyardImage?: string | null;\n lanyardWidth?: number;\n}\n\nfunction Band({\n maxSpeed = 50,\n minSpeed = 0,\n isMobile = false,\n frontImage = null,\n backImage = null,\n imageFit = 'cover',\n lanyardImage = null,\n lanyardWidth = 1\n}: BandProps) {\n // Using \"any\" for refs since the exact types depend on Rapier's internals\n const band = useRef(null);\n const fixed = useRef(null);\n const j1 = useRef(null);\n const j2 = useRef(null);\n const j3 = useRef(null);\n const card = useRef(null);\n\n const vec = new THREE.Vector3();\n const ang = new THREE.Vector3();\n const rot = new THREE.Vector3();\n const dir = new THREE.Vector3();\n\n const segmentProps: any = {\n type: 'dynamic' as RigidBodyProps['type'],\n canSleep: true,\n colliders: false,\n angularDamping: 4,\n linearDamping: 4\n };\n\n const { nodes, materials } = useGLTF(cardGLB) as any;\n const texture = useTexture(lanyardImage || lanyard);\n // useTexture must be called unconditionally; use a blank pixel when an image\n // isn't supplied for a given face, then skip compositing it below.\n const frontTex = useTexture(frontImage || BLANK_PIXEL);\n const backTex = useTexture(backImage || BLANK_PIXEL);\n\n // Composite the front/back images into the card's texture atlas (front = left\n // half, back = right half). Each image is drawn aspect-preserving (no stretch).\n const cardMap = useMemo(() => {\n const baseMap = materials.base.map as THREE.Texture;\n if (!frontImage && !backImage) return baseMap;\n\n const baseImg = baseMap.image as any;\n const W = baseImg.width;\n const H = baseImg.height;\n const canvas = document.createElement('canvas');\n canvas.width = W;\n canvas.height = H;\n const ctx = canvas.getContext('2d');\n if (!ctx) return baseMap;\n // Keep the original baked atlas for the card edges and any untouched face.\n ctx.drawImage(baseImg, 0, 0, W, H);\n\n const drawFitted = (img: any, rect: typeof FRONT_UV_RECT) => {\n const rx = rect.x * W;\n const ry = rect.y * H;\n const rw = rect.w * W;\n const rh = rect.h * H;\n const pick = imageFit === 'contain' ? Math.min : Math.max;\n const scale = pick(rw / img.width, rh / img.height);\n const dw = img.width * scale;\n const dh = img.height * scale;\n const dx = rx + (rw - dw) / 2;\n const dy = ry + (rh - dh) / 2;\n ctx.save();\n ctx.beginPath();\n ctx.rect(rx, ry, rw, rh);\n ctx.clip();\n ctx.drawImage(img, dx, dy, dw, dh);\n ctx.restore();\n };\n\n if (frontImage && frontTex.image) drawFitted(frontTex.image, FRONT_UV_RECT);\n if (backImage && backTex.image) drawFitted(backTex.image, BACK_UV_RECT);\n\n const composite = new THREE.CanvasTexture(canvas);\n composite.colorSpace = THREE.SRGBColorSpace;\n composite.flipY = baseMap.flipY;\n composite.anisotropy = 16;\n composite.needsUpdate = true;\n return composite;\n }, [frontImage, backImage, imageFit, frontTex, backTex, materials.base.map]);\n const [curve] = useState(\n () =>\n new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()])\n );\n const [dragged, drag] = useState(false);\n const [hovered, hover] = useState(false);\n\n useRopeJoint(fixed, j1, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j1, j2, [[0, 0, 0], [0, 0, 0], 1]);\n useRopeJoint(j2, j3, [[0, 0, 0], [0, 0, 0], 1]);\n useSphericalJoint(j3, card, [\n [0, 0, 0],\n [0, 1.45, 0]\n ]);\n\n useEffect(() => {\n if (hovered) {\n document.body.style.cursor = dragged ? 'grabbing' : 'grab';\n return () => {\n document.body.style.cursor = 'auto';\n };\n }\n }, [hovered, dragged]);\n\n useFrame((state, delta) => {\n if (dragged && typeof dragged !== 'boolean') {\n vec.set(state.pointer.x, state.pointer.y, 0.5).unproject(state.camera);\n dir.copy(vec).sub(state.camera.position).normalize();\n vec.add(dir.multiplyScalar(state.camera.position.length()));\n [card, j1, j2, j3, fixed].forEach(ref => ref.current?.wakeUp());\n card.current?.setNextKinematicTranslation({\n x: vec.x - dragged.x,\n y: vec.y - dragged.y,\n z: vec.z - dragged.z\n });\n }\n if (fixed.current) {\n [j1, j2].forEach(ref => {\n if (!ref.current.lerped) ref.current.lerped = new THREE.Vector3().copy(ref.current.translation());\n const clampedDistance = Math.max(0.1, Math.min(1, ref.current.lerped.distanceTo(ref.current.translation())));\n ref.current.lerped.lerp(\n ref.current.translation(),\n delta * (minSpeed + clampedDistance * (maxSpeed - minSpeed))\n );\n });\n curve.points[0].copy(j3.current.translation());\n curve.points[1].copy(j2.current.lerped);\n curve.points[2].copy(j1.current.lerped);\n curve.points[3].copy(fixed.current.translation());\n band.current.geometry.setPoints(curve.getPoints(isMobile ? 16 : 32));\n ang.copy(card.current.angvel());\n rot.copy(card.current.rotation());\n card.current.setAngvel({ x: ang.x, y: ang.y - rot.y * 0.25, z: ang.z });\n }\n });\n\n curve.curveType = 'chordal';\n texture.wrapS = texture.wrapT = THREE.RepeatWrapping;\n\n return (\n <>\n \n \n \n \n \n \n \n \n \n \n \n \n \n hover(true)}\n onPointerOut={() => hover(false)}\n onPointerUp={(e: any) => {\n e.target.releasePointerCapture(e.pointerId);\n drag(false);\n }}\n onPointerDown={(e: any) => {\n e.target.setPointerCapture(e.pointerId);\n drag(new THREE.Vector3().copy(e.point).sub(vec.copy(card.current.translation())));\n }}\n >\n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n" } ], "registryDependencies": [], diff --git a/public/r/LaserFlow-JS-CSS.json b/public/r/LaserFlow-JS-CSS.json index a859349d9..ca9f50458 100644 --- a/public/r/LaserFlow-JS-CSS.json +++ b/public/r/LaserFlow-JS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/LaserFlow-JS-TW.json b/public/r/LaserFlow-JS-TW.json index e8dee1bfc..0a65ac1e9 100644 --- a/public/r/LaserFlow-JS-TW.json +++ b/public/r/LaserFlow-JS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/LaserFlow-TS-CSS.json b/public/r/LaserFlow-TS-CSS.json index 1e74d0a67..f2898c6f4 100644 --- a/public/r/LaserFlow-TS-CSS.json +++ b/public/r/LaserFlow-TS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/LaserFlow-TS-TW.json b/public/r/LaserFlow-TS-TW.json index 7c773d76a..996ee483c 100644 --- a/public/r/LaserFlow-TS-TW.json +++ b/public/r/LaserFlow-TS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/LightPillar-JS-CSS.json b/public/r/LightPillar-JS-CSS.json index c933d4de8..46df2ab4f 100644 --- a/public/r/LightPillar-JS-CSS.json +++ b/public/r/LightPillar-JS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/LightPillar-JS-TW.json b/public/r/LightPillar-JS-TW.json index 54efeb335..d832fdcac 100644 --- a/public/r/LightPillar-JS-TW.json +++ b/public/r/LightPillar-JS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/LightPillar-TS-CSS.json b/public/r/LightPillar-TS-CSS.json index 0e10b6962..c3a94d903 100644 --- a/public/r/LightPillar-TS-CSS.json +++ b/public/r/LightPillar-TS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/LightPillar-TS-TW.json b/public/r/LightPillar-TS-TW.json index f69651284..742ff7826 100644 --- a/public/r/LightPillar-TS-TW.json +++ b/public/r/LightPillar-TS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/LiquidEther-JS-CSS.json b/public/r/LiquidEther-JS-CSS.json index 6701e2dc0..4ad13ce05 100644 --- a/public/r/LiquidEther-JS-CSS.json +++ b/public/r/LiquidEther-JS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/LiquidEther-JS-TW.json b/public/r/LiquidEther-JS-TW.json index 67fa76892..9518167e3 100644 --- a/public/r/LiquidEther-JS-TW.json +++ b/public/r/LiquidEther-JS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/LiquidEther-TS-CSS.json b/public/r/LiquidEther-TS-CSS.json index e05b16b8b..0caf2786b 100644 --- a/public/r/LiquidEther-TS-CSS.json +++ b/public/r/LiquidEther-TS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/LiquidEther-TS-TW.json b/public/r/LiquidEther-TS-TW.json index 89944f185..bc1c82e4f 100644 --- a/public/r/LiquidEther-TS-TW.json +++ b/public/r/LiquidEther-TS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/MagicRings-JS-CSS.json b/public/r/MagicRings-JS-CSS.json index 2b198eda1..23aab4b04 100644 --- a/public/r/MagicRings-JS-CSS.json +++ b/public/r/MagicRings-JS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/MagicRings-JS-TW.json b/public/r/MagicRings-JS-TW.json index 336cdf1f2..d8f610bea 100644 --- a/public/r/MagicRings-JS-TW.json +++ b/public/r/MagicRings-JS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/MagicRings-TS-CSS.json b/public/r/MagicRings-TS-CSS.json index 3d725085a..5d8320a98 100644 --- a/public/r/MagicRings-TS-CSS.json +++ b/public/r/MagicRings-TS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/MagicRings-TS-TW.json b/public/r/MagicRings-TS-TW.json index 9c33836ab..33ed390f2 100644 --- a/public/r/MagicRings-TS-TW.json +++ b/public/r/MagicRings-TS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ModelViewer-JS-CSS.json b/public/r/ModelViewer-JS-CSS.json index dbd6fab60..0bcaf7eb9 100644 --- a/public/r/ModelViewer-JS-CSS.json +++ b/public/r/ModelViewer-JS-CSS.json @@ -15,6 +15,6 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ModelViewer-JS-TW.json b/public/r/ModelViewer-JS-TW.json index c6ae1a818..294321625 100644 --- a/public/r/ModelViewer-JS-TW.json +++ b/public/r/ModelViewer-JS-TW.json @@ -15,6 +15,6 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ModelViewer-TS-CSS.json b/public/r/ModelViewer-TS-CSS.json index 43d9dcc4c..29ac1db8d 100644 --- a/public/r/ModelViewer-TS-CSS.json +++ b/public/r/ModelViewer-TS-CSS.json @@ -15,6 +15,6 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ModelViewer-TS-TW.json b/public/r/ModelViewer-TS-TW.json index 0014560f4..c61e54fc4 100644 --- a/public/r/ModelViewer-TS-TW.json +++ b/public/r/ModelViewer-TS-TW.json @@ -15,6 +15,6 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/PixelBlast-JS-CSS.json b/public/r/PixelBlast-JS-CSS.json index 690907995..dd71b151d 100644 --- a/public/r/PixelBlast-JS-CSS.json +++ b/public/r/PixelBlast-JS-CSS.json @@ -19,6 +19,6 @@ "registryDependencies": [], "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/PixelBlast-JS-TW.json b/public/r/PixelBlast-JS-TW.json index 22a0ef843..458f1eaa5 100644 --- a/public/r/PixelBlast-JS-TW.json +++ b/public/r/PixelBlast-JS-TW.json @@ -14,6 +14,6 @@ "registryDependencies": [], "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/PixelBlast-TS-CSS.json b/public/r/PixelBlast-TS-CSS.json index ca121431e..c9b9f6b51 100644 --- a/public/r/PixelBlast-TS-CSS.json +++ b/public/r/PixelBlast-TS-CSS.json @@ -19,6 +19,6 @@ "registryDependencies": [], "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/PixelBlast-TS-TW.json b/public/r/PixelBlast-TS-TW.json index c6c0f4f3e..9506e87b4 100644 --- a/public/r/PixelBlast-TS-TW.json +++ b/public/r/PixelBlast-TS-TW.json @@ -14,6 +14,6 @@ "registryDependencies": [], "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/PixelSnow-JS-CSS.json b/public/r/PixelSnow-JS-CSS.json index 6fc426533..ac0f5bcce 100644 --- a/public/r/PixelSnow-JS-CSS.json +++ b/public/r/PixelSnow-JS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/PixelSnow-JS-TW.json b/public/r/PixelSnow-JS-TW.json index 4f982f141..bf22ad191 100644 --- a/public/r/PixelSnow-JS-TW.json +++ b/public/r/PixelSnow-JS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/PixelSnow-TS-CSS.json b/public/r/PixelSnow-TS-CSS.json index f1b4b918a..b644c64be 100644 --- a/public/r/PixelSnow-TS-CSS.json +++ b/public/r/PixelSnow-TS-CSS.json @@ -18,6 +18,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/PixelSnow-TS-TW.json b/public/r/PixelSnow-TS-TW.json index c0831fe75..77f1ec0c8 100644 --- a/public/r/PixelSnow-TS-TW.json +++ b/public/r/PixelSnow-TS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/PixelTrail-JS-CSS.json b/public/r/PixelTrail-JS-CSS.json index f4b31391b..fb2cf90b2 100644 --- a/public/r/PixelTrail-JS-CSS.json +++ b/public/r/PixelTrail-JS-CSS.json @@ -20,6 +20,6 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/PixelTrail-JS-TW.json b/public/r/PixelTrail-JS-TW.json index 8f76c27b4..b2c6c9652 100644 --- a/public/r/PixelTrail-JS-TW.json +++ b/public/r/PixelTrail-JS-TW.json @@ -15,6 +15,6 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/PixelTrail-TS-CSS.json b/public/r/PixelTrail-TS-CSS.json index 428e73640..b7f428f8d 100644 --- a/public/r/PixelTrail-TS-CSS.json +++ b/public/r/PixelTrail-TS-CSS.json @@ -20,6 +20,6 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/PixelTrail-TS-TW.json b/public/r/PixelTrail-TS-TW.json index 4048c0f04..3110313b6 100644 --- a/public/r/PixelTrail-TS-TW.json +++ b/public/r/PixelTrail-TS-TW.json @@ -15,6 +15,6 @@ "dependencies": [ "@react-three/drei@^10.7.4", "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ShapeBlur-JS-CSS.json b/public/r/ShapeBlur-JS-CSS.json index 7df306d1a..e11f35e16 100644 --- a/public/r/ShapeBlur-JS-CSS.json +++ b/public/r/ShapeBlur-JS-CSS.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ShapeBlur-JS-TW.json b/public/r/ShapeBlur-JS-TW.json index 4ef46f328..b2cfc8c35 100644 --- a/public/r/ShapeBlur-JS-TW.json +++ b/public/r/ShapeBlur-JS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ShapeBlur-TS-CSS.json b/public/r/ShapeBlur-TS-CSS.json index 0ed5299f2..f8ef31991 100644 --- a/public/r/ShapeBlur-TS-CSS.json +++ b/public/r/ShapeBlur-TS-CSS.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/ShapeBlur-TS-TW.json b/public/r/ShapeBlur-TS-TW.json index 422f6de73..b0dd7de4b 100644 --- a/public/r/ShapeBlur-TS-TW.json +++ b/public/r/ShapeBlur-TS-TW.json @@ -13,6 +13,6 @@ ], "registryDependencies": [], "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Silk-JS-CSS.json b/public/r/Silk-JS-CSS.json index 2000757fb..a15183a3a 100644 --- a/public/r/Silk-JS-CSS.json +++ b/public/r/Silk-JS-CSS.json @@ -14,6 +14,6 @@ "registryDependencies": [], "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Silk-JS-TW.json b/public/r/Silk-JS-TW.json index 8d199a81b..6bfb4958e 100644 --- a/public/r/Silk-JS-TW.json +++ b/public/r/Silk-JS-TW.json @@ -14,6 +14,6 @@ "registryDependencies": [], "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Silk-TS-CSS.json b/public/r/Silk-TS-CSS.json index 9788cb4e6..ccf6d4dae 100644 --- a/public/r/Silk-TS-CSS.json +++ b/public/r/Silk-TS-CSS.json @@ -14,6 +14,6 @@ "registryDependencies": [], "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/Silk-TS-TW.json b/public/r/Silk-TS-TW.json index 23d412d43..fd808a7aa 100644 --- a/public/r/Silk-TS-TW.json +++ b/public/r/Silk-TS-TW.json @@ -14,6 +14,6 @@ "registryDependencies": [], "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ] } \ No newline at end of file diff --git a/public/r/registry.json b/public/r/registry.json index 0244e2d9b..28f8028f6 100644 --- a/public/r/registry.json +++ b/public/r/registry.json @@ -593,7 +593,7 @@ "description": "Semi-transparent ghost cursor that smoothly follows the real cursor with a trailing effect.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -613,7 +613,7 @@ "description": "Semi-transparent ghost cursor that smoothly follows the real cursor with a trailing effect.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -629,7 +629,7 @@ "description": "Semi-transparent ghost cursor that smoothly follows the real cursor with a trailing effect.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -649,7 +649,7 @@ "description": "Semi-transparent ghost cursor that smoothly follows the real cursor with a trailing effect.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1123,7 +1123,7 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1145,7 +1145,7 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1163,7 +1163,7 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1185,7 +1185,7 @@ "dependencies": [ "@react-three/drei@^10.7.4", "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1345,7 +1345,7 @@ "description": "Morphing blurred geometric shape. The effect occurs on hover.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1361,7 +1361,7 @@ "description": "Morphing blurred geometric shape. The effect occurs on hover.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1377,7 +1377,7 @@ "description": "Morphing blurred geometric shape. The effect occurs on hover.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1393,7 +1393,7 @@ "description": "Morphing blurred geometric shape. The effect occurs on hover.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1673,7 +1673,7 @@ "description": "Dynamic laser light that flows onto a surface, customizable effect.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1693,7 +1693,7 @@ "description": "Dynamic laser light that flows onto a surface, customizable effect.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1709,7 +1709,7 @@ "description": "Dynamic laser light that flows onto a surface, customizable effect.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1729,7 +1729,7 @@ "description": "Dynamic laser light that flows onto a surface, customizable effect.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1746,7 +1746,7 @@ "type": "registry:component", "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1763,7 +1763,7 @@ "type": "registry:component", "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1780,7 +1780,7 @@ "type": "registry:component", "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1797,7 +1797,7 @@ "type": "registry:component", "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1885,7 +1885,7 @@ "description": "Interactive magic rings effect with customizable parameters.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1905,7 +1905,7 @@ "description": "Interactive magic rings effect with customizable parameters.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1921,7 +1921,7 @@ "description": "Interactive magic rings effect with customizable parameters.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1941,7 +1941,7 @@ "description": "Interactive magic rings effect with customizable parameters.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1957,7 +1957,7 @@ "description": "Renders text with an animated ASCII background for a retro feel.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1973,7 +1973,7 @@ "description": "Renders text with an animated ASCII background for a retro feel.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -1989,7 +1989,7 @@ "description": "Renders text with an animated ASCII background for a retro feel.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -2005,7 +2005,7 @@ "description": "Renders text with an animated ASCII background for a retro feel.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -4553,7 +4553,7 @@ "description": "Glassmorphism container with animated liquid distortion refraction.", "type": "registry:component", "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", "maath@^0.10.8" @@ -4572,7 +4572,7 @@ "description": "Glassmorphism container with animated liquid distortion refraction.", "type": "registry:component", "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", "maath@^0.10.8" @@ -4591,7 +4591,7 @@ "description": "Glassmorphism container with animated liquid distortion refraction.", "type": "registry:component", "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", "maath@^0.10.8" @@ -4610,7 +4610,7 @@ "description": "Glassmorphism container with animated liquid distortion refraction.", "type": "registry:component", "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", "maath@^0.10.8" @@ -5239,7 +5239,7 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -5257,7 +5257,7 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -5275,7 +5275,7 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -5293,7 +5293,7 @@ "dependencies": [ "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -6217,7 +6217,7 @@ "description": "Physics ball pit simulation with bouncing colorful spheres.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -6233,7 +6233,7 @@ "description": "Physics ball pit simulation with bouncing colorful spheres.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -6250,7 +6250,7 @@ "type": "registry:component", "dependencies": [ "gsap@^3.13.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -6267,7 +6267,7 @@ "type": "registry:component", "dependencies": [ "gsap@^3.13.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -6283,7 +6283,7 @@ "description": "Crossing animated ribbons with customizable properties.", "type": "registry:component", "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4" ], @@ -6305,7 +6305,7 @@ "description": "Crossing animated ribbons with customizable properties.", "type": "registry:component", "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4" ], @@ -6323,7 +6323,7 @@ "description": "Crossing animated ribbons with customizable properties.", "type": "registry:component", "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4" ], @@ -6345,7 +6345,7 @@ "description": "Crossing animated ribbons with customizable properties.", "type": "registry:component", "dependencies": [ - "three@^0.167.1", + "three@^0.180.0", "@react-three/fiber@^9.3.0", "@react-three/drei@^10.7.4" ], @@ -6363,7 +6363,7 @@ "description": "Vibrant color bends with smooth flowing animation.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -6383,7 +6383,7 @@ "description": "Vibrant color bends with smooth flowing animation.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -6399,7 +6399,7 @@ "description": "Vibrant color bends with smooth flowing animation.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -6419,7 +6419,7 @@ "description": "Vibrant color bends with smooth flowing animation.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -6510,7 +6510,7 @@ "@react-three/fiber@^9.3.0", "@react-three/postprocessing@^3.0.4", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -6533,7 +6533,7 @@ "@react-three/fiber@^9.3.0", "@react-three/postprocessing@^3.0.4", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -6552,7 +6552,7 @@ "@react-three/fiber@^9.3.0", "@react-three/postprocessing@^3.0.4", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -6575,7 +6575,7 @@ "@react-three/fiber@^9.3.0", "@react-three/postprocessing@^3.0.4", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -7161,7 +7161,7 @@ "dependencies": [ "face-api.js@^0.22.2", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -7183,7 +7183,7 @@ "dependencies": [ "face-api.js@^0.22.2", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -7201,7 +7201,7 @@ "dependencies": [ "face-api.js@^0.22.2", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -7223,7 +7223,7 @@ "dependencies": [ "face-api.js@^0.22.2", "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -7239,7 +7239,7 @@ "description": "Warped grid mesh distorts smoothly reacting to cursor.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -7259,7 +7259,7 @@ "description": "Warped grid mesh distorts smoothly reacting to cursor.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -7275,7 +7275,7 @@ "description": "Warped grid mesh distorts smoothly reacting to cursor.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -7295,7 +7295,7 @@ "description": "Warped grid mesh distorts smoothly reacting to cursor.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -7384,7 +7384,7 @@ "type": "registry:component", "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -7409,7 +7409,7 @@ "type": "registry:component", "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -7430,7 +7430,7 @@ "type": "registry:component", "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -7455,7 +7455,7 @@ "type": "registry:component", "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -8244,7 +8244,7 @@ "type": "registry:component", "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -8265,7 +8265,7 @@ "type": "registry:component", "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -8282,7 +8282,7 @@ "type": "registry:component", "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -8303,7 +8303,7 @@ "type": "registry:component", "dependencies": [ "postprocessing@^6.36.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -8680,7 +8680,7 @@ "type": "registry:component", "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -8697,7 +8697,7 @@ "type": "registry:component", "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -8714,7 +8714,7 @@ "type": "registry:component", "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -8731,7 +8731,7 @@ "type": "registry:component", "dependencies": [ "@react-three/fiber@^9.3.0", - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9019,7 +9019,7 @@ "description": "Interactive liquid shader with flowing distortion and customizable colors.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9039,7 +9039,7 @@ "description": "Interactive liquid shader with flowing distortion and customizable colors.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9055,7 +9055,7 @@ "description": "Interactive liquid shader with flowing distortion and customizable colors.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9075,7 +9075,7 @@ "description": "Interactive liquid shader with flowing distortion and customizable colors.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9091,7 +9091,7 @@ "description": "3D floating lines that react to cursor movement.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9111,7 +9111,7 @@ "description": "3D floating lines that react to cursor movement.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9127,7 +9127,7 @@ "description": "3D floating lines that react to cursor movement.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9147,7 +9147,7 @@ "description": "3D floating lines that react to cursor movement.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9163,7 +9163,7 @@ "description": "Vertical pillar of light with glow effects.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9183,7 +9183,7 @@ "description": "Vertical pillar of light with glow effects.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9199,7 +9199,7 @@ "description": "Vertical pillar of light with glow effects.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9219,7 +9219,7 @@ "description": "Vertical pillar of light with glow effects.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9235,7 +9235,7 @@ "description": "Falling pixelated snow effect with customizable density and speed.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9255,7 +9255,7 @@ "description": "Falling pixelated snow effect with customizable density and speed.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9271,7 +9271,7 @@ "description": "Falling pixelated snow effect with customizable density and speed.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ @@ -9291,7 +9291,7 @@ "description": "Falling pixelated snow effect with customizable density and speed.", "type": "registry:component", "dependencies": [ - "three@^0.167.1" + "three@^0.180.0" ], "registryDependencies": [], "files": [ diff --git a/src/content/Backgrounds/Ballpit/Ballpit.jsx b/src/content/Backgrounds/Ballpit/Ballpit.jsx index a7d844d0b..35175f7cd 100644 --- a/src/content/Backgrounds/Ballpit/Ballpit.jsx +++ b/src/content/Backgrounds/Ballpit/Ballpit.jsx @@ -3,7 +3,7 @@ import { Vector3 as a, MeshPhysicalMaterial as c, InstancedMesh as d, - Clock as e, + Timer as e, AmbientLight as f, SphereGeometry as g, ShaderChunk as h, @@ -186,6 +186,7 @@ class x { if (this.#n) return; const animate = () => { this.#l = requestAnimationFrame(animate); + this.#c.update(); this.#h.delta = this.#c.getDelta(); this.#h.elapsed += this.#h.delta; this.onBeforeRender(this.#h); @@ -193,14 +194,13 @@ class x { this.onAfterRender(this.#h); }; this.#n = true; - this.#c.start(); + this.#c.reset(); animate(); } #z() { if (this.#n) { cancelAnimationFrame(this.#l); this.#n = false; - this.#c.stop(); } } #i() { @@ -224,6 +224,7 @@ class x { dispose() { this.#y(); this.#z(); + this.#c.dispose(); this.clear(); this.#t?.dispose(); this.renderer.dispose(); diff --git a/src/tailwind/Backgrounds/Ballpit/Ballpit.jsx b/src/tailwind/Backgrounds/Ballpit/Ballpit.jsx index ae20a33df..4265168a2 100644 --- a/src/tailwind/Backgrounds/Ballpit/Ballpit.jsx +++ b/src/tailwind/Backgrounds/Ballpit/Ballpit.jsx @@ -3,7 +3,7 @@ import { Vector3 as a, MeshPhysicalMaterial as c, InstancedMesh as d, - Clock as e, + Timer as e, AmbientLight as f, SphereGeometry as g, ShaderChunk as h, @@ -186,6 +186,7 @@ class x { if (this.#n) return; const animate = () => { this.#l = requestAnimationFrame(animate); + this.#c.update(); this.#h.delta = this.#c.getDelta(); this.#h.elapsed += this.#h.delta; this.onBeforeRender(this.#h); @@ -193,14 +194,13 @@ class x { this.onAfterRender(this.#h); }; this.#n = true; - this.#c.start(); + this.#c.reset(); animate(); } #z() { if (this.#n) { cancelAnimationFrame(this.#l); this.#n = false; - this.#c.stop(); } } #i() { @@ -224,6 +224,7 @@ class x { dispose() { this.#y(); this.#z(); + this.#c.dispose(); this.clear(); this.#t?.dispose(); this.renderer.dispose(); diff --git a/src/ts-default/Backgrounds/Ballpit/Ballpit.tsx b/src/ts-default/Backgrounds/Ballpit/Ballpit.tsx index 1ca981476..2b30c0c96 100644 --- a/src/ts-default/Backgrounds/Ballpit/Ballpit.tsx +++ b/src/ts-default/Backgrounds/Ballpit/Ballpit.tsx @@ -4,7 +4,6 @@ import React, { useEffect, useRef } from 'react'; import { ACESFilmicToneMapping, AmbientLight, - Clock, Color, InstancedMesh, MathUtils, @@ -19,6 +18,7 @@ import { ShaderChunk, SphereGeometry, SRGBColorSpace, + Timer, Vector2, Vector3, WebGLRenderer, @@ -51,7 +51,7 @@ class X { #intersectionObserver?: IntersectionObserver; #resizeTimer?: number; #animationFrameId: number = 0; - #clock: Clock = new Clock(); + #timer: Timer = new Timer(); #animationState = { elapsed: 0, delta: 0 }; #isAnimating: boolean = false; #isVisible: boolean = false; @@ -232,14 +232,15 @@ class X { if (this.#isVisible) return; const animateFrame = () => { this.#animationFrameId = requestAnimationFrame(animateFrame); - this.#animationState.delta = this.#clock.getDelta(); + this.#timer.update(); + this.#animationState.delta = this.#timer.getDelta(); this.#animationState.elapsed += this.#animationState.delta; this.onBeforeRender(this.#animationState); this.render(); this.onAfterRender(this.#animationState); }; this.#isVisible = true; - this.#clock.start(); + this.#timer.reset(); animateFrame(); } @@ -247,7 +248,6 @@ class X { if (this.#isVisible) { cancelAnimationFrame(this.#animationFrameId); this.#isVisible = false; - this.#clock.stop(); } } @@ -274,6 +274,7 @@ class X { dispose() { this.#onResizeCleanup(); this.#stopAnimation(); + this.#timer.dispose(); this.clear(); this.#postprocessing?.dispose(); this.renderer.dispose(); diff --git a/src/ts-tailwind/Backgrounds/Ballpit/Ballpit.tsx b/src/ts-tailwind/Backgrounds/Ballpit/Ballpit.tsx index dba1f670b..820ee15fa 100644 --- a/src/ts-tailwind/Backgrounds/Ballpit/Ballpit.tsx +++ b/src/ts-tailwind/Backgrounds/Ballpit/Ballpit.tsx @@ -4,7 +4,6 @@ import React, { useEffect, useRef } from 'react'; import { ACESFilmicToneMapping, AmbientLight, - Clock, Color, InstancedMesh, MathUtils, @@ -19,6 +18,7 @@ import { ShaderChunk, SphereGeometry, SRGBColorSpace, + Timer, Vector2, Vector3, WebGLRenderer, @@ -51,7 +51,7 @@ class X { #intersectionObserver?: IntersectionObserver; #resizeTimer?: number; #animationFrameId: number = 0; - #clock: Clock = new Clock(); + #timer: Timer = new Timer(); #animationState = { elapsed: 0, delta: 0 }; #isAnimating: boolean = false; #isVisible: boolean = false; @@ -232,14 +232,15 @@ class X { if (this.#isVisible) return; const animateFrame = () => { this.#animationFrameId = requestAnimationFrame(animateFrame); - this.#animationState.delta = this.#clock.getDelta(); + this.#timer.update(); + this.#animationState.delta = this.#timer.getDelta(); this.#animationState.elapsed += this.#animationState.delta; this.onBeforeRender(this.#animationState); this.render(); this.onAfterRender(this.#animationState); }; this.#isVisible = true; - this.#clock.start(); + this.#timer.reset(); animateFrame(); } @@ -247,7 +248,6 @@ class X { if (this.#isVisible) { cancelAnimationFrame(this.#animationFrameId); this.#isVisible = false; - this.#clock.stop(); } } @@ -274,6 +274,7 @@ class X { dispose() { this.#onResizeCleanup(); this.#stopAnimation(); + this.#timer.dispose(); this.clear(); this.#postprocessing?.dispose(); this.renderer.dispose();