@shopify/react-native-skia 2.6.9 → 2.7.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 (145) hide show
  1. package/android/cpp/jni/include/JniSkiaBaseView.h +6 -4
  2. package/android/cpp/jni/include/JniSkiaPictureView.h +8 -6
  3. package/android/cpp/rnskia-android/OpenGLContext.h +10 -1
  4. package/android/cpp/rnskia-android/RNSkAndroidView.h +8 -8
  5. package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.cpp +9 -5
  6. package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.h +4 -2
  7. package/android/src/main/java/com/shopify/reactnative/skia/SkiaBaseView.java +48 -12
  8. package/android/src/main/java/com/shopify/reactnative/skia/SkiaBaseViewManager.java +5 -0
  9. package/android/src/main/java/com/shopify/reactnative/skia/SkiaPictureView.java +2 -2
  10. package/android/src/main/java/com/shopify/reactnative/skia/ViewScreenshotService.java +11 -7
  11. package/apple/MetalContext.h +7 -6
  12. package/apple/MetalLayerColorSpace.mm +39 -0
  13. package/apple/MetalLayerColorSpaceUtils.h +34 -0
  14. package/apple/MetalWindowContext.h +3 -1
  15. package/apple/MetalWindowContext.mm +9 -8
  16. package/apple/RNSkAppleView.h +6 -0
  17. package/apple/RNSkMetalCanvasProvider.h +2 -0
  18. package/apple/RNSkMetalCanvasProvider.mm +15 -2
  19. package/apple/SkiaPictureView.mm +1 -0
  20. package/apple/SkiaPictureViewManager.mm +5 -0
  21. package/apple/SkiaUIView.h +1 -0
  22. package/apple/SkiaUIView.mm +9 -0
  23. package/cpp/api/JsiSkSurface.h +8 -2
  24. package/cpp/rnskia/RNDawnContext.h +4 -3
  25. package/cpp/rnskia/RNDawnUtils.h +25 -6
  26. package/cpp/rnskia/RNDawnWindowContext.h +44 -10
  27. package/cpp/rnskia/RNMetalLayerColorSpace.h +16 -0
  28. package/cpp/rnskia/RNSkJsiViewApi.h +46 -24
  29. package/cpp/rnwgpu/SurfaceRegistry.h +10 -4
  30. package/lib/commonjs/Platform/Platform.web.js +2 -90
  31. package/lib/commonjs/Platform/Platform.web.js.map +1 -1
  32. package/lib/commonjs/mock/index.js +1 -0
  33. package/lib/commonjs/mock/index.js.map +1 -1
  34. package/lib/commonjs/renderer/Canvas.d.ts +10 -1
  35. package/lib/commonjs/renderer/Canvas.js +2 -0
  36. package/lib/commonjs/renderer/Canvas.js.map +1 -1
  37. package/lib/commonjs/skia/types/Surface/Surface.d.ts +5 -1
  38. package/lib/commonjs/skia/types/Surface/Surface.js.map +1 -1
  39. package/lib/commonjs/skia/web/JsiSkParagraphStyle.js +14 -3
  40. package/lib/commonjs/skia/web/JsiSkParagraphStyle.js.map +1 -1
  41. package/lib/commonjs/skia/web/JsiSkSurface.d.ts +1 -1
  42. package/lib/commonjs/skia/web/JsiSkSurface.js +2 -1
  43. package/lib/commonjs/skia/web/JsiSkSurface.js.map +1 -1
  44. package/lib/commonjs/sksg/Container.native.d.ts +1 -0
  45. package/lib/commonjs/sksg/Container.native.js +11 -0
  46. package/lib/commonjs/sksg/Container.native.js.map +1 -1
  47. package/lib/commonjs/sksg/Container.web.d.ts +1 -0
  48. package/lib/commonjs/sksg/Container.web.js +11 -0
  49. package/lib/commonjs/sksg/Container.web.js.map +1 -1
  50. package/lib/commonjs/specs/NativeSkiaModule.web.d.ts +2 -0
  51. package/lib/commonjs/specs/NativeSkiaModule.web.js +32 -7
  52. package/lib/commonjs/specs/NativeSkiaModule.web.js.map +1 -1
  53. package/lib/commonjs/specs/SkiaPictureViewNativeComponent.d.ts +1 -0
  54. package/lib/commonjs/specs/SkiaPictureViewNativeComponent.js.map +1 -1
  55. package/lib/commonjs/views/SkiaPictureView.js +2 -0
  56. package/lib/commonjs/views/SkiaPictureView.js.map +1 -1
  57. package/lib/commonjs/views/SkiaPictureView.web.js +279 -94
  58. package/lib/commonjs/views/SkiaPictureView.web.js.map +1 -1
  59. package/lib/commonjs/views/formats.d.ts +1 -0
  60. package/lib/commonjs/views/formats.js +28 -0
  61. package/lib/commonjs/views/formats.js.map +1 -0
  62. package/lib/commonjs/views/index.d.ts +1 -0
  63. package/lib/commonjs/views/index.js +11 -0
  64. package/lib/commonjs/views/index.js.map +1 -1
  65. package/lib/commonjs/views/types.d.ts +6 -0
  66. package/lib/commonjs/views/types.js.map +1 -1
  67. package/lib/module/Platform/Platform.web.js +3 -91
  68. package/lib/module/Platform/Platform.web.js.map +1 -1
  69. package/lib/module/mock/index.js +1 -0
  70. package/lib/module/mock/index.js.map +1 -1
  71. package/lib/module/renderer/Canvas.d.ts +10 -1
  72. package/lib/module/renderer/Canvas.js +2 -0
  73. package/lib/module/renderer/Canvas.js.map +1 -1
  74. package/lib/module/skia/types/Surface/Surface.d.ts +5 -1
  75. package/lib/module/skia/types/Surface/Surface.js.map +1 -1
  76. package/lib/module/skia/web/JsiSkParagraphStyle.js +14 -3
  77. package/lib/module/skia/web/JsiSkParagraphStyle.js.map +1 -1
  78. package/lib/module/skia/web/JsiSkSurface.d.ts +1 -1
  79. package/lib/module/skia/web/JsiSkSurface.js +2 -1
  80. package/lib/module/skia/web/JsiSkSurface.js.map +1 -1
  81. package/lib/module/sksg/Container.native.d.ts +1 -0
  82. package/lib/module/sksg/Container.native.js +11 -0
  83. package/lib/module/sksg/Container.native.js.map +1 -1
  84. package/lib/module/sksg/Container.web.d.ts +1 -0
  85. package/lib/module/sksg/Container.web.js +11 -0
  86. package/lib/module/sksg/Container.web.js.map +1 -1
  87. package/lib/module/specs/NativeSkiaModule.web.d.ts +2 -0
  88. package/lib/module/specs/NativeSkiaModule.web.js +32 -7
  89. package/lib/module/specs/NativeSkiaModule.web.js.map +1 -1
  90. package/lib/module/specs/SkiaPictureViewNativeComponent.d.ts +1 -0
  91. package/lib/module/specs/SkiaPictureViewNativeComponent.js.map +1 -1
  92. package/lib/module/views/SkiaPictureView.js +2 -0
  93. package/lib/module/views/SkiaPictureView.js.map +1 -1
  94. package/lib/module/views/SkiaPictureView.web.js +280 -95
  95. package/lib/module/views/SkiaPictureView.web.js.map +1 -1
  96. package/lib/module/views/formats.d.ts +1 -0
  97. package/lib/module/views/formats.js +22 -0
  98. package/lib/module/views/formats.js.map +1 -0
  99. package/lib/module/views/index.d.ts +1 -0
  100. package/lib/module/views/index.js +1 -0
  101. package/lib/module/views/index.js.map +1 -1
  102. package/lib/module/views/types.d.ts +6 -0
  103. package/lib/module/views/types.js.map +1 -1
  104. package/lib/typescript/lib/commonjs/Platform/Platform.web.d.ts +1 -2
  105. package/lib/typescript/lib/commonjs/mock/index.d.ts +1 -0
  106. package/lib/typescript/lib/commonjs/renderer/Canvas.d.ts +2 -1
  107. package/lib/typescript/lib/commonjs/skia/web/JsiSkSurface.d.ts +1 -1
  108. package/lib/typescript/lib/commonjs/sksg/Container.native.d.ts +1 -1
  109. package/lib/typescript/lib/commonjs/sksg/Container.web.d.ts +1 -1
  110. package/lib/typescript/lib/commonjs/views/formats.d.ts +2 -0
  111. package/lib/typescript/lib/module/Platform/Platform.web.d.ts +1 -3
  112. package/lib/typescript/lib/module/mock/index.d.ts +2 -2
  113. package/lib/typescript/lib/module/renderer/Canvas.d.ts +2 -1
  114. package/lib/typescript/lib/module/skia/web/JsiSkSurface.d.ts +1 -1
  115. package/lib/typescript/lib/module/sksg/Container.native.d.ts +1 -1
  116. package/lib/typescript/lib/module/sksg/Container.web.d.ts +1 -1
  117. package/lib/typescript/lib/module/views/formats.d.ts +1 -0
  118. package/lib/typescript/lib/module/views/index.d.ts +1 -0
  119. package/lib/typescript/src/renderer/Canvas.d.ts +10 -1
  120. package/lib/typescript/src/skia/types/Surface/Surface.d.ts +5 -1
  121. package/lib/typescript/src/skia/web/JsiSkSurface.d.ts +1 -1
  122. package/lib/typescript/src/sksg/Container.native.d.ts +1 -0
  123. package/lib/typescript/src/sksg/Container.web.d.ts +1 -0
  124. package/lib/typescript/src/specs/NativeSkiaModule.web.d.ts +2 -0
  125. package/lib/typescript/src/specs/SkiaPictureViewNativeComponent.d.ts +1 -0
  126. package/lib/typescript/src/views/formats.d.ts +1 -0
  127. package/lib/typescript/src/views/index.d.ts +1 -0
  128. package/lib/typescript/src/views/types.d.ts +6 -0
  129. package/package.json +4 -1
  130. package/react-native-skia.podspec +1 -0
  131. package/src/Platform/Platform.web.tsx +7 -99
  132. package/src/mock/index.ts +1 -0
  133. package/src/renderer/Canvas.tsx +11 -0
  134. package/src/skia/types/Surface/Surface.ts +5 -1
  135. package/src/skia/web/JsiSkParagraphStyle.ts +16 -2
  136. package/src/skia/web/JsiSkSurface.ts +2 -1
  137. package/src/sksg/Container.native.ts +12 -0
  138. package/src/sksg/Container.web.ts +12 -0
  139. package/src/specs/NativeSkiaModule.web.ts +39 -7
  140. package/src/specs/SkiaPictureViewNativeComponent.ts +1 -0
  141. package/src/views/SkiaPictureView.tsx +2 -0
  142. package/src/views/SkiaPictureView.web.tsx +318 -106
  143. package/src/views/formats.ts +21 -0
  144. package/src/views/index.ts +1 -0
  145. package/src/views/types.ts +7 -0
