@shopify/react-native-skia 2.12.0-next.1 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/Package.swift +117 -0
  2. package/android/CMakeLists.txt +1 -2
  3. package/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h +0 -10
  4. package/android/src/main/java/com/shopify/reactnative/skia/ViewScreenshotService.java +9 -12
  5. package/apple/RNSkApplePlatformContext.h +0 -3
  6. package/apple/RNSkApplePlatformContext.mm +0 -11
  7. package/cpp/api/JsiSkApi.h +0 -6
  8. package/cpp/api/JsiSkFont.h +5 -10
  9. package/cpp/api/JsiSkMatrix.h +4 -2
  10. package/cpp/api/JsiSkSVGFactory.h +4 -0
  11. package/cpp/api/recorder/Convertor.h +5 -2
  12. package/cpp/api/recorder/Drawings.h +13 -3
  13. package/cpp/api/recorder/ImageFilters.h +15 -1
  14. package/cpp/api/recorder/Paint.h +8 -1
  15. package/cpp/jsi/JSICache.cpp +99 -0
  16. package/cpp/jsi/JSICache.h +90 -0
  17. package/cpp/jsi/NativeObject.h +38 -87
  18. package/cpp/jsi/ViewProperty.h +0 -24
  19. package/cpp/rnskia/RNDawnContext.h +2 -1
  20. package/cpp/rnskia/RNDawnWindowContext.h +22 -0
  21. package/cpp/rnskia/RNSkManager.cpp +0 -6
  22. package/cpp/rnskia/RNSkPlatformContext.h +0 -3
  23. package/cpp/skia/include/core/RasterContext.h +25 -0
  24. package/cpp/skia/include/core/SkCanvas.h +0 -1
  25. package/cpp/skia/include/core/SkContext.h +62 -0
  26. package/cpp/skia/include/core/SkContextOptions.h +15 -0
  27. package/cpp/skia/include/core/SkMilestone.h +1 -1
  28. package/cpp/skia/include/core/SkPicture.h +36 -18
  29. package/cpp/skia/include/core/SkRecorder.h +8 -0
  30. package/cpp/skia/include/core/SkSerialProcs.h +2 -6
  31. package/cpp/skia/include/effects/SkImageFilters.h +15 -2
  32. package/cpp/skia/include/gpu/ganesh/GrDirectContext.h +73 -47
  33. package/cpp/skia/include/gpu/ganesh/SkSurfaceGanesh.h +7 -4
  34. package/cpp/skia/include/gpu/ganesh/d3d/GrD3DDirectContext.h +12 -0
  35. package/cpp/skia/include/gpu/ganesh/gl/GrGLDirectContext.h +11 -0
  36. package/cpp/skia/include/gpu/ganesh/mtl/GrMtlDirectContext.h +11 -0
  37. package/cpp/skia/include/gpu/ganesh/vk/GrVkDirectContext.h +13 -0
  38. package/cpp/skia/include/gpu/graphite/ContextOptions.h +6 -0
  39. package/cpp/skia/include/gpu/graphite/GraphiteTypes.h +5 -4
  40. package/cpp/skia/include/gpu/graphite/PrecompileContext.h +9 -1
  41. package/cpp/skia/include/gpu/graphite/Recorder.h +0 -2
  42. package/cpp/skia/include/gpu/graphite/Recording.h +3 -0
  43. package/cpp/skia/include/gpu/graphite/dawn/DawnBackendContext.h +8 -0
  44. package/cpp/skia/include/gpu/graphite/mtl/MtlBackendContext.h +11 -0
  45. package/cpp/skia/include/gpu/graphite/vk/VulkanGraphiteContext.h +11 -0
  46. package/cpp/skia/include/private/SkLog.h +1 -19
  47. package/cpp/skia/include/private/chromium/GrVkSecondaryCBDrawContext.h +2 -1
  48. package/cpp/skia/include/utils/SkLogHandler.h +53 -0
  49. package/cpp/skia/include/utils/SkNWayCanvas.h +1 -0
  50. package/cpp/skia/modules/skcms/src/Transform_inl.h +9 -10
  51. package/cpp/skia/modules/skcms/src/skcms_internals.h +8 -3
  52. package/cpp/skia/modules/skottie/include/SkottieProperty.h +2 -0
  53. package/cpp/skia/modules/skottie/include/TextShaper.h +1 -0
  54. package/cpp/skia/modules/skshaper/include/SkShaper.h +23 -1
  55. package/include/ReactNativeSkia.h +6 -0
  56. package/lib/commonjs/dom/nodes/datatypes/Rect.js +5 -1
  57. package/lib/commonjs/dom/nodes/datatypes/Rect.js.map +1 -1
  58. package/lib/commonjs/external/reanimated/useVideo.js +8 -2
  59. package/lib/commonjs/external/reanimated/useVideo.js.map +1 -1
  60. package/lib/commonjs/renderer/components/Mask.js +3 -9
  61. package/lib/commonjs/renderer/components/Mask.js.map +1 -1
  62. package/lib/commonjs/skia/types/JsiInstance.d.ts +1 -0
  63. package/lib/commonjs/skia/types/JsiInstance.js.map +1 -1
  64. package/lib/commonjs/skia/types/Matrix4.js +7 -2
  65. package/lib/commonjs/skia/types/Matrix4.js.map +1 -1
  66. package/lib/commonjs/skia/types/Skia.d.ts +1 -6
  67. package/lib/commonjs/skia/types/Skia.js.map +1 -1
  68. package/lib/commonjs/skia/web/JsiSkCanvas.js +4 -4
  69. package/lib/commonjs/skia/web/JsiSkCanvas.js.map +1 -1
  70. package/lib/commonjs/skia/web/JsiSkImage.js +6 -3
  71. package/lib/commonjs/skia/web/JsiSkImage.js.map +1 -1
  72. package/lib/commonjs/skia/web/JsiSkPathBuilder.d.ts +1 -1
  73. package/lib/commonjs/skia/web/JsiSkPathBuilder.js +2 -2
  74. package/lib/commonjs/skia/web/JsiSkPathBuilder.js.map +1 -1
  75. package/lib/commonjs/skia/web/JsiSkTextStyle.js +5 -5
  76. package/lib/commonjs/skia/web/JsiSkTextStyle.js.map +1 -1
  77. package/lib/commonjs/skia/web/JsiSkia.js +0 -3
  78. package/lib/commonjs/skia/web/JsiSkia.js.map +1 -1
  79. package/lib/commonjs/skia/web/JsiSkottieAnimation.js +8 -8
  80. package/lib/commonjs/skia/web/JsiSkottieAnimation.js.map +1 -1
  81. package/lib/commonjs/sksg/Recorder/DrawingContext.d.ts +1 -0
  82. package/lib/commonjs/sksg/Recorder/DrawingContext.js +15 -0
  83. package/lib/commonjs/sksg/Recorder/DrawingContext.js.map +1 -1
  84. package/lib/commonjs/sksg/Recorder/Player.js +1 -1
  85. package/lib/commonjs/sksg/Recorder/Player.js.map +1 -1
  86. package/lib/commonjs/sksg/Recorder/Visitor.js +6 -1
  87. package/lib/commonjs/sksg/Recorder/Visitor.js.map +1 -1
  88. package/lib/commonjs/sksg/Recorder/commands/Drawing.js +12 -9
  89. package/lib/commonjs/sksg/Recorder/commands/Drawing.js.map +1 -1
  90. package/lib/commonjs/sksg/Recorder/commands/ImageFilters.js +15 -1
  91. package/lib/commonjs/sksg/Recorder/commands/ImageFilters.js.map +1 -1
  92. package/lib/commonjs/sksg/Recorder/commands/Shaders.js +1 -1
  93. package/lib/commonjs/sksg/Recorder/commands/Shaders.js.map +1 -1
  94. package/lib/commonjs/views/SkiaPictureView.web.js +247 -57
  95. package/lib/commonjs/views/SkiaPictureView.web.js.map +1 -1
  96. package/lib/module/dom/nodes/datatypes/Rect.js +5 -1
  97. package/lib/module/dom/nodes/datatypes/Rect.js.map +1 -1
  98. package/lib/module/external/reanimated/useVideo.js +8 -2
  99. package/lib/module/external/reanimated/useVideo.js.map +1 -1
  100. package/lib/module/renderer/components/Mask.js +3 -9
  101. package/lib/module/renderer/components/Mask.js.map +1 -1
  102. package/lib/module/skia/types/JsiInstance.d.ts +1 -0
  103. package/lib/module/skia/types/JsiInstance.js.map +1 -1
  104. package/lib/module/skia/types/Matrix4.js +7 -2
  105. package/lib/module/skia/types/Matrix4.js.map +1 -1
  106. package/lib/module/skia/types/Skia.d.ts +1 -6
  107. package/lib/module/skia/types/Skia.js.map +1 -1
  108. package/lib/module/skia/web/JsiSkCanvas.js +4 -4
  109. package/lib/module/skia/web/JsiSkCanvas.js.map +1 -1
  110. package/lib/module/skia/web/JsiSkImage.js +6 -3
  111. package/lib/module/skia/web/JsiSkImage.js.map +1 -1
  112. package/lib/module/skia/web/JsiSkPathBuilder.d.ts +1 -1
  113. package/lib/module/skia/web/JsiSkPathBuilder.js +2 -2
  114. package/lib/module/skia/web/JsiSkPathBuilder.js.map +1 -1
  115. package/lib/module/skia/web/JsiSkTextStyle.js +5 -5
  116. package/lib/module/skia/web/JsiSkTextStyle.js.map +1 -1
  117. package/lib/module/skia/web/JsiSkia.js +0 -3
  118. package/lib/module/skia/web/JsiSkia.js.map +1 -1
  119. package/lib/module/skia/web/JsiSkottieAnimation.js +8 -8
  120. package/lib/module/skia/web/JsiSkottieAnimation.js.map +1 -1
  121. package/lib/module/sksg/Recorder/DrawingContext.d.ts +1 -0
  122. package/lib/module/sksg/Recorder/DrawingContext.js +15 -0
  123. package/lib/module/sksg/Recorder/DrawingContext.js.map +1 -1
  124. package/lib/module/sksg/Recorder/Player.js +1 -1
  125. package/lib/module/sksg/Recorder/Player.js.map +1 -1
  126. package/lib/module/sksg/Recorder/Visitor.js +6 -1
  127. package/lib/module/sksg/Recorder/Visitor.js.map +1 -1
  128. package/lib/module/sksg/Recorder/commands/Drawing.js +12 -9
  129. package/lib/module/sksg/Recorder/commands/Drawing.js.map +1 -1
  130. package/lib/module/sksg/Recorder/commands/ImageFilters.js +15 -1
  131. package/lib/module/sksg/Recorder/commands/ImageFilters.js.map +1 -1
  132. package/lib/module/sksg/Recorder/commands/Shaders.js +1 -1
  133. package/lib/module/sksg/Recorder/commands/Shaders.js.map +1 -1
  134. package/lib/module/views/SkiaPictureView.web.js +247 -57
  135. package/lib/module/views/SkiaPictureView.web.js.map +1 -1
  136. package/lib/typescript/lib/commonjs/skia/web/JsiSkPathBuilder.d.ts +1 -1
  137. package/lib/typescript/lib/commonjs/skia/web/JsiSkia.d.ts +0 -1
  138. package/lib/typescript/lib/commonjs/sksg/Recorder/DrawingContext.d.ts +1 -0
  139. package/lib/typescript/lib/module/skia/Skia.web.d.ts +0 -1
  140. package/lib/typescript/lib/module/skia/web/JsiSkPathBuilder.d.ts +1 -1
  141. package/lib/typescript/lib/module/skia/web/JsiSkia.d.ts +0 -1
  142. package/lib/typescript/lib/module/sksg/Recorder/DrawingContext.d.ts +1 -0
  143. package/lib/typescript/react-native.config.d.ts +3 -0
  144. package/lib/typescript/src/skia/types/JsiInstance.d.ts +1 -0
  145. package/lib/typescript/src/skia/types/Skia.d.ts +1 -6
  146. package/lib/typescript/src/skia/web/JsiSkPathBuilder.d.ts +1 -1
  147. package/lib/typescript/src/sksg/Recorder/DrawingContext.d.ts +1 -0
  148. package/package.json +13 -10
  149. package/react-native.config.js +5 -0
  150. package/src/dom/nodes/datatypes/Rect.ts +6 -1
  151. package/src/external/reanimated/useVideo.ts +7 -2
  152. package/src/renderer/components/Mask.tsx +10 -3
  153. package/src/skia/types/JsiInstance.ts +2 -0
  154. package/src/skia/types/Matrix4.ts +6 -2
  155. package/src/skia/types/Skia.ts +1 -7
  156. package/src/skia/web/JsiSkCanvas.ts +11 -6
  157. package/src/skia/web/JsiSkImage.ts +6 -3
  158. package/src/skia/web/JsiSkPathBuilder.ts +2 -2
  159. package/src/skia/web/JsiSkTextStyle.ts +20 -15
  160. package/src/skia/web/JsiSkia.ts +0 -4
  161. package/src/skia/web/JsiSkottieAnimation.ts +8 -8
  162. package/src/sksg/Recorder/DrawingContext.ts +15 -0
  163. package/src/sksg/Recorder/Player.ts +1 -1
  164. package/src/sksg/Recorder/Visitor.ts +9 -6
  165. package/src/sksg/Recorder/commands/Drawing.ts +12 -9
  166. package/src/sksg/Recorder/commands/ImageFilters.ts +20 -1
  167. package/src/sksg/Recorder/commands/Shaders.ts +1 -1
  168. package/src/views/SkiaPictureView.web.tsx +245 -57
  169. package/cpp/api/JsiSkiaContext.h +0 -94
  170. package/cpp/dawn/include/dawn/dawn_proc.h +0 -53
  171. package/cpp/dawn/include/dawn/dawn_proc_table.h +0 -330
  172. package/cpp/dawn/include/dawn/dawn_thread_dispatch_proc.h +0 -47
  173. package/cpp/dawn/include/dawn/dawn_version.h +0 -41
  174. package/cpp/dawn/include/dawn/native/D3D11Backend.h +0 -65
  175. package/cpp/dawn/include/dawn/native/D3D12Backend.h +0 -103
  176. package/cpp/dawn/include/dawn/native/D3DBackend.h +0 -56
  177. package/cpp/dawn/include/dawn/native/DawnNative.h +0 -368
  178. package/cpp/dawn/include/dawn/native/MetalBackend.h +0 -56
  179. package/cpp/dawn/include/dawn/native/NullBackend.h +0 -39
  180. package/cpp/dawn/include/dawn/native/OpenGLBackend.h +0 -89
  181. package/cpp/dawn/include/dawn/native/VulkanBackend.h +0 -183
  182. package/cpp/dawn/include/dawn/native/WebGPUBackend.h +0 -49
  183. package/cpp/dawn/include/dawn/native/dawn_native_export.h +0 -49
  184. package/cpp/dawn/include/dawn/platform/DawnPlatform.h +0 -207
  185. package/cpp/dawn/include/dawn/platform/dawn_platform_export.h +0 -49
  186. package/cpp/dawn/include/dawn/replay/Replay.h +0 -99
  187. package/cpp/dawn/include/dawn/replay/dawn_replay_export.h +0 -49
  188. package/cpp/dawn/include/dawn/webgpu_cpp_print.h +0 -2856
  189. package/cpp/dawn/include/tint/tint.h +0 -91
  190. package/cpp/dawn/include/webgpu/webgpu.h +0 -5029
  191. package/cpp/dawn/include/webgpu/webgpu_cpp.h +0 -10353
  192. package/cpp/dawn/include/webgpu/webgpu_cpp_chained_struct.h +0 -57
  193. package/cpp/dawn/include/webgpu/webgpu_enum_class_bitmasks.h +0 -162
  194. package/cpp/dawn/include/webgpu/webgpu_glfw.h +0 -88
  195. package/cpp/jsi/RuntimeAwareCache.cpp +0 -7
  196. package/cpp/jsi/RuntimeAwareCache.h +0 -117
  197. package/cpp/jsi/RuntimeLifecycleMonitor.cpp +0 -72
  198. package/cpp/jsi/RuntimeLifecycleMonitor.h +0 -32
  199. package/cpp/skia/src/gpu/graphite/ContextOptionsPriv.h +0 -45
  200. package/cpp/skia/src/gpu/graphite/ResourceTypes.h +0 -360
  201. package/cpp/skia/src/gpu/graphite/TextureProxyView.h +0 -105
  202. package/libs/.graphite +0 -1
