Skip to content

Commit

Permalink
feat(🎨): Enable new reconciler by default on iOS and Android (#2865)
Browse files Browse the repository at this point in the history
The old reconciler is still available via `CanvasOld` if needed
  • Loading branch information
wcandillon authored Jan 12, 2025
1 parent ec9c8c9 commit 6bc0508
Show file tree
Hide file tree
Showing 13 changed files with 579 additions and 228 deletions.
2 changes: 1 addition & 1 deletion apps/paper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"react-native-reanimated": "^3.16.5",
"react-native-safe-area-context": "^4.10.9",
"react-native-screens": "^4.3.0",
"react-native-svg": "^15.9.0",
"react-native-svg": "^15.10.1",
"react-native-wgpu": "0.1.19",
"typescript": "^5.2.2"
},
Expand Down
106 changes: 106 additions & 0 deletions apps/paper/src/Examples/Breathe/TestReconciller.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { useCallback, useMemo, useState } from "react";
import { Button, StyleSheet, useWindowDimensions, View } from "react-native";
import {
BlurMask,
vec,
Canvas,
Circle,
Fill,
Group,
polar2Canvas,
mix,
} from "@shopify/react-native-skia";
import type { SharedValue } from "react-native-reanimated";
import { useDerivedValue } from "react-native-reanimated";

import { useLoop } from "../../components/Animations";

const c1 = "#61bea2";
const c2 = "#529ca0";

interface RingProps {
index: number;
progress: SharedValue<number>;
total: number;
}

const Ring = ({ index, progress, total }: RingProps) => {
const { width, height } = useWindowDimensions();
const R = width / 4;
const center = useMemo(
() => vec(width / 2, height / 2 - 64),
[height, width]
);

const theta = (index * (2 * Math.PI)) / total;
const transform = useDerivedValue(() => {
const { x, y } = polar2Canvas(
{ theta, radius: progress.value * R },
{ x: 0, y: 0 }
);
const scale = mix(progress.value, 0.3, 1);
return [{ translateX: x }, { translateY: y }, { scale }];
});

return (
<Circle
c={center}
r={R}
color={index % 2 ? c1 : c2}
origin={center}
transform={transform}
/>
);
};

export const Breathe = () => {
const [rings, setRings] = useState(12);
const { width, height } = useWindowDimensions();
const center = useMemo(
() => vec(width / 2, height / 2 - 64),
[height, width]
);

const progress = useLoop({ duration: 3000 });

const transform = useDerivedValue(() => [
{ rotate: mix(progress.value, -Math.PI, 0) },
]);

const add = useCallback(() => {
setRings((r) => r + 1);
}, []);
const remove = useCallback(() => {
setRings((r) => Math.max(1, r - 1));
}, []);
return (
<View style={{ flex: 1 }}>
<View>
<Button onPress={add} title="add" />
<Button onPress={remove} title="remove" />
</View>
<Canvas style={styles.container} opaque>
<Fill color="rgb(36,43,56)" />
<Group origin={center} transform={transform} blendMode="screen">
<BlurMask style="solid" blur={40} />
{new Array(rings).fill(0).map((_, index) => {
return (
<Ring
key={index}
index={index}
progress={progress}
total={rings}
/>
);
})}
</Group>
</Canvas>
</View>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
},
});
2 changes: 1 addition & 1 deletion packages/skia/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import "./skia/NativeSetup";
export { JsiSkImage } from "./skia/web/JsiSkImage";
export * from "./renderer";
export * from "./renderer/CanvasOld";
export * from "./renderer/Canvas";
export * from "./renderer/Canvas2";
export * from "./renderer/Offscreen";
export * from "./views";
export * from "./skia";
Expand Down
158 changes: 80 additions & 78 deletions packages/skia/src/renderer/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
import React, {
useEffect,
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
forwardRef,
useRef,
} from "react";
import type {
RefObject,
ReactNode,
MutableRefObject,
ForwardedRef,
FunctionComponent,
} from "react";
import type { LayoutChangeEvent } from "react-native";
import type { LayoutChangeEvent, ViewProps } from "react-native";
import type { SharedValue } from "react-native-reanimated";

import { SkiaDomView } from "../views";
import { SkiaViewNativeId } from "../views/SkiaViewNativeId";
import SkiaPictureViewNativeComponent from "../specs/SkiaPictureViewNativeComponent";
import type { SkRect, SkSize } from "../skia/types";
import { SkiaSGRoot } from "../sksg/Reconciler";
import { Skia } from "../skia";
import type { SkiaBaseViewProps } from "../views";

