PixiJS + React で実装する時のメモ
指針
PixiJSをラップして命令的に記述できるライブラリを使う
ライブラリ選定
@inlet/react-pixi を使う
メインのコミッターが非常に活発に動いており、ドキュメントやissueでの問題解決が期待できる
比較対象としてよく上がるのがreact-pixi-fiber
プロジェクト自体は動いてるっぽいがnpmには古いバージョンしか上がってない
<Stage />
@inlet/react-pixi の基本的な使い方をしては、
Stage を配置し、その配下に描画したいオブジェクトなどを追加していく
import { Stage } from '@inlet/react-pixi'
const App = () => {
return (
<Stage>
</Stage>
);
}
export default App;
height
,width
,options
などの設定をStage に継承して初期設定を追加する事ができる
レスポンシブな画面表示
固定値なら関係ないが、フル画面やウインドウサイズによってStageサイズを変更したい思いがある時、
そのままwidth
にwindow.innerWidth
などを渡してもStage のサイズを更新してくれないので(当たり前)、resizeイベントをListenするなどしてheight
,width
を取得・更新する必要がある。
カスタムフックを作って上げるのが楽
import { useLayoutEffect, useState } from "react";
export const useWindowSize = () => {
const [size, setSize] = useState([0, 0]);
useLayoutEffect(() => {
function updateSize() {
setSize([window.innerWidth, window.innerHeight]);
}
window.addEventListener("resize", updateSize);
updateSize();
return () => window.removeEventListener("resize", updateSize);
}, []);
return size;
};
解像度対応
const resolution = Math.min(window.devicePixelRatio, 2);
const stageProps = {
options: {
autoDensity: true,
resolution: resolution || 1,
antialias: resolution <= 1,
},
};
return <Stage {...stageProps}></Stage>
<Container />
幾つかのコンテンツをまとめる為のContainer
このコンポーネント自体にインタラクティブなイベントを仕込むことはできない
pixi-viewportの利用
Stage 全体を拡大・縮小、移動などをしたいと思う時があると思う(GoogleMapの様な)
基本的にStage は描画する境界線を定義する物なので、Stage に対して何か操作をするのは得策では無く、コンテンツをContainer などでラップして拡大・縮小、移動させるべき
ただ、拡大・縮小、移動の数値を保持したりと色々と手間などでpixi-viewport というライブラリを使う
Stage 配下にViewport コンポーネントを作っていく
ref を渡しておくと、トップ層からviewport API にアクセスできるので便利
import { Stage } from '@inlet/react-pixi'
import React, { useRef } from "react";
import { useWindowSize } from "./hooks/useWindowSize";
const App = () => {
const viewportRef = useRef<PixiViewport>(null);
const [width, height] = useWindowSize();
return (
<Stage>
<ViewPort ref={viewportRef} {...{width, height}} >
<...SomeContents />
</ViewPort>
</Stage>
);
}
export default App;
Viewport 本体をラップし、@inlet/react-pixi で提供されているuseApp() を使い、PIXI.Application
を継承してあげるコンポーネントを作成
@inlet/react-pixi で提供されているhooks
import { useApp } from '@inlet/react-pixi'
import { Viewport as PixiViewport } from "pixi-viewport";
interface ViewportProps {
width: number;
height: number;
children?: React.ReactNode;
}
const Viewport = React.forwardRef<PixiViewport, ViewportProps>((props, ref) => {
const app = useApp();
return <PixiComponentViewport ref={ref} app={app} {...props} />;
});
Viewport コンポーネントを作成
@inlet/react-pixi で提供されているPixiComponent
を使うことで、PixiJS内でのマウント時などの挙動を調整できる
詳しい説明
import { PixiComponent } from '@inlet/react-pixi'
import { Viewport as PixiViewport } from "pixi-viewport";
import * as PIXI from "pixi.js";
interface PixiComponentViewportProps extends ViewportProps {
app: PIXI.Application;
}
const PixiComponentViewport = PixiComponent("Viewport", {
create: (props: PixiComponentViewportProps) => {
const viewport = new PixiViewport({
screenWidth: props.width,
screenHeight: props.height,
worldWidth: props.width * 2,
worldHeight: props.height * 2,
ticker: props.app.ticker,
interaction: props.app.renderer.plugins.interaction
});
viewport
.drag()
.pinch()
.wheel()
.clampZoom({ minScale: 0.3, maxScale: 2 })
.setZoom(0.5)
.moveCenter(props.width, props.height);
return viewport;
},
applyProps: (viewport, _oldProps, newProps) => {
viewport.resize(newProps.width, newProps.height);
}
});
設定しているViewport の機能
default
- drag()
- Viewportをdrag操作で移動できるようにする
- pinch()
- Viewportをpinch操作で拡大・縮小できるようにする
- wheel()
- Viewportをwheel操作で拡大・縮小できるようにする
- clampZoom({minScale: 0.3, maxScale: 2})
- Viewportの拡大率に制限を設けれる
- setZoom(0.5)
- Viewportの初期倍率を指定している
- moveCenter(props.width, props.height)
- ViewportのscreenWidth,screenHeight に対して中心に移動する
applyProps(propsが変わった時に働く)
- resize(newProps.width, newProps.height)
- applyProps で受け取った新しいStage のサイズに合わせてViewport をリサイズ
pixi-cullの利用
pixi-viewport と同じ作者のculling ライブラリ
カリングとは?
画面外のオブジェクトの描画をスキップし、パフォーマンスを向上させえる
使い方
上述のViewport コンポーネント内でイベント設定してあげる
画面の更新が走る時にカリング処理をする
import { Simple } from "pixi-cull";
const cull = new Simple();
cull.addList(
(viewport.children as PIXI.Container[])
.map((layer) => {
return layer.children;
})
.flat()
);
cull.cull(viewport.getVisibleBounds());
viewport.on("moved", () => {
if (viewport.dirty) {
cull.cull(viewport.getVisibleBounds());
viewport.dirty = false;
}
});
全体
import { PixiComponent } from '@inlet/react-pixi'
import { Viewport as PixiViewport } from "pixi-viewport";
import * as PIXI from "pixi.js";
import { Simple } from "pixi-cull";
interface PixiComponentViewportProps extends ViewportProps {
app: PIXI.Application;
}
const PixiComponentViewport = PixiComponent("Viewport", {
create: (props: PixiComponentViewportProps) => {
const viewport = new PixiViewport({
screenWidth: props.width,
screenHeight: props.height,
worldWidth: props.width * 2,
worldHeight: props.height * 2,
ticker: props.app.ticker,
interaction: props.app.renderer.plugins.interaction
});
viewport
.drag()
.pinch()
.wheel()
.clampZoom({ minScale: 0.3, maxScale: 2 })
.setZoom(0.5)
.moveCenter(props.width, props.height);
const cull = new Simple();
cull.addList(
(viewport.children as PIXI.Container[])
.map((layer) => {
return layer.children;
})
.flat()
);
cull.cull(viewport.getVisibleBounds());
viewport.on("moved", () => {
if (viewport.dirty) {
cull.cull(viewport.getVisibleBounds());
viewport.dirty = false;
}
});
return viewport;
},
applyProps: (viewport, _oldProps, newProps) => {
viewport.resize(newProps.width, newProps.height);
}
});
問題
Viewport 配下のコンポーネントが静的なオブジェクトの場合は正常にカリングが働くが、アニメーションなどをしてるとカリングしてくれない
アニメーションをさせたい
PixiJS内でオブジェクトにアニメーションをさせる方法は主に2つある
- ticker を使って毎フレームの処理を書く
- window.requestAnimationFrame() の様に
- 外部のアニメーションライブラリを使う
- GSAP
- Spine
- react-spring
react-spring
@inlet/react-pixi の場合、ライブラリ側でreact-spring 向けのコンポーネントも配布されており、
導入の手間が少ない
import { Stage } from '@inlet/react-pixi'
を
import { Stage } from '@inlet/react-pixi/animated'
にするだけでコンポーネントをreact-springに対応させることが出来る
簡単な回転アニメーション
import { Container, Sprite } from '@inlet/react-pixi/animated'
import { useSpring } from "react-spring";
const Box = props => {
const [props, api] = useSpring(() => ({
from: { rotation: 0 }
}));
return (
<Container>
<Sprite
texture={Texture.WHITE}
tint={0xaddb67}
anchor={0.5}
interactive
pointerover={() => {
api.start({ rotation: 10 * Math.random() - 10 });
}}
pointerout={() => {
api.start({ rotation: 0 });
}}
{...props}
/>
</Container>
);
};
大量の画像を表示される時にチラつきを抑えるための小技
@inlet/react-pixi で画像を表示するには、Sprite コンポーネントのprops のimageにurlを渡して上げる必要がある
つまり、Sprite コンポーネントがマウントされるまでは画像を読み込まないので大量のSprite を表示する時は必ずチラつきが発生する(読み込みが終わった画像から表示される)
PixiJSには画像のキャッシュ機構が存在し、画像を読み込むとそこに保存されるので同じ画像のurlが渡された場合は即座に表示される
なので、Stage が表示される前にPixiJSに画像の配列を渡して事前ロードさせよう
同じurlをキャッシュしようとすると警告を吐かれるので、
毎回キャッシュをクリアしたり[...new Set()] で同じurlを弾く前処理をする
import React, { useEffect, useRef, useState } from "react";
import * as PIXI from "pixi.js";
const App: React.VFC<{images: string[]}> = ({images}) => {
const [render, setRender] = useState<boolean>(false);
const loader = useRef<PIXI.Loader>(new PIXI.Loader());
useEffect(() => {
PIXI.utils.clearTextureCache();
loader.current.destroy();
setRender(false);
if (images.length) {
const cache = [...new Set(images)];
loader.current.add(cache).load(() => {
setRender(true);
});
}
return () => {
PIXI.utils.clearTextureCache();
loader.current.destroy();
};
}, [images]);
return render && <...SomeContents />
}