Junkerposts
 a-farme-球に文字test40 

<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 高機能情報パネル v2</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;"></a-entity>

<a-entity id="rightHand"
oculus-touch-controls="hand: right; model: true;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;"
laser-controls="hand: right; model: false; lineColor: white; lineOpacity: 0.75"
>
</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 leftHandEl = null; // 左手イベント用は一旦削除 (右手メインのため)
let rightHandEl = null; // ★★★ 右手コントローラーの要素を取得するため宣言 ★★★

// --- ★★★ パネルの表示オフセット距離を調整可能に ★★★ ---
const PANEL_OFFSET_FROM_OBJECT = 1.0; // 立方体の表面からパネルまでの距離 (例: 1.0メートル)

// --- 立方体ごとのコメントデータを定義 (コメントは文字列の配列として複数ページ対応) ---
const CUBE_COMMENTS = {
1: ["最初の立方体だ!", "何か秘密が隠されているかもしれない…"],
5: ["これは5番目の特別な立方体。", "長いコメントもこのように複数ページにわたって表示できます。", "素晴らしい発見だ!"],
10: ["ようこそ、宇宙の果てへ。"],
// 必要に応じて他の立方体のコメントを追加
// 例: 20: ["コメントページ1", "コメントページ2"],
};
const DEFAULT_COMMENT_ARRAY = ["コメントはありません"]; // コメント未設定時のデフォルト

// --- 定数定義 ---
const numSpheres = 60;
const spread = 2000;
const PAGES = ['index', 'color', 'size', 'comment']; // 'comment' ページを追加
const TOTAL_MAIN_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; 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';
// コメントはJSON文字列として保存されているのでパースする
const commentsArray = JSON.parse(infoPanelEl.dataset.comments || JSON.stringify(DEFAULT_COMMENT_ARRAY));
const commentSubPageCount = parseInt(infoPanelEl.dataset.commentSubPageCount || '1', 10);
const commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10);

const mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);

let displayText = '';
const pageType = PAGES[mainPageIndex];
let pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`;

if (pageType === 'index') { displayText = `立方体: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${color}`; }
else if (pageType === 'size') { displayText = `サイズ: ${size}`; }
else if (pageType === 'comment') {
displayText = `コメント:\n${commentsArray[commentSubPageIndex]}`;
// コメントページの場合はサブページ番号も表示
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`;
}

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'; // メインページは最初にリセット

// コメントを取得して設定
const sphereIdx = parseInt(clickedCube.dataset.sphereIndex, 10);
const commentsForThisCube = CUBE_COMMENTS[sphereIdx] || DEFAULT_COMMENT_ARRAY;
infoPanelEl.dataset.comments = JSON.stringify(commentsForThisCube); // コメント配列を文字列で保存
infoPanelEl.dataset.commentSubPageCount = commentsForThisCube.length.toString();
infoPanelEl.dataset.commentSubPageIndex = '0'; // コメントサブページもリセット

console.log("Data (including comments) 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 + PANEL_OFFSET_FROM_OBJECT;
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 mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10);
const commentSubPageCount = parseInt(infoPanelEl.dataset.commentSubPageCount || '1', 10);

if (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1) {
// コメントページ内でサブページをめくる
commentSubPageIndex--;
if (commentSubPageIndex < 0) { // コメントの最初のサブページより前に戻ろうとした
mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES;
// 新しいメインページがコメントなら、そのコメントの最後のサブページに
if (PAGES[mainPageIndex] === 'comment') {
// このままだと新しい球のコメントではないので、次の球クリックまでコメントは変わらない
// handleSphereClickで選択された球のコメントがセットされる
// ここでは単純に新しいメインページのコメントの最後のサブページとする
// (実際には新しい球のコメントを再ロードする必要があるが、ここでは単純化)
const newCommentsArray = JSON.parse(infoPanelEl.dataset.comments || JSON.stringify(DEFAULT_COMMENT_ARRAY));
commentSubPageIndex = Math.max(0, newCommentsArray.length - 1);

} else {
commentSubPageIndex = 0; // 他のページならサブページは0
}
}
} else {
// メインページをめくる
mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES;
commentSubPageIndex = 0; // 他のページに移動したらコメントサブページはリセット
// もし新しいメインページがコメントなら、そのサブページ数を再設定(表示時に行われる)
}
infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString();
infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString();
updatePanelDisplay();
console.log("Prev button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex);
});

nextButtonEl.addEventListener('click', function (event) {
event.stopPropagation();
if (!infoPanelEl.getAttribute('visible')) return;
let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10);
const commentSubPageCount = parseInt(infoPanelEl.dataset.commentSubPageCount || '1', 10);

if (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1) {
// コメントページ内でサブページをめくる
commentSubPageIndex++;
if (commentSubPageIndex >= commentSubPageCount) { // コメントの最後のサブページより次に進もうとした
mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES;
commentSubPageIndex = 0; // 新しいメインページではコメントサブページはリセット
}
} else {
// メインページをめくる
mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES;
commentSubPageIndex = 0; // 他のページに移動したらコメントサブページはリセット
}
infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString();
infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString();
updatePanelDisplay();
console.log("Next button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex);
});

// --- ★★★ 閉じる{タンのクリック処理 (コメントデータ削除追加) ★★★ ---
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;
delete infoPanelEl.dataset.comments; // ★ コメント配列削除
delete infoPanelEl.dataset.commentSubPageIndex; // ★ サブページインデックス削除
delete infoPanelEl.dataset.commentSubPageCount; // ★ サブページ総数削除
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'); // ★★★ 右手を取得 ★★★
if (rightHandEl) {
rightHandEl.addEventListener('triggerdown', function (evt) { console.log('Right hand TRIGGER DOWN event!', evt); 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'); } else { console.log('Right 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
angle
argetWorldPosition
at
attribute
AULT_COMMENT_ARRAY
axis
background
begin
bottom
camera
cameraDirection
cameraEl
cameraRight
class
clickedCube
closeButtonEl
color
comments
commentsArray
controls
cubeEl
cubeSide
CUBE_COMMENTS
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
leftHandEl
leftRight
lerpFactor
light
mainPage
mainPageIndex
material
mouseCursorEl
moveDirection
newCommentsArray
nextButtonEl
numSpheres
offsetDistance
OFFSET_FROM_OBJECT
ommentsForThisCube
ommentSubPageCount
ommentSubPageIndex
onKeyDown
onKeyUp
opacity
pageIndicator
PAGES
pageType
panelPosition
panelTextEl
position
prevButtonEl
radius
raycaster
raycasterComponent
rightHandEl
rotation
RotationQuaternion
sceneEl
shader
side
size
speed
speedRad
sphereIdx
sphereIndex
spread
src
subPage
text
thumbstickInput
to
top
TOTAL_MAIN_PAGES
ui
updatePanelDisplay -------( Function )
visible
width
x
y
z
ZERO_VECTOR