Skip to content

Commit

Permalink
🗺️ Atlas API (#2134)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcandillon authored Jan 26, 2024
1 parent f9118c5 commit f321480
Show file tree
Hide file tree
Showing 53 changed files with 1,142 additions and 168 deletions.
37 changes: 37 additions & 0 deletions docs/docs/animations/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,40 @@ const Demo = () => {
);
};
```

## useRectBuffer

Creates an array for rectangle to be animated.
Can be used by any component that takes an array of rectangles as property, like the [Atlas API](/docs/shapes/atlas).

```tsx twoslash
import {useRectBuffer} from "@shopify/react-native-skia";

const width = 256;
const size = 10;
const rects = 100;
// Important to not forget the worklet directive
const rectBuffer = useRectBuffer(rects, (rect, i) => {
"worklet";
rect.setXYWH((i * size) % width, Math.floor(i / (width / size)) * size, size, size);
});
```

## useRSXformBuffer

Creates an array for [rotate scale transforms](/docs/shapes/atlas#rsxform) to be animated.
Can be used by any component that takes an array of rotate scale transforms as property, like the [Atlas API](/docs/shapes/atlas).

```tsx twoslash
import {useRSXformBuffer} from "@shopify/react-native-skia";
import {useSharedValue} from "react-native-reanimated";

const xforms = 100;
const pos = useSharedValue({ x: 0, y: 0 });
// Important to not forget the worklet directive
const transforms = useRSXformBuffer(xforms, (val, i) => {
"worklet";
const r = Math.atan2(pos.value.y, pos.value.x);
val.set(Math.cos(r), Math.sin(r), 0, 0);
});
```
160 changes: 160 additions & 0 deletions docs/docs/shapes/atlas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
---
id: atlas
title: Atlas
sidebar_label: Atlas
slug: /shapes/atlas
---

The Atlas component is used for efficient rendering of multiple instances of the same texture or image. It is especially useful for drawing a very large number of similar objects, like sprites, with varying transformations.

Its design particularly useful when using with [Reanimated](#animations).

| Name | Type | Description |
|:--------|:-----------------|:-----------------|
| image | `SkImage or null` | Altas: image containing the sprites. |
| sprites | `SkRect[]` | locations of sprites in atlas. |
| transforms | `RSXform[]` | Rotation/scale transforms to be applied for each sprite. |
| colors? | `SkColor[]` | Optional. Color to blend the sprites with. |
| blendMode? | `BlendMode` | Optional. Blend mode used to combine sprites and colors together. |

## RSXform

The RSXform object used by the altas API is the compression of the following matrix: `[fSCos -fSSin fTx, fSSin fSCos fTy, 0, 0, 1]`. Below are few transformations that you will find useful:

```tsx twoslash
import {Skia} from "@shopify/react-native-skia";

// 1. Identify (doesn't do anything)
let rsxForm = Skia.RSXform(1, 0, 0, 0);

// 2. Scale by 2 and translate by (50, 100)
rsxForm = Skia.RSXform(2, 0, 50, 100);

// 3. Rotate by PI/4, default pivot point is (0,0), translate by (50, 100)
const r = Math.PI/4;
rsxForm = Skia.RSXform(Math.cos(r), Math.sin(r), 50, 100);

// 4. Scale by 2, rotate by PI/4 with pivot point (25, 25)
rsxForm = Skia.RSXformFromRadians(2, r, 0, 0, 25, 25);

// 5. translate by (125, 0), rotate by PI/4 with pivot point (125, 25)
rsxForm = Skia.RSXformFromRadians(1, r, 100, 0, 125, 25);
```

## Hello World

In the example below, we draw in simple rectangle as an image.
Then we display that rectangle 150 times with a simple transformation applied to each rectangle.

```tsx twoslash
import {Skia, drawAsImage, Group, Rect, Canvas, Atlas, rect} from "@shopify/react-native-skia";

const size = { width: 25, height: 11.25 };
const strokeWidth = 2;
const imageSize = {
width: size.width + strokeWidth,
height: size.height + strokeWidth,
};
const image = drawAsImage(
<Group>
<Rect
rect={rect(strokeWidth / 2, strokeWidth / 2, size.width, size.height)}
color="cyan"
/>
<Rect
rect={rect(strokeWidth / 2, strokeWidth / 2, size.width, size.height)}
color="blue"
style="stroke"
strokeWidth={strokeWidth}
/>
</Group>,
imageSize
);

