最初の描画とレンダリングモード
最小構成のCanvasを読み解きながら、Retained ModeとImmediate Modeという2つの描画パラダイムの違いを解説します。
左は構造を保持して値を更新、右はフレームごとに描く内容そのものを組み立て直します。
前の章でSkiaの描画パイプラインの全体像を見たので、ここでは実際に動くコードを読みながら、最小構成の<Canvas>がどう組み立てられているかを確認します。
最初のCanvas
以下が、React Native Skiaで書ける最小のCanvasです。
import { Canvas, Circle, Fill } from "@shopify/react-native-skia";
import { StyleSheet, View } from "react-native";
export default function FirstCanvas() {
return (
<View style={styles.container}>
<Canvas style={styles.canvas}>
<Fill color="#1e1e2e" />
<Circle cx={128} cy={128} r={64} color="#b58df1" />
</Canvas>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
canvas: {
flex: 1,
},
});
<Canvas>は通常のViewと同じようにstylepropでレイアウトを指定でき、他のReact Nativeコンポーネントの中に自由に配置できます。中身は次の2つの図形だけです。
<Fill color="#1e1e2e" />はCanvas全体を単色で塗りつぶします。背景を敷くときによく使う書き方です<Circle cx={128} cy={128} r={64} color="#b58df1" />は中心座標(128, 128)、半径64の円を描きます
座標系はCanvasの左上が原点(0, 0)で、右方向がx軸のプラス、下方向がy軸のプラスです。これは通常のRN Viewの座標系や、SVG・Canvas 2D APIの座標系と同じ感覚で扱えます。
描いた図形は「後から書いたものが上」
<Fill>を先に、<Circle>を後に書いているのには理由があります。Skiaは子要素を書かれた順番通りに描画するため、後から描いた図形が先に描いた図形の上に重なります。もし順番を逆にすると、円が背景色で塗りつぶされて見えなくなってしまいます。
複数の図形を重ねるレイアウトでは、この「上から下へのz順序」を意識しておくと意図通りの見た目になります。
Retained ModeとImmediate Mode
React Native Skiaには、描画のやり方として大きく2つのパラダイムがあります。**保持モード(Retained Mode)と即時モード(Immediate Mode)**です。どちらも同じ<Canvas>の中で使えるので、混在させることもできます。この章以降は、公式ドキュメントでの表記にならい英語名(Retained Mode / Immediate Mode)で呼びます。
Retained Mode(デフォルト)
ここまで見てきた書き方が、まさにRetained Modeです。シーンをReactコンポーネントのツリーとして宣言し、React Native Skiaがそれをディスプレイリストに変換します。
const RetainedModeExample = () => {
const radius = useSharedValue(50);
useEffect(() => {
radius.value = withSpring(radius.value === 50 ? 100 : 50);
}, []);
return (
<Canvas style={{ flex: 1 }}>
<Circle cx={128} cy={128} r={radius} color="cyan" />
</Canvas>
);
};
radiusのようなReanimatedの共有値をpropsに渡しておけば、値の更新はUIスレッド上で完結し、React側の再レンダーはほとんど発生しません。ツリーの構造自体は変わらず、プロパティの値だけがアニメーションするようなユーザーインターフェースやインタラクティブな描画に向いています。
Immediate Mode
一方Immediate Modeでは、フレームごとに描画命令を直接発行します。PictureコンポーネントとPictureRecorderを使い、コンポーネントツリーの形そのものを毎フレーム変えられます。
const ImmediateModeExample = () => {
const progress = useSharedValue(0);
const recorder = Skia.PictureRecorder();
const paint = Skia.Paint();
const picture = useDerivedValue(() => {
"worklet";
const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, 256, 256));
const count = Math.floor(progress.value * 20);
for (let i = 0; i < count; i++) {
paint.setColor(Skia.Color(`rgba(0, 122, 255, ${(i + 1) / 20})`));
canvas.drawCircle(128, 128, (i + 1) * 6, paint);
}
return recorder.finishRecordingAsPicture();
});
return (
<Canvas style={{ flex: 1 }}>
<Picture picture={picture} />
</Canvas>
);
};
描く円の数(count)がフレームごとに変わっている点に注目してください。Retained Modeではこのように要素数が動的に変わる表現は苦手ですが、Immediate Modeならネイティブ呼び出し(FFI)のコストを払う代わりに、毎フレーム自由に描画命令を組み立てられます。
どちらを選ぶか
| シナリオ | 推奨モード | 理由 |
|---|---|---|
| プロパティがアニメーションするUI | Retained | アニメーション中のFFIコストがほぼゼロ |
| データ可視化 | Retained | 通常は構造が固定されている |
| スプライトやタイルの枚数が固定の表現 | Retained | Atlas APIと組み合わせれば1回の描画命令で済む |
| エンティティが増減するゲーム | Immediate | オブジェクトが動的に生成・破棄される |
| ジェネラティブアート | Immediate | 描画命令自体が毎フレーム変わる |
迷ったときはRetained Modeから始めるのがおすすめです。第2部以降で扱う図形やアニメーションの大半はRetained Modeで書けます。Immediate Modeは、Pictureを扱う章(第2部のPictureと再利用可能な描画命令)で改めて取り上げます。
環境構築やインストール手順は本書では扱いません。プロジェクトへの導入方法は公式のGetting Startedを参照してください。