@@ -9,23 +9,27 @@ export class JsiSkTextStyle {
9
9
  color: value.color,
10
10
  decoration: value.decoration,
11
11
  decorationColor: value.decorationColor,
12
- decorationStyle: value.decorationStyle
13
- ? { value: value.decorationStyle }
14
- : undefined,
12
+ decorationStyle:
13
+ value.decorationStyle !== undefined
14
+ ? { value: value.decorationStyle }
15
+ : undefined,
15
16
  decorationThickness: value.decorationThickness,
16
17
  fontFamilies: value.fontFamilies,
17
18
  fontSize: value.fontSize,
18
19
  fontStyle: value.fontStyle
19
20
  ? {
20
- slant: value.fontStyle.slant
21
- ? { value: value.fontStyle.slant }
22
- : undefined,
23
- weight: value.fontStyle.weight
24
- ? { value: value.fontStyle.weight }
25
- : undefined,
26
- width: value.fontStyle.width
27
- ? { value: value.fontStyle.width }
28
- : undefined,
21
+ slant:
22
+ value.fontStyle.slant !== undefined
23
+ ? { value: value.fontStyle.slant }
24
+ : undefined,
25
+ weight:
26
+ value.fontStyle.weight !== undefined
27
+ ? { value: value.fontStyle.weight }
28
+ : undefined,
29
+ width:
30
+ value.fontStyle.width !== undefined
31
+ ? { value: value.fontStyle.width }
32
+ : undefined,
29
33
  }
30
34
  : undefined,
31
35
  fontFeatures: value.fontFeatures,
@@ -44,9 +48,10 @@ export class JsiSkTextStyle {
44
48
  : undefined,
45
49
  }))
