Vue.js と CreateJS でなんとかジェネレーターを作ろう

最終更新日
2017.12.10

これは Vue.js #2 Advent Calendar 2017 4日目の記事です。

Vue.js ❤ CreateJS

Vue.js と言えば SVG との相性が抜群ですが、Canvas にも勿論良いところがあります。 私は業務で画像生成系のツールを作る事がよくあり、最近は Vue.js と CreateJS を使っています。CreateJS は主に 2D 向けの Canvas 操作に特化したライブラリです。 Canvas は複雑な変換処理をしなくても画像をそのままサーバーやローカルに保存できるので画像を使った ジェネレーターやシミュレーター だったり、要素が多く動きが複雑なブラウザゲームでも活躍してくれます。

Vue.js を既に使っていて、Canvas を使って何かしてみたい という人向けの記事ですが、Canvas 使うのもおもしろそう。ってチョットでも思ってもらえたら嬉しいです。

簡単にデータバインドする

CreateJS のシェイプやコマンドは Vue.js の data に登録するとプロパティをバインドする事が出来ます。

コマンドとは?

CreateJS は シェイプ内のグラフィックへのアプローチにコマンドという機能を使用します。 通常は最後のメソッドのコマンドにしかアクセスする事はできません。

shape.graphics.beginFill('#000')
shape.graphics.drawRect(0, 0, 500, 500)

これは shape.graphics.command を使って一番最後の drawRect のみ操作出来ます。

しかし、コマンドを変数化する事で途中のコマンドも使用可能になります。

const cmd = shape.graphics.beginFill('#000').command
shape.graphics.drawRect(0, 0, 500, 500)

この場合は beginFill が操作可能になります。

cmd.style = newColor という感じにプロパティを更新するだけでとキャンバスのオブジェクトに反映されるので最初から作り直さなくてもよくなります。

フォームバインディング

CreateJS のオブジェクトはそのままデータバインドする事が出来ます。

const stage = new createjs.Stage()
new Vue({
  el: '#app',
  data: {
    label: {}
  },
  watch: {
    'label.text': function() {
      stage.update()
    }
  },
  mounted: function() {
    stage.canvas = this.$refs.canvas
    // テキストインスタンスをバインドする
    this.label = new createjs.Text('冷盛!', '30px Ubuntu Mono, monospace', '#000')
    stage.addChild(this.label)
  }
})
<canvas ref="canvas"></canvas>
<input v-model="label.text">

これはキャンバス内のテキストをフォームバインディング出来るようにしていて、label のプロパティは双方向バインディングになっています。

this.label.text = 'Hello'

プログラムによる更新はフォームの値と Canvas 両方に反映されます。

コマンドの共有とバインド

コマンドはインスタンス化することで複数のオブジェクトで共有する事ができます。複数のグラフィックで状態がリンクするような場合は切り離したものをバインドさせるといいです。

const stage = new createjs.Stage()

// Circle のインスタンスを作る
const circleCmd = new createjs.Graphics.Circle(0, 50, 30)

const circle1 = new createjs.Shape()
circle1.graphics.beginFill('#42c9e4')
circle1.graphics.append(circleCmd) // 同じものを使う
circle1.x = 100
stage.addChild(circle1)

const circle2 = new createjs.Shape()
circle2.graphics.beginFill('#ffa7a7')
circle2.graphics.append(circleCmd) // 同じものを使う
circle2.x = 200
stage.addChild(circle2)

new Vue({
  el: '#app2',
  data: {
    cmd: circleCmd
  },
  watch: {
    'cmd.radius': function () {
      stage.update()
    }
  },
  mounted: function () {
    stage.canvas = this.$refs.canvas
    stage.update()
  }
})

<input type="range" min="0" max="100" v-model.number="cmd.radius">

これは2つのグラフィックで半径の数値を共有します。cache/updateCache を使うとタイミングをずらしたり出来る。

中間処理が無い場合はこれは楽ちんですね!

が、シェイプ類は結構ネストが深いので全部 Vue によって監視オブジェクトに変換されます。 元々セッターゲッターの付いたオブジェクトを登録してもそれが消されたりすることはないのだけど、このやり方は簡単なものだけでしょうか。

例えば CreateJS の仕様が変わったり CreateJS をやめて別の新しいライブラリを使用したいと思った時ハードコーディングしてあると修正が大変なので部分的に抽象化しておく方が楽になると思います。

クリックとかのイベント

Vue.js の this を使いたい場合は on を使うとスコープの指定ができます。

// on を使うとすっきり。
shape.on('mousedown', this.handleDown, this)

後乗せした時のイベントの有効化

stage.canvas = this.$refs.canvas

この例のように先に空の Stage インスタンスを作成して後からキャンバスを設定する場合イベントが発火しなくなります。

mounted で Stage インスタンスを作成するか、キャンバスを指定した後に enableDOMEvents メソッドを使う必要があります。

stage.canvas = this.$refs.canvas
stage.enableDOMEvents(true)

