@shopify/react-native-skia 2.1.1 → 2.2.1

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 (129) hide show
  1. package/android/CMakeLists.txt +1 -1
  2. package/android/cpp/rnskia-android/OpenGLWindowContext.h +1 -1
  3. package/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h +1 -1
  4. package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp +1 -1
  5. package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.cpp +1 -1
  6. package/android/cpp/rnskia-android/RNSkOpenGLCanvasProvider.h +1 -1
  7. package/android/src/main/java/com/shopify/reactnative/skia/SkiaPictureViewManager.java +6 -0
  8. package/android/src/paper/java/com/facebook/react/viewmanagers/SkiaPictureViewManagerInterface.java +1 -0
  9. package/apple/MetalContext.h +2 -2
  10. package/apple/MetalWindowContext.h +2 -2
  11. package/apple/MetalWindowContext.mm +7 -4
  12. package/apple/RNSkApplePlatformContext.mm +1 -1
  13. package/apple/RNSkAppleView.h +7 -1
  14. package/apple/RNSkMetalCanvasProvider.h +4 -1
  15. package/apple/RNSkMetalCanvasProvider.mm +9 -4
  16. package/apple/SkiaPictureView.mm +4 -0
  17. package/apple/SkiaUIView.h +1 -0
  18. package/apple/SkiaUIView.mm +9 -0
  19. package/cpp/api/JsiSkImage.h +1 -1
  20. package/cpp/api/JsiSkSurface.h +1 -1
  21. package/cpp/api/JsiSkiaContext.h +1 -1
  22. package/cpp/api/recorder/ImageFilters.h +3 -3
  23. package/cpp/api/recorder/Paint.h +4 -0
  24. package/cpp/rnskia/{DawnContext.h → RNDawnContext.h} +3 -3
  25. package/cpp/rnskia/{DawnWindowContext.cpp → RNDawnWindowContext.cpp} +3 -3
  26. package/cpp/rnskia/{DawnWindowContext.h → RNDawnWindowContext.h} +2 -2
  27. package/cpp/rnskia/RNSkJsiViewApi.h +36 -1
  28. package/cpp/rnskia/RNSkPlatformContext.h +1 -1
  29. package/cpp/rnskia/RNSkView.h +10 -0
  30. package/lib/commonjs/dom/types/Drawings.d.ts +1 -1
  31. package/lib/commonjs/dom/types/Drawings.js.map +1 -1
  32. package/lib/commonjs/renderer/Canvas.d.ts +12 -4
  33. package/lib/commonjs/renderer/Canvas.js +49 -25
  34. package/lib/commonjs/renderer/Canvas.js.map +1 -1
  35. package/lib/commonjs/renderer/components/ImageFilter.js.map +1 -1
  36. package/lib/commonjs/skia/types/Matrix4.d.ts +4 -0
  37. package/lib/commonjs/skia/types/Matrix4.js +18 -1
  38. package/lib/commonjs/skia/types/Matrix4.js.map +1 -1
  39. package/lib/commonjs/sksg/Container.d.ts +13 -7
  40. package/lib/commonjs/sksg/Container.js +44 -18
  41. package/lib/commonjs/sksg/Container.js.map +1 -1
  42. package/lib/commonjs/sksg/Reconciler.d.ts +3 -2
  43. package/lib/commonjs/sksg/Reconciler.js +2 -2
  44. package/lib/commonjs/sksg/Reconciler.js.map +1 -1
  45. package/lib/commonjs/sksg/Recorder/Player.js +9 -3
  46. package/lib/commonjs/sksg/Recorder/Player.js.map +1 -1
  47. package/lib/commonjs/sksg/Recorder/commands/ImageFilters.js +2 -2
  48. package/lib/commonjs/sksg/Recorder/commands/ImageFilters.js.map +1 -1
  49. package/lib/commonjs/specs/SkiaPictureViewNativeComponent.d.ts +1 -0
  50. package/lib/commonjs/specs/SkiaPictureViewNativeComponent.js.map +1 -1
  51. package/lib/commonjs/views/types.d.ts +1 -0
  52. package/lib/commonjs/views/types.js.map +1 -1
  53. package/lib/module/dom/types/Drawings.d.ts +1 -1
  54. package/lib/module/dom/types/Drawings.js.map +1 -1
  55. package/lib/module/renderer/Canvas.d.ts +12 -4
  56. package/lib/module/renderer/Canvas.js +48 -25
  57. package/lib/module/renderer/Canvas.js.map +1 -1
  58. package/lib/module/renderer/components/ImageFilter.js.map +1 -1
  59. package/lib/module/skia/types/Matrix4.d.ts +4 -0
  60. package/lib/module/skia/types/Matrix4.js +16 -0
  61. package/lib/module/skia/types/Matrix4.js.map +1 -1
  62. package/lib/module/sksg/Container.d.ts +13 -7
  63. package/lib/module/sksg/Container.js +44 -18
  64. package/lib/module/sksg/Container.js.map +1 -1
  65. package/lib/module/sksg/Reconciler.d.ts +3 -2
  66. package/lib/module/sksg/Reconciler.js +2 -2
  67. package/lib/module/sksg/Reconciler.js.map +1 -1
  68. package/lib/module/sksg/Recorder/Player.js +9 -3
  69. package/lib/module/sksg/Recorder/Player.js.map +1 -1
  70. package/lib/module/sksg/Recorder/commands/ImageFilters.js +2 -2
  71. package/lib/module/sksg/Recorder/commands/ImageFilters.js.map +1 -1
  72. package/lib/module/specs/SkiaPictureViewNativeComponent.d.ts +1 -0
  73. package/lib/module/specs/SkiaPictureViewNativeComponent.js.map +1 -1
  74. package/lib/module/views/types.d.ts +1 -0
  75. package/lib/module/views/types.js.map +1 -1
  76. package/lib/typescript/lib/commonjs/renderer/Canvas.d.ts +8 -2
  77. package/lib/typescript/lib/commonjs/skia/types/Matrix4.d.ts +1 -0
  78. package/lib/typescript/lib/commonjs/sksg/Container.d.ts +11 -3
  79. package/lib/typescript/lib/commonjs/sksg/Reconciler.d.ts +7 -4
  80. package/lib/typescript/lib/module/mock/index.d.ts +1 -0
  81. package/lib/typescript/lib/module/renderer/Canvas.d.ts +11 -2
  82. package/lib/typescript/lib/module/skia/types/Matrix4.d.ts +1 -0
  83. package/lib/typescript/lib/module/sksg/Container.d.ts +11 -3
  84. package/lib/typescript/lib/module/sksg/Reconciler.d.ts +7 -4
  85. package/lib/typescript/src/dom/types/Drawings.d.ts +1 -1
  86. package/lib/typescript/src/renderer/Canvas.d.ts +12 -4
  87. package/lib/typescript/src/skia/types/Matrix4.d.ts +4 -0
  88. package/lib/typescript/src/sksg/Container.d.ts +13 -7
  89. package/lib/typescript/src/sksg/Reconciler.d.ts +3 -2
  90. package/lib/typescript/src/specs/SkiaPictureViewNativeComponent.d.ts +1 -0
  91. package/lib/typescript/src/views/types.d.ts +1 -0
  92. package/libs/android/arm64-v8a/libskia.a +0 -0
  93. package/libs/android/armeabi-v7a/libskia.a +0 -0
  94. package/libs/android/x86/libskia.a +0 -0
  95. package/libs/android/x86_64/libskia.a +0 -0
  96. package/libs/apple/libpathops.xcframework/Info.plist +15 -15
  97. package/libs/apple/libskia.xcframework/Info.plist +11 -11
  98. package/libs/apple/libskia.xcframework/ios-arm64_arm64e/libskia.a +0 -0
  99. package/libs/apple/libskia.xcframework/ios-arm64_arm64e_x86_64-simulator/libskia.a +0 -0
  100. package/libs/apple/libskia.xcframework/macos-arm64_x86_64/libskia.a +0 -0
  101. package/libs/apple/libskia.xcframework/tvos-arm64_arm64e/libskia.a +0 -0
  102. package/libs/apple/libskia.xcframework/tvos-arm64_arm64e_x86_64-simulator/libskia.a +0 -0
  103. package/libs/apple/libskottie.xcframework/Info.plist +16 -16
  104. package/libs/apple/libskparagraph.xcframework/Info.plist +8 -8
  105. package/libs/apple/libsksg.xcframework/Info.plist +6 -6
  106. package/libs/apple/libskshaper.xcframework/Info.plist +10 -10
  107. package/libs/apple/libskunicode_core.xcframework/Info.plist +10 -10
  108. package/libs/apple/libskunicode_libgrapheme.xcframework/Info.plist +14 -14
  109. package/libs/apple/libsvg.xcframework/Info.plist +15 -15
  110. package/package.json +1 -1
  111. package/react-native-skia.podspec +5 -5
  112. package/src/dom/types/Drawings.ts +1 -1
  113. package/src/renderer/Canvas.tsx +53 -30
  114. package/src/renderer/__tests__/FitBox.spec.tsx +556 -4
  115. package/src/renderer/__tests__/e2e/ImageFilter.spec.tsx +4 -4
  116. package/src/renderer/__tests__/e2e/Paint.spec.tsx +18 -0
  117. package/src/renderer/__tests__/e2e/Skottie.spec.tsx +24 -1
  118. package/src/renderer/__tests__/setup.tsx +2 -0
  119. package/src/renderer/components/ImageFilter.tsx +1 -1
  120. package/src/skia/types/Matrix4.ts +16 -0
  121. package/src/sksg/Container.ts +73 -20
  122. package/src/sksg/Reconciler.ts +5 -3
  123. package/src/sksg/Recorder/Player.ts +7 -7
  124. package/src/sksg/Recorder/commands/ImageFilters.ts +3 -6
  125. package/src/specs/SkiaPictureViewNativeComponent.ts +1 -0
  126. package/src/views/types.ts +1 -0
  127. /package/cpp/rnskia/{DawnUtils.h → RNDawnUtils.h} +0 -0
  128. /package/cpp/rnskia/{ImageProvider.h → RNImageProvider.h} +0 -0
  129. /package/cpp/rnskia/{WindowContext.h → RNWindowContext.h} +0 -0
