@shopify/react-native-skia 2.2.9 → 2.2.10

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 (45) hide show
  1. package/lib/commonjs/renderer/Canvas.d.ts +1 -0
  2. package/lib/commonjs/renderer/Canvas.js.map +1 -1
  3. package/lib/commonjs/specs/NativeSkiaModule.web.d.ts +3 -3
  4. package/lib/commonjs/specs/NativeSkiaModule.web.js.map +1 -1
  5. package/lib/commonjs/specs/SkiaPictureViewNativeComponent.web.d.ts +1 -2
  6. package/lib/commonjs/views/SkiaPictureView.web.d.ts +10 -6
  7. package/lib/commonjs/views/SkiaPictureView.web.js +253 -20
  8. package/lib/commonjs/views/SkiaPictureView.web.js.map +1 -1
  9. package/lib/commonjs/views/types.d.ts +1 -0
  10. package/lib/commonjs/views/types.js.map +1 -1
  11. package/lib/module/renderer/Canvas.d.ts +1 -0
  12. package/lib/module/renderer/Canvas.js.map +1 -1
  13. package/lib/module/specs/NativeSkiaModule.web.d.ts +3 -3
  14. package/lib/module/specs/NativeSkiaModule.web.js.map +1 -1
  15. package/lib/module/specs/SkiaPictureViewNativeComponent.web.d.ts +1 -2
  16. package/lib/module/views/SkiaPictureView.web.d.ts +10 -6
  17. package/lib/module/views/SkiaPictureView.web.js +251 -18
  18. package/lib/module/views/SkiaPictureView.web.js.map +1 -1
  19. package/lib/module/views/types.d.ts +1 -0
  20. package/lib/module/views/types.js.map +1 -1
  21. package/lib/typescript/lib/commonjs/specs/SkiaPictureViewNativeComponent.web.d.ts +11 -1
  22. package/lib/typescript/lib/commonjs/views/SkiaPictureView.web.d.ts +1 -6
  23. package/lib/typescript/lib/module/specs/SkiaPictureViewNativeComponent.web.d.ts +1 -2
  24. package/lib/typescript/lib/module/views/SkiaPictureView.web.d.ts +2 -6
  25. package/lib/typescript/src/renderer/Canvas.d.ts +1 -0
  26. package/lib/typescript/src/specs/NativeSkiaModule.web.d.ts +3 -3
  27. package/lib/typescript/src/specs/SkiaPictureViewNativeComponent.web.d.ts +1 -2
  28. package/lib/typescript/src/views/SkiaPictureView.web.d.ts +10 -6
  29. package/lib/typescript/src/views/types.d.ts +1 -0
  30. package/package.json +1 -1
  31. package/src/renderer/Canvas.tsx +1 -0
  32. package/src/renderer/__tests__/e2e/ParagraphMethods.spec.tsx +115 -110
  33. package/src/specs/NativeSkiaModule.web.ts +4 -4
  34. package/src/views/SkiaPictureView.web.tsx +312 -18
  35. package/src/views/types.ts +4 -0
  36. package/lib/commonjs/views/SkiaBaseWebView.d.ts +0 -40
  37. package/lib/commonjs/views/SkiaBaseWebView.js +0 -143
  38. package/lib/commonjs/views/SkiaBaseWebView.js.map +0 -1
  39. package/lib/module/views/SkiaBaseWebView.d.ts +0 -40
  40. package/lib/module/views/SkiaBaseWebView.js +0 -136
  41. package/lib/module/views/SkiaBaseWebView.js.map +0 -1
  42. package/lib/typescript/lib/commonjs/views/SkiaBaseWebView.d.ts +0 -39
  43. package/lib/typescript/lib/module/views/SkiaBaseWebView.d.ts +0 -36
  44. package/lib/typescript/src/views/SkiaBaseWebView.d.ts +0 -40
  45. package/src/views/SkiaBaseWebView.tsx +0 -140