export const Demo = () => {
const numberOfBoxes = 150;
const pos = { x: 128, y: 128 };
const width = 256;
const sprites = new Array(numberOfBoxes)
.fill(0)
.map(() => rect(0, 0, imageSize.width, imageSize.height));
const transforms = new Array(numberOfBoxes).fill(0).map((_, i) => {
const tx = 5 + ((i * size.width) % width);
const ty = 25 + Math.floor(i / (width / size.width)) * size.width;
const r = Math.atan2(pos.y - ty, pos.x - tx);
return Skia.RSXform(Math.cos(r), Math.sin(r), tx, ty);
});

return (
<Canvas style={{ flex: 1 }}>
<Atlas image={image} sprites={sprites} transforms={transforms} />
</Canvas>
);
};
```

<img src={require("/static/img/atlas/hello-world.png").default} width="256" height="256" />


## Animations

The Atlas component should usually be used with Reanimated.
First, the [useTextueValue](/docs/animations/textures#usetexturevalue) hook will enable you to create a texture on the UI thread directly without needing to make any copies.
Secondly, we provide you with hooks such as [`useRectBuffer`](/docs/animations/hooks#userectbuffer) and [`useRSXformBuffer`](/docs/animations/hooks#usersxformbuffer) to efficiently animates on the sprites and transformations.

The example below is identical to the one above but the position is an animation value bound to a gesture.


```tsx twoslash
import {Skia, drawAsImage, Group, Rect, Canvas, Atlas, rect, useTextureValue, useRSXformBuffer} from "@shopify/react-native-skia";
import {useSharedValue, useDerivedValue} from "react-native-reanimated";
import {GestureDetector, Gesture} from "react-native-gesture-handler";

const size = { width: 25, height: 11.25 };
const strokeWidth = 2;
const textureSize = {
width: size.width + strokeWidth,
height: size.height + strokeWidth,
};