46
50
  : undefined,
47
- textBaseline: value.textBaseline
48
- ? { value: value.textBaseline }
49
- : undefined,
51
+ textBaseline:
52
+ value.textBaseline !== undefined
53
+ ? { value: value.textBaseline }
54
+ : undefined,
50
55
  wordSpacing: value.wordSpacing,
51
56
  };
52
57
  }
@@ -4,7 +4,6 @@ import type {
4
4
  JsiRecorder,
5
5
  SkContourMeasureIter,
6
6
  Skia,
7
- SkiaContext,
8
7
  SkPath,
9
8
  SkRect,
10
9
  SkRuntimeEffect,
@@ -137,9 +136,6 @@ export const JsiSkApi = (CanvasKit: CanvasKit): Skia => ({
137
136
  NativeBuffer: new JsiSkNativeBufferFactory(CanvasKit),
138
137
  Skottie: new JsiSkottieFactory(CanvasKit),
139
138
  Video: createVideo.bind(null, CanvasKit),
140
- Context: (_surface: bigint, _width: number, _height: number) => {
141
- return throwNotImplementedOnRNWeb<SkiaContext>();
142
- },
143
139
  Recorder: () => {
144
140
  return throwNotImplementedOnRNWeb<JsiRecorder>();
145
141
  },
@@ -164,28 +164,28 @@ export class JsiSkottieAnimation
164
164
  if (result.text) {
165
165
  textSlot.text = result.text;
166
166
  }
167
- if (result.textSize) {
167
+ if (result.textSize !== undefined) {
168
168
  textSlot.textSize = result.textSize;
169
169
  }
170
- if (result.minTextSize) {
170
+ if (result.minTextSize !== undefined) {
171
171
  textSlot.minTextSize = result.minTextSize;
172
172
  }
173
- if (result.maxTextSize) {
173
+ if (result.maxTextSize !== undefined) {
174
174
  textSlot.maxTextSize = result.maxTextSize;
175
175
  }
176
- if (result.strokeWidth) {
176
+ if (result.strokeWidth !== undefined) {
177
177
  textSlot.strokeWidth = result.strokeWidth;
178
178
  }
179
- if (result.lineHeight) {
179
+ if (result.lineHeight !== undefined) {
180
180
  textSlot.lineHeight = result.lineHeight;
181
181
  }
182
- if (result.lineShift) {
182
+ if (result.lineShift !== undefined) {
183
183
  textSlot.lineShift = result.lineShift;
184
184
  }
185
- if (result.ascent) {
185
+ if (result.ascent !== undefined) {
186
186
  textSlot.ascent = result.ascent;
187
187
  }
188
- if (result.maxLines) {
188
+ if (result.maxLines !== undefined) {
189
189
  textSlot.maxLines = result.maxLines;
190
190
  }
191
191
  // if (result.horizAlign) {
@@ -37,7 +37,10 @@ export const createDrawingContext = (
37
37
  if (paintPool.length === 0) {
38
38
  paintPool.push(Skia.Paint());
39
39
  } else {
40
+ // reset() produces an anti-alias false paint, unlike the Skia.Paint()
41
+ // factory: restore the default so reused pools render like the first frame.
40
42
  paintPool[0].reset();
43
+ paintPool[0].setAntiAlias(true);
41
44
  }
42
45
  paints.push(paintPool[0]);
43
46
  opacities.push(1);
@@ -56,6 +59,17 @@ export const createDrawingContext = (
56
59
  nextPaintIndex++;
57
60
  };
58
61
 
62
+ // Pushes an externally owned paint (the `paint` prop) onto the stack. It
63
+ // pushes a frame-scoped copy, like the C++ DrawingCtx: materializePaint()
64
+ // mutates the top of the stack, and those mutations must not leak into the
65
+ // user-owned paint. It must also push an opacity alongside it: restorePaint()
66
+ // pops both, so pushing only the paint would underflow the opacity stack and
67
+ // leak the enclosing group's opacity onto everything drawn afterwards.
68
+ const pushPaint = (paint: SkPaint) => {
69
+ paints.push(track(paint.copy()));
70
+ opacities.push(opacities[opacities.length - 1]);
71
+ };
72
+
59
73
  const getOpacity = () => {
60
74
  return opacities[opacities.length - 1];
61
75
  };
@@ -144,6 +158,7 @@ export const createDrawingContext = (
144
158
 
145
159
  // Public methods
146
160
  savePaint,
161
+ pushPaint,
147
162
  saveBackdropFilter,
148
163
  get paint() {
149
164
  return paints[paints.length - 1];
@@ -123,7 +123,7 @@ const play = (ctx: DrawingContext, _command: Command) => {
123
123
  ctx.canvas.saveLayer(paint);
124
124
  } else if (isDrawCommand(command, CommandType.SavePaint)) {
125
125
  if (command.props.paint) {
126
- ctx.paints.push(command.props.paint);
126
+ ctx.pushPaint(command.props.paint);
127
127
  } else {
128
128
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
129
  const { standalone } = command as any;
@@ -104,13 +104,16 @@ const processCTM = ({
104
104
  if (layer) {
105
105
  ctm.layer = layer;
106
106
  }
107
+ // Only record a CTM command when saveCTM() will actually save the canvas.
108
+ // saveCTM() saves on transform/matrix, clip or layer; restoreCTM() always
109
+ // restores, so recording a CTM that saves nothing - `origin` with no
110
+ // transform, `clip={false}` from a conditional, a bare `invertClip` - pops
111
+ // an enclosing save that belongs to an ancestor.
107
112
  if (
108
- clip !== undefined ||
109
- invertClip !== undefined ||
110
- transform !== undefined ||
111
- origin !== undefined ||
112
- matrix !== undefined ||
113
- layer !== undefined
113
+ ctm.clip !== undefined ||
114
+ ctm.transform !== undefined ||
115
+ ctm.matrix !== undefined ||
116
+ ctm.layer !== undefined
114
117
  ) {
115
118
  return ctm;
116
119
  }
@@ -217,6 +217,16 @@ export const drawPath = (ctx: DrawingContext, props: PathProps) => {
217
217
 
218
218
  let path = processPath(ctx.Skia, pathProps.path);
219
219
 
220
+ // Trim runs first, so start/end address the path the caller drew. Stroking
221
+ // first would replace it with its outline and leave the offsets walking that
222
+ // outline's perimeter instead. The native recorder orders these the same way.
223
+ if (hasStartOffset || hasEndOffset) {
224
+ const trimmed = ctx.Skia.Path.Trim(path, start, end, false);
225
+ if (trimmed) {
226
+ path = trimmed;
227
+ }
228
+ }
229
+
220
230
  // Apply fill type using PathBuilder
221
231
  if (hasFillType) {
222
232
  const builder = ctx.Skia.PathBuilder.MakeFromPath(path);
@@ -232,14 +242,6 @@ export const drawPath = (ctx: DrawingContext, props: PathProps) => {
232
242
  }
233
243
  }
234
244
 
235
- // Apply trim using static Path.Trim
236
- if (hasStartOffset || hasEndOffset) {
237
- const trimmed = ctx.Skia.Path.Trim(path, start, end, false);
238
- if (trimmed) {
239
- path = trimmed;
240
- }
241
- }
242
-
243
245
  ctx.canvas.drawPath(path, ctx.paint);
244
246
  };
245
247
 
@@ -295,7 +297,8 @@ export const drawImageSVG = (ctx: DrawingContext, props: ImageSVGProps) => {
295
297
  return;
296
298
  }
297
299
  canvas.save();
298
- if (x && y) {
300
+ // An offset of 0 is a valid position, so it must not be read as "unset".
301
+ if (x !== undefined && y !== undefined) {
299
302
  canvas.translate(x, y);
300
303
  }
301
304
  canvas.drawSvg(svg, width, height);
@@ -31,6 +31,14 @@ export enum MorphologyOperator {
31
31
 
32
32
  const Black = Float32Array.of(0, 0, 0, 1);
33
33
 
34
+ // Turns the source graphic into a hard silhouette: alpha is multiplied by 255
35
+ // and clamped, so any pixel the shape covers at all becomes fully opaque, and
36
+ // the colour channels are dropped. 255 is the saturation point of an 8-bit
37
+ // alpha channel, so one unit of coverage is enough to reach 1.
38
+ const AlphaSaturate = [
39
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0,
40
+ ];
41
+
34
42
  const MakeInnerShadow = (
35
43
  Skia: Skia,
36
44
  shadowOnly: boolean | undefined,
@@ -50,9 +58,20 @@ const MakeInnerShadow = (
50
58
  Skia.ColorFilter.MakeBlend(Black, BlendMode.SrcIn),
51
59
  null
52
60
  );
61
+ // The shadow is generated outside the shape and then clipped back into it, so
62
+ // "outside" has to be the complement of the shape's silhouette. Taking SrcOut
63
+ // against the source graphic itself makes it the complement of the source's
64
+ // *alpha*: inside a translucent shape 1 - alpha is non-zero, so the shadow is
65
+ // generated across the whole interior and tints it, no matter how small the
66
+ // blur and the offset are (issue #2990). Saturating alpha first keeps the
67
+ // shadow tied to the shape's outline instead of to its opacity.
68
+ const coverage = Skia.ImageFilter.MakeColorFilter(
69
+ Skia.ColorFilter.MakeMatrix(AlphaSaturate),
70
+ null
71
+ );
53
72
  const f1 = Skia.ImageFilter.MakeColorFilter(
54
73
  Skia.ColorFilter.MakeBlend(color, BlendMode.SrcOut),
55
- null
74
+ coverage
56
75
  );
57
76
  const f2 = Skia.ImageFilter.MakeOffset(dx, dy, f1);
58
77
  const f3 = Skia.ImageFilter.MakeBlur(sigmaX, sigmaY, TileMode.Decal, f2);
@@ -221,7 +221,7 @@ const declareImageShader = (ctx: DrawingContext, props: ImageShaderProps) => {
221
221
  );
222
222
  } else {
223
223
  shader = ctx.track(
224
- image.makeShaderCubic(
224
+ image.makeShaderOptions(
225
225
  TileMode[enumKey(tx)],
226
226
  TileMode[enumKey(ty)],
227
227
  sampling?.filter ?? FilterMode.Linear,
@@ -36,32 +36,191 @@ interface Renderer {
36
36
  dispose(): void;
37
37
  }
38
38
 
39
+ // A <canvas> element owns at most one WebGL context for its whole lifetime.
40
+ // A renderer, on the other hand, lives as long as the layout effect that
41
+ // creates it, which is shorter: React re-runs layout effects on a *preserved*
42
+ // host node under StrictMode's DEV double-invoke and when an Activity/offscreen
43
+ // subtree is hidden and revealed (#3976). What belongs to the element rather
44
+ // than to a renderer is tracked here, per element:
45
+ // - the context, so that a new renderer can tell whether the element already
46
+ // has one (and whether it is lost) without calling getContext() itself,
47
+ // which on a fresh element would create a context with the wrong attributes
48
+ // (CanvasKit.GetWebGLContext requests its own);
49
+ // - the WEBGL_lose_context extension, captured while the context is healthy:
50
+ // getExtension() returns null on a lost context, and this object is the only
51
+ // way to lose a context on purpose or to ask the browser to restore one;
52
+ // - whether the browser has announced the loss of the context yet: a lost
53
+ // context can only be restored once its webglcontextlost event has been
54
+ // dispatched, and that event is asynchronous;
55
+ // - the renderer currently attached to the element, if any.
56
+ interface CanvasWebGL {
57
+ gl: WebGLRenderingContext | WebGL2RenderingContext;
58
+ // The WEBGL_lose_context extension.
59
+ loseContext: { loseContext(): void; restoreContext(): void } | null;
60
+ lossAnnounced: boolean;
61
+ owner: WebGLRenderer | null;
62
+ }
63
+ const canvasWebGL = new WeakMap<HTMLCanvasElement, CanvasWebGL>();
64
+
65
+ // Unregisters a context from CanvasKit's registry, which otherwise retains the
66
+ // canvas element (and its detached DOM tree) forever (#3924).
67
+ const deleteContextHandle = (handle: WebGLContextHandle) => {
68
+ CanvasKit.deleteContext(handle);
69
+ // Making the now-deleted handle current clears CanvasKit's current-context
70
+ // globals (GLctx/Module.ctx), which would otherwise keep referencing the
71
+ // context (and the canvas) until another surface becomes current. With a
72
+ // deleted handle this is a no-op that returns null without creating
73
+ // anything.
74
+ CanvasKit.MakeWebGLContext(handle);
75
+ };
76
+
39
77
  class WebGLRenderer implements Renderer {
40
78
  private surface: JsiSkSurface | null = null;
41
79
  private grContext: GrDirectContext | null = null;
42
80
  private contextHandle: WebGLContextHandle = 0;
43
81
  private pd = 1;
82
+ // Set while the renderer is waiting for a lost context to be restored.
83
+ private restoreWanted = false;
84
+ private restoreRequest: ReturnType<typeof setTimeout> | null = null;
85
+
86
+ constructor(
87
+ private canvas: HTMLCanvasElement,
88
+ // Called when the renderer becomes able to paint again and the current
89
+ // picture should be drawn.
90
+ private requestRedraw: () => void
91
+ ) {
92
+ const entry = canvasWebGL.get(canvas);
93
+ if (entry) {
94
+ entry.owner = this;
95
+ }
96
+ canvas.addEventListener("webglcontextlost", this.onContextLost);
97
+ canvas.addEventListener("webglcontextrestored", this.onContextRestored);
98
+ this.acquire();
99
+ }
44
100
 
45
- constructor(private canvas: HTMLCanvasElement) {
46
- this.contextHandle = CanvasKit.GetWebGLContext(canvas);
47
- if (!this.contextHandle) {
101
+ // Builds the GrDirectContext and the surface on the element's WebGL
102
+ // context, creating the context first if the element has none.
103
+ private acquire() {
104
+ const { canvas } = this;
105
+ const known = canvasWebGL.get(canvas);
106
+ if (known?.gl.isContextLost()) {
107
+ // Nothing can be built on a lost context: CanvasKit.GetWebGLContext
108
+ // happily registers it, and MakeWebGLContext then faults inside wasm
109
+ // (#3976). Ask the browser for it back and finish in onContextRestored;
110
+ // if the loss hasn't been announced yet, onContextLost asks.
111
+ this.restoreWanted = true;
112
+ if (known.lossAnnounced) {
113
+ known.loseContext?.restoreContext();
114
+ }
115
+ return;
116
+ }
117
+ const handle = CanvasKit.GetWebGLContext(canvas);
118
+ if (!handle) {
48
119
  throw new Error("Could not create a WebGL context");
49
120
  }
50
- this.grContext = CanvasKit.MakeWebGLContext(this.contextHandle);
51
- if (!this.grContext) {
52
- CanvasKit.deleteContext(this.contextHandle);
53
- this.contextHandle = 0;
121
+ const entry = known ?? this.trackContext();
122
+ let grContext: GrDirectContext | null = null;
123
+ try {
124
+ grContext = CanvasKit.MakeWebGLContext(handle);
125
+ } finally {
126
+ if (!grContext) {
127
+ // Whether it returned null or threw, don't leave the registry
128
+ // holding on to the canvas.
129
+ deleteContextHandle(handle);
130
+ }
131
+ }
132
+ if (!grContext) {
54
133
  throw new Error("Could not create a graphics context");
55
134
  }
56
- const ctx = canvas.getContext("webgl2");
57
- if (ctx) {
58
- ctx.drawingBufferColorSpace = "display-p3";
135
+ this.restoreWanted = false;
136
+ this.contextHandle = handle;
137
+ this.grContext = grContext;
138
+ if (entry) {
139
+ entry.gl.drawingBufferColorSpace = "display-p3";
59
140
  }
60
141
  this.onResize();
61
142
  }
62
143
 
144
+ // CanvasKit just created the element's context; asking for it again hands
145
+ // back the same object (the attributes are only honored the first time).
146
+ private trackContext(): CanvasWebGL | null {
147
+ const { canvas } = this;
148
+ const gl = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
149
+ if (!gl) {
150
+ return null;
151
+ }
152
+ const entry: CanvasWebGL = {
153
+ gl,
154
+ loseContext: gl.getExtension("WEBGL_lose_context"),
155
+ lossAnnounced: false,
156
+ owner: this,
157
+ };
158
+ canvasWebGL.set(canvas, entry);
159
+ // Browsers restore a lost context (evicted as the oldest one when the
160
+ // page exceeds the active-context limit, or lost to a GPU reset) only if
161
+ // the page called preventDefault() on the webglcontextlost event. A
162
+ // renderer isn't necessarily attached when that happens (the canvas may
163
+ // be in a hidden Activity), so these listeners live as long as the
164
+ // element does. They are registered ahead of any renderer's, and the
165
+ // loss can't be announced before they exist since the event is
166
+ // asynchronous.
167
+ canvas.addEventListener("webglcontextlost", (event) => {
168
+ event.preventDefault();
169
+ entry.lossAnnounced = true;
170
+ });
171
+ canvas.addEventListener("webglcontextrestored", () => {
172
+ entry.lossAnnounced = false;
173
+ });
174
+ return entry;
175
+ }
176
+
177
+ // Frees everything built on the context. The context itself belongs to the
178
+ // element, see dispose().
179
+ private release() {
180
+ this.surface?.ref.delete();
181
+ this.surface = null;
182
+ if (this.grContext) {
183
+ this.grContext.releaseResourcesAndAbandonContext();
184
+ this.grContext.delete();
185
+ this.grContext = null;
186
+ }
187
+ if (this.contextHandle) {
188
+ deleteContextHandle(this.contextHandle);
189
+ this.contextHandle = 0;
190
+ }
191
+ }
192
+
193
+ private isContextLost() {
194
+ return canvasWebGL.get(this.canvas)?.gl.isContextLost() ?? false;
195
+ }
196
+
197
+ private onContextLost = () => {
198
+ // The browser already dropped every GL object; drop our references to
199
+ // them. Restoration is left to the browser (it restores an evicted
200
+ // context as soon as a slot frees up), except when this renderer was
201
+ // built on an already-lost context and is waiting to paint for the first
202
+ // time: chasing an eviction from here would evict the next-oldest context
203
+ // in turn, and with more live canvases than the browser allows the
204
+ // canvases would endlessly evict each other.
205
+ this.release();
206
+ if (this.restoreWanted) {
207
+ // The permission granted by preventDefault() is only recorded by the
208
+ // browser once the event dispatch is over: a restoreContext() call
209
+ // from inside the handler is refused, so ask from a later task.
210
+ this.restoreRequest = setTimeout(() => {
211
+ this.restoreRequest = null;
212
+ canvasWebGL.get(this.canvas)?.loseContext?.restoreContext();
213
+ }, 0);
214
+ }
215
+ };
216
+
217
+ private onContextRestored = () => {
218
+ this.acquire();
219
+ this.requestRedraw();
220
+ };
221
+
63
222
  makeImageSnapshot(picture: SkPicture, rect?: SkRect): SkImage | null {
64
- if (!this.surface) {
223
+ if (!this.surface || this.isContextLost()) {
65
224
  return null;
66
225
  }
67
226
  const canvas = this.surface.getCanvas();
@@ -73,7 +232,10 @@ class WebGLRenderer implements Renderer {
73
232
 
74
233
  onResize() {
75
234
  const { canvas } = this;
76
- if (!this.grContext) {
235
+ if (!this.grContext || this.isContextLost()) {
236
+ // Waiting for the context to be restored (the restore path resizes),
237
+ // or the loss hasn't been announced yet and onContextLost is about to
238
+ // release everything: either way there is nothing to build on.
77
239
  return;
78
240
  }
79
241
  this.pd = window.devicePixelRatio;
@@ -101,41 +263,57 @@ class WebGLRenderer implements Renderer {
101
263
  }
102
264
 
103
265
  draw(picture: SkPicture) {
104
- if (this.surface) {
105
- const canvas = this.surface.getCanvas();
106
- canvas.clear(Float32Array.of(0, 0, 0, 0));
107
- canvas.save();
108
- canvas.scale(this.pd, this.pd);
109
- canvas.drawPicture(picture);
110
- canvas.restore();
111
- this.surface.ref.flush();
266
+ if (!this.surface || this.isContextLost()) {
267
+ return;
112
268
  }
269
+ const canvas = this.surface.getCanvas();
270
+ canvas.clear(Float32Array.of(0, 0, 0, 0));
271
+ canvas.save();
272
+ canvas.scale(this.pd, this.pd);
273
+ canvas.drawPicture(picture);
274
+ canvas.restore();
275
+ this.surface.ref.flush();
113
276
  }
114
277
 
115
278
  dispose(): void {
116
- this.surface?.ref.delete();
117
- this.surface = null;
118
- if (this.grContext) {
119
- this.grContext.releaseResourcesAndAbandonContext();
120
- this.grContext.delete();
121
- this.grContext = null;
279
+ const { canvas } = this;
280
+ canvas.removeEventListener("webglcontextlost", this.onContextLost);
281
+ canvas.removeEventListener("webglcontextrestored", this.onContextRestored);
282
+ if (this.restoreRequest !== null) {
283
+ clearTimeout(this.restoreRequest);
284
+ this.restoreRequest = null;
122
285
  }
123
- this.canvas
124
- ?.getContext("webgl2")
125
- ?.getExtension("WEBGL_lose_context")
126
- ?.loseContext();
127
- if (this.contextHandle) {
128
- // Unregister the context from CanvasKit's internal registry, otherwise
129
- // it retains the canvas element (and its detached DOM tree) forever.
130
- CanvasKit.deleteContext(this.contextHandle);
131
- // Making the now-deleted handle current clears CanvasKit's
132
- // current-context globals (GLctx/Module.ctx), which would otherwise
133
- // keep referencing the context (and the canvas) until another surface
134
- // becomes current. With a deleted handle this is a no-op that returns
135
- // null without creating anything.
136
- CanvasKit.MakeWebGLContext(this.contextHandle);
137
- this.contextHandle = 0;
286
+ this.restoreWanted = false;
287
+ this.release();
288
+ // Free the drawing buffer, which CanvasKit requests with depth and stencil
289
+ // attachments (about 8 bytes per pixel): a canvas that stays in the
290
+ // document without a renderer, hidden in an Activity say, would otherwise
291
+ // keep a full-size buffer around. The next renderer's onResize() sizes it
292
+ // again.
293
+ canvas.width = 0;
294
+ canvas.height = 0;
295
+ const entry = canvasWebGL.get(canvas);
296
+ if (!entry) {
297
+ return;
138
298
  }
299
+ entry.owner = null;
300
+ // Whether the element is going away is only known once the commit is
301
+ // over: on a real unmount React runs this cleanup *before* removing the
302
+ // node from the document, while StrictMode's double-invoke and an
303
+ // Activity hide keep the node in place (and the former builds the next
304
+ // renderer on it right away). Losing the context of a detached element
305
+ // matters: it would otherwise stay alive until the element is garbage
306
+ // collected, and browsers cap the number of live contexts per page
307
+ // (16 in Chrome, which then evicts the oldest one, visible or not).
308
+ queueMicrotask(() => {
309
+ if (
310
+ entry.owner === null &&
311
+ !canvas.isConnected &&
312
+ !entry.gl.isContextLost()
313
+ ) {
314
+ entry.loseContext?.loseContext();
315
+ }
316
+ });
139
317
  }
140
318
  }
141
319
 
@@ -370,22 +548,30 @@ export const SkiaPictureView = (props: SkiaPictureViewProps) => {
370
548
  // - The renderer is created synchronously on mount (the canvas element is
371
549
  // guaranteed to exist in useLayoutEffect; a zero size at that point is
372
550
  // fine, the surface is created once the canvas is measurable).
373
- // - Redraw requests coalesce into a single animation frame. A picture
374
- // dispatched before the canvas has a size stays in pictureRef (drawing
375
- // while unmeasured is a no-op) and is painted by the resize path below
376
- // once the canvas becomes measurable, so it is never lost.
551
+ // - Redraw requests coalesce into a single microtask. A microtask (not an
552
+ // animation frame) so that a picture produced inside a rAF callback (the
553
+ // Reanimated mapper) is drawn before the current frame paints: deferring
554
+ // to the next rAF alternates between "flush pending in this frame" and
555
+ // "flush scheduled for the next frame", drawing on every other frame
556
+ // only and halving the effective frame rate. A picture dispatched before
557
+ // the canvas has a size stays in pictureRef (drawing while unmeasured is
558
+ // a no-op) and is painted by the resize path below once the canvas
559
+ // becomes measurable, so it is never lost.
377
560
  // - A ResizeObserver on the canvas itself recreates the surface and repaints
378
561
  // synchronously (its callbacks run after layout, before paint), and is
379
562
  // also the source of the user-facing onLayout event.
563
+ // - The renderer belongs to the layout effect, the WebGL context to the
564
+ // <canvas> element, whose lifetime is longer; see WebGLRenderer for how
565
+ // the two are reconciled.
380
566
  const redrawPendingRef = useRef(false);
381
- const frameRef = useRef(0);
567
+ const flushScheduledRef = useRef(false);
382
568
  const onLayoutRef = useRef(onLayout);
383
569
  useLayoutEffect(() => {
384
570
  onLayoutRef.current = onLayout;
385
571
  }, [onLayout]);
386
572
 
387
573
  const flushRedraw = useCallback(() => {
388
- frameRef.current = 0;
574
+ flushScheduledRef.current = false;
389
575
  if (redrawPendingRef.current && rendererRef.current && pictureRef.current) {
390
576
  redrawPendingRef.current = false;
391
577
  rendererRef.current.draw(pictureRef.current);
@@ -396,8 +582,9 @@ export const SkiaPictureView = (props: SkiaPictureViewProps) => {
396
582
 
397
583
  const redraw = useCallback(() => {
398
584
  redrawPendingRef.current = true;
399
- if (frameRef.current === 0) {
400
- frameRef.current = requestAnimationFrame(flushRedraw);
585
+ if (!flushScheduledRef.current) {
586
+ flushScheduledRef.current = true;
587
+ queueMicrotask(flushRedraw);
401
588
  }
402
589
  }, [flushRedraw]);
403
590
 
@@ -482,7 +669,7 @@ export const SkiaPictureView = (props: SkiaPictureViewProps) => {
482
669
  }
483
670
  const renderer = isStatic
484
671
  ? new StaticWebGLRenderer(canvas)
485
- : new WebGLRenderer(canvas);
672
+ : new WebGLRenderer(canvas, redraw);
486
673
  rendererRef.current = renderer;
487
674
 
488
675
  const drawPicture = () => {
@@ -566,14 +753,11 @@ export const SkiaPictureView = (props: SkiaPictureViewProps) => {
566
753
  rendererRef.current = null;
567
754
  renderer.dispose();
568
755
  };
569
- }, [isStatic]);
756
+ }, [isStatic, redraw]);
570
757
 
571
- useEffect(() => {
572
- return () => {
573
- cancelAnimationFrame(frameRef.current);
574
- frameRef.current = 0;
575
- };
576
- }, []);
758
+ // No flush cancellation is needed on unmount: a microtask queued before
759
+ // unmount runs within the same task, and flushRedraw no-ops once the
760
+ // layout-effect cleanup has nulled rendererRef.
577
761
 
578
762
  useImperativeHandle(
579
763
  ref,
@@ -632,6 +816,10 @@ export const SkiaPictureView = (props: SkiaPictureViewProps) => {
632
816
  return (
633
817
  <Platform.View {...viewProps}>
634
818
  <canvas
819
+ // A canvas element is bound to one context kind for life (WebGL for
820
+ // the live renderer, 2D for the static one), so switching renderers
821
+ // needs a fresh element.
822
+ key={isStatic ? "static" : "webgl"}
635
823
  ref={canvasRef}
636
824
  style={{ display: "block", width: "100%", height: "100%" }}
637
825
  />