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); } }