Junkerposts
■
みせられない
完全に私的なメモ
├
<!DOCTYPE html>
<html>
<head>
<title>太陽系 aframe- WebXRテスト 2025_5_23ver</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 },
rotationSpeed: { type: 'number', default: 1.5 },
pitchLimit: { type: 'number', default: 85 },
verticalSpeed: { type: 'number', default: 30 } // 前回追加した上下移動速度
},
init: function () {
this.keys = {};
this.leftThumbstickInput = { x: 0, y: 0 };
this.rightThumbstickInput = { 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.rigEl = this.el;
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.onLeftThumbstickMoved.bind(this));
} else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); }
this.rightHand = document.getElementById('rightHand');
if (this.rightHand) {
this.rightHand.addEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this));
} else { console.warn("camera-relative-controls: 右手コントローラー(#rightHand)が見つかりません。"); }
});
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.onLeftThumbstickMoved.bind(this)); } catch(e){} } if (this.rightHand) { try { this.rightHand.removeEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); } catch(e){} } },
onLeftThumbstickMoved: function (evt) { this.leftThumbstickInput.x = evt.detail.x; this.leftThumbstickInput.y = evt.detail.y; },
onRightThumbstickMoved: function (evt) { this.rightThumbstickInput.x = evt.detail.x; this.rightThumbstickInput.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 || !this.rigEl || !this.rigEl.object3D) { return; }
const data = this.data;
const dt = timeDelta / 1000;
const rigObject = this.rigEl.object3D;
const cameraObject = this.cameraEl.object3D;
// --- 右スティックによる視点回転と上下移動処理 ---
if (this.rigEl.sceneEl.is('vr-mode')) {
if (Math.abs(this.rightThumbstickInput.x) > 0.1) {
const yawAngle = -this.rightThumbstickInput.x * data.rotationSpeed * dt;
rigObject.rotation.y += yawAngle;
}
if (Math.abs(this.rightThumbstickInput.y) > 0.1) {
const verticalMovement = this.rightThumbstickInput.y * data.verticalSpeed * dt;
rigObject.position.y -= verticalMovement;
}
}
// --- 視点回転と上下移動処理ここまで ---
// --- 移動処理 (左スティックとキーボード) ---
const position = rigObject.position; // rigの現在位置
cameraObject.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.leftThumbstickInput.y) > 0.1) {
const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.leftThumbstickInput.y);
this.moveDirection.add(forwardBackward);
}
if (Math.abs(this.leftThumbstickInput.x) > 0.1) {
const leftRight = this.cameraRight.clone().multiplyScalar(this.leftThumbstickInput.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" vr-mode-ui="enabled: true">
<a-sky id="backgroundSkyElement" src="./pic/u6.jpg"></a-sky>
<a-assets>
<img id="tex_a1" src="./pic/e1.jpg" crossOrigin="anonymous">
<img id="tex_a2" src="./pic/e2.jpg" crossOrigin="anonymous">
<img id="tex_a3" src="./pic/e3.jpg" crossOrigin="anonymous">
<img id="tex_a4" src="./pic/e4.jpg" crossOrigin="anonymous">
<img id="tex_a5" src="./pic/e5.jpg" crossOrigin="anonymous">
<img id="tex_a6" src="./pic/e6.jpg" crossOrigin="anonymous">
<img id="tex_a7" src="./pic/e7.jpg" crossOrigin="anonymous">
<img id="tex_a8" src="./pic/e8.jpg" crossOrigin="anonymous">
<img id="tex_a9" src="./pic/e9.jpg" crossOrigin="anonymous">
</a-assets>
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1; rotationSpeed: 1.5; pitchLimit: 85; verticalSpeed: 30;">
<a-entity id="camera" camera="far: 20000;" look-controls="pointerLockEnabled: false; touchEnabled: false" position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;" raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" laser-controls="hand: right; model: false; lineColor: white; lineOpacity: 0.75" ></a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-cone id="prevButton" class="clickableButton" position="-20.0 0 0.1" rotation="0 0 90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-cone id="nextButton" class="clickableButton" position="20.0 0 0.1" rotation="0 0 -90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-sphere id="closeButton" class="clickableButton" position="24 11 0.1" radius="1.5" color="red" shader="flat"> </a-sphere>
<a-entity id="linkButtonsContainer" position="-22 -5 0.1">
<a-sphere id="linkButton0" class="clickableButton linkButton" visible="false" radius="1.5" position="0 0 0" shader="flat"></a-sphere>
<a-sphere id="linkButton1" class="clickableButton linkButton" visible="false" radius="1.5" position="0 -4 0" shader="flat"></a-sphere>
<a-sphere id="linkButton2" class="clickableButton linkButton" visible="false" radius="1.5" position="0 -8 0" shader="flat"></a-sphere>
</a-entity>
</a-entity>
<script>
// --- SCRIPT BLOCK 2: Main Scene Logic ---
const sceneEl = document.getElementById('myScene');
const skyElement = document.getElementById('backgroundSkyElement');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
const linkButtons = [];
let rightHandEl = null;
const EXE_MODE = 0;
const SKY_BACKGROUND_IMAGE_PATH = "./pic/u6.jpg";
if (SKY_BACKGROUND_IMAGE_PATH && SKY_BACKGROUND_IMAGE_PATH !== "") { skyElement.setAttribute('src', SKY_BACKGROUND_IMAGE_PATH); skyElement.setAttribute('visible', 'true');}
else { skyElement.setAttribute('visible', false); skyElement.removeAttribute('src'); sceneEl.setAttribute('background', 'color', '#000000');}
const PANEL_OFFSET_FROM_OBJECT = 5.0;
const BASE_TEXT_STYLE = { maxWidth: 46, align: 'center', anchorX: 'center', anchorY: 'middle', baseline: 'middle' };
const PAGE_TEXT_STYLES = { 'index': { ...BASE_TEXT_STYLE, fontSize: 0.72, color: 'white' }, 'color': { ...BASE_TEXT_STYLE, fontSize: 0.65, color: '#A0F0A0' }, 'size': { ...BASE_TEXT_STYLE, fontSize: 0.80, color: '#FFEC8B' }, 'comment': { ...BASE_TEXT_STYLE, fontSize: 0.70, color: '#ADD8E6' } };
// ★★★ オブジェクトごとの定義 (見やすく改行、整形) ★★★
const OBJECT_DEFINITIONS = {
1: { type: 'sphere', useTextureForIndex: 1, sizeType: 'fixed', fixedSize: 6.0,},
2: { type: 'sphere', useTextureForIndex: 2, sizeType: 'fixed', fixedSize: 6.0,},
3: { type: 'sphere', useTextureForIndex: 3, sizeType: 'fixed', fixedSize: 6.0,},
4: { type: 'sphere', useTextureForIndex: 4, sizeType: 'fixed', fixedSize: 6.0,},
5: { type: 'sphere', useTextureForIndex: 5, sizeType: 'fixed', fixedSize: 6.0,},
6: { type: 'sphere', useTextureForIndex: 6, sizeType: 'fixed', fixedSize: 6.0,},
7: { type: 'sphere', useTextureForIndex: 7, sizeType: 'fixed', fixedSize: 6.0,},
8: { type: 'sphere', useTextureForIndex: 8, sizeType: 'fixed', fixedSize: 6.0,},
9: { type: 'sphere', useTextureForIndex: 9, sizeType: 'fixed', fixedSize: 6.0,},
// 2: { type: 'box', specificColor: 'blue', sizeType: 'random' },
// 3: { type: 'sphere', specificColor: '#FF1493', sizeType: 'fixed', fixedSize: 4.0 }, // ディープピンク
};
const DEFAULT_OBJECT_TYPE = 'box'; const DEFAULT_SIZE_TYPE = 'random'; const TEXTURE_ID_PREFIX = 'tex_a'; const MAX_TEXTURE_INDEX = 10;
// ★★★ コメントデータ (見やすく改行、整形) ★★★
const CUBE_COMMENTS = {
1: {
text: ["水星\n正式名称:Mercury(マーキュリー)\n直径は約4879kmで、太陽系の中で最も小さな惑星\n主に鉄で構成された巨大な核と岩石質の外層を持つ\n",
"水星は太陽に最も近い惑星で、太陽から約5800万kmの距離を\n公転し、わずか88日で1年が過ぎる。自転周期は約59日で、\n太陽に同じ面を長く向ける。昼間の気温は摂氏約430度まで\n上昇し、夜間は氷点下180度に下がる。極端な温度差を持つ。\n大気はほとんど存在せず、微量のナトリウムや酸素を含む。\n地表はクレーターに覆われ、月に似た外観を持つのが特徴。\n磁場があり、中心に巨大な鉄の核が存在すると考えられる。\n探査機マリナー10号とメッセンジャーが詳細な観測を行った。\n",
"水星の名前はローマ神話の神マーキュリーに由来する。彼は\n商人・旅人・盗賊の守護神で、俊敏さと素早さを象徴した。\nこれは水星が空を速く移動する様子にちなむ。1日で空に\n何度も姿を現すように見えることから名付けられたという。\nギリシャ神話ではヘルメスに相当する神とされている。\n古代から観測されていたが、太陽に近いため肉眼で見るには\n条件が限られる。日の出前や日没直後の短時間に観測可能。\nその神秘的で移ろいやすい性質が多くの文化で語られてきた。\n",
"水星には「カロリス盆地」と呼ばれる巨大な衝突クレーター\nがあり、直径は約1500kmにも達する。これは太陽系でも最大\n級の衝突痕のひとつだ。また、重力の影響で内部構造に歪み\nが生じた証拠もある。内部は鉄で満たされており、地殻は薄い。\nNASAの探査機「メッセンジャー」は2004年に打ち上げられ、\n2011年から2015年まで周回し、膨大なデータを収集した。\nその結果、極地に水の氷が存在する可能性が確認された。\nまた、磁場や断崖状の地形も詳細に記録された貴重な成果だ。\n",
"現在はヨーロッパと日本が共同で進める「ベピ・コロンボ」\nという探査機が水星に向かって飛行中である。これはESAと\nJAXAの国際協力ミッションで、より精密な観測を目指す。\n水星の起源や進化、磁場、大気、地表組成などを調べる予定。\n水星は太陽系の形成初期を知る重要な手がかりとされる。\nまた、その極端な環境は惑星科学にとって興味深い対象だ。\n将来的には人類の太陽観測基地の候補として研究されるかも。\n過酷でありながらも、多くの謎と魅力を秘めた存在である。\n"]
},
2: {
text: ["金星\n正式名称:Venus(ヴィーナス/ビーナス)\n直径は約12104kmで、地球とほぼ同じ大きさを持つ\n二酸化炭素が主成分の厚い大気と硫酸の雲で覆われる\n",
"金星は太陽から2番目に近い惑星で、公転周期は約225日。\n自転周期は非常に長く、約243日で1回転、しかも逆回転だ。\nこれは他の惑星とは逆方向にゆっくりと回っていることを\n意味する。大気は主に二酸化炭素で、温室効果が極めて強く\n地表温度は約470℃に達し、太陽系で最も高温の惑星となる。\nまた、分厚い雲が太陽光をほぼ遮断しており、表面は暗い。\n風は秒速数十メートルで吹き荒れており、気圧は地球の約90倍。\n地表は火山と溶岩平原が広がる、灼熱の世界である。\n",
"「ヴィーナス」はローマ神話の愛と美の女神の名前に由来。\nギリシャ神話ではアフロディーテに相当し、美と調和の象徴。\nその名にふさわしく、金星は夜空で最も明るく輝く星であり、\n「明けの明星」や「宵の明星」とも呼ばれて古代から愛された。\n日の出前や日没後にだけ姿を見せ、太陽に近い軌道のため、\n常に太陽の近くにある。観測しやすく、神秘性も高かった。\n古代バビロニア、ギリシャ、インド、中国など各文明で崇拝。\n地球に最も近い惑星でもあり、よく「地球の双子」と呼ばれる。\n",
"金星探査は1950年代から始まり、旧ソ連の「ベネラ計画」が\n世界で初めて金星の大気と地表に関する情報を送った。特に\nベネラ13号は1982年に地表への着陸に成功し、カラー写真を\n地球に送信した。これにより岩石状の地表と厚い雲の存在が確認。\nその後、アメリカの「マゼラン探査機」はレーダー観測によって\n金星全体の詳細な地形マップを作成した。火山、溝、断層など\n多様な地形が判明し、地質活動の痕跡が多く見つかっている。\nただし現在も火山活動が継続しているかは不明である。\n",
"現在、NASAの「VERITAS」やESAの「EnVision」など、金星に\n再び焦点を当てる探査計画が進行中である。金星の過去には\n液体の水が存在していた可能性もあり、生命の痕跡を探る研究も。\nまた、2020年には金星の大気中にホスフィンという物質が発見\nされ、これは生命活動の可能性として大きな話題となった。\nしかし、この発見の解釈には議論があり、今後の調査が期待される。\n金星は過去に地球と似た環境だった可能性があり、温暖化の極限を\n知る重要な比較対象として、注目を集め続けている惑星である。\n"]
},
3: {
text: ["地球\n正式名称:Earth(アース/地球)\n直径は約12742kmで、太陽系の中で5番目に大きな惑星\n水と酸素を含む大気に包まれ、唯一生命が確認されている\n",
"地球は太陽系で3番目に位置し、公転周期は約365.25日。\n自転周期は24時間で、昼と夜を生み出している。大気には\n窒素が約78%、酸素が約21%含まれ、生命活動を支えている。\n地表の約70%は液体の水で覆われ、これは地球の大きな特徴。\n大陸・海洋・大気・磁場が相互に作用し、複雑な気候と環境\nを作り出している。地球の内部は核・マントル・地殻で構成。\nプレートテクトニクスによって大陸は移動し、地震や火山活動\nも発生する。これらは地球独特の地質活動の証でもある。\n",
"「Earth」という名前はローマ神話やギリシャ神話に登場する\n神ではなく、ゲルマン語系の「大地」を意味する言葉に由来。\nラテン語では「Terra」、ギリシャ語では「Gaia(ガイア)」と\n呼ばれ、どちらも大地を司る女神を象徴する名である。\n他の惑星が神の名を持つ中で、唯一神名に由来しない名称だ。\n古代文明では大地母神として崇められ、生命の源とされた。\n地球は人類にとって唯一の居住可能な天体であり、自然現象や\n宇宙観の中心に置かれてきた特別な存在である。\n",
"地球には多様な生態系が存在し、微生物から高等生物まで\nあらゆる生命が互いに関係しながら共存している。生命誕生\nは約38億年前とされ、最初の生命は海中で発生したと考えられ\nている。植物は光合成により酸素を生み出し、大気を変化させ、\n動物や人類の進化へとつながった。人類は地球上に文明を築き\n環境に大きな影響を与えている。地球温暖化や生物多様性の\n減少といった問題も深刻で、地球の未来は我々に委ねられている。\nこの星を守るため、持続可能な共生の道が求められている。\n",
"地球は多数の探査衛星により継続的に観測されている。\n気象衛星や地球観測衛星は気候変動や自然災害の監視に活躍。\nまた、宇宙から見た地球は「ブルーマーブル」と呼ばれ、\n1972年のアポロ17号が撮影した写真は世界に感動を与えた。\n国際宇宙ステーション(ISS)からも日々地球が観測される。\nこうした視点は「宇宙に浮かぶかけがえのない存在」として\n地球への意識を変える役割を果たしている。科学技術の進歩と\nともに、地球の理解と保護は人類の使命として続いていく。\n"]
},
4: {
text: ["火星\n正式名称:Mars(マーズ)\n直径は約6779kmで、地球の約半分の大きさ\n岩石質の惑星で、鉄分を多く含む地表は赤い色をしている\n",
"火星は太陽から4番目の惑星で、公転周期は約687日。\n自転周期は約24.6時間で、昼夜のリズムは地球に近い。\n表面は赤茶けた色をしており、「赤い惑星」と呼ばれる。\nこれは地表に酸化鉄(赤錆)が多く含まれているため。\n薄い大気は主に二酸化炭素で構成され、気圧は地球の1%以下。\n気温は日中20℃でも夜間は-100℃になるなど、寒暖差が激しい。\n風による砂嵐が頻繁に発生し、時には惑星全体を覆うことも。\n北半球は低地が多く、南半球は高地とクレーターに富む。\n",
"火星の名前はローマ神話の戦の神「マルス」に由来し、\nギリシャ神話ではアレスに相当する。赤く血のような色が\n戦いを象徴するとされ、この名がつけられた。夜空で明るく、\n赤く輝く様子は古代から人々の注目を集めてきた。\n古代エジプト、バビロニア、マヤ文明でも火星は神格化され、\n不吉な兆しや軍神の象徴として語られた。漢字では「火」を\nあてられ、中国の五行思想では「火」に分類されたことに由来。\nこうした文化的背景が、火星の名に深い意味を与えている。\n",
"火星には太陽系最大の火山「オリンポス山」があり、高さは\n約22kmにもなる。また、長さ4000km以上の「マリネリス渓谷」\nという巨大な谷も存在する。かつて川が流れた跡や氾濫原、\n三角州のような地形が見つかり、古代には水が存在していたと\n考えられている。極地にはドライアイス(固体の二酸化炭素)\nと水の氷から成る極冠があり、季節ごとに変化する。\n地質活動は現在ではほぼ停止しているが、過去には火山活動や\n地殻変動が活発だった証拠が多く残されている。\n",
"火星探査は今も続いており、NASAの「パーサヴィアランス」や\n「キュリオシティ」などの探査車が地表を走行中である。\nこれらは生命の痕跡、水の痕跡、地質や気候の調査を進めている。\nまた、中国の「天問一号」も探査に成功し、多国間の競争が激化。\n火星は将来的な人類移住の候補地として注目され、スペースXなど\n民間企業も火星探査・植民を目指している。酸素の生成実験や\n水資源の確認なども進んでおり、SFの夢が現実に近づきつつある。\n未知の可能性を秘めた「次なるフロンティア」として期待される。\n"]
},
5: {
text: ["木星\n正式名称:Jupiter(ジュピター)\n直径は約142984kmで、太陽系最大の巨大ガス惑星\n主に水素とヘリウムからなり、固体の表面は存在しない\n",
"木星は太陽から5番目の惑星で、公転周期は約12年。\n自転周期は約10時間と非常に速く、赤道が膨らんでいる。\n大気は縞模様が特徴で、これは気流の帯が作るものである。\n最大の特徴は「大赤斑」と呼ばれる巨大な嵐で、地球2個分\nほどの大きさがあり、数百年続いていると考えられている。\n木星には強大な磁場があり、放射線も非常に強い。\n内部には高圧で金属水素状態となった層があるとされ、\n中心には岩石質の核がある可能性が指摘されている。\n",
"「ジュピター」はローマ神話の主神ユピテルに由来する。\nギリシャ神話ではゼウスに相当し、神々の王として崇拝された。\n夜空では非常に明るく、古代から「王の星」として知られている。\n中国では「歳星」と呼ばれ、12年周期で天球を巡る星として、\n暦や占星術でも重視された。インド占星術では「グル(導師)」\nの象徴であり、吉星とされる。木星という名称は五行思想に\n基づくもので、木にあてられたことから付けられている。\nこのように、世界中で特別な意味を持つ天体である。\n",
"木星は約95個もの衛星を持ち、その中でも最大の「ガニメデ」は\n太陽系最大の衛星で、水の氷と岩石でできている。次いで「カリスト」、\n「イオ」、「エウロパ」も有名で、これらはガリレオ衛星と呼ばれる。\nイオは活火山が噴火を続ける活動的な衛星であり、エウロパの地下には\n液体の海が存在すると考えられている。これにより、生命の可能性が\n注目されている。木星の重力は彗星や小惑星を引き寄せ、太陽系の\n「盾」の役割を果たしているとも言われる。その影響力は極めて大きい。\n木星はまさに太陽系の守護神的な存在である。\n",
"探査機「パイオニア」「ボイジャー」「ガリレオ」などが木星を訪問。\n現在はNASAの探査機「ジュノー(Juno)」が周回し、磁場や大気の\n詳細を観測している。ジュノーは木星の極軌道から画像を撮影し、\n内部構造や重力分布の研究に役立っている。今後はESAの探査機\n「JUICE」がガニメデやエウロパの調査を予定しており、地下海や\n生命の可能性を探るミッションが注目されている。木星はただの\n巨大惑星ではなく、太陽系の構造や進化を理解するための鍵でもある。\n人類に多くの謎と期待を与える壮大な天体だ。\n"]
},
6: {
text: ["土星\n正式名称:Saturn(サターン)\n直径は約120536kmで、太陽系で2番目に大きなガス惑星\n主に水素とヘリウムで構成され、美しい輪を持つことで有名\n",
"土星は太陽から6番目の惑星で、公転周期は約29.5年。\n自転周期は約10.7時間と早く、赤道がふくらんでいる。\n特徴的なのはその壮麗な「環(リング)」で、数千の薄い輪が\n集まってできている。主成分は氷と岩石の粒で、直径は数cm\nから数mまで様々。環の幅は非常に広いが厚さは数十m程度。\nこの輪は一時的なものと考えられ、長い時間で崩壊する可能性も。\n土星の大気は黄褐色で、縞模様や嵐も観測されている。\n内部は金属水素層と岩石核があると推定されている。\n",
"「サターン」はローマ神話の農耕神サトゥルヌスに由来し、\nギリシャ神話ではクロノスにあたる。父なる存在として知られ、\n豊穣と時間を司る神とされている。土星は肉眼で見える最遠の\n惑星であり、古代から「限界の星」として意識されていた。\n中国では「鎮星」と呼ばれ、五行では「土」にあてられている。\n土星の名前もここに由来している。古来から占星術でも重要視され、\n試練や忍耐の象徴とされている。輪の存在は17世紀にガリレオが\n望遠鏡で初めて観測し、人類に衝撃を与えた。\n",
"土星には確認されているだけで140個以上の衛星が存在する。\n最大の衛星「タイタン」は大気を持つ希少な月で、メタンの雨や\n川、湖が存在する。地球以外で安定した液体が地表にある唯一の例。\n他に氷の火山や地下海があるとされ、生命の可能性も注目されている。\n小さな衛星「エンケラドゥス」は氷の殻の下に海を持ち、南極から\n水蒸気の噴出が観測されている。これらは今後の探査対象となる。\n土星の重力は環の維持や衛星の軌道安定にも影響し、太陽系内の\n力学的なバランスにも大きな役割を果たしている。\n",
"NASAの探査機「カッシーニ」は2004年から2017年まで土星を詳細に観測。\nリングの構造、タイタンやエンケラドゥスの探査、磁場の測定など、\n膨大なデータを収集した。最期は「グランドフィナーレ」として土星の輪と\n大気の隙間に突入し、燃え尽きながら観測を続けた。現在は新たな探査計画も\n進行中で、タイタンへのドローン着陸ミッション「ドラゴンフライ」が予定されている。\n土星はその美しさだけでなく、系外惑星の研究や生命探査の鍵としても注目。\n人類の知的探究心に応える、壮大で神秘的な惑星だ。\n"]
},
7: {
text: ["天王星\n正式名称:Uranus(ウラヌス)\n直径は約51118kmで、太陽系で3番目に大きな惑星\n主に水素・ヘリウム・メタンを含む氷状のガス惑星(天王星型)\n",
"天王星は太陽から7番目の惑星で、公転周期は約84年。\n自転周期は約17時間だが、自転軸が横倒しに近く傾いている。\nこの極端な傾き(約98度)は、過去の巨大衝突によると考えられる。\nそのため季節変化が極端で、1年で昼や夜が42年続く地域もある。\n淡い青緑色の外観は、大気中のメタンが赤い光を吸収するため。\n気温は太陽系で最も低く、-224℃まで下がることもある。\n大気には雲や嵐があり、近年は活発な気象変化も観測されている。\n環を持ち、13本ほどの暗い細いリングが天王星を取り巻いている。\n",
"「ウラヌス」の名はギリシャ神話の天空神ウーラノスに由来し、\nこれは土星(クロノス)の父、すなわちゼウスの祖父にあたる。\n天王星は1781年にウィリアム・ハーシェルによって発見された。\n肉眼では見えにくく、望遠鏡による最初の惑星発見である。\n当初はイギリス王にちなんで「ジョージの星」と名付けられたが、\n後に神話の系譜を保つ形で「Uranus」に改名された。\n中国では五行思想の「天」に対応し、「天王星」と名付けられた。\n発見当時は革命と科学の時代であり、「変革の象徴」とされた。\n",
"天王星には確認されているだけで27個の衛星がある。\n主要な衛星には「ミランダ」「アリエル」「ウィンザー」などがあり、\nこれらの名前はシェイクスピアやポープの作品から採られている。\n「ミランダ」は断崖絶壁と巨大な地形の混在が特徴で、形成史に\n大規模な再結合や衝突の可能性が示唆されている。\n天王星の衛星は他の巨大惑星に比べると小さいが、地質活動や\n氷の層、暗い色の物質など多様性がある。衛星は赤道面ではなく、\n天王星の自転軸を中心に公転しているのも特徴的である。\n",
"天王星を訪れた探査機は1986年の「ボイジャー2号」のみ。\nこのときの観測で初めてリングや磁場、衛星の詳細が判明した。\nその後は地上観測とハッブル宇宙望遠鏡などによる監視が続いた。\n近年ではNASAやESAによる新たな探査計画も議論されており、\n天王星の内部構造、大気、衛星系の詳細解明が期待されている。\n氷巨星(アイスジャイアント)の代表である天王星の理解は、\n系外惑星研究にもつながり、今後の宇宙探査の焦点の一つだ。\n静かに輝く天王星は、まだ多くの謎を秘めた未知の世界である。\n"]
},
8: {
text: ["海王星\n正式名称:Neptune(ネプチューン)\n直径は約49244kmで、太陽系で4番目に大きな惑星\n主に水素・ヘリウム・メタンを含む氷状ガス惑星(天王星型)\n",
"海王星は太陽から8番目の惑星で、公転周期は約165年。\n自転周期は約16時間で、青色の大気が特徴的である。\nこの青色は大気中のメタンが赤色光を吸収し、青色光を反射。\n強い風速を持つ嵐が発生し、最高2000km/hにも達する。\n大赤斑に似た「ダークスポット」という嵐も観測された。\n海王星の内部は氷と岩石が混じり合ったコアと、厚いガス層。\n衛星は14個確認され、最大の衛星はトリトン。逆行軌道を持つ。\nトリトンは氷の火山活動や薄い大気を持つ、非常に興味深い天体。\n",
"名前の由来はローマ神話の海神ネプチューンにちなみ、\nその名は古代から海の力を象徴している。海王星は1846年に\n数学的予測により発見された珍しい惑星である。\n天文学者のアダムズとレヴィエが位置を計算し、観測で確定。\n海王星の発見は天文学の精度と計算力の象徴とされている。\n中国では「海王星」と訳され、五行思想の「水」に関連づけ。\n惑星の青さは神秘的な海のイメージを強め、多くの文化で注目。\n現代でも未知の氷巨星として研究が進み、系外惑星にも影響大。\n",
"海王星の衛星は14個あり、中でもトリトンは最大で、\n逆行軌道を持つ珍しい衛星だ。トリトンは太陽系の衛星の中でも\n最も低温で、氷の火山が活動している。薄い窒素大気も確認済み。\nトリトンの地質は複雑で、氷と岩石が混ざる地表や峡谷がある。\n海王星は衛星系がダイナミックで、捕獲衛星の可能性も指摘されている。\n内部は重い氷状物質と岩石の核があり、厚いガス層に包まれている。\n強い磁場を持ち、その起源は内部の動く氷の層によるものと考えられる。\n探査機「ボイジャー2号」が1989年に詳細な観測を行った。\n",
"ボイジャー2号の飛来以来、海王星は遠く離れているため観測が限られる。\nしかし近年は地上望遠鏡やハッブル宇宙望遠鏡による観測が充実。\n気象変動や嵐の発生状況が定期的に追跡されている。\n海王星は氷巨星として、太陽系の外縁の謎を解く鍵を握る。\n系外惑星研究の比較対象としても重要であり、今後の探査が期待。\nその神秘的な青色と強風は多くの天文学者の関心を集めている。\n海王星はまだ未知の多い惑星だが、宇宙の理解に大きな役割を果たす。\n"]
},
9: {
text: ["冥王星\n正式名称:Pluto(プルート)\n直径は約2376kmで、かつては第9番目の惑星として知られたが\n現在は準惑星に分類され、氷と岩石で構成されている天体\n",
"冥王星は1930年にクライド・トンボーが発見し、1930年代以降、\n太陽系の第9惑星として扱われてきた。だが2006年に国際天文学連合が\n準惑星の定義を設け、冥王星は正式に準惑星に分類された。\n冥王星の軌道は非常に楕円形で、海王星の軌道を時に交差する。\n軌道傾斜角も大きく、軌道周期は約248年。太陽からの距離は非常に遠い。\n表面は主に窒素氷、メタン氷、一酸化炭素氷で覆われており、\n表面温度は約-230℃と極寒である。大気は希薄で、太陽からの熱で\n時に薄く膨張し、夜間には凍結してしまう。\n",
"名前はローマ神話の冥界の神プルートに由来する。\n発見当時の命名は子供の意見も取り入れられたという逸話がある。\n冥王星の準惑星への降格は天文学界で大きな議論を呼び、今なお話題。\nまた、その独特の軌道と氷の地表は、太陽系外縁天体の研究に重要な意味を持つ。\n冥王星の最大の衛星はカロンで、カロンとの重心は冥王星の外にある。\nこのため、冥王星-カロンは二重天体とも呼ばれている。\n他に4つの小さな衛星(ニクス、ヒドラなど)が知られている。\n冥王星は氷と岩石が混ざった複雑な天体だ。\n",
"2015年にNASAの探査機「ニューホライズンズ」が初めて冥王星に接近。\n高解像度の画像とデータを送り返し、地質や大気の詳細が明らかになった。\n冥王星の表面には巨大な氷の平原「スプートニク・プラニティ」や、\n氷の山脈、クレーター、氷火山など多様な地形が確認された。\nカロンも詳細に観測され、峡谷や凍った地表の特徴が判明。\nこれらの発見により、冥王星は単なる冷たい塊ではなく、活動的な天体と認識。\n今後も太陽系外縁天体の理解に重要な役割を果たすと期待されている。\n宇宙のフロンティアの一つである。\n",
"冥王星の分類変更は惑星の定義と天文学の進歩を象徴している。\n太陽系の境界を理解するために、冥王星は重要な研究対象だ。\nその軌道の変化や衛星系の詳細は、太陽系形成の歴史を物語る。\n準惑星の仲間にはエリスやマケマケなどもあり、冥王星は代表格。\nこれらは「散乱円盤天体」と呼ばれ、外縁の小天体群として注目される。\n冥王星は今後も宇宙探査や理論研究の中心にあり続けるだろう。\n人類の宇宙理解に新たな視点をもたらした天体である。\n"]
},
10: {
text: ["このオブジェクトには情報源が複数あります:", "緑ボタン: A-Frame公式ドキュメント", "青ボタン: Google検索", "黄色ボタン: A-Frame GitHub"],
mainCommentTextColor: "#B0E0E6",
links: [
{ label: "A-Frame Docs", url: "https://aframe.io/docs/", buttonColor: "#2ECC71" },
{ label: "Google", url: "https://google.com", buttonColor: "#3498DB" },
{ label: "GitHub", url: "https://github.com/aframevr", buttonColor: "#F1C40F" }
]
},
};
const DEFAULT_COMMENT_ARRAY_INFO = { text: ["コメントはありません"], links: [] };
const numObjects = 9; const spread = 300; const PAGES = ['index', 'color', 'size', 'comment']; const TOTAL_MAIN_PAGES = PAGES.length;
const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3();
const LAYOUT_PATTERN = 5; console.log(`Using LAYOUT_PATTERN: ${LAYOUT_PATTERN}`);
const RIG_INITIAL_X = 0; const RIG_INITIAL_Y_CAMERA_LEVEL = 1.6; const RIG_INITIAL_Z = 5; const SPAWN_ANIM_START_POS_STRING = `${RIG_INITIAL_X} ${RIG_INITIAL_Y_CAMERA_LEVEL} ${RIG_INITIAL_Z}`;
const H_LINE_SPACING = 15; const H_LINE_Y = RIG_INITIAL_Y_CAMERA_LEVEL; const H_LINE_Z_OFFSET = -35;
const H_RING_RADIUS = 50; const H_RING_OBJECT_Y = RIG_INITIAL_Y_CAMERA_LEVEL; const HORIZONTAL_RING_LOOK_AT_TARGET = new THREE.Vector3(RIG_INITIAL_X, H_RING_OBJECT_Y, RIG_INITIAL_Z);
const GRID_COLUMNS = 4; const GRID_SPACING_X = 15; const GRID_SPACING_Y = 15; const GRID_INITIAL_Y = RIG_INITIAL_Y_CAMERA_LEVEL + 10; const GRID_Z_DEPTH = H_LINE_Z_OFFSET;
const V_LINE_SPACING = 15; const V_LINE_X_OFFSET = 0; const V_LINE_START_Y_OFFSET_FROM_CENTER = ( (numObjects-1) * V_LINE_SPACING / 2 ); const V_LINE_Z_OFFSET = -20;
const V_RING_RADIUS = 50; const V_RING_CENTER_Y = RIG_INITIAL_Y_CAMERA_LEVEL; const V_RING_FIXED_Z_OFFSET = -25; const VERTICAL_RING_LOOK_AT_TARGET = new THREE.Vector3(RIG_INITIAL_X, V_RING_CENTER_Y, RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET);
const H_RING_ANGLE_STEP_DEGREES = 0; const V_RING_ANGLE_STEP_DEGREES = 0;
// --- オブジェクト生成ループ (変更なし) ---
for (let i = 0; i < numObjects; i++) { const objectIndex = i + 1; const definition = OBJECT_DEFINITIONS[objectIndex] || {}; const objectType = definition.type || DEFAULT_OBJECT_TYPE; let objectEl; if (objectType === 'sphere') { objectEl = document.createElement('a-sphere'); } else { objectEl = document.createElement('a-box'); } let dimension; const sizeType = definition.sizeType || DEFAULT_SIZE_TYPE; if (sizeType === 'fixed' && typeof definition.fixedSize === 'number' && definition.fixedSize > 0) { dimension = definition.fixedSize; } else { if (sizeType === 'fixed') { console.warn(`Object ${objectIndex}: sizeType 'fixed' but fixedSize is invalid (${definition.fixedSize}). Defaulting to random size.`); } dimension = Math.random() * 10.0 + 0.5; } let finalX, finalY, finalZ; let applyRandomRotation = false; let initialRotation = null; switch (LAYOUT_PATTERN) { case 1: finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotation = true; break; case 2: finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotation = false; break; case 3: const totalLineWidthH_3 = (numObjects - 1) * H_LINE_SPACING; finalX = (-totalLineWidthH_3 / 2) + (i * H_LINE_SPACING) + RIG_INITIAL_X; finalY = H_LINE_Y; finalZ = RIG_INITIAL_Z + H_LINE_Z_OFFSET; applyRandomRotation = true; break; case 4: const totalLineWidthV_4 = (numObjects - 1) * H_LINE_SPACING; finalX = (-totalLineWidthV_4 / 2) + (i * H_LINE_SPACING) + RIG_INITIAL_X; finalY = H_LINE_Y; finalZ = RIG_INITIAL_Z + H_LINE_Z_OFFSET; applyRandomRotation = false; break; case 5: case 6: let angleStep360H; if (H_RING_ANGLE_STEP_DEGREES > 0) { angleStep360H = THREE.MathUtils.degToRad(H_RING_ANGLE_STEP_DEGREES); } else { angleStep360H = (Math.PI * 2) / numObjects; } const angle360H = i * angleStep360H; finalX = RIG_INITIAL_X + H_RING_RADIUS * Math.cos(angle360H); finalY = H_RING_OBJECT_Y; finalZ = RIG_INITIAL_Z + H_RING_RADIUS * Math.sin(angle360H); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(HORIZONTAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 5); break; case 7: case 8: const totalAngleSpan180H = Math.PI; const startAngleOffset180H = -Math.PI / 2; let angleStep180H_; if (H_RING_ANGLE_STEP_DEGREES > 0) { angleStep180H_ = THREE.MathUtils.degToRad(H_RING_ANGLE_STEP_DEGREES); } else { angleStep180H_ = numObjects > 1 ? totalAngleSpan180H / (numObjects - 1) : 0; } const thetaH = startAngleOffset180H + (i * angleStep180H_); finalX = RIG_INITIAL_X + H_RING_RADIUS * Math.sin(thetaH); finalY = H_RING_OBJECT_Y; finalZ = RIG_INITIAL_Z - H_RING_RADIUS * Math.cos(thetaH); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(HORIZONTAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 7); break; case 9: case 10: const column = i % GRID_COLUMNS; const row = Math.floor(i / GRID_COLUMNS); const totalGridWidth = (GRID_COLUMNS - 1) * GRID_SPACING_X; const startX = -totalGridWidth / 2; finalX = startX + column * GRID_SPACING_X + RIG_INITIAL_X; finalY = GRID_INITIAL_Y - row * GRID_SPACING_Y; finalZ = RIG_INITIAL_Z + GRID_Z_DEPTH; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (LAYOUT_PATTERN === 9); break; case 11: case 12: finalX = V_LINE_X_OFFSET + RIG_INITIAL_X; finalY = (RIG_INITIAL_Y_CAMERA_LEVEL + V_LINE_START_Y_OFFSET_FROM_CENTER) - (i * V_LINE_SPACING); finalZ = RIG_INITIAL_Z + V_LINE_Z_OFFSET; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (LAYOUT_PATTERN === 11); break; case 13: case 14: let angleStep360V; if (V_RING_ANGLE_STEP_DEGREES > 0) { angleStep360V = THREE.MathUtils.degToRad(V_RING_ANGLE_STEP_DEGREES); } else { angleStep360V = (Math.PI * 2) / numObjects; } const angle360V = i * angleStep360V; finalX = RIG_INITIAL_X; finalY = V_RING_CENTER_Y + V_RING_RADIUS * Math.cos(angle360V); finalZ = (RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET) + V_RING_RADIUS * Math.sin(angle360V); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(VERTICAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 13); break; case 15: case 16: const arcSpan180V = Math.PI; const startAngle180V = -Math.PI / 2; let angleStep180V_; if (V_RING_ANGLE_STEP_DEGREES > 0) { angleStep180V_ = THREE.MathUtils.degToRad(V_RING_ANGLE_STEP_DEGREES); } else { angleStep180V_ = numObjects > 1 ? arcSpan180V / (numObjects - 1) : 0; } const thetaV = startAngle180V + (i * angleStep180V_); finalX = RIG_INITIAL_X; finalY = V_RING_CENTER_Y + V_RING_RADIUS * Math.sin(thetaV); finalZ = (RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET) - V_RING_RADIUS * Math.cos(thetaV); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(VERTICAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 15); break; default: console.warn(`Unknown LAYOUT_PATTERN: ${LAYOUT_PATTERN}, defaulting to pattern 1.`); finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotation = true; }
if (objectType === 'sphere') { objectEl.setAttribute('radius', dimension); } else { objectEl.setAttribute('width', dimension); objectEl.setAttribute('height', dimension); objectEl.setAttribute('depth', dimension); } let appliedColor; if (definition.useTextureForIndex && definition.useTextureForIndex >= 1 && definition.useTextureForIndex <= MAX_TEXTURE_INDEX) { const textureId = `${TEXTURE_ID_PREFIX}${definition.useTextureForIndex}`; objectEl.setAttribute('material', 'src', `#${textureId}`); appliedColor = '#FFFFFF'; objectEl.setAttribute('color', appliedColor); } else if (definition.specificColor) { appliedColor = definition.specificColor; objectEl.setAttribute('color', appliedColor); if(objectEl.hasAttribute('material')) objectEl.removeAttribute('material','src'); } else { appliedColor = `hsl(${Math.random() * 360}, 50%, 75%)`; objectEl.setAttribute('color', appliedColor); if(objectEl.hasAttribute('material')) objectEl.removeAttribute('material','src'); } objectEl.setAttribute('scale', '0.01 0.01 0.01'); objectEl.setAttribute('position', SPAWN_ANIM_START_POS_STRING); if (initialRotation) { objectEl.setAttribute('rotation', initialRotation); } else if (!applyRandomRotation) { objectEl.setAttribute('rotation', '0 0 0');} objectEl.classList.add('clickableObject'); objectEl.dataset.objectIndex = objectIndex; objectEl.dataset.color = appliedColor; objectEl.dataset.dimension = dimension.toFixed(2); objectEl.dataset.objectType = objectType; if (applyRandomRotation) { objectEl.setAttribute('random-rotate', {maxSpeed: 5}); } objectEl.addEventListener('click', handleObjectClick); sceneEl.appendChild(objectEl); objectEl.setAttribute('animation__spawnscale', { property: 'scale', to: '1 1 1', dur: 3000, easing: 'easeOutQuad' }); objectEl.setAttribute('animation__spawnposition', { property: 'position', to: `${finalX} ${finalY} ${finalZ}`, dur: 3000, easing: 'easeOutQuad'}); }
// --- ★★★ 情報パネルの表示更新関数 (コメント(1/1)非表示ロジック修正) ★★★ ---
function updatePanelDisplay() {
if (!infoPanelEl.dataset.objectIndex) return;
const index = parseInt(infoPanelEl.dataset.objectIndex || '0', 10);
const sphereColor = infoPanelEl.dataset.color || 'N/A';
const dimensionValue = infoPanelEl.dataset.dimension || 'N/A';
const objectType = infoPanelEl.dataset.objectType || DEFAULT_OBJECT_TYPE;
const commentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO));
const commentsArray = commentInfo.text;
const mainCommentTextColor = commentInfo.mainCommentTextColor;
const activeLinks = JSON.parse(infoPanelEl.dataset.activeLinks || "[]");
const commentSubPageCount = commentsArray.length;
infoPanelEl.dataset.commentSubPageCount = commentSubPageCount.toString();
const commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10);
const mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[mainPageIndex];
let pageIndicator = '';
let currentStyle = { ...(PAGE_TEXT_STYLES[pageType] || PAGE_TEXT_STYLES['index']) };
if (EXE_MODE === 0) {
displayText = commentsArray[commentSubPageIndex] || DEFAULT_COMMENT_ARRAY_INFO.text[0];
if (commentSubPageCount > 1) { // サブページが2ページ以上ある時のみインジケータ表示
pageIndicator = `(${commentSubPageIndex + 1}/${commentSubPageCount})`;
} else {
pageIndicator = ''; // 1ページのみ、または「コメントはありません」の場合はインジケータなし
}
if (mainCommentTextColor) { currentStyle.color = mainCommentTextColor; }
} else { // EXE_MODE === 1
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`;
if (pageType === 'index') { displayText = `${objectType === 'sphere' ? '球' : '立方体'}: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${sphereColor}`; }
else if (pageType === 'size') { displayText = `${objectType === 'sphere' ? '半径' : 'サイズ'}: ${dimensionValue}`; }
else if (pageType === 'comment') {
displayText = `コメント:\n${commentsArray[commentSubPageIndex] || DEFAULT_COMMENT_ARRAY_INFO.text[0]}`;
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`;
if (mainCommentTextColor) { currentStyle.color = mainCommentTextColor; }
}
}
linkButtons.forEach(btn => btn.setAttribute('visible', false));
if ( (EXE_MODE === 0 || pageType === 'comment') && activeLinks && activeLinks.length > 0) {
activeLinks.forEach((link, i) => {
if (linkButtons[i]) {
linkButtons[i].setAttribute('visible', true);
linkButtons[i].setAttribute('material', 'color', link.buttonColor || 'lime');
linkButtons[i].dataset.url = link.url;
}
});
}
const finalDisplayText = EXE_MODE === 0 ? `${displayText}${pageIndicator ? '\n\n' + pageIndicator : ''}`.trim() : `${pageIndicator}\n${displayText}`;
panelTextEl.setAttribute('troika-text', { value: finalDisplayText, color: currentStyle.color, fontSize: currentStyle.fontSize, maxWidth: currentStyle.maxWidth, align: currentStyle.align, anchorX: currentStyle.anchorX, anchorY: currentStyle.anchorY, baseline: currentStyle.baseline });
}
// --- オブジェクトクリック時の処理 (変更なし) ---
function handleObjectClick(event) { /* ... (前回と同じ) ... */ event.stopPropagation(); console.log("--- handleObjectClick triggered --- Target:", event.target.id || event.target.tagName); const clickedObject = event.target; if (!clickedObject.dataset.objectIndex || !clickedObject.dataset.color || !clickedObject.dataset.dimension || !clickedObject.dataset.objectType) { console.error("Object data missing from dataset!", clickedObject.dataset); return; } console.log("Object data found:", clickedObject.dataset); infoPanelEl.dataset.objectIndex = clickedObject.dataset.objectIndex; infoPanelEl.dataset.color = clickedObject.dataset.color; infoPanelEl.dataset.dimension = clickedObject.dataset.dimension; infoPanelEl.dataset.objectType = clickedObject.dataset.objectType; if (EXE_MODE === 0) { infoPanelEl.dataset.currentPageIndex = PAGES.indexOf('comment').toString(); } else { infoPanelEl.dataset.currentPageIndex = '0'; } const objectIdx = parseInt(clickedObject.dataset.objectIndex, 10); const commentDataForThisObject = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(commentDataForThisObject); infoPanelEl.dataset.commentSubPageCount = commentDataForThisObject.text.length.toString(); infoPanelEl.dataset.commentSubPageIndex = '0'; if (commentDataForThisObject.links && commentDataForThisObject.links.length > 0) { infoPanelEl.dataset.activeLinks = JSON.stringify(commentDataForThisObject.links); } else { infoPanelEl.dataset.activeLinks = JSON.stringify([]); } if (commentDataForThisObject.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = commentDataForThisObject.mainCommentTextColor; } else { delete infoPanelEl.dataset.commentPageTextColor; } console.log("Data (including comments, links, text color) stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedObject.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const dimensionVal = parseFloat(clickedObject.dataset.dimension || 0); const objectTypeVal = clickedObject.dataset.objectType || DEFAULT_OBJECT_TYPE; const baseOffset = (objectTypeVal === 'sphere') ? dimensionVal : dimensionVal / 2; const offsetDistance = baseOffset + PANEL_OFFSET_FROM_OBJECT; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); } catch(e) { console.error("Error during position calculation:", e); return; } infoPanelEl.setAttribute('visible', true); console.log("Panel visibility set to true. --- handleObjectClick end ---"); }
// --- パネルのボタンクリック処理 (変更なし) ---
prevButtonEl.addEventListener('click', function (event) { /* ... (前回と同じ) ... */ event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const currentCommentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); const commentSubPageCount = currentCommentInfo.text.length; if (EXE_MODE === 0 || (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1)) { commentSubPageIndex--; if (commentSubPageIndex < 0) { if (EXE_MODE === 0) { commentSubPageIndex = Math.max(0, commentSubPageCount - 1); } else { mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; if (PAGES[mainPageIndex] === 'comment') { const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; commentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } else { commentSubPageIndex = 0; } } } } else if (EXE_MODE === 1) { mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; commentSubPageIndex = 0; if (PAGES[mainPageIndex] === 'comment') { const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; commentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString(); updatePanelDisplay(); console.log("Prev button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex); });
nextButtonEl.addEventListener('click', function (event) { /* ... (前回と同じ) ... */ event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const currentCommentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); const commentSubPageCount = currentCommentInfo.text.length; if (EXE_MODE === 0 || (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1)) { commentSubPageIndex++; if (commentSubPageIndex >= commentSubPageCount) { if (EXE_MODE === 0) { commentSubPageIndex = 0; } else { mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES; commentSubPageIndex = 0; } } } else if (EXE_MODE === 1) { mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES; commentSubPageIndex = 0; } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString(); updatePanelDisplay(); console.log("Next button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex); });
// --- 閉じるボタンのクリック処理 (変更なし) ---
closeButtonEl.addEventListener('click', function (event) { /* ... (前回と同じ) ... */ event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.objectIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.dimension; delete infoPanelEl.dataset.objectType; delete infoPanelEl.dataset.currentPageIndex; delete infoPanelEl.dataset.commentInfo; delete infoPanelEl.dataset.commentSubPageIndex; delete infoPanelEl.dataset.commentSubPageCount; delete infoPanelEl.dataset.activeLinks; delete infoPanelEl.dataset.commentPageTextColor; linkButtons.forEach(btn => btn.setAttribute('visible', false)); console.log("Close button clicked, panel hidden."); });
// --- リンクボタンのクリック処理 (変更なし) ---
function handleLinkButtonClick(event) { /* ... (前回と同じ) ... */ event.stopPropagation(); const urlToOpen = event.target.dataset.url; if (!urlToOpen) { console.log("Link button clicked, but no URL found on this button."); return; } console.log("Link button clicked, starting absorption animation for URL:", urlToOpen); const linkButtonWorldPos = new THREE.Vector3(); event.target.object3D.getWorldPosition(linkButtonWorldPos); const allClickableObjects = document.querySelectorAll('.clickableObject'); const animationDuration = 3000; allClickableObjects.forEach(objEl => { objEl.removeAttribute('animation__scale吸い込み'); objEl.removeAttribute('animation__position吸い込み'); objEl.setAttribute('animation__scale吸い込み', { property: 'scale', to: '0.01 0.01 0.01', dur: animationDuration, easing: 'easeInQuad' }); objEl.setAttribute('animation__position吸い込み', { property: 'position', to: `${linkButtonWorldPos.x} ${linkButtonWorldPos.y} ${linkButtonWorldPos.z}`, dur: animationDuration, easing: 'easeInQuad' }); }); setTimeout(() => { console.log("Animation finished, navigating to URL:", urlToOpen); window.location.href = urlToOpen; }, animationDuration); }
// --- VRモード制御とコントローラーイベントリスナー (変更なし) ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
sceneEl.addEventListener('loaded', function() { for (let i = 0; i < 3; i++) { const btn = document.getElementById(`linkButton${i}`); if (btn) { linkButtons.push(btn); btn.addEventListener('click', handleLinkButtonClick); } } rightHandEl = document.getElementById('rightHand'); if (rightHandEl) { rightHandEl.addEventListener('triggerdown', function (evt) { console.log('Right hand TRIGGER DOWN event!', evt); const raycasterComponent = rightHandEl.components.raycaster; if (raycasterComponent) { const intersectedEls = raycasterComponent.intersectedEls; if (intersectedEls.length > 0) { console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id'); } else { console.log('Right Trigger pressed, but no intersection.'); } } }); rightHandEl.addEventListener('triggerup', function (evt) { console.log('Right hand TRIGGER UP event!', evt); }); } else { console.error("Could not find rightHand element to attach trigger listener."); } });
</script>
</a-scene>
</body>
</html>
taiyoukei-test1
├
<!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 },
rotationSpeed: { type: 'number', default: 1.5 },
pitchLimit: { type: 'number', default: 85 },
verticalSpeed: { type: 'number', default: 30 } // 前回追加した上下移動速度
},
init: function () {
this.keys = {};
this.leftThumbstickInput = { x: 0, y: 0 };
this.rightThumbstickInput = { 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.rigEl = this.el;
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.onLeftThumbstickMoved.bind(this));
} else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); }
this.rightHand = document.getElementById('rightHand');
if (this.rightHand) {
this.rightHand.addEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this));
} else { console.warn("camera-relative-controls: 右手コントローラー(#rightHand)が見つかりません。"); }
});
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.onLeftThumbstickMoved.bind(this)); } catch(e){} } if (this.rightHand) { try { this.rightHand.removeEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); } catch(e){} } },
onLeftThumbstickMoved: function (evt) { this.leftThumbstickInput.x = evt.detail.x; this.leftThumbstickInput.y = evt.detail.y; },
onRightThumbstickMoved: function (evt) { this.rightThumbstickInput.x = evt.detail.x; this.rightThumbstickInput.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 || !this.rigEl || !this.rigEl.object3D) { return; }
const data = this.data;
const dt = timeDelta / 1000;
const rigObject = this.rigEl.object3D;
const cameraObject = this.cameraEl.object3D;
// --- 右スティックによる視点回転と上下移動処理 ---
if (this.rigEl.sceneEl.is('vr-mode')) {
if (Math.abs(this.rightThumbstickInput.x) > 0.1) {
const yawAngle = -this.rightThumbstickInput.x * data.rotationSpeed * dt;
rigObject.rotation.y += yawAngle;
}
if (Math.abs(this.rightThumbstickInput.y) > 0.1) {
const verticalMovement = this.rightThumbstickInput.y * data.verticalSpeed * dt;
rigObject.position.y -= verticalMovement;
}
}
// --- 視点回転と上下移動処理ここまで ---
// --- 移動処理 (左スティックとキーボード) ---
const position = rigObject.position; // rigの現在位置
cameraObject.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.leftThumbstickInput.y) > 0.1) {
const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.leftThumbstickInput.y);
this.moveDirection.add(forwardBackward);
}
if (Math.abs(this.leftThumbstickInput.x) > 0.1) {
const leftRight = this.cameraRight.clone().multiplyScalar(this.leftThumbstickInput.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" vr-mode-ui="enabled: true">
<a-sky id="backgroundSkyElement" src="./pic/u7.jpg"></a-sky>
<a-assets>
<img id="tex_a1" src="./pic/a1.jpg" crossOrigin="anonymous">
<img id="tex_a2" src="./pic/a2.jpg" crossOrigin="anonymous">
<img id="tex_a3" src="./pic/a3.jpg" crossOrigin="anonymous">
<img id="tex_a4" src="./pic/a4.jpg" crossOrigin="anonymous">
<img id="tex_a5" src="./pic/a5.jpg" crossOrigin="anonymous">
<img id="tex_a6" src="./pic/a6.jpg" crossOrigin="anonymous">
<img id="tex_a7" src="./pic/a7.jpg" crossOrigin="anonymous">
<img id="tex_a8" src="./pic/a8.jpg" crossOrigin="anonymous">
<img id="tex_a9" src="./pic/a9.jpg" crossOrigin="anonymous">
<img id="tex_a10" src="./pic/a10.jpg" crossOrigin="anonymous">
</a-assets>
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1; rotationSpeed: 1.5; pitchLimit: 85; verticalSpeed: 30;">
<a-entity id="camera" camera="far: 20000;" look-controls="pointerLockEnabled: false; touchEnabled: false" position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;" raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" laser-controls="hand: right; model: false; lineColor: white; lineOpacity: 0.75" ></a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-cone id="prevButton" class="clickableButton" position="-20.0 0 0.1" rotation="0 0 90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-cone id="nextButton" class="clickableButton" position="20.0 0 0.1" rotation="0 0 -90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-sphere id="closeButton" class="clickableButton" position="24 11 0.1" radius="1.5" color="red" shader="flat"> </a-sphere>
<a-entity id="linkButtonsContainer" position="-22 -5 0.1">
<a-sphere id="linkButton0" class="clickableButton linkButton" visible="false" radius="1.5" position="0 0 0" shader="flat"></a-sphere>
<a-sphere id="linkButton1" class="clickableButton linkButton" visible="false" radius="1.5" position="0 -4 0" shader="flat"></a-sphere>
<a-sphere id="linkButton2" class="clickableButton linkButton" visible="false" radius="1.5" position="0 -8 0" shader="flat"></a-sphere>
</a-entity>
</a-entity>
<script>
// --- SCRIPT BLOCK 2: Main Scene Logic ---
const sceneEl = document.getElementById('myScene');
const skyElement = document.getElementById('backgroundSkyElement');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
const linkButtons = [];
let rightHandEl = null;
const EXE_MODE = 0;
const SKY_BACKGROUND_IMAGE_PATH = "./pic/u6.jpg";
if (SKY_BACKGROUND_IMAGE_PATH && SKY_BACKGROUND_IMAGE_PATH !== "") { skyElement.setAttribute('src', SKY_BACKGROUND_IMAGE_PATH); skyElement.setAttribute('visible', 'true');}
else { skyElement.setAttribute('visible', false); skyElement.removeAttribute('src'); sceneEl.setAttribute('background', 'color', '#000000');}
const PANEL_OFFSET_FROM_OBJECT = 5.0;
const BASE_TEXT_STYLE = { maxWidth: 46, align: 'center', anchorX: 'center', anchorY: 'middle', baseline: 'middle' };
const PAGE_TEXT_STYLES = { 'index': { ...BASE_TEXT_STYLE, fontSize: 0.72, color: 'white' }, 'color': { ...BASE_TEXT_STYLE, fontSize: 0.65, color: '#A0F0A0' }, 'size': { ...BASE_TEXT_STYLE, fontSize: 0.80, color: '#FFEC8B' }, 'comment': { ...BASE_TEXT_STYLE, fontSize: 0.70, color: '#ADD8E6' } };
// ★★★ オブジェクトごとの定義 (見やすく改行、整形) ★★★
const OBJECT_DEFINITIONS = {
1: { type: 'sphere', useTextureForIndex: 1, sizeType: 'fixed', fixedSize: 3.0 },
2: { type: 'box', specificColor: 'blue', sizeType: 'random' },
3: { type: 'sphere', specificColor: '#FF1493', sizeType: 'fixed', fixedSize: 4.0 }, // ディープピンク
4: { type: 'box', useTextureForIndex: 4 },
5: { type: 'box', sizeType: 'fixed', fixedSize: 2.0, specificColor: 'cyan' },
6: { type: 'sphere', useTextureForIndex: 6 },
7: { type: 'box', specificColor: 'orange', sizeType: 'fixed', fixedSize: 4.5 },
8: { type: 'sphere', useTextureForIndex: 8 },
9: { type: 'box', useTextureForIndex: 9, sizeType: 'random'},
10: { type: 'sphere', useTextureForIndex: 10, sizeType: 'fixed', fixedSize: 6.0 },
};
const DEFAULT_OBJECT_TYPE = 'box'; const DEFAULT_SIZE_TYPE = 'random'; const TEXTURE_ID_PREFIX = 'tex_a'; const MAX_TEXTURE_INDEX = 10;
// ★★★ コメントデータ (見やすく改行、整形) ★★★
const CUBE_COMMENTS = {
1: {
text: ["最初のオブジェクト!", "何か秘密が隠されているかもしれない…"],
mainCommentTextColor: "#FFDA63"
},
2: {
text: ["オブジェクト2のコメント。\n改行もできます。"]
},
3: {
text: ["オブジェクト3にはリンクがあります。"],
links: [{label:"A-Frame Site", url:"https://aframe.io", buttonColor:"green"}]
},
10: {
text: ["このオブジェクトには情報源が複数あります:", "緑ボタン: A-Frame公式ドキュメント", "青ボタン: Google検索", "黄色ボタン: A-Frame GitHub"],
mainCommentTextColor: "#B0E0E6",
links: [
{ label: "A-Frame Docs", url: "https://aframe.io/docs/", buttonColor: "#2ECC71" },
{ label: "Google", url: "https://google.com", buttonColor: "#3498DB" },
{ label: "GitHub", url: "https://github.com/aframevr", buttonColor: "#F1C40F" }
]
},
};
const DEFAULT_COMMENT_ARRAY_INFO = { text: ["コメントはありません"], links: [] };
const numObjects = 10; const spread = 300; const PAGES = ['index', 'color', 'size', 'comment']; const TOTAL_MAIN_PAGES = PAGES.length;
const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3();
const LAYOUT_PATTERN = 16; console.log(`Using LAYOUT_PATTERN: ${LAYOUT_PATTERN}`);
const RIG_INITIAL_X = 0; const RIG_INITIAL_Y_CAMERA_LEVEL = 1.6; const RIG_INITIAL_Z = 5; const SPAWN_ANIM_START_POS_STRING = `${RIG_INITIAL_X} ${RIG_INITIAL_Y_CAMERA_LEVEL} ${RIG_INITIAL_Z}`;
const H_LINE_SPACING = 15; const H_LINE_Y = RIG_INITIAL_Y_CAMERA_LEVEL; const H_LINE_Z_OFFSET = -35;
const H_RING_RADIUS = 50; const H_RING_OBJECT_Y = RIG_INITIAL_Y_CAMERA_LEVEL; const HORIZONTAL_RING_LOOK_AT_TARGET = new THREE.Vector3(RIG_INITIAL_X, H_RING_OBJECT_Y, RIG_INITIAL_Z);
const GRID_COLUMNS = 4; const GRID_SPACING_X = 15; const GRID_SPACING_Y = 15; const GRID_INITIAL_Y = RIG_INITIAL_Y_CAMERA_LEVEL + 10; const GRID_Z_DEPTH = H_LINE_Z_OFFSET;
const V_LINE_SPACING = 15; const V_LINE_X_OFFSET = 0; const V_LINE_START_Y_OFFSET_FROM_CENTER = ( (numObjects-1) * V_LINE_SPACING / 2 ); const V_LINE_Z_OFFSET = -20;
const V_RING_RADIUS = 50; const V_RING_CENTER_Y = RIG_INITIAL_Y_CAMERA_LEVEL; const V_RING_FIXED_Z_OFFSET = -25; const VERTICAL_RING_LOOK_AT_TARGET = new THREE.Vector3(RIG_INITIAL_X, V_RING_CENTER_Y, RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET);
const H_RING_ANGLE_STEP_DEGREES = 0; const V_RING_ANGLE_STEP_DEGREES = 0;
// --- オブジェクト生成ループ (変更なし) ---
for (let i = 0; i < numObjects; i++) { const objectIndex = i + 1; const definition = OBJECT_DEFINITIONS[objectIndex] || {}; const objectType = definition.type || DEFAULT_OBJECT_TYPE; let objectEl; if (objectType === 'sphere') { objectEl = document.createElement('a-sphere'); } else { objectEl = document.createElement('a-box'); } let dimension; const sizeType = definition.sizeType || DEFAULT_SIZE_TYPE; if (sizeType === 'fixed' && typeof definition.fixedSize === 'number' && definition.fixedSize > 0) { dimension = definition.fixedSize; } else { if (sizeType === 'fixed') { console.warn(`Object ${objectIndex}: sizeType 'fixed' but fixedSize is invalid (${definition.fixedSize}). Defaulting to random size.`); } dimension = Math.random() * 10.0 + 0.5; } let finalX, finalY, finalZ; let applyRandomRotation = false; let initialRotation = null; switch (LAYOUT_PATTERN) { case 1: finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotation = true; break; case 2: finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotation = false; break; case 3: const totalLineWidthH_3 = (numObjects - 1) * H_LINE_SPACING; finalX = (-totalLineWidthH_3 / 2) + (i * H_LINE_SPACING) + RIG_INITIAL_X; finalY = H_LINE_Y; finalZ = RIG_INITIAL_Z + H_LINE_Z_OFFSET; applyRandomRotation = true; break; case 4: const totalLineWidthV_4 = (numObjects - 1) * H_LINE_SPACING; finalX = (-totalLineWidthV_4 / 2) + (i * H_LINE_SPACING) + RIG_INITIAL_X; finalY = H_LINE_Y; finalZ = RIG_INITIAL_Z + H_LINE_Z_OFFSET; applyRandomRotation = false; break; case 5: case 6: let angleStep360H; if (H_RING_ANGLE_STEP_DEGREES > 0) { angleStep360H = THREE.MathUtils.degToRad(H_RING_ANGLE_STEP_DEGREES); } else { angleStep360H = (Math.PI * 2) / numObjects; } const angle360H = i * angleStep360H; finalX = RIG_INITIAL_X + H_RING_RADIUS * Math.cos(angle360H); finalY = H_RING_OBJECT_Y; finalZ = RIG_INITIAL_Z + H_RING_RADIUS * Math.sin(angle360H); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(HORIZONTAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 5); break; case 7: case 8: const totalAngleSpan180H = Math.PI; const startAngleOffset180H = -Math.PI / 2; let angleStep180H_; if (H_RING_ANGLE_STEP_DEGREES > 0) { angleStep180H_ = THREE.MathUtils.degToRad(H_RING_ANGLE_STEP_DEGREES); } else { angleStep180H_ = numObjects > 1 ? totalAngleSpan180H / (numObjects - 1) : 0; } const thetaH = startAngleOffset180H + (i * angleStep180H_); finalX = RIG_INITIAL_X + H_RING_RADIUS * Math.sin(thetaH); finalY = H_RING_OBJECT_Y; finalZ = RIG_INITIAL_Z - H_RING_RADIUS * Math.cos(thetaH); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(HORIZONTAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 7); break; case 9: case 10: const column = i % GRID_COLUMNS; const row = Math.floor(i / GRID_COLUMNS); const totalGridWidth = (GRID_COLUMNS - 1) * GRID_SPACING_X; const startX = -totalGridWidth / 2; finalX = startX + column * GRID_SPACING_X + RIG_INITIAL_X; finalY = GRID_INITIAL_Y - row * GRID_SPACING_Y; finalZ = RIG_INITIAL_Z + GRID_Z_DEPTH; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (LAYOUT_PATTERN === 9); break; case 11: case 12: finalX = V_LINE_X_OFFSET + RIG_INITIAL_X; finalY = (RIG_INITIAL_Y_CAMERA_LEVEL + V_LINE_START_Y_OFFSET_FROM_CENTER) - (i * V_LINE_SPACING); finalZ = RIG_INITIAL_Z + V_LINE_Z_OFFSET; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (LAYOUT_PATTERN === 11); break; case 13: case 14: let angleStep360V; if (V_RING_ANGLE_STEP_DEGREES > 0) { angleStep360V = THREE.MathUtils.degToRad(V_RING_ANGLE_STEP_DEGREES); } else { angleStep360V = (Math.PI * 2) / numObjects; } const angle360V = i * angleStep360V; finalX = RIG_INITIAL_X; finalY = V_RING_CENTER_Y + V_RING_RADIUS * Math.cos(angle360V); finalZ = (RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET) + V_RING_RADIUS * Math.sin(angle360V); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(VERTICAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 13); break; case 15: case 16: const arcSpan180V = Math.PI; const startAngle180V = -Math.PI / 2; let angleStep180V_; if (V_RING_ANGLE_STEP_DEGREES > 0) { angleStep180V_ = THREE.MathUtils.degToRad(V_RING_ANGLE_STEP_DEGREES); } else { angleStep180V_ = numObjects > 1 ? arcSpan180V / (numObjects - 1) : 0; } const thetaV = startAngle180V + (i * angleStep180V_); finalX = RIG_INITIAL_X; finalY = V_RING_CENTER_Y + V_RING_RADIUS * Math.sin(thetaV); finalZ = (RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET) - V_RING_RADIUS * Math.cos(thetaV); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(VERTICAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 15); break; default: console.warn(`Unknown LAYOUT_PATTERN: ${LAYOUT_PATTERN}, defaulting to pattern 1.`); finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotation = true; }
if (objectType === 'sphere') { objectEl.setAttribute('radius', dimension); } else { objectEl.setAttribute('width', dimension); objectEl.setAttribute('height', dimension); objectEl.setAttribute('depth', dimension); } let appliedColor; if (definition.useTextureForIndex && definition.useTextureForIndex >= 1 && definition.useTextureForIndex <= MAX_TEXTURE_INDEX) { const textureId = `${TEXTURE_ID_PREFIX}${definition.useTextureForIndex}`; objectEl.setAttribute('material', 'src', `#${textureId}`); appliedColor = '#FFFFFF'; objectEl.setAttribute('color', appliedColor); } else if (definition.specificColor) { appliedColor = definition.specificColor; objectEl.setAttribute('color', appliedColor); if(objectEl.hasAttribute('material')) objectEl.removeAttribute('material','src'); } else { appliedColor = `hsl(${Math.random() * 360}, 50%, 75%)`; objectEl.setAttribute('color', appliedColor); if(objectEl.hasAttribute('material')) objectEl.removeAttribute('material','src'); } objectEl.setAttribute('scale', '0.01 0.01 0.01'); objectEl.setAttribute('position', SPAWN_ANIM_START_POS_STRING); if (initialRotation) { objectEl.setAttribute('rotation', initialRotation); } else if (!applyRandomRotation) { objectEl.setAttribute('rotation', '0 0 0');} objectEl.classList.add('clickableObject'); objectEl.dataset.objectIndex = objectIndex; objectEl.dataset.color = appliedColor; objectEl.dataset.dimension = dimension.toFixed(2); objectEl.dataset.objectType = objectType; if (applyRandomRotation) { objectEl.setAttribute('random-rotate', {maxSpeed: 5}); } objectEl.addEventListener('click', handleObjectClick); sceneEl.appendChild(objectEl); objectEl.setAttribute('animation__spawnscale', { property: 'scale', to: '1 1 1', dur: 3000, easing: 'easeOutQuad' }); objectEl.setAttribute('animation__spawnposition', { property: 'position', to: `${finalX} ${finalY} ${finalZ}`, dur: 3000, easing: 'easeOutQuad'}); }
// --- ★★★ 情報パネルの表示更新関数 (コメント(1/1)非表示ロジック修正) ★★★ ---
function updatePanelDisplay() {
if (!infoPanelEl.dataset.objectIndex) return;
const index = parseInt(infoPanelEl.dataset.objectIndex || '0', 10);
const sphereColor = infoPanelEl.dataset.color || 'N/A';
const dimensionValue = infoPanelEl.dataset.dimension || 'N/A';
const objectType = infoPanelEl.dataset.objectType || DEFAULT_OBJECT_TYPE;
const commentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO));
const commentsArray = commentInfo.text;
const mainCommentTextColor = commentInfo.mainCommentTextColor;
const activeLinks = JSON.parse(infoPanelEl.dataset.activeLinks || "[]");
const commentSubPageCount = commentsArray.length;
infoPanelEl.dataset.commentSubPageCount = commentSubPageCount.toString();
const commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10);
const mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[mainPageIndex];
let pageIndicator = '';
let currentStyle = { ...(PAGE_TEXT_STYLES[pageType] || PAGE_TEXT_STYLES['index']) };
if (EXE_MODE === 0) {
displayText = commentsArray[commentSubPageIndex] || DEFAULT_COMMENT_ARRAY_INFO.text[0];
if (commentSubPageCount > 1) { // サブページが2ページ以上ある時のみインジケータ表示
pageIndicator = `(${commentSubPageIndex + 1}/${commentSubPageCount})`;
} else {
pageIndicator = ''; // 1ページのみ、または「コメントはありません」の場合はインジケータなし
}
if (mainCommentTextColor) { currentStyle.color = mainCommentTextColor; }
} else { // EXE_MODE === 1
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`;
if (pageType === 'index') { displayText = `${objectType === 'sphere' ? '球' : '立方体'}: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${sphereColor}`; }
else if (pageType === 'size') { displayText = `${objectType === 'sphere' ? '半径' : 'サイズ'}: ${dimensionValue}`; }
else if (pageType === 'comment') {
displayText = `コメント:\n${commentsArray[commentSubPageIndex] || DEFAULT_COMMENT_ARRAY_INFO.text[0]}`;
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`;
if (mainCommentTextColor) { currentStyle.color = mainCommentTextColor; }
}
}
linkButtons.forEach(btn => btn.setAttribute('visible', false));
if ( (EXE_MODE === 0 || pageType === 'comment') && activeLinks && activeLinks.length > 0) {
activeLinks.forEach((link, i) => {
if (linkButtons[i]) {
linkButtons[i].setAttribute('visible', true);
linkButtons[i].setAttribute('material', 'color', link.buttonColor || 'lime');
linkButtons[i].dataset.url = link.url;
}
});
}
const finalDisplayText = EXE_MODE === 0 ? `${displayText}${pageIndicator ? '\n\n' + pageIndicator : ''}`.trim() : `${pageIndicator}\n${displayText}`;
panelTextEl.setAttribute('troika-text', { value: finalDisplayText, color: currentStyle.color, fontSize: currentStyle.fontSize, maxWidth: currentStyle.maxWidth, align: currentStyle.align, anchorX: currentStyle.anchorX, anchorY: currentStyle.anchorY, baseline: currentStyle.baseline });
}
// --- オブジェクトクリック時の処理 (変更なし) ---
function handleObjectClick(event) { /* ... (前回と同じ) ... */ event.stopPropagation(); console.log("--- handleObjectClick triggered --- Target:", event.target.id || event.target.tagName); const clickedObject = event.target; if (!clickedObject.dataset.objectIndex || !clickedObject.dataset.color || !clickedObject.dataset.dimension || !clickedObject.dataset.objectType) { console.error("Object data missing from dataset!", clickedObject.dataset); return; } console.log("Object data found:", clickedObject.dataset); infoPanelEl.dataset.objectIndex = clickedObject.dataset.objectIndex; infoPanelEl.dataset.color = clickedObject.dataset.color; infoPanelEl.dataset.dimension = clickedObject.dataset.dimension; infoPanelEl.dataset.objectType = clickedObject.dataset.objectType; if (EXE_MODE === 0) { infoPanelEl.dataset.currentPageIndex = PAGES.indexOf('comment').toString(); } else { infoPanelEl.dataset.currentPageIndex = '0'; } const objectIdx = parseInt(clickedObject.dataset.objectIndex, 10); const commentDataForThisObject = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(commentDataForThisObject); infoPanelEl.dataset.commentSubPageCount = commentDataForThisObject.text.length.toString(); infoPanelEl.dataset.commentSubPageIndex = '0'; if (commentDataForThisObject.links && commentDataForThisObject.links.length > 0) { infoPanelEl.dataset.activeLinks = JSON.stringify(commentDataForThisObject.links); } else { infoPanelEl.dataset.activeLinks = JSON.stringify([]); } if (commentDataForThisObject.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = commentDataForThisObject.mainCommentTextColor; } else { delete infoPanelEl.dataset.commentPageTextColor; } console.log("Data (including comments, links, text color) stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedObject.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const dimensionVal = parseFloat(clickedObject.dataset.dimension || 0); const objectTypeVal = clickedObject.dataset.objectType || DEFAULT_OBJECT_TYPE; const baseOffset = (objectTypeVal === 'sphere') ? dimensionVal : dimensionVal / 2; const offsetDistance = baseOffset + PANEL_OFFSET_FROM_OBJECT; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); } catch(e) { console.error("Error during position calculation:", e); return; } infoPanelEl.setAttribute('visible', true); console.log("Panel visibility set to true. --- handleObjectClick end ---"); }
// --- パネルのボタンクリック処理 (変更なし) ---
prevButtonEl.addEventListener('click', function (event) { /* ... (前回と同じ) ... */ event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const currentCommentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); const commentSubPageCount = currentCommentInfo.text.length; if (EXE_MODE === 0 || (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1)) { commentSubPageIndex--; if (commentSubPageIndex < 0) { if (EXE_MODE === 0) { commentSubPageIndex = Math.max(0, commentSubPageCount - 1); } else { mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; if (PAGES[mainPageIndex] === 'comment') { const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; commentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } else { commentSubPageIndex = 0; } } } } else if (EXE_MODE === 1) { mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; commentSubPageIndex = 0; if (PAGES[mainPageIndex] === 'comment') { const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; commentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString(); updatePanelDisplay(); console.log("Prev button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex); });
nextButtonEl.addEventListener('click', function (event) { /* ... (前回と同じ) ... */ event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const currentCommentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); const commentSubPageCount = currentCommentInfo.text.length; if (EXE_MODE === 0 || (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1)) { commentSubPageIndex++; if (commentSubPageIndex >= commentSubPageCount) { if (EXE_MODE === 0) { commentSubPageIndex = 0; } else { mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES; commentSubPageIndex = 0; } } } else if (EXE_MODE === 1) { mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES; commentSubPageIndex = 0; } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString(); updatePanelDisplay(); console.log("Next button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex); });
// --- 閉じるボタンのクリック処理 (変更なし) ---
closeButtonEl.addEventListener('click', function (event) { /* ... (前回と同じ) ... */ event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.objectIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.dimension; delete infoPanelEl.dataset.objectType; delete infoPanelEl.dataset.currentPageIndex; delete infoPanelEl.dataset.commentInfo; delete infoPanelEl.dataset.commentSubPageIndex; delete infoPanelEl.dataset.commentSubPageCount; delete infoPanelEl.dataset.activeLinks; delete infoPanelEl.dataset.commentPageTextColor; linkButtons.forEach(btn => btn.setAttribute('visible', false)); console.log("Close button clicked, panel hidden."); });
// --- リンクボタンのクリック処理 (変更なし) ---
function handleLinkButtonClick(event) { /* ... (前回と同じ) ... */ event.stopPropagation(); const urlToOpen = event.target.dataset.url; if (!urlToOpen) { console.log("Link button clicked, but no URL found on this button."); return; } console.log("Link button clicked, starting absorption animation for URL:", urlToOpen); const linkButtonWorldPos = new THREE.Vector3(); event.target.object3D.getWorldPosition(linkButtonWorldPos); const allClickableObjects = document.querySelectorAll('.clickableObject'); const animationDuration = 3000; allClickableObjects.forEach(objEl => { objEl.removeAttribute('animation__scale吸い込み'); objEl.removeAttribute('animation__position吸い込み'); objEl.setAttribute('animation__scale吸い込み', { property: 'scale', to: '0.01 0.01 0.01', dur: animationDuration, easing: 'easeInQuad' }); objEl.setAttribute('animation__position吸い込み', { property: 'position', to: `${linkButtonWorldPos.x} ${linkButtonWorldPos.y} ${linkButtonWorldPos.z}`, dur: animationDuration, easing: 'easeInQuad' }); }); setTimeout(() => { console.log("Animation finished, navigating to URL:", urlToOpen); window.location.href = urlToOpen; }, animationDuration); }
// --- VRモード制御とコントローラーイベントリスナー (変更なし) ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
sceneEl.addEventListener('loaded', function() { for (let i = 0; i < 3; i++) { const btn = document.getElementById(`linkButton${i}`); if (btn) { linkButtons.push(btn); btn.addEventListener('click', handleLinkButtonClick); } } rightHandEl = document.getElementById('rightHand'); if (rightHandEl) { rightHandEl.addEventListener('triggerdown', function (evt) { console.log('Right hand TRIGGER DOWN event!', evt); const raycasterComponent = rightHandEl.components.raycaster; if (raycasterComponent) { const intersectedEls = raycasterComponent.intersectedEls; if (intersectedEls.length > 0) { console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id'); } else { console.log('Right Trigger pressed, but no intersection.'); } } }); rightHandEl.addEventListener('triggerup', function (evt) { console.log('Right hand TRIGGER UP event!', evt); }); } else { console.error("Could not find rightHand element to attach trigger listener."); } });
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test59
├
<!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 }, rotationSpeed: { type: 'number', default: 1.5 }, pitchLimit: { type: 'number', default: 85 } }, init: function () { this.keys = {}; this.leftThumbstickInput = { x: 0, y: 0 }; this.rightThumbstickInput = { 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.rigEl = this.el; 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.onLeftThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } this.rightHand = document.getElementById('rightHand'); if (this.rightHand) { this.rightHand.addEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 右手コントローラー(#rightHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); this.currentPitch = 0; }, remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onLeftThumbstickMoved.bind(this)); } catch(e){} } if (this.rightHand) { try { this.rightHand.removeEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); } catch(e){} } }, onLeftThumbstickMoved: function (evt) { this.leftThumbstickInput.x = evt.detail.x; this.leftThumbstickInput.y = evt.detail.y; }, onRightThumbstickMoved: function (evt) { this.rightThumbstickInput.x = evt.detail.x; this.rightThumbstickInput.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 || !this.rigEl || !this.rigEl.object3D) { return; } const data = this.data; const dt = timeDelta / 1000; if (this.rigEl.sceneEl.is('vr-mode')) { if (Math.abs(this.rightThumbstickInput.x) > 0.1) { const yawAngle = -this.rightThumbstickInput.x * data.rotationSpeed * dt; this.rigEl.object3D.rotation.y += yawAngle; } if (Math.abs(this.rightThumbstickInput.y) > 0.1) { let currentCameraPitchRad = this.cameraEl.object3D.rotation.x; let pitchAngleDelta = -this.rightThumbstickInput.y * data.rotationSpeed * dt; let newPitchRad = currentCameraPitchRad + pitchAngleDelta; const pitchLimitRad = THREE.MathUtils.degToRad(data.pitchLimit); newPitchRad = THREE.MathUtils.clamp(newPitchRad, -pitchLimitRad, pitchLimitRad); this.cameraEl.object3D.rotation.x = newPitchRad; } } const position = this.rigEl.object3D.position; 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.leftThumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.leftThumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.leftThumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.leftThumbstickInput.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" vr-mode-ui="enabled: true">
<a-sky id="backgroundSkyElement" src="./pic/u5.jpg"></a-sky>
<a-assets>
<img id="tex_a1" src="./pic/a1.jpg" crossOrigin="anonymous">
<img id="tex_a2" src="./pic/a2.jpg" crossOrigin="anonymous">
<img id="tex_a3" src="./pic/a3.jpg" crossOrigin="anonymous">
<img id="tex_a4" src="./pic/a4.jpg" crossOrigin="anonymous">
<img id="tex_a5" src="./pic/a5.jpg" crossOrigin="anonymous">
<img id="tex_a6" src="./pic/a6.jpg" crossOrigin="anonymous">
<img id="tex_a7" src="./pic/a7.jpg" crossOrigin="anonymous">
<img id="tex_a8" src="./pic/a8.jpg" crossOrigin="anonymous">
<img id="tex_a9" src="./pic/a9.jpg" crossOrigin="anonymous">
<img id="tex_a10" src="./pic/a10.jpg" crossOrigin="anonymous">
</a-assets>
<a-entity id="rig" position="0 0 5" camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1; rotationSpeed: 1.5; pitchLimit: 85">
<a-entity id="camera" camera="far: 20000;" look-controls="pointerLockEnabled: false; touchEnabled: false" position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;" raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" laser-controls="hand: right; model: false; lineColor: white; lineOpacity: 0.75" ></a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-cone id="prevButton" class="clickableButton" position="-20.0 0 0.1" rotation="0 0 90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-cone id="nextButton" class="clickableButton" position="20.0 0 0.1" rotation="0 0 -90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-sphere id="closeButton" class="clickableButton" position="24 11 0.1" radius="1.5" color="red" shader="flat"> </a-sphere>
<a-entity id="linkButtonsContainer" position="-22 -5 0.1">
<a-sphere id="linkButton0" class="clickableButton linkButton" visible="false" radius="1.5" position="0 0 0" shader="flat"></a-sphere>
<a-sphere id="linkButton1" class="clickableButton linkButton" visible="false" radius="1.5" position="0 -4 0" shader="flat"></a-sphere>
<a-sphere id="linkButton2" class="clickableButton linkButton" visible="false" radius="1.5" position="0 -8 0" shader="flat"></a-sphere>
</a-entity>
</a-entity>
<script>
// --- SCRIPT BLOCK 2: Main Scene Logic ---
const sceneEl = document.getElementById('myScene');
const skyElement = document.getElementById('backgroundSkyElement');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
const linkButtons = [];
let rightHandEl = null;
// ★★★ 表示モード設定用変数 (0: コメントのみモード, 1: 通常モード) ★★★
const EXE_MODE = 0; // 初期値を0に設定
const SKY_BACKGROUND_IMAGE_PATH = "./pic/u5.jpg";
if (SKY_BACKGROUND_IMAGE_PATH && SKY_BACKGROUND_IMAGE_PATH !== "") { skyElement.setAttribute('src', SKY_BACKGROUND_IMAGE_PATH); skyElement.setAttribute('visible', 'true');}
else { skyElement.setAttribute('visible', false); skyElement.removeAttribute('src'); sceneEl.setAttribute('background', 'color', '#000000');}
const PANEL_OFFSET_FROM_OBJECT = 5.0;
const BASE_TEXT_STYLE = { maxWidth: 46, align: 'center', anchorX: 'center', anchorY: 'middle', baseline: 'middle' };
const PAGE_TEXT_STYLES = { 'index': { ...BASE_TEXT_STYLE, fontSize: 0.72, color: 'white' }, 'color': { ...BASE_TEXT_STYLE, fontSize: 0.65, color: '#A0F0A0' }, 'size': { ...BASE_TEXT_STYLE, fontSize: 0.80, color: '#FFEC8B' }, 'comment': { ...BASE_TEXT_STYLE, fontSize: 0.70, color: '#ADD8E6' } }; // コメントページのデフォルトスタイル
const OBJECT_DEFINITIONS = { 1: { type: 'sphere', useTextureForIndex: 1, sizeType: 'fixed', fixedSize: 3.0 }, 2: { type: 'box', specificColor: 'blue', sizeType: 'random' }, 3: { type: 'sphere' } };
const DEFAULT_OBJECT_TYPE = 'box'; const DEFAULT_SIZE_TYPE = 'random'; const TEXTURE_ID_PREFIX = 'tex_a'; const MAX_TEXTURE_INDEX = 10;
const CUBE_COMMENTS = { 1: { text: ["最初のオブジェクト!"], mainCommentTextColor: "#FFDA63" }, 5: { text: ["特別なオブジェクトその1。"], links: [{label:"A-Frame Site", url:"https://aframe.io", buttonColor:"orange"}] }, 10: { text: ["このオブジェクトには情報源が複数あります:", "緑ボタン: A-Frame公式ドキュメント", "青ボタン: Google検索", "黄色ボタン: A-Frame GitHub"], mainCommentTextColor: "#B0E0E6", links: [ { label: "A-Frame Docs", url: "https://aframe.io/docs/", buttonColor: "#2ECC71" }, { label: "Google", url: "https://google.com", buttonColor: "#3498DB" }, { label: "GitHub", url: "https://github.com/aframevr", buttonColor: "#F1C40F" } ] }, };
const DEFAULT_COMMENT_ARRAY_INFO = { text: ["コメントはありません"], links: [] };
const numObjects = 10; const spread = 300;
const PAGES = ['index', 'color', 'size', 'comment']; // EXE_MODE = 1 の場合のメインページ
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();
const LAYOUT_PATTERN = 16; console.log(`Using LAYOUT_PATTERN: ${LAYOUT_PATTERN}`);
const RIG_INITIAL_X = 0; const RIG_INITIAL_Y_CAMERA_LEVEL = 1.6; const RIG_INITIAL_Z = 5; const SPAWN_ANIM_START_POS_STRING = `${RIG_INITIAL_X} ${RIG_INITIAL_Y_CAMERA_LEVEL} ${RIG_INITIAL_Z}`;
const H_LINE_SPACING = 15; const H_LINE_Y = RIG_INITIAL_Y_CAMERA_LEVEL; const H_LINE_Z_OFFSET = -35;
const H_RING_RADIUS = 50; const H_RING_OBJECT_Y = RIG_INITIAL_Y_CAMERA_LEVEL; const HORIZONTAL_RING_LOOK_AT_TARGET = new THREE.Vector3(RIG_INITIAL_X, H_RING_OBJECT_Y, RIG_INITIAL_Z);
const GRID_COLUMNS = 4; const GRID_SPACING_X = 15; const GRID_SPACING_Y = 15; const GRID_INITIAL_Y = RIG_INITIAL_Y_CAMERA_LEVEL + 10; const GRID_Z_DEPTH = H_LINE_Z_OFFSET;
const V_LINE_SPACING = 15; const V_LINE_X_OFFSET = 0; const V_LINE_START_Y_OFFSET_FROM_CENTER = ( (numObjects-1) * V_LINE_SPACING / 2 ); const V_LINE_Z_OFFSET = -20;
const V_RING_RADIUS = 50; const V_RING_CENTER_Y = RIG_INITIAL_Y_CAMERA_LEVEL; const V_RING_FIXED_Z_OFFSET = -25; const VERTICAL_RING_LOOK_AT_TARGET = new THREE.Vector3(RIG_INITIAL_X, V_RING_CENTER_Y, RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET);
const H_RING_ANGLE_STEP_DEGREES = 0; const V_RING_ANGLE_STEP_DEGREES = 0;
// --- オブジェクト生成ループ (変更なし) ---
for (let i = 0; i < numObjects; i++) { /* ... (前回と同じ) ... */ const objectIndex = i + 1; const definition = OBJECT_DEFINITIONS[objectIndex] || {}; const objectType = definition.type || DEFAULT_OBJECT_TYPE; let objectEl; if (objectType === 'sphere') { objectEl = document.createElement('a-sphere'); } else { objectEl = document.createElement('a-box'); } let dimension; const sizeType = definition.sizeType || DEFAULT_SIZE_TYPE; if (sizeType === 'fixed' && typeof definition.fixedSize === 'number' && definition.fixedSize > 0) { dimension = definition.fixedSize; } else { if (sizeType === 'fixed') { console.warn(`Object ${objectIndex}: sizeType 'fixed' but fixedSize is invalid (${definition.fixedSize}). Defaulting to random size.`); } dimension = Math.random() * 10.0 + 0.5; } let finalX, finalY, finalZ; let applyRandomRotation = false; let initialRotation = null; switch (LAYOUT_PATTERN) { case 1: finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotation = true; break; case 2: finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotation = false; break; case 3: const totalLineWidthH_3 = (numObjects - 1) * H_LINE_SPACING; finalX = (-totalLineWidthH_3 / 2) + (i * H_LINE_SPACING) + RIG_INITIAL_X; finalY = H_LINE_Y; finalZ = RIG_INITIAL_Z + H_LINE_Z_OFFSET; applyRandomRotation = true; break; case 4: const totalLineWidthV_4 = (numObjects - 1) * H_LINE_SPACING; finalX = (-totalLineWidthV_4 / 2) + (i * H_LINE_SPACING) + RIG_INITIAL_X; finalY = H_LINE_Y; finalZ = RIG_INITIAL_Z + H_LINE_Z_OFFSET; applyRandomRotation = false; break; case 5: case 6: let angleStep360H; if (H_RING_ANGLE_STEP_DEGREES > 0) { angleStep360H = THREE.MathUtils.degToRad(H_RING_ANGLE_STEP_DEGREES); } else { angleStep360H = (Math.PI * 2) / numObjects; } const angle360H = i * angleStep360H; finalX = RIG_INITIAL_X + H_RING_RADIUS * Math.cos(angle360H); finalY = H_RING_OBJECT_Y; finalZ = RIG_INITIAL_Z + H_RING_RADIUS * Math.sin(angle360H); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(HORIZONTAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 5); break; case 7: case 8: const totalAngleSpan180H = Math.PI; const startAngleOffset180H = -Math.PI / 2; let angleStep180H_; if (H_RING_ANGLE_STEP_DEGREES > 0) { angleStep180H_ = THREE.MathUtils.degToRad(H_RING_ANGLE_STEP_DEGREES); } else { angleStep180H_ = numObjects > 1 ? totalAngleSpan180H / (numObjects - 1) : 0; } const thetaH = startAngleOffset180H + (i * angleStep180H_); finalX = RIG_INITIAL_X + H_RING_RADIUS * Math.sin(thetaH); finalY = H_RING_OBJECT_Y; finalZ = RIG_INITIAL_Z - H_RING_RADIUS * Math.cos(thetaH); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(HORIZONTAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 7); break; case 9: case 10: const column = i % GRID_COLUMNS; const row = Math.floor(i / GRID_COLUMNS); const totalGridWidth = (GRID_COLUMNS - 1) * GRID_SPACING_X; const startX = -totalGridWidth / 2; finalX = startX + column * GRID_SPACING_X + RIG_INITIAL_X; finalY = GRID_INITIAL_Y - row * GRID_SPACING_Y; finalZ = RIG_INITIAL_Z + GRID_Z_DEPTH; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (LAYOUT_PATTERN === 9); break; case 11: case 12: finalX = V_LINE_X_OFFSET + RIG_INITIAL_X; finalY = (RIG_INITIAL_Y_CAMERA_LEVEL + V_LINE_START_Y_OFFSET_FROM_CENTER) - (i * V_LINE_SPACING); finalZ = RIG_INITIAL_Z + V_LINE_Z_OFFSET; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (LAYOUT_PATTERN === 11); break; case 13: case 14: let angleStep360V; if (V_RING_ANGLE_STEP_DEGREES > 0) { angleStep360V = THREE.MathUtils.degToRad(V_RING_ANGLE_STEP_DEGREES); } else { angleStep360V = (Math.PI * 2) / numObjects; } const angle360V = i * angleStep360V; finalX = RIG_INITIAL_X; finalY = V_RING_CENTER_Y + V_RING_RADIUS * Math.cos(angle360V); finalZ = (RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET) + V_RING_RADIUS * Math.sin(angle360V); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(VERTICAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 13); break; case 15: case 16: const arcSpan180V = Math.PI; const startAngle180V = -Math.PI / 2; let angleStep180V_; if (V_RING_ANGLE_STEP_DEGREES > 0) { angleStep180V_ = THREE.MathUtils.degToRad(V_RING_ANGLE_STEP_DEGREES); } else { angleStep180V_ = numObjects > 1 ? arcSpan180V / (numObjects - 1) : 0; } const thetaV = startAngle180V + (i * angleStep180V_); finalX = RIG_INITIAL_X; finalY = V_RING_CENTER_Y + V_RING_RADIUS * Math.sin(thetaV); finalZ = (RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET) - V_RING_RADIUS * Math.cos(thetaV); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(VERTICAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 15); break; default: console.warn(`Unknown LAYOUT_PATTERN: ${LAYOUT_PATTERN}, defaulting to pattern 1.`); finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotation = true; }
if (objectType === 'sphere') { objectEl.setAttribute('radius', dimension); } else { objectEl.setAttribute('width', dimension); objectEl.setAttribute('height', dimension); objectEl.setAttribute('depth', dimension); } let appliedColor; if (definition.useTextureForIndex && definition.useTextureForIndex >= 1 && definition.useTextureForIndex <= MAX_TEXTURE_INDEX) { const textureId = `${TEXTURE_ID_PREFIX}${definition.useTextureForIndex}`; objectEl.setAttribute('material', 'src', `#${textureId}`); appliedColor = '#FFFFFF'; objectEl.setAttribute('color', appliedColor); } else if (definition.specificColor) { appliedColor = definition.specificColor; objectEl.setAttribute('color', appliedColor); if(objectEl.hasAttribute('material')) objectEl.removeAttribute('material','src'); } else { appliedColor = `hsl(${Math.random() * 360}, 50%, 75%)`; objectEl.setAttribute('color', appliedColor); if(objectEl.hasAttribute('material')) objectEl.removeAttribute('material','src'); } objectEl.setAttribute('scale', '0.01 0.01 0.01'); objectEl.setAttribute('position', SPAWN_ANIM_START_POS_STRING); if (initialRotation) { objectEl.setAttribute('rotation', initialRotation); } else if (!applyRandomRotation) { objectEl.setAttribute('rotation', '0 0 0');} objectEl.classList.add('clickableObject'); objectEl.dataset.objectIndex = objectIndex; objectEl.dataset.color = appliedColor; objectEl.dataset.dimension = dimension.toFixed(2); objectEl.dataset.objectType = objectType; if (applyRandomRotation) { objectEl.setAttribute('random-rotate', {maxSpeed: 5}); } objectEl.addEventListener('click', handleObjectClick); sceneEl.appendChild(objectEl); objectEl.setAttribute('animation__spawnscale', { property: 'scale', to: '1 1 1', dur: 3000, easing: 'easeOutQuad' }); objectEl.setAttribute('animation__spawnposition', { property: 'position', to: `${finalX} ${finalY} ${finalZ}`, dur: 3000, easing: 'easeOutQuad'}); }
// --- ★★★ 情報パネルの表示更新関数 (EXE_MODE 対応) ★★★ ---
function updatePanelDisplay() {
if (!infoPanelEl.dataset.objectIndex) return;
const index = parseInt(infoPanelEl.dataset.objectIndex || '0', 10);
const sphereColor = infoPanelEl.dataset.color || 'N/A';
const dimensionValue = infoPanelEl.dataset.dimension || 'N/A';
const objectType = infoPanelEl.dataset.objectType || DEFAULT_OBJECT_TYPE;
const commentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO));
const commentsArray = commentInfo.text;
const mainCommentTextColor = commentInfo.mainCommentTextColor;
const activeLinks = JSON.parse(infoPanelEl.dataset.activeLinks || "[]");
const commentSubPageCount = commentsArray.length;
infoPanelEl.dataset.commentSubPageCount = commentSubPageCount.toString(); // Ensure it's a string for dataset
const commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10);
const mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[mainPageIndex]; // 'index', 'color', 'size', 'comment'
let pageIndicator = ''; // EXE_MODE = 0 ではメインページインジケータは基本的に非表示
// EXE_MODE = 0 の場合は、強制的にコメントページのみの表示ロジックへ
if (EXE_MODE === 0) {
displayText = commentsArray[commentSubPageIndex] || "コメントはありません";
if (commentSubPageCount > 1) { // コメントが複数サブページある場合のみサブページ番号表示
pageIndicator = `(${commentSubPageIndex + 1}/${commentSubPageCount})`;
} else if (commentsArray[0] === "コメントはありません" && commentSubPageCount === 1) {
// 「コメントはありません」の場合でも、(1/1) を表示
pageIndicator = `(1/1)`;
}
// コメント専用色が設定されていればそれを使う。なければデフォルトのコメントスタイル。
currentStyle = { ...(PAGE_TEXT_STYLES['comment']) }; // まずコメントの基本スタイル
if (mainCommentTextColor) {
currentStyle.color = mainCommentTextColor;
}
} else { // EXE_MODE === 1 (通常モード)
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`;
currentStyle = { ...(PAGE_TEXT_STYLES[pageType] || PAGE_TEXT_STYLES['index']) };
if (pageType === 'index') { displayText = `${objectType === 'sphere' ? '球' : '立方体'}: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${sphereColor}`; }
else if (pageType === 'size') { displayText = `${objectType === 'sphere' ? '半径' : 'サイズ'}: ${dimensionValue}`; }
else if (pageType === 'comment') {
displayText = `コメント:\n${commentsArray[commentSubPageIndex] || "(このサブページにコメントはありません)"}`;
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`;
if (mainCommentTextColor) { currentStyle.color = mainCommentTextColor; }
}
}
// リンクボタンの表示制御 (これはEXE_MODEに関わらずコメントページなら評価)
linkButtons.forEach(btn => btn.setAttribute('visible', false));
if ( (EXE_MODE === 0 || pageType === 'comment') && activeLinks && activeLinks.length > 0) {
activeLinks.forEach((link, i) => {
if (linkButtons[i]) {
linkButtons[i].setAttribute('visible', true);
linkButtons[i].setAttribute('material', 'color', link.buttonColor || 'lime');
linkButtons[i].dataset.url = link.url;
}
});
}
const finalDisplayText = EXE_MODE === 0 ? `${displayText}\n${pageIndicator}`.trim() : `${pageIndicator}\n${displayText}`;
panelTextEl.setAttribute('troika-text', {
value: finalDisplayText, color: currentStyle.color, fontSize: currentStyle.fontSize,
maxWidth: currentStyle.maxWidth, align: currentStyle.align, anchorX: currentStyle.anchorX,
anchorY: currentStyle.anchorY, baseline: currentStyle.baseline
});
}
// --- オブジェクトクリック時の処理 (EXE_MODE 対応) ---
function handleObjectClick(event) {
event.stopPropagation();
console.log("--- handleObjectClick triggered --- Target:", event.target.id || event.target.tagName);
const clickedObject = event.target;
if (!clickedObject.dataset.objectIndex || !clickedObject.dataset.color || !clickedObject.dataset.dimension || !clickedObject.dataset.objectType) {
console.error("Object data missing from dataset!", clickedObject.dataset); return;
}
console.log("Object data found:", clickedObject.dataset);
infoPanelEl.dataset.objectIndex = clickedObject.dataset.objectIndex;
infoPanelEl.dataset.color = clickedObject.dataset.color;
infoPanelEl.dataset.dimension = clickedObject.dataset.dimension;
infoPanelEl.dataset.objectType = clickedObject.dataset.objectType;
// ★★★ EXE_MODE で初期ページを分岐 ★★★
if (EXE_MODE === 0) {
infoPanelEl.dataset.currentPageIndex = PAGES.indexOf('comment').toString(); // 強制的にコメントページへ
} else {
infoPanelEl.dataset.currentPageIndex = '0'; // 通常は最初のページ (index)
}
const objectIdx = parseInt(clickedObject.dataset.objectIndex, 10);
const commentDataForThisObject = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO;
infoPanelEl.dataset.commentInfo = JSON.stringify(commentDataForThisObject);
infoPanelEl.dataset.commentSubPageCount = commentDataForThisObject.text.length.toString();
infoPanelEl.dataset.commentSubPageIndex = '0';
if (commentDataForThisObject.links && commentDataForThisObject.links.length > 0) {
infoPanelEl.dataset.activeLinks = JSON.stringify(commentDataForThisObject.links);
} else {
infoPanelEl.dataset.activeLinks = JSON.stringify([]);
}
if (commentDataForThisObject.mainCommentTextColor) {
infoPanelEl.dataset.commentPageTextColor = commentDataForThisObject.mainCommentTextColor;
} else {
delete infoPanelEl.dataset.commentPageTextColor;
}
console.log("Data (including comments, links, text color) stored in panel dataset.");
try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); }
catch (e) { console.error("Error during updatePanelDisplay:", e); return; }
try { /* ... 位置計算 ... */ clickedObject.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const dimensionVal = parseFloat(clickedObject.dataset.dimension || 0); const objectTypeVal = clickedObject.dataset.objectType || DEFAULT_OBJECT_TYPE; const baseOffset = (objectTypeVal === 'sphere') ? dimensionVal : dimensionVal / 2; const offsetDistance = baseOffset + PANEL_OFFSET_FROM_OBJECT; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); }
catch(e) { console.error("Error during position calculation:", e); return; }
infoPanelEl.setAttribute('visible', true);
console.log("Panel visibility set to true. --- handleObjectClick end ---");
}
// --- ★★★ パネルのボタンクリック処理 (EXE_MODE 対応) ★★★ ---
prevButtonEl.addEventListener('click', function (event) {
event.stopPropagation();
if (!infoPanelEl.getAttribute('visible')) return;
let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10);
const currentCommentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO));
const commentSubPageCount = currentCommentInfo.text.length;
if (EXE_MODE === 0 || (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1)) {
// コメントのみモード、または通常モードでコメントページかつサブページがある場合
commentSubPageIndex--;
if (commentSubPageIndex < 0) {
if (EXE_MODE === 0) { // コメントのみモードならサブページ循環
commentSubPageIndex = Math.max(0, commentSubPageCount - 1);
} else { // 通常モードならメインページを前に
mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES;
if (PAGES[mainPageIndex] === 'comment') {
const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10);
const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO;
commentSubPageIndex = Math.max(0, newCommentData.text.length - 1);
} else { commentSubPageIndex = 0; }
}
}
} else if (EXE_MODE === 1) { // 通常モードでコメントページ以外、またはコメントが1ページのみ
mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES;
commentSubPageIndex = 0;
if (PAGES[mainPageIndex] === 'comment') { // 新しいメインページがコメントならサブページ設定
const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10);
const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO;
// コメントの最後のサブページから表示 (通常は不要だが、一応)
commentSubPageIndex = Math.max(0, newCommentData.text.length - 1);
}
}
infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString();
infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString();
updatePanelDisplay();
console.log("Prev button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex);
});
nextButtonEl.addEventListener('click', function (event) {
event.stopPropagation();
if (!infoPanelEl.getAttribute('visible')) return;
let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10);
const currentCommentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO));
const commentSubPageCount = currentCommentInfo.text.length;
if (EXE_MODE === 0 || (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1)) {
// コメントのみモード、または通常モードでコメントページかつサブページがある場合
commentSubPageIndex++;
if (commentSubPageIndex >= commentSubPageCount) {
if (EXE_MODE === 0) { // コメントのみモードならサブページ循環
commentSubPageIndex = 0;
} else { // 通常モードならメインページを次に
mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES;
commentSubPageIndex = 0; // 新しいメインページではサブページは0から
}
}
} else if (EXE_MODE === 1) { // 通常モードでコメントページ以外、またはコメントが1ページのみ
mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES;
commentSubPageIndex = 0; // 新しいメインページではサブページは0から
}
infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString();
infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString();
updatePanelDisplay();
console.log("Next button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex);
});
// --- 閉じるボタンのクリック処理 (変更なし) ---
closeButtonEl.addEventListener('click', function (event) { /* ... (前回と同じ) ... */ event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.objectIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.dimension; delete infoPanelEl.dataset.objectType; delete infoPanelEl.dataset.currentPageIndex; delete infoPanelEl.dataset.commentInfo; delete infoPanelEl.dataset.commentSubPageIndex; delete infoPanelEl.dataset.commentSubPageCount; delete infoPanelEl.dataset.activeLinks; delete infoPanelEl.dataset.commentPageTextColor; linkButtons.forEach(btn => btn.setAttribute('visible', false)); console.log("Close button clicked, panel hidden."); });
// --- リンクボタンのクリック処理 (変更なし) ---
function handleLinkButtonClick(event) { /* ... (前回と同じ) ... */ event.stopPropagation(); const urlToOpen = event.target.dataset.url; if (!urlToOpen) { console.log("Link button clicked, but no URL found on this button."); return; } console.log("Link button clicked, starting absorption animation for URL:", urlToOpen); const linkButtonWorldPos = new THREE.Vector3(); event.target.object3D.getWorldPosition(linkButtonWorldPos); const allClickableObjects = document.querySelectorAll('.clickableObject'); const animationDuration = 3000; allClickableObjects.forEach(objEl => { objEl.removeAttribute('animation__scale吸い込み'); objEl.removeAttribute('animation__position吸い込み'); objEl.setAttribute('animation__scale吸い込み', { property: 'scale', to: '0.01 0.01 0.01', dur: animationDuration, easing: 'easeInQuad' }); objEl.setAttribute('animation__position吸い込み', { property: 'position', to: `${linkButtonWorldPos.x} ${linkButtonWorldPos.y} ${linkButtonWorldPos.z}`, dur: animationDuration, easing: 'easeInQuad' }); }); setTimeout(() => { console.log("Animation finished, navigating to URL:", urlToOpen); window.location.href = urlToOpen; }, animationDuration); }
// --- VRモード制御とコントローラーイベントリスナー (変更なし) ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
sceneEl.addEventListener('loaded', function() { for (let i = 0; i < 3; i++) { const btn = document.getElementById(`linkButton${i}`); if (btn) { linkButtons.push(btn); btn.addEventListener('click', handleLinkButtonClick); } } rightHandEl = document.getElementById('rightHand'); if (rightHandEl) { rightHandEl.addEventListener('triggerdown', function (evt) { console.log('Right hand TRIGGER DOWN event!', evt); const raycasterComponent = rightHandEl.components.raycaster; if (raycasterComponent) { const intersectedEls = raycasterComponent.intersectedEls; if (intersectedEls.length > 0) { console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id'); } else { console.log('Right Trigger pressed, but no intersection.'); } } }); rightHandEl.addEventListener('triggerup', function (evt) { console.log('Right hand TRIGGER UP event!', evt); }); } else { console.error("Could not find rightHand element to attach trigger listener."); } });
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test58
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - LookAtTargetエラー修正</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>
// --- SCRIPT BLOCK 1: Component Definitions ---
// (カメラコントロールコンポーネントとランダム回転コンポーネントの定義は変更なし - 省略)
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 }, rotationSpeed: { type: 'number', default: 1.5 }, pitchLimit: { type: 'number', default: 85 } }, init: function () { this.keys = {}; this.leftThumbstickInput = { x: 0, y: 0 }; this.rightThumbstickInput = { 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.rigEl = this.el; 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.onLeftThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } this.rightHand = document.getElementById('rightHand'); if (this.rightHand) { this.rightHand.addEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 右手コントローラー(#rightHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); this.currentPitch = 0; }, remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onLeftThumbstickMoved.bind(this)); } catch(e){} } if (this.rightHand) { try { this.rightHand.removeEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); } catch(e){} } }, onLeftThumbstickMoved: function (evt) { this.leftThumbstickInput.x = evt.detail.x; this.leftThumbstickInput.y = evt.detail.y; }, onRightThumbstickMoved: function (evt) { this.rightThumbstickInput.x = evt.detail.x; this.rightThumbstickInput.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 || !this.rigEl || !this.rigEl.object3D) { return; } const data = this.data; const dt = timeDelta / 1000; if (this.rigEl.sceneEl.is('vr-mode')) { if (Math.abs(this.rightThumbstickInput.x) > 0.1) { const yawAngle = -this.rightThumbstickInput.x * data.rotationSpeed * dt; this.rigEl.object3D.rotation.y += yawAngle; } if (Math.abs(this.rightThumbstickInput.y) > 0.1) { let currentCameraPitchRad = this.cameraEl.object3D.rotation.x; let pitchAngleDelta = -this.rightThumbstickInput.y * data.rotationSpeed * dt; let newPitchRad = currentCameraPitchRad + pitchAngleDelta; const pitchLimitRad = THREE.MathUtils.degToRad(data.pitchLimit); newPitchRad = THREE.MathUtils.clamp(newPitchRad, -pitchLimitRad, pitchLimitRad); this.cameraEl.object3D.rotation.x = newPitchRad; } } const position = this.rigEl.object3D.position; 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.leftThumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.leftThumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.leftThumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.leftThumbstickInput.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]; } }
});
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); } });
// --- End of SCRIPT BLOCK 1 ---
</script>
</head>
<body>
<a-scene id="myScene" vr-mode-ui="enabled: true">
<a-sky id="backgroundSkyElement" src="./pic/u5.jpg"></a-sky>
<a-assets>
<img id="tex_a1" src="./pic/a1.jpg" crossOrigin="anonymous">
<img id="tex_a2" src="./pic/a2.jpg" crossOrigin="anonymous">
<img id="tex_a3" src="./pic/a3.jpg" crossOrigin="anonymous">
<img id="tex_a4" src="./pic/a4.jpg" crossOrigin="anonymous">
<img id="tex_a5" src="./pic/a5.jpg" crossOrigin="anonymous">
<img id="tex_a6" src="./pic/a6.jpg" crossOrigin="anonymous">
<img id="tex_a7" src="./pic/a7.jpg" crossOrigin="anonymous">
<img id="tex_a8" src="./pic/a8.jpg" crossOrigin="anonymous">
<img id="tex_a9" src="./pic/a9.jpg" crossOrigin="anonymous">
<img id="tex_a10" src="./pic/a10.jpg" crossOrigin="anonymous">
</a-assets>
<a-entity id="rig" position="0 0 5" camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1; rotationSpeed: 1.5; pitchLimit: 85">
<a-entity id="camera" camera="far: 20000;" look-controls="pointerLockEnabled: false; touchEnabled: false" position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;" raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" laser-controls="hand: right; model: false; lineColor: white; lineOpacity: 0.75" ></a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]"> <a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane> <a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity> <a-cone id="prevButton" class="clickableButton" position="-20.0 0 0.1" rotation="0 0 90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone> <a-cone id="nextButton" class="clickableButton" position="20.0 0 0.1" rotation="0 0 -90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone> <a-sphere id="closeButton" class="clickableButton" position="24 11 0.1" radius="1.5" color="red" shader="flat"> </a-sphere> <a-entity id="linkButtonsContainer" position="-22 -5 0.1"> <a-sphere id="linkButton0" class="clickableButton linkButton" visible="false" radius="1.5" position="0 0 0" shader="flat"></a-sphere> <a-sphere id="linkButton1" class="clickableButton linkButton" visible="false" radius="1.5" position="0 -4 0" shader="flat"></a-sphere> <a-sphere id="linkButton2" class="clickableButton linkButton" visible="false" radius="1.5" position="0 -8 0" shader="flat"></a-sphere> </a-entity> </a-entity>
<script>
// --- SCRIPT BLOCK 2: Main Scene Logic ---
const sceneEl = document.getElementById('myScene');
const skyElement = document.getElementById('backgroundSkyElement');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
const linkButtons = [];
let rightHandEl = null;
const SKY_BACKGROUND_IMAGE_PATH = "./pic/u5.jpg";
if (SKY_BACKGROUND_IMAGE_PATH && SKY_BACKGROUND_IMAGE_PATH !== "") { skyElement.setAttribute('src', SKY_BACKGROUND_IMAGE_PATH); skyElement.setAttribute('visible', 'true'); } else { skyElement.setAttribute('visible', false); skyElement.removeAttribute('src'); sceneEl.setAttribute('background', 'color', '#000000'); }
const PANEL_OFFSET_FROM_OBJECT = 5.0;
const BASE_TEXT_STYLE = { maxWidth: 46, align: 'center', anchorX: 'center', anchorY: 'middle', baseline: 'middle' };
const PAGE_TEXT_STYLES = { 'index': { ...BASE_TEXT_STYLE, fontSize: 0.72, color: 'white' }, 'color': { ...BASE_TEXT_STYLE, fontSize: 0.65, color: '#A0F0A0' }, 'size': { ...BASE_TEXT_STYLE, fontSize: 0.80, color: '#FFEC8B' }, 'comment': { ...BASE_TEXT_STYLE, fontSize: 0.70, color: '#ADD8E6' } };
const OBJECT_DEFINITIONS = { 1: { type: 'sphere', useTextureForIndex: 1, sizeType: 'fixed', fixedSize: 3.0 }, 2: { type: 'box', specificColor: 'blue', sizeType: 'random' }, 3: { type: 'sphere' } };
const DEFAULT_OBJECT_TYPE = 'box'; const DEFAULT_SIZE_TYPE = 'random'; const TEXTURE_ID_PREFIX = 'tex_a'; const MAX_TEXTURE_INDEX = 10;
const CUBE_COMMENTS = { 1: { text: ["最初のオブジェクト!"], mainCommentTextColor: "#FFDA63" }, 5: { text: ["特別なオブジェクトその1。"], links: [{label:"A-Frame Site", url:"https://aframe.io", buttonColor:"orange"}] }, 10: { text: ["このオブジェクトには情報源が複数あります:", "緑ボタン: A-Frame公式ドキュメント", "青ボタン: Google検索", "黄色ボタン: A-Frame GitHub"], mainCommentTextColor: "#B0E0E6", links: [ { label: "A-Frame Docs", url: "https://aframe.io/docs/", buttonColor: "#2ECC71" }, { label: "Google", url: "https://google.com", buttonColor: "#3498DB" }, { label: "GitHub", url: "https://github.com/aframevr", buttonColor: "#F1C40F" } ] }, };
const DEFAULT_COMMENT_ARRAY_INFO = { text: ["コメントはありません"], links: [] };
const numObjects = 10; const spread = 300; const PAGES = ['index', 'color', 'size', 'comment']; const TOTAL_MAIN_PAGES = PAGES.length;
const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3();
const LAYOUT_PATTERN = 10;
console.log(`Using LAYOUT_PATTERN: ${LAYOUT_PATTERN}`);
const RIG_INITIAL_X = 0; const RIG_INITIAL_Y_CAMERA_LEVEL = 1.6; const RIG_INITIAL_Z = 5;
const SPAWN_ANIM_START_POS_STRING = `${RIG_INITIAL_X} ${RIG_INITIAL_Y_CAMERA_LEVEL} ${RIG_INITIAL_Z}`;
const H_LINE_SPACING = 15; const H_LINE_Y = RIG_INITIAL_Y_CAMERA_LEVEL; const H_LINE_Z_OFFSET = -35;
const H_RING_RADIUS = 50; const H_RING_OBJECT_Y = RIG_INITIAL_Y_CAMERA_LEVEL;
const GRID_COLUMNS = 4; const GRID_SPACING_X = 15; const GRID_SPACING_Y = 15;
const GRID_INITIAL_Y = RIG_INITIAL_Y_CAMERA_LEVEL + 10; const GRID_Z_DEPTH = H_LINE_Z_OFFSET;
const V_LINE_SPACING = 15; const V_LINE_X_OFFSET = 0;
const V_LINE_START_Y_OFFSET_FROM_CENTER = ( (numObjects-1) * V_LINE_SPACING / 2 );
const V_LINE_Z_OFFSET = -20;
const V_RING_RADIUS = 40; const V_RING_CENTER_Y = RIG_INITIAL_Y_CAMERA_LEVEL;
const V_RING_FIXED_Z_OFFSET = -25;
// ★★★ LookAtターゲットの宣言を、依存する定数の後に移動 ★★★
const HORIZONTAL_RING_LOOK_AT_TARGET = new THREE.Vector3(RIG_INITIAL_X, H_RING_OBJECT_Y, RIG_INITIAL_Z);
const VERTICAL_RING_LOOK_AT_TARGET = new THREE.Vector3(RIG_INITIAL_X, V_RING_CENTER_Y, RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET);
const H_RING_ANGLE_STEP_DEGREES = 15; const V_RING_ANGLE_STEP_DEGREES = 15;
// let tempVec = new THREE.Vector3(); // ピッチ処理削除したので不要
for (let i = 0; i < numObjects; i++) {
const objectIndex = i + 1;
const definition = OBJECT_DEFINITIONS[objectIndex] || {};
const objectType = definition.type || DEFAULT_OBJECT_TYPE;
let objectEl;
if (objectType === 'sphere') { objectEl = document.createElement('a-sphere'); }
else { objectEl = document.createElement('a-box'); }
let dimension;
const sizeType = definition.sizeType || DEFAULT_SIZE_TYPE;
if (sizeType === 'fixed' && typeof definition.fixedSize === 'number' && definition.fixedSize > 0) {
dimension = definition.fixedSize;
} else {
if (sizeType === 'fixed') { console.warn(`Object ${objectIndex}: sizeType 'fixed' but fixedSize is invalid (${definition.fixedSize}). Defaulting to random size.`); }
dimension = Math.random() * 10.0 + 0.5;
}
let finalX, finalY, finalZ; let applyRandomRotation = false; let initialRotation = null;
switch (LAYOUT_PATTERN) {
case 1: finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotation = true; break;
case 2: finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotation = false; break;
case 3: const totalLineWidthH_3 = (numObjects - 1) * H_LINE_SPACING; finalX = (-totalLineWidthH_3 / 2) + (i * H_LINE_SPACING) + RIG_INITIAL_X; finalY = H_LINE_Y; finalZ = RIG_INITIAL_Z + H_LINE_Z_OFFSET; applyRandomRotation = true; break;
case 4: const totalLineWidthV_4 = (numObjects - 1) * H_LINE_SPACING; finalX = (-totalLineWidthV_4 / 2) + (i * H_LINE_SPACING) + RIG_INITIAL_X; finalY = H_LINE_Y; finalZ = RIG_INITIAL_Z + H_LINE_Z_OFFSET; applyRandomRotation = false; break;
case 5: case 6: let angleStep360H; if (H_RING_ANGLE_STEP_DEGREES > 0) { angleStep360H = THREE.MathUtils.degToRad(H_RING_ANGLE_STEP_DEGREES); } else { angleStep360H = (Math.PI * 2) / numObjects; } const angle360H = i * angleStep360H; finalX = RIG_INITIAL_X + H_RING_RADIUS * Math.cos(angle360H); finalY = H_RING_OBJECT_Y; finalZ = RIG_INITIAL_Z + H_RING_RADIUS * Math.sin(angle360H); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(HORIZONTAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 5); break;
case 7: case 8: const totalAngleSpan180H = Math.PI; const startAngleOffset180H = -Math.PI / 2; let angleStep180H_; if (H_RING_ANGLE_STEP_DEGREES > 0) { angleStep180H_ = THREE.MathUtils.degToRad(H_RING_ANGLE_STEP_DEGREES); } else { angleStep180H_ = numObjects > 1 ? totalAngleSpan180H / (numObjects - 1) : 0; } const thetaH = startAngleOffset180H + (i * angleStep180H_); finalX = RIG_INITIAL_X + H_RING_RADIUS * Math.sin(thetaH); finalY = H_RING_OBJECT_Y; finalZ = RIG_INITIAL_Z - H_RING_RADIUS * Math.cos(thetaH); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(HORIZONTAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 7); break;
case 9: case 10: const column = i % GRID_COLUMNS; const row = Math.floor(i / GRID_COLUMNS); const totalGridWidth = (GRID_COLUMNS - 1) * GRID_SPACING_X; const startX = -totalGridWidth / 2; finalX = startX + column * GRID_SPACING_X + RIG_INITIAL_X; finalY = GRID_INITIAL_Y - row * GRID_SPACING_Y; finalZ = RIG_INITIAL_Z + GRID_Z_DEPTH; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (LAYOUT_PATTERN === 9); break;
case 11: case 12: finalX = V_LINE_X_OFFSET + RIG_INITIAL_X; finalY = (RIG_INITIAL_Y_CAMERA_LEVEL + V_LINE_START_Y_OFFSET_FROM_CENTER) - (i * V_LINE_SPACING); finalZ = RIG_INITIAL_Z + V_LINE_Z_OFFSET; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (LAYOUT_PATTERN === 11); break;
case 13: case 14: let angleStep360V; if (V_RING_ANGLE_STEP_DEGREES > 0) { angleStep360V = THREE.MathUtils.degToRad(V_RING_ANGLE_STEP_DEGREES); } else { angleStep360V = (Math.PI * 2) / numObjects; } const angle360V = i * angleStep360V; finalX = RIG_INITIAL_X; finalY = V_RING_CENTER_Y + V_RING_RADIUS * Math.cos(angle360V); finalZ = (RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET) + V_RING_RADIUS * Math.sin(angle360V); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(VERTICAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 13); break;
case 15: case 16: const arcSpan180V = Math.PI; const startAngle180V = -Math.PI / 2; let angleStep180V_; if (V_RING_ANGLE_STEP_DEGREES > 0) { angleStep180V_ = THREE.MathUtils.degToRad(V_RING_ANGLE_STEP_DEGREES); } else { angleStep180V_ = numObjects > 1 ? arcSpan180V / (numObjects - 1) : 0; } const thetaV = startAngle180V + (i * angleStep180V_); finalX = RIG_INITIAL_X; finalY = V_RING_CENTER_Y + V_RING_RADIUS * Math.sin(thetaV); finalZ = (RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET) - V_RING_RADIUS * Math.cos(thetaV); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(VERTICAL_RING_LOOK_AT_TARGET); initialRotation = { x: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(objectEl.object3D.rotation.z) }; applyRandomRotation = (LAYOUT_PATTERN === 15); break;
default: console.warn(`Unknown LAYOUT_PATTERN: ${LAYOUT_PATTERN}, defaulting to pattern 1.`); finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotation = true;
}
if (objectType === 'sphere') { objectEl.setAttribute('radius', dimension); }
else { objectEl.setAttribute('width', dimension); objectEl.setAttribute('height', dimension); objectEl.setAttribute('depth', dimension); }
let appliedColor; if (definition.useTextureForIndex && definition.useTextureForIndex >= 1 && definition.useTextureForIndex <= MAX_TEXTURE_INDEX) { const textureId = `${TEXTURE_ID_PREFIX}${definition.useTextureForIndex}`; objectEl.setAttribute('material', 'src', `#${textureId}`); appliedColor = '#FFFFFF'; objectEl.setAttribute('color', appliedColor); } else if (definition.specificColor) { appliedColor = definition.specificColor; objectEl.setAttribute('color', appliedColor); if(objectEl.hasAttribute('material')) objectEl.removeAttribute('material','src'); } else { appliedColor = `hsl(${Math.random() * 360}, 50%, 75%)`; objectEl.setAttribute('color', appliedColor); if(objectEl.hasAttribute('material')) objectEl.removeAttribute('material','src'); }
objectEl.setAttribute('scale', '0.01 0.01 0.01');
objectEl.setAttribute('position', SPAWN_ANIM_START_POS_STRING);
if (initialRotation) { objectEl.setAttribute('rotation', initialRotation); }
else if (!applyRandomRotation) { objectEl.setAttribute('rotation', '0 0 0');}
objectEl.classList.add('clickableObject');
objectEl.dataset.objectIndex = objectIndex; objectEl.dataset.color = appliedColor; objectEl.dataset.dimension = dimension.toFixed(2); objectEl.dataset.objectType = objectType;
if (applyRandomRotation) { objectEl.setAttribute('random-rotate', {maxSpeed: 5}); }
objectEl.addEventListener('click', handleObjectClick);
sceneEl.appendChild(objectEl);
objectEl.setAttribute('animation__spawnscale', { property: 'scale', to: '1 1 1', dur: 3000, easing: 'easeOutQuad' });
objectEl.setAttribute('animation__spawnposition', { property: 'position', to: `${finalX} ${finalY} ${finalZ}`, dur: 3000, easing: 'easeOutQuad'});
}
// --- 情報パネルの表示更新関数 (変更なし) ---
function updatePanelDisplay() { if (!infoPanelEl.dataset.objectIndex) return; const index = parseInt(infoPanelEl.dataset.objectIndex || '0', 10); const sphereColor = infoPanelEl.dataset.color || 'N/A'; const dimensionValue = infoPanelEl.dataset.dimension || 'N/A'; const objectType = infoPanelEl.dataset.objectType || DEFAULT_OBJECT_TYPE; const commentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); const commentsArray = commentInfo.text; const mainCommentTextColor = commentInfo.mainCommentTextColor; const activeLinks = JSON.parse(infoPanelEl.dataset.activeLinks || "[]"); const commentSubPageCount = commentsArray.length; infoPanelEl.dataset.commentSubPageCount = commentSubPageCount.toString(); const commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let displayText = ''; const pageType = PAGES[mainPageIndex]; let pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`; let currentStyle = { ...(PAGE_TEXT_STYLES[pageType] || PAGE_TEXT_STYLES['index']) }; linkButtons.forEach(btn => btn.setAttribute('visible', false)); if (pageType === 'comment' && activeLinks && activeLinks.length > 0) { activeLinks.forEach((link, i) => { if (linkButtons[i]) { linkButtons[i].setAttribute('visible', true); linkButtons[i].setAttribute('material', 'color', link.buttonColor || 'lime'); linkButtons[i].dataset.url = link.url; } }); } if (pageType === 'index') { const typeLabel = objectType === 'sphere' ? '球' : '立方体'; displayText = `${typeLabel}: ${index}`; } else if (pageType === 'color') { displayText = `色: ${sphereColor}`; } else if (pageType === 'size') { const sizeLabel = objectType === 'sphere' ? '半径' : 'サイズ'; displayText = `${sizeLabel}: ${dimensionValue}`; } else if (pageType === 'comment') { displayText = `コメント:\n${commentsArray[commentSubPageIndex] || "(このサブページにコメントはありません)"}`; pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`; if (mainCommentTextColor) { currentStyle.color = mainCommentTextColor; } } const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', { value: finalDisplayText, color: currentStyle.color, fontSize: currentStyle.fontSize, maxWidth: currentStyle.maxWidth, align: currentStyle.align, anchorX: currentStyle.anchorX, anchorY: currentStyle.anchorY, baseline: currentStyle.baseline }); }
// --- オブジェクトクリック時の処理 (変更なし) ---
function handleObjectClick(event) { event.stopPropagation(); console.log("--- handleObjectClick triggered --- Target:", event.target.id || event.target.tagName); const clickedObject = event.target; if (!clickedObject.dataset.objectIndex || !clickedObject.dataset.color || !clickedObject.dataset.dimension || !clickedObject.dataset.objectType) { console.error("Object data missing from dataset!", clickedObject.dataset); return; } console.log("Object data found:", clickedObject.dataset); infoPanelEl.dataset.objectIndex = clickedObject.dataset.objectIndex; infoPanelEl.dataset.color = clickedObject.dataset.color; infoPanelEl.dataset.dimension = clickedObject.dataset.dimension; infoPanelEl.dataset.objectType = clickedObject.dataset.objectType; infoPanelEl.dataset.currentPageIndex = '0'; const objectIdx = parseInt(clickedObject.dataset.objectIndex, 10); const commentDataForThisObject = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(commentDataForThisObject); infoPanelEl.dataset.commentSubPageCount = commentDataForThisObject.text.length.toString(); infoPanelEl.dataset.commentSubPageIndex = '0'; if (commentDataForThisObject.links && commentDataForThisObject.links.length > 0) { infoPanelEl.dataset.activeLinks = JSON.stringify(commentDataForThisObject.links); } else { infoPanelEl.dataset.activeLinks = JSON.stringify([]); } if (commentDataForThisObject.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = commentDataForThisObject.mainCommentTextColor; } else { delete infoPanelEl.dataset.commentPageTextColor; } console.log("Data (including comments, links, text color) stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedObject.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const dimensionVal = parseFloat(clickedObject.dataset.dimension || 0); const objectTypeVal = clickedObject.dataset.objectType || DEFAULT_OBJECT_TYPE; const baseOffset = (objectTypeVal === 'sphere') ? dimensionVal : dimensionVal / 2; const offsetDistance = baseOffset + PANEL_OFFSET_FROM_OBJECT; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); } catch(e) { console.error("Error during position calculation:", e); return; } infoPanelEl.setAttribute('visible', true); console.log("Panel visibility set to true. --- handleObjectClick end ---"); }
// --- パネルのボタンクリック処理 (変更なし) ---
prevButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const currentCommentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); const commentSubPageCount = currentCommentInfo.text.length; if (PAGES[mainPageIndex] === 'comment' && commentSubPageIndex > 0) { commentSubPageIndex--; } else { mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; if (PAGES[mainPageIndex] === 'comment') { const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; commentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } else { commentSubPageIndex = 0; } } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString(); updatePanelDisplay(); console.log("Prev button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const currentCommentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); const commentSubPageCount = currentCommentInfo.text.length; if (PAGES[mainPageIndex] === 'comment' && commentSubPageIndex < commentSubPageCount - 1) { commentSubPageIndex++; } else { mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES; commentSubPageIndex = 0; } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString(); updatePanelDisplay(); console.log("Next button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex); });
// --- 閉じるボタンのクリック処理 (変更なし) ---
closeButtonEl.addEventListener('click', function (event) { event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.objectIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.dimension; delete infoPanelEl.dataset.objectType; delete infoPanelEl.dataset.currentPageIndex; delete infoPanelEl.dataset.commentInfo; delete infoPanelEl.dataset.commentSubPageIndex; delete infoPanelEl.dataset.commentSubPageCount; delete infoPanelEl.dataset.activeLinks; delete infoPanelEl.dataset.commentPageTextColor; linkButtons.forEach(btn => btn.setAttribute('visible', false)); console.log("Close button clicked, panel hidden."); });
// --- リンクボタンのクリック処理 (変更なし) ---
function handleLinkButtonClick(event) { event.stopPropagation(); const urlToOpen = event.target.dataset.url; if (!urlToOpen) { console.log("Link button clicked, but no URL found on this button."); return; } console.log("Link button clicked, starting absorption animation for URL:", urlToOpen); const linkButtonWorldPos = new THREE.Vector3(); event.target.object3D.getWorldPosition(linkButtonWorldPos); const allClickableObjects = document.querySelectorAll('.clickableObject'); const animationDuration = 3000; allClickableObjects.forEach(objEl => { objEl.removeAttribute('animation__scale吸い込み'); objEl.removeAttribute('animation__position吸い込み'); objEl.setAttribute('animation__scale吸い込み', { property: 'scale', to: '0.01 0.01 0.01', dur: animationDuration, easing: 'easeInQuad' }); objEl.setAttribute('animation__position吸い込み', { property: 'position', to: `${linkButtonWorldPos.x} ${linkButtonWorldPos.y} ${linkButtonWorldPos.z}`, dur: animationDuration, easing: 'easeInQuad' }); }); setTimeout(() => { console.log("Animation finished, navigating to URL:", urlToOpen); window.location.href = urlToOpen; }, animationDuration); }
// --- VRモード制御とコントローラーイベントリスナー (変更なし) ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
sceneEl.addEventListener('loaded', function() { for (let i = 0; i < 3; i++) { const btn = document.getElementById(`linkButton${i}`); if (btn) { linkButtons.push(btn); btn.addEventListener('click', handleLinkButtonClick); } } rightHandEl = document.getElementById('rightHand'); if (rightHandEl) { rightHandEl.addEventListener('triggerdown', function (evt) { console.log('Right hand TRIGGER DOWN event!', evt); const raycasterComponent = rightHandEl.components.raycaster; if (raycasterComponent) { const intersectedEls = raycasterComponent.intersectedEls; if (intersectedEls.length > 0) { console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id'); } else { console.log('Right Trigger pressed, but no intersection.'); } } }); rightHandEl.addEventListener('triggerup', function (evt) { console.log('Right hand TRIGGER UP event!', evt); }); } else { console.error("Could not find rightHand element to attach trigger listener."); } });
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test57
├
afarme-test
├
C#のtest
├
ファイルリストアップ
├
はい、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 まで。