2022-08-07

ブログにreact-native-skiaを導入しました🎨

react-native-skiaがExpo GoとWebでサポートされたそうです。

React Native Skia is now supported in Expo Go and on web. Skia is a high-performance 2D graphics library used by Chrome, among many other great tools, and React Native Skia provides an idiomatic React API for using Skia in your apps. This library raises the ceiling over what you can do in React Native — it’s truly a “game changer” for producing performant visual effects with React. React Native Skia is built by William Candillon and Christian Falch with support from Shopify.

Expo SDK 46. | by Brent Vatne | Aug, 2022 | Exposition

SkiaはGoogleが開発しているOSSの2Dグラフィックライブラリです。ブラウザやAndroid等、様々なプラットフォームで利用することができます。FlutterはUIの描画エンジンにSkiaを使っています。

React Nativeでは2D描画をしたい場合、有力な手段がSVGしかなく表現力に限界がありました。例えばシャドウやぼかしといった効果が使えなかったりするのもその一例です。SVGを利用した場合描画のパフォーマンスにも課題がありました。

これに対しreact-native-skiaが提供されたことにより、React Nativeでさまざまな2D描画を実行することができるようになりました。React Nativeの新しいアーキテクチャであるJSIを利用しておりパフォーマンスも期待できそうです。またFlutterのAPIと100%互換になっているそうです。Web版ではFlutterと同様にCanvasKit (WASM)が利用されています。このあたりもFlutterと同じAPIにしている理由がありそうです。

以下はreact-native-skiaの記事・動画・開発者のTwitterアカウント等のリンクです。

ブログへの導入

当ブログはreact-native-webを導入しているので記事のMarkdown内でReact Nativeのコンポーネントを表示することができます(以前の記事: "当ブログにReact Native for Webを導入しました" )。これに加えSkiaでの2D描画が使えれば記事内で更に色々な表現ができそうなので早速導入してみることにしました。

Webでのreact-native-skiaのセットアップ方法は以下の公式ドキュメントに従います。

Web Support | React Native Skia

まずライブラリ自体をインストールします。

yarn add @shopify/react-native-skia

その後WebPackの設定が必要になりますが、このブログはnext.jsを利用しているためnext.config.jsで設定を追加しました。

const configs = {
  webpack: config => {
    config.resolve.alias = {
      ...(config.resolve.alias || {}),
      // Transform all direct `react-native` imports to `react-native-web`
      "react-native$": "react-native-web",
      // (1)
      react: path.resolve(__dirname, "./node_modules/react"),
      "react-native-web": path.resolve(
        __dirname,
        "./node_modules/react-native-web",
      ),
    }
    // (2)
    config.resolve.fallback = {
      fs: false,
      path: false,
    }
    // (3)
    config.plugins = [
      ...config.plugins,
      new CopyPlugin({
        patterns: [
          {
            from: "node_modules/canvaskit-wasm/bin/full/canvaskit.wasm",
            to: "static/chunks/pages/posts/canvaskit.wasm",
          },
        ],
      }),
    ]
    // (4)
    config.externals = {
      ...config.externals,
      "react-native-reanimated": "require('react-native-reanimated')",
      "react-native-reanimated/lib/reanimated2/core":
        "require('react-native-reanimated/lib/reanimated2/core')",
    }
    return config
  },
}

(1)のresolve.aliasの部分はreactの混在により"Hooks can only be called inside the body of a function component"とエラーが出るためその回避のためです。

(2)のconfig.resolve.fallbackは、canvaskit-wasmでcannot resolve fsのエラーを回避するための設定です。

(3)のCopyPluginの箇所ではcanvaskit.wasmをビルドディレクトリにコピーしています。Webpackのそのままの設定だとnode_modules配下のwasmファイルが公開されないためコピーする必要があります。今回コピー先をstatic/chunks/pages/postsにしているのですが、 Next.jsでそのままトップにコピーした場合、canvaskit.wasmを取得するリクエスト先のパスが/pages/posts/canvaskit.wasm のように該当ページのフォルダの下になってしまうため、配下に直接コピーしています。(トップに置いてうまく取得先のパスを変えられればよいのですが、方法がわからなかったため今回はこの形にしました)

