a-farme-球に文字test37
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 3D{タン</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- プレイヤー移動制御用カスタムコンポーネント ('camera-relative-controls') ---
AFRAME.registerComponent('camera-relative-controls', {
schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true } },
init: function () { this.keys = {}; this.thumbstickInput = { x: 0, y: 0 }; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.cameraDirection = new THREE.Vector3(); this.cameraRight = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); } this.el.sceneEl.addEventListener('loaded', () => { this.leftHand = document.getElementById('leftHand'); if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); },
remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } catch(e) { console.warn("camera-relative-controls: スティックリスナー解除失敗の可能性あり"); } } },
onThumbstickMoved: function (evt) { this.thumbstickInput.x = evt.detail.x; this.thumbstickInput.y = evt.detail.y; },
tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); } if (Math.abs(this.thumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.thumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x); this.moveDirection.add(leftRight); } const isInputActive = this.moveDirection.lengthSq() > 0.0001; if (isInputActive) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; if (isInputActive) { let isOpposingInput = false; if (isCurrentlyMoving) { const dotProduct = this.currentVelocity.dot(this.moveDirection); if (dotProduct < -0.1) { isOpposingInput = true; } } if (isOpposingInput) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } },
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// --- 立方体をランダムに回転させるカスタムコンポーネント ('random-rotate') ---
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); }
});
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;" raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" laser-controls="hand: left; model: false; lineColor: white; lineOpacity: 0.75" ></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;" ></a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-cone id="prevButton" class="clickableButton"
position="-20.0 0 0.1" rotation="0 0 90"
radius-bottom="1.2"
radius-top="0"
height="2.0"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-cone>
<a-cone id="nextButton" class="clickableButton"
position="20.0 0 0.1" rotation="0 0 -90"
radius-bottom="1.2"
radius-top="0"
height="2.0"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-cone>
<a-sphere id="closeButton" class="clickableButton"
position="24 11 0.1" radius="1.5" color="red" shader="flat">
</a-sphere>
</a-entity>
<script>
// --- グローバル変数とDOM要素の取得 ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
let rightHandEl = null;
// --- 定数定義 ---
const numSpheres = 60; const spread = 2000; const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3(); const PAGES = ['index', 'color', 'size']; const TOTAL_PAGES = PAGES.length;
// --- 立方体生成ループ ---
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// --- 情報パネルの表示更新関数 ---
function updatePanelDisplay() { if (!infoPanelEl.dataset.sphereIndex) return; const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10); const color = infoPanelEl.dataset.color || 'N/A'; const size = infoPanelEl.dataset.size || 'N/A'; const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let displayText = ''; const pageType = PAGES[pageIndex]; if (pageType === 'index') { displayText = `立方体: ${index}`; } else if (pageType === 'color') { displayText = `色: ${color}`; } else if (pageType === 'size') { displayText = `サイズ: ${size}`; } const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', 'value', finalDisplayText); }
// --- 立方体クリック時の処理 ---
function handleSphereClick(event) { event.stopPropagation(); console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName); const clickedCube = event.target; if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) { console.error("Cube data missing from dataset!", clickedCube.dataset); return; } console.log("Cube data found:", clickedCube.dataset); infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex; infoPanelEl.dataset.color = clickedCube.dataset.color; infoPanelEl.dataset.size = clickedCube.dataset.size; infoPanelEl.dataset.currentPageIndex = '0'; console.log("Data stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedCube.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const cubeSide = parseFloat(clickedCube.dataset.size || 0); const offsetDistance = cubeSide / 2 + 0.5; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); } catch(e) { console.error("Error during position calculation:", e); return; } infoPanelEl.setAttribute('visible', true); console.log("Panel visibility set to true. --- handleSphereClick end ---"); }
// --- パネルの{タンクリック処理 ---
prevButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex + 1) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex); });
closeButtonEl.addEventListener('click', function (event) { event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex; console.log("Close button clicked, panel hidden."); });
// --- VRモードによる}ウスカーソル表示制御 ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
// --- 右手コントローラーのトリガーイベントをリッスン (デバッグ用) ---
sceneEl.addEventListener('loaded', function() {
rightHandEl = document.getElementById('rightHand'); // This was for right hand, now interaction is left.
// We don't strictly need trigger logging if laser-controls works.
// Let's get leftHandEl for consistency if needed.
const leftHandForTriggerLog = document.getElementById('leftHand');
if (leftHandForTriggerLog) {
leftHandForTriggerLog.addEventListener('triggerdown', function (evt) {
console.log('Left hand TRIGGER DOWN event!', evt);
const raycasterComponent = leftHandForTriggerLog.components.raycaster;
if (raycasterComponent) {
const intersectedEls = raycasterComponent.intersectedEls;
if (intersectedEls.length > 0) {
console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id');
} else {
console.log('Left Trigger pressed, but no intersection.');
}
}
});
leftHandForTriggerLog.addEventListener('triggerup', function (evt) {
console.log('Left hand TRIGGER UP event!', evt);
});
} else {
console.error("Could not find leftHand element to attach trigger listener.");
}
});
</script>
</a-scene>
</body>
</html>
使用変数
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 3D{タン</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- プレイヤー移動制御用カスタムコンポーネント ('camera-relative-controls') ---
AFRAME.registerComponent('camera-relative-controls', {
schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true } },
init: function () { this.keys = {}; this.thumbstickInput = { x: 0, y: 0 }; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.cameraDirection = new THREE.Vector3(); this.cameraRight = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); } this.el.sceneEl.addEventListener('loaded', () => { this.leftHand = document.getElementById('leftHand'); if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); },
remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } catch(e) { console.warn("camera-relative-controls: スティックリスナー解除失敗の可能性あり"); } } },
onThumbstickMoved: function (evt) { this.thumbstickInput.x = evt.detail.x; this.thumbstickInput.y = evt.detail.y; },
tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); } if (Math.abs(this.thumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.thumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x); this.moveDirection.add(leftRight); } const isInputActive = this.moveDirection.lengthSq() > 0.0001; if (isInputActive) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; if (isInputActive) { let isOpposingInput = false; if (isCurrentlyMoving) { const dotProduct = this.currentVelocity.dot(this.moveDirection); if (dotProduct < -0.1) { isOpposingInput = true; } } if (isOpposingInput) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } },
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// --- 立方体をランダムに回転させるカスタムコンポーネント ('random-rotate') ---
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); }
});
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;" raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" laser-controls="hand: left; model: false; lineColor: white; lineOpacity: 0.75" ></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;" ></a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-cone id="prevButton" class="clickableButton"
position="-20.0 0 0.1" rotation="0 0 90"
radius-bottom="1.2"
radius-top="0"
height="2.0"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-cone>
<a-cone id="nextButton" class="clickableButton"
position="20.0 0 0.1" rotation="0 0 -90"
radius-bottom="1.2"
radius-top="0"
height="2.0"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-cone>
<a-sphere id="closeButton" class="clickableButton"
position="24 11 0.1" radius="1.5" color="red" shader="flat">
</a-sphere>
</a-entity>
<script>
// --- グローバル変数とDOM要素の取得 ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
let rightHandEl = null;
// --- 定数定義 ---
const numSpheres = 60; const spread = 2000; const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3(); const PAGES = ['index', 'color', 'size']; const TOTAL_PAGES = PAGES.length;
// --- 立方体生成ループ ---
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// --- 情報パネルの表示更新関数 ---
function updatePanelDisplay() { if (!infoPanelEl.dataset.sphereIndex) return; const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10); const color = infoPanelEl.dataset.color || 'N/A'; const size = infoPanelEl.dataset.size || 'N/A'; const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let displayText = ''; const pageType = PAGES[pageIndex]; if (pageType === 'index') { displayText = `立方体: ${index}`; } else if (pageType === 'color') { displayText = `色: ${color}`; } else if (pageType === 'size') { displayText = `サイズ: ${size}`; } const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', 'value', finalDisplayText); }
// --- 立方体クリック時の処理 ---
function handleSphereClick(event) { event.stopPropagation(); console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName); const clickedCube = event.target; if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) { console.error("Cube data missing from dataset!", clickedCube.dataset); return; } console.log("Cube data found:", clickedCube.dataset); infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex; infoPanelEl.dataset.color = clickedCube.dataset.color; infoPanelEl.dataset.size = clickedCube.dataset.size; infoPanelEl.dataset.currentPageIndex = '0'; console.log("Data stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedCube.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const cubeSide = parseFloat(clickedCube.dataset.size || 0); const offsetDistance = cubeSide / 2 + 0.5; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); } catch(e) { console.error("Error during position calculation:", e); return; } infoPanelEl.setAttribute('visible', true); console.log("Panel visibility set to true. --- handleSphereClick end ---"); }
// --- パネルの{タンクリック処理 ---
prevButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex + 1) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex); });
closeButtonEl.addEventListener('click', function (event) { event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex; console.log("Close button clicked, panel hidden."); });
// --- VRモードによる}ウスカーソル表示制御 ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
// --- 右手コントローラーのトリガーイベントをリッスン (デバッグ用) ---
sceneEl.addEventListener('loaded', function() {
rightHandEl = document.getElementById('rightHand'); // This was for right hand, now interaction is left.
// We don't strictly need trigger logging if laser-controls works.
// Let's get leftHandEl for consistency if needed.
const leftHandForTriggerLog = document.getElementById('leftHand');
if (leftHandForTriggerLog) {
leftHandForTriggerLog.addEventListener('triggerdown', function (evt) {
console.log('Left hand TRIGGER DOWN event!', evt);
const raycasterComponent = leftHandForTriggerLog.components.raycaster;
if (raycasterComponent) {
const intersectedEls = raycasterComponent.intersectedEls;
if (intersectedEls.length > 0) {
console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id');
} else {
console.log('Left Trigger pressed, but no intersection.');
}
}
});
leftHandForTriggerLog.addEventListener('triggerup', function (evt) {
console.log('Left hand TRIGGER UP event!', evt);
});
} else {
console.error("Could not find leftHand element to attach trigger listener.");
}
});
</script>
</a-scene>
</body>
</html>
使用変数
-------( Function ) | |
) { rightHandEl = document.getElementById -------( Function ) | |
ameraWorldPosition | |
angle | |
argetWorldPosition | |
at | |
attribute | |
axis | |
background | |
begin | |
bottom | |
camera | |
cameraDirection | |
cameraEl | |
cameraRight | |
class | |
clickedCube | |
closeButtonEl | |
color | |
controls | |
cubeEl | |
cubeSide | |
currentPageIndex | |
currentVelocity | |
cursor | |
data | |
deltaPosition | |
desiredVelocity | |
direction | |
displayText | |
dotProduct | |
dt | |
dur | |
easing | |
el | |
eraWorldQuaternion | |
ffectiveLerpFactor | |
fill | |
finalDisplayText | |
forwardBackward | |
from | |
geometry | |
handleSphereClick -------( Function ) | |
height | |
i | |
id | |
index | |
infoPanelEl | |
intersectedEls | |
isCurrentlyMoving | |
isInputActive | |
isOpposingInput | |
isReady | |
keys | |
leftHand | |
leftRight | |
lerpFactor | |
light | |
material | |
mouseCursorEl | |
moveDirection | |
nextButtonEl | |
numSpheres | |
offsetDistance | |
onKeyDown | |
onKeyUp | |
opacity | |
pageIndex | |
pageIndicator | |
PAGES | |
pageType | |
panelPosition | |
panelTextEl | |
position | |
prevButtonEl | |
radius | |
raycaster | |
raycasterComponent | |
rightHandEl | |
rotation | |
RotationQuaternion | |
sceneEl | |
shader | |
side | |
size | |
speed | |
speedRad | |
sphereIndex | |
spread | |
src | |
text | |
tHandForTriggerLog | |
thumbstickInput | |
to | |
top | |
TOTAL_PAGES | |
ui | |
updatePanelDisplay -------( Function ) | |
visible | |
width | |
x | |
y | |
z | |
ZERO_VECTOR |