Canvasの基本要素
`<Canvas>`が持つprops、サイズの取得方法、そして子要素をまとめて扱う`<Group>`によるスタイル継承・変形・クリッピングを解説します。
座標を決め、Groupをまとめて変形し、必要な範囲だけを見せ、最後に描画順で重なりを作ります。
前の章では最小構成の<Canvas>を動かしましたが、<Canvas>自身が持つpropsにはまだ触れていませんでした。この章では<Canvas>本体の機能と、複数の図形をまとめて扱うための<Group>を見ていきます。
Canvasのprops
<Canvas>は通常のViewと同じ感覚で扱えますが、Skia特有のpropsもいくつか持っています。
| Name | Type | Description |
|---|---|---|
| style? | ViewStyle |
Viewのスタイル |
| ref? | Ref<SkiaView> |
SkiaViewオブジェクトへの参照 |
| onSize? | SharedValue<Size> |
Canvasのサイズが変わるたびに更新される共有値(Shared Value) |
| highBitDepth? | boolean |
8bitより高い精度のサーフェスに描画する |
| androidWarmup? | boolean |
Androidのコンポジターに最初のフレームを直接描画する |
Canvasのサイズを取得する
Canvasのサイズはレイアウト完了後でないと確定しません。UIスレッド・JSスレッドそれぞれで取得する方法が用意されています。
UIスレッドで取得する: onSize
onSizeにReanimatedの共有値を渡すと、Canvasのサイズが変わるたびにその値が更新されます。UIスレッド上で完結するため、リサイズに合わせて図形を追従させたい場合に向いています。
import { useDerivedValue, useSharedValue } from "react-native-reanimated";
import { Canvas, Rect } from "@shopify/react-native-skia";
const SizeDemo = () => {
const size = useSharedValue({ width: 0, height: 0 });
const rect = useDerivedValue(() => {
const { width, height } = size.value;
return { x: 0, y: 0, width, height };
});
return (
<Canvas style={{ flex: 1 }} onSize={size}>
<Rect color="cyan" rect={rect} />
</Canvas>
);
};
JSスレッドで取得する: useCanvasSize
JSスレッド側でサイズが欲しい場合はuseCanvasSizeフックが使えます。refとmeasure()を組み合わせてuseLayoutEffectで取得する定番パターンを、フック1つにまとめたものです。
import { Canvas, Rect, useCanvasSize } from "@shopify/react-native-skia";
const SizeDemo = () => {
const {
ref,
size: { width, height },
} = useCanvasSize();
return (
<Canvas style={{ flex: 1 }} ref={ref}>
<Rect color="cyan" rect={{ x: 0, y: 0, width, height }} />
</Canvas>
);
};
高ビット深度で描画する
デフォルトのCanvasは8bit(チャンネルあたり256階調)のサーフェスに描画します。グラデーションのように色が滑らかに変化する表現では、この階調不足がバンディング(縞模様)として見えてしまうことがあります。
highBitDepthを有効にすると、iOSでは16bit浮動小数点、Androidでは10bitのサーフェスに描画されます。色の値そのものは変わらず、精度だけが上がる点に注意してください。HDR対応ではなく、あくまでビット深度の話です。
<Canvas style={{ flex: 1 }} opaque highBitDepth>
{/* グラデーションなど、階調の滑らかさが重要な描画 */}
</Canvas>
AndroidではhighBitDepthはGraphiteバックエンドを必要とし、デフォルトのOpenGLバックエンドでは8bitにフォールバックします。
描画結果をスナップショットとして保存する
makeImageSnapshot(同期)またはmakeImageSnapshotAsync(非同期)を使うと、Canvasの描画結果をImageとして取得できます。取得したImageはencodeToBytes()でバイト列に変換し、ファイル保存や共有に使えます。
import { Canvas, Circle, useCanvasRef } from "@shopify/react-native-skia";
const SnapshotDemo = () => {
const ref = useCanvasRef();
const handleSnapshot = () => {
const image = ref.current?.makeImageSnapshot();
const bytes = image?.encodeToBytes();
// bytesをファイル保存や共有に利用する
};
return (
<Canvas style={{ flex: 1 }} ref={ref}>
<Circle r={128} cx={128} cy={128} color="red" />
</Canvas>
);
};
テクスチャを含む描画を確実に取得したい場合は非同期のmakeImageSnapshotAsyncを使ってください。UIスレッド上で実行され、画面表示中のCanvasと同じSkiaコンテキストにアクセスできます。
以下のデモでボタンを押すと、右側にその瞬間のCanvasのスナップショット(<Image>として表示)が撮影されます。
以下のデモではmethodを切り替えてonSize(UIスレッド)とuseCanvasSize(JSスレッド)の両方を試せます。widthのスライダーでCanvasの幅を変えると、どちらの方法でも矩形が追従することが確認できます。
Group: 子要素をまとめて扱う
<Group>は複数の図形をまとめて扱うための基本的な構成要素です。深くネストでき、子要素に対して次の4つの操作をまとめて適用できます。
- Paintのプロパティ(色・スタイルなど)
- 変形(transform)
- クリッピング(clip)
- レイヤー効果(layer)
Paintの継承
<Group>に指定した色やスタイルは、子孫要素すべてに継承されます。個々の図形に同じpropsを繰り返し書かずに済みます。
import { Canvas, Circle, Group } from "@shopify/react-native-skia";
const PaintGroupDemo = () => {
const r = 100;
return (
<Canvas style={{ flex: 1 }}>
<Circle cx={r} cy={r} r={r} color="#51afed" />
{/* Group配下は色・スタイルが継承される */}
<Group color="#f5c451" style="stroke" strokeWidth={10}>
<Circle cx={r} cy={r} r={r / 2} />
<Circle cx={r} cy={r} r={r / 3} color="white" />
</Group>
</Canvas>
);
};
色以外にも、<LinearGradient>のようなシェーダーやフィルターも子として渡せば同様に継承されます。Paintまわりの詳細は次の章(Paintを理解する)で扱います。
変形(Transformations)
transformpropはReact Native本体の同名プロパティとほぼ同じ書き方ですが、2点異なります。変形の原点が要素の左上(RN Viewでは中心)であること、回転はラジアン指定であることです。
import { Canvas, Group, RoundedRect } from "@shopify/react-native-skia";
const SkewDemo = () => {
return (
<Canvas style={{ flex: 1 }}>
<Group color="lightblue" transform={[{ skewX: Math.PI / 6 }]}>
<RoundedRect x={64} y={64} width={128} height={128} r={10} />
</Group>
</Canvas>
);
};
原点を変えたいときはoriginpropを使います。originは子要素には継承されません。
クリッピング(Clipping)
clippropに矩形・角丸矩形・パスを渡すと、その範囲だけが表示されます。invertClipを付けると内外の表示が反転します。
import { Canvas, Circle, Group, Rect, Skia } from "@shopify/react-native-skia";
const star = Skia.Path.MakeFromSVGString(
"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"
)!;
const ClipDemo = () => {
return (
<Canvas style={{ flex: 1 }}>
<Group clip={star}>
<Rect x={0} y={0} width={256} height={256} color="#51afed" />
<Circle cx={128} cy={128} r={80} color="#f5c451" />
</Group>
</Canvas>
);
};
矩形の代わりにrrect()で作った角丸矩形を渡せば同じ要領で角丸クリップになります。パスの作り方は多角形とパスの章で扱います。
レイヤー効果(layer)
layerpropに<Paint>を渡すと、子要素はいったんビットマップとして描画されてからそのPaintの効果が適用されます。個々の図形にではなく、グループ全体にまとめてぼかしや色変換をかけたいときに使います。
import { Blur, Canvas, Circle, ColorMatrix, Group, Paint } from "@shopify/react-native-skia";
const GooDemo = () => {
return (
<Canvas style={{ flex: 1 }}>
<Group
color="lightblue"
layer={
<Paint>
<Blur blur={20} />
<ColorMatrix
matrix={[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 18, -7]}
/>
</Paint>
}
>
<Circle cx={0} cy={128} r={128 * 0.95} />
<Circle cx={256} cy={128} r={128 * 0.95} />
</Group>
</Canvas>
);
};
ぼかした円をビットマップ化したうえでColorMatrixのアルファ強調をかけているため、2つの円が滑らかに融合した「メタボール」のような見た目になります。個別に<Blur>をかけただけでは、円が重なった部分に境界線が残りこの効果は得られません。
描画順序(zIndex)
<Group>の子要素は基本的に書かれた順に描画されますが、zIndexpropを指定すると順序を上書きできます。値が大きいほど後(上)に描画され、デフォルトは0、負の値も使えます。zIndexはその<Group>内の兄弟要素にのみ影響するスコープを持ちます。
import { BlurMask, Canvas, Circle, Group } from "@shopify/react-native-skia";
const ZIndexDemo = () => {
const r = 80;
return (
<Canvas style={{ width: 256, height: 256 }}>
<Group>
<BlurMask style="solid" blur={10} />
<Circle cx={r} cy={r} r={r} color="cyan" zIndex={2} />
<Circle cx={256 - r} cy={r} r={r} color="magenta" zIndex={1} />
<Circle cx={128} cy={256 - r} r={r} color="yellow" zIndex={0} />
</Group>
</Canvas>
);
};
この例ではシアンの円(zIndex={2})が最前面、マゼンタ(zIndex={1})、イエロー(zIndex={0})の順に重なります。
FitBox: 描画を自動的にスケールする
<FitBox>は<Group>をベースにした特殊なコンポーネントで、元のサイズの矩形(src)を目的の矩形(dst)にフィットするよう自動でスケールしてくれます。SVGからエクスポートしたパスなど、元のサイズが分かっている描画をCanvasのサイズに合わせたいときに便利です。
<FitBox src={rect(0, 0, 664, 308)} dst={rect(0, 0, 256, 256)}>
<Path path={svgPath} style="stroke" strokeWidth={30} />
</FitBox>
fitpropでcontain(デフォルト)、fill、coverなどのフィット方法を選べます。詳細は公式ドキュメントのGroupを参照してください。
以下のデモでfeatureを切り替えると、ここまで見てきたtransform・clip・layer・zIndex・FitBoxをそれぞれ確認できます。