a-farme-球に文字test50
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 配置パターン調整</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-assets>
<img id="tex_a1" src="./pic/a1.jpg" crossOrigin="anonymous">
<img id="tex_a2" src="./pic/a2.jpg" crossOrigin="anonymous">
<img id="tex_a3" src="./pic/a3.jpg" crossOrigin="anonymous">
<img id="tex_a4" src="./pic/a4.jpg" crossOrigin="anonymous">
<img id="tex_a5" src="./pic/a5.jpg" crossOrigin="anonymous">
<img id="tex_a6" src="./pic/a6.jpg" crossOrigin="anonymous">
<img id="tex_a7" src="./pic/a7.jpg" crossOrigin="anonymous">
<img id="tex_a8" src="./pic/a8.jpg" crossOrigin="anonymous">
<img id="tex_a9" src="./pic/a9.jpg" crossOrigin="anonymous">
<img id="tex_a10" src="./pic/a10.jpg" crossOrigin="anonymous">
</a-assets>
<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 id="linkButtonsContainer" position="-22 -5 0.1">
<a-sphere id="linkButton0" class="clickableButton linkButton" visible="false" radius="1.5" position="0 0 0" shader="flat"></a-sphere>
<a-sphere id="linkButton1" class="clickableButton linkButton" visible="false" radius="1.5" position="0 -4 0" shader="flat"></a-sphere>
<a-sphere id="linkButton2" class="clickableButton linkButton" visible="false" radius="1.5" position="0 -8 0" shader="flat"></a-sphere>
</a-entity>
</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'); const linkButtons = []; let rightHandEl = null;
// パネルの表示オフセット距離 (変更なし)
const PANEL_OFFSET_FROM_OBJECT = 5.0;
// ページごとのテキストスタイル定義 (変更なし)
const BASE_TEXT_STYLE = { maxWidth: 46, align: 'center', anchorX: 'center', anchorY: 'middle', baseline: 'middle' };
const PAGE_TEXT_STYLES = { 'index': { ...BASE_TEXT_STYLE, fontSize: 0.72, color: 'white' }, 'color': { ...BASE_TEXT_STYLE, fontSize: 0.65, color: '#A0F0A0' }, 'size': { ...BASE_TEXT_STYLE, fontSize: 0.80, color: '#FFEC8B' }, 'comment': { ...BASE_TEXT_STYLE, fontSize: 0.70, color: '#ADD8E6' } };
// オブジェクトごとのタイプと}テリアル定義 (変更なし)
const OBJECT_DEFINITIONS = { 1: { type: 'sphere', useTextureForIndex: 1 }, 2: { type: 'box', useTextureForIndex: 2 }, 3: { type: 'sphere', specificColor: '#FF69B4' }, 4: { type: 'box', useTextureForIndex: 4 }, 6: { type: 'sphere', useTextureForIndex: 6 }, 7: { type: 'box', specificColor: 'orange' }, 8: { type: 'sphere', useTextureForIndex: 8 }, 9: { type: 'box', useTextureForIndex: 9 }, 10: { type: 'sphere', useTextureForIndex: 10 },};
const DEFAULT_OBJECT_TYPE = 'box'; const TEXTURE_ID_PREFIX = 'tex_a'; const MAX_TEXTURE_INDEX = 10;
// コメントデータ (変更なし)
const CUBE_COMMENTS = { 1: { text: ["最初のオブジェクト!"], mainCommentTextColor: "#FFDA63" }, 5: { text: ["特別なオブジェクトその1。"], links: [{ label: "A-Frame IO", url: "https://aframe.io", buttonColor: "orange" }] }, 10: { text: ["このオブジェクトには情報源が複数あります:", "緑{タン: A-Frame公式ドキュメント", "青{タン: Google検索", "黄色{タン: A-Frame GitHub"], mainCommentTextColor: "#B0E0E6", links: [ { label: "A-Frame Docs", url: "https://aframe.io/docs/", buttonColor: "#2ECC71" }, { label: "Google", url: "https://google.com", buttonColor: "#3498DB" }, { label: "GitHub", url: "https://github.com/aframevr", buttonColor: "#F1C40F" } ] }, };
const DEFAULT_COMMENT_ARRAY_INFO = { text: ["コメントはありません"], links: [] };
// --- 定数定義 ---
const numObjects = 10; const spread = 300; const PAGES = ['index', 'color', 'size', 'comment']; const TOTAL_MAIN_PAGES = PAGES.length;
const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3();
// --- ★★★ 配置パターン設定 (初期値を8に変更) ★★★ ---
const LAYOUT_PATTERN = 6;
// パターン3, 4 (水平ライン) 用の定数
const H_LINE_SPACING = 15;
const H_LINE_Y = 1.6;
const H_LINE_Z_OFFSET = -35; // ★★★ 値を変更 ★★★
// パターン5, 6, 7, 8 (リング / 半リング) 用の定数
const RING_RADIUS = 50; // ★★★ 値を変更 ★★★
const RING_OBJECT_Y = 1.6;
const RIG_INITIAL_X = 0;
const RIG_INITIAL_Z = 5;
const SPAWN_ANIM_START_POS_STRING = '0 1.6 0';
const RING_LOOK_AT_TARGET = new THREE.Vector3(RIG_INITIAL_X, RING_OBJECT_Y, RIG_INITIAL_Z); // ★★★ lookAtターゲットを明確化 ★★★
// --- オブジェクト生成ループ (配置パターン5-8の角度計算修正) ---
for (let i = 0; i < numObjects; i++) {
const objectIndex = i + 1;
const definition = OBJECT_DEFINITIONS[objectIndex] || {};
const objectType = definition.type || DEFAULT_OBJECT_TYPE;
let objectEl;
if (objectType === 'sphere') { objectEl = document.createElement('a-sphere'); }
else { objectEl = document.createElement('a-box'); }
const dimension = Math.random() * 10.0 + 0.5;
const objectNumericIndex = i + 1;
let finalX, finalY, finalZ;
let applyRandomRotation = false;
let initialRotation = null;
switch (LAYOUT_PATTERN) {
case 1:
finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread;
applyRandomRotation = true;
break;
case 2:
finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread;
applyRandomRotation = false;
break;
case 3:
const totalLineWidthH = (numObjects - 1) * H_LINE_SPACING; finalX = (-totalLineWidthH / 2) + (i * H_LINE_SPACING);
finalY = H_LINE_Y; finalZ = RIG_INITIAL_Z + H_LINE_Z_OFFSET;
applyRandomRotation = true;
break;
case 4:
const totalLineWidthV = (numObjects - 1) * H_LINE_SPACING; finalX = (-totalLineWidthV / 2) + (i * H_LINE_SPACING);
finalY = H_LINE_Y; finalZ = RIG_INITIAL_Z + H_LINE_Z_OFFSET;
applyRandomRotation = false;
break;
case 5: // 360度リング (プレイヤー中心)、回転あり
case 6: // 360度リング (プレイヤー中心)、回転なし
const angleStep360 = (Math.PI * 2) / numObjects;
const angle360 = i * angleStep360;
finalX = RIG_INITIAL_X + RING_RADIUS * Math.cos(angle360);
finalY = RING_OBJECT_Y;
finalZ = RIG_INITIAL_Z + RING_RADIUS * Math.sin(angle360);
objectEl.object3D.position.set(finalX, finalY, finalZ);
objectEl.object3D.lookAt(RING_LOOK_AT_TARGET); // リング中心を向く
initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) };
applyRandomRotation = (LAYOUT_PATTERN === 5);
break;
case 7: // 180度半リング (プレイヤー中心、前方)、回転あり
case 8: // 180度半リング (プレイヤー中心、前方)、回転なし
const arcSpan180 = Math.PI;
// ★★★ 開始角度をプレイヤーの真左 PI に変更 ★★★
const startAngle180 =Math.PI;
const angleStep180 = numObjects > 1 ? arcSpan180 / (numObjects - 1) : 0;
const angle180 = startAngle180 + (i * angleStep180);
finalX = RIG_INITIAL_X + RING_RADIUS * Math.cos(angle180);
finalY = RING_OBJECT_Y;
finalZ = RIG_INITIAL_Z + RING_RADIUS * Math.sin(angle180);
objectEl.object3D.position.set(finalX, finalY, finalZ);
objectEl.object3D.lookAt(RING_LOOK_AT_TARGET); // リング中心を向く
initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) };
applyRandomRotation = (LAYOUT_PATTERN === 7);
break;
default:
console.warn(`Unknown LAYOUT_PATTERN: ${LAYOUT_PATTERN}, defaulting to pattern 1.`);
finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread;
applyRandomRotation = true;
}
if (objectType === 'sphere') { objectEl.setAttribute('radius', dimension); }
else { objectEl.setAttribute('width', dimension); objectEl.setAttribute('height', dimension); objectEl.setAttribute('depth', dimension); }
let appliedColor; if (definition.useTextureForIndex && definition.useTextureForIndex >= 1 && definition.useTextureForIndex <= MAX_TEXTURE_INDEX) { const textureId = `${TEXTURE_ID_PREFIX}${definition.useTextureForIndex}`; objectEl.setAttribute('material', 'src', `#${textureId}`); appliedColor = '#FFFFFF'; objectEl.setAttribute('color', appliedColor); } else if (definition.specificColor) { appliedColor = definition.specificColor; objectEl.setAttribute('color', appliedColor); if(objectEl.hasAttribute('material')) objectEl.removeAttribute('material','src'); } else { appliedColor = `hsl(${Math.random() * 360}, 50%, 75%)`; objectEl.setAttribute('color', appliedColor); if(objectEl.hasAttribute('material')) objectEl.removeAttribute('material','src'); }
objectEl.setAttribute('scale', '0.01 0.01 0.01');
objectEl.setAttribute('position', SPAWN_ANIM_START_POS_STRING);
if (initialRotation) { objectEl.setAttribute('rotation', initialRotation); }
objectEl.classList.add('clickableObject');
objectEl.dataset.objectIndex = objectNumericIndex; objectEl.dataset.color = appliedColor; objectEl.dataset.dimension = dimension.toFixed(2); objectEl.dataset.objectType = objectType;
if (applyRandomRotation) { objectEl.setAttribute('random-rotate', {maxSpeed: 5}); }
objectEl.addEventListener('click', handleObjectClick);
sceneEl.appendChild(objectEl);
objectEl.setAttribute('animation__spawnscale', { property: 'scale', to: '1 1 1', dur: 3000, easing: 'easeOutQuad' });
objectEl.setAttribute('animation__spawnposition', { property: 'position', to: `${finalX} ${finalY} ${finalZ}`, dur: 3000, easing: 'easeOutQuad'});
}
// --- 情報パネルの表示更新関数 (変更なし) ---
function updatePanelDisplay() { /* ... (前回と同じ) ... */ if (!infoPanelEl.dataset.objectIndex) return; const index = parseInt(infoPanelEl.dataset.objectIndex || '0', 10); const sphereColor = infoPanelEl.dataset.color || 'N/A'; const dimensionValue = infoPanelEl.dataset.dimension || 'N/A'; const objectType = infoPanelEl.dataset.objectType || DEFAULT_OBJECT_TYPE; const commentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); const commentsArray = commentInfo.text; const mainCommentTextColor = commentInfo.mainCommentTextColor; const activeLinks = JSON.parse(infoPanelEl.dataset.activeLinks || "[]"); const commentSubPageCount = commentsArray.length; infoPanelEl.dataset.commentSubPageCount = commentSubPageCount.toString(); 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})`; let currentStyle = { ...(PAGE_TEXT_STYLES[pageType] || PAGE_TEXT_STYLES['index']) }; linkButtons.forEach(btn => btn.setAttribute('visible', false)); if (pageType === 'comment' && activeLinks && activeLinks.length > 0) { activeLinks.forEach((link, i) => { if (linkButtons[i]) { linkButtons[i].setAttribute('visible', true); linkButtons[i].setAttribute('material', 'color', link.buttonColor || 'lime'); linkButtons[i].dataset.url = link.url; } }); } if (pageType === 'index') { const typeLabel = objectType === 'sphere' ? '球' : '立方体'; displayText = `${typeLabel}: ${index}`; } else if (pageType === 'color') { displayText = `色: ${sphereColor}`; } else if (pageType === 'size') { const sizeLabel = objectType === 'sphere' ? '半径' : 'サイズ'; displayText = `${sizeLabel}: ${dimensionValue}`; } else if (pageType === 'comment') { displayText = `コメント:\n${commentsArray[commentSubPageIndex] || "(このサブページにコメントはありません)"}`; pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`; if (mainCommentTextColor) { currentStyle.color = mainCommentTextColor; } } const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', { value: finalDisplayText, color: currentStyle.color, fontSize: currentStyle.fontSize, maxWidth: currentStyle.maxWidth, align: currentStyle.align, anchorX: currentStyle.anchorX, anchorY: currentStyle.anchorY, baseline: currentStyle.baseline }); }
// --- オブジェクトクリック時の処理 (変更なし) ---
function handleObjectClick(event) { /* ... (前回と同じ) ... */ event.stopPropagation(); console.log("--- handleObjectClick triggered --- Target:", event.target.id || event.target.tagName); const clickedObject = event.target; if (!clickedObject.dataset.objectIndex || !clickedObject.dataset.color || !clickedObject.dataset.dimension || !clickedObject.dataset.objectType) { console.error("Object data missing from dataset!", clickedObject.dataset); return; } console.log("Object data found:", clickedObject.dataset); infoPanelEl.dataset.objectIndex = clickedObject.dataset.objectIndex; infoPanelEl.dataset.color = clickedObject.dataset.color; infoPanelEl.dataset.dimension = clickedObject.dataset.dimension; infoPanelEl.dataset.objectType = clickedObject.dataset.objectType; infoPanelEl.dataset.currentPageIndex = '0'; const objectIdx = parseInt(clickedObject.dataset.objectIndex, 10); const commentDataForThisObject = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(commentDataForThisObject); infoPanelEl.dataset.commentSubPageCount = commentDataForThisObject.text.length.toString(); infoPanelEl.dataset.commentSubPageIndex = '0'; if (commentDataForThisObject.links && commentDataForThisObject.links.length > 0) { infoPanelEl.dataset.activeLinks = JSON.stringify(commentDataForThisObject.links); } else { infoPanelEl.dataset.activeLinks = JSON.stringify([]); } if (commentDataForThisObject.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = commentDataForThisObject.mainCommentTextColor; } else { delete infoPanelEl.dataset.commentPageTextColor; } console.log("Data (including comments, links, text color) stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedObject.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const dimensionVal = parseFloat(clickedObject.dataset.dimension || 0); const objectTypeVal = clickedObject.dataset.objectType || DEFAULT_OBJECT_TYPE; const baseOffset = (objectTypeVal === 'sphere') ? dimensionVal : dimensionVal / 2; const offsetDistance = baseOffset + 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. --- handleObjectClick 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 currentCommentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); const commentSubPageCount = currentCommentInfo.text.length; if (PAGES[mainPageIndex] === 'comment' && commentSubPageIndex > 0) { commentSubPageIndex--; } else { mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; if (PAGES[mainPageIndex] === 'comment') { const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; commentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } else { 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 currentCommentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); const commentSubPageCount = currentCommentInfo.text.length; if (PAGES[mainPageIndex] === 'comment' && commentSubPageIndex < commentSubPageCount - 1) { commentSubPageIndex++; } 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.objectIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.dimension; delete infoPanelEl.dataset.objectType; delete infoPanelEl.dataset.currentPageIndex; delete infoPanelEl.dataset.commentInfo; delete infoPanelEl.dataset.commentSubPageIndex; delete infoPanelEl.dataset.commentSubPageCount; delete infoPanelEl.dataset.activeLinks; delete infoPanelEl.dataset.commentPageTextColor; linkButtons.forEach(btn => btn.setAttribute('visible', false)); console.log("Close button clicked, panel hidden."); });
// --- リンク{タンのクリック処理 (変更なし) ---
function handleLinkButtonClick(event) { /* ... (前回と同じ) ... */ event.stopPropagation(); const urlToOpen = event.target.dataset.url; if (!urlToOpen) { console.log("Link button clicked, but no URL found on this button."); return; } console.log("Link button clicked, starting absorption animation for URL:", urlToOpen); const linkButtonWorldPos = new THREE.Vector3(); event.target.object3D.getWorldPosition(linkButtonWorldPos); const allClickableObjects = document.querySelectorAll('.clickableObject'); const animationDuration = 3000; allClickableObjects.forEach(objEl => { objEl.removeAttribute('animation__scale吸い込み'); objEl.removeAttribute('animation__position吸い込み'); objEl.setAttribute('animation__scale吸い込み', { property: 'scale', to: '0.01 0.01 0.01', dur: animationDuration, easing: 'easeInQuad' }); objEl.setAttribute('animation__position吸い込み', { property: 'position', to: `${linkButtonWorldPos.x} ${linkButtonWorldPos.y} ${linkButtonWorldPos.z}`, dur: animationDuration, easing: 'easeInQuad' }); }); setTimeout(() => { console.log("Animation finished, navigating to URL:", urlToOpen); window.location.href = urlToOpen; }, animationDuration); }
// --- 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() { for (let i = 0; i < 3; i++) { const btn = document.getElementById(`linkButton${i}`); if (btn) { linkButtons.push(btn); btn.addEventListener('click', handleLinkButtonClick); } } 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>
使用変数
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 配置パターン調整</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-assets>
<img id="tex_a1" src="./pic/a1.jpg" crossOrigin="anonymous">
<img id="tex_a2" src="./pic/a2.jpg" crossOrigin="anonymous">
<img id="tex_a3" src="./pic/a3.jpg" crossOrigin="anonymous">
<img id="tex_a4" src="./pic/a4.jpg" crossOrigin="anonymous">
<img id="tex_a5" src="./pic/a5.jpg" crossOrigin="anonymous">
<img id="tex_a6" src="./pic/a6.jpg" crossOrigin="anonymous">
<img id="tex_a7" src="./pic/a7.jpg" crossOrigin="anonymous">
<img id="tex_a8" src="./pic/a8.jpg" crossOrigin="anonymous">
<img id="tex_a9" src="./pic/a9.jpg" crossOrigin="anonymous">
<img id="tex_a10" src="./pic/a10.jpg" crossOrigin="anonymous">
</a-assets>
<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 id="linkButtonsContainer" position="-22 -5 0.1">
<a-sphere id="linkButton0" class="clickableButton linkButton" visible="false" radius="1.5" position="0 0 0" shader="flat"></a-sphere>
<a-sphere id="linkButton1" class="clickableButton linkButton" visible="false" radius="1.5" position="0 -4 0" shader="flat"></a-sphere>
<a-sphere id="linkButton2" class="clickableButton linkButton" visible="false" radius="1.5" position="0 -8 0" shader="flat"></a-sphere>
</a-entity>
</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'); const linkButtons = []; let rightHandEl = null;
// パネルの表示オフセット距離 (変更なし)
const PANEL_OFFSET_FROM_OBJECT = 5.0;
// ページごとのテキストスタイル定義 (変更なし)
const BASE_TEXT_STYLE = { maxWidth: 46, align: 'center', anchorX: 'center', anchorY: 'middle', baseline: 'middle' };
const PAGE_TEXT_STYLES = { 'index': { ...BASE_TEXT_STYLE, fontSize: 0.72, color: 'white' }, 'color': { ...BASE_TEXT_STYLE, fontSize: 0.65, color: '#A0F0A0' }, 'size': { ...BASE_TEXT_STYLE, fontSize: 0.80, color: '#FFEC8B' }, 'comment': { ...BASE_TEXT_STYLE, fontSize: 0.70, color: '#ADD8E6' } };
// オブジェクトごとのタイプと}テリアル定義 (変更なし)
const OBJECT_DEFINITIONS = { 1: { type: 'sphere', useTextureForIndex: 1 }, 2: { type: 'box', useTextureForIndex: 2 }, 3: { type: 'sphere', specificColor: '#FF69B4' }, 4: { type: 'box', useTextureForIndex: 4 }, 6: { type: 'sphere', useTextureForIndex: 6 }, 7: { type: 'box', specificColor: 'orange' }, 8: { type: 'sphere', useTextureForIndex: 8 }, 9: { type: 'box', useTextureForIndex: 9 }, 10: { type: 'sphere', useTextureForIndex: 10 },};
const DEFAULT_OBJECT_TYPE = 'box'; const TEXTURE_ID_PREFIX = 'tex_a'; const MAX_TEXTURE_INDEX = 10;
// コメントデータ (変更なし)
const CUBE_COMMENTS = { 1: { text: ["最初のオブジェクト!"], mainCommentTextColor: "#FFDA63" }, 5: { text: ["特別なオブジェクトその1。"], links: [{ label: "A-Frame IO", url: "https://aframe.io", buttonColor: "orange" }] }, 10: { text: ["このオブジェクトには情報源が複数あります:", "緑{タン: A-Frame公式ドキュメント", "青{タン: Google検索", "黄色{タン: A-Frame GitHub"], mainCommentTextColor: "#B0E0E6", links: [ { label: "A-Frame Docs", url: "https://aframe.io/docs/", buttonColor: "#2ECC71" }, { label: "Google", url: "https://google.com", buttonColor: "#3498DB" }, { label: "GitHub", url: "https://github.com/aframevr", buttonColor: "#F1C40F" } ] }, };
const DEFAULT_COMMENT_ARRAY_INFO = { text: ["コメントはありません"], links: [] };
// --- 定数定義 ---
const numObjects = 10; const spread = 300; const PAGES = ['index', 'color', 'size', 'comment']; const TOTAL_MAIN_PAGES = PAGES.length;
const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3();
// --- ★★★ 配置パターン設定 (初期値を8に変更) ★★★ ---
const LAYOUT_PATTERN = 6;
// パターン3, 4 (水平ライン) 用の定数
const H_LINE_SPACING = 15;
const H_LINE_Y = 1.6;
const H_LINE_Z_OFFSET = -35; // ★★★ 値を変更 ★★★
// パターン5, 6, 7, 8 (リング / 半リング) 用の定数
const RING_RADIUS = 50; // ★★★ 値を変更 ★★★
const RING_OBJECT_Y = 1.6;
const RIG_INITIAL_X = 0;
const RIG_INITIAL_Z = 5;
const SPAWN_ANIM_START_POS_STRING = '0 1.6 0';
const RING_LOOK_AT_TARGET = new THREE.Vector3(RIG_INITIAL_X, RING_OBJECT_Y, RIG_INITIAL_Z); // ★★★ lookAtターゲットを明確化 ★★★
// --- オブジェクト生成ループ (配置パターン5-8の角度計算修正) ---
for (let i = 0; i < numObjects; i++) {
const objectIndex = i + 1;
const definition = OBJECT_DEFINITIONS[objectIndex] || {};
const objectType = definition.type || DEFAULT_OBJECT_TYPE;
let objectEl;
if (objectType === 'sphere') { objectEl = document.createElement('a-sphere'); }
else { objectEl = document.createElement('a-box'); }
const dimension = Math.random() * 10.0 + 0.5;
const objectNumericIndex = i + 1;
let finalX, finalY, finalZ;
let applyRandomRotation = false;
let initialRotation = null;
switch (LAYOUT_PATTERN) {
case 1:
finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread;
applyRandomRotation = true;
break;
case 2:
finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread;
applyRandomRotation = false;
break;
case 3:
const totalLineWidthH = (numObjects - 1) * H_LINE_SPACING; finalX = (-totalLineWidthH / 2) + (i * H_LINE_SPACING);
finalY = H_LINE_Y; finalZ = RIG_INITIAL_Z + H_LINE_Z_OFFSET;
applyRandomRotation = true;
break;
case 4:
const totalLineWidthV = (numObjects - 1) * H_LINE_SPACING; finalX = (-totalLineWidthV / 2) + (i * H_LINE_SPACING);
finalY = H_LINE_Y; finalZ = RIG_INITIAL_Z + H_LINE_Z_OFFSET;
applyRandomRotation = false;
break;
case 5: // 360度リング (プレイヤー中心)、回転あり
case 6: // 360度リング (プレイヤー中心)、回転なし
const angleStep360 = (Math.PI * 2) / numObjects;
const angle360 = i * angleStep360;
finalX = RIG_INITIAL_X + RING_RADIUS * Math.cos(angle360);
finalY = RING_OBJECT_Y;
finalZ = RIG_INITIAL_Z + RING_RADIUS * Math.sin(angle360);
objectEl.object3D.position.set(finalX, finalY, finalZ);
objectEl.object3D.lookAt(RING_LOOK_AT_TARGET); // リング中心を向く
initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) };
applyRandomRotation = (LAYOUT_PATTERN === 5);
break;
case 7: // 180度半リング (プレイヤー中心、前方)、回転あり
case 8: // 180度半リング (プレイヤー中心、前方)、回転なし
const arcSpan180 = Math.PI;
// ★★★ 開始角度をプレイヤーの真左 PI に変更 ★★★
const startAngle180 =Math.PI;
const angleStep180 = numObjects > 1 ? arcSpan180 / (numObjects - 1) : 0;
const angle180 = startAngle180 + (i * angleStep180);
finalX = RIG_INITIAL_X + RING_RADIUS * Math.cos(angle180);
finalY = RING_OBJECT_Y;
finalZ = RIG_INITIAL_Z + RING_RADIUS * Math.sin(angle180);
objectEl.object3D.position.set(finalX, finalY, finalZ);
objectEl.object3D.lookAt(RING_LOOK_AT_TARGET); // リング中心を向く
initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) };
applyRandomRotation = (LAYOUT_PATTERN === 7);
break;
default:
console.warn(`Unknown LAYOUT_PATTERN: ${LAYOUT_PATTERN}, defaulting to pattern 1.`);
finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread;
applyRandomRotation = true;
}
if (objectType === 'sphere') { objectEl.setAttribute('radius', dimension); }
else { objectEl.setAttribute('width', dimension); objectEl.setAttribute('height', dimension); objectEl.setAttribute('depth', dimension); }
let appliedColor; if (definition.useTextureForIndex && definition.useTextureForIndex >= 1 && definition.useTextureForIndex <= MAX_TEXTURE_INDEX) { const textureId = `${TEXTURE_ID_PREFIX}${definition.useTextureForIndex}`; objectEl.setAttribute('material', 'src', `#${textureId}`); appliedColor = '#FFFFFF'; objectEl.setAttribute('color', appliedColor); } else if (definition.specificColor) { appliedColor = definition.specificColor; objectEl.setAttribute('color', appliedColor); if(objectEl.hasAttribute('material')) objectEl.removeAttribute('material','src'); } else { appliedColor = `hsl(${Math.random() * 360}, 50%, 75%)`; objectEl.setAttribute('color', appliedColor); if(objectEl.hasAttribute('material')) objectEl.removeAttribute('material','src'); }
objectEl.setAttribute('scale', '0.01 0.01 0.01');
objectEl.setAttribute('position', SPAWN_ANIM_START_POS_STRING);
if (initialRotation) { objectEl.setAttribute('rotation', initialRotation); }
objectEl.classList.add('clickableObject');
objectEl.dataset.objectIndex = objectNumericIndex; objectEl.dataset.color = appliedColor; objectEl.dataset.dimension = dimension.toFixed(2); objectEl.dataset.objectType = objectType;
if (applyRandomRotation) { objectEl.setAttribute('random-rotate', {maxSpeed: 5}); }
objectEl.addEventListener('click', handleObjectClick);
sceneEl.appendChild(objectEl);
objectEl.setAttribute('animation__spawnscale', { property: 'scale', to: '1 1 1', dur: 3000, easing: 'easeOutQuad' });
objectEl.setAttribute('animation__spawnposition', { property: 'position', to: `${finalX} ${finalY} ${finalZ}`, dur: 3000, easing: 'easeOutQuad'});
}
// --- 情報パネルの表示更新関数 (変更なし) ---
function updatePanelDisplay() { /* ... (前回と同じ) ... */ if (!infoPanelEl.dataset.objectIndex) return; const index = parseInt(infoPanelEl.dataset.objectIndex || '0', 10); const sphereColor = infoPanelEl.dataset.color || 'N/A'; const dimensionValue = infoPanelEl.dataset.dimension || 'N/A'; const objectType = infoPanelEl.dataset.objectType || DEFAULT_OBJECT_TYPE; const commentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); const commentsArray = commentInfo.text; const mainCommentTextColor = commentInfo.mainCommentTextColor; const activeLinks = JSON.parse(infoPanelEl.dataset.activeLinks || "[]"); const commentSubPageCount = commentsArray.length; infoPanelEl.dataset.commentSubPageCount = commentSubPageCount.toString(); 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})`; let currentStyle = { ...(PAGE_TEXT_STYLES[pageType] || PAGE_TEXT_STYLES['index']) }; linkButtons.forEach(btn => btn.setAttribute('visible', false)); if (pageType === 'comment' && activeLinks && activeLinks.length > 0) { activeLinks.forEach((link, i) => { if (linkButtons[i]) { linkButtons[i].setAttribute('visible', true); linkButtons[i].setAttribute('material', 'color', link.buttonColor || 'lime'); linkButtons[i].dataset.url = link.url; } }); } if (pageType === 'index') { const typeLabel = objectType === 'sphere' ? '球' : '立方体'; displayText = `${typeLabel}: ${index}`; } else if (pageType === 'color') { displayText = `色: ${sphereColor}`; } else if (pageType === 'size') { const sizeLabel = objectType === 'sphere' ? '半径' : 'サイズ'; displayText = `${sizeLabel}: ${dimensionValue}`; } else if (pageType === 'comment') { displayText = `コメント:\n${commentsArray[commentSubPageIndex] || "(このサブページにコメントはありません)"}`; pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`; if (mainCommentTextColor) { currentStyle.color = mainCommentTextColor; } } const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', { value: finalDisplayText, color: currentStyle.color, fontSize: currentStyle.fontSize, maxWidth: currentStyle.maxWidth, align: currentStyle.align, anchorX: currentStyle.anchorX, anchorY: currentStyle.anchorY, baseline: currentStyle.baseline }); }
// --- オブジェクトクリック時の処理 (変更なし) ---
function handleObjectClick(event) { /* ... (前回と同じ) ... */ event.stopPropagation(); console.log("--- handleObjectClick triggered --- Target:", event.target.id || event.target.tagName); const clickedObject = event.target; if (!clickedObject.dataset.objectIndex || !clickedObject.dataset.color || !clickedObject.dataset.dimension || !clickedObject.dataset.objectType) { console.error("Object data missing from dataset!", clickedObject.dataset); return; } console.log("Object data found:", clickedObject.dataset); infoPanelEl.dataset.objectIndex = clickedObject.dataset.objectIndex; infoPanelEl.dataset.color = clickedObject.dataset.color; infoPanelEl.dataset.dimension = clickedObject.dataset.dimension; infoPanelEl.dataset.objectType = clickedObject.dataset.objectType; infoPanelEl.dataset.currentPageIndex = '0'; const objectIdx = parseInt(clickedObject.dataset.objectIndex, 10); const commentDataForThisObject = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(commentDataForThisObject); infoPanelEl.dataset.commentSubPageCount = commentDataForThisObject.text.length.toString(); infoPanelEl.dataset.commentSubPageIndex = '0'; if (commentDataForThisObject.links && commentDataForThisObject.links.length > 0) { infoPanelEl.dataset.activeLinks = JSON.stringify(commentDataForThisObject.links); } else { infoPanelEl.dataset.activeLinks = JSON.stringify([]); } if (commentDataForThisObject.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = commentDataForThisObject.mainCommentTextColor; } else { delete infoPanelEl.dataset.commentPageTextColor; } console.log("Data (including comments, links, text color) stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedObject.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const dimensionVal = parseFloat(clickedObject.dataset.dimension || 0); const objectTypeVal = clickedObject.dataset.objectType || DEFAULT_OBJECT_TYPE; const baseOffset = (objectTypeVal === 'sphere') ? dimensionVal : dimensionVal / 2; const offsetDistance = baseOffset + 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. --- handleObjectClick 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 currentCommentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); const commentSubPageCount = currentCommentInfo.text.length; if (PAGES[mainPageIndex] === 'comment' && commentSubPageIndex > 0) { commentSubPageIndex--; } else { mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; if (PAGES[mainPageIndex] === 'comment') { const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; commentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } else { 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 currentCommentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); const commentSubPageCount = currentCommentInfo.text.length; if (PAGES[mainPageIndex] === 'comment' && commentSubPageIndex < commentSubPageCount - 1) { commentSubPageIndex++; } 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.objectIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.dimension; delete infoPanelEl.dataset.objectType; delete infoPanelEl.dataset.currentPageIndex; delete infoPanelEl.dataset.commentInfo; delete infoPanelEl.dataset.commentSubPageIndex; delete infoPanelEl.dataset.commentSubPageCount; delete infoPanelEl.dataset.activeLinks; delete infoPanelEl.dataset.commentPageTextColor; linkButtons.forEach(btn => btn.setAttribute('visible', false)); console.log("Close button clicked, panel hidden."); });
// --- リンク{タンのクリック処理 (変更なし) ---
function handleLinkButtonClick(event) { /* ... (前回と同じ) ... */ event.stopPropagation(); const urlToOpen = event.target.dataset.url; if (!urlToOpen) { console.log("Link button clicked, but no URL found on this button."); return; } console.log("Link button clicked, starting absorption animation for URL:", urlToOpen); const linkButtonWorldPos = new THREE.Vector3(); event.target.object3D.getWorldPosition(linkButtonWorldPos); const allClickableObjects = document.querySelectorAll('.clickableObject'); const animationDuration = 3000; allClickableObjects.forEach(objEl => { objEl.removeAttribute('animation__scale吸い込み'); objEl.removeAttribute('animation__position吸い込み'); objEl.setAttribute('animation__scale吸い込み', { property: 'scale', to: '0.01 0.01 0.01', dur: animationDuration, easing: 'easeInQuad' }); objEl.setAttribute('animation__position吸い込み', { property: 'position', to: `${linkButtonWorldPos.x} ${linkButtonWorldPos.y} ${linkButtonWorldPos.z}`, dur: animationDuration, easing: 'easeInQuad' }); }); setTimeout(() => { console.log("Animation finished, navigating to URL:", urlToOpen); window.location.href = urlToOpen; }, animationDuration); }
// --- 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() { for (let i = 0; i < 3; i++) { const btn = document.getElementById(`linkButton${i}`); if (btn) { linkButtons.push(btn); btn.addEventListener('click', handleLinkButtonClick); } } 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 ) | |
) { for -------( Function ) | |
activeLinks | |
ameraWorldPosition | |
angle180 | |
angle360 | |
angle | |
angleStep180 | |
angleStep360 | |
animationDuration | |
appliedColor | |
arcSpan180 | |
argetWorldPosition | |
at | |
attribute | |
axis | |
background | |
baseOffset | |
BASE_TEXT_STYLE | |
begin | |
bottom | |
btn | |
camera | |
cameraDirection | |
cameraEl | |
cameraRight | |
class | |
clickedObject | |
closeButtonEl | |
color | |
commentInfo | |
commentsArray | |
COMMENT_ARRAY_INFO | |
controls | |
crossOrigin | |
CUBE_COMMENTS | |
currentCommentInfo | |
currentPageIndex | |
currentStyle | |
currentVelocity | |
cursor | |
data | |
definition | |
deltaPosition | |
desiredVelocity | |
dimension | |
dimensionVal | |
dimensionValue | |
direction | |
displayText | |
dotProduct | |
dt | |
dur | |
easing | |
EFAULT_OBJECT_TYPE | |
el | |
eraWorldQuaternion | |
ffectiveLerpFactor | |
fill | |
finalDisplayText | |
finalX | |
finalY | |
finalZ | |
forwardBackward | |
from | |
geometry | |
handleLinkButtonClick -------( Function ) | |
handleObjectClick -------( Function ) | |
height | |
href | |
H_LINE_SPACING | |
H_LINE_Y | |
H_LINE_Z_OFFSET | |
i | |
id | |
inCommentTextColor | |
index | |
infoPanelEl | |
ING_LOOK_AT_TARGET | |
initialRotation | |
intersectedEls | |
isCurrentlyMoving | |
isInputActive | |
isOpposingInput | |
isReady | |
keys | |
LAYOUT_PATTERN | |
leftHand | |
leftRight | |
lerpFactor | |
light | |
linkButtons | |
linkButtonWorldPos | |
llClickableObjects | |
mainPage | |
mainPageIndex | |
material | |
MAX_TEXTURE_INDEX | |
mmentPageTextColor | |
mouseCursorEl | |
moveDirection | |
M_START_POS_STRING | |
newCommentData | |
nextButtonEl | |
numObjects | |
objectEl | |
objectIdx | |
objectIndex | |
objectNumericIndex | |
objectType | |
objectTypeVal | |
OBJECT_DEFINITIONS | |
objEl | |
offsetDistance | |
OFFSET_FROM_OBJECT | |
ommentSubPageCount | |
ommentSubPageIndex | |
onKeyDown | |
onKeyUp | |
opacity | |
pageIndicator | |
PAGES | |
pageType | |
PAGE_TEXT_STYLES | |
panelPosition | |
panelTextEl | |
position | |
pplyRandomRotation | |
prevButtonEl | |
radius | |
raycaster | |
raycasterComponent | |
rightHandEl | |
RIG_INITIAL_X | |
RIG_INITIAL_Z | |
RING_OBJECT_Y | |
RING_RADIUS | |
rotation | |
RotationQuaternion | |
sceneEl | |
shader | |
side | |
sizeLabel | |
speed | |
speedRad | |
sphereColor | |
spread | |
src | |
startAngle180 | |
subPage | |
tDataForThisObject | |
text | |
textureId | |
TEXTURE_ID_PREFIX | |
thumbstickInput | |
to | |
top | |
totalLineWidthH | |
totalLineWidthV | |
TOTAL_MAIN_PAGES | |
typeLabel | |
ui | |
updatePanelDisplay -------( Function ) | |
url | |
urlToOpen | |
visible | |
width | |
x | |
y | |
ZERO_VECTOR |