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を作らず使い回すことで、余計なオブジェクト生成を避けています。
以下のデモのmodeをtrailのままにすると、上のコードとほぼ同じアニメーションが再生されます。blur・serializeはこの後の節で説明する内容に対応しているので、読み進めながら切り替えてみてください。
効果を適用する
<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はパーティクルやジェネラティブアートのような、要素数が読めない表現のための道具だと考えてください。