JavaScript, Events, DOM APIs
A-Frameは単なるHTMLなので、通常のWeb開発と同様にJavaScriptとDOM (opens new window) APIを使ってシーンとそのエンティティを制御することができます。
Image by Ruben Mueller from The VR Jump (opens new window).
シーン内のすべての要素は、<a-box>
や<a-sky>
といった要素であっても、エンティティ(<a-entity>
として示される)となります。A-Frameは、HTML要素のプロトタイプを修正し、特定のDOM APIに対して、A-Frameに合わせた特別なビヘイビアを追加しA-Frame向けにカスタマイズします。以下に説明するほとんどのAPIに関するリファレンスは、 Entity API ドキュメント を参照してください。
# A-FrameのJSコードをどこに記述するか
重要事項:
JavaScriptとDOM APIのさまざまな使い方を説明する前に、JavaScriptコードをA-Frame components内にカプセル化することをお勧めします。コンポーネントは、コードをモジュール化し、ロジックや動作をHTMLから見えるようにし、コードが正しいタイミングで実行されるようにします(例えば、シーンやエンティティがアタッチされ初期化された後など)。最も基本的な例として、<a-scene>
の前に console.log
コンポーネントを登録登録してみます。
AFRAME.registerComponent('log', {
schema: {type: 'string'},
init: function () {
var stringToLog = this.data;
console.log(stringToLog);
}
});
そしてこの登録の後に、このコンポーネントを入れてみてください。
<a-scene log="Hello, Scene!">
<a-box log="Hello, Box!"></a-box>
</a-scene>
コンポーネントは私達のコードを再利用可能な形で、宣言的に利用し、共有可能な形でカプセル化してくれます。 ただし、実行時にちょっと調べたいなら、ブラウザの開発者向けツールのコンソールから、シーン上でJavaScriptを実行することができます。
Aフレームに関連するJavaScriptは<a-scene>
の後に<script>
タグに内に直接書き込まないようにしてください。これは"Dでのスクリプトでは問題ないですが、このようにしてしまうと
、コードが適切なタイミングで実行されるようにするために特別な対策を講じなければならなくなります
(シーン内のコンテンツスクリプトを実行するを参照)
# 要求と横断によってエンティティを取得する Getting Entities by Querying and Traversing
シーンを書くための道具としての DOM の素晴らしいところは、標準のDOM で .querySelector()
と .querySelectorAll()
を通じて横断(トラバーサル)、問い合わせ(クエリー)、検索、選択のためのユーティリティを提供することです。元々は jQuery のセレクタ (opens new window)に触発されたものです。クエリセレクタについてはMDN (opens new window)で学ぶことができます。
それでは、いくつかのクエリセレクタの例を実行してみましょう。下のシーンを例にとってみましょう。
<html>
<a-scene>
<a-box id="redBox" class="clickable" color="red"></a-box>
<a-sphere class="clickable" color="blue"></a-sphere>
<a-box color="green"></a-box>
<a-entity light="type: ambient"></a-entity>
<a-entity light="type: directional"></a-entity>
</a-scene>
</html>
# .querySelector()
を使う方法
ひとつのエレメントを問い合わせたい場合は、.querySelector()
を使います。これは一つのエレメントを返します。シーンのエレメントに対してクエリをかけてみましょう。
var sceneEl = document.querySelector('a-scene');
注 コンポーネント内で作業していれば、クエリを使わなくても、既にシーンエレメントへのリファレンスを持っているはずです。 すべてのエンティティは、そのシーンエレメントへのファレンスを持っています。
AFRAME.registerComponent('foo', {
init: function () {
console.log(this.el.sceneEl); // Reference to the scene element.
}
});
もし要素がIDを持つ場合は、IDセレクタ(例:#<ID
>)を使用することができます。
IDを含んだ赤いボックスを指定してみましょう。ドキュメント全体に対してクエリセレクタを実行する前に、ここでは、シーンの範囲内に対してクエリセレクタを実行してみます。
クエリセレクタを使うと、クエリの範囲を任意のエレメント内に限定することができます。
var sceneEl = document.querySelector('a-scene');
console.log(sceneEl.querySelector('#redBox'));
// <a-box id="redBox" class="clickable" color="red"></a-box>
# .querySelectorAll()
を使う方法
エレメントのグループを問い合わせたい場合には .querySelectorAll()
を使用します。これはエレメントの配列を返します。これにはエレメント名を用いて問い合わせます。
console.log(sceneEl.querySelectorAll('a-box'));
// [
// <a-box id="redBox" class="clickable" color="red"></a-box>,
// <a-box color="green"></a-box>
// ]
クラスを持つ要素はクラスセレクタ(例:.<CLASS_NAME>
)で問い合わせることができます。clickable
クラスを持つすべてのエンティティを取得してみます。
console.log(sceneEl.querySelectorAll('.clickable'));
// [
// <a-box id="redBox" class="clickable" color="red"></a-box>
// <a-sphere class="clickable" color="blue"></a-sphere>
// ]
属性(この場合はコンポーネント)を含む要素は、属性セレクタ(つまり [<ATTRIBUTE_NAME>
] )
で問い合わせることができるのです。light
を持っているすべてのエンティティを取得してみましょう。
console.log(sceneEl.querySelectorAll('[light]'));
// [
// <a-entity light="type: ambient"></a-entity>
// <a-entity light="type: directional"></a-entity>
// ]
# .querySelectorAll()
からエンティティをループさせる
.querySelectorAll()
を使ってエンティティ群を指定したら、for
ループでループさせることができます。シーン内のすべての要素に対して、*
でループしてみましょう。
var els = sceneEl.querySelectorAll('*');
for (var i = 0; i < els.length; i++) {
console.log(els[i]);
}
# パフォーマンスに関する注意
.querySelector
と .querySelectorAll
をフレームごとに呼び出される tick
tock
関数では使わないでください。DOMがそれを追跡しループ処理を行う際に時間を取られます。
代わりに、あらかじめクエリセレクタを呼び出したエンティティのリストをキャッシュしておき、その上でループさせるようにします。
AFRAME.registerComponent('query-selector-example', {
init: function () {
this.entities = document.querySelectorAll('.box');
},
tick: function () {
// Don't call query selector in here, query beforehand.
for (let i = 0; i < this.entities.length; i++) {
// Do something with entities.
}
}
});
# .getAttribute()
を使ってコンポーネントデータを取得する
エンティティのコンポーネントのデータは、.getAttribute
で取得することができます。
A-Frameは.getAttribute
を拡張しており、文字列ではなく値を返すようになっています
(例えば、コンポーネントは通常複数のプロパティで構成されているので、ほとんどの場合オブジェクトを返したり、.getAttribute('visible')のように実際のブール値を返したりします。
多くの場合、.getAttributeはコンポーネントの内部データオブジェクトを返すので、オブジェクトを直接変更しないでください。
// <a-entity geometry="primitive: sphere; radius: 2"></a-entity>
el.getAttribute('geometry');
// >> {"primitive": "sphere", "radius": 2, ...}
# position
and scale
を取得する
el.getAttribute('position')
または el.getAttribute('scale')
を実行すると、次のような結果が得られます。
three.js
の Object3D (opens new window) の position と scale のプロパティを[Vector3][ベクター3]で返します。
これらのオブジェクトを変更すると、そのエンティティのデータも更新されます。
これは、A-Frameではthree.jsのレベルで[位置、回転、スケールを変更する]ことができ、
.getAttribute
で正しいデータを返すため、A-Frameはthree.jsのObject3Dオブジェクトを返します。
これは、.getAttribute('rotation')
の場合は当てはまりません。
これはA-Frameが、ラジアンではなく度数を使うためです。
このような場合、通常のx/y/zのプロパティを持つJavaScriptオブジェクトが返されます。
ラジアンを使いたい場合は、el.object3D.rotation
で取得します。
# A-Frame のシーングラフを変更する
JavaScript と DOM API を使用すると、エンティティを動的に追加したり削除したりすることができます。 通常のHTML要素と同様の方法です。
# .createElement()
を使ってエンティティを作成する
エンティティを作成するには、document.createElement
を使用します。これは空白のエンティティを出力します。
``js var el = document.createElement('a-entity');
しかし、このエンティティは初期化されず、シーンに付与するまではシーンの一部にもなりません。
### `.appendChild()` を使ってエンティティを追加する
DOMにエンティティを追加するには、`.appendChild(element)`を使用します。具体的には
をシーンに追加したいです。これでシーンを取得し、エンティティを作成し、そして
エンティティをシーンに追加できます。
```js
var sceneEl = document.querySelector('a-scene');
var entityEl = document.createElement('a-entity');
// Do `.setAttribute()`s to initialize the entity.
sceneEl.appendChild(entityEl);
注
.appendChild()は、ブラウザでは非同期 オペレーションであることに注意してください。
エンティティがDOMに追加完了するまで、エンティティに対して多くの操作(例えば.getAttribute()の呼び出し)を行うことができません。
追加されたばかりのエンティティの属性を問い合わせる必要がある場合は、エンティティのloaded
イベントを使うことが可能です。もしくはA-Frameのコンポーネントの中にロジックを配置します。
そうすることで準備ができたら実行されます。
var sceneEl = document.querySelector('a-scene');
AFRAME.registerComponent('do-something-once-loaded', {
init: function () {
// This will be called after the entity has properly attached and loaded.
console.log('I am ready!');
}
});
var entityEl = document.createElement('a-entity');
entityEl.setAttribute('do-something-once-loaded', '');
sceneEl.appendChild(entityEl);
# .removeChild()
を使ってエンティティを削除する
DOM、しかるにシーンからエンティティを削除するには、親要素から .removeChild(element)
を呼び出します。
エンティティがある場合は、その親(parentNode
)に実体を削除するよう依頼する必要があります。
entityEl.parentNode.removeChild(entityEl);
# エンティティを変更する
ブランクのエンティティは何もしません。コンポーネントを追加したり、コンポーネントのプロパティを設定したり、 コンポーネントを削除することで、エンティティを変更することができます。
# .setAttribute()
でコンポーネントを追加する
コンポーネントを追加するには、.setAttribute(componentName, data)
を使うことができます。
エンティティにジオメトリコンポーネントを追加してみましょう。
entityEl.setAttribute('geometry', {
primitive: 'box',
height: 3,
width: 1
});
もしくは コミュニティ物理コンポーネント (opens new window)を追加します。
entityEl.setAttribute('dynamic-body', {
shape: 'box',
mass: 1.5,
linearDamping: 0.005
});
通常のHTMLの.setAttribute()
とは異なり、エンティティの.setAttribute()
は、オブジェクトなどの
様々なタイプの引数を取得したり、コンポーネントの単一のプロパティを更新できるように改良されています。
Entity.setAttribute()
の詳細については、こちらをご覧ください。
# .setAttribute()
を使ってエンティティを更新する
コンポーネントを更新するにも.setAttribute()
を使います。コンポーネントの更新はいくつかの形態があります。
# シングル・プロパティ・コンポーネントのプロパティを更新する
シングルプロパティコンポーネントであるポジションコンポーネントのプロパティを更新してみましょう。 オブジェクトか文字列のどちらかを渡すことができます。オブジェクトを渡すと、A-Frameが文字列をパースする必要がないので、オブジェクトのほうが好ましいです。
entityEl.setAttribute('position', {x: 1, y: 2, z: -3});
// Read on to see why `entityEl.object3D.position.set(1, 2, -3)` is preferred though.
# プロパティ・コンポーネント内の単体プロパティを更新する
マルチプロパティコンポーネントであるマテリアルコンポーネントの1つのプロパティを更新してみましょう。
.setAttribute()
にコンポーネント名、プロパティ名、そしてプロパティ値を指定することで、これを行います。
entityEl.setAttribute('material', 'color', 'red');
# マルチプロパティコンポーネントの複数のプロパティを更新する
マルチプロパティコンポーネントである ライトコンポーネントの複数のプロパティを一度に更新してみましょう。
これは、コンポーネント名とプロパティのオブジェクトを .setAttribute()
に指定することで行います。
ここでは、ライトの色と強さを変更しますが、タイプはそのままにしておきます。
// <a-entity light="type: directional; color: #CAC; intensity: 0.5"></a-entity>
entityEl.setAttribute('light', {color: '#ACC', intensity: 0.75});
// <a-entity light="type: directional; color: #ACC; intensity: 0.75"></a-entity>
# オブジェクトの 位置position
、 回転rotation
、縮尺 scale
、 透明度visible
.を更新する
特殊なケースですが、パフォーマンス、メモリ、ユーティリティへのアクセスを改善するために、.setAttribute
ではなく、
エンティティのObject3D (opens new window)を介してthree.jsレベルで直接位置、回転、スケール、可視性を変更することを推奨します。
// Examples for position.
entityEl.object3D.position.set(1, 2, 3);
entityEl.object3D.position.x += 5;
entityEl.object3D.position.multiplyScalar(5);
// Examples for rotation.
entityEl.object3D.rotation.y = THREE.Math.degToRad(45);
entityEl.object3D.rotation.divideScalar(2);
// Examples for scale.
entityEl.object3D.scale.set(2, 2, 2);
entityEl.object3D.scale.z += 1.5;
// Examples for visible.
entityEl.object3D.visible = false;
entityEl.object3D.visible = true;
これにより、.setAttribute
のオーバーヘッドをスキップして、代わりに最も頻繁に更新されるコンポーネントの
プロパティを簡単に設定することができます。
three.jsレベルでの更新は、例えばentityEl.getAttribute('position');
のようにしたときにも反映されます。
# マルチプロパティコンポーネントのプロパティを置き換える
マルチプロパティコンポーネントであるジオメトリコンポーネントのプロパティをすべて置き換えてみましょう。
コンポーネント名、.setAttribute()
に指定するプロパティのオブジェクト、そして既存のプロパティを削除する
フラグを指定することで実行します。ジオメトリの既存のプロパティをすべて新しいプロパティに置き換えます。
// <a-entity geometry="primitive: cylinder; height: 4; radius: 2"></a-entity>
entityEl.setAttribute('geometry', {primitive: 'torusKnot', p: 1, q: 3, radiusTubular: 4}, true);
// <a-entity geometry="primitive: torusKnot; p: 1; q: 3; radiusTubular: 4"></a-entity>
# .removeAttribute()
を使ってによるコンポーネントを削除する
エンティティからコンポーネントを削除したり、切り離したりするには、 .removeAttribute(componentName)
を使用することができます。カメラエンティティからデフォルトのwasd-controls
を削除してみましょう。
var cameraEl = document.querySelector('[camera]');
cameraEl.removeAttribute('wasd-controls');
# イベント及びイベントリスナー
JavaScript と DOM では、エンティティやコンポーネントが互いに通信するための簡単な方法があります。 イベントとイベントリスナーです。イベントとは、他のコードが拾って反応できるようなシグナルを送る方法です。ブラウザのイベントについては、こちらをご覧ください (opens new window)。
# .emit()
ででイベントを発信する
A-Frame要素では、.emit(eventName, eventDetail, bubbles)
を使って簡単にカスタムイベントを発信することができます。
例えば、物理コンポーネントを作っていて、エンティティが他のエンティティと衝突したときにシグナルを送出するようにしたいとします。
entityEl.emit('physicscollided', {collidingEntity: anotherEntityEl}, false);
そして、コードの他の部分は、このイベントを待ち、リッスンし、それに応答してコードを実行することができます。 第2引数として、イベントの詳細情報を介して情報やデータを渡すことができます。 また、イベントがバブル化するかどうかを指定することができます。これは親エンティティも同じようにイベントを発するかどうかを指定することができるという意味です。 つまり、コードの他の部分は、イベントリスナーを登録することができます。
# .addEventListener()
を使ってでイベントリスナーを追加する
通常のHTML要素と同様に、.addEventListener(eventName, function)
でイベントリスナーを
登録することができます。
実行されるリスナーが登録されたイベントが発生すると、その関数が呼び出され、イベントを処理します。
例えば、先程の物理衝突のイベントの続きです。
entityEl.addEventListener('physicscollided', function (event) {
console.log('Entity collided with', event.detail.collidingEntity);
});
エンティティがphysicscollided
を発すると、そのイベント・オブジェクトを使って関数が呼び出されます。
イベントオブジェクトには、イベントを通して渡されるデータや情報が含まれるイベント詳細があることに注目しましょう。
# .removeEventListener()
を用いてイベントリスナーを削除する
通常のHTML要素と同様に、イベントリスナーを削除したい場合は、.removeEventListener(eventName, function)
を使用します。
使用する際にはリスナーが登録されていたのと同じイベント名と関数を渡さなければなりません。
例えば、先ほどの物理衝突イベントの例から続けると、以下のようになります。
// We have to define this function with a name if we later remove it.
function collisionHandler (event) {
console.log('Entity collided with', event.detail.collidingEntity);
}
entityEl.addEventListener('physicscollided', collisionHandler);
entityEl.removeEventListener('physicscollided', collisionHandler);
# イベントリスナーのバインド
Javascriptの実行コンテキストルールでは基本的に、'this'をグローバルコンテキスト(ウィンドウ)に紐付け、独立した関数にしています。 これはこの関数は初期段階ではコンポーネント内の'this'にアクセスする方法がないことを意味します。
イベントリスナー内でコンポーネントの'this'にアクセスできるようにするためには、'this'をバインドする必要があります。
これを行うには、いくつかの方法があります。
1.イベントリスナーを定義するためにアロー関数を使用する方法。アロー関数は自動的にthisをバインドします。
this.el.addEventListener('physicscollided', (event) => {
console.log(this.el.id);
});
2.イベントリスナーをコンポーネントのイベントオブジェクト内に定義する方法(これにより、リスナーの追加と削除も自動的に行われます)。
こちらの解説をご覧ください。
3.関数のバインドバージョンである別の関数を作成することで、その関数を使用することができます。
this.listeners = {
clickListener: this.clickListener.bind(this);
}
entityEl.addEventListener('click', this.listeners.clickListener);
});
# 注意事項
A-Frameのエンティティおよびプリミティブは、パフォーマンスを優先して実装されているため、 一部のHTML APIが期待通りに動作しないことがあります。 例えば、値を含む属性セレクタ (opens new window)は動作しませんし、mutation observer (opens new window) は、エンティティのコンポーネントが変更されたときでも変更内容を発射しません。