@@ -1,5 +1,5 @@
1
1
  import fs from "fs";
2
- import path from "path";
2
+ import nodePath from "path";
3
3
 
4
4
  import React from "react";
5
5
 
@@ -43,7 +43,7 @@ describe("FitBox", () => {
43
43
  const image = Skia.Image.MakeImageFromEncoded(
44
44
  Skia.Data.fromBytes(
45
45
  fs.readFileSync(
46
- path.resolve(__dirname, "../../skia/__tests__/assets/box.png")
46
+ nodePath.resolve(__dirname, "../../skia/__tests__/assets/box.png")
47
47
  )
48
48
  )
49
49
  )!;
@@ -71,7 +71,7 @@ describe("FitBox", () => {
71
71
  const image = Skia.Image.MakeImageFromEncoded(
72
72
  Skia.Data.fromBytes(
73
73
  fs.readFileSync(
74
- path.resolve(__dirname, "../../skia/__tests__/assets/box.png")
74
+ nodePath.resolve(__dirname, "../../skia/__tests__/assets/box.png")
75
75
  )
76
76
  )
77
77
  )!;
@@ -100,7 +100,7 @@ describe("FitBox", () => {
100
100
  const image = Skia.Image.MakeImageFromEncoded(
101
101
  Skia.Data.fromBytes(
102
102
  fs.readFileSync(
103
- path.resolve(__dirname, "../../skia/__tests__/assets/box2.png")
103
+ nodePath.resolve(__dirname, "../../skia/__tests__/assets/box2.png")
104
104
  )
105
105
  )
106
106
  )!;
@@ -123,4 +123,556 @@ describe("FitBox", () => {
123
123
  );
124
124
  processResult(surface, "snapshots/drawings/rotated-scaled-image.png");
125
125
  });
126
+
127
+ test("transform simple rectangle with fitbox fill", () => {
128
+ const { Skia, processTransform2d } = importSkia();
129
+
130
+ const path = Skia.Path.Make();
131
+ path.moveTo(0, 0);
132
+ path.lineTo(10, 0);
133
+ path.lineTo(10, 10);
134
+ path.lineTo(0, 10);
135
+ path.close();
136
+
137
+ const src = path.computeTightBounds();
138
+ const dst = Skia.XYWHRect(0, 0, 20, 20);
139
+ const transform = fitbox("fill", src, dst);
140
+ const c = path.copy().transform(processTransform2d(transform));
141
+ const newBounds = c.computeTightBounds();
142
+ expect(newBounds.x).toBe(0);
143
+ expect(newBounds.y).toBe(0);
144
+ expect(newBounds.width).toBe(20);
145
+ expect(newBounds.height).toBe(20);
146
+ });
147
+
148
+ test("transform simple rectangle with fitbox contain", () => {
149
+ const { Skia, processTransform2d } = importSkia();
150
+
151
+ const path = Skia.Path.Make();
152
+ path.moveTo(0, 0);
153
+ path.lineTo(10, 0);
154
+ path.lineTo(10, 5);
155
+ path.lineTo(0, 5);
156
+ path.close();
157
+
158
+ const src = path.computeTightBounds();
159
+
160
+ const dst = Skia.XYWHRect(0, 0, 20, 20);
161
+ const matrix = fitbox("contain", src, dst);
162
+
163
+ path.transform(processTransform2d(matrix));
164
+ const newBounds = path.computeTightBounds();
165
+ expect(newBounds.x).toBe(0);
166
+ expect(newBounds.y).toBe(5);
167
+ expect(newBounds.width).toBe(20);
168
+ expect(newBounds.height).toBe(10);
169
+ });
170
+
171
+ test("transform line with fitbox scale", () => {
172
+ const { Skia, processTransform2d } = importSkia();
173
+ const path = Skia.Path.Make();
174
+ path.moveTo(0, 0);
175
+ path.lineTo(5, 5);
176
+
177
+ const src = path.computeTightBounds();
178
+ const dst = Skia.XYWHRect(0, 0, 10, 10);
179
+ const matrix = fitbox("fill", src, dst);
180
+ path.transform(processTransform2d(matrix));
181
+ const newBounds = path.computeTightBounds();
182
+ expect(newBounds.x).toBe(0);
183
+ expect(newBounds.y).toBe(0);
184
+ expect(newBounds.width).toBe(10);
185
+ expect(newBounds.height).toBe(10);
186
+ });
187
+
188
+ test("transform with offset destination", () => {
189
+ const { Skia, processTransform2d } = importSkia();
190
+ const path = Skia.Path.Make();
191
+ path.moveTo(0, 0);
192
+ path.lineTo(4, 0);
193
+ path.lineTo(4, 4);
194
+ path.lineTo(0, 4);
195
+ path.close();
196
+
197
+ const src = path.computeTightBounds();
198
+ const dst = Skia.XYWHRect(10, 20, 8, 8);
199
+ const matrix = fitbox("fill", src, dst);
200
+ path.transform(processTransform2d(matrix));
201
+ const newBounds = path.computeTightBounds();
202
+ expect(newBounds.x).toBe(10);
203
+ expect(newBounds.y).toBe(20);
204
+ expect(newBounds.width).toBe(8);
205
+ expect(newBounds.height).toBe(8);
206
+ });
207
+
208
+ test("transform with cover fit mode", () => {
209
+ const { Skia, processTransform2d } = importSkia();
210
+ const path = Skia.Path.Make();
211
+ path.moveTo(0, 0);
212
+ path.lineTo(10, 0);
213
+ path.lineTo(10, 5);
214
+ path.lineTo(0, 5);
215
+ path.close();
216
+
217
+ const src = path.computeTightBounds();
218
+ const dst = Skia.XYWHRect(0, 0, 10, 10);
219
+ const matrix = fitbox("cover", src, dst);
220
+ path.transform(processTransform2d(matrix));
221
+ const newBounds = path.computeTightBounds();
222
+ expect(newBounds.x).toBe(-5);
223
+ expect(newBounds.y).toBe(0);
224
+ expect(newBounds.width).toBe(20);
225
+ expect(newBounds.height).toBe(10);
226
+ });
227
+
228
+ describe("rect2rect", () => {
229
+ test("basic scaling from unit square to double size", () => {
230
+ const { Skia, rect2rect } = importSkia();
231
+ const src = Skia.XYWHRect(0, 0, 1, 1);
232
+ const dst = Skia.XYWHRect(0, 0, 2, 2);
233
+ const result = rect2rect(src, dst);
234
+
235
+ expect(result).toEqual([
236
+ { translateX: 0 },
237
+ { translateY: 0 },
238
+ { scaleX: 2 },
239
+ { scaleY: 2 },
240
+ ]);
241
+ });
242
+
243
+ test("scaling with translation", () => {
244
+ const { Skia, rect2rect } = importSkia();
245
+ const src = Skia.XYWHRect(0, 0, 10, 10);
246
+ const dst = Skia.XYWHRect(5, 5, 20, 20);
247
+ const result = rect2rect(src, dst);
248
+
249
+ expect(result).toEqual([
250
+ { translateX: 5 },
251
+ { translateY: 5 },
252
+ { scaleX: 2 },
253
+ { scaleY: 2 },
254
+ ]);
255
+ });
256
+
257
+ test("scaling with non-zero source origin", () => {
258
+ const { Skia, rect2rect } = importSkia();
259
+ const src = Skia.XYWHRect(10, 10, 20, 20);
260
+ const dst = Skia.XYWHRect(0, 0, 40, 40);
261
+ const result = rect2rect(src, dst);
262
+
263
+ expect(result).toEqual([
264
+ { translateX: -20 },
265
+ { translateY: -20 },
266
+ { scaleX: 2 },
267
+ { scaleY: 2 },
268
+ ]);
269
+ });
270
+
271
+ test("shrinking transformation", () => {
272
+ const { Skia, rect2rect } = importSkia();
273
+ const src = Skia.XYWHRect(0, 0, 100, 100);
274
+ const dst = Skia.XYWHRect(0, 0, 50, 50);
275
+ const result = rect2rect(src, dst);
276
+
277
+ expect(result).toEqual([
278
+ { translateX: 0 },
279
+ { translateY: 0 },
280
+ { scaleX: 0.5 },
281
+ { scaleY: 0.5 },
282
+ ]);
283
+ });
284
+
285
+ test("non-uniform scaling", () => {
286
+ const { Skia, rect2rect } = importSkia();
287
+ const src = Skia.XYWHRect(0, 0, 10, 20);
288
+ const dst = Skia.XYWHRect(0, 0, 30, 40);
289
+ const result = rect2rect(src, dst);
290
+
291
+ expect(result).toEqual([
292
+ { translateX: 0 },
293
+ { translateY: 0 },
294
+ { scaleX: 3 },
295
+ { scaleY: 2 },
296
+ ]);
297
+ });
298
+
299
+ test("transformation with offset source and destination", () => {
300
+ const { Skia, rect2rect } = importSkia();
301
+ const src = Skia.XYWHRect(5, 10, 15, 20);
302
+ const dst = Skia.XYWHRect(50, 100, 30, 40);
303
+ const result = rect2rect(src, dst);
304
+
305
+ expect(result).toEqual([
306
+ { translateX: 40 },
307
+ { translateY: 80 },
308
+ { scaleX: 2 },
309
+ { scaleY: 2 },
310
+ ]);
311
+ });
312
+
313
+ test("identity transformation", () => {
314
+ const { Skia, rect2rect } = importSkia();
315
+ const src = Skia.XYWHRect(10, 20, 30, 40);
316
+ const dst = Skia.XYWHRect(10, 20, 30, 40);
317
+ const result = rect2rect(src, dst);
318
+
319
+ expect(result).toEqual([
320
+ { translateX: 0 },
321
+ { translateY: 0 },
322
+ { scaleX: 1 },
323
+ { scaleY: 1 },
324
+ ]);
325
+ });
326
+
327
+ test("fractional scaling", () => {
328
+ const { Skia, rect2rect } = importSkia();
329
+ const src = Skia.XYWHRect(0, 0, 3, 4);
330
+ const dst = Skia.XYWHRect(0, 0, 1.5, 2);
331
+ const result = rect2rect(src, dst);
332
+
333
+ expect(result).toEqual([
334
+ { translateX: 0 },
335
+ { translateY: 0 },
336
+ { scaleX: 0.5 },
337
+ { scaleY: 0.5 },
338
+ ]);
339
+ });
340
+
341
+ test("complex transformation with negative coordinates", () => {
342
+ const { Skia, rect2rect } = importSkia();
343
+ const src = Skia.XYWHRect(-10, -20, 20, 30);
344
+ const dst = Skia.XYWHRect(10, 5, 40, 60);
345
+ const result = rect2rect(src, dst);
346
+
347
+ expect(result).toEqual([
348
+ { translateX: 30 },
349
+ { translateY: 45 },
350
+ { scaleX: 2 },
351
+ { scaleY: 2 },
352
+ ]);
353
+ });
354
+
355
+ test("very small dimensions", () => {
356
+ const { Skia, rect2rect } = importSkia();
357
+ const src = Skia.XYWHRect(0, 0, 0.1, 0.1);
358
+ const dst = Skia.XYWHRect(0, 0, 1, 1);
359
+ const result = rect2rect(src, dst);
360
+
361
+ expect(result[0]).toEqual({ translateX: 0 });
362
+ expect(result[1]).toEqual({ translateY: 0 });
363
+ expect(result[2].scaleX).toBeCloseTo(10, 5);
364
+ expect(result[3].scaleY).toBeCloseTo(10, 5);
365
+ });
366
+ });
367
+
368
+ describe("fitRects", () => {
369
+ test("fill mode - source fills destination completely", () => {
370
+ const { Skia, fitRects } = importSkia();
371
+ const srcRect = Skia.XYWHRect(0, 0, 100, 50);
372
+ const dstRect = Skia.XYWHRect(10, 20, 200, 100);
373
+ const result = fitRects("fill", srcRect, dstRect);
374
+
375
+ expect(result.src).toEqual({
376
+ x: 0,
377
+ y: 0,
378
+ width: 100,
379
+ height: 50,
380
+ });
381
+ expect(result.dst).toEqual({
382
+ x: 10,
383
+ y: 20,
384
+ width: 200,
385
+ height: 100,
386
+ });
387
+ });
388
+
389
+ test("contain mode - source fits inside destination maintaining aspect ratio", () => {
390
+ const { Skia, fitRects } = importSkia();
391
+ const srcRect = Skia.XYWHRect(0, 0, 100, 50);
392
+ const dstRect = Skia.XYWHRect(0, 0, 200, 200);
393
+ const result = fitRects("contain", srcRect, dstRect);
394
+
395
+ expect(result.src).toEqual({
396
+ x: 0,
397
+ y: 0,
398
+ width: 100,
399
+ height: 50,
400
+ });
401
+ // Should maintain aspect ratio 2:1, so width=200, height=100, centered
402
+ expect(result.dst.width).toBe(200);
403
+ expect(result.dst.height).toBe(100);
404
+ expect(result.dst.x).toBe(0);
405
+ expect(result.dst.y).toBe(50); // Centered vertically
406
+ });
407
+
408
+ test("cover mode - source covers destination completely", () => {
409
+ const { Skia, fitRects } = importSkia();
410
+ const srcRect = Skia.XYWHRect(0, 0, 100, 50);
411
+ const dstRect = Skia.XYWHRect(0, 0, 100, 100);
412
+ const result = fitRects("cover", srcRect, dstRect);
413
+
414
+ // Source should be cropped to fit destination aspect ratio
415
+ expect(result.src.width).toBe(50); // Cropped to maintain 1:1 aspect ratio
416
+ expect(result.src.height).toBe(50);
417
+ expect(result.src.x).toBe(25); // Centered horizontally in original rect
418
+ expect(result.src.y).toBe(0);
419
+
420
+ expect(result.dst).toEqual({
421
+ x: 0,
422
+ y: 0,
423
+ width: 100,
424
+ height: 100,
425
+ });
426
+ });
427
+
428
+ test("fitWidth mode - fits to width, adjusts height", () => {
429
+ const { Skia, fitRects } = importSkia();
430
+ const srcRect = Skia.XYWHRect(0, 0, 100, 50);
431
+ const dstRect = Skia.XYWHRect(0, 0, 200, 200);
432
+ const result = fitRects("fitWidth", srcRect, dstRect);
433
+
434
+ // Source is scaled to fit the destination width aspect ratio
435
+ expect(result.src.width).toBe(100);
436
+ expect(result.src.height).toBe(100);
437
+ expect(result.src.x).toBe(0);
438
+ expect(result.src.y).toBe(-25); // Extends beyond original rect to maintain aspect ratio
439
+
440
+ // Destination uses the full destination rectangle
441
+ expect(result.dst.width).toBe(200);
442
+ expect(result.dst.height).toBe(200);
443
+ expect(result.dst.x).toBe(0);
444
+ expect(result.dst.y).toBe(0);
445
+ });
446
+
447
+ test("fitHeight mode - fits to height, adjusts width", () => {
448
+ const { Skia, fitRects } = importSkia();
449
+ const srcRect = Skia.XYWHRect(0, 0, 100, 50);
450
+ const dstRect = Skia.XYWHRect(0, 0, 200, 200);
451
+ const result = fitRects("fitHeight", srcRect, dstRect);
452
+
453
+ // Source is cropped to fit the destination height aspect ratio
454
+ expect(result.src.width).toBe(50);
455
+ expect(result.src.height).toBe(50);
456
+ expect(result.src.x).toBe(25); // Centered horizontally in original rect
457
+ expect(result.src.y).toBe(0);
458
+
459
+ // Destination uses the full destination rectangle
460
+ expect(result.dst.width).toBe(200);
461
+ expect(result.dst.height).toBe(200);
462
+ expect(result.dst.x).toBe(0);
463
+ expect(result.dst.y).toBe(0);
464
+ });
465
+
466
+ test("none mode - uses minimum dimensions", () => {
467
+ const { Skia, fitRects } = importSkia();
468
+ const srcRect = Skia.XYWHRect(0, 0, 100, 50);
469
+ const dstRect = Skia.XYWHRect(0, 0, 80, 120);
470
+ const result = fitRects("none", srcRect, dstRect);
471
+
472
+ // Should use minimum of source and destination dimensions
473
+ expect(result.src.width).toBe(80); // min(100, 80)
474
+ expect(result.src.height).toBe(50); // min(50, 120)
475
+ expect(result.src.x).toBe(10); // Centered in source rect
476
+ expect(result.src.y).toBe(0);
477
+
478
+ expect(result.dst.width).toBe(80);
479
+ expect(result.dst.height).toBe(50);
480
+ expect(result.dst.x).toBe(0);
481
+ expect(result.dst.y).toBe(35); // Centered in destination rect
482
+ });
483
+
484
+ test("scaleDown mode - scales down if needed", () => {
485
+ const { Skia, fitRects } = importSkia();
486
+ const srcRect = Skia.XYWHRect(0, 0, 200, 100);
487
+ const dstRect = Skia.XYWHRect(0, 0, 100, 80);
488
+ const result = fitRects("scaleDown", srcRect, dstRect);
489
+
490
+ expect(result.src).toEqual({
491
+ x: 0,
492
+ y: 0,
493
+ width: 200,
494
+ height: 100,
495
+ });
496
+
497
+ // Should scale down to fit within destination
498
+ expect(result.dst.width).toBe(100);
499
+ expect(result.dst.height).toBe(50); // Maintains aspect ratio 2:1
500
+ expect(result.dst.x).toBe(0);
501
+ expect(result.dst.y).toBe(15); // Centered vertically
502
+ });
503
+
504
+ test("scaleDown mode - no scaling if source is smaller", () => {
505
+ const { Skia, fitRects } = importSkia();
506
+ const srcRect = Skia.XYWHRect(0, 0, 50, 25);
507
+ const dstRect = Skia.XYWHRect(0, 0, 100, 100);
508
+ const result = fitRects("scaleDown", srcRect, dstRect);
509
+
510
+ expect(result.src).toEqual({
511
+ x: 0,
512
+ y: 0,
513
+ width: 50,
514
+ height: 25,
515
+ });
516
+
517
+ // Should not scale up, keep original size
518
+ expect(result.dst.width).toBe(50);
519
+ expect(result.dst.height).toBe(25);
520
+ expect(result.dst.x).toBe(25); // Centered horizontally
521
+ expect(result.dst.y).toBe(37.5); // Centered vertically
522
+ });
523
+
524
+ test("with offset source rectangle", () => {
525
+ const { Skia, fitRects } = importSkia();
526
+ const srcRect = Skia.XYWHRect(50, 100, 100, 50);
527
+ const dstRect = Skia.XYWHRect(10, 20, 200, 100);
528
+ const result = fitRects("fill", srcRect, dstRect);
529
+
530
+ expect(result.src).toEqual({
531
+ x: 50,
532
+ y: 100,
533
+ width: 100,
534
+ height: 50,
535
+ });
536
+ expect(result.dst).toEqual({
537
+ x: 10,
538
+ y: 20,
539
+ width: 200,
540
+ height: 100,
541
+ });
542
+ });
543
+
544
+ test("with very small source rectangle", () => {
545
+ const { Skia, fitRects } = importSkia();
546
+ const srcRect = Skia.XYWHRect(0, 0, 1, 1);
547
+ const dstRect = Skia.XYWHRect(0, 0, 100, 100);
548
+ const result = fitRects("contain", srcRect, dstRect);
549
+
550
+ expect(result.src).toEqual({
551
+ x: 0,
552
+ y: 0,
553
+ width: 1,
554
+ height: 1,
555
+ });
556
+ expect(result.dst).toEqual({
557
+ x: 0,
558
+ y: 0,
559
+ width: 100,
560
+ height: 100,
561
+ });
562
+ });
563
+
564
+ test("with zero dimensions - should return empty sizes", () => {
565
+ const { Skia, fitRects } = importSkia();
566
+ const srcRect = Skia.XYWHRect(0, 0, 0, 100);
567
+ const dstRect = Skia.XYWHRect(0, 0, 100, 100);
568
+ const result = fitRects("fill", srcRect, dstRect);
569
+
570
+ expect(result.src).toEqual({
571
+ x: 0,
572
+ y: 50,
573
+ width: 0,
574
+ height: 0,
575
+ });
576
+ expect(result.dst).toEqual({
577
+ x: 50,
578
+ y: 50,
579
+ width: 0,
580
+ height: 0,
581
+ });
582
+ });
583
+
584
+ test("cover mode with wide source rectangle", () => {
585
+ const { Skia, fitRects } = importSkia();
586
+ const srcRect = Skia.XYWHRect(0, 0, 200, 50);
587
+ const dstRect = Skia.XYWHRect(0, 0, 100, 100);
588
+ const result = fitRects("cover", srcRect, dstRect);
589
+
590
+ // Source should be cropped to square aspect ratio
591
+ expect(result.src.width).toBe(50); // Cropped width
592
+ expect(result.src.height).toBe(50);
593
+ expect(result.src.x).toBe(75); // Centered horizontally
594
+ expect(result.src.y).toBe(0);
595
+
596
+ expect(result.dst).toEqual({
597
+ x: 0,
598
+ y: 0,
599
+ width: 100,
600
+ height: 100,
601
+ });
602
+ });
603
+
604
+ test("cover mode with tall destination rectangle", () => {
605
+ const { Skia, fitRects } = importSkia();
606
+ const srcRect = Skia.XYWHRect(0, 0, 100, 100);
607
+ const dstRect = Skia.XYWHRect(0, 0, 50, 200);
608
+ const result = fitRects("cover", srcRect, dstRect);
609
+
610
+ // Source should be cropped to 1:4 aspect ratio
611
+ expect(result.src.width).toBe(25); // Cropped to match destination aspect ratio
612
+ expect(result.src.height).toBe(100);
613
+ expect(result.src.x).toBe(37.5); // Centered horizontally
614
+ expect(result.src.y).toBe(0);
615
+
616
+ expect(result.dst).toEqual({
617
+ x: 0,
618
+ y: 0,
619
+ width: 50,
620
+ height: 200,
621
+ });
622
+ });
623
+ });
624
+ describe("Path bounds", () => {
625
+ test("computes bounds for cubic with extreme control points", () => {
626
+ const { Skia } = importSkia();
627
+ const path = Skia.Path.Make();
628
+ path.moveTo(0, 0);
629
+ path.cubicTo(-50, 100, 150, -50, 100, 100);
630
+ const bounds = path.computeTightBounds();
631
+ // bounds is -8.09475040435791 0 116.1894998550415 100
632
+ expect(bounds.x).toBeCloseTo(-8.09475, 2);
633
+ expect(bounds.y).toBeCloseTo(0, 2);
634
+ expect(bounds.width).toBeCloseTo(116.1895, 2);
635
+ expect(bounds.height).toBeCloseTo(100, 2);
636
+ });
637
+ test("computes bounds for cubic forming loop", () => {
638
+ const { Skia } = importSkia();
639
+ const path = Skia.Path.Make();
640
+ path.moveTo(0, 0);
641
+ path.cubicTo(100, 0, 100, 100, 0, 50);
642
+ const bounds = path.computeTightBounds();
643
+ expect(bounds.x).toBeCloseTo(0, 2);
644
+ expect(bounds.y).toBeCloseTo(0, 2);
645
+ expect(bounds.width).toBeCloseTo(75, 2);
646
+ expect(bounds.height).toBeCloseTo(64, 2);
647
+ });
648
+ test("computes bounds for multiple cubics forming complex path", () => {
649
+ const { Skia } = importSkia();
650
+ const path = Skia.Path.Make();
651
+ path.moveTo(0, 50);
652
+ path.cubicTo(25, 0, 75, 100, 100, 50);
653
+ path.moveTo(100, 50);
654
+ path.cubicTo(125, 0, 175, 100, 200, 50);
655
+ path.moveTo(200, 50);
656
+ path.cubicTo(225, 100, 275, 0, 300, 50);
657
+
658
+ const bounds = path.computeTightBounds();
659
+ expect(bounds.x).toBeCloseTo(0, 2);
660
+ expect(bounds.y).toBeCloseTo(35.56624221801758, 2);
661
+ expect(bounds.width).toBeCloseTo(300, 2);
662
+ expect(bounds.height).toBeCloseTo(28.867511749267578, 2);
663
+ });
664
+ test("draw Skia logo", () => {
665
+ const skiaLogo =
666
+ // eslint-disable-next-line max-len
667
+ "M465.63 273.956C409.11 212.516 348.87 258.846 362.34 310.646C367.09 328.936 381.05 347.906 407.6 363.056C444.3 383.986 460.05 408.286 461.42 430.426C464.57 481.036 392.6 520.356 324 482.376M490 430.426C589.17 299.226 590.11 228.576 568.77 222.366C554.04 218.586 529.13 244.036 518 301.366C505.52 367.526 500.67 405.066 490 494.956C505.58 451.676 514.49 414.746 545.45 389.956C554.589 382.551 565.818 378.197 577.56 377.506C628.71 374.806 621.17 446.096 541.95 430.406M541.95 430.426C575.55 436.726 571.27 458.036 580.75 482.326C582.111 485.913 584.445 489.051 587.49 491.386C607.49 506.386 643.49 476.616 654.36 457.216C671.21 432.636 684.24 404.916 697.36 378.486M697.38 378.486C684.12 411.766 675.597 437.196 671.81 454.776C668.54 481.546 675.24 493.636 686.32 496.256C710.7 502.016 756.23 461.896 763.13 431.256C776.37 372.396 862.18 350.556 881.97 419.656M881.97 419.636C862.18 350.536 776.21 372.376 763.13 431.236C759.37 455.676 766.85 473.336 779.67 483.966C786.621 489.63 794.908 493.417 803.74 494.966C818.132 497.496 832.957 495.178 845.89 488.376C846.69 487.956 847.49 487.516 848.29 487.056C856.441 482.27 863.382 475.672 868.574 467.773C873.765 459.875 877.069 450.887 878.23 441.506C881.09 419.196 888.04 394.656 892.59 378.086M892.59 378.076C885.36 404.426 872.1 449.746 878.77 476.156C880.283 482.292 884.122 487.599 889.475 490.958C894.828 494.316 901.277 495.463 907.46 494.156C925.39 490.256 943.78 472.156 955.09 454.336M714.5 338C714.5 339.933 712.933 341.5 711 341.5C709.067 341.5 707.5 339.933 707.5 338C707.5 336.067 709.067 334.5 711 334.5C712.933 334.5 714.5 336.067 714.5 338Z";
668
+ const { Skia } = importSkia();
669
+ const path = Skia.Path.MakeFromSVGString(skiaLogo)!;
670
+ const bounds = path.computeTightBounds();
671
+ console.log(bounds.x, bounds.y, bounds.width, bounds.height);
672
+ expect(bounds.x).toBeCloseTo(324, 2);
673
+ expect(bounds.y).toBeCloseTo(222, 2);
674
+ expect(bounds.width).toBeCloseTo(631.09, 2);
675
+ expect(bounds.height).toBeCloseTo(275.55, 2);
676
+ });
677
+ });
126
678
  });
@@ -17,7 +17,7 @@ describe("ImageFilter", () => {
17
17
  // END OF INTERNAL TESTING ONLY
18
18
  const img = await surface.draw(
19
19
  <Group>
20
- <ImageFilter imageFilter={blurFilter} />
20
+ <ImageFilter filter={blurFilter} />
21
21
  <Circle cx={50} cy={50} r={30} color="red" />
22
22
  </Group>
23
23
  );
@@ -36,7 +36,7 @@ describe("ImageFilter", () => {
36
36
 
37
37
  const img = await surface.draw(
38
38
  <Group>
39
- <ImageFilter imageFilter={offsetFilter} />
39
+ <ImageFilter filter={offsetFilter} />
40
40
  <Circle cx={50} cy={50} r={30} color="blue" />
41
41
  </Group>
42
42
  );
@@ -63,7 +63,7 @@ describe("ImageFilter", () => {
63
63
 
64
64
  const img = await surface.draw(
65
65
  <Group>
66
- <ImageFilter imageFilter={dropShadowFilter} />
66
+ <ImageFilter filter={dropShadowFilter} />
67
67
  <Circle cx={50} cy={50} r={30} color="green" />
68
68
  </Group>
69
69
  );
@@ -89,7 +89,7 @@ describe("ImageFilter", () => {
89
89
 
90
90
  const img = await surface.draw(
91
91
  <Group>
92
- <ImageFilter imageFilter={offsetFilter} />
92
+ <ImageFilter filter={offsetFilter} />
93
93
  <Circle cx={50} cy={50} r={30} color="purple" />
94
94
  </Group>
95
95
  );
@@ -9,6 +9,7 @@ import {
9
9
  LinearGradient,
10
10
  Paint,
11
11
  Path,
12
+ SweepGradient,
12
13
  } from "../../components";
13
14
  import { checkImage, docPath } from "../../../__tests__/setup";
14
15
  import { fitbox } from "../../components/shapes/FitBox";
@@ -210,4 +211,21 @@ describe("Paint", () => {
210
211
  );
211
212
  checkImage(result, docPath("paint/semi-transparent-circle.png"));
212
213
  });
214
+ it("test paint", async () => {
215
+ const { vec, Skia } = importSkia();
216
+ const strokeWidth = 10;
217
+ const { width, height } = surface;
218
+ const c = vec(width / 2, height / 2);
219
+ const r = (width - strokeWidth) / 2;
220
+ const path = Skia.Path.Make();
221
+ path.addCircle(c.x, c.y, r);
222
+ const result = await surface.draw(
223
+ <Path path={path} color="transparent">
224
+ <Paint style="stroke" strokeWidth={20} strokeCap="round">
225
+ <SweepGradient c={c} colors={["#64BC65", "#4488ff"]} />
226
+ </Paint>
227
+ </Path>
228
+ );
229
+ checkImage(result, docPath("paint/test-paint.png"));
230
+ });
213
231
  });
@@ -3,7 +3,7 @@ import React from "react";
3
3
 
4
4
  import { checkImage, docPath } from "../../../__tests__/setup";
5
5
  import { dataAssets, importSkia, surface } from "../setup";
6
- import { Group, Skottie } from "../../components";
6
+ import { Blur, Group, Paint, Skottie } from "../../components";
7
7
 
8
8
  const legoLoaderJSON = require("./setup/skottie/lego_loader.json");
9
9
  const drinksJSON = require("./setup/skottie/drinks.json");
@@ -30,6 +30,29 @@ describe("Skottie", () => {
30
30
  );
31
31
  checkImage(img, docPath("skottie/skottie-component-lego.png"));
32
32
  });
33
+ it("Should render Skottie component with raster effect", async () => {
34
+ const { Skia } = importSkia();
35
+ const source = JSON.stringify(legoLoaderJSON);
36
+ const legoAnimation = Skia.Skottie.Make(source);
37
+ // THIS IS FOR INTERNAL TESTING ONLY
38
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
39
+ // @ts-expect-error
40
+ legoAnimation.source = source;
41
+ // END OF INTERNAL TESTING ONLY
42
+ const img = await surface.draw(
43
+ <Group
44
+ transform={[{ scale: 0.5 }]}
45
+ layer={
46
+ <Paint>
47
+ <Blur blur={10} />
48
+ </Paint>
49
+ }
50
+ >
51
+ <Skottie animation={legoAnimation} frame={41} />
52
+ </Group>
53
+ );
54
+ checkImage(img, docPath("skottie/skottie-component-lego-blur.png"));
55
+ });
33
56
  it("Should render Skottie component with drinks animation", async () => {
34
57
  const { Skia } = importSkia();
35
58
  const source = JSON.stringify(drinksJSON);