(4)のconfig.externalsの設定はreanimatedをインストールしていない場合に必要です。

また、node_modules/@shopify/react-native-skiaをtranspileする必要があるため、next-transpile-modulesを使います。transpileしないとJSXの記述でsyntaxエラーが出ます。

const withTM = require("next-transpile-modules")([
  "@shopify/react-native-skia",
])

withTM(configs)

webpackの設定で迷った場合は、react-native-skiaのexampleの設定等を参考にするとよいと思います。また、このブログのnext.config.jsはこちらで確認可能です。

実際にReactでSkiaのComponentを描画するには<WithSkiaWeb />getComponentpropを指定し該当のコンポーネントをdynamic importで読み込みます。こうすることで全体が待ち状態になることなく、Skiaのロードが終わった後にコンポーネントを読み込んで描画することができます。

<WithSkiaWeb
  getComponent={() => import("./MySkiaComponent")}
  fallback={<Text>Loading Skia...</Text>} />

全体を遅延させてもよい場合はLoadSkiaWeb()を利用します。詳しくは公式ドキュメントを参考にしてください。Web Support | React Native Skia

実際にreact-native-skiaで描画してみる

さっそくSkiaで描画を試してみます。星を<Path />で描いてグラデーションをつけてみました。長押しするとぼかし効果が入るようにアニメーションを設定しています。

  const onTouch = useTouchHandler({
    onStart: () => {
      runTiming(value, 10, {
        duration: 600,
      })
    },
    onEnd: () => {
      runTiming(value, 0, {
        duration: 1000,
      })
    },
  })
  return (
    <Canvas onTouch={onTouch}>
      <Group>
        <Path
          strokeCap={"round"}
          strokeWidth={10}
          style="fill"
          path="M 128 0 L 168 80 L 256 93 L 192 155 L 207 244 L 128 202 L 49 244 L 64 155 L 0 93 L 88 80 L 128 0 Z"
        >
          <Blur blur={value} />
          <LinearGradient
            start={vec(0, 0)}
            end={vec(280, 280)}
            colors={["#30cfd0", "#330867"]}
          />
        </Path>
      </Group>
    </Canvas>
  )

グラデーションやぼかし効果が簡単に行えるのが驚きです。

ついでにグラデーションボタンも作ってみました。

<TouchableOpacity>
  <View
    style={{
      alignItems: "center",
      justifyContent: "center",
      width: 256,
      height: 48,
    }}
  >
    <Canvas
      style={[StyleSheet.absoluteFill, { width: "100%", height: "100%" }]}
    >
      <RoundedRect x={0} y={0} width={256} height={48} r={8}>
        <LinearGradient
          start={vec(0, 10)}
          end={vec(256, 38)}
          colors={["#10408E", "#11A4FF"]}
        />
      </RoundedRect>
    </Canvas>
    <View
      style={[
        StyleSheet.absoluteFill,
        {
          width: "100%",
          height: "100%",
          alignItems: "center",
          justifyContent: "center",
        },
      ]}
    >
      <Text
        style={{
          color: "white",
          fontWeight: "500",
          fontSize: 16,
          letterSpacing: 2,
        }}
      >
        Title
      </Text>
    </View>
  </View>
</TouchableOpacity>

所感

今までReact Nativeで特定の表現のUIを実装しようとした場合、労力を要することが多々ありました。例えばグラデーションの描画をしたい場合react-native-linear-gradient等、iOS/Androidネイティブの機能をブリッジしたライブラリをインストールして使う必要があり、グラデーションボタン1つ作るのに一苦労でした。

react-native-skiaが提供されたことによりUI表現に関するこれらのネックが大幅に解消されそうです。今後はReact NativeのUIの作り方も変わってくるのではないかと思います。表現や効果、アニメーション等がより充実したアプリ等が作れるようになったのはとてもありがたいです。

また、Reactで宣言的UIを使って描画を行えるという点も大変魅力的な点だと思います。

最終更新: 2022-10-05 00:40
筆者: @gaishimo 主にReact Nativeでのアプリ開発を行っています。
© 2021 Omoidasu, Inc. All rights reserved.