Junkerposts
■
みせられない
完全に私的なメモ
├
<!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-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 rightHandEl = null;
// ★★★ パネルの表示オフセット距離 (デフォルト値を5に変更) ★★★
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 CUBE_COMMENTS = { 1: ["最初の立方体だ!", "何か秘密が隠されているかもしれない…"], 5: ["これは5番目の特別な立方体。", "長いコメントもこのように複数ページにわたって表示できます。", "素晴らしい発見だ!"], 10: ["ようこそ、宇宙の果てへ。"], 25: ["折り返し地点です。"] };
const DEFAULT_COMMENT_ARRAY = ["コメントはありません"];
// --- 定数定義 (変更なし) ---
const numSpheres = 60; const spread = 2000; 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();
// --- 立方体生成ループ (変更なし) ---
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// --- ★★★ 情報パネルの表示更新関数 (テキストスタイル適用) ★★★ ---
function updatePanelDisplay() {
if (!infoPanelEl.dataset.sphereIndex) return;
const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10);
const color = infoPanelEl.dataset.color || 'N/A';
const size = infoPanelEl.dataset.size || 'N/A';
const 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})`;
const currentStyle = PAGE_TEXT_STYLES[pageType] || PAGE_TEXT_STYLES['index']; // スタイル取得
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}`;
// troika-text属性をオブジェクトで設定(スタイルも含む)
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
});
}
// --- ★★★ 立方体クリック時の処理 (PANEL_OFFSET_FROM_OBJECT を使用) ★★★ ---
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; const newCommentsArray = JSON.parse(infoPanelEl.dataset.comments || JSON.stringify(DEFAULT_COMMENT_ARRAY)); commentSubPageIndex = Math.max(0, newCommentsArray.length - 1); } } 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>
a-farme-球に文字test41
├
afarme-test
│├
<!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>
a-farme-球に文字test40
│├
<!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>
// --- カスタムコンポーネント定義 (変更なし) ---
AFRAME.registerComponent('camera-relative-controls', {
schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true } },
init: function () { this.keys = {}; this.thumbstickInput = { x: 0, y: 0 }; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.cameraDirection = new THREE.Vector3(); this.cameraRight = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); } this.el.sceneEl.addEventListener('loaded', () => { this.leftHand = document.getElementById('leftHand'); if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); },
remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } catch(e) { console.warn("camera-relative-controls: スティックリスナー解除失敗の可能性あり"); } } },
onThumbstickMoved: function (evt) { this.thumbstickInput.x = evt.detail.x; this.thumbstickInput.y = evt.detail.y; },
tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); } if (Math.abs(this.thumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.thumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x); this.moveDirection.add(leftRight); } const isInputActive = this.moveDirection.lengthSq() > 0.0001; if (isInputActive) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; if (isInputActive) { let isOpposingInput = false; if (isCurrentlyMoving) { const dotProduct = this.currentVelocity.dot(this.moveDirection); if (dotProduct < -0.1) { isOpposingInput = true; } } if (isOpposingInput) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } },
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// --- 立方体をランダムに回転させるカスタムコンポーネント ('random-rotate') ---
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); }
});
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;" raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" laser-controls="hand: left; model: false; lineColor: white; lineOpacity: 0.75" ></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;" ></a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-cone id="prevButton" class="clickableButton" position="-20.0 0 0.1" rotation="0 0 90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-cone id="nextButton" class="clickableButton" position="20.0 0 0.1" rotation="0 0 -90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-sphere id="closeButton" class="clickableButton" position="24 11 0.1" radius="1.5" color="red" shader="flat"> </a-sphere>
</a-entity>
<script>
// --- グローバル変数とDOM要素の取得 ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
// let rightHandEl = null; // 右手イベント用は前回削除済み
// --- ★★★ 立方体ごとのコメントデータを定義 ★★★ ---
// キーは立方体のインデックス (1始まり)、値はコメント文字列
const CUBE_COMMENTS = {
1: "これは最初の立方体です!\nユニークな発見があるかも?", // \n で改行できます
5: "特別な立方体その1。\n触れてくれてありがとう。",
10: "宇宙の果てへようこそ。",
25: "折り返し地点です。",
// 他の立方体のコメントもここに追加できます
// 例: 30: "30番目の立方体です。",
};
// --- ここまでコメントデータ ---
// --- 定数定義 ---
const numSpheres = 60;
const spread = 2000;
// ★★★ PAGES 配列に 'comment' を追加 ★★★
const PAGES = ['index', 'color', 'size', 'comment'];
const TOTAL_PAGES = PAGES.length; // 自動的に4になる
// --- 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';
const commentText = infoPanelEl.dataset.commentText || "コメントはありません"; // ★ コメント取得
const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[pageIndex]; // 現在表示すべきページタイプ
// ページタイプに応じて表示するメインテキストを決定
if (pageType === 'index') { displayText = `立方体: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${color}`; }
else if (pageType === 'size') { displayText = `サイズ: ${size}`; }
else if (pageType === 'comment') { displayText = `コメント:\n${commentText}`; } // ★ コメント表示
const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; // (X/4) のようなページ番号表示
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 comment = CUBE_COMMENTS[sphereIdx] || "コメントはありません"; // コメントがなければデフォルトメッセージ
infoPanelEl.dataset.commentText = comment;
// ★★★ ここまでコメント処理 ★★★
console.log("Data (including comment) 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;
delete infoPanelEl.dataset.commentText; // ★ コメントデータも削除
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() {
const leftHandForTriggerLog = document.getElementById('leftHand');
if (leftHandForTriggerLog) {
leftHandForTriggerLog.addEventListener('triggerdown', function (evt) { console.log('Left hand TRIGGER DOWN event!', evt); const raycasterComponent = leftHandForTriggerLog.components.raycaster; if (raycasterComponent) { const intersectedEls = raycasterComponent.intersectedEls; if (intersectedEls.length > 0) { console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id'); } else { console.log('Left Trigger pressed, but no intersection.'); } } });
leftHandForTriggerLog.addEventListener('triggerup', function (evt) { console.log('Left hand TRIGGER UP event!', evt); });
} else { console.error("Could not find leftHand element to attach trigger listener."); }
});
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test38
│├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 3Dボタン</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- プレイヤー移動制御用カスタムコンポーネント ('camera-relative-controls') ---
AFRAME.registerComponent('camera-relative-controls', {
schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true } },
init: function () { this.keys = {}; this.thumbstickInput = { x: 0, y: 0 }; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.cameraDirection = new THREE.Vector3(); this.cameraRight = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); } this.el.sceneEl.addEventListener('loaded', () => { this.leftHand = document.getElementById('leftHand'); if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); },
remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } catch(e) { console.warn("camera-relative-controls: スティックリスナー解除失敗の可能性あり"); } } },
onThumbstickMoved: function (evt) { this.thumbstickInput.x = evt.detail.x; this.thumbstickInput.y = evt.detail.y; },
tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); } if (Math.abs(this.thumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.thumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x); this.moveDirection.add(leftRight); } const isInputActive = this.moveDirection.lengthSq() > 0.0001; if (isInputActive) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; if (isInputActive) { let isOpposingInput = false; if (isCurrentlyMoving) { const dotProduct = this.currentVelocity.dot(this.moveDirection); if (dotProduct < -0.1) { isOpposingInput = true; } } if (isOpposingInput) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } },
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// --- 立方体をランダムに回転させるカスタムコンポーネント ('random-rotate') ---
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); }
});
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;" raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" laser-controls="hand: left; model: false; lineColor: white; lineOpacity: 0.75" ></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;" ></a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-cone id="prevButton" class="clickableButton"
position="-20.0 0 0.1" rotation="0 0 90"
radius-bottom="1.2"
radius-top="0"
height="2.0"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-cone>
<a-cone id="nextButton" class="clickableButton"
position="20.0 0 0.1" rotation="0 0 -90"
radius-bottom="1.2"
radius-top="0"
height="2.0"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-cone>
<a-sphere id="closeButton" class="clickableButton"
position="24 11 0.1" radius="1.5" color="red" shader="flat">
</a-sphere>
</a-entity>
<script>
// --- グローバル変数とDOM要素の取得 ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
let rightHandEl = null;
// --- 定数定義 ---
const numSpheres = 60; const spread = 2000; const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3(); const PAGES = ['index', 'color', 'size']; const TOTAL_PAGES = PAGES.length;
// --- 立方体生成ループ ---
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// --- 情報パネルの表示更新関数 ---
function updatePanelDisplay() { if (!infoPanelEl.dataset.sphereIndex) return; const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10); const color = infoPanelEl.dataset.color || 'N/A'; const size = infoPanelEl.dataset.size || 'N/A'; const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let displayText = ''; const pageType = PAGES[pageIndex]; if (pageType === 'index') { displayText = `立方体: ${index}`; } else if (pageType === 'color') { displayText = `色: ${color}`; } else if (pageType === 'size') { displayText = `サイズ: ${size}`; } const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', 'value', finalDisplayText); }
// --- 立方体クリック時の処理 ---
function handleSphereClick(event) { event.stopPropagation(); console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName); const clickedCube = event.target; if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) { console.error("Cube data missing from dataset!", clickedCube.dataset); return; } console.log("Cube data found:", clickedCube.dataset); infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex; infoPanelEl.dataset.color = clickedCube.dataset.color; infoPanelEl.dataset.size = clickedCube.dataset.size; infoPanelEl.dataset.currentPageIndex = '0'; console.log("Data stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedCube.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const cubeSide = parseFloat(clickedCube.dataset.size || 0); const offsetDistance = cubeSide / 2 + 0.5; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); } catch(e) { console.error("Error during position calculation:", e); return; } infoPanelEl.setAttribute('visible', true); console.log("Panel visibility set to true. --- handleSphereClick end ---"); }
// --- パネルのボタンクリック処理 ---
prevButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex + 1) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex); });
closeButtonEl.addEventListener('click', function (event) { event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex; console.log("Close button clicked, panel hidden."); });
// --- VRモードによるマウスカーソル表示制御 ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
// --- 右手コントローラーのトリガーイベントをリッスン (デバッグ用) ---
sceneEl.addEventListener('loaded', function() {
rightHandEl = document.getElementById('rightHand'); // This was for right hand, now interaction is left.
// We don't strictly need trigger logging if laser-controls works.
// Let's get leftHandEl for consistency if needed.
const leftHandForTriggerLog = document.getElementById('leftHand');
if (leftHandForTriggerLog) {
leftHandForTriggerLog.addEventListener('triggerdown', function (evt) {
console.log('Left hand TRIGGER DOWN event!', evt);
const raycasterComponent = leftHandForTriggerLog.components.raycaster;
if (raycasterComponent) {
const intersectedEls = raycasterComponent.intersectedEls;
if (intersectedEls.length > 0) {
console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id');
} else {
console.log('Left Trigger pressed, but no intersection.');
}
}
});
leftHandForTriggerLog.addEventListener('triggerup', function (evt) {
console.log('Left hand TRIGGER UP event!', evt);
});
} else {
console.error("Could not find leftHand element to attach trigger listener.");
}
});
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test37
│├
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
</head>
<body style="background-color: black;">
<a-scene background="color: black">
<!-- カメラ(VR視点) -->
<a-entity camera position="0 1.6 0"></a-entity>
<!-- 左コントローラーを表示 -->
<a-entity
id="leftController"
tracked-controls="controller: oculus-touch; hand: left"
raycaster="objects: .clickable"
laser-controls="hand: left"
position="-0.3 1.4 -0.5">
</a-entity>
<!-- 立方体 -->
<a-box
class="clickable"
position="0 1 -3"
color="blue"></a-box>
<!-- クリック後に表示されるテキストウィンドウ -->
<a-entity id="textWindow" position="0 1 -2" visible="false">
<a-plane width="1.5" height="1" color="black">
<a-text value="こんにちは!" position="0 0 0.1" align="center"></a-text>
</a-plane>
</a-entity>
<script>
const cube = document.querySelector('.clickable');
const textWindow = document.getElementById('textWindow');
// コントローラーによるクリック
cube.addEventListener('click', () => {
textWindow.setAttribute('visible', true);
});
// PCマウスクリック対応
cube.addEventListener('mousedown', () => {
textWindow.setAttribute('visible', true);
});
</script>
</a-scene>
</body>
</html>
aframe-co-t2
│├
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
</head>
<body style="background-color: black;">
<a-scene background="color: black">
<!-- カメラとコントローラー -->
<a-entity camera position="0 1.6 0">
<a-entity
id="leftController"
laser-controls="hand: left"
raycaster="objects: .clickable"></a-entity>
</a-entity>
<!-- 立方体 -->
<a-box
class="clickable"
position="0 1 -3"
color="blue"></a-box>
<!-- クリック後に表示されるテキストウィンドウ -->
<a-entity id="textWindow" position="0 1 -2" visible="false">
<a-plane width="1.5" height="1" color="black">
<a-text value="こんにちは!" position="0 0 0.1" align="center"></a-text>
</a-plane>
</a-entity>
<script>
const cube = document.querySelector('.clickable');
const textWindow = document.getElementById('textWindow');
// コントローラーによるクリック
cube.addEventListener('click', () => {
textWindow.setAttribute('visible', true);
});
// マウスクリック対応(PC用)
cube.addEventListener('mouseenter', () => {
cube.setAttribute('color', 'red'); // ホバー時に色変更
});
cube.addEventListener('mouseleave', () => {
cube.setAttribute('color', 'blue'); // 元の色に戻す
});
cube.addEventListener('mousedown', () => {
textWindow.setAttribute('visible', true);
});
</script>
</a-scene>
</body>
</html>
aframe-co-t1
│├
<!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') ---
// (このコンポーネントの定義は前回から変更ありません。コメントは主要な部分に絞ります)
AFRAME.registerComponent('camera-relative-controls', {
schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true } },
init: function () { this.keys = {}; this.thumbstickInput = { x: 0, y: 0 }; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.cameraDirection = new THREE.Vector3(); this.cameraRight = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); } this.el.sceneEl.addEventListener('loaded', () => { this.leftHand = document.getElementById('leftHand'); if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); },
remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } catch(e) { console.warn("camera-relative-controls: スティックリスナー解除失敗の可能性あり"); } } },
onThumbstickMoved: function (evt) { this.thumbstickInput.x = evt.detail.x; this.thumbstickInput.y = evt.detail.y; },
tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); } if (Math.abs(this.thumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.thumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x); this.moveDirection.add(leftRight); } const isInputActive = this.moveDirection.lengthSq() > 0.0001; if (isInputActive) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; if (isInputActive) { let isOpposingInput = false; if (isCurrentlyMoving) { const dotProduct = this.currentVelocity.dot(this.moveDirection); if (dotProduct < -0.1) { isOpposingInput = true; } } if (isOpposingInput) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } },
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// --- 立方体をランダムに回転させるカスタムコンポーネント ('random-rotate') ---
// (このコンポーネントの定義は前回から変更ありません)
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); }
});
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor"
cursor="rayOrigin: mouse; fuse: false;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000;"
position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;"
material="color: black; shader: flat; opacity: 0.7">
<a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation>
</a-entity>
</a-entity>
<a-entity id="leftHand"
oculus-touch-controls="hand: left; model: true;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;"
laser-controls="hand: left; model: false; lineColor: white; lineOpacity: 0.75"
>
</a-entity>
<a-entity id="rightHand"
oculus-touch-controls="hand: right; model: true;"
>
</a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-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; const spread = 2000; const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3(); const PAGES = ['index', 'color', 'size']; const TOTAL_PAGES = PAGES.length;
// --- 立方体生成ループ (変更なし) ---
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// --- 情報パネルの表示更新関数 (変更なし) ---
function updatePanelDisplay() { if (!infoPanelEl.dataset.sphereIndex) return; const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10); const color = infoPanelEl.dataset.color || 'N/A'; const size = infoPanelEl.dataset.size || 'N/A'; const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let displayText = ''; const pageType = PAGES[pageIndex]; if (pageType === 'index') { displayText = `立方体: ${index}`; } else if (pageType === 'color') { displayText = `色: ${color}`; } else if (pageType === 'size') { displayText = `サイズ: ${size}`; } const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', 'value', finalDisplayText); }
// --- 立方体クリック時の処理 (変更なし) ---
function handleSphereClick(event) { event.stopPropagation(); console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName); const clickedCube = event.target; if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) { console.error("Cube data missing from dataset!", clickedCube.dataset); return; } console.log("Cube data found:", clickedCube.dataset); infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex; infoPanelEl.dataset.color = clickedCube.dataset.color; infoPanelEl.dataset.size = clickedCube.dataset.size; infoPanelEl.dataset.currentPageIndex = '0'; console.log("Data stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedCube.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const cubeSide = parseFloat(clickedCube.dataset.size || 0); const offsetDistance = cubeSide / 2 + 0.5; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); } catch(e) { console.error("Error during position calculation:", e); return; } infoPanelEl.setAttribute('visible', true); console.log("Panel visibility set to true. --- handleSphereClick end ---"); }
// --- パネルのボタンクリック処理 (変更なし) ---
prevButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex + 1) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex); });
closeButtonEl.addEventListener('click', function (event) { event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex; console.log("Close button clicked, panel hidden."); });
// --- VRモードによるマウスカーソル表示制御 (変更なし) ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
// 右手トリガーイベントのリスナーは、laser-controlsがクリックを処理するので一旦削除
// sceneEl.addEventListener('loaded', function() { ... });
// 背景クリックリスナーは削除済み
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test36
│├
<!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>
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') - 完全版 ★★★ ---
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コントローラのサムスティック入力値
// 各種ベクトルを事前に生成して再利用 (パフォーマンスのため)
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 requires a child entity with the [camera] component.');
}
// --- VRコントローラーのセットアップ ---
// 少し待ってからコントローラーを取得・設定 (シーンの準備完了後の方が確実)
this.el.sceneEl.addEventListener('loaded', () => {
this.leftHand = document.getElementById('leftHand'); // 左手コントローラーのエンティティ
if (this.leftHand) {
// 左手スティックの動きを監視
this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} else {
console.warn("Left hand controller (leftHand) not found for thumbstick movement.");
}
});
// --- キーボードイベントリスナーのセットアップ ---
this.onKeyDown = this.onKeyDown.bind(this); // thisの束縛
this.onKeyUp = this.onKeyUp.bind(this); // thisの束縛
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
},
// コンポーネント削除時の処理
remove: function () {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
// リスナーを削除する際も leftHand が存在するか確認
if (this.leftHand) {
try {
this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} catch(e) {
// removeEventListener が失敗してもエラーを出さないようにする (bindされた関数が原因の場合)
// より堅牢にするなら、bindした関数をプロパティに保存しておく
console.log("Could not remove thumbstickmoved listener cleanly.");
}
}
},
// 左手コントローラーのサムスティックが動いた時の処理
onThumbstickMoved: function (evt) {
// スティックのX軸(左右)とY軸(前後)の入力値を取得
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; // まだ準備できていない
}
}
// cameraEl や object3D がまだ存在しない場合は早期リターン
if (!this.cameraEl || !this.cameraEl.object3D) { return; }
const el = this.el; // このコンポーネントがアタッチされているエンティティ (rig)
const data = this.data; // スキーマで定義されたパラメータ
const position = el.object3D.position; // rigの現在位置
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); }
// VRサムスティック入力
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; }
// --- 目標速度と補間係数決定ここまで ---
// --- 現在速度を目標速度に向けて滑らかに補間 (Lerp) ---
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]; } }
});
// --- ここまでカメラ移動コンポーネント ---
// --- ランダム回転コンポーネント (変更なし) ---
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;" cursor="rayOrigin: entity; fuse: false;"
line="color: white; opacity: 0.5; length: 3">
</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>
// --- グローバル変数と初期設定 (変更なし) ---
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 numSpheres = 60; const spread = 2000; const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3(); const PAGES = ['index', 'color', 'size']; const TOTAL_PAGES = PAGES.length;
// --- 立方体生成ループ (変更なし) ---
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// --- 情報パネルの表示更新関数 (変更なし) ---
function updatePanelDisplay() { if (!infoPanelEl.dataset.sphereIndex) return; const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10); const color = infoPanelEl.dataset.color || 'N/A'; const size = infoPanelEl.dataset.size || 'N/A'; const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let displayText = ''; const pageType = PAGES[pageIndex]; if (pageType === 'index') { displayText = `立方体: ${index}`; } else if (pageType === 'color') { displayText = `色: ${color}`; } else if (pageType === 'size') { displayText = `サイズ: ${size}`; } const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', 'value', finalDisplayText); }
// --- 立方体クリック時の処理 (変更なし) ---
function handleSphereClick(event) { event.stopPropagation(); console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName); const clickedCube = event.target; if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) { console.error("Cube data missing from dataset!", clickedCube.dataset); return; } console.log("Cube data found:", clickedCube.dataset); infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex; infoPanelEl.dataset.color = clickedCube.dataset.color; infoPanelEl.dataset.size = clickedCube.dataset.size; infoPanelEl.dataset.currentPageIndex = '0'; console.log("Data stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedCube.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const cubeSide = parseFloat(clickedCube.dataset.size || 0); const offsetDistance = cubeSide / 2 + 0.5; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); } catch(e) { console.error("Error during position calculation:", e); return; } infoPanelEl.setAttribute('visible', true); console.log("Panel visibility set to true. --- handleSphereClick end ---"); }
// --- パネルのボタンクリック処理 (変更なし) ---
prevButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex + 1) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex); });
closeButtonEl.addEventListener('click', function (event) { event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex; console.log("Close button clicked, panel hidden."); });
// --- VRモードによるマウスカーソル表示制御 (変更なし) ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
// --- マウスカーソル制御ここまで ---
// 背景クリックリスナーは削除済み
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test34
│├
<!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') ---
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コントローラのサムスティック入力値
// 各種ベクトルを事前に生成して再利用 (パフォーマンスのため)
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 requires a child entity with the [camera] component.');
}
// --- VRコントローラーのセットアップ ---
this.leftHand = document.getElementById('leftHand'); // 左手コントローラーのエンティティ
if (this.leftHand) {
// 左手スティックの動きを監視
this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} else {
console.warn("Left hand controller (leftHand) not found for thumbstick movement.");
}
// --- キーボードイベントリスナーのセットアップ ---
this.onKeyDown = this.onKeyDown.bind(this); // thisの束縛
this.onKeyUp = this.onKeyUp.bind(this); // 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) {
this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
}
},
// 左手コントローラーのサムスティックが動いた時の処理
onThumbstickMoved: function (evt) {
// スティックのX軸(左右)とY軸(前後)の入力値を取得
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; // このコンポーネントがアタッチされているエンティティ (rig)
const data = this.data; // スキーマで定義されたパラメータ
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();
// 右方向 (カメラのローカルX軸がワールド右方向に対応、Y軸は無視して水平移動)
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(); // 入力があれば正規化
}
// --- 目標速度と補間係数の決定 (慣性とブレーキ) ---
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) { // 内積が負なら逆方向への入力と判断 (閾値-0.1)
isOpposingInput = true;
}
}
if (isOpposingInput) { // 逆方向への入力の場合 (ブレーキ)
this.desiredVelocity.copy(this.ZERO_VECTOR); // 目標速度を0に
lerpFactor = data.brakingDeceleration; // ブレーキ用の減速係数を使用
} else { // 順方向または停止からの入力の場合 (加速)
this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); // 目標速度を設定
lerpFactor = data.acceleration; // 加速用の係数を使用
}
} else { // 入力がない場合 (自然減速)
this.desiredVelocity.copy(this.ZERO_VECTOR); // 目標速度を0に
lerpFactor = data.damping; // 自然減速用の係数を使用
}
// --- 目標速度と補間係数決定ここまで ---
// --- 現在速度を目標速度に向けて滑らかに補間 (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; }
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];
}
}
});
// --- ランダム回転コンポーネント (変更なし) ---
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;" cursor="rayOrigin: entity; fuse: false;" line="color: white; opacity: 0.5; length: 3" >
</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>
// --- グローバル変数と初期設定 ---
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 numSpheres = 60;
const spread = 2000;
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3(); // パネル位置計算用
const panelPosition = new THREE.Vector3();
const PAGES = ['index', 'color', 'size'];
const TOTAL_PAGES = PAGES.length;
// --- グローバル変数ここまで ---
// --- 立方体(旧球体)の生成ループ (変更なし) ---
for (let i = 0; i < numSpheres; i++) {
const cubeEl = document.createElement('a-box');
const side = Math.random() * 10.0 + 0.5;
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + side / 2;
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 50%, 75%)`;
const sphereIndex = i + 1;
cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side);
cubeEl.setAttribute('color', color);
cubeEl.setAttribute('position', { x: x, y: y, z: z });
cubeEl.classList.add('clickableObject');
cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2);
cubeEl.setAttribute('random-rotate', {maxSpeed: 5});
cubeEl.addEventListener('click', handleSphereClick);
sceneEl.appendChild(cubeEl);
}
// --- 立方体生成ここまで ---
// --- 情報パネルの表示更新関数 (変更なし) ---
function updatePanelDisplay() {
if (!infoPanelEl.dataset.sphereIndex) return;
const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10);
const color = infoPanelEl.dataset.color || 'N/A';
const size = infoPanelEl.dataset.size || 'N/A';
const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[pageIndex];
if (pageType === 'index') { displayText = `立方体: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${color}`; }
else if (pageType === 'size') { displayText = `サイズ: ${size}`; }
const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`;
const finalDisplayText = `${pageIndicator}\n${displayText}`;
panelTextEl.setAttribute('troika-text', 'value', finalDisplayText);
}
// --- パネル表示更新ここまで ---
// --- 立方体クリック時の処理 (変更なし) ---
function handleSphereClick(event) {
event.stopPropagation(); // イベントのバブリングを停止
console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName);
const clickedCube = event.target;
if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) { console.error("Cube data missing from dataset!", clickedCube.dataset); return; }
console.log("Cube data found:", clickedCube.dataset);
infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex;
infoPanelEl.dataset.color = clickedCube.dataset.color;
infoPanelEl.dataset.size = clickedCube.dataset.size;
infoPanelEl.dataset.currentPageIndex = '0';
console.log("Data stored in panel dataset.");
try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); }
catch (e) { console.error("Error during updatePanelDisplay:", e); return; }
try {
clickedCube.object3D.getWorldPosition(targetWorldPosition);
cameraEl.object3D.getWorldPosition(cameraWorldPosition);
const cubeSide = parseFloat(clickedCube.dataset.size || 0);
const offsetDistance = cubeSide / 2 + 0.5; // 立方体の中心からのオフセット
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition);
console.log("Panel position calculated and applied.");
}
catch(e) { console.error("Error during position calculation:", e); return; }
infoPanelEl.setAttribute('visible', true);
console.log("Panel visibility set to true. --- handleSphereClick end ---");
}
// --- 立方体クリック処理ここまで ---
// --- パネルのボタンクリック処理 (変更なし) ---
prevButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return;
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES;
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex);
});
nextButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return;
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex + 1) % TOTAL_PAGES;
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex);
});
closeButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); infoPanelEl.setAttribute('visible', false);
delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color;
delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex;
console.log("Close button clicked, panel hidden.");
});
// --- パネルボタン処理ここまで ---
// --- VRモードによるマウスカーソル表示制御 ---
sceneEl.addEventListener('enter-vr', function () {
console.log("Entered VR mode");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); // VRモードではマウスカーソルを非表示
// 必要であれば、VRコントローラーのraycasterを有効にするなどの処理
if (document.getElementById('rightHand')) {
document.getElementById('rightHand').setAttribute('raycaster', 'enabled', true);
}
});
sceneEl.addEventListener('exit-vr', function () {
console.log("Exited VR mode");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); // VRモードを抜けたらマウスカーソルを再表示
// VRコントローラーのraycasterを無効にするなど
if (document.getElementById('rightHand')) {
document.getElementById('rightHand').setAttribute('raycaster', 'enabled', false);
}
});
// --- マウスカーソル制御ここまで ---
// 背景クリックリスナーは削除済み
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test33
│├
<!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://cdn.jsdelivr.net/npm/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-troika-text@0.11.0/dist/aframe-troika-text.min.js"></script>
<script>
// --- ★★★ カメラ移動コンポーネント (VR対応版) ★★★ ---
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.currentVelocity = new THREE.Vector3();
this.ZERO_VECTOR = new THREE.Vector3();
this.direction = new THREE.Vector3();
this.rightDirection = 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 requires a child entity with the [camera] component.');
}
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);
},
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.direction.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion);
if (this.direction.lengthSq() > 0.0001) this.direction.normalize();
this.rightDirection.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion);
this.rightDirection.y = 0;
if (this.rightDirection.lengthSq() > 0.0001) this.rightDirection.normalize();
this.moveDirection.set(0, 0, 0);
if (this.keys['KeyW'] || this.keys['ArrowUp']) this.moveDirection.add(this.direction);
if (this.keys['KeyS'] || this.keys['ArrowDown']) this.moveDirection.sub(this.direction);
if (this.keys['KeyA'] || this.keys['ArrowLeft']) this.moveDirection.sub(this.rightDirection);
if (this.keys['KeyD'] || this.keys['ArrowRight']) this.moveDirection.add(this.rightDirection);
// --- VRコントローラー対応の移動処理追加 ---
const leftController = this.el.sceneEl.querySelector('[hand-controls="hand: left"]');
if (leftController && leftController.components['tracked-controls']) {
const gamepad = leftController.components['tracked-controls'].controller;
if (gamepad && gamepad.axes.length >= 2) {
const x = gamepad.axes[0];
const y = gamepad.axes[1];
if (Math.abs(x) > 0.1 || Math.abs(y) > 0.1) {
const moveX = this.rightDirection.clone().multiplyScalar(x);
const moveZ = this.direction.clone().multiplyScalar(-y);
this.moveDirection.add(moveX).add(moveZ);
}
}
}
// --- ここまでVR対応追加 ---
const isKeyPressed = this.moveDirection.lengthSq() > 0.0001;
if (isKeyPressed) this.moveDirection.normalize();
let lerpFactor = data.damping;
const isMoving = this.currentVelocity.lengthSq() > 0.01;
if (isKeyPressed) {
let isOpposing = false;
if (isMoving) {
const dot = this.currentVelocity.dot(this.moveDirection);
if (dot < -0.1) isOpposing = true;
}
if (isOpposing) {
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];
}
}
});
// --- ここまでカメラ移動コンポーネント ---
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">
<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 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>
<!-- VRハンドコントローラーの追加 -->
<a-entity hand-controls="hand: left"></a-entity>
<a-entity hand-controls="hand: right"></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>
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 numSpheres = 60;
const spread = 2000;
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3();
const panelPosition = new THREE.Vector3();
const PAGES = ['index', 'color', 'size'];
const TOTAL_PAGES = PAGES.length;
for (let i = 0; i < numSpheres; i++) {
const cubeEl = document.createElement('a-box');
const side = Math.random() * 10.0 + 0.5;
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + side / 2;
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 50%, 75%)`;
const sphereIndex = i + 1;
cubeEl.setAttribute('width', side);
cubeEl.setAttribute('height', side);
cubeEl.setAttribute('depth', side);
cubeEl.setAttribute('color', color);
cubeEl.setAttribute('position', { x: x, y: y, z: z });
cubeEl.classList.add('clickableObject');
cubeEl.dataset.sphereIndex = sphereIndex;
cubeEl.dataset.color = color;
cubeEl.dataset.size = side.toFixed(2);
cubeEl.setAttribute('random-rotate', { maxSpeed: 5 });
cubeEl.addEventListener('click', handleSphereClick);
sceneEl.appendChild(cubeEl);
}
function updatePanelDisplay() {
if (!infoPanelEl.dataset.sphereIndex) return;
const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10);
const color = infoPanelEl.dataset.color || 'N/A';
const size = infoPanelEl.dataset.size || 'N/A';
const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[pageIndex];
if (pageType === 'index') displayText = `立方体: ${index}`;
else if (pageType === 'color') displayText = `色: ${color}`;
else if (pageType === 'size') displayText = `サイズ: ${size}`;
const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`;
const finalDisplayText = `${pageIndicator}\n${displayText}`;
panelTextEl.setAttribute('troika-text', 'value', finalDisplayText);
}
function handleSphereClick(event) {
event.stopPropagation();
const clickedCube = event.target;
if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) return;
infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex;
infoPanelEl.dataset.color = clickedCube.dataset.color;
infoPanelEl.dataset.size = clickedCube.dataset.size;
infoPanelEl.dataset.currentPageIndex = '0';
updatePanelDisplay();
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);
infoPanelEl.setAttribute('visible', true);
}
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();
});
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();
});
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;
});
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test-gpt-v2
│├
<!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>
// --- ★★★ カメラ移動コンポーネント (完全版) ★★★ ---
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.currentVelocity = new THREE.Vector3();
this.ZERO_VECTOR = new THREE.Vector3();
this.direction = new THREE.Vector3();
this.rightDirection = 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 requires a child entity with the [camera] component.'); }
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);
},
// フレーム毎の処理 (移動計算)
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.direction.set(0, 0, -1); this.direction.applyQuaternion(this.cameraWorldQuaternion); if (this.direction.lengthSq() > 0.0001) this.direction.normalize();
this.rightDirection.set(1, 0, 0); this.rightDirection.applyQuaternion(this.cameraWorldQuaternion); this.rightDirection.y = 0; if (this.rightDirection.lengthSq() > 0.0001) this.rightDirection.normalize();
this.moveDirection.set(0, 0, 0);
if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.direction); }
if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.direction); }
if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.rightDirection); }
if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.rightDirection); }
const isKeyPressed = this.moveDirection.lengthSq() > 0.0001;
if (isKeyPressed) { this.moveDirection.normalize(); }
let lerpFactor = data.damping;
const isMoving = this.currentVelocity.lengthSq() > 0.01;
if (isKeyPressed) {
let isOpposing = false;
if (isMoving) {
const dot = this.currentVelocity.dot(this.moveDirection);
if (dot < -0.1) { isOpposing = true; }
}
if (isOpposing) {
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];
}
}
});
// --- ここまでカメラ移動コンポーネント ---
// --- ランダム回転コンポーネント (変更なし) ---
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">
<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 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>
<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>
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 numSpheres = 60;
const spread = 2000;
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3();
const panelPosition = new THREE.Vector3();
const PAGES = ['index', 'color', 'size'];
const TOTAL_PAGES = PAGES.length;
// 立方体生成ループ (変更なし)
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// パネル表示更新関数 (変更なし)
function updatePanelDisplay() { if (!infoPanelEl.dataset.sphereIndex) return; const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10); const color = infoPanelEl.dataset.color || 'N/A'; const size = infoPanelEl.dataset.size || 'N/A'; const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let displayText = ''; const pageType = PAGES[pageIndex]; if (pageType === 'index') { displayText = `立方体: ${index}`; } else if (pageType === 'color') { displayText = `色: ${color}`; } else if (pageType === 'size') { displayText = `サイズ: ${size}`; } const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', 'value', finalDisplayText); }
// handleSphereClick (変更なし)
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."); });
// 背景クリックリスナーは削除済み
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test32
│├
<!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>
// --- カスタムコンポーネント定義 (変更なし) ---
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.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.direction = new THREE.Vector3(); this.rightDirection = 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 requires a child entity with the [camera] component.'); } 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); },
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.direction.set(0, 0, -1); this.direction.applyQuaternion(this.cameraWorldQuaternion); if (this.direction.lengthSq() > 0.0001) this.direction.normalize(); this.rightDirection.set(1, 0, 0); this.rightDirection.applyQuaternion(this.cameraWorldQuaternion); this.rightDirection.y = 0; if (this.rightDirection.lengthSq() > 0.0001) this.rightDirection.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.direction); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.direction); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.rightDirection); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.rightDirection); } const isKeyPressed = this.moveDirection.lengthSq() > 0.0001; if (isKeyPressed) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isMoving = this.currentVelocity.lengthSq() > 0.01; if (isKeyPressed) { let isOpposing = false; if (isMoving) { const dot = this.currentVelocity.dot(this.moveDirection); if (dot < -0.1) { isOpposing = true; } } if (isOpposing) { 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]; } }
});
// --- ここまでカスタムコンポーネント定義 ---
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000">
<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 cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableSphere, .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>
<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="2.5" height="1.2" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.24; maxWidth: 2.3; align: center; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
<a-triangle id="prevButton" class="clickableButton"
position="-1.0 0 0.01" rotation="0 0 90" scale="0.2 0.2 0.2"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-triangle>
<a-triangle id="nextButton" class="clickableButton"
position="1.0 0 0.01" rotation="0 0 -90" scale="0.2 0.2 0.2"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-triangle>
</a-entity>
<script>
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 numSpheres = 60;
const spread = 2000;
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3();
const panelPosition = new THREE.Vector3();
const PAGES = ['index', 'color', 'radius'];
const TOTAL_PAGES = PAGES.length;
// 球体生成 (変更なし)
for (let i = 0; i < numSpheres; i++) { /* ... */ const sphereEl = document.createElement('a-sphere'); const radius = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + radius; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; sphereEl.setAttribute('radius', radius); sphereEl.setAttribute('color', color); sphereEl.setAttribute('position', { x: x, y: y, z: z }); sphereEl.classList.add('clickableSphere'); sphereEl.dataset.sphereIndex = sphereIndex; sphereEl.dataset.color = color; sphereEl.dataset.radius = radius.toFixed(2); sphereEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(sphereEl); }
// パネル表示更新関数 (変更なし)
function updatePanelDisplay() { if (!infoPanelEl.dataset.sphereIndex) return; const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10); const color = infoPanelEl.dataset.color || 'N/A'; const radius = infoPanelEl.dataset.radius || 'N/A'; const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let displayText = ''; const pageType = PAGES[pageIndex]; if (pageType === 'index') { displayText = `球: ${index}`; } else if (pageType === 'color') { displayText = `色: ${color}`; } else if (pageType === 'radius') { displayText = `半径: ${radius}`; } const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', 'value', finalDisplayText); }
// handleSphereClick (stopPropagation を含む)
function handleSphereClick(event) {
event.stopPropagation(); // イベント伝播停止
console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName);
const clickedSphere = event.target;
if (!clickedSphere.dataset.sphereIndex || !clickedSphere.dataset.color || !clickedSphere.dataset.radius) { console.error("Sphere data missing!"); return; }
console.log("Sphere data found:", clickedSphere.dataset);
infoPanelEl.dataset.sphereIndex = clickedSphere.dataset.sphereIndex;
infoPanelEl.dataset.color = clickedSphere.dataset.color;
infoPanelEl.dataset.radius = clickedSphere.dataset.radius;
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 { clickedSphere.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const sphereRadius = parseFloat(clickedSphere.dataset.radius || 0); const offsetDistance = sphereRadius + 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 ---");
}
// ボタンクリック時の処理 (stopPropagation を含む)
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);
});
// --- ★★★ 背景クリックでパネルを隠す処理を一時的に全体コメントアウト ★★★ ---
/*
sceneEl.addEventListener('click', function(event) {
if (!event.target.classList.contains('clickableSphere') &&
!event.target.classList.contains('clickableButton')) {
infoPanelEl.setAttribute('visible', false);
delete infoPanelEl.dataset.sphereIndex;
delete infoPanelEl.dataset.color;
delete infoPanelEl.dataset.radius;
delete infoPanelEl.dataset.currentPageIndex;
console.log("Panel hidden by background click.");
}
});
*/
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test29
│├
<!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>
// --- カスタムコンポーネント定義 (変更なし) ---
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.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.direction = new THREE.Vector3(); this.rightDirection = 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 requires a child entity with the [camera] component.'); } 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); },
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.direction.set(0, 0, -1); this.direction.applyQuaternion(this.cameraWorldQuaternion); if (this.direction.lengthSq() > 0.0001) this.direction.normalize(); this.rightDirection.set(1, 0, 0); this.rightDirection.applyQuaternion(this.cameraWorldQuaternion); this.rightDirection.y = 0; if (this.rightDirection.lengthSq() > 0.0001) this.rightDirection.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.direction); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.direction); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.rightDirection); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.rightDirection); } const isKeyPressed = this.moveDirection.lengthSq() > 0.0001; if (isKeyPressed) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isMoving = this.currentVelocity.lengthSq() > 0.01; if (isKeyPressed) { let isOpposing = false; if (isMoving) { const dot = this.currentVelocity.dot(this.moveDirection); if (dot < -0.1) { isOpposing = true; } } if (isOpposing) { 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]; } }
});
// --- ここまでカスタムコンポーネント定義 ---
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000">
<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 cursor="rayOrigin: mouse; fuse: false;" raycaster="objects:.clickableSphere; 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>
<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="2.5" height="1.2" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.08; maxWidth: 2.3; align: left; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
</a-entity>
<script>
// --- 球体生成などのスクリプト (変更なし) ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const numSpheres = 60;
const spread = 2000;
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3();
const panelPosition = new THREE.Vector3();
for (let i = 0; i < numSpheres; i++) {
const sphereEl = document.createElement('a-sphere');
const radius = Math.random() * 4.0 + 2.0;
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + radius;
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 50%, 75%)`;
sphereEl.setAttribute('radius', radius);
sphereEl.setAttribute('color', color);
sphereEl.setAttribute('position', { x: x, y: y, z: z });
sphereEl.classList.add('clickableSphere');
sphereEl.setAttribute('data-info', `球 ${i + 1}\n色: ${color}\n半径: ${radius.toFixed(2)}`);
sphereEl.addEventListener('click', handleSphereClick);
sceneEl.appendChild(sphereEl);
}
function handleSphereClick(event) {
const clickedSphere = event.target;
const sphereInfo = clickedSphere.dataset.info;
clickedSphere.object3D.getWorldPosition(targetWorldPosition);
cameraEl.object3D.getWorldPosition(cameraWorldPosition);
const sphereRadius = clickedSphere.getAttribute('geometry').radius;
const offsetDistance = sphereRadius + 0.5;
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition);
panelTextEl.setAttribute('troika-text', 'value', sphereInfo);
infoPanelEl.setAttribute('visible', true);
}
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test18
│├
<!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>
// --- カスタムコンポーネント定義 (変更なし) ---
AFRAME.registerComponent('camera-relative-controls', {
schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, enabled: { type: 'boolean', default: true } },
init: function () { this.keys = {}; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.direction = new THREE.Vector3(); this.rightDirection = 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 requires a child entity with the [camera] component.'); } 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); },
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.direction.set(0, 0, -1); this.direction.applyQuaternion(this.cameraWorldQuaternion); if (this.direction.lengthSq() > 0.0001) this.direction.normalize(); this.rightDirection.set(1, 0, 0); this.rightDirection.applyQuaternion(this.cameraWorldQuaternion); this.rightDirection.y = 0; if (this.rightDirection.lengthSq() > 0.0001) this.rightDirection.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.direction); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.direction); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.rightDirection); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.rightDirection); } if (this.moveDirection.lengthSq() > 0.0001) { this.moveDirection.normalize(); this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); } else { this.desiredVelocity.copy(this.ZERO_VECTOR); } const lerpFactor = (this.moveDirection.lengthSq() > 0.0001) ? data.acceleration : 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]; } }
});
// --- ここまでカスタムコンポーネント定義 ---
</script>
</head>
<body>
<a-scene id="myScene" background="color: #ECECEC">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 200; damping: 2">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity cursor="rayOrigin: mouse; fuse: false;" raycaster="objects:.clickableSphere; 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>
<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="2.5" height="1.2" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.08; maxWidth: 2.3; align: left; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
</a-entity>
<script>
// --- 球体生成などのスクリプト ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const numSpheres = 30; // 数は変えない
const spread = 2000; // ★★★ 範囲を 20 * 100 = 2000 に変更 ★★★
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 sphereEl = document.createElement('a-sphere');
// ★★★ 半径の計算式を変更 (範囲 2.0 ~ 6.0) ★★★
const radius = Math.random() * 4.0 + 2.0;
// ★★★ 位置計算は新しい spread と radius を使う ★★★
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + radius; // Y方向にも広がる
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 70%, 60%)`;
sphereEl.setAttribute('radius', radius);
sphereEl.setAttribute('color', color);
sphereEl.setAttribute('position', { x: x, y: y, z: z });
sphereEl.classList.add('clickableSphere');
sphereEl.setAttribute('data-info', `球 ${i + 1}\n色: ${color}\n半径: ${radius.toFixed(2)}`);
sphereEl.addEventListener('click', handleSphereClick);
sceneEl.appendChild(sphereEl);
}
// 球体クリック時の処理関数 (変更なし)
function handleSphereClick(event) {
const clickedSphere = event.target;
const sphereInfo = clickedSphere.dataset.info;
clickedSphere.object3D.getWorldPosition(targetWorldPosition);
cameraEl.object3D.getWorldPosition(cameraWorldPosition);
const sphereRadius = clickedSphere.getAttribute('geometry').radius;
const offsetDistance = sphereRadius + 0.5;
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition);
panelTextEl.setAttribute('troika-text', 'value', sphereInfo);
infoPanelEl.setAttribute('visible', true);
}
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test14
│├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - Troika Text 日本語表示 (エラー対策強化版)</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>
// --- カスタムコンポーネント定義 (tick内の準備完了チェックを強化) ---
AFRAME.registerComponent('camera-relative-controls', {
schema: { speed: { type: 'number', default: 3 }, enabled: { type: 'boolean', default: true } },
init: function () {
this.keys = {};
this.velocity = new THREE.Vector3();
this.direction = new THREE.Vector3();
this.cameraWorldQuaternion = new THREE.Quaternion();
this.rightDirection = new THREE.Vector3();
this.cameraEl = this.el.querySelector('[camera]'); // 先に取得試行
this.isReady = false; // ★★★ 準備完了フラグを追加 ★★★
if (!this.cameraEl) { console.error('camera-relative-controls requires a child entity with the [camera] component.'); }
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); },
tick: function (time, timeDelta) {
if (!this.data.enabled) return;
// ▼▼▼ 準備完了チェック ▼▼▼
if (!this.isReady) {
// cameraEl と object3D と matrixWorld が存在するか確認
if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) {
this.isReady = true; // 準備OKとする
} else {
// もし init で cameraEl が見つからなかった場合に再試行 (念のため)
if (!this.cameraEl) {
this.cameraEl = this.el.querySelector('[camera]');
}
return; // まだ準備できていないので tick 処理を中断
}
}
// ▲▲▲ 準備完了チェック ▲▲▲
// --- 準備完了後の処理 (元のtick処理) ---
// 念のためここでもチェック (より安全に)
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.direction.set(0, 0, -1);
this.direction.applyQuaternion(this.cameraWorldQuaternion);
if (this.direction.lengthSq() > 0.0001) this.direction.normalize();
this.rightDirection.set(1, 0, 0);
this.rightDirection.applyQuaternion(this.cameraWorldQuaternion);
this.rightDirection.y = 0;
if (this.rightDirection.lengthSq() > 0.0001) this.rightDirection.normalize();
this.velocity.set(0, 0, 0);
if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.velocity.add(this.direction); }
if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.velocity.sub(this.direction); }
if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.velocity.sub(this.rightDirection); }
if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.velocity.add(this.rightDirection); }
if (this.velocity.lengthSq() > 0.0001) {
this.velocity.normalize().multiplyScalar(data.speed * dt);
position.add(this.velocity);
}
},
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]; } }
});
// --- ここまでカスタムコンポーネント定義 ---
</script>
</head>
<body>
<a-scene id="myScene" background="color: #ECECEC">
<a-entity id="rig" position="0 0 5" camera-relative-controls="speed: 3">
<a-entity id="camera" camera look-controls position="0 1.6 0">
<a-entity cursor="rayOrigin: mouse; fuse: false;" raycaster="objects:.clickableSphere; far: 15;" 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>
<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="2.5" height="1.2" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.08; maxWidth: 2.3; align: left; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
</a-entity>
<script>
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const numSpheres = 30;
const spread = 20;
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 sphereEl = document.createElement('a-sphere');
const radius = Math.random() * 0.4 + 0.2;
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + radius;
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 70%, 60%)`;
sphereEl.setAttribute('radius', radius);
sphereEl.setAttribute('color', color);
sphereEl.setAttribute('position', { x: x, y: y, z: z });
sphereEl.classList.add('clickableSphere');
sphereEl.setAttribute('data-info', `球 ${i + 1}\n色: ${color}\n半径: ${radius.toFixed(2)}`);
sphereEl.addEventListener('click', handleSphereClick);
sceneEl.appendChild(sphereEl);
}
function handleSphereClick(event) {
const clickedSphere = event.target;
const sphereInfo = clickedSphere.dataset.info;
clickedSphere.object3D.getWorldPosition(targetWorldPosition);
cameraEl.object3D.getWorldPosition(cameraWorldPosition);
const sphereRadius = clickedSphere.getAttribute('geometry').radius;
const offsetDistance = sphereRadius + 0.5;
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition);
panelTextEl.setAttribute('troika-text', 'value', sphereInfo); // troika-text を更新
infoPanelEl.setAttribute('visible', true);
}
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test11
│├
vr
│├
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>A-Frame Cube Rotation Example (Timed and Converging)</title>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
</head>
<body>
<a-scene>
<!-- 4x4 の立方体(上と横に並べる) -->
<a-entity id="cubes" position="0 1.5 -5">
<!-- 16個の明るくカラフルな立方体を4x4のグリッドで並べる -->
<!-- 横方向と上方向に配置 -->
<a-box position="0 0 0" color="#FF5733" class="cube"></a-box>
<a-box position="2 0 0" color="#FFBD33" class="cube"></a-box>
<a-box position="4 0 0" color="#33FF57" class="cube"></a-box>
<a-box position="6 0 0" color="#33B5FF" class="cube"></a-box>
<a-box position="0 2 0" color="#8E44AD" class="cube"></a-box>
<a-box position="2 2 0" color="#E74C3C" class="cube"></a-box>
<a-box position="4 2 0" color="#F39C12" class="cube"></a-box>
<a-box position="6 2 0" color="#1ABC9C" class="cube"></a-box>
<a-box position="0 4 0" color="#9B59B6" class="cube"></a-box>
<a-box position="2 4 0" color="#16A085" class="cube"></a-box>
<a-box position="4 4 0" color="#F1C40F" class="cube"></a-box>
<a-box position="6 4 0" color="#D35400" class="cube"></a-box>
<a-box position="0 6 0" color="#2980B9" class="cube"></a-box>
<a-box position="2 6 0" color="#27AE60" class="cube"></a-box>
<a-box position="4 6 0" color="#34495E" class="cube"></a-box>
<a-box position="6 6 0" color="#F5B041" class="cube"></a-box>
</a-entity>
<!-- 自分のカーソル(棒) -->
<a-camera>
<a-cursor color="black" raycaster="objects:.cube" fuse="false"></a-cursor>
</a-camera>
<!-- より強い照明 -->
<a-light type="directional" position="2 4 -3" intensity="1.5"></a-light>
<a-light type="ambient" position="0 5 0" intensity="0.7"></a-light>
</a-scene>
<script>
// 立方体が接触したときに回転する処理
document.querySelectorAll('.cube').forEach((cube, index) => {
cube.addEventListener('mouseenter', function () {
// マウスが立方体に触れたとき、時間差で回転を開始
setTimeout(() => {
cube.setAttribute('animation', {
property: 'rotation',
to: '360 360 360',
dur: 1000,
easing: 'easeInOutQuad',
loop: true // 回転を繰り返す設定
});
}, index * 300); // 各立方体に300msの遅延を追加
// しばらく経つと収束して回転が止まるようにする
setTimeout(() => {
cube.setAttribute('animation', {
property: 'rotation',
to: '0 0 0',
dur: 1500,
easing: 'easeInOutQuad',
loop: false // 停止
});
}, (index * 300) + 4000); // 各立方体に遅延後、4秒後に収束(停止)
});
});
</script>
</body>
</html>
vr-test3
│├
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, maximum-scale=1, width=device-width">
<title>A-Frame</title>
<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-animation-component@3.2.1/dist/aframe-animation-component.min.js"></script>
<script src="https://unpkg.com/aframe-particle-system-component@1.0.x/dist/aframe-particle-system-component.min.js"></script>
<script src="https://unpkg.com/aframe-extras.ocean@%5E3.5.x/dist/aframe-extras.ocean.min.js"></script>
<script src="https://unpkg.com/aframe-gradient-sky@1.0.4/dist/gradientsky.min.js"></script>
<script src="https://rawgit.com/mayognaise/aframe-mouse-cursor-component/master/dist/aframe-mouse-cursor-component.min.js"></script>
</head>
<body>
<a-scene>
<!-- 1つ目の立方体 -->
<a-box position="-2 3.5 -2" rotation="30 30 0" color="skyblue"
event-set__enter="_event: mouseenter; material.color: yellowgreen; scale: 3 1 1"
event-set__leave="_event: mouseleave; material.color: skyblue; scale: 1 1 1">
<a-animation attribute="rotation" begin="click" dur="500" fill="backwards" to="30 30 360"></a-animation>
<a-text id="cube1-text" value="Cube 1" position="0 1.5 0" align="center" visible="false"></a-text>
</a-box>
<!-- 2つ目の立方体 -->
<a-box position="0 3.5 -2" rotation="30 30 0" color="skyblue"
event-set__enter="_event: mouseenter; material.color: yellowgreen; scale: 3 1 1"
event-set__leave="_event: mouseleave; material.color: skyblue; scale: 1 1 1">
<a-animation attribute="rotation" begin="click" dur="500" fill="backwards" to="30 30 360"></a-animation>
<a-text id="cube2-text" value="Cube 2" position="0 1.5 0" align="center" visible="false"></a-text>
</a-box>
<!-- 3つ目の立方体 -->
<a-box position="2 3.5 -2" rotation="30 30 0" color="skyblue"
event-set__enter="_event: mouseenter; material.color: yellowgreen; scale: 3 1 1"
event-set__leave="_event: mouseleave; material.color: skyblue; scale: 1 1 1">
<a-animation attribute="rotation" begin="click" dur="500" fill="backwards" to="30 30 360"></a-animation>
<a-text id="cube3-text" value="Cube 3" position="0 1.5 0" align="center" visible="false"></a-text>
</a-box>
<a-sky color="pink"></a-sky>
<!-- カメラの動きのためのコンポーネント -->
<a-entity position="0 1.8 4">
<a-entity camera look-controls="pointerLockEnabled: true" wasd-controls="enabled: true" mouse-cursor>
<a-cursor fuse="true" color="yellow"></a-cursor>
</a-entity>
</a-entity>
</a-scene>
</body>
</html>
vr-test2
│├
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0,user-scalable=no,maximum-scale=1,width=device-width">
<title>A-Frame</title>
<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-animation-component@3.2.1/dist/aframe-animation-component.min.js"></script>
<script src="https://unpkg.com/aframe-particle-system-component@1.0.x/dist/aframe-particle-system-component.min.js"></script>
<script src="https://unpkg.com/aframe-extras.ocean@%5E3.5.x/dist/aframe-extras.ocean.min.js"></script>
<script src="https://unpkg.com/aframe-gradient-sky@1.0.4/dist/gradientsky.min.js"></script>
<script src="https://rawgit.com/mayognaise/aframe-mouse-cursor-component/master/dist/aframe-mouse-cursor-component.min.js"></script>
</head>
<body>
<a-scene>
<a-box position="0 3.5 -2" rotation="30 30 0" color="skyblue"
event-set__enter="_event: mouseenter; material.color: yellowgreen; scale: 3 1 1"
event-set__leave="_event: mouseleave; material.color: skyblue; scale: 1 1 1">
<a-animation attribute="rotation" begin="click" dur="500" fill="backwards" to="30 30 360"></a-animation>
</a-box>
<a-sky color="pink"></a-sky>
<a-entity position="0 1.8 4">
<a-entity camera look-controls mouse-cursor>
<a-cursor fuse="true" color="yellow"></a-cursor>
</a-entity>
</a-entity>
</a-entity>
</a-scene>
</body>
</html>
vr-test1
│└
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0,user-scalable=no,maximum-scale=1,width=device-width">
<title>A-Frame</title>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-animation-component@3.2.1/dist/aframe-animation-component.min.js"></script>
<script src="https://unpkg.com/aframe-particle-system-component@1.0.x/dist/aframe-particle-system-component.min.js"></script>
<script src="https://unpkg.com/aframe-extras.ocean@%5E3.5.x/dist/aframe-extras.ocean.min.js"></script>
<script src="https://unpkg.com/aframe-gradient-sky@1.0.4/dist/gradientsky.min.js"></script>
<script src="https://rawgit.com/mayognaise/aframe-mouse-cursor-component/master/dist/aframe-mouse-cursor-component.min.js"></script>
</head>
<body>
<a-scene>
<a-box position="0 3.5 -2" rotation="30 30 0" color="skyblue"
event-set__enter="_event: mouseenter; material.color: yellowgreen; scale: 3 1 1"
event-set__leave="_event: mouseleave; material.color: skyblue; scale: 1 1 1"
animation="property: scale; to: 2 2 2; dur: 500; easing: easeInOutQuad;">
</a-box>
<a-sky color="pink"></a-sky>
<a-entity position="0 1.8 4">
<a-entity camera look-controls mouse-cursor>
<a-cursor fuse="true" color="yellow"></a-cursor>
</a-entity>
</a-entity>
</a-entity>
</a-scene>
</body>
</html>
vr-test1-2
├
C#のtest
├
ファイルリストアップ
│├
# ==============================================================================
# Perl Script: 最新ファイル及び古いファイルのリストアップ(リネーム前情報付き)
# ==============================================================================
#
# 説明:
# 指定されたディレクトリ内にある特定の命名規則を持つファイルの中から、
# 各グループで最も数値が大きいファイルを選び出し、その情報を表示します。
# さらに、それらのファイルのうち、指定された日数以上前に最終更新された
# ファイルについては、リネーム前の名前(バッチファイルから取得)も表示します。
#
# 主な機能:
# 1. 対象ディレクトリ、拡張子、古いと判断する日数、リネーム情報ファイル名を
# スクリプト冒頭で設定可能。
# 2. ファイル名パターン: グループ名<半角スペース1つ>数値.拡張子
# (グループ名にはスペースを含むことができます)
# 3. 各グループの最新ファイル(名前に含まれる数値が最大)をリストアップ。
# 4. 最新ファイルの中から、指定日数以上古いものを抽出し、リネーム前の名前と共にリストアップ。
#
# 必要なモジュール:
# - POSIX (strftime): 日付フォーマット用 (通常、標準で利用可能)
# - File::Basename (basename): ファイルパスからファイル名部分を抽出 (通常、標準で利用可能)
#
# 最終更新日時: 2025-05-09
#
# ==============================================================================
use strict; # 変数宣言の強制など、厳密な文法チェックを有効にする
use warnings; # 潜在的な問題やよくある間違いに対して警告を出すようにする
use POSIX qw(strftime); # 日付を人間が読める形式にフォーマットするために使用
use File::Basename qw(basename); # ファイルパスからファイル名部分のみを取得するために使用
# --- 設定 (ここをユーザーが編集します) ---
# 処理対象のフォルダを指定してください。
# Windowsのパス区切りは '/' または '\\' (バックスラッシュ2つ) を使用してください。
# 例: my $target_directory = "C:/Users/YourName/Documents/MyFiles";
# 例: my $target_directory = "/home/yourname/my_files";
# 例: my $target_directory = "."; (スクリプトが置かれているディレクトリ)
my $target_directory = "."; # ★★★ 対象フォルダを実際のパスに書き換えてください ★★★
# 対象とするファイルの拡張子を指定してください (ドットは含めず、例: "txt", "log")
my $file_extension = "txt"; # ★★★ 対象の拡張子を書き換えてください ★★★
# 何日以上前のファイルを「古いファイル」と判断するかの日数を指定してください
my $days_threshold = 7; # ★★★ 日数を書き換えてください ★★★
# リネーム情報が記載されたバッチファイル名を指定してください。
# このバッチファイルは、Perlスクリプトと同じフォルダに置かれていることを前提とします。
# バッチファイル形式: rename "元のファイル名" "新しいファイル名(数値なし)"
my $rename_batch_filename = "xxx.bat"; # ★★★ バッチファイル名を書き換えてください ★★★
# --- グローバル変数 ---
# 各グループの最新ファイル情報を格納するためのハッシュ
# キー : グループ名 (例: "ggg aaa", "ccc")
# 値 : ハッシュリファレンス {
# name => ファイル名 (例: "ggg aaa 01.txt"),
# number => 数値部分 (例: 1),
# timestamp => 最終更新日時 (エポック秒),
# path => 元のファイルパス (例: "./test_files/ggg aaa 01.txt")
# }
my %latest_files_in_group;
# リネーム情報バッチファイルから読み取った対応表を格納するためのハッシュ
# キー : リネーム後のファイル名 (数値なし、例: "ggg aaa.txt", "ccc.txt")
# 値 : リネーム前のファイル名 (例: "zzzzz zzzzz.eee", "qwqwqwqw.rrr")
my %rename_map;
# --- メイン処理開始 ---
# ==============================================================================
# ステップ 0: リネーム情報バッチファイルの読み込み
# ==============================================================================
# 指定されたバッチファイルが存在すれば、その内容を読み込み、リネーム前後の対応を %rename_map ハッシュに格納します。
if (-f $rename_batch_filename) { # -f はファイルが存在し、かつ通常のファイルであるかを確認
# バッチファイルを開く (読み込みモード "<")
# openに失敗した場合は警告を出し、$fh_batch は undef のままなので、続く if($fh_batch) で処理をスキップ
open(my $fh_batch, "<", $rename_batch_filename)
or warn "警告: リネーム情報バッチファイル '$rename_batch_filename' を開けませんでした: $! (エラー内容)。リネーム前の名前は表示されません。\n";
if ($fh_batch) { # ファイルオープンに成功した場合のみ処理
print "リネーム情報ファイル '$rename_batch_filename' を読み込んでいます...\n";
while (my $line = <$fh_batch>) { # 1行ずつ読み込む
chomp $line; # 行末の改行文字を削除
# rename "元のファイル名" "新しいファイル名(数値なし)" の形式の行を正規表現で検索
# /i オプションで大文字・小文字を区別しない
# \s* は0個以上の空白文字(行頭・行末のスペースやコマンドと引数の間の余分なスペースに対応)
# "([^"]+)" はダブルクォートで囲まれた任意の文字列(ファイル名部分)をキャプチャ
if ($line =~ /^\s*rename\s+"([^"]+)"\s+"([^"]+)"\s*$/i) {
my $original_name_from_bat = basename($1); # キャプチャした元のファイル名 (パス部分を除外)
my $renamed_to_name_base = basename($2); # キャプチャした新しいファイル名(数値なし) (パス部分を除外)
# %rename_map ハッシュに格納: キーは新しいファイル名(数値なし)、値は元のファイル名
$rename_map{$renamed_to_name_base} = $original_name_from_bat;
}
}
close $fh_batch; # ファイルハンドルを閉じる
print "リネーム情報の読み込み完了。\n";
}
} else {
warn "警告: 指定されたリネーム情報バッチファイル '$rename_batch_filename' が見つかりません。リネーム前の名前は表示されません。\n";
}
# ==============================================================================
# ステップ 1: 対象ディレクトリ内のファイル一覧の取得と解析
# ==============================================================================
# 指定されたディレクトリから、指定された拡張子を持つファイルの一覧を取得します。
# glob はワイルドカードを使ってファイル名を展開する関数です。
my $glob_pattern = "$target_directory/*.$file_extension";
my @filepaths = glob $glob_pattern; # @filepaths 配列にファイルパスのリストが格納される
# 対象ファイルが見つからなかった場合の処理
unless (@filepaths) { # @filepaths 配列が空かどうかをチェック
print "対象となる *.$file_extension ファイルがディレクトリ '$target_directory' に見つかりませんでした。\n";
print "指定したパスや拡張子が正しいか、そのフォルダに該当ファイルが存在するか確認してください。\n";
exit; # スクリプトを終了
}
# 正規表現内で使用するために拡張子文字列をエスケープします。
# 例えば、$file_extension が "txt.bak" のように "." を含む場合、"." は正規表現のメタ文字なので、
# quotemeta を使うことで "\." のようにエスケープされ、リテラルのドットとして扱われます。
my $escaped_extension = quotemeta($file_extension);
# 取得したファイルパスのリストをループ処理
foreach my $filepath (@filepaths) {
my $filename = basename($filepath); # パス情報を取り除き、ファイル名部分のみを取得
# ファイル名が期待するパターンにマッチするかどうかを正規表現でチェック
# パターン: グループ名(スペース含む可能性あり) <半角スペース1つ> 数値 .拡張子
# ^(.*?) : 行頭から始まり、任意の文字列の最短マッチ (キャプチャグループ1: グループ名)
# ( ) : 半角スペース1つ (グループ名と数値の区切り)
# (\d+) : 1文字以上の数字 (キャプチャグループ2: 数値部分)
# \. : リテラルのドット
# $escaped_extension : エスケープされた拡張子文字列
# $ : 行末
if ($filename =~ /^(.*?) (\d+)\.$escaped_extension$/) {
my $group_name_from_file = $1; # キャプチャグループ1 (グループ名)
my $number = int($2); # キャプチャグループ2 (数値)、整数に変換
# ファイルの最終更新日時 (mtime) をエポック秒で取得
# (stat $filepath)[9] は stat 関数のリストの10番目の要素 (インデックス9) で、mtime を示します。
my $timestamp = (stat $filepath)[9];
unless (defined $timestamp) { # タイムスタンプが取得できなかった場合
warn "警告: ファイル '$filepath' の日時情報を取得できませんでした。スキップします。\n";
next; # このファイルの処理をスキップして次のファイルへ
}
# グループごとに、数値が最も大きいファイルを記録
# まだこのグループ名が %latest_files_in_group に存在しないか、
# または、現在のファイルの数値が記録されている数値より大きい場合に情報を更新
if ( (not exists $latest_files_in_group{$group_name_from_file}) or
($number > $latest_files_in_group{$group_name_from_file}{number}) ) {
$latest_files_in_group{$group_name_from_file} = {
name => $filename, # ファイル名 (basename)
number => $number, # 数値
timestamp => $timestamp, # 最終更新日時 (エポック秒)
path => $filepath # 元のファイルパス
};
}
}
}
# パターンにマッチするファイルが一つも見つからなかった場合の処理
unless (keys %latest_files_in_group) { # %latest_files_in_group ハッシュが空かどうかをチェック
print "指定された命名パターン(グループ名<半角スペース1つ>数値.$file_extension)にマッチするファイルが\n";
print "ディレクトリ '$target_directory' に見つかりませんでした。\n";
exit; # スクリプトを終了
}
# ==============================================================================
# ステップ 2: 各グループで数値が一番高いファイルの一覧を表示
# ==============================================================================
print "\n"; # 見やすさのための改行
print "------------------------------------------------------------------------------------------------\n"; # 幅変更(75+1+15 = 91)
print "各グループで数値が一番高いファイルの一覧 (対象: $target_directory/*.$file_extension):\n";
print "------------------------------------------------------------------------------------------------\n";
# printf でフォーマット指定して表示: %-75s (左寄せ75文字の文字列), %-15s (左寄せ15文字の文字列)
printf "%-75s %-15s\n", "ファイル名", "最終更新日時"; # ヘッダー表示
print "-" x (75+1+15), "\n"; # 区切り線の長さを調整 (75 + 1(スペース) + 15 = 91)
# グループ名でソートして表示するための準備
my @sorted_group_names = sort keys %latest_files_in_group;
my @all_latest_file_details; # 後のフィルタリング(古いファイルの抽出)で使用するため、詳細情報を一時保存する配列
# 各グループの最新ファイル情報を表示
foreach my $group_name_key (@sorted_group_names) {
my $file_info = $latest_files_in_group{$group_name_key}; #該当グループのファイル情報ハッシュリファレンス
# タイムスタンプ(エポック秒)を人間が読める日付形式 (YYYY-MM-DD) に変換
# POSIXモジュールのstrftime関数を使用: %Y(4桁年), %m(2桁月), %d(2桁日)
my $formatted_date = strftime "%Y-%m-%d", localtime($file_info->{timestamp});
printf "%-75s %-15s\n", $file_info->{name}, $formatted_date; # フォーマットして表示
# 後の処理(古いファイルの抽出)のために、グループ名も含むファイル情報を配列に保存
# %$file_info はハッシュリファレンスを展開して元のハッシュのキーと値をコピー
push @all_latest_file_details, { %$file_info, group_name_for_lookup => $group_name_key };
}
print "------------------------------------------------------------------------------------------------\n"; # 幅変更
# ==============================================================================
# ステップ 3: 指定された日数以上昔に更新されたファイルのみの一覧を表示 (リネーム前の名前も表示)
# ==============================================================================
print "\n"; # 見やすさのための改行
# ヘッダー幅: ファイル名(75) + スペース(1) + 更新日時(15) + スペース(1) + リネーム前(75) = 167
print "---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n";
print "上記一覧のうち、最終更新日が$days_threshold日以上前のファイルの一覧 (リネーム前情報付き):\n";
print "---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n";
# ヘッダー表示: ファイル名(左寄せ75文字)、最終更新日時(左寄せ15文字)、リネーム前(左寄せ75文字)
printf "%-75s %-15s %-75s\n", "ファイル名", "最終更新日時", "リネーム前";
print "-" x (75+1+15+1+75), "\n"; # 区切り線の長さを調整 (75 + 1 + 15 + 1 + 75 = 167)
my $current_time = time(); # 現在の時刻をエポック秒で取得
# 指定された日数($days_threshold)を秒数に変換
my $age_threshold_seconds = $days_threshold * 24 * 60 * 60;
# カットオフとなるタイムスタンプを計算 (これより古いタイムスタンプのファイルが対象)
my $cutoff_timestamp = $current_time - $age_threshold_seconds;
my $found_old_files_count = 0; # 条件に合致する古いファイルが見つかったかどうかのカウンター
# ステップ2で集めた最新ファイル情報のリストをループ処理
foreach my $file_detail_item (@all_latest_file_details) {
# ファイルのタイムスタンプがカットオフタイムスタンプより古い(小さい)か判定
if ($file_detail_item->{timestamp} < $cutoff_timestamp) {
my $current_filename = $file_detail_item->{name}; # 現在のファイル名 (リネーム後)
my $formatted_date = strftime "%Y-%m-%d", localtime($file_detail_item->{timestamp}); # 日付フォーマット
# リネームマップ検索用のキーを作成
# キー: ファイルから抽出したグループ名 + "." + 設定された拡張子 (例: "ggg aaa.txt")
my $group_for_lookup = $file_detail_item->{group_name_for_lookup};
my $lookup_key_for_rename_map = $group_for_lookup . '.' . $file_extension;
# %rename_map ハッシュからリネーム前の名前を検索
# exists演算子でキーが存在するか確認し、あればその値を、なければ "-" を表示
my $original_name_display = (exists $rename_map{$lookup_key_for_rename_map})
? $rename_map{$lookup_key_for_rename_map}
: "-"; # リネーム前の名前が見つからない場合はハイフンを表示
# フォーマットして表示
printf "%-75s %-15s %-75s\n", $current_filename, $formatted_date, $original_name_display;
$found_old_files_count++; # 古いファイルが見つかったのでカウントアップ
}
}
# もし古いファイルが一つも見つからなかった場合
if ($found_old_files_count == 0) {
print "最終更新日が$days_threshold日以上前のファイルはありませんでした。\n";
}
print "---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n";
print "\nスクリプトの実行が完了しました。\n";
# Perlスクリプトは実行後すぐに終了します。
# もし実行結果をコンソールで確認するために一時停止したい場合は、
# 以下のコメントアウトを解除してください。何かキーを押してEnterで終了します。
print "何かキーを押してEnterで終了します: "; <STDIN>;
file-sample-plus2
│├
use strict;
use warnings;
use POSIX qw(strftime); # 日付をフォーマットするために使用
use File::Basename qw(basename); # ファイルパスからファイル名部分のみを取得するために使用
# --- 設定 (ここを編集してください) ---
# 処理対象のフォルダを指定してください。
my $target_directory = "."; # ★★★ 対象フォルダを実際のパスに書き換えてください ★★★
# 対象とするファイルの拡張子を指定してください (ドットなし、例: "txt", "log")
my $file_extension = "txt"; # ★★★ 対象の拡張子を書き換えてください ★★★
# 何日以上前のファイルを古いと判断するかの日数を指定してください
my $days_threshold = 8; # ★★★ 日数を書き換えてください ★★★
# リネーム情報が記載されたバッチファイル名を指定してください (スクリプトと同じフォルダにある前提)
my $rename_batch_filename = "xxx.bat"; # ★★★ バッチファイル名を書き換えてください ★★★
# --- グローバル変数 ---
my %latest_files_in_group; # 各グループの最新ファイル情報
my %rename_map; # リネーム情報 (キー: 新しいファイル名(数値なし), 値: 元のファイル名)
# --- メイン処理 ---
# 0. リネーム情報バッチファイルの読み込み
if (-f $rename_batch_filename) {
open(my $fh_batch, "<", $rename_batch_filename)
or warn "警告: バッチファイル '$rename_batch_filename' を開けませんでした: $!. リネーム前の名前は表示されません。\n";
if ($fh_batch) {
print "リネーム情報ファイル '$rename_batch_filename' を読み込んでいます...\n";
while (my $line = <$fh_batch>) {
chomp $line;
# rename "元のファイル名" "新しいファイル名(数値なし)"
if ($line =~ /^\s*rename\s+"([^"]+)"\s+"([^"]+)"\s*$/i) {
my $original_name_from_bat = basename($1); # 元のファイル名もbasenameで
my $renamed_to_name_base = basename($2); # 新しいファイル名(数値なし)
# 新しいファイル名(数値なし)をキーとして、元のファイル名を保存
$rename_map{$renamed_to_name_base} = $original_name_from_bat;
}
}
close $fh_batch;
print "リネーム情報の読み込み完了。\n";
}
} else {
warn "警告: 指定されたリネーム情報バッチファイル '$rename_batch_filename' が見つかりません。リネーム前の名前は表示されません。\n";
}
# 1. ファイル一覧の取得と解析
my $glob_pattern = "$target_directory/*.$file_extension";
my @filepaths = glob $glob_pattern;
unless (@filepaths) {
print "対象となる *.$file_extension ファイルがディレクトリ '$target_directory' に見つかりませんでした。\n";
print "指定したパスや拡張子が正しいか、そのフォルダに該当ファイルが存在するか確認してください。\n";
exit;
}
my $escaped_extension = quotemeta($file_extension);
foreach my $filepath (@filepaths) {
my $filename = basename($filepath);
# グループ名(スペース含む可能性あり) <スペース1つ> 数値 .拡張子
if ($filename =~ /^(.*?) (\d+)\.$escaped_extension$/) {
my $group_name_from_file = $1; # ファイルから抽出したグループ名部分
my $number = int($2);
my $timestamp = (stat $filepath)[9];
unless (defined $timestamp) {
warn "警告: ファイル '$filepath' の日時情報を取得できませんでした。スキップします。\n";
next;
}
if ( (not exists $latest_files_in_group{$group_name_from_file}) or
($number > $latest_files_in_group{$group_name_from_file}{number}) ) {
$latest_files_in_group{$group_name_from_file} = {
name => $filename,
number => $number,
timestamp => $timestamp,
path => $filepath
# group_name_key は後で @all_latest_file_details に追加する際に設定
};
}
}
}
unless (keys %latest_files_in_group) {
print "指定された命名パターン(グループ名<半角スペース1つ>数値.$file_extension)にマッチするファイルが\n";
print "ディレクトリ '$target_directory' に見つかりませんでした。\n";
exit;
}
# 2. 各グループで数値が一番高いファイルの一覧を表示
print "\n";
print "------------------------------------------------------------------------------\n";
print "各グループで数値が一番高いファイルの一覧 (対象: $target_directory/*.$file_extension):\n";
print "------------------------------------------------------------------------------\n";
printf "%-30s %-15s\n", "ファイル名", "最終更新日時";
print "-" x (30+1+15), "\n";
my @sorted_group_names = sort keys %latest_files_in_group;
my @all_latest_file_details;
foreach my $group_name_key (@sorted_group_names) { # ループ変数を変更
my $file_info = $latest_files_in_group{$group_name_key};
my $formatted_date = strftime "%Y-%m-%d", localtime($file_info->{timestamp});
printf "%-30s %-15s\n", $file_info->{name}, $formatted_date;
# グループ名も一緒に保存しておく
push @all_latest_file_details, { %$file_info, group_name_for_lookup => $group_name_key };
}
print "------------------------------------------------------------------------------\n";
# 3. 指定された日数以上昔に更新されたファイルのみの一覧を表示 (リネーム前の名前も表示)
print "\n";
print "-------------------------------------------------------------------------------------------------\n"; # 幅調整
print "上記一覧のうち、最終更新日が$days_threshold日以上前のファイルの一覧 (リネーム前情報付き):\n";
print "-------------------------------------------------------------------------------------------------\n"; # 幅調整
printf "%-30s %-15s %-30s\n", "ファイル名", "最終更新日時", "リネーム前";
print "-" x (30+1+15+1+30+2), "\n"; # 幅調整 (リネーム前ファイル名が長くなる可能性を考慮)
my $current_time = time();
my $age_threshold_seconds = $days_threshold * 24 * 60 * 60;
my $cutoff_timestamp = $current_time - $age_threshold_seconds;
my $found_old_files_count = 0;
foreach my $file_detail_item (@all_latest_file_details) { # ループ変数を変更
if ($file_detail_item->{timestamp} < $cutoff_timestamp) {
my $current_filename = $file_detail_item->{name};
my $formatted_date = strftime "%Y-%m-%d", localtime($file_detail_item->{timestamp});
# リネームマップ検索用のキーを作成
# (ファイルから抽出したグループ名 + "." + 設定された拡張子)
my $group_for_lookup = $file_detail_item->{group_name_for_lookup};
my $lookup_key_for_rename_map = $group_for_lookup . '.' . $file_extension;
my $original_name_display = (exists $rename_map{$lookup_key_for_rename_map})
? $rename_map{$lookup_key_for_rename_map}
: "-";
printf "%-30s %-15s %-30s\n", $current_filename, $formatted_date, $original_name_display;
$found_old_files_count++;
}
}
if ($found_old_files_count == 0) {
print "最終更新日が$days_threshold日以上前のファイルはありませんでした。\n";
}
print "-------------------------------------------------------------------------------------------------\n"; # 幅調整
print "\nスクリプトの実行が完了しました。\n";
$a=<>;
file-sample-plus
│└
use strict;
use warnings;
use POSIX qw(strftime); # 日付をフォーマットするために使用
use File::Basename qw(basename); # ファイルパスからファイル名部分のみを取得するために使用
# --- 設定 (ここを編集してください) ---
# 処理対象のフォルダを指定してください。
my $target_directory = "."; # ★★★ 対象フォルダを実際のパスに書き換えてください ★★★
# 対象とするファイルの拡張子を指定してください (ドットなし、例: "txt", "log")
my $file_extension = "txt"; # ★★★ 対象の拡張子を書き換えてください ★★★
# 何日以上前のファイルを古いと判断するかの日数を指定してください
my $days_threshold = 8; # ★★★ 日数を書き換えてください ★★★
# --- グローバル変数 ---
# 各グループの最新ファイル情報を格納するハッシュ
my %latest_files_in_group;
# --- メイン処理 ---
# 1. ファイル一覧の取得と解析
# 指定されたディレクトリの指定された拡張子のファイルを取得
my $glob_pattern = "$target_directory/*.$file_extension";
my @filepaths = glob $glob_pattern;
unless (@filepaths) {
print "対象となる *.$file_extension ファイルがディレクトリ '$target_directory' に見つかりませんでした。\n";
print "指定したパスや拡張子が正しいか、そのフォルダに該当ファイルが存在するか確認してください。\n";
exit;
}
# 正規表現内で使用するために拡張子をエスケープ
my $escaped_extension = quotemeta($file_extension);
foreach my $filepath (@filepaths) {
my $filename = basename($filepath); # パスからファイル名部分のみを抽出
# ファイル名からグループ名と数値を抽出
# 正規表現: グループ名部分(.*?) と 数値部分(\d+) を抽出。
# グループ名にはスペースが含まれる可能性があり、グループ名と数値の間の区切りは必ず半角スペース1つ。
if ($filename =~ /^(.*?) (\d+)\.$escaped_extension$/) { # <--- 正規表現を修正しました
my $group_name = $1;
my $number = int($2); # 数値として比較するため整数に変換
# ファイルの最終更新日時 (mtime) を取得 (エポック秒)
my $timestamp = (stat $filepath)[9];
unless (defined $timestamp) {
warn "警告: ファイル '$filepath' の日時情報を取得できませんでした。スキップします。\n";
next;
}
# グループごとに、数値が最も大きいファイルを記録
if ( (not exists $latest_files_in_group{$group_name}) or
($number > $latest_files_in_group{$group_name}{number}) ) {
$latest_files_in_group{$group_name} = {
name => $filename,
number => $number,
timestamp => $timestamp,
path => $filepath
};
}
}
}
# パターンにマッチするファイルが一つも見つからなかった場合の処理
unless (keys %latest_files_in_group) {
print "指定された命名パターン(グループ名<半角スペース1つ>数値.$file_extension)にマッチするファイルが\n";
print "ディレクトリ '$target_directory' に見つかりませんでした。\n";
exit;
}
# 2. 各グループで数値が一番高いファイルの一覧を表示
print "\n";
print "--------------------------------------------------\n";
print "各グループで数値が一番高いファイルの一覧 (対象: $target_directory/*.$file_extension):\n";
print "--------------------------------------------------\n";
printf "%-35s %-20s\n", "ファイル名", "最終更新日時"; # ヘッダー表示
print "-" x 55, "\n"; # 区切り線
my @sorted_group_names = sort keys %latest_files_in_group;
my @all_latest_file_details; # 後のフィルタリングで使用するため、詳細情報を一時保存
foreach my $group_name (@sorted_group_names) {
my $file_info = $latest_files_in_group{$group_name};
my $formatted_date = strftime "%Y-%m-%d", localtime($file_info->{timestamp});
printf "%-35s %-20s\n", $file_info->{name}, $formatted_date;
push @all_latest_file_details, $file_info; # 詳細情報を保存
}
print "--------------------------------------------------\n";
# 3. 指定された日数以上昔に更新されたファイルのみの一覧を表示
print "\n";
print "--------------------------------------------------\n";
print "上記一覧のうち、最終更新日が$days_threshold日以上前のファイルの一覧:\n";
print "--------------------------------------------------\n";
printf "%-35s %-20s\n", "ファイル名", "最終更新日時"; # ヘッダー表示
print "-" x 55, "\n"; # 区切り線
my $current_time = time(); # 現在のエポック秒
my $age_threshold_seconds = $days_threshold * 24 * 60 * 60; # 指定された日数の秒数
my $cutoff_timestamp = $current_time - $age_threshold_seconds; # カットオフとなるエポック秒
my $found_old_files_count = 0;
foreach my $file_info (@all_latest_file_details) {
if ($file_info->{timestamp} < $cutoff_timestamp) {
my $formatted_date = strftime "%Y-%m-%d", localtime($file_info->{timestamp});
printf "%-35s %-20s\n", $file_info->{name}, $formatted_date;
$found_old_files_count++;
}
}
if ($found_old_files_count == 0) {
print "最終更新日が$days_threshold日以上前のファイルはありませんでした。\n";
}
print "--------------------------------------------------\n";
print "\nスクリプトの実行が完了しました。\n";
$a=<>;
file-sample
├
はい、Python を使って Blogger API 経由で投稿を作成することは可能です。Google API Client Library for Python を利用するのが一般的です。
以下に、大まかな手順と考慮すべき点を説明します。
**主な手順**
1. **Google API Console でプロジェクトを作成し、Blogger API を有効にする:**
* Google API Console ([https://console.developers.google.com/](https://console.developers.google.com/)) にアクセスします。
* 新しいプロジェクトを作成するか、既存のプロジェクトを選択します。
* 「APIとサービスを有効化」から Blogger API v3 を検索し、有効にします。
2. **認証情報を作成する:**
* Blogger API を使用するには、認証情報が必要です。通常、OAuth 2.0 を使用します。
* API Console の「認証情報」ページで、OAuth 2.0 クライアント ID を作成します。アプリケーションの種類に応じて、「ウェブ アプリケーション」や「デスクトップ アプリ」などを選択します。
* 作成されたクライアント ID とクライアント シークレットを安全に保管してください。
* リダイレクト URI も適切に設定する必要があります。
3. **Google API Client Library for Python をインストールする:**
ターミナルまたはコマンドプロンプトで以下のコマンドを実行します。
```bash
pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib
```
4. **Python コードで認証と API 呼び出しを実装する:**
* **認証:**
* `google-auth-oauthlib` ライブラリを使用して、ユーザーに認証を求め、アクセストークンを取得します。このプロセスでは、ユーザーがブラウザでGoogleアカウントにログインし、アプリケーションに権限を付与する必要があります。
* 取得したアクセストークンは、API リクエストの際に使用します。必要に応じてリフレッシュトークンも保存し、アクセストークンの有効期限が切れた際に再取得できるようにします。
* **Blogger API サービスの構築:**
* `googleapiclient.discovery.build` 関数を使用して、Blogger API のサービスオブジェクトを構築します。
* **投稿の作成:**
* サービスオブジェクトの `posts().insert()` メソッドを呼び出して新しい投稿を作成します。
* リクエストボディには、投稿のタイトル、コンテンツ(HTML形式)、ラベル(オプション)などを含めます。
* `blogId` パラメータで、投稿先のブログの ID を指定する必要があります。ブログ ID は、Blogger の管理画面の URL などから確認できます。
**コードの基本的な流れ(概念的な例)**
```python
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
import pickle
import os.path
# OAuth 2.0 のスコープ (Blogger API への書き込み権限)
SCOPES = ['https://www.googleapis.com/auth/blogger']
# 認証情報ファイルのパス (API Console からダウンロードした JSON ファイル)
CLIENT_SECRETS_FILE = 'client_secret.json' # ご自身のファイル名に置き換えてください
# トークン保存ファイルのパス
TOKEN_PICKLE_FILE = 'token.pickle'
def get_credentials():
"""OAuth 2.0 認証情報を取得または更新する"""
creds = None
# 既に保存されたトークンがあるか確認
if os.path.exists(TOKEN_PICKLE_FILE):
with open(TOKEN_PICKLE_FILE, 'rb') as token:
creds = pickle.load(token)
# 有効な認証情報がない場合、または期限切れの場合は、新しく取得
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
CLIENT_SECRETS_FILE, SCOPES)
# ローカルサーバーを起動して認証フローを実行する場合
# creds = flow.run_local_server(port=0)
# コンソールベースで認証フローを実行する場合
creds = flow.run_console()
# 取得した認証情報を保存
with open(TOKEN_PICKLE_FILE, 'wb') as token:
pickle.dump(creds, token)
return creds
def main():
"""Blogger API を使用して投稿を作成するメイン関数"""
creds = get_credentials()
service = build('blogger', 'v3', credentials=creds)
# ブログID (Blogger の管理画面 URL などから確認)
blog_id = 'YOUR_BLOG_ID' # ご自身のブログIDに置き換えてください
# 投稿内容
post_body = {
"title": "Pythonからのテスト投稿",
"content": "<p>これはPythonスクリプトからBlogger APIを使って投稿されたテスト記事です。</p>",
# "labels": ["Python", "API", "Test"] # オプションでラベルも指定可能
}
try:
# 投稿を作成 (isDraft=False で公開、True で下書き)
request = service.posts().insert(blogId=blog_id, body=post_body, isDraft=False)
post = request.execute()
print(f"投稿が作成されました。投稿ID: {post['id']}")
print(f"投稿URL: {post['url']}")
except Exception as e:
print(f"エラーが発生しました: {e}")
if __name__ == '__main__':
main()
```
**重要な注意点:**
* **エラーハンドリング:** API 呼び出しは失敗する可能性があるため、適切に `try...except` ブロックを使用してエラーを処理してください。
* **セキュリティ:** クライアントシークレットやトークンなどの機密情報は、安全に管理してください。コードに直接ハードコーディングするのではなく、環境変数や設定ファイルから読み込むことを検討しましょう。
* **API の制限:** Blogger API には、リクエスト数などの制限がある場合があります。ドキュメントを確認し、制限を超えないように注意してください。
* **ブログ ID:** 正しいブログ ID を指定する必要があります。これは、Blogger のダッシュボードの URL などで確認できます(通常は数字の羅列です)。
* **コンテンツの形式:** `content` フィールドには HTML 形式の文字列を指定します。
* **下書きとしての投稿:** `insert()` メソッドの `isDraft` パラメータを `True` に設定すると、投稿を下書きとして保存できます。公開する場合は `False` にします。
**参考情報:**
* **Blogger API v3 ドキュメント:** [https://developers.google.com/blogger/docs/3.0/reference](https://developers.google.com/blogger/docs/3.0/reference)
* **Google API Client Library for Python:** [https://github.com/googleapis/google-api-python-client](https://github.com/googleapis/google-api-python-client)
* **Python Quickstart (Google API):** [https://developers.google.com/docs/api/quickstart/python](https://developers.google.com/docs/api/quickstart/python) (特定の API 向けですが、認証フローの参考になります)
この情報が、Python を使った Blogger への投稿作成の助けになれば幸いです。もし具体的なコードで詰まっている点があれば、遠慮なくご質問ください。
ブロガーに投稿
├
geminiの2.5pro DSで調査しても詳しくは不明 カテゴリ親子関係についてはWEB上で手動設定以外なく、XMLRPCで設定はできない。
ChatGPT
ログイン
新規登録
あなた:
perl の Xmlprc
ChatGPT:
Perl で XML-RPC を使用する場合、XML::RPC モジュールを使うことが一般的です。このモジュールは、XML-RPC プロトコルを使ってリモートの Web サービスと通信するためのライブラリです。
以下は、XML::RPC モジュールを使って XML-RPC リクエストを送信する基本的な例です。
1. 必要なモジュールのインストール
まず、XML::RPC モジュールを CPAN を使ってインストールします。
bash
コピーする
cpan install XML::RPC
2. 基本的な XML-RPC リクエストの例
以下は、XML-RPC サーバーにリクエストを送信し、レスポンスを受け取る Perl のコード例です。
perl
コピーする
use XML::RPC;
# サーバーの URL(例: http://example.com/RPC2)
my $url = 'http://example.com/RPC2';
# XML-RPC クライアントの作成
my $rpc = XML::RPC->new($url);
# メソッドの呼び出しと引数
my $method = 'example.method'; # 呼び出すメソッド名
my @params = ('param1', 'param2'); # メソッドの引数
# リクエストの送信とレスポンスの取得
my $response = $rpc->call($method, @params);
# レスポンスの処理
if ($response) {
print "レスポンス: ", $response, "\n";
} else {
print "エラー: ", $rpc->status, "\n";
}
3. エラーハンドリング
XML::RPC では、通信エラーやレスポンスエラーを適切に処理するために、エラーハンドリングを行うことが重要です。上記の例で示したように、$rpc->status でエラーメッセージを確認できます。
4. XML-RPC サーバーへの POST リクエスト
XML-RPC サーバーに対して POST リクエストを送信する必要がある場合もあります。この場合、XML::RPC は内部で自動的に HTTP リクエストを作成しますので、ユーザーが直接 HTTP リクエストを作成する必要はありません。
まとめ
このように、Perl で XML-RPC を使うには XML::RPC モジュールを活用することで、簡単にリモートサーバーと通信できます。呼び出したいメソッドとその引数を指定して、XML-RPC リクエストを送信し、レスポンスを受け取ることができます。
もし、さらに詳しい使い方や特定の要件があれば、教えてください!
あなた:
FC2ブログに XML-RPCで投稿したい
ChatGPT:
FC2ブログに XML-RPC を使って投稿するためには、FC2のブログの XML-RPC インターフェースを利用する方法があります。FC2ブログは XML-RPC をサポートしており、これを使用して自動的に記事を投稿したり、管理したりすることができます。
必要な情報
まず、FC2ブログに投稿するために必要な情報は以下の通りです:
XML-RPC エンドポイント URL: FC2ブログの場合、http://xmlrpc.fc2.com が基本のエンドポイントです。
ユーザー名 (ID): FC2ブログのログインID。
パスワード: FC2ブログのログインパスワード。
XML-RPC メソッド
FC2ブログの XML-RPC インターフェースにはいくつかのメソッドがありますが、投稿を行うためには metaWeblog.newPost メソッドを使用します。
必要なモジュール
Perl で XML-RPC を使用するためには、先ほども言及した通り XML::RPC モジュールが便利です。
1. モジュールのインストール
まず、Perl の環境に XML::RPC をインストールします。
bash
コピーする
cpan install XML::RPC
2. FC2ブログに投稿する Perl スクリプト
以下の Perl スクリプトを使って、FC2ブログに XML-RPC 経由で記事を投稿できます。
perl
コピーする
use XML::RPC;
use strict;
# FC2ブログのXML-RPCサーバーURL
my $url = 'http://xmlrpc.fc2.com';
# ユーザー名(FC2ブログのID)
my $username = 'your_username';
# パスワード(FC2ブログのパスワード)
my $password = 'your_password';
# 投稿のタイトルと内容
my $title = 'この記事のタイトル';
my $content = 'この記事の内容です。';
# 記事を公開するかどうか(公開する場合は1、下書きにする場合は0)
my $publish = 1;
# XML-RPC クライアントの作成
my $rpc = XML::RPC->new($url);
# FC2ブログの XML-RPC メソッドを呼び出し
# metaWeblog.newPost の引数は次の通り:
# 1. ブログID(ユーザー名)
# 2. パスワード
# 3. 投稿内容(タイトル、内容、公開設定)
my $response = $rpc->call('metaWeblog.newPost', $username, $password, {
title => $title,
description => $content,
categories => ['default'], # カテゴリの指定(必要に応じて変更)
}, $publish);
# 結果を表示
if ($response) {
print "記事の投稿に成功しました。\n";
print "投稿ID: ", $response, "\n";
} else {
print "記事の投稿に失敗しました。\n";
print "エラー: ", $rpc->status, "\n";
}
スクリプトの説明
URL: http://xmlrpc.fc2.com は FC2 の XML-RPC サーバーのエンドポイントです。
metaWeblog.newPost メソッド: これを使って新しい記事を投稿します。引数としてブログID、パスワード、投稿する内容(タイトル、内容、カテゴリ)、および公開設定(1: 公開, 0: 下書き)を渡します。
記事の内容とタイトル: $title と $content に投稿する記事のタイトルと内容を設定します。
レスポンス: 投稿が成功すると、投稿IDが返されます。失敗した場合はエラーメッセージを表示します。
3. 実行
このスクリプトを実行すると、FC2ブログに指定したタイトルと内容で投稿されます。投稿が成功すれば、その投稿IDも表示されます。
追加オプション
カテゴリの指定: categories => ['default'] の部分で、投稿するカテゴリを指定できます。カテゴリはブログ側であらかじめ作成しておく必要があります。
記事の公開設定: $publish = 1; を 0 に変更すると、記事は公開されずに下書きとして保存されます。
注意点
パスワードが正しいこと、FC2ブログのアカウントが有効であることを確認してください。
スクリプト内の your_username と your_password を実際の FC2 アカウントの情報に置き換えてください。
これで、XML-RPC 経由で FC2ブログに投稿する準備が整いました!
添付
検索
推論
Voice
ChatGPT の回答は必ずしも正しいとは限りません。重要な情報は確認するようにしてください。
?
FC2ブログに XML-RPCで投稿したい カテゴリについて書かれている
├
ブラウザのURL欄にJavaScriptのコードを直接入力して実行することで、ページに特殊な効果を与えるという試みは、過去にはよく見られました。しかし、おっしゃる通り、セキュリティ上の懸念から、現代の多くのブラウザではこのようなJavaScriptの実行は大幅に制限されています。特に、URL欄に `javascript:` で始まるコードをペーストして実行しようとすると、ブラウザが `javascript:` の部分を自動的に削除したり、実行をブロックしたりすることが一般的です。
それでも、完全に不可能になったわけではなく、状況によっては動作するものや、ブックマークレットという形でなら同様の機能を実現できる場合があります。
以下に、過去に知られていた、あるいはブックマークレットとして応用できる可能性のあるJavaScriptのアイデアと、それらが現代のブラウザでどのように扱われるかについて説明します。
**注意:以下のコードを試す場合は、自己責任でお願いします。特に、信頼できないソースからのJavaScriptコードを実行することは、セキュリティリスクを伴う可能性があります。**
### 過去に話題になった、またはブックマークレットとして応用できるアイデア
1. **ページ上の全要素を回転させる (CSS3を利用)**
* **アイデア:** ページ内の全てのHTML要素を取得し、それぞれに回転するCSSスタイルを適用します。
* **コード例 (ブックマークレット向け):**
```javascript
javascript:(function(){document.body.style.transition='transform 2s'; document.body.style.transform='rotate(360deg)'; setTimeout(function(){document.body.style.transition=''; document.body.style.transform='';}, 2000);})();
```
または、各要素を個別に回転させるようなより複雑なものも考えられます。
```javascript
javascript:(function(){[].forEach.call(document.querySelectorAll('*'),function(a){a.style.transition='transform 1s';a.style.transform='rotate('+(Math.random()*720-360)+'deg)';});setTimeout(function(){[].forEach.call(document.querySelectorAll('*'),function(a){a.style.transition='';a.style.transform='';});},5000)})();
```
* **URL欄での動作:** 最近のブラウザでは直接入力での実行は難しいでしょう。ブックマークレットとして保存・実行すれば動作する可能性があります。
2. **ページ上の画像を全て同じ画像に置き換える**
* **アイデア:** ページ内の全ての `<img>` タグを取得し、その `src` 属性を指定した画像のURLに書き換えます。
* **コード例 (ブックマークレット向け):**
```javascript
javascript:(function(){var newImageURL='ここに置き換えたい画像のURL'; [].forEach.call(document.getElementsByTagName('img'),function(img){img.src=newImageURL;});})();
```
* **URL欄での動作:** 同様に、直接入力は難しいですが、ブックマークレットなら可能です。
3. **ページ上の文字をランダムな文字や記号に書き換える(いわゆる「文字化け」風)**
* **アイデア:** ページ内のテキストノードを探索し、その内容をランダムな文字列に置き換えます。これは元に戻すのが難しく、実用性は低いですが、視覚的な「崩壊」効果はあります。
* **コード例 (非常に破壊的なので注意・ブックマークレット向け):**
```javascript
javascript:(function(){var all=document.getElementsByTagName("*");for(var i=0,max=all.length;i<max;i++){if(all[i].childNodes.length>0){for(var j=0,max_j=all[i].childNodes.length;j<max_j;j++){if(all[i].childNodes[j].nodeType==3){all[i].childNodes[j].nodeValue=all[i].childNodes[j].nodeValue.replace(/[^\s]/g,function(){return String.fromCharCode(Math.floor(Math.random()*65536));});}}}}}();
```
* **URL欄での動作:** 直接入力は難しいでしょう。**この種のスクリプトはページの表示を完全に破壊する可能性があるため、使用には最大限の注意が必要です。**
4. **ページ全体を編集可能にする**
* **アイデア:** `document.body.contentEditable` プロパティを `true` に設定します。
* **コード:**
```javascript
javascript:document.body.contentEditable='true'; void 0;
```
* **URL欄での動作:** これは比較的シンプルなため、ブラウザや設定によってはまだURL欄から直接実行できる可能性があります(ただし、`javascript:` が削除されないように手入力する必要があるかもしれません)。しかし、これもセキュリティポリシーによりブロックされることが増えています。ブックマークレットとしては依然として有効な場合が多いです。
5. **画面を点滅させる、色を反転させる**
* **アイデア:** CSSを使って `filter: invert(1)` や、背景色と文字色を短時間で切り替えることで実現します。
* **コード例 (色反転ブックマークレット):**
```javascript
javascript:(function(){var s=document.body.style; if(s.filter=='invert(1)'){s.filter=''}else{s.filter='invert(1)'}})();
```
* **URL欄での動作:** 直接入力は難しいでしょう。ブックマークレットなら可能です。
### なぜURL欄からのJavaScript実行は制限されるのか
主な理由はセキュリティです。
* **XSS (クロスサイトスクリプティング) 対策:** 悪意のあるウェブサイトや第三者が、ユーザーに特定のJavaScriptコードをURL欄に貼り付けて実行させることで、セッション情報を盗んだり、予期せぬ操作を行わせたりする攻撃を防ぐためです。
* **ユーザー保護:** 知識のないユーザーが、意図せず危険なコードを実行してしまうことを防ぐためです。
### ブックマークレットとしての利用
上記のようなコードの多くは、ブックマークレットとしてブラウザのブックマークに登録することで、クリックひとつで実行できるようになります。ブックマークレットを作成するには、通常、ブックマークのURL欄に `javascript:` で始まるコードを記述します。
**ブックマークレットの例 (ページタイトルをアラート表示):**
`javascript:alert(document.title);`
### まとめ
URL欄に直接JavaScriptを打ち込んでページに派手な視覚効果を与えるというのは、現代のブラウザ環境ではセキュリティ上の制約から非常に難しくなっています。もしそのような効果を試したい場合は、
* **ブラウザの開発者コンソール (F12キーなどで開けます) を使う:** コンソールからは任意のJavaScriptを実行できます。
* **ブックマークレットとして登録する:** 安全性を確認した上で、自己責任でブックマークレットとして利用する。
という方法が現実的です。
繰り返しになりますが、インターネット上で見つけた見慣れないJavaScriptコードを安易に実行することは避けてください。コードの内容を理解し、安全性を確認した上で、自己責任において試すようにしましょう。
URL欄からのJavaScript実行
├
掲示板に新しい機能を追加するというのは、とても面白い試みですね。提示された例は、入力されたHTMLをそのまま別ウィンドウで実行するというもので、アイデア次第で様々な応用ができそうです。
以下に、独創的または参考となりそうな機能のアイデアをいくつか提案します。多くはクライアントサイドのJavaScriptで実現できる可能性があります。
### 1. テキスト分析・可視化系の機能
* **簡易マークダウンプレビュー機能:**
* 掲示板にMarkdown記法(例: `# 見出し`, `*太字*`, `[リンク](URL)`)で書かれたテキストを、リアルタイムまたはボタンクリックでHTMLに変換してプレビュー表示します。
* **実現方法:** JavaScriptでMarkdownをHTMLに変換するライブラリ(例: Marked.js, Showdown.js)を利用します。変換後のHTMLを別ウィンドウや指定の要素内に表示します。
```javascript
// Marked.jsを使う場合のイメージ
function previewMarkdown(textareaId, previewAreaId) {
const markdownText = document.getElementById(textareaId).value;
const html = marked.parse(markdownText); // marked.parse() はMarked.jsの関数
const previewWindow = window.open();
previewWindow.document.open();
previewWindow.document.write(html);
previewWindow.document.close();
// もしくは特定のdivに表示
// document.getElementById(previewAreaId).innerHTML = html;
}
```
```html
<TEXTAREA id="markdownInput"># テスト見出し\n*太字*\n[Google](https://google.com)</TEXTAREA>
<INPUT type="button" value="Markdownプレビュー" onClick="previewMarkdown('markdownInput', 'previewArea');">
<DIV id="previewArea"></DIV>
```
* **テキストのQRコード生成機能:**
* 掲示板に書かれたテキスト(URL、メールアドレス、自由な文章など)をQRコードとして表示します。スマートフォンで簡単に読み取れるようになります。
* **実現方法:** JavaScriptのQRコード生成ライブラリ(例: qrcode.js, Ringo_QRジェネレーター)を利用します。生成されたQRコードを画像として表示します。
```javascript
// qrcode.js を使う場合のイメージ (別途ライブラリの読み込みが必要)
function generateQR(textareaId, qrcodeDivId) {
const text = document.getElementById(textareaId).value;
const qrcodeDiv = document.getElementById(qrcodeDivId);
qrcodeDiv.innerHTML = ""; // 既存のQRコードをクリア
new QRCode(qrcodeDiv, {
text: text,
width: 128,
height: 128
});
}
```
```html
<TEXTAREA id="qrText">ここにQRコードにしたいテキストを入力</TEXTAREA>
<INPUT type="button" value="QRコード生成" onClick="generateQR('qrText', 'qrcodeDisplay');">
<DIV id="qrcodeDisplay"></DIV>
```
* **テキスト読み上げ機能:**
* 掲示板に書かれたテキストを音声で読み上げます。アクセシビリティ向上にも繋がります。
* **実現方法:** Web Speech API の `SpeechSynthesisUtterance` を利用します。
```javascript
function speakText(textareaId) {
if ('speechSynthesis' in window) {
const text = document.getElementById(textareaId).value;
const utterance = new SpeechSynthesisUtterance(text);
// オプションで言語や声を設定可能
// utterance.lang = 'ja-JP';
window.speechSynthesis.speak(utterance);
} else {
alert('ごめんなさい、お使いのブラウザは音声読み上げに対応していません。');
}
}
```
```html
<TEXTAREA id="speechText">読み上げたいテキストを入力してください。</TEXTAREA>
<INPUT type="button" value="読み上げる" onClick="speakText('speechText');">
```
### 2. インタラクティブ・エンタメ系の機能
* **簡易計算機能:**
* 掲示板に書かれた簡単な数式(例: `(100+50)*2`)を計算して結果を表示します。
* **実現方法:** JavaScriptの `eval()` 関数を利用できますが、セキュリティリスクがあるため注意が必要です。より安全な数式パーサーライブラリ(例: math.js)の利用を推奨します。
```javascript
// eval() を使う場合の簡易的な例 (セキュリティリスクに注意!)
function calculate(textareaId) {
try {
const expression = document.getElementById(textareaId).value;
const result = eval(expression);
alert('計算結果: ' + result);
} catch (e) {
alert('計算式が正しくないか、計算できませんでした。');
}
}
```
```html
<TEXTAREA id="calcText">2 * (3 + 4)</TEXTAREA>
<INPUT type="button" value="計算する" onClick="calculate('calcText');">
```
* **アスキーアート(AA)整形表示機能:**
* 掲示板に投稿されたアスキーアートが綺麗に表示されるように、フォントを指定したり、`<pre>` タグで囲んで表示したりします。
* **実現方法:** 表示する際に、CSSでAAに適したフォント(例: `MS PGothic`, `monospace`)を指定し、`<pre>` タグで整形済みテキストとして扱います。
```javascript
function showAA(textareaId) {
const aaText = document.getElementById(textareaId).value;
const aaWindow = window.open();
aaWindow.document.open();
aaWindow.document.write('<pre style="font-family: \'MS PGothic\', \'IPAMonaPGothic\', monospace; line-height: 1.2em;">' + escapeHtml(aaText) + '</pre>');
aaWindow.document.close();
}
// HTMLエスケープ関数 (XSS対策)
function escapeHtml(str) {
const div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
```
```html
<TEXTAREA id="aaText">
,.へ
___ ム i
「 ヒ_i〉 ゝ 〈
ト ノ iニ(()
i { | ヽ
i i i }
| i | 「
| i | |
</TEXTAREA>
<INPUT type="button" value="AAを整形して表示" onClick="showAA('aaText');">
```
* **縦書き表示機能:**
* 掲示板のテキストを縦書きで表示します。小説や詩などを扱う掲示板で喜ばれるかもしれません。
* **実現方法:** CSSの `writing-mode: vertical-rl;` を利用します。
```javascript
function showVertical(textareaId) {
const text = document.getElementById(textareaId).value;
const verticalWindow = window.open();
verticalWindow.document.open();
verticalWindow.document.write('<div style="writing-mode: vertical-rl; height: 90vh; font-family: \'MS Mincho\', \'Hiragino Mincho ProN\', serif;">' + escapeHtml(text) + '</div>');
verticalWindow.document.close();
}
```
```html
<TEXTAREA id="verticalText" style="width:300px; height:100px;">ここに縦書きにしたい文章を入力します。
日本語の表示に適しています。</TEXTAREA>
<INPUT type="button" value="縦書きで表示" onClick="showVertical('verticalText');">
```
### 3. 外部連携・ユーティリティ系の機能
* **選択テキストで検索エンジン検索:**
* テキストエリア内で選択した文字列を、GoogleやTwitterなどの検索エンジンで検索するボタンを追加します。
* **実現方法:** `window.getSelection().toString()` で選択範囲のテキストを取得し、検索エンジンのURLに組み込んで新しいタブで開きます。
```javascript
function searchSelectedText(textareaId, engine) {
const textarea = document.getElementById(textareaId);
const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
if (selectedText) {
let searchUrl;
if (engine === 'google') {
searchUrl = 'https://www.google.com/search?q=' + encodeURIComponent(selectedText);
} else if (engine === 'twitter') {
searchUrl = 'https://twitter.com/search?q=' + encodeURIComponent(selectedText);
}
if (searchUrl) {
window.open(searchUrl, '_blank');
}
} else {
alert('検索したいテキストを選択してください。');
}
}
```
```html
<TEXTAREA id="searchTextarea">検索したいテキストをここに入力し、一部を選択してください。</TEXTAREA>
<INPUT type="button" value="選択範囲をGoogle検索" onClick="searchSelectedText('searchTextarea', 'google');">
<INPUT type="button" value="選択範囲をTwitter検索" onClick="searchSelectedText('searchTextarea', 'twitter');">
```
* **地図連携機能:**
* 掲示板に地名や住所が書かれていた場合、それをGoogle Mapsなどの地図サービスで表示するボタンを追加します。
* **実現方法:** テキストエリアの文字列(または選択された部分)を地図サービスのURLパラメータとして渡します。
```javascript
function openMap(textareaId) {
const address = document.getElementById(textareaId).value;
if (address) {
const mapUrl = 'https://www.google.com/maps?q=' + encodeURIComponent(address);
window.open(mapUrl, '_blank');
} else {
alert('地名や住所を入力してください。');
}
}
```
```html
<TEXTAREA id="addressText">東京スカイツリー</TEXTAREA>
<INPUT type="button" value="地図で表示" onClick="openMap('addressText');">
```
### 注意点
* **セキュリティ:** ユーザーが入力した内容をHTMLとして実行したり、`eval()` を使用したりする際には、クロスサイトスクリプティング(XSS)などのセキュリティリスクに十分注意してください。必要に応じて、入力値のサニタイズ(無害化処理)や、より安全な代替手段の利用を検討しましょう。提示された `jikkou` 関数の例は、入力された内容をそのまま `document.write` しているため、悪意のあるスクリプトを埋め込まれると実行されてしまう可能性があります。`escapeHtml` のような関数でエスケープ処理を挟むなどの対策が考えられます。
* **ユーザビリティ:** 機能を追加する際は、ユーザーが直感的に使えるようなインターフェースを心がけることが大切です。ボタンの配置や説明文なども工夫してみてください。
* **掲示板のテーマとの整合性:** 掲示板のテーマや利用者に合った機能を選ぶと、より活用されやすくなります。例えば、プログラミング系の掲示板であればコードハイライトや実行環境連携、創作系の掲示板であれば縦書き表示やルビ振り機能などが考えられます。
これらのアイデアが、あなたの掲示板開発の参考になれば幸いです。独創的な機能で、より魅力的な掲示板を作成してください。
掲示板に新しい機能を追加する
├
非公開
これらSMEMOの変更予定
├
<html xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns="http://www.w3.org/TR/REC-html40">
<head>
<meta http-equiv=Content-Type content="text/html; charset=shift_jis">
<meta name=ProgId content=Excel.Sheet>
<meta name=Generator content="Microsoft Excel 15">
<link id=Main-File rel=Main-File href="../Book1.htm">
<link rel=File-List href=filelist.xml>
<![if IE]>
<base href="file:///C:/CE594991/Book1.files/sheet001.htm"
id="webarch_temp_base_tag">
<![endif]>
<link rel=Stylesheet href=stylesheet.css>
<style>
<!--table
{mso-displayed-decimal-separator:"\.";
mso-displayed-thousand-separator:"\,";}
@page
{margin:.75in .7in .75in .7in;
mso-header-margin:.3in;
mso-footer-margin:.3in;}
ruby
{ruby-align:left;}
rt
{color:windowtext;
font-size:6.0pt;
font-weight:400;
font-style:normal;
text-decoration:none;
font-family:游ゴシック, monospace;
mso-font-charset:128;
mso-char-type:katakana;
display:none;}
-->
</style>
<![if !supportTabStrip]><script language="JavaScript">
<!--
function fnUpdateTabs()
{
if (parent.window.g_iIEVer>=4) {
if (parent.document.readyState=="complete"
&& parent.frames['frTabs'].document.readyState=="complete")
parent.fnSetActiveSheet(0);
else
window.setTimeout("fnUpdateTabs();",150);
}
}
if (window.name!="frSheet")
window.location.replace("../Book1.htm");
else
fnUpdateTabs();
//-->
</script>
<![endif]>
</head>
<body link="#0563C1" vlink="#954F72">
<div _ngcontent-ng-c2986768429="" dir=ltr style='--animation-duration: 600ms;
--fade-animation-function: linear;animation: 0s ease 0s 1 normal none running none;
appearance: none;background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;
columns: auto;contain: none;container: none;content: normal;cx: 0px;cy: 0px;
d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;font-kerning: auto;
font-optical-sizing: auto;font-feature-settings: normal;font-variation-settings: normal;
font-stretch: normal;gap: normal;hyphens: manual;interactivity: auto;
isolation: auto;marker: none;mask: none;offset: normal;opacity: 1;order: 0;
orphans: 2;outline: rgb(27, 28, 29) none 0px;overlay: none;perspective: none;
quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;ry: auto;scale: none;
speak: normal;stroke: none;transform: none;transition: all;translate: none;
widows: 2;x: 0px;y: 0px;zoom: 1'>
<table border=0 cellpadding=0 cellspacing=0 width=2376 style='border-collapse:
collapse;table-layout:fixed;width:1782pt'>
<col width=72 span=33 style='width:54pt'>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl65 colspan=22 dir=LTR width=1584 style='height:18.75pt;
mso-ignore:colspan;width:1188pt'>Meta Quest
3の標準ブラウザで楽しめる3Dサイトですね!WebXR技術を使ったサイトや、3Dモデルをインタラクティブに表示するサイトなど、様々な体験が可能です。いくつかおすすめのサイトをご紹介します。</td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl66 colspan=4 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>1. Sketchfab (スケッチファブ)</td>
<td colspan=29 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=29 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>内容:<font class="font5">
膨大な数の3Dモデルが公開されているプラットフォームです。ユーザーが作成したキャラクター、建物、乗り物、アート作品など、多種多様なモデルをブラウザ上で3Dで鑑賞できます。多くのモデルはVRモードにも対応しており、Meta
Quest 3で臨場感あふれる体験が可能です。</font></td>
<td colspan=4 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=19 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>楽しみ方:<font class="font5">
興味のあるカテゴリーを検索したり、人気のあるモデルを眺めたりするだけでも楽しめます。気に入ったモデルを拡大・縮小・回転させながら細部まで観察できます。</font></td>
<td colspan=14 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl68 colspan=4 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a href="https://sketchfab.com/" target="_blank" rel=noopener
externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_60412c52abd1a943","c_bdc636b0cf977e2f",null,"rc_9257646b5e9128ca",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>URL:<font class="font8">
https://sketchfab.com/</font></a></td>
<td colspan=29 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl66 colspan=4 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>2. Mozilla Hubs (モジラハブズ)</td>
<td colspan=29 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=31 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>内容:<font class="font5">
Mozillaが開発した、ブラウザベースのソーシャルVRプラットフォームです。アバターを使って仮想空間に入り、他のユーザーとコミュニケーションを取ったり、3Dオブジェクトを共有したりできます。特別なソフトウェアのインストールは不要で、Meta
Quest 3のブラウザから直接アクセスできます。</font></td>
<td colspan=2 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=17 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>楽しみ方:<font class="font5">
公開されているルームに参加したり、自分でルームを作成して友人を招待したりできます。会議やイベント、気軽な雑談など、様々な用途で活用できます。</font></td>
<td colspan=16 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl68 colspan=4 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a href="https://hubs.mozilla.com/" target="_blank" rel=noopener
externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_60412c52abd1a943","c_bdc636b0cf977e2f",null,"rc_9257646b5e9128ca",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>URL:<font class="font8">
https://hubs.mozilla.com/</font></a></td>
<td colspan=29 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl66 colspan=8 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>3. WebXR Experiments (ウェブエックスアール エクスペリメンツ)</td>
<td colspan=25 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=23 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>内容:<font class="font5">
Googleが提供する、WebXR技術を使った様々な実験的なデモを体験できるサイトです。シンプルなゲームや、インタラクティブなアート、教育的なコンテンツなど、WebXRの可能性を感じられるものが多くあります。</font></td>
<td colspan=10 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=19 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>楽しみ方:<font class="font5">
色々なデモを試してみて、WebXRでどのようなことができるのかを体験してみてください。Meta Quest
3のコントローラーを使った操作に対応しているものもあります。</font></td>
<td colspan=14 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl68 colspan=12 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a href="https://experiments.withgoogle.com/collection/webxr"
target="_blank" rel=noopener externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_60412c52abd1a943","c_bdc636b0cf977e2f",null,"rc_9257646b5e9128ca",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>URL:<font class="font8">
https://experiments.withgoogle.com/collection/webxr
(具体的なデモは時期によって入れ替わる可能性があります)</font></a></td>
<td colspan=21 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl66 colspan=3 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>4. A-Frame (エーフレーム)</td>
<td colspan=30 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=24 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>内容:<font class="font5">
WebXRコンテンツを簡単に作成できるオープンソースのウェブフレームワークです。A-Frameの公式サイトには、A-Frameを使って作られた様々なデモや事例が紹介されており、その多くをMeta
Quest 3のブラウザで体験できます。</font></td>
<td colspan=9 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=23 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>楽しみ方:<font class="font5">
公式サイトの「Examples」や「Showcase」のセクションをチェックして、気に入ったデモを試してみてください。プログラミングの知識が少しあれば、自分で簡単なWebXRコンテンツを作ることも可能です。</font></td>
<td colspan=10 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl68 colspan=3 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a href="https://aframe.io/" target="_blank" rel=noopener
externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_60412c52abd1a943","c_bdc636b0cf977e2f",null,"rc_9257646b5e9128ca",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>URL:<font class="font8">
https://aframe.io/</font></a></td>
<td colspan=30 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl66 colspan=4 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>5. Matterport (マーターポート)</td>
<td colspan=29 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=26 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>内容:<font class="font5">
現実の空間を3Dスキャンして、没入型のデジタルツインを作成するプラットフォームです。不動産の内見、美術館のバーチャルツアー、店舗の紹介などに活用されています。Meta
Quest 3のブラウザでこれらの3D空間をウォークスルー体験できます。</font></td>
<td colspan=7 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=15 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>楽しみ方:<font class="font5">
公開されている様々な物件や施設をバーチャルで訪れてみましょう。まるでその場にいるかのような感覚で空間を探索できます。</font></td>
<td colspan=18 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl68 colspan=4 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a href="https://matterport.com/discover" target="_blank"
rel=noopener externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_60412c52abd1a943","c_bdc636b0cf977e2f",null,"rc_9257646b5e9128ca",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>URL:<font class="font8">
https://matterport.com/discover</font></a></td>
<td colspan=29 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl66 colspan=2 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>その他・注意点</td>
<td colspan=31 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=22 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>ウェブサイト側の対応:<font class="font5"> すべての3DコンテンツがMeta Quest
3のブラウザで完璧に動作するとは限りません。サイトによっては、VRモードへの切り替えボタン(ゴーグルのアイコンなど)がある場合があります。</font></td>
<td colspan=11 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=19 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>ブラウザの更新:<font class="font5"> Meta Quest
3のブラウザは定期的にアップデートされます。最新バージョンにすることで、より多くのWebXRコンテンツが快適に楽しめるようになる可能性があります。</font></td>
<td colspan=14 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=10 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>インターネット接続:<font class="font5"> 快適な体験のためには、安定したインターネット接続が必要です。</font></td>
<td colspan=23 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl65 colspan=24 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>これらのサイトを参考に、Meta Quest 3の標準ブラウザで3Dの世界をお楽しみください!
新しいサイトも続々と登場しているので、ぜひご自身でも「WebXR」などのキーワードで検索して、お気に入りのサイトを見つけてみてください。</div>
</td>
<td colspan=9 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 colspan=33 style='height:18.75pt;mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl65 colspan=14 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>
<div _ngcontent-ng-c2986768429="" dir=ltr style='--animation-duration: 600ms;
--fade-animation-function: linear;animation: 0s ease 0s 1 normal none running none;
appearance: none;background:padding-box border-box rgba(0, 0, 0, 0);
inset: auto;columns: auto;contain: none;container: none;content: normal;
cx: 0px;cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(27, 28, 29) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>かしこまりました。Meta Quest
3の標準ブラウザで楽しめる3Dサイトについて、さらにリサーチを行い、より多くの情報をお届けします。</td>
<td colspan=19 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl65 colspan=11 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>前回ご紹介したサイトに加えて、さらにいくつかのカテゴリーや具体的なサイトを探してみました。</td>
<td colspan=22 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl66 colspan=5 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>1. ゲーム・インタラクティブコンテンツ</td>
<td colspan=28 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=4 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>PlayCanvas (プレイキャンバス)</td>
<td colspan=29 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=24 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>内容:<font class="font5">
WebGLを利用した3Dゲームやインタラクティブなコンテンツを開発・公開できるプラットフォームです。多くのデモやユーザー作成ゲームが公開されており、Meta
Quest 3のブラウザで直接プレイできるものもあります。</font></td>
<td colspan=9 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=23 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>楽しみ方:<font class="font5">
「Explore」セクションや「Learn」セクションから様々なプロジェクトを探し、気に入ったものを試してみてください。高品質なグラフィックのゲームや、物理演算を使った面白いデモなどが見つかります。</font></td>
<td colspan=10 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl70 colspan=4 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a href="https://playcanvas.com/explore" target="_blank"
rel=noopener externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_161723bf14105b48","c_bdc636b0cf977e2f",null,"rc_6884f423b0b82aa3",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>URL:<font class="font8">
https://playcanvas.com/explore</font></a></td>
<td colspan=29 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=5 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>Construct 3 (コンストラクト3) - Arcade</td>
<td colspan=28 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=26 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>内容:<font class="font5"> プログラミング知識がなくてもゲームを制作できるツール「Construct
3」の公式サイトには、ユーザーが作成したゲームを公開・プレイできる「Arcade」というセクションがあります。WebXRに対応したゲームもいくつか見つけることができます。</font></td>
<td colspan=7 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=17 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>楽しみ方:<font class="font5">
「Arcade」で「VR」や「WebXR」といったキーワードで検索してみると、Meta Quest
3で楽しめるカジュアルなゲームが見つかるかもしれません。</font></td>
<td colspan=16 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl70 colspan=9 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a
href="https://www.google.com/search?q=https://www.construct.net/en/arcade"
target="_blank" rel=noopener externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_161723bf14105b48","c_bdc636b0cf977e2f",null,"rc_6884f423b0b82aa3",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>URL:<font class="font8">
https://www.google.com/search?q=https://www.construct.net/en/arcade</font></a></td>
<td colspan=24 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=9 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>CrazyGames (クレイジーゲームズ) や Poki (ポキ) などのブラウザゲームサイト</td>
<td colspan=24 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=29 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>内容:<font class="font5">
これらのサイトには、多数の無料ブラウザゲームが集まっています。数は少ないかもしれませんが、「VR」や「3D」といったカテゴリーで検索すると、Meta
Quest 3のブラウザでプレイ可能なシンプルな3Dゲームや、擬似的なVR体験ができるゲームが見つかることがあります。</font></td>
<td colspan=4 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=17 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>楽しみ方:<font class="font5"> 気軽に遊べるゲームを探している場合に試してみてください。すべてのゲームがMeta
Quest 3に最適化されているわけではない点にご注意ください。</font></td>
<td colspan=16 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl70 colspan=6 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a href="https://www.crazygames.com/" target="_blank" rel=noopener
externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_161723bf14105b48","c_bdc636b0cf977e2f",null,"rc_6884f423b0b82aa3",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>CrazyGames URL:<font
class="font8"> https://www.crazygames.com/</font></a></td>
<td colspan=27 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl70 colspan=5 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a href="https://poki.jp/" target="_blank" rel=noopener
externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_161723bf14105b48","c_bdc636b0cf977e2f",null,"rc_6884f423b0b82aa3",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>Poki URL:<font class="font8">
https://poki.jp/ (日本語対応)</font></a></td>
<td colspan=28 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl66 colspan=3 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>2. 教育・学習・アート</td>
<td colspan=30 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=7 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>Google Arts & Culture (グーグル アーツアンドカルチャー)</td>
<td colspan=26 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=26 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>内容:<font class="font5">
世界中の美術館や博物館のコレクションをオンラインで鑑賞できるプラットフォームです。一部の展示や作品は3Dで表示されたり、ストリートビューのように館内を巡ったりすることが可能です。WebXRに対応したコンテンツも増えつつあります。</font></td>
<td colspan=7 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=19 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>楽しみ方:<font class="font5">
有名な絵画を高解像度で細部まで鑑賞したり、歴史的な遺物を3Dで様々な角度から眺めたりできます。バーチャルツアーで世界中の文化遺産を訪れるのもおすすめです。</font></td>
<td colspan=14 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl70 colspan=5 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a href="https://artsandculture.google.com/" target="_blank"
rel=noopener externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_161723bf14105b48","c_bdc636b0cf977e2f",null,"rc_6884f423b0b82aa3",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>URL:<font class="font8">
https://artsandculture.google.com/</font></a></td>
<td colspan=28 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=7 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>The Extended Mind (ザ・エクステンデッド・マインド)</td>
<td colspan=26 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=19 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>内容:<font class="font5">
様々なWebXRアート作品や実験的なプロジェクトをキュレーションしているサイトです。ブラウザでアクセスできるユニークで示唆に富んだVR/AR体験が見つかります。</font></td>
<td colspan=14 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=18 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>楽しみ方:<font class="font5">
アーティスティックな表現や、新しいテクノロジーの可能性に触れたい方におすすめです。各作品のコンセプトを理解しながら体験すると、より深く楽しめます。</font></td>
<td colspan=15 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl70 colspan=8 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a
href="https://www.google.com/search?q=https://theextendedmind.com/"
target="_blank" rel=noopener externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_161723bf14105b48","c_bdc636b0cf977e2f",null,"rc_6884f423b0b82aa3",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>URL:<font class="font8">
https://www.google.com/search?q=https://theextendedmind.com/</font></a></td>
<td colspan=25 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=3 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>Within (ウィズイン)</td>
<td colspan=30 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=25 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>内容:<font class="font5">
高品質なVRドキュメンタリーやストーリーテリングコンテンツを制作・配信しているプラットフォームです。一部の作品はWebXR経由でブラウザから視聴できる場合があります。(専用アプリでの視聴がメインの場合もあります)</font></td>
<td colspan=8 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=11 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>楽しみ方:<font class="font5"> 感動的な物語や、社会的なメッセージ性の強い作品など、没入感の高い映像体験ができます。</font></td>
<td colspan=22 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl70 colspan=3 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a href="https://www.with.in/" target="_blank" rel=noopener
externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_161723bf14105b48","c_bdc636b0cf977e2f",null,"rc_6884f423b0b82aa3",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>URL:<font class="font8">
https://www.with.in/</font></a></td>
<td colspan=30 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl66 colspan=6 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>3. 3Dモデルビューア・プラットフォーム (再掲・補足)</td>
<td colspan=27 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=4 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>Sketchfab (スケッチファブ)</td>
<td colspan=29 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=33 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>補足:<font class="font5">
前回もご紹介しましたが、教育的なコンテンツ(例:人体の解剖モデル、歴史的建造物の再現モデル)や、製品の3Dモデルなども豊富にあります。検索機能を活用して、興味のある分野のモデルを探してみてください。Meta
Quest 3のブラウザで「VR」モードを選択すると、より没入感のある鑑賞が可能です。</font></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl66 colspan=4 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>4. その他のWebXR関連リソース</td>
<td colspan=29 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=5 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>WebXR Samples (WebXR サンプル)</td>
<td colspan=28 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=20 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>内容:<font class="font5"> WebXR
APIの様々な機能を使った基本的なサンプルコードとデモが公開されています。開発者向けではありますが、どのようなことができるのかを具体的に知りたい場合に参考になります。</font></td>
<td colspan=13 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=18 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>楽しみ方:<font class="font5"> 各デモをMeta Quest
3のブラウザで試すことで、WebXRの基本的なインタラクション(コントローラーの入力、オブジェクトの選択など)を体験できます。</font></td>
<td colspan=15 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl70 colspan=6 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a href="https://immersive-web.github.io/webxr-samples/"
target="_blank" rel=noopener externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_161723bf14105b48","c_bdc636b0cf977e2f",null,"rc_6884f423b0b82aa3",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>URL:<font class="font8">
https://immersive-web.github.io/webxr-samples/</font></a></td>
<td colspan=27 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=2 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>Hello WebXR!</td>
<td colspan=31 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=12 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>内容:<font class="font5">
WebXRコンテンツのポータルサイトの一つで、様々なデモやゲーム、体験へのリンクが集められています。</font></td>
<td colspan=21 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl69 colspan=7 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>楽しみ方:<font class="font5"> 新しいWebXRコンテンツを発見するのに役立ちます。</font></td>
<td colspan=26 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl70 colspan=8 dir=LTR style='height:18.75pt;mso-ignore:
colspan'><a href="https://www.google.com/search?q=https://www.hellowebxr.com/"
target="_blank" rel=noopener externallink="" _nghost-ng-c3138787759=""
jslog="197247;track:generic_click,impression;BardVeMetadataKey:[["r_161723bf14105b48","c_bdc636b0cf977e2f",null,"rc_6884f423b0b82aa3",null,null,"ja",null,null,null,null,1,0]]"
style='animation: 0s ease 0s 1 normal none running none;appearance: none;
background:padding-box border-box rgba(0, 0, 0, 0);inset: auto;columns: auto;
contain: none;container: none;content: normal;cursor:pointer;cx: 0px;
cy: 0px;d: none;fill: rgb(0, 0, 0);flex: 0 1 auto;font-size-adjust: none;
font-kerning: auto;font-optical-sizing: auto;font-feature-settings: normal;
font-variation-settings: normal;font-stretch: normal;gap: normal;hyphens: manual;
interactivity: auto;isolation: auto;marker: none;mask: none;offset: normal;
opacity: 1;order: 0;orphans: 2;outline: rgb(11, 87, 208) none 0px;overlay: none;
perspective: none;quotes: auto;r: 0px;resize: none;rotate: none;rx: auto;
ry: auto;scale: none;speak: normal;stroke: none;transform: none;transition: all;
translate: none;widows: 2;x: 0px;y: 0px;zoom: 1'>URL:<font class="font8">
https://www.google.com/search?q=https://www.hellowebxr.com/</font></a></td>
<td colspan=25 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl66 colspan=7 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>Meta Quest 3のブラウザで3Dサイトを快適に楽しむためのヒント</td>
<td colspan=26 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=11 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>ブックマークの活用:<font class="font5"> 気に入ったサイトや、頻繁に訪れるサイトはブックマークしておくと便利です。</font></td>
<td colspan=22 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=23 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>ウィンドウモードとVRモードの切り替え:<font class="font5"> サイトによっては、VRゴーグルのアイコンや「Enter
VR」といったボタンが表示されます。これをクリックすることで、より没入感のあるVRモードに切り替えることができます。</font></td>
<td colspan=10 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=19 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>コントローラー操作の確認:<font class="font5">
サイトによってコントローラーの操作方法が異なる場合があります。多くの場合、ポインターで選択し、トリガーボタンで決定する操作が基本となります。</font></td>
<td colspan=14 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=16 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>キャッシュのクリア:<font class="font5">
まれにサイトの表示がおかしくなることがあります。その場合は、ブラウザのキャッシュをクリアすると改善されることがあります。</font></td>
<td colspan=17 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl67 colspan=11 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>Wi-Fi環境の確認:<font class="font5">
途切れのないスムーズな体験のためには、高速で安定したWi-Fi環境が推奨されます。</font></td>
<td colspan=22 style='mso-ignore:colspan'></td>
</tr>
<tr height=25 style='height:18.75pt'>
<td height=25 class=xl65 colspan=21 dir=LTR style='height:18.75pt;mso-ignore:
colspan'>これらの情報が、Meta Quest
3での3Dサイト体験をさらに豊かなものにするお役に立てれば幸いです。WebXRの世界は日々進化していますので、新しいサイトやコンテンツも積極的に探してみてください。</div>
</td>
<td colspan=12 style='mso-ignore:colspan'></td>
</tr>
<![if supportMisalignedColumns]>
<tr height=0 style='display:none'>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
<td width=72 style='width:54pt'></td>
</tr>
<![endif]>
</table>
</body>
</html>
Meta Quest 3の標準ブラウザで楽しめる3Dサイトーー
├
geminiの回答を、・・・からコピーを選んでExcelに貼り付け、単一のHTMを選んでbook1.hmt で保存 ブラウザで開いてソースコードを持ってきたのが上のやつ
これやれば リンクは一応切れない。
Meta Quest 3の標準ブラウザで楽しめる3Dサイトですね!WebXR技術を使ったサイトや、3Dモデルをインタラクティブに表示するサイトなど、様々な体験が可能です。いくつかおすすめのサイトをご紹介します。
1. Sketchfab (スケッチファブ)
内容: 膨大な数の3Dモデルが公開されているプラットフォームです。ユーザーが作成したキャラクター、建物、乗り物、アート作品など、多種多様なモデルをブラウザ上で3Dで鑑賞できます。多くのモデルはVRモードにも対応しており、Meta Quest 3で臨場感あふれる体験が可能です。
楽しみ方: 興味のあるカテゴリーを検索したり、人気のあるモデルを眺めたりするだけでも楽しめます。気に入ったモデルを拡大・縮小・回転させながら細部まで観察できます。
URL: https://sketchfab.com/
2. Mozilla Hubs (モジラハブズ)
内容: Mozillaが開発した、ブラウザベースのソーシャルVRプラットフォームです。アバターを使って仮想空間に入り、他のユーザーとコミュニケーションを取ったり、3Dオブジェクトを共有したりできます。特別なソフトウェアのインストールは不要で、Meta Quest 3のブラウザから直接アクセスできます。
楽しみ方: 公開されているルームに参加したり、自分でルームを作成して友人を招待したりできます。会議やイベント、気軽な雑談など、様々な用途で活用できます。
URL: https://hubs.mozilla.com/
3. WebXR Experiments (ウェブエックスアール エクスペリメンツ)
内容: Googleが提供する、WebXR技術を使った様々な実験的なデモを体験できるサイトです。シンプルなゲームや、インタラクティブなアート、教育的なコンテンツなど、WebXRの可能性を感じられるものが多くあります。
楽しみ方: 色々なデモを試してみて、WebXRでどのようなことができるのかを体験してみてください。Meta Quest 3のコントローラーを使った操作に対応しているものもあります。
URL: https://experiments.withgoogle.com/collection/webxr (具体的なデモは時期によって入れ替わる可能性があります)
4. A-Frame (エーフレーム)
内容: WebXRコンテンツを簡単に作成できるオープンソースのウェブフレームワークです。A-Frameの公式サイトには、A-Frameを使って作られた様々なデモや事例が紹介されており、その多くをMeta Quest 3のブラウザで体験できます。
楽しみ方: 公式サイトの「Examples」や「Showcase」のセクションをチェックして、気に入ったデモを試してみてください。プログラミングの知識が少しあれば、自分で簡単なWebXRコンテンツを作ることも可能です。
URL: https://aframe.io/
5. Matterport (マーターポート)
内容: 現実の空間を3Dスキャンして、没入型のデジタルツインを作成するプラットフォームです。不動産の内見、美術館のバーチャルツアー、店舗の紹介などに活用されています。Meta Quest 3のブラウザでこれらの3D空間をウォークスルー体験できます。
楽しみ方: 公開されている様々な物件や施設をバーチャルで訪れてみましょう。まるでその場にいるかのような感覚で空間を探索できます。
URL: https://matterport.com/discover
その他・注意点
ウェブサイト側の対応: すべての3DコンテンツがMeta Quest 3のブラウザで完璧に動作するとは限りません。サイトによっては、VRモードへの切り替えボタン(ゴーグルのアイコンなど)がある場合があります。
ブラウザの更新: Meta Quest 3のブラウザは定期的にアップデートされます。最新バージョンにすることで、より多くのWebXRコンテンツが快適に楽しめるようになる可能性があります。
インターネット接続: 快適な体験のためには、安定したインターネット接続が必要です。
これらのサイトを参考に、Meta Quest 3の標準ブラウザで3Dの世界をお楽しみください! 新しいサイトも続々と登場しているので、ぜひご自身でも「WebXR」などのキーワードで検索して、お気に入りのサイトを見つけてみてください。
かしこまりました。Meta Quest 3の標準ブラウザで楽しめる3Dサイトについて、さらにリサーチを行い、より多くの情報をお届けします。
前回ご紹介したサイトに加えて、さらにいくつかのカテゴリーや具体的なサイトを探してみました。
1. ゲーム・インタラクティブコンテンツ
PlayCanvas (プレイキャンバス)
内容: WebGLを利用した3Dゲームやインタラクティブなコンテンツを開発・公開できるプラットフォームです。多くのデモやユーザー作成ゲームが公開されており、Meta Quest 3のブラウザで直接プレイできるものもあります。
楽しみ方: 「Explore」セクションや「Learn」セクションから様々なプロジェクトを探し、気に入ったものを試してみてください。高品質なグラフィックのゲームや、物理演算を使った面白いデモなどが見つかります。
URL: https://playcanvas.com/explore
Construct 3 (コンストラクト3) - Arcade
内容: プログラミング知識がなくてもゲームを制作できるツール「Construct 3」の公式サイトには、ユーザーが作成したゲームを公開・プレイできる「Arcade」というセクションがあります。WebXRに対応したゲームもいくつか見つけることができます。
楽しみ方: 「Arcade」で「VR」や「WebXR」といったキーワードで検索してみると、Meta Quest 3で楽しめるカジュアルなゲームが見つかるかもしれません。
URL: https://www.google.com/search?q=https://www.construct.net/en/arcade
CrazyGames (クレイジーゲームズ) や Poki (ポキ) などのブラウザゲームサイト
内容: これらのサイトには、多数の無料ブラウザゲームが集まっています。数は少ないかもしれませんが、「VR」や「3D」といったカテゴリーで検索すると、Meta Quest 3のブラウザでプレイ可能なシンプルな3Dゲームや、擬似的なVR体験ができるゲームが見つかることがあります。
楽しみ方: 気軽に遊べるゲームを探している場合に試してみてください。すべてのゲームがMeta Quest 3に最適化されているわけではない点にご注意ください。
CrazyGames URL: https://www.crazygames.com/
Poki URL: https://poki.jp/ (日本語対応)
2. 教育・学習・アート
Google Arts & Culture (グーグル アーツアンドカルチャー)
内容: 世界中の美術館や博物館のコレクションをオンラインで鑑賞できるプラットフォームです。一部の展示や作品は3Dで表示されたり、ストリートビューのように館内を巡ったりすることが可能です。WebXRに対応したコンテンツも増えつつあります。
楽しみ方: 有名な絵画を高解像度で細部まで鑑賞したり、歴史的な遺物を3Dで様々な角度から眺めたりできます。バーチャルツアーで世界中の文化遺産を訪れるのもおすすめです。
URL: https://artsandculture.google.com/
The Extended Mind (ザ・エクステンデッド・マインド)
内容: 様々なWebXRアート作品や実験的なプロジェクトをキュレーションしているサイトです。ブラウザでアクセスできるユニークで示唆に富んだVR/AR体験が見つかります。
楽しみ方: アーティスティックな表現や、新しいテクノロジーの可能性に触れたい方におすすめです。各作品のコンセプトを理解しながら体験すると、より深く楽しめます。
URL: https://www.google.com/search?q=https://theextendedmind.com/
Within (ウィズイン)
内容: 高品質なVRドキュメンタリーやストーリーテリングコンテンツを制作・配信しているプラットフォームです。一部の作品はWebXR経由でブラウザから視聴できる場合があります。(専用アプリでの視聴がメインの場合もあります)
楽しみ方: 感動的な物語や、社会的なメッセージ性の強い作品など、没入感の高い映像体験ができます。
URL: https://www.with.in/
3. 3Dモデルビューア・プラットフォーム (再掲・補足)
Sketchfab (スケッチファブ)
補足: 前回もご紹介しましたが、教育的なコンテンツ(例:人体の解剖モデル、歴史的建造物の再現モデル)や、製品の3Dモデルなども豊富にあります。検索機能を活用して、興味のある分野のモデルを探してみてください。Meta Quest 3のブラウザで「VR」モードを選択すると、より没入感のある鑑賞が可能です。
4. その他のWebXR関連リソース
WebXR Samples (WebXR サンプル)
内容: WebXR APIの様々な機能を使った基本的なサンプルコードとデモが公開されています。開発者向けではありますが、どのようなことができるのかを具体的に知りたい場合に参考になります。
楽しみ方: 各デモをMeta Quest 3のブラウザで試すことで、WebXRの基本的なインタラクション(コントローラーの入力、オブジェクトの選択など)を体験できます。
URL: https://immersive-web.github.io/webxr-samples/
Hello WebXR!
内容: WebXRコンテンツのポータルサイトの一つで、様々なデモやゲーム、体験へのリンクが集められています。
楽しみ方: 新しいWebXRコンテンツを発見するのに役立ちます。
URL: https://www.google.com/search?q=https://www.hellowebxr.com/
Meta Quest 3のブラウザで3Dサイトを快適に楽しむためのヒント
ブックマークの活用: 気に入ったサイトや、頻繁に訪れるサイトはブックマークしておくと便利です。
ウィンドウモードとVRモードの切り替え: サイトによっては、VRゴーグルのアイコンや「Enter VR」といったボタンが表示されます。これをクリックすることで、より没入感のあるVRモードに切り替えることができます。
コントローラー操作の確認: サイトによってコントローラーの操作方法が異なる場合があります。多くの場合、ポインターで選択し、トリガーボタンで決定する操作が基本となります。
キャッシュのクリア: まれにサイトの表示がおかしくなることがあります。その場合は、ブラウザのキャッシュをクリアすると改善されることがあります。
Wi-Fi環境の確認: 途切れのないスムーズな体験のためには、高速で安定したWi-Fi環境が推奨されます。
これらの情報が、Meta Quest 3での3Dサイト体験をさらに豊かなものにするお役に立てれば幸いです。WebXRの世界は日々進化していますので、新しいサイトやコンテンツも積極的に探してみてください。
Meta Quest 3の標準ブラウザで楽しめる3Dサイト
├
複数のファイルが有り、先頭が一定の種類でまとまっている。例えば、momo-123.jpg momo-assd.jpg dda-qqqwqw.jpg dda-ttttrerer.jpgなど。これらを、まずフォルダ分けして移動する。momoフォルダ ddaフォルダなど。 次に圧縮して、1つずつの圧縮ファイルにしたい momo.zip dda.zip のように。このようなバッチを作成できないか
できます!Windows環境で使える「バッチファイル(.bat)」を作成すれば、以下のような処理が可能です:
---
### ✅ やりたいこと(まとめ)
1. 複数のファイル(例:`momo-123.jpg`, `dda-abc.jpg`)がある。
2. 接頭辞(例:`momo`, `dda`)ごとにフォルダを作り、ファイルを移動。
3. 各フォルダを個別に ZIP 圧縮(例:`momo.zip`, `dda.zip`)。
---
### 🛠 バッチファイルのコード(`sort_and_zip.bat`)
```bat
@echo off
setlocal enabledelayedexpansion
REM 作業ディレクトリ(スクリプトのある場所に設定)
cd /d "%~dp0"
REM 拡張子を指定(jpg以外も対応可能)
set EXT=jpg
REM ファイルをループ処理
for %%f in (*.%EXT%) do (
REM 接頭辞をファイル名の先頭からハイフンまで抽出(例:momo)
for /f "tokens=1 delims=-" %%a in ("%%~nxf") do (
set PREFIX=%%a
REM フォルダがなければ作成
if not exist "!PREFIX!" (
mkdir "!PREFIX!"
)
REM 該当フォルダにファイル移動
move "%%f" "!PREFIX!\"
)
)
REM フォルダをZIP圧縮(PowerShell使用)
for /d %%d in (*) do (
REM 拡張子を除いたZIPファイル名を作成
powershell -command "Compress-Archive -Path '%%d\*' -DestinationPath '%%d.zip'"
)
echo 完了しました!
pause
```
---
### 🔧 使い方
1. 上記コードをメモ帳などにコピペ。
2. `sort_and_zip.bat` として保存。
3. 対象の画像ファイル(例:`momo-1.jpg`など)があるフォルダにこの `.bat` を配置。
4. ダブルクリックで実行。
---
### 📝 注意点
- Windows 標準の `Compress-Archive` は PowerShell で使えるので、特別なソフト不要。
- 拡張子が `.jpg` 以外(例:`.png`, `.txt`など)の場合は、`set EXT=jpg` を変更。
---
もしファイル名のルールがもう少し複雑だったり、他のOS(Mac / Linux)用スクリプトも必要なら、その辺りも対応できるよ!
圧縮-chatgpt
├
複数のファイルが有り、先頭が一定の種類でまとまっている。例えば、momo-123.jpg momo-assd.jpg dda-qqqwqw.jpg dda-ttttrerer.jpgなど。これらを、まずフォルダ分けして移動する。momoフォルダ ddaフォルダなど。 次に圧縮して、1つずつの圧縮ファイルにしたい momo.zip dda.zip のように。このようなバッチを作成できないか
はい、承知いたしました。指定された形式のファイル名(例: `momo-123.jpg`, `dda-qqqwqw.jpg`)に基づいて、ファイルをプレフィックスごと(`momo`, `dda`)にフォルダ分けし、その後、各フォルダを個別のZIPファイル(`momo.zip`, `dda.zip`)に圧縮するバッチファイルを作成します。
このバッチファイルは、Windows 10 (バージョン1803以降) または Windows 11 に標準搭載されている `tar` コマンドを使用してZIPファイルを作成します。外部ツールのインストールは不要です。
```batch
@echo off
setlocal enabledelayedexpansion
REM --- 設定項目 ---
REM 処理したいファイルがあるフォルダのパスを指定してください
REM バッチファイルと同じフォルダにある場合は "." のままでOK
set "TARGET_DIR=."
REM --- 設定項目ここまで ---
REM 作業ディレクトリに移動 (エラーチェック付き)
pushd "%TARGET_DIR%"
if errorlevel 1 (
echo ERROR: 指定されたフォルダが見つかりません: %TARGET_DIR%
pause
exit /b 1
)
echo ファイルのフォルダ分けを開始します...
echo 対象フォルダ: %CD%
echo ---
REM フォルダ分け処理
for %%F in (*-*) do (
REM ファイル名にハイフンが含まれるファイルのみ処理
echo Processing file: "%%F"
REM ハイフンより前の部分(プレフィックス)を取得
for /f "tokens=1 delims=-" %%P in ("%%~nF") do (
set "Prefix=%%P"
REM プレフィックス名のフォルダが存在しない場合は作成
if not exist "!Prefix!" (
echo Creating folder: "!Prefix!"
mkdir "!Prefix!"
if errorlevel 1 (
echo ERROR: Folder creation failed for "!Prefix!". Skipping file "%%F".
goto :skip_move
)
)
REM ファイルをプレフィックス名のフォルダに移動
echo Moving "%%F" to "!Prefix!\"
move "%%F" "!Prefix!\" > nul
if errorlevel 1 (
echo WARN: Failed to move "%%F". It might already exist in "!Prefix!" or be in use.
)
)
:skip_move
)
echo ---
echo フォルダ分けが完了しました。
echo ---
echo 各フォルダのZIP圧縮を開始します...
echo ---
REM 圧縮処理 (カレントディレクトリにあるフォルダを対象)
for /d %%D in (*) do (
REM "." や ".." ではなく、実際に作成または存在するフォルダのみ対象
if not "%%~nxD"=="." if not "%%~nxD"==".." (
set "FolderName=%%~nxD"
set "ZipFileName=!FolderName!.zip"
REM 同名のZIPファイルが既に存在するか確認(上書きしない場合)
REM if exist "!ZipFileName!" (
REM echo Skipping: "!ZipFileName!" already exists.
REM ) else (
echo Compressing folder "!FolderName!" to "!ZipFileName!"...
REM tarコマンドでフォルダをZIP圧縮 (-a: 自動圧縮形式選択, -c: 作成, -f: ファイル名指定)
tar -a -c -f "!ZipFileName!" "!FolderName!" > nul 2> nul
if errorlevel 1 (
echo ERROR: Failed to compress "!FolderName!". Check if tar command is available or if the folder is empty/inaccessible.
) else (
echo Success: "!ZipFileName!" created.
)
REM )
REM 上記の if exist ... else (...) のコメントアウトを外すと、既存のZIPファイルを上書きしません。
REM 現在は既存ファイルがあっても上書きします。
)
)
echo ---
echo 全ての処理が完了しました。
REM 元のディレクトリに戻る
popd
endlocal
pause
```
**バッチファイルの使い方**
1. 上記のコードをテキストエディタ(メモ帳など)にコピー&ペーストします。
2. **`REM --- 設定項目 ---`** の部分を編集します。
* `TARGET_DIR`: 処理したいファイル(`momo-123.jpg` など)が保存されているフォルダのパスを指定します。このバッチファイルと同じフォルダにファイルがある場合は、 `.` のままで大丈夫です。
3. ファイルを保存します。ファイル名は自由ですが、拡張子は `.bat` または `.cmd` にします。(例: `organize_and_zip.bat`)
4. **注意:** このバッチファイルを実行すると、`TARGET_DIR` 内のファイルがサブフォルダに移動され、その後フォルダがZIP圧縮されます。**元に戻す機能はないため、事前にファイルのバックアップを取るか、コピーしたファイルで試すことを強く推奨します。**
5. 作成したバッチファイルをダブルクリックして実行します。
**処理の流れ**
1. 指定された `TARGET_DIR` に移動します。
2. フォルダ内のファイルのうち、名前にハイフン (`-`) が含まれるものを一つずつチェックします。
3. ファイル名からハイフンより前の部分(プレフィックス)を抽出します。
4. そのプレフィックスと同じ名前のフォルダが存在しなければ作成します。
5. ファイルをそのフォルダに移動します。
6. 全てのファイルの移動が終わったら、`TARGET_DIR` 内にある全てのフォルダ(ファイル移動によって作成されたフォルダ)を一つずつチェックします。
7. 各フォルダを、フォルダ名と同じ名前のZIPファイル(例: `momo` フォルダなら `momo.zip`)に圧縮します。標準の `tar` コマンドを使用します。
8. 全ての処理が終わったらメッセージを表示して終了します。
**補足事項**
* **`tar` コマンド:** Windows 10 (バージョン1803以降) / Windows 11 に標準搭載されています。もし古いWindows OSを使用している場合は、別途 7-Zip などのコマンドライン圧縮ツールをインストールし、バッチファイル内の `tar` コマンド部分をそのツールのコマンドに書き換える必要があります。
* **エラー処理:** 簡単なエラーチェック(フォルダ作成失敗、ファイル移動失敗、圧縮失敗)を含めていますが、完璧ではありません。
* **既存ファイルの扱い:** ファイル移動時に移動先に同名ファイルがあると、`move` コマンドはデフォルトでは上書きしません(エラーになる場合があります)。ZIP圧縮は、既存のZIPファイルがあれば `tar` コマンドは通常上書きします。
* **ファイル名の制限:** フォルダ名として使用できない文字(`\ / : * ? " < > |` など)がプレフィックスに含まれている場合、フォルダ作成に失敗する可能性があります。
* **空フォルダ:** ファイル移動の結果、空になったフォルダもZIP圧縮の対象になります(中身が空のZIPファイルが作成されます)。
圧縮ーgemini
├
【蒸留モデル】DeepSeek-R1ローカル実行時におすすめのモデル #macOS - Qiita
https://qiita.com/yonaka15/items/a69790be6eacd726f770
Ollamaを活用したDeepSeek-R1のローカルLLM構築ガイド - GPT Master
https://chatgpt-enterprise.jp/blog/ollama-deepseek-r1/
無料で学ぶ!生成AIとバズった技術まとめ(LLM、LangChain、Dify、SAM2、Stable Diffusion、RVCなど) #langchain - Qiita
https://qiita.com/birdwatcher/items/b5cc66ce59095dee5625
DeepSeek R1をほぼ準備なしからローカルGPUで動かす|しぴちゃん
https://note.com/cppp_cpchan/n/n3c1039c80dd0
妻にDeepSeek R1をローカルPCで動かす方法を教えたのでここに手順を残します|胡椒 - こしょう
https://note.com/torauma9/n/n1b2447ab4398
でえぷしいく
├
old
├
javascript shortcode area
└
postnote
・その場でjavascriptの編集
実行、デバッグOK
・ツリー管理
・変数管理
・括弧の色分け(オマケ程度)
・無限Undo
・完全オリジナルの環境です。
・このCGIのお問い合わせは
・momonga-v4.live.jp まで。
非公開