Unable to replicate an old example with useDerivedValue() instead of useComputerValue()
Description
My expo project returns "leftPath is not a function (It is Object)" error when i replicated season5/src/Headspace/Play.tsx
Version
1.2.3
Steps to reproduce
import React from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import Slider from '@react-native-community/slider';
import type { SkPath } from '@shopify/react-native-skia';
import { Canvas, Path, Skia } from '@shopify/react-native-skia';
import { useDerivedValue, useSharedValue } from 'react-native-reanimated';
import { interpolate } from 'flubber';
const { width } = Dimensions.get("window");
const Flubber2SkiaInterpolator = (from: SkPath, to: SkPath) => {
const interpolator = interpolate(from.toSVGString(), to.toSVGString());
const d = 1e-3;
const i0 = Skia.Path.MakeFromSVGString(interpolator(d))!;
const i1 = Skia.Path.MakeFromSVGString(interpolator(1 - d))!;
return (t: number) => {
if (t < d) {
return from;
}
if (1 - t < d) {
return to;
}
return i1.interpolate(i0, t)!;
};
};
const playLeft = Skia.Path.MakeFromSVGString(
"M51 23V33.3V43.6V64.1V84.7V105.2L73.3 89.9L95.5 74.5C97.2 73.3 98.6 71.8 99.6 70C100.5 68.2 101 66.2 101 64.1C101 62.1 100.5 60 99.6 58.2C98.6 56.4 97.2 54.9 95.5 53.7L73.3 38.4L51 23Z"
)!;
const pauseLeft = Skia.Path.MakeFromSVGString(
"M84.7 1C80.2 1 76 2.8 72.9 5.9C69.8 9 68 13.2 68 17.7V106.6C68 111 69.8 115.2 72.9 118.3C76 121.5 80.2 123.2 84.7 123.2C89.1 123.2 93.3 121.5 96.5 118.3C99.6 115.2 101.3 111 101.3 106.6V17.7C101.3 13.2 99.6 9 96.5 5.9C93.3 2.8 89.1 1 84.7 1Z"
)!;
const leftPath = Flubber2SkiaInterpolator(playLeft, pauseLeft);
const PathInterpolated = () => {
const sliderSharedValue = useSharedValue(0);
const path = useDerivedValue(() => leftPath(sliderSharedValue.value));
return (
<SafeAreaView style={styles.container}>
<Canvas style={styles.canvas}>
<Path
path={path}
color="white"
style="stroke"
strokeWidth={4}
/>
</Canvas>
<View style={styles.content}>
<Slider
style={styles.slider}
minimumValue={0}
maximumValue={1}
value={sliderSharedValue.value}
onValueChange={(value) => {
sliderSharedValue.value = value
}}
minimumTrackTintColor='grey'
maximumTrackTintColor='grey'
thumbTintColor='white'
/>
</View>
</SafeAreaView>
);
};
export default PathInterpolated;
In season5/src/Headspace/Play.tsx, useComputeValue is being used which is now preceded by useDerivedValue.
But using the above code on my expo managed project returns "leftPath is not a function (It is Object)" error after the first call (which returns from);
Tried to reproduce this in a bare react-native by prebuilding, but was lacking knowledge on worklets for getting it to work.
Snack, code example, screenshot, or link to a repository
leftPath isn't a worklet function here. please check docs: https://docs.swmansion.com/react-native-reanimated/docs/guides/worklets
I'm closing this issue as there doesn't seem to be a Skia bug here
@uxfuture Hi, did you manage to make it work?
Is there any up to date example for this?
Negative.
I'm closing this issue as there doesn't seem to be a Skia bug here
In the docs this example is referenced, but this example isn't reproducable anymore right? Because Flubber runs on the JS thread and we can't get it to work on the UI thread if I'm not misstaken? Maybe we should remove the suggestion to use Flubber.
https://shopify.github.io/react-native-skia/docs/animations/hooks/#usepathinterpolation
Oups I see what you mean. Indeed the example with Flubber was not using Reanimated but back then I remembered to have tried it. You make the path decomposition (with Flubber) on the JS thread but then you can path the data on the UI thread and use reanimated to do it. I think if you search the github issues/discussions you might find a reference to it.
On Fri, Feb 7, 2025 at 1:21 PM Brandon Eichhorn @.***> wrote:
I'm closing this issue as there doesn't seem to be a Skia bug here
In the docs this example is referenced, but this example isn't reproducable anymore right? Because Flubber runs on the JS thread and we can't get it to work on the UI thread if I'm not misstaken? Maybe we should remove the suggestion to use Flubber.
— Reply to this email directly, view it on GitHub or unsubscribe. You are receiving this email because you commented on the thread.
Triage notifications on the go with GitHub Mobile for iOS or Android.
Oups I see what you mean. Indeed the example with Flubber was not using Reanimated but back then I remembered to have tried it. You make the path decomposition (with Flubber) on the JS thread but then you can path the data on the UI thread and use reanimated to do it. I think if you search the github issues/discussions you might find a reference to it. …
On Fri, Feb 7, 2025 at 1:21 PM Brandon Eichhorn @.***> wrote:
I'm closing this issue as there doesn't seem to be a Skia bug here
In the docs this example is referenced, but this example isn't reproducable anymore right? Because Flubber runs on the JS thread and we can't get it to work on the UI thread if I'm not misstaken? Maybe we should remove the suggestion to use Flubber.
— Reply to this email directly, view it on GitHub or unsubscribe. You are receiving this email because you commented on the thread.
Triage notifications on the go with GitHub Mobile for iOS or Android.
I have been trying this, let me try your approach and also try search for that reference and we'll have a example for the docs. 🙏
Oups I see what you mean. Indeed the example with Flubber was not using Reanimated but back then I remembered to have tried it. You make the path decomposition (with Flubber) on the JS thread but then you can path the data on the UI thread and use reanimated to do it. I think if you search the github issues/discussions you might find a reference to it. …
I think this might be what you were intending.
https://github.com/interhub/react-native-flubber
Hope this helps someone.
Oups I see what you mean. Indeed the example with Flubber was not using Reanimated but back then I remembered to have tried it. You make the path decomposition (with Flubber) on the JS thread but then you can path the data on the UI thread and use reanimated to do it. I think if you search the github issues/discussions you might find a reference to it. …
I think this might be what you were intending.
https://github.com/interhub/react-native-flubber
Hope this helps someone.
Looks like their implementation works on the JS thread, i think it would be better if the processing is working on the UI thread. But for that we have to copy every function and turn them into worklets, right?
Oups I see what you mean. Indeed the example with Flubber was not using Reanimated but back then I remembered to have tried it. You make the path decomposition (with Flubber) on the JS thread but then you can path the data on the UI thread and use reanimated to do it. I think if you search the github issues/discussions you might find a reference to it. …
I think this might be what you were intending. https://github.com/interhub/react-native-flubber Hope this helps someone.
Looks like their implementation works on the JS thread, i think it would be better if the processing is working on the UI thread. But for that we have to copy every function and turn them into worklets, right?
Yes exactly, we would have to clone everything I think and turn them into worklets. But @wcandillon suggested to precompute it on the JS thread I think?
import { useMemo } from 'react';
import { SharedValue, useDerivedValue, useSharedValue } from 'react-native-reanimated';
import { interpolate as flubberInterpolate } from 'flubber2';
import { Skia, type SkPath } from '@shopify/react-native-skia';
interface UseFlubberInterpolationProps {
progress: SharedValue<number>;
progressPoints: number[];
paths: string[];
}
const useFlubberInterpolation = ({
progress,
progressPoints,
paths,
}: UseFlubberInterpolationProps): SharedValue<SkPath> => {
const numFramesPerSegment = 150;
const sortedProgressPoints = useMemo(() => {
return [...progressPoints].sort((a, b) => a - b);
}, [progressPoints]);
const precomputedPaths = useMemo(() => {
const allPaths: SkPath[][] = [];
for (let i = 0; i < paths.length - 1; i++) {
const startPath = paths[i];
const endPath = paths[i + 1];
const interpolator = flubberInterpolate(startPath, endPath, { maxSegmentLength: 8 });
const segmentPaths: SkPath[] = [];
for (let j = 0; j <= numFramesPerSegment; j++) {
const t = j / numFramesPerSegment;
const pathString = interpolator(t);
const path = Skia.Path.MakeFromSVGString(pathString);
if (path) segmentPaths.push(path);
}
allPaths.push(segmentPaths);
}
return allPaths;
}, [paths]);
const currentPath = useSharedValue(precomputedPaths[0][0]);
useDerivedValue(() => {
const totalSegments = sortedProgressPoints.length - 1;
if (totalSegments <= 0 || !precomputedPaths.length) {
currentPath.value = precomputedPaths[0][0];
return;
}
if (progress.value <= sortedProgressPoints[0]) {
currentPath.value = precomputedPaths[0][0];
return;
}
if (progress.value >= sortedProgressPoints[totalSegments]) {
currentPath.value = precomputedPaths[totalSegments - 1][numFramesPerSegment];
return;
}
let segmentIndex = 0;
while (
segmentIndex < totalSegments - 1 &&
progress.value >= sortedProgressPoints[segmentIndex + 1]
) {
segmentIndex++;
}
const segmentStart = sortedProgressPoints[segmentIndex];
const segmentEnd = sortedProgressPoints[segmentIndex + 1];
const segmentProgress = (progress.value - segmentStart) / (segmentEnd - segmentStart);
const clampedProgress = Math.max(0, Math.min(1, segmentProgress));
const frameIndex = Math.round(clampedProgress * numFramesPerSegment);
currentPath.value = precomputedPaths[segmentIndex][frameIndex];
}, [progress, sortedProgressPoints, precomputedPaths]);
return currentPath;
};
export default useFlubberInterpolation;
You can just use it like the regular usePathInterpolation hook, but instead of using the Skia API, we just use raw SVGs directly.
const currentPath = useFlubberInterpolation({
progress,
progressPoints: [0, 1],
paths: [normalizedStart, normalizedEnd],
});
This has worked absolutely amazingly for me. You can also control the maxSegmentLength in the hook to impact performance/quality:
const interpolator = flubberInterpolate(startPath, endPath, { maxSegmentLength: 8 }); the higher, the less the performance.
And this also works better than the interhub version because interhub wouldn't allow me to leverage the Reanimated higher-order functions.
@wcandillon Thankyou for the idea.
Hope this helps anyone and any feedback is appreciated! Have been working with Reanimated for a short time so still learning.