diff --git a/.github/workflows/ci-graphite.yml b/.github/workflows/ci-graphite.yml
index 9f0f1a2323..a30982ac92 100644
--- a/.github/workflows/ci-graphite.yml
+++ b/.github/workflows/ci-graphite.yml
@@ -257,4 +257,4 @@ jobs:
- name: Run e2e tests
working-directory: packages/skia
- run: CI=true yarn e2e --testPathIgnorePatterns "Paragraph|Atlas"
+ run: CI=true yarn e2e --testPathIgnorePatterns "Paragraph"
diff --git a/.gitmodules b/.gitmodules
index 7ce029cf77..66a508ee5c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,7 +1,7 @@
[submodule "externals/skia"]
path = externals/skia
url = https://chromium.googlesource.com/skia/
- branch = chrome/m150
+ branch = chrome/m152
[submodule "externals/depot_tools"]
path = externals/depot_tools
url = https://chromium.googlesource.com/chromium/tools/depot_tools.git
diff --git a/apps/docs/docs/animations/reanimated3.md b/apps/docs/docs/animations/reanimated3.md
index 5d2d7f6d91..9e34bb6a51 100644
--- a/apps/docs/docs/animations/reanimated3.md
+++ b/apps/docs/docs/animations/reanimated3.md
@@ -46,6 +46,42 @@ export const HelloWorld = () => {
We offer some [Skia specific animation hooks](/docs/animations/hooks), especially for paths.
+## Grouped values
+
+`select` lets a single shared value, whose value is an object, drive multiple props by binding each prop to one key of that object.
+
+```tsx
+import { Canvas, Circle, select } from "@shopify/react-native-skia";
+import { useSharedValue, useFrameCallback } from "react-native-reanimated";
+
+export const Grouped = () => {
+ // A single shared value holds every animated field.
+ const circle = useSharedValue({ cx: 0, cy: 0, r: 10 });
+ useFrameCallback(({ timeSinceFirstFrame }) => {
+ "worklet";
+ const t = timeSinceFirstFrame / 1000;
+ circle.value = {
+ cx: 100 + Math.cos(t) * 50,
+ cy: 100 + Math.sin(t) * 50,
+ r: 10 + (Math.sin(t * 2) + 1) * 5,
+ };
+ }, true);
+ return (
+
+ );
+};
+```
+Here one shared value drives three props with a single subscription, instead of three derived values with three subscriptions.
+
+Reanimated only animates values assigned directly to a shared value's `.value`, not values nested inside an object. You therefore cannot place `withTiming`/`withSpring` on a key (e.g. `{ cx: withTiming(100) }`); assign plain values to the object (as above), or build the object in a `useDerivedValue` from individually animated shared values.
+
## Colors
For colors, React Native Skia uses a different storage format from Reanimated.
diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock
index bb216bdf52..c2ac90f9f2 100644
--- a/apps/example/ios/Podfile.lock
+++ b/apps/example/ios/Podfile.lock
@@ -2009,7 +2009,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - react-native-webgpu (0.7.1):
+ - react-native-webgpu (0.8.0):
- boost
- DoubleConversion
- fast_float
@@ -3238,90 +3238,90 @@ SPEC CHECKSUMS:
FBLazyVector: 309703e71d3f2f1ed7dc7889d58309c9d77a95a4
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
- hermes-engine: ad676c360175e5b8af471b8ce1389e6cf4f9e1ee
- RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
+ hermes-engine: 11b010917f5f15150b2c015abddef1573d2bb05d
+ RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f
RCTDeprecation: a41bbdd9af30bf2e5715796b313e44ec43eefff1
RCTRequired: 7be34aabb0b77c3cefe644528df0fa0afad4e4d0
RCTSwiftUI: a6c7271c39098bf00dbdad8f8ed997a59bbfbe44
- RCTSwiftUIWrapper: ff9098ccf7727e58218f2f8ea110349863f43438
+ RCTSwiftUIWrapper: 5ec163e8fde163d3fba714a992b50a266e1ece37
RCTTypeSafety: 27927d0ca04e419ed9467578b3e6297e37210b5c
React: 4bc1f928568ad4bcfd147260f907b4ea5873a03b
React-callinvoker: 87f8728235a0dc62e9dc19b3851c829d9347d015
- React-Core: 76bed73b02821e5630e7f2cb2e82432ee964695d
- React-CoreModules: 752dbfdaeb096658aa0adc4a03ba6214815a08df
- React-cxxreact: b6798528aa601c6db66e6adc7e2da2b059c8be74
+ React-Core: 19e0183e28d7a6613ecacebd7525fe6650efa3b6
+ React-CoreModules: 73cc86f2a0ff84b93d6325073ad2e4874d21ad40
+ React-cxxreact: 4bf734645c77c9b86e2f3e933e0411cf2f14d1ba
React-debug: 8978deb306f6f38c28b5091e52b0ac9f942b157e
- React-defaultsnativemodule: 682b77ef4acfb298017be15f4f93c1d998deb174
- React-domnativemodule: 4c4b44f7eb68dbc3a2218db088bef318a7302017
- React-Fabric: b6f82a4d8498ce4475586f71ca8397a771fe292d
- React-FabricComponents: c8695f4b11918a127c4560d66f7d3fdb01a17986
- React-FabricImage: d64f48830f63830e8ffaaf69fa487116856fbbf1
- React-featureflags: 2a46b229903e906d33dbaf9207ce57c59306c369
- React-featureflagsnativemodule: cba6c0814051a0934f8bcee4a436ee2a6bcc9754
- React-graphics: 3d0435051e1ab8904d065f8ffbe981a9fc202841
- React-hermes: 32fc9c231c1aa5c2fcfe851b0d19ee9269f88f4c
- React-idlecallbacksnativemodule: f8ee42581795c4844d97147596bcc2d824c0f188
- React-ImageManager: e8f7377ef0585fd2df05559a17e01a03e187d5cf
- React-intersectionobservernativemodule: b1bea12ca29accdd2eda60c87605a6030b894eb9
- React-jserrorhandler: 1a86df895b4eaf4e771abe8cf34cbb26d821f771
- React-jsi: adf8527fec197ad9d0480cc5b8945eb56de627f0
- React-jsiexecutor: 315fa2f879b43e3a9ca08f5f4b733472f7e4e8a4
- React-jsinspector: b4fd1933666bcb2549b566b40656c1e45e9d7632
- React-jsinspectorcdp: 80141710f2668e5b8f00417298d9b76b4abf90fa
- React-jsinspectornetwork: 1d3ea717dbbec316cd8c21a0af53928a7bf74901
- React-jsinspectortracing: 4ce745374d4b2bfbd164cce9f8de8383d3d818a0
- React-jsitooling: fc4ac4c3b1f3f9f7fedf0c777c6ff3f244f568bd
- React-jsitracing: bff08a6faeef4a9bd286487da191f5e5329e21a9
- React-logger: b8483fa08e0d62e430c76d864309d90576ca2f68
- React-Mapbuffer: 7b72a669e94662359dad4f42b5af005eb24b4e83
- React-microtasksnativemodule: cdc02da075f2857803ed63f24f5f72fc40e094c0
- react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460
- react-native-skia: 398d81d07392758762cf114280cceb24ebc9b6f3
- react-native-webgpu: 763fca525e9f3186e3b12adc237d95c36984f1a7
- React-NativeModulesApple: a2c3d2cbec893956a5b3e4060322db2984fff75b
- React-networking: 3f98bd96893a294376e7e03730947a08d474c380
+ React-defaultsnativemodule: 724eb9ec388d494f1e2057d83355ee8fe6f1d780
+ React-domnativemodule: 9068f41092f725acd09950233d2847364c731947
+ React-Fabric: 945cc8abf08d9d0966acef605bffce7b501c49d9
+ React-FabricComponents: 4c4ad6f0d16c964a68f945e029505e2eeec6654b
+ React-FabricImage: a8b628fd98db21b9f8588e06f14a9194dda11b40
+ React-featureflags: 0937601c1af1cc125851ec5bbf4654285d47a3e7
+ React-featureflagsnativemodule: ac1a3e0353e1a6e15411b17ed6c7122adb0468a4
+ React-graphics: cca521e06463608be46207a4aa160f8a7f725f8b
+ React-hermes: ec50b9fcea2c3bfdd42f8cec845eac3f35888572
+ React-idlecallbacksnativemodule: effcae5b7b4473211adb154aaa321d5d9e2fbcc9
+ React-ImageManager: b38459e538f1840fa5c3e7612a4bcb0029a3c366
+ React-intersectionobservernativemodule: 8d33366661971200cf2e151727f6fe007b62ae7b
+ React-jserrorhandler: f94c688a0dbe2e045b91b992722b92e97d56f77f
+ React-jsi: 3216c876cd4c571a57909e22d77c8fd9530aa067
+ React-jsiexecutor: 475563c0042841a85930a455d3199f6b1483a5fe
+ React-jsinspector: bc484fb32bf1b9fed80afe8793e614eba4f7b39e
+ React-jsinspectorcdp: 5a574d1d35016968a67e78e6b8a7917473ffbb77
+ React-jsinspectornetwork: dce3a5a1351b527ee8c28ad4a8bdd211507e1a45
+ React-jsinspectortracing: 65f6b166bd67e5adc31eba027e1570bacf7a3cc7
+ React-jsitooling: d5463f5489a31640b0fa0ec4e31566ca8aa86c13
+ React-jsitracing: 3c7fc18821aba64855acb8658aa857ca6a7fddf6
+ React-logger: 6ac901f5c7f7321d2be1a40b203bccc2e23411e3
+ React-Mapbuffer: 2e0e7cc5b7064eaed9c8b8afc3a87621cb7ef5cd
+ React-microtasksnativemodule: dd4d33b251b57e5027c572c6d0b45cbfbcfaa386
+ react-native-safe-area-context: 54d812805f3c4e08a4580ad086cbde1d8780c2e4
+ react-native-skia: 765e9975e9ccde5ce3c9a0f740f195108c7d0280
+ react-native-webgpu: d7021164f316160273312cc13d9d4e0ea0a1308b
+ React-NativeModulesApple: 7f2f2fed3f6c858889eb61d09941be965d52df58
+ React-networking: 43e5e6773ac2ca2a93261a1388fed269c9fce092
React-oscompat: 80166b66da22e7af7fad94474e9997bd52d4c8c6
- React-perflogger: d6797918d2b1031e91a9d8f5e7fdd2c8728fb390
- React-performancecdpmetrics: 5570be61e2f97c4741c5d432c91570e8e5a39892
- React-performancetimeline: 5763499ae1991fc18dcf416e340ce7bc829bb298
+ React-perflogger: 63c90e0d8c24df87ffa14dad01aeafc352847dd0
+ React-performancecdpmetrics: 5a9b81c08f75045635127d626440d9ada01e774b
+ React-performancetimeline: 31cebfff69ec9174b3fb54b0606fcb12ef91cbad
React-RCTActionSheet: 3bd5f5db9f983cf38d51bb9a7a198e2ebea94821
- React-RCTAnimation: 46a9978f27dc434dbeed16afa7b82619b690a9af
- React-RCTAppDelegate: 62ecd60a2b2a8cae26ce6a066bfa59cfde97af01
- React-RCTBlob: 8285c859513023ee3cc8c806d9b59d4da078c4ba
- React-RCTFabric: 05ed09347e938de985052f791a6a0698816d5761
- React-RCTFBReactNativeSpec: 83ba579fca9a51e774ac32578ef5dd3262edd7e2
- React-RCTImage: a5364d0f098692cfbf5bef1e8a63e7712ecb14b7
- React-RCTLinking: 34b63b0aa0e92d30f5d7aa2c255a8f95fa75ee8f
- React-RCTNetwork: 1ef88b7a5310b8f915d3556b5b247def113191ed
- React-RCTRuntime: ed29cf68a46782fec891e5afe1d8d758ca6ccd9b
- React-RCTSettings: 2c45623d6c0f30851a123f621eb9d32298bcbb0c
- React-RCTText: 0ee70f5dc18004b4d81b2c214267c6cbec058587
- React-RCTVibration: 88557e21e7cc3fe76b5b174cba28ff45c6def997
+ React-RCTAnimation: 346865a809fa5132f6c594c8b376c6cf46b44e88
+ React-RCTAppDelegate: b2d1e0d3663c987f49f45094883b9e36fcbf0181
+ React-RCTBlob: 74759ebb7ff9077d19f60c301782c1f8c3eb2813
+ React-RCTFabric: 7b4b14dad21ca99333ebcbc0bf5c205647a315a8
+ React-RCTFBReactNativeSpec: 39151968adb68b8c59f29a8bd4223d4d7780a793
+ React-RCTImage: 60763f56e8a5e45d861d7c4777e428bb820ec52a
+ React-RCTLinking: 52aee78b0b3163167c7fcf58f80a42943c03a056
+ React-RCTNetwork: f5e1e8ae5eff6982efff6289b06ec0a76d0a6ac2
+ React-RCTRuntime: 0e99199322afd372e74b95ae5c58f4e074cc2855
+ React-RCTSettings: 298bb40d3412bf32e0b4f0797e48416b0b7278a1
+ React-RCTText: dfb74800e27d792d1188fa975a3b9807c3362e3e
+ React-RCTVibration: ffe5fd4f50a835e353a3b6869eb005dab11eea44
React-rendererconsistency: d280314a3e7f0097152f89e815b4de821c2be8b9
- React-renderercss: f8cbf83d95c2c2bbf893d37fe50c73f046584411
- React-rendererdebug: 37216ddfcd38e49d1e92bf9052ea4bc9d7b932e5
- React-RuntimeApple: 1c0e7cb8e1c2c5775585afcaaa666ec151629a8d
- React-RuntimeCore: 925fe2ca24cf8e6ed87586dbb92827306b93b83f
- React-runtimeexecutor: 962dae024f6df760d029512a7d99e3f73d570935
- React-RuntimeHermes: 19a7c59ec1bc9908516f0bbc29b49425f6ec64ba
- React-runtimescheduler: 62f21127cd97f4d8f164eee5150d3ce53dd36f66
- React-timing: 8757bf6fb96227c264f2d1609f4ba5c68217b8ce
- React-utils: 8ab26781c2f5c2f7fafb2022c8ab39d39f231b80
- React-webperformancenativemodule: 7953b7fe519f76fa595111fe18ff3d5de131bfe9
- ReactAppDependencyProvider: 0eb286cc274abb059ee601b862ebddac2e681d01
- ReactCodegen: b8e56b780fffe6edd6405be0af4a1e3049a937f7
- ReactCommon: ac934cb340aee91282ecd6f273a26d24d4c55cae
- ReactNativeHost: eef98ec49b55d88ad4cabf5a4378a12b42b551ee
- ReactTestApp-DevSupport: ea18f446cff64b6c9a3e28788600c82ecf51bde6
+ React-renderercss: 8a1a346f3665fd5ea7a7be7b3b9f95d4743e1180
+ React-rendererdebug: af74afdfb3d6c5382ebab35562efd8eb9e690473
+ React-RuntimeApple: 06e33d291e72fd0c73ac47046c3536d77d5aeedd
+ React-RuntimeCore: 99273d2af072062eb07f0b2d2d4a0f2de697ea14
+ React-runtimeexecutor: 2063c03c18810ee57939d138142e6493333360ef
+ React-RuntimeHermes: 2253a7f4c8d56b449230b330b0b15383ed4b3df4
+ React-runtimescheduler: ff37ac6720a943da91645c06274282ac46b71f23
+ React-timing: 831d7e081ba4c332ca5cccf389b88e363f13f2b4
+ React-utils: 25db6c17598c4fed22b5956d7551bb8bddf1f95b
+ React-webperformancenativemodule: 57e41e6193cfb815bde0b5534bef68673f1270eb
+ ReactAppDependencyProvider: bfb12ead469222b022a2024f32aba47ce50de512
+ ReactCodegen: 9ca1bd49eee1eccf6e427e406d2163f49e9c48c0
+ ReactCommon: 05ad684db7d88e194272ae26baddf6300e30b8b7
+ ReactNativeHost: e7e0a518b0120f0070b3e1f13c7006d3e0e8ee13
+ ReactTestApp-DevSupport: 6994b53b5b81139a8ce63e0776c726c95de079a1
ReactTestApp-Resources: 1bd9ff10e4c24f2ad87101a32023721ae923bccf
- RNGestureHandler: cd4be101cfa17ea6bbd438710caa02e286a84381
- RNReanimated: c26dfcd831add485c2ed93de9d7bfb90b035eeaa
- RNScreens: 714e10b6b554f7dc7ad9f78dcf36dc8e3fc73415
- RNSVG: 11354d28dd6cb71a59570b68c91ba6772a2d781d
- RNWorklets: b89b501d37972e6419d6f87effe41d6d76157648
+ RNGestureHandler: 77eecab5fd636666ca73a55bb61e2f1a685b7e84
+ RNReanimated: d1a7a4c20eefc371e062990ce1debeaff4f1b9be
+ RNScreens: b2a5c76af24a02a2fd71bfce42780fdd9c79cc6d
+ RNSVG: ea9cbf6dcdbebdfff5822b0ad9311bbc4510a0b7
+ RNWorklets: 5f6e5664c1819eac103ca75cc2f36191f55aa110
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
- Yoga: 5456bb010373068fc92221140921b09d126b116e
+ Yoga: 5bd0956bf9cb16f75101e78b5e852c7577bc5a45
-PODFILE CHECKSUM: 7b64fbddf0a0f7c0c19eaa9ca71bc7cdf4e2c959
+PODFILE CHECKSUM: d3796f1f719a0788265d1cd391be0a04200ce6cd
-COCOAPODS: 1.16.2
+COCOAPODS: 1.15.2
diff --git a/apps/example/src/Examples/Reanimated/NullableGroupedValue.tsx b/apps/example/src/Examples/Reanimated/NullableGroupedValue.tsx
new file mode 100644
index 0000000000..5b74a65d0e
--- /dev/null
+++ b/apps/example/src/Examples/Reanimated/NullableGroupedValue.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+import { StyleSheet } from "react-native";
+import { useFrameCallback, useSharedValue } from "react-native-reanimated";
+import { Canvas, Circle, Fill, select } from "@shopify/react-native-skia";
+
+import { AnimationDemo } from "./Components";
+
+interface CircleState {
+ cx: number;
+ cy: number;
+ r: number;
+}
+
+export const NullableGroupedValue = () => {
+ const data = useSharedValue(null as unknown as CircleState);
+
+ useFrameCallback(({ timeSinceFirstFrame }) => {
+ "worklet";
+ if (timeSinceFirstFrame < 600) {
+ return;
+ }
+ const t = (timeSinceFirstFrame - 600) / 1000;
+ data.value = {
+ cx: 60 + Math.cos(t * 2) * 40,
+ cy: 60 + Math.sin(t * 2) * 30,
+ r: 20,
+ };
+ }, true);
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ canvas: {
+ height: 120,
+ width: "100%" as const,
+ backgroundColor: "#FEFEFE" as const,
+ },
+});
diff --git a/apps/example/src/Examples/Reanimated/SharedValueComparison.tsx b/apps/example/src/Examples/Reanimated/SharedValueComparison.tsx
new file mode 100644
index 0000000000..483035beee
--- /dev/null
+++ b/apps/example/src/Examples/Reanimated/SharedValueComparison.tsx
@@ -0,0 +1,208 @@
+import React, { useState } from "react";
+import {
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+ useWindowDimensions,
+} from "react-native";
+import type { SharedValue } from "react-native-reanimated";
+import {
+ useDerivedValue,
+ useFrameCallback,
+ useSharedValue,
+} from "react-native-reanimated";
+import { Canvas, Circle, Fill, select } from "@shopify/react-native-skia";
+
+import { AnimationDemo } from "./Components";
+
+// Demonstrates driving many animated props from a SINGLE shared value
+// instead of one derived value per prop. Both modes render the exact same
+// animation; only the wiring differs:
+//
+// • Grouped — one value holds every coordinate; each prop reads its own
+// key from it. Reanimated subscribes once (1 mapper).
+// • Per-prop — every prop gets its own derived value, so Reanimated
+// subscribes once per prop (COUNT * 3 mappers).
+//
+// Grouping wins on:
+// - performance fewer UI-thread mappers recomputing each frame
+// - maintainability one source of truth per component, no hook-per-prop
+// - locality a component's animated state lives in one value
+const COUNT = 24;
+const BASE_RADIUS = 6;
+const CANVAS_HEIGHT = 280;
+const COLORS = ["#8556E5", "#3EB489", "#FF7A1A", "#E5563F"];
+
+// Motion of dot `i` at time `t`, shared by both approaches so the animation
+// is visually identical — only the wiring underneath differs.
+const dotX = (t: number, i: number, cx: number, orbit: number) => {
+ "worklet";
+ const a = t + (i / COUNT) * Math.PI * 2;
+ const ring = 0.4 + 0.6 * ((i % 6) / 6);
+ return cx + Math.cos(a) * orbit * ring;
+};
+const dotY = (t: number, i: number, cy: number, orbit: number) => {
+ "worklet";
+ const a = t + (i / COUNT) * Math.PI * 2;
+ const ring = 0.4 + 0.6 * ((i % 6) / 6);
+ return cy + Math.sin(a) * orbit * ring;
+};
+const dotR = (t: number, i: number) => {
+ "worklet";
+ return BASE_RADIUS + (Math.sin(t * 2 + i) + 1) * 3;
+};
+
+interface SceneProps {
+ clock: SharedValue;
+ cx: number;
+ cy: number;
+ orbit: number;
+}
+
+// "Separate prop" approach: every prop of every dot gets its own derived
+// value, i.e. its own Reanimated mapper. COUNT * 3 mappers total.
+const DerivedDot = ({
+ clock,
+ i,
+ cx,
+ cy,
+ orbit,
+}: SceneProps & { i: number }) => {
+ const x = useDerivedValue(() => dotX(clock.value, i, cx, orbit));
+ const y = useDerivedValue(() => dotY(clock.value, i, cy, orbit));
+ const r = useDerivedValue(() => dotR(clock.value, i));
+ return ;
+};
+
+const DerivedScene = (props: SceneProps) => (
+ <>
+ {new Array(COUNT).fill(0).map((_, i) => (
+
+ ))}
+ >
+);
+
+// "Single value" approach: ONE derived value computes every coordinate into
+// an object, and each prop reads its own key from it. 1 mapper total.
+const SingleValueScene = ({ clock, cx, cy, orbit }: SceneProps) => {
+ const data = useDerivedValue(() => {
+ const t = clock.value;
+ const obj: Record = {};
+ for (let i = 0; i < COUNT; i++) {
+ obj[`x${i}`] = dotX(t, i, cx, orbit);
+ obj[`y${i}`] = dotY(t, i, cy, orbit);
+ obj[`r${i}`] = dotR(t, i);
+ }
+ return obj;
+ });
+ return (
+ <>
+ {new Array(COUNT).fill(0).map((_, i) => (
+
+ ))}
+ >
+ );
+};
+
+export const SharedValueComparison = () => {
+ const { width } = useWindowDimensions();
+ const [single, setSingle] = useState(true);
+
+ const clock = useSharedValue(0);
+ useFrameCallback(({ timeSinceFirstFrame }) => {
+ "worklet";
+ clock.value = timeSinceFirstFrame / 1000;
+ }, true);
+
+ const cx = width / 2;
+ const cy = CANVAS_HEIGHT / 2;
+ const orbit = Math.min(width, 360) / 2 - 24;
+ const mapperCount = single ? 1 : COUNT * 3;
+
+ return (
+
+
+ {`Same ${COUNT * 3}-prop animation, wired two ways. "Grouped" drives ` +
+ `every prop from one shared value; "Per-prop" gives each prop its ` +
+ `own derived value. Watch the Reanimated mapper count change.`}
+
+
+ setSingle(true)}
+ >
+
+ Grouped
+
+
+ setSingle(false)}
+ >
+
+ Per-prop
+
+
+
+
+ {`Reanimated mappers (animation subscriptions): ${mapperCount}`}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ subheading: {
+ color: "#444",
+ marginBottom: 12,
+ lineHeight: 18,
+ },
+ row: {
+ flexDirection: "row",
+ marginBottom: 8,
+ },
+ btn: {
+ flex: 1,
+ paddingVertical: 8,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: "#8556E5",
+ marginHorizontal: 4,
+ alignItems: "center",
+ },
+ btnActive: {
+ backgroundColor: "#8556E5",
+ },
+ btnText: {
+ color: "#8556E5",
+ fontWeight: "600",
+ },
+ btnTextActive: {
+ color: "white",
+ },
+ caption: {
+ color: "black",
+ marginBottom: 8,
+ fontVariant: ["tabular-nums"],
+ },
+ canvas: {
+ height: CANVAS_HEIGHT,
+ width: "100%" as const,
+ backgroundColor: "#FEFEFE" as const,
+ },
+});
diff --git a/apps/example/src/Examples/Reanimated/index.tsx b/apps/example/src/Examples/Reanimated/index.tsx
index ca9786bd07..35aea8553e 100644
--- a/apps/example/src/Examples/Reanimated/index.tsx
+++ b/apps/example/src/Examples/Reanimated/index.tsx
@@ -6,6 +6,8 @@ import { AnimateTextOnPath } from "./AnimateTextOnPath";
import { AnimationWithTouchHandler } from "./AnimationWithTouchHandler";
import { BokehExample } from "./BokehExample";
import { InterpolationWithEasing } from "./InterpolationWithEasing";
+import { NullableGroupedValue } from "./NullableGroupedValue";
+import { SharedValueComparison } from "./SharedValueComparison";
import { SimpleAnimation } from "./SimpleAnimation";
import { SpringBackTouchAnimation } from "./SpringBackTouch";
@@ -18,6 +20,8 @@ export const ReanimatedExample: React.FC = () => {
+
+
);
};
diff --git a/externals/depot_tools b/externals/depot_tools
index c46c2e9057..6afa997717 160000
--- a/externals/depot_tools
+++ b/externals/depot_tools
@@ -1 +1 @@
-Subproject commit c46c2e905741fa94ce6870f9c95978117b8a7917
+Subproject commit 6afa997717b2c0e1382e1465bedbe1a6855b9388
diff --git a/externals/skia b/externals/skia
index a7fb0b3c2e..2a9b593bab 160000
--- a/externals/skia
+++ b/externals/skia
@@ -1 +1 @@
-Subproject commit a7fb0b3c2e109e4e68f50bf823cf6b8d524c4a0c
+Subproject commit 2a9b593bab4b2fd019fa494c8d401ff1fab0b883
diff --git a/packages/skia/cpp/api/recorder/Convertor.h b/packages/skia/cpp/api/recorder/Convertor.h
index 6f15f3d904..f3c9e0c18a 100644
--- a/packages/skia/cpp/api/recorder/Convertor.h
+++ b/packages/skia/cpp/api/recorder/Convertor.h
@@ -69,7 +69,51 @@ template struct unwrap_optional> {
template
T getPropertyValue(jsi::Runtime &runtime, const jsi::Value &value);
-// Base template for convertProperty
+template
+bool convertSelectorProperty(jsi::Runtime &runtime, const jsi::Value &prop,
+ Target &target, Variables &variables) {
+ if (!prop.isObject()) {
+ return false;
+ }
+ auto wrapper = prop.asObject(runtime);
+ if (!wrapper.hasProperty(runtime, "__sv") ||
+ !wrapper.hasProperty(runtime, "__key")) {
+ return false;
+ }
+ auto svVal = wrapper.getProperty(runtime, "__sv");
+ auto keyVal = wrapper.getProperty(runtime, "__key");
+ if (!isSharedValue(runtime, svVal) || !keyVal.isString()) {
+ return false;
+ }
+ auto sharedValue = svVal.asObject(runtime);
+ auto key = keyVal.asString(runtime).utf8(runtime);
+ auto name =
+ sharedValue.getProperty(runtime, "name").asString(runtime).utf8(runtime);
+
+ auto conv = [target = &target, key](jsi::Runtime &runtime,
+ const jsi::Object &val) {
+ auto value = val.getProperty(runtime, "value");
+ if (!value.isObject()) {
+ return;
+ }
+ auto values = value.asObject(runtime);
+ if (!values.hasProperty(runtime, key.c_str())) {
+ return;
+ }
+
+ auto selected = values.getProperty(runtime, key.c_str());
+ if (selected.isUndefined() || selected.isNull() ||
+ (selected.isObject() && selected.asObject(runtime).isFunction(runtime))) {
+ return;
+ }
+ *target = getPropertyValue(runtime, selected);
+ };
+
+ variables[name].push_back(conv);
+ conv(runtime, sharedValue);
+ return true;
+}
+
template
void convertPropertyImpl(jsi::Runtime &runtime, const jsi::Object &object,
const std::string &propertyName, Target &target,
@@ -78,13 +122,16 @@ void convertPropertyImpl(jsi::Runtime &runtime, const jsi::Object &object,
return;
}
- auto property = object.getProperty(runtime, propertyName.c_str());
+ auto prop = object.getProperty(runtime, propertyName.c_str());
+
+ if (convertSelectorProperty(runtime, prop, target, variables)) {
+ return;
+ }
- if (isSharedValue(runtime, property)) {
- auto sharedValue = property.asObject(runtime);
- auto name = sharedValue.getProperty(runtime, "name")
- .asString(runtime)
- .utf8(runtime);
+ if (isSharedValue(runtime, prop)) {
+ auto sharedValue = prop.asObject(runtime);
+ auto name =
+ sharedValue.getProperty(runtime, "name").asString(runtime).utf8(runtime);
auto conv = [target = &target](jsi::Runtime &runtime,
const jsi::Object &val) {
auto value = val.getProperty(runtime, "value");
@@ -92,9 +139,10 @@ void convertPropertyImpl(jsi::Runtime &runtime, const jsi::Object &object,
};
variables[name].push_back(conv);
conv(runtime, sharedValue);
- } else {
- target = getPropertyValue(runtime, property);
+ return;
}
+
+ target = getPropertyValue(runtime, prop);
}
// Main convertProperty template
diff --git a/packages/skia/cpp/api/third_party/SkottieUtils.cpp b/packages/skia/cpp/api/third_party/SkottieUtils.cpp
index 98d8156e3f..c5657b881c 100644
--- a/packages/skia/cpp/api/third_party/SkottieUtils.cpp
+++ b/packages/skia/cpp/api/third_party/SkottieUtils.cpp
@@ -10,7 +10,7 @@
#include "include/core/SkData.h"
#include "include/core/SkRect.h"
#include "include/core/SkSize.h"
-#include "include/private/base/SkAssert.h"
+#include "include/private/SkAssert.h"
#include "modules/skottie/include/Skottie.h"
#include "modules/skresources/include/SkResources.h"
diff --git a/packages/skia/package.json b/packages/skia/package.json
index b8694e1685..815dcb70bc 100644
--- a/packages/skia/package.json
+++ b/packages/skia/package.json
@@ -139,16 +139,16 @@
},
"dependencies": {
"canvaskit-wasm": "0.41.0",
- "react-native-skia-android": "150.0.0",
- "react-native-skia-apple-ios": "150.0.0",
- "react-native-skia-apple-macos": "150.0.0",
- "react-native-skia-apple-tvos": "150.0.0",
+ "react-native-skia-android": "152.0.0",
+ "react-native-skia-apple-ios": "152.0.0",
+ "react-native-skia-apple-macos": "152.0.0",
+ "react-native-skia-apple-tvos": "152.0.0",
"react-reconciler": "0.31.0"
},
"graphiteDependencies": {
- "react-native-skia-graphite-android": "150.0.0",
- "react-native-skia-graphite-apple-ios": "150.0.0",
- "react-native-skia-graphite-apple-macos": "150.0.0"
+ "react-native-skia-graphite-android": "152.0.0",
+ "react-native-skia-graphite-apple-ios": "152.0.0",
+ "react-native-skia-graphite-apple-macos": "152.0.0"
},
"eslintIgnore": [
"node_modules/",
diff --git a/packages/skia/scripts/build-skia.ts b/packages/skia/scripts/build-skia.ts
index 8761213e8a..7d667b8ace 100644
--- a/packages/skia/scripts/build-skia.ts
+++ b/packages/skia/scripts/build-skia.ts
@@ -310,11 +310,6 @@ const buildXCFramework = (platformName: ApplePlatformName) => {
const arm64ePatchFile = path.join(__dirname, "dawn-arm64e-simulator.patch");
$(`cd ${SkiaSrc} && git apply ${arm64ePatchFile}`);
- // Implement drawAtlas for the Graphite backend (removes the no-op override
- // so the inherited drawVertices-based default lights up the API).
- const drawAtlasPatchFile = path.join(__dirname, "graphite-drawatlas.patch");
- $(`cd ${SkiaSrc} && git apply ${drawAtlasPatchFile}`);
-
// Remove arm64e arch flags (not available on simulator)
{
const filePath = `${SkiaSrc}/gn/skia/BUILD.gn`;
diff --git a/packages/skia/scripts/graphite-drawatlas.patch b/packages/skia/scripts/graphite-drawatlas.patch
deleted file mode 100644
index 07eae3acc8..0000000000
--- a/packages/skia/scripts/graphite-drawatlas.patch
+++ /dev/null
@@ -1,57 +0,0 @@
-../../packages/skia/scripts/graphite-drawatlas.patch From 4d25f90dae867435707631ce0e16d2f8899ae29d Mon Sep 17 00:00:00 2001
-From: Claude
-Date: Fri, 19 Jun 2026 13:54:32 +0000
-Subject: [PATCH] Implement drawAtlas for the Graphite backend
-
-The Graphite Device overrode SkDevice::drawAtlas() with an empty no-op,
-so SkCanvas::drawAtlas() silently drew nothing on Graphite.
-
-drawAtlas is definitionally "drawVertices with an image shader": the
-canvas layer (SkCanvas::onDrawAtlas2) already installs the atlas image
-as the paint's shader, and SkDevice's default drawAtlas() expands the
-RSXform/tex/color spans into an SkVertices and forwards to drawVertices().
-Graphite already fully implements drawVertices() (per-vertex colors,
-primitive blender, and shader-sourced texture coordinates), so removing
-the no-op override lets the inherited implementation light the API up.
-
-This mirrors how Graphite already defers drawRegion and drawPatch to
-their default drawVertices/drawPath/drawRect routing instead of
-specializing them.
-
-Co-Authored-By: Claude Opus 4.8
-Claude-Session: https://claude.ai/code/session_01LDRiP7kX2Wa8pD7ewEuhFf
----
- src/gpu/graphite/Device.h | 8 +++-----
- 1 file changed, 3 insertions(+), 5 deletions(-)
-
-diff --git a/src/gpu/graphite/Device.h b/src/gpu/graphite/Device.h
-index c5669d2..8b5e9ad 100644
---- a/src/gpu/graphite/Device.h
-+++ b/src/gpu/graphite/Device.h
-@@ -210,8 +210,8 @@ public:
- void drawPath(const SkPath& path, const SkPaint&) override;
- void drawDRRect(const SkRRect& outer, const SkRRect& inner, const SkPaint&) override;
-
-- // No need to specialize drawRegion or drawPatch as the default impls all route to drawPath,
-- // drawRect, or drawVertices as desired.
-+ // No need to specialize drawRegion, drawPatch, or drawAtlas as the default impls all route to
-+ // drawPath, drawRect, or drawVertices as desired.
-
- void drawEdgeAAQuad(const SkRect& rect, const SkPoint clip[4],
- SkCanvas::QuadAAFlags aaFlags, const SkColor4f& color,
-@@ -234,11 +234,9 @@ public:
- const SkSamplingOptions&,
- const SkPaint&,
- SkCanvas::SrcRectConstraint) override;
-- // TODO: Implement these using per-edge AA quads and an inlined image shader program.
-+ // TODO: Implement this using per-edge AA quads and an inlined image shader program.
- void drawImageLattice(const SkImage*, const SkCanvas::Lattice&,
- const SkRect& dst, SkFilterMode, const SkPaint&) override {}
-- void drawAtlas(SkSpan, SkSpan, SkSpan,
-- sk_sp, const SkPaint&) override {}
-
- void drawDrawable(SkCanvas*, SkDrawable*, const SkMatrix*) override {}
- void drawMesh(const SkMesh&, sk_sp, const SkPaint&) override {}
---
-2.43.0
-
diff --git a/packages/skia/scripts/install-skia-graphite.ts b/packages/skia/scripts/install-skia-graphite.ts
index e8056fabf0..d177a95b01 100644
--- a/packages/skia/scripts/install-skia-graphite.ts
+++ b/packages/skia/scripts/install-skia-graphite.ts
@@ -29,27 +29,32 @@ import { fileOps } from "./utils";
// Graphite configuration
const GRAPHITE_CONFIG = {
- version: "m150",
+ version: "m152",
checksums: {
"android-armeabi-v7a":
- "c8a1f9d259599280b737497a914e6fcb1b47fbf6e59537cffe3e3ebbc3aa0394",
+ "edc363fb63d3e629d7023d63d505c2d075030dfc40e04890d988eaff5c31c2fc",
"android-arm64-v8a":
- "21667587386ebbaba1b61926f81dc1562443eefec5d1489b9278084f18ebb8e4",
+ "cefc18191d46deec3e164f3b717b06c6ae7845eeec21fe11cca53339063cf1e2",
"android-x86":
- "0679a34612b98b5397180e027f7592026eb4af950b36e55bf7882ea86e2cb1e3",
+ "78041f4d58fafda1821c96f83e4818cce492a7047b16f267527e3e140d0b4c0d",
"android-x86_64":
- "eec546cf240e76129e9e6d16e51c61acfd62b7c00d655b133eddfab890c3488b",
+ "8cfcd40b87b42aa0aaaba89852aa51f3316c90debcdf24dcc5a9b7b562a8aa6f",
"apple-ios-xcframeworks":
- "0ad5434961b22a59541c0364be20a54c5cde599c1ffd4d6b653fefb19c2119fc",
+ "9a64915dd95bf22ab3f38624ac2fc7e4a14cfd155f218170317ccef6ab9dfc69",
"apple-macos-xcframeworks":
- "e3861d45386309dfed851488293027f3026a35684007595071426f4e46bef023",
+ "995cc77d575368d2df08a16ed0345395ba66308b00ba57ef190e70eb710d74f9",
},
} as const;
// Dawn prebuilt binaries. These are the exact artifacts react-native-webgpu
// links; both packages must consume the same Dawn build so that only one Dawn
-// copy exists in an app that installs both. The Dawn commit is the one from
-// this Skia milestone's DEPS.
+// copy exists in an app that installs both, which is why the release tag is
+// pinned here rather than derived from GRAPHITE_CONFIG.version.
+//
+// STALE: this is still the m150 Dawn (63f25feec51e9351fb25222b6d5de1af791d7c4f)
+// while m152's DEPS pins 1e897275172a23f27b0022fa6beae3084ed54a9b. Bump this to
+// dawn-chrome-m152 (with new checksums) once react-native-webgpu publishes that
+// release; until then Graphite installs pair m152 Skia with m150 Dawn.
const DAWN_CONFIG = {
releaseTag: "dawn-chrome-m150",
baseUrl:
diff --git a/packages/skia/scripts/skia-configuration.ts b/packages/skia/scripts/skia-configuration.ts
index eb7a0377ca..ccbb9de98a 100644
--- a/packages/skia/scripts/skia-configuration.ts
+++ b/packages/skia/scripts/skia-configuration.ts
@@ -88,6 +88,11 @@ export const commonArgs = [
//["skia_enable_ganesh", !GRAPHITE],
["skia_enable_graphite", GRAPHITE],
["skia_use_dawn", GRAPHITE],
+ // m152 turns PartitionAlloc on by default for clang builds, which leaves
+ // raw_ptr/PartitionAddressSpace symbols undefined when linking against the
+ // prebuilt libskia.a (the allocator lives in its own target we don't ship).
+ // Skia's noop raw_ptr shims are used instead.
+ ["skia_use_partition_alloc", false],
// C++20 is required for Graphite builds (Dawn uses C++20 concepts)
// Passed via extra_cflags_cc per-target instead of skia_use_cpp20 (not available in all Skia versions)
];
@@ -635,31 +640,30 @@ export const copyHeaders = () => {
console.log("✅ Skia headers copied successfully");
- // Copy src/base files
- fileOps.mkdir("./cpp/skia/src/base");
+ // These used to live in src/base; they were folded into src/core in m152.
fileOps.cp(
- "../../externals/skia/src/base/SkTLazy.h",
- "./cpp/skia/src/base/SkTLazy.h"
+ "../../externals/skia/src/core/SkTLazy.h",
+ "./cpp/skia/src/core/SkTLazy.h"
);
fileOps.cp(
- "../../externals/skia/src/base/SkMathPriv.h",
- "./cpp/skia/src/base/SkMathPriv.h"
+ "../../externals/skia/src/core/SkMathPriv.h",
+ "./cpp/skia/src/core/SkMathPriv.h"
);
fileOps.cp(
- "../../externals/skia/src/base/SkTInternalLList.h",
- "./cpp/skia/src/base/SkTInternalLList.h"
+ "../../externals/skia/src/core/SkTInternalLList.h",
+ "./cpp/skia/src/core/SkTInternalLList.h"
);
fileOps.cp(
- "../../externals/skia/src/base/SkUTF.h",
- "./cpp/skia/src/base/SkUTF.h"
+ "../../externals/skia/src/core/SkUTF.h",
+ "./cpp/skia/src/core/SkUTF.h"
);
fileOps.cp(
- "../../externals/skia/src/base/SkArenaAlloc.h",
- "./cpp/skia/src/base/SkArenaAlloc.h"
+ "../../externals/skia/src/core/SkArenaAlloc.h",
+ "./cpp/skia/src/core/SkArenaAlloc.h"
);
fileOps.cp(
- "../../externals/skia/src/base/SkAutoLocaleSetter.h",
- "./cpp/skia/src/base/SkAutoLocaleSetter.h"
+ "../../externals/skia/src/core/SkAutoLocaleSetter.h",
+ "./cpp/skia/src/core/SkAutoLocaleSetter.h"
);
// Copy skunicode
diff --git a/packages/skia/src/renderer/processors/Animations/Animations.ts b/packages/skia/src/renderer/processors/Animations/Animations.ts
index 1fd34c79f1..229b155a40 100644
--- a/packages/skia/src/renderer/processors/Animations/Animations.ts
+++ b/packages/skia/src/renderer/processors/Animations/Animations.ts
@@ -1,4 +1,37 @@
-export type AnimatedProp = T | { value: T };
+import type { SharedValue } from "react-native-reanimated";
+
+/**
+ * Binds a prop to a single key of a "grouped" shared value: one shared value
+ * whose `.value` is an object can drive many props (one key each) instead of
+ * requiring a separate shared value per prop.
+ *
+ * Created via {@link select}. `T` is the type of the selected value; `__type`
+ * is a phantom marker carrying that type and is never present at runtime.
+ */
+export type SharedValueSelector = {
+ __sv: { value: Record };
+ __key: string;
+ readonly __type?: T;
+};
+
+export type AnimatedProp = T | { value: T } | SharedValueSelector;
+
+/**
+ * Selects a single key of a shared value so it can drive an animated prop.
+ * This lets one shared value (whose value is an object) drive multiple props
+ * — one per key — instead of using a separate shared value for each.
+ *
+ * @example
+ * const data = useSharedValue({ x: 0, y: 0, r: 10 });
+ *
+ */
+export const select = (
+ value: SharedValue,
+ key: K
+): SharedValueSelector => ({
+ __sv: value as unknown as { value: Record },
+ __key: key,
+});
export type AnimatedProps = {
[K in keyof T]: K extends "children"
diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts
index da0106467a..8ced233975 100644
--- a/packages/skia/src/sksg/Recorder/Core.ts
+++ b/packages/skia/src/sksg/Recorder/Core.ts
@@ -23,6 +23,7 @@ import type {
DrawingNodeProps,
SkottieProps,
} from "../../dom/types";
+import { isSharedValueSelector } from "../utils";
export enum CommandType {
// Context
@@ -79,7 +80,16 @@ export const materializeCommand = (command: any) => {
const newProps = { ...command.props };
if (command.animatedProps) {
for (const key in command.animatedProps) {
- newProps[key] = command.animatedProps[key].value;
+ const entry = command.animatedProps[key];
+ if (isSharedValueSelector(entry)) {
+ const group = entry.__sv.value;
+ newProps[key] =
+ group && typeof group === "object"
+ ? (group as Record)[entry.__key]
+ : undefined;
+ } else {
+ newProps[key] = entry.value;
+ }
}
}
return { ...command, props: newProps };
diff --git a/packages/skia/src/sksg/Recorder/ReanimatedRecorder.ts b/packages/skia/src/sksg/Recorder/ReanimatedRecorder.ts
index c18d5e272a..2421981a28 100644
--- a/packages/skia/src/sksg/Recorder/ReanimatedRecorder.ts
+++ b/packages/skia/src/sksg/Recorder/ReanimatedRecorder.ts
@@ -31,7 +31,7 @@ import type {
DrawingNodeProps,
} from "../../dom/types";
import type { AnimatedProps } from "../../renderer";
-import { isSharedValue } from "../utils";
+import { isSharedValue, isSharedValueSelector } from "../utils";
/**
* Currently the recorder only work if the GPU resources (e.g Images) are owned by the main thread.
@@ -55,6 +55,15 @@ export class ReanimatedRecorder implements BaseRecorder {
return;
}
Object.values(props).forEach((value) => {
+ if (isSharedValueSelector(value) && !this.values.has(value.__sv)) {
+ const sv = value.__sv;
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ sv.name = `variable${this.values.size}`;
+ this.values.add(sv as SharedValue);
+ return;
+ }
+
if (isSharedValue(value) && !this.values.has(value)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts
index f80916cf71..a01f3ce4bb 100644
--- a/packages/skia/src/sksg/Recorder/Recorder.ts
+++ b/packages/skia/src/sksg/Recorder/Recorder.ts
@@ -30,7 +30,7 @@ import type {
DrawingNodeProps,
} from "../../dom/types";
import type { AnimatedProps } from "../../renderer";
-import { isSharedValue } from "../utils";
+import { isSharedValue, isSharedValueSelector } from "../utils";
import { isColorFilter, isImageFilter, isPathEffect, isShader } from "../Node";
import type { SkPaint, SkPicture, BaseRecorder } from "../../skia/types";
@@ -78,7 +78,7 @@ export class Recorder implements BaseRecorder {
}
private processProps(props: Record) {
- const animatedProps: Record> = {};
+ const animatedProps: Record = {};
let hasAnimatedProps = false;
for (const key in props) {
@@ -87,6 +87,10 @@ export class Recorder implements BaseRecorder {
this.animationValues.add(prop);
animatedProps[key] = prop;
hasAnimatedProps = true;
+ } else if (isSharedValueSelector(prop)) {
+ this.animationValues.add(prop.__sv);
+ animatedProps[key] = prop;
+ hasAnimatedProps = true;
}
}
@@ -222,7 +226,7 @@ export class Recorder implements BaseRecorder {
boxProps: AnimatedProps,
shadows: {
props: BoxShadowProps;
- animatedProps?: Record>;
+ animatedProps?: Record;
}[]
) {
shadows.forEach((shadow) => {
diff --git a/packages/skia/src/sksg/__tests__/SharedValueSelector.spec.ts b/packages/skia/src/sksg/__tests__/SharedValueSelector.spec.ts
new file mode 100644
index 0000000000..57dca67d5e
--- /dev/null
+++ b/packages/skia/src/sksg/__tests__/SharedValueSelector.spec.ts
@@ -0,0 +1,107 @@
+import type { SharedValue } from "react-native-reanimated";
+
+import { select } from "../../renderer/processors/Animations/Animations";
+import { isSharedValueSelector } from "../utils";
+import { materializeCommand, CommandType } from "../Recorder/Core";
+import { Recorder } from "../Recorder/Recorder";
+
+// Minimal stand-in for a Reanimated shared value: the guard only checks the
+// `_isReanimatedSharedValue` flag, so we don't need the Reanimated runtime.
+const makeSharedValue = (value: T) =>
+ ({ _isReanimatedSharedValue: true, value }) as unknown as SharedValue;
+
+describe("isSharedValueSelector", () => {
+ it("returns true for a selector", () => {
+ const sv = makeSharedValue({ x: 1 });
+ expect(isSharedValueSelector({ __sv: sv, __key: "x" })).toBe(true);
+ });
+
+ it("returns false for a plain shared value", () => {
+ expect(isSharedValueSelector(makeSharedValue(1))).toBe(false);
+ });
+
+ it("returns false for invalid shapes", () => {
+ expect(isSharedValueSelector(null)).toBe(false);
+ expect(isSharedValueSelector(42)).toBe(false);
+ expect(isSharedValueSelector({})).toBe(false);
+ // __sv missing
+ expect(isSharedValueSelector({ __key: "x" })).toBe(false);
+ // __sv is not a shared value
+ expect(isSharedValueSelector({ __sv: { value: {} }, __key: "x" })).toBe(
+ false
+ );
+ // __key missing
+ expect(isSharedValueSelector({ __sv: makeSharedValue({}) })).toBe(false);
+ // __key is not a string
+ expect(isSharedValueSelector({ __sv: makeSharedValue({}), __key: 3 })).toBe(
+ false
+ );
+ });
+});
+
+describe("select", () => {
+ it("creates a selector the guard recognizes", () => {
+ const sv = makeSharedValue({ x: 10, y: 20 });
+ const selector = select(sv, "x");
+ expect(selector).toEqual({ __sv: sv, __key: "x" });
+ expect(isSharedValueSelector(selector)).toBe(true);
+ });
+});
+
+describe("materializeCommand", () => {
+ it("resolves a plain shared value via .value", () => {
+ const command = {
+ type: CommandType.DrawCircle,
+ props: { r: 0 },
+ animatedProps: { r: makeSharedValue(10) },
+ };
+ expect(materializeCommand(command).props.r).toBe(10);
+ });
+
+ it("resolves a selector via __sv.value[__key]", () => {
+ const sv = makeSharedValue({ x: 5, y: 8 });
+ const command = {
+ type: CommandType.DrawCircle,
+ props: { cx: 0, cy: 0 },
+ animatedProps: { cx: select(sv, "x"), cy: select(sv, "y") },
+ };
+ const { props } = materializeCommand(command);
+ expect(props.cx).toBe(5);
+ expect(props.cy).toBe(8);
+ });
+
+ it("does not crash when the grouped value is null", () => {
+ const sv = makeSharedValue<{ x: number }>(null as unknown as { x: number });
+ const command = {
+ type: CommandType.DrawCircle,
+ props: { cx: 0 },
+ animatedProps: { cx: select(sv, "x") },
+ };
+ expect(() => materializeCommand(command)).not.toThrow();
+ expect(materializeCommand(command).props.cx).toBeUndefined();
+ });
+});
+
+describe("Recorder selector collection (web path)", () => {
+ it("registers one shared value for many selectors of the same value", () => {
+ const sv = makeSharedValue({ x: 1, y: 2 });
+ const recorder = new Recorder();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const props: any = { cx: select(sv, "x"), cy: select(sv, "y"), r: 3 };
+ recorder.drawCircle(props);
+ const { animationValues } = recorder.getRecording();
+ expect(animationValues.size).toBe(1);
+ expect([...animationValues][0]).toBe(sv);
+ });
+
+ it("registers a plain shared value and a selector's underlying value", () => {
+ const grouped = makeSharedValue({ x: 1 });
+ const plain = makeSharedValue(5);
+ const recorder = new Recorder();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const props: any = { cx: select(grouped, "x"), r: plain };
+ recorder.drawCircle(props);
+ const { animationValues } = recorder.getRecording();
+ expect(animationValues.size).toBe(2);
+ });
+});
diff --git a/packages/skia/src/sksg/utils.ts b/packages/skia/src/sksg/utils.ts
index 66945d036d..366f31321a 100644
--- a/packages/skia/src/sksg/utils.ts
+++ b/packages/skia/src/sksg/utils.ts
@@ -10,6 +10,17 @@ export const isSharedValue = (
return (value as Record)?._isReanimatedSharedValue === true;
};
+export const isSharedValueSelector = (
+ value: unknown
+): value is { __sv: SharedValue; __key: string } => {
+ "worklet";
+ if (!value || typeof value !== "object") {
+ return false;
+ }
+ const obj = value as Record;
+ return isSharedValue(obj.__sv) && typeof obj.__key === "string";
+};
+
export const materialize = (props: T) => {
"worklet";
const result: T = Object.assign({}, props);
@@ -17,6 +28,13 @@ export const materialize = (props: T) => {
const value = result[key];
if (isSharedValue(value)) {
result[key] = value.value as never;
+ } else if (isSharedValueSelector(value)) {
+ const group = value.__sv.value;
+ result[key] = (
+ group && typeof group === "object"
+ ? (group as Record)[value.__key]
+ : undefined
+ ) as never;
}
});
return result;
diff --git a/yarn.lock b/yarn.lock
index c749fbdce0..fda999bd1c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9102,10 +9102,10 @@ __metadata:
react-native: 0.83.1
react-native-builder-bob: 0.18.2
react-native-reanimated: 4.3.1
- react-native-skia-android: 150.0.0
- react-native-skia-apple-ios: 150.0.0
- react-native-skia-apple-macos: 150.0.0
- react-native-skia-apple-tvos: 150.0.0
+ react-native-skia-android: 152.0.0
+ react-native-skia-apple-ios: 152.0.0
+ react-native-skia-apple-macos: 152.0.0
+ react-native-skia-apple-tvos: 152.0.0
react-native-worklets: 0.8.3
react-reconciler: 0.31.0
rimraf: 3.0.2
@@ -27158,31 +27158,31 @@ __metadata:
languageName: node
linkType: hard
-"react-native-skia-android@npm:150.0.0":
- version: 150.0.0
- resolution: "react-native-skia-android@npm:150.0.0"
- checksum: e33eecf360925bc0402c1d6b0db5845f2a38d19244f9c128b7c208abb7dc2c07158cdfcd0c998c31271075ce8ad8a3b1bed329cd010a9df9a3ae58a752e8fded
+"react-native-skia-android@npm:152.0.0":
+ version: 152.0.0
+ resolution: "react-native-skia-android@npm:152.0.0"
+ checksum: c01ae79040ea04997340861828d79fb4591ad922a50a94cbcdb1b4d7f4e7fa1ad9a892cdb87f40a059489350cb2cfdd647448570d109dcab25342d9d287d1bbc
languageName: node
linkType: hard
-"react-native-skia-apple-ios@npm:150.0.0":
- version: 150.0.0
- resolution: "react-native-skia-apple-ios@npm:150.0.0"
- checksum: d96bfdf9b5bd8eb038b29b4949f591acd9064b00e8d4229f8932b8a9e7bbcf206c6e57a32f1ad0ba6b58ff033388a6dbe059f73d9eb91686afb0f8a3742e5392
+"react-native-skia-apple-ios@npm:152.0.0":
+ version: 152.0.0
+ resolution: "react-native-skia-apple-ios@npm:152.0.0"
+ checksum: 42ef9929664d197eaf1116e172573ad1ed68944b7349ee692bb542761fd4aa6d171d23a293ee64b0a1f0d0bf3c6cd2f1f32ef8bda2b4dd1749466af5f25dd8a3
languageName: node
linkType: hard
-"react-native-skia-apple-macos@npm:150.0.0":
- version: 150.0.0
- resolution: "react-native-skia-apple-macos@npm:150.0.0"
- checksum: 0963df1f238a03a6390e9c11eb3cc662a81db5ad2daee77eb7555f35f4ccf06ac32e94c37d42704a33bf8b09d1ebb31193361a5183662df29b9e28f10aa6937d
+"react-native-skia-apple-macos@npm:152.0.0":
+ version: 152.0.0
+ resolution: "react-native-skia-apple-macos@npm:152.0.0"
+ checksum: d8e220ce0ac0f41c35916c5b416cbdb6436ad4189884a70524949949df8990a43c5d116865932dd244dad412e77cab368f3f820e1400b3bd00736ee37435810b
languageName: node
linkType: hard
-"react-native-skia-apple-tvos@npm:150.0.0":
- version: 150.0.0
- resolution: "react-native-skia-apple-tvos@npm:150.0.0"
- checksum: c96428599c75febf5f0ea9a9282d28dd100be1ddc5953cbd6afc845379ef24de2fd3e8f3643a16c1d864b395ce225f01e35621e613e88edcb5b0b9ba78011a3e
+"react-native-skia-apple-tvos@npm:152.0.0":
+ version: 152.0.0
+ resolution: "react-native-skia-apple-tvos@npm:152.0.0"
+ checksum: ae80993eb127b5ee4502b0428204b980347d6cf31444de74984ae64373e1bc4a992b79ddae0aa9983cd3a6e5b979ae5d810f88eca92976b40907fdb578b508d5
languageName: node
linkType: hard