@@ -11,109 +11,109 @@ const RobotoRegular = Array.from(
11
11
 
12
12
  describe("Paragraph Methods", () => {
13
13
  describe("getRectsForPlaceholders", () => {
14
- it("should handle multiple placeholders with different alignments", async () => {
15
- const placeholderRects = await surface.eval(
16
- (Skia, ctx) => {
17
- const robotoRegular = Skia.Typeface.MakeFreeTypeFaceFromData(
18
- Skia.Data.fromBytes(new Uint8Array(ctx.RobotoRegular))
19
- )!;
20
- const provider = Skia.TypefaceFontProvider.Make();
21
- provider.registerFont(robotoRegular, "Roboto");
22
-
23
- const builder = Skia.ParagraphBuilder.Make(
24
- {
25
- textStyle: {
26
- color: Skia.Color("black"),
27
- fontFamilies: ["Roboto"],
28
- fontSize: 16,
14
+ it("should handle multiple placeholders with different alignments", async () => {
15
+ const placeholderRects = await surface.eval(
16
+ (Skia, ctx) => {
17
+ const robotoRegular = Skia.Typeface.MakeFreeTypeFaceFromData(
18
+ Skia.Data.fromBytes(new Uint8Array(ctx.RobotoRegular))
19
+ )!;
20
+ const provider = Skia.TypefaceFontProvider.Make();
21
+ provider.registerFont(robotoRegular, "Roboto");
22
+
23
+ const builder = Skia.ParagraphBuilder.Make(
24
+ {
25
+ textStyle: {
26
+ color: Skia.Color("black"),
27
+ fontFamilies: ["Roboto"],
28
+ fontSize: 16,
29
+ },
29
30
  },
30
- },
31
- provider
32
- );
33
-
34
- builder.addText("Start ");
35
- builder.addPlaceholder(
36
- 20,
37
- 20,
38
- ctx.PlaceholderAlignment.Baseline,
39
- ctx.TextBaseline.Alphabetic
40
- );
41
- builder.addText(" middle ");
42
- builder.addPlaceholder(
43
- 15,
44
- 15,
45
- ctx.PlaceholderAlignment.Top,
46
- ctx.TextBaseline.Alphabetic
47
- );
48
- builder.addText(" end");
49
-
50
- const paragraph = builder.build();
51
- paragraph.layout(200);
52
-
53
- const rects = paragraph.getRectsForPlaceholders();
54
- return rects.map((r) => ({
55
- x: r.rect.x,
56
- y: r.rect.y,
57
- width: r.rect.width,
58
- height: r.rect.height,
59
- direction: r.direction,
60
- }));
61
- },
62
- {
63
- RobotoRegular,
64
- PlaceholderAlignment,
65
- TextBaseline,
66
- }
67
- );
68
-
69
- expect(placeholderRects).toHaveLength(2);
70
- expect(placeholderRects[0].width).toBe(20);
71
- expect(placeholderRects[0].height).toBe(20);
72
- expect(placeholderRects[1].width).toBeCloseTo(15);
73
- expect(placeholderRects[1].height).toBeCloseTo(15);
74
- });
31
+ provider
32
+ );
75
33
 
76
- it("should return correct direction for placeholders", async () => {
77
- const placeholderInfo = await surface.eval(
78
- (Skia, ctx) => {
79
- const robotoRegular = Skia.Typeface.MakeFreeTypeFaceFromData(
80
- Skia.Data.fromBytes(new Uint8Array(ctx.RobotoRegular))
81
- )!;
82
- const provider = Skia.TypefaceFontProvider.Make();
83
- provider.registerFont(robotoRegular, "Roboto");
84
-
85
- const builder = Skia.ParagraphBuilder.Make(
86
- {
87
- textStyle: {
88
- color: Skia.Color("black"),
89
- fontFamilies: ["Roboto"],
90
- fontSize: 16,
34
+ builder.addText("Start ");
35
+ builder.addPlaceholder(
36
+ 20,
37
+ 20,
38
+ ctx.PlaceholderAlignment.Baseline,
39
+ ctx.TextBaseline.Alphabetic
40
+ );
41
+ builder.addText(" middle ");
42
+ builder.addPlaceholder(
43
+ 15,
44
+ 15,
45
+ ctx.PlaceholderAlignment.Top,
46
+ ctx.TextBaseline.Alphabetic
47
+ );
48
+ builder.addText(" end");
49
+
50
+ const paragraph = builder.build();
51
+ paragraph.layout(200);
52
+
53
+ const rects = paragraph.getRectsForPlaceholders();
54
+ return rects.map((r) => ({
55
+ x: r.rect.x,
56
+ y: r.rect.y,
57
+ width: r.rect.width,
58
+ height: r.rect.height,
59
+ direction: r.direction,
60
+ }));
61
+ },
62
+ {
63
+ RobotoRegular,
64
+ PlaceholderAlignment,
65
+ TextBaseline,
66
+ }
67
+ );
68
+
69
+ expect(placeholderRects).toHaveLength(2);
70
+ expect(placeholderRects[0].width).toBe(20);
71
+ expect(placeholderRects[0].height).toBe(20);
72
+ expect(placeholderRects[1].width).toBeCloseTo(15);
73
+ expect(placeholderRects[1].height).toBeCloseTo(15);
74
+ });
75
+
76
+ it("should return correct direction for placeholders", async () => {
77
+ const placeholderInfo = await surface.eval(
78
+ (Skia, ctx) => {
79
+ const robotoRegular = Skia.Typeface.MakeFreeTypeFaceFromData(
80
+ Skia.Data.fromBytes(new Uint8Array(ctx.RobotoRegular))
81
+ )!;
82
+ const provider = Skia.TypefaceFontProvider.Make();
83
+ provider.registerFont(robotoRegular, "Roboto");
84
+
85
+ const builder = Skia.ParagraphBuilder.Make(
86
+ {
87
+ textStyle: {
88
+ color: Skia.Color("black"),
89
+ fontFamilies: ["Roboto"],
90
+ fontSize: 16,
91
+ },
91
92
  },
92
- },
93
- provider
94
- );
95
-
96
- builder.addText("Text with ");
97
- builder.addPlaceholder(30, 30);
98
- builder.addText(" placeholder");
99
-
100
- const paragraph = builder.build();
101
- paragraph.layout(300);
102
-
103
- const rects = paragraph.getRectsForPlaceholders();
104
- return rects.map((r) => ({
105
- direction: r.direction === ctx.TextDirection.LTR ? "LTR" : "RTL",
106
- }));
107
- },
108
- {
109
- RobotoRegular,
110
- TextDirection,
111
- }
112
- );
113
-
114
- expect(placeholderInfo).toHaveLength(1);
115
- expect(placeholderInfo[0].direction).toBe("LTR");
116
- });
93
+ provider
94
+ );
95
+
96
+ builder.addText("Text with ");
97
+ builder.addPlaceholder(30, 30);
98
+ builder.addText(" placeholder");
99
+
100
+ const paragraph = builder.build();
101
+ paragraph.layout(300);
102
+
103
+ const rects = paragraph.getRectsForPlaceholders();
104
+ return rects.map((r) => ({
105
+ direction: r.direction === ctx.TextDirection.LTR ? "LTR" : "RTL",
106
+ }));
107
+ },
108
+ {
109
+ RobotoRegular,
110
+ TextDirection,
111
+ }
112
+ );
113
+
114
+ expect(placeholderInfo).toHaveLength(1);
115
+ expect(placeholderInfo[0].direction).toBe("LTR");
116
+ });
117
117
 
118
118
  it("should return empty array when no placeholders", async () => {
119
119
  const placeholderCount = await surface.eval(
@@ -194,7 +194,7 @@ describe("Paragraph Methods", () => {
194
194
  expect(lineMetrics[0].ascent).toBeGreaterThan(0);
195
195
  expect(lineMetrics[0].descent).toBeGreaterThan(0);
196
196
  // Note: Even single lines without explicit breaks may report isHardBreak as true
197
- expect(typeof lineMetrics[0].isHardBreak).toBe('boolean');
197
+ expect(typeof lineMetrics[0].isHardBreak).toBe("boolean");
198
198
  });
199
199
 
200
200
  it("should return line metrics for multi-line text with wrapping", async () => {
@@ -232,12 +232,12 @@ describe("Paragraph Methods", () => {
232
232
  );
233
233
 
234
234
  expect(lineMetrics.length).toBeGreaterThan(1);
235
-
235
+
236
236
  // Check first line
237
237
  expect(lineMetrics[0].lineNumber).toBe(0);
238
238
  expect(lineMetrics[0].startIndex).toBe(0);
239
239
  expect(lineMetrics[0].width).toBeLessThanOrEqual(100);
240
-
240
+
241
241
  // Check second line
242
242
  expect(lineMetrics[1].lineNumber).toBe(1);
243
243
  expect(lineMetrics[1].startIndex).toBeGreaterThan(0);
@@ -277,12 +277,12 @@ describe("Paragraph Methods", () => {
277
277
  );
278
278
 
279
279
  expect(lineMetrics).toHaveLength(3);
280
-
280
+
281
281
  // All lines report isHardBreak as true in this implementation
282
282
  expect(lineMetrics[0].isHardBreak).toBe(true);
283
283
  expect(lineMetrics[1].isHardBreak).toBe(true);
284
284
  expect(lineMetrics[2].isHardBreak).toBe(true);
285
-
285
+
286
286
  // Check line numbers
287
287
  expect(lineMetrics[0].lineNumber).toBe(0);
288
288
  expect(lineMetrics[1].lineNumber).toBe(1);
@@ -322,19 +322,24 @@ describe("Paragraph Methods", () => {
322
322
  );
323
323
 
324
324
  expect(lineMetrics).toHaveLength(2);
325
-
325
+
326
326
  // First line
327
327
  const firstLine = lineMetrics[0];
328
328
  // Height should be close to ascent + descent
329
329
  expect(firstLine.height).toBeGreaterThan(0);
330
- expect(Math.abs(firstLine.height - (firstLine.ascent + firstLine.descent))).toBeLessThan(1);
330
+ expect(
331
+ Math.abs(firstLine.height - (firstLine.ascent + firstLine.descent))
332
+ ).toBeLessThan(1);
331
333
  expect(firstLine.left).toBe(0);
332
334
  expect(firstLine.baseline).toBeGreaterThan(0);
333
-
335
+
334
336
  // Second line should be below the first
335
337
  const secondLine = lineMetrics[1];
336
338
  expect(secondLine.baseline).toBeGreaterThan(firstLine.baseline);
337
- expect(secondLine.baseline - firstLine.baseline).toBeCloseTo(firstLine.height, 1);
339
+ expect(secondLine.baseline - firstLine.baseline).toBeCloseTo(
340
+ firstLine.height,
341
+ 1
342
+ );
338
343
  });
339
344
 
340
345
  it("should handle empty lines correctly", async () => {
@@ -370,7 +375,7 @@ describe("Paragraph Methods", () => {
370
375
  );
371
376
 
372
377
  expect(lineMetrics).toHaveLength(3);
373
-
378
+
374
379
  // Middle line should be empty but still have metrics
375
380
  const emptyLine = lineMetrics[1];
376
381
  // Empty line might have startIndex != endIndex depending on implementation
@@ -1,19 +1,19 @@
1
1
  /* eslint-disable import/no-anonymous-default-export */
2
2
  import type { SkPicture, SkRect } from "../skia/types";
3
3
  import type { ISkiaViewApi } from "../views/types";
4
- import type { SkiaPictureView } from "../views/SkiaPictureView.web";
4
+ import type { SkiaPictureViewHandle } from "../views/SkiaPictureView.web";
5
5
 
6
6
  export type ISkiaViewApiWeb = ISkiaViewApi & {
7
- views: Record<string, SkiaPictureView>;
7
+ views: Record<string, SkiaPictureViewHandle>;
8
8
  deferedPictures: Record<string, SkPicture>;
9
- registerView(nativeId: string, view: SkiaPictureView): void;
9
+ registerView(nativeId: string, view: SkiaPictureViewHandle): void;
10
10
  };
11
11
 
12
12
  global.SkiaViewApi = {
13
13
  views: {},
14
14
  deferedPictures: {},
15
15
  web: true,
16
- registerView(nativeId: string, view: SkiaPictureView) {
16
+ registerView(nativeId: string, view: SkiaPictureViewHandle) {
17
17
  // Maybe a picture for this view was already set
18
18
  if (this.deferedPictures[nativeId]) {
19
19
  view.setPicture(this.deferedPictures[nativeId] as SkPicture);
@@ -1,31 +1,325 @@
1
- import type { SkCanvas, SkPicture } from "../skia/types";
1
+ /* global HTMLCanvasElement */
2
+ import React, {
3
+ useRef,
4
+ useEffect,
5
+ useCallback,
6
+ useImperativeHandle,
7
+ forwardRef,
8
+ } from "react";
9
+ import type { LayoutChangeEvent } from "react-native";
10
+
11
+ import type { SkRect, SkPicture, SkImage } from "../skia/types";
12
+ import { JsiSkSurface } from "../skia/web/JsiSkSurface";
13
+ import { Platform } from "../Platform";
2
14
  import type { ISkiaViewApiWeb } from "../specs/NativeSkiaModule.web";
3
15
 
4
16
  import type { SkiaPictureViewNativeProps } from "./types";
5
- import { SkiaBaseWebView } from "./SkiaBaseWebView";
17
+ import { SkiaViewNativeId } from "./SkiaViewNativeId";
18
+
19
+ interface Renderer {
20
+ onResize(): void;
21
+ draw(picture: SkPicture): void;
22
+ makeImageSnapshot(picture: SkPicture, rect?: SkRect): SkImage | null;
23
+ dispose(): void;
24
+ }
25
+
26
+ class WebGLRenderer implements Renderer {
27
+ private surface: JsiSkSurface | null = null;
28
+
29
+ constructor(private canvas: HTMLCanvasElement, private pd: number) {
30
+ this.onResize();
31
+ }
32
+
33
+ makeImageSnapshot(picture: SkPicture, rect?: SkRect): SkImage | null {
34
+ if (!this.surface) {
35
+ return null;
36
+ }
37
+ const canvas = this.surface.getCanvas();
38
+ canvas!.clear(CanvasKit.TRANSPARENT);
39
+ this.draw(picture);
40
+ this.surface.ref.flush();
41
+ return this.surface.makeImageSnapshot(rect);
42
+ }
43
+
44
+ onResize() {
45
+ const { canvas, pd } = this;
46
+ canvas.width = canvas.clientWidth * pd;
47
+ canvas.height = canvas.clientHeight * pd;
48
+ const surface = CanvasKit.MakeWebGLCanvasSurface(canvas);
49
+ const ctx = canvas.getContext("webgl2");
50
+ if (ctx) {
51
+ ctx.drawingBufferColorSpace = "display-p3";
52
+ }
53
+ if (!surface) {
54
+ throw new Error("Could not create surface");
55
+ }
56
+ this.surface = new JsiSkSurface(CanvasKit, surface);
57
+ }
58
+
59
+ draw(picture: SkPicture) {
60
+ if (this.surface) {
61
+ const canvas = this.surface.getCanvas();
62
+ canvas.clear(Float32Array.of(0, 0, 0, 0));
63
+ canvas.save();
64
+ canvas.scale(pd, pd);
65
+ canvas.drawPicture(picture);
66
+ canvas.restore();
67
+ this.surface.ref.flush();
68
+ }
69
+ }
6
70
 
7
- export class SkiaPictureView extends SkiaBaseWebView<SkiaPictureViewNativeProps> {
8
- private picture: SkPicture | null = null;
71
+ dispose(): void {
72
+ if (this.surface) {
73
+ this.canvas
74
+ ?.getContext("webgl2")
75
+ ?.getExtension("WEBGL_lose_context")
76
+ ?.loseContext();
77
+ this.surface.ref.delete();
78
+ this.surface = null;
79
+ }
80
+ }
81
+ }
82
+
83
+ class StaticWebGLRenderer implements Renderer {
84
+ private cachedImage: SkImage | null = null;
85
+
86
+ constructor(private canvas: HTMLCanvasElement, private pd: number) {}
87
+
88
+ onResize(): void {
89
+ this.cachedImage = null;
90
+ }
91
+
92
+ private renderPictureToSurface(
93
+ picture: SkPicture
94
+ ): { surface: JsiSkSurface; tempCanvas: OffscreenCanvas } | null {
95
+ const tempCanvas = new OffscreenCanvas(
96
+ this.canvas.clientWidth * this.pd,
97
+ this.canvas.clientHeight * this.pd
98
+ );
99
+
100
+ let surface: JsiSkSurface | null = null;
101
+
102
+ try {
103
+ const webglSurface = CanvasKit.MakeWebGLCanvasSurface(tempCanvas);
104
+ const ctx = tempCanvas.getContext("webgl2");
105
+ if (ctx) {
106
+ ctx.drawingBufferColorSpace = "display-p3";
107
+ }
108
+
109
+ if (!webglSurface) {
110
+ throw new Error("Could not create WebGL surface");
111
+ }
112
+
113
+ surface = new JsiSkSurface(CanvasKit, webglSurface);
114
+
115
+ const skiaCanvas = surface.getCanvas();
116
+ skiaCanvas.clear(Float32Array.of(0, 0, 0, 0));
117
+ skiaCanvas.save();
118
+ skiaCanvas.scale(this.pd, this.pd);
119
+ skiaCanvas.drawPicture(picture);
120
+ skiaCanvas.restore();
121
+ surface.ref.flush();
122
+
123
+ return { surface, tempCanvas };
124
+ } catch (error) {
125
+ if (surface) {
126
+ surface.ref.delete();
127
+ }
128
+ this.cleanupWebGLContext(tempCanvas);
129
+ return null;
130
+ }
131
+ }
9
132
 
10
- constructor(props: SkiaPictureViewNativeProps) {
11
- super(props);
12
- const { nativeID } = props;
13
- if (!nativeID) {
14
- throw new Error("SkiaPictureView requires a nativeID prop");
133
+ private cleanupWebGLContext(tempCanvas: OffscreenCanvas): void {
134
+ const ctx = tempCanvas.getContext("webgl2");
135
+ if (ctx) {
136
+ const loseContext = ctx.getExtension("WEBGL_lose_context");
137
+ if (loseContext) {
138
+ loseContext.loseContext();
139
+ }
15
140
  }
16
- (global.SkiaViewApi as ISkiaViewApiWeb).registerView(nativeID, this);
17
141
  }
18
142
 
19
- public setPicture(picture: SkPicture) {
20
- this.picture = picture;
21
- this.redraw();
143
+ draw(picture: SkPicture): void {
144
+ const renderResult = this.renderPictureToSurface(picture);
145
+ if (!renderResult) {
146
+ return;
147
+ }
148
+ const { tempCanvas } = renderResult;
149
+ const ctx2d = this.canvas.getContext("2d");
150
+ if (!ctx2d) {
151
+ throw new Error("Could not get 2D context");
152
+ }
153
+
154
+ // Set canvas dimensions to match pixel density
155
+ this.canvas.width = this.canvas.clientWidth * this.pd;
156
+ this.canvas.height = this.canvas.clientHeight * this.pd;
157
+
158
+ // Draw the tempCanvas scaled down to the display size
159
+ ctx2d.drawImage(
160
+ tempCanvas,
161
+ 0,
162
+ 0,
163
+ tempCanvas.width,
164
+ tempCanvas.height,
165
+ 0,
166
+ 0,
167
+ this.canvas.clientWidth * this.pd,
168
+ this.canvas.clientHeight * this.pd
169
+ );
170
+
171
+ this.cleanupWebGLContext(tempCanvas);
22
172
  }
23
173
 
24
- protected renderInCanvas(canvas: SkCanvas): void {
25
- if (this.props.picture) {
26
- canvas.drawPicture(this.props.picture);
27
- } else if (this.picture) {
28
- canvas.drawPicture(this.picture);
174
+ makeImageSnapshot(picture: SkPicture, rect?: SkRect): SkImage | null {
175
+ if (!this.cachedImage) {
176
+ const renderResult = this.renderPictureToSurface(picture);
177
+ if (!renderResult) {
178
+ return null;
179
+ }
180
+
181
+ const { surface, tempCanvas } = renderResult;
182
+
183
+ try {
184
+ this.cachedImage = surface.makeImageSnapshot(rect);
185
+ } catch (error) {
186
+ console.error("Error creating image snapshot:", error);
187
+ } finally {
188
+ surface.ref.delete();
189
+ this.cleanupWebGLContext(tempCanvas);
190
+ }
29
191
  }
192
+
193
+ return this.cachedImage;
194
+ }
195
+
196
+ dispose(): void {
197
+ this.cachedImage?.dispose();
198
+ this.cachedImage = null;
30
199
  }
31
200
  }
201
+
202
+ const pd = Platform.PixelRatio;
203
+
204
+ export interface SkiaPictureViewHandle {
205
+ setPicture(picture: SkPicture): void;
206
+ getSize(): { width: number; height: number };
207
+ redraw(): void;
208
+ makeImageSnapshot(rect?: SkRect): SkImage | null;
209
+ }
210
+
211
+ export const SkiaPictureView = forwardRef<
212
+ SkiaPictureViewHandle,
213
+ SkiaPictureViewNativeProps
214
+ >((props, ref) => {
215
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
216
+ const renderer = useRef<Renderer | null>(null);
217
+ const redrawRequestsRef = useRef(0);
218
+ const requestIdRef = useRef(0);
219
+ const pictureRef = useRef<SkPicture | null>(null);
220
+
221
+ const { picture, onLayout } = props;
222
+
223
+ const redraw = useCallback(() => {
224
+ redrawRequestsRef.current++;
225
+ }, []);
226
+
227
+ const getSize = useCallback(() => {
228
+ return {
229
+ width: canvasRef.current?.clientWidth || 0,
230
+ height: canvasRef.current?.clientHeight || 0,
231
+ };
232
+ }, []);
233
+
234
+ const setPicture = useCallback(
235
+ (newPicture: SkPicture) => {
236
+ pictureRef.current = newPicture;
237
+ redraw();
238
+ },
239
+ [redraw]
240
+ );
241
+
242
+ const makeImageSnapshot = useCallback((rect?: SkRect) => {
243
+ if (renderer.current && pictureRef.current) {
244
+ return renderer.current.makeImageSnapshot(pictureRef.current, rect);
245
+ }
246
+ return null;
247
+ }, []);
248
+
249
+ const tick = useCallback(() => {
250
+ if (redrawRequestsRef.current > 0) {
251
+ redrawRequestsRef.current = 0;
252
+ if (renderer.current && pictureRef.current) {
253
+ renderer.current.draw(pictureRef.current);
254
+ }
255
+ }
256
+ requestIdRef.current = requestAnimationFrame(tick);
257
+ }, []);
258
+
259
+ const onLayoutEvent = useCallback(
260
+ (evt: LayoutChangeEvent) => {
261
+ const canvas = canvasRef.current;
262
+ if (canvas) {
263
+ renderer.current =
264
+ props.__destroyWebGLContextAfterRender === true
265
+ ? new StaticWebGLRenderer(canvas, pd)
266
+ : new WebGLRenderer(canvas, pd);
267
+ if (pictureRef.current) {
268
+ renderer.current.draw(pictureRef.current);
269
+ }
270
+ }
271
+ if (onLayout) {
272
+ onLayout(evt);
273
+ }
274
+ },
275
+ [onLayout, props.__destroyWebGLContextAfterRender]
276
+ );
277
+
278
+ useImperativeHandle(
279
+ ref,
280
+ () => ({
281
+ setPicture,
282
+ getSize,
283
+ redraw,
284
+ makeImageSnapshot,
285
+ }),
286
+ [setPicture, getSize, redraw, makeImageSnapshot]
287
+ );
288
+
289
+ useEffect(() => {
290
+ const nativeID = props.nativeID ?? `${SkiaViewNativeId.current++}`;
291
+ (global.SkiaViewApi as ISkiaViewApiWeb).registerView(nativeID, {
292
+ setPicture,
293
+ getSize,
294
+ redraw,
295
+ makeImageSnapshot,
296
+ } as SkiaPictureViewHandle);
297
+ if (props.picture) {
298
+ setPicture(props.picture);
299
+ }
300
+ }, [setPicture, getSize, redraw, makeImageSnapshot, props]);
301
+
302
+ useEffect(() => {
303
+ tick();
304
+ return () => {
305
+ cancelAnimationFrame(requestIdRef.current);
306
+ if (renderer.current) {
307
+ renderer.current.dispose();
308
+ renderer.current = null;
309
+ }
310
+ };
311
+ }, [tick]);
312
+
313
+ useEffect(() => {
314
+ if (renderer.current && pictureRef.current) {
315
+ renderer.current.draw(pictureRef.current);
316
+ }
317
+ }, [picture, redraw]);
318
+
319
+ const { debug = false, ...viewProps } = props;
320
+ return (
321
+ <Platform.View {...viewProps} onLayout={onLayoutEvent}>
322
+ <canvas ref={canvasRef} style={{ display: "flex", flex: 1 }} />
323
+ </Platform.View>
324
+ );
325
+ });
@@ -31,6 +31,10 @@ export interface SkiaBaseViewProps extends ViewProps {
31
31
  onSize?: SharedValue<SkSize>;
32
32
 
33
33
  opaque?: boolean;
34
+
35
+ // On web, only 16 WebGL contextes are allowed. If the drawing is non-animated, set
36
+ // __destroyWebGLContextAfterRender to true to release the context after each draw.
37
+ __destroyWebGLContextAfterRender?: boolean;
34
38
  }
35
39
 
36
40
  export interface SkiaPictureViewNativeProps extends SkiaBaseViewProps {