a-farme-球に文字test35
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - VR対応 詳細コメント版</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') ---
// このコンポーネントは、カメラ(視点)の向きを基準とした移動(前後左右)を処理します。
// キー{ード入力とVRコントローラーのサムスティック入力に対応し、慣性やブレーキも考慮します。
AFRAME.registerComponent('camera-relative-controls', {
// コンポーネントが受け付けるパラメータとそのデフォルト値を定義
schema: {
targetSpeed: { type: 'number', default: 5 }, // 目標とする最高速度 (m/s)
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 }; // VRコントローラーのサムスティックの入力値 (x:左右, y:前後)
// Three.jsのベクトルオブジェクトを事前に生成 (パフォー}ンス向上のため、毎フレーム生成するのを避ける)
this.currentVelocity = new THREE.Vector3(); // プレイヤーの現在の速度ベクトル
this.ZERO_VECTOR = new THREE.Vector3(0, 0, 0); // 比較やリセット用のゼロベクトル
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]'); // このコンポーネントがアタッチされたエンティティ(rig)の子要素からカメラを取得
this.isReady = false; // カメラやシーンの準備が完了したかを示すフラグ
if (!this.cameraEl) {
console.error('camera-relative-controls: カメラエンティティが見つかりません。rigの子要素にcameraコンポーネントを持つエンティティを配置してください。');
}
// --- VRコントローラーのセットアップ ---
// シーンの 'loaded' イベントを待ってからコントローラーを取得・設定 (DOMの準備完了を待つため)
this.el.sceneEl.addEventListener('loaded', () => {
this.leftHand = document.getElementById('leftHand'); // 左手コントローラーのエンティティを取得
if (this.leftHand) {
// 左手スティックの 'thumbstickmoved' イベントに関数を紐付け
this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} else {
console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。スティック移動は無効になります。");
}
});
// --- キー{ードイベントリスナーのセットアップ ---
// thisを束縛して、イベントリスナー内でコンポーネントのプロパティにアクセスできるようにする
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 {
// bindされた関数を解除するには、bind後の関数を保存しておく必要があるが、ここでは簡易的に試行
this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} catch(e) {
console.warn("camera-relative-controls: スティックイベントリスナーの解除に失敗した可能性があります。");
}
}
},
// 左手コントローラーのサムスティックが動いた時に呼び出される関数
onThumbstickMoved: function (evt) {
// スティックのX軸(左右)とY軸(前後)の入力値 (-1.0 から 1.0 の範囲) を保存
this.thumbstickInput.x = evt.detail.x;
this.thumbstickInput.y = evt.detail.y;
},
// 毎フレーム呼び出される関数 (移動計算の主要ロジック)
tick: function (time, timeDelta) {
if (!this.data.enabled) return; // enabledがfalseなら何もしない
// カメラなどの準備が完了するまで待機 (主に初回フレーム用)
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; // 準備ができていなければ、このフレームの処理は中断
}
}
// isReadyになった後も、万が一カメラがなければ処理中断
if (!this.cameraEl || !this.cameraEl.object3D) { return; }
const el = this.el; // このコンポーネントがアタッチされているエンティティ (通常はrig)
const data = this.data; // schemaで定義されたパラメータ (targetSpeedなど)
const position = el.object3D.position; // rigの現在のワールド座標
const dt = timeDelta / 1000; // 前フレームからの経過時間 (ミリ秒を秒に変換)
// --- カメラの現在の向きから、ワールド座標系での前方ベクトルと右方向ベクトルを計算 ---
this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); // カメラのワールド回転を取得
// 前方ベクトル: カメラのローカル座標系での前方(-Z)をワールド回転で変換
this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion);
if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); // 正規化 (長さを1に)
// 右方向ベクトル: カメラのローカル座標系での右(+X)をワールド回転で変換
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); } // 右平行移動
// VRサムスティック入力の処理 (Y軸は上下反転していることが多いため、-1を掛ける)
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(); // 移動方向ベクトルを正規化 (斜め移動が速くなりすぎないように)
}
// --- 目標速度 (desiredVelocity) と補間係数 (lerpFactor) の決定 ---
// この部分で慣性(加速・減速)とブレーキの挙動を制御
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; // 自然減速用の係数を使用
}
// --- 目標速度と補間係数決定ここまで ---
// --- 現在の速度 (currentVelocity) を目標速度 (desiredVelocity) に向けて滑らかに補間 (Lerp) ---
// dt (フレーム時間) を考慮して、フレームレートに依存しにくい補間係数を計算
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);
}
// 速度があれば、その速度と経過時間に基づいてrigの位置を移動
if (this.currentVelocity.lengthSq() > 0) {
const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt);
position.add(deltaPosition);
}
// --- 位置更新ここまで ---
},
// キー{ードのキーが押された時に呼び出される関数
onKeyDown: function (event) {
if (!this.data.enabled) { return; } // コンポーネントが無効なら何もしない
// WASDまたは矢印キーが押されたら、keysオブジェクトに記録
if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) {
this.keys[event.code] = true;
}
},
// キー{ードのキーが離された時に呼び出される関数
onKeyUp: function (event) {
// keysオブジェクトから該当キーの記録を削除
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 () {
// ランダムな回転軸を生成 (x, y, z 成分が -0.5 から 0.5 のベクトルを作り正規化)
this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize();
// 回転速度をランダムに決定 (0.2*maxSpeed 〜 maxSpeed の範囲で、回転方向もランダム)
const speedDegPerSec = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1);
this.speedRadPerSec = THREE.MathUtils.degToRad(speedDegPerSec); // ラジアン/秒に変換
this.deltaRotationQuaternion = new THREE.Quaternion(); // フレーム毎の回転量を格納するクォータニオン
},
// フレーム毎の処理
tick: function (time, timeDelta) {
const dt = timeDelta / 1000; // 経過時間 (秒)
const angleChangeRad = this.speedRadPerSec * dt; // このフレームでの回転角度(ラジアン)
// 回転軸と角度から、このフレームでの回転を表すクォータニオンを計算
this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angleChangeRad);
// エンティティの現在の回転に、このフレームの回転を乗算して適用
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;"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" cursor="rayOrigin: entity; fuse: false;" line="color: red; opacity: 1.0; length: 10" >
</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-triangle id="prevButton" class="clickableButton" position="-20.0 0 0.01" rotation="0 0 90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"></a-triangle>
<a-triangle id="nextButton" class="clickableButton" position="20.0 0 0.01" rotation="0 0 -90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"></a-triangle>
<a-circle id="closeButton" class="clickableButton" position="24 11 0.05" radius="1.5" color="red" shader="flat"></a-circle>
</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; // 生成する立方体の数 (変数名はsphereだが実体はcube)
const spread = 2000; // 立方体が広がる範囲
const PAGES = ['index', 'color', 'size']; // 情報パネルのページ種別
const TOTAL_PAGES = PAGES.length; // 総ページ数
// --- Three.jsベクトル (座標や方向の計算に再利用) ---
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3(); // 主にパネル位置計算時の方向ベクトルとして使用
const panelPosition = new THREE.Vector3();
// --- グローバル変数・定数ここまで ---
// --- 立方体(旧球体)の生成ループ ---
for (let i = 0; i < numSpheres; i++) {
const cubeEl = document.createElement('a-box'); // 立方体を生成
const side = Math.random() * 10.0 + 0.5; // 辺の長さ (0.5〜10.5のランダム)
// XYZ座標をランダムに決定
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + side / 2; // Y座標 (地面より少し上)
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 50%, 75%)`; // パステル調のランダムな色
const sphereIndex = i + 1; // 通し番号 (変数名はsphereだが実体はcube)
// 立方体の各属性を設定
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}); // 最大回転速度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]; // 現在のページタイプ (index, color, size)
// ページタイプに応じて表示テキストを組み立て
if (pageType === 'index') { displayText = `立方体: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${color}`; }
else if (pageType === 'size') { displayText = `サイズ: ${size}`; }
const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; // (1/3)のようなページ表示
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'); // VRモードでは}ウスカーソルを隠す
});
sceneEl.addEventListener('exit-vr', function () {
console.log("Exited VR mode");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); // VRモードを抜けたら}ウスカーソルを表示
});
// --- }ウスカーソル制御ここまで ---
// --- 右手コントローラーのトリガーイベントをリッスン (デバッグ用) ---
// シーンが完全に読み込まれてからコントローラー要素を取得しリスナーを設定
sceneEl.addEventListener('loaded', function() {
rightHandEl = document.getElementById('rightHand'); // ここで取得
if (rightHandEl) {
rightHandEl.addEventListener('triggerdown', function (evt) {
console.log('Right hand TRIGGER DOWN event!', evt);
// Raycasterが何かと交差しているか確認
const raycasterComponent = rightHandEl.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');
// 必要であれば、ここで手動でクリックイベントを発行することもできます:
// intersectedEls[0].dispatchEvent(new CustomEvent('click', {bubbles: true}));
} else {
console.log('Trigger pressed, but no intersection.');
}
}
});
rightHandEl.addEventListener('triggerup', function (evt) {
console.log('Right hand TRIGGER UP event!', evt);
});
} else {
console.error("Could not find rightHand element to attach trigger listener.");
}
});
// --- 右手トリガーイベントここまで ---
// 背景クリックリスナーは以前の修正で削除済み
</script>
</a-scene>
</body>
</html>
使用変数
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - VR対応 詳細コメント版</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') ---
// このコンポーネントは、カメラ(視点)の向きを基準とした移動(前後左右)を処理します。
// キー{ード入力とVRコントローラーのサムスティック入力に対応し、慣性やブレーキも考慮します。
AFRAME.registerComponent('camera-relative-controls', {
// コンポーネントが受け付けるパラメータとそのデフォルト値を定義
schema: {
targetSpeed: { type: 'number', default: 5 }, // 目標とする最高速度 (m/s)
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 }; // VRコントローラーのサムスティックの入力値 (x:左右, y:前後)
// Three.jsのベクトルオブジェクトを事前に生成 (パフォー}ンス向上のため、毎フレーム生成するのを避ける)
this.currentVelocity = new THREE.Vector3(); // プレイヤーの現在の速度ベクトル
this.ZERO_VECTOR = new THREE.Vector3(0, 0, 0); // 比較やリセット用のゼロベクトル
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]'); // このコンポーネントがアタッチされたエンティティ(rig)の子要素からカメラを取得
this.isReady = false; // カメラやシーンの準備が完了したかを示すフラグ
if (!this.cameraEl) {
console.error('camera-relative-controls: カメラエンティティが見つかりません。rigの子要素にcameraコンポーネントを持つエンティティを配置してください。');
}
// --- VRコントローラーのセットアップ ---
// シーンの 'loaded' イベントを待ってからコントローラーを取得・設定 (DOMの準備完了を待つため)
this.el.sceneEl.addEventListener('loaded', () => {
this.leftHand = document.getElementById('leftHand'); // 左手コントローラーのエンティティを取得
if (this.leftHand) {
// 左手スティックの 'thumbstickmoved' イベントに関数を紐付け
this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} else {
console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。スティック移動は無効になります。");
}
});
// --- キー{ードイベントリスナーのセットアップ ---
// thisを束縛して、イベントリスナー内でコンポーネントのプロパティにアクセスできるようにする
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 {
// bindされた関数を解除するには、bind後の関数を保存しておく必要があるが、ここでは簡易的に試行
this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} catch(e) {
console.warn("camera-relative-controls: スティックイベントリスナーの解除に失敗した可能性があります。");
}
}
},
// 左手コントローラーのサムスティックが動いた時に呼び出される関数
onThumbstickMoved: function (evt) {
// スティックのX軸(左右)とY軸(前後)の入力値 (-1.0 から 1.0 の範囲) を保存
this.thumbstickInput.x = evt.detail.x;
this.thumbstickInput.y = evt.detail.y;
},
// 毎フレーム呼び出される関数 (移動計算の主要ロジック)
tick: function (time, timeDelta) {
if (!this.data.enabled) return; // enabledがfalseなら何もしない
// カメラなどの準備が完了するまで待機 (主に初回フレーム用)
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; // 準備ができていなければ、このフレームの処理は中断
}
}
// isReadyになった後も、万が一カメラがなければ処理中断
if (!this.cameraEl || !this.cameraEl.object3D) { return; }
const el = this.el; // このコンポーネントがアタッチされているエンティティ (通常はrig)
const data = this.data; // schemaで定義されたパラメータ (targetSpeedなど)
const position = el.object3D.position; // rigの現在のワールド座標
const dt = timeDelta / 1000; // 前フレームからの経過時間 (ミリ秒を秒に変換)
// --- カメラの現在の向きから、ワールド座標系での前方ベクトルと右方向ベクトルを計算 ---
this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); // カメラのワールド回転を取得
// 前方ベクトル: カメラのローカル座標系での前方(-Z)をワールド回転で変換
this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion);
if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); // 正規化 (長さを1に)
// 右方向ベクトル: カメラのローカル座標系での右(+X)をワールド回転で変換
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); } // 右平行移動
// VRサムスティック入力の処理 (Y軸は上下反転していることが多いため、-1を掛ける)
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(); // 移動方向ベクトルを正規化 (斜め移動が速くなりすぎないように)
}
// --- 目標速度 (desiredVelocity) と補間係数 (lerpFactor) の決定 ---
// この部分で慣性(加速・減速)とブレーキの挙動を制御
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; // 自然減速用の係数を使用
}
// --- 目標速度と補間係数決定ここまで ---
// --- 現在の速度 (currentVelocity) を目標速度 (desiredVelocity) に向けて滑らかに補間 (Lerp) ---
// dt (フレーム時間) を考慮して、フレームレートに依存しにくい補間係数を計算
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);
}
// 速度があれば、その速度と経過時間に基づいてrigの位置を移動
if (this.currentVelocity.lengthSq() > 0) {
const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt);
position.add(deltaPosition);
}
// --- 位置更新ここまで ---
},
// キー{ードのキーが押された時に呼び出される関数
onKeyDown: function (event) {
if (!this.data.enabled) { return; } // コンポーネントが無効なら何もしない
// WASDまたは矢印キーが押されたら、keysオブジェクトに記録
if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) {
this.keys[event.code] = true;
}
},
// キー{ードのキーが離された時に呼び出される関数
onKeyUp: function (event) {
// keysオブジェクトから該当キーの記録を削除
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 () {
// ランダムな回転軸を生成 (x, y, z 成分が -0.5 から 0.5 のベクトルを作り正規化)
this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize();
// 回転速度をランダムに決定 (0.2*maxSpeed 〜 maxSpeed の範囲で、回転方向もランダム)
const speedDegPerSec = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1);
this.speedRadPerSec = THREE.MathUtils.degToRad(speedDegPerSec); // ラジアン/秒に変換
this.deltaRotationQuaternion = new THREE.Quaternion(); // フレーム毎の回転量を格納するクォータニオン
},
// フレーム毎の処理
tick: function (time, timeDelta) {
const dt = timeDelta / 1000; // 経過時間 (秒)
const angleChangeRad = this.speedRadPerSec * dt; // このフレームでの回転角度(ラジアン)
// 回転軸と角度から、このフレームでの回転を表すクォータニオンを計算
this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angleChangeRad);
// エンティティの現在の回転に、このフレームの回転を乗算して適用
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;"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" cursor="rayOrigin: entity; fuse: false;" line="color: red; opacity: 1.0; length: 10" >
</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-triangle id="prevButton" class="clickableButton" position="-20.0 0 0.01" rotation="0 0 90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"></a-triangle>
<a-triangle id="nextButton" class="clickableButton" position="20.0 0 0.01" rotation="0 0 -90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"></a-triangle>
<a-circle id="closeButton" class="clickableButton" position="24 11 0.05" radius="1.5" color="red" shader="flat"></a-circle>
</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; // 生成する立方体の数 (変数名はsphereだが実体はcube)
const spread = 2000; // 立方体が広がる範囲
const PAGES = ['index', 'color', 'size']; // 情報パネルのページ種別
const TOTAL_PAGES = PAGES.length; // 総ページ数
// --- Three.jsベクトル (座標や方向の計算に再利用) ---
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3(); // 主にパネル位置計算時の方向ベクトルとして使用
const panelPosition = new THREE.Vector3();
// --- グローバル変数・定数ここまで ---
// --- 立方体(旧球体)の生成ループ ---
for (let i = 0; i < numSpheres; i++) {
const cubeEl = document.createElement('a-box'); // 立方体を生成
const side = Math.random() * 10.0 + 0.5; // 辺の長さ (0.5〜10.5のランダム)
// XYZ座標をランダムに決定
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + side / 2; // Y座標 (地面より少し上)
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 50%, 75%)`; // パステル調のランダムな色
const sphereIndex = i + 1; // 通し番号 (変数名はsphereだが実体はcube)
// 立方体の各属性を設定
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}); // 最大回転速度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]; // 現在のページタイプ (index, color, size)
// ページタイプに応じて表示テキストを組み立て
if (pageType === 'index') { displayText = `立方体: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${color}`; }
else if (pageType === 'size') { displayText = `サイズ: ${size}`; }
const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; // (1/3)のようなページ表示
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'); // VRモードでは}ウスカーソルを隠す
});
sceneEl.addEventListener('exit-vr', function () {
console.log("Exited VR mode");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); // VRモードを抜けたら}ウスカーソルを表示
});
// --- }ウスカーソル制御ここまで ---
// --- 右手コントローラーのトリガーイベントをリッスン (デバッグ用) ---
// シーンが完全に読み込まれてからコントローラー要素を取得しリスナーを設定
sceneEl.addEventListener('loaded', function() {
rightHandEl = document.getElementById('rightHand'); // ここで取得
if (rightHandEl) {
rightHandEl.addEventListener('triggerdown', function (evt) {
console.log('Right hand TRIGGER DOWN event!', evt);
// Raycasterが何かと交差しているか確認
const raycasterComponent = rightHandEl.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');
// 必要であれば、ここで手動でクリックイベントを発行することもできます:
// intersectedEls[0].dispatchEvent(new CustomEvent('click', {bubbles: true}));
} else {
console.log('Trigger pressed, but no intersection.');
}
}
});
rightHandEl.addEventListener('triggerup', function (evt) {
console.log('Right hand TRIGGER UP event!', evt);
});
} else {
console.error("Could not find rightHand element to attach trigger listener.");
}
});
// --- 右手トリガーイベントここまで ---
// 背景クリックリスナーは以前の修正で削除済み
</script>
</a-scene>
</body>
</html>
使用変数
-------( Function ) | |
) { rightHandEl = document.getElementById -------( Function ) | |
ameraWorldPosition | |
angleChangeRad | |
argetWorldPosition | |
at | |
attribute | |
axis | |
background | |
begin | |
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 | |
line | |
material | |
mouseCursorEl | |
moveDirection | |
nextButtonEl | |
numSpheres | |
offsetDistance | |
onKeyDown | |
onKeyUp | |
opacity | |
pageIndex | |
pageIndicator | |
PAGES | |
pageType | |
panelPosition | |
panelTextEl | |
position | |
prevButtonEl | |
radius | |
raycaster | |
raycasterComponent | |
rightHandEl | |
rotation | |
RotationQuaternion | |
scale | |
sceneEl | |
shader | |
side | |
size | |
speedDegPerSec | |
speedRadPerSec | |
sphereIndex | |
spread | |
src | |
text | |
thumbstickInput | |
to | |
TOTAL_PAGES | |
ui | |
updatePanelDisplay -------( Function ) | |
visible | |
width | |
x | |
y | |
z | |
ZERO_VECTOR | |
パネルの位置 |