Closed20

PixiJS + React で実装する時のメモ

ivgtrivgtr

指針

PixiJSをラップして命令的に記述できるライブラリを使う

ivgtrivgtr

ライブラリ選定

@inlet/react-pixi を使う
メインのコミッターが非常に活発に動いており、ドキュメントやissueでの問題解決が期待できる

比較対象としてよく上がるのがreact-pixi-fiber
プロジェクト自体は動いてるっぽいがnpmには古いバージョンしか上がってない

ivgtrivgtr

@inlet/react-pixi の使い方

ほぼほぼドキュメントに書いてある

ライブラリの各種コンポーネントの使い方などを補足したりする

ivgtrivgtr

<Stage />

@inlet/react-pixi の基本的な使い方をしては、
Stage を配置し、その配下に描画したいオブジェクトなどを追加していく

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

export default App;

height,width,options などの設定をStage に継承して初期設定を追加する事ができる

レスポンシブな画面表示

固定値なら関係ないが、フル画面やウインドウサイズによってStageサイズを変更したい思いがある時、
そのままwidthwindow.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>
ivgtrivgtr

<Container />

幾つかのコンテンツをまとめる為のContainer
このコンポーネント自体にインタラクティブなイベントを仕込むことはできない

ivgtrivgtr

pixi-viewportの利用

Stage 全体を拡大・縮小、移動などをしたいと思う時があると思う(GoogleMapの様な)
基本的にStage は描画する境界線を定義する物なので、Stage に対して何か操作をするのは得策では無く、コンテンツをContainer などでラップして拡大・縮小、移動させるべき

ただ、拡大・縮小、移動の数値を保持したりと色々と手間などでpixi-viewport というライブラリを使う

ivgtrivgtr

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;
ivgtrivgtr

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} />;
});
ivgtrivgtr

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);
  }
});

ivgtrivgtr

設定している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 をリサイズ
ivgtrivgtr

pixi-cullの利用

pixi-viewport と同じ作者のculling ライブラリ

カリングとは?

画面外のオブジェクトの描画をスキップし、パフォーマンスを向上させえる

ivgtrivgtr

使い方

上述のViewport コンポーネント内でイベント設定してあげる
画面の更新が走る時にカリング処理をする

参考: 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;
  }
});
ivgtrivgtr

全体

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);
  }
});
ivgtrivgtr

問題

Viewport 配下のコンポーネントが静的なオブジェクトの場合は正常にカリングが働くが、アニメーションなどをしてるとカリングしてくれない

ivgtrivgtr

アニメーションをさせたい

PixiJS内でオブジェクトにアニメーションをさせる方法は主に2つある

ivgtrivgtr

react-spring

@inlet/react-pixi の場合、ライブラリ側でreact-spring 向けのコンポーネントも配布されており、
導入の手間が少ない

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

import { Stage } from '@inlet/react-pixi/animated'
にするだけでコンポーネントをreact-springに対応させることが出来る

参考

ivgtrivgtr

簡単な回転アニメーション

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>
  );
};
ivgtrivgtr

大量の画像を表示される時にチラつきを抑えるための小技

@inlet/react-pixi で画像を表示するには、Sprite コンポーネントのprops のimageにurlを渡して上げる必要がある
つまり、Sprite コンポーネントがマウントされるまでは画像を読み込まないので大量のSprite を表示する時は必ずチラつきが発生する(読み込みが終わった画像から表示される)

PixiJSには画像のキャッシュ機構が存在し、画像を読み込むとそこに保存されるので同じ画像のurlが渡された場合は即座に表示される
なので、Stage が表示される前にPixiJSに画像の配列を渡して事前ロードさせよう

ivgtrivgtr

同じ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 />
}
このスクラップは2021/06/25にクローズされました