PixiJSはインタラクティブなアプリケーションに利用できる2Dのレンダリングライブラリで、アニメショーンを取り入れたWEBサイトやゲームにも利用されています。オンラインコラボレーションホワイトボードツールで有名なMiroでもPixi.jsとReactを利用してアプリケーションが構築されています。

Reactを使ってPixi.jsを操作したい場合にreact-pixi-libary, react-pixiといったライブラリがありますが、本文書ではreact-pixiを利用して動作確認を行っています。react-pixiライブラリを利用することでReact上で簡単にPixi.jsを使ってアニメーション機能を実装することができます。

環境の構築

動作確認の環境を構築するためにReactのプロジェクトを作成します。Reactのバージョンは17を利用しています。


% npx create-react-app reactpixi
% cd reactpixi

Reactプロジェクト作成後にreact-pixiのインストールを行います。


% npm install pixi.js @inlet/react-pixi --save

pixi.jsではcanvas要素を上に文字や画像やグラフィックを描写していきます。Stageコンポーネントを利用してブラウザ上にcanvas要素を作成します。


import { Stage } from '@inlet/react-pixi'
function App() {
  return (
    <Stage>
    </Stage>
  );
}

export default App;

ブラウザで確認するとブラウザの画面にcanvas要素が広がり、デフォルトの設定では背景色は黒になります。

canvas要素の表示
canvas要素の表示

ブラウザのデベロッパーツールで確認するとwidthが1600, heightが1200のcanvas要素がブラウザ上に広がっていることが確認できます。


<canvas style="touch-action: none; cursor: inherit;" width="1600" height="1200"></canvas<

文字列を表示

ReactとPixi.jsを利用してcanvas上にテキスト文字の表示方法を確認します。文字を表示する場合はTextコンポーネントを利用することができます。


import { Stage, Text } from '@inlet/react-pixi'
function App() {
  return (
    <Stage>
      <Text text="Hello World" x={100} y={100} />
    </Stage>
  );
}

export default App;

文字の色と背景色が同じであるためにブラウザで確認しても何も表示されません。TextStyleをimportして文字に色をつけるとブラウザ上に文字が表示されます。x, yを使ってcanvas上での文字列を表示する場所を指定しています。x,yの値を変更すると文字列の表示場所を変更できることが確認できます。


import { TextStyle } from 'pixi.js';
//略
<Text text="Hello World" x={100} y={100} style={new TextStyle({fill:'white'})} />
Textコンポーネントで文字列を表示
Textコンポーネントで文字列を表示

文字列の回転

Pixi.jsを利用するとアニメーションのような動きを短いコードで実装することができます。文字列の表示方法を確認したので次は文字列の回転を行ってみましょう。

srcフォルダにComponentsフォルダを作成して、RotatingText.jsファイルを作成します。

React PIXIにはuseTickフックが準備されているのでuseTickとuseStateを利用して文字列の回転を行います。useTickを利用することでループ処理が行われるので少しずつ文字列が回転します。Textコンポーネントのanchorでは回転軸の設定を行っています。ancherを0.5に設定すると文字列の中心が回転軸となります。


import { useState } from 'react'
import { Text, useTick } from '@inlet/react-pixi'
import { TextStyle } from 'pixi.js';
function RotatingText() {
    const [rotation, setRotation] = useState(0)
    useTick(delta => {
        setRotation(rotation + 0.01 * delta)
    })
    return (
       <Text text="Hello World"
            x={100} 
            y={100}
            rotation={rotation}
            anchor={0.5}
            style={new TextStyle({fill:'white'})} 
      />           
    )
}

export default RotatingText

作成したRotatingTextコンポーネントをApp.jsでimportします。


import { Stage } from '@inlet/react-pixi'
import RotatingText from './Components/RotatingText'

function App() {
  return (
    <Stage>
      <RotatingText />
    </Stage>
  );
}

export default App;

ブラウザ上ではゆっくりと回転する文字列を確認することができます。

