Skip to content

Commit 6688b1e

Browse files
authored
Merge pull request #100 from Shopify/feature/offscreen-surface-snapshots
Added support for offscreen surface and image toByteArray/toBase64
2 parents 4d13cc7 + 5fab08b commit 6688b1e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+2358
-355
lines changed

docs/docs/canvas/canvas.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
id: canvas
3+
title: Canvas
4+
sidebar_label: Overview
5+
slug: /canvas/overview
6+
---
7+
8+
The Canvas component is the root of your Skia drawing.
9+
You can treat it as a regular React Native view and assign a view style to it.
10+
Behind the scenes, it is using its own React renderer.
11+
12+
| Name | Type | Description. |
13+
|:-----|:---------|:-----------------|
14+
| style | `ViewStyle` | View style. |
15+
| ref? | `Ref<SkiaView>` | Reference to the `SkiaView` object |
16+
| onTouch? | `TouchHandler` | Touch handler for the Canvas (see [touch handler](/docs/animations/overview#usetouchhandler)). |
17+
18+
## Getting a Canvas Snapshot
19+
20+
You can save your drawings as an image, using `makeImageSnapshot`. This method will return an [Image instance](/docs/images#instance-methods). This instance can be used to do anything: drawing it via the `<Image>` component, or being saved or shared using binary or base64 encoding.
21+
22+
### Example
23+
24+
```tsx twoslash
25+
import {useEffect} from "react";
26+
import {Canvas, Image, useCanvasRef, Circle} from "@shopify/react-native-skia";
27+
28+
export const Demo = () => {
29+
const ref = useCanvasRef();
30+
const onPress = useEffect(() => {
31+
setTimeout(() => {
32+
// you can pass an optional rectangle
33+
// to only save part of the image
34+
const image = ref.current?.makeImageSnapshot();
35+
if (image) {
36+
// you can use image in an <Image> component
37+
// Or save to file using encodeToBytes -> Uint8Array
38+
const bytes = image.encodeToBytes();
39+
}
40+
}, 1000)
41+
});
42+
return (
43+
<Canvas style={{ flex: 1 }} ref={ref}>
44+
<Circle r={128} cx={128} cy={128} color="red" />
45+
</Canvas>
46+
);
47+
};
48+
```
49+
50+
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
id: contexts
33
title: Contexts
44
sidebar_label: Contexts
5-
slug: /getting-started/contexts
5+
slug: /canvas/contexts
66
---
77

88
React Native Skia is using its own React renderer.

docs/docs/image.md

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,36 @@ slug: /images
77

88
Images can be draw by specifying the output rectangle and how the image should fit into that rectangle.
99

10-
| Name | Type | Description |
11-
|:----------|:----------|:--------------------------------------------------------------|
12-
| source | `require` or `string` | Source of the image or an HTTP(s) URL. |
13-
| x | `number` | Left position of the destination image. |
14-
| y | `number` | Right position of the destination image. |
15-
| width | `number` | Width of the destination image. |
16-
| height | `number` | Height of the destination image. |
17-
| fit? | `Fit` | Method to make the image fit into the rectangle. Value can be `contain`, `fill`, `cover` `fitHeight`, `fitWidth`, `scaleDown`, `none` (default is `contain`). |
10+
| Name | Type | Description |
11+
| :----- | :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ |
12+
| source | `require` or `string` | Source of the image or an HTTP(s) URL. |
13+
| x | `number` | Left position of the destination image. |
14+
| y | `number` | Right position of the destination image. |
15+
| width | `number` | Width of the destination image. |
16+
| height | `number` | Height of the destination image. |
17+
| fit? | `Fit` | Method to make the image fit into the rectangle. Value can be `contain`, `fill`, `cover` `fitHeight`, `fitWidth`, `scaleDown`, `none` (default is `contain`). |
1818

1919
### Example
2020

2121
```tsx twoslash
22-
import {
23-
Canvas,
24-
Image,
25-
useImage
26-
} from "@shopify/react-native-skia";
22+
import { Canvas, Image, useImage } from "@shopify/react-native-skia";
2723

2824
const ImageDemo = () => {
2925
// Alternatively, you can pass an image URL directly
3026
// for instance: const source = useImage("https://bit.ly/3fkulX5");
3127
const source = useImage(require("../../assets/oslo.jpg"));
3228
return (
3329
<Canvas style={{ flex: 1 }}>
34-
{ source && (
30+
{source && (
3531
<Image
3632
source={source}
3733
fit="contain"
3834
x={0}
3935
y={0}
4036
width={256}
4137
height={256}
42-
/>)
43-
}
38+
/>
39+
)}
4440
</Canvas>
4541
);
4642
};
@@ -73,3 +69,12 @@ const ImageDemo = () => {
7369
### fit="none"
7470

7571
![fit="none"](assets/images/none.png)
72+
73+
## Instance Methods
74+
75+
| Name | Description |
76+
| :---------------- | :------------------------------------------------------------------------------------ |
77+
| height | Returns the possibly scaled height of the image. |
78+
| width | Returns the possibly scaled width of the image. |
79+
| encodeToBytes | Encodes Image pixels, returning result as UInt8Array |
80+
| encodeToBase64 | Encodes Image pixels, returning result as a base64 encoded string |

docs/sidebars.js

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ const sidebars = {
1818
collapsed: false,
1919
type: "category",
2020
label: "Getting started",
21-
items: [
22-
"getting-started/installation",
23-
"getting-started/hello-world",
24-
"getting-started/contexts",
25-
],
21+
items: ["getting-started/installation", "getting-started/hello-world"],
2622
},
2723
{
28-
collapsed: false,
24+
collapsed: true,
25+
type: "category",
26+
label: "Canvas",
27+
items: ["canvas/canvas", "canvas/contexts"],
28+
},
29+
{
30+
collapsed: true,
2931
type: "category",
3032
label: "Paint",
3133
items: ["paint/overview", "paint/properties"],
@@ -36,19 +38,19 @@ const sidebars = {
3638
id: "group",
3739
},
3840
{
39-
collapsed: false,
41+
collapsed: true,
4042
type: "category",
4143
label: "Image",
4244
items: ["image", "image-svg"],
4345
},
4446
{
45-
collapsed: false,
47+
collapsed: true,
4648
type: "category",
4749
label: "Text",
4850
items: ["text/fonts", "text/text"],
4951
},
5052
{
51-
collapsed: false,
53+
collapsed: true,
5254
type: "category",
5355
label: "Shaders",
5456
items: [
@@ -60,13 +62,13 @@ const sidebars = {
6062
],
6163
},
6264
{
63-
collapsed: false,
65+
collapsed: true,
6466
type: "category",
6567
label: "Effects",
6668
items: ["mask-filters", "color-filters", "image-filters", "path-effects"],
6769
},
6870
{
69-
collapsed: false,
71+
collapsed: true,
7072
type: "category",
7173
label: "Shapes",
7274
items: [
@@ -77,7 +79,7 @@ const sidebars = {
7779
],
7880
},
7981
{
80-
collapsed: false,
82+
collapsed: true,
8183
type: "category",
8284
label: "Animations",
8385
items: ["animations/overview", "animations/reanimated"],

example/ios/RNSkia.xcodeproj/project.pbxproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,7 @@
500500
PRODUCT_NAME = RNSkia;
501501
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
502502
SWIFT_VERSION = 5.0;
503+
TARGETED_DEVICE_FAMILY = "1,2";
503504
VERSIONING_SYSTEM = "apple-generic";
504505
};
505506
name = Debug;
@@ -525,6 +526,7 @@
525526
PRODUCT_BUNDLE_IDENTIFIER = org.shopify.reactnative.skia.example;
526527
PRODUCT_NAME = RNSkia;
527528
SWIFT_VERSION = 5.0;
529+
TARGETED_DEVICE_FAMILY = "1,2";
528530
VERSIONING_SYSTEM = "apple-generic";
529531
};
530532
name = Release;
@@ -687,4 +689,4 @@
687689
/* End XCConfigurationList section */
688690
};
689691
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
690-
}
692+
}

example/ios/RNSkia/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
<array>
4848
<string>armv7</string>
4949
</array>
50+
<key>UIRequiresFullScreen</key>
51+
<true/>
5052
<key>UISupportedInterfaceOrientations</key>
5153
<array>
5254
<string>UIInterfaceOrientationPortrait</string>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { Point } from "@shopify/react-native-skia";
2+
3+
import type { DrawingElements } from "../types";
4+
5+
import { getBounds } from "./getBounds";
6+
7+
export const findClosestElementToPoint = (
8+
point: Point,
9+
elements: DrawingElements
10+
) => {
11+
// Empty elements returns undefined
12+
if (elements.length === 0) {
13+
return undefined;
14+
}
15+
// Check if we any of the paths (in reverse top-down order) contains the point
16+
for (let i = elements.length - 1; i >= 0; i--) {
17+
if (elements[i].path.contains(point.x, point.y)) {
18+
return elements[i];
19+
}
20+
}
21+
// If not, measure distance to the closest path
22+
const distances = elements
23+
.map((element) => {
24+
const rect = getBounds(element);
25+
// check if point is in rect
26+
if (
27+
point.x >= rect.x - 10 &&
28+
point.x < rect.x + rect.width + 10 &&
29+
point.y >= rect.y - 10 &&
30+
point.y < rect.y + rect.height + 10
31+
) {
32+
// Find distance from click to center of element
33+
var dx = Math.max(rect.x - point.x, point.x - (rect.x + rect.width));
34+
var dy = Math.max(rect.y - point.y, point.y - (rect.y + rect.height));
35+
return { ...element, distance: Math.sqrt(dx * dx + dy * dy) };
36+
} else {
37+
return { ...element, distance: Number.MAX_VALUE };
38+
}
39+
})
40+
.sort((a, b) => a.distance - b.distance);
41+
42+
return elements.find(
43+
(el) =>
44+
el.path === distances[0].path && distances[0].distance < Number.MAX_VALUE
45+
);
46+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { IRect } from "@shopify/react-native-skia";
2+
3+
import type { DrawingElements } from "../types";
4+
5+
import { getBounds } from "./getBounds";
6+
7+
export const findElementsInRect = (
8+
rect: IRect,
9+
elements: DrawingElements
10+
): DrawingElements | undefined => {
11+
const retVal: DrawingElements = [];
12+
const normalizedRect = {
13+
x: rect.width < 0 ? rect.x + rect.width : rect.x,
14+
y: rect.height < 0 ? rect.y + rect.height : rect.y,
15+
width: Math.abs(rect.width),
16+
height: Math.abs(rect.height),
17+
};
18+
elements.forEach((element) => {
19+
const bounds = getBounds(element);
20+
if (
21+
bounds.x >= normalizedRect.x &&
22+
bounds.x + bounds.width <= normalizedRect.x + normalizedRect.width &&
23+
bounds.y >= normalizedRect.y &&
24+
bounds.y + bounds.height <= normalizedRect.y + normalizedRect.height
25+
) {
26+
retVal.push(element);
27+
}
28+
});
29+
30+
if (retVal.length > 0) {
31+
return retVal;
32+
}
33+
return undefined;
34+
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { Point } from "@shopify/react-native-skia";
2+
3+
import type { DrawingElements, ResizeMode } from "../types";
4+
5+
import { getBoundingBox } from "./getBoundingBox";
6+
7+
const hitSlop = 8;
8+
9+
export const findResizeMode = (
10+
point: Point,
11+
selectedElements: DrawingElements
12+
): ResizeMode | undefined => {
13+
const bounds = getBoundingBox(selectedElements);
14+
if (!bounds) {
15+
return undefined;
16+
}
17+
18+
if (
19+
point.x >= bounds.x - hitSlop &&
20+
point.x <= bounds.x + hitSlop &&
21+
point.y >= bounds.y - hitSlop &&
22+
point.y <= bounds.y + hitSlop
23+
) {
24+
return "topLeft";
25+
} else if (
26+
point.x >= bounds.x + bounds.width - hitSlop &&
27+
point.x <= bounds.x + bounds.width + hitSlop &&
28+
point.y >= bounds.y - hitSlop &&
29+
point.y <= bounds.y + hitSlop
30+
) {
31+
return "topRight";
32+
} else if (
33+
point.x >= bounds.x + bounds.width - hitSlop &&
34+
point.x <= bounds.x + bounds.width + hitSlop &&
35+
point.y >= bounds.y + bounds.height - hitSlop &&
36+
point.y <= bounds.y + bounds.height + hitSlop
37+
) {
38+
return "bottomRight";
39+
} else if (
40+
point.x >= bounds.x - hitSlop &&
41+
point.x <= bounds.x + hitSlop &&
42+
point.y >= bounds.y + bounds.height - hitSlop &&
43+
point.y <= bounds.y + bounds.height + hitSlop
44+
) {
45+
return "bottomLeft";
46+
}
47+
return undefined;
48+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { DrawingElements } from "../types";
2+
3+
import { getBounds } from "./getBounds";
4+
5+
export const getBoundingBox = (elements: DrawingElements) => {
6+
if (elements.length === 0) {
7+
return undefined;
8+
}
9+
10+
const bb = {
11+
x: Number.MAX_VALUE,
12+
y: Number.MAX_VALUE,
13+
right: Number.MIN_VALUE,
14+
bottom: Number.MIN_VALUE,
15+
};
16+
17+
for (let i = 0; i < elements.length; i++) {
18+
const element = elements[i];
19+
const bounds = getBounds(element);
20+
21+
if (bounds.x < bb.x) {
22+
bb.x = bounds.x;
23+
}
24+
if (bounds.y < bb.y) {
25+
bb.y = bounds.y;
26+
}
27+
if (bounds.x + bounds.width > bb.right) {
28+
bb.right = bounds.x + bounds.width;
29+
}
30+
if (bounds.y + bounds.height > bb.bottom) {
31+
bb.bottom = bounds.y + bounds.height;
32+
}
33+
}
34+
35+
return { x: bb.x, y: bb.y, width: bb.right - bb.x, height: bb.bottom - bb.y };
36+
};

0 commit comments

Comments
 (0)