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

Pictureと再利用可能な描画命令

毎フレーム描画命令の数自体が変わる表現のためのPicture APIと、PictureRecorderによる記録・再利用の仕組みを解説します。

第1部の最初の描画とレンダリングモードで触れた通り、React Native Skiaは基本的に保持モード(Retained Mode)で動きます。毎回のレンダーでコンポーネントツリーからディスプレイリストが作られ、プロパティ値のアニメーションはほぼゼロコストです。

ただし、1フレームごとに描画命令の数そのものが変わる表現は、コンポーネントツリーの形が固定されているRetained Modeでは表現できません。そこで使うのが即時モード(Immediate Mode)のAPIである<Picture>です。

Pictureとは何か

Pictureは、描く順に並んだ描画命令のリストです。一度作られたPictureはイミュータブルで、後から編集できません。同じPictureを複数のCanvasで使い回すこともできます。

Name Type Description
picture SkPicture 描画するPicture

Hello World: 円の数が変わるアニメーション

以下は、時間経過とともに円の数が増減する「トレイル」アニメーションです。円の数自体がフレームごとに変わるため、<Circle>をいくつ並べるかをコンポーネントツリーでは表現できず、Pictureが必要になります。

import { Canvas, Picture, Skia } from "@shopify/react-native-skia";
import { useDerivedValue, useSharedValue, withRepeat, withTiming } from "react-native-reanimated";
import { useEffect } from "react";

const size = 256;
const n = 20;
const paint = Skia.Paint();
const recorder = Skia.PictureRecorder();

const TrailDemo = () => {
  const progress = useSharedValue(0);

  useEffect(() => {
    progress.value = withRepeat(withTiming(1, { duration: 3000 }), -1, true);
  }, [progress]);

  const picture = useDerivedValue(() => {
    "worklet";
    const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, size, size));
    const numberOfCircles = Math.floor(progress.value * n);
    for (let i = 0; i < numberOfCircles; i++) {
      const alpha = (i + 1) / n;
      const r = ((i + 1) / n) * (size / 2);
      paint.setColor(Skia.Color(`rgba(0, 122, 255, ${alpha})`));
      canvas.drawCircle(size / 2, size / 2, r, paint);
    }
    return recorder.finishRecordingAsPicture();
  });

  return (
    <Canvas style={{ flex: 1 }}>
      <Picture picture={picture} />
    </Canvas>
  );
};

useDerivedValueのworklet内で毎回beginRecording → 描画命令の発行 → finishRecordingAsPictureという3ステップを踏んでいます。progressの値に応じてnumberOfCirclesが変わり、ループの回数(つまり描画命令の数)自体がフレームごとに変化しています。この「命令の数が動的に変わる」点こそが、Retained Modeでは扱えずPictureが必要になる理由です。

Skia.Paint()Skia.PictureRecorder()をコンポーネントの外で1度だけ生成している点にも注目してください。毎フレームbeginRecordingのたびに新しいPaintを作らず使い回すことで、余計なオブジェクト生成を避けています。

以下のデモのmodetrailのままにすると、上のコードとほぼ同じアニメーションが再生されます。blurserializeはこの後の節で説明する内容に対応しているので、読み進めながら切り替えてみてください。

Skiaを読み込み中…

効果を適用する

<Picture><Circle><Rect>のような通常の描画コンポーネントとは異なり、propsとして塗りの属性を直接受け取りません。<Group>layerpropでラップし、レイヤー効果として適用します。

import { BlendMode, Blur, Canvas, Group, Paint, Picture, Skia } from "@shopify/react-native-skia";
import { useMemo } from "react";

const PictureBlurDemo = () => {
  const picture = useMemo(() => {
    const recorder = Skia.PictureRecorder();
    const size = 256;
    const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, size, size));
    const r = 0.33 * size;
    const paint = Skia.Paint();
    paint.setBlendMode(BlendMode.Multiply);

    paint.setColor(Skia.Color("cyan"));
    canvas.drawCircle(r, r, r, paint);
    paint.setColor(Skia.Color("magenta"));
    canvas.drawCircle(size - r, r, r, paint);
    paint.setColor(Skia.Color("yellow"));
    canvas.drawCircle(size / 2, size - r, r, paint);

    return recorder.finishRecordingAsPicture();
  }, []);

  return (
    <Canvas style={{ flex: 1 }}>
      <Group layer={<Paint><Blur blur={10} /></Paint>}>
        <Picture picture={picture} />
      </Group>
    </Canvas>
  );
};

この例では静的なPictureなのでuseDerivedValueではなくuseMemoで作っています。アニメーションさせる場合は前の例のようにuseDerivedValueを使ってください。

シリアライズして保存・再利用する

Pictureはバイト配列にシリアライズできます。シリアライズしたデータは、作成時と同じバージョンのSkiaでのみ復元できる点に注意してください。

import { Canvas, Group, Picture, Skia } from "@shopify/react-native-skia";
import { useMemo } from "react";

const SerializeDemo = () => {
  const picture = useMemo(() => {
    const recorder = Skia.PictureRecorder();
    const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, 100, 100));
    const rectPaint = Skia.Paint();
    rectPaint.setColor(Skia.Color("pink"));
    canvas.drawRect({ x: 0, y: 0, width: 100, height: 100 }, rectPaint);
    const circlePaint = Skia.Paint();
    circlePaint.setColor(Skia.Color("orange"));
    canvas.drawCircle(50, 50, 50, circlePaint);
    return recorder.finishRecordingAsPicture();
  }, []);

  const serialized = useMemo(() => picture.serialize(), [picture]);
  const copy = useMemo(
    () => (serialized ? Skia.Picture.MakePicture(serialized) : null),
    [serialized]
  );

  return (
    <Canvas style={{ flex: 1 }}>
      <Picture picture={picture} />
      <Group transform={[{ translateX: 200 }]}>
        {copy && <Picture picture={copy} />}
      </Group>
    </Canvas>
  );
};

serialize()で得たバイト列は、Skiaデバッガーのような外部ツールとの連携や、描画内容をファイル・ネットワーク経由で受け渡す用途に使えます。

インスタンスメソッド

Name Description
makeShader このPictureで描画するシェーダーを新しく作る
serialize Pictureの描画命令をUint8Arrayとして返す

Retained ModeとImmediate Modeの使い分け

第1部の早見表でも触れた通り、迷ったときはまずRetained Modeで書き、「描画命令の数自体が動的に変わる」場面に出会ったときだけPictureに切り替えるのがおすすめです。データ可視化やUIアニメーションの大半はRetained Modeのままで問題なく、Pictureはパーティクルやジェネラティブアートのような、要素数が読めない表現のための道具だと考えてください。