文字列の回転
文字列の回転

文字列の移動

文字列の回転ができたので次はuseTickを利用して文字列の移動方法の確認を行います。文字列の移動の処理は回転の処理とほとんど変わりません。違いは回転ではrotationをuseTickで更新していたのが文字列の座標のxに変わっただけです。

新たにComponentsフォルダにMovingText.jsファイルを作成して下記を記述します。


import { useState } from 'react'
import { Text, useTick} from '@inlet/react-pixi'
import { TextStyle } from 'pixi.js';
function MovingText() {
    const [x, setX] = useState(0)
    useTick(delta => {
        setX(x + 1 * delta)
    })
    return (
       <Text text="Hello World"
            x={x} 
            y={100}
            anchor={0.5}
            style={new TextStyle({fill:'white'})} 
      />           
    )
}

export default MovingText

作成時はApp.jsでMovingText.jsファイルをimportします。


import {Stage} from '@inlet/react-pixi'
import MovingText from './Components/MovingText'

function App() {
  return (
    <Stage>
      <MovingText />
    </Stage>
  );
}

export default App;

ブラウザをリロードすると左から右にゆっくりと移動する文字列が確認できます。一度移動が完了すると再リロードするまで画面には何も表示されません。

文字列の移動
文字列の移動

文字列の回転、文字列の移動を同時に行いたい場合はこれまで確認した2つの変数をx, rotationをTextコンポーネントに設定するだけなので簡単です。


import { useState } from 'react'
import { Text, useTick} from '@inlet/react-pixi'
import { TextStyle } from 'pixi.js';
function MovingText() {
    const [x, setX] = useState(0)
    const [rotation, setRotation] = useState(0)
    useTick(delta => {
        setX(x + 1 * delta)
        setRotation(rotation + 0.01 * delta)   
    })
    return (
       <Text text="Hello World"
            x={x} 
            y={100}
            anchor={0.5}
            rotation={rotation}
            style={new TextStyle({fill:'white'})} 
      />           
    )
}

export default MovingText

ここまでの動作確認だけでも動きのあるものではPixi.jsを利用すればあアニメーションの動きを簡単に実装することができることがわかるかと思います。

ここまではuseTickを利用して文字列に動きをつけていましたが、イベント機能を利用してインタラクティブに変化できるような処理を確認していきます。

イベントの設定

イベントを設定することでユーザが行った処理に反応してなにか決められた別の処理を行うことができます。ここからは変数の値を保持するためにReactのHookであるuseStateも利用します。

クリックイベント

文字列にクリックイベントを設定して文字列のカラーを変更してみましょう。イベントを動作させるためにはコンポーネントのinteractiveをtrueに設定しておく必要があります。


<Text text="Hello World"
  x={x} 
  y={100}
  interactive={true}
  style={new TextStyle({fill:color})}
/>  

カラーを設定するために変数colorを追加します。初期値にはwhiteを設定しています。


const [color, setColor] = useState('white')

クリックイベントはclickを利用します。文字列をクリックするとclickイベントによりsetColorが実行され文字列が赤に変わります。

イベントにはclickの他にも後ほど説明するmouseup, mouvemove, mousedown, pointerup, rightclickなどさまざまなものがあります。どのようなイベントがあるか知りたい場合はpixi.jsのマニュアルを確認してください。http://pixijs.download/release/docs/index.html

import { useState } from 'react'
import { Text } from '@inlet/react-pixi'
import { TextStyle } from 'pixi.js';
function MovingText() {
  const [color, setColor] = useState('white')
  return (
    <Text text="Hello World"
      x={100} 
      y={100}
      interactive={true}
      style={new TextStyle({fill:color})}
      click={() => {
          setColor('red')
      }}
  />           
  )
}

export default MovingText

clickイベントではなくmousedownイベントを利用しても同様の処理を行うことができます。


<Text text="Hello World"
  x={100} 
  y={100}
  interactive={true}
  style={new TextStyle({fill:color})}
  mousedown={() => {
      setColor('red')
  }}
