Vue.jsカスタムコンポーネントレンダラー
ほとんどのWebアプリでは、DOMにレンダリングする可能性がありますが、他の目的でVueを使用したい場合がいくつかあります。 WebGLを使用してアプリを開発していて、コンポーネントのツリーとしてVueを使用してアプリを記述したいとします。 現時点では、Vueはこれを明示的にサポートしていませんが、以下で説明するように、自分で実装することは完全に可能です。
このガイドではpixi.jsを使用するため、必ずnpmからインストールしてください。 これは高度な記事であるため、webpackおよびvue2.2+で準備されたスケルトンアプリがすでにあることを前提としています。 さらに、コンポーネントの複雑な性質のため、説明は主にコードコメントを通じて行われます。
目標は、 pixi.js を使用して2DWebGLキャンバスにテクスチャを描画するために、3つのVueコンポーネント、レンダラー、コンテナー、およびスプライトコンポーネントのセットを作成することです。
最終結果は次のようになります。
注:これは、Vueで完全なPIXIレンダラーを実装するためのガイドではなく、基本的なものです。 もっと深刻なことをするつもりなら、自分でさらに何かを処理する必要があります。
レンダラーコンポーネント
これは、PIXIステージを初期化し、そのすべての子孫にPIXIオブジェクトを提供するコンポーネントです。 (経由。 Vue 2.2+ ‘ s 提供/注入システム。)
<template>
<div class="pixi-renderer">
<canvas ref="renderCanvas"></canvas>
<!-- All child <template> elements get added in here -->
<slot></slot>
</div>
</template>
<script>
import Vue from 'vue';
import * as PIXI from 'pixi.js';
export default {
data() {
return {
// These need to be contained in an object because providers are not reactive.
PIXIWrapper: {
// Expose PIXI and the created app to all descendants.
PIXI,
PIXIApp: null,
},
// Expose the event bus to all descendants so they can listen for the app-ready event.
EventBus: new Vue()
}
},
// Allows descendants to inject everything.
provide() {
return {
PIXIWrapper: this.PIXIWrapper,
EventBus: this.EventBus
}
},
mounted() {
// Determine the width and height of the renderer wrapper element.
const renderCanvas = this.$refs.renderCanvas;
const w = renderCanvas.offsetWidth;
const h = renderCanvas.offsetHeight;
// Create a new PIXI app.
this.PIXIWrapper.PIXIApp = new PIXI.Application(w, h, {
view: renderCanvas,
backgroundColor: 0x1099bb
});
this.EventBus.$emit('ready');
}
}
</script>
<style scoped>
canvas {
width: 100%;
height: 100%;
}
</style>
このコンポーネントは主に2つのことを行います。
- レンダラーがDOMに追加されたら、キャンバス上に新しいPIXIアプリを作成し、readyイベントを発行します。
- すべての子孫コンポーネントにPIXIアプリとイベントバスを提供します。
コンテナコンポーネント
コンテナコンポーネントには、任意の数のスプライトまたは他のコンテナを含めることができ、グループのネストが可能になります。
<script>
export default {
// Inject the EventBus and PIXIWrapper objects from the ancestor renderer component.
inject: ['EventBus', 'PIXIWrapper'],
// Take properties for the x and y position. (Basic, no validation)
props: ['x', 'y'],
data() {
return {
// Keep a reference to the container so children can be added to it.
container: null
}
},
// At the current time, Vue does not allow empty components to be created without a DOM element if they have children.
// To work around this, we create a tiny render function that renders to <template><!-- children --></template>.
render: function(h) {
return h('template', this.$slots.default)
},
created() {
// Create a new PIXI container and set some default values on it.
this.container = new this.PIXIWrapper.PIXI.Container();
// You should probably use computed properties to set the position instead.
this.container.x = this.x || 0;
this.container.y = this.y || 0;
// Allow the container to be interacted with.
this.container.interactive = true;
// Forward PIXI's pointerdown event through Vue.
this.container.on('pointerdown', () => this.$emit('pointerdown', this.container));
// Once the PIXI app in the renderer component is ready, add this container to its parent.
this.EventBus.$on('ready', () => {
if (this.$parent.container) {
// If the parent is another container, add to it.
this.$parent.container.addChild(this.container)
} else {
// Otherwise it's a direct descendant of the renderer stage.
this.PIXIWrapper.PIXIApp.stage.addChild(this.container)
}
// Emit a Vue event on every tick with the container and tick delta for an easy way to do frame-by-frame animation.
// (Not performant)
this.PIXIWrapper.PIXIApp.ticker.add(delta => this.$emit('tick', this.container, delta))
})
}
}
</script>
コンテナコンポーネントは、位置にxとyの2つのプロパティを取り、クリックとフレームを処理するためにpointerdownとtickの2つのイベントを発行します。それぞれ更新します。 また、子として任意の数のコンテナまたはスプライトを持つことができます。
スプライトコンポーネント
スプライトコンポーネントはコンテナコンポーネントとほぼ同じですが、PIXIのスプライトAPIにいくつかの追加の調整が加えられています。
<script>
export default {
inject: ['EventBus', 'PIXIWrapper'],
// x, y define the sprite's position in the parent.
// imagePath is the path to the image on the server to render as the sprite.
props: ['x', 'y', 'imagePath'],
data() {
return {
sprite: null
}
},
render(h) { return h() },
created() {
this.sprite = this.PIXIWrapper.PIXI.Sprite.fromImage(this.imagePath);
// Set the initial position.
this.sprite.x = this.x || 0;
this.sprite.y = this.y || 0;
this.sprite.anchor.set(0.5);
// Opt-in to interactivity.
this.sprite.interactive = true;
// Forward the pointerdown event.
this.sprite.on('pointerdown', () => this.$emit('pointerdown', this.sprite));
// When the PIXI renderer starts.
this.EventBus.$on('ready', () => {
// Add the sprite to the parent container or the root app stage.
if (this.$parent.container) {
this.$parent.container.addChild(this.sprite);
} else {
this.PIXIWrapper.PIXIApp.stage.addChild(this.sprite);
}
// Emit an event for this sprite on every tick.
// Great way to kill performance.
this.PIXIWrapper.PIXIApp.ticker.add(delta => this.$emit('tick', this.sprite, delta));
})
}
}
</script>
スプライトはコンテナとほとんど同じですが、サーバーからロードして表示する画像を選択できるimagePathプロップがあります。
使用法
これらの3つのコンポーネントを使用して、記事の冒頭で画像をレンダリングするシンプルなVueアプリ:
<template>
<div id="app">
<pixi-renderer>
<container
:x="200" :y="400"
@tick="tickInfo" @pointerdown="scaleObject"
>
<sprite :x="0" :y="0" imagePath="./assets/vue-logo.png"/>
</container>
</pixi-renderer>
</div>
</template>
<script>
import PixiRenderer from './PIXIRenderer.vue'
import Sprite from './PIXISprite.vue'
import Container from './PIXIContainer.vue'
export default {
components: {
PixiRenderer,
Sprite,
Container
},
methods: {
scaleObject(container) {
container.scale.x *= 1.25;
container.scale.y *= 1.25;
},
tickInfo(container, delta) {
console.log(`Tick delta: ${delta}`)
}
}
}
</script>
<style>
#app {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
</style>
警告
- 残念ながら、Vueでは、コンポーネントに子を追加する場合にレンダリングする要素が必要であるため、コンテナコンポーネントにはまだDOM表現が存在します。 うまくいけば、これは近い将来パッチによって解決されるでしょう。
- アプリからの入力プロパティと出力イベントをプロキシする必要があります。 これは他のライブラリと何ら変わりはありませんが、大規模なライブラリのバインディングを維持している場合は、非常に広範囲でテストが難しいタスクになる可能性があります。
- スクリーングラフとレンダラーはこの手法の特に理想的なユースケースですが、AJAXリクエストを含め、ほぼすべてに適用できます。 とはいえ、プレゼンテーションとロジックを組み合わせるのは、ほとんどの場合、ひどい考えです。
👉Vueコンポーネントで何ができるかについての知識が大幅に拡大され、アイデアが滝のように流れていることを願っています。 これらは実際には未踏の理由であるため、選択できることはたくさんあります。
慎重に踏みます! (またはしないでください!)