カレンダージェネレーターを作った

デモ用に Vue.js を使ってスマホ壁紙用のオリジナルのカレンダーを作るジェネレーターを作ってみました。 type="color" など IE 未実装の機能を使っています。あと iPhone では動作確認していませんスミマセン…。 キモになりそうな箇所をゆるふわに解説していきます。

デモを別ウィンドウで表示 ソースコード(GitHub)

本日の強調が欲しいのでウィジェットアプリ向けだと思いますが、アプリは作ったことなくてサンプルとしてそこから用意するのも大変だったので手軽なものにしました。ベース画像にイラストやローカルからのアップロードファイルを使ったり、作った画像をサーバーへアップロード出来たりするともっと面白そうです。

テンプレート

テンプレートは Vue-cli の webpack-simple を使いました。1画面のシンプルなものなので、Vue.js 本体と CreateJS のみです。 普段あまりクラスを使っていないので、勉強のために少し使いながら書いてみました。設計難しいけど面白いのでどんどん使って勉強しようと思います。

設定ファイルとか

使用できるフォントや画像のリストは使えるものが固定された単純なオブジェクトの定義ファイルになっており、このオブジェクトもテンプレートで使用したいと思います。これは変化のないデータなので算出プロパティを使うことにします。

import { defineFonts, defineImages } from '../config.js'
// 省略
computed: {
  fontOptions: () => defineFonts,
  imageOptions: () => defineImages
}

UI コンポーネント

普段は Element UI など定番の UI キットを使っていますが、簡単なドロップダウンメニューを自作しました。

スコープ付きスロットを使うと、例えば v-for の個々の値をスロット側に渡して、より細かいカスタマイズをする事が出来ます。

UI コンポーネント側 my-select.vue

<li v-for="item in computedOptions">
  <slot name="option" :val="item.value">{{ item.label }}</slot>
</li>

親コンポーネント側 canvas-control.vue

フォント用のメニューはオプション部分のフォントスタイルを変えるようにします。

<my-select :options="fontOptions" v-model="font">
  <span slot="option" slot-scope="{val}" :style="{'font-family':fontLabel(val)}">
    {{ fontLabel(val) }}
  </span>
</my-select>

V2.5 で scopeslot-scope に変わって、普通の要素が使えるようになったみたいです。

UI と CreateJS の連携

最初のようにオブジェクトは登録しないでカレンダーシェイプ用の API を作って操作するようにしましたが、見るからにゴチャゴチャしてるのでもう少し改善させたい。

ウォッチャの監視が「year または month が更新された時…」というパターンは、インスタンスメソッドを使ってウォッチャを登録するなら対象を関数にすれば良いのだけど、プロパティに登録する場合は関数が使えないので代わりに算出プロパティを使うと同じように監視出来ます。

computed: {
  // この値をウォッチする
  fullMonth() {
    return [this.year, this.month]
  }
},
watch: {
  fullMonth([year, month]) {
    calendar.setDate(year, month)
  },
  font(value) {
    calendar.setFont(value)
  },
  // 大体同じなので省略
}

キャンバスの更新

ticker を使わない場合でも1個ウォッチャを用意するだけで更新できます。(オブジェクトをバインドしてる時はループしてしまうので状態階層をウォッチする)

watch: {
  $data:{
    handler() {
      stage.update()
    }, deep: true
  }
}

アニメーション

背景色とか単純に変えるだけだといまいち面白みが無かったので、変更したときにアニメーションで切り替えるようにしました。更新はウォッチャは使わずに ticker で常時されています。

createjs.Tween.get(item.fillCmd).wait(item.row % 7 * 50)
  .to({ style: createjs.Graphics.getRGB(220, 220, 220, 0) }, 600, createjs.Ease.cubicOut)
  .to({ style: value }, 600, createjs.Ease.cubicOut)

createjs.Tween.get(item.shape).wait(item.row % 7 * 50)
  .to({ scale: 1.6 }, 600, createjs.Ease.cubicOut)
  .to({ scale: 1 }, 600, createjs.Ease.cubicOut)

最近は三角関数も勉強していて楽しくなって来た所ですが、一時的なアニメーションだと Tween ライブラリを使た方がお手軽ですね。

おわりに

ファイルダウンロード周りは実装がまちまち過ぎてだるいです。Edge になっても変わらないなあと思いました。

CreateJS は cache 周りは自分で操作しないといけないのだけど、仮想 DOM の概念って DOM 以外にも応用出来るみたいなので Canvas の描画でもそういったライブラリが登場するのかも?むしろ既にあるのかもしれないです。(オススメのライブラリあったら教えてください)

Vue.js のカレンダーなのに CareateJS 関係の事が多くなってしまってスミマセン! SVG と比べるとだいぶ限定的な使い方になりますが、かつて FLASHer だったので動くもの作るの楽しい。みなさんも面白いジェネレーター作ってみて下さいね!

明日のカレンダーは、はりぼてさんの多分 SVG のお話です(っ˙︶˙c)