/>

ドラッグ&ドロップの動作確認

clikc, mousedownによるイベントの設定方法が理解できたのでmousedownとmousemove, mouseupイベントを利用してドラッグ&ドロップを行ってみましょう。

mousedownイベント

ドラッグ&ドロップを行うためには、ドラッグ&ドロップしたい要素の位置を知る必要があります。mousedownイベントとeventを利用することでクリックした場所の位置を取得することができます。event.data.global.xでx座標が取得できます。


import { Text} from '@inlet/react-pixi'
import { TextStyle } from 'pixi.js';
function MovingText() {
  const mouseDown = (e) => {
    console.log(e.data.global.x)
    console.log(e.data.global.y)
  }
  
  return (
    <Text text="Hello World"
      x={100} 
      y={100}
      interactive={true}
      style={new TextStyle({fill:'white'})}
      mousedown={mouseDown}
  />           
  )
}

export default MovingText

mousedownイベントにmouseDownメソッドを設定し文字列をクリックするとブラウザのデベロッパーツールのコンソールにx, yの座標が表示されます。文字列の左上がx, yで指定した100,100の位置です。右にいくほどxの値は大きくなり、下にいくほどyの値は大きくなります。左上が文字列の位置の基準点になるので文字列のどこをクリックしても100より大きくなります。

クリックした場所のx, y座標
クリックした場所のx, y座標

イベントを利用することでx,y座標が取得できることがわかりました。

mousemoveイベント

mousemoveイベントを利用してマウスの移動に合わせて文字列を移動させる方法を確認します。マウスの動きに合わせて文字列の場所x,yの値が更新したいので変数x, yを定義しています。


import { useState } from 'react'
import { Text} from '@inlet/react-pixi'
import { TextStyle } from 'pixi.js';
function MovingText() {
  
  const [x, setX] = useState(100)
  const [y, setY] = useState(100)

  const mouseDown = (e) => {
    setX(e.data.global.x)
    setY(e.data.global.y)
  }

  const mouseMove = (e) => {
    setX(e.data.global.x)
    setY(e.data.global.y)
  }
  
  return (
    <Text text="Hello World"
      x={x} 
      y={y}
      interactive={true}
      style={new TextStyle({fill:'white'})}
      mousedown={mouseDown}
      mousemove={mouseMove}
  />           
  )
}

export default MovingText

文字列をクリックするとクリックした場所に文字列の基準である左上が移動し、マウスの移動と一緒に文字列が一緒に移動します。一度クリックすると文字列がマウスの移動から離れることはありせん。mouseupイベントも加えて、マウスの移動との同期を解除する仕組みを作ります。

mouseupイベント

mouseupイベントを設定する前に新たに変数draggingを追加します。draggingがtrueの場合はマウスに合わせて文字列が移動し、draggingがfalseの場合はマウスに合わせて移動は行わず文字列は停止します。

mousedownを行った時にdraggingをtrueにしマウスが離れた時(mouseup)にdraggingをfalseにします。


import { useState } from 'react'
import { Text} from '@inlet/react-pixi'
import { TextStyle } from 'pixi.js';
function MovingText() {
  
  const [dragging, setDragging] = useState(false)
  const [x, setX] = useState(100)
  const [y, setY] = useState(100)

  const mouseDown = (e) => {
    setDragging(true)
    setX(e.data.global.x)
    setY(e.data.global.y)
  }

  const mouseMove = (e) => {
    if(dragging){
      setX(e.data.global.x)
      setY(e.data.global.y)
    }
  }

  const mouseUp = () => {
    setDragging(false)
  }
  
  return (
    <Text text="Hello World"
      x={x} 
      y={y}
      interactive={true}
      style={new TextStyle({fill:'white'})}
      mousedown={mouseDown}
      mousemove={mouseMove}
      mouseup={mouseUp}
  />           
  )
}

export default MovingText

