As shown in the figureExample image, how to make the red ball move along the black path? And it can apply animation effects such as React Native Reanimated withSpring.
In React Native Skia, I try to do things like this:
const progress = useSharedValue(0);
const geo = new PathGeometry(path);
const totalLength = geo.getTotalLength();
const transform = useDerivedValue(() => {
const currentLen = interpolate(progress.value, [0, 1], [0, totalLength]);
const pos = geo.getPointAtLength(currentLen);
return translate({x:pos.x,y:pos.y});
});
const handlePress = () => {
progress.value = withSpring(1);
};
But received an error message:
ReanimatedError: [Reanimated] Trying to access property getPointAtLength of an object which cannot be sent to the UI runtime. js engine: reanimated
Later, I used:
const geo = new PathGeometry(path);
const totalLength = geo.getTotalLength();
const pos = geo.getPointAtLength(0);
const posX = useSharedValue(pos.x);
const posY = useSharedValue(pos.y);
let i = 0;
setInterval(() => {
const nPos = geo.getPointAtLength(i);
posX.value = nPos.x;
posY.value = nPos.y;
i++;
}, 16);
It can work stiffly, but I know this is not a good solution because we cannot apply animation effects such as React Native Reanimated withSpring
May I know how to implement it? I am a beginner
Finally succeeded, thanks to @PhantomSpooks for discussion and help! Here is the successful js code, which includes an example of a curved path. You can switch pathStr to see the effect.
import { Canvas, Circle, Path, SkPath, Skia } from '@shopify/react-native-skia';
import { useCallback } from 'react';
import { Button, StyleSheet, useWindowDimensions } from 'react-native';
import {
cancelAnimation,
interpolate,
useDerivedValue,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
class PathGeometry {
totalLength = 0;
contour: SkContourMeasure;
constructor(path: SkPath, resScale = 1) {
const it = Skia.ContourMeasureIter(path, false, resScale);
const contour = it.next();
this.totalLength = contour.length();
this.contour = contour;
}
getTotalLength() {
return this.totalLength;
}
getPointAtLength(length) {
const [pos] = this.contour.getPosTan(length);
return pos;
}
}
const pathStr =
'M 128 0 L 168 80 L 256 93 L 192 155 L 207 244 L 128 202 L 49 244 L 64 155 L 0 93 L 88 80 L 128 0 Z';
// curve path
//const pathStr = 'M1 73C1 73 44 9 94 14C144 19 173 83 209 73C245 63 237 1 237 1';
const path = Skia.Path.MakeFromSVGString(pathStr);
const geo = new PathGeometry(path);
const totalLength = Math.floor(geo.getTotalLength());
const getPoints = (geo) => {
let points = [];
for (let i = 0; i < totalLength; i++) {
let pos = geo.getPointAtLength(i);
points.push({ x: pos.x, y: pos.y });
}
return points;
};
const points = getPoints(geo);
const SkiaRoot = () => {
const { width, height } = useWindowDimensions();
const progress = useSharedValue(0);
const currPoint = useDerivedValue(() => {
const interpolatedIndex = interpolate(
progress.value,
[0, 1],
[0, totalLength - 1]
);
const index = Math.floor(interpolatedIndex);
return points[index];
});
const cx = useDerivedValue(() => currPoint.value.x);
const cy = useDerivedValue(() => currPoint.value.y);
const moveCircle = useCallback(() => {
progress.value = withTiming(progress.value == 1 ? 0 : 1, {
duration: 2000,
});
}, [width, height]);
const stopAnimation = useCallback(() => {
'worklet';
cancelAnimation(progress);
}, []);
return (
<SafeAreaProvider>
<SafeAreaView style={styles.container}>
<Button title="Move" onPress={moveCircle} />
<Button title="Stop" onPress={stopAnimation} />
<Canvas style={{ width, height }}>
<Path
path={path}
color="lightblue"
style="stroke"
strokeJoin="round"
strokeWidth={2}
/>
<Circle cx={cx} cy={cy} r={16} color="red" />
</Canvas>
</SafeAreaView>
</SafeAreaProvider>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
margin: 5,
},
});
export default SkiaRoot;