dist/plugins/mobile-animation-skia/skills/mobile-animation-skia/SKILL.md
React Native Skia GPU-accelerated 2D graphics - Canvas, declarative drawing, shaders, image filters, Paragraph text, Atlas batch rendering, Reanimated animations
npx skillsauth add agents-inc/skills mobile-animation-skiaInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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>
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:
When NOT to use:
<Image> from React Native)<Text>)Key patterns covered:
Detailed Resources:
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:
<Canvas> uses Skia's renderer, everything outside is regular React NativeSkia.Path(), Skia.Paint()) only when you need dynamic constructionlayer property)When to reach for Skia:
When NOT to reach for Skia:
<Image> is sufficientCanvas is the root Skia drawing surface. It behaves like a regular React Native view (accepts style), but hosts its own React renderer internally.
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 for Canvas props (onSize, androidWarmup, snapshots) and all shape primitives.
Groups apply paint attributes (color, opacity, shaders, filters) to all children. Groups also provide transforms, clipping, and z-ordering.
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 for transforms, clipping, and the layer escape hatch.
Use the declarative <Path> component with SVG path strings for static paths. Use Skia.Path() imperatively for dynamic path construction.
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 for path operations (dash effects, trim, boolean ops).
Pass Reanimated shared values directly as Skia component props. No createAnimatedComponent or useAnimatedProps needed -- Skia reads shared values on the UI thread automatically.
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 for path interpolation, color animation, derived values, and Atlas animation.
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).
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 for Shadow, ColorMatrix, RuntimeShader, and composed filter chains.
Write GPU shaders in SkSL (Skia's GLSL-like language). Use Skia.RuntimeEffect.Make() to compile shaders. Pass uniforms as a plain object.
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 for RuntimeShader as image filter, child shaders, and pixel density considerations.
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.
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 for font loading, text styles, and effects on paragraphs.
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.
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 for animated Atlas with useRSXformBuffer.
<decision_framework>
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
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
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>
High Priority Issues:
createAnimatedComponent or useAnimatedProps with Skia components -- unnecessary; pass shared values directly as propsinterpolateColor for Skia color animations -- produces wrong colors; use interpolateColors from @shopify/react-native-skialayer propertyuseDerivedValue to keep animations on UI threadMedium Priority Issues:
origin on Group transforms and expecting center-based rotation -- default is top-leftPixelRatio.get(), then scale back by 1/pd)usePathValue to avoid garbage collection pressureonSize shared value or useCanvasSize -- breaks on different screen sizesandroidWarmup={true} for opaque canvases -- first frame renders white on AndroidGotchas & Edge Cases:
useCanvasSize returns { width: 0, height: 0 } on first render -- guard against zero dimensions<text> elements, CSS styles, or <animate> -- preprocess SVGs with SVGOSkia.RuntimeEffect.Make() returns null if the shader has syntax errors -- always handle the null caseSkia.RSXform() or Skia.RSXformFromRadians()useImage returns null while loading -- always guard rendering on image availabilitymakeImageSnapshotAsync() for images with textures, makeImageSnapshot() only for texture-free drawingsuseFonts returns null while fonts load -- guard Paragraph rendering until fonts are ready</red_flags>
<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>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events