動作確認を行うとマウスをクリックしている間はマウスと一緒に文字列が移動します。マウスを離すと離した位置に文字列が停止します。これでドラッグ&ドロップができることが確認できます。

マウスに合わせて移動することができるようになりましたが、クリックした瞬間にクリック位置に文字列の左上の基準点の位置が移動してくるので不自然な動きになります。

クリックした際にクリックした場所と文字列の基準点のずれを保存します。不自然な動きを解消します。


import { useState } from 'react'
import { Text} from '@inlet/react-pixi'
import { TextStyle } from 'pixi.js';
function MovingText() {
  
  const [dragging, setDragging] = useState(false)
  const [x, setX] = useState(100)
  const [y, setY] = useState(100)
  const [diffX, setDiffX] = useState(0)
  const [diffY, setDiffY] = useState(0)  

  const mouseDown = (e) => {
    let distanceX = e.data.global.x - x;
    let distanceY = e.data.global.y - y;
    setDiffX(distanceX)
    setDiffY(distanceY)
    setDragging(true)
    setX(e.data.global.x - distanceX)
    setY(e.data.global.y - distanceY)
  }

  const mouseMove = (e) => {
    if(dragging){
      setX(e.data.global.x - diffX)
      setY(e.data.global.y - diffY)
    }
  }

  const mouseUp = () => {
    setDragging(false)
  }
  
  return (
    <Text text="Hello World"
      x={x} 
      y={y}
      interactive={true}
      style={new TextStyle({fill:'white'})}
      mousedown={mouseDown}
      mousemove={mouseMove}
      mouseup={mouseUp}
  />           
  )
}

export default MovingText

文字列の移動
文字列の移動

複数の文字列を表示

ここまでは一つの文字列のみ表示させていましたが、複数の文字列を同時に表示させることができます。またコンポーネント化されているのでそれぞれのコンポーネントを個別に移動させることも可能です。

最初の位置とX, Yと表示されるテキストの内容を持つオブジェクトを3つ用意します。オブジェクトはmap関数で展開し、各値はpropsとしてMovingTextコンポーネントに渡します。


import {Stage} from '@inlet/react-pixi'
import MovingText from './Components/MovingText'

function App() {

  const objects =[
    {
      text:'React',
      x:100,
      y:100,
    },
    {
      text:'Vue.js',
      x:200,
      y:50,
    },
    {
      text:'JavaScript',
      x:240,
      y:180,
    }
  ]

  return (
    <Stage>
      {
        objects.map((object,index) => {
          return (
            <MovingText 
              text={object.text} 
              posX={object.x} 
              posY={object.y} 
              key={index} 
            />
          )
        })
      }
    </Stage>
  );
}

export default App;

MovingTextコンポーネントもpropsで受け取った値を設定できるように変更を行います。


import { useState } from 'react'
import { Text} from '@inlet/react-pixi'
import { TextStyle } from 'pixi.js';
function MovingText({text, posX, posY}) {
  
  const [dragging, setDragging] = useState(false)
  const [x, setX] = useState(posX)
  const [y, setY] = useState(posY)
  const [diffX, setDiffX] = useState(0)
  const [diffY, setDiffY] = useState(0)  

  const mouseDown = (e) => {
    let distanceX = e.data.global.x - x;
    let distanceY = e.data.global.y - y;
    setDiffX(distanceX)
    setDiffY(distanceY)
    setDragging(true)
    setX(e.data.global.x - distanceX)
    setY(e.data.global.y - distanceY)
  }

  const mouseMove = (e) => {
    if(dragging){
      setX(e.data.global.x - diffX)
      setY(e.data.global.y - diffY)
    }
  }

  const mouseUp = () => {
    setDragging(false)
  }
  
  return (
    <Text text={text}
      x={x} 
      y={y}
      interactive={true}
      style={new TextStyle({fill:'white'})}
      mousedown={mouseDown}
      mousemove={mouseMove}
      mouseup={mouseUp}
  />           
  )
}

export default MovingText
個別の文字の移動
個別の文字の移動