react-native-skia icon indicating copy to clipboard operation
react-native-skia copied to clipboard

Unable to replicate an old example with useDerivedValue() instead of useComputerValue()

Open ajstun opened this issue 1 year ago • 1 comments

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

Image

ajstun avatar Sep 06 '24 07:09 ajstun

leftPath isn't a worklet function here. please check docs: https://docs.swmansion.com/react-native-reanimated/docs/guides/worklets

mrEuler avatar Sep 18 '24 21:09 mrEuler

I'm closing this issue as there doesn't seem to be a Skia bug here

wcandillon avatar Dec 04 '24 12:12 wcandillon

@uxfuture Hi, did you manage to make it work?

Miigaarino avatar Jan 22 '25 10:01 Miigaarino

Is there any up to date example for this?

eduance avatar Feb 04 '25 12:02 eduance

Negative.

ajstun avatar Feb 04 '25 12:02 ajstun

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.

Image

https://shopify.github.io/react-native-skia/docs/animations/hooks/#usepathinterpolation

eduance avatar Feb 07 '25 12:02 eduance

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.

wcandillon avatar Feb 07 '25 12:02 wcandillon

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. 🙏

eduance avatar Feb 07 '25 12:02 eduance

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.

eduance avatar Feb 07 '25 15:02 eduance

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?

Miigaarino avatar Feb 12 '25 04:02 Miigaarino

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?

eduance avatar Feb 17 '25 08:02 eduance

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.

Image

Hope this helps anyone and any feedback is appreciated! Have been working with Reanimated for a short time so still learning.

eduance avatar Feb 24 '25 20:02 eduance