export const Demo = () => {
const pos = useSharedValue({ x: 0, y: 0 });
const texture = useTextureValue(
<Group>
<Rect
rect={rect(strokeWidth / 2, strokeWidth / 2, size.width, size.height)}
color="cyan"
/>
<Rect
rect={rect(strokeWidth / 2, strokeWidth / 2, size.width, size.height)}
color="blue"
style="stroke"
strokeWidth={strokeWidth}
/>
</Group>,
textureSize
);
const gesture = Gesture.Pan().onChange((e) => (pos.value = e));
const numberOfBoxes = 150;
const width = 256;
const sprites = new Array(numberOfBoxes)
.fill(0)
.map(() => rect(0, 0, textureSize.width, textureSize.height));

const transforms = useRSXformBuffer(numberOfBoxes, (val, i) => {
"worklet";
const tx = 5 + ((i * size.width) % width);
const ty = 25 + Math.floor(i / (width / size.width)) * size.width;
const r = Math.atan2(pos.value.y - ty, pos.value.x - tx);
val.set(Math.cos(r), Math.sin(r), tx, ty);
});

return (
<GestureDetector gesture={gesture}>
<Canvas style={{ flex: 1 }} mode="continuous">
<Atlas image={texture} sprites={sprites} transforms={transforms} />
</Canvas>
</GestureDetector>
);
};
```
1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const sidebars = {
"shapes/path",
"shapes/polygons",
"shapes/ellipses",
"shapes/atlas",
"shapes/vertices",
"shapes/patch",
"shapes/pictures",
Expand Down
Binary file added docs/static/img/atlas/colors-and-blend-mode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/static/img/atlas/colors.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/static/img/atlas/hello-world.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion example/src/Examples/Examples.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Aurora } from "./Aurora";
import { Wallet } from "./Wallet";
import { Vertices } from "./Vertices";
import { Severance } from "./Severance";
import { PerformanceDrawingTest } from "./Performance";
import { PerformanceDrawingTest } from "./Performance/PerformanceRects";
import { GraphsScreen } from "./Graphs";
import { Neumorphism } from "./Neumorphism";
import { Matrix } from "./Matrix";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,72 +1,73 @@
import {
Canvas,
Rect,
Skia,
Group,
Atlas,
useTextureValue,
Image,
Group,
rect,
Rect,
useRSXformBuffer,
} from "@shopify/react-native-skia";
import type { SkImage, SkRect } from "@shopify/react-native-skia";
import React, { useMemo, useState } from "react";
import {
StyleSheet,
useWindowDimensions,
View,
Text,
Button,
PixelRatio,
} from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import type { SharedValue } from "react-native-reanimated";
import Animated, {
useDerivedValue,
useSharedValue,
} from "react-native-reanimated";
import { useSharedValue } from "react-native-reanimated";

const Increaser = 50;

const size = { width: 25, height: 25 * 0.45 };
const strokeWidth = 2;
const textureSize = {
width: size.width + strokeWidth,
height: size.height + strokeWidth,
};

export const PerformanceDrawingTest = () => {
const [numberOfBoxes, setNumberOfBoxes] = useState(300);

export const PerformanceDrawingTest: React.FC = () => {
const texture = useTextureValue(
<Group>
<Rect
rect={rect(strokeWidth / 2, strokeWidth / 2, size.width, size.height)}
color="#00ff00"
color="cyan"
/>
<Rect
rect={rect(strokeWidth / 2, strokeWidth / 2, size.width, size.height)}
color="#4060A3"
color="blue"
style="stroke"
strokeWidth={strokeWidth}
/>
</Group>,
{ width: size.width + strokeWidth, height: size.height + strokeWidth }
textureSize
);

const sprites = useMemo(
() =>
new Array(numberOfBoxes)
.fill(0)
.map(() => rect(0, 0, textureSize.width, textureSize.height)),
[numberOfBoxes]
);
const [numberOfBoxes, setNumberOfBoxes] = useState(150);

const { width, height } = useWindowDimensions();

const pos = useSharedValue<{ x: number; y: number }>({
const pos = useSharedValue({
x: width / 2,
y: height * 0.25,
});

const rects = useMemo(
() =>
new Array(numberOfBoxes)
.fill(0)
.map((_, i) =>
Skia.XYWHRect(
5 + ((i * size.width) % width),
25 + Math.floor(i / (width / size.width)) * size.width,
size.width,
size.height
)
),
[numberOfBoxes, width]
);
const transforms = useRSXformBuffer(numberOfBoxes, (val, i) => {
"worklet";
const tx = 5 + ((i * size.width) % width);
const ty = 25 + Math.floor(i / (width / size.width)) * size.width;
const r = Math.atan2(pos.value.y - ty, pos.value.x - tx);
val.set(Math.cos(r), Math.sin(r), tx, ty);
});

const gesture = Gesture.Pan().onChange((e) => (pos.value = e));

Expand All @@ -88,51 +89,16 @@ export const PerformanceDrawingTest: React.FC = () => {
</View>
</View>
<View style={{ flex: 1 }}>
<Canvas style={styles.container} mode="default">
{rects.map((_, i) => (
<Rct pos={pos} key={i} translation={rects[i]} texture={texture} />
))}
</Canvas>
<GestureDetector gesture={gesture}>
<Animated.View style={StyleSheet.absoluteFill} />
<Canvas style={styles.container} mode="continuous">
<Atlas image={texture} sprites={sprites} transforms={transforms} />
</Canvas>
</GestureDetector>
</View>
</View>
);
};

interface RctProps {
pos: SharedValue<{ x: number; y: number }>;
translation: SkRect;
texture: SharedValue<SkImage | null>;
}

const Rct = ({ pos, texture, translation }: RctProps) => {
const transform = useDerivedValue(() => {
const p1 = { x: translation.x, y: translation.y };
const p2 = pos.value;
const r = Math.atan2(p2.y - p1.y, p2.x - p1.x);
return [
{ translateX: translation.x },
{ translateY: translation.y },
{ rotate: r },
];
});
const rct = useDerivedValue(() => {
return {
x: 0,
y: 0,
width: texture.value?.width() ?? 0 / PixelRatio.get(),
height: texture.value?.height() ?? 0 / PixelRatio.get(),
};
});
return (
<Group transform={transform}>
<Image image={texture} rect={rct} />
</Group>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
Expand Down
2 changes: 1 addition & 1 deletion example/src/Examples/Performance/PerformanceRects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const PerformanceDrawingTest: React.FC = () => {
const SizeWidth = Size;
const SizeHeight = Size * 0.45;

const pos = useSharedValue<{ x: number; y: number }>({
const pos = useSharedValue({
x: width / 2,
y: height * 0.25,
});
Expand Down
4 changes: 2 additions & 2 deletions example/src/Examples/Performance/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { PerformanceDrawingTest } from "./PerformanceRects";
//export { PerformanceDrawingTest } from "./PerformanceRectsTexture";
export { PerformanceDrawingTest } from "./Atlas";
//export { PerformanceDrawingTest } from "./PerformanceRects";
//export { PerformanceDrawingTest } from "./PerformanceCanvases";
Loading

0 comments on commit f321480

Please sign in to comment.