1. はじめに
ウェブ コンポーネント
ウェブ コンポーネントは、デベロッパーがカスタム要素を使用して HTML を拡張できるようにする一連のウェブ標準です。この Codelab では、レンガモデルを表示できる <brick-viewer>
要素を定義します。
lit-element
カスタム要素 <brick-viewer>
を定義するために、lit-element を使用します。lit-element は、軽量の基本クラスで、ウェブ コンポーネントの標準に糖衣構文を追加します。これにより、カスタム要素を簡単に起動して使用できるようになります。
使ってみる
オンラインの Stackblitz 環境でコーディングを行います。このリンクを新しいウィンドウで開きます。
stackblitz.com/edit/brick-viewer
では始めましょう。
2. カスタム要素を定義する
クラスの定義
カスタム要素を定義するには、LitElement
を拡張するクラスを作成し、@customElement
で修飾します。@customElement
の引数は、カスタム要素の名前です。
brick-viewer.ts に次のように入力します。
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
}
これで、<brick-viewer></brick-viewer>
要素を HTML で使用できるようになりました。ただし、実際に試してみると、何もレンダリングされません。これを修正しましょう。
レンダリング方法
コンポーネントのビューを実装するには、render という名前のメソッドを定義します。このメソッドは、html
関数でタグ付けされたテンプレート リテラルを返します。タグ付きテンプレート文字列に任意の HTML を挿入します。これは、<brick-viewer>
を使用するとレンダリングされます。
render
メソッドを追加します。
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick viewer</div>`;
}
}
3. LDraw ファイルの指定
プロパティを定義する
<brick-viewer>
のユーザーが、次のように属性を使用して、表示するレンガのモデルを指定できれば便利です。
<brick-viewer src="path/to/model.ldraw"></brick-viewer>
HTML 要素を作成しているため、宣言型 API を利用して、<img>
タグや <video>
タグと同様にソース属性を定義できます。lit-element では、クラス プロパティに @property
を適用するだけで簡単にできます。type
オプションを使用すると、lit-element がプロパティを解析して HTML 属性として使用する方法を指定できます。
src
プロパティと属性を定義します。
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
<brick-viewer>
に、HTML で設定可能な src
属性が追加されました。lit-element のおかげで、その値は BrickViewer
クラス内からすでに読み取れます。
値の表示
src
属性の値を表示するには、レンダリング メソッドのテンプレート文字列で使用します。${value}
構文を使用して、値をテンプレート文字列に補間します。
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick model: ${this.src}</div>`;
}
}
これで、ウィンドウの <brick-viewer>
要素の src 属性の値が表示されます。方法: ブラウザのデベロッパー ツールを開き、src 属性を手動で変更します。試してみましょう...
要素内のテキストが自動的に更新されることにお気づきでしょうか?lit-element は @property
で装飾されたクラス プロパティを監視し、ビューを再レンダリングします。複雑な処理は lit-element が行うため、お客様による作業は必要ありません。
4. Three.js を使用する
ライト、カメラ、レンダリング
カスタム要素では、three.js を使用して 3D レンガモデルをレンダリングします。Three.js のシーン、カメラ、照明の設定など、<brick-viewer>
要素のインスタンスごとに 1 回だけ行う処理があります。これらを BrickViewer クラスのコンストラクタに追加します。カメラ、シーン、コントロール、レンダラは、後で使用できるようにクラス プロパティとして保持します。
3.js シーンのセットアップを追加します。
export class BrickViewer extends LitElement {
private _camera: THREE.PerspectiveCamera;
private _scene: THREE.Scene;
private _controls: OrbitControls;
private _renderer: THREE.WebGLRenderer;
constructor() {
super();
this._camera = new THREE.PerspectiveCamera(45, this.clientWidth/this.clientHeight, 1, 10000);
this._camera.position.set(150, 200, 250);
this._scene = new THREE.Scene();
this._scene.background = new THREE.Color(0xdeebed);
const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
this._scene.add( ambientLight );
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(-1000, 1200, 1500);
this._scene.add(directionalLight);
this._renderer = new THREE.WebGLRenderer({antialias: true});
this._renderer.setPixelRatio(window.devicePixelRatio);
this._renderer.setSize(this.offsetWidth, this.offsetHeight);
this._controls = new OrbitControls(this._camera, this._renderer.domElement);
this._controls.addEventListener("change", () =>
requestAnimationFrame(this._animate)
);
this._animate();
const resizeObserver = new ResizeObserver(this._onResize);
resizeObserver.observe(this);
}
private _onResize = (entries: ResizeObserverEntry[]) => {
const { width, height } = entries[0].contentRect;
this._renderer.setSize(width, height);
this._camera.aspect = width / height;
this._camera.updateProjectionMatrix();
requestAnimationFrame(this._animate);
};
private _animate = () => {
this._renderer.render(this._scene, this._camera);
};
}
WebGLRenderer
オブジェクトは、レンダリングされた {/5}3.js シーンを表示する DOM 要素を提供します。domElement
プロパティを介してアクセスします。${value}
構文を使用して、この値をレンダリング テンプレート リテラルに補間できます。
テンプレートに含まれていた src
メッセージを削除し、レンダラの DOM 要素を挿入します。
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
`;
}
}
レンダラ DOM 要素全体を表示できるようにするには、<brick-viewer>
要素自体も display: block
に設定する必要があります。styles
という静的プロパティでスタイルを指定し、css
テンプレート リテラルに設定します。
クラスに次のスタイルを追加します。
export class BrickViewer extends LitElement {
static styles = css`
/* The :host selector styles the brick-viewer itself! */
:host {
display: block;
}
`;
}
これで、<brick-viewer>
がレンダリングされた 3.js シーンを表示できるようになりました。
でも... 空っぽです。モデルを指定しましょう。
ブリック ローダー
前に定義した src
プロパティを LDrawLoader に渡します。このローダーは、3.js で提供されます。
LDraw ファイルでは、Brick モデルを別々の作成ステップに分割できます。ステップの総数と個々のブロックの表示は、LDrawLoader API を通じて確認できます。
次のプロパティ、新しい _loadModel
メソッド、コンストラクタの新しい行をコピーします。
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
private _loader = new LDrawLoader();
private _model: any;
private _numConstructionSteps?: number;
step?: number;
constructor() {
// ...
// Add this line right before this._animate();
(this._loader as any).separateObjects = true;
this._animate();
}
private _loadModel() {
if (this.src === null) {
return;
}
this._loader
.setPath('')
// Using our src property!
.load(this.src, (newModel) => {
if (this._model !== undefined) {
this._scene.remove(this._model);
this._model = undefined;
}
this._model = newModel;
// Convert from LDraw coordinates: rotate 180 degrees around OX
this._model.rotation.x = Math.PI;
this._scene.add(this._model);
this._numConstructionSteps = this._model.userData.numConstructionSteps;
this.step = this._numConstructionSteps!;
const bbox = new THREE.Box3().setFromObject(this._model);
this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
this._controls.update();
this._controls.saveState();
});
}
}
_loadModel
を呼び出すタイミングsrc 属性が変更されるたびに呼び出す必要があります。src
プロパティを @property
で装飾することで、このプロパティを lit-element の更新ライフサイクルにオプトインしました。これらの装飾プロパティのいずれかの値が変更されるたびに、プロパティの新旧の値にアクセスできる一連のメソッドが呼び出されます。ここでは、update
というライフサイクル メソッドを使用します。update
メソッドは PropertyValues
引数を受け取ります。この引数には、変更されたプロパティに関する情報が含まれます。ここは _loadModel
さんに電話をかけるのに最適です。
update
メソッドを追加します。
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
super.update(changedProperties);
}
}
<brick-viewer>
要素で、src
属性で指定したレンガファイルを表示できるようになりました。
5. 部分モデルの表示
次に、現在のコンストラクション ステップを構成可能にします。<brick-viewer step="5"></brick-viewer>
を指定できるようにし、5 番目の構築ステップでレンガモデルの外観を確認できるようにします。そのためには、step
プロパティを @property
で装飾し、監視対象のプロパティにします。
step
プロパティを装飾します。
export class BrickViewer extends LitElement {
@property({type: Number})
step?: number;
}
次に、現在のビルドステップまでのブロックのみを表示するヘルパー メソッドを追加します。update メソッドでヘルパーを呼び出して、step
プロパティが変更されるたびに実行されるようにします。
update
メソッドを更新し、新しい _updateBricksVisibility
メソッドを追加します。
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
if (changedProperties.has('step')) {
this._updateBricksVisibility();
}
super.update(changedProperties);
}
private _updateBricksVisibility() {
this._model && this._model.traverse((c: any) => {
if (c.isGroup && this.step) {
c.visible = c.userData.constructionStep <= this.step;
}
});
requestAnimationFrame(this._animate);
}
}
では、ブラウザの devtools を開き、<brick-viewer>
要素を調べます。以下のように step
属性を追加します。
レンダリングされたモデルに何が起こるかをご覧ください。step
属性を使用して、モデルの表示範囲を制御できます。step
属性が "10"
に設定されている場合は次のようになります。
6. ブロックセット ナビゲーション
MWC アイコンボタン
<brick-viewer>
のエンドユーザーも、UI を介してビルドステップをナビゲートできる必要があります。次のステップ、前のステップ、最初のステップに移動するボタンを追加しましょう。簡単になるように、マテリアル デザインのボタンウェブ コンポーネントを使用します。@material/mwc-icon-button
はすでにインポートされているため、<mwc-icon-button></mwc-icon-button>
を挿入できます。使用するアイコンは、icon 属性で指定します(例: <mwc-icon-button icon="thumb_up"></mwc-icon-button>
)。利用可能なすべてのアイコンは、material.io/resources/icons で確認できます。
レンダリング メソッドにアイコンボタンを追加しましょう。
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
<div id="controls">
<mwc-icon-button icon="replay"></mwc-icon-button>
<mwc-icon-button icon="navigate_before"></mwc-icon-button>
<mwc-icon-button icon="navigate_next"></mwc-icon-button>
</div>
`;
}
}
ウェブ コンポーネントのおかげで、ページでマテリアル デザインを簡単に使用できます。
イベント バインディング
これらのボタンは実際に何かを行う必要があります。[返信] ボタンを押すと、作成ステップが 1 にリセットされます。「nav_before」ボタンでステップ数が減少し、「nav_next」ボタンでステップの値がインクリメントされます。lit-element を使用すると、イベント バインディングを使用してこの機能を簡単に追加できます。html テンプレート文字列で、要素属性として @eventname=${eventHandler}
の構文を使用します。これで、その要素で eventname
イベントが検出されると、eventHandler
が実行されます。例として、3 つのアイコンボタンにクリック イベント ハンドラを追加してみましょう。
export class BrickViewer extends LitElement {
private _restart() {
this.step! = 1;
}
private _stepBack() {
this.step! -= 1;
}
private _stepForward() {
this.step! += 1;
}
render() {
return html`
${this._renderer.domElement}
<div id="controls">
<mwc-icon-button @click=${this._restart} icon="replay"></mwc-icon-button>
<mwc-icon-button @click=${this._stepBack} icon="navigate_before"></mwc-icon-button>
<mwc-icon-button @click=${this._stepForward} icon="navigate_next"></mwc-icon-button>
</div>
`;
}
}
今すぐボタンをクリックしてみてください。よくできました。
スタイル
ボタンは機能しているが、見栄えが良くない。すべて下部に集まっています。スタイルを設定して、シーンに重ねてみましょう。
これらのボタンにスタイルを適用するには、static styles
プロパティに戻ります。これらのスタイルはスコープが設定されているため、このウェブ コンポーネント内の要素にのみ適用されます。それこそが、ウェブ コンポーネントを作成するメリットの 1 つです。セレクタはシンプルになり、CSS は読み取りと書き込みが簡単になります。BEM にさようなら
スタイルを次のように更新します。
export class BrickViewer extends LitElement {
static styles = css`
:host {
display: block;
position: relative;
}
#controls {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
}
`;
}
[カメラをリセット] ボタン
<brick-viewer>
のエンドユーザーは、マウス操作でシーンを回転できます。ボタンを追加するついでに、カメラをデフォルトの位置にリセットするボタンも追加しましょう。クリック イベント バインディングを持つ別の <mwc-icon-button>
によって処理が完了します。
export class BrickViewer extends LitElement {
private _resetCamera() {
this._controls.reset();
}
render() {
return html`
<div id="controls">
<!-- ... -->
<!-- Add this button: -->
<mwc-icon-button @click=${this._resetCamera} icon="center_focus_strong"></mwc-icon-button>
</div>
`;
}
}
簡単な操作
階段がたくさんあるブロックもあります。ユーザーは特定のステップにスキップできます。ステップ番号のスライダーを追加すると、クイック ナビゲーションに役立ちます。ここでは <mwc-slider>
要素を使用します。
mwc-slider
スライダー要素には、スライダーの最小値や最大値など、いくつかの重要なデータが必要です。スライダーの最小値は常に「1」です。モデルが読み込まれている場合、スライダーの最大値は this._numConstructionSteps
にする必要があります。これらの値は、属性を使用して <mwc-slider>
に伝えることができます。ifDefined
lit-html ディレクティブを使用して、_numConstructionSteps
プロパティが定義されていない場合に max
属性を設定することもできます。
[戻る] ボタンと [進む] ボタンの間に <mwc-slider>
を追加します。
export class BrickViewer extends LitElement {
render() {
return html`
<div id="controls">
<!-- ... backwards button -->
<!-- Add this slider: -->
<mwc-slider
step="1"
pin
markers
min="1"
max=${ifDefined(this._numConstructionSteps)}
></mwc-slider>
<!-- ... forwards button -->
</div>
`;
}
}
データの「アップ」
ユーザーがスライダーを動かすと、現在の作成ステップが変わり、それに応じてモデルの公開設定が更新されます。スライダー要素は、スライダーがドラッグされるたびに入力イベントを送信します。スライダー自体にイベント バインディングを追加して、このイベントをキャッチし、作成ステップを変更します。
イベント バインディングを追加します。
export class BrickViewer extends LitElement {
render() {
return html`
<div id="controls">
<!-- ... -->
<!-- Add the @input event binding: -->
<mwc-slider
...
@input=${(e: CustomEvent) => this.step = e.detail.value}
></mwc-slider>
<!-- ... -->
</div>
`;
}
}
おめでとうございます!スライダーを使用して、表示するステップを変更できます。
データ「ダウン」
もう 1 つあります。[戻る] ボタンと [次へ] ボタンを使用してステップを変更する場合は、スライダーハンドルを更新する必要があります。<mwc-slider>
の value 属性を this.step
にバインドします。
value
バインディングを追加します。
export class BrickViewer extends LitElement {
render() {
return html`
<div id="controls">
<!-- ... -->
<!-- Add the value property binding: -->
<mwc-slider
...
value=${ifDefined(this.step)}
></mwc-slider>
<!-- ... -->
</div>
`;
}
}
スライダーの設定はほぼ完了です。フレックス スタイルを追加して、他のコントロールとうまく組み合わせられるようにします。
export class BrickViewer extends LitElement {
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
}
また、スライダー要素自体で layout
を呼び出す必要があります。これは、DOM が最初に配置されたときに呼び出される firstUpdated
ライフサイクル メソッドで行います。query
デコレータを使用すると、テンプレート内のスライダー要素への参照を取得できます。
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
}
追加したスライダーをまとめると次のようになります(見やすくするために、スライダーに pin
属性と markers
属性を追加しています)。
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
render() {
return html`
${this._renderer.domElement}
<div id="controls">
<mwc-icon-button @click=${this._restart} icon="replay"></mwc-icon-button>
<mwc-icon-button @click=${this._stepBack} icon="navigate_before"></mwc-icon-button>
<mwc-slider
step="1"
pin
markers
min="1"
max=${ifDefined(this._numConstructionSteps)}
?disabled=${this._numConstructionSteps === undefined}
value=${ifDefined(this.step)}
@input=${(e: CustomEvent) => this.constructionStep = e.detail.value}
></mwc-slider>
<mwc-icon-button @click=${this._stepForward} icon="navigate_next"></mwc-icon-button>
<mwc-icon-button @click=${this._resetCamera} icon="center_focus_strong"></mwc-icon-button>
</div>
`;
}
}
これが最終的な成果物です。
7. まとめ
lit-element を使用して独自の HTML 要素を作成する方法について、多くのことを学びました。以下の方法を学びました。
- カスタム要素を定義する
- 属性 API を宣言する
- カスタム要素のビューをレンダリングする
- スタイルをカプセル化する
- イベントとプロパティを使用してデータを渡す
lit-element について詳しくは、公式サイトをご覧ください。
完成した brick-viewer 要素は stackblitz.com/edit/brick-viewer-complete で確認できます。
brick-viewer も NPM に付属しており、GitHub リポジトリでソースを確認できます。