Claude Skill

mobile-animation-skia

React Native Skia GPU-accelerated 2D graphics - Canvas, declarative drawing, shaders, image filters, Paragraph text, Atlas batch rendering, Reanimated animations

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_mobile-animation-skia_skills_mobile-animation-skia-3a51ef5.zip · 20 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-animation-skia/skills/mobile-animation-skia
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git 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-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:




<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>

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.

No comments yet.

Reviews (0)

No reviews yet.

Related