こんにちは。エンジニアの辻です。
前回はSpineアニメーションをWebページに表示してみました。
さて、今回は表示だけではなく、JavaScriptを使ってSpineアニメーションを制御してみたいと思います。
成果物の東北ずん子ちゃんの旗揚げアニメーション
さっそくですが、今回の成果物についてです。
本ページ右下に、東北ずん子ちゃんの旗揚げアニメーションが表示されていると思います。
(イラストは東北ずん子ちゃん 公式HPを参考にしつつ、描いてみました。)
今回のずん子ちゃんのアニメーションですが、ブラウザ上のマウスの位置と連動します。
ブラウザの左半分にマウスがあると、赤旗を持った右手が反応します。
反対に、ブラウザの右半分にマウスがあると、白旗を持った左手が反応します。
また、どちらの手もマウスの位置の高さに連動しています。
マウスがブラウザ上端に近ければ手が上がり、下端に近ければ手が下がります。
試しに、ブラウザ上でマウスをグリングリンと動かしてみてください。
マウスの位置に連動して、ずん子ちゃんが赤旗をあげたり、白旗をあげたりしてくれます。
(ブラウザをリサイズしたときの処理は実装していないので、一度リサイズするとマウスとの連動がうまくいかないかもしれません。その時はリロードしてください。
また、マウスの動きに応じてアニメーションさせていますので、スマホでは待機モーションのずん子ちゃんが表示されるだけだと思います。スマホでご覧の方は、PCでご確認ください。)
Spineのトラックとアルファ
ずん子ちゃんの旗揚げアニメーションですが、Spineのトラックとアルファという機能を使って実現しています。
トラックとは
Spineの「トラック」とは、アニメーションをセットする枠のようなものです。
トラックは最大で15コまであり、1つのトラックに対して、1つのアニメーションをセットできます。
アニメーションがセットされたトラックのステータスを変更することで、複数のアニメーションやポーズを同時に適用できます。
アルファとは
Spineの「アルファ」とは、トラックにセットされたアニメーションのポーズを合成する機能です。
アルファは数値で設定します。最小値が0。最大値が100です。(※Spineランタイムでは最小値0。最大値1です。)
上位トラックに設定されたアニメーションポーズのアルファ値が大きいほど、下位トラックのアニメーションにポーズが合成されます。
Spineのプレビュービューでトラックとアルファを使ってみる
文字だけの解説ですと、なんとも分かりづらいので、動画を用意しました。
以下は、Spineのプレビュービューの動画キャプチャです。
プレビュービューは、Spineで製作したアニメーションが実際にどう動くかを確認するためのものです。
今回のずん子ちゃんには、以下の3つのアニメーションを用意しています。
- idle … 待機中アニメーション。ループします。
- red_flag_hand … 赤旗あげアニメーション
- white_flag_hand … 白旗あげアニメーション
動画内では、まず「idle」アニメーションをトラック0に設定していますね。
その後に、「red_flag_hand」と「white_flag_hand」アニメーションを、トラック1へ順に適用しています。
そして、トラック1にセットした後でアルファ値を変更しています。
動画を見ていただくと分かると思いますが、アルファ値を変更した時に、アニメーションも連動して変化していますね。
この時、ずん子ちゃんはトラック0の「idle」アニメーションをベースとして、「red_flag_hand」・「white_flag_hand」アニメーションが合成されたポーズをとっています。
このように、Spineにおけるアニメーション製作では、複雑なアニメーションを作らずとも、シンプルなアニメーションとポーズを組み合わせる事で、様々な表現が可能です。
トラックとアルファを使った柔軟なアニメーション合成が、Spineの魅力の1つです。
ちなみにですが…、実は、red_flag_handとwhite_flag_handの2つのアニメーションは、キーフレームが1つだけのアニメーションです。
アニメーションではありますが、キーフレームが1つだけですので動きません。実質はただの静止画です。
つまり、先程の動画では、ループアニメーションのidleに対して、静止したポーズのred_flag_hand・white_flag_handを合成しています。
Spineのすごいところは、ループするアニメーションに対して、静止したアニメーション(ポーズ)を合成しても破綻せずによしなにしてくれる点です。
合成するアニメーション同士でキーフレームを対応させたり…などの下準備は必要ありません。
どれくらい合成するかは、アルファ値の調整1つで事足ります。
さて、ここまで読み進めていただいた方は、既にお察しかと思いますが…、このトラックとアルファはSpineランタイムから制御できます。
(実は前回も、しれっとトラックの設定をやっていました。)
本ページ右下のずん子ちゃんには、「マウスの位置が更新される度に、アルファ値を更新する」処理をJavaScript(TypeScript)で実装しています。
…なので、マウス位置と連動して、うまいことアニメーションするわけです。
では、そのソースコードを見ていきましょう。
ソースコードについて
サンプルプロジェクトとmain.ts
今回の旗揚げアニメーションのサンプルプロジェクトはコチラです。
ディレクトリ構造やファイルの内容は、基本的に前回のサンプルプロジェクトと同様です。
更新したのは、out/ディレクトリ配下のspineデータ3点とsrc/main.tsになります。
spineデータ3点は単純に差し替えただけですので、前回から大きく変更したのはsrc/main.tsの1点だけですね。
…ですので、src/main.tsだけ見ていけばOKです。
▼src/main.ts 全文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
import * as PIXI from 'pixi.js' import { Spine } from 'pixi-spine' const app = new PIXI.Application({ backgroundAlpha: 0.5, backgroundColor: 0x1099bb, height: window.innerHeight, width: window.innerWidth }) document.body.appendChild(app.view) /** * onAssetsLoaded * 東北ずん子のspineデータ(zunko)を読み込んだ後に発火させる関数 * @param {object} loader * @param {object} res */ const onAssetsLoaded = (loader: any, res: any) => { // Spine インスタンスを生成 const zunko = new Spine(res.zunko.spineData) // zunkoの位置をセット zunko.x = app.screen.width / 2 zunko.y = app.screen.height // zunkoの大きさをセット zunko.scale.set(0.75) // stageにzunkoを追加 app.stage.addChild(zunko) // 初期アニメーションとして、idleをセット zunko.state.setAnimation(0, 'idle', true) // 他アニメーションをセット zunko.state.addAnimation(1, 'white_flag_hand', false, 0) zunko.state.addAnimation(2, 'red_flag_hand', false, 0) // 他アニメーションのアルファ値を0にする zunko.state.tracks[1].alpha = 0 zunko.state.tracks[2].alpha = 0 // 画面上のマウスの位置を検知する処理 window.addEventListener('mousemove', (event) => { // マウスが画面の右側か左側かを取得 const mousePositon = getMousePosition(window.innerWidth, event.clientX) // マウスの位置の高さ(比率)を取得 const heightRatio = getRatio(window.innerHeight, event.clientY) // 各アニメーションのアルファ値を更新 switch (mousePositon) { // 画面右側 case 'right': zunko.state.tracks[1].alpha = 1 - heightRatio zunko.state.tracks[2].alpha = 0 break // 画面左側 case 'left': zunko.state.tracks[2].alpha = 1 - heightRatio zunko.state.tracks[1].alpha = 0 break default: zunko.state.tracks[1].alpha = 0 zunko.state.tracks[2].alpha = 0 break } }) } // zunkoのspineデータを読み込む app.loader.add('zunko', '/illust_zunko_flag.json').load(onAssetsLoaded) /** * getMousePosition * マウスの位置が画面の右側か左側かを取得する * @param {number} allWidth 全体サイズ * @param {number} currentPositionX マウスの現在位置のXの値 * @return {"right" | "left"} */ const getMousePosition = (allWidth: number, currentPositionX: number) => { return currentPositionX >= Math.round(allWidth / 2) ? 'right' : 'left' } /** * getRatio * 全体(all)を元にして、部分(part)の比率を取得する * @param {number} all 全体 * @param {number} part 部分 * @return {number} 0〜1。小数点第二位まで含む。 */ const getRatio = (all: number, part: number) => { return Math.round((part / all) * 100) / 100 } |
pixi.jsのセットアップや、Spineデータの読み込みは前回と同様ですので、解説は省略しますね。
大きく変わった点だけ解説していきます。
white_flag_hand・red_flag_handをトラックに追加する
前回は初期アニメーションとしてidleをセットして、poseと切り替えてました。
この時は、トラック番号を0でセットしていましたね。
1 2 3 4 5 6 7 8 |
// 初期アニメーションとして、idleをセット zunko.state.setAnimation(0, 'idle', true) // 他アニメーションをセット zunko.state.addAnimation(1, 'white_flag_hand', false, 0) // ① zunko.state.addAnimation(2, 'red_flag_hand', false, 0) // ① // 他アニメーションのアルファ値を0にする zunko.state.tracks[1].alpha = 0 // ② zunko.state.tracks[2].alpha = 0 // ② |
今回は0以外の他のトラック番号も使用します。
addAnimation関数で、トラック1にはwhite_flag_handアニメーションを、トラック2にはred_flag_handアニメーションをセットします。(①)
これで先程の動画でやっていたトラックへのアニメーションのセットができました。
次に、トラック1のwhite_flag_handと、トラック2のred_flag_handのアルファ値を0にしておきます。(②)
トラックの状態を変更したい場合は、XXXXX.state.tracks[x]からアクセスして、各プロパティを変更します。
また、setAnimation関数・addAnimation関数の第一引数に入れた数値が、そのままトラック(tracks)のindex番号になります。
これで、トラックには以下のようにアニメーションがセットされました。
- トラック0: idle
- トラック1: white_flag_hand (アルファ値: 0)
- トラック2: red_flag_hand (アルファ値: 0)
マウスの位置をアルファ値に反映する
マウスの位置をアルファ値に反映する処理を見ていきます。今回の肝です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 画面上のマウスの位置を検知する処理 window.addEventListener('mousemove', (event) => { // ① // マウスが画面の右側か左側かを取得 const mousePositon = getMousePosition(window.innerWidth, event.clientX) // ② // マウスの位置の高さ(比率)を取得 const heightRatio = getRatio(window.innerHeight, event.clientY) // ③ // 各アニメーションのアルファ値を更新 switch (mousePositon) { // ④ // 画面右側 case 'right': zunko.state.tracks[1].alpha = 1 - heightRatio zunko.state.tracks[2].alpha = 0 break // 画面左側 case 'left': zunko.state.tracks[2].alpha = 1 - heightRatio zunko.state.tracks[1].alpha = 0 break default: zunko.state.tracks[1].alpha = 0 zunko.state.tracks[2].alpha = 0 break } }) |
最初は、window.addEventListener(‘mousemove’,…)で、マウスが動いた時に関数が発火するようにします。(①)
では、その関数の中身を見ていきましょう。
はじめに、マウスが画面上の右側にいるのか、左側にいるのかを取得しています。(②)
マウスが画面右側にいれば、mousePositonには、’right’が入ります。
反対に左側にいれば、mousePositonには、’left’が入ります。
その後に、マウスの位置の高さを取得します。(③)
この時に取得する高さですが、そのままの値(event.clientY)ではなく、画面全体の高さを1とした時の比率を取得しています。
なぜかと言うと、Spineランタイムのアルファ値には、0〜1の範囲の数値を入れる必要があるためです。
アルファ値 = 0の場合は、アニメーションの合成が全くされていない状態。
アルファ値 = 1の場合は、100%アニメーションが合成された状態、となります。
(厳密には1より大きい値も入れられるのですが…、アニメーションが破綻してしまいます。。)
最後のswitch文にて、②、③の値を使ってアルファ値を更新しています。(④)
マウスが画面右側にいる時(mousePositon === ‘right’)の時、
トラック1(white_flag_handアニメーション)のアルファ値を 1 – heightRatio で更新し、トラック2(red_flag_handアニメーション)のアルファ値を0にしています。
これで白旗を持った手がマウスの位置に連動し、反対に赤旗を持った手は全く動かなくなる…というわけです。
(ちなみに、なぜ (1 – heightRatio) としているかと言うと、event.clientYはブラウザ上端が0であり、下端へ行けば行くほど正に増大するためです。
今回は、マウスがブラウザ上端に行けば手が挙がる。下端に行けば手が下がるアニメーションを作りたかったため、1からheightRatioを引いています。)
マウスが画面左側にいる時(mousePositon === ‘left’)は、上記の逆ですね。
トラック1(white_flag_handアニメーション)のアルファ値を0に。
トラック2(red_flag_handアニメーション)のアルファ値を 1 – heightRatio で更新すれば、赤旗だけをあげるアニメーションになります。
こうしてマウスの位置と、アニメーションが連動するわけです。
まとめ
ここまでお付き合いいただき、ありがとうございました!
今回紹介したトラックとアルファ以外にも、メッシュ変形とパス・コンストレイント組み合わせてのアニメーションや、境界ボックスアタッチメントを使って当たり判定を自作するなど…、Spineには様々な機能が用意されています。
私もまだまだSpineを使いこなせずでして…、もっと練習してリッチな表現ができるようになりたいものです。
また機会があれば、Spine x JavaScriptの記事を書いてみます。
それでは、また。