Skip to content

Commit 4d13cc7

Browse files
authored
Merge pull request #153 from Shopify/feature/73-path-interpolation
Path interpolation
2 parents 043b3da + 4b665e7 commit 4d13cc7

File tree

11 files changed

+354
-2
lines changed

11 files changed

+354
-2
lines changed

example/src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from "react";
33
import { createNativeStackNavigator } from "@react-navigation/native-stack";
44
import { StatusBar } from "react-native";
55

6-
import { AnimationExample, DrawingExample } from "./Examples";
6+
import { AnimationExample, DrawingExample, GraphsScreen } from "./Examples";
77
import { API } from "./Examples/API";
88
import { Breathe } from "./Examples/Breathe";
99
import { Filters } from "./Examples/Filters";
@@ -40,6 +40,7 @@ const App = () => {
4040
}}
4141
/>
4242
<Stack.Screen name="Drawing" component={DrawingExample} />
43+
<Stack.Screen name="Graphs" component={GraphsScreen} />
4344
<Stack.Screen name="Animation" component={AnimationExample} />
4445
</Stack.Navigator>
4546
</NavigationContainer>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {
2+
Canvas,
3+
Fill,
4+
LinearGradient,
5+
Paint,
6+
Path,
7+
Spring,
8+
useValue,
9+
vec,
10+
} from "@shopify/react-native-skia";
11+
import { runSpring } from "@shopify/react-native-skia/src/animation/Animation/functions";
12+
import React, { useCallback, useEffect, useMemo, useState } from "react";
13+
import { StyleSheet, Text, View } from "react-native";
14+
15+
import { createGraphPath } from "./createGraphPath";
16+
import type { GraphProps } from "./types";
17+
18+
export const Interpolation: React.FC<GraphProps> = ({ height, width }) => {
19+
const path = useMemo(
20+
() => createGraphPath(width, height, 60),
21+
[height, width]
22+
);
23+
const path2 = useMemo(
24+
() => createGraphPath(width, height, 60),
25+
[height, width]
26+
);
27+
28+
const progress = useValue(0);
29+
const [toggled, setToggled] = useState(false);
30+
const onPress = useCallback(() => setToggled((p) => !p), []);
31+
useEffect(() => {
32+
runSpring(progress, toggled ? 1 : 0, Spring.Config.Gentle);
33+
}, [progress, toggled]);
34+
35+
return (
36+
<View style={{ height, marginBottom: 10 }} onTouchEnd={onPress}>
37+
<Canvas style={styles.graph}>
38+
<Fill color="black" />
39+
<Paint>
40+
<LinearGradient
41+
start={vec(0, height * 0.5)}
42+
end={vec(width * 0.5, height * 0.5)}
43+
colors={["black", "#cccc66"]}
44+
/>
45+
</Paint>
46+
<Path
47+
path={() => path.interpolate(path2, progress.value)}
48+
strokeWidth={4}
49+
style="stroke"
50+
strokeJoin="round"
51+
strokeCap="round"
52+
/>
53+
</Canvas>
54+
<Text>Touch graph to interpolate</Text>
55+
</View>
56+
);
57+
};
58+
59+
const styles = StyleSheet.create({
60+
graph: {
61+
flex: 1,
62+
},
63+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {
2+
Canvas,
3+
Easing,
4+
Fill,
5+
LinearGradient,
6+
Paint,
7+
Path,
8+
useValue,
9+
vec,
10+
} from "@shopify/react-native-skia";
11+
import { runTiming } from "@shopify/react-native-skia/src/animation/Animation/functions";
12+
import React, { useCallback, useEffect, useMemo, useState } from "react";
13+
import { StyleSheet, Text, View } from "react-native";
14+
15+
import { createGraphPath, createZeroPath } from "./createGraphPath";
16+
import type { GraphProps } from "./types";
17+
18+
export const MountAnimation: React.FC<GraphProps> = ({ height, width }) => {
19+
const zeroPath = useMemo(
20+
() => createZeroPath(width, height, 60),
21+
[height, width]
22+
);
23+
const path = useMemo(
24+
() => createGraphPath(width, height, 60, false),
25+
[height, width]
26+
);
27+
28+
const progress = useValue(0);
29+
const [toggled, setToggled] = useState(false);
30+
const onPress = useCallback(() => setToggled((p) => !p), []);
31+
32+
useEffect(() => {
33+
runTiming(progress, {
34+
to: toggled ? 1 : 0,
35+
duration: 350,
36+
easing: Easing.inOut(Easing.cubic),
37+
});
38+
}, [progress, toggled]);
39+
40+
return (
41+
<View style={{ height, marginBottom: 10 }} onTouchEnd={onPress}>
42+
<Canvas style={styles.graph}>
43+
<Fill color="black" />
44+
<Paint>
45+
<LinearGradient
46+
start={vec(0, height * 0.5)}
47+
end={vec(width * 0.5, height * 0.5)}
48+
colors={["black", "#3B8EA5"]}
49+
/>
50+
</Paint>
51+
<Path
52+
path={() => path.interpolate(zeroPath, progress.value)}
53+
strokeWidth={4}
54+
style="stroke"
55+
strokeJoin="round"
56+
strokeCap="round"
57+
/>
58+
</Canvas>
59+
<Text>Touch to toggle between "unmounted" and "mounted"</Text>
60+
</View>
61+
);
62+
};
63+
64+
const styles = StyleSheet.create({
65+
graph: {
66+
flex: 1,
67+
},
68+
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import type { IPath } from "@shopify/react-native-skia";
2+
import {
3+
Line,
4+
Canvas,
5+
Circle,
6+
Fill,
7+
LinearGradient,
8+
Paint,
9+
Path,
10+
useTouchHandler,
11+
useValue,
12+
Text as SkiaText,
13+
vec,
14+
} from "@shopify/react-native-skia";
15+
import React, { useMemo } from "react";
16+
import { StyleSheet, Text, View } from "react-native";
17+
18+
import { createGraphPath } from "./createGraphPath";
19+
import type { GraphProps } from "./types";
20+
21+
export const Slider: React.FC<GraphProps> = ({ height, width }) => {
22+
const path = useMemo(
23+
() => createGraphPath(width, height, 60, false),
24+
[height, width]
25+
);
26+
27+
const progress = useValue(
28+
getPointAtPositionInPath(width / 2, width, 60, path)
29+
);
30+
31+
const touchHandler = useTouchHandler({
32+
onActive: ({ x }) =>
33+
(progress.value = getPointAtPositionInPath(x, width, 60, path)),
34+
});
35+
36+
return (
37+
<View style={{ height, marginBottom: 10 }}>
38+
<Canvas style={styles.graph} onTouch={touchHandler}>
39+
<Fill color="black" />
40+
<Paint>
41+
<LinearGradient
42+
start={vec(0, height * 0.5)}
43+
end={vec(width * 0.5, height * 0.5)}
44+
colors={["black", "#DA4167"]}
45+
/>
46+
</Paint>
47+
<Path
48+
path={path}
49+
strokeWidth={4}
50+
style="stroke"
51+
strokeJoin="round"
52+
strokeCap="round"
53+
/>
54+
<Paint color="#fff" />
55+
<Circle c={() => progress.value} r={10} />
56+
<Circle color="#DA4167" c={() => progress.value} r={7.5} />
57+
<SkiaText
58+
familyName="Arial"
59+
size={12}
60+
x={() => progress.value.x - 24}
61+
y={() => progress.value.y - 18}
62+
value={() => "$ " + progress.value.x.toFixed(2)}
63+
/>
64+
<Line
65+
p1={() => vec(progress.value.x, progress.value.y + 14)}
66+
p2={() => vec(progress.value.x, height)}
67+
/>
68+
</Canvas>
69+
<Text>Touch and drag to move center point</Text>
70+
</View>
71+
);
72+
};
73+
74+
const getPointAtPositionInPath = (
75+
x: number,
76+
width: number,
77+
steps: number,
78+
path: IPath
79+
) => {
80+
const index = Math.max(0, Math.floor(x / (width / steps)));
81+
const fraction = (x / (width / steps)) % 1;
82+
const p1 = path.getPoint(index);
83+
if (index < path.countPoints() - 1) {
84+
const p2 = path.getPoint(index + 1);
85+
// Interpolate between p1 and p2
86+
return {
87+
x: p1.x + (p2.x - p1.x) * fraction,
88+
y: p1.y + (p2.y - p1.y) * fraction,
89+
};
90+
}
91+
return p1;
92+
};
93+
94+
const styles = StyleSheet.create({
95+
graph: {
96+
flex: 1,
97+
},
98+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Skia } from "@shopify/react-native-skia";
2+
3+
export const createGraphPath = (
4+
width: number,
5+
height: number,
6+
steps: number,
7+
round = true
8+
) => {
9+
const retVal = Skia.Path.Make();
10+
let y = height / 2;
11+
retVal.moveTo(0, y);
12+
const prevPt = { x: 0, y };
13+
for (let i = 0; i < width; i += width / steps) {
14+
// increase y by a random amount between -10 and 10
15+
y += Math.random() * 30 - 15;
16+
y = Math.max(height * 0.2, Math.min(y, height * 0.7));
17+
18+
if (round && i > 0) {
19+
const xMid = (prevPt.x + i) / 2;
20+
const yMid = (prevPt!.y + y) / 2;
21+
retVal.quadTo(prevPt.x, prevPt.y, xMid, yMid);
22+
prevPt.x = i;
23+
prevPt.y = y;
24+
} else {
25+
retVal.lineTo(i, y);
26+
}
27+
}
28+
return retVal;
29+
};
30+
31+
export const createZeroPath = (
32+
width: number,
33+
height: number,
34+
steps: number
35+
) => {
36+
const retVal = Skia.Path.Make();
37+
const y = height / 2;
38+
retVal.moveTo(0, y);
39+
for (let i = 0; i < width; i += width / steps) {
40+
retVal.lineTo(i, y);
41+
}
42+
return retVal;
43+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from "react";
2+
import { View, StyleSheet, useWindowDimensions } from "react-native";
3+
4+
import { Interpolation } from "./Interpolation";
5+
import { MountAnimation } from "./Mount";
6+
import { Slider } from "./Slider";
7+
8+
const Padding = 10;
9+
10+
export const GraphsScreen: React.FC = () => {
11+
const { width, height } = useWindowDimensions();
12+
return (
13+
<View style={styles.container}>
14+
<MountAnimation height={height * 0.25} width={width - Padding * 2} />
15+
<Interpolation height={height * 0.25} width={width - Padding * 2} />
16+
<Slider height={height * 0.25} width={width - Padding * 2} />
17+
</View>
18+
);
19+
};
20+
21+
const styles = StyleSheet.create({
22+
container: {
23+
flex: 1,
24+
padding: Padding,
25+
},
26+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type GraphProps = {
2+
height: number;
3+
width: number;
4+
};

example/src/Examples/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from "./Drawing";
55
export * from "./Filters";
66
export * from "./Gooey";
77
export * from "./Matrix";
8+
export * from "./Graphs";

example/src/Home/HomeScreen.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ export const HomeScreen: React.FC = () => {
3737
description="Use touches to draw with Skia"
3838
route="Drawing"
3939
/>
40+
<HomeScreenButton
41+
title="📉 Graphs"
42+
description="Animated graphs with Skia"
43+
route="Graphs"
44+
/>
4045
<HomeScreenButton
4146
title="Animation"
4247
description="Animated with Skia"

package/cpp/api/JsiSkPath.h

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,20 @@ class JsiSkPath : public JsiSkWrappingSharedPtrHostObject<SkPath> {
460460
return jsi::Value(false);
461461
}
462462

463+
JSI_HOST_FUNCTION(isInterpolatable) {
464+
auto path2 = JsiSkPath::fromValue(runtime, arguments[0]);
465+
return getObject()->isInterpolatable(*path2);
466+
}
467+
468+
JSI_HOST_FUNCTION(interpolate) {
469+
auto path2 = JsiSkPath::fromValue(runtime, arguments[0]);
470+
auto weight = arguments[1].asNumber();
471+
SkPath result;
472+
getObject()->interpolate(*path2, weight, &result);
473+
return jsi::Object::createFromHostObject(
474+
runtime, std::make_shared<JsiSkPath>(getContext(), result));
475+
}
476+
463477
JSI_EXPORT_FUNCTIONS(
464478
JSI_EXPORT_FUNC(JsiSkPath, addArc), JSI_EXPORT_FUNC(JsiSkPath, addOval),
465479
JSI_EXPORT_FUNC(JsiSkPath, addPoly), JSI_EXPORT_FUNC(JsiSkPath, addRect),
@@ -491,7 +505,8 @@ class JsiSkPath : public JsiSkWrappingSharedPtrHostObject<SkPath> {
491505
JSI_EXPORT_FUNC(JsiSkPath, getLastPt), JSI_EXPORT_FUNC(JsiSkPath, close),
492506
JSI_EXPORT_FUNC(JsiSkPath, simplify),
493507
JSI_EXPORT_FUNC(JsiSkPath, countPoints), JSI_EXPORT_FUNC(JsiSkPath, copy),
494-
JSI_EXPORT_FUNC(JsiSkPath, fromText), JSI_EXPORT_FUNC(JsiSkPath, op))
508+
JSI_EXPORT_FUNC(JsiSkPath, fromText), JSI_EXPORT_FUNC(JsiSkPath, op),
509+
JSI_EXPORT_FUNC(JsiSkPath, isInterpolatable), JSI_EXPORT_FUNC(JsiSkPath, interpolate))
495510

496511
JsiSkPath(std::shared_ptr<RNSkPlatformContext> context, SkPath path)
497512
: JsiSkWrappingSharedPtrHostObject<SkPath>(

0 commit comments

Comments
 (0)