@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.
- package/lib/commonjs/renderer/Canvas.d.ts +1 -0
- package/lib/commonjs/renderer/Canvas.js.map +1 -1
- package/lib/commonjs/specs/NativeSkiaModule.web.d.ts +3 -3
- package/lib/commonjs/specs/NativeSkiaModule.web.js.map +1 -1
- package/lib/commonjs/specs/SkiaPictureViewNativeComponent.web.d.ts +1 -2
- package/lib/commonjs/views/SkiaPictureView.web.d.ts +10 -6
- package/lib/commonjs/views/SkiaPictureView.web.js +253 -20
- package/lib/commonjs/views/SkiaPictureView.web.js.map +1 -1
- package/lib/commonjs/views/types.d.ts +1 -0
- package/lib/commonjs/views/types.js.map +1 -1
- package/lib/module/renderer/Canvas.d.ts +1 -0
- package/lib/module/renderer/Canvas.js.map +1 -1
- package/lib/module/specs/NativeSkiaModule.web.d.ts +3 -3
- package/lib/module/specs/NativeSkiaModule.web.js.map +1 -1
- package/lib/module/specs/SkiaPictureViewNativeComponent.web.d.ts +1 -2
- package/lib/module/views/SkiaPictureView.web.d.ts +10 -6
- package/lib/module/views/SkiaPictureView.web.js +251 -18
- package/lib/module/views/SkiaPictureView.web.js.map +1 -1
- package/lib/module/views/types.d.ts +1 -0
- package/lib/module/views/types.js.map +1 -1
- package/lib/typescript/lib/commonjs/specs/SkiaPictureViewNativeComponent.web.d.ts +11 -1
- package/lib/typescript/lib/commonjs/views/SkiaPictureView.web.d.ts +1 -6
- package/lib/typescript/lib/module/specs/SkiaPictureViewNativeComponent.web.d.ts +1 -2
- package/lib/typescript/lib/module/views/SkiaPictureView.web.d.ts +2 -6
- package/lib/typescript/src/renderer/Canvas.d.ts +1 -0
- package/lib/typescript/src/specs/NativeSkiaModule.web.d.ts +3 -3
- package/lib/typescript/src/specs/SkiaPictureViewNativeComponent.web.d.ts +1 -2
- package/lib/typescript/src/views/SkiaPictureView.web.d.ts +10 -6
- package/lib/typescript/src/views/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/renderer/Canvas.tsx +1 -0
- package/src/renderer/__tests__/e2e/ParagraphMethods.spec.tsx +115 -110
- package/src/specs/NativeSkiaModule.web.ts +4 -4
- package/src/views/SkiaPictureView.web.tsx +312 -18
- package/src/views/types.ts +4 -0
- package/lib/commonjs/views/SkiaBaseWebView.d.ts +0 -40
- package/lib/commonjs/views/SkiaBaseWebView.js +0 -143
- package/lib/commonjs/views/SkiaBaseWebView.js.map +0 -1
- package/lib/module/views/SkiaBaseWebView.d.ts +0 -40
- package/lib/module/views/SkiaBaseWebView.js +0 -136
- package/lib/module/views/SkiaBaseWebView.js.map +0 -1
- package/lib/typescript/lib/commonjs/views/SkiaBaseWebView.d.ts +0 -39
- package/lib/typescript/lib/module/views/SkiaBaseWebView.d.ts +0 -36
- package/lib/typescript/src/views/SkiaBaseWebView.d.ts +0 -40
- 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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
}
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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(
|
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(
|
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(
|
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 {
|
4
|
+
import type { SkiaPictureViewHandle } from "../views/SkiaPictureView.web";
|
5
5
|
|
6
6
|
export type ISkiaViewApiWeb = ISkiaViewApi & {
|
7
|
-
views: Record<string,
|
7
|
+
views: Record<string, SkiaPictureViewHandle>;
|
8
8
|
deferedPictures: Record<string, SkPicture>;
|
9
|
-
registerView(nativeId: string, view:
|
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:
|
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
|
-
|
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 {
|
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
|
-
|
8
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
25
|
-
if (this.
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
+
});
|
package/src/views/types.ts
CHANGED
@@ -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 {
|