wcandillon / react-native-redash

The React Native Reanimated and Gesture Handler Toolbelt
https://wcandillon.gitbook.io/redash/
MIT License
1.99k stars 117 forks source link

AnimatedPath #112

Open ShaMan123 opened 5 years ago

ShaMan123 commented 5 years ago

There's no reference of AnimatedPath in the docs. Is it just Animated.createAnimatedComponent(Path)?? I can't get it working.

sebqq commented 5 years ago

Here is code that I use in my projects. It will draw the path by params you set.

import React from "react";
import Animated, { Easing } from "react-native-reanimated";
import { timing, parsePath, delay } from "react-native-redash";
import { Path } from "react-native-svg";
import { useMemoOne } from "use-memo-one";

const { Value, Clock, clockRunning, cond, useCode, block, not, set } = Animated;
const AnimatedPath = Animated.createAnimatedComponent(Path);
const DEFAULT_DISPLAY_DELAY = 350;

function runTiming(value, clock, from, to, duration, easing) {
  return set(value, timing({ clock, duration, from, to, easing }));
}

const AnimatedSvgPath = ({
  d,
  fill,
  stroke,
  strokeWidth,
  durationMs,
  delayMs,
  easing
}) => {
  const length = useMemoOne(() => parsePath(d).totalLength, [d]);

  const [strokeDashoffset, clock, active, delayed] = useMemoOne(
    () => [new Value(length), new Clock(), new Value(0), new Value(0)],
    [length]
  );

  useCode(
    block([
      cond(
        not(delayed),
        delay(
          [set(active, 1), set(delayed, 1)],
          delayMs + DEFAULT_DISPLAY_DELAY
        )
      ),
      cond(active, [
        runTiming(strokeDashoffset, clock, length, 0, durationMs, easing),
        cond(not(clockRunning(clock)), set(active, 0))
      ])
    ]),
    []
  );

  return (
    <AnimatedPath
      d={d}
      strokeDasharray={[length, length]}
      strokeDashoffset={strokeDashoffset}
      fill={fill}
      stroke={stroke}
      strokeWidth={strokeWidth}
    />
  );
};
ShaMan123 commented 5 years ago

Awesome. Question: can d be an Animated.Value? That's what I'm looking for.

wcandillon commented 5 years ago

Absolutely. And starting with Reanimated 1.3.0, d as an animated values work on both ios and android.

ShaMan123 commented 5 years ago

looking into it!

ShaMan123 commented 5 years ago

The following snippet doesn't work. It fails when it tries to parse the path using parse-svg-path. I'm guessing I should avoid parsePath...?

const d1="M25 10 L98 65 L70 25 L16 77 L11 30 L0 4 L90 50 L50 10 L11 22 L77 95 L20 25";

return (
    <Svg style={{ flex: 1 }}>
        <AnimatedSvgPath
            d={concat(d1, "")}
            stroke="yellow"
            strokeWidth={2}
            delayMs={2000}
            durationMs={500}
            easing={Easing.linear}
            fill="none"
        />
    </Svg>
);
sebqq commented 5 years ago

hello @ShaMan123

I also had some problems with redash's parsePath length getting function.

When I use package called svg-path-properties especially:

import { svgPathProperties } from 'svg-path-properties';

...

const length = useMemoOne(() => svgPathProperties(d).getTotalLength(), [d]);

to get length, everything worked without any problem. Anyway, for my use-case I'm fine with redash length calculation.

Maybe @wcandillon have any suggestions where the problem can be?

ShaMan123 commented 5 years ago

I tried this:


    const d = useValue("M0 0");
    const counter = useValue(0);
    const length = useValue(0);
    const lastX = useValue(0);
    const lastY = useValue(0);

    return (
        <PanGestureHandler
            onGestureEvent={event<PanGestureHandlerGestureEvent>([{
                nativeEvent: ({ x, y, state }) => block([
                    cond(eq(state, State.ACTIVE), [
                        set(d, concat(d, " L", x, " ", y)),
                        set(counter, add(counter, 1)),
                        set(length, add(length, hypot(sub(x, lastX), sub(y, lastY)))),
                        set(lastX, x),
                        set(lastY, y),
                    ])

                ])
            }])}
        >
            <View collapsable={false} style={{ flex: 1 }}>
                <Svg style={{ flex: 1 }}>
                    <AnimatedSvgPath
                        d={d}
                        length={length}
                        stroke="yellow"
                        strokeWidth={2}
                        delayMs={2000}
                        durationMs={500}
                        easing={Easing.linear}
                        fill="none"
                    />
                </Svg>
            </View>
        </PanGestureHandler>
    )

function hypot(...values: Animated.Adaptable<number>[]) {
    return sqrt(_.reduce<Animated.Adaptable<number>, Animated.Adaptable<number>>(values, (acc, curr) => add(acc, pow(curr, 2)), 0));
}

function useValue(value) {
    return useMemo(() => new Value(value), [value]);
}

Revised your code:


const AnimatedSvgPath = ({
  d,
+ length,
  fill,
  stroke,
  strokeWidth,
  durationMs,
  delayMs,
  easing
}) => {
-  const length = useMemoOne(() => parsePath(d).totalLength, [d]);
...

The problem with this solution seems to be the prop strokeDasharray={[length, length]}, because length is an Animated.Node.

ShaMan123 commented 5 years ago

Revising as described below and above allows animating d in response to PanGestureHandler. It has caveats related to Reanimated 1.3.0

return (
    <AnimatedPath
      d={d}
-      strokeDasharray={[length, length]}
-      strokeDashoffset={strokeDashoffset}
      fill={fill}
      stroke={stroke}
      strokeWidth={strokeWidth}
    />
  );