Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

↕️ useCanvasSize() #380

Merged
merged 5 commits into from
Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/docs/animations/values.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,55 @@ const Demo = () => {
};
```

## Canvas Size

The `useCanvasSize` hook is a value that updates every time the canvas size updates.


:::caution

`useCanvasSize` can only be used inside the Canvas element because it relies on context.

:::

```tsx twoslash
import React from "react";
import {
Canvas,
Fill,
Group,
Rect,
rect,
useCanvasSize,
useDerivedValue,
} from "@shopify/react-native-skia";

const MyComp = () => {
// 💚 useCanvasSize() can safely be used here
const canvas = useCanvasSize();
const rct = useDerivedValue(() => {
return rect(0, 0, canvas.current.width, canvas.current.height / 2);
}, [canvas]);
return (
<Group>
<Fill color="magenta" />
<Rect color="cyan" rect={rct} />
</Group>
);
};

const Example = () => {
// ❌ Using useCanvasSize() here would crash
return (
<Canvas style={{ flex: 1 }}>
<MyComp />
</Canvas>
);
};

```


## Value Effect

The `useValueEffect` hook allows you to execute change on value change.
Expand Down
11 changes: 9 additions & 2 deletions docs/docs/canvas/canvas.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@ Behind the scenes, it is using its own React renderer.

| Name | Type | Description. |
|:-----|:---------|:-----------------|
| style | `ViewStyle` | View style. |
| style? | `ViewStyle` | View style |
| ref? | `Ref<SkiaView>` | Reference to the `SkiaView` object |
| onTouch? | `TouchHandler` | Touch handler for the Canvas (see [touch handler](/docs/animations/touch-events#usetouchhandler)). |
| onTouch? | `TouchHandler` | Touch handler for the Canvas (see [touch handler](/docs/animations/touch-events#usetouchhandler)) |
| onLayout? | `NativeEvent<LayoutEvent>` | Invoked on mount and on layout changes. (see [onLayout](https://reactnative.dev/docs/view#onlayout)) |

## Getting the Canvas size

If the size of the Canvas is unknown, there are two ways to access it:
* In React components using the `onLayout` prop like you would on any regular React Native View.
* As a Skia value using `useCanvasSize()` (see [useCanvasSize()](/docs/animations/values#canvas-size)).

## Getting a Canvas Snapshot

Expand Down
4 changes: 4 additions & 0 deletions example/src/Examples/API/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ const examples = [
screen: "Picture",
title: "🖼 Picture",
},
{
screen: "UseCanvas",
title: "↕️ useCanvas()",
},
] as const;

const styles = StyleSheet.create({
Expand Down
1 change: 1 addition & 0 deletions example/src/Examples/API/Routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export type Routes = {
BlendModes: undefined;
Data: undefined;
Picture: undefined;
UseCanvas: undefined;
};
45 changes: 45 additions & 0 deletions example/src/Examples/API/UseCanvas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
Canvas,
Fill,
Group,
Rect,
rect,
useCanvasSize,
useDerivedValue,
} from "@shopify/react-native-skia";
import React, { useEffect, useRef } from "react";
import { View, Animated } from "react-native";

const MyComp = () => {
const canvas = useCanvasSize();
const rct = useDerivedValue(() => {
return rect(0, 0, canvas.current.width, canvas.current.height / 2);
}, [canvas]);
return (
<Group>
<Fill color="magenta" />
<Rect color="cyan" rect={rct} />
</Group>
);
};

export const UseCanvas = () => {
const height = useRef(new Animated.Value(0));
useEffect(() => {
Animated.loop(
Animated.timing(height.current, {
toValue: 500,
duration: 4000,
useNativeDriver: false,
})
).start();
}, []);
return (
<View style={{ flex: 1 }}>
<Canvas style={{ flex: 1 }}>
<MyComp />
</Canvas>
<Animated.View style={{ height: height.current }} />
</View>
);
};
8 changes: 8 additions & 0 deletions example/src/Examples/API/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { BlendModes } from "./BlendModes";
import { Data } from "./Data";
import { PictureExample } from "./Picture";
import { ImageFilters } from "./ImageFilters";
import { UseCanvas } from "./UseCanvas";

const Stack = createNativeStackNavigator<Routes>();
export const API = () => {
Expand Down Expand Up @@ -120,6 +121,13 @@ export const API = () => {
title: "🖼 Picture",
}}
/>
<Stack.Screen
name="UseCanvas"
component={UseCanvas}
options={{
title: "↕️ UseCanvas",
}}
/>
</Stack.Navigator>
);
};
44 changes: 14 additions & 30 deletions package/src/renderer/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import type {
RefObject,
ReactNode,
ComponentProps,
Context,
ReactElement,
MutableRefObject,
ForwardedRef,
} from "react";
Expand All @@ -23,40 +21,21 @@ import { SkiaView, useDrawCallback } from "../views";
import type { TouchHandler } from "../views";
import { Skia } from "../skia";
import type { FontMgr } from "../skia/FontMgr/FontMgr";
import { useValue } from "../values/hooks/useValue";
import type { SkiaReadonlyValue } from "../values/types";

import { debug as hostDebug, skHostConfig } from "./HostConfig";
// import { debugTree } from "./nodes";
import { vec } from "./processors";
import { Container } from "./nodes";
import { DependencyManager } from "./DependencyManager";

// useContextBridge() is taken from https://github.com/pmndrs/drei#usecontextbridge
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const useContextBridge = (...contexts: Context<any>[]) => {
const values =
// eslint-disable-next-line react-hooks/rules-of-hooks
contexts.map((context) => useContext(context));
return useMemo(
() =>
({ children }: { children: ReactNode }) =>
contexts.reduceRight(
(acc, Context, i) => (
<Context.Provider value={values[i]} children={acc} />
),
children
) as ReactElement,
[contexts, values]
);
};

interface CanvasContext {
const CanvasContext = React.createContext<SkiaReadonlyValue<{
width: number;
height: number;
}
}> | null>(null);

const CanvasContext = React.createContext<CanvasContext | null>(null);

export const useCanvas = () => {
export const useCanvasSize = () => {
const canvas = useContext(CanvasContext);
if (!canvas) {
throw new Error("Canvas context is not available");
Expand Down Expand Up @@ -93,6 +72,7 @@ const defaultFontMgr = Skia.FontMgr.RefDefault();

export const Canvas = forwardRef<SkiaView, CanvasProps>(
({ children, style, debug, mode, onTouch, fontMgr }, forwardedRef) => {
const canvasCtx = useValue({ width: 0, height: 0 });
const innerRef = useCanvasRef();
const ref = useCombinedRefs(forwardedRef, innerRef);
const [tick, setTick] = useState(0);
Expand All @@ -103,21 +83,20 @@ export const Canvas = forwardRef<SkiaView, CanvasProps>(
[redraw, ref]
);

const canvasCtx = useRef({ width: 0, height: 0 });
const root = useMemo(
() => skiaReconciler.createContainer(container, 0, false, null),
[container]
);
// Render effect
useEffect(() => {
render(
<CanvasContext.Provider value={canvasCtx.current}>
<CanvasContext.Provider value={canvasCtx}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be play well with hot reload, could this be improved?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does the reload problem materialise itself? It looks correct since the effect is dependent on the canvasCtx?

{children}
</CanvasContext.Provider>,
root,
container
);
}, [children, root, redraw, container]);
}, [children, root, redraw, container, canvasCtx]);

// Draw callback
const onDraw = useDrawCallback(
Expand All @@ -127,6 +106,12 @@ export const Canvas = forwardRef<SkiaView, CanvasProps>(
if (onTouch) {
onTouch(info.touches);
}
if (
width !== canvasCtx.current.width ||
height !== canvasCtx.current.height
) {
canvasCtx.current = { width, height };
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking it might be possible to include more of the drawing context - I saw your comment about wanting to do so. Is there a specific reason for not doing so?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was that useDerived() that depends on the canvasCtx value should only be run if we modify the width/height. But the expression is always re-evaluated on redraw? I will double check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checked and this does save some unnecessary JS evaluation. but of course let's keep it in mind (as well as offering a way to access the ref)

const paint = Skia.Paint();
paint.setAntiAlias(true);
const ctx = {
Expand All @@ -140,7 +125,6 @@ export const Canvas = forwardRef<SkiaView, CanvasProps>(
center: vec(width / 2, height / 2),
fontMgr: fontMgr ?? defaultFontMgr,
};
canvasCtx.current = ctx;
container.draw(ctx);
},
[tick, onTouch]
Expand Down
20 changes: 1 addition & 19 deletions package/src/renderer/HostConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { HostConfig } from "react-reconciler";

import type { Node, Container, DeclarationProps, DrawingProps } from "./nodes";
import { DeclarationNode, DrawingNode, NodeType } from "./nodes";
import { exhaustiveCheck, mapKeys } from "./typeddash";
import { exhaustiveCheck, shallowEq } from "./typeddash";

const DEBUG = false;
export const debug = (...args: Parameters<typeof console.log>) => {
Expand Down Expand Up @@ -53,24 +53,6 @@ type SkiaHostConfig = HostConfig<
NoTimeout
>;

// Shallow eq on props (without children)
const shallowEq = <P extends Props>(p1: P, p2: P): boolean => {
const keys1 = mapKeys(p1);
const keys2 = mapKeys(p2);
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
if (key === "children") {
continue;
}
if (p1[key] !== p2[key]) {
return false;
}
}
return true;
};

const allChildrenAreMemoized = (node: Instance) => {
if (!node.memoizable) {
return false;
Expand Down
1 change: 1 addition & 0 deletions package/src/renderer/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./Canvas";
export * from "./components";
export * from "./nodes";
export * from "./useContextBridge";
18 changes: 18 additions & 0 deletions package/src/renderer/typeddash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,21 @@ export const mapKeys = <T>(obj: T) => Object.keys(obj) as (keyof T)[];
export const exhaustiveCheck = (a: never): never => {
throw new Error(`Unexhaustive handling for ${a}`);
};

// Shallow eq on props (without children)
export const shallowEq = <P>(p1: P, p2: P): boolean => {
const keys1 = mapKeys(p1);
const keys2 = mapKeys(p2);
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
if (key === "children") {
continue;
}
if (p1[key] !== p2[key]) {
return false;
}
}
return true;
};
21 changes: 21 additions & 0 deletions package/src/renderer/useContextBridge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { useMemo, useContext } from "react";
import type { ReactNode, Context, ReactElement } from "react";

// useContextBridge() is taken from https://github.com/pmndrs/drei#usecontextbridge
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const useContextBridge = (...contexts: Context<any>[]) => {
const values =
// eslint-disable-next-line react-hooks/rules-of-hooks
contexts.map((context) => useContext(context));
return useMemo(
() =>
({ children }: { children: ReactNode }) =>
contexts.reduceRight(
(acc, Context, i) => (
<Context.Provider value={values[i]} children={acc} />
),
children
) as ReactElement,
[contexts, values]
);
};
2 changes: 1 addition & 1 deletion package/src/values/hooks/useDerivedValue.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from "react";

import { ValueApi } from "../api";
import { isValue } from "../../renderer";
import { isValue } from "../../renderer/processors";

/**
* Creates a new derived value - a value that will calculate its value depending
Expand Down
Loading