Skip to content

Commit

Permalink
Merge pull request #100 from Shopify/feature/offscreen-surface-snapshots
Browse files Browse the repository at this point in the history
Added support for offscreen surface and image toByteArray/toBase64
  • Loading branch information
chrfalch authored Jan 28, 2022
2 parents 4d13cc7 + 5fab08b commit 6688b1e
Show file tree
Hide file tree
Showing 72 changed files with 2,358 additions and 355 deletions.
50 changes: 50 additions & 0 deletions docs/docs/canvas/canvas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
id: canvas
title: Canvas
sidebar_label: Overview
slug: /canvas/overview
---

The Canvas component is the root of your Skia drawing.
You can treat it as a regular React Native view and assign a view style to it.
Behind the scenes, it is using its own React renderer.

| Name | Type | Description. |
|:-----|:---------|:-----------------|
| style | `ViewStyle` | View style. |
| ref? | `Ref<SkiaView>` | Reference to the `SkiaView` object |
| onTouch? | `TouchHandler` | Touch handler for the Canvas (see [touch handler](/docs/animations/overview#usetouchhandler)). |

## Getting a Canvas Snapshot

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.

### Example

```tsx twoslash
import {useEffect} from "react";
import {Canvas, Image, useCanvasRef, Circle} from "@shopify/react-native-skia";

export const Demo = () => {
const ref = useCanvasRef();
const onPress = useEffect(() => {
setTimeout(() => {
// you can pass an optional rectangle
// to only save part of the image
const image = ref.current?.makeImageSnapshot();
if (image) {
// you can use image in an <Image> component
// Or save to file using encodeToBytes -> Uint8Array
const bytes = image.encodeToBytes();
}
}, 1000)
});
return (
<Canvas style={{ flex: 1 }} ref={ref}>
<Circle r={128} cx={128} cy={128} color="red" />
</Canvas>
);
};
```


Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
id: contexts
title: Contexts
sidebar_label: Contexts
slug: /getting-started/contexts
slug: /canvas/contexts
---

React Native Skia is using its own React renderer.
Expand Down
37 changes: 21 additions & 16 deletions docs/docs/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,36 @@ slug: /images

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

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

### Example

```tsx twoslash
import {
Canvas,
Image,
useImage
} from "@shopify/react-native-skia";
import { Canvas, Image, useImage } from "@shopify/react-native-skia";

const ImageDemo = () => {
// Alternatively, you can pass an image URL directly
// for instance: const source = useImage("https://bit.ly/3fkulX5");
const source = useImage(require("../../assets/oslo.jpg"));
return (
<Canvas style={{ flex: 1 }}>
{ source && (
{source && (
<Image
source={source}
fit="contain"
x={0}
y={0}
width={256}
height={256}
/>)
}
/>
)}
</Canvas>
);
};
Expand Down Expand Up @@ -73,3 +69,12 @@ const ImageDemo = () => {
### fit="none"

![fit="none"](assets/images/none.png)

## Instance Methods

| Name | Description |
| :---------------- | :------------------------------------------------------------------------------------ |
| height | Returns the possibly scaled height of the image. |
| width | Returns the possibly scaled width of the image. |
| encodeToBytes | Encodes Image pixels, returning result as UInt8Array |
| encodeToBase64 | Encodes Image pixels, returning result as a base64 encoded string |
26 changes: 14 additions & 12 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ const sidebars = {
collapsed: false,
type: "category",
label: "Getting started",
items: [
"getting-started/installation",
"getting-started/hello-world",
"getting-started/contexts",
],
items: ["getting-started/installation", "getting-started/hello-world"],
},
{
collapsed: false,
collapsed: true,
type: "category",
label: "Canvas",
items: ["canvas/canvas", "canvas/contexts"],
},
{
collapsed: true,
type: "category",
label: "Paint",
items: ["paint/overview", "paint/properties"],
Expand All @@ -36,19 +38,19 @@ const sidebars = {
id: "group",
},
{
collapsed: false,
collapsed: true,
type: "category",
label: "Image",
items: ["image", "image-svg"],
},
{
collapsed: false,
collapsed: true,
type: "category",
label: "Text",
items: ["text/fonts", "text/text"],
},
{
collapsed: false,
collapsed: true,
type: "category",
label: "Shaders",
items: [
Expand All @@ -60,13 +62,13 @@ const sidebars = {
],
},
{
collapsed: false,
collapsed: true,
type: "category",
label: "Effects",
items: ["mask-filters", "color-filters", "image-filters", "path-effects"],
},
{
collapsed: false,
collapsed: true,
type: "category",
label: "Shapes",
items: [
Expand All @@ -77,7 +79,7 @@ const sidebars = {
],
},
{
collapsed: false,
collapsed: true,
type: "category",
label: "Animations",
items: ["animations/overview", "animations/reanimated"],
Expand Down
4 changes: 3 additions & 1 deletion example/ios/RNSkia.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@
PRODUCT_NAME = RNSkia;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
Expand All @@ -525,6 +526,7 @@
PRODUCT_BUNDLE_IDENTIFIER = org.shopify.reactnative.skia.example;
PRODUCT_NAME = RNSkia;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
Expand Down Expand Up @@ -687,4 +689,4 @@
/* End XCConfigurationList section */
};
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
}
}
2 changes: 2 additions & 0 deletions example/ios/RNSkia/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
<array>
<string>armv7</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Point } from "@shopify/react-native-skia";

import type { DrawingElements } from "../types";

import { getBounds } from "./getBounds";

export const findClosestElementToPoint = (
point: Point,
elements: DrawingElements
) => {
// Empty elements returns undefined
if (elements.length === 0) {
return undefined;
}
// Check if we any of the paths (in reverse top-down order) contains the point
for (let i = elements.length - 1; i >= 0; i--) {
if (elements[i].path.contains(point.x, point.y)) {
return elements[i];
}
}
// If not, measure distance to the closest path
const distances = elements
.map((element) => {
const rect = getBounds(element);
// check if point is in rect
if (
point.x >= rect.x - 10 &&
point.x < rect.x + rect.width + 10 &&
point.y >= rect.y - 10 &&
point.y < rect.y + rect.height + 10
) {
// Find distance from click to center of element
var dx = Math.max(rect.x - point.x, point.x - (rect.x + rect.width));
var dy = Math.max(rect.y - point.y, point.y - (rect.y + rect.height));
return { ...element, distance: Math.sqrt(dx * dx + dy * dy) };
} else {
return { ...element, distance: Number.MAX_VALUE };
}
})
.sort((a, b) => a.distance - b.distance);

return elements.find(
(el) =>
el.path === distances[0].path && distances[0].distance < Number.MAX_VALUE
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { IRect } from "@shopify/react-native-skia";

import type { DrawingElements } from "../types";

import { getBounds } from "./getBounds";

export const findElementsInRect = (
rect: IRect,
elements: DrawingElements
): DrawingElements | undefined => {
const retVal: DrawingElements = [];
const normalizedRect = {
x: rect.width < 0 ? rect.x + rect.width : rect.x,
y: rect.height < 0 ? rect.y + rect.height : rect.y,
width: Math.abs(rect.width),
height: Math.abs(rect.height),
};
elements.forEach((element) => {
const bounds = getBounds(element);
if (
bounds.x >= normalizedRect.x &&
bounds.x + bounds.width <= normalizedRect.x + normalizedRect.width &&
bounds.y >= normalizedRect.y &&
bounds.y + bounds.height <= normalizedRect.y + normalizedRect.height
) {
retVal.push(element);
}
});

if (retVal.length > 0) {
return retVal;
}
return undefined;
};
48 changes: 48 additions & 0 deletions example/src/Examples/Drawing/Context/functions/findResizeMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Point } from "@shopify/react-native-skia";

import type { DrawingElements, ResizeMode } from "../types";

import { getBoundingBox } from "./getBoundingBox";

const hitSlop = 8;

export const findResizeMode = (
point: Point,
selectedElements: DrawingElements
): ResizeMode | undefined => {
const bounds = getBoundingBox(selectedElements);
if (!bounds) {
return undefined;
}

if (
point.x >= bounds.x - hitSlop &&
point.x <= bounds.x + hitSlop &&
point.y >= bounds.y - hitSlop &&
point.y <= bounds.y + hitSlop
) {
return "topLeft";
} else if (
point.x >= bounds.x + bounds.width - hitSlop &&
point.x <= bounds.x + bounds.width + hitSlop &&
point.y >= bounds.y - hitSlop &&
point.y <= bounds.y + hitSlop
) {
return "topRight";
} else if (
point.x >= bounds.x + bounds.width - hitSlop &&
point.x <= bounds.x + bounds.width + hitSlop &&
point.y >= bounds.y + bounds.height - hitSlop &&
point.y <= bounds.y + bounds.height + hitSlop
) {
return "bottomRight";
} else if (
point.x >= bounds.x - hitSlop &&
point.x <= bounds.x + hitSlop &&
point.y >= bounds.y + bounds.height - hitSlop &&
point.y <= bounds.y + bounds.height + hitSlop
) {
return "bottomLeft";
}
return undefined;
};
36 changes: 36 additions & 0 deletions example/src/Examples/Drawing/Context/functions/getBoundingBox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { DrawingElements } from "../types";

import { getBounds } from "./getBounds";

export const getBoundingBox = (elements: DrawingElements) => {
if (elements.length === 0) {
return undefined;
}

const bb = {
x: Number.MAX_VALUE,
y: Number.MAX_VALUE,
right: Number.MIN_VALUE,
bottom: Number.MIN_VALUE,
};

for (let i = 0; i < elements.length; i++) {
const element = elements[i];
const bounds = getBounds(element);

if (bounds.x < bb.x) {
bb.x = bounds.x;
}
if (bounds.y < bb.y) {
bb.y = bounds.y;
}
if (bounds.x + bounds.width > bb.right) {
bb.right = bounds.x + bounds.width;
}
if (bounds.y + bounds.height > bb.bottom) {
bb.bottom = bounds.y + bounds.height;
}
}

return { x: bb.x, y: bb.y, width: bb.right - bb.x, height: bb.bottom - bb.y };
};
Loading

0 comments on commit 6688b1e

Please sign in to comment.