From 6622e62e6d599496e3397eed1f20511307395489 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 29 Jan 2026 10:40:33 +0100 Subject: [PATCH 1/9] =?UTF-8?q?chore(=F0=9F=93=9D):=20minor=20enhancement?= =?UTF-8?q?=20to=20=20Atlas=20component=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/docs/docs/shapes/atlas.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/docs/shapes/atlas.md b/apps/docs/docs/shapes/atlas.md index f1c2af46fb..2cba635f65 100644 --- a/apps/docs/docs/shapes/atlas.md +++ b/apps/docs/docs/shapes/atlas.md @@ -5,9 +5,9 @@ sidebar_label: Atlas slug: /shapes/atlas --- -The Atlas component is used for efficient rendering of multiple instances of the same texture or image. It is especially useful for drawing a very large number of similar objects, like sprites, with varying transformations. +The Atlas component is used for efficient rendering of multiple instances of the same texture or image. It is especially useful for drawing a very large number of similar objects, like sprites or tiles, with varying transformations. -Its design particularly useful when using with [Reanimated](#animations). +Atlas transforms can be animated with near-zero cost using worklets. This makes it ideal for tile-based maps, sprite animations, and any scenario where you have many instances of similar textures. Its design is particularly useful when combined with [Reanimated](#animations). | Name | Type | Description | |:--------|:-----------------|:-----------------| From 679c21408298be24fd9684eb7696b51980de15e1 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 29 Jan 2026 11:14:10 +0100 Subject: [PATCH 2/9] :wrench: --- apps/docs/docs/canvas/rendering-modes.md | 89 ++++++++++++++++++++++++ apps/docs/sidebars.js | 2 +- 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 apps/docs/docs/canvas/rendering-modes.md diff --git a/apps/docs/docs/canvas/rendering-modes.md b/apps/docs/docs/canvas/rendering-modes.md new file mode 100644 index 0000000000..bcdbca2de9 --- /dev/null +++ b/apps/docs/docs/canvas/rendering-modes.md @@ -0,0 +1,89 @@ +--- +id: rendering-modes +title: Rendering Modes +sidebar_label: Rendering Modes +slug: /canvas/rendering-modes +--- + +React Native Skia supports two rendering paradigms: **Retained Mode** and **Immediate Mode**. Understanding when to use each is key to building performant graphics applications. + +## Retained Mode (Default) + +In retained mode, you declare your scene as a tree of React components. React Native Skia converts this tree into a display list that is extremely efficient to animate with Reanimated. +This approach is extremely fast and is best suited for user-interfaces and interactive graphics where the structure doesn't change at animation time. + +```tsx twoslash +import React, {useEffect} from "react"; +import { Canvas, Circle, Group } from "@shopify/react-native-skia"; +import { useSharedValue, withSpring, useDerivedValue } from "react-native-reanimated"; + +export const RetainedModeExample = () => { + const radius = useSharedValue(50); + useEffect(() => { + radius.value = withSpring(radius.value === 50 ? 100 : 50); + }, []); + return ( + + + + + + ); +}; +``` + +## Immediate Mode + +In immediate mode, you issue drawing commands directly to a canvas on every frame. This gives you complete control over what gets drawn and when, but requires you to manage the drawing logic yourself. + +React Native Skia provides immediate mode through the [Picture API](/docs/shapes/pictures). +This mode is extremely well-suited for scenes where the number of drawing commands changes on every animation frame. This is often the case for games, generative art, and particle systems where the scene changes unpredictably on each animation frame. + +```tsx twoslash +import { Canvas, Picture, Skia } from "@shopify/react-native-skia"; +import { useDerivedValue, useSharedValue, withRepeat, withTiming } from "react-native-reanimated"; +import { useEffect } from "react"; + +const size = 256; + +export const ImmediateModeExample = () => { + const progress = useSharedValue(0); + const recorder = Skia.PictureRecorder(); + const paint = Skia.Paint(); + + useEffect(() => { + progress.value = withRepeat(withTiming(1, { duration: 2000 }), -1, true); + }, [progress]); + + const picture = useDerivedValue(() => { + "worklet"; + const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, size, size)); + + // Variable number of circles based on progress + const count = Math.floor(progress.value * 20); + for (let i = 0; i < count; i++) { + const r = (i + 1) * 6; + paint.setColor(Skia.Color(`rgba(0, 122, 255, ${(i + 1) / 20})`)); + canvas.drawCircle(size / 2, size / 2, r, paint); + } + + return recorder.finishRecordingAsPicture(); + }); + + return ( + + + + ); +}; +``` + +## Choosing the Right Mode + +| Scenario | Recommended Mode | Why | +|:---------|:-----------------|:----| +| UI with animated properties | Retained | Zero FFI cost during animation | +| Data visualization | Retained | Structure usually fixed | +| Fixed number of sprites/tiles | Retained | With the [Atlas API](/docs/shapes/atlas), single draw call | +| Game with dynamic entities | Immediate | Entities created/destroyed | +| Procedural/generative art | Immediate | Dynamic drawing commands | diff --git a/apps/docs/sidebars.js b/apps/docs/sidebars.js index 20c49e7175..e819f28f0e 100644 --- a/apps/docs/sidebars.js +++ b/apps/docs/sidebars.js @@ -30,7 +30,7 @@ const sidebars = { collapsed: true, type: "category", label: "Canvas", - items: ["canvas/canvas", "canvas/contexts"], + items: ["canvas/canvas", "canvas/rendering-modes", "canvas/contexts"], }, { collapsed: true, From fb2f3a057693af62787a73f579cfc689fd898c19 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 29 Jan 2026 11:27:24 +0100 Subject: [PATCH 3/9] :wrench: --- apps/docs/docs/canvas/rendering-modes.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/docs/docs/canvas/rendering-modes.md b/apps/docs/docs/canvas/rendering-modes.md index bcdbca2de9..cdbf376082 100644 --- a/apps/docs/docs/canvas/rendering-modes.md +++ b/apps/docs/docs/canvas/rendering-modes.md @@ -6,6 +6,9 @@ slug: /canvas/rendering-modes --- React Native Skia supports two rendering paradigms: **Retained Mode** and **Immediate Mode**. Understanding when to use each is key to building performant graphics applications. +The Retained Mode allows for extremely fast animation time with a virtually zero FFI-cost if the drawing list is updated at low frequency. The immediate mode allows for dynamic drawing list but has a higher FFI-cost to pay. +Since immediate mode uses the same `` element, you can seamlessly combine both rendering modes in a single scene. + ## Retained Mode (Default) @@ -80,6 +83,8 @@ export const ImmediateModeExample = () => { ## Choosing the Right Mode +Here is a small list of use-cases and which mode would be best for that scenario. Keep in mind that since these modes use the same `` element they can be nicely composed with each other. For instance a game where the scene is dynamic on every animation frame and some game UI elements are built in Retained Mode. + | Scenario | Recommended Mode | Why | |:---------|:-----------------|:----| | UI with animated properties | Retained | Zero FFI cost during animation | From bf3d29d8d6c9b86f6d10261ca0cabc05e33cc7e5 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 29 Jan 2026 12:20:31 +0100 Subject: [PATCH 4/9] =?UTF-8?q?=E2=9A=99=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/docs/docs/pictures.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/docs/pictures.md b/apps/docs/docs/pictures.md index 19e27e0649..0d9d20cda4 100644 --- a/apps/docs/docs/pictures.md +++ b/apps/docs/docs/pictures.md @@ -5,8 +5,8 @@ sidebar_label: Pictures slug: /shapes/pictures --- -React Native Skia works in retained mode: every re-render, we create a display list with support for animation values. -This is great to animate property values. However, if you want to execute a variable number of drawing commands, this is where you need to use pictures. +React Native Skia works in [retained mode](/docs/canvas/rendering-modes): every re-render, we create a display list with support for animation values. +This is great for animating property values with near-zero performance cost. However, if you need to execute a **variable number of drawing commands** each frame, this is where you need to use the Picture API which works in [immediate mode](/docs/canvas/rendering-modes#immediate-mode) API. A Picture contains a list of drawing operations to be drawn on a canvas. The picture is immutable and cannot be edited or changed after it has been created. It can be used multiple times in any canvas. From 27d81ce7406b930698ba10a8f5f17b6acecb370c Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 18 Mar 2026 11:26:57 +0100 Subject: [PATCH 5/9] =?UTF-8?q?chore(=F0=9F=90=99):=20add=20CI=20workflow?= =?UTF-8?q?=20for=20building=20and=20testing=20Graphite=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-graphite.yml | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.github/workflows/ci-graphite.yml b/.github/workflows/ci-graphite.yml index 167131657d..fccdeb9963 100644 --- a/.github/workflows/ci-graphite.yml +++ b/.github/workflows/ci-graphite.yml @@ -229,3 +229,41 @@ jobs: - name: Run e2e tests working-directory: packages/skia run: CI=true E2E=true yarn test -i Paths + + build-library-graphite: + runs-on: ubuntu-latest + outputs: + run_id: ${{ github.run_id }} + steps: + - name: Checkout + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + with: + submodules: recursive + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + graphite: true + + - name: Build package + run: yarn build + + - name: Pack package + working-directory: packages/skia + run: yarn pack + + - name: Upload package artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v6.0.0 + with: + name: package-tgz + path: packages/skia/package.tgz + + test-package-e2e-graphite: + needs: build-library-graphite + uses: ./.github/workflows/test-skia-package.yml + with: + skia_version: artifact:${{ needs.build-library-graphite.outputs.run_id }} + test_ios: true + test_android: true + test_web: true From f5072808467533b50516ef601976ac66fe6b70d4 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 18 Mar 2026 12:08:46 +0100 Subject: [PATCH 6/9] :green_heart: --- .github/workflows/ci-graphite.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci-graphite.yml b/.github/workflows/ci-graphite.yml index fccdeb9963..5d0bff16ec 100644 --- a/.github/workflows/ci-graphite.yml +++ b/.github/workflows/ci-graphite.yml @@ -249,6 +249,10 @@ jobs: - name: Build package run: yarn build + - name: Prepare package for graphite bundle + working-directory: packages/skia + run: node -e "const p=require('./package.json'); delete p.scripts.postinstall; p.files.push('libs/**'); require('fs').writeFileSync('./package.json', JSON.stringify(p, null, 2) + '\n')" + - name: Pack package working-directory: packages/skia run: yarn pack From c2fbb9e39f9122faa085c16a36038b9c5a23a0de Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 18 Mar 2026 14:20:00 +0100 Subject: [PATCH 7/9] :wrench: --- .github/workflows/ci-graphite.yml | 1 + .github/workflows/test-skia-package.yml | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/.github/workflows/ci-graphite.yml b/.github/workflows/ci-graphite.yml index 5d0bff16ec..c786d92c5c 100644 --- a/.github/workflows/ci-graphite.yml +++ b/.github/workflows/ci-graphite.yml @@ -271,3 +271,4 @@ jobs: test_ios: true test_android: true test_web: true + android_min_sdk: '26' diff --git a/.github/workflows/test-skia-package.yml b/.github/workflows/test-skia-package.yml index 7985a11b4b..94f1883d61 100644 --- a/.github/workflows/test-skia-package.yml +++ b/.github/workflows/test-skia-package.yml @@ -32,6 +32,11 @@ on: required: false default: 'Nexus 5X' type: string + android_min_sdk: + description: 'Android minSdkVersion override' + required: false + default: '' + type: string test_web: description: 'Run Web tests' required: false @@ -308,6 +313,18 @@ jobs: echo "Generating native Android directory..." npx expo prebuild --platform android + - name: Override minSdkVersion + if: inputs.android_min_sdk != '' + working-directory: /Users/runner/skia-test-app/my-app/android + run: | + echo "Setting minSdkVersion to ${{ inputs.android_min_sdk }}..." + if grep -q 'minSdkVersion' gradle.properties; then + sed -i '' "s/minSdkVersion=.*/minSdkVersion=${{ inputs.android_min_sdk }}/" gradle.properties + fi + if grep -q 'react.minSdkVersion' gradle.properties; then + sed -i '' "s/react.minSdkVersion=.*/react.minSdkVersion=${{ inputs.android_min_sdk }}/" gradle.properties + fi + - name: Start Android emulator run: | echo "Starting Android emulator in background..." From 76efecd7016beb8bdf9964d6ee97ddebc65b1c8d Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 18 Mar 2026 15:59:10 +0100 Subject: [PATCH 8/9] :green_heart: --- .github/workflows/test-skia-package.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-skia-package.yml b/.github/workflows/test-skia-package.yml index 94f1883d61..830c9f40e0 100644 --- a/.github/workflows/test-skia-package.yml +++ b/.github/workflows/test-skia-package.yml @@ -318,12 +318,8 @@ jobs: working-directory: /Users/runner/skia-test-app/my-app/android run: | echo "Setting minSdkVersion to ${{ inputs.android_min_sdk }}..." - if grep -q 'minSdkVersion' gradle.properties; then - sed -i '' "s/minSdkVersion=.*/minSdkVersion=${{ inputs.android_min_sdk }}/" gradle.properties - fi - if grep -q 'react.minSdkVersion' gradle.properties; then - sed -i '' "s/react.minSdkVersion=.*/react.minSdkVersion=${{ inputs.android_min_sdk }}/" gradle.properties - fi + echo "react.minSdkVersion=${{ inputs.android_min_sdk }}" >> gradle.properties + cat gradle.properties - name: Start Android emulator run: | From baafb9c17a51621e58673bd8dc70ba108de5709d Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 18 Mar 2026 17:08:46 +0100 Subject: [PATCH 9/9] :green_heart: --- .github/workflows/test-skia-package.yml | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-skia-package.yml b/.github/workflows/test-skia-package.yml index 830c9f40e0..c940742072 100644 --- a/.github/workflows/test-skia-package.yml +++ b/.github/workflows/test-skia-package.yml @@ -307,20 +307,29 @@ jobs: echo "Installing expo-dev-client..." npx expo install expo-dev-client + - name: Override minSdkVersion + if: inputs.android_min_sdk != '' + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Setting minSdkVersion to ${{ inputs.android_min_sdk }} in app.json..." + node -e " + const f = './app.json'; + const app = require(f); + app.expo = app.expo || {}; + app.expo.android = app.expo.android || {}; + app.expo.plugins = app.expo.plugins || []; + app.expo.plugins.push(['expo-build-properties', { android: { minSdkVersion: ${{ inputs.android_min_sdk }} } }]); + require('fs').writeFileSync(f, JSON.stringify(app, null, 2) + '\n'); + " + npx expo install expo-build-properties + cat app.json + - name: Prebuild native directories working-directory: /Users/runner/skia-test-app/my-app run: | echo "Generating native Android directory..." npx expo prebuild --platform android - - name: Override minSdkVersion - if: inputs.android_min_sdk != '' - working-directory: /Users/runner/skia-test-app/my-app/android - run: | - echo "Setting minSdkVersion to ${{ inputs.android_min_sdk }}..." - echo "react.minSdkVersion=${{ inputs.android_min_sdk }}" >> gradle.properties - cat gradle.properties - - name: Start Android emulator run: | echo "Starting Android emulator in background..."