@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.
- package/android/cpp/jni/include/JniSkiaBaseView.h +6 -4
- package/android/cpp/jni/include/JniSkiaPictureView.h +8 -6
- package/android/cpp/rnskia-android/OpenGLContext.h +10 -1
- package/android/cpp/rnskia-android/RNSkAndroidView.h +8 -8
- package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.cpp +9 -5
- package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.h +4 -2
- package/android/src/main/java/com/shopify/reactnative/skia/SkiaBaseView.java +48 -12
- package/android/src/main/java/com/shopify/reactnative/skia/SkiaBaseViewManager.java +5 -0
- package/android/src/main/java/com/shopify/reactnative/skia/SkiaPictureView.java +2 -2
- package/android/src/main/java/com/shopify/reactnative/skia/ViewScreenshotService.java +11 -7
- package/apple/MetalContext.h +7 -6
- package/apple/MetalLayerColorSpace.mm +39 -0
- package/apple/MetalLayerColorSpaceUtils.h +34 -0
- package/apple/MetalWindowContext.h +3 -1
- package/apple/MetalWindowContext.mm +9 -8
- package/apple/RNSkAppleView.h +6 -0
- package/apple/RNSkMetalCanvasProvider.h +2 -0
- package/apple/RNSkMetalCanvasProvider.mm +15 -2
- package/apple/SkiaPictureView.mm +1 -0
- package/apple/SkiaPictureViewManager.mm +5 -0
- package/apple/SkiaUIView.h +1 -0
- package/apple/SkiaUIView.mm +9 -0
- package/cpp/api/JsiSkSurface.h +8 -2
- package/cpp/rnskia/RNDawnContext.h +4 -3
- package/cpp/rnskia/RNDawnUtils.h +25 -6
- package/cpp/rnskia/RNDawnWindowContext.h +44 -10
- package/cpp/rnskia/RNMetalLayerColorSpace.h +16 -0
- package/cpp/rnskia/RNSkJsiViewApi.h +46 -24
- package/cpp/rnwgpu/SurfaceRegistry.h +10 -4
- package/lib/commonjs/Platform/Platform.web.js +2 -90
- package/lib/commonjs/Platform/Platform.web.js.map +1 -1
- package/lib/commonjs/mock/index.js +1 -0
- package/lib/commonjs/mock/index.js.map +1 -1
- package/lib/commonjs/renderer/Canvas.d.ts +10 -1
- package/lib/commonjs/renderer/Canvas.js +2 -0
- package/lib/commonjs/renderer/Canvas.js.map +1 -1
- package/lib/commonjs/skia/types/Surface/Surface.d.ts +5 -1
- package/lib/commonjs/skia/types/Surface/Surface.js.map +1 -1
- package/lib/commonjs/skia/web/JsiSkParagraphStyle.js +14 -3
- package/lib/commonjs/skia/web/JsiSkParagraphStyle.js.map +1 -1
- package/lib/commonjs/skia/web/JsiSkSurface.d.ts +1 -1
- package/lib/commonjs/skia/web/JsiSkSurface.js +2 -1
- package/lib/commonjs/skia/web/JsiSkSurface.js.map +1 -1
- package/lib/commonjs/sksg/Container.native.d.ts +1 -0
- package/lib/commonjs/sksg/Container.native.js +11 -0
- package/lib/commonjs/sksg/Container.native.js.map +1 -1
- package/lib/commonjs/sksg/Container.web.d.ts +1 -0
- package/lib/commonjs/sksg/Container.web.js +11 -0
- package/lib/commonjs/sksg/Container.web.js.map +1 -1
- package/lib/commonjs/specs/NativeSkiaModule.web.d.ts +2 -0
- package/lib/commonjs/specs/NativeSkiaModule.web.js +32 -7
- package/lib/commonjs/specs/NativeSkiaModule.web.js.map +1 -1
- package/lib/commonjs/specs/SkiaPictureViewNativeComponent.d.ts +1 -0
- package/lib/commonjs/specs/SkiaPictureViewNativeComponent.js.map +1 -1
- package/lib/commonjs/views/SkiaPictureView.js +2 -0
- package/lib/commonjs/views/SkiaPictureView.js.map +1 -1
- package/lib/commonjs/views/SkiaPictureView.web.js +279 -94
- package/lib/commonjs/views/SkiaPictureView.web.js.map +1 -1
- package/lib/commonjs/views/formats.d.ts +1 -0
- package/lib/commonjs/views/formats.js +28 -0
- package/lib/commonjs/views/formats.js.map +1 -0
- package/lib/commonjs/views/index.d.ts +1 -0
- package/lib/commonjs/views/index.js +11 -0
- package/lib/commonjs/views/index.js.map +1 -1
- package/lib/commonjs/views/types.d.ts +6 -0
- package/lib/commonjs/views/types.js.map +1 -1
- package/lib/module/Platform/Platform.web.js +3 -91
- package/lib/module/Platform/Platform.web.js.map +1 -1
- package/lib/module/mock/index.js +1 -0
- package/lib/module/mock/index.js.map +1 -1
- package/lib/module/renderer/Canvas.d.ts +10 -1
- package/lib/module/renderer/Canvas.js +2 -0
- package/lib/module/renderer/Canvas.js.map +1 -1
- package/lib/module/skia/types/Surface/Surface.d.ts +5 -1
- package/lib/module/skia/types/Surface/Surface.js.map +1 -1
- package/lib/module/skia/web/JsiSkParagraphStyle.js +14 -3
- package/lib/module/skia/web/JsiSkParagraphStyle.js.map +1 -1
- package/lib/module/skia/web/JsiSkSurface.d.ts +1 -1
- package/lib/module/skia/web/JsiSkSurface.js +2 -1
- package/lib/module/skia/web/JsiSkSurface.js.map +1 -1
- package/lib/module/sksg/Container.native.d.ts +1 -0
- package/lib/module/sksg/Container.native.js +11 -0
- package/lib/module/sksg/Container.native.js.map +1 -1
- package/lib/module/sksg/Container.web.d.ts +1 -0
- package/lib/module/sksg/Container.web.js +11 -0
- package/lib/module/sksg/Container.web.js.map +1 -1
- package/lib/module/specs/NativeSkiaModule.web.d.ts +2 -0
- package/lib/module/specs/NativeSkiaModule.web.js +32 -7
- package/lib/module/specs/NativeSkiaModule.web.js.map +1 -1
- package/lib/module/specs/SkiaPictureViewNativeComponent.d.ts +1 -0
- package/lib/module/specs/SkiaPictureViewNativeComponent.js.map +1 -1
- package/lib/module/views/SkiaPictureView.js +2 -0
- package/lib/module/views/SkiaPictureView.js.map +1 -1
- package/lib/module/views/SkiaPictureView.web.js +280 -95
- package/lib/module/views/SkiaPictureView.web.js.map +1 -1
- package/lib/module/views/formats.d.ts +1 -0
- package/lib/module/views/formats.js +22 -0
- package/lib/module/views/formats.js.map +1 -0
- package/lib/module/views/index.d.ts +1 -0
- package/lib/module/views/index.js +1 -0
- package/lib/module/views/index.js.map +1 -1
- package/lib/module/views/types.d.ts +6 -0
- package/lib/module/views/types.js.map +1 -1
- package/lib/typescript/lib/commonjs/Platform/Platform.web.d.ts +1 -2
- package/lib/typescript/lib/commonjs/mock/index.d.ts +1 -0
- package/lib/typescript/lib/commonjs/renderer/Canvas.d.ts +2 -1
- package/lib/typescript/lib/commonjs/skia/web/JsiSkSurface.d.ts +1 -1
- package/lib/typescript/lib/commonjs/sksg/Container.native.d.ts +1 -1
- package/lib/typescript/lib/commonjs/sksg/Container.web.d.ts +1 -1
- package/lib/typescript/lib/commonjs/views/formats.d.ts +2 -0
- package/lib/typescript/lib/module/Platform/Platform.web.d.ts +1 -3
- package/lib/typescript/lib/module/mock/index.d.ts +2 -2
- package/lib/typescript/lib/module/renderer/Canvas.d.ts +2 -1
- package/lib/typescript/lib/module/skia/web/JsiSkSurface.d.ts +1 -1
- package/lib/typescript/lib/module/sksg/Container.native.d.ts +1 -1
- package/lib/typescript/lib/module/sksg/Container.web.d.ts +1 -1
- package/lib/typescript/lib/module/views/formats.d.ts +1 -0
- package/lib/typescript/lib/module/views/index.d.ts +1 -0
- package/lib/typescript/src/renderer/Canvas.d.ts +10 -1
- package/lib/typescript/src/skia/types/Surface/Surface.d.ts +5 -1
- package/lib/typescript/src/skia/web/JsiSkSurface.d.ts +1 -1
- package/lib/typescript/src/sksg/Container.native.d.ts +1 -0
- package/lib/typescript/src/sksg/Container.web.d.ts +1 -0
- package/lib/typescript/src/specs/NativeSkiaModule.web.d.ts +2 -0
- package/lib/typescript/src/specs/SkiaPictureViewNativeComponent.d.ts +1 -0
- package/lib/typescript/src/views/formats.d.ts +1 -0
- package/lib/typescript/src/views/index.d.ts +1 -0
- package/lib/typescript/src/views/types.d.ts +6 -0
- package/package.json +4 -1
- package/react-native-skia.podspec +1 -0
- package/src/Platform/Platform.web.tsx +7 -99
- package/src/mock/index.ts +1 -0
- package/src/renderer/Canvas.tsx +11 -0
- package/src/skia/types/Surface/Surface.ts +5 -1
- package/src/skia/web/JsiSkParagraphStyle.ts +16 -2
- package/src/skia/web/JsiSkSurface.ts +2 -1
- package/src/sksg/Container.native.ts +12 -0
- package/src/sksg/Container.web.ts +12 -0
- package/src/specs/NativeSkiaModule.web.ts +39 -7
- package/src/specs/SkiaPictureViewNativeComponent.ts +1 -0
- package/src/views/SkiaPictureView.tsx +2 -0
- package/src/views/SkiaPictureView.web.tsx +318 -106
- package/src/views/formats.ts +21 -0
- package/src/views/index.ts +1 -0
- 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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.
|
|
92
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
surface
|
|
144
|
-
|
|
145
|
-
|
|
209
|
+
this.cleanupRenderResult({
|
|
210
|
+
surface,
|
|
211
|
+
tempCanvas,
|
|
212
|
+
grContext,
|
|
213
|
+
contextHandle,
|
|
214
|
+
});
|
|
146
215
|
return null;
|
|
147
216
|
}
|
|
148
217
|
}
|
|
149
218
|
|
|
150
|
-
private
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
276
|
-
return
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
476
|
+
const isStatic = props.__destroyWebGLContextAfterRender === true;
|
|
477
|
+
|
|
478
|
+
useLayoutEffect(() => {
|
|
479
|
+
const canvas = canvasRef.current;
|
|
480
|
+
if (!canvas) {
|
|
481
|
+
return undefined;
|
|
337
482
|
}
|
|
338
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
398
|
-
setPicture(
|
|
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
|
-
}, [
|
|
418
|
-
|
|
419
|
-
const {
|
|
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}
|
|
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
|
+
};
|
package/src/views/index.ts
CHANGED
package/src/views/types.ts
CHANGED
|
@@ -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;
|