Skip to content
React Native Skia ガイドブック
Esc
navigateopen⌘Jpreview
On this page

Skiaのアニメーションモデル

Skiaのコンポーネントがどのように「動き」を表現するのか、useState+requestAnimationFrameによる素朴な方法から、Skiaが提供するuseClockやパス補間フックまでを見ていきます。

これまでの章で扱ってきたのは、いずれも静止した1枚の絵でした。ここからの第5部では、その絵をどう動かすかを扱います。

<Canvas>配下のpropsは、cx={128}のような固定値だけでなく、時間とともに変わる値も受け取れます。値が変われば、Skiaの専用レンダラー(第1部で見たシーングラフへの変換)がその差分を検知して再描画します。「毎フレームcanvas.drawCircle()を呼ぶ」ような命令的なループを自分で書く必要はありません。propsの値を更新するだけで、あとはSkia側が面倒を見てくれます。

もっとも素朴な方法: useState + requestAnimationFrame

Reanimatedを使わなくても、ReactのuseStaterequestAnimationFrameだけでアニメーションは書けます。

import { useEffect, useState } from "react";
import { Canvas, Circle } from "@shopify/react-native-skia";

const RawLoopDemo = () => {
  const [angle, setAngle] = useState(0);

  useEffect(() => {
    let frame: number;
    const tick = () => {
      setAngle((a) => a + 0.02);
      frame = requestAnimationFrame(tick);
    };
    frame = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(frame);
  }, []);

  const cx = 128 + 60 * Math.cos(angle);
  const cy = 128 + 60 * Math.sin(angle);

  return (
    <Canvas style={{ flex: 1 }}>
      <Circle cx={cx} cy={cy} r={20} color="cyan" />
    </Canvas>
  );
};

外部システム(ブラウザのフレームタイミングAPI)との同期なので、useEffectを使うのはReactの流儀として妥当です。ただしこの方法は、毎フレームsetAngleを呼ぶたびにJavaScriptスレッド上でコンポーネントの再レンダリングが起き、その結果をSkiaのレンダラーに渡す、という経路を通ります。フレームレートが低いデバイスや、他の処理でJSスレッドが混み合っている場面では、動きがカクつく原因になります。

React Native SkiaはReanimatedと組み合わせることで、この再レンダリングの経路を丸ごと省略できます。次章で詳しく扱いますが、この章ではまずSkiaが用意しているアニメーション専用のフックを見ていきましょう。

useClock: 経過時間を返すクロック

useClockは、マウントしてからの経過時間(ミリ秒)を返すフックです。戻り値はReanimatedの共有値(Shared Value)で、useDerivedValueの中で読み取ると、UIスレッド上で毎フレーム再計算されます(Web上でこの自動再計算を機能させる条件については、この章末の「注意点」を参照してください)。

import { Canvas, Circle, useClock } from "@shopify/react-native-skia";
import { useDerivedValue } from "react-native-reanimated";

const ClockDemo = () => {
  const clock = useClock();

  // 第2引数のdependenciesについては章末の「注意点」を参照。
  const cx = useDerivedValue(
    () => 128 + 60 * Math.cos(clock.value / 500),
    [clock]
  );
  const cy = useDerivedValue(
    () => 128 + 60 * Math.sin(clock.value / 500),
    [clock]
  );

  return (
    <Canvas style={{ flex: 1 }}>
      <Circle cx={cx} cy={cy} r={20} color="cyan" />
    </Canvas>
  );
};

先ほどのrequestAnimationFrame版と見た目の結果はほぼ同じですが、仕組みはまったく違います。clockの更新もcxcyの再計算もUIスレッド上で完結し、Reactの再レンダリングは1度も発生しません。共有値やworkletの詳細は次章で扱いますが、ここでは「Skiaは時間経過そのものを共有値として扱えるフックを用意している」という点だけ押さえておいてください。

Skiaを読み込み中…

パスやバッファを補間するフック

useClock以外にも、Skiaはアニメーション向けのフックをいくつか提供しています。いずれもReanimatedの共有値を返すか、worklet内で更新する前提で作られています。

フック 用途
usePathValue PathBuilderを使ってパスを構築し、その結果を共有値として返す
usePathInterpolation progress値に応じて、複数のパスの間をなめらかに補間する
useVectorInterpolation ベクトル({x, y})値の補間
useRectBuffer Atlasなどが受け取る矩形の配列をworklet内から更新できるバッファにする
useRSXformBuffer Atlasが受け取る回転・スケール変換の配列をworklet内から更新できるバッファにする

usePathInterpolationは、アイコンの形が切り替わるようなアニメーションに向いています。ただし補間対象のパスは、コマンドの数と種類が揃っている(interpolatableである)必要があります。

import { usePathInterpolation, Canvas, Path, Skia } from "@shopify/react-native-skia";
import { useSharedValue } from "react-native-reanimated";

const angryPath = Skia.Path.MakeFromSVGString("...")!;
const normalPath = Skia.Path.MakeFromSVGString("...")!;
const goodPath = Skia.Path.MakeFromSVGString("...")!;

const FaceDemo = () => {
  const progress = useSharedValue(0);
  const path = usePathInterpolation(
    progress,
    [0, 0.5, 1],
    [angryPath, normalPath, goodPath]
  );
  return (
    <Canvas style={{ flex: 1 }}>
      <Path path={path} style="stroke" strokeWidth={5} strokeCap="round" />
    </Canvas>
  );
};

下のデモは、頂点数を揃えた3つの形(円に近い形・星形・波形)をusePathInterpolationで滑らかに繋いでいます。スライダーでprogressを動かすと、形が連続的に変形していく様子がわかります。

Skiaを読み込み中…

useRectBufferuseRSXformBufferは、第2部で扱った<Atlas>のように、大量のインスタンスをまとめて動かしたい場面で使います。useTextureと組み合わせたテクスチャアニメーションは第4章で改めて扱います。

注意点

これらのフックはどれも内部でReanimatedの共有値・useDerivedValueuseAnimatedReactionなどに依存しています。つまりreact-native-reanimatedのインストールが前提です。単発のトランジションだけならuseStateでも十分ですが、毎フレーム値が変わり続けるアニメーションでは、次章で扱うReanimated連携を使うほうがパフォーマンス面で有利です。

Web上でのuseDerivedValueについて: useDerivedValueは本来、渡したコールバックが参照している共有値(この例ではclock)をBabel/SWCのreact-native-workletsプラグインが自動検出し、その値が変わるたびに再計算します。しかしこのプロジェクトのようにMetro/Babelを使わないVite製のWebビルドでは、そのプラグインが動いていないため自動検出が機能せず、useDerivedValueは初回計算のまま値が凍りついてしまいます。回避策は、上のコード例のように第2引数のdependencies配列に、コールバックが参照している共有値を明示的に渡すことです。これはReanimated自身が「Babelプラグイン無しでWebで使う場合のためのAPI」として公式にドキュメント化している挙動です。ネイティブ(iOS/Android)ではBabelプラグインが動くため、dependenciesを省略してもこの問題は起こりません。

公式ドキュメントのAnimationsHooksも参照してください。