Junkerposts
■
みせられない
完全に私的なメモ
├
はい、承知いたしました。パソコン上のどんなファイルでもアップロードできる、最も簡単なPerl CGIスクリプトの例を以下に示します。
この例では、HTMLフォームと、アップロード処理を行うPerlスクリプトの2つのファイルを作成します。
---
## 1. HTMLフォーム (uploader.html)
まず、ファイルをユーザーが選択し、サーバーに送信するためのHTMLフォームを作成します。このファイルを `uploader.html` などの名前で保存してください。
```html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ファイルアップローダー</title>
</head>
<body>
<h1>ファイルをアップロード</h1>
<form action="/cgi-bin/upload.cgi" method="post" enctype="multipart/form-data">
<p>
<input type="file" name="upload_file">
</p>
<p>
<input type="submit" value="アップロード">
</p>
</form>
</body>
</html>
```
**ポイント:**
* `form` タグの `method` 属性は `post` にします。
* `form` タグの `enctype` 属性は `multipart/form-data` にします。これはファイルアップロードに必須です。
* `input type="file"` でファイル選択ダイアログを表示します。`name` 属性(ここでは `upload_file`)はPerlスクリプト側でファイルを受け取る際に使用します。
* `action` 属性には、次に作成するPerl CGIスクリプトのパスを指定します(例: `/cgi-bin/upload.cgi`)。サーバーの設定に合わせて調整してください。
---
## 2. Perl CGIスクリプト (upload.cgi)
次に、アップロードされたファイルをサーバー上で受け取り、保存するPerlスクリプトを作成します。このファイルを `upload.cgi` などの名前で、サーバーのCGI実行可能ディレクトリ(例: `cgi-bin`)に保存してください。
```perl
#!/usr/bin/perl
use strict;
use warnings;
use CGI;
use File::Basename;
# --- 設定項目 ---
# ★★★ アップロードされたファイルを保存するディレクトリを指定してください ★★★
# このディレクトリは、Webサーバーのプロセスが書き込み可能なパーミッションを持っている必要があります。
my $upload_dir = "/tmp/uploads"; # 例: Linuxサーバーの場合
# my $upload_dir = "C:/temp/uploads"; # 例: Windowsサーバーの場合
# ------------------
# CGIオブジェクトの作成
my $q = CGI->new;
# HTTPヘッダーの出力 (文字コードをUTF-8に設定)
print $q->header(-charset => 'utf-8');
# HTMLの開始
print <<HTML_START;
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>アップロード結果</title>
</head>
<body>
HTML_START
# アップロード処理
if (my $file_param = $q->param('upload_file')) {
# アップロードされたファイル名を取得
my $original_filename = $file_param;
# セキュリティのため、ファイル名からディレクトリパス情報を取り除く
my $safe_filename = basename($original_filename);
# 注意: これだけでは不十分な場合があります。より厳密なサニタイズを推奨します。
# ファイルハンドルを取得
my $filehandle = $q->upload('upload_file');
if ($filehandle) {
# 保存先ディレクトリが存在しない場合は作成 (簡易的なエラー処理)
unless (-d $upload_dir) {
mkdir $upload_dir or die "ディレクトリを作成できません: $upload_dir ($!)";
}
# 保存するファイルのパス
my $save_path = "$upload_dir/$safe_filename";
# ファイルを開いてバイナリモードで書き込み
open my $out_fh, '>', $save_path or die "ファイルを開けません: $save_path ($!)";
binmode $out_fh;
my $buffer;
while (read($filehandle, $buffer, 1024)) {
print $out_fh $buffer;
}
close $out_fh;
close $filehandle;
print "<h1>アップロード成功!</h1>";
print "<p>ファイル「${safe_filename}」は正常にアップロードされました。</p>";
# print "<p>保存先: ${save_path}</p>"; # デバッグ用に表示。必要に応じてコメントアウトしてください。
} else {
print "<h1>アップロード失敗</h1>";
print "<p>ファイルハンドルを取得できませんでした。</p>";
}
} else {
print "<h1>エラー</h1>";
print "<p>アップロードするファイルが選択されていません。</p>";
}
# HTMLの終了と戻るリンク
print "<hr>";
print "<p><a href='../uploader.html'>アップロードページに戻る</a></p>"; # uploader.html の場所に合わせて調整
print <<HTML_END;
</body>
</html>
HTML_END
exit 0;
```
**ポイント:**
* `use CGI;` でCGIモジュールを使用します。
* `use File::Basename;` でファイル名からパス情報を取り除く `basename` 関数を使用します。
* **`$upload_dir` 変数:** **非常に重要です。** アップロードされたファイルを保存するディレクトリをサーバー上の絶対パスで指定してください。このディレクトリは、Webサーバーの実行ユーザー(例: `apache`, `www-data` など)が書き込み権限を持っている必要があります。
* `$q->param('upload_file')` で、HTMLフォームで指定した `name="upload_file"` のファイル名を取得します。
* `$q->upload('upload_file')` で、アップロードされたファイルの内容を読み取るためのファイルハンドルを取得します。
* セキュリティのため、`basename()` を使ってファイル名からディレクトリトラバーサルの可能性のある情報を取り除いています。 **これは最低限の対策であり、本番環境ではファイル名に使用できる文字を制限したり、拡張子をチェックしたりするなど、より厳密な検証が必要です。**
* ファイルはバイナリモード (`binmode`) で書き込みます。これにより、画像や実行ファイルなど、どんな種類のファイルでも正しく保存できます。
* 簡単なエラー処理(ディレクトリが存在しない場合、ファイルが選択されなかった場合など)を含んでいます。
---
## サーバーへの設置と注意点 ⚠️
1. **Perlのパス:** スクリプトの1行目 `#!/usr/bin/perl` は、サーバー上のPerlインタプリタのパスです。環境によっては `/usr/local/bin/perl` など異なる場合があるので、確認して修正してください。
2. **パーミッション:**
* `upload.cgi` ファイルには実行権限が必要です(例: `chmod 755 upload.cgi`)。
* `$upload_dir` で指定したディレクトリには、Webサーバーのプロセスが書き込みできる権限が必要です。
3. **セキュリティ:**
* **このサンプルは「最も簡単な例」であり、セキュリティ対策は最小限です。**
* **そのまま本番環境で使用するのは危険です。** 悪意のあるユーザーによって、サーバーに不正なファイルがアップロードされたり、既存のファイルが上書きされたりする可能性があります。
* 本番環境で利用する場合は、以下のようなセキュリティ対策を必ず検討・実装してください。
* **ファイルサイズの制限:** 大きすぎるファイルがアップロードされるのを防ぎます。
* **ファイル種類の制限:** 特定の拡張子(例: `.jpg`, `.png`, `.pdf`)のみを許可します。実行可能なスクリプト(`.php`, `.pl`, `.sh` など)のアップロードは特に危険です。
* **ファイル名のサニタイズ:** ファイル名に不正な文字が含まれていないか、より厳密にチェックし、必要であればランダムな文字列などに置き換えます。
* **保存ディレクトリの分離:** アップロードされたファイルをWebから直接アクセスできないディレクトリに保存し、ダウンロード用の別スクリプト経由でのみアクセスできるようにする。
* **ウィルススキャン:** アップロードされたファイルをウィルススキャンする。
4. **エラー処理:** この例のエラー処理は非常に基本的なものです。実際の運用では、より詳細なエラーログを出力したり、ユーザーに分かりやすいエラーメッセージを表示したりすることが望ましいです。
5. **CGIモジュール:** 通常、PerlがインストールされていればCGIモジュールも利用可能ですが、もし利用できない場合はサーバー管理者に相談してください。
---
この例が、Perl CGIでのファイルアップローダー作成の第一歩となれば幸いです。セキュリティには十分注意して開発を進めてください。
アップローダーを設置
├
液晶表示部とキー入力&計算機部 の部分を繋げた物を作成 Arduino
へびHTMLのスライド入力
18650をソーラーパネル + 充電モジュール(TP4056など)で蓄電し、夜間になるとフォトレジスタ(CdSセル)によりLEDが1つだけ点灯する回路を知りたい。
太陽電池の解説と充電方法、夜間点灯する物を自作したい時の方法
Git Docker Typescript React Node.js を学ぼう
ニキシー管 IN2を2個直列した時の方法 必須:10kΩ〜22kΩの電流制限抵抗
ファイル圧縮ルーチンの利用
めも
├
<!DOCTYPE html>
<html>
<head>
<title>3Dオブジェクト配置Ver1.001</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>
<style>
#xr-buttons {
position: fixed;
bottom: 20px;
left: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
}
#xr-buttons button {
padding: 12px 24px;
font-size: 16px;
border: 2px solid white;
background-color: rgba(0, 0, 0, 0.5);
color: white;
cursor: pointer;
border-radius: 8px;
}
#xr-buttons button:hover {
background-color: rgba(255, 255, 255, 0.3);
}
</style>
<script>
// --- SCRIPT BLOCK 1: Component Definitions (変更なし) ---
// --- プレイヤー移動制御用カスタムコンポーネント ('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; 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) { const verticalMovement = this.rightThumbstickInput.y * data.verticalSpeed * dt; this.rigEl.object3D.position.y -= verticalMovement; } } const position = this.rigEl.object3D.position; const cameraObject = this.cameraEl.object3D; 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 }, initialAxis: { type: 'vec3', default: null }, initialSpeed: { type: 'number', default: NaN } }, init: function () { const axisIsDefined = this.data.initialAxis && typeof this.data.initialAxis.x === 'number' && typeof this.data.initialAxis.y === 'number' && typeof this.data.initialAxis.z === 'number'; if (axisIsDefined && !isNaN(this.data.initialSpeed)) { this.axis = new THREE.Vector3(this.data.initialAxis.x, this.data.initialAxis.y, this.data.initialAxis.z); if (this.axis.lengthSq() === 0) { this.axis.set(0,1,0); } else { this.axis.normalize(); } this.speedRad = THREE.MathUtils.degToRad(this.data.initialSpeed); } else { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5); if (this.axis.lengthSq() < 0.001) { this.axis.set(0.01, 1, 0.01); } this.axis.normalize(); const speedSource = !isNaN(this.data.initialSpeed) ? this.data.initialSpeed : (((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1)); this.speedRad = THREE.MathUtils.degToRad(speedSource); } this.deltaRotationQuaternion = new THREE.Quaternion(); }, tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angleChangeRad = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angleChangeRad); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); this.el.object3D.quaternion.normalize(); } });
// --- End of SCRIPT BLOCK 1 ---
</script>
</head>
<body>
<a-scene id="myScene" vr-mode-ui="enabled: false" webxr="optionalFeatures: dom-overlay; overlayElement: #xr-buttons;">
<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" 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-image id="panelImage" src="" visible="false" width="40" height="18" position="0 0 0.06" material="shader: flat;"></a-image> <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 panelImageEl = document.getElementById('panelImage');
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 rigEl = document.getElementById('rig');
// --- ★★★ ネスト配置かどうかのフラグ ★★★ ---
const USE_NESTED_LAYOUT = true; // true にするとネスト配置、false で従来の単一パターン
// --- ★★★ ネスト配置用の設定 (USE_NESTED_LAYOUT = true の場合に参照) ★★★ ---
const NESTED_BASE_PATTERN = 1; // グループ中心の配置パターン (1: ランダム)
const NESTED_NUM_GROUPS = 3; // グループの数
const NESTED_GROUP_SPREAD = 200; // ベースがランダムの場合のグループ中心の広がり範囲
const NESTED_SUB_PATTERN = 9; // 各グループ内のオブジェクト配置パターン (9: グリッド回転あり)
const NESTED_SUB_GRID_COLUMNS = 3; // サブがグリッドの場合の列数
const NESTED_SUB_GRID_ROWS = 3; // サブがグリッドの場合の行数 (これでアイテム数が決まる)
const NESTED_SUB_ITEMS_PER_GROUP = NESTED_SUB_GRID_COLUMNS * NESTED_SUB_GRID_ROWS;
const NESTED_SUB_GRID_SPACING_X = 10; // サブグリッドのX方向の間隔
const NESTED_SUB_GRID_SPACING_Y = 10; // サブグリッドのY方向の間隔
// サブグリッドのグループ中心からの相対的なZ深度とYレベル
const NESTED_SUB_GRID_Z_DEPTH_RELATIVE = 0;
const NESTED_SUB_GRID_Y_LEVEL_RELATIVE = 0;
// --- 従来の単一配置パターン用の設定 (USE_NESTED_LAYOUT = false の場合に参照) ---
let SINGLE_LAYOUT_PATTERN = 5; // ★★★ 初期値を5に変更 ★★★ (ネストしない場合のデフォルト)
// --- グローバルな設定値 ---
const EXE_MODE = 0;
const SKY_BACKGROUND_IMAGE_PATH = "./pic/u5.jpg";
let RIG_INITIAL_X = 0; let RIG_INITIAL_Y = 0; let RIG_INITIAL_Z = 5;
const RIG_INITIAL_Y_CAMERA_LEVEL = 1.6;
let SPAWN_ANIM_START_POS_STRING = `${RIG_INITIAL_X} ${RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL} ${RIG_INITIAL_Z}`;
const PANEL_OFFSET_FROM_OBJECT = 5.0;
const DEFAULT_PANEL_TEXT_Y = 0; const DEFAULT_PANEL_IMAGE_Y = -3; const DEFAULT_CAPTION_Y = 8;
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' }, 'image_caption': { ...BASE_TEXT_STYLE, fontSize: 0.50, color: '#E0E0E0', align: 'center', anchorY: 'top', baseline:'top' } };
const OBJECT_DEFINITIONS = { 1: { type: 'sphere', useTextureForIndex: 1, sizeType: 'fixed', fixedSize: 3.0, rotationSettings: { axis: { x: 0, y: 1, z: 0 }, speed: 15 } }, 2: { type: 'box', useTextureForIndex: 2, 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" }, 2: { text: ["オブジェクト2のコメント。\n改行もできます。"] }, 3: { text: ["オブジェクト3にはリンクがあります。"], links: [{label:"A-Frame Site", url:"https://aframe.io", buttonColor:"green"}] } };
const DEFAULT_COMMENT_ARRAY_INFO = { text: ["コメントはありません"], links: [] };
// ★★★ numObjects はネスト設定に基づいて決定 ★★★
const numObjects = USE_NESTED_LAYOUT ? (NESTED_NUM_GROUPS * NESTED_SUB_ITEMS_PER_GROUP) : 10; // 元のデフォルトは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();
// 配置パターン用定数 (リグ位置に依存するものは initializeSceneAndObjects 内で更新)
let H_LINE_SPACING = 15; let H_LINE_Y = RIG_INITIAL_Y_CAMERA_LEVEL; let H_LINE_Z_OFFSET = -35;
let H_RING_RADIUS = 50; let H_RING_OBJECT_Y = RIG_INITIAL_Y_CAMERA_LEVEL;
let 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;
let GRID_INITIAL_Y = RIG_INITIAL_Y_CAMERA_LEVEL + 10; let GRID_Z_DEPTH = H_LINE_Z_OFFSET;
const V_LINE_SPACING = 15; const V_LINE_X_OFFSET = 0;
let V_LINE_START_Y_OFFSET_FROM_CENTER = ( (numObjects-1) * V_LINE_SPACING / 2 ); // numObjectsはここで正しい値に
let V_LINE_Z_OFFSET = -20;
let V_RING_RADIUS = 50; let V_RING_CENTER_Y = RIG_INITIAL_Y_CAMERA_LEVEL;
let V_RING_FIXED_Z_OFFSET = -25;
let 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;
const objectsFinalPositions = [];
// --- ★★★ オブジェクトの最終位置と回転を計算するヘルパー関数 ★★★ ---
function calculateObjectFinalPositionAndRotation(pattern, totalItemsInThisLayout, currentItemIndex, layoutCenterPos, layoutSpread, objectDefinition, isSub, tempLookAtEl) {
let finalX, finalY, finalZ;
let applyRandomRotation = false;
let initialRotation = null;
const objDef = objectDefinition || {}; // definitionが渡されない場合に備える
// isSubがtrueの場合、サブレイアウト用の定数を使用
const currentGridCols = isSub ? NESTED_SUB_GRID_COLUMNS : GRID_COLUMNS;
const currentGridSpacingX = isSub ? NESTED_SUB_GRID_SPACING_X : GRID_SPACING_X;
const currentGridSpacingY = isSub ? NESTED_SUB_GRID_SPACING_Y : GRID_SPACING_Y;
const currentGridInitialY = isSub ? (layoutCenterPos.y + NESTED_SUB_GRID_Y_LEVEL_RELATIVE) : GRID_INITIAL_Y;
const currentGridZDepth = isSub ? (layoutCenterPos.z + NESTED_SUB_GRID_Z_DEPTH_RELATIVE) : (RIG_INITIAL_Z + GRID_Z_DEPTH);
switch (pattern) {
case 1: finalX = (Math.random() - 0.5) * layoutSpread + layoutCenterPos.x; finalY = (Math.random() - 0.5) * layoutSpread + layoutCenterPos.y; finalZ = (Math.random() - 0.5) * layoutSpread + layoutCenterPos.z; applyRandomRotation = true; break;
case 2: finalX = (Math.random() - 0.5) * layoutSpread + layoutCenterPos.x; finalY = (Math.random() - 0.5) * layoutSpread + layoutCenterPos.y; finalZ = (Math.random() - 0.5) * layoutSpread + layoutCenterPos.z; applyRandomRotation = false; break;
case 3: const totalLineWidthH_3 = (totalItemsInThisLayout - 1) * H_LINE_SPACING; finalX = (-totalLineWidthH_3 / 2) + (currentItemIndex * H_LINE_SPACING) + layoutCenterPos.x; finalY = H_LINE_Y; finalZ = layoutCenterPos.z + H_LINE_Z_OFFSET; applyRandomRotation = true; break;
case 4: const totalLineWidthV_4 = (totalItemsInThisLayout - 1) * H_LINE_SPACING; finalX = (-totalLineWidthV_4 / 2) + (currentItemIndex * H_LINE_SPACING) + layoutCenterPos.x; finalY = H_LINE_Y; finalZ = layoutCenterPos.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) / totalItemsInThisLayout; } const angle360H = currentItemIndex * angleStep360H; finalX = layoutCenterPos.x + H_RING_RADIUS * Math.cos(angle360H); finalY = H_RING_OBJECT_Y; finalZ = layoutCenterPos.z + H_RING_RADIUS * Math.sin(angle360H); if(tempLookAtEl){ tempLookAtEl.object3D.position.set(finalX, finalY, finalZ); tempLookAtEl.object3D.lookAt(new THREE.Vector3(layoutCenterPos.x, H_RING_OBJECT_Y, layoutCenterPos.z)); initialRotation = { x: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.z) };} applyRandomRotation = (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_ = totalItemsInThisLayout > 1 ? totalAngleSpan180H / (totalItemsInThisLayout - 1) : 0; } const thetaH = startAngleOffset180H + (currentItemIndex * angleStep180H_); finalX = layoutCenterPos.x + H_RING_RADIUS * Math.sin(thetaH); finalY = H_RING_OBJECT_Y; finalZ = layoutCenterPos.z - H_RING_RADIUS * Math.cos(thetaH); if(tempLookAtEl){ tempLookAtEl.object3D.position.set(finalX, finalY, finalZ); tempLookAtEl.object3D.lookAt(new THREE.Vector3(layoutCenterPos.x, H_RING_OBJECT_Y, layoutCenterPos.z)); initialRotation = { x: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.z) };} applyRandomRotation = (pattern === 7); break;
case 9: case 10: const column = currentItemIndex % currentGridCols; const row = Math.floor(currentItemIndex / currentGridCols); const totalGridWidth = (currentGridCols - 1) * currentGridSpacingX; const startX_g = -totalGridWidth / 2; finalX = startX_g + column * currentGridSpacingX + layoutCenterPos.x; finalY = currentGridInitialY - row * currentGridSpacingY; finalZ = currentGridZDepth; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (pattern === 9); break;
case 11: case 12: finalX = V_LINE_X_OFFSET + layoutCenterPos.x; finalY = (layoutCenterPos.y + V_LINE_START_Y_OFFSET_FROM_CENTER) - (currentItemIndex * V_LINE_SPACING); finalZ = layoutCenterPos.z + V_LINE_Z_OFFSET; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (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) / totalItemsInThisLayout; } const angle360V = currentItemIndex * angleStep360V; finalX = layoutCenterPos.x; finalY = layoutCenterPos.y + V_RING_RADIUS * Math.cos(angle360V); finalZ = (layoutCenterPos.z + V_RING_FIXED_Z_OFFSET) + V_RING_RADIUS * Math.sin(angle360V); if(tempLookAtEl){ tempLookAtEl.object3D.position.set(finalX, finalY, finalZ); tempLookAtEl.object3D.lookAt(new THREE.Vector3(layoutCenterPos.x, layoutCenterPos.y, layoutCenterPos.z + V_RING_FIXED_Z_OFFSET)); initialRotation = { x: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.z) };} applyRandomRotation = (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_ = totalItemsInThisLayout > 1 ? arcSpan180V / (totalItemsInThisLayout - 1) : 0; } const thetaV = startAngle180V + (currentItemIndex * angleStep180V_); finalX = layoutCenterPos.x; finalY = layoutCenterPos.y + V_RING_RADIUS * Math.sin(thetaV); finalZ = (layoutCenterPos.z + V_RING_FIXED_Z_OFFSET) - V_RING_RADIUS * Math.cos(thetaV); if(tempLookAtEl){ tempLookAtEl.object3D.position.set(finalX, finalY, finalZ); tempLookAtEl.object3D.lookAt(new THREE.Vector3(layoutCenterPos.x, layoutCenterPos.y, layoutCenterPos.z + V_RING_FIXED_Z_OFFSET)); initialRotation = { x: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.z) };} applyRandomRotation = (pattern === 15); break;
default: console.warn(`calculateObjectFinalPositionAndRotation: Unknown pattern ${pattern}, defaulting to center.`); finalX = layoutCenterPos.x; finalY = layoutCenterPos.y; finalZ = layoutCenterPos.z; applyRandomRotation = true;
}
return { finalX, finalY, finalZ, initialRotation, applyRandomRotationComponent: applyRandomRotation };
}
// --- シーン初期化とオブジェクト生成を行うメイン関数 ---
function initializeSceneAndObjects() {
console.log("initializeSceneAndObjects: Started.");
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 params = new URLSearchParams(window.location.search);
const jxParam = params.get('jx'); const jyParam = params.get('jy'); const jzParam = params.get('jz');
const hosiParam = params.get('hosi'); const layoutParam = params.get('layout');
// URLパラメータで単一配置パターンが指定されていればそれを優先
if (layoutParam !== null && !isNaN(parseInt(layoutParam)) && !USE_NESTED_LAYOUT) {
SINGLE_LAYOUT_PATTERN = parseInt(layoutParam, 10);
}
console.log(`Active Layout Pattern: ${USE_NESTED_LAYOUT ? 'Nested' : SINGLE_LAYOUT_PATTERN}`);
if(USE_NESTED_LAYOUT) {
console.log(`Nested: Base=${NESTED_BASE_PATTERN}, Sub=${NESTED_SUB_PATTERN}, Groups=${NESTED_NUM_GROUPS}, Items/Group=${NESTED_SUB_ITEMS_PER_GROUP}`);
}
let htmlRigPos = rigEl.getAttribute('position');
RIG_INITIAL_X = htmlRigPos.x; RIG_INITIAL_Y = htmlRigPos.y; RIG_INITIAL_Z = htmlRigPos.z;
let tempHosiTargetPos = null;
const hosiTargetObjectOverallIndex = hosiParam ? parseInt(hosiParam, 10) : null;
if (hosiTargetObjectOverallIndex !== null && hosiTargetObjectOverallIndex >= 1 && hosiTargetObjectOverallIndex <= (USE_NESTED_LAYOUT ? NESTED_NUM_GROUPS * NESTED_SUB_ITEMS_PER_GROUP : numObjects)) {
console.log(`Hosi parameter found for overall object index: ${hosiTargetObjectOverallIndex}. jx,jy,jz will be ignored.`);
// hosi対象オブジェクトのワールド最終位置を計算 (リグが0,0,5にあると仮定)
let temp_rig_pos_for_hosi_calc = {x:0, y:0, z:5}; // 仮のリグ位置
let temp_camera_y_offset_for_hosi_calc = 1.6;
if (USE_NESTED_LAYOUT) {
const groupIndexForHosi = Math.floor((hosiTargetObjectOverallIndex - 1) / NESTED_SUB_ITEMS_PER_GROUP);
const itemIndexInSubForHosi = (hosiTargetObjectOverallIndex - 1) % NESTED_SUB_ITEMS_PER_GROUP;
const groupPosData = calculateObjectFinalPositionAndRotation(
NESTED_BASE_PATTERN, NESTED_NUM_GROUPS, groupIndexForHosi,
temp_rig_pos_for_hosi_calc, // 仮のリグ位置を基準にグループ中心を計算
NESTED_GROUP_SPREAD, {}, false, null
);
const subPosData = calculateObjectFinalPositionAndRotation(
NESTED_SUB_PATTERN, NESTED_SUB_ITEMS_PER_GROUP, itemIndexInSubForHosi,
groupPosData, // 計算されたグループ中心を基準に
0, {}, true, document.createElement('a-entity') // lookAt用にダミーelement
);
tempHosiTargetPos = { x: subPosData.finalX, y: subPosData.finalY, z: subPosData.finalZ };
} else {
const posData = calculateObjectFinalPositionAndRotation(
SINGLE_LAYOUT_PATTERN, numObjects, hosiTargetObjectOverallIndex - 1,
temp_rig_pos_for_hosi_calc, // 仮のリグ位置を基準に
spread, {}, false, document.createElement('a-entity')
);
tempHosiTargetPos = { x: posData.finalX, y: posData.finalY, z: posData.finalZ };
}
if (tempHosiTargetPos) {
RIG_INITIAL_X = tempHosiTargetPos.x;
RIG_INITIAL_Y = 0; // リグのYは地面レベル
RIG_INITIAL_Z = tempHosiTargetPos.z + 8;
} else {
if (jxParam !== null && jyParam !== null && jzParam !== null) { RIG_INITIAL_X = parseFloat(jxParam); RIG_INITIAL_Y = parseFloat(jyParam); RIG_INITIAL_Z = parseFloat(jzParam); }
}
} else if (jxParam !== null && jyParam !== null && jzParam !== null) {
RIG_INITIAL_X = parseFloat(jxParam); RIG_INITIAL_Y = parseFloat(jyParam); RIG_INITIAL_Z = parseFloat(jzParam);
}
rigEl.setAttribute('position', `${RIG_INITIAL_X} ${RIG_INITIAL_Y} ${RIG_INITIAL_Z}`);
SPAWN_ANIM_START_POS_STRING = `${RIG_INITIAL_X} ${RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL} ${RIG_INITIAL_Z}`;
H_LINE_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL; H_RING_OBJECT_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL;
HORIZONTAL_RING_LOOK_AT_TARGET.set(RIG_INITIAL_X, H_RING_OBJECT_Y, RIG_INITIAL_Z);
GRID_INITIAL_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL + 10;
V_RING_CENTER_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL;
VERTICAL_RING_LOOK_AT_TARGET.set(RIG_INITIAL_X, V_RING_CENTER_Y, RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET);
V_LINE_START_Y_OFFSET_FROM_CENTER = ( ((USE_NESTED_LAYOUT ? NESTED_SUB_ITEMS_PER_GROUP : numObjects)-1) * V_LINE_SPACING / 2 );
console.log(`Final Rig Initial Position set to: X=${RIG_INITIAL_X}, Y=${RIG_INITIAL_Y}, Z=${RIG_INITIAL_Z}`);
console.log(`Spawn Start Position String set to: ${SPAWN_ANIM_START_POS_STRING}`);
let overallObjectIndex = 0; // 全オブジェクトの通し番号
if (USE_NESTED_LAYOUT) {
const tempLookAtHelper = document.createElement('a-entity'); // lookAt計算用のダミー
for (let g = 0; g < NESTED_NUM_GROUPS; g++) {
const groupCenterData = calculateObjectFinalPositionAndRotation(
NESTED_BASE_PATTERN,
NESTED_NUM_GROUPS, g,
{x: RIG_INITIAL_X, y: RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL, z: RIG_INITIAL_Z}, // ベース配置の中心
NESTED_GROUP_SPREAD,
{}, // グループ自体には個別回転設定なしと仮定
false, // isSubLayout = false
tempLookAtHelper
);
const groupCenter = {x: groupCenterData.finalX, y: groupCenterData.finalY, z: groupCenterData.finalZ};
for (let k = 0; k < NESTED_SUB_ITEMS_PER_GROUP; k++) {
overallObjectIndex++;
const objectIndexForDefs = overallObjectIndex; // 1始まりのインデックス
const definition = OBJECT_DEFINITIONS[objectIndexForDefs] || {};
const objectType = definition.type || DEFAULT_OBJECT_TYPE;
let objectEl = document.createElement(objectType === 'sphere' ? 'a-sphere' : '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 ${objectIndexForDefs}: sizeType 'fixed' but fixedSize is invalid. Defaulting to random.`); }
dimension = Math.random() * 10.0 + 0.5;
}
const subPosData = calculateObjectFinalPositionAndRotation(
NESTED_SUB_PATTERN, NESTED_SUB_ITEMS_PER_GROUP, k,
groupCenter, // サブ配置の基準点は計算されたグループ中心
0, // サブ配置のspreadは通常0
definition.rotationSettings || {},
true, // isSubLayout = true
tempLookAtHelper
);
const finalX = subPosData.finalX; const finalY = subPosData.finalY; const finalZ = subPosData.finalZ;
const initialRotation = subPosData.initialRotation; const applyRandomRotationComponent = subPosData.applyRandomRotationComponent;
objectsFinalPositions[objectIndexForDefs] = {x: finalX, y: finalY, z: finalZ};
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 (!applyRandomRotationComponent && !(definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number'))) { objectEl.setAttribute('rotation', '0 0 0');}
objectEl.classList.add('clickableObject'); objectEl.dataset.objectIndex = objectIndexForDefs; objectEl.dataset.color = appliedColor; objectEl.dataset.dimension = dimension.toFixed(2); objectEl.dataset.objectType = objectType;
const hasSpecificRotation = definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number');
if (applyRandomRotationComponent || hasSpecificRotation) { let rotationParams = { maxSpeed: 5 }; if (definition.rotationSettings) { if (definition.rotationSettings.axis) { rotationParams.initialAxis = definition.rotationSettings.axis; } if (typeof definition.rotationSettings.speed === 'number') { rotationParams.initialSpeed = definition.rotationSettings.speed; } } objectEl.setAttribute('random-rotate', rotationParams); }
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'});
}
}
} else { // 従来の単一配置パターン
const singlePatternNumObjects = numObjects; // この変数は、単一パターンの時の総オブジェクト数
for (let i = 0; i < singlePatternNumObjects; i++) {
const objectIndex = i + 1;
const definition = OBJECT_DEFINITIONS[objectIndex] || {};
const objectType = definition.type || DEFAULT_OBJECT_TYPE;
let objectEl = document.createElement(objectType === 'sphere' ? 'a-sphere' : '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. Defaulting to random.`); } dimension = Math.random() * 10.0 + 0.5; }
const posData = calculateObjectFinalPositionAndRotation(
SINGLE_LAYOUT_PATTERN, singlePatternNumObjects, i,
{x: RIG_INITIAL_X, y: RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL, z: RIG_INITIAL_Z},
spread, definition.rotationSettings || {}, false, objectEl
);
const finalX = posData.finalX; const finalY = posData.finalY; const finalZ = posData.finalZ;
const initialRotation = posData.initialRotation; const applyRandomRotationComponent = posData.applyRandomRotationComponent;
objectsFinalPositions[objectIndex] = {x: finalX, y: finalY, z: finalZ};
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 (!applyRandomRotationComponent && !(definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number'))) { 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;
const hasSpecificRotation = definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number');
if (applyRandomRotationComponent || hasSpecificRotation) { let rotationParams = { maxSpeed: 5 }; if (definition.rotationSettings) { if (definition.rotationSettings.axis) { rotationParams.initialAxis = definition.rotationSettings.axis; } if (typeof definition.rotationSettings.speed === 'number') { rotationParams.initialSpeed = definition.rotationSettings.speed; } } objectEl.setAttribute('random-rotate', rotationParams); }
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'});
}
}
// hosiパラメータが指定されていた場合のリグの向きの最終調整
if (hosiTargetObjectOverallIndex !== null && objectsFinalPositions[hosiTargetObjectOverallIndex]) {
const targetObjectPos = objectsFinalPositions[hosiTargetObjectOverallIndex];
console.log(`Hosi target found for initial rig lookAt:`, targetObjectPos);
const lookAtPos = new THREE.Vector3(targetObjectPos.x, RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL, targetObjectPos.z);
rigEl.object3D.lookAt(lookAtPos);
const currentRigRotationEuler = new THREE.Euler().setFromQuaternion(rigEl.object3D.quaternion, 'YXZ');
rigEl.setAttribute('rotation', `0 ${THREE.MathUtils.radToDeg(currentRigRotationEuler.y)} 0`);
console.log("Rig initial rotation set by hosi parameter.");
}
initializeEventListeners();
}
function updatePanelDisplay() { if (!infoPanelEl.dataset.objectIndex) return; const index = parseInt(infoPanelEl.dataset.objectIndex || '0', 10); const objectBaseColor = 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 || [DEFAULT_COMMENT_ARRAY_INFO.text[0]]; const mainCommentTextColorForThisObject = 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']) }; panelTextEl.setAttribute('visible', false); panelTextEl.setAttribute('troika-text', 'value', ''); panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src'); linkButtons.forEach(btn => btn.setAttribute('visible', false)); const currentCommentItem = commentsArray[commentSubPageIndex]; if (EXE_MODE === 0) { if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') { const imgY = typeof currentCommentItem.imageY === 'number' ? currentCommentItem.imageY : DEFAULT_PANEL_IMAGE_Y; panelImageEl.object3D.position.set(0, imgY, 0.06); panelImageEl.setAttribute('src', currentCommentItem.src); panelImageEl.setAttribute('width', currentCommentItem.width || 40); const imgHeight = currentCommentItem.height; if (imgHeight && imgHeight !== 'auto') { panelImageEl.setAttribute('height', imgHeight); } else { panelImageEl.removeAttribute('height'); } panelImageEl.setAttribute('visible', true); displayText = currentCommentItem.caption || ''; if (displayText) { const capY = typeof currentCommentItem.captionY === 'number' ? currentCommentItem.captionY : DEFAULT_CAPTION_Y; panelTextEl.object3D.position.set(0, capY, 0.05); currentStyle = {...(PAGE_TEXT_STYLES['image_caption'] || PAGE_TEXT_STYLES['comment'])}; if (mainCommentTextColorForThisObject) currentStyle.color = mainCommentTextColorForThisObject; panelTextEl.setAttribute('visible', true); } } else { displayText = typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0]; panelTextEl.object3D.position.set(0, DEFAULT_PANEL_TEXT_Y, 0.05); if (mainCommentTextColorForThisObject) currentStyle.color = mainCommentTextColorForThisObject; panelTextEl.setAttribute('visible', true); } if (commentSubPageCount > 1) { pageIndicator = `(${commentSubPageIndex + 1}/${commentSubPageCount})`; } else { pageIndicator = ''; } } else { pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`; panelTextEl.setAttribute('visible', true); panelTextEl.object3D.position.set(0, DEFAULT_PANEL_TEXT_Y, 0.05); if (pageType === 'index') { displayText = `${objectType === 'sphere' ? '球' : '立方体'}: ${index}`; } else if (pageType === 'color') { displayText = `色: ${objectBaseColor}`; } else if (pageType === 'size') { displayText = `${objectType === 'sphere' ? '半径' : 'サイズ'}: ${dimensionValue}`; } else if (pageType === 'comment') { pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`; if (mainCommentTextColorForThisObject) { currentStyle.color = mainCommentTextColorForThisObject; } if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') { const imgY = typeof currentCommentItem.imageY === 'number' ? currentCommentItem.imageY : DEFAULT_PANEL_IMAGE_Y; panelImageEl.object3D.position.set(0, imgY, 0.06); panelImageEl.setAttribute('src', currentCommentItem.src); panelImageEl.setAttribute('width', currentCommentItem.width || 40); const imgHeight = currentCommentItem.height; if (imgHeight && imgHeight !== 'auto') { panelImageEl.setAttribute('height', imgHeight); } else { panelImageEl.removeAttribute('height'); } panelImageEl.setAttribute('visible', true); displayText = `コメント:\n${currentCommentItem.caption || ''}`; const capY = typeof currentCommentItem.captionY === 'number' ? currentCommentItem.captionY : DEFAULT_CAPTION_Y; panelTextEl.object3D.position.set(0, capY, 0.05); } else { displayText = `コメント:\n${typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0]}`; } } } 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}`; if (panelTextEl.getAttribute('visible')) { 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 }); } else if (EXE_MODE === 0 && pageIndicator && !(typeof currentCommentItem === 'object' && currentCommentItem.type === 'image' && !currentCommentItem.caption)) { panelTextEl.setAttribute('troika-text', { value: pageIndicator, color: PAGE_TEXT_STYLES['image_caption'].color, fontSize: PAGE_TEXT_STYLES['image_caption'].fontSize, maxWidth: currentStyle.maxWidth, align: currentStyle.align, anchorX: currentStyle.anchorX, anchorY: currentStyle.anchorY, baseline: currentStyle.baseline }); panelTextEl.setAttribute('visible', true); } }
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 ? commentDataForThisObject.text.length : 1).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 currentMainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let currentCommentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const currentObjectCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; const currentCommentSubPageCount = currentObjectCommentData.text.length; if (EXE_MODE === 0) { currentCommentSubPageIndex = (currentCommentSubPageIndex - 1 + currentCommentSubPageCount) % currentCommentSubPageCount; } else { if (PAGES[currentMainPageIndex] === 'comment' && currentCommentSubPageIndex > 0) { currentCommentSubPageIndex--; } else { currentMainPageIndex = (currentMainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; if (PAGES[currentMainPageIndex] === 'comment') { const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} currentCommentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } else { currentCommentSubPageIndex = 0; infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } } infoPanelEl.dataset.currentPageIndex = currentMainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = currentCommentSubPageIndex.toString(); updatePanelDisplay(); console.log("Prev button: mainPage=", currentMainPageIndex, "subPage=", currentCommentSubPageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let currentMainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let currentCommentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const currentObjectCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; const currentCommentSubPageCount = currentObjectCommentData.text.length; if (EXE_MODE === 0) { currentCommentSubPageIndex = (currentCommentSubPageIndex + 1) % currentCommentSubPageCount; } else { if (PAGES[currentMainPageIndex] === 'comment' && currentCommentSubPageIndex < currentCommentSubPageCount - 1) { currentCommentSubPageIndex++; } else { currentMainPageIndex = (currentMainPageIndex + 1) % TOTAL_MAIN_PAGES; currentCommentSubPageIndex = 0; if (PAGES[currentMainPageIndex] === 'comment') { const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} } else {infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } } infoPanelEl.dataset.currentPageIndex = currentMainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = currentCommentSubPageIndex.toString(); updatePanelDisplay(); console.log("Next button: mainPage=", currentMainPageIndex, "subPage=", currentCommentSubPageIndex); });
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)); panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src'); 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); }
// ★★★ enter-vr, exit-vrリスナーのロジックを更新 ★★★
sceneEl.addEventListener('enter-vr', function () {
console.log("Entered XR mode (AR or VR)");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false');
});
sceneEl.addEventListener('exit-vr', function () {
console.log("Exited XR mode");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true');
// XRモード終了時に必ず空を再表示する
if (skyElement) skyElement.setAttribute('visible', 'true');
});
document.addEventListener('DOMContentLoaded', function () {const scene = document.querySelector('a-scene'); if (scene) { if (scene.hasLoaded) {initializeSceneAndObjects();} else {scene.addEventListener('loaded', initializeSceneAndObjects, {once: true});}} else {console.error("a-scene element not found at DOMContentLoaded!");}});
function initializeEventListeners() {
console.log("Initializing event listeners for buttons and controllers.");
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.");
}
// ★★★ AR/VR切り替えボタンのイベントリスナーをここに追加 ★★★
const arButton = document.getElementById('ar-button');
const vrButton = document.getElementById('vr-button');
if (arButton) {
arButton.addEventListener('click', async () => {
console.log("AR Button clicked");
// ARモードに入る前に空を非表示にする
if (skyElement) skyElement.setAttribute('visible', 'false');
try {
await sceneEl.enterAR();
} catch (e) {
console.error("Failed to enter AR mode", e);
// 失敗した場合は空を再表示する
if (skyElement) skyElement.setAttribute('visible', 'true');
}
});
}
if (vrButton) {
vrButton.addEventListener('click', async () => {
console.log("VR Button clicked");
// VRモードに入る前に空を表示する
if (skyElement) skyElement.setAttribute('visible', 'true');
try {
await sceneEl.enterVR();
} catch (e) {
console.error("Failed to enter VR mode", e);
}
});
}
}
// --- End of SCRIPT BLOCK 2 ---
</script>
</a-scene>
<div id="xr-buttons">
<button id="ar-button">Enter AR</button>
<button id="vr-button">Enter VR</button>
</div>
</body>
</html>
a-farme-球に文字B-test68
├
<!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>
// --- SCRIPT BLOCK 1: Component Definitions ---
// --- プレイヤー移動制御用カスタムコンポーネント ('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; 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) { const verticalMovement = this.rightThumbstickInput.y * data.verticalSpeed * dt; this.rigEl.object3D.position.y -= verticalMovement; } } const position = this.rigEl.object3D.position; const cameraObject = this.cameraEl.object3D; 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 }, initialAxis: { type: 'vec3', default: null }, initialSpeed: { type: 'number', default: NaN } }, init: function () { const axisIsDefined = this.data.initialAxis && typeof this.data.initialAxis.x === 'number' && typeof this.data.initialAxis.y === 'number' && typeof this.data.initialAxis.z === 'number'; if (axisIsDefined && !isNaN(this.data.initialSpeed)) { this.axis = new THREE.Vector3(this.data.initialAxis.x, this.data.initialAxis.y, this.data.initialAxis.z); if (this.axis.lengthSq() === 0) { this.axis.set(0,1,0); } else { this.axis.normalize(); } this.speedRad = THREE.MathUtils.degToRad(this.data.initialSpeed); } else { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5); if (this.axis.lengthSq() < 0.001) { this.axis.set(0.01, 1, 0.01); } this.axis.normalize(); const speedSource = !isNaN(this.data.initialSpeed) ? this.data.initialSpeed : (((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1)); this.speedRad = THREE.MathUtils.degToRad(speedSource); } this.deltaRotationQuaternion = new THREE.Quaternion(); }, tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angleChangeRad = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angleChangeRad); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); this.el.object3D.quaternion.normalize(); } });
// --- 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" 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-image id="panelImage" src="" visible="false" width="40" height="18" position="0 0 0.06" material="shader: flat;"></a-image> <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 panelImageEl = document.getElementById('panelImage');
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 rigEl = document.getElementById('rig');
// --- ★★★ ネスト配置かどうかのフラグ ★★★ ---
const USE_NESTED_LAYOUT = true; // true にするとネスト配置、false で従来の単一パターン
// --- ★★★ ネスト配置用の設定 (USE_NESTED_LAYOUT = true の場合に参照) ★★★ ---
const NESTED_BASE_PATTERN = 1; // グループ中心の配置パターン (1: ランダム)
const NESTED_NUM_GROUPS = 3; // グループの数
const NESTED_GROUP_SPREAD = 200; // ベースがランダムの場合のグループ中心の広がり範囲
const NESTED_SUB_PATTERN = 9; // 各グループ内のオブジェクト配置パターン (9: グリッド回転あり)
const NESTED_SUB_GRID_COLUMNS = 3; // サブがグリッドの場合の列数
const NESTED_SUB_GRID_ROWS = 3; // サブがグリッドの場合の行数 (これでアイテム数が決まる)
const NESTED_SUB_ITEMS_PER_GROUP = NESTED_SUB_GRID_COLUMNS * NESTED_SUB_GRID_ROWS;
const NESTED_SUB_GRID_SPACING_X = 10; // サブグリッドのX方向の間隔
const NESTED_SUB_GRID_SPACING_Y = 10; // サブグリッドのY方向の間隔
// サブグリッドのグループ中心からの相対的なZ深度とYレベル
const NESTED_SUB_GRID_Z_DEPTH_RELATIVE = 0;
const NESTED_SUB_GRID_Y_LEVEL_RELATIVE = 0;
// --- 従来の単一配置パターン用の設定 (USE_NESTED_LAYOUT = false の場合に参照) ---
let SINGLE_LAYOUT_PATTERN = 5; // ★★★ 初期値を5に変更 ★★★ (ネストしない場合のデフォルト)
// --- グローバルな設定値 ---
const EXE_MODE = 0;
const SKY_BACKGROUND_IMAGE_PATH = "./pic/u5.jpg";
let RIG_INITIAL_X = 0; let RIG_INITIAL_Y = 0; let RIG_INITIAL_Z = 5;
const RIG_INITIAL_Y_CAMERA_LEVEL = 1.6;
let SPAWN_ANIM_START_POS_STRING = `${RIG_INITIAL_X} ${RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL} ${RIG_INITIAL_Z}`;
const PANEL_OFFSET_FROM_OBJECT = 5.0;
const DEFAULT_PANEL_TEXT_Y = 0; const DEFAULT_PANEL_IMAGE_Y = -3; const DEFAULT_CAPTION_Y = 8;
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' }, 'image_caption': { ...BASE_TEXT_STYLE, fontSize: 0.50, color: '#E0E0E0', align: 'center', anchorY: 'top', baseline:'top' } };
const OBJECT_DEFINITIONS = { 1: { type: 'sphere', useTextureForIndex: 1, sizeType: 'fixed', fixedSize: 3.0, rotationSettings: { axis: { x: 0, y: 1, z: 0 }, speed: 15 } }, 2: { type: 'box', useTextureForIndex: 2, 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" }, 2: { text: ["オブジェクト2のコメント。\n改行もできます。"] }, 3: { text: ["オブジェクト3にはリンクがあります。"], links: [{label:"A-Frame Site", url:"https://aframe.io", buttonColor:"green"}] } };
const DEFAULT_COMMENT_ARRAY_INFO = { text: ["コメントはありません"], links: [] };
// ★★★ numObjects はネスト設定に基づいて決定 ★★★
const numObjects = USE_NESTED_LAYOUT ? (NESTED_NUM_GROUPS * NESTED_SUB_ITEMS_PER_GROUP) : 10; // 元のデフォルトは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();
// 配置パターン用定数 (リグ位置に依存するものは initializeSceneAndObjects 内で更新)
let H_LINE_SPACING = 15; let H_LINE_Y = RIG_INITIAL_Y_CAMERA_LEVEL; let H_LINE_Z_OFFSET = -35;
let H_RING_RADIUS = 50; let H_RING_OBJECT_Y = RIG_INITIAL_Y_CAMERA_LEVEL;
let 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;
let GRID_INITIAL_Y = RIG_INITIAL_Y_CAMERA_LEVEL + 10; let GRID_Z_DEPTH = H_LINE_Z_OFFSET;
const V_LINE_SPACING = 15; const V_LINE_X_OFFSET = 0;
let V_LINE_START_Y_OFFSET_FROM_CENTER = ( (numObjects-1) * V_LINE_SPACING / 2 ); // numObjectsはここで正しい値に
let V_LINE_Z_OFFSET = -20;
let V_RING_RADIUS = 50; let V_RING_CENTER_Y = RIG_INITIAL_Y_CAMERA_LEVEL;
let V_RING_FIXED_Z_OFFSET = -25;
let 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;
const objectsFinalPositions = [];
// --- ★★★ オブジェクトの最終位置と回転を計算するヘルパー関数 ★★★ ---
function calculateObjectFinalPositionAndRotation(pattern, totalItemsInThisLayout, currentItemIndex, layoutCenterPos, layoutSpread, objectDefinition, isSub, tempLookAtEl) {
let finalX, finalY, finalZ;
let applyRandomRotation = false;
let initialRotation = null;
const objDef = objectDefinition || {}; // definitionが渡されない場合に備える
// isSubがtrueの場合、サブレイアウト用の定数を使用
const currentGridCols = isSub ? NESTED_SUB_GRID_COLUMNS : GRID_COLUMNS;
const currentGridSpacingX = isSub ? NESTED_SUB_GRID_SPACING_X : GRID_SPACING_X;
const currentGridSpacingY = isSub ? NESTED_SUB_GRID_SPACING_Y : GRID_SPACING_Y;
const currentGridInitialY = isSub ? (layoutCenterPos.y + NESTED_SUB_GRID_Y_LEVEL_RELATIVE) : GRID_INITIAL_Y;
const currentGridZDepth = isSub ? (layoutCenterPos.z + NESTED_SUB_GRID_Z_DEPTH_RELATIVE) : (RIG_INITIAL_Z + GRID_Z_DEPTH);
switch (pattern) {
case 1: finalX = (Math.random() - 0.5) * layoutSpread + layoutCenterPos.x; finalY = (Math.random() - 0.5) * layoutSpread + layoutCenterPos.y; finalZ = (Math.random() - 0.5) * layoutSpread + layoutCenterPos.z; applyRandomRotation = true; break;
case 2: finalX = (Math.random() - 0.5) * layoutSpread + layoutCenterPos.x; finalY = (Math.random() - 0.5) * layoutSpread + layoutCenterPos.y; finalZ = (Math.random() - 0.5) * layoutSpread + layoutCenterPos.z; applyRandomRotation = false; break;
case 3: const totalLineWidthH_3 = (totalItemsInThisLayout - 1) * H_LINE_SPACING; finalX = (-totalLineWidthH_3 / 2) + (currentItemIndex * H_LINE_SPACING) + layoutCenterPos.x; finalY = H_LINE_Y; finalZ = layoutCenterPos.z + H_LINE_Z_OFFSET; applyRandomRotation = true; break;
case 4: const totalLineWidthV_4 = (totalItemsInThisLayout - 1) * H_LINE_SPACING; finalX = (-totalLineWidthV_4 / 2) + (currentItemIndex * H_LINE_SPACING) + layoutCenterPos.x; finalY = H_LINE_Y; finalZ = layoutCenterPos.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) / totalItemsInThisLayout; } const angle360H = currentItemIndex * angleStep360H; finalX = layoutCenterPos.x + H_RING_RADIUS * Math.cos(angle360H); finalY = H_RING_OBJECT_Y; finalZ = layoutCenterPos.z + H_RING_RADIUS * Math.sin(angle360H); if(tempLookAtEl){ tempLookAtEl.object3D.position.set(finalX, finalY, finalZ); tempLookAtEl.object3D.lookAt(new THREE.Vector3(layoutCenterPos.x, H_RING_OBJECT_Y, layoutCenterPos.z)); initialRotation = { x: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.z) };} applyRandomRotation = (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_ = totalItemsInThisLayout > 1 ? totalAngleSpan180H / (totalItemsInThisLayout - 1) : 0; } const thetaH = startAngleOffset180H + (currentItemIndex * angleStep180H_); finalX = layoutCenterPos.x + H_RING_RADIUS * Math.sin(thetaH); finalY = H_RING_OBJECT_Y; finalZ = layoutCenterPos.z - H_RING_RADIUS * Math.cos(thetaH); if(tempLookAtEl){ tempLookAtEl.object3D.position.set(finalX, finalY, finalZ); tempLookAtEl.object3D.lookAt(new THREE.Vector3(layoutCenterPos.x, H_RING_OBJECT_Y, layoutCenterPos.z)); initialRotation = { x: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.z) };} applyRandomRotation = (pattern === 7); break;
case 9: case 10: const column = currentItemIndex % currentGridCols; const row = Math.floor(currentItemIndex / currentGridCols); const totalGridWidth = (currentGridCols - 1) * currentGridSpacingX; const startX_g = -totalGridWidth / 2; finalX = startX_g + column * currentGridSpacingX + layoutCenterPos.x; finalY = currentGridInitialY - row * currentGridSpacingY; finalZ = currentGridZDepth; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (pattern === 9); break;
case 11: case 12: finalX = V_LINE_X_OFFSET + layoutCenterPos.x; finalY = (layoutCenterPos.y + V_LINE_START_Y_OFFSET_FROM_CENTER) - (currentItemIndex * V_LINE_SPACING); finalZ = layoutCenterPos.z + V_LINE_Z_OFFSET; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (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) / totalItemsInThisLayout; } const angle360V = currentItemIndex * angleStep360V; finalX = layoutCenterPos.x; finalY = layoutCenterPos.y + V_RING_RADIUS * Math.cos(angle360V); finalZ = (layoutCenterPos.z + V_RING_FIXED_Z_OFFSET) + V_RING_RADIUS * Math.sin(angle360V); if(tempLookAtEl){ tempLookAtEl.object3D.position.set(finalX, finalY, finalZ); tempLookAtEl.object3D.lookAt(new THREE.Vector3(layoutCenterPos.x, layoutCenterPos.y, layoutCenterPos.z + V_RING_FIXED_Z_OFFSET)); initialRotation = { x: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.z) };} applyRandomRotation = (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_ = totalItemsInThisLayout > 1 ? arcSpan180V / (totalItemsInThisLayout - 1) : 0; } const thetaV = startAngle180V + (currentItemIndex * angleStep180V_); finalX = layoutCenterPos.x; finalY = layoutCenterPos.y + V_RING_RADIUS * Math.sin(thetaV); finalZ = (layoutCenterPos.z + V_RING_FIXED_Z_OFFSET) - V_RING_RADIUS * Math.cos(thetaV); if(tempLookAtEl){ tempLookAtEl.object3D.position.set(finalX, finalY, finalZ); tempLookAtEl.object3D.lookAt(new THREE.Vector3(layoutCenterPos.x, layoutCenterPos.y, layoutCenterPos.z + V_RING_FIXED_Z_OFFSET)); initialRotation = { x: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.x), y: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.y), z: THREE.MathUtils.radToDeg(tempLookAtEl.object3D.rotation.z) };} applyRandomRotation = (pattern === 15); break;
default: console.warn(`calculateObjectFinalPositionAndRotation: Unknown pattern ${pattern}, defaulting to center.`); finalX = layoutCenterPos.x; finalY = layoutCenterPos.y; finalZ = layoutCenterPos.z; applyRandomRotation = true;
}
return { finalX, finalY, finalZ, initialRotation, applyRandomRotationComponent: applyRandomRotation };
}
// --- シーン初期化とオブジェクト生成を行うメイン関数 ---
function initializeSceneAndObjects() {
console.log("initializeSceneAndObjects: Started.");
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 params = new URLSearchParams(window.location.search);
const jxParam = params.get('jx'); const jyParam = params.get('jy'); const jzParam = params.get('jz');
const hosiParam = params.get('hosi'); const layoutParam = params.get('layout');
// URLパラメータで単一配置パターンが指定されていればそれを優先
if (layoutParam !== null && !isNaN(parseInt(layoutParam)) && !USE_NESTED_LAYOUT) {
SINGLE_LAYOUT_PATTERN = parseInt(layoutParam, 10);
}
console.log(`Active Layout Pattern: ${USE_NESTED_LAYOUT ? 'Nested' : SINGLE_LAYOUT_PATTERN}`);
if(USE_NESTED_LAYOUT) {
console.log(`Nested: Base=${NESTED_BASE_PATTERN}, Sub=${NESTED_SUB_PATTERN}, Groups=${NESTED_NUM_GROUPS}, Items/Group=${NESTED_SUB_ITEMS_PER_GROUP}`);
}
let htmlRigPos = rigEl.getAttribute('position');
RIG_INITIAL_X = htmlRigPos.x; RIG_INITIAL_Y = htmlRigPos.y; RIG_INITIAL_Z = htmlRigPos.z;
let tempHosiTargetPos = null;
const hosiTargetObjectOverallIndex = hosiParam ? parseInt(hosiParam, 10) : null;
if (hosiTargetObjectOverallIndex !== null && hosiTargetObjectOverallIndex >= 1 && hosiTargetObjectOverallIndex <= (USE_NESTED_LAYOUT ? NESTED_NUM_GROUPS * NESTED_SUB_ITEMS_PER_GROUP : numObjects)) {
console.log(`Hosi parameter found for overall object index: ${hosiTargetObjectOverallIndex}. jx,jy,jz will be ignored.`);
// hosi対象オブジェクトのワールド最終位置を計算 (リグが0,0,5にあると仮定)
let temp_rig_pos_for_hosi_calc = {x:0, y:0, z:5}; // 仮のリグ位置
let temp_camera_y_offset_for_hosi_calc = 1.6;
if (USE_NESTED_LAYOUT) {
const groupIndexForHosi = Math.floor((hosiTargetObjectOverallIndex - 1) / NESTED_SUB_ITEMS_PER_GROUP);
const itemIndexInSubForHosi = (hosiTargetObjectOverallIndex - 1) % NESTED_SUB_ITEMS_PER_GROUP;
const groupPosData = calculateObjectFinalPositionAndRotation(
NESTED_BASE_PATTERN, NESTED_NUM_GROUPS, groupIndexForHosi,
temp_rig_pos_for_hosi_calc, // 仮のリグ位置を基準にグループ中心を計算
NESTED_GROUP_SPREAD, {}, false, null
);
const subPosData = calculateObjectFinalPositionAndRotation(
NESTED_SUB_PATTERN, NESTED_SUB_ITEMS_PER_GROUP, itemIndexInSubForHosi,
groupPosData, // 計算されたグループ中心を基準に
0, {}, true, document.createElement('a-entity') // lookAt用にダミーelement
);
tempHosiTargetPos = { x: subPosData.finalX, y: subPosData.finalY, z: subPosData.finalZ };
} else {
const posData = calculateObjectFinalPositionAndRotation(
SINGLE_LAYOUT_PATTERN, numObjects, hosiTargetObjectOverallIndex - 1,
temp_rig_pos_for_hosi_calc, // 仮のリグ位置を基準に
spread, {}, false, document.createElement('a-entity')
);
tempHosiTargetPos = { x: posData.finalX, y: posData.finalY, z: posData.finalZ };
}
if (tempHosiTargetPos) {
RIG_INITIAL_X = tempHosiTargetPos.x;
RIG_INITIAL_Y = 0; // リグのYは地面レベル
RIG_INITIAL_Z = tempHosiTargetPos.z + 8;
} else {
if (jxParam !== null && jyParam !== null && jzParam !== null) { RIG_INITIAL_X = parseFloat(jxParam); RIG_INITIAL_Y = parseFloat(jyParam); RIG_INITIAL_Z = parseFloat(jzParam); }
}
} else if (jxParam !== null && jyParam !== null && jzParam !== null) {
RIG_INITIAL_X = parseFloat(jxParam); RIG_INITIAL_Y = parseFloat(jyParam); RIG_INITIAL_Z = parseFloat(jzParam);
}
rigEl.setAttribute('position', `${RIG_INITIAL_X} ${RIG_INITIAL_Y} ${RIG_INITIAL_Z}`);
SPAWN_ANIM_START_POS_STRING = `${RIG_INITIAL_X} ${RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL} ${RIG_INITIAL_Z}`;
H_LINE_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL; H_RING_OBJECT_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL;
HORIZONTAL_RING_LOOK_AT_TARGET.set(RIG_INITIAL_X, H_RING_OBJECT_Y, RIG_INITIAL_Z);
GRID_INITIAL_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL + 10;
V_RING_CENTER_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL;
VERTICAL_RING_LOOK_AT_TARGET.set(RIG_INITIAL_X, V_RING_CENTER_Y, RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET);
V_LINE_START_Y_OFFSET_FROM_CENTER = ( ((USE_NESTED_LAYOUT ? NESTED_SUB_ITEMS_PER_GROUP : numObjects)-1) * V_LINE_SPACING / 2 );
console.log(`Final Rig Initial Position set to: X=${RIG_INITIAL_X}, Y=${RIG_INITIAL_Y}, Z=${RIG_INITIAL_Z}`);
console.log(`Spawn Start Position String set to: ${SPAWN_ANIM_START_POS_STRING}`);
let overallObjectIndex = 0; // 全オブジェクトの通し番号
if (USE_NESTED_LAYOUT) {
const tempLookAtHelper = document.createElement('a-entity'); // lookAt計算用のダミー
for (let g = 0; g < NESTED_NUM_GROUPS; g++) {
const groupCenterData = calculateObjectFinalPositionAndRotation(
NESTED_BASE_PATTERN,
NESTED_NUM_GROUPS, g,
{x: RIG_INITIAL_X, y: RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL, z: RIG_INITIAL_Z}, // ベース配置の中心
NESTED_GROUP_SPREAD,
{}, // グループ自体には個別回転設定なしと仮定
false, // isSubLayout = false
tempLookAtHelper
);
const groupCenter = {x: groupCenterData.finalX, y: groupCenterData.finalY, z: groupCenterData.finalZ};
for (let k = 0; k < NESTED_SUB_ITEMS_PER_GROUP; k++) {
overallObjectIndex++;
const objectIndexForDefs = overallObjectIndex; // 1始まりのインデックス
const definition = OBJECT_DEFINITIONS[objectIndexForDefs] || {};
const objectType = definition.type || DEFAULT_OBJECT_TYPE;
let objectEl = document.createElement(objectType === 'sphere' ? 'a-sphere' : '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 ${objectIndexForDefs}: sizeType 'fixed' but fixedSize is invalid. Defaulting to random.`); }
dimension = Math.random() * 10.0 + 0.5;
}
const subPosData = calculateObjectFinalPositionAndRotation(
NESTED_SUB_PATTERN, NESTED_SUB_ITEMS_PER_GROUP, k,
groupCenter, // サブ配置の基準点は計算されたグループ中心
0, // サブ配置のspreadは通常0
definition.rotationSettings || {},
true, // isSubLayout = true
tempLookAtHelper
);
const finalX = subPosData.finalX; const finalY = subPosData.finalY; const finalZ = subPosData.finalZ;
const initialRotation = subPosData.initialRotation; const applyRandomRotationComponent = subPosData.applyRandomRotationComponent;
objectsFinalPositions[objectIndexForDefs] = {x: finalX, y: finalY, z: finalZ};
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 (!applyRandomRotationComponent && !(definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number'))) { objectEl.setAttribute('rotation', '0 0 0');}
objectEl.classList.add('clickableObject'); objectEl.dataset.objectIndex = objectIndexForDefs; objectEl.dataset.color = appliedColor; objectEl.dataset.dimension = dimension.toFixed(2); objectEl.dataset.objectType = objectType;
const hasSpecificRotation = definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number');
if (applyRandomRotationComponent || hasSpecificRotation) { let rotationParams = { maxSpeed: 5 }; if (definition.rotationSettings) { if (definition.rotationSettings.axis) { rotationParams.initialAxis = definition.rotationSettings.axis; } if (typeof definition.rotationSettings.speed === 'number') { rotationParams.initialSpeed = definition.rotationSettings.speed; } } objectEl.setAttribute('random-rotate', rotationParams); }
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'});
}
}
} else { // 従来の単一配置パターン
const singlePatternNumObjects = numObjects; // この変数は、単一パターンの時の総オブジェクト数
for (let i = 0; i < singlePatternNumObjects; i++) {
const objectIndex = i + 1;
const definition = OBJECT_DEFINITIONS[objectIndex] || {};
const objectType = definition.type || DEFAULT_OBJECT_TYPE;
let objectEl = document.createElement(objectType === 'sphere' ? 'a-sphere' : '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. Defaulting to random.`); } dimension = Math.random() * 10.0 + 0.5; }
const posData = calculateObjectFinalPositionAndRotation(
SINGLE_LAYOUT_PATTERN, singlePatternNumObjects, i,
{x: RIG_INITIAL_X, y: RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL, z: RIG_INITIAL_Z},
spread, definition.rotationSettings || {}, false, objectEl
);
const finalX = posData.finalX; const finalY = posData.finalY; const finalZ = posData.finalZ;
const initialRotation = posData.initialRotation; const applyRandomRotationComponent = posData.applyRandomRotationComponent;
objectsFinalPositions[objectIndex] = {x: finalX, y: finalY, z: finalZ};
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 (!applyRandomRotationComponent && !(definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number'))) { 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;
const hasSpecificRotation = definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number');
if (applyRandomRotationComponent || hasSpecificRotation) { let rotationParams = { maxSpeed: 5 }; if (definition.rotationSettings) { if (definition.rotationSettings.axis) { rotationParams.initialAxis = definition.rotationSettings.axis; } if (typeof definition.rotationSettings.speed === 'number') { rotationParams.initialSpeed = definition.rotationSettings.speed; } } objectEl.setAttribute('random-rotate', rotationParams); }
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'});
}
}
// hosiパラメータが指定されていた場合のリグの向きの最終調整
if (hosiTargetObjectOverallIndex !== null && objectsFinalPositions[hosiTargetObjectOverallIndex]) {
const targetObjectPos = objectsFinalPositions[hosiTargetObjectOverallIndex];
console.log(`Hosi target found for initial rig lookAt:`, targetObjectPos);
const lookAtPos = new THREE.Vector3(targetObjectPos.x, RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL, targetObjectPos.z);
rigEl.object3D.lookAt(lookAtPos);
const currentRigRotationEuler = new THREE.Euler().setFromQuaternion(rigEl.object3D.quaternion, 'YXZ');
rigEl.setAttribute('rotation', `0 ${THREE.MathUtils.radToDeg(currentRigRotationEuler.y)} 0`);
console.log("Rig initial rotation set by hosi parameter.");
}
initializeEventListeners();
}
function updatePanelDisplay() { if (!infoPanelEl.dataset.objectIndex) return; const index = parseInt(infoPanelEl.dataset.objectIndex || '0', 10); const objectBaseColor = 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 || [DEFAULT_COMMENT_ARRAY_INFO.text[0]]; const mainCommentTextColorForThisObject = 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']) }; panelTextEl.setAttribute('visible', false); panelTextEl.setAttribute('troika-text', 'value', ''); panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src'); linkButtons.forEach(btn => btn.setAttribute('visible', false)); const currentCommentItem = commentsArray[commentSubPageIndex]; if (EXE_MODE === 0) { if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') { const imgY = typeof currentCommentItem.imageY === 'number' ? currentCommentItem.imageY : DEFAULT_PANEL_IMAGE_Y; panelImageEl.object3D.position.set(0, imgY, 0.06); panelImageEl.setAttribute('src', currentCommentItem.src); panelImageEl.setAttribute('width', currentCommentItem.width || 40); const imgHeight = currentCommentItem.height; if (imgHeight && imgHeight !== 'auto') { panelImageEl.setAttribute('height', imgHeight); } else { panelImageEl.removeAttribute('height'); } panelImageEl.setAttribute('visible', true); displayText = currentCommentItem.caption || ''; if (displayText) { const capY = typeof currentCommentItem.captionY === 'number' ? currentCommentItem.captionY : DEFAULT_CAPTION_Y; panelTextEl.object3D.position.set(0, capY, 0.05); currentStyle = {...(PAGE_TEXT_STYLES['image_caption'] || PAGE_TEXT_STYLES['comment'])}; if (mainCommentTextColorForThisObject) currentStyle.color = mainCommentTextColorForThisObject; panelTextEl.setAttribute('visible', true); } } else { displayText = typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0]; panelTextEl.object3D.position.set(0, DEFAULT_PANEL_TEXT_Y, 0.05); if (mainCommentTextColorForThisObject) currentStyle.color = mainCommentTextColorForThisObject; panelTextEl.setAttribute('visible', true); } if (commentSubPageCount > 1) { pageIndicator = `(${commentSubPageIndex + 1}/${commentSubPageCount})`; } else { pageIndicator = ''; } } else { pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`; panelTextEl.setAttribute('visible', true); panelTextEl.object3D.position.set(0, DEFAULT_PANEL_TEXT_Y, 0.05); if (pageType === 'index') { displayText = `${objectType === 'sphere' ? '球' : '立方体'}: ${index}`; } else if (pageType === 'color') { displayText = `色: ${objectBaseColor}`; } else if (pageType === 'size') { displayText = `${objectType === 'sphere' ? '半径' : 'サイズ'}: ${dimensionValue}`; } else if (pageType === 'comment') { pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`; if (mainCommentTextColorForThisObject) { currentStyle.color = mainCommentTextColorForThisObject; } if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') { const imgY = typeof currentCommentItem.imageY === 'number' ? currentCommentItem.imageY : DEFAULT_PANEL_IMAGE_Y; panelImageEl.object3D.position.set(0, imgY, 0.06); panelImageEl.setAttribute('src', currentCommentItem.src); panelImageEl.setAttribute('width', currentCommentItem.width || 40); const imgHeight = currentCommentItem.height; if (imgHeight && imgHeight !== 'auto') { panelImageEl.setAttribute('height', imgHeight); } else { panelImageEl.removeAttribute('height'); } panelImageEl.setAttribute('visible', true); displayText = `コメント:\n${currentCommentItem.caption || ''}`; const capY = typeof currentCommentItem.captionY === 'number' ? currentCommentItem.captionY : DEFAULT_CAPTION_Y; panelTextEl.object3D.position.set(0, capY, 0.05); } else { displayText = `コメント:\n${typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0]}`; } } } 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}`; if (panelTextEl.getAttribute('visible')) { 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 }); } else if (EXE_MODE === 0 && pageIndicator && !(typeof currentCommentItem === 'object' && currentCommentItem.type === 'image' && !currentCommentItem.caption)) { panelTextEl.setAttribute('troika-text', { value: pageIndicator, color: PAGE_TEXT_STYLES['image_caption'].color, fontSize: PAGE_TEXT_STYLES['image_caption'].fontSize, maxWidth: currentStyle.maxWidth, align: currentStyle.align, anchorX: currentStyle.anchorX, anchorY: currentStyle.anchorY, baseline: currentStyle.baseline }); panelTextEl.setAttribute('visible', true); } }
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 ? commentDataForThisObject.text.length : 1).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 currentMainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let currentCommentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const currentObjectCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; const currentCommentSubPageCount = currentObjectCommentData.text.length; if (EXE_MODE === 0) { currentCommentSubPageIndex = (currentCommentSubPageIndex - 1 + currentCommentSubPageCount) % currentCommentSubPageCount; } else { if (PAGES[currentMainPageIndex] === 'comment' && currentCommentSubPageIndex > 0) { currentCommentSubPageIndex--; } else { currentMainPageIndex = (currentMainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; if (PAGES[currentMainPageIndex] === 'comment') { const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} currentCommentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } else { currentCommentSubPageIndex = 0; infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } } infoPanelEl.dataset.currentPageIndex = currentMainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = currentCommentSubPageIndex.toString(); updatePanelDisplay(); console.log("Prev button: mainPage=", currentMainPageIndex, "subPage=", currentCommentSubPageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let currentMainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let currentCommentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const currentObjectCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; const currentCommentSubPageCount = currentObjectCommentData.text.length; if (EXE_MODE === 0) { currentCommentSubPageIndex = (currentCommentSubPageIndex + 1) % currentCommentSubPageCount; } else { if (PAGES[currentMainPageIndex] === 'comment' && currentCommentSubPageIndex < currentCommentSubPageCount - 1) { currentCommentSubPageIndex++; } else { currentMainPageIndex = (currentMainPageIndex + 1) % TOTAL_MAIN_PAGES; currentCommentSubPageIndex = 0; if (PAGES[currentMainPageIndex] === 'comment') { const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} } else {infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } } infoPanelEl.dataset.currentPageIndex = currentMainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = currentCommentSubPageIndex.toString(); updatePanelDisplay(); console.log("Next button: mainPage=", currentMainPageIndex, "subPage=", currentCommentSubPageIndex); });
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)); panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src'); 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); }
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'); });
document.addEventListener('DOMContentLoaded', function () {const scene = document.querySelector('a-scene'); if (scene) { if (scene.hasLoaded) {initializeSceneAndObjects();} else {scene.addEventListener('loaded', initializeSceneAndObjects, {once: true});}} else {console.error("a-scene element not found at DOMContentLoaded!");}});
function initializeEventListeners() { console.log("Initializing event listeners for buttons and controllers."); 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."); } }
// --- End of SCRIPT BLOCK 2 ---
</script>
</a-scene>
</body>
</html>
a-farme-球に文字B-test67
├
<!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>
// --- SCRIPT BLOCK 1: Component Definitions ---
// --- プレイヤー移動制御用カスタムコンポーネント ('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; 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) { const verticalMovement = this.rightThumbstickInput.y * data.verticalSpeed * dt; this.rigEl.object3D.position.y -= verticalMovement; } } const position = this.rigEl.object3D.position; const cameraObject = this.cameraEl.object3D; 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 }, initialAxis: { type: 'vec3', default: null }, initialSpeed: { type: 'number', default: NaN } }, init: function () { const axisIsDefined = this.data.initialAxis && typeof this.data.initialAxis.x === 'number' && typeof this.data.initialAxis.y === 'number' && typeof this.data.initialAxis.z === 'number'; if (axisIsDefined && !isNaN(this.data.initialSpeed)) { this.axis = new THREE.Vector3(this.data.initialAxis.x, this.data.initialAxis.y, this.data.initialAxis.z); if (this.axis.lengthSq() === 0) { this.axis.set(0,1,0); } else { this.axis.normalize(); } this.speedRad = THREE.MathUtils.degToRad(this.data.initialSpeed); } else { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5); if (this.axis.lengthSq() < 0.001) { this.axis.set(0.01, 1, 0.01); } this.axis.normalize(); const speedSource = !isNaN(this.data.initialSpeed) ? this.data.initialSpeed : (((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1)); this.speedRad = THREE.MathUtils.degToRad(speedSource); } this.deltaRotationQuaternion = new THREE.Quaternion(); }, tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angleChangeRad = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angleChangeRad); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); this.el.object3D.quaternion.normalize(); } });
// --- 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" 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-image id="panelImage" src="" visible="false" width="40" height="18" position="0 0 0.06" material="shader: flat;"></a-image> <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 panelImageEl = document.getElementById('panelImage');
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 rigEl = document.getElementById('rig');
const EXE_MODE = 0;
const SKY_BACKGROUND_IMAGE_PATH = "./pic/u5.jpg";
let RIG_INITIAL_X = 0; let RIG_INITIAL_Y = 0; let RIG_INITIAL_Z = 5;
const RIG_INITIAL_Y_CAMERA_LEVEL = 1.6;
let SPAWN_ANIM_START_POS_STRING = `${RIG_INITIAL_X} ${RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL} ${RIG_INITIAL_Z}`;
const PANEL_OFFSET_FROM_OBJECT = 5.0;
const DEFAULT_PANEL_TEXT_Y = 0; const DEFAULT_PANEL_IMAGE_Y = -3; const DEFAULT_CAPTION_Y = 8;
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' }, 'image_caption': { ...BASE_TEXT_STYLE, fontSize: 0.50, color: '#E0E0E0', align: 'center', anchorY: 'top', baseline:'top' } };
const OBJECT_DEFINITIONS = { 1: { type: 'sphere', useTextureForIndex: 1, sizeType: 'fixed', fixedSize: 3.0, rotationSettings: { axis: { x: 0, y: 1, z: 0 }, speed: 15 } }, 2: { type: 'box', useTextureForIndex: 2, 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" }, 2: { text: ["オブジェクト2のコメント。\n改行もできます。"] }, 3: { text: ["オブジェクト3にはリンクがあります。"], links: [{label:"A-Frame Site", url:"https://aframe.io", buttonColor:"green"}] } };
const DEFAULT_COMMENT_ARRAY_INFO = { text: ["コメントはありません"], links: [] };
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();
// --- 配置パターン用 定数 ---
let LAYOUT_PATTERN = 5; // ★★★ 初期値を5に変更 (ご要望) ★★★
// パターン1,2 (ランダム) 用
let spread = 300;
// パターン3,4 (水平ライン) 用
let H_LINE_SPACING = 15; let H_LINE_Y = RIG_INITIAL_Y_CAMERA_LEVEL; let H_LINE_Z_OFFSET = -35;
// パターン5,6,7,8 (水平リング/半リング) 用
let H_RING_RADIUS = 50; let H_RING_OBJECT_Y = RIG_INITIAL_Y_CAMERA_LEVEL;
let HORIZONTAL_RING_LOOK_AT_TARGET = new THREE.Vector3(RIG_INITIAL_X, H_RING_OBJECT_Y, RIG_INITIAL_Z);
const H_RING_ANGLE_STEP_DEGREES = 0;
// パターン9,10 (グリッド) 用
let GRID_COLUMNS = 4; // ★★★ デフォルト4列 ★★★
let GRID_SPACING_X = 15; let GRID_SPACING_Y = 15;
let GRID_INITIAL_Y = RIG_INITIAL_Y_CAMERA_LEVEL + 10; let GRID_Z_DEPTH = H_LINE_Z_OFFSET;
// パターン11,12 (垂直ライン) 用
let V_LINE_SPACING = 15; const V_LINE_X_OFFSET = 0;
let V_LINE_START_Y_OFFSET_FROM_CENTER; // numObjects決定後に計算
let V_LINE_Z_OFFSET = -20;
// パターン13,14,15,16 (垂直リング/半リング) 用
let V_RING_RADIUS = 50; let V_RING_CENTER_Y = RIG_INITIAL_Y_CAMERA_LEVEL;
let V_RING_FIXED_Z_OFFSET = -25;
let VERTICAL_RING_LOOK_AT_TARGET = new THREE.Vector3(RIG_INITIAL_X, V_RING_CENTER_Y, RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET);
const V_RING_ANGLE_STEP_DEGREES = 0;
let numObjects = 10; // デフォルトオブジェクト数 (ネストの場合は上書きされる)
const objectsFinalPositions = [];
// --- ★★★ ネスト配置用設定 ★★★ ---
const USE_NESTED_LAYOUT = false; // trueでネスト配置、falseで従来の単一パターン
// ベースレイアウト (グループ中心の配置)
const NESTED_BASE_PATTERN = 1; // 例: 1=ランダム
const NESTED_NUM_GROUPS = 3;
const NESTED_GROUP_SPREAD = 200; // ベースがランダムの場合の広がり
// サブレイアウト (各グループ内のオブジェクト配置)
const NESTED_SUB_PATTERN = 9; // 例: 9=グリッド回転あり
const NESTED_SUB_GRID_COLUMNS = 3;
const NESTED_SUB_GRID_ROWS = 3;
const NESTED_SUB_ITEMS_PER_GROUP = NESTED_SUB_GRID_COLUMNS * NESTED_SUB_GRID_ROWS; // グループごとのアイテム数
const NESTED_SUB_GRID_SPACING_X = 10;
const NESTED_SUB_GRID_SPACING_Y = 10;
const NESTED_SUB_GRID_Z_DEPTH_RELATIVE = 0; // グループ中心からの相対Z
const NESTED_SUB_GRID_Y_LEVEL_RELATIVE = 0; // グループ中心からの相対Y
// --- シーン初期化とオブジェクト生成を行うメイン関数 ---
function initializeSceneAndObjects() {
console.log("initializeSceneAndObjects: Started.");
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 params = new URLSearchParams(window.location.search);
console.log("URL Parameters:", window.location.search);
const jxParam = params.get('jx'); const jyParam = params.get('jy'); const jzParam = params.get('jz'); const hosiParam = params.get('hosi'); const layoutParam = params.get('layout');
if (layoutParam !== null && !isNaN(parseInt(layoutParam))) { LAYOUT_PATTERN = parseInt(layoutParam, 10); }
console.log(`Using LAYOUT_PATTERN: ${LAYOUT_PATTERN} (Nested Layout: ${USE_NESTED_LAYOUT})`);
let htmlRigPos = rigEl.getAttribute('position');
RIG_INITIAL_X = htmlRigPos.x; RIG_INITIAL_Y = htmlRigPos.y; RIG_INITIAL_Z = htmlRigPos.z;
// hosiが最優先、次にjx,jy,jz
let tempHosiTargetPos = null; // hosiが指定された場合の対象オブジェクトの「仮の」最終位置
if (hosiParam !== null) {
const targetHosiNum = parseInt(hosiParam, 10);
if (targetHosiNum >= 1 && targetHosiNum <= (USE_NESTED_LAYOUT ? NESTED_NUM_GROUPS * NESTED_SUB_ITEMS_PER_GROUP : numObjects)) {
console.log(`Hosi parameter found for object index: ${targetHosiNum}. jx,jy,jz will be ignored.`);
// hosi対象オブジェクトのワールド最終位置を計算するために、一時的にリグを(0,0,5)と仮定して計算。
// この計算は、実際のオブジェクト生成ループの前に、対象オブジェクトの分だけ行う。
tempHosiTargetPos = calculateSpecificObjectFinalPosition(targetHosiNum -1, // 0-indexed
{x:0, y:0, z:5}, // 仮のリグ位置
1.6); // 仮のカメラYオフセット
if (tempHosiTargetPos) {
RIG_INITIAL_X = tempHosiTargetPos.x;
RIG_INITIAL_Y = 0; // リグのYは地面レベル
RIG_INITIAL_Z = tempHosiTargetPos.z + 8; // 少し手前
} else {
console.warn(`Could not calculate position for hosi target ${targetHosiNum}. Using jx,jy,jz or default.`);
if (jxParam !== null && jyParam !== null && jzParam !== null) { RIG_INITIAL_X = parseFloat(jxParam); RIG_INITIAL_Y = parseFloat(jyParam); RIG_INITIAL_Z = parseFloat(jzParam); }
}
} else {
console.warn(`Invalid hosi index: ${targetHosiNum}. Using jx,jy,jz or default.`);
if (jxParam !== null && jyParam !== null && jzParam !== null) { RIG_INITIAL_X = parseFloat(jxParam); RIG_INITIAL_Y = parseFloat(jyParam); RIG_INITIAL_Z = parseFloat(jzParam); }
}
} else if (jxParam !== null && jyParam !== null && jzParam !== null) {
RIG_INITIAL_X = parseFloat(jxParam); RIG_INITIAL_Y = parseFloat(jyParam); RIG_INITIAL_Z = parseFloat(jzParam);
}
rigEl.setAttribute('position', `${RIG_INITIAL_X} ${RIG_INITIAL_Y} ${RIG_INITIAL_Z}`);
SPAWN_ANIM_START_POS_STRING = `${RIG_INITIAL_X} ${RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL} ${RIG_INITIAL_Z}`;
H_LINE_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL; H_RING_OBJECT_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL;
HORIZONTAL_RING_LOOK_AT_TARGET.set(RIG_INITIAL_X, H_RING_OBJECT_Y, RIG_INITIAL_Z);
GRID_INITIAL_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL + 10;
V_RING_CENTER_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL;
VERTICAL_RING_LOOK_AT_TARGET.set(RIG_INITIAL_X, V_RING_CENTER_Y, RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET);
V_LINE_START_Y_OFFSET_FROM_CENTER = ( ((USE_NESTED_LAYOUT ? NESTED_SUB_ITEMS_PER_GROUP : numObjects)-1) * V_LINE_SPACING / 2 );
console.log(`Final Rig Initial Position set to: X=${RIG_INITIAL_X}, Y=${RIG_INITIAL_Y}, Z=${RIG_INITIAL_Z}`);
console.log(`Spawn Start Position String set to: ${SPAWN_ANIM_START_POS_STRING}`);
const totalObjectsToGenerate = USE_NESTED_LAYOUT ? (NESTED_NUM_GROUPS * NESTED_SUB_ITEMS_PER_GROUP) : numObjects;
let currentObjectGlobalIndex = 0;
if (USE_NESTED_LAYOUT) {
console.log(`Generating ${NESTED_NUM_GROUPS} groups with ${NESTED_SUB_ITEMS_PER_GROUP} objects each.`);
for (let g = 0; g < NESTED_NUM_GROUPS; g++) { // グループごとのループ (アウター)
let groupCenterX, groupCenterY, groupCenterZ;
// NESTED_BASE_PATTERN に基づいてグループ中心位置を計算
// (この部分は calculateObjectFinalPositionAndRotation を流用/適用)
const groupPosData = calculateObjectFinalPositionAndRotation(
NESTED_BASE_PATTERN,
NESTED_NUM_GROUPS, // グループ数をアイテム数とみなす
g, // 現在のグループインデックス
{x: RIG_INITIAL_X, y: RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL, z: RIG_INITIAL_Z}, // グループ配置の基準点
NESTED_GROUP_SPREAD, // グループ用の spread
{} // グループ用の特別な回転はないと仮定
);
groupCenterX = groupPosData.finalX;
groupCenterY = groupPosData.finalY;
groupCenterZ = groupPosData.finalZ;
for (let k = 0; k < NESTED_SUB_ITEMS_PER_GROUP; k++) { // グループ内のオブジェクトごとのループ (インナー)
currentObjectGlobalIndex++;
const objectIndex = currentObjectGlobalIndex; // 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. Defaulting to random.`); }
dimension = Math.random() * 10.0 + 0.5;
}
// NESTED_SUB_PATTERN に基づいてグループ中心からの相対位置を計算
const subPosData = calculateObjectFinalPositionAndRotation(
NESTED_SUB_PATTERN,
NESTED_SUB_ITEMS_PER_GROUP,
k, // サブグループ内のインデックス
{x: groupCenterX, y: groupCenterY, z: groupCenterZ}, // サブ配置の基準点はグループ中心
0, // サブ配置のspreadは0 (専用のspacing等で制御)
definition.rotationSettings || {}, // 個別回転設定を渡す
true // isSubLayout = true を渡して専用パラメータを使わせる
);
const finalX = subPosData.finalX;
const finalY = subPosData.finalY;
const finalZ = subPosData.finalZ;
const initialRotation = subPosData.initialRotation;
const applyRandomRotationComponent = subPosData.applyRandomRotationComponent;
objectsFinalPositions[objectIndex] = {x: finalX, y: finalY, z: finalZ};
// ... (残りのオブジェクト設定、スポーンアニメーションは共通)
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 (!applyRandomRotationComponent && !(definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number'))) { 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;
const hasSpecificRotation = definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number');
if (applyRandomRotationComponent || hasSpecificRotation) { let rotationParams = { maxSpeed: 5 }; if (definition.rotationSettings) { if (definition.rotationSettings.axis) { rotationParams.initialAxis = definition.rotationSettings.axis; } if (typeof definition.rotationSettings.speed === 'number') { rotationParams.initialSpeed = definition.rotationSettings.speed; } } objectEl.setAttribute('random-rotate', rotationParams); }
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'});
}
}
} else { // 従来の単一配置パターン
console.log(`Generating ${numObjects} objects with single layout pattern.`);
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. Defaulting to random.`); }
dimension = Math.random() * 10.0 + 0.5;
}
const posData = calculateObjectFinalPositionAndRotation(
LAYOUT_PATTERN,
numObjects,
i,
{x: RIG_INITIAL_X, y: RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL, z: RIG_INITIAL_Z},
spread, // spread はランダム系でのみ有効
definition.rotationSettings || {},
false, // isSubLayout = false
objectEl // lookAt 計算のために渡す
);
const finalX = posData.finalX;
const finalY = posData.finalY;
const finalZ = posData.finalZ;
const initialRotation = posData.initialRotation;
const applyRandomRotationComponent = posData.applyRandomRotationComponent;
objectsFinalPositions[objectIndex] = {x: finalX, y: finalY, z: finalZ};
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 (!applyRandomRotationComponent && !(definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number'))) { 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;
const hasSpecificRotation = definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number');
if (applyRandomRotationComponent || hasSpecificRotation) { let rotationParams = { maxSpeed: 5 }; if (definition.rotationSettings) { if (definition.rotationSettings.axis) { rotationParams.initialAxis = definition.rotationSettings.axis; } if (typeof definition.rotationSettings.speed === 'number') { rotationParams.initialSpeed = definition.rotationSettings.speed; } } objectEl.setAttribute('random-rotate', rotationParams); }
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'});
}
}
// hosiパラメータが指定されていた場合のrigの「初期」位置と向きの最終調整
// (この処理は全てのオブジェクトの最終位置が計算された後、かつスポーンアニメーション開始前に行うのが理想)
// 今回は、オブジェクト生成後にリグを動かす形は取らないため、
// hosi指定時のリグ位置は、オブジェクト生成ループの前にRIG_INITIAL_X,Y,Zに反映済み。
// ただし、lookAtはオブジェクトの最終位置に対して行うので、ここで再計算が必要。
const finalHosiParam = params.get('hosi'); // URLから再度取得
if (finalHosiParam !== null) {
const targetHosiNum = parseInt(finalHosiParam, 10);
const effectiveNumObjects = USE_NESTED_LAYOUT ? (NESTED_NUM_GROUPS * NESTED_SUB_ITEMS_PER_GROUP) : numObjects;
if (targetHosiNum >= 1 && targetHosiNum <= effectiveNumObjects && objectsFinalPositions[targetHosiNum]) {
const targetObjectPos = objectsFinalPositions[targetHosiNum];
console.log(`Hosi target found for initial rig lookAt:`, targetObjectPos);
// リグがオブジェクトの方を向くようにする(カメラの高さで)
const lookAtPos = new THREE.Vector3(targetObjectPos.x, RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL, targetObjectPos.z);
rigEl.object3D.lookAt(lookAtPos);
const currentRigRotationEuler = new THREE.Euler().setFromQuaternion(rigEl.object3D.quaternion, 'YXZ');
rigEl.setAttribute('rotation', `0 ${THREE.MathUtils.radToDeg(currentRigRotationEuler.y)} 0`);
console.log("Rig initial rotation set by hosi parameter.");
}
}
initializeEventListeners();
} // End of initializeSceneAndObjects
// ★★★ オブジェクトの最終位置と回転を計算するヘルパー関数 ★★★
function calculateObjectFinalPositionAndRotation(pattern, numItems, itemIndex, centerPos, spreadForRandom, individualRotationSettings, isSubLayout, tempObjectElForLookAt) {
let finalX, finalY, finalZ;
let applyRandomRotation = false;
let initialRotation = null;
const localSpread = isSubLayout ? 0 : spreadForRandom; // サブレイアウトは通常spreadを使わない
// サブレイアウト用のパラメータ (isSubLayoutがtrueの時)
const subGridCols = NESTED_SUB_GRID_COLUMNS;
const subGridSpacingX = NESTED_SUB_GRID_SPACING_X;
const subGridSpacingY = NESTED_SUB_GRID_SPACING_Y;
const subGridInitialY = centerPos.y + NESTED_SUB_GRID_Y_LEVEL_RELATIVE; // グループ中心からの相対Y
const subGridZDepth = centerPos.z + NESTED_SUB_GRID_Z_DEPTH_RELATIVE; // グループ中心からの相対Z
// 注意: パターンごとの定数も、isSubLayoutに応じて専用のものを使うようにする必要があるが、
// 今回はまず、ユーザー指定のサブパターン9 (グリッド) のケースに絞って調整する。
// 他のサブパターンを汎用的に扱うには、この関数へのパラメータ渡しをさらに工夫する必要がある。
switch (pattern) {
case 1: finalX = (Math.random() - 0.5) * localSpread + centerPos.x; finalY = (Math.random() - 0.5) * localSpread + centerPos.y; finalZ = (Math.random() - 0.5) * localSpread + centerPos.z; applyRandomRotation = true; break;
case 2: finalX = (Math.random() - 0.5) * localSpread + centerPos.x; finalY = (Math.random() - 0.5) * localSpread + centerPos.y; finalZ = (Math.random() - 0.5) * localSpread + centerPos.z; applyRandomRotation = false; break;
case 3: const totalLineWidthH_3 = (numItems - 1) * H_LINE_SPACING; finalX = (-totalLineWidthH_3 / 2) + (itemIndex * H_LINE_SPACING) + centerPos.x; finalY = H_LINE_Y; finalZ = centerPos.z + H_LINE_Z_OFFSET; applyRandomRotation = true; break; // H_LINE_Y はワールドY, Zは中心からのオフセット
case 4: const totalLineWidthV_4 = (numItems - 1) * H_LINE_SPACING; finalX = (-totalLineWidthV_4 / 2) + (itemIndex * H_LINE_SPACING) + centerPos.x; finalY = H_LINE_Y; finalZ = centerPos.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) / numItems; } const angle360H = itemIndex * angleStep360H; finalX = centerPos.x + H_RING_RADIUS * Math.cos(angle360H); finalY = H_RING_OBJECT_Y; finalZ = centerPos.z + H_RING_RADIUS * Math.sin(angle360H); if(tempObjectElForLookAt){ tempObjectElForLookAt.object3D.position.set(finalX, finalY, finalZ); tempObjectElForLookAt.object3D.lookAt(new THREE.Vector3(centerPos.x, H_RING_OBJECT_Y, centerPos.z)); initialRotation = { x: THREE.MathUtils.radToDeg(tempObjectElForLookAt.object3D.rotation.x), y: THREE.MathUtils.radToDeg(tempObjectElForLookAt.object3D.rotation.y), z: THREE.MathUtils.radToDeg(tempObjectElForLookAt.object3D.rotation.z) };} applyRandomRotation = (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_ = numItems > 1 ? totalAngleSpan180H / (numItems - 1) : 0; } const thetaH = startAngleOffset180H + (itemIndex * angleStep180H_); finalX = centerPos.x + H_RING_RADIUS * Math.sin(thetaH); finalY = H_RING_OBJECT_Y; finalZ = centerPos.z - H_RING_RADIUS * Math.cos(thetaH); if(tempObjectElForLookAt){ tempObjectElForLookAt.object3D.position.set(finalX, finalY, finalZ); tempObjectElForLookAt.object3D.lookAt(new THREE.Vector3(centerPos.x, H_RING_OBJECT_Y, centerPos.z)); initialRotation = { x: THREE.MathUtils.radToDeg(tempObjectElForLookAt.object3D.rotation.x), y: THREE.MathUtils.radToDeg(tempObjectElForLookAt.object3D.rotation.y), z: THREE.MathUtils.radToDeg(tempObjectElForLookAt.object3D.rotation.z) };} applyRandomRotation = (pattern === 7); break;
case 9: case 10: // グリッド (サブレイアウトでも使えるように調整)
const currentGridCols = isSubLayout ? NESTED_SUB_GRID_COLUMNS : GRID_COLUMNS;
const currentGridSpacingX = isSubLayout ? NESTED_SUB_GRID_SPACING_X : GRID_SPACING_X;
const currentGridSpacingY = isSubLayout ? NESTED_SUB_GRID_SPACING_Y : GRID_SPACING_Y;
const currentGridInitialY = isSubLayout ? subGridInitialY : GRID_INITIAL_Y;
const currentGridZ = isSubLayout ? subGridZDepth : centerPos.z + GRID_Z_DEPTH;
const column = itemIndex % currentGridCols;
const row = Math.floor(itemIndex / currentGridCols);
const totalGridWidth = (currentGridCols - 1) * currentGridSpacingX;
const startX_g = -totalGridWidth / 2;
finalX = startX_g + column * currentGridSpacingX + (isSubLayout ? centerPos.x : RIG_INITIAL_X);
finalY = currentGridInitialY - row * currentGridSpacingY;
finalZ = currentGridZ;
initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (pattern === 9);
break;
case 11: case 12: finalX = V_LINE_X_OFFSET + centerPos.x; finalY = (centerPos.y + V_LINE_START_Y_OFFSET_FROM_CENTER) - (itemIndex * V_LINE_SPACING); finalZ = centerPos.z + V_LINE_Z_OFFSET; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = (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) / numItems; } const angle360V = itemIndex * angleStep360V; finalX = centerPos.x; finalY = (centerPos.y) + V_RING_RADIUS * Math.cos(angle360V); finalZ = (centerPos.z + V_RING_FIXED_Z_OFFSET) + V_RING_RADIUS * Math.sin(angle360V); if(tempObjectElForLookAt){ tempObjectElForLookAt.object3D.position.set(finalX, finalY, finalZ); tempObjectElForLookAt.object3D.lookAt(new THREE.Vector3(centerPos.x, centerPos.y, centerPos.z + V_RING_FIXED_Z_OFFSET)); initialRotation = { x: THREE.MathUtils.radToDeg(tempObjectElForLookAt.object3D.rotation.x), y: THREE.MathUtils.radToDeg(tempObjectElForLookAt.object3D.rotation.y), z: THREE.MathUtils.radToDeg(tempObjectElForLookAt.object3D.rotation.z) };} applyRandomRotation = (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_ = numItems > 1 ? arcSpan180V / (numItems - 1) : 0; } const thetaV = startAngle180V + (itemIndex * angleStep180V_); finalX = centerPos.x; finalY = (centerPos.y) + V_RING_RADIUS * Math.sin(thetaV); finalZ = (centerPos.z + V_RING_FIXED_Z_OFFSET) - V_RING_RADIUS * Math.cos(thetaV); if(tempObjectElForLookAt){ tempObjectElForLookAt.object3D.position.set(finalX, finalY, finalZ); tempObjectElForLookAt.object3D.lookAt(new THREE.Vector3(centerPos.x, centerPos.y, centerPos.z + V_RING_FIXED_Z_OFFSET)); initialRotation = { x: THREE.MathUtils.radToDeg(tempObjectElForLookAt.object3D.rotation.x), y: THREE.MathUtils.radToDeg(tempObjectElForLookAt.object3D.rotation.y), z: THREE.MathUtils.radToDeg(tempObjectElForLookAt.object3D.rotation.z) };} applyRandomRotation = (pattern === 15); break;
default: console.warn(`calculateObjectFinalPositionAndRotation: Unknown pattern ${pattern}`); finalX = centerPos.x; finalY = centerPos.y; finalZ = centerPos.z; applyRandomRotation = true;
}
return { finalX, finalY, finalZ, initialRotation, applyRandomRotationComponent: applyRandomRotation };
}
function updatePanelDisplay() { /* (省略しない完全なコード) */ if (!infoPanelEl.dataset.objectIndex) return; const index = parseInt(infoPanelEl.dataset.objectIndex || '0', 10); const objectBaseColor = 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 || [DEFAULT_COMMENT_ARRAY_INFO.text[0]]; const mainCommentTextColorForThisObject = 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']) }; panelTextEl.setAttribute('visible', false); panelTextEl.setAttribute('troika-text', 'value', ''); panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src'); linkButtons.forEach(btn => btn.setAttribute('visible', false)); const currentCommentItem = commentsArray[commentSubPageIndex]; if (EXE_MODE === 0) { if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') { const imgY = typeof currentCommentItem.imageY === 'number' ? currentCommentItem.imageY : DEFAULT_PANEL_IMAGE_Y; panelImageEl.object3D.position.set(0, imgY, 0.06); panelImageEl.setAttribute('src', currentCommentItem.src); panelImageEl.setAttribute('width', currentCommentItem.width || 40); const imgHeight = currentCommentItem.height; if (imgHeight && imgHeight !== 'auto') { panelImageEl.setAttribute('height', imgHeight); } else { panelImageEl.removeAttribute('height'); } panelImageEl.setAttribute('visible', true); displayText = currentCommentItem.caption || ''; if (displayText) { const capY = typeof currentCommentItem.captionY === 'number' ? currentCommentItem.captionY : DEFAULT_CAPTION_Y; panelTextEl.object3D.position.set(0, capY, 0.05); currentStyle = {...(PAGE_TEXT_STYLES['image_caption'] || PAGE_TEXT_STYLES['comment'])}; if (mainCommentTextColorForThisObject) currentStyle.color = mainCommentTextColorForThisObject; panelTextEl.setAttribute('visible', true); } } else { displayText = typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0]; panelTextEl.object3D.position.set(0, DEFAULT_PANEL_TEXT_Y, 0.05); if (mainCommentTextColorForThisObject) currentStyle.color = mainCommentTextColorForThisObject; panelTextEl.setAttribute('visible', true); } if (commentSubPageCount > 1) { pageIndicator = `(${commentSubPageIndex + 1}/${commentSubPageCount})`; } else { pageIndicator = ''; } } else { pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`; panelTextEl.setAttribute('visible', true); panelTextEl.object3D.position.set(0, DEFAULT_PANEL_TEXT_Y, 0.05); if (pageType === 'index') { displayText = `${objectType === 'sphere' ? '球' : '立方体'}: ${index}`; } else if (pageType === 'color') { displayText = `色: ${objectBaseColor}`; } else if (pageType === 'size') { displayText = `${objectType === 'sphere' ? '半径' : 'サイズ'}: ${dimensionValue}`; } else if (pageType === 'comment') { pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`; if (mainCommentTextColorForThisObject) { currentStyle.color = mainCommentTextColorForThisObject; } if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') { const imgY = typeof currentCommentItem.imageY === 'number' ? currentCommentItem.imageY : DEFAULT_PANEL_IMAGE_Y; panelImageEl.object3D.position.set(0, imgY, 0.06); panelImageEl.setAttribute('src', currentCommentItem.src); panelImageEl.setAttribute('width', currentCommentItem.width || 40); const imgHeight = currentCommentItem.height; if (imgHeight && imgHeight !== 'auto') { panelImageEl.setAttribute('height', imgHeight); } else { panelImageEl.removeAttribute('height'); } panelImageEl.setAttribute('visible', true); displayText = `コメント:\n${currentCommentItem.caption || ''}`; const capY = typeof currentCommentItem.captionY === 'number' ? currentCommentItem.captionY : DEFAULT_CAPTION_Y; panelTextEl.object3D.position.set(0, capY, 0.05); } else { displayText = `コメント:\n${typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0]}`; } } } 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}`; if (panelTextEl.getAttribute('visible')) { 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 }); } else if (EXE_MODE === 0 && pageIndicator && !(typeof currentCommentItem === 'object' && currentCommentItem.type === 'image' && !currentCommentItem.caption)) { panelTextEl.setAttribute('troika-text', { value: pageIndicator, color: PAGE_TEXT_STYLES['image_caption'].color, fontSize: PAGE_TEXT_STYLES['image_caption'].fontSize, maxWidth: currentStyle.maxWidth, align: currentStyle.align, anchorX: currentStyle.anchorX, anchorY: currentStyle.anchorY, baseline: currentStyle.baseline }); panelTextEl.setAttribute('visible', true); } }
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 ? commentDataForThisObject.text.length : 1).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 currentMainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let currentCommentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const currentObjectCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; const currentCommentSubPageCount = currentObjectCommentData.text.length; if (EXE_MODE === 0) { currentCommentSubPageIndex = (currentCommentSubPageIndex - 1 + currentCommentSubPageCount) % currentCommentSubPageCount; } else { if (PAGES[currentMainPageIndex] === 'comment' && currentCommentSubPageIndex > 0) { currentCommentSubPageIndex--; } else { currentMainPageIndex = (currentMainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; if (PAGES[currentMainPageIndex] === 'comment') { const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} currentCommentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } else { currentCommentSubPageIndex = 0; infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } } infoPanelEl.dataset.currentPageIndex = currentMainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = currentCommentSubPageIndex.toString(); updatePanelDisplay(); console.log("Prev button: mainPage=", currentMainPageIndex, "subPage=", currentCommentSubPageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let currentMainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let currentCommentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const currentObjectCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; const currentCommentSubPageCount = currentObjectCommentData.text.length; if (EXE_MODE === 0) { currentCommentSubPageIndex = (currentCommentSubPageIndex + 1) % currentCommentSubPageCount; } else { if (PAGES[currentMainPageIndex] === 'comment' && currentCommentSubPageIndex < currentCommentSubPageCount - 1) { currentCommentSubPageIndex++; } else { currentMainPageIndex = (currentMainPageIndex + 1) % TOTAL_MAIN_PAGES; currentCommentSubPageIndex = 0; if (PAGES[currentMainPageIndex] === 'comment') { const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} } else {infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } } infoPanelEl.dataset.currentPageIndex = currentMainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = currentCommentSubPageIndex.toString(); updatePanelDisplay(); console.log("Next button: mainPage=", currentMainPageIndex, "subPage=", currentCommentSubPageIndex); });
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)); panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src'); 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); }
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'); });
document.addEventListener('DOMContentLoaded', function () {const scene = document.querySelector('a-scene'); if (scene) { if (scene.hasLoaded) {initializeSceneAndObjects();} else {scene.addEventListener('loaded', initializeSceneAndObjects, {once: true});}} else {console.error("a-scene element not found at DOMContentLoaded!");}});
function initializeEventListeners() { console.log("Initializing event listeners for buttons and controllers."); 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."); } }
// --- End of SCRIPT BLOCK 2 ---
</script>
</a-scene>
</body>
</html>
a-farme-球に文字B-test66
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - URL初期位置設定 (完全版・省略なし)</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 ---
// --- プレイヤー移動制御用カスタムコンポーネント ('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; 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 }, initialAxis: { type: 'vec3', default: null }, initialSpeed: { type: 'number', default: NaN } },
init: function () { const axisIsDefined = this.data.initialAxis && typeof this.data.initialAxis.x === 'number' && typeof this.data.initialAxis.y === 'number' && typeof this.data.initialAxis.z === 'number'; if (axisIsDefined && !isNaN(this.data.initialSpeed)) { this.axis = new THREE.Vector3(this.data.initialAxis.x, this.data.initialAxis.y, this.data.initialAxis.z); if (this.axis.lengthSq() === 0) { this.axis.set(0,1,0); } else { this.axis.normalize(); } this.speedRad = THREE.MathUtils.degToRad(this.data.initialSpeed); } else { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5); if (this.axis.lengthSq() < 0.001) { this.axis.set(0.01, 1, 0.01); } this.axis.normalize(); const speedSource = !isNaN(this.data.initialSpeed) ? this.data.initialSpeed : (((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1)); this.speedRad = THREE.MathUtils.degToRad(speedSource); } this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angleChangeRad = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angleChangeRad); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); this.el.object3D.quaternion.normalize(); }
});
// --- 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; 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-image id="panelImage" src="" visible="false" width="40" height="18" position="0 0 0.06" material="shader: flat;"></a-image> <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 panelImageEl = document.getElementById('panelImage');
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 rigEl = document.getElementById('rig');
const EXE_MODE = 0;
const SKY_BACKGROUND_IMAGE_PATH = "./pic/u5.jpg";
let RIG_INITIAL_X = 0;
let RIG_INITIAL_Y = 0;
let RIG_INITIAL_Z = 5;
const RIG_INITIAL_Y_CAMERA_LEVEL = 1.6;
let SPAWN_ANIM_START_POS_STRING = `${RIG_INITIAL_X} ${RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL} ${RIG_INITIAL_Z}`;
const PANEL_OFFSET_FROM_OBJECT = 5.0;
const DEFAULT_PANEL_TEXT_Y = 0; const DEFAULT_PANEL_IMAGE_Y = -3; const DEFAULT_CAPTION_Y = 8;
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' }, 'image_caption': { ...BASE_TEXT_STYLE, fontSize: 0.50, color: '#E0E0E0', align: 'center', anchorY: 'top', baseline:'top' } };
const OBJECT_DEFINITIONS = { 1: { type: 'sphere', useTextureForIndex: 1, sizeType: 'fixed', fixedSize: 3.0, rotationSettings: { axis: { x: 0, y: 1, z: 0 }, speed: 15 } }, 2: { type: 'box', useTextureForIndex: 2, 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" }, 2: { text: ["オブジェクト2のコメント。\n改行もできます。"] }, 3: { text: ["オブジェクト3にはリンクがあります。"], links: [{label:"A-Frame Site", url:"https://aframe.io", buttonColor:"green"}] } };
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();
// ★★★ 配置パターン初期値を1に変更 ★★★
let LAYOUT_PATTERN = 1;
let H_LINE_SPACING = 15; let H_LINE_Y = RIG_INITIAL_Y_CAMERA_LEVEL; let H_LINE_Z_OFFSET = -35;
let H_RING_RADIUS = 50; let H_RING_OBJECT_Y = RIG_INITIAL_Y_CAMERA_LEVEL;
let 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;
let GRID_INITIAL_Y = RIG_INITIAL_Y_CAMERA_LEVEL + 10; let GRID_Z_DEPTH = H_LINE_Z_OFFSET;
const V_LINE_SPACING = 15; const V_LINE_X_OFFSET = 0;
let V_LINE_START_Y_OFFSET_FROM_CENTER = ( (numObjects-1) * V_LINE_SPACING / 2 );
let V_LINE_Z_OFFSET = -20;
let V_RING_RADIUS = 50; let V_RING_CENTER_Y = RIG_INITIAL_Y_CAMERA_LEVEL;
let V_RING_FIXED_Z_OFFSET = -25;
let 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;
const objectsFinalPositions = [];
function initializeSceneAndObjects() {
console.log("initializeSceneAndObjects: Started.");
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 params = new URLSearchParams(window.location.search);
console.log("URL Parameters:", window.location.search);
const jxParam = params.get('jx');
const jyParam = params.get('jy');
const jzParam = params.get('jz');
const hosiParam = params.get('hosi');
const layoutParam = params.get('layout');
if (layoutParam !== null && !isNaN(parseInt(layoutParam))) {
LAYOUT_PATTERN = parseInt(layoutParam, 10);
}
console.log(`Using LAYOUT_PATTERN: ${LAYOUT_PATTERN}`); // これは正しいタイミングでログ出力
// まず、hosiパラメータが指定されているかどうかでリグの初期位置の決定方法を変える
// hosiパラメータが指定されていれば、そのオブジェクトの位置を計算し、それを元にリグ位置を決める
// hosiパラメータがなければ、jx,jy,jzパラメータ、それもなければHTMLのデフォルトを使う
let tempTargetObjectPos = null; // hosi指定の場合の対象オブジェクトの仮の最終位置
if (hosiParam !== null) {
const targetHosiNum = parseInt(hosiParam, 10);
if (targetHosiNum >= 1 && targetHosiNum <= numObjects) {
console.log(`Hosi parameter found for object index: ${targetHosiNum}. jx,jy,jz will be ignored if present.`);
// hosi対象オブジェクトの最終位置を「仮の」リグ位置(0,0,5)を基準に計算する
// この計算は、オブジェクト生成ループ内のfinalX,Y,Z計算ロジックを流用する
let temp_finalX, temp_finalY, temp_finalZ;
const temp_i_for_hosi = targetHosiNum - 1; // 0-indexed
const temp_rig_x = 0, temp_rig_y_cam_level = 1.6, temp_rig_z = 5; // 仮のリグ位置
// --- ここにLAYOUT_PATTERNに応じたtemp_finalX,Y,Z計算を挿入 (オブジェクト生成ループから抜粋・ Anpassung) ---
// (この部分は非常に長くなるので、関数化するのが理想的だが、今回は直接記述を試みる)
// (注: この計算内で objectEl.object3D.lookAt を呼ぶのは objectEl が未生成なので不可。
// 代わりに lookAtTarget を使って向きを別途計算するか、lookAt しないパターンでは不要)
switch (LAYOUT_PATTERN) {
case 1: case 2: temp_finalX = (Math.random() - 0.5) * spread + temp_rig_x; temp_finalY = (Math.random() - 0.5) * spread + temp_rig_y_cam_level; temp_finalZ = (Math.random() - 0.5) * spread + temp_rig_z; break;
case 3: case 4: const temp_totalLineWidthH = (numObjects - 1) * H_LINE_SPACING; temp_finalX = (-temp_totalLineWidthH / 2) + (temp_i_for_hosi * H_LINE_SPACING) + temp_rig_x; temp_finalY = temp_rig_y_cam_level; temp_finalZ = temp_rig_z + H_LINE_Z_OFFSET; break; // H_LINE_Yはtemp_rig_y_cam_levelと解釈
case 5: case 6: let temp_angleStep360H; if (H_RING_ANGLE_STEP_DEGREES > 0) { temp_angleStep360H = THREE.MathUtils.degToRad(H_RING_ANGLE_STEP_DEGREES); } else { temp_angleStep360H = (Math.PI * 2) / numObjects; } const temp_angle360H = temp_i_for_hosi * temp_angleStep360H; temp_finalX = temp_rig_x + H_RING_RADIUS * Math.cos(temp_angle360H); temp_finalY = temp_rig_y_cam_level; temp_finalZ = temp_rig_z + H_RING_RADIUS * Math.sin(temp_angle360H); break; // H_RING_OBJECT_Y は temp_rig_y_cam_level と解釈
case 7: case 8: const temp_totalAngleSpan180H = Math.PI; const temp_startAngleOffset180H = -Math.PI / 2; let temp_angleStep180H_; if (H_RING_ANGLE_STEP_DEGREES > 0) { temp_angleStep180H_ = THREE.MathUtils.degToRad(H_RING_ANGLE_STEP_DEGREES); } else { temp_angleStep180H_ = numObjects > 1 ? temp_totalAngleSpan180H / (numObjects - 1) : 0; } const temp_thetaH = temp_startAngleOffset180H + (temp_i_for_hosi * temp_angleStep180H_); temp_finalX = temp_rig_x + H_RING_RADIUS * Math.sin(temp_thetaH); temp_finalY = temp_rig_y_cam_level; temp_finalZ = temp_rig_z - H_RING_RADIUS * Math.cos(temp_thetaH); break;
case 9: case 10: const temp_column = temp_i_for_hosi % GRID_COLUMNS; const temp_row = Math.floor(temp_i_for_hosi / GRID_COLUMNS); const temp_totalGridWidth = (GRID_COLUMNS - 1) * GRID_SPACING_X; const temp_startX_g = -temp_totalGridWidth / 2; temp_finalX = temp_startX_g + temp_column * GRID_SPACING_X + temp_rig_x; temp_finalY = (temp_rig_y_cam_level + 10) - temp_row * GRID_SPACING_Y; temp_finalZ = temp_rig_z + GRID_Z_DEPTH; break; // GRID_INITIAL_Y は temp_rig_y_cam_level + 10 と解釈
case 11: case 12: temp_finalX = V_LINE_X_OFFSET + temp_rig_x; temp_finalY = (temp_rig_y_cam_level + V_LINE_START_Y_OFFSET_FROM_CENTER) - (temp_i_for_hosi * V_LINE_SPACING); temp_finalZ = temp_rig_z + V_LINE_Z_OFFSET; break;
case 13: case 14: let temp_angleStep360V; if (V_RING_ANGLE_STEP_DEGREES > 0) { temp_angleStep360V = THREE.MathUtils.degToRad(V_RING_ANGLE_STEP_DEGREES); } else { temp_angleStep360V = (Math.PI * 2) / numObjects; } const temp_angle360V = temp_i_for_hosi * temp_angleStep360V; temp_finalX = temp_rig_x; temp_finalY = (temp_rig_y_cam_level) + V_RING_RADIUS * Math.cos(temp_angle360V); temp_finalZ = (temp_rig_z + V_RING_FIXED_Z_OFFSET) + V_RING_RADIUS * Math.sin(temp_angle360V); break; // V_RING_CENTER_Y は temp_rig_y_cam_level と解釈
case 15: case 16: const temp_arcSpan180V = Math.PI; const temp_startAngle180V = -Math.PI / 2; let temp_angleStep180V_; if (V_RING_ANGLE_STEP_DEGREES > 0) { temp_angleStep180V_ = THREE.MathUtils.degToRad(V_RING_ANGLE_STEP_DEGREES); } else { temp_angleStep180V_ = numObjects > 1 ? temp_arcSpan180V / (numObjects - 1) : 0; } const temp_thetaV = temp_startAngle180V + (temp_i_for_hosi * temp_angleStep180V_); temp_finalX = temp_rig_x; temp_finalY = (temp_rig_y_cam_level) + V_RING_RADIUS * Math.sin(temp_thetaV); temp_finalZ = (temp_rig_z + V_RING_FIXED_Z_OFFSET) - V_RING_RADIUS * Math.cos(temp_thetaV); break;
default: temp_finalX = (Math.random() - 0.5) * spread + temp_rig_x; temp_finalY = (Math.random() - 0.5) * spread + temp_rig_y_cam_level; temp_finalZ = (Math.random() - 0.5) * spread + temp_rig_z;
}
hosiTargetObjectPos = {x: temp_finalX, y: temp_finalY, z: temp_finalZ};
// hosi対象オブジェクトの位置を元にリグの初期位置を決定
RIG_INITIAL_X = hosiTargetObjectPos.x;
RIG_INITIAL_Y = 0; // リグのYは地面レベル(0)に一旦設定 (カメラの高さはrigEl内のcameraエンティティで調整)
RIG_INITIAL_Z = hosiTargetObjectPos.z + 8; // ★★★ オブジェクトの少し手前 (+8ユニット) ★★★
} else {
console.warn(`Invalid hosi index: ${targetHosiNum}. Using default/jx,jy,jz.`);
if (jxParam !== null && jyParam !== null && jzParam !== null) {
RIG_INITIAL_X = parseFloat(jxParam); RIG_INITIAL_Y = parseFloat(jyParam); RIG_INITIAL_Z = parseFloat(jzParam);
} else { /* HTMLのデフォルト値が使われる */ }
}
} else if (jxParam !== null && jyParam !== null && jzParam !== null) {
RIG_INITIAL_X = parseFloat(jxParam); RIG_INITIAL_Y = parseFloat(jyParam); RIG_INITIAL_Z = parseFloat(jzParam);
}
// HTMLのデフォルト値はここで最終的に上書きされる
rigEl.setAttribute('position', `${RIG_INITIAL_X} ${RIG_INITIAL_Y} ${RIG_INITIAL_Z}`);
// スポーン開始位置と依存する定数を更新
SPAWN_ANIM_START_POS_STRING = `${RIG_INITIAL_X} ${RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL} ${RIG_INITIAL_Z}`;
H_LINE_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL;
H_RING_OBJECT_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL;
HORIZONTAL_RING_LOOK_AT_TARGET.set(RIG_INITIAL_X, H_RING_OBJECT_Y, RIG_INITIAL_Z);
GRID_INITIAL_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL + 10;
V_RING_CENTER_Y = RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL;
VERTICAL_RING_LOOK_AT_TARGET.set(RIG_INITIAL_X, V_RING_CENTER_Y, RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET); // V_RING_FIXED_Z_OFFSET はリグZからの相対
V_LINE_START_Y_OFFSET_FROM_CENTER = ( (numObjects-1) * V_LINE_SPACING / 2 );
console.log(`Final Rig Initial Position set to: X=${RIG_INITIAL_X}, Y=${RIG_INITIAL_Y}, Z=${RIG_INITIAL_Z}`);
console.log(`Spawn Start Position String set to: ${SPAWN_ANIM_START_POS_STRING}`);
// オブジェクト生成ループ (ここから開始)
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 applyRandomRotationComponent = false; let initialRotation = null;
// オブジェクトの最終位置計算 (更新された RIG_INITIAL_X,Y,Z を使用)
switch (LAYOUT_PATTERN) {
case 1: finalX = (Math.random() - 0.5) * spread + RIG_INITIAL_X; finalY = (Math.random() - 0.5) * spread + (RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL); finalZ = (Math.random() - 0.5) * spread + RIG_INITIAL_Z; applyRandomRotationComponent = true; break;
case 2: finalX = (Math.random() - 0.5) * spread + RIG_INITIAL_X; finalY = (Math.random() - 0.5) * spread + (RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL); finalZ = (Math.random() - 0.5) * spread + RIG_INITIAL_Z; applyRandomRotationComponent = 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; applyRandomRotationComponent = 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; applyRandomRotationComponent = 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) }; applyRandomRotationComponent = (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) }; applyRandomRotationComponent = (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_g = -totalGridWidth / 2; finalX = startX_g + column * GRID_SPACING_X + RIG_INITIAL_X; finalY = GRID_INITIAL_Y; finalZ = RIG_INITIAL_Z + GRID_Z_DEPTH; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotationComponent = (LAYOUT_PATTERN === 9); break;
case 11: case 12: finalX = V_LINE_X_OFFSET + RIG_INITIAL_X; finalY = (RIG_INITIAL_Y + 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}; applyRandomRotationComponent = (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 = (RIG_INITIAL_Y + 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(new THREE.Vector3(RIG_INITIAL_X, RIG_INITIAL_Y + V_RING_CENTER_Y, RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET)); 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) }; applyRandomRotationComponent = (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 = (RIG_INITIAL_Y + 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(new THREE.Vector3(RIG_INITIAL_X, RIG_INITIAL_Y + V_RING_CENTER_Y, RIG_INITIAL_Z + V_RING_FIXED_Z_OFFSET)); 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) }; applyRandomRotationComponent = (LAYOUT_PATTERN === 15); break;
default: console.warn(`Unknown LAYOUT_PATTERN: ${LAYOUT_PATTERN}, defaulting to pattern 1.`); finalX = (Math.random() - 0.5) * spread + RIG_INITIAL_X; finalY = (Math.random() - 0.5) * spread + (RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL) ; finalZ = (Math.random() - 0.5) * spread + RIG_INITIAL_Z; applyRandomRotationComponent = true;
}
objectsFinalPositions[objectIndex] = {x: finalX, y: finalY, z: finalZ}; // ワールド座標での最終位置を保存
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 (!applyRandomRotationComponent && !(definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number'))) { 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;
const hasSpecificRotation = definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number');
if (applyRandomRotationComponent || hasSpecificRotation) { let rotationParams = { maxSpeed: 5 }; if (definition.rotationSettings) { if (definition.rotationSettings.axis) { rotationParams.initialAxis = definition.rotationSettings.axis; } if (typeof definition.rotationSettings.speed === 'number') { rotationParams.initialSpeed = definition.rotationSettings.speed; } } objectEl.setAttribute('random-rotate', rotationParams); }
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'});
} // End of object generation loop
// hosiパラメータが指定されていた場合のリグの最終的な再配置と向き調整
// この処理はオブジェクトのスポーンアニメーションとは独立して、リグの初期状態を決定する
if (hosiParam !== null) {
const targetHosiNum = parseInt(hosiParam, 10);
if (targetHosiNum >= 1 && targetHosiNum <= numObjects && objectsFinalPositions[targetHosiNum]) {
const targetObjectPos = objectsFinalPositions[targetHosiNum];
console.log(`Setting initial rig position for hosi=${targetHosiNum} based on object final pos:`, targetObjectPos);
const newRigX_hosi = targetObjectPos.x;
const newRigY_hosi = RIG_INITIAL_Y; // リグのYは既にURLパラメータ等で決定済み
const newRigZ_hosi = targetObjectPos.z + 8; // ★★★ オブジェクトから8ユニット手前 ★★★
rigEl.setAttribute('position', `${newRigX_hosi} ${newRigY_hosi} ${newRigZ_hosi}`);
RIG_INITIAL_X = newRigX_hosi; RIG_INITIAL_Y = newRigY_hosi; RIG_INITIAL_Z = newRigZ_hosi;
SPAWN_ANIM_START_POS_STRING = `${RIG_INITIAL_X} ${RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL} ${RIG_INITIAL_Z}`;
console.log(`Rig initial position set by hosi: X=${RIG_INITIAL_X}, Y=${RIG_INITIAL_Y}, Z=${RIG_INITIAL_Z}`);
// リグがオブジェクトの方を向く
const lookAtPos = new THREE.Vector3(targetObjectPos.x, RIG_INITIAL_Y + RIG_INITIAL_Y_CAMERA_LEVEL, targetObjectPos.z);
rigEl.object3D.lookAt(lookAtPos);
const currentRigRotationEuler = new THREE.Euler().setFromQuaternion(rigEl.object3D.quaternion, 'YXZ');
rigEl.setAttribute('rotation', `0 ${THREE.MathUtils.radToDeg(currentRigRotationEuler.y)} 0`);
console.log("Rig initial rotation set to look at hosi target.");
// スポーンアニメーションの開始点を更新したので、既存オブジェクトの初期位置も更新
document.querySelectorAll('.clickableObject').forEach(objEl => {
objEl.setAttribute('position', SPAWN_ANIM_START_POS_STRING);
});
} else {
console.warn(`Hosi target object with index ${targetHosiNum} not found or its final position is not available.`);
}
}
initializeEventListeners();
}
function initializeEventListeners() { /* ... (省略せず、前回の完全なコードをここに記述) ... */ 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 objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const currentCommentInfo = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; const commentSubPageCount = currentCommentInfo.text.length; if (EXE_MODE === 0) { currentCommentSubPageIndex = (commentSubPageIndex - 1 + commentSubPageCount) % commentSubPageCount; } else { if (PAGES[mainPageIndex] === 'comment' && commentSubPageIndex > 0) { currentCommentSubPageIndex--; } else { mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; if (PAGES[mainPageIndex] === 'comment') { const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} currentCommentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } else { currentCommentSubPageIndex = 0; infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = currentCommentSubPageIndex.toString(); updatePanelDisplay(); console.log("Prev button: mainPage=", mainPageIndex, "subPage=", currentCommentSubPageIndex); }); 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 objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const currentCommentInfo = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; const commentSubPageCount = currentCommentInfo.text.length; if (EXE_MODE === 0) { currentCommentSubPageIndex = (commentSubPageIndex + 1) % commentSubPageCount; } else { if (PAGES[mainPageIndex] === 'comment' && commentSubPageIndex < commentSubPageCount - 1) { currentCommentSubPageIndex++; } else { mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES; currentCommentSubPageIndex = 0; if (PAGES[mainPageIndex] === 'comment') { const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} } else {infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = currentCommentSubPageIndex.toString(); updatePanelDisplay(); console.log("Next button: mainPage=", mainPageIndex, "subPage=", currentCommentSubPageIndex); }); 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)); panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src'); console.log("Close button clicked, panel hidden."); });}
function updatePanelDisplay() { if (!infoPanelEl.dataset.objectIndex) return; const index = parseInt(infoPanelEl.dataset.objectIndex || '0', 10); const objectBaseColor = 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 || [DEFAULT_COMMENT_ARRAY_INFO.text[0]]; const mainCommentTextColorForThisObject = 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']) }; panelTextEl.setAttribute('visible', false); panelTextEl.setAttribute('troika-text', 'value', ''); panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src'); linkButtons.forEach(btn => btn.setAttribute('visible', false)); const currentCommentItem = commentsArray[commentSubPageIndex]; if (EXE_MODE === 0) { if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') { const imgY = typeof currentCommentItem.imageY === 'number' ? currentCommentItem.imageY : DEFAULT_PANEL_IMAGE_Y; panelImageEl.object3D.position.set(0, imgY, 0.06); panelImageEl.setAttribute('src', currentCommentItem.src); panelImageEl.setAttribute('width', currentCommentItem.width || 40); const imgHeight = currentCommentItem.height; if (imgHeight && imgHeight !== 'auto') { panelImageEl.setAttribute('height', imgHeight); } else { panelImageEl.removeAttribute('height'); } panelImageEl.setAttribute('visible', true); displayText = currentCommentItem.caption || ''; if (displayText) { const capY = typeof currentCommentItem.captionY === 'number' ? currentCommentItem.captionY : DEFAULT_CAPTION_Y; panelTextEl.object3D.position.set(0, capY, 0.05); currentStyle = {...(PAGE_TEXT_STYLES['image_caption'] || PAGE_TEXT_STYLES['comment'])}; if (mainCommentTextColorForThisObject) currentStyle.color = mainCommentTextColorForThisObject; panelTextEl.setAttribute('visible', true); } } else { displayText = typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0]; panelTextEl.object3D.position.set(0, DEFAULT_PANEL_TEXT_Y, 0.05); if (mainCommentTextColorForThisObject) currentStyle.color = mainCommentTextColorForThisObject; panelTextEl.setAttribute('visible', true); } if (commentSubPageCount > 1) { pageIndicator = `(${commentSubPageIndex + 1}/${commentSubPageCount})`; } else { pageIndicator = ''; } } else { pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`; panelTextEl.setAttribute('visible', true); panelTextEl.object3D.position.set(0, DEFAULT_PANEL_TEXT_Y, 0.05); if (pageType === 'index') { displayText = `${objectType === 'sphere' ? '球' : '立方体'}: ${index}`; } else if (pageType === 'color') { displayText = `色: ${objectBaseColor}`; } else if (pageType === 'size') { displayText = `${objectType === 'sphere' ? '半径' : 'サイズ'}: ${dimensionValue}`; } else if (pageType === 'comment') { pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`; if (mainCommentTextColorForThisObject) { currentStyle.color = mainCommentTextColorForThisObject; } if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') { const imgY = typeof currentCommentItem.imageY === 'number' ? currentCommentItem.imageY : DEFAULT_PANEL_IMAGE_Y; panelImageEl.object3D.position.set(0, imgY, 0.06); panelImageEl.setAttribute('src', currentCommentItem.src); panelImageEl.setAttribute('width', currentCommentItem.width || 40); const imgHeight = currentCommentItem.height; if (imgHeight && imgHeight !== 'auto') { panelImageEl.setAttribute('height', imgHeight); } else { panelImageEl.removeAttribute('height'); } panelImageEl.setAttribute('visible', true); displayText = `コメント:\n${currentCommentItem.caption || ''}`; const capY = typeof currentCommentItem.captionY === 'number' ? currentCommentItem.captionY : DEFAULT_CAPTION_Y; panelTextEl.object3D.position.set(0, capY, 0.05); } else { displayText = `コメント:\n${typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0]}`; } } } 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}`; if (panelTextEl.getAttribute('visible')) { 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 }); } else if (EXE_MODE === 0 && pageIndicator && !(typeof currentCommentItem === 'object' && currentCommentItem.type === 'image' && !currentCommentItem.caption)) { panelTextEl.setAttribute('troika-text', { value: pageIndicator, color: PAGE_TEXT_STYLES['image_caption'].color, fontSize: PAGE_TEXT_STYLES['image_caption'].fontSize, maxWidth: currentStyle.maxWidth, align: currentStyle.align, anchorX: currentStyle.anchorX, anchorY: currentStyle.anchorY, baseline: currentStyle.baseline }); panelTextEl.setAttribute('visible', true); } }
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 ? commentDataForThisObject.text.length : 1).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 objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const currentObjectCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; const currentCommentSubPageCount = currentObjectCommentData.text.length; if (EXE_MODE === 0) { currentCommentSubPageIndex = (commentSubPageIndex - 1 + currentCommentSubPageCount) % currentCommentSubPageCount; } else { if (PAGES[mainPageIndex] === 'comment' && currentCommentSubPageIndex > 0) { currentCommentSubPageIndex--; } else { mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; if (PAGES[mainPageIndex] === 'comment') { const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} currentCommentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } else { currentCommentSubPageIndex = 0; infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = currentCommentSubPageIndex.toString(); updatePanelDisplay(); console.log("Prev button: mainPage=", mainPageIndex, "subPage=", currentCommentSubPageIndex); });
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 objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const currentObjectCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; const currentCommentSubPageCount = currentObjectCommentData.text.length; if (EXE_MODE === 0) { currentCommentSubPageIndex = (commentSubPageIndex + 1) % currentCommentSubPageCount; } else { if (PAGES[mainPageIndex] === 'comment' && currentCommentSubPageIndex < currentCommentSubPageCount - 1) { currentCommentSubPageIndex++; } else { mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES; currentCommentSubPageIndex = 0; if (PAGES[mainPageIndex] === 'comment') { const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} } else {infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString(); updatePanelDisplay(); console.log("Next button: mainPage=", mainPageIndex, "subPage=", currentCommentSubPageIndex); });
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)); panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src'); 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); }
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'); });
document.addEventListener('DOMContentLoaded', function () {const scene = document.querySelector('a-scene'); if (scene) { if (scene.hasLoaded) {initializeSceneAndObjects();} else {scene.addEventListener('loaded', initializeSceneAndObjects, {once: true});}} else {console.error("a-scene element not found at DOMContentLoaded!");}});
function initializeEventListeners() { console.log("Initializing event listeners for buttons and controllers."); 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."); } }
// --- End of SCRIPT BLOCK 2 ---
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test65
├
<!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>
// --- SCRIPT BLOCK 1: Component Definitions ---
// --- プレイヤー移動制御用カスタムコンポーネント ('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; 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) { const verticalMovement = this.rightThumbstickInput.y * data.verticalSpeed * dt; this.rigEl.object3D.position.y -= verticalMovement; } } const position = this.rigEl.object3D.position; const cameraObject = this.cameraEl.object3D; 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 }, initialAxis: { type: 'vec3', default: null }, initialSpeed: { type: 'number', default: NaN } }, init: function () { const axisIsDefined = this.data.initialAxis && typeof this.data.initialAxis.x === 'number' && typeof this.data.initialAxis.y === 'number' && typeof this.data.initialAxis.z === 'number'; if (axisIsDefined && !isNaN(this.data.initialSpeed)) { this.axis = new THREE.Vector3(this.data.initialAxis.x, this.data.initialAxis.y, this.data.initialAxis.z); if (this.axis.lengthSq() === 0) { this.axis.set(0,1,0); } else { this.axis.normalize(); } this.speedRad = THREE.MathUtils.degToRad(this.data.initialSpeed); } else { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5); if (this.axis.lengthSq() < 0.001) { this.axis.set(0.01, 1, 0.01); } this.axis.normalize(); const speedSource = !isNaN(this.data.initialSpeed) ? this.data.initialSpeed : (((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1)); this.speedRad = THREE.MathUtils.degToRad(speedSource); } this.deltaRotationQuaternion = new THREE.Quaternion(); }, tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angleChangeRad = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angleChangeRad); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); this.el.object3D.quaternion.normalize(); } });
// --- 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; 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-image id="panelImage" src="" visible="false" width="40" height="18" position="0 0 0.06" material="shader: flat;"></a-image>
<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 panelImageEl = document.getElementById('panelImage');
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/u5.jpg";
const PANEL_OFFSET_FROM_OBJECT = 5.0;
const DEFAULT_PANEL_TEXT_Y = 0;
const DEFAULT_PANEL_IMAGE_Y = -3;
const DEFAULT_CAPTION_Y = 8;
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' },
'image_caption': { ...BASE_TEXT_STYLE, fontSize: 0.50, color: '#E0E0E0', align: 'center', anchorY: 'top', baseline:'top' }
};
const OBJECT_DEFINITIONS = {
1: { type: 'sphere', useTextureForIndex: 1, sizeType: 'fixed', fixedSize: 3.0,
rotationSettings: { axis: { x: 0, y: 1, z: 0 }, speed: 15 } },
2: { type: 'box', useTextureForIndex: 2, sizeType: 'random' },
3: { type: 'sphere', specificColor: '#FF1493', sizeType: 'fixed', fixedSize: 4.0,
rotationSettings: { axis: {x:1, y:1, z:0}, speed: -10 } },
4: { type: 'box', useTextureForIndex: 4,
rotationSettings: { speed: 5 } },
};
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"}] },
5: { text: [ "これはテキストのサブページ1です。", { type: 'image', src: './pic/e1.jpg', caption: "これがe1の画像です。(Y座標はデフォルト)", width: 40, height: 'auto' }, { type: 'image', src: './pic/a3.jpg', caption: "a3画像 (画像Y=0, キャプションY=-8)", width: 30, height: 20, imageY: 0, captionY: -8 }, "画像の後にもテキストを続けられます。" ], mainCommentTextColor: "#FFFF00", links: [{label:"画像ソース元(例)", url:"https://example.com/e1", 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 = 16;
console.log(`Using LAYOUT_PATTERN: ${LAYOUT_PATTERN}`);
// Rigの初期位置 (全パターンの基準点として使用)
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}`;
// パターン3, 4 (水平ライン) 用の定数
const H_LINE_SPACING = 15;
const H_LINE_Y = RIG_INITIAL_Y_CAMERA_LEVEL;
const H_LINE_Z_OFFSET = -35;
// パターン5, 6, 7, 8 (水平リング / 水平半リング) 用の定数
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);
// パターン9, 10 (グリッド) 用の定数
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;
// パターン11, 12 (垂直ライン) 用の定数
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;
// パターン13, 14, 15, 16 (垂直リング / 垂直半リング) 用の定数
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; // 0なら均等割
const V_RING_ANGLE_STEP_DEGREES = 0; // 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 applyRandomRotationComponent = 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; applyRandomRotationComponent = true; break;
case 2: finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotationComponent = 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; applyRandomRotationComponent = 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; applyRandomRotationComponent = 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) }; applyRandomRotationComponent = (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) }; applyRandomRotationComponent = (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_g = -totalGridWidth / 2; finalX = startX_g + 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}; applyRandomRotationComponent = (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}; applyRandomRotationComponent = (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) }; applyRandomRotationComponent = (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) }; applyRandomRotationComponent = (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; applyRandomRotationComponent = 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 (!applyRandomRotationComponent && !(definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number'))) { 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;
const hasSpecificRotation = definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number');
if (applyRandomRotationComponent || hasSpecificRotation) { let rotationParams = { maxSpeed: 5 }; if (definition.rotationSettings) { if (definition.rotationSettings.axis) { rotationParams.initialAxis = definition.rotationSettings.axis; } if (typeof definition.rotationSettings.speed === 'number') { rotationParams.initialSpeed = definition.rotationSettings.speed; } } objectEl.setAttribute('random-rotate', rotationParams); }
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 objectBaseColor = 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 || [DEFAULT_COMMENT_ARRAY_INFO.text[0]];
const mainCommentTextColorForThisObject = 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']) };
panelTextEl.setAttribute('visible', false); panelTextEl.setAttribute('troika-text', 'value', '');
panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src');
linkButtons.forEach(btn => btn.setAttribute('visible', false));
const currentCommentItem = commentsArray[commentSubPageIndex];
if (EXE_MODE === 0) {
if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') {
const imgY = typeof currentCommentItem.imageY === 'number' ? currentCommentItem.imageY : DEFAULT_PANEL_IMAGE_Y;
panelImageEl.object3D.position.set(0, imgY, 0.06);
panelImageEl.setAttribute('src', currentCommentItem.src);
panelImageEl.setAttribute('width', currentCommentItem.width || 40);
const imgHeight = currentCommentItem.height;
if (imgHeight && imgHeight !== 'auto') { panelImageEl.setAttribute('height', imgHeight); }
else { panelImageEl.removeAttribute('height'); }
panelImageEl.setAttribute('visible', true);
displayText = currentCommentItem.caption || '';
if (displayText) { const capY = typeof currentCommentItem.captionY === 'number' ? currentCommentItem.captionY : DEFAULT_CAPTION_Y; panelTextEl.object3D.position.set(0, capY, 0.05); currentStyle = {...(PAGE_TEXT_STYLES['image_caption'] || PAGE_TEXT_STYLES['comment'])}; if (mainCommentTextColorForThisObject) currentStyle.color = mainCommentTextColorForThisObject; panelTextEl.setAttribute('visible', true); }
} else {
displayText = typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0];
panelTextEl.object3D.position.set(0, DEFAULT_PANEL_TEXT_Y, 0.05);
if (mainCommentTextColorForThisObject) currentStyle.color = mainCommentTextColorForThisObject;
panelTextEl.setAttribute('visible', true);
}
if (commentSubPageCount > 1) { pageIndicator = `(${commentSubPageIndex + 1}/${commentSubPageCount})`; }
else { pageIndicator = ''; }
} else {
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`;
panelTextEl.setAttribute('visible', true);
panelTextEl.object3D.position.set(0, DEFAULT_PANEL_TEXT_Y, 0.05);
if (pageType === 'index') { displayText = `${objectType === 'sphere' ? '球' : '立方体'}: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${objectBaseColor}`; }
else if (pageType === 'size') { displayText = `${objectType === 'sphere' ? '半径' : 'サイズ'}: ${dimensionValue}`; }
else if (pageType === 'comment') {
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`;
if (mainCommentTextColorForThisObject) { currentStyle.color = mainCommentTextColorForThisObject; }
if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') {
const imgY = typeof currentCommentItem.imageY === 'number' ? currentCommentItem.imageY : DEFAULT_PANEL_IMAGE_Y;
panelImageEl.object3D.position.set(0, imgY, 0.06);
panelImageEl.setAttribute('src', currentCommentItem.src);
panelImageEl.setAttribute('width', currentCommentItem.width || 40);
const imgHeight = currentCommentItem.height;
if (imgHeight && imgHeight !== 'auto') { panelImageEl.setAttribute('height', imgHeight); }
else { panelImageEl.removeAttribute('height'); }
panelImageEl.setAttribute('visible', true);
displayText = `コメント:\n${currentCommentItem.caption || ''}`;
const capY = typeof currentCommentItem.captionY === 'number' ? currentCommentItem.captionY : DEFAULT_CAPTION_Y;
panelTextEl.object3D.position.set(0, capY, 0.05);
} else {
displayText = `コメント:\n${typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0]}`;
}
}
}
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}`;
if (panelTextEl.getAttribute('visible')) { 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 }); }
else if (EXE_MODE === 0 && pageIndicator && !(typeof currentCommentItem === 'object' && currentCommentItem.type === 'image' && !currentCommentItem.caption)) { panelTextEl.setAttribute('troika-text', { value: pageIndicator, color: PAGE_TEXT_STYLES['image_caption'].color, fontSize: PAGE_TEXT_STYLES['image_caption'].fontSize, maxWidth: currentStyle.maxWidth, align: currentStyle.align, anchorX: currentStyle.anchorX, anchorY: currentStyle.anchorY, baseline: currentStyle.baseline }); panelTextEl.setAttribute('visible', true); }
}
// --- オブジェクトクリック時の処理 ---
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 ? commentDataForThisObject.text.length : 1).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; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} commentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } else { commentSubPageIndex = 0; infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } } } 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; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} commentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } else {infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } 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; if (PAGES[mainPageIndex] === 'comment') { const objectIdx = parseInt(infoPanelEl.dataset.objectIndex, 10); const newCommentData = CUBE_COMMENTS[objectIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} } else {infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } } } else if (EXE_MODE === 1) { mainPageIndex = (mainPageIndex + 1) % 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; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinks = JSON.stringify(newCommentData.links || []); if (newCommentData.mainCommentTextColor) { infoPanelEl.dataset.commentPageTextColor = newCommentData.mainCommentTextColor;} else { delete infoPanelEl.dataset.commentPageTextColor;} } else {infoPanelEl.dataset.activeLinks = JSON.stringify([]); delete infoPanelEl.dataset.commentPageTextColor;} } 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)); panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src'); 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); }
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."); } });
// --- End of SCRIPT BLOCK 2 ---
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test64
├
<!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>
// --- 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 }, 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; 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) { const verticalMovement = this.rightThumbstickInput.y * data.verticalSpeed * dt; this.rigEl.object3D.position.y -= verticalMovement; } } const position = this.rigEl.object3D.position; const cameraObject = this.cameraEl.object3D; 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]; } }
});
AFRAME.registerComponent('random-rotate', { schema: { maxSpeed: { type: 'number', default: 5 }, initialAxis: { type: 'vec3', default: null }, initialSpeed: { type: 'number', default: NaN } }, init: function () { const axisIsDefined = this.data.initialAxis && typeof this.data.initialAxis.x === 'number' && typeof this.data.initialAxis.y === 'number' && typeof this.data.initialAxis.z === 'number'; if (axisIsDefined && !isNaN(this.data.initialSpeed)) { this.axis = new THREE.Vector3(this.data.initialAxis.x, this.data.initialAxis.y, this.data.initialAxis.z); if (this.axis.lengthSq() === 0) { this.axis.set(0,1,0); } else { this.axis.normalize(); } this.speedRad = THREE.MathUtils.degToRad(this.data.initialSpeed); } else { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5); if (this.axis.lengthSq() < 0.001) { this.axis.set(0.01, 1, 0.01); } this.axis.normalize(); const speedSource = !isNaN(this.data.initialSpeed) ? this.data.initialSpeed : (((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1)); this.speedRad = THREE.MathUtils.degToRad(speedSource); } this.deltaRotationQuaternion = new THREE.Quaternion(); }, tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angleChangeRad = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angleChangeRad); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); this.el.object3D.quaternion.normalize(); } });
// --- 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; 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-image id="panelImage" src="" visible="false" width="40" height="18" position="0 0 0.05" material="shader: flat;"></a-image> <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 panelImageEl = document.getElementById('panelImage');
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/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 DEFAULT_PANEL_TEXT_Y = 0; const DEFAULT_PANEL_IMAGE_Y = -3; const DEFAULT_CAPTION_Y = 8;
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' }, 'image_caption': { ...BASE_TEXT_STYLE, fontSize: 0.50, color: '#E0E0E0', align: 'center', anchorY: 'top', baseline:'top' } };
const OBJECT_DEFINITIONS = { 1: { type: 'sphere', useTextureForIndex: 1, sizeType: 'fixed', fixedSize: 3.0, rotationSettings: { axis: { x: 0, y: 1, z: 0 }, speed: 15 } }, 2: { type: 'box', useTextureForIndex: 2, sizeType: 'random' }, 3: { type: 'sphere', specificColor: '#FF1493', sizeType: 'fixed', fixedSize: 4.0, rotationSettings: { axis: {x:1, y:1, z:0}, speed: -10 } }, 4: { type: 'box', useTextureForIndex: 4, rotationSettings: { speed: 5 } }, };
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"}] }, 5: { text: [ "これはテキストのサブページ1です。", { type: 'image', src: './pic/e1.jpg', caption: "これがe1の画像です。(Y座標はデフォルト)", width: 40, height: 'auto' }, { type: 'image', src: './pic/a3.jpg', caption: "a3画像 (画像Y=0, キャプションY=-8)", width: 30, height: 20, imageY: 0, captionY: -8 }, "画像の後にもテキストを続けられます。" ], mainCommentTextColor: "#FFFF00", links: [{label:"画像ソース元(例)", url:"https://example.com/e1", 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 = 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 applyRandomRotationComponent = 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; applyRandomRotationComponent = true; break; case 2: finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotationComponent = 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; applyRandomRotationComponent = 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; applyRandomRotationComponent = 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) }; applyRandomRotationComponent = (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) }; applyRandomRotationComponent = (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_g = -totalGridWidth / 2; finalX = startX_g + 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}; applyRandomRotationComponent = (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}; applyRandomRotationComponent = (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) }; applyRandomRotationComponent = (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) }; applyRandomRotationComponent = (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; applyRandomRotationComponent = 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 (!applyRandomRotationComponent && !(definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number'))) { 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;
const hasSpecificRotation = definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number');
if (applyRandomRotationComponent || hasSpecificRotation) { let rotationParams = { maxSpeed: 5 }; if (definition.rotationSettings) { if (definition.rotationSettings.axis) { rotationParams.initialAxis = definition.rotationSettings.axis; } if (typeof definition.rotationSettings.speed === 'number') { rotationParams.initialSpeed = definition.rotationSettings.speed; } } objectEl.setAttribute('random-rotate', rotationParams); }
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 objectBaseColor = 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 || [DEFAULT_COMMENT_ARRAY_INFO.text[0]];
const mainCommentTextColorForThisObject = 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']) };
panelTextEl.setAttribute('visible', false); panelTextEl.setAttribute('troika-text', 'value', '');
panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src');
linkButtons.forEach(btn => btn.setAttribute('visible', false));
const currentCommentItem = commentsArray[commentSubPageIndex];
if (EXE_MODE === 0) {
if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') {
const imgY = typeof currentCommentItem.imageY === 'number' ? currentCommentItem.imageY : DEFAULT_PANEL_IMAGE_Y;
panelImageEl.object3D.position.set(0, imgY, 0.05);
panelImageEl.setAttribute('src', currentCommentItem.src);
panelImageEl.setAttribute('width', currentCommentItem.width || 40);
panelImageEl.setAttribute('height', currentCommentItem.height || 'auto');
panelImageEl.setAttribute('visible', true);
displayText = currentCommentItem.caption || '';
if (displayText) {
const capY = typeof currentCommentItem.captionY === 'number' ? currentCommentItem.captionY : DEFAULT_CAPTION_Y;
panelTextEl.object3D.position.set(0, capY, 0.05);
currentStyle = {...(PAGE_TEXT_STYLES['image_caption'] || PAGE_TEXT_STYLES['comment'])};
if (mainCommentTextColorForThisObject) currentStyle.color = mainCommentTextColorForThisObject;
panelTextEl.setAttribute('visible', true);
}
} else {
displayText = typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0];
panelTextEl.object3D.position.set(0, DEFAULT_PANEL_TEXT_Y, 0.05);
if (mainCommentTextColorForThisObject) currentStyle.color = mainCommentTextColorForThisObject;
panelTextEl.setAttribute('visible', true);
}
if (commentSubPageCount > 1) { pageIndicator = `(${commentSubPageIndex + 1}/${commentSubPageCount})`; }
else { pageIndicator = ''; }
} else {
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`;
panelTextEl.setAttribute('visible', true);
panelTextEl.object3D.position.set(0, DEFAULT_PANEL_TEXT_Y, 0.05);
if (pageType === 'index') { displayText = `${objectType === 'sphere' ? '球' : '立方体'}: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${objectBaseColor}`; }
else if (pageType === 'size') { displayText = `${objectType === 'sphere' ? '半径' : 'サイズ'}: ${dimensionValue}`; }
else if (pageType === 'comment') {
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`;
if (mainCommentTextColorForThisObject) { currentStyle.color = mainCommentTextColorForThisObject; }
if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') {
const imgY = typeof currentCommentItem.imageY === 'number' ? currentCommentItem.imageY : DEFAULT_PANEL_IMAGE_Y;
panelImageEl.object3D.position.set(0, imgY, 0.05);
panelImageEl.setAttribute('src', currentCommentItem.src);
panelImageEl.setAttribute('width', currentCommentItem.width || 40);
panelImageEl.setAttribute('height', currentCommentItem.height || 'auto');
panelImageEl.setAttribute('visible', true);
displayText = `コメント:\n${currentCommentItem.caption || ''}`;
const capY = typeof currentCommentItem.captionY === 'number' ? currentCommentItem.captionY : DEFAULT_CAPTION_Y;
panelTextEl.object3D.position.set(0, capY, 0.05);
} else {
displayText = `コメント:\n${typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0]}`;
}
}
}
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}`;
if (panelTextEl.getAttribute('visible')) { 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 }); }
else if (EXE_MODE === 0 && pageIndicator && !(typeof currentCommentItem === 'object' && currentCommentItem.type === 'image' && !currentCommentItem.caption)) { panelTextEl.setAttribute('troika-text', { value: pageIndicator, color: PAGE_TEXT_STYLES['image_caption'].color, fontSize: PAGE_TEXT_STYLES['image_caption'].fontSize, maxWidth: currentStyle.maxWidth, align: currentStyle.align, anchorX: currentStyle.anchorX, anchorY: currentStyle.anchorY, baseline: currentStyle.baseline }); panelTextEl.setAttribute('visible', true); }
}
// --- オブジェクトクリック時の処理 ---
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 ? commentDataForThisObject.text.length : 1).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)); panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src'); 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); }
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."); } });
// --- End of SCRIPT BLOCK 2 ---
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test63
├
<!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>
// --- SCRIPT BLOCK 1: Component Definitions ---
// --- プレイヤー移動制御用カスタムコンポーネント ('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){console.warn("Could not remove leftHand thumbstickmoved listener cleanly.", e);} }
if (this.rightHand) { try { this.rightHand.removeEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); } catch(e){console.warn("Could not remove rightHand thumbstickmoved listener cleanly.", 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;
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 },
initialAxis: { type: 'vec3', default: null },
initialSpeed: { type: 'number', default: NaN }
},
init: function () {
const axisIsDefined = this.data.initialAxis && typeof this.data.initialAxis.x === 'number' && typeof this.data.initialAxis.y === 'number' && typeof this.data.initialAxis.z === 'number';
if (axisIsDefined && !isNaN(this.data.initialSpeed)) {
this.axis = new THREE.Vector3(this.data.initialAxis.x, this.data.initialAxis.y, this.data.initialAxis.z);
if (this.axis.lengthSq() === 0) { this.axis.set(0,1,0); } else { this.axis.normalize(); }
this.speedRad = THREE.MathUtils.degToRad(this.data.initialSpeed);
} else {
this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5);
if (this.axis.lengthSq() < 0.001) { this.axis.set(0.01, 1, 0.01); }
this.axis.normalize();
const speedSource = !isNaN(this.data.initialSpeed) ? this.data.initialSpeed : (((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1));
this.speedRad = THREE.MathUtils.degToRad(speedSource);
}
this.deltaRotationQuaternion = new THREE.Quaternion();
},
tick: function (time, timeDelta) {
const dt = timeDelta / 1000;
const angleChangeRad = this.speedRad * dt;
this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angleChangeRad);
this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion);
this.el.object3D.quaternion.normalize();
}
});
// --- 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; 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 2 0.05">
</a-entity>
<a-image id="panelImage" src="" visible="false" width="40" height="18" position="0 -4 0.05" material="shader: flat;"></a-image> <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 panelImageEl = document.getElementById('panelImage'); // 画像要素取得
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/u5.jpg";
if (SKY_BACKGROUND_IMAGE_PATH && SKY_BACKGROUND_IMAGE_PATH !== "") {
skyElement.setAttribute('src', SKY_BACKGROUND_IMAGE_PATH);
skyElement.setAttribute('visible', 'true');
// sceneEl.setAttribute('background', 'color', 'transparent'); // Skyが不透明なら不要
} 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' }, // テキストコメント用
'image_caption': { ...BASE_TEXT_STYLE, fontSize: 0.50, color: '#E0E0E0', align: 'center', anchorY: 'bottom', baseline:'bottom' } // 画像キャプション用
};
const OBJECT_DEFINITIONS = {
1: { type: 'sphere', useTextureForIndex: 1, sizeType: 'fixed', fixedSize: 3.0, rotationSettings: { axis: { x: 0, y: 1, z: 0 }, speed: 15 } },
2: { type: 'box', useTextureForIndex: 2, sizeType: 'random' },
3: { type: 'sphere', specificColor: '#FF1493', sizeType: 'fixed', fixedSize: 4.0, rotationSettings: { axis: {x:1, y:1, z:0}, speed: -10 } },
4: { type: 'box', useTextureForIndex: 4, rotationSettings: { speed: 5 } },
};
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"}] },
5: { // 画像表示テスト用
text: [
"これはテキストのサブページ1です。",
{ type: 'image', src: './pic/e1.jpg', caption: "これがe1の画像です。\n(キャプションは複数行OK)", width: 45, height: 15 }, // height: 'auto' だとtroika-textのbbox問題で大きさが不安定になる可能性あり
"画像の後にもテキストを続けられます。"
],
mainCommentTextColor: "#FFFF00", // コメントページ全体のテキスト色(画像キャプション除く)
links: [{label:"画像ソース元(例)", url:"https://example.com/e1", 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 = 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 applyRandomRotationComponent = 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; applyRandomRotationComponent = true; break;
case 2: finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotationComponent = 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; applyRandomRotationComponent = 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; applyRandomRotationComponent = 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) }; applyRandomRotationComponent = (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) }; applyRandomRotationComponent = (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_g = -totalGridWidth / 2; finalX = startX_g + 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}; applyRandomRotationComponent = (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}; applyRandomRotationComponent = (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) }; applyRandomRotationComponent = (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) }; applyRandomRotationComponent = (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; applyRandomRotationComponent = 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 (!applyRandomRotationComponent && !(definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number'))) { 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;
const hasSpecificRotation = definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number');
if (applyRandomRotationComponent || hasSpecificRotation) { let rotationParams = { maxSpeed: 5 }; if (definition.rotationSettings) { if (definition.rotationSettings.axis) { rotationParams.initialAxis = definition.rotationSettings.axis; } if (typeof definition.rotationSettings.speed === 'number') { rotationParams.initialSpeed = definition.rotationSettings.speed; } } objectEl.setAttribute('random-rotate', rotationParams); }
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 objectBaseColor = 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 mainCommentTextColorForThisObject = 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']) };
// リセット
panelTextEl.setAttribute('troika-text', 'value', ''); // テキストをクリア
panelTextEl.setAttribute('visible', false);
panelImageEl.setAttribute('visible', false);
panelImageEl.removeAttribute('src');
linkButtons.forEach(btn => btn.setAttribute('visible', false));
const currentCommentItem = commentsArray[commentSubPageIndex]; // 現在のサブページのアイテム
if (EXE_MODE === 0) {
if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') {
panelImageEl.setAttribute('src', currentCommentItem.src);
panelImageEl.setAttribute('width', currentCommentItem.width || 40);
panelImageEl.setAttribute('height', currentCommentItem.height || 'auto');
panelImageEl.setAttribute('visible', true);
displayText = currentCommentItem.caption || '';
currentStyle = {...(PAGE_TEXT_STYLES['image_caption'] || PAGE_TEXT_STYLES['comment'])};
if (displayText) panelTextEl.setAttribute('visible', true);
} else {
displayText = typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0];
panelTextEl.setAttribute('visible', true);
}
if (commentSubPageCount > 1 || (displayText === DEFAULT_COMMENT_ARRAY_INFO.text[0] && commentSubPageCount === 1 && commentsArray[0] === DEFAULT_COMMENT_ARRAY_INFO.text[0] )) {
// 「コメントはありません」でページ数が1の場合は表示しない、は削除済み
// サブページが複数ある場合のみ表示
if(commentSubPageCount > 1) pageIndicator = `(${commentSubPageIndex + 1}/${commentSubPageCount})`;
} else { pageIndicator = ''; }
if (mainCommentTextColorForThisObject) { currentStyle.color = mainCommentTextColorForThisObject; }
} else { // EXE_MODE === 1
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`;
panelTextEl.setAttribute('visible', true);
if (pageType === 'index') { displayText = `${objectType === 'sphere' ? '球' : '立方体'}: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${objectBaseColor}`; }
else if (pageType === 'size') { displayText = `${objectType === 'sphere' ? '半径' : 'サイズ'}: ${dimensionValue}`; }
else if (pageType === 'comment') {
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`;
if (mainCommentTextColorForThisObject) { currentStyle.color = mainCommentTextColorForThisObject; }
if (typeof currentCommentItem === 'object' && currentCommentItem.type === 'image') {
panelImageEl.setAttribute('src', currentCommentItem.src);
panelImageEl.setAttribute('width', currentCommentItem.width || 40);
panelImageEl.setAttribute('height', currentCommentItem.height || 'auto');
panelImageEl.setAttribute('visible', true);
displayText = `コメント:\n${currentCommentItem.caption || ''}`;
} else {
displayText = `コメント:\n${typeof currentCommentItem === 'string' ? currentCommentItem : DEFAULT_COMMENT_ARRAY_INFO.text[0]}`;
}
}
}
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}`;
if (panelTextEl.getAttribute('visible')) { // テキストエンティティが表示される場合のみ値を設定
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
});
} else if (finalDisplayText.trim() !== "" && !(typeof currentCommentItem === 'object' && currentCommentItem.type === 'image' && !currentCommentItem.caption)) {
// 画像のみでキャプションなしの場合以外で、テキストが空でないなら表示試行(主にEXE_MODE=0で画像のみの場合のインジケータのため)
panelTextEl.setAttribute('troika-text', {
value: finalDisplayText, color: currentStyle.color, fontSize: PAGE_TEXT_STYLES['image_caption'].fontSize, // キャプションスタイルで
maxWidth: currentStyle.maxWidth, align: currentStyle.align, anchorX: currentStyle.anchorX,
anchorY: currentStyle.anchorY, baseline: currentStyle.baseline
});
panelTextEl.setAttribute('visible', true);
}
}
// --- オブジェクトクリック時の処理 (変更なし) ---
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 ? commentDataForThisObject.text.length : 1).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)); panelImageEl.setAttribute('visible', false); panelImageEl.removeAttribute('src'); 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-球に文字test62
├
<!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>
// --- SCRIPT BLOCK 1: Component Definitions ---
// --- プレイヤー移動制御用カスタムコンポーネント ('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; 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 },
initialAxis: { type: 'vec3', default: null }, // `default: null` を使うことで未指定を検出しやすく
initialSpeed: { type: 'number', default: NaN }
},
init: function () {
// initialAxis が有効なvec3オブジェクトか、またはnullでないかチェック
const axisIsDefined = this.data.initialAxis &&
typeof this.data.initialAxis.x === 'number' &&
typeof this.data.initialAxis.y === 'number' &&
typeof this.data.initialAxis.z === 'number';
if (axisIsDefined && !isNaN(this.data.initialSpeed)) {
this.axis = new THREE.Vector3(this.data.initialAxis.x, this.data.initialAxis.y, this.data.initialAxis.z);
if (this.axis.lengthSq() === 0) { this.axis.set(0,1,0); } // ゼロベクトルならY軸に
else { this.axis.normalize(); }
this.speedRad = THREE.MathUtils.degToRad(this.data.initialSpeed);
} else { // 個別設定がない、または不完全な場合はランダム
this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5);
if (this.axis.lengthSq() < 0.001) { this.axis.set(0.01, 1, 0.01); }
this.axis.normalize();
// initialSpeedだけ指定されている場合は、maxSpeedの代わりにそれを使う(軸はランダム)
const speedSource = !isNaN(this.data.initialSpeed) ? this.data.initialSpeed : (((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1));
this.speedRad = THREE.MathUtils.degToRad(speedSource);
}
this.deltaRotationQuaternion = new THREE.Quaternion();
},
tick: function (time, timeDelta) {
const dt = timeDelta / 1000;
const angleChangeRad = this.speedRad * dt;
this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angleChangeRad);
this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion);
this.el.object3D.quaternion.normalize();
}
});
// --- 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; 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/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' } };
// ★★★ オブジェクトごとの定義 (rotationSettings を追加) ★★★
const OBJECT_DEFINITIONS = {
1: {
type: 'sphere', useTextureForIndex: 1, sizeType: 'fixed', fixedSize: 3.0,
rotationSettings: { axis: { x: 0, y: 1, z: 0 }, speed: 30 } // Y軸中心に30度/秒
},
2: {
type: 'box', useTextureForIndex: 2, sizeType: 'random'
// rotationSettings の指定がない -> 配置パターンの回転設定に従う
},
3: {
type: 'sphere', specificColor: '#FF1493', sizeType: 'fixed', fixedSize: 4.0,
rotationSettings: { axis: { x: 1, y: 0, z: 1 }, speed: -15 } // XZ軸合成で逆回転
},
4: { // このオブジェクトはパターン2,4,6,8,10,12,14,16(回転なしパターン)では回転しない
type: 'box', useTextureForIndex: 4,
rotationSettings: { speed: 20 } // 軸はランダム(コンポーネント内で決定)、速度だけ指定
},
// 他のオブジェクト定義...
};
const DEFAULT_OBJECT_TYPE = 'box'; const DEFAULT_SIZE_TYPE = 'random'; const TEXTURE_ID_PREFIX = 'tex_a'; const MAX_TEXTURE_INDEX = 10;
const CUBE_COMMENTS = { /* ... (変更なし) ... */ };
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;
// --- オブジェクト生成ループ (random-rotateへのパラメータ渡し変更) ---
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 patternDefinesRotation = 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; patternDefinesRotation = true; break;
case 2: finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; patternDefinesRotation = 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; patternDefinesRotation = 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; patternDefinesRotation = 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) }; patternDefinesRotation = (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) }; patternDefinesRotation = (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_g = -totalGridWidth / 2; finalX = startX_g + 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}; patternDefinesRotation = (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}; patternDefinesRotation = (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) }; patternDefinesRotation = (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) }; patternDefinesRotation = (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; patternDefinesRotation = 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 (!patternDefinesRotation && !(definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number'))) { objectEl.setAttribute('rotation', '0 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;
// ★★★ random-rotate の適用条件変更 ★★★
const hasSpecificRotation = definition.rotationSettings && (definition.rotationSettings.axis || typeof definition.rotationSettings.speed === 'number');
if (patternDefinesRotation || hasSpecificRotation) {
let rotationParams = { maxSpeed: 5 }; // デフォルトランダム用
if (definition.rotationSettings) {
if (definition.rotationSettings.axis) {
rotationParams.initialAxis = definition.rotationSettings.axis;
}
if (typeof definition.rotationSettings.speed === 'number') {
rotationParams.initialSpeed = definition.rotationSettings.speed;
}
}
objectEl.setAttribute('random-rotate', rotationParams);
}
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 = ''; 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) { pageIndicator = `(${commentSubPageIndex + 1}/${commentSubPageCount})`; } else if (!(commentsArray[0] === DEFAULT_COMMENT_ARRAY_INFO.text[0] && commentSubPageCount === 1)) { /* 1ページのみで「コメントはありません」以外の場合、インジケータは不要なことが多いので空 */ } else { pageIndicator = ''; } if (mainCommentTextColor) { currentStyle.color = mainCommentTextColor; } } else { 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); }
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-球に文字test61
├
<!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>
// --- SCRIPT BLOCK 1: Component Definitions ---
// --- プレイヤー移動制御用カスタムコンポーネント ('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){console.warn("Could not remove leftHand thumbstickmoved listener cleanly.", e);} }
if (this.rightHand) { try { this.rightHand.removeEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); } catch(e){console.warn("Could not remove rightHand thumbstickmoved listener cleanly.", 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;
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 },
initialAxis: { type: 'vec3', default: null },
initialSpeed: { type: 'number', default: NaN }
},
init: function () {
if (this.data.initialAxis &&
typeof this.data.initialAxis.x === 'number' &&
typeof this.data.initialAxis.y === 'number' &&
typeof this.data.initialAxis.z === 'number' &&
!isNaN(this.data.initialSpeed)) {
this.axis = new THREE.Vector3(this.data.initialAxis.x, this.data.initialAxis.y, this.data.initialAxis.z);
if (this.axis.lengthSq() === 0) { this.axis.set(0,1,0); }
else { this.axis.normalize(); }
this.speedRad = THREE.MathUtils.degToRad(this.data.initialSpeed);
} else {
this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5);
if (this.axis.lengthSq() < 0.001) { this.axis.set(0.01, 1, 0.01); }
this.axis.normalize();
const speedDegPerSec = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1);
this.speedRad = THREE.MathUtils.degToRad(speedDegPerSec);
}
this.deltaRotationQuaternion = new THREE.Quaternion();
},
tick: function (time, timeDelta) {
const dt = timeDelta / 1000;
const angleChangeRad = this.speedRad * dt;
this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angleChangeRad);
this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion);
this.el.object3D.quaternion.normalize();
}
});
// --- 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; 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/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,
rotationSettings: { axis: { x: 0, y: 1, z: 0 }, speed: 15 } },
2: { type: 'box', useTextureForIndex: 2, sizeType: 'random' },
3: { type: 'sphere', specificColor: '#FF1493', sizeType: 'fixed', fixedSize: 4.0,
rotationSettings: { axis: {x:1, y:1, z:0}, speed: -10 } },
4: { type: 'box', useTextureForIndex: 4,
rotationSettings: { speed: 5 } },
};
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 = 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 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 applyRandomRotationComponent = 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; applyRandomRotationComponent = true; break;
case 2: finalX = (Math.random() - 0.5) * spread; finalY = (Math.random() - 0.5) * spread; finalZ = (Math.random() - 0.5) * spread; applyRandomRotationComponent = 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; applyRandomRotationComponent = 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; applyRandomRotationComponent = 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) }; applyRandomRotationComponent = (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) }; applyRandomRotationComponent = (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_g = -totalGridWidth / 2; finalX = startX_g + 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}; applyRandomRotationComponent = (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}; applyRandomRotationComponent = (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) }; applyRandomRotationComponent = (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) }; applyRandomRotationComponent = (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; applyRandomRotationComponent = 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 (!applyRandomRotationComponent) { 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 (applyRandomRotationComponent) { let rotationParams = { maxSpeed: 5 }; if (definition.rotationSettings) { if (definition.rotationSettings.axis) { rotationParams.initialAxis = definition.rotationSettings.axis; } if (typeof definition.rotationSettings.speed === 'number') { rotationParams.initialSpeed = definition.rotationSettings.speed; } } objectEl.setAttribute('random-rotate', rotationParams); } 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 = '';
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) { pageIndicator = `(${commentSubPageIndex + 1}/${commentSubPageCount})`; }
else { pageIndicator = ''; } // 「コメントはありません」で1ページのみの場合は(1/1)を表示しない
if (mainCommentTextColor) { currentStyle.color = mainCommentTextColor; }
} else {
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); }
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-球に文字test60
├
承知いたしました。キャッシュを無効化しても現象が変わらないとのこと、詳細なご報告ありがとうございます。
前回のGSUB/GPOSという標準的なフォント機能の線で解決しなかったこと、そして「漢字の後の『。』でのみ発生する」という非常に具体的な条件から、これはフォント機能の解釈ではなく、**Troikaの組版エンジン内に直接記述された、特殊な日本語組版向けの例外的なロジック**である可能性が極めて高いと考えられます。
再度コードを精査した結果、この特殊な条件分岐に合致する可能性のある、ただ一つの箇所が見つかりました。
### 原因の可能性が最も高い箇所
問題は、個々の文字(グリフ)を行に配置していくメインループの中で、**直前の文字のグリフが持つ縦方向のサイズ(高さ)** を見て、現在の文字「。」のベースラインを調整していると考えられます。
多くの日本語フォントでは、
* **漢字**:emスクエア(正方形の仮想ボディ)いっぱいにデザインされることが多い。
* **ひらがな**:漢字よりも小ぶりに、またベースライン寄りの低い位置にデザインされることが多い。
この「グリフの縦方向の大きさ」の違いを「漢字」か「ひらがな」かの判断基準として利用し、「。」の位置を動的に変更しているロジックが存在するようです。
この処理を行っているのは、以下の`forEachGlyph`のコールバック関数の中です。
```javascript
// この関数が、計算済みの各グリフを行に配置していくメインのループです
t.forEachGlyph(x, p, m, ((t, r, s, l) => {
r += V, l += e.start, S = r, k = t;
const u = i.charAt(l), // u が現在の文字 (例: "。")
h = t.advanceWidth * g,
d = q.count;
let y;
// ... (改行処理などのロジック) ...
// ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
// 【最重要】この直下で、直前の文字の高さに基づいて
// 現在の文字のY座標 's' が変更されている可能性があります。
//
// 元のmin.jsでは一行に圧縮されていて見つけにくいですが、
// ここに `if (u === '。' && [条件]) { s = ... }` のような
// コードが存在するはずです。
// ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
let x = q.glyphAt(q.count);
x.glyphObj = t,
x.x = r + N,
x.y = s, // <--- 最終的なY座標がここでセットされる
x.width = h,
x.charIndex = l,
x.fontData = v,
"\n" === u && (q = new f, X.push(q), N = -(r + h + m * p) + U)
})), V = S + k.advanceWidth * g + m * p}));
```
### 問題を特定するための最終手段:デバッグコードの挿入
ここまで来ると、コードを静的に解析するだけでは限界があります。そこで、この仮説が正しいかを確かめるために、**お客様ご自身でコード内にデバッグ用のログ出力処理を一時的に挿入していただく**のが最も確実な方法となります。
お手数ですが、以下の手順で`aframe-troika-text.min.js`ファイルを再度編集し、現象が起きるページでブラウザの開発者コンソールを開いてログを確認していただけないでしょうか。
1. `aframe-troika-text.min.js` をテキストエディタで開きます。
2. `x.glyphObj=t,x.x=r+N,x.y=s,` という部分を探してください。これはファイル内で一箇所しか存在しないはずです。(検索しやすいように前後の文字も記載しています)
3. その**直前**に、以下のデバッグ用コードを挿入します。
```javascript
// ===== ここからデバッグコード =====
if (u === '。' && d > 0) {
const prevGlyph = q.glyphAt(d - 1).glyphObj;
const prevChar = i.charAt(l - 1);
console.log(
`[。] の前の文字: "${prevChar}", ` +
`前のグリフのyMax: ${prevGlyph.yMax}, ` +
`Y座標(s): ${s}`
);
}
// ===== ここまでデバッグコード =====
```
4. ファイルを保存し、**ブラウザのキャッシュを完全に無効化して**ページをリロードします。
#### 修正後のコードのイメージ
```javascript
// ... (min.jsコードの一部)
/* ... */ let y; if("isEmpty"in t|| /* ... */) /* ... */ }
// ===== ここからデバッグコード =====
if (u === '。' && d > 0) {
const prevGlyph = q.glyphAt(d - 1).glyphObj;
const prevChar = i.charAt(l - 1);
console.log(
`[。] の前の文字: "${prevChar}", ` +
`前のグリフのyMax: ${prevGlyph.yMax}, ` +
`Y座標(s): ${s}`
);
}
// ===== ここまでデバッグコード =====
let x=q.glyphAt(q.count);x.glyphObj=t,x.x=r+N,x.y=s,x.width=h,x.charIndex=l,x.fontData=v,"\n"===u&& /* ... */
```
### コンソールログで確認すべきこと
開発者コンソールに、以下のようなログが出力されるはずです。
```
// ひらがなの後の「。」の場合
[。] の前の文字: "あ", 前のグリフのyMax: 750, Y座標(s): -19.2
// 漢字の後の「。」の場合
[。] の前の文字: "漢", 前のグリフのyMax: 950, Y座標(s): -28.5
```
もし、このログで\*\*「前のグリフのyMax」の値がひらがなと漢字で明確に異なり\*\*、かつ\*\*「Y座標(s)」の値もそれに伴って変化している\*\*場合、私の仮説(=直前のグリフの高さで位置を調整している)が正しいことになります。
その場合、このデバッグコードを挿入した箇所のすぐ近くに、`yMax`の値や文字コードを判定して `s` の値を変更している、圧縮されて読みにくいコードが存在するはずです。その特定ができれば、いよいよ根本的な修正が可能になります。
大変お手数をおかけしますが、このデバッグによる情報収集にご協力いただけますと幸いです。
だめだった↓
詳細な情報をありがとうございます。挙動がより明確になったことで、原因箇所をさらに絞り込むことができました。
「漢字の後に『。』が来た場合のみ、位置が中央に移動する」という挙動は、非常に高度な文字組版ルールであり、これはフォントに内蔵された\*\*文脈依存のグリフ置換(GSUB)**または**グリフ位置調整(GPOS)\*\*の機能によって実現されていると考えられます。
特に「前の文字が漢字(特定の文字クラス)の場合に、次の文字(。)を別のグリフ(中央配置されたもの)に置き換える」という処理は、GSUBの\*\*合字(Ligature)**や**文脈依存置換(Contextual Substitution)\*\*という機能で定義できます。
前回のGPOSの無効化で変化がなかったとのこと、承知いたしました。その場合、原因はGPOS(位置調整)ではなく、GSUB(グリフ自体の置き換え)である可能性が非常に高いです。
### 問題の箇所(再特定)
問題の箇所は、文字のグリフIDを解決し、フォントのGSUBテーブルに基づいてグリフを置き換える処理を行っている部分です。具体的には、Typesetterワーカー内で、`forEachGlyph`が呼び出される直前にある、以下の無名関数が該当します。
この関数は、与えられたテキストを解析し、フォントのGSUBルール(合字、異体字など)を適用して、最終的に描画されるべきグリフのID配列を生成します。漢字の後に「。」が来た場合に、ベースラインに沿った「。」のグリフを、中央配置された別の「。」のグリフに\*\*置換(Substitution)\*\*する処理がここで行われていると推測されます。
```javascript
// この関数がテキストを解析し、GSUBテーブルを適用してグリフIDの配列を生成します
p = function(t, r) {
const n = [];
// まず、各文字を基本的なグリフIDに変換
for (let a = 0; a < r.length; a++) {
const o = r.codePointAt(a);
o > 65535 && a++, n.push(e.U.codeToGlyph(t, o))
}
// ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
// ここからがGSUBテーブルの処理です。
// 「漢字の後に『。』が来た場合にグリフを置き換える」といった
// 文脈依存のルールがここで適用されている可能性が高いです。
// ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
const a = t.GSUB;
if (a) {
const {
lookupList: t,
featureList: o
} = a;
let l;
const u = /^(rlig|liga|mset|isol|init|fina|medi|half|pres|blws|ccmp)$/,
f = [];
o.forEach((a => {
if (u.test(a.tag))
for (let o = 0; o < a.tab.length; o++) {
if (f[a.tab[o]]) continue;
f[a.tab[o]] = !0;
const u = t[a.tab[o]],
c = /^(isol|init|fina|medi)$/.test(a.tag);
c && !l && (l = s(r));
// GSUBルールを適用するループ
for (let r = 0; r < n.length; r++) {
l && c && i[l[r]] !== a.tag || e.U._applySubs(n, r, u, t)
}
}
}))
}
// ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
return n // ルール適用後のグリフID配列を返す
}(t, a);
```
### なぜここだと考えられるか
1. **ひらがなと漢字の区別:** フォントの`GDEF`テーブルには、グリフを「漢字クラス」「ひらがなクラス」といったグループに分類する情報を含めることができます。GSUBの文脈依存ルールは、このクラス情報を使って「『漢字クラス』のグリフの後に『句点グリフ』が来たら、それを『中央配置の句点グリフ』に置き換える」という複雑な条件分岐を実現できます。ご報告いただいた現象と完全に一致します。
2. **GPOSでの失敗:** GPOSはあくまで「位置の微調整」であり、前回ここを無効化しても変化がなかったということは、問題が位置調整(-10px動かす等)ではなく、グリフ自体の「すり替え」(Aという形のグリフをBという形のグリフに変える)である可能性を示唆しています。その「すり替え」を行っているのがこのGSUBの処理ブロックです。
### 再度の検証のお願い
前回はGPOSのブロックを修正いただきましたが、今回はGSUBの処理を無効化して現象が変化するかどうか、ご確認いただけますでしょうか。
**修正案:**
上記のコードブロック内にある `if (a) { ... }` の `a` は `t.GSUB` を指します。この `if` 文の条件を偽にすることで、GSUBの処理全体をスキップできます。
min.jsファイルの中から `const a=t.GSUB;if(a){` という部分を探し、 `if(a)` を `if(false)` に書き換えてみてください。
**【重要】ブラウザのキャッシュについて**
JavaScriptファイルを修正しても、ブラウザに古いファイルがキャッシュされているために変更が反映されないことがよくあります。以下のいずれかの方法で、必ずキャッシュをクリアしてから再度お試しください。
* **スーパーリロード:** ページを開いた状態で `Ctrl + F5` (Windows) または `Cmd + Shift + R` (Mac) を押す。
* **開発者ツールでキャッシュを無効化:** ブラウザの開発者ツール(F12キー)を開き、「ネットワーク(Network)」タブにある「キャッシュを無効化(Disable cache)」にチェックを入れた状態でページをリロードする。
お手数ですが、こちらの修正案で再度お試しいただけますでしょうか。
taiyoukei-めも
├
<!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://p-bookmark.sakura.ne.jp/cgi-bin/smemo/js/aframe-troika-text-test2.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-test2
├
<!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
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 高機能情報パネル v2</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- プレイヤー移動制御用カスタムコンポーネント ('camera-relative-controls') ---
// (このコンポーネントの定義は前回から変更ありません)
AFRAME.registerComponent('camera-relative-controls', { schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true } }, init: function () { this.keys = {}; this.thumbstickInput = { x: 0, y: 0 }; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.cameraDirection = new THREE.Vector3(); this.cameraRight = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); } this.el.sceneEl.addEventListener('loaded', () => { this.leftHand = document.getElementById('leftHand'); if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); }, remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } catch(e) { console.warn("camera-relative-controls: スティックリスナー解除失敗の可能性あり"); } } }, onThumbstickMoved: function (evt) { this.thumbstickInput.x = evt.detail.x; this.thumbstickInput.y = evt.detail.y; }, tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); } if (Math.abs(this.thumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.thumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x); this.moveDirection.add(leftRight); } const isInputActive = this.moveDirection.lengthSq() > 0.0001; if (isInputActive) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; if (isInputActive) { let isOpposingInput = false; if (isCurrentlyMoving) { const dotProduct = this.currentVelocity.dot(this.moveDirection); if (dotProduct < -0.1) { isOpposingInput = true; } } if (isOpposingInput) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } }, onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } }, onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } } });
// --- 立方体をランダムに回転させるカスタムコンポーネント ('random-rotate') ---
// (このコンポーネントの定義は前回から変更ありません)
AFRAME.registerComponent('random-rotate', { schema: { maxSpeed: { type: 'number', default: 5 } }, init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); }, tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); } });
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;"></a-entity>
<a-entity id="rightHand"
oculus-touch-controls="hand: right; model: true;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;"
laser-controls="hand: right; model: false; lineColor: white; lineOpacity: 0.75"
>
</a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-cone id="prevButton" class="clickableButton" position="-20.0 0 0.1" rotation="0 0 90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-cone id="nextButton" class="clickableButton" position="20.0 0 0.1" rotation="0 0 -90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-sphere id="closeButton" class="clickableButton" position="24 11 0.1" radius="1.5" color="red" shader="flat"> </a-sphere>
</a-entity>
<script>
// --- グローバル変数とDOM要素の取得 ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
let leftHandEl = null; // 左手イベント用は一旦削除 (右手メインのため)
let rightHandEl = null; // ★★★ 右手コントローラーの要素を取得するため宣言 ★★★
// --- ★★★ パネルの表示オフセット距離を調整可能に ★★★ ---
const PANEL_OFFSET_FROM_OBJECT = 1.0; // 立方体の表面からパネルまでの距離 (例: 1.0メートル)
// --- 立方体ごとのコメントデータを定義 (コメントは文字列の配列として複数ページ対応) ---
const CUBE_COMMENTS = {
1: ["最初の立方体だ!", "何か秘密が隠されているかもしれない…"],
5: ["これは5番目の特別な立方体。", "長いコメントもこのように複数ページにわたって表示できます。", "素晴らしい発見だ!"],
10: ["ようこそ、宇宙の果てへ。"],
// 必要に応じて他の立方体のコメントを追加
// 例: 20: ["コメントページ1", "コメントページ2"],
};
const DEFAULT_COMMENT_ARRAY = ["コメントはありません"]; // コメント未設定時のデフォルト
// --- 定数定義 ---
const numSpheres = 60;
const spread = 2000;
const PAGES = ['index', 'color', 'size', 'comment']; // 'comment' ページを追加
const TOTAL_MAIN_PAGES = PAGES.length; // メインのページ数
// --- Three.jsベクトル (変更なし) ---
const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3();
// --- 立方体生成ループ (変更なし) ---
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// --- ★★★ 情報パネルの表示更新関数 (コメント複数ページ対応) ★★★ ---
function updatePanelDisplay() {
if (!infoPanelEl.dataset.sphereIndex) return;
const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10);
const color = infoPanelEl.dataset.color || 'N/A';
const size = infoPanelEl.dataset.size || 'N/A';
// コメントはJSON文字列として保存されているのでパースする
const commentsArray = JSON.parse(infoPanelEl.dataset.comments || JSON.stringify(DEFAULT_COMMENT_ARRAY));
const commentSubPageCount = parseInt(infoPanelEl.dataset.commentSubPageCount || '1', 10);
const commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10);
const mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[mainPageIndex];
let pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`;
if (pageType === 'index') { displayText = `立方体: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${color}`; }
else if (pageType === 'size') { displayText = `サイズ: ${size}`; }
else if (pageType === 'comment') {
displayText = `コメント:\n${commentsArray[commentSubPageIndex]}`;
// コメントページの場合はサブページ番号も表示
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`;
}
const finalDisplayText = `${pageIndicator}\n${displayText}`;
panelTextEl.setAttribute('troika-text', 'value', finalDisplayText);
}
// --- ★★★ 立方体クリック時の処理 (コメント複数ページ対応) ★★★ ---
function handleSphereClick(event) {
event.stopPropagation();
console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName);
const clickedCube = event.target;
if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) {
console.error("Cube data missing from dataset!", clickedCube.dataset); return;
}
console.log("Cube data found:", clickedCube.dataset);
infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex;
infoPanelEl.dataset.color = clickedCube.dataset.color;
infoPanelEl.dataset.size = clickedCube.dataset.size;
infoPanelEl.dataset.currentPageIndex = '0'; // メインページは最初にリセット
// コメントを取得して設定
const sphereIdx = parseInt(clickedCube.dataset.sphereIndex, 10);
const commentsForThisCube = CUBE_COMMENTS[sphereIdx] || DEFAULT_COMMENT_ARRAY;
infoPanelEl.dataset.comments = JSON.stringify(commentsForThisCube); // コメント配列を文字列で保存
infoPanelEl.dataset.commentSubPageCount = commentsForThisCube.length.toString();
infoPanelEl.dataset.commentSubPageIndex = '0'; // コメントサブページもリセット
console.log("Data (including comments) stored in panel dataset.");
try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); }
catch (e) { console.error("Error during updatePanelDisplay:", e); return; }
try {
clickedCube.object3D.getWorldPosition(targetWorldPosition);
cameraEl.object3D.getWorldPosition(cameraWorldPosition);
const cubeSide = parseFloat(clickedCube.dataset.size || 0);
// ★★★ パネルオフセット距離に新しい変数を使用 ★★★
const offsetDistance = cubeSide / 2 + PANEL_OFFSET_FROM_OBJECT;
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition);
console.log("Panel position calculated and applied.");
}
catch(e) { console.error("Error during position calculation:", e); return; }
infoPanelEl.setAttribute('visible', true);
console.log("Panel visibility set to true. --- handleSphereClick end ---");
}
// --- ★★★ パネルのボタンクリック処理 (コメント複数ページ対応) ★★★ ---
prevButtonEl.addEventListener('click', function (event) {
event.stopPropagation();
if (!infoPanelEl.getAttribute('visible')) return;
let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10);
const commentSubPageCount = parseInt(infoPanelEl.dataset.commentSubPageCount || '1', 10);
if (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1) {
// コメントページ内でサブページをめくる
commentSubPageIndex--;
if (commentSubPageIndex < 0) { // コメントの最初のサブページより前に戻ろうとした
mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES;
// 新しいメインページがコメントなら、そのコメントの最後のサブページに
if (PAGES[mainPageIndex] === 'comment') {
// このままだと新しい球のコメントではないので、次の球クリックまでコメントは変わらない
// handleSphereClickで選択された球のコメントがセットされる
// ここでは単純に新しいメインページのコメントの最後のサブページとする
// (実際には新しい球のコメントを再ロードする必要があるが、ここでは単純化)
const newCommentsArray = JSON.parse(infoPanelEl.dataset.comments || JSON.stringify(DEFAULT_COMMENT_ARRAY));
commentSubPageIndex = Math.max(0, newCommentsArray.length - 1);
} else {
commentSubPageIndex = 0; // 他のページならサブページは0
}
}
} else {
// メインページをめくる
mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES;
commentSubPageIndex = 0; // 他のページに移動したらコメントサブページはリセット
// もし新しいメインページがコメントなら、そのサブページ数を再設定(表示時に行われる)
}
infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString();
infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString();
updatePanelDisplay();
console.log("Prev button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex);
});
nextButtonEl.addEventListener('click', function (event) {
event.stopPropagation();
if (!infoPanelEl.getAttribute('visible')) return;
let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10);
const commentSubPageCount = parseInt(infoPanelEl.dataset.commentSubPageCount || '1', 10);
if (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1) {
// コメントページ内でサブページをめくる
commentSubPageIndex++;
if (commentSubPageIndex >= commentSubPageCount) { // コメントの最後のサブページより次に進もうとした
mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES;
commentSubPageIndex = 0; // 新しいメインページではコメントサブページはリセット
}
} else {
// メインページをめくる
mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES;
commentSubPageIndex = 0; // 他のページに移動したらコメントサブページはリセット
}
infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString();
infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString();
updatePanelDisplay();
console.log("Next button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex);
});
// --- ★★★ 閉じるボタンのクリック処理 (コメントデータ削除追加) ★★★ ---
closeButtonEl.addEventListener('click', function (event) {
event.stopPropagation();
infoPanelEl.setAttribute('visible', false);
delete infoPanelEl.dataset.sphereIndex;
delete infoPanelEl.dataset.color;
delete infoPanelEl.dataset.size;
delete infoPanelEl.dataset.currentPageIndex;
delete infoPanelEl.dataset.comments; // ★ コメント配列削除
delete infoPanelEl.dataset.commentSubPageIndex; // ★ サブページインデックス削除
delete infoPanelEl.dataset.commentSubPageCount; // ★ サブページ総数削除
console.log("Close button clicked, panel hidden.");
});
// --- VRモードによるマウスカーソル表示制御 (変更なし) ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
// --- 右手コントローラーのトリガーイベントをリッスン (デバッグ用) ---
sceneEl.addEventListener('loaded', function() {
rightHandEl = document.getElementById('rightHand'); // ★★★ 右手を取得 ★★★
if (rightHandEl) {
rightHandEl.addEventListener('triggerdown', function (evt) { console.log('Right hand TRIGGER DOWN event!', evt); const raycasterComponent = rightHandEl.components.raycaster; if (raycasterComponent) { const intersectedEls = raycasterComponent.intersectedEls; if (intersectedEls.length > 0) { console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id'); } else { console.log('Right Trigger pressed, but no intersection.'); } } });
rightHandEl.addEventListener('triggerup', function (evt) { console.log('Right hand TRIGGER UP event!', evt); });
} else { console.error("Could not find rightHand element to attach trigger listener."); }
});
// 背景クリックリスナーは削除済み
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test40
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 立方体コメント機能</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- カスタムコンポーネント定義 (変更なし) ---
AFRAME.registerComponent('camera-relative-controls', {
schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true } },
init: function () { this.keys = {}; this.thumbstickInput = { x: 0, y: 0 }; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.cameraDirection = new THREE.Vector3(); this.cameraRight = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); } this.el.sceneEl.addEventListener('loaded', () => { this.leftHand = document.getElementById('leftHand'); if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); },
remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } catch(e) { console.warn("camera-relative-controls: スティックリスナー解除失敗の可能性あり"); } } },
onThumbstickMoved: function (evt) { this.thumbstickInput.x = evt.detail.x; this.thumbstickInput.y = evt.detail.y; },
tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); } if (Math.abs(this.thumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.thumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x); this.moveDirection.add(leftRight); } const isInputActive = this.moveDirection.lengthSq() > 0.0001; if (isInputActive) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; if (isInputActive) { let isOpposingInput = false; if (isCurrentlyMoving) { const dotProduct = this.currentVelocity.dot(this.moveDirection); if (dotProduct < -0.1) { isOpposingInput = true; } } if (isOpposingInput) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } },
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// --- 立方体をランダムに回転させるカスタムコンポーネント ('random-rotate') ---
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); }
});
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;" raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" laser-controls="hand: left; model: false; lineColor: white; lineOpacity: 0.75" ></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;" ></a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-cone id="prevButton" class="clickableButton" position="-20.0 0 0.1" rotation="0 0 90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-cone id="nextButton" class="clickableButton" position="20.0 0 0.1" rotation="0 0 -90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-sphere id="closeButton" class="clickableButton" position="24 11 0.1" radius="1.5" color="red" shader="flat"> </a-sphere>
</a-entity>
<script>
// --- グローバル変数とDOM要素の取得 ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
// let rightHandEl = null; // 右手イベント用は前回削除済み
// --- ★★★ 立方体ごとのコメントデータを定義 ★★★ ---
// キーは立方体のインデックス (1始まり)、値はコメント文字列
const CUBE_COMMENTS = {
1: "これは最初の立方体です!\nユニークな発見があるかも?", // \n で改行できます
5: "特別な立方体その1。\n触れてくれてありがとう。",
10: "宇宙の果てへようこそ。",
25: "折り返し地点です。",
// 他の立方体のコメントもここに追加できます
// 例: 30: "30番目の立方体です。",
};
// --- ここまでコメントデータ ---
// --- 定数定義 ---
const numSpheres = 60;
const spread = 2000;
// ★★★ PAGES 配列に 'comment' を追加 ★★★
const PAGES = ['index', 'color', 'size', 'comment'];
const TOTAL_PAGES = PAGES.length; // 自動的に4になる
// --- Three.jsベクトル (変更なし) ---
const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3();
// --- 立方体生成ループ (変更なし) ---
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// --- ★★★ 情報パネルの表示更新関数 (コメント表示対応) ★★★ ---
function updatePanelDisplay() {
if (!infoPanelEl.dataset.sphereIndex) return; // 表示すべきデータがない場合は処理中断
// パネルに保存されたデータを取得
const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10);
const color = infoPanelEl.dataset.color || 'N/A';
const size = infoPanelEl.dataset.size || 'N/A';
const commentText = infoPanelEl.dataset.commentText || "コメントはありません"; // ★ コメント取得
const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[pageIndex]; // 現在表示すべきページタイプ
// ページタイプに応じて表示するメインテキストを決定
if (pageType === 'index') { displayText = `立方体: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${color}`; }
else if (pageType === 'size') { displayText = `サイズ: ${size}`; }
else if (pageType === 'comment') { displayText = `コメント:\n${commentText}`; } // ★ コメント表示
const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; // (X/4) のようなページ番号表示
const finalDisplayText = `${pageIndicator}\n${displayText}`; // 最終的な表示テキスト
// テキストエンティティに値を設定して更新
panelTextEl.setAttribute('troika-text', 'value', finalDisplayText);
}
// --- ★★★ 立方体クリック時の処理 (コメントデータ取得・保存追加) ★★★ ---
function handleSphereClick(event) {
event.stopPropagation();
console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName);
const clickedCube = event.target;
if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) {
console.error("Cube data missing from dataset!", clickedCube.dataset); return;
}
console.log("Cube data found:", clickedCube.dataset);
// クリックされた立方体の基本情報をパネルのデータ属性に保存
infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex;
infoPanelEl.dataset.color = clickedCube.dataset.color;
infoPanelEl.dataset.size = clickedCube.dataset.size;
infoPanelEl.dataset.currentPageIndex = '0'; // 最初のページを表示するようにリセット
// ★★★ コメントを取得してパネルのデータ属性に保存 ★★★
const sphereIdx = parseInt(clickedCube.dataset.sphereIndex, 10);
const comment = CUBE_COMMENTS[sphereIdx] || "コメントはありません"; // コメントがなければデフォルトメッセージ
infoPanelEl.dataset.commentText = comment;
// ★★★ ここまでコメント処理 ★★★
console.log("Data (including comment) stored in panel dataset.");
try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); }
catch (e) { console.error("Error during updatePanelDisplay:", e); return; }
try { clickedCube.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const cubeSide = parseFloat(clickedCube.dataset.size || 0); const offsetDistance = cubeSide / 2 + 0.5; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); }
catch(e) { console.error("Error during position calculation:", e); return; }
infoPanelEl.setAttribute('visible', true);
console.log("Panel visibility set to true. --- handleSphereClick end ---");
}
// --- パネルのボタンクリック処理 (変更なし) ---
prevButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex + 1) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex); });
// --- ★★★ 閉じるボタンのクリック処理 (コメントデータ削除追加) ★★★ ---
closeButtonEl.addEventListener('click', function (event) {
event.stopPropagation();
infoPanelEl.setAttribute('visible', false);
delete infoPanelEl.dataset.sphereIndex;
delete infoPanelEl.dataset.color;
delete infoPanelEl.dataset.size;
delete infoPanelEl.dataset.currentPageIndex;
delete infoPanelEl.dataset.commentText; // ★ コメントデータも削除
console.log("Close button clicked, panel hidden.");
});
// --- VRモードによるマウスカーソル表示制御 (変更なし) ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
// --- 右手コントローラーのトリガーイベントをリッスン (変更なし) ---
sceneEl.addEventListener('loaded', function() {
const leftHandForTriggerLog = document.getElementById('leftHand');
if (leftHandForTriggerLog) {
leftHandForTriggerLog.addEventListener('triggerdown', function (evt) { console.log('Left hand TRIGGER DOWN event!', evt); const raycasterComponent = leftHandForTriggerLog.components.raycaster; if (raycasterComponent) { const intersectedEls = raycasterComponent.intersectedEls; if (intersectedEls.length > 0) { console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id'); } else { console.log('Left Trigger pressed, but no intersection.'); } } });
leftHandForTriggerLog.addEventListener('triggerup', function (evt) { console.log('Left hand TRIGGER UP event!', evt); });
} else { console.error("Could not find leftHand element to attach trigger listener."); }
});
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test38
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 3Dボタン</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- プレイヤー移動制御用カスタムコンポーネント ('camera-relative-controls') ---
AFRAME.registerComponent('camera-relative-controls', {
schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true } },
init: function () { this.keys = {}; this.thumbstickInput = { x: 0, y: 0 }; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.cameraDirection = new THREE.Vector3(); this.cameraRight = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); } this.el.sceneEl.addEventListener('loaded', () => { this.leftHand = document.getElementById('leftHand'); if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); },
remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } catch(e) { console.warn("camera-relative-controls: スティックリスナー解除失敗の可能性あり"); } } },
onThumbstickMoved: function (evt) { this.thumbstickInput.x = evt.detail.x; this.thumbstickInput.y = evt.detail.y; },
tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); } if (Math.abs(this.thumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.thumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x); this.moveDirection.add(leftRight); } const isInputActive = this.moveDirection.lengthSq() > 0.0001; if (isInputActive) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; if (isInputActive) { let isOpposingInput = false; if (isCurrentlyMoving) { const dotProduct = this.currentVelocity.dot(this.moveDirection); if (dotProduct < -0.1) { isOpposingInput = true; } } if (isOpposingInput) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } },
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// --- 立方体をランダムに回転させるカスタムコンポーネント ('random-rotate') ---
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); }
});
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;" raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" laser-controls="hand: left; model: false; lineColor: white; lineOpacity: 0.75" ></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;" ></a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-cone id="prevButton" class="clickableButton"
position="-20.0 0 0.1" rotation="0 0 90"
radius-bottom="1.2"
radius-top="0"
height="2.0"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-cone>
<a-cone id="nextButton" class="clickableButton"
position="20.0 0 0.1" rotation="0 0 -90"
radius-bottom="1.2"
radius-top="0"
height="2.0"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-cone>
<a-sphere id="closeButton" class="clickableButton"
position="24 11 0.1" radius="1.5" color="red" shader="flat">
</a-sphere>
</a-entity>
<script>
// --- グローバル変数とDOM要素の取得 ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
let rightHandEl = null;
// --- 定数定義 ---
const numSpheres = 60; const spread = 2000; const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3(); const PAGES = ['index', 'color', 'size']; const TOTAL_PAGES = PAGES.length;
// --- 立方体生成ループ ---
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// --- 情報パネルの表示更新関数 ---
function updatePanelDisplay() { if (!infoPanelEl.dataset.sphereIndex) return; const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10); const color = infoPanelEl.dataset.color || 'N/A'; const size = infoPanelEl.dataset.size || 'N/A'; const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let displayText = ''; const pageType = PAGES[pageIndex]; if (pageType === 'index') { displayText = `立方体: ${index}`; } else if (pageType === 'color') { displayText = `色: ${color}`; } else if (pageType === 'size') { displayText = `サイズ: ${size}`; } const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', 'value', finalDisplayText); }
// --- 立方体クリック時の処理 ---
function handleSphereClick(event) { event.stopPropagation(); console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName); const clickedCube = event.target; if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) { console.error("Cube data missing from dataset!", clickedCube.dataset); return; } console.log("Cube data found:", clickedCube.dataset); infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex; infoPanelEl.dataset.color = clickedCube.dataset.color; infoPanelEl.dataset.size = clickedCube.dataset.size; infoPanelEl.dataset.currentPageIndex = '0'; console.log("Data stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedCube.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const cubeSide = parseFloat(clickedCube.dataset.size || 0); const offsetDistance = cubeSide / 2 + 0.5; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); } catch(e) { console.error("Error during position calculation:", e); return; } infoPanelEl.setAttribute('visible', true); console.log("Panel visibility set to true. --- handleSphereClick end ---"); }
// --- パネルのボタンクリック処理 ---
prevButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex + 1) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex); });
closeButtonEl.addEventListener('click', function (event) { event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex; console.log("Close button clicked, panel hidden."); });
// --- VRモードによるマウスカーソル表示制御 ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
// --- 右手コントローラーのトリガーイベントをリッスン (デバッグ用) ---
sceneEl.addEventListener('loaded', function() {
rightHandEl = document.getElementById('rightHand'); // This was for right hand, now interaction is left.
// We don't strictly need trigger logging if laser-controls works.
// Let's get leftHandEl for consistency if needed.
const leftHandForTriggerLog = document.getElementById('leftHand');
if (leftHandForTriggerLog) {
leftHandForTriggerLog.addEventListener('triggerdown', function (evt) {
console.log('Left hand TRIGGER DOWN event!', evt);
const raycasterComponent = leftHandForTriggerLog.components.raycaster;
if (raycasterComponent) {
const intersectedEls = raycasterComponent.intersectedEls;
if (intersectedEls.length > 0) {
console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id');
} else {
console.log('Left Trigger pressed, but no intersection.');
}
}
});
leftHandForTriggerLog.addEventListener('triggerup', function (evt) {
console.log('Left hand TRIGGER UP event!', evt);
});
} else {
console.error("Could not find leftHand element to attach trigger listener.");
}
});
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test37
├
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
</head>
<body style="background-color: black;">
<a-scene background="color: black">
<!-- カメラ(VR視点) -->
<a-entity camera position="0 1.6 0"></a-entity>
<!-- 左コントローラーを表示 -->
<a-entity
id="leftController"
tracked-controls="controller: oculus-touch; hand: left"
raycaster="objects: .clickable"
laser-controls="hand: left"
position="-0.3 1.4 -0.5">
</a-entity>
<!-- 立方体 -->
<a-box
class="clickable"
position="0 1 -3"
color="blue"></a-box>
<!-- クリック後に表示されるテキストウィンドウ -->
<a-entity id="textWindow" position="0 1 -2" visible="false">
<a-plane width="1.5" height="1" color="black">
<a-text value="こんにちは!" position="0 0 0.1" align="center"></a-text>
</a-plane>
</a-entity>
<script>
const cube = document.querySelector('.clickable');
const textWindow = document.getElementById('textWindow');
// コントローラーによるクリック
cube.addEventListener('click', () => {
textWindow.setAttribute('visible', true);
});
// PCマウスクリック対応
cube.addEventListener('mousedown', () => {
textWindow.setAttribute('visible', true);
});
</script>
</a-scene>
</body>
</html>
aframe-co-t2
├
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
</head>
<body style="background-color: black;">
<a-scene background="color: black">
<!-- カメラとコントローラー -->
<a-entity camera position="0 1.6 0">
<a-entity
id="leftController"
laser-controls="hand: left"
raycaster="objects: .clickable"></a-entity>
</a-entity>
<!-- 立方体 -->
<a-box
class="clickable"
position="0 1 -3"
color="blue"></a-box>
<!-- クリック後に表示されるテキストウィンドウ -->
<a-entity id="textWindow" position="0 1 -2" visible="false">
<a-plane width="1.5" height="1" color="black">
<a-text value="こんにちは!" position="0 0 0.1" align="center"></a-text>
</a-plane>
</a-entity>
<script>
const cube = document.querySelector('.clickable');
const textWindow = document.getElementById('textWindow');
// コントローラーによるクリック
cube.addEventListener('click', () => {
textWindow.setAttribute('visible', true);
});
// マウスクリック対応(PC用)
cube.addEventListener('mouseenter', () => {
cube.setAttribute('color', 'red'); // ホバー時に色変更
});
cube.addEventListener('mouseleave', () => {
cube.setAttribute('color', 'blue'); // 元の色に戻す
});
cube.addEventListener('mousedown', () => {
textWindow.setAttribute('visible', true);
});
</script>
</a-scene>
</body>
</html>
aframe-co-t1
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 左手VRコントローラー操作</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- プレイヤー移動制御用カスタムコンポーネント ('camera-relative-controls') ---
// (このコンポーネントの定義は前回から変更ありません。コメントは主要な部分に絞ります)
AFRAME.registerComponent('camera-relative-controls', {
schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true } },
init: function () { this.keys = {}; this.thumbstickInput = { x: 0, y: 0 }; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.cameraDirection = new THREE.Vector3(); this.cameraRight = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); } this.el.sceneEl.addEventListener('loaded', () => { this.leftHand = document.getElementById('leftHand'); if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); },
remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } catch(e) { console.warn("camera-relative-controls: スティックリスナー解除失敗の可能性あり"); } } },
onThumbstickMoved: function (evt) { this.thumbstickInput.x = evt.detail.x; this.thumbstickInput.y = evt.detail.y; },
tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); } if (Math.abs(this.thumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.thumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x); this.moveDirection.add(leftRight); } const isInputActive = this.moveDirection.lengthSq() > 0.0001; if (isInputActive) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; if (isInputActive) { let isOpposingInput = false; if (isCurrentlyMoving) { const dotProduct = this.currentVelocity.dot(this.moveDirection); if (dotProduct < -0.1) { isOpposingInput = true; } } if (isOpposingInput) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } },
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// --- 立方体をランダムに回転させるカスタムコンポーネント ('random-rotate') ---
// (このコンポーネントの定義は前回から変更ありません)
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); }
});
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor"
cursor="rayOrigin: mouse; fuse: false;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000;"
position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;"
material="color: black; shader: flat; opacity: 0.7">
<a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation>
</a-entity>
</a-entity>
<a-entity id="leftHand"
oculus-touch-controls="hand: left; model: true;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;"
laser-controls="hand: left; model: false; lineColor: white; lineOpacity: 0.75"
>
</a-entity>
<a-entity id="rightHand"
oculus-touch-controls="hand: right; model: true;"
>
</a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-triangle id="prevButton" class="clickableButton" position="-20.0 0 0.01" rotation="0 0 90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-triangle>
<a-triangle id="nextButton" class="clickableButton" position="20.0 0 0.01" rotation="0 0 -90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-triangle>
<a-circle id="closeButton" class="clickableButton" position="24 11 0.05" radius="1.5" color="red" shader="flat"> </a-circle>
</a-entity>
<script>
// --- グローバル変数とDOM要素の取得 ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
// let rightHandEl = null; // 右手イベント用は一旦削除 (左手メインのため)
// --- 定数定義 (変更なし) ---
const numSpheres = 60; const spread = 2000; const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3(); const PAGES = ['index', 'color', 'size']; const TOTAL_PAGES = PAGES.length;
// --- 立方体生成ループ (変更なし) ---
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// --- 情報パネルの表示更新関数 (変更なし) ---
function updatePanelDisplay() { if (!infoPanelEl.dataset.sphereIndex) return; const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10); const color = infoPanelEl.dataset.color || 'N/A'; const size = infoPanelEl.dataset.size || 'N/A'; const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let displayText = ''; const pageType = PAGES[pageIndex]; if (pageType === 'index') { displayText = `立方体: ${index}`; } else if (pageType === 'color') { displayText = `色: ${color}`; } else if (pageType === 'size') { displayText = `サイズ: ${size}`; } const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', 'value', finalDisplayText); }
// --- 立方体クリック時の処理 (変更なし) ---
function handleSphereClick(event) { event.stopPropagation(); console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName); const clickedCube = event.target; if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) { console.error("Cube data missing from dataset!", clickedCube.dataset); return; } console.log("Cube data found:", clickedCube.dataset); infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex; infoPanelEl.dataset.color = clickedCube.dataset.color; infoPanelEl.dataset.size = clickedCube.dataset.size; infoPanelEl.dataset.currentPageIndex = '0'; console.log("Data stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedCube.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const cubeSide = parseFloat(clickedCube.dataset.size || 0); const offsetDistance = cubeSide / 2 + 0.5; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); } catch(e) { console.error("Error during position calculation:", e); return; } infoPanelEl.setAttribute('visible', true); console.log("Panel visibility set to true. --- handleSphereClick end ---"); }
// --- パネルのボタンクリック処理 (変更なし) ---
prevButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex + 1) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex); });
closeButtonEl.addEventListener('click', function (event) { event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex; console.log("Close button clicked, panel hidden."); });
// --- VRモードによるマウスカーソル表示制御 (変更なし) ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
// 右手トリガーイベントのリスナーは、laser-controlsがクリックを処理するので一旦削除
// sceneEl.addEventListener('loaded', function() { ... });
// 背景クリックリスナーは削除済み
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test36
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - VR対応 詳細コメント版</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- プレイヤー移動制御用カスタムコンポーネント ('camera-relative-controls') ---
// このコンポーネントは、カメラ(視点)の向きを基準とした移動(前後左右)を処理します。
// キーボード入力とVRコントローラーのサムスティック入力に対応し、慣性やブレーキも考慮します。
AFRAME.registerComponent('camera-relative-controls', {
// コンポーネントが受け付けるパラメータとそのデフォルト値を定義
schema: {
targetSpeed: { type: 'number', default: 5 }, // 目標とする最高速度 (m/s)
acceleration: { type: 'number', default: 10 }, // 加速の度合い (値が大きいほど速く最高速に達する)
damping: { type: 'number', default: 8 }, // 自然減速の度合い (入力がない時。値が大きいほど早く止まる)
brakingDeceleration: { type: 'number', default: 20 },// 逆キー入力時のブレーキの度合い (値が大きいほど早く止まる)
enabled: { type: 'boolean', default: true } // このコンポーネントの動作を有効/無効にするフラグ
},
// コンポーネント初期化時に一度だけ呼び出される関数
init: function () {
// --- 各種プロパティの初期化 ---
this.keys = {}; // 押されているキーボードのキーを記録するためのオブジェクト
this.thumbstickInput = { x: 0, y: 0 }; // VRコントローラーのサムスティックの入力値 (x:左右, y:前後)
// Three.jsのベクトルオブジェクトを事前に生成 (パフォーマンス向上のため、毎フレーム生成するのを避ける)
this.currentVelocity = new THREE.Vector3(); // プレイヤーの現在の速度ベクトル
this.ZERO_VECTOR = new THREE.Vector3(0, 0, 0); // 比較やリセット用のゼロベクトル
this.cameraDirection = new THREE.Vector3(); // カメラの前方を示すワールド座標系ベクトル
this.cameraRight = new THREE.Vector3(); // カメラの右方向を示すワールド座標系ベクトル
this.moveDirection = new THREE.Vector3(); // 最終的な移動方向を示すベクトル (入力とカメラ向きから計算)
this.desiredVelocity = new THREE.Vector3(); // 目標とする速度ベクトル (この速度に向かって現在の速度が変化)
this.cameraWorldQuaternion = new THREE.Quaternion(); // カメラのワールド空間での回転を格納するクォータニオン
this.cameraEl = this.el.querySelector('[camera]'); // このコンポーネントがアタッチされたエンティティ(rig)の子要素からカメラを取得
this.isReady = false; // カメラやシーンの準備が完了したかを示すフラグ
if (!this.cameraEl) {
console.error('camera-relative-controls: カメラエンティティが見つかりません。rigの子要素にcameraコンポーネントを持つエンティティを配置してください。');
}
// --- VRコントローラーのセットアップ ---
// シーンの 'loaded' イベントを待ってからコントローラーを取得・設定 (DOMの準備完了を待つため)
this.el.sceneEl.addEventListener('loaded', () => {
this.leftHand = document.getElementById('leftHand'); // 左手コントローラーのエンティティを取得
if (this.leftHand) {
// 左手スティックの 'thumbstickmoved' イベントに関数を紐付け
this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} else {
console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。スティック移動は無効になります。");
}
});
// --- キーボードイベントリスナーのセットアップ ---
// thisを束縛して、イベントリスナー内でコンポーネントのプロパティにアクセスできるようにする
this.onKeyDown = this.onKeyDown.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
window.addEventListener('keydown', this.onKeyDown); // キーが押された時のイベント
window.addEventListener('keyup', this.onKeyUp); // キーが離された時のイベント
},
// コンポーネントがエンティティから削除される時の処理 (主にイベントリスナーの解除)
remove: function () {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
if (this.leftHand) {
try {
// bindされた関数を解除するには、bind後の関数を保存しておく必要があるが、ここでは簡易的に試行
this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} catch(e) {
console.warn("camera-relative-controls: スティックイベントリスナーの解除に失敗した可能性があります。");
}
}
},
// 左手コントローラーのサムスティックが動いた時に呼び出される関数
onThumbstickMoved: function (evt) {
// スティックのX軸(左右)とY軸(前後)の入力値 (-1.0 から 1.0 の範囲) を保存
this.thumbstickInput.x = evt.detail.x;
this.thumbstickInput.y = evt.detail.y;
},
// 毎フレーム呼び出される関数 (移動計算の主要ロジック)
tick: function (time, timeDelta) {
if (!this.data.enabled) return; // enabledがfalseなら何もしない
// カメラなどの準備が完了するまで待機 (主に初回フレーム用)
if (!this.isReady) {
if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) {
this.isReady = true; // 準備完了とみなす
} else {
if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } // 念のため再取得
return; // 準備ができていなければ、このフレームの処理は中断
}
}
// isReadyになった後も、万が一カメラがなければ処理中断
if (!this.cameraEl || !this.cameraEl.object3D) { return; }
const el = this.el; // このコンポーネントがアタッチされているエンティティ (通常はrig)
const data = this.data; // schemaで定義されたパラメータ (targetSpeedなど)
const position = el.object3D.position; // rigの現在のワールド座標
const dt = timeDelta / 1000; // 前フレームからの経過時間 (ミリ秒を秒に変換)
// --- カメラの現在の向きから、ワールド座標系での前方ベクトルと右方向ベクトルを計算 ---
this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); // カメラのワールド回転を取得
// 前方ベクトル: カメラのローカル座標系での前方(-Z)をワールド回転で変換
this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion);
if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); // 正規化 (長さを1に)
// 右方向ベクトル: カメラのローカル座標系での右(+X)をワールド回転で変換
this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion);
this.cameraRight.y = 0; // 上下方向の傾きは無視して水平な右方向を維持
if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize(); // 正規化
// --- 方向ベクトル計算ここまで ---
// --- キーボードとサムスティックの入力に基づいて、目標とする移動方向を決定 ---
this.moveDirection.set(0, 0, 0); // フレームごとに移動方向ベクトルをリセット
// キーボード入力の処理
if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); } // 前進
if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); } // 後退
if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); } // 左平行移動
if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); } // 右平行移動
// VRサムスティック入力の処理 (Y軸は上下反転していることが多いため、-1を掛ける)
if (Math.abs(this.thumbstickInput.y) > 0.1) { // スティックの遊び(デッドゾーン)を考慮
const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y);
this.moveDirection.add(forwardBackward);
}
if (Math.abs(this.thumbstickInput.x) > 0.1) { // スティックの遊び(デッドゾーン)を考慮
const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x);
this.moveDirection.add(leftRight);
}
// --- 移動方向決定ここまで ---
const isInputActive = this.moveDirection.lengthSq() > 0.0001; // 何らかの入力があるか
if (isInputActive) {
this.moveDirection.normalize(); // 移動方向ベクトルを正規化 (斜め移動が速くなりすぎないように)
}
// --- 目標速度 (desiredVelocity) と補間係数 (lerpFactor) の決定 ---
// この部分で慣性(加速・減速)とブレーキの挙動を制御
let lerpFactor = data.damping; // デフォルトは自然減速時の係数
const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; // 現在、ある程度の速度で動いているか
if (isInputActive) { // 何らかの移動入力がある場合
let isOpposingInput = false; // 現在の移動方向と逆向きの入力かどうかのフラグ
if (isCurrentlyMoving) {
// 現在の速度ベクトルと入力方向ベクトルの内積を計算
const dotProduct = this.currentVelocity.dot(this.moveDirection);
if (dotProduct < -0.1) { // 内積が十分に負なら逆向き入力と判断 (値を調整することでブレーキ感度変更可)
isOpposingInput = true;
}
}
if (isOpposingInput) { // 逆向きの入力があった場合 (ブレーキ)
this.desiredVelocity.copy(this.ZERO_VECTOR); // 目標速度をゼロに設定
lerpFactor = data.brakingDeceleration; // ブレーキ用の減速係数を使用
} else { // 順方向または停止からの入力の場合 (加速)
this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); // 目標速度を計算
lerpFactor = data.acceleration; // 加速用の係数を使用
}
} else { // 移動入力がない場合 (自然減速)
this.desiredVelocity.copy(this.ZERO_VECTOR); // 目標速度をゼロに設定
lerpFactor = data.damping; // 自然減速用の係数を使用
}
// --- 目標速度と補間係数決定ここまで ---
// --- 現在の速度 (currentVelocity) を目標速度 (desiredVelocity) に向けて滑らかに補間 (Lerp) ---
// dt (フレーム時間) を考慮して、フレームレートに依存しにくい補間係数を計算
const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt);
this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor);
// --- 速度補間ここまで ---
// --- 位置の更新 ---
// 速度が非常に小さい場合は完全に停止させる (微小なドリフト/滑りを防ぐ)
if (this.currentVelocity.lengthSq() < 0.0001) {
this.currentVelocity.copy(this.ZERO_VECTOR);
}
// 速度があれば、その速度と経過時間に基づいてrigの位置を移動
if (this.currentVelocity.lengthSq() > 0) {
const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt);
position.add(deltaPosition);
}
// --- 位置更新ここまで ---
},
// キーボードのキーが押された時に呼び出される関数
onKeyDown: function (event) {
if (!this.data.enabled) { return; } // コンポーネントが無効なら何もしない
// WASDまたは矢印キーが押されたら、keysオブジェクトに記録
if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) {
this.keys[event.code] = true;
}
},
// キーボードのキーが離された時に呼び出される関数
onKeyUp: function (event) {
// keysオブジェクトから該当キーの記録を削除
if (this.keys[event.code] !== undefined) {
delete this.keys[event.code];
}
}
});
// --- ここまでカメラ移動コンポーネントの定義 ---
// --- 立方体をランダムに回転させるカスタムコンポーネント ('random-rotate') ---
AFRAME.registerComponent('random-rotate', {
// パラメータ定義
schema: {
maxSpeed: { type: 'number', default: 5 } // 最大回転速度 (度/秒)
},
// 初期化処理
init: function () {
// ランダムな回転軸を生成 (x, y, z 成分が -0.5 から 0.5 のベクトルを作り正規化)
this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize();
// 回転速度をランダムに決定 (0.2*maxSpeed 〜 maxSpeed の範囲で、回転方向もランダム)
const speedDegPerSec = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1);
this.speedRadPerSec = THREE.MathUtils.degToRad(speedDegPerSec); // ラジアン/秒に変換
this.deltaRotationQuaternion = new THREE.Quaternion(); // フレーム毎の回転量を格納するクォータニオン
},
// フレーム毎の処理
tick: function (time, timeDelta) {
const dt = timeDelta / 1000; // 経過時間 (秒)
const angleChangeRad = this.speedRadPerSec * dt; // このフレームでの回転角度(ラジアン)
// 回転軸と角度から、このフレームでの回転を表すクォータニオンを計算
this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angleChangeRad);
// エンティティの現在の回転に、このフレームの回転を乗算して適用
this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion);
}
});
// --- ここまでランダム回転コンポーネントの定義 ---
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor"
cursor="rayOrigin: mouse; fuse: false;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000;"
position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;"
material="color: black; shader: flat; opacity: 0.7">
<a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation>
</a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" cursor="rayOrigin: entity; fuse: false;" line="color: red; opacity: 1.0; length: 10" >
</a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
<a-triangle id="prevButton" class="clickableButton" position="-20.0 0 0.01" rotation="0 0 90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"></a-triangle>
<a-triangle id="nextButton" class="clickableButton" position="20.0 0 0.01" rotation="0 0 -90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"></a-triangle>
<a-circle id="closeButton" class="clickableButton" position="24 11 0.05" radius="1.5" color="red" shader="flat"></a-circle>
</a-entity>
<script>
// --- グローバル変数とDOM要素の取得 ---
const sceneEl = document.getElementById('myScene'); // シーン全体
const infoPanelEl = document.getElementById('infoPanel'); // 情報パネル
const panelTextEl = document.getElementById('panelText'); // 情報パネル内のテキスト表示部分
const cameraEl = document.getElementById('camera'); // カメラ
const prevButtonEl = document.getElementById('prevButton'); // 「前へ」ボタン
const nextButtonEl = document.getElementById('nextButton'); // 「次へ」ボタン
const closeButtonEl = document.getElementById('closeButton'); // 「閉じる」ボタン
const mouseCursorEl = document.getElementById('mouseCursor'); // デスクトップ用マウスカーソル
let rightHandEl = null; // 右手コントローラー (シーン読み込み後に取得)
// --- 定数定義 ---
const numSpheres = 60; // 生成する立方体の数 (変数名はsphereだが実体はcube)
const spread = 2000; // 立方体が広がる範囲
const PAGES = ['index', 'color', 'size']; // 情報パネルのページ種別
const TOTAL_PAGES = PAGES.length; // 総ページ数
// --- Three.jsベクトル (座標や方向の計算に再利用) ---
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3(); // 主にパネル位置計算時の方向ベクトルとして使用
const panelPosition = new THREE.Vector3();
// --- グローバル変数・定数ここまで ---
// --- 立方体(旧球体)の生成ループ ---
for (let i = 0; i < numSpheres; i++) {
const cubeEl = document.createElement('a-box'); // 立方体を生成
const side = Math.random() * 10.0 + 0.5; // 辺の長さ (0.5〜10.5のランダム)
// XYZ座標をランダムに決定
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + side / 2; // Y座標 (地面より少し上)
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 50%, 75%)`; // パステル調のランダムな色
const sphereIndex = i + 1; // 通し番号 (変数名はsphereだが実体はcube)
// 立方体の各属性を設定
cubeEl.setAttribute('width', side);
cubeEl.setAttribute('height', side);
cubeEl.setAttribute('depth', side);
cubeEl.setAttribute('color', color);
cubeEl.setAttribute('position', { x: x, y: y, z: z });
cubeEl.classList.add('clickableObject'); // クリック対象とするためのクラス名
// データ属性に情報を格納 (情報パネル表示用)
cubeEl.dataset.sphereIndex = sphereIndex;
cubeEl.dataset.color = color;
cubeEl.dataset.size = side.toFixed(2); // サイズ情報を保存
// ランダム回転コンポーネントをアタッチ
cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); // 最大回転速度5度/秒
// クリックイベントリスナーを設定
cubeEl.addEventListener('click', handleSphereClick);
sceneEl.appendChild(cubeEl); // シーンに立方体を追加
}
// --- 立方体生成ここまで ---
// --- 情報パネルの表示内容を更新する関数 ---
function updatePanelDisplay() {
// パネルに表示すべきデータがなければ何もしない
if (!infoPanelEl.dataset.sphereIndex) return;
// パネルに保存されたデータを取得
const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10);
const color = infoPanelEl.dataset.color || 'N/A';
const size = infoPanelEl.dataset.size || 'N/A';
const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = ''; // 表示するメインテキスト
const pageType = PAGES[pageIndex]; // 現在のページタイプ (index, color, size)
// ページタイプに応じて表示テキストを組み立て
if (pageType === 'index') { displayText = `立方体: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${color}`; }
else if (pageType === 'size') { displayText = `サイズ: ${size}`; }
const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; // (1/3)のようなページ表示
const finalDisplayText = `${pageIndicator}\n${displayText}`; // 最終的な表示テキスト
// パネルのテキストエンティティに値を設定
panelTextEl.setAttribute('troika-text', 'value', finalDisplayText);
}
// --- パネル表示更新ここまで ---
// --- 立方体クリック時に呼び出される処理 ---
function handleSphereClick(event) {
event.stopPropagation(); // クリックイベントが親要素へ伝播するのを防ぐ
console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName);
const clickedCube = event.target; // クリックされた立方体要素
// 必要なデータが立方体に設定されているか確認
if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) {
console.error("Cube data missing from dataset!", clickedCube.dataset); return;
}
console.log("Cube data found:", clickedCube.dataset);
// クリックされた立方体の情報を情報パネルのデータ属性に保存
infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex;
infoPanelEl.dataset.color = clickedCube.dataset.color;
infoPanelEl.dataset.size = clickedCube.dataset.size;
infoPanelEl.dataset.currentPageIndex = '0'; // 最初のページを表示するようにリセット
console.log("Data stored in panel dataset.");
// パネルの表示内容を更新
try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); }
catch (e) { console.error("Error during updatePanelDisplay:", e); return; }
// パネルの表示位置を計算して設定
try {
clickedCube.object3D.getWorldPosition(targetWorldPosition); // クリックされた立方体のワールド座標
cameraEl.object3D.getWorldPosition(cameraWorldPosition); // カメラのワールド座標
const cubeSide = parseFloat(clickedCube.dataset.size || 0); // 立方体のサイズ
const offsetDistance = cubeSide / 2 + 0.5; // 立方体の表面から少し離れた距離
// 立方体からカメラへ向かう方向ベクトルを計算
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
// パネルの位置 = 立方体の中心 + (立方体からカメラへの方向 * オフセット距離)
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition); // 計算した位置をパネルに適用
console.log("Panel position calculated and applied.");
}
catch(e) { console.error("Error during position calculation:", e); return; }
// パネルを表示状態にする
infoPanelEl.setAttribute('visible', true);
console.log("Panel visibility set to true. --- handleSphereClick end ---");
}
// --- 立方体クリック処理ここまで ---
// --- 情報パネルの「前へ」ボタンクリック処理 ---
prevButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); // イベント伝播停止
if (!infoPanelEl.getAttribute('visible')) return; // パネル非表示時は何もしない
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES; // 前のページへ (循環)
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay(); // 表示更新
console.log("Prev button clicked, page index:", pageIndex);
});
// --- 「前へ」ボタン処理ここまで ---
// --- 情報パネルの「次へ」ボタンクリック処理 ---
nextButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); // イベント伝播停止
if (!infoPanelEl.getAttribute('visible')) return; // パネル非表示時は何もしない
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex + 1) % TOTAL_PAGES; // 次のページへ (循環)
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay(); // 表示更新
console.log("Next button clicked, page index:", pageIndex);
});
// --- 「次へ」ボタン処理ここまで ---
// --- 情報パネルの「閉じる」ボタンクリック処理 ---
closeButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); // イベント伝播停止
infoPanelEl.setAttribute('visible', false); // パネルを非表示に
// パネルに保存していたデータを削除
delete infoPanelEl.dataset.sphereIndex;
delete infoPanelEl.dataset.color;
delete infoPanelEl.dataset.size;
delete infoPanelEl.dataset.currentPageIndex;
console.log("Close button clicked, panel hidden.");
});
// --- 「閉じる」ボタン処理ここまで ---
// --- VRモードへの出入りでマウスカーソルの表示/非表示を制御 ---
sceneEl.addEventListener('enter-vr', function () {
console.log("Entered VR mode");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); // VRモードではマウスカーソルを隠す
});
sceneEl.addEventListener('exit-vr', function () {
console.log("Exited VR mode");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); // VRモードを抜けたらマウスカーソルを表示
});
// --- マウスカーソル制御ここまで ---
// --- 右手コントローラーのトリガーイベントをリッスン (デバッグ用) ---
// シーンが完全に読み込まれてからコントローラー要素を取得しリスナーを設定
sceneEl.addEventListener('loaded', function() {
rightHandEl = document.getElementById('rightHand'); // ここで取得
if (rightHandEl) {
rightHandEl.addEventListener('triggerdown', function (evt) {
console.log('Right hand TRIGGER DOWN event!', evt);
// Raycasterが何かと交差しているか確認
const raycasterComponent = rightHandEl.components.raycaster;
if (raycasterComponent) {
const intersectedEls = raycasterComponent.intersectedEls;
if (intersectedEls.length > 0) {
console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id');
// 必要であれば、ここで手動でクリックイベントを発行することもできます:
// intersectedEls[0].dispatchEvent(new CustomEvent('click', {bubbles: true}));
} else {
console.log('Trigger pressed, but no intersection.');
}
}
});
rightHandEl.addEventListener('triggerup', function (evt) {
console.log('Right hand TRIGGER UP event!', evt);
});
} else {
console.error("Could not find rightHand element to attach trigger listener.");
}
});
// --- 右手トリガーイベントここまで ---
// 背景クリックリスナーは以前の修正で削除済み
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test35
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - VR対応 回転立方体 (完全版)</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- ★★★ カメラ移動コンポーネント ('camera-relative-controls') - 完全版 ★★★ ---
AFRAME.registerComponent('camera-relative-controls', {
// スキーマ定義 (ブレーキ減速を含む)
schema: {
targetSpeed: { type: 'number', default: 5 }, // 目標とする最高速度 (m/s)
acceleration: { type: 'number', default: 10 }, // 加速の度合い
damping: { type: 'number', default: 8 }, // 自然減速の度合い (キー入力がない時)
brakingDeceleration: { type: 'number', default: 20 },// 逆キー入力時のブレーキの度合い
enabled: { type: 'boolean', default: true } // このコンポーネントが有効か
},
// コンポーネント初期化時の処理
init: function () {
this.keys = {}; // 押されているキーボードのキーを記録するオブジェクト
this.thumbstickInput = { x: 0, y: 0 }; // VRコントローラのサムスティック入力値
// 各種ベクトルを事前に生成して再利用 (パフォーマンスのため)
this.currentVelocity = new THREE.Vector3(); // 現在の速度ベクトル
this.ZERO_VECTOR = new THREE.Vector3(); // ゼロベクトル (停止状態の目標速度)
this.cameraDirection = new THREE.Vector3(); // カメラの前方ベクトル (ワールド座標系)
this.cameraRight = new THREE.Vector3(); // カメラの右方向ベクトル (ワールド座標系)
this.moveDirection = new THREE.Vector3(); // キー入力やスティック入力に基づく最終的な移動方向
this.desiredVelocity = new THREE.Vector3(); // 目標とする速度ベクトル
this.cameraWorldQuaternion = new THREE.Quaternion(); // カメラのワールド回転
this.cameraEl = this.el.querySelector('[camera]'); // カメラエンティティへの参照
this.isReady = false; // カメラなどの準備ができたかどうかのフラグ
if (!this.cameraEl) {
console.error('camera-relative-controls requires a child entity with the [camera] component.');
}
// --- VRコントローラーのセットアップ ---
// 少し待ってからコントローラーを取得・設定 (シーンの準備完了後の方が確実)
this.el.sceneEl.addEventListener('loaded', () => {
this.leftHand = document.getElementById('leftHand'); // 左手コントローラーのエンティティ
if (this.leftHand) {
// 左手スティックの動きを監視
this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} else {
console.warn("Left hand controller (leftHand) not found for thumbstick movement.");
}
});
// --- キーボードイベントリスナーのセットアップ ---
this.onKeyDown = this.onKeyDown.bind(this); // thisの束縛
this.onKeyUp = this.onKeyUp.bind(this); // thisの束縛
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
},
// コンポーネント削除時の処理
remove: function () {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
// リスナーを削除する際も leftHand が存在するか確認
if (this.leftHand) {
try {
this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} catch(e) {
// removeEventListener が失敗してもエラーを出さないようにする (bindされた関数が原因の場合)
// より堅牢にするなら、bindした関数をプロパティに保存しておく
console.log("Could not remove thumbstickmoved listener cleanly.");
}
}
},
// 左手コントローラーのサムスティックが動いた時の処理
onThumbstickMoved: function (evt) {
// スティックのX軸(左右)とY軸(前後)の入力値を取得
this.thumbstickInput.x = evt.detail.x;
this.thumbstickInput.y = evt.detail.y;
},
// 毎フレーム呼び出される処理 (移動計算のコアロジック)
tick: function (time, timeDelta) {
if (!this.data.enabled) return; // 無効なら何もしない
// カメラの準備ができるまで待つ (初回のみ)
if (!this.isReady) {
if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) {
this.isReady = true;
} else {
if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } // 再度取得試行
return; // まだ準備できていない
}
}
// cameraEl や object3D がまだ存在しない場合は早期リターン
if (!this.cameraEl || !this.cameraEl.object3D) { return; }
const el = this.el; // このコンポーネントがアタッチされているエンティティ (rig)
const data = this.data; // スキーマで定義されたパラメータ
const position = el.object3D.position; // rigの現在位置
const dt = timeDelta / 1000; // 経過時間 (秒)
// --- カメラの向きから前方・右方向ベクトルを計算 ---
this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion);
this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion);
if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize();
this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion);
this.cameraRight.y = 0;
if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize();
// --- 方向ベクトル計算ここまで ---
// --- 入力に基づいて移動方向を決定 ---
this.moveDirection.set(0, 0, 0); // まずリセット
// キーボード入力
if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); }
if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); }
if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); }
if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); }
// VRサムスティック入力
if (Math.abs(this.thumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y); this.moveDirection.add(forwardBackward); }
if (Math.abs(this.thumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x); this.moveDirection.add(leftRight); }
// --- 移動方向決定ここまで ---
const isInputActive = this.moveDirection.lengthSq() > 0.0001;
if (isInputActive) { this.moveDirection.normalize(); }
// --- 目標速度と補間係数の決定 (慣性とブレーキ) ---
let lerpFactor = data.damping;
const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01;
if (isInputActive) {
let isOpposingInput = false;
if (isCurrentlyMoving) { const dotProduct = this.currentVelocity.dot(this.moveDirection); if (dotProduct < -0.1) { isOpposingInput = true; } }
if (isOpposingInput) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; }
else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; }
} else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; }
// --- 目標速度と補間係数決定ここまで ---
// --- 現在速度を目標速度に向けて滑らかに補間 (Lerp) ---
const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt);
this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor);
// --- 速度補間ここまで ---
// --- 位置の更新 ---
if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); }
if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); }
// --- 位置更新ここまで ---
},
// キーボードのキーが押された時の処理
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
// キーボードのキーが離された時の処理
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// --- ここまでカメラ移動コンポーネント ---
// --- ランダム回転コンポーネント (変更なし) ---
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); }
});
// --- ここまでランダム回転コンポーネント ---
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" cursor="rayOrigin: entity; fuse: false;"
line="color: white; opacity: 0.5; length: 3">
</a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-triangle id="prevButton" class="clickableButton" position="-20.0 0 0.01" rotation="0 0 90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-triangle>
<a-triangle id="nextButton" class="clickableButton" position="20.0 0 0.01" rotation="0 0 -90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-triangle>
<a-circle id="closeButton" class="clickableButton" position="24 11 0.05" radius="1.5" color="red" shader="flat"> </a-circle>
</a-entity>
<script>
// --- グローバル変数と初期設定 (変更なし) ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
const numSpheres = 60; const spread = 2000; const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3(); const PAGES = ['index', 'color', 'size']; const TOTAL_PAGES = PAGES.length;
// --- 立方体生成ループ (変更なし) ---
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// --- 情報パネルの表示更新関数 (変更なし) ---
function updatePanelDisplay() { if (!infoPanelEl.dataset.sphereIndex) return; const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10); const color = infoPanelEl.dataset.color || 'N/A'; const size = infoPanelEl.dataset.size || 'N/A'; const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let displayText = ''; const pageType = PAGES[pageIndex]; if (pageType === 'index') { displayText = `立方体: ${index}`; } else if (pageType === 'color') { displayText = `色: ${color}`; } else if (pageType === 'size') { displayText = `サイズ: ${size}`; } const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', 'value', finalDisplayText); }
// --- 立方体クリック時の処理 (変更なし) ---
function handleSphereClick(event) { event.stopPropagation(); console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName); const clickedCube = event.target; if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) { console.error("Cube data missing from dataset!", clickedCube.dataset); return; } console.log("Cube data found:", clickedCube.dataset); infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex; infoPanelEl.dataset.color = clickedCube.dataset.color; infoPanelEl.dataset.size = clickedCube.dataset.size; infoPanelEl.dataset.currentPageIndex = '0'; console.log("Data stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedCube.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const cubeSide = parseFloat(clickedCube.dataset.size || 0); const offsetDistance = cubeSide / 2 + 0.5; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); } catch(e) { console.error("Error during position calculation:", e); return; } infoPanelEl.setAttribute('visible', true); console.log("Panel visibility set to true. --- handleSphereClick end ---"); }
// --- パネルのボタンクリック処理 (変更なし) ---
prevButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex + 1) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex); });
closeButtonEl.addEventListener('click', function (event) { event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex; console.log("Close button clicked, panel hidden."); });
// --- VRモードによるマウスカーソル表示制御 (変更なし) ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
// --- マウスカーソル制御ここまで ---
// 背景クリックリスナーは削除済み
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test34
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - VR対応 回転立方体と情報パネル</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- プレイヤー移動制御コンポーネント ('camera-relative-controls') ---
AFRAME.registerComponent('camera-relative-controls', {
// コンポーネントのパラメータ定義
schema: {
targetSpeed: { type: 'number', default: 5 }, // 目標とする最高速度 (m/s)
acceleration: { type: 'number', default: 10 }, // 加速の度合い
damping: { type: 'number', default: 8 }, // 自然減速の度合い (キー入力がない時)
brakingDeceleration: { type: 'number', default: 20 },// 逆キー入力時のブレーキの度合い
enabled: { type: 'boolean', default: true } // このコンポーネントが有効か
},
// コンポーネント初期化時の処理
init: function () {
this.keys = {}; // 押されているキーボードのキーを記録するオブジェクト
this.thumbstickInput = { x: 0, y: 0 }; // VRコントローラのサムスティック入力値
// 各種ベクトルを事前に生成して再利用 (パフォーマンスのため)
this.currentVelocity = new THREE.Vector3(); // 現在の速度ベクトル
this.ZERO_VECTOR = new THREE.Vector3(); // ゼロベクトル (停止状態の目標速度)
this.cameraDirection = new THREE.Vector3(); // カメラの前方ベクトル (ワールド座標系)
this.cameraRight = new THREE.Vector3(); // カメラの右方向ベクトル (ワールド座標系)
this.moveDirection = new THREE.Vector3(); // キー入力やスティック入力に基づく最終的な移動方向
this.desiredVelocity = new THREE.Vector3(); // 目標とする速度ベクトル
this.cameraWorldQuaternion = new THREE.Quaternion(); // カメラのワールド回転
this.cameraEl = this.el.querySelector('[camera]'); // カメラエンティティへの参照
this.isReady = false; // カメラなどの準備ができたかどうかのフラグ
if (!this.cameraEl) {
console.error('camera-relative-controls requires a child entity with the [camera] component.');
}
// --- VRコントローラーのセットアップ ---
this.leftHand = document.getElementById('leftHand'); // 左手コントローラーのエンティティ
if (this.leftHand) {
// 左手スティックの動きを監視
this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
} else {
console.warn("Left hand controller (leftHand) not found for thumbstick movement.");
}
// --- キーボードイベントリスナーのセットアップ ---
this.onKeyDown = this.onKeyDown.bind(this); // thisの束縛
this.onKeyUp = this.onKeyUp.bind(this); // thisの束縛
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
},
// コンポーネント削除時の処理
remove: function () {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
if (this.leftHand) {
this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this));
}
},
// 左手コントローラーのサムスティックが動いた時の処理
onThumbstickMoved: function (evt) {
// スティックのX軸(左右)とY軸(前後)の入力値を取得
this.thumbstickInput.x = evt.detail.x;
this.thumbstickInput.y = evt.detail.y;
},
// 毎フレーム呼び出される処理 (移動計算のコアロジック)
tick: function (time, timeDelta) {
if (!this.data.enabled) return; // 無効なら何もしない
// カメラの準備ができるまで待つ (初回のみ)
if (!this.isReady) {
if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) {
this.isReady = true;
} else {
if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } // 再度取得試行
return; // まだ準備できていない
}
}
if (!this.cameraEl || !this.cameraEl.object3D) { return; } // 念のため再度チェック
const el = this.el; // このコンポーネントがアタッチされているエンティティ (rig)
const data = this.data; // スキーマで定義されたパラメータ
const position = el.object3D.position; // rigの現在位置
const dt = timeDelta / 1000; // 経過時間 (秒)
// --- カメラの向きから前方・右方向ベクトルを計算 ---
this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion);
// 前方 (カメラのローカル-Z軸がワールド前方に対応)
this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion);
if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize();
// 右方向 (カメラのローカルX軸がワールド右方向に対応、Y軸は無視して水平移動)
this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion);
this.cameraRight.y = 0; // 上下には動かないように
if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize();
// --- 方向ベクトル計算ここまで ---
// --- 入力に基づいて移動方向を決定 ---
this.moveDirection.set(0, 0, 0); // まずリセット
// キーボード入力
if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); }
if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); }
if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); }
if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); }
// VRサムスティック入力 (Y軸は反転していることが多いので-1を掛ける)
if (Math.abs(this.thumbstickInput.y) > 0.1) { // デッドゾーン
const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y);
this.moveDirection.add(forwardBackward);
}
if (Math.abs(this.thumbstickInput.x) > 0.1) { // デッドゾーン
const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x);
this.moveDirection.add(leftRight);
}
// --- 移動方向決定ここまで ---
const isInputActive = this.moveDirection.lengthSq() > 0.0001;
if (isInputActive) {
this.moveDirection.normalize(); // 入力があれば正規化
}
// --- 目標速度と補間係数の決定 (慣性とブレーキ) ---
let lerpFactor = data.damping; // デフォルトは自然減速の係数
const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; // 現在ある程度動いているか
if (isInputActive) { // 何かしらの入力がある場合
let isOpposingInput = false;
if (isCurrentlyMoving) {
// 現在の速度ベクトルと入力方向ベクトルの内積を計算
const dotProduct = this.currentVelocity.dot(this.moveDirection);
if (dotProduct < -0.1) { // 内積が負なら逆方向への入力と判断 (閾値-0.1)
isOpposingInput = true;
}
}
if (isOpposingInput) { // 逆方向への入力の場合 (ブレーキ)
this.desiredVelocity.copy(this.ZERO_VECTOR); // 目標速度を0に
lerpFactor = data.brakingDeceleration; // ブレーキ用の減速係数を使用
} else { // 順方向または停止からの入力の場合 (加速)
this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); // 目標速度を設定
lerpFactor = data.acceleration; // 加速用の係数を使用
}
} else { // 入力がない場合 (自然減速)
this.desiredVelocity.copy(this.ZERO_VECTOR); // 目標速度を0に
lerpFactor = data.damping; // 自然減速用の係数を使用
}
// --- 目標速度と補間係数決定ここまで ---
// --- 現在速度を目標速度に向けて滑らかに補間 (Lerp) ---
// dt を使ってフレームレート変動の影響を軽減する補間係数の計算
const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt);
this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor);
// --- 速度補間ここまで ---
// --- 位置の更新 ---
// 非常に遅い速度なら完全に停止させる (微小なドリフト防止)
if (this.currentVelocity.lengthSq() < 0.0001) {
this.currentVelocity.copy(this.ZERO_VECTOR);
}
// 現在速度に基づいてrigの位置を移動
if (this.currentVelocity.lengthSq() > 0) {
const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt);
position.add(deltaPosition);
}
// --- 位置更新ここまで ---
},
// キーボードのキーが押された時の処理
onKeyDown: function (event) {
if (!this.data.enabled) { return; }
if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) {
this.keys[event.code] = true;
}
},
// キーボードのキーが離された時の処理
onKeyUp: function (event) {
if (this.keys[event.code] !== undefined) {
delete this.keys[event.code];
}
}
});
// --- ランダム回転コンポーネント (変更なし) ---
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); }
});
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor"
cursor="rayOrigin: mouse; fuse: false;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000;"
position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;"
material="color: black; shader: flat; opacity: 0.7">
<a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation>
</a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;"
raycaster="objects: .clickableObject, .clickableButton; far: 3000;" cursor="rayOrigin: entity; fuse: false;" line="color: white; opacity: 0.5; length: 3" >
</a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
<a-triangle id="prevButton" class="clickableButton" position="-20.0 0 0.01" rotation="0 0 90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"></a-triangle>
<a-triangle id="nextButton" class="clickableButton" position="20.0 0 0.01" rotation="0 0 -90" scale="2.0 2.0 2.0" material="color: #CCC; shader: flat; opacity: 0.8;"></a-triangle>
<a-circle id="closeButton" class="clickableButton" position="24 11 0.05" radius="1.5" color="red" shader="flat"></a-circle>
</a-entity>
<script>
// --- グローバル変数と初期設定 ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor'); // マウスカーソル要素を取得
const numSpheres = 60;
const spread = 2000;
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3(); // パネル位置計算用
const panelPosition = new THREE.Vector3();
const PAGES = ['index', 'color', 'size'];
const TOTAL_PAGES = PAGES.length;
// --- グローバル変数ここまで ---
// --- 立方体(旧球体)の生成ループ (変更なし) ---
for (let i = 0; i < numSpheres; i++) {
const cubeEl = document.createElement('a-box');
const side = Math.random() * 10.0 + 0.5;
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + side / 2;
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 50%, 75%)`;
const sphereIndex = i + 1;
cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side);
cubeEl.setAttribute('color', color);
cubeEl.setAttribute('position', { x: x, y: y, z: z });
cubeEl.classList.add('clickableObject');
cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2);
cubeEl.setAttribute('random-rotate', {maxSpeed: 5});
cubeEl.addEventListener('click', handleSphereClick);
sceneEl.appendChild(cubeEl);
}
// --- 立方体生成ここまで ---
// --- 情報パネルの表示更新関数 (変更なし) ---
function updatePanelDisplay() {
if (!infoPanelEl.dataset.sphereIndex) return;
const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10);
const color = infoPanelEl.dataset.color || 'N/A';
const size = infoPanelEl.dataset.size || 'N/A';
const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[pageIndex];
if (pageType === 'index') { displayText = `立方体: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${color}`; }
else if (pageType === 'size') { displayText = `サイズ: ${size}`; }
const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`;
const finalDisplayText = `${pageIndicator}\n${displayText}`;
panelTextEl.setAttribute('troika-text', 'value', finalDisplayText);
}
// --- パネル表示更新ここまで ---
// --- 立方体クリック時の処理 (変更なし) ---
function handleSphereClick(event) {
event.stopPropagation(); // イベントのバブリングを停止
console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName);
const clickedCube = event.target;
if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) { console.error("Cube data missing from dataset!", clickedCube.dataset); return; }
console.log("Cube data found:", clickedCube.dataset);
infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex;
infoPanelEl.dataset.color = clickedCube.dataset.color;
infoPanelEl.dataset.size = clickedCube.dataset.size;
infoPanelEl.dataset.currentPageIndex = '0';
console.log("Data stored in panel dataset.");
try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); }
catch (e) { console.error("Error during updatePanelDisplay:", e); return; }
try {
clickedCube.object3D.getWorldPosition(targetWorldPosition);
cameraEl.object3D.getWorldPosition(cameraWorldPosition);
const cubeSide = parseFloat(clickedCube.dataset.size || 0);
const offsetDistance = cubeSide / 2 + 0.5; // 立方体の中心からのオフセット
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition);
console.log("Panel position calculated and applied.");
}
catch(e) { console.error("Error during position calculation:", e); return; }
infoPanelEl.setAttribute('visible', true);
console.log("Panel visibility set to true. --- handleSphereClick end ---");
}
// --- 立方体クリック処理ここまで ---
// --- パネルのボタンクリック処理 (変更なし) ---
prevButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return;
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES;
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex);
});
nextButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return;
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex + 1) % TOTAL_PAGES;
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex);
});
closeButtonEl.addEventListener('click', function (event) {
event.stopPropagation(); infoPanelEl.setAttribute('visible', false);
delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color;
delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex;
console.log("Close button clicked, panel hidden.");
});
// --- パネルボタン処理ここまで ---
// --- VRモードによるマウスカーソル表示制御 ---
sceneEl.addEventListener('enter-vr', function () {
console.log("Entered VR mode");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); // VRモードではマウスカーソルを非表示
// 必要であれば、VRコントローラーのraycasterを有効にするなどの処理
if (document.getElementById('rightHand')) {
document.getElementById('rightHand').setAttribute('raycaster', 'enabled', true);
}
});
sceneEl.addEventListener('exit-vr', function () {
console.log("Exited VR mode");
if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); // VRモードを抜けたらマウスカーソルを再表示
// VRコントローラーのraycasterを無効にするなど
if (document.getElementById('rightHand')) {
document.getElementById('rightHand').setAttribute('raycaster', 'enabled', false);
}
});
// --- マウスカーソル制御ここまで ---
// 背景クリックリスナーは削除済み
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test33
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 回転する立方体 (移動修正+VR対応)</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-troika-text@0.11.0/dist/aframe-troika-text.min.js"></script>
<script>
// --- ★★★ カメラ移動コンポーネント (VR対応版) ★★★ ---
AFRAME.registerComponent('camera-relative-controls', {
schema: {
targetSpeed: { type: 'number', default: 5 },
acceleration: { type: 'number', default: 10 },
damping: { type: 'number', default: 8 },
brakingDeceleration: { type: 'number', default: 20 },
enabled: { type: 'boolean', default: true }
},
init: function () {
this.keys = {};
this.currentVelocity = new THREE.Vector3();
this.ZERO_VECTOR = new THREE.Vector3();
this.direction = new THREE.Vector3();
this.rightDirection = new THREE.Vector3();
this.moveDirection = new THREE.Vector3();
this.desiredVelocity = new THREE.Vector3();
this.cameraWorldQuaternion = new THREE.Quaternion();
this.cameraEl = this.el.querySelector('[camera]');
this.isReady = false;
if (!this.cameraEl) {
console.error('camera-relative-controls requires a child entity with the [camera] component.');
}
this.onKeyDown = this.onKeyDown.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
},
remove: function () {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
},
tick: function (time, timeDelta) {
if (!this.data.enabled) return;
if (!this.isReady) {
if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) {
this.isReady = true;
} else {
if (!this.cameraEl) {
this.cameraEl = this.el.querySelector('[camera]');
}
return;
}
}
if (!this.cameraEl || !this.cameraEl.object3D) return;
const el = this.el;
const data = this.data;
const position = el.object3D.position;
const dt = timeDelta / 1000;
this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion);
this.direction.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion);
if (this.direction.lengthSq() > 0.0001) this.direction.normalize();
this.rightDirection.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion);
this.rightDirection.y = 0;
if (this.rightDirection.lengthSq() > 0.0001) this.rightDirection.normalize();
this.moveDirection.set(0, 0, 0);
if (this.keys['KeyW'] || this.keys['ArrowUp']) this.moveDirection.add(this.direction);
if (this.keys['KeyS'] || this.keys['ArrowDown']) this.moveDirection.sub(this.direction);
if (this.keys['KeyA'] || this.keys['ArrowLeft']) this.moveDirection.sub(this.rightDirection);
if (this.keys['KeyD'] || this.keys['ArrowRight']) this.moveDirection.add(this.rightDirection);
// --- VRコントローラー対応の移動処理追加 ---
const leftController = this.el.sceneEl.querySelector('[hand-controls="hand: left"]');
if (leftController && leftController.components['tracked-controls']) {
const gamepad = leftController.components['tracked-controls'].controller;
if (gamepad && gamepad.axes.length >= 2) {
const x = gamepad.axes[0];
const y = gamepad.axes[1];
if (Math.abs(x) > 0.1 || Math.abs(y) > 0.1) {
const moveX = this.rightDirection.clone().multiplyScalar(x);
const moveZ = this.direction.clone().multiplyScalar(-y);
this.moveDirection.add(moveX).add(moveZ);
}
}
}
// --- ここまでVR対応追加 ---
const isKeyPressed = this.moveDirection.lengthSq() > 0.0001;
if (isKeyPressed) this.moveDirection.normalize();
let lerpFactor = data.damping;
const isMoving = this.currentVelocity.lengthSq() > 0.01;
if (isKeyPressed) {
let isOpposing = false;
if (isMoving) {
const dot = this.currentVelocity.dot(this.moveDirection);
if (dot < -0.1) isOpposing = true;
}
if (isOpposing) {
this.desiredVelocity.copy(this.ZERO_VECTOR);
lerpFactor = data.brakingDeceleration;
} else {
this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed);
lerpFactor = data.acceleration;
}
} else {
this.desiredVelocity.copy(this.ZERO_VECTOR);
lerpFactor = data.damping;
}
const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt);
this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor);
if (this.currentVelocity.lengthSq() < 0.0001) this.currentVelocity.copy(this.ZERO_VECTOR);
if (this.currentVelocity.lengthSq() > 0) {
const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt);
position.add(deltaPosition);
}
},
onKeyDown: function (event) {
if (!this.data.enabled) return;
if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) {
this.keys[event.code] = true;
}
},
onKeyUp: function (event) {
if (this.keys[event.code] !== undefined) {
delete this.keys[event.code];
}
}
});
// --- ここまでカメラ移動コンポーネント ---
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () {
this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize();
const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1);
this.speedRad = THREE.MathUtils.degToRad(speed);
this.deltaRotationQuaternion = new THREE.Quaternion();
},
tick: function (time, timeDelta) {
const dt = timeDelta / 1000;
const angle = this.speedRad * dt;
this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle);
this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion);
}
});
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;"
position="0 0 -1"
geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;"
material="color: black; shader: flat; opacity: 0.7">
<a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1"
to="1 1 1" dur="150"></a-animation>
</a-entity>
</a-entity>
</a-entity>
<!-- VRハンドコントローラーの追加 -->
<a-entity hand-controls="hand: left"></a-entity>
<a-entity hand-controls="hand: right"></a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
<a-triangle id="prevButton" class="clickableButton"
position="-20.0 0 0.01" rotation="0 0 90" scale="2.0 2.0 2.0"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-triangle>
<a-triangle id="nextButton" class="clickableButton"
position="20.0 0 0.01" rotation="0 0 -90" scale="2.0 2.0 2.0"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-triangle>
<a-circle id="closeButton" class="clickableButton"
position="24 11 0.05" radius="1.5" color="red" shader="flat">
</a-circle>
</a-entity>
<script>
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const numSpheres = 60;
const spread = 2000;
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3();
const panelPosition = new THREE.Vector3();
const PAGES = ['index', 'color', 'size'];
const TOTAL_PAGES = PAGES.length;
for (let i = 0; i < numSpheres; i++) {
const cubeEl = document.createElement('a-box');
const side = Math.random() * 10.0 + 0.5;
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + side / 2;
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 50%, 75%)`;
const sphereIndex = i + 1;
cubeEl.setAttribute('width', side);
cubeEl.setAttribute('height', side);
cubeEl.setAttribute('depth', side);
cubeEl.setAttribute('color', color);
cubeEl.setAttribute('position', { x: x, y: y, z: z });
cubeEl.classList.add('clickableObject');
cubeEl.dataset.sphereIndex = sphereIndex;
cubeEl.dataset.color = color;
cubeEl.dataset.size = side.toFixed(2);
cubeEl.setAttribute('random-rotate', { maxSpeed: 5 });
cubeEl.addEventListener('click', handleSphereClick);
sceneEl.appendChild(cubeEl);
}
function updatePanelDisplay() {
if (!infoPanelEl.dataset.sphereIndex) return;
const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10);
const color = infoPanelEl.dataset.color || 'N/A';
const size = infoPanelEl.dataset.size || 'N/A';
const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[pageIndex];
if (pageType === 'index') displayText = `立方体: ${index}`;
else if (pageType === 'color') displayText = `色: ${color}`;
else if (pageType === 'size') displayText = `サイズ: ${size}`;
const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`;
const finalDisplayText = `${pageIndicator}\n${displayText}`;
panelTextEl.setAttribute('troika-text', 'value', finalDisplayText);
}
function handleSphereClick(event) {
event.stopPropagation();
const clickedCube = event.target;
if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) return;
infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex;
infoPanelEl.dataset.color = clickedCube.dataset.color;
infoPanelEl.dataset.size = clickedCube.dataset.size;
infoPanelEl.dataset.currentPageIndex = '0';
updatePanelDisplay();
clickedCube.object3D.getWorldPosition(targetWorldPosition);
cameraEl.object3D.getWorldPosition(cameraWorldPosition);
const cubeSide = parseFloat(clickedCube.dataset.size || 0);
const offsetDistance = cubeSide / 2 + 0.5;
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition);
infoPanelEl.setAttribute('visible', true);
}
prevButtonEl.addEventListener('click', function (event) {
event.stopPropagation();
if (!infoPanelEl.getAttribute('visible')) return;
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES;
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay();
});
nextButtonEl.addEventListener('click', function (event) {
event.stopPropagation();
if (!infoPanelEl.getAttribute('visible')) return;
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex + 1) % TOTAL_PAGES;
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay();
});
closeButtonEl.addEventListener('click', function (event) {
event.stopPropagation();
infoPanelEl.setAttribute('visible', false);
delete infoPanelEl.dataset.sphereIndex;
delete infoPanelEl.dataset.color;
delete infoPanelEl.dataset.size;
delete infoPanelEl.dataset.currentPageIndex;
});
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test-gpt-v2
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 回転する立方体 (移動修正)</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- ★★★ カメラ移動コンポーネント (完全版) ★★★ ---
AFRAME.registerComponent('camera-relative-controls', {
// スキーマ定義 (ブレーキ減速を含む)
schema: {
targetSpeed: { type: 'number', default: 5 },
acceleration: { type: 'number', default: 10 },
damping: { type: 'number', default: 8 },
brakingDeceleration: { type: 'number', default: 20 },
enabled: { type: 'boolean', default: true }
},
// 初期化処理
init: function () {
this.keys = {};
this.currentVelocity = new THREE.Vector3();
this.ZERO_VECTOR = new THREE.Vector3();
this.direction = new THREE.Vector3();
this.rightDirection = new THREE.Vector3();
this.moveDirection = new THREE.Vector3();
this.desiredVelocity = new THREE.Vector3();
this.cameraWorldQuaternion = new THREE.Quaternion();
this.cameraEl = this.el.querySelector('[camera]');
this.isReady = false;
if (!this.cameraEl) { console.error('camera-relative-controls requires a child entity with the [camera] component.'); }
this.onKeyDown = this.onKeyDown.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
},
// 削除時の処理
remove: function () {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
},
// フレーム毎の処理 (移動計算)
tick: function (time, timeDelta) {
if (!this.data.enabled) return;
if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } }
if (!this.cameraEl || !this.cameraEl.object3D) { return; }
const el = this.el;
const data = this.data;
const position = el.object3D.position;
const dt = timeDelta / 1000;
this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion);
this.direction.set(0, 0, -1); this.direction.applyQuaternion(this.cameraWorldQuaternion); if (this.direction.lengthSq() > 0.0001) this.direction.normalize();
this.rightDirection.set(1, 0, 0); this.rightDirection.applyQuaternion(this.cameraWorldQuaternion); this.rightDirection.y = 0; if (this.rightDirection.lengthSq() > 0.0001) this.rightDirection.normalize();
this.moveDirection.set(0, 0, 0);
if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.direction); }
if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.direction); }
if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.rightDirection); }
if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.rightDirection); }
const isKeyPressed = this.moveDirection.lengthSq() > 0.0001;
if (isKeyPressed) { this.moveDirection.normalize(); }
let lerpFactor = data.damping;
const isMoving = this.currentVelocity.lengthSq() > 0.01;
if (isKeyPressed) {
let isOpposing = false;
if (isMoving) {
const dot = this.currentVelocity.dot(this.moveDirection);
if (dot < -0.1) { isOpposing = true; }
}
if (isOpposing) {
this.desiredVelocity.copy(this.ZERO_VECTOR);
lerpFactor = data.brakingDeceleration;
} else {
this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed);
lerpFactor = data.acceleration;
}
} else {
this.desiredVelocity.copy(this.ZERO_VECTOR);
lerpFactor = data.damping;
}
const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt);
this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor);
if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); }
if (this.currentVelocity.lengthSq() > 0) {
const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt);
position.add(deltaPosition);
}
},
// キー入力処理
onKeyDown: function (event) {
if (!this.data.enabled) { return; }
if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) {
this.keys[event.code] = true;
}
},
onKeyUp: function (event) {
if (this.keys[event.code] !== undefined) {
delete this.keys[event.code];
}
}
});
// --- ここまでカメラ移動コンポーネント ---
// --- ランダム回転コンポーネント (変更なし) ---
AFRAME.registerComponent('random-rotate', {
schema: { maxSpeed: { type: 'number', default: 5 } },
init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); },
tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); }
});
// --- ここまでランダム回転コンポーネント ---
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1"
geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;"
material="color: black; shader: flat; opacity: 0.7">
<a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation>
</a-entity>
</a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
<a-triangle id="prevButton" class="clickableButton"
position="-20.0 0 0.01" rotation="0 0 90" scale="2.0 2.0 2.0"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-triangle>
<a-triangle id="nextButton" class="clickableButton"
position="20.0 0 0.01" rotation="0 0 -90" scale="2.0 2.0 2.0"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-triangle>
<a-circle id="closeButton" class="clickableButton"
position="24 11 0.05" radius="1.5" color="red" shader="flat">
</a-circle>
</a-entity>
<script>
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const numSpheres = 60;
const spread = 2000;
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3();
const panelPosition = new THREE.Vector3();
const PAGES = ['index', 'color', 'size'];
const TOTAL_PAGES = PAGES.length;
// 立方体生成ループ (変更なし)
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// パネル表示更新関数 (変更なし)
function updatePanelDisplay() { if (!infoPanelEl.dataset.sphereIndex) return; const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10); const color = infoPanelEl.dataset.color || 'N/A'; const size = infoPanelEl.dataset.size || 'N/A'; const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let displayText = ''; const pageType = PAGES[pageIndex]; if (pageType === 'index') { displayText = `立方体: ${index}`; } else if (pageType === 'color') { displayText = `色: ${color}`; } else if (pageType === 'size') { displayText = `サイズ: ${size}`; } const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', 'value', finalDisplayText); }
// handleSphereClick (変更なし)
function handleSphereClick(event) { event.stopPropagation(); console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName); const clickedCube = event.target; if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) { console.error("Cube data missing from dataset!", clickedCube.dataset); return; } console.log("Cube data found:", clickedCube.dataset); infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex; infoPanelEl.dataset.color = clickedCube.dataset.color; infoPanelEl.dataset.size = clickedCube.dataset.size; infoPanelEl.dataset.currentPageIndex = '0'; console.log("Data stored in panel dataset."); try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); } catch (e) { console.error("Error during updatePanelDisplay:", e); return; } try { clickedCube.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const cubeSide = parseFloat(clickedCube.dataset.size || 0); const offsetDistance = cubeSide / 2 + 0.5; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); } catch(e) { console.error("Error during position calculation:", e); return; } infoPanelEl.setAttribute('visible', true); console.log("Panel visibility set to true. --- handleSphereClick end ---"); }
// ボタンクリック時の処理 (変更なし)
prevButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Prev button clicked, page index:", pageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); pageIndex = (pageIndex + 1) % TOTAL_PAGES; infoPanelEl.dataset.currentPageIndex = pageIndex.toString(); updatePanelDisplay(); console.log("Next button clicked, page index:", pageIndex); });
closeButtonEl.addEventListener('click', function (event) { event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex; console.log("Close button clicked, panel hidden."); });
// 背景クリックリスナーは削除済み
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test32
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 情報パネル (背景クリック無効化テスト)</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- カスタムコンポーネント定義 (変更なし) ---
AFRAME.registerComponent('camera-relative-controls', {
schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true } },
init: function () { this.keys = {}; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.direction = new THREE.Vector3(); this.rightDirection = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls requires a child entity with the [camera] component.'); } this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); },
remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); },
tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.direction.set(0, 0, -1); this.direction.applyQuaternion(this.cameraWorldQuaternion); if (this.direction.lengthSq() > 0.0001) this.direction.normalize(); this.rightDirection.set(1, 0, 0); this.rightDirection.applyQuaternion(this.cameraWorldQuaternion); this.rightDirection.y = 0; if (this.rightDirection.lengthSq() > 0.0001) this.rightDirection.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.direction); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.direction); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.rightDirection); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.rightDirection); } const isKeyPressed = this.moveDirection.lengthSq() > 0.0001; if (isKeyPressed) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isMoving = this.currentVelocity.lengthSq() > 0.01; if (isKeyPressed) { let isOpposing = false; if (isMoving) { const dot = this.currentVelocity.dot(this.moveDirection); if (dot < -0.1) { isOpposing = true; } } if (isOpposing) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } },
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// --- ここまでカスタムコンポーネント定義 ---
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableSphere, .clickableButton; far: 3000;"
position="0 0 -1"
geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;"
material="color: black; shader: flat; opacity: 0.7">
<a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation>
</a-entity>
</a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="2.5" height="1.2" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.24; maxWidth: 2.3; align: center; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
<a-triangle id="prevButton" class="clickableButton"
position="-1.0 0 0.01" rotation="0 0 90" scale="0.2 0.2 0.2"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-triangle>
<a-triangle id="nextButton" class="clickableButton"
position="1.0 0 0.01" rotation="0 0 -90" scale="0.2 0.2 0.2"
material="color: #CCC; shader: flat; opacity: 0.8;">
</a-triangle>
</a-entity>
<script>
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const numSpheres = 60;
const spread = 2000;
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3();
const panelPosition = new THREE.Vector3();
const PAGES = ['index', 'color', 'radius'];
const TOTAL_PAGES = PAGES.length;
// 球体生成 (変更なし)
for (let i = 0; i < numSpheres; i++) { /* ... */ const sphereEl = document.createElement('a-sphere'); const radius = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + radius; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; sphereEl.setAttribute('radius', radius); sphereEl.setAttribute('color', color); sphereEl.setAttribute('position', { x: x, y: y, z: z }); sphereEl.classList.add('clickableSphere'); sphereEl.dataset.sphereIndex = sphereIndex; sphereEl.dataset.color = color; sphereEl.dataset.radius = radius.toFixed(2); sphereEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(sphereEl); }
// パネル表示更新関数 (変更なし)
function updatePanelDisplay() { if (!infoPanelEl.dataset.sphereIndex) return; const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10); const color = infoPanelEl.dataset.color || 'N/A'; const radius = infoPanelEl.dataset.radius || 'N/A'; const pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let displayText = ''; const pageType = PAGES[pageIndex]; if (pageType === 'index') { displayText = `球: ${index}`; } else if (pageType === 'color') { displayText = `色: ${color}`; } else if (pageType === 'radius') { displayText = `半径: ${radius}`; } const pageIndicator = `(${pageIndex + 1}/${TOTAL_PAGES})`; const finalDisplayText = `${pageIndicator}\n${displayText}`; panelTextEl.setAttribute('troika-text', 'value', finalDisplayText); }
// handleSphereClick (stopPropagation を含む)
function handleSphereClick(event) {
event.stopPropagation(); // イベント伝播停止
console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName);
const clickedSphere = event.target;
if (!clickedSphere.dataset.sphereIndex || !clickedSphere.dataset.color || !clickedSphere.dataset.radius) { console.error("Sphere data missing!"); return; }
console.log("Sphere data found:", clickedSphere.dataset);
infoPanelEl.dataset.sphereIndex = clickedSphere.dataset.sphereIndex;
infoPanelEl.dataset.color = clickedSphere.dataset.color;
infoPanelEl.dataset.radius = clickedSphere.dataset.radius;
infoPanelEl.dataset.currentPageIndex = '0';
console.log("Data stored in panel dataset.");
try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); }
catch (e) { console.error("Error during updatePanelDisplay:", e); return; }
try { clickedSphere.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const sphereRadius = parseFloat(clickedSphere.dataset.radius || 0); const offsetDistance = sphereRadius + 0.5; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); }
catch(e) { console.error("Error during position calculation:", e); return; }
infoPanelEl.setAttribute('visible', true);
console.log("Panel visibility set to true. --- handleSphereClick end ---");
}
// ボタンクリック時の処理 (stopPropagation を含む)
prevButtonEl.addEventListener('click', function (event) {
event.stopPropagation();
if (!infoPanelEl.getAttribute('visible')) return;
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex - 1 + TOTAL_PAGES) % TOTAL_PAGES;
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay();
console.log("Prev button clicked, page index:", pageIndex);
});
nextButtonEl.addEventListener('click', function (event) {
event.stopPropagation();
if (!infoPanelEl.getAttribute('visible')) return;
let pageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
pageIndex = (pageIndex + 1) % TOTAL_PAGES;
infoPanelEl.dataset.currentPageIndex = pageIndex.toString();
updatePanelDisplay();
console.log("Next button clicked, page index:", pageIndex);
});
// --- ★★★ 背景クリックでパネルを隠す処理を一時的に全体コメントアウト ★★★ ---
/*
sceneEl.addEventListener('click', function(event) {
if (!event.target.classList.contains('clickableSphere') &&
!event.target.classList.contains('clickableButton')) {
infoPanelEl.setAttribute('visible', false);
delete infoPanelEl.dataset.sphereIndex;
delete infoPanelEl.dataset.color;
delete infoPanelEl.dataset.radius;
delete infoPanelEl.dataset.currentPageIndex;
console.log("Panel hidden by background click.");
}
});
*/
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test29
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 黒背景とパステル球</title> <script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- カスタムコンポーネント定義 (変更なし) ---
AFRAME.registerComponent('camera-relative-controls', {
schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true } },
init: function () { this.keys = {}; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.direction = new THREE.Vector3(); this.rightDirection = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls requires a child entity with the [camera] component.'); } this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); },
remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); },
tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.direction.set(0, 0, -1); this.direction.applyQuaternion(this.cameraWorldQuaternion); if (this.direction.lengthSq() > 0.0001) this.direction.normalize(); this.rightDirection.set(1, 0, 0); this.rightDirection.applyQuaternion(this.cameraWorldQuaternion); this.rightDirection.y = 0; if (this.rightDirection.lengthSq() > 0.0001) this.rightDirection.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.direction); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.direction); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.rightDirection); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.rightDirection); } const isKeyPressed = this.moveDirection.lengthSq() > 0.0001; if (isKeyPressed) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isMoving = this.currentVelocity.lengthSq() > 0.01; if (isKeyPressed) { let isOpposing = false; if (isMoving) { const dot = this.currentVelocity.dot(this.moveDirection); if (dot < -0.1) { isOpposing = true; } } if (isOpposing) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } },
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// --- ここまでカスタムコンポーネント定義 ---
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity cursor="rayOrigin: mouse; fuse: false;" raycaster="objects:.clickableSphere; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
</a-entity>
<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="2.5" height="1.2" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.08; maxWidth: 2.3; align: left; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
</a-entity>
<script>
// --- 球体生成などのスクリプト (変更なし) ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const numSpheres = 60;
const spread = 2000;
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3();
const panelPosition = new THREE.Vector3();
for (let i = 0; i < numSpheres; i++) {
const sphereEl = document.createElement('a-sphere');
const radius = Math.random() * 4.0 + 2.0;
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + radius;
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 50%, 75%)`;
sphereEl.setAttribute('radius', radius);
sphereEl.setAttribute('color', color);
sphereEl.setAttribute('position', { x: x, y: y, z: z });
sphereEl.classList.add('clickableSphere');
sphereEl.setAttribute('data-info', `球 ${i + 1}\n色: ${color}\n半径: ${radius.toFixed(2)}`);
sphereEl.addEventListener('click', handleSphereClick);
sceneEl.appendChild(sphereEl);
}
function handleSphereClick(event) {
const clickedSphere = event.target;
const sphereInfo = clickedSphere.dataset.info;
clickedSphere.object3D.getWorldPosition(targetWorldPosition);
cameraEl.object3D.getWorldPosition(cameraWorldPosition);
const sphereRadius = clickedSphere.getAttribute('geometry').radius;
const offsetDistance = sphereRadius + 0.5;
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition);
panelTextEl.setAttribute('troika-text', 'value', sphereInfo);
infoPanelEl.setAttribute('visible', true);
}
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test18
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - 広範囲・巨大球</title> <script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- カスタムコンポーネント定義 (変更なし) ---
AFRAME.registerComponent('camera-relative-controls', {
schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, enabled: { type: 'boolean', default: true } },
init: function () { this.keys = {}; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.direction = new THREE.Vector3(); this.rightDirection = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls requires a child entity with the [camera] component.'); } this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); },
remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); },
tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.direction.set(0, 0, -1); this.direction.applyQuaternion(this.cameraWorldQuaternion); if (this.direction.lengthSq() > 0.0001) this.direction.normalize(); this.rightDirection.set(1, 0, 0); this.rightDirection.applyQuaternion(this.cameraWorldQuaternion); this.rightDirection.y = 0; if (this.rightDirection.lengthSq() > 0.0001) this.rightDirection.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.direction); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.direction); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.rightDirection); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.rightDirection); } if (this.moveDirection.lengthSq() > 0.0001) { this.moveDirection.normalize(); this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); } else { this.desiredVelocity.copy(this.ZERO_VECTOR); } const lerpFactor = (this.moveDirection.lengthSq() > 0.0001) ? data.acceleration : data.damping; const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } },
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// --- ここまでカスタムコンポーネント定義 ---
</script>
</head>
<body>
<a-scene id="myScene" background="color: #ECECEC">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 200; damping: 2">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity cursor="rayOrigin: mouse; fuse: false;" raycaster="objects:.clickableSphere; far: 3000;"
position="0 0 -1"
geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;"
material="color: black; shader: flat; opacity: 0.7">
<a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation>
</a-entity>
</a-entity>
</a-entity>
<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="2.5" height="1.2" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.08; maxWidth: 2.3; align: left; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
</a-entity>
<script>
// --- 球体生成などのスクリプト ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const numSpheres = 30; // 数は変えない
const spread = 2000; // ★★★ 範囲を 20 * 100 = 2000 に変更 ★★★
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3();
const panelPosition = new THREE.Vector3();
// 球体生成ループ
for (let i = 0; i < numSpheres; i++) {
const sphereEl = document.createElement('a-sphere');
// ★★★ 半径の計算式を変更 (範囲 2.0 〜 6.0) ★★★
const radius = Math.random() * 4.0 + 2.0;
// ★★★ 位置計算は新しい spread と radius を使う ★★★
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + radius; // Y方向にも広がる
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 70%, 60%)`;
sphereEl.setAttribute('radius', radius);
sphereEl.setAttribute('color', color);
sphereEl.setAttribute('position', { x: x, y: y, z: z });
sphereEl.classList.add('clickableSphere');
sphereEl.setAttribute('data-info', `球 ${i + 1}\n色: ${color}\n半径: ${radius.toFixed(2)}`);
sphereEl.addEventListener('click', handleSphereClick);
sceneEl.appendChild(sphereEl);
}
// 球体クリック時の処理関数 (変更なし)
function handleSphereClick(event) {
const clickedSphere = event.target;
const sphereInfo = clickedSphere.dataset.info;
clickedSphere.object3D.getWorldPosition(targetWorldPosition);
cameraEl.object3D.getWorldPosition(cameraWorldPosition);
const sphereRadius = clickedSphere.getAttribute('geometry').radius;
const offsetDistance = sphereRadius + 0.5;
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition);
panelTextEl.setAttribute('troika-text', 'value', sphereInfo);
infoPanelEl.setAttribute('visible', true);
}
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test14
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - Troika Text 日本語表示 (エラー対策強化版)</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- カスタムコンポーネント定義 (tick内の準備完了チェックを強化) ---
AFRAME.registerComponent('camera-relative-controls', {
schema: { speed: { type: 'number', default: 3 }, enabled: { type: 'boolean', default: true } },
init: function () {
this.keys = {};
this.velocity = new THREE.Vector3();
this.direction = new THREE.Vector3();
this.cameraWorldQuaternion = new THREE.Quaternion();
this.rightDirection = new THREE.Vector3();
this.cameraEl = this.el.querySelector('[camera]'); // 先に取得試行
this.isReady = false; // ★★★ 準備完了フラグを追加 ★★★
if (!this.cameraEl) { console.error('camera-relative-controls requires a child entity with the [camera] component.'); }
this.onKeyDown = this.onKeyDown.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
},
remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); },
tick: function (time, timeDelta) {
if (!this.data.enabled) return;
// ▼▼▼ 準備完了チェック ▼▼▼
if (!this.isReady) {
// cameraEl と object3D と matrixWorld が存在するか確認
if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) {
this.isReady = true; // 準備OKとする
} else {
// もし init で cameraEl が見つからなかった場合に再試行 (念のため)
if (!this.cameraEl) {
this.cameraEl = this.el.querySelector('[camera]');
}
return; // まだ準備できていないので tick 処理を中断
}
}
// ▲▲▲ 準備完了チェック ▲▲▲
// --- 準備完了後の処理 (元のtick処理) ---
// 念のためここでもチェック (より安全に)
if (!this.cameraEl || !this.cameraEl.object3D) { return; }
const el = this.el;
const data = this.data;
const position = el.object3D.position;
const dt = timeDelta / 1000;
// この呼び出しが安全になっているはず
this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion);
this.direction.set(0, 0, -1);
this.direction.applyQuaternion(this.cameraWorldQuaternion);
if (this.direction.lengthSq() > 0.0001) this.direction.normalize();
this.rightDirection.set(1, 0, 0);
this.rightDirection.applyQuaternion(this.cameraWorldQuaternion);
this.rightDirection.y = 0;
if (this.rightDirection.lengthSq() > 0.0001) this.rightDirection.normalize();
this.velocity.set(0, 0, 0);
if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.velocity.add(this.direction); }
if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.velocity.sub(this.direction); }
if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.velocity.sub(this.rightDirection); }
if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.velocity.add(this.rightDirection); }
if (this.velocity.lengthSq() > 0.0001) {
this.velocity.normalize().multiplyScalar(data.speed * dt);
position.add(this.velocity);
}
},
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});
// --- ここまでカスタムコンポーネント定義 ---
</script>
</head>
<body>
<a-scene id="myScene" background="color: #ECECEC">
<a-entity id="rig" position="0 0 5" camera-relative-controls="speed: 3">
<a-entity id="camera" camera look-controls position="0 1.6 0">
<a-entity cursor="rayOrigin: mouse; fuse: false;" raycaster="objects:.clickableSphere; far: 15;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
</a-entity>
<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="2.5" height="1.2" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText"
troika-text="value: Placeholder; color: white; fontSize: 0.08; maxWidth: 2.3; align: left; anchorX: center; anchorY: middle; baseline: middle;"
position="0 0 0.05">
</a-entity>
</a-entity>
<script>
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const numSpheres = 30;
const spread = 20;
const targetWorldPosition = new THREE.Vector3();
const cameraWorldPosition = new THREE.Vector3();
const direction = new THREE.Vector3();
const panelPosition = new THREE.Vector3();
for (let i = 0; i < numSpheres; i++) {
const sphereEl = document.createElement('a-sphere');
const radius = Math.random() * 0.4 + 0.2;
const x = (Math.random() - 0.5) * spread;
const y = Math.random() * (spread / 2) + radius;
const z = (Math.random() - 0.5) * spread;
const color = `hsl(${Math.random() * 360}, 70%, 60%)`;
sphereEl.setAttribute('radius', radius);
sphereEl.setAttribute('color', color);
sphereEl.setAttribute('position', { x: x, y: y, z: z });
sphereEl.classList.add('clickableSphere');
sphereEl.setAttribute('data-info', `球 ${i + 1}\n色: ${color}\n半径: ${radius.toFixed(2)}`);
sphereEl.addEventListener('click', handleSphereClick);
sceneEl.appendChild(sphereEl);
}
function handleSphereClick(event) {
const clickedSphere = event.target;
const sphereInfo = clickedSphere.dataset.info;
clickedSphere.object3D.getWorldPosition(targetWorldPosition);
cameraEl.object3D.getWorldPosition(cameraWorldPosition);
const sphereRadius = clickedSphere.getAttribute('geometry').radius;
const offsetDistance = sphereRadius + 0.5;
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition);
panelTextEl.setAttribute('troika-text', 'value', sphereInfo); // troika-text を更新
infoPanelEl.setAttribute('visible', true);
}
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test11
├
vr
├
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>A-Frame Cube Rotation Example (Timed and Converging)</title>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
</head>
<body>
<a-scene>
<!-- 4x4 の立方体(上と横に並べる) -->
<a-entity id="cubes" position="0 1.5 -5">
<!-- 16個の明るくカラフルな立方体を4x4のグリッドで並べる -->
<!-- 横方向と上方向に配置 -->
<a-box position="0 0 0" color="#FF5733" class="cube"></a-box>
<a-box position="2 0 0" color="#FFBD33" class="cube"></a-box>
<a-box position="4 0 0" color="#33FF57" class="cube"></a-box>
<a-box position="6 0 0" color="#33B5FF" class="cube"></a-box>
<a-box position="0 2 0" color="#8E44AD" class="cube"></a-box>
<a-box position="2 2 0" color="#E74C3C" class="cube"></a-box>
<a-box position="4 2 0" color="#F39C12" class="cube"></a-box>
<a-box position="6 2 0" color="#1ABC9C" class="cube"></a-box>
<a-box position="0 4 0" color="#9B59B6" class="cube"></a-box>
<a-box position="2 4 0" color="#16A085" class="cube"></a-box>
<a-box position="4 4 0" color="#F1C40F" class="cube"></a-box>
<a-box position="6 4 0" color="#D35400" class="cube"></a-box>
<a-box position="0 6 0" color="#2980B9" class="cube"></a-box>
<a-box position="2 6 0" color="#27AE60" class="cube"></a-box>
<a-box position="4 6 0" color="#34495E" class="cube"></a-box>
<a-box position="6 6 0" color="#F5B041" class="cube"></a-box>
</a-entity>
<!-- 自分のカーソル(棒) -->
<a-camera>
<a-cursor color="black" raycaster="objects:.cube" fuse="false"></a-cursor>
</a-camera>
<!-- より強い照明 -->
<a-light type="directional" position="2 4 -3" intensity="1.5"></a-light>
<a-light type="ambient" position="0 5 0" intensity="0.7"></a-light>
</a-scene>
<script>
// 立方体が接触したときに回転する処理
document.querySelectorAll('.cube').forEach((cube, index) => {
cube.addEventListener('mouseenter', function () {
// マウスが立方体に触れたとき、時間差で回転を開始
setTimeout(() => {
cube.setAttribute('animation', {
property: 'rotation',
to: '360 360 360',
dur: 1000,
easing: 'easeInOutQuad',
loop: true // 回転を繰り返す設定
});
}, index * 300); // 各立方体に300msの遅延を追加
// しばらく経つと収束して回転が止まるようにする
setTimeout(() => {
cube.setAttribute('animation', {
property: 'rotation',
to: '0 0 0',
dur: 1500,
easing: 'easeInOutQuad',
loop: false // 停止
});
}, (index * 300) + 4000); // 各立方体に遅延後、4秒後に収束(停止)
});
});
</script>
</body>
</html>
vr-test3
├
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, maximum-scale=1, width=device-width">
<title>A-Frame</title>
<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-animation-component@3.2.1/dist/aframe-animation-component.min.js"></script>
<script src="https://unpkg.com/aframe-particle-system-component@1.0.x/dist/aframe-particle-system-component.min.js"></script>
<script src="https://unpkg.com/aframe-extras.ocean@%5E3.5.x/dist/aframe-extras.ocean.min.js"></script>
<script src="https://unpkg.com/aframe-gradient-sky@1.0.4/dist/gradientsky.min.js"></script>
<script src="https://rawgit.com/mayognaise/aframe-mouse-cursor-component/master/dist/aframe-mouse-cursor-component.min.js"></script>
</head>
<body>
<a-scene>
<!-- 1つ目の立方体 -->
<a-box position="-2 3.5 -2" rotation="30 30 0" color="skyblue"
event-set__enter="_event: mouseenter; material.color: yellowgreen; scale: 3 1 1"
event-set__leave="_event: mouseleave; material.color: skyblue; scale: 1 1 1">
<a-animation attribute="rotation" begin="click" dur="500" fill="backwards" to="30 30 360"></a-animation>
<a-text id="cube1-text" value="Cube 1" position="0 1.5 0" align="center" visible="false"></a-text>
</a-box>
<!-- 2つ目の立方体 -->
<a-box position="0 3.5 -2" rotation="30 30 0" color="skyblue"
event-set__enter="_event: mouseenter; material.color: yellowgreen; scale: 3 1 1"
event-set__leave="_event: mouseleave; material.color: skyblue; scale: 1 1 1">
<a-animation attribute="rotation" begin="click" dur="500" fill="backwards" to="30 30 360"></a-animation>
<a-text id="cube2-text" value="Cube 2" position="0 1.5 0" align="center" visible="false"></a-text>
</a-box>
<!-- 3つ目の立方体 -->
<a-box position="2 3.5 -2" rotation="30 30 0" color="skyblue"
event-set__enter="_event: mouseenter; material.color: yellowgreen; scale: 3 1 1"
event-set__leave="_event: mouseleave; material.color: skyblue; scale: 1 1 1">
<a-animation attribute="rotation" begin="click" dur="500" fill="backwards" to="30 30 360"></a-animation>
<a-text id="cube3-text" value="Cube 3" position="0 1.5 0" align="center" visible="false"></a-text>
</a-box>
<a-sky color="pink"></a-sky>
<!-- カメラの動きのためのコンポーネント -->
<a-entity position="0 1.8 4">
<a-entity camera look-controls="pointerLockEnabled: true" wasd-controls="enabled: true" mouse-cursor>
<a-cursor fuse="true" color="yellow"></a-cursor>
</a-entity>
</a-entity>
</a-scene>
</body>
</html>
vr-test2
├
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0,user-scalable=no,maximum-scale=1,width=device-width">
<title>A-Frame</title>
<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-animation-component@3.2.1/dist/aframe-animation-component.min.js"></script>
<script src="https://unpkg.com/aframe-particle-system-component@1.0.x/dist/aframe-particle-system-component.min.js"></script>
<script src="https://unpkg.com/aframe-extras.ocean@%5E3.5.x/dist/aframe-extras.ocean.min.js"></script>
<script src="https://unpkg.com/aframe-gradient-sky@1.0.4/dist/gradientsky.min.js"></script>
<script src="https://rawgit.com/mayognaise/aframe-mouse-cursor-component/master/dist/aframe-mouse-cursor-component.min.js"></script>
</head>
<body>
<a-scene>
<a-box position="0 3.5 -2" rotation="30 30 0" color="skyblue"
event-set__enter="_event: mouseenter; material.color: yellowgreen; scale: 3 1 1"
event-set__leave="_event: mouseleave; material.color: skyblue; scale: 1 1 1">
<a-animation attribute="rotation" begin="click" dur="500" fill="backwards" to="30 30 360"></a-animation>
</a-box>
<a-sky color="pink"></a-sky>
<a-entity position="0 1.8 4">
<a-entity camera look-controls mouse-cursor>
<a-cursor fuse="true" color="yellow"></a-cursor>
</a-entity>
</a-entity>
</a-entity>
</a-scene>
</body>
</html>
vr-test1
├
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0,user-scalable=no,maximum-scale=1,width=device-width">
<title>A-Frame</title>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-animation-component@3.2.1/dist/aframe-animation-component.min.js"></script>
<script src="https://unpkg.com/aframe-particle-system-component@1.0.x/dist/aframe-particle-system-component.min.js"></script>
<script src="https://unpkg.com/aframe-extras.ocean@%5E3.5.x/dist/aframe-extras.ocean.min.js"></script>
<script src="https://unpkg.com/aframe-gradient-sky@1.0.4/dist/gradientsky.min.js"></script>
<script src="https://rawgit.com/mayognaise/aframe-mouse-cursor-component/master/dist/aframe-mouse-cursor-component.min.js"></script>
</head>
<body>
<a-scene>
<a-box position="0 3.5 -2" rotation="30 30 0" color="skyblue"
event-set__enter="_event: mouseenter; material.color: yellowgreen; scale: 3 1 1"
event-set__leave="_event: mouseleave; material.color: skyblue; scale: 1 1 1"
animation="property: scale; to: 2 2 2; dur: 500; easing: easeInOutQuad;">
</a-box>
<a-sky color="pink"></a-sky>
<a-entity position="0 1.8 4">
<a-entity camera look-controls mouse-cursor>
<a-cursor fuse="true" color="yellow"></a-cursor>
</a-entity>
</a-entity>
</a-entity>
</a-scene>
</body>
</html>
vr-test1-2
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - コメントリンク機能</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- プレイヤー移動制御用カスタムコンポーネント ('camera-relative-controls') ---
// (変更なし - 省略)
AFRAME.registerComponent('camera-relative-controls', { schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true } }, init: function () { this.keys = {}; this.thumbstickInput = { x: 0, y: 0 }; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.cameraDirection = new THREE.Vector3(); this.cameraRight = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); } this.el.sceneEl.addEventListener('loaded', () => { this.leftHand = document.getElementById('leftHand'); if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); }, remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } catch(e) { console.warn("camera-relative-controls: スティックリスナー解除失敗の可能性あり"); } } }, onThumbstickMoved: function (evt) { this.thumbstickInput.x = evt.detail.x; this.thumbstickInput.y = evt.detail.y; }, tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); } if (Math.abs(this.thumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.thumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x); this.moveDirection.add(leftRight); } const isInputActive = this.moveDirection.lengthSq() > 0.0001; if (isInputActive) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; if (isInputActive) { let isOpposingInput = false; if (isCurrentlyMoving) { const dotProduct = this.currentVelocity.dot(this.moveDirection); if (dotProduct < -0.1) { isOpposingInput = true; } } if (isOpposingInput) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } }, onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } }, onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } } });
// --- 立方体をランダムに回転させるカスタムコンポーネント ('random-rotate') ---
// (変更なし - 省略)
AFRAME.registerComponent('random-rotate', { schema: { maxSpeed: { type: 'number', default: 5 } }, init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); }, tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); } });
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;" raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" laser-controls="hand: right; model: false; lineColor: white; lineOpacity: 0.75" ></a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-cone id="prevButton" class="clickableButton" position="-20.0 0 0.1" rotation="0 0 90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-cone id="nextButton" class="clickableButton" position="20.0 0 0.1" rotation="0 0 -90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-sphere id="closeButton" class="clickableButton" position="24 11 0.1" radius="1.5" color="red" shader="flat"> </a-sphere>
<a-sphere id="linkButton" class="clickableButton" visible="false"
position="-24 -11 0.1" radius="1.5" color="green" shader="flat">
</a-sphere>
</a-entity>
<script>
// --- グローバル変数とDOM要素の取得 ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
const linkButtonEl = document.getElementById('linkButton'); // ★★★ リンクボタン要素取得 ★★★
let rightHandEl = null;
// パネルの表示オフセット距離 (デフォルト値を5に変更済み)
const PANEL_OFFSET_FROM_OBJECT = 5.0;
// ページごとのテキストスタイル定義 (変更なし)
const BASE_TEXT_STYLE = { maxWidth: 46, align: 'center', anchorX: 'center', anchorY: 'middle', baseline: 'middle' };
const PAGE_TEXT_STYLES = { 'index': { ...BASE_TEXT_STYLE, fontSize: 0.72, color: 'white' }, 'color': { ...BASE_TEXT_STYLE, fontSize: 0.65, color: '#A0F0A0' }, 'size': { ...BASE_TEXT_STYLE, fontSize: 0.80, color: '#FFEC8B' }, 'comment': { ...BASE_TEXT_STYLE, fontSize: 0.70, color: '#ADD8E6' } };
// ★★★ 立方体ごとのコメントデータと、そのコメント用の文字色、リンクURLを定義 ★★★
const CUBE_COMMENTS = {
1: { text: ["最初の立方体だ!", "何か秘密が隠されているかもしれない…"], color: "#FF6347" },
5: { text: ["これは5番目の特別な立方体。", "長いコメントもこのように複数ページにわたって表示できます。", "素晴らしい発見だ!"], color: "#FFFF00" },
10: { text: ["ようこそ、宇宙の果てへ。\n詳細は下記リンクで!"], link: "https://aframe.io/docs/" }, // 色指定なし、リンクあり
15: { text: ["ただのコメント。"], link: "https://google.com", color: "#FFC0CB" }, // 色指定あり、リンクあり
25: { text: ["折り返し地点です。"] }, // リンクも色指定もなし
};
const DEFAULT_COMMENT_ARRAY_INFO = { text: ["コメントはありません"] }; // デフォルトコメント情報
// --- 定数定義 (変更なし) ---
const numSpheres = 60; const spread = 2000; const PAGES = ['index', 'color', 'size', 'comment']; const TOTAL_MAIN_PAGES = PAGES.length;
const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3();
// --- 立方体生成ループ (変更なし) ---
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// --- ★★★ 情報パネルの表示更新関数 (リンクボタン表示制御追加) ★★★ ---
function updatePanelDisplay() {
if (!infoPanelEl.dataset.sphereIndex) return;
const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10);
const sphereColor = infoPanelEl.dataset.color || 'N/A';
const size = infoPanelEl.dataset.size || 'N/A';
const commentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO));
const commentsArray = commentInfo.text;
const specificCommentColor = commentInfo.color;
const activeLinkUrl = infoPanelEl.dataset.activeLinkUrl || ""; // ★ リンクURLを取得
const commentSubPageCount = parseInt(infoPanelEl.dataset.commentSubPageCount || '1', 10);
const commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10);
const mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[mainPageIndex];
let pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`;
let currentStyle = { ...PAGE_TEXT_STYLES[pageType] } || { ...PAGE_TEXT_STYLES['index'] };
// ★★★ リンクボタンの表示制御 ★★★
if (pageType === 'comment' && activeLinkUrl) {
linkButtonEl.setAttribute('visible', true);
} else {
linkButtonEl.setAttribute('visible', false);
}
if (pageType === 'index') { displayText = `立方体: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${sphereColor}`; }
else if (pageType === 'size') { displayText = `サイズ: ${size}`; }
else if (pageType === 'comment') {
displayText = `コメント:\n${commentsArray[commentSubPageIndex]}`;
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`;
if (specificCommentColor) { currentStyle.color = specificCommentColor; }
}
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
});
}
// --- ★★★ 立方体クリック時の処理 (リンクURL保存追加) ★★★ ---
function handleSphereClick(event) {
event.stopPropagation();
console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName);
const clickedCube = event.target;
if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) {
console.error("Cube data missing from dataset!", clickedCube.dataset); return;
}
console.log("Cube data found:", clickedCube.dataset);
infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex;
infoPanelEl.dataset.color = clickedCube.dataset.color;
infoPanelEl.dataset.size = clickedCube.dataset.size;
infoPanelEl.dataset.currentPageIndex = '0';
const sphereIdx = parseInt(clickedCube.dataset.sphereIndex, 10);
const commentDataForThisCube = CUBE_COMMENTS[sphereIdx] || DEFAULT_COMMENT_ARRAY_INFO;
infoPanelEl.dataset.commentInfo = JSON.stringify(commentDataForThisCube);
infoPanelEl.dataset.commentSubPageCount = commentDataForThisCube.text.length.toString();
infoPanelEl.dataset.commentSubPageIndex = '0';
// ★★★ リンクURLを保存 (存在すれば) ★★★
infoPanelEl.dataset.activeLinkUrl = commentDataForThisCube.link || "";
console.log("Data (including link) stored in panel dataset.");
try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); }
catch (e) { console.error("Error during updatePanelDisplay:", e); return; }
try { clickedCube.object3D.getWorldPosition(targetWorldPosition); cameraEl.object3D.getWorldPosition(cameraWorldPosition); const cubeSide = parseFloat(clickedCube.dataset.size || 0); const offsetDistance = cubeSide / 2 + PANEL_OFFSET_FROM_OBJECT; direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize(); panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance); infoPanelEl.object3D.position.copy(panelPosition); console.log("Panel position calculated and applied."); }
catch(e) { console.error("Error during position calculation:", e); return; }
infoPanelEl.setAttribute('visible', true);
console.log("Panel visibility set to true. --- handleSphereClick end ---");
}
// --- パネルのボタンクリック処理 (変更なし) ---
prevButtonEl.addEventListener('click', function (event) { /* ... (省略) ... */ event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const commentSubPageCount = parseInt(infoPanelEl.dataset.commentSubPageCount || '1', 10); const currentCommentInfo = JSON.parse(infoPanelEl.dataset.commentInfo || JSON.stringify(DEFAULT_COMMENT_ARRAY_INFO)); if (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1) { commentSubPageIndex--; if (commentSubPageIndex < 0) { mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; const newSphereIdx = parseInt(infoPanelEl.dataset.sphereIndex, 10); /* このままだと新しい球のコメントに即座に変わらない */ const newCommentData = CUBE_COMMENTS[newSphereIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinkUrl = newCommentData.link || ""; commentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } } else { mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; commentSubPageIndex = 0; if (PAGES[mainPageIndex] === 'comment') { const newSphereIdx = parseInt(infoPanelEl.dataset.sphereIndex, 10); const newCommentData = CUBE_COMMENTS[newSphereIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinkUrl = newCommentData.link || ""; commentSubPageIndex = Math.max(0, newCommentData.text.length - 1); } else { infoPanelEl.dataset.activeLinkUrl = ""; } } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString(); updatePanelDisplay(); console.log("Prev button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex); });
nextButtonEl.addEventListener('click', function (event) { /* ... (省略) ... */ event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const commentSubPageCount = parseInt(infoPanelEl.dataset.commentSubPageCount || '1', 10); if (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1) { commentSubPageIndex++; if (commentSubPageIndex >= commentSubPageCount) { mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES; commentSubPageIndex = 0; if (PAGES[mainPageIndex] === 'comment') { const newSphereIdx = parseInt(infoPanelEl.dataset.sphereIndex, 10); const newCommentData = CUBE_COMMENTS[newSphereIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinkUrl = newCommentData.link || ""; } else { infoPanelEl.dataset.activeLinkUrl = ""; } } } else { mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES; commentSubPageIndex = 0; if (PAGES[mainPageIndex] === 'comment') { const newSphereIdx = parseInt(infoPanelEl.dataset.sphereIndex, 10); const newCommentData = CUBE_COMMENTS[newSphereIdx] || DEFAULT_COMMENT_ARRAY_INFO; infoPanelEl.dataset.commentInfo = JSON.stringify(newCommentData); infoPanelEl.dataset.commentSubPageCount = newCommentData.text.length.toString(); infoPanelEl.dataset.activeLinkUrl = newCommentData.link || ""; } else { infoPanelEl.dataset.activeLinkUrl = ""; } } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString(); updatePanelDisplay(); console.log("Next button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex); });
// --- ★★★ 閉じるボタンのクリック処理 (リンクURLデータ削除追加) ★★★ ---
closeButtonEl.addEventListener('click', function (event) {
event.stopPropagation();
infoPanelEl.setAttribute('visible', false);
delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.size;
delete infoPanelEl.dataset.currentPageIndex; delete infoPanelEl.dataset.commentInfo;
delete infoPanelEl.dataset.commentSubPageIndex; delete infoPanelEl.dataset.commentSubPageCount;
delete infoPanelEl.dataset.activeLinkUrl; // ★ リンクURLも削除
linkButtonEl.setAttribute('visible', false); // 念のためリンクボタンも非表示に
console.log("Close button clicked, panel hidden.");
});
// --- ★★★ リンクボタンのクリック処理を追加 ★★★ ---
linkButtonEl.addEventListener('click', function(event) {
event.stopPropagation();
const url = infoPanelEl.dataset.activeLinkUrl;
if (url) {
console.log("Link button clicked, opening URL:", url);
window.open(url, '_blank'); // 新しいタブでURLを開く
} else {
console.log("Link button clicked, but no URL found.");
}
});
// --- VRモードによるマウスカーソル表示制御 (変更なし) ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
// --- 右手コントローラーのトリガーイベントをリッスン (変更なし) ---
sceneEl.addEventListener('loaded', function() { rightHandEl = document.getElementById('rightHand'); if (rightHandEl) { rightHandEl.addEventListener('triggerdown', function (evt) { console.log('Right hand TRIGGER DOWN event!', evt); const raycasterComponent = rightHandEl.components.raycaster; if (raycasterComponent) { const intersectedEls = raycasterComponent.intersectedEls; if (intersectedEls.length > 0) { console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id'); } else { console.log('Right Trigger pressed, but no intersection.'); } } }); rightHandEl.addEventListener('triggerup', function (evt) { console.log('Right hand TRIGGER UP event!', evt); }); } else { console.error("Could not find rightHand element to attach trigger listener."); } });
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test43
├
<!DOCTYPE html>
<html>
<head>
<title>A-Frame - パネルテキストスタイル変更</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>
// --- プレイヤー移動制御用カスタムコンポーネント ('camera-relative-controls') ---
// (変更なし - 省略)
AFRAME.registerComponent('camera-relative-controls', { schema: { targetSpeed: { type: 'number', default: 5 }, acceleration: { type: 'number', default: 10 }, damping: { type: 'number', default: 8 }, brakingDeceleration: { type: 'number', default: 20 }, enabled: { type: 'boolean', default: true } }, init: function () { this.keys = {}; this.thumbstickInput = { x: 0, y: 0 }; this.currentVelocity = new THREE.Vector3(); this.ZERO_VECTOR = new THREE.Vector3(); this.cameraDirection = new THREE.Vector3(); this.cameraRight = new THREE.Vector3(); this.moveDirection = new THREE.Vector3(); this.desiredVelocity = new THREE.Vector3(); this.cameraWorldQuaternion = new THREE.Quaternion(); this.cameraEl = this.el.querySelector('[camera]'); this.isReady = false; if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); } this.el.sceneEl.addEventListener('loaded', () => { this.leftHand = document.getElementById('leftHand'); if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } else { console.warn("camera-relative-controls: 左手コントローラー(#leftHand)が見つかりません。"); } }); this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); window.addEventListener('keydown', this.onKeyDown); window.addEventListener('keyup', this.onKeyUp); }, remove: function () { window.removeEventListener('keydown', this.onKeyDown); window.removeEventListener('keyup', this.onKeyUp); if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onThumbstickMoved.bind(this)); } catch(e) { console.warn("camera-relative-controls: スティックリスナー解除失敗の可能性あり"); } } }, onThumbstickMoved: function (evt) { this.thumbstickInput.x = evt.detail.x; this.thumbstickInput.y = evt.detail.y; }, tick: function (time, timeDelta) { if (!this.data.enabled) return; if (!this.isReady) { if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; } else { if (!this.cameraEl) { this.cameraEl = this.el.querySelector('[camera]'); } return; } } if (!this.cameraEl || !this.cameraEl.object3D) { return; } const el = this.el; const data = this.data; const position = el.object3D.position; const dt = timeDelta / 1000; this.cameraEl.object3D.getWorldQuaternion(this.cameraWorldQuaternion); this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); if (this.cameraDirection.lengthSq() > 0.0001) this.cameraDirection.normalize(); this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; if (this.cameraRight.lengthSq() > 0.0001) this.cameraRight.normalize(); this.moveDirection.set(0, 0, 0); if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); } if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); } if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); } if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); } if (Math.abs(this.thumbstickInput.y) > 0.1) { const forwardBackward = this.cameraDirection.clone().multiplyScalar(-this.thumbstickInput.y); this.moveDirection.add(forwardBackward); } if (Math.abs(this.thumbstickInput.x) > 0.1) { const leftRight = this.cameraRight.clone().multiplyScalar(this.thumbstickInput.x); this.moveDirection.add(leftRight); } const isInputActive = this.moveDirection.lengthSq() > 0.0001; if (isInputActive) { this.moveDirection.normalize(); } let lerpFactor = data.damping; const isCurrentlyMoving = this.currentVelocity.lengthSq() > 0.01; if (isInputActive) { let isOpposingInput = false; if (isCurrentlyMoving) { const dotProduct = this.currentVelocity.dot(this.moveDirection); if (dotProduct < -0.1) { isOpposingInput = true; } } if (isOpposingInput) { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.brakingDeceleration; } else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; } } else { this.desiredVelocity.copy(this.ZERO_VECTOR); lerpFactor = data.damping; } const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt); this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor); if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.copy(this.ZERO_VECTOR); } if (this.currentVelocity.lengthSq() > 0) { const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt); position.add(deltaPosition); } }, onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight'].includes(event.code)) { this.keys[event.code] = true; } }, onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } } });
// --- 立方体をランダムに回転させるカスタムコンポーネント ('random-rotate') ---
// (変更なし - 省略)
AFRAME.registerComponent('random-rotate', { schema: { maxSpeed: { type: 'number', default: 5 } }, init: function () { this.axis = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); const speed = ((Math.random() * 0.8) + 0.2) * this.data.maxSpeed * (Math.random() < 0.5 ? 1 : -1); this.speedRad = THREE.MathUtils.degToRad(speed); this.deltaRotationQuaternion = new THREE.Quaternion(); }, tick: function (time, timeDelta) { const dt = timeDelta / 1000; const angle = this.speedRad * dt; this.deltaRotationQuaternion.setFromAxisAngle(this.axis, angle); this.el.object3D.quaternion.multiplyQuaternions(this.deltaRotationQuaternion, this.el.object3D.quaternion); } });
</script>
</head>
<body>
<a-scene id="myScene" background="color: #000000" vr-mode-ui="enabled: true">
<a-entity id="rig" position="0 0 5"
camera-relative-controls="targetSpeed: 250; acceleration: 3; damping: 5; brakingDeceleration: 1;">
<a-entity id="camera" camera="far: 20000;" look-controls position="0 1.6 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse; fuse: false;" raycaster="objects: .clickableObject, .clickableButton; far: 3000;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"> <a-animation begin="click" easing="ease-in" attribute="scale" fill="backwards" from="0.1 0.1 0.1" to="1 1 1" dur="150"></a-animation> </a-entity>
</a-entity>
<a-entity id="leftHand" oculus-touch-controls="hand: left; model: true;"></a-entity>
<a-entity id="rightHand" oculus-touch-controls="hand: right; model: true;" raycaster="objects: .clickableObject, .clickableButton; far: 3000; enabled: true;" laser-controls="hand: right; model: false; lineColor: white; lineOpacity: 0.75" ></a-entity>
</a-entity>
<a-entity light="type: ambient; color: #444"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="-1 1.5 1"></a-entity>
<a-entity light="type: directional; color: #AAA; intensity: 0.4" position="1 1 -1"></a-entity>
<a-entity id="infoPanel" visible="false" position="0 -1000 0" look-at="[camera]">
<a-plane id="panelBackground" width="50" height="24" color="#333" opacity="0.9" side="double"></a-plane>
<a-entity id="panelText" troika-text="value: Placeholder; color: white; fontSize: 0.72; maxWidth: 46; align: center; anchorX: center; anchorY: middle; baseline: middle;" position="0 0 0.05"> </a-entity>
<a-cone id="prevButton" class="clickableButton" position="-20.0 0 0.1" rotation="0 0 90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-cone id="nextButton" class="clickableButton" position="20.0 0 0.1" rotation="0 0 -90" radius-bottom="1.2" radius-top="0" height="2.0" material="color: #CCC; shader: flat; opacity: 0.8;"> </a-cone>
<a-sphere id="closeButton" class="clickableButton" position="24 11 0.1" radius="1.5" color="red" shader="flat"> </a-sphere>
</a-entity>
<script>
// --- グローバル変数とDOM要素の取得 ---
const sceneEl = document.getElementById('myScene');
const infoPanelEl = document.getElementById('infoPanel');
const panelTextEl = document.getElementById('panelText');
const cameraEl = document.getElementById('camera');
const prevButtonEl = document.getElementById('prevButton');
const nextButtonEl = document.getElementById('nextButton');
const closeButtonEl = document.getElementById('closeButton');
const mouseCursorEl = document.getElementById('mouseCursor');
let rightHandEl = null;
// ★★★ パネルの表示オフセット距離 (デフォルト値を5に変更) ★★★
const PANEL_OFFSET_FROM_OBJECT = 5.0;
// ★★★ ページごとのテキストスタイル定義 ★★★
const BASE_TEXT_STYLE = {
maxWidth: 46, align: 'center', anchorX: 'center', anchorY: 'middle', baseline: 'middle'
};
const PAGE_TEXT_STYLES = {
'index': { ...BASE_TEXT_STYLE, fontSize: 0.72, color: 'white' },
'color': { ...BASE_TEXT_STYLE, fontSize: 0.65, color: '#A0F0A0' }, // 少し彩度を落として見やすく
'size': { ...BASE_TEXT_STYLE, fontSize: 0.80, color: '#FFEC8B' }, // 少し彩度を落として見やすく
'comment': { ...BASE_TEXT_STYLE, fontSize: 0.70, color: '#ADD8E6' }
};
// ★★★ ここまでスタイル定義 ★★★
// --- 立方体ごとのコメントデータ (変更なし) ---
const CUBE_COMMENTS = { 1: ["最初の立方体だ!", "何か秘密が隠されているかもしれない…"], 5: ["これは5番目の特別な立方体。", "長いコメントもこのように複数ページにわたって表示できます。", "素晴らしい発見だ!"], 10: ["ようこそ、宇宙の果てへ。"], 25: ["折り返し地点です。"] };
const DEFAULT_COMMENT_ARRAY = ["コメントはありません"];
// --- 定数定義 (変更なし) ---
const numSpheres = 60; const spread = 2000; const PAGES = ['index', 'color', 'size', 'comment']; const TOTAL_MAIN_PAGES = PAGES.length;
const targetWorldPosition = new THREE.Vector3(); const cameraWorldPosition = new THREE.Vector3(); const direction = new THREE.Vector3(); const panelPosition = new THREE.Vector3();
// --- 立方体生成ループ (変更なし) ---
for (let i = 0; i < numSpheres; i++) { const cubeEl = document.createElement('a-box'); const side = Math.random() * 10.0 + 0.5; const x = (Math.random() - 0.5) * spread; const y = Math.random() * (spread / 2) + side / 2; const z = (Math.random() - 0.5) * spread; const color = `hsl(${Math.random() * 360}, 50%, 75%)`; const sphereIndex = i + 1; cubeEl.setAttribute('width', side); cubeEl.setAttribute('height', side); cubeEl.setAttribute('depth', side); cubeEl.setAttribute('color', color); cubeEl.setAttribute('position', { x: x, y: y, z: z }); cubeEl.classList.add('clickableObject'); cubeEl.dataset.sphereIndex = sphereIndex; cubeEl.dataset.color = color; cubeEl.dataset.size = side.toFixed(2); cubeEl.setAttribute('random-rotate', {maxSpeed: 5}); cubeEl.addEventListener('click', handleSphereClick); sceneEl.appendChild(cubeEl); }
// --- ★★★ 情報パネルの表示更新関数 (テキストスタイル適用) ★★★ ---
function updatePanelDisplay() {
if (!infoPanelEl.dataset.sphereIndex) return;
const index = parseInt(infoPanelEl.dataset.sphereIndex || '0', 10);
const color = infoPanelEl.dataset.color || 'N/A';
const size = infoPanelEl.dataset.size || 'N/A';
const commentsArray = JSON.parse(infoPanelEl.dataset.comments || JSON.stringify(DEFAULT_COMMENT_ARRAY));
const commentSubPageCount = parseInt(infoPanelEl.dataset.commentSubPageCount || '1', 10);
const commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10);
const mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10);
let displayText = '';
const pageType = PAGES[mainPageIndex];
let pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES})`;
const currentStyle = PAGE_TEXT_STYLES[pageType] || PAGE_TEXT_STYLES['index']; // スタイル取得
if (pageType === 'index') { displayText = `立方体: ${index}`; }
else if (pageType === 'color') { displayText = `色: ${color}`; }
else if (pageType === 'size') { displayText = `サイズ: ${size}`; }
else if (pageType === 'comment') {
displayText = `コメント:\n${commentsArray[commentSubPageIndex]}`;
pageIndicator = `(${mainPageIndex + 1}/${TOTAL_MAIN_PAGES} - コメント ${commentSubPageIndex + 1}/${commentSubPageCount})`;
}
const finalDisplayText = `${pageIndicator}\n${displayText}`;
// troika-text属性をオブジェクトで設定(スタイルも含む)
panelTextEl.setAttribute('troika-text', {
value: finalDisplayText,
color: currentStyle.color,
fontSize: currentStyle.fontSize,
maxWidth: currentStyle.maxWidth,
align: currentStyle.align,
anchorX: currentStyle.anchorX,
anchorY: currentStyle.anchorY,
baseline: currentStyle.baseline
});
}
// --- ★★★ 立方体クリック時の処理 (PANEL_OFFSET_FROM_OBJECT を使用) ★★★ ---
function handleSphereClick(event) {
event.stopPropagation();
console.log("--- handleSphereClick triggered --- Target:", event.target.id || event.target.tagName);
const clickedCube = event.target;
if (!clickedCube.dataset.sphereIndex || !clickedCube.dataset.color || !clickedCube.dataset.size) {
console.error("Cube data missing from dataset!", clickedCube.dataset); return;
}
console.log("Cube data found:", clickedCube.dataset);
infoPanelEl.dataset.sphereIndex = clickedCube.dataset.sphereIndex;
infoPanelEl.dataset.color = clickedCube.dataset.color;
infoPanelEl.dataset.size = clickedCube.dataset.size;
infoPanelEl.dataset.currentPageIndex = '0';
const sphereIdx = parseInt(clickedCube.dataset.sphereIndex, 10);
const commentsForThisCube = CUBE_COMMENTS[sphereIdx] || DEFAULT_COMMENT_ARRAY;
infoPanelEl.dataset.comments = JSON.stringify(commentsForThisCube);
infoPanelEl.dataset.commentSubPageCount = commentsForThisCube.length.toString();
infoPanelEl.dataset.commentSubPageIndex = '0';
console.log("Data (including comments) stored in panel dataset.");
try { updatePanelDisplay(); console.log("updatePanelDisplay completed."); }
catch (e) { console.error("Error during updatePanelDisplay:", e); return; }
try {
clickedCube.object3D.getWorldPosition(targetWorldPosition);
cameraEl.object3D.getWorldPosition(cameraWorldPosition);
const cubeSide = parseFloat(clickedCube.dataset.size || 0);
// パネルオフセット距離に新しい変数を使用
const offsetDistance = cubeSide / 2 + PANEL_OFFSET_FROM_OBJECT;
direction.subVectors(cameraWorldPosition, targetWorldPosition).normalize();
panelPosition.copy(targetWorldPosition).addScaledVector(direction, offsetDistance);
infoPanelEl.object3D.position.copy(panelPosition);
console.log("Panel position calculated and applied.");
}
catch(e) { console.error("Error during position calculation:", e); return; }
infoPanelEl.setAttribute('visible', true);
console.log("Panel visibility set to true. --- handleSphereClick end ---");
}
// --- パネルのボタンクリック処理 (コメント複数ページ対応) (変更なし) ---
prevButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const commentSubPageCount = parseInt(infoPanelEl.dataset.commentSubPageCount || '1', 10); if (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1) { commentSubPageIndex--; if (commentSubPageIndex < 0) { mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; const newCommentsArray = JSON.parse(infoPanelEl.dataset.comments || JSON.stringify(DEFAULT_COMMENT_ARRAY)); commentSubPageIndex = Math.max(0, newCommentsArray.length - 1); } } else { mainPageIndex = (mainPageIndex - 1 + TOTAL_MAIN_PAGES) % TOTAL_MAIN_PAGES; commentSubPageIndex = 0; } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString(); updatePanelDisplay(); console.log("Prev button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex); });
nextButtonEl.addEventListener('click', function (event) { event.stopPropagation(); if (!infoPanelEl.getAttribute('visible')) return; let mainPageIndex = parseInt(infoPanelEl.dataset.currentPageIndex || '0', 10); let commentSubPageIndex = parseInt(infoPanelEl.dataset.commentSubPageIndex || '0', 10); const commentSubPageCount = parseInt(infoPanelEl.dataset.commentSubPageCount || '1', 10); if (PAGES[mainPageIndex] === 'comment' && commentSubPageCount > 1) { commentSubPageIndex++; if (commentSubPageIndex >= commentSubPageCount) { mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES; commentSubPageIndex = 0; } } else { mainPageIndex = (mainPageIndex + 1) % TOTAL_MAIN_PAGES; commentSubPageIndex = 0; } infoPanelEl.dataset.currentPageIndex = mainPageIndex.toString(); infoPanelEl.dataset.commentSubPageIndex = commentSubPageIndex.toString(); updatePanelDisplay(); console.log("Next button: mainPage=", mainPageIndex, "subPage=", commentSubPageIndex); });
// --- 閉じるボタンのクリック処理 (コメントデータ削除追加) (変更なし) ---
closeButtonEl.addEventListener('click', function (event) { event.stopPropagation(); infoPanelEl.setAttribute('visible', false); delete infoPanelEl.dataset.sphereIndex; delete infoPanelEl.dataset.color; delete infoPanelEl.dataset.size; delete infoPanelEl.dataset.currentPageIndex; delete infoPanelEl.dataset.comments; delete infoPanelEl.dataset.commentSubPageIndex; delete infoPanelEl.dataset.commentSubPageCount; console.log("Close button clicked, panel hidden."); });
// --- VRモードによるマウスカーソル表示制御 (変更なし) ---
sceneEl.addEventListener('enter-vr', function () { console.log("Entered VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'false'); });
sceneEl.addEventListener('exit-vr', function () { console.log("Exited VR mode"); if (mouseCursorEl) mouseCursorEl.setAttribute('visible', 'true'); });
// --- 右手コントローラーのトリガーイベントをリッスン (変更なし) ---
sceneEl.addEventListener('loaded', function() { rightHandEl = document.getElementById('rightHand'); if (rightHandEl) { rightHandEl.addEventListener('triggerdown', function (evt) { console.log('Right hand TRIGGER DOWN event!', evt); const raycasterComponent = rightHandEl.components.raycaster; if (raycasterComponent) { const intersectedEls = raycasterComponent.intersectedEls; if (intersectedEls.length > 0) { console.log('Trigger pressed while intersecting:', intersectedEls[0].tagName, intersectedEls[0].id || 'no id'); } else { console.log('Right Trigger pressed, but no intersection.'); } } }); rightHandEl.addEventListener('triggerup', function (evt) { console.log('Right hand TRIGGER UP event!', evt); }); } else { console.error("Could not find rightHand element to attach trigger listener."); } });
</script>
</a-scene>
</body>
</html>
a-farme-球に文字test41
└
<!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"></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>
// --- ★★★ 背景画像パスの定数を追加 ★★★ ---
const SKY_BACKGROUND_IMAGE_PATH = "./pic/u3.jpg"; // 背景なしにする場合は "" (空文字列) にする
// --- グローバル変数とDOM要素の取得 ---
const sceneEl = document.getElementById('myScene');
const skyElement = document.getElementById('backgroundSkyElement'); // ★★★ a-sky要素を取得 ★★★
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;
// --- ★★★ 背景設定処理 ★★★ ---
if (SKY_BACKGROUND_IMAGE_PATH && SKY_BACKGROUND_IMAGE_PATH !== "") {
skyElement.setAttribute('src', SKY_BACKGROUND_IMAGE_PATH);
skyElement.setAttribute('visible', 'true');
// skyが表示される場合、シーンの背景は透明または干渉しない色にする
sceneEl.setAttribute('background', 'color', 'transparent');
console.log('Background sky set to:', SKY_BACKGROUND_IMAGE_PATH);
} else {
skyElement.setAttribute('visible', 'false');
skyElement.removeAttribute('src');
sceneEl.setAttribute('background', 'color', '#000000');
console.log('Background set to black (no sky image).');
}
// パネルの表示オフセット距離 (変更なし)
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 = 16;
console.log(`Using LAYOUT_PATTERN: ${LAYOUT_PATTERN}`);
const H_LINE_SPACING = 15; const H_LINE_Y = 1.6;
const H_LINE_Z_OFFSET = -35;
const H_RING_RADIUS = 50; const H_RING_OBJECT_Y = 1.6;
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 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 = RIG_INITIAL_X;
const V_LINE_START_Y_OFFSET = ( (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_DEPTH_OFFSET = -25;
// --- オブジェクト生成ループ (配置パターンロジックは変更なし) ---
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. Defaulting to random.`); }
dimension = Math.random() * 10.0 + 0.5;
}
let finalX, finalY, finalZ;
let applyRandomRotation = false;
let initialRotation = null;
const horizontalRingLookAtTarget = new THREE.Vector3(RIG_INITIAL_X, H_RING_OBJECT_Y, RIG_INITIAL_Z);
const verticalRingCenter = new THREE.Vector3(RIG_INITIAL_X, V_RING_CENTER_Y, RIG_INITIAL_Z + V_RING_FIXED_Z_DEPTH_OFFSET);
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); 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); finalY = H_LINE_Y; finalZ = RIG_INITIAL_Z + H_LINE_Z_OFFSET; applyRandomRotation = false; break;
case 5: case 6: const 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(horizontalRingLookAtTarget); 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; const 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(horizontalRingLookAtTarget); 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: 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; finalY = GRID_INITIAL_Y - row * GRID_SPACING_Y; finalZ = RIG_INITIAL_Z + GRID_Z_DEPTH; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = true; break;
case 10: const column10 = i % GRID_COLUMNS; const row10 = Math.floor(i / GRID_COLUMNS); const totalGridWidth10 = (GRID_COLUMNS - 1) * GRID_SPACING_X; const startX10 = -totalGridWidth10 / 2; finalX = startX10 + column10 * GRID_SPACING_X; finalY = GRID_INITIAL_Y - row10 * GRID_SPACING_Y; finalZ = RIG_INITIAL_Z + GRID_Z_DEPTH; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = false; break;
case 11: finalX = V_LINE_X + RIG_INITIAL_X; finalY = (RIG_INITIAL_Y_CAMERA_LEVEL + V_LINE_START_Y_OFFSET) - (i * V_LINE_SPACING); finalZ = RIG_INITIAL_Z + V_LINE_Z_OFFSET; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = true; break;
case 12: finalX = V_LINE_X + RIG_INITIAL_X; finalY = (RIG_INITIAL_Y_CAMERA_LEVEL + V_LINE_START_Y_OFFSET) - (i * V_LINE_SPACING); finalZ = RIG_INITIAL_Z + V_LINE_Z_OFFSET; initialRotation = {x: 0, y: 0, z: 0}; applyRandomRotation = false; break;
case 13: const angleStep360V_13 = (Math.PI * 2) / numObjects; const angle360V_13 = i * angleStep360V_13; finalX = RIG_INITIAL_X; finalY = RIG_INITIAL_Y_CAMERA_LEVEL + V_RING_RADIUS * Math.cos(angle360V_13); finalZ = RIG_INITIAL_Z + V_RING_RADIUS * Math.sin(angle360V_13); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(RIG_INITIAL_X, RIG_INITIAL_Y_CAMERA_LEVEL, RIG_INITIAL_Z); 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 = true; break;
case 14: const angleStep360V_14 = (Math.PI * 2) / numObjects; const angle360V_14 = i * angleStep360V_14; finalX = RIG_INITIAL_X; finalY = RIG_INITIAL_Y_CAMERA_LEVEL + V_RING_RADIUS * Math.cos(angle360V_14); finalZ = RIG_INITIAL_Z + V_RING_RADIUS * Math.sin(angle360V_14); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(RIG_INITIAL_X, RIG_INITIAL_Y_CAMERA_LEVEL, RIG_INITIAL_Z); 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 = false; break;
case 15: const arcSpan180V_15 = Math.PI; const startAngle180V_15 = -Math.PI / 2; const angleStep180V_15 = numObjects > 1 ? arcSpan180V_15 / (numObjects - 1) : 0; const thetaV_15 = startAngle180V_15 + (i * angleStep180V_15); finalX = RIG_INITIAL_X; finalY = RIG_INITIAL_Y_CAMERA_LEVEL + V_RING_RADIUS * Math.sin(thetaV_15); finalZ = RIG_INITIAL_Z - V_RING_RADIUS * Math.cos(thetaV_15); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(RIG_INITIAL_X, RIG_INITIAL_Y_CAMERA_LEVEL, RIG_INITIAL_Z); 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 = true; break;
case 16: const arcSpan180V_16 = Math.PI; const startAngle180V_16 = -Math.PI / 2; const angleStep180V_16 = numObjects > 1 ? arcSpan180V_16 / (numObjects - 1) : 0; const thetaV_16 = startAngle180V_16 + (i * angleStep180V_16); finalX = RIG_INITIAL_X; finalY = RIG_INITIAL_Y_CAMERA_LEVEL + V_RING_RADIUS * Math.sin(thetaV_16); finalZ = RIG_INITIAL_Z - V_RING_RADIUS * Math.cos(thetaV_16); objectEl.object3D.position.set(finalX, finalY, finalZ); objectEl.object3D.lookAt(RIG_INITIAL_X, RIG_INITIAL_Y_CAMERA_LEVEL, RIG_INITIAL_Z); 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 = false; 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
a-farme-球に文字test56
・その場でjavascriptの編集
実行、デバッグOK
・ツリー管理
・変数管理
・括弧の色分け(オマケ程度)
・無限Undo
・完全オリジナルの環境です。
・このCGIのお問い合わせは
・momonga-v4.live.jp まで。