@@ -1,11 +1,13 @@
1
- /* global HTMLCanvasElement */
1
+ /* global HTMLCanvasElement, ResizeObserver, MediaQueryList */
2
2
  import React, {
3
3
  useRef,
4
4
  useEffect,
5
5
  useCallback,
6
6
  useImperativeHandle,
7
+ useLayoutEffect,
7
8
  } from "react";
8
9
  import type { LayoutChangeEvent } from "react-native";
10
+ import type { GrDirectContext, WebGLContextHandle } from "canvaskit-wasm";
9
11
 
10
12
  import type { SkRect, SkPicture, SkImage } from "../skia/types";
11
13
  import { JsiSkSurface } from "../skia/web/JsiSkSurface";
@@ -36,11 +38,25 @@ interface Renderer {
36
38
 
37
39
  class WebGLRenderer implements Renderer {
38
40
  private surface: JsiSkSurface | null = null;
39
-
40
- constructor(
41
- private canvas: HTMLCanvasElement,
42
- private pd: number
43
- ) {
41
+ private grContext: GrDirectContext | null = null;
42
+ private contextHandle: WebGLContextHandle = 0;
43
+ private pd = 1;
44
+
45
+ constructor(private canvas: HTMLCanvasElement) {
46
+ this.contextHandle = CanvasKit.GetWebGLContext(canvas);
47
+ if (!this.contextHandle) {
48
+ throw new Error("Could not create a WebGL context");
49
+ }
50
+ this.grContext = CanvasKit.MakeWebGLContext(this.contextHandle);
51
+ if (!this.grContext) {
52
+ CanvasKit.deleteContext(this.contextHandle);
53
+ this.contextHandle = 0;
54
+ throw new Error("Could not create a graphics context");
55
+ }
56
+ const ctx = canvas.getContext("webgl2");
57
+ if (ctx) {
58
+ ctx.drawingBufferColorSpace = "display-p3";
59
+ }
44
60
  this.onResize();
45
61
  }
46
62
 
@@ -56,14 +72,28 @@ class WebGLRenderer implements Renderer {
56
72
  }
57
73
 
58
74
  onResize() {
59
- const { canvas, pd } = this;
60
- canvas.width = canvas.clientWidth * pd;
61
- canvas.height = canvas.clientHeight * pd;
62
- const surface = CanvasKit.MakeWebGLCanvasSurface(canvas);
63
- const ctx = canvas.getContext("webgl2");
64
- if (ctx) {
65
- ctx.drawingBufferColorSpace = "display-p3";
75
+ const { canvas } = this;
76
+ if (!this.grContext) {
77
+ return;
78
+ }
79
+ this.pd = window.devicePixelRatio;
80
+ canvas.width = canvas.clientWidth * this.pd;
81
+ canvas.height = canvas.clientHeight * this.pd;
82
+ this.surface?.ref.delete();
83
+ this.surface = null;
84
+ if (canvas.width === 0 || canvas.height === 0) {
85
+ // The canvas hasn't been laid out yet (or is hidden). The view's
86
+ // ResizeObserver calls us again as soon as it has a size.
87
+ return;
66
88
  }
89
+ // Reuse the existing WebGL context and GrDirectContext: only the surface
90
+ // needs to be recreated when the canvas is resized.
91
+ const surface = CanvasKit.MakeOnScreenGLSurface(
92
+ this.grContext,
93
+ canvas.width,
94
+ canvas.height,
95
+ CanvasKit.ColorSpace.SRGB
96
+ );
67
97
  if (!surface) {
68
98
  throw new Error("Could not create surface");
69
99
  }
@@ -75,7 +105,7 @@ class WebGLRenderer implements Renderer {
75
105
  const canvas = this.surface.getCanvas();
76
106
  canvas.clear(Float32Array.of(0, 0, 0, 0));
77
107
  canvas.save();
78
- canvas.scale(pd, pd);
108
+ canvas.scale(this.pd, this.pd);
79
109
  canvas.drawPicture(picture);
80
110
  canvas.restore();
81
111
  this.surface.ref.flush();
@@ -83,45 +113,82 @@ class WebGLRenderer implements Renderer {
83
113
  }
84
114
 
85
115
  dispose(): void {
86
- if (this.surface) {
87
- this.canvas
88
- ?.getContext("webgl2")
89
- ?.getExtension("WEBGL_lose_context")
90
- ?.loseContext();
91
- this.surface.ref.delete();
92
- this.surface = null;
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;
122
+ }
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;
93
138
  }
94
139
  }
95
140
  }
96
141
 
142
+ interface TempRenderResult {
143
+ surface: JsiSkSurface;
144
+ tempCanvas: OffscreenCanvas;
145
+ grContext: GrDirectContext;
146
+ contextHandle: WebGLContextHandle;
147
+ }
148
+
97
149
  class StaticWebGLRenderer implements Renderer {
98
150
  private cachedImage: SkImage | null = null;
151
+ private pd = 1;
99
152
 
100
- constructor(
101
- private canvas: HTMLCanvasElement,
102
- private pd: number
103
- ) {}
153
+ constructor(private canvas: HTMLCanvasElement) {}
104
154
 
105
155
  onResize(): void {
106
156
  this.cachedImage = null;
107
157
  }
108
158
 
109
- private renderPictureToSurface(
110
- picture: SkPicture
111
- ): { surface: JsiSkSurface; tempCanvas: OffscreenCanvas } | null {
159
+ private renderPictureToSurface(picture: SkPicture): TempRenderResult | null {
160
+ this.pd = window.devicePixelRatio;
161
+ if (this.canvas.clientWidth === 0 || this.canvas.clientHeight === 0) {
162
+ return null;
163
+ }
112
164
  const tempCanvas = new OffscreenCanvas(
113
165
  this.canvas.clientWidth * this.pd,
114
166
  this.canvas.clientHeight * this.pd
115
167
  );
116
168
 
117
169
  let surface: JsiSkSurface | null = null;
170
+ let grContext: GrDirectContext | null = null;
171
+ let contextHandle: WebGLContextHandle = 0;
118
172
 
119
173
  try {
120
- const webglSurface = CanvasKit.MakeWebGLCanvasSurface(tempCanvas);
174
+ contextHandle = CanvasKit.GetWebGLContext(tempCanvas);
175
+ if (!contextHandle) {
176
+ throw new Error("Could not create a WebGL context");
177
+ }
178
+ grContext = CanvasKit.MakeWebGLContext(contextHandle);
179
+ if (!grContext) {
180
+ throw new Error("Could not create a graphics context");
181
+ }
121
182
  const ctx = tempCanvas.getContext("webgl2");
122
183
  if (ctx) {
123
184
  ctx.drawingBufferColorSpace = "display-p3";
124
185
  }
186
+ const webglSurface = CanvasKit.MakeOnScreenGLSurface(
187
+ grContext,
188
+ tempCanvas.width,
189
+ tempCanvas.height,
190
+ CanvasKit.ColorSpace.SRGB
191
+ );
125
192
 
126
193
  if (!webglSurface) {
127
194
  throw new Error("Could not create WebGL surface");
@@ -137,23 +204,39 @@ class StaticWebGLRenderer implements Renderer {
137
204
  skiaCanvas.restore();
138
205
  surface.ref.flush();
139
206
 
140
- return { surface, tempCanvas };
207
+ return { surface, tempCanvas, grContext, contextHandle };
141
208
  } catch (error) {
142
- if (surface) {
143
- surface.ref.delete();
144
- }
145
- this.cleanupWebGLContext(tempCanvas);
209
+ this.cleanupRenderResult({
210
+ surface,
211
+ tempCanvas,
212
+ grContext,
213
+ contextHandle,
214
+ });
146
215
  return null;
147
216
  }
148
217
  }
149
218
 
150
- private cleanupWebGLContext(tempCanvas: OffscreenCanvas): void {
151
- const ctx = tempCanvas.getContext("webgl2");
152
- if (ctx) {
153
- const loseContext = ctx.getExtension("WEBGL_lose_context");
154
- if (loseContext) {
155
- loseContext.loseContext();
156
- }
219
+ private cleanupRenderResult(result: {
220
+ surface: JsiSkSurface | null;
221
+ tempCanvas: OffscreenCanvas;
222
+ grContext: GrDirectContext | null;
223
+ contextHandle: WebGLContextHandle;
224
+ }): void {
225
+ result.surface?.ref.delete();
226
+ if (result.grContext) {
227
+ result.grContext.releaseResourcesAndAbandonContext();
228
+ result.grContext.delete();
229
+ }
230
+ result.tempCanvas
231
+ .getContext("webgl2")
232
+ ?.getExtension("WEBGL_lose_context")
233
+ ?.loseContext();
234
+ if (result.contextHandle) {
235
+ // Unregister the context from CanvasKit's internal registry, otherwise
236
+ // it retains the OffscreenCanvas forever.
237
+ CanvasKit.deleteContext(result.contextHandle);
238
+ // Clear CanvasKit's current-context globals (see WebGLRenderer.dispose).
239
+ CanvasKit.MakeWebGLContext(result.contextHandle);
157
240
  }
158
241
  }
159
242
 
@@ -165,6 +248,7 @@ class StaticWebGLRenderer implements Renderer {
165
248
  const { tempCanvas } = renderResult;
166
249
  const ctx2d = this.canvas.getContext("2d");
167
250
  if (!ctx2d) {
251
+ this.cleanupRenderResult(renderResult);
168
252
  throw new Error("Could not get 2D context");
169
253
  }
170
254
 
@@ -185,7 +269,7 @@ class StaticWebGLRenderer implements Renderer {
185
269
  this.canvas.clientHeight * this.pd
186
270
  );
187
271
 
188
- this.cleanupWebGLContext(tempCanvas);
272
+ this.cleanupRenderResult(renderResult);
189
273
  }
190
274
 
191
275
  makeImageSnapshot(picture: SkPicture, rect?: SkRect): SkImage | null {
@@ -195,15 +279,14 @@ class StaticWebGLRenderer implements Renderer {
195
279
  return null;
196
280
  }
197
281
 
198
- const { surface, tempCanvas } = renderResult;
199
-
200
282
  try {
201
- this.cachedImage = surface.makeImageSnapshot(dp2Pixel(this.pd, rect));
283
+ this.cachedImage = renderResult.surface.makeImageSnapshot(
284
+ dp2Pixel(this.pd, rect)
285
+ );
202
286
  } catch (error) {
203
287
  console.error("Error creating image snapshot:", error);
204
288
  } finally {
205
- surface.ref.delete();
206
- this.cleanupWebGLContext(tempCanvas);
289
+ this.cleanupRenderResult(renderResult);
207
290
  }
208
291
  }
209
292
 
@@ -216,7 +299,43 @@ class StaticWebGLRenderer implements Renderer {
216
299
  }
217
300
  }
218
301
 
219
- const pd = Platform.PixelRatio;
302
+ // Mirrors the event react-native-web synthesizes for onLayout.
303
+ const makeLayoutEvent = (
304
+ x: number,
305
+ y: number,
306
+ width: number,
307
+ height: number
308
+ ): LayoutChangeEvent => ({
309
+ timeStamp: Date.now(),
310
+ nativeEvent: { layout: { x, y, width, height } },
311
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
312
+ // @ts-expect-error
313
+ currentTarget: 0,
314
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
315
+ // @ts-expect-error
316
+ target: 0,
317
+ bubbles: false,
318
+ cancelable: false,
319
+ defaultPrevented: false,
320
+ eventPhase: 0,
321
+ isDefaultPrevented() {
322
+ throw new Error("Method not supported on web.");
323
+ },
324
+ isPropagationStopped() {
325
+ throw new Error("Method not supported on web.");
326
+ },
327
+ persist() {
328
+ throw new Error("Method not supported on web.");
329
+ },
330
+ preventDefault() {
331
+ throw new Error("Method not supported on web.");
332
+ },
333
+ stopPropagation() {
334
+ throw new Error("Method not supported on web.");
335
+ },
336
+ isTrusted: true,
337
+ type: "",
338
+ });
220
339
 
221
340
  export interface SkiaPictureViewHandle {
222
341
  setPicture(picture: SkPicture): void;
@@ -243,18 +362,44 @@ export interface SkiaPictureViewProps extends SkiaPictureViewNativeProps {
243
362
  }
244
363
 
245
364
  export const SkiaPictureView = (props: SkiaPictureViewProps) => {
246
- const { ref } = props;
365
+ const { ref, picture, onLayout } = props;
247
366
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
248
- const renderer = useRef<Renderer | null>(null);
249
- const redrawRequestsRef = useRef(0);
250
- const requestIdRef = useRef(0);
367
+ const rendererRef = useRef<Renderer | null>(null);
251
368
  const pictureRef = useRef<SkPicture | null>(null);
252
-
253
- const { picture, onLayout } = props;
369
+ // The rendering lifecycle deliberately does not depend on layout events:
370
+ // - The renderer is created synchronously on mount (the canvas element is
371
+ // guaranteed to exist in useLayoutEffect; a zero size at that point is
372
+ // 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.
377
+ // - A ResizeObserver on the canvas itself recreates the surface and repaints
378
+ // synchronously (its callbacks run after layout, before paint), and is
379
+ // also the source of the user-facing onLayout event.
380
+ const redrawPendingRef = useRef(false);
381
+ const frameRef = useRef(0);
382
+ const onLayoutRef = useRef(onLayout);
383
+ useLayoutEffect(() => {
384
+ onLayoutRef.current = onLayout;
385
+ }, [onLayout]);
386
+
387
+ const flushRedraw = useCallback(() => {
388
+ frameRef.current = 0;
389
+ if (redrawPendingRef.current && rendererRef.current && pictureRef.current) {
390
+ redrawPendingRef.current = false;
391
+ rendererRef.current.draw(pictureRef.current);
392
+ }
393
+ // If the renderer or picture isn't available yet, the request stays
394
+ // pending and is flushed by whichever arrives last.
395
+ }, []);
254
396
 
255
397
  const redraw = useCallback(() => {
256
- redrawRequestsRef.current++;
257
- }, []);
398
+ redrawPendingRef.current = true;
399
+ if (frameRef.current === 0) {
400
+ frameRef.current = requestAnimationFrame(flushRedraw);
401
+ }
402
+ }, [flushRedraw]);
258
403
 
259
404
  const getSize = useCallback(() => {
260
405
  return {
@@ -272,8 +417,8 @@ export const SkiaPictureView = (props: SkiaPictureViewProps) => {
272
417
  );
273
418
 
274
419
  const makeImageSnapshot = useCallback((rect?: SkRect) => {
275
- if (renderer.current && pictureRef.current) {
276
- return renderer.current.makeImageSnapshot(pictureRef.current, rect);
420
+ if (rendererRef.current && pictureRef.current) {
421
+ return rendererRef.current.makeImageSnapshot(pictureRef.current, rect);
277
422
  }
278
423
  return null;
279
424
  }, []);
@@ -328,34 +473,107 @@ export const SkiaPictureView = (props: SkiaPictureViewProps) => {
328
473
  []
329
474
  );
330
475
 
331
- const tick = useCallback(() => {
332
- if (redrawRequestsRef.current > 0) {
333
- redrawRequestsRef.current = 0;
334
- if (renderer.current && pictureRef.current) {
335
- renderer.current.draw(pictureRef.current);
336
- }
476
+ const isStatic = props.__destroyWebGLContextAfterRender === true;
477
+
478
+ useLayoutEffect(() => {
479
+ const canvas = canvasRef.current;
480
+ if (!canvas) {
481
+ return undefined;
337
482
  }
338
- requestIdRef.current = requestAnimationFrame(tick);
339
- }, []);
483
+ const renderer = isStatic
484
+ ? new StaticWebGLRenderer(canvas)
485
+ : new WebGLRenderer(canvas);
486
+ rendererRef.current = renderer;
487
+
488
+ const drawPicture = () => {
489
+ if (pictureRef.current) {
490
+ redrawPendingRef.current = false;
491
+ renderer.draw(pictureRef.current);
492
+ }
493
+ };
340
494
 
341
- const onLayoutEvent = useCallback(
342
- (evt: LayoutChangeEvent) => {
343
- const canvas = canvasRef.current;
344
- if (canvas) {
345
- renderer.current =
346
- props.__destroyWebGLContextAfterRender === true
347
- ? new StaticWebGLRenderer(canvas, pd)
348
- : new WebGLRenderer(canvas, pd);
349
- if (pictureRef.current) {
350
- renderer.current.draw(pictureRef.current);
351
- }
495
+ // The renderer constructor already sized the surface from the current
496
+ // layout, so the observer's initial delivery only repaints if the size
497
+ // changed in between.
498
+ let lastWidth = canvas.clientWidth;
499
+ let lastHeight = canvas.clientHeight;
500
+ let lastPixelDensity = window.devicePixelRatio;
501
+ const resizeIfNeeded = () => {
502
+ const width = canvas.clientWidth;
503
+ const height = canvas.clientHeight;
504
+ const pixelDensity = window.devicePixelRatio;
505
+ if (
506
+ width === lastWidth &&
507
+ height === lastHeight &&
508
+ pixelDensity === lastPixelDensity
509
+ ) {
510
+ return;
352
511
  }
353
- if (onLayout) {
354
- onLayout(evt);
512
+ lastWidth = width;
513
+ lastHeight = height;
514
+ lastPixelDensity = pixelDensity;
515
+ renderer.onResize();
516
+ drawPicture();
517
+ };
518
+
519
+ const observer = new ResizeObserver((entries) => {
520
+ resizeIfNeeded();
521
+ const layoutHandler = onLayoutRef.current;
522
+ if (layoutHandler) {
523
+ const { left, top, width, height } = entries[0].contentRect;
524
+ // setTimeout 0 is taken from react-native-web (UIManager)
525
+ setTimeout(
526
+ () => layoutHandler(makeLayoutEvent(left, top, width, height)),
527
+ 0
528
+ );
355
529
  }
356
- },
357
- [onLayout, props.__destroyWebGLContextAfterRender]
358
- );
530
+ });
531
+ observer.observe(canvas);
532
+
533
+ // A pixel-density change with no CSS size change (browser zoom, moving
534
+ // the window to another display) doesn't trigger the ResizeObserver:
535
+ // watch it via matchMedia, re-arming the query for each new density.
536
+ // This degrades gracefully where unsupported: without matchMedia or
537
+ // MediaQueryList.addEventListener (older Safari), or where the query
538
+ // never matches because the resolution feature is unknown (Safari < 16),
539
+ // density-only changes simply don't repaint — everything else still does.
540
+ let media: MediaQueryList | null = null;
541
+ const onPixelDensityChange = () => {
542
+ resizeIfNeeded();
543
+ watchPixelDensity();
544
+ };
545
+ const watchPixelDensity = () => {
546
+ media?.removeEventListener("change", onPixelDensityChange);
547
+ media = null;
548
+ if (typeof window.matchMedia === "function") {
549
+ const query = window.matchMedia(
550
+ `(resolution: ${window.devicePixelRatio}dppx)`
551
+ );
552
+ if (typeof query.addEventListener === "function") {
553
+ media = query;
554
+ media.addEventListener("change", onPixelDensityChange);
555
+ }
556
+ }
557
+ };
558
+ watchPixelDensity();
559
+
560
+ // Paint any picture that was dispatched before the renderer existed.
561
+ drawPicture();
562
+
563
+ return () => {
564
+ observer.disconnect();
565
+ media?.removeEventListener("change", onPixelDensityChange);
566
+ rendererRef.current = null;
567
+ renderer.dispose();
568
+ };
569
+ }, [isStatic]);
570
+
571
+ useEffect(() => {
572
+ return () => {
573
+ cancelAnimationFrame(frameRef.current);
574
+ frameRef.current = 0;
575
+ };
576
+ }, []);
359
577
 
360
578
  useImperativeHandle(
361
579
  ref,
@@ -375,7 +593,8 @@ export const SkiaPictureView = (props: SkiaPictureViewProps) => {
375
593
 
376
594
  useEffect(() => {
377
595
  const nativeID = props.nativeID ?? `${SkiaViewNativeId.current++}`;
378
- (global.SkiaViewApi as ISkiaViewApiWeb).registerView(nativeID, {
596
+ const api = global.SkiaViewApi as ISkiaViewApiWeb;
597
+ api.registerView(nativeID, {
379
598
  setPicture,
380
599
  getSize,
381
600
  redraw,
@@ -383,6 +602,9 @@ export const SkiaPictureView = (props: SkiaPictureViewProps) => {
383
602
  measure,
384
603
  measureInWindow,
385
604
  } as SkiaPictureViewHandle);
605
+ return () => {
606
+ api.unregisterView(nativeID);
607
+ };
386
608
  }, [
387
609
  setPicture,
388
610
  getSize,
@@ -394,31 +616,21 @@ export const SkiaPictureView = (props: SkiaPictureViewProps) => {
394
616
  ]);
395
617
 
396
618
  useEffect(() => {
397
- if (props.picture) {
398
- setPicture(props.picture);
399
- }
400
- }, [setPicture, props.picture]);
401
-
402
- useEffect(() => {
403
- tick();
404
- return () => {
405
- cancelAnimationFrame(requestIdRef.current);
406
- if (renderer.current) {
407
- renderer.current.dispose();
408
- renderer.current = null;
409
- }
410
- };
411
- }, [tick]);
412
-
413
- useEffect(() => {
414
- if (renderer.current && pictureRef.current) {
415
- renderer.current.draw(pictureRef.current);
619
+ if (picture) {
620
+ setPicture(picture);
416
621
  }
417
- }, [picture, redraw]);
418
-
419
- const { debug = false, ref: _ref, ...viewProps } = props;
622
+ }, [setPicture, picture]);
623
+
624
+ const {
625
+ debug: _debug,
626
+ ref: _ref,
627
+ onLayout: _onLayout,
628
+ picture: _picture,
629
+ __destroyWebGLContextAfterRender: _isStatic,
630
+ ...viewProps
631
+ } = props;
420
632
  return (
421
- <Platform.View {...viewProps} onLayout={onLayoutEvent}>
633
+ <Platform.View {...viewProps}>
422
634
  <canvas
423
635
  ref={canvasRef}
424
636
  style={{ display: "block", width: "100%", height: "100%" }}
@@ -0,0 +1,21 @@
1
+ import { Platform } from "react-native";
2
+
3
+ // Non-spec helper: the canvas texture format most likely to reach the display
4
+ // with more than 8 bits per channel on the current platform. The returned
5
+ // format keeps standard (SDR) sRGB semantics: values are encoded exactly like
6
+ // on an 8-bit canvas, only with more precision, so colors match across
7
+ // formats. This is about bit depth, not HDR (no extended range).
8
+ //
9
+ // - Apple platforms: "rgba16float" (16-bit float per channel). The library
10
+ // tags the CAMetalLayer with the extended sRGB colorspace automatically so
11
+ // the values display identically to an 8-bit canvas.
12
+ // - Android: "rgb10a2unorm". A float16 swapchain works but is tagged as an
13
+ // SDR sRGB layer, and SurfaceFlinger quantizes SDR layers to the display
14
+ // pipeline depth during composition, discarding the extra precision.
15
+ // RGBA_1010102 buffers keep SDR semantics and can pass through composition,
16
+ // including direct scanout on 10-bit panels. Requires the Vulkan surface to
17
+ // expose the 10-bit format (common on modern devices).
18
+ // - Web: "rgba16float", the float canvas format supported by browsers.
19
+ export const getPreferredHighBitDepthCanvasFormat = (): GPUTextureFormat => {
20
+ return Platform.OS === "android" ? "rgb10a2unorm" : "rgba16float";
21
+ };
@@ -1,3 +1,4 @@
1
1
  export * from "./SkiaPictureView";
2
2
  export * from "./types";
3
3
  export * from "./WebGPUCanvas";
4
+ export * from "./formats";
@@ -32,6 +32,13 @@ export interface SkiaBaseViewProps extends ViewProps {
32
32
 
33
33
  opaque?: boolean;
34
34
 
35
+ /**
36
+ * Renders into a surface with more than 8 bits per channel (16-bit float on
37
+ * iOS, 10-bit on Android) to avoid banding in subtle gradients. On Android
38
+ * the extra precision survives composition only when combined with `opaque`.
39
+ */
40
+ highBitDepth?: boolean;
41
+
35
42
  // On web, only 16 WebGL contextes are allowed. If the drawing is non-animated, set
36
43
  // __destroyWebGLContextAfterRender to true to release the context after each draw.
37
44
  __destroyWebGLContextAfterRender?: boolean;