Spaces:
Running
Running
| import * as THREE from 'three'; | |
| const RING_SEGMENTS = 48; | |
| const Z = 1.5; | |
| function createReusableLine(color, opacity) { | |
| const positions = new Float32Array(6); | |
| const geom = new THREE.BufferGeometry(); | |
| geom.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity, depthTest: false, linewidth: 1 }); | |
| const line = new THREE.Line(geom, mat); | |
| line.visible = false; | |
| return line; | |
| } | |
| function createReusableRing(segments, color, opacity) { | |
| const positions = new Float32Array((segments + 1) * 3); | |
| const geom = new THREE.BufferGeometry(); | |
| geom.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity, depthTest: false }); | |
| const line = new THREE.Line(geom, mat); | |
| line.visible = false; | |
| return line; | |
| } | |
| // Pentagon is drawn as a single LineLoop-style line with 6 points (closed) | |
| function createReusablePentagon(color, opacity) { | |
| const positions = new Float32Array(18); // 6 points × 3 | |
| const geom = new THREE.BufferGeometry(); | |
| geom.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity, depthTest: false }); | |
| const line = new THREE.Line(geom, mat); | |
| line.visible = false; | |
| return line; | |
| } | |
| // Pentagram star: 6 points (0,2,4,1,3,0) | |
| function createReusableStar(color, opacity) { | |
| const positions = new Float32Array(18); // 6 points × 3 | |
| const geom = new THREE.BufferGeometry(); | |
| geom.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity, depthTest: false }); | |
| const line = new THREE.Line(geom, mat); | |
| line.visible = false; | |
| return line; | |
| } | |
| export class MandalaVisualizer { | |
| constructor(scene) { | |
| this.scene = scene; | |
| this.group = new THREE.Group(); | |
| this.group.renderOrder = 2; | |
| this.scene.add(this.group); | |
| this.clock = new THREE.Clock(); | |
| this.fingertips = [4, 8, 12, 16, 20]; | |
| this.palmIndex = 9; | |
| // Pre-allocate all geometry for 2 hands | |
| this.hands = []; | |
| for (let h = 0; h < 2; h++) { | |
| const hand = { | |
| pentagon: createReusablePentagon(0x00ffff, 0.7), | |
| star: createReusableStar(0xff00ff, 0.6), | |
| rays: [], | |
| rings: [], | |
| }; | |
| for (let i = 0; i < 5; i++) { | |
| hand.rays.push(createReusableLine(0xffaa00, 0.7)); | |
| } | |
| for (let r = 0; r < 3; r++) { | |
| hand.rings.push(createReusableRing(RING_SEGMENTS, 0x00ffff, 0.35)); | |
| } | |
| this.group.add(hand.pentagon); | |
| this.group.add(hand.star); | |
| hand.rays.forEach(l => this.group.add(l)); | |
| hand.rings.forEach(l => this.group.add(l)); | |
| this.hands.push(hand); | |
| } | |
| // Inter-hand: 5 bridge lines + 2 cross lines | |
| this.bridges = []; | |
| for (let i = 0; i < 5; i++) { | |
| const line = createReusableLine(0xff00ff, 0.6); | |
| this.bridges.push(line); | |
| this.group.add(line); | |
| } | |
| this.crosses = []; | |
| for (let i = 0; i < 2; i++) { | |
| const line = createReusableLine(0xffaa00, 0.5); | |
| this.crosses.push(line); | |
| this.group.add(line); | |
| } | |
| } | |
| update(handsData, canvasWidth, canvasHeight) { | |
| // Hide everything | |
| for (let h = 0; h < 2; h++) { | |
| const hand = this.hands[h]; | |
| hand.pentagon.visible = false; | |
| hand.star.visible = false; | |
| for (let i = 0; i < 5; i++) hand.rays[i].visible = false; | |
| for (let i = 0; i < 3; i++) hand.rings[i].visible = false; | |
| } | |
| for (let i = 0; i < 5; i++) this.bridges[i].visible = false; | |
| for (let i = 0; i < 2; i++) this.crosses[i].visible = false; | |
| if (!handsData || handsData.length === 0) return; | |
| const time = this.clock.getElapsedTime(); | |
| for (let h = 0; h < handsData.length && h < 2; h++) { | |
| const points3D = handsData[h]; | |
| if (!points3D || points3D.length < 21) continue; | |
| const hand = this.hands[h]; | |
| const tips = this.fingertips.map(idx => points3D[idx]); | |
| const palm = points3D[this.palmIndex]; | |
| const area = this._polygonArea(tips); | |
| const areaFactor = Math.min(1, area / 40000); | |
| const hue = 0.5 - areaFactor * 0.3; | |
| const glowOpacity = 0.5 + areaFactor * 0.3; | |
| // 1. Pentagon | |
| const pentArr = hand.pentagon.geometry.attributes.position.array; | |
| for (let i = 0; i < 5; i++) { | |
| pentArr[i * 3] = tips[i].x; | |
| pentArr[i * 3 + 1] = tips[i].y; | |
| pentArr[i * 3 + 2] = Z; | |
| } | |
| pentArr[15] = tips[0].x; | |
| pentArr[16] = tips[0].y; | |
| pentArr[17] = Z; | |
| hand.pentagon.geometry.attributes.position.needsUpdate = true; | |
| hand.pentagon.material.color.setHSL(hue, 1.0, 0.5); | |
| hand.pentagon.material.opacity = glowOpacity; | |
| hand.pentagon.visible = true; | |
| // 2. Pentagram star (order: 0,2,4,1,3,0) | |
| const starOrder = [0, 2, 4, 1, 3, 0]; | |
| const starArr = hand.star.geometry.attributes.position.array; | |
| for (let i = 0; i < 6; i++) { | |
| const idx = starOrder[i]; | |
| starArr[i * 3] = tips[idx].x; | |
| starArr[i * 3 + 1] = tips[idx].y; | |
| starArr[i * 3 + 2] = Z; | |
| } | |
| hand.star.geometry.attributes.position.needsUpdate = true; | |
| hand.star.material.opacity = glowOpacity * 0.8; | |
| hand.star.visible = true; | |
| // 3. Palm-to-tip rays | |
| for (let i = 0; i < 5; i++) { | |
| const arr = hand.rays[i].geometry.attributes.position.array; | |
| arr[0] = palm.x; arr[1] = palm.y; arr[2] = Z; | |
| arr[3] = tips[i].x; arr[4] = tips[i].y; arr[5] = Z; | |
| hand.rays[i].geometry.attributes.position.needsUpdate = true; | |
| hand.rays[i].visible = true; | |
| } | |
| // 4. Concentric rings | |
| let avgDist = 0; | |
| for (let i = 0; i < 5; i++) { | |
| const dx = tips[i].x - palm.x; | |
| const dy = tips[i].y - palm.y; | |
| avgDist += Math.sqrt(dx * dx + dy * dy); | |
| } | |
| avgDist /= 5; | |
| const ringRadii = [avgDist * 0.4, avgDist * 0.7, avgDist * 1.0]; | |
| for (let r = 0; r < 3; r++) { | |
| const radius = ringRadii[r]; | |
| const arr = hand.rings[r].geometry.attributes.position.array; | |
| for (let s = 0; s <= RING_SEGMENTS; s++) { | |
| const angle = (s / RING_SEGMENTS) * Math.PI * 2 + time * (0.5 + r * 0.3); | |
| arr[s * 3] = palm.x + Math.cos(angle) * radius; | |
| arr[s * 3 + 1] = palm.y + Math.sin(angle) * radius; | |
| arr[s * 3 + 2] = Z; | |
| } | |
| hand.rings[r].geometry.attributes.position.needsUpdate = true; | |
| hand.rings[r].material.color.setHSL((hue + r * 0.15) % 1, 1.0, 0.5); | |
| hand.rings[r].visible = true; | |
| } | |
| } | |
| // 5 & 6. Inter-hand | |
| if (handsData.length >= 2 && handsData[0] && handsData[1] && handsData[0].length >= 21 && handsData[1].length >= 21) { | |
| const tips1 = this.fingertips.map(idx => handsData[0][idx]); | |
| const tips2 = this.fingertips.map(idx => handsData[1][idx]); | |
| for (let i = 0; i < 5; i++) { | |
| const arr = this.bridges[i].geometry.attributes.position.array; | |
| arr[0] = tips1[i].x; arr[1] = tips1[i].y; arr[2] = Z; | |
| arr[3] = tips2[i].x; arr[4] = tips2[i].y; arr[5] = Z; | |
| this.bridges[i].geometry.attributes.position.needsUpdate = true; | |
| this.bridges[i].visible = true; | |
| } | |
| const crossPairs = [[tips1[0], tips2[4]], [tips1[4], tips2[0]]]; | |
| for (let c = 0; c < 2; c++) { | |
| const arr = this.crosses[c].geometry.attributes.position.array; | |
| arr[0] = crossPairs[c][0].x; arr[1] = crossPairs[c][0].y; arr[2] = Z; | |
| arr[3] = crossPairs[c][1].x; arr[4] = crossPairs[c][1].y; arr[5] = Z; | |
| this.crosses[c].geometry.attributes.position.needsUpdate = true; | |
| this.crosses[c].visible = true; | |
| } | |
| } | |
| } | |
| _polygonArea(points) { | |
| let area = 0; | |
| const n = points.length; | |
| for (let i = 0; i < n; i++) { | |
| const j = (i + 1) % n; | |
| area += points[i].x * points[j].y; | |
| area -= points[j].x * points[i].y; | |
| } | |
| return Math.abs(area) / 2; | |
| } | |
| dispose() { | |
| // Dispose all pre-allocated geometry and materials | |
| for (const hand of this.hands) { | |
| hand.pentagon.geometry.dispose(); | |
| hand.pentagon.material.dispose(); | |
| hand.star.geometry.dispose(); | |
| hand.star.material.dispose(); | |
| for (const ray of hand.rays) { | |
| ray.geometry.dispose(); | |
| ray.material.dispose(); | |
| } | |
| for (const ring of hand.rings) { | |
| ring.geometry.dispose(); | |
| ring.material.dispose(); | |
| } | |
| } | |
| for (const bridge of this.bridges) { | |
| bridge.geometry.dispose(); | |
| bridge.material.dispose(); | |
| } | |
| for (const cross of this.crosses) { | |
| cross.geometry.dispose(); | |
| cross.material.dispose(); | |
| } | |
| this.scene.remove(this.group); | |
| } | |
| } | |