🔧

Next.js で next epoxrt (静的HTML出力) したときに PixiJS の Sprite が描画されない

2022/11/12に公開約4,300字

導入

開発時の next dev では PixiJS > Sprite の [背景色の指定 や 読み込んだ画像] が適切に描画されていたのに、next build && next export で出力された静的HTMLでは描画されない問題がありました。一日かけて解決しましたので参考まで。

使用するライブラリのバージョン

  • next - 13.0.3
  • pixi.js - 7.0.4

ReactPixiJS を使用する場合、通常は ReactPixi を使用する方が楽かと思いますが、現在 (2022年11月13日) 時点の Next.js では ReactPixi が動作しなかったため、pixi.js を直接読み込んで使用しました。

方針

  1. pixi.js をクライアントサイド (CDN経由) で読み込む
  2. TypeScript の型も解決したい

Sprite 描画されなかった原因は、next build した際のライブラリの最適化(コンパイル)にありそうでした。
これを回避するため、ESMのCDNを直接 import したり、import するライブラリを最適化(コンパイル)させない方法も考えられそうですが、Next 13 から Webpack → Turbopack が進んだりとビルド方面が不安定なので、今回は汎用的に使えそうなクライアントサイドでの読み込みで解決としました。

実装方法

1.) pixi.js をクライアントサイド (CDN経由) で読み込む

pages/_document.tsx
class MyDocument extends Document {
  override render() {
    return (
      <Html className="h-full">
        <Head>
+          <script
+            src="https://cdn.jsdelivr.net/npm/pixi.js@7.0.4/dist/pixi.min.js"
+            integrity="sha256-7p4g8H3cJS5tVFDTbx2ExxiJwQ/PTAZLwdc6kyvXa+0="
+            crossOrigin="anonymous"
+          />
        </Head>
        <body className="h-full">
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument

2.) TypeScript の型も解決したい

クライアントサイドの PIXI を使用しつつ、import type * as PIXI from 'pixi.js' で得られる型を使いたい。

表題の不具合の解決は getGlobalPIXI()useEffect() 内で読み込むとのことで解決しますが、React で pixi.js を初期化するサンプルとしてもコード一式を載せておきます。

index.tsx
import type { FC } from 'react'
import { useEffect, useState, useRef } from 'react'
+ import type * as PIXI from 'pixi.js'
import { useEffectOnce, useMeasure } from 'react-use'

+ const global =
+   typeof window !== 'undefined'
+     ? (window as Window &
+         typeof globalThis & {
+           PIXI: {
+             Application: typeof PIXI.Application
+             Container: typeof PIXI.Container
+             Graphics: typeof PIXI.Graphics
+             Sprite: typeof PIXI.Sprite
+           }
+         })
+     : null
+ const getGlobalPIXI = () => {
+   if (!global || !global.PIXI) throw new Error('PIXI is not defined')
+   return global.PIXI
+ }

const useHook = () => {
  const ref = useRef<HTMLDivElement>(null)
  const [measureRef, { width, height }] = useMeasure<HTMLDivElement>()
  const [isReady, setIsReady] = useState(false)

  const stateRef = useRef({
    app: null as PIXI.Application | null,
    container: null as PIXI.Container | null,
    width,
    height,
  })

  // Update stateRef by states
  useEffect(() => {
    stateRef.current.width = width
    stateRef.current.height = height
  }, [width, height])

  // Initialize PIXI App
  useEffectOnce(() => {
    const el = ref.current as HTMLDivElement
    measureRef(el)

    const gPIXI = getGlobalPIXI()
    const app = new gPIXI.Application({
      resolution: window.devicePixelRatio,
      backgroundAlpha: 0,
    })

    const container = new gPIXI.Container()
    app.stage.addChild(container)
    el.appendChild(app.view as any)

    stateRef.current.app = app
    stateRef.current.container = container
    setIsReady(true)

    return () => {
      app.destroy(true, true)
      stateRef.current.app = null
      stateRef.current.container = null
      setIsReady(false)
    }
  })

  // Resize canvas
  useEffect(() => {
    const { app } = stateRef.current
    if (!app) return
    app.renderer.resize(width, height)
  }, [height, width])

  useEffect(() => {
    const { app, container } = stateRef.current
    if (!isReady || !app || !container) return

    const gPIXI = getGlobalPIXI()
    const frame = new gPIXI.Container()
    const image = gPIXI.Sprite.from(
      'https://avatars.githubusercontent.com/u/1271863?v=4'
    )
    image.anchor.set(0.5)

    frame.addChild(image)
    container.addChild(frame)

    // Implement animation
    app.ticker.add((delta: number) => {
      const { width, height } = stateRef.current

      image.position.set(width / 2, height / 2)
      image.rotation += 0.01 * delta
    })

    return () => {
      while (container.children[0]) {
        container.removeChild(container.children[0])
      }
    }
  }, [isReady])

  return {
    ref,
  }
}

const Page: FC = () => {
  const { ref } = useHook()
  return <div ref={ref} className="h-full" />
}

export default Page

Discussion

ログインするとコメントできます