import { SkiaRoot } from "./Reconciler";

export const useCanvasRef = () => useRef<SkiaDomView>(null);

export interface CanvasProps extends SkiaBaseViewProps {
ref?: RefObject<SkiaDomView>;
children: ReactNode;
mode?: "default" | "continuous";
}
const NativeSkiaPictureView = SkiaPictureViewNativeComponent;

// TODO: no need to go through the JS thread for this
const useOnSizeEvent = (
resultValue: SkiaBaseViewProps["onSize"],
onLayout?: (event: LayoutChangeEvent) => void
Expand All @@ -46,81 +38,91 @@ const useOnSizeEvent = (
);
};

export const Canvas = forwardRef<SkiaDomView, CanvasProps>(
export interface CanvasProps extends ViewProps {
debug?: boolean;
opaque?: boolean;
onSize?: SharedValue<SkSize>;
mode?: "continuous" | "default";
}

export const Canvas = forwardRef(
(
{
children,
style,
mode,
debug,
mode = "default",
onSize: _onSize,
opaque,
children,
onSize,
onLayout: _onLayout,
...props
},
forwardedRef
...viewProps
}: CanvasProps,
ref
) => {
const onLayout = useOnSizeEvent(_onSize, _onLayout);
const innerRef = useCanvasRef();
const ref = useCombinedRefs(forwardedRef, innerRef);
const redraw = useCallback(() => {
innerRef.current?.redraw();
}, [innerRef]);
const getNativeId = useCallback(() => {
const id = innerRef.current?.nativeId ?? -1;
return id;
}, [innerRef]);
const rafId = useRef<number | null>(null);
const onLayout = useOnSizeEvent(onSize, _onLayout);
// Native ID
const nativeId = useMemo(() => {
return SkiaViewNativeId.current++;
}, []);

const root = useMemo(
() => new SkiaRoot(redraw, getNativeId),
[redraw, getNativeId]
);
// Root
const root = useMemo(() => new SkiaSGRoot(Skia, nativeId), [nativeId]);

// Render effect
// Render effects
useEffect(() => {
root.render(children);
}, [children, root, redraw]);
}, [children, root]);

useEffect(() => {
return () => {
root.unmount();
};
}, [root]);

const requestRedraw = useCallback(() => {
rafId.current = requestAnimationFrame(() => {
root.render(children);
if (mode === "continuous") {
requestRedraw();
}
});
}, [children, mode, root]);

useEffect(() => {
if (mode === "continuous") {
console.warn("The `mode` property in `Canvas` is deprecated.");
requestRedraw();
}
return () => {
if (rafId.current !== null) {
cancelAnimationFrame(rafId.current);
}
};
}, [mode, requestRedraw]);
// Component methods
useImperativeHandle(ref, () => ({
makeImageSnapshot: (rect?: SkRect) => {
return SkiaViewApi.makeImageSnapshot(nativeId, rect);
},
makeImageSnapshotAsync: (rect?: SkRect) => {
return SkiaViewApi.makeImageSnapshotAsync(nativeId, rect);
},
redraw: () => {
SkiaViewApi.requestRedraw(nativeId);
},
getNativeId: () => {
return nativeId;
},
}));
return (
<SkiaDomView
ref={ref}
style={style}
root={root.dom}
onLayout={onLayout}
<NativeSkiaPictureView
collapsable={false}
nativeID={`${nativeId}`}
debug={debug}
mode={mode}
{...props}
opaque={opaque}
onLayout={onLayout}
{...viewProps}
/>
);
}
) as FunctionComponent<CanvasProps & React.RefAttributes<SkiaDomView>>;

/**
* Combines a list of refs into a single ref. This can be used to provide
* both a forwarded ref and an internal ref keeping the same functionality
* on both of the refs.
* @param refs Array of refs to combine
* @returns A single ref that can be used in a ref prop.
*/
const useCombinedRefs = <T,>(
...refs: Array<MutableRefObject<T> | ForwardedRef<T>>
) => {
const targetRef = React.useRef<T>(null);
React.useEffect(() => {
refs.forEach((ref) => {
if (ref) {
if (typeof ref === "function") {
ref(targetRef.current);
} else {
ref.current = targetRef.current;
}
}
});
}, [refs]);
return targetRef;
};
);
Loading

0 comments on commit 6bc0508

Please sign in to comment.