mobile-animation-skia
React Native Skia GPU-accelerated 2D graphics - Canvas, declarative drawing, shaders, image filters, Paragraph text, Atlas batch rendering, Reanimated animations
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-animation-skia/skills/mobile-animation-skia
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
React Native Skia Patterns
Quick Guide: Use
@shopify/react-native-skiafor GPU-accelerated 2D drawing in React Native. The Canvas component hosts a separate React renderer. Drawing primitives (Circle, Rect, Path, Image) compose declaratively. Paint attributes cascade through Groups. Animations use Reanimated shared values passed directly as props -- nocreateAnimatedComponentneeded. UseinterpolateColorsfrom Skia for color animations (not Reanimated'sinterpolateColor). Use Atlas for batch rendering thousands of sprites. Use Paragraph for rich text layout. Group transforms default to top-left origin, not center.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST pass Reanimated shared values directly as Skia component props -- do NOT use createAnimatedComponent or useAnimatedProps)
(You MUST use interpolateColors from @shopify/react-native-skia for color animations -- Reanimated's interpolateColor uses a different internal color format and produces wrong results)
(You MUST use the layer property to apply paint effects to Paragraph, Picture, and ImageSVG components -- they do not follow standard paint inheritance rules)
(You MUST remember that Group transform origin defaults to top-left, not center -- set origin prop explicitly for center-based rotations)
</critical_requirements>
Auto-detection: react-native-skia, @shopify/react-native-skia, Canvas, Skia.Path, Skia.Paint, Circle, Rect, Path, Group, Paint, ImageSVG, BackdropBlur, BackdropFilter, RuntimeShader, Atlas, Paragraph, ParagraphBuilder, usePathInterpolation, useClock, useRSXformBuffer, SkSL, image filter, shader, offscreen, Picture, FitBox
When to use:
- Drawing custom 2D graphics (charts, diagrams, custom shapes)
- Applying GPU-accelerated blur, shadow, or color filter effects
- Animating paths, shapes, or shader uniforms at 60 FPS
- Rendering rich text with mixed fonts using Paragraph API
- Batch rendering sprites/tiles with Atlas
- Creating custom image filters with SkSL shaders
- Generating images offscreen (thumbnails, exports)
When NOT to use:
- Standard UI layouts (use regular React Native views)
- Simple static images (use
<Image>from React Native) - 3D graphics (Skia is 2D only -- use a 3D solution)
- Text-only screens (use React Native
<Text>)
Key patterns covered:
- Canvas setup and threading model
- Declarative drawing with shapes, paths, and images
- Paint inheritance and composition through Groups
- Reanimated integration (shared values as props, color interpolation)
- Image filters (Blur, Shadow, RuntimeShader) and backdrop filters
- Paragraph text layout with custom fonts
- Atlas batch rendering for sprites and tiles
- SkSL custom shaders with uniforms
- Picture recording for dynamic drawing operations
- SVG rendering with limitations
Detailed Resources:
- examples/core.md - Canvas, shapes, paths, paint, groups, images
- examples/animations.md - Reanimated integration, path interpolation, color animation, Atlas animation
- examples/effects.md - Image filters, backdrop blur, shaders, RuntimeShader
- examples/text-and-media.md - Paragraph API, SVG rendering, Picture recording, offscreen rendering
- reference.md - Decision frameworks, paint property reference, SkSL types
<decision_framework>
Decision Framework
When to Use Skia vs Native Views
Does the feature need custom drawing (paths, gradients, blur, shaders)?
├─ YES → Skia Canvas
└─ NO → Does it need high-performance batch rendering (100+ similar elements)?
├─ YES → Skia Atlas
└─ NO → Does it need rich text with mixed fonts/decorations?
├─ YES → Skia Paragraph (or native Text if simple)
└─ NO → Standard React Native views
Declarative vs Imperative API
Is the shape/path static or defined at build time?
├─ YES → Declarative JSX (<Circle />, <Path path="..." />)
└─ NO → Is the shape computed dynamically per frame?
├─ YES → Imperative (Skia.Path.Make()) inside worklets or useDerivedValue
└─ NO → Is the shape created once based on data?
├─ YES → Imperative, created outside render, passed as prop
└─ NO → Declarative with animated shared value props
Choosing the Right Text API
Need simple single-style text?
├─ YES → Skia <Text> component (single font, single style)
└─ NO → Need mixed fonts, weights, or line breaking?
├─ YES → Paragraph API (ParagraphBuilder)
└─ NO → Need text on a path?
├─ YES → <TextPath> component
└─ NO → <Text> with Glyphs for advanced positioning
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Using
createAnimatedComponentoruseAnimatedPropswith Skia components -- unnecessary; pass shared values directly as props - Using Reanimated's
interpolateColorfor Skia color animations -- produces wrong colors; useinterpolateColorsfrom@shopify/react-native-skia - Applying paint children (Shader, Blur) directly to Paragraph, Picture, or ImageSVG -- they ignore standard paint inheritance; use the
layerproperty - Using ScrollView with map() to render many Skia shapes -- use Atlas for batch rendering, or Picture for dynamic draw counts
- Performing heavy computations in Canvas children on JS thread -- offload to worklets or
useDerivedValueto keep animations on UI thread
Medium Priority Issues:
- Not setting
originon Group transforms and expecting center-based rotation -- default is top-left - RuntimeShader ignoring pixel density -- content appears blurry on high-DPI screens; wrap in a scale layer (scale by
PixelRatio.get(), then scale back by1/pd) - Creating new Path objects every render -- memoize or use
usePathValueto avoid garbage collection pressure - Hardcoding Canvas dimensions instead of using
onSizeshared value oruseCanvasSize-- breaks on different screen sizes - Missing
androidWarmup={true}for opaque canvases -- first frame renders white on Android
Gotchas & Edge Cases:
- Canvas uses its own React renderer -- React context from outside the Canvas is NOT available inside it
useCanvasSizereturns{ width: 0, height: 0 }on first render -- guard against zero dimensions- ImageSVG does not render
<text>elements, CSS styles, or<animate>-- preprocess SVGs with SVGO Skia.RuntimeEffect.Make()returns null if the shader has syntax errors -- always handle the null case- Atlas transforms are RSXform (rotation-scale-translation), not standard Transform2d -- use
Skia.RSXform()orSkia.RSXformFromRadians() - Path interpolation requires paths with the same number and types of commands -- mismatched paths crash
useImagereturns null while loading -- always guard rendering on image availability- Bundle size impact: +6MB iOS, +4MB Android, +2.9MB web (CanvasKit WASM)
- React Native >= 0.79 and React >= 19 required for current versions (v1.12.4 for RN <= 0.78)
- Canvas snapshot: use
makeImageSnapshotAsync()for images with textures,makeImageSnapshot()only for texture-free drawings useFontsreturns null while fonts load -- guard Paragraph rendering until fonts are ready
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST pass Reanimated shared values directly as Skia component props -- do NOT use createAnimatedComponent or useAnimatedProps)
(You MUST use interpolateColors from @shopify/react-native-skia for color animations -- Reanimated's interpolateColor uses a different internal color format and produces wrong results)
(You MUST use the layer property to apply paint effects to Paragraph, Picture, and ImageSVG components -- they do not follow standard paint inheritance rules)
(You MUST remember that Group transform origin defaults to top-left, not center -- set origin prop explicitly for center-based rotations)
Failure to follow these rules will produce broken color animations, invisible paint effects, and incorrectly positioned rotations.
</critical_reminders>
Files (skills)
-
examples
-
animations.md 7 KB
# React Native Skia - Animation Patterns > Reanimated integration, path interpolation, color animation, and Atlas animation. See [SKILL.md](../SKILL.md) for decision guidance. **Related:** [core.md](core.md) for basic shapes and canvas setup, [effects.md](effects.md) for animated shaders. --- ## Pattern 1: Shared Values as Props Pass Reanimated shared values directly to Skia component props. No `createAnimatedComponent` needed. ```tsx import { Canvas, Circle, RoundedRect } from "@shopify/react-native-skia"; import { useSharedValue, useDerivedValue, withRepeat, withTiming, } from "react-native-reanimated"; import { useEffect } from "react"; const DURATION_MS = 1500; const SIZE = 256; const MIN_RADIUS = 10; const MAX_RADIUS = 80; export function AnimatedShapes() { const progress = useSharedValue(0); useEffect(() => { progress.value = withRepeat( withTiming(1, { duration: DURATION_MS }), -1, true, ); }, []); // Derive values from the animated progress const radius = useDerivedValue( () => MIN_RADIUS + progress.value * (MAX_RADIUS - MIN_RADIUS), ); const x = useDerivedValue(() => progress.value * (SIZE - MAX_RADIUS * 2)); return ( <Canvas style={{ width: SIZE, height: SIZE }}> <Circle cx={128} cy={80} r={radius} color="cyan" /> <RoundedRect x={x} y={150} width={60} height={60} r={8} color="orange" /> </Canvas> ); } ``` **Why good:** `radius` and `x` are derived values that update on UI thread, zero JS bridge cost, 60 FPS guaranteed ```tsx // BAD: Using createAnimatedComponent (unnecessary with Skia) import Animated from "react-native-reanimated"; const AnimatedCircle = Animated.createAnimatedComponent(Circle); // WRONG ``` **Why bad:** Skia components natively accept shared values -- wrapping in Animated adds overhead and complexity for no benefit --- ## Pattern 2: Color Animation with interpolateColors Skia uses a different internal color format than Reanimated. Always use `interpolateColors` from Skia. ```tsx import { Canvas, Circle, interpolateColors } from "@shopify/react-native-skia"; import { useSharedValue, useDerivedValue, withRepeat, withTiming, } from "react-native-reanimated"; import { useEffect } from "react"; const CYCLE_DURATION = 3000; const COLORS = ["cyan", "magenta", "yellow", "cyan"]; const COLOR_STOPS = [0, 0.33, 0.66, 1]; export function ColorCycleCircle() { const progress = useSharedValue(0); useEffect(() => { progress.value = withRepeat( withTiming(1, { duration: CYCLE_DURATION }), -1, ); }, []); const color = useDerivedValue(() => interpolateColors(progress.value, COLOR_STOPS, COLORS), ); return ( <Canvas style={{ width: 200, height: 200 }}> <Circle cx={100} cy={100} r={60} color={color} /> </Canvas> ); } ``` **Why good:** `interpolateColors` from Skia handles Skia's internal color format correctly ```tsx // BAD: Using Reanimated's interpolateColor import { interpolateColor } from "react-native-reanimated"; // WRONG for Skia const color = useDerivedValue(() => interpolateColor(progress.value, [0, 1], ["red", "blue"]), ); ``` **Why bad:** Reanimated's `interpolateColor` uses a different internal format, produces wrong/flickering colors in Skia components --- ## Pattern 3: Path Interpolation Morph between multiple paths smoothly. Paths must have the same number and types of commands. ```tsx import { Canvas, Path, usePathInterpolation } from "@shopify/react-native-skia"; import { useSharedValue, withRepeat, withTiming, } from "react-native-reanimated"; import { useEffect } from "react"; const MORPH_DURATION = 2000; // All paths must have identical command structure (same moveTo, lineTo, etc. count) const TRIANGLE = "M 128 20 L 236 200 L 20 200 Z"; const DIAMOND = "M 128 20 L 236 128 L 128 236 Z"; const SQUARE = "M 40 40 L 216 40 L 216 216 Z"; export function MorphingShape() { const progress = useSharedValue(0); useEffect(() => { progress.value = withRepeat( withTiming(1, { duration: MORPH_DURATION }), -1, true, ); }, []); const path = usePathInterpolation( progress, [0, 0.5, 1], [TRIANGLE, DIAMOND, SQUARE], ); return ( <Canvas style={{ width: 256, height: 256 }}> <Path path={path} color="purple" /> </Canvas> ); } ``` **Why good:** `usePathInterpolation` handles the interpolation on UI thread, multiple keyframes supported **Gotcha:** paths with different command counts crash at runtime. For incompatible paths, preprocess with a library like Flubber. --- ## Pattern 4: Dynamic Paths with usePathValue For paths that change shape every frame (e.g., waveforms, trails), use `usePathValue` with a worklet callback. ```tsx import { Canvas, Path, usePathValue, Skia } from "@shopify/react-native-skia"; import { useClock } from "@shopify/react-native-skia"; const WAVE_POINTS = 50; const AMPLITUDE = 30; const CANVAS_WIDTH = 300; const CANVAS_HEIGHT = 200; export function AnimatedWave() { const clock = useClock(); const path = usePathValue((cPath) => { "worklet"; const t = clock.value / 1000; cPath.reset(); cPath.moveTo(0, CANVAS_HEIGHT / 2); for (let i = 0; i <= WAVE_POINTS; i++) { const x = (i / WAVE_POINTS) * CANVAS_WIDTH; const y = CANVAS_HEIGHT / 2 + Math.sin(x * 0.05 + t * 3) * AMPLITUDE; cPath.lineTo(x, y); } }); return ( <Canvas style={{ width: CANVAS_WIDTH, height: CANVAS_HEIGHT }}> <Path path={path} color="teal" style="stroke" strokeWidth={2} /> </Canvas> ); } ``` **Why good:** worklet runs on UI thread, path updated every frame without JS thread involvement, `useClock` provides elapsed time --- ## Pattern 5: Animated Atlas with useRSXformBuffer Animate thousands of sprites with worklet-based transforms at near-zero cost. ```tsx import { Canvas, Atlas, useImage, rect, useRSXformBuffer, } from "@shopify/react-native-skia"; import { useClock } from "@shopify/react-native-skia"; const SPRITE_COUNT = 100; const SPRITE_SIZE = 16; const CANVAS_SIZE = 256; export function AnimatedSprites() { const texture = useImage(require("./particle.png")); const clock = useClock(); const sprites = Array.from({ length: SPRITE_COUNT }, () => rect(0, 0, SPRITE_SIZE, SPRITE_SIZE), ); const transforms = useRSXformBuffer(SPRITE_COUNT, (val, i) => { "worklet"; const t = clock.value / 1000; const angle = (i / SPRITE_COUNT) * Math.PI * 2 + t; const radius = 60 + Math.sin(t + i * 0.1) * 30; const x = CANVAS_SIZE / 2 + Math.cos(angle) * radius; const y = CANVAS_SIZE / 2 + Math.sin(angle) * radius; const scale = 0.5 + Math.sin(t * 2 + i) * 0.3; val.set(Math.cos(angle) * scale, Math.sin(angle) * scale, x, y); }); if (!texture) return null; return ( <Canvas style={{ width: CANVAS_SIZE, height: CANVAS_SIZE }}> <Atlas image={texture} sprites={sprites} transforms={transforms} /> </Canvas> ); } ``` **Why good:** `useRSXformBuffer` runs worklet per sprite per frame on UI thread, single draw call for all 100 sprites, `val.set(scos, ssin, tx, ty)` is the RSXform interface -
core.md 8.1 KB
# React Native Skia - Core Patterns > Canvas setup, shapes, paths, paint, groups, and images. See [SKILL.md](../SKILL.md) for decision guidance and red flags. **Prerequisites:** Familiarity with React Native and basic 2D graphics concepts. --- ## Pattern 1: Canvas with Reactive Sizing ```tsx import { Canvas, Circle, useCanvasSize } from "@shopify/react-native-skia"; import { useSharedValue } from "react-native-reanimated"; import type { SharedValue } from "react-native-reanimated"; // onSize: UI thread reactive dimensions (preferred for animations) export function ReactiveCanvas() { const size = useSharedValue({ width: 0, height: 0 }); return ( <Canvas style={{ flex: 1 }} onSize={size}> <CenteredCircle size={size} /> </Canvas> ); } // useCanvasSize: JS thread dimensions (for layout calculations) function CenteredCircle({ size, }: { size: SharedValue<{ width: number; height: number }>; }) { // Derive center from shared value for UI thread animation return <Circle cx={128} cy={128} r={40} color="cyan" />; } ``` **Why good:** `onSize` updates on UI thread without JS bridge, `useCanvasSize` available for JS-thread calculations --- ## Pattern 2: All Shape Primitives ```tsx import { Canvas, Circle, Rect, RoundedRect, Line, Oval, Points, DiffRect, rrect, rect, } from "@shopify/react-native-skia"; const CANVAS_SIZE = 300; const CORNER_RADIUS = 12; const STROKE_WIDTH = 2; export function ShapeShowcase() { return ( <Canvas style={{ width: CANVAS_SIZE, height: CANVAS_SIZE }}> {/* Filled shapes */} <Circle cx={50} cy={50} r={30} color="cyan" /> <Rect x={100} y={20} width={60} height={60} color="red" /> <RoundedRect x={180} y={20} width={60} height={60} r={CORNER_RADIUS} color="blue" /> <Oval x={20} y={100} width={80} height={50} color="green" /> {/* Stroked shapes */} <Circle cx={180} cy={130} r={30} color="purple" style="stroke" strokeWidth={STROKE_WIDTH} /> <Line p1={{ x: 0, y: 200 }} p2={{ x: 300, y: 200 }} color="gray" strokeWidth={1} /> {/* Points */} <Points points={[ { x: 50, y: 250 }, { x: 100, y: 230 }, { x: 150, y: 260 }, ]} mode="polygon" color="orange" style="stroke" strokeWidth={STROKE_WIDTH} /> {/* DiffRect: outer rect minus inner rect (ring/frame shape) */} <DiffRect outer={rrect(rect(180, 200, 80, 80), CORNER_RADIUS, CORNER_RADIUS)} inner={rrect(rect(195, 215, 50, 50), 8, 8)} color="pink" /> </Canvas> ); } ``` --- ## Pattern 3: Path Operations ```tsx import { Canvas, Path, DashPathEffect, Skia } from "@shopify/react-native-skia"; const STROKE_WIDTH = 3; // Declarative: SVG path string export function DeclarativePath() { return ( <Canvas style={{ width: 256, height: 256 }}> <Path path="M 10 80 Q 95 10 180 80 T 350 80" color="purple" style="stroke" strokeWidth={STROKE_WIDTH} strokeCap="round" /> </Canvas> ); } // Imperative: dynamic construction export function ImperativePath() { const path = Skia.Path.Make(); path.moveTo(10, 80); path.quadTo(95, 10, 180, 80); path.cubicTo(200, 120, 250, 50, 300, 80); path.close(); return ( <Canvas style={{ width: 320, height: 160 }}> <Path path={path} color="teal" /> </Canvas> ); } // Path with dash effect export function DashedPath() { return ( <Canvas style={{ width: 256, height: 256 }}> <Path path="M 10 128 L 246 128" color="black" style="stroke" strokeWidth={2} > {/* DashPathEffect: [dashLength, gapLength] */} <DashPathEffect intervals={[10, 5]} /> </Path> </Canvas> ); } ``` **Why good:** declarative SVG strings for static paths, imperative API for data-driven shapes, path effects compose as children --- ## Pattern 4: Paint Composition (Multiple Fills/Strokes) A single shape can have multiple paint layers by adding `<Paint>` children. This creates effects like fill + stroke, or fill + inner shadow. ```tsx import { Canvas, Circle, Paint } from "@shopify/react-native-skia"; const OUTER_STROKE = 3; export function MultiPaintCircle() { return ( <Canvas style={{ width: 200, height: 200 }}> <Circle cx={100} cy={100} r={60}> {/* First paint: blue fill */} <Paint color="blue" /> {/* Second paint: white stroke on top */} <Paint color="white" style="stroke" strokeWidth={OUTER_STROKE} /> </Circle> </Canvas> ); } ``` **Why good:** multiple paints on one shape without duplicating the shape, paints render in order (fill first, then stroke) --- ## Pattern 5: Group Transforms and Clipping ```tsx import { Canvas, Group, Rect, Circle, Skia } from "@shopify/react-native-skia"; const ROTATION_DEGREES = 45; const ROTATION_RADIANS = (ROTATION_DEGREES * Math.PI) / 180; // Transform with explicit origin (default is top-left, NOT center) export function RotatedGroup() { return ( <Canvas style={{ width: 256, height: 256 }}> <Group transform={[{ rotate: ROTATION_RADIANS }]} origin={{ x: 128, y: 128 }} > <Rect x={88} y={88} width={80} height={80} color="red" /> </Group> </Canvas> ); } // Clipping with a path export function ClippedGroup() { const clipPath = Skia.Path.Make(); clipPath.addCircle(128, 128, 80); return ( <Canvas style={{ width: 256, height: 256 }}> <Group clip={clipPath}> <Rect x={0} y={0} width={256} height={256} color="blue" /> {/* Only the portion inside the circle clip is visible */} </Group> </Canvas> ); } // Inverted clip: show everything OUTSIDE the clip region export function InvertedClip() { return ( <Canvas style={{ width: 256, height: 256 }}> <Group clip={{ x: 80, y: 80, width: 96, height: 96 }} invertClip> <Rect x={0} y={0} width={256} height={256} color="green" /> </Group> </Canvas> ); } ``` **Why good:** explicit origin prevents rotation surprise (default top-left), clip accepts rect/rrect/path, invertClip for masking effects --- ## Pattern 6: Images and FitBox ```tsx import { Canvas, Image, useImage, FitBox, Circle, rect, } from "@shopify/react-native-skia"; export function ImageExample() { const image = useImage(require("./photo.png")); if (!image) return null; // Always guard: useImage returns null while loading return ( <Canvas style={{ width: 256, height: 256 }}> {/* fit modes: contain, cover, fill, fitWidth, fitHeight, none */} <Image image={image} x={0} y={0} width={256} height={256} fit="cover" /> </Canvas> ); } // FitBox: auto-scale content to fit a destination rectangle export function FitBoxExample() { const SRC = rect(0, 0, 100, 100); // Original coordinate space const DST = rect(0, 0, 256, 256); // Target display size return ( <Canvas style={{ width: 256, height: 256 }}> <FitBox src={SRC} dst={DST} fit="contain"> {/* These coordinates are in the 100x100 source space */} <Circle cx={50} cy={50} r={40} color="cyan" /> </FitBox> </Canvas> ); } ``` **Why good:** FitBox auto-scales children from source to destination coordinates, image `fit` modes match React Native Image behavior --- ## Pattern 7: Layer Property for Non-Standard Components Paragraph, Picture, and ImageSVG do not inherit paint from parent Groups. Use the `layer` property to apply effects. ```tsx import { Canvas, Group, Paragraph, Paint, Blur } from "@shopify/react-native-skia"; // BAD: Blur on Group does NOT apply to Paragraph <Group> <Blur blur={4} /> <Paragraph paragraph={para} x={0} y={0} width={300} /> </Group> // GOOD: Use layer property <Group layer={<Paint><Blur blur={4} /></Paint>}> <Paragraph paragraph={para} x={0} y={0} width={300} /> </Group> ``` **Why bad:** Paragraph ignores paint children on parent Group, blur silently has no effect **Why good:** `layer` creates a bitmap rendering layer, effects apply to the entire group output including Paragraph -
effects.md 8.4 KB
# React Native Skia - Effects Patterns > Image filters, backdrop blur, shaders, and RuntimeShader. See [SKILL.md](../SKILL.md) for decision guidance. **Related:** [core.md](core.md) for shapes and paint, [animations.md](animations.md) for animating shader uniforms. --- ## Pattern 1: Image Filters (Blur, Shadow, ColorMatrix) Image filters compose as children of shapes or Groups. Multiple filters can be nested. ```tsx import { Canvas, Image, Rect, Blur, Shadow, ColorMatrix, useImage, } from "@shopify/react-native-skia"; const BLUR_SIGMA = 8; const SHADOW_DX = 4; const SHADOW_DY = 4; const SHADOW_BLUR = 6; // Blur filter export function BlurredImage() { const image = useImage(require("./photo.png")); if (!image) return null; return ( <Canvas style={{ width: 256, height: 256 }}> <Image image={image} fit="cover" x={0} y={0} width={256} height={256}> <Blur blur={BLUR_SIGMA} mode="clamp" /> </Image> </Canvas> ); } // Shadow filter export function ShadowedRect() { return ( <Canvas style={{ width: 256, height: 256 }}> <Rect x={40} y={40} width={160} height={160} color="white"> <Shadow dx={SHADOW_DX} dy={SHADOW_DY} blur={SHADOW_BLUR} color="rgba(0,0,0,0.4)" /> </Rect> </Canvas> ); } // Composed: Blur + ColorMatrix (grayscale) const GRAYSCALE_MATRIX = [ 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0, 0, 0, 1, 0, ]; export function GrayscaleBlurredImage() { const image = useImage(require("./photo.png")); if (!image) return null; return ( <Canvas style={{ width: 256, height: 256 }}> <Image image={image} fit="cover" x={0} y={0} width={256} height={256}> <Blur blur={4} mode="clamp"> <ColorMatrix matrix={GRAYSCALE_MATRIX} /> </Blur> </Image> </Canvas> ); } ``` **Why good:** filters compose by nesting (inner applied first), Blur `mode` controls edge behavior ("clamp", "repeat", "mirror", "decal") --- ## Pattern 2: Backdrop Blur and Backdrop Filter Apply effects to content behind a clipping region (like CSS `backdrop-filter`). BackdropBlur is a convenience wrapper; BackdropFilter accepts any image filter child. ```tsx import { Canvas, Image, BackdropBlur, BackdropFilter, Fill, ColorMatrix, useImage, } from "@shopify/react-native-skia"; const BACKDROP_BLUR_RADIUS = 10; export function GlassMorphism() { const image = useImage(require("./background.png")); if (!image) return null; return ( <Canvas style={{ width: 300, height: 400 }}> {/* Background image */} <Image image={image} fit="cover" x={0} y={0} width={300} height={400} /> {/* Frosted glass panel in bottom half */} <BackdropBlur blur={BACKDROP_BLUR_RADIUS} clip={{ x: 20, y: 200, width: 260, height: 180 }} > <Fill color="rgba(255, 255, 255, 0.2)" /> </BackdropBlur> </Canvas> ); } // BackdropFilter with custom ColorMatrix const SEPIA_MATRIX = [ 0.393, 0.769, 0.189, 0, 0, 0.349, 0.686, 0.168, 0, 0, 0.272, 0.534, 0.131, 0, 0, 0, 0, 0, 1, 0, ]; export function SepiaBackdrop() { return ( <Canvas style={{ width: 256, height: 256 }}> {/* Content behind the filter */} <Fill color="cyan" /> {/* Apply sepia to bottom half */} <BackdropFilter clip={{ x: 0, y: 128, width: 256, height: 128 }}> <ColorMatrix matrix={SEPIA_MATRIX} /> </BackdropFilter> </Canvas> ); } ``` **Why good:** BackdropBlur/BackdropFilter apply to content already drawn on canvas (behind the clip), not to their own children --- ## Pattern 3: Custom SkSL Shader Write GPU shaders with `Skia.RuntimeEffect.Make()`. Uniforms are passed as a plain object. Animate uniforms with shared values. ```tsx import { Canvas, Fill, Shader, Skia } from "@shopify/react-native-skia"; import { useSharedValue, useDerivedValue, withRepeat, withTiming, } from "react-native-reanimated"; import { useEffect } from "react"; const ANIMATION_DURATION = 4000; const CANVAS_SIZE = 256; const GRADIENT_SHADER = ` uniform float2 iResolution; uniform float iTime; vec4 main(vec2 pos) { vec2 uv = pos / iResolution; float r = 0.5 + 0.5 * sin(iTime + uv.x * 6.28); float g = 0.5 + 0.5 * sin(iTime * 1.3 + uv.y * 6.28); float b = 0.5 + 0.5 * sin(iTime * 0.7 + (uv.x + uv.y) * 3.14); return vec4(r, g, b, 1.0); } `; const effect = Skia.RuntimeEffect.Make(GRADIENT_SHADER); export function AnimatedGradientShader() { const time = useSharedValue(0); useEffect(() => { time.value = withRepeat( withTiming(6.28, { duration: ANIMATION_DURATION }), -1, ); }, []); if (!effect) return null; // Shader compilation failed return ( <Canvas style={{ width: CANVAS_SIZE, height: CANVAS_SIZE }}> <Fill> <Shader source={effect} uniforms={{ iResolution: [CANVAS_SIZE, CANVAS_SIZE], iTime: time }} /> </Fill> </Canvas> ); } ``` **Why good:** uniforms accept shared values directly for animation, `iResolution` as `float2` matches SkSL's `uniform float2`, null check handles shader compilation errors --- ## Pattern 4: RuntimeShader as Image Filter RuntimeShader processes existing canvas content as a shader uniform. The filtered image is available as `uniform shader image` implicitly. ```tsx import { Canvas, Image, RuntimeShader, Skia, useImage, } from "@shopify/react-native-skia"; const PIXELATE_SHADER = ` uniform shader image; uniform float2 resolution; uniform float pixelSize; vec4 main(vec2 pos) { vec2 uv = pos / resolution; vec2 blockUV = floor(uv * pixelSize) / pixelSize; return image.eval(blockUV * resolution); } `; const pixelateEffect = Skia.RuntimeEffect.Make(PIXELATE_SHADER); const PIXEL_COUNT = 32; const CANVAS_SIZE = 256; export function PixelatedImage() { const photo = useImage(require("./photo.png")); if (!photo || !pixelateEffect) return null; return ( <Canvas style={{ width: CANVAS_SIZE, height: CANVAS_SIZE }}> <Image image={photo} fit="cover" x={0} y={0} width={CANVAS_SIZE} height={CANVAS_SIZE} > <RuntimeShader source={pixelateEffect} uniforms={{ resolution: [CANVAS_SIZE, CANVAS_SIZE], pixelSize: PIXEL_COUNT, }} /> </Image> </Canvas> ); } ``` **Why good:** `image.eval(xy)` samples the source image at given coordinates, uniforms control the effect parameters **Gotcha:** RuntimeShader does not account for pixel density scaling. On high-DPI screens, content appears blurry. Fix by wrapping in a Group with `transform={[{ scale: pd }]}` and rendering into a canvas scaled by `1/pd`. --- ## Pattern 5: Shader with Child Shaders Compose shaders by declaring `uniform shader` and sampling with `.eval(xy)`. ```tsx import { Canvas, Fill, Shader, ImageShader, Skia, useImage, } from "@shopify/react-native-skia"; const SWIRL_SHADER = ` uniform shader image; uniform float2 center; uniform float radius; uniform float angle; vec4 main(vec2 pos) { vec2 d = pos - center; float dist = length(d); if (dist < radius) { float percent = (radius - dist) / radius; float theta = percent * percent * angle; float cosT = cos(theta); float sinT = sin(theta); d = vec2(d.x * cosT - d.y * sinT, d.x * sinT + d.y * cosT); } return image.eval(d + center); } `; const swirlEffect = Skia.RuntimeEffect.Make(SWIRL_SHADER); const SWIRL_ANGLE = 3.14; const SWIRL_RADIUS = 100; export function SwirlImage() { const photo = useImage(require("./photo.png")); if (!photo || !swirlEffect) return null; return ( <Canvas style={{ width: 256, height: 256 }}> <Fill> <Shader source={swirlEffect} uniforms={{ center: [128, 128], radius: SWIRL_RADIUS, angle: SWIRL_ANGLE, }} > {/* Child shader: image texture available as `uniform shader image` */} <ImageShader image={photo} fit="cover" x={0} y={0} width={256} height={256} /> </Shader> </Fill> </Canvas> ); } ``` **Why good:** child shader automatically binds to the first `uniform shader` declaration, `.eval(xy)` samples the child at computed coordinates, enables complex image distortion effects -
text-and-media.md 7.9 KB
# React Native Skia - Text and Media Patterns > Paragraph API, SVG rendering, Picture recording, and offscreen rendering. See [SKILL.md](../SKILL.md) for decision guidance. **Related:** [core.md](core.md) for basic shapes, [effects.md](effects.md) for applying effects to Paragraphs via `layer`. --- ## Pattern 1: Paragraph with Mixed Styles The Paragraph API handles rich text with multiple fonts, weights, and alignment. Build paragraphs with `ParagraphBuilder`. ```tsx import { Canvas, Paragraph, Skia, useFonts, TextAlign, FontStyle, } from "@shopify/react-native-skia"; const PARAGRAPH_WIDTH = 280; const TITLE_SIZE = 28; const BODY_SIZE = 16; const LINE_HEIGHT_MULTIPLIER = 1.5; export function RichTextParagraph() { const fonts = useFonts({ Roboto: [ require("./fonts/Roboto-Regular.ttf"), require("./fonts/Roboto-Bold.ttf"), require("./fonts/Roboto-Italic.ttf"), ], }); if (!fonts) return null; // Fonts loading -- always guard const paraStyle = { textAlign: TextAlign.Left, maxLines: 10, ellipsis: "...", }; const para = Skia.ParagraphBuilder.Make(paraStyle, fonts) .pushStyle({ fontSize: TITLE_SIZE, fontFamilies: ["Roboto"], fontStyle: FontStyle.Bold, color: Skia.Color("black"), }) .addText("Welcome to Skia\n") .pop() .pushStyle({ fontSize: BODY_SIZE, fontFamilies: ["Roboto"], color: Skia.Color("#444"), heightMultiplier: LINE_HEIGHT_MULTIPLIER, }) .addText("React Native Skia provides a ") .pushStyle({ fontSize: BODY_SIZE, fontFamilies: ["Roboto"], fontStyle: FontStyle.Italic, color: Skia.Color("#444"), }) .addText("powerful") .pop() .addText( " Paragraph API for rich text layout with automatic line breaking.", ) .pop() .build(); para.layout(PARAGRAPH_WIDTH); return ( <Canvas style={{ width: PARAGRAPH_WIDTH, height: para.getHeight() + 20 }}> <Paragraph paragraph={para} x={0} y={10} width={PARAGRAPH_WIDTH} /> </Canvas> ); } ``` **Why good:** mixed bold/italic/regular in one text block, automatic line breaking, measurable height for dynamic canvas sizing **Key methods after `build()`:** - `para.layout(width)` -- compute layout for given width (must call before rendering) - `para.getHeight()` -- total paragraph height after layout - `para.getLongestLine()` -- width of the longest line (useful for centering) --- ## Pattern 2: SVG Rendering Render SVG files or strings with the `ImageSVG` component. SVGs use Skia's SVG module and have specific limitations. ```tsx import { Canvas, ImageSVG, useSVG, Skia } from "@shopify/react-native-skia"; // From file export function SVGFromFile() { const svg = useSVG(require("./icon.svg")); if (!svg) return null; // Loading -- always guard return ( <Canvas style={{ width: 200, height: 200 }}> <ImageSVG svg={svg} x={0} y={0} width={200} height={200} /> </Canvas> ); } // From string export function SVGFromString() { const svg = Skia.SVG.MakeFromString(` <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"> <circle cx="50" cy="50" r="40" fill="coral" /> <rect x="30" y="30" width="40" height="40" fill="white" opacity="0.5" /> </svg> `); if (!svg) return null; return ( <Canvas style={{ width: 200, height: 200 }}> <ImageSVG svg={svg} x={0} y={0} width={200} height={200} /> </Canvas> ); } ``` **SVG limitations (not supported):** - `<text>` elements - CSS styles (preprocess with SVGO to convert to attributes) - RGBA color syntax (use `rgb()` + `fill-opacity`/`stroke-opacity`) - `<animate>`, `<foreignObject>`, `<script>`, `<view>` - Gradient `xlink:href` (deprecated) **Why good:** `useSVG` handles async loading, `Skia.SVG.MakeFromString` for inline SVGs --- ## Pattern 3: Picture Recording Pictures record drawing operations and replay them. Useful for dynamic draw counts (e.g., trails, particle effects) and sharing drawings between canvases. ```tsx import { Canvas, Picture, Skia, Group, Paint, Blur, } from "@shopify/react-native-skia"; const TRAIL_LENGTH = 20; const TRAIL_RADIUS = 5; // Record dynamic number of drawing operations export function TrailEffect({ points, }: { points: Array<{ x: number; y: number }>; }) { const recorder = Skia.PictureRecorder(); const canvas = recorder.beginRecording({ x: 0, y: 0, width: 256, height: 256, }); // Draw variable number of circles (can't do this with JSX without changing component count) const visiblePoints = points.slice(-TRAIL_LENGTH); visiblePoints.forEach((point, i) => { const opacity = (i + 1) / visiblePoints.length; const paint = Skia.Paint(); paint.setColor(Skia.Color(`rgba(0, 200, 255, ${opacity})`)); canvas.drawCircle(point.x, point.y, TRAIL_RADIUS, paint); }); const picture = recorder.finishRecordingAsPicture(); return ( <Canvas style={{ width: 256, height: 256 }}> {/* Apply effects via layer since Picture doesn't inherit paint */} <Group layer={ <Paint> <Blur blur={2} /> </Paint> } > <Picture picture={picture} /> </Group> </Canvas> ); } ``` **When to use Picture:** - Variable number of draw operations per frame (trails, particles) - Reusing the same drawing in multiple locations - Immediate-mode drawing with imperative Skia API - Serializing drawings for debugging (`picture.serialize()`) **Why good:** Picture operates in immediate mode (variable draw count), immutable after recording (safe to share), can be serialized for debugging --- ## Pattern 4: Canvas Snapshots Capture canvas content as an image for sharing, saving, or processing. ```tsx import { Canvas, Circle, Rect, type SkiaView, } from "@shopify/react-native-skia"; import { useRef, useCallback } from "react"; export function SnapshotExample() { const canvasRef = useRef<SkiaView>(null); const captureSnapshot = useCallback(async () => { if (!canvasRef.current) return; // Use async version for drawings that include images/textures const image = await canvasRef.current.makeImageSnapshotAsync(); if (!image) return; // Convert to base64 for sharing const data = image.encodeToBase64(); // Use data as needed (save, share, upload) }, []); return ( <Canvas ref={canvasRef} style={{ width: 256, height: 256 }}> <Rect x={0} y={0} width={256} height={256} color="white" /> <Circle cx={128} cy={128} r={60} color="blue" /> </Canvas> ); } ``` **Key distinction:** - `makeImageSnapshotAsync()` -- use when canvas has images, shaders, or textures (promise-based) - `makeImageSnapshot(rect?)` -- use for texture-free drawings (synchronous, optional crop rect) --- ## Pattern 5: Offscreen Rendering (Headless) Generate images without displaying a Canvas. Useful for thumbnails, export, or server-side rendering. ```tsx import { LoadSkiaWeb, makeOffscreenSurface, drawOffscreen, } from "@shopify/react-native-skia/lib/commonjs/headless"; import { Circle, Fill } from "@shopify/react-native-skia"; const SURFACE_WIDTH = 512; const SURFACE_HEIGHT = 512; export async function generateThumbnail(): Promise<string> { // 1. Initialize CanvasKit (required for headless) await LoadSkiaWeb(); // 2. Create offscreen surface const surface = makeOffscreenSurface(SURFACE_WIDTH, SURFACE_HEIGHT); // 3. Draw using React Native Skia components const image = drawOffscreen( surface, <> <Fill color="white" /> <Circle cx={256} cy={256} r={120} color="blue" /> </>, ); // 4. Encode to base64 return image.encodeToBase64(); } ``` **When to use offscreen rendering:** - Generating thumbnails or previews without a visible Canvas - Server-side image generation (Node.js with CanvasKit) - Pre-rendering complex scenes for later display **Import path:** headless mode requires CommonJS imports: `@shopify/react-native-skia/lib/commonjs/headless`
-
-
reference.md 5.8 KB
# React Native Skia Quick Reference > Decision frameworks, paint properties, SkSL types, and checklists. See [SKILL.md](SKILL.md) for patterns, red flags, and critical rules. --- ## Paint Properties Reference | Property | Type | Default | Description | | ------------- | ------------------------------- | ----------- | -------------------- | | `color` | `string \| Color` | `"black"` | Fill or stroke color | | `style` | `"fill" \| "stroke"` | `"fill"` | Drawing mode | | `strokeWidth` | `number` | `1` | Stroke width | | `strokeJoin` | `"miter" \| "round" \| "bevel"` | `"miter"` | Stroke join style | | `strokeCap` | `"butt" \| "round" \| "square"` | `"butt"` | Stroke cap style | | `strokeMiter` | `number` | `4` | Miter limit | | `opacity` | `number` | `1` | Opacity (0-1) | | `blendMode` | `BlendMode` | `"srcOver"` | How pixels compose | | `antiAlias` | `boolean` | `true` | Anti-aliasing | **Paint children (complex effects):** Shader, ImageFilter (Blur, Shadow, Morphology, Offset, DisplacementMap, RuntimeShader), ColorFilter, MaskFilter, PathEffect --- ## Canvas Props | Prop | Type | Purpose | | --------------- | ------------------- | -------------------------------------- | | `style` | `ViewStyle` | Standard RN view styling | | `ref` | `Ref<SkiaView>` | Access for snapshots | | `onSize` | `SharedValue<Size>` | Reactive canvas dimensions (UI thread) | | `androidWarmup` | `boolean` | Avoids white first frame on Android | **Snapshot methods:** - `makeImageSnapshotAsync()` -- for drawings with textures (images, shaders) - `makeImageSnapshot(rect?)` -- for texture-free drawings (shapes, paths only) --- ## SkSL Uniform Types | Type | Description | | ---------------------------------- | ----------------------------------------------- | | `float` | Single float | | `float2`, `float3`, `float4` | Float vectors (aliases: `vec2`, `vec3`, `vec4`) | | `int`, `int2`, `int3`, `int4` | Integer types | | `float2x2`, `float3x3`, `float4x4` | Matrices | | `uniform shader` | Child shader (sampled via `.eval(xy)`) | Arrays supported: `uniform float3 colors[12]` **Key SkSL differences from GLSL:** - Use `.eval(xy)` instead of `sample()` / `texture()` for child shaders - Entry point: `vec4 main(vec2 pos)` -- `pos` is in canvas coordinates - `half4` and `vec4` both work for color output --- ## RSXform Reference RSXform encodes scale, rotation, and translation in 4 floats: | Factory | Parameters | Use Case | | --------------------------------------------------------- | ------------------------ | ------------------------------ | | `Skia.RSXform(scos, ssin, tx, ty)` | Pre-computed cos/sin | When you have raw trig values | | `Skia.RSXformFromRadians(scale, radians, tx, ty, px, py)` | Angle in radians + pivot | Rotation around a center point | **Identity:** `Skia.RSXform(1, 0, tx, ty)` -- no rotation, no scale, just translate --- ## Image Loading Hooks | Hook | Input | Returns | Notes | | ------------------- | ------------------------- | -------------------------------- | -------------------------- | | `useImage(source)` | `require()` or URL | `SkImage \| null` | Returns null while loading | | `useSVG(source)` | `require()` or URL | `SkSVG \| null` | Returns null while loading | | `useFonts(fontMap)` | `{ family: [require()] }` | `SkTypefaceFontProvider \| null` | Required for Paragraph | --- ## Animation Hooks | Hook | Purpose | Key Detail | | --------------------------------------- | ------------------------ | ------------------------------------------- | | `usePathInterpolation(progress, paths)` | Morph between paths | Paths must have same command count/types | | `usePathValue(callback)` | Efficient dynamic paths | Callback must include `"worklet"` directive | | `useClock()` | Elapsed time in ms | Independent of shared values | | `useRSXformBuffer(count, callback)` | Animate Atlas transforms | Worklet-based, near-zero cost | | `useRectBuffer(count, callback)` | Animate Atlas sprites | For sprite sheet animations | --- ## Version Compatibility | @shopify/react-native-skia | React Native | React | | -------------------------- | ------------ | ----- | | >= 1.13 (v2.x) | >= 0.79 | >= 19 | | <= 1.12.4 | <= 0.78 | <= 18 | **Bundle size impact:** iOS +6MB, Android +4MB, Web +2.9MB (CanvasKit WASM) --- ## Components That Need `layer` for Paint Effects These components use Skia's own rendering modules and do not inherit paint from parent Groups: - `<Paragraph>` -- use `layer` prop with Paint for text effects - `<Picture>` -- use `layer` prop with Paint for recorded drawing effects - `<ImageSVG>` -- use `layer` prop with Paint for SVG effects ```tsx // Apply blur to a Paragraph via layer <Group layer={ <Paint> <Blur blur={2} /> </Paint> } > <Paragraph paragraph={para} x={0} y={0} width={300} /> </Group> ``` -
SKILL.md 19 KB
--- name: mobile-animation-skia description: React Native Skia GPU-accelerated 2D graphics - Canvas, declarative drawing, shaders, image filters, Paragraph text, Atlas batch rendering, Reanimated animations --- # React Native Skia Patterns > **Quick Guide:** Use `@shopify/react-native-skia` for GPU-accelerated 2D drawing in React Native. The Canvas component hosts a separate React renderer. Drawing primitives (Circle, Rect, Path, Image) compose declaratively. Paint attributes cascade through Groups. Animations use Reanimated shared values passed directly as props -- no `createAnimatedComponent` needed. Use `interpolateColors` from Skia for color animations (not Reanimated's `interpolateColor`). Use Atlas for batch rendering thousands of sprites. Use Paragraph for rich text layout. Group transforms default to top-left origin, not center. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST pass Reanimated shared values directly as Skia component props -- do NOT use createAnimatedComponent or useAnimatedProps)** **(You MUST use `interpolateColors` from `@shopify/react-native-skia` for color animations -- Reanimated's `interpolateColor` uses a different internal color format and produces wrong results)** **(You MUST use the `layer` property to apply paint effects to Paragraph, Picture, and ImageSVG components -- they do not follow standard paint inheritance rules)** **(You MUST remember that Group transform origin defaults to top-left, not center -- set `origin` prop explicitly for center-based rotations)** </critical_requirements> --- **Auto-detection:** react-native-skia, @shopify/react-native-skia, Canvas, Skia.Path, Skia.Paint, Circle, Rect, Path, Group, Paint, ImageSVG, BackdropBlur, BackdropFilter, RuntimeShader, Atlas, Paragraph, ParagraphBuilder, usePathInterpolation, useClock, useRSXformBuffer, SkSL, image filter, shader, offscreen, Picture, FitBox **When to use:** - Drawing custom 2D graphics (charts, diagrams, custom shapes) - Applying GPU-accelerated blur, shadow, or color filter effects - Animating paths, shapes, or shader uniforms at 60 FPS - Rendering rich text with mixed fonts using Paragraph API - Batch rendering sprites/tiles with Atlas - Creating custom image filters with SkSL shaders - Generating images offscreen (thumbnails, exports) **When NOT to use:** - Standard UI layouts (use regular React Native views) - Simple static images (use `<Image>` from React Native) - 3D graphics (Skia is 2D only -- use a 3D solution) - Text-only screens (use React Native `<Text>`) **Key patterns covered:** - Canvas setup and threading model - Declarative drawing with shapes, paths, and images - Paint inheritance and composition through Groups - Reanimated integration (shared values as props, color interpolation) - Image filters (Blur, Shadow, RuntimeShader) and backdrop filters - Paragraph text layout with custom fonts - Atlas batch rendering for sprites and tiles - SkSL custom shaders with uniforms - Picture recording for dynamic drawing operations - SVG rendering with limitations **Detailed Resources:** - [examples/core.md](examples/core.md) - Canvas, shapes, paths, paint, groups, images - [examples/animations.md](examples/animations.md) - Reanimated integration, path interpolation, color animation, Atlas animation - [examples/effects.md](examples/effects.md) - Image filters, backdrop blur, shaders, RuntimeShader - [examples/text-and-media.md](examples/text-and-media.md) - Paragraph API, SVG rendering, Picture recording, offscreen rendering - [reference.md](reference.md) - Decision frameworks, paint property reference, SkSL types --- <philosophy> ## Philosophy React Native Skia brings Skia (the graphics engine behind Chrome, Android, and Flutter) to React Native. It provides a **separate React renderer** inside the Canvas component -- you write JSX, but it renders to a GPU-accelerated Skia surface, not native views. **Core mental model:** 1. **Canvas is the boundary** -- everything inside `<Canvas>` uses Skia's renderer, everything outside is regular React Native 2. **Declarative by default** -- compose shapes, paints, and effects as JSX children; use imperative API (`Skia.Path()`, `Skia.Paint()`) only when you need dynamic construction 3. **Paint cascades through Groups** -- a Group's color, opacity, shader, or filter applies to all children unless overridden (exception: Paragraph, Picture, and ImageSVG need the `layer` property) 4. **Animations run on UI thread** -- pass Reanimated shared values directly as props; Skia reads them on the UI thread with zero bridge cost 5. **Hybrid architecture** -- use Skia canvases only where custom visuals are needed; standard views handle layout and navigation **When to reach for Skia:** - Custom graphics that native views cannot express (gradients on paths, blur effects, charts) - Performance-critical animations that must run on the GPU - Batch rendering (Atlas for thousands of sprites) - Rich text layout needing mixed fonts or decorations (Paragraph) - Custom image processing (RuntimeShader with SkSL) **When NOT to reach for Skia:** - Standard UI (buttons, lists, forms) -- native views are simpler and more accessible - Simple images -- React Native's `<Image>` is sufficient - Accessibility-critical content -- Skia canvas elements are not accessible to screen readers; overlay native views for accessibility </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Canvas and Basic Shapes Canvas is the root Skia drawing surface. It behaves like a regular React Native view (accepts `style`), but hosts its own React renderer internally. ```tsx import { Canvas, Circle, Rect, RoundedRect, Line, } from "@shopify/react-native-skia"; const CANVAS_SIZE = 256; const CIRCLE_RADIUS = 50; <Canvas style={{ width: CANVAS_SIZE, height: CANVAS_SIZE }}> <Circle cx={128} cy={128} r={CIRCLE_RADIUS} color="cyan" /> <Rect x={10} y={10} width={100} height={80} color="red" /> <RoundedRect x={10} y={120} width={100} height={80} r={16} color="blue" /> <Line p1={{ x: 0, y: 0 }} p2={{ x: 256, y: 256 }} color="green" strokeWidth={2} style="stroke" /> </Canvas>; ``` **Why good:** declarative JSX, paint properties (color, style, strokeWidth) applied directly as props See [examples/core.md](examples/core.md) for Canvas props (`onSize`, `androidWarmup`, snapshots) and all shape primitives. --- ### Pattern 2: Paint Inheritance and Groups Groups apply paint attributes (color, opacity, shaders, filters) to all children. Groups also provide transforms, clipping, and z-ordering. ```tsx import { Canvas, Group, Circle, Rect } from "@shopify/react-native-skia"; <Canvas style={{ width: 256, height: 256 }}> <Group color="blue" opacity={0.5}> <Circle cx={80} cy={128} r={40} /> <Rect x={140} y={88} width={80} height={80} /> </Group> </Canvas>; ``` **Why good:** color and opacity cascade to both children without repetition, Group provides single point for transforms and clipping **Gotcha:** Paragraph, Picture, and ImageSVG do not follow standard paint inheritance. Apply effects via the `layer` property instead. See [examples/core.md](examples/core.md) for transforms, clipping, and the layer escape hatch. --- ### Pattern 3: Paths (Declarative and Imperative) Use the declarative `<Path>` component with SVG path strings for static paths. Use `Skia.Path()` imperatively for dynamic path construction. ```tsx import { Canvas, Path, Skia } from "@shopify/react-native-skia"; // Declarative: SVG path string <Path path="M 10 80 Q 95 10 180 80" color="purple" style="stroke" strokeWidth={3} />; // Imperative: dynamic construction const path = Skia.Path.Make(); path.moveTo(10, 80); path.quadTo(95, 10, 180, 80); path.close(); ``` **When to use imperative:** dynamic shapes computed at runtime (e.g., chart data), paths that change based on user input, paths needed outside JSX (worklets, offscreen) See [examples/core.md](examples/core.md) for path operations (dash effects, trim, boolean ops). --- ### Pattern 4: Reanimated Integration Pass Reanimated shared values directly as Skia component props. No `createAnimatedComponent` or `useAnimatedProps` needed -- Skia reads shared values on the UI thread automatically. ```tsx import { Canvas, Circle } from "@shopify/react-native-skia"; import { useSharedValue, withRepeat, withTiming, } from "react-native-reanimated"; import { useEffect } from "react"; const DURATION = 2000; const MIN_RADIUS = 20; const MAX_RADIUS = 100; export function PulsingCircle() { const r = useSharedValue(MIN_RADIUS); useEffect(() => { r.value = withRepeat( withTiming(MAX_RADIUS, { duration: DURATION }), -1, true, ); }, []); return ( <Canvas style={{ flex: 1 }}> <Circle cx={128} cy={128} r={r} color="cyan" /> </Canvas> ); } ``` **Why good:** shared value `r` passed directly as prop, animation runs entirely on UI thread, zero bridge communication, 60 FPS **Critical:** Use `interpolateColors` from `@shopify/react-native-skia` for color animations -- Reanimated's `interpolateColor` produces wrong results with Skia's internal color format. See [examples/animations.md](examples/animations.md) for path interpolation, color animation, derived values, and Atlas animation. --- ### Pattern 5: Image Filters and Backdrop Blur Image filters (Blur, Shadow, ColorMatrix) apply as children to shapes or Groups. BackdropBlur applies blur to content behind a clipping mask (like CSS `backdrop-filter`). ```tsx import { Canvas, Image, Blur, BackdropBlur, Fill, useImage, } from "@shopify/react-native-skia"; const BLUR_RADIUS = 10; const BACKDROP_BLUR = 4; export function BlurExample() { const image = useImage(require("./photo.png")); if (!image) return null; return ( <Canvas style={{ width: 256, height: 256 }}> <Image image={image} fit="cover" x={0} y={0} width={256} height={256}> <Blur blur={BLUR_RADIUS} mode="clamp" /> </Image> <BackdropBlur blur={BACKDROP_BLUR} clip={{ x: 0, y: 128, width: 256, height: 128 }} > <Fill color="rgba(0, 0, 0, 0.3)" /> </BackdropBlur> </Canvas> ); } ``` **Why good:** Blur as child applies to image only, BackdropBlur applies to content behind the clipping region, composable See [examples/effects.md](examples/effects.md) for Shadow, ColorMatrix, RuntimeShader, and composed filter chains. --- ### Pattern 6: Custom Shaders (SkSL) Write GPU shaders in SkSL (Skia's GLSL-like language). Use `Skia.RuntimeEffect.Make()` to compile shaders. Pass uniforms as a plain object. ```tsx import { Canvas, Shader, Fill, Skia } from "@shopify/react-native-skia"; const SHADER_SOURCE = ` uniform float2 iResolution; uniform float iTime; vec4 main(vec2 pos) { vec2 uv = pos / iResolution; float wave = sin(uv.x * 10.0 + iTime * 2.0) * 0.5 + 0.5; return vec4(uv.x, wave, uv.y, 1.0); } `; const effect = Skia.RuntimeEffect.Make(SHADER_SOURCE)!; // In component: pass animated shared values as uniforms <Canvas style={{ flex: 1 }}> <Fill> <Shader source={effect} uniforms={{ iResolution: [256, 256], iTime: time }} /> </Fill> </Canvas>; ``` **Key SkSL differences from GLSL:** use `.eval(xy)` instead of `sample()` for child shaders, `uniform shader` for child shader declarations, supported uniform types: `float`, `float2`-`float4`, `int`, `int2`-`int4`, matrices, and arrays. See [examples/effects.md](examples/effects.md) for RuntimeShader as image filter, child shaders, and pixel density considerations. --- ### Pattern 7: Paragraph Text Layout The Paragraph API handles rich text with mixed fonts, line breaking, and alignment. Requires building text with `ParagraphBuilder`. Paragraph does not follow standard paint inheritance -- use `layer` for effects. ```tsx import { Canvas, Paragraph, Skia, useFonts, TextAlign, } from "@shopify/react-native-skia"; const PARAGRAPH_WIDTH = 300; export function RichText() { const fonts = useFonts({ Roboto: [require("./Roboto-Regular.ttf")] }); if (!fonts) return null; const para = Skia.ParagraphBuilder.Make( { textAlign: TextAlign.Center }, fonts, ) .pushStyle({ fontSize: 24, fontFamilies: ["Roboto"], color: Skia.Color("black"), }) .addText("Hello ") .pushStyle({ fontSize: 24, fontFamilies: ["Roboto"], fontStyle: { weight: 700 }, }) .addText("Skia") .pop() .build(); para.layout(PARAGRAPH_WIDTH); return ( <Canvas style={{ width: PARAGRAPH_WIDTH, height: para.getHeight() }}> <Paragraph paragraph={para} x={0} y={0} width={PARAGRAPH_WIDTH} /> </Canvas> ); } ``` **Why good:** mixed font weights in a single text block, automatic line breaking, measurable dimensions with `getHeight()` and `getLongestLine()` See [examples/text-and-media.md](examples/text-and-media.md) for font loading, text styles, and effects on paragraphs. --- ### Pattern 8: Atlas Batch Rendering Atlas draws thousands of sprites in a single draw call using one texture. Each sprite gets an individual RSXform (rotation + scale + translation). Ideal for tile maps, particle systems, and sprite animations. ```tsx import { Canvas, Atlas, useImage, Skia, rect, } from "@shopify/react-native-skia"; const SPRITE_SIZE = 32; const GRID_COLS = 10; const GRID_ROWS = 10; export function TileMap() { const texture = useImage(require("./spritesheet.png")); if (!texture) return null; const sprites = Array.from({ length: GRID_COLS * GRID_ROWS }, () => rect(0, 0, SPRITE_SIZE, SPRITE_SIZE), ); const transforms = Array.from({ length: GRID_COLS * GRID_ROWS }, (_, i) => { const col = i % GRID_COLS; const row = Math.floor(i / GRID_COLS); return Skia.RSXform(1, 0, col * SPRITE_SIZE, row * SPRITE_SIZE); }); return ( <Canvas style={{ width: GRID_COLS * SPRITE_SIZE, height: GRID_ROWS * SPRITE_SIZE, }} > <Atlas image={texture} sprites={sprites} transforms={transforms} /> </Canvas> ); } ``` **Why good:** single draw call for 100 sprites, RSXform encodes scale+rotation+translation efficiently, transforms can be animated via `useRSXformBuffer` worklets at near-zero cost See [examples/animations.md](examples/animations.md) for animated Atlas with `useRSXformBuffer`. </patterns> --- <decision_framework> ## Decision Framework ### When to Use Skia vs Native Views ``` Does the feature need custom drawing (paths, gradients, blur, shaders)? ├─ YES → Skia Canvas └─ NO → Does it need high-performance batch rendering (100+ similar elements)? ├─ YES → Skia Atlas └─ NO → Does it need rich text with mixed fonts/decorations? ├─ YES → Skia Paragraph (or native Text if simple) └─ NO → Standard React Native views ``` ### Declarative vs Imperative API ``` Is the shape/path static or defined at build time? ├─ YES → Declarative JSX (<Circle />, <Path path="..." />) └─ NO → Is the shape computed dynamically per frame? ├─ YES → Imperative (Skia.Path.Make()) inside worklets or useDerivedValue └─ NO → Is the shape created once based on data? ├─ YES → Imperative, created outside render, passed as prop └─ NO → Declarative with animated shared value props ``` ### Choosing the Right Text API ``` Need simple single-style text? ├─ YES → Skia <Text> component (single font, single style) └─ NO → Need mixed fonts, weights, or line breaking? ├─ YES → Paragraph API (ParagraphBuilder) └─ NO → Need text on a path? ├─ YES → <TextPath> component └─ NO → <Text> with Glyphs for advanced positioning ``` </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Using `createAnimatedComponent` or `useAnimatedProps` with Skia components -- unnecessary; pass shared values directly as props - Using Reanimated's `interpolateColor` for Skia color animations -- produces wrong colors; use `interpolateColors` from `@shopify/react-native-skia` - Applying paint children (Shader, Blur) directly to Paragraph, Picture, or ImageSVG -- they ignore standard paint inheritance; use the `layer` property - Using ScrollView with map() to render many Skia shapes -- use Atlas for batch rendering, or Picture for dynamic draw counts - Performing heavy computations in Canvas children on JS thread -- offload to worklets or `useDerivedValue` to keep animations on UI thread **Medium Priority Issues:** - Not setting `origin` on Group transforms and expecting center-based rotation -- default is top-left - RuntimeShader ignoring pixel density -- content appears blurry on high-DPI screens; wrap in a scale layer (scale by `PixelRatio.get()`, then scale back by `1/pd`) - Creating new Path objects every render -- memoize or use `usePathValue` to avoid garbage collection pressure - Hardcoding Canvas dimensions instead of using `onSize` shared value or `useCanvasSize` -- breaks on different screen sizes - Missing `androidWarmup={true}` for opaque canvases -- first frame renders white on Android **Gotchas & Edge Cases:** - Canvas uses its own React renderer -- React context from outside the Canvas is NOT available inside it - `useCanvasSize` returns `{ width: 0, height: 0 }` on first render -- guard against zero dimensions - ImageSVG does not render `<text>` elements, CSS styles, or `<animate>` -- preprocess SVGs with SVGO - `Skia.RuntimeEffect.Make()` returns null if the shader has syntax errors -- always handle the null case - Atlas transforms are RSXform (rotation-scale-translation), not standard Transform2d -- use `Skia.RSXform()` or `Skia.RSXformFromRadians()` - Path interpolation requires paths with the same number and types of commands -- mismatched paths crash - `useImage` returns null while loading -- always guard rendering on image availability - Bundle size impact: +6MB iOS, +4MB Android, +2.9MB web (CanvasKit WASM) - React Native >= 0.79 and React >= 19 required for current versions (v1.12.4 for RN <= 0.78) - Canvas snapshot: use `makeImageSnapshotAsync()` for images with textures, `makeImageSnapshot()` only for texture-free drawings - `useFonts` returns null while fonts load -- guard Paragraph rendering until fonts are ready </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** **(You MUST pass Reanimated shared values directly as Skia component props -- do NOT use createAnimatedComponent or useAnimatedProps)** **(You MUST use `interpolateColors` from `@shopify/react-native-skia` for color animations -- Reanimated's `interpolateColor` uses a different internal color format and produces wrong results)** **(You MUST use the `layer` property to apply paint effects to Paragraph, Picture, and ImageSVG components -- they do not follow standard paint inheritance rules)** **(You MUST remember that Group transform origin defaults to top-left, not center -- set `origin` prop explicitly for center-based rotations)** **Failure to follow these rules will produce broken color animations, invisible paint effects, and incorrectly positioned rotations.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.