Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animated Path d attribute #951

Closed
francois-pasquier opened this issue Feb 22, 2019 · 23 comments
Closed

Animated Path d attribute #951

francois-pasquier opened this issue Feb 22, 2019 · 23 comments

Comments

@francois-pasquier
Copy link

Hi, I stumbled onto this issue regarding my problem: #908
In my case, I would prefer to use the react native animated library and not react-native-reanimated.
I woule like to interpolate on multiple points from an animated.Value() and forge my d attribute from it.

Is it supported ?

Maybe is there a better way, I want to have a 'rising' animation.
so a path starting at y 0 and rising progressively.

@msand
Copy link
Collaborator

msand commented Feb 25, 2019

If you're fine with using Animated without useNativeDriver, then it should work as is. If you want the native driven animation, you'll need to use a PR I made but didn't get merged yet/hasn't been rebased to the latest version of react-native: facebook/react-native#18187
If you'd be available to work on that PR it would be greatly appreciated!

@msand
Copy link
Collaborator

msand commented Feb 25, 2019

I just tried rebasing to the latest 0.59 release candidate and it seems to work fine in both iOS and Android.

You can try it using: yarn add react-native@msand/react-native#f462f97

Commit:
facebook/react-native@f462f97

Branch:
https://github.com/msand/react-native/tree/native-animated-string-interpolation-RN059

Diff to react-native 0.59-stable (if you want to rebase/apply it yourself to e.g. 0.58, or adapt it to the master branch):
facebook/react-native@0.59-stable...msand:native-animated-string-interpolation-RN059

And e.g. this:

import React, { Component } from 'react';
import { StyleSheet, View, Dimensions, Animated } from 'react-native';
import { Svg, Rect, Path } from 'react-native-svg';

const { width, height } = Dimensions.get('window');
const AnimatedRect = Animated.createAnimatedComponent(Rect);
const AnimatedPath = Animated.createAnimatedComponent(Path);

function getInitialState() {
  const anim = new Animated.Value(0);
  const fillOpacity = anim.interpolate({
    inputRange: [0, 1],
    outputRange: [0, 1],
  });
  const offset = fillOpacity.interpolate({
    inputRange: [0, 1],
    outputRange: [0, 10],
  });
  const strokeOpacity = offset.interpolate({
    inputRange: [0, 10],
    outputRange: [0, 1],
    extrapolateRight: 'clamp',
  });
  const path = anim.interpolate({
    inputRange: [0, 1],
    outputRange: ['M20,20L20,80L80,80L80,20Z', 'M40,40L33,60L60,60L65,40Z'],
  });
  const fill = strokeOpacity.interpolate({
    inputRange: [0, 1],
    outputRange: ['rgba(255, 0, 0, 0.5)', 'rgba(0, 255, 0, 0.99)'],
  });
  const oneToFivePx = offset.interpolate({
    inputRange: [0, 10],
    outputRange: ['1px', '5px'],
  });
  return { anim, fillOpacity, offset, strokeOpacity, path, fill, oneToFivePx };
}

export default class App extends Component {
  state = getInitialState();

  componentDidMount() {
    const { anim } = this.state;
    Animated.timing(anim, {
      toValue: 1,
      duration: 10000,
      useNativeDriver: true,
    }).start();
  }

  render() {
    const { fillOpacity, offset, strokeOpacity, path, fill, oneToFivePx } = this.state;
    return (
      <View style={styles.container}>
        <Svg width={width} height={height} viewBox="0 0 100 100">
          <AnimatedRect
            x="5"
            y="5"
            width="90"
            height="90"
            stroke="blue"
            fill={fill}
            strokeDasharray="1 1"
            strokeWidth={oneToFivePx}
            strokeDashoffset={offset}
            strokeOpacity={strokeOpacity}
            fillOpacity={fillOpacity}
          />
          <AnimatedPath
            d={path}
            stroke="blue"
          />
        </Svg>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#ecf0f1',
  },
});

@yodaheis
Copy link

yodaheis commented Apr 12, 2019

Will passing an Animated value to an 'arc' from 'd3-shape' and using that as a d property for Path, work with nativeDriver: true?

@msand
Copy link
Collaborator

msand commented Apr 13, 2019

You can use arc from d3-shape to create path data strings, and use Animated.Value to interpolate between them. But, animated doesn't support scientific notation of numbers represented as strings, so you'll need to convert to fixed notation using something like this, or make a pr to react-native

import React, { Component } from 'react';
import { StyleSheet, View, Animated } from 'react-native';
import { Svg, Circle, Path, G } from 'react-native-svg';
import { arc } from 'd3-shape';

const AnimatedPath = Animated.createAnimatedComponent(Path);

function convertToFixed(m, integer, decimal = '', sign, power) {
  const fixed = integer + decimal;
  return sign === '+'
    ? fixed + '0'.repeat(power - decimal.length)
    : '0.' + '0'.repeat(power - 1) + fixed;
}

function exponentialToFixedNotation(num) {
  return num.replace(/(\d)(?:\.(\d+))?e([+-])(\d+)/, convertToFixed);
}

function getInitialState() {
  const anim = new Animated.Value(0);
  const arcGenerator = arc();
  const range = [
    arcGenerator({
      innerRadius: 0,
      outerRadius: 10,
      startAngle: 0,
      endAngle: Math.PI / 2,
    }), // "M6.123233995736766e-16,-10A10,10,0,0,1,10,0L0,0Z"
    arcGenerator({
      innerRadius: 0,
      outerRadius: 100,
      startAngle: 0,
      endAngle: Math.PI / 2,
    }), // "M6.123233995736766e-15,-100A100,100,0,0,1,100,0L0,0Z"
  ];
  const outputRange = range.map(exponentialToFixedNotation);
  //  ["M0.0000000000000006123233995736766,-10A10,10,0,0,1,10,0L0,0Z", "M0.000000000000006123233995736766,-100A100,100,0,0,1,100,0L0,0Z"]
  const path = anim.interpolate({
    inputRange: [0, 1],
    outputRange,
  });
  return { anim, path };
}

export default class App extends Component {
  state = getInitialState();

  componentDidMount() {
    const { anim } = this.state;
    Animated.timing(anim, {
      toValue: 1,
      duration: 3000,
    }).start();
  }

  render() {
    const { path } = this.state;
    return (
      <View style={styles.container}>
        <Svg
          width="50%"
          height="50%"
          viewBox="0 0 200 200"
          style={{ borderWidth: 1, borderColor: 'black', borderStyle: 'solid' }}
        >
          <G transform="translate(100, 100)">
            <Circle r="100" fill="none" stroke="black" />
            <AnimatedPath d={path} stroke="blue" />
          </G>
        </Svg>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#ecf0f1',
    justifyContent: 'center',
    alignItems: 'center',
    flex: 1,
  },
});

Screenshot_1555173614

@yodaheis
Copy link

Thanks for the prompt reply.
The example you provided doesn't work with useNativeDriver: true.
Although, with useNativeDriver: false, even passing animated values in place of numbers works if you just wrap the Path component with createAnimatedComponent like so:

import React, { Component } from 'react';
import { StyleSheet, View, Animated } from 'react-native';
import { Svg, Circle, Path, G } from 'react-native-svg';
import { arc } from 'd3-shape';

const AnimatedArc = Animated.createAnimatedComponent(Arc);
const AnimatedPath = Animated.createAnimatedComponent(Path);


class Arc extends Component {
  render() {
    const {outerRadius} = this.props
    const path = arc()
      .innerRadius(0)
      .outerRadius(outerRadius)
      .startAngle(0)
      .endAngle(Math.PI / 2)
    return <Path stroke={'blue'} d={path}/>
  }
}

export default class App extends Component {
  state = {anim: new Animated.Value(0)}
  componentDidMount() {
    const { anim } = this.state;
    Animated.timing(anim, {
      toValue: 1,
      duration: 3000
    }).start();
  }

  render() {
    const {anim} = this.state
    return (
      <View style={styles.container}>
        <Svg
          width="50%"
          height="50%"
          viewBox="0 0 200 200"
          style={{ borderWidth: 1, borderColor: 'black', borderStyle: 'solid' }}
        >
          <G transform="translate(100, 100)">
            <Circle r="100" fill="none" stroke="black" />
            <AnimatedArc outerRadius={anim.interpolate({inputRange: [0, 1], outputRange: [10, 100]})} />
          </G>
        </Svg>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#ecf0f1',
    justifyContent: 'center',
    alignItems: 'center',
    flex: 1,
  },
});

I'm hoping that it works with your PR for string interpolation for d attribute is merged. I will test it out once I upgrade to RN 0.59 as currently I'm using 0.57.

P.S. I'm a fan of your work and this library. Keep it up!

@matpaul
Copy link

matpaul commented Apr 15, 2019

@msand just check your example with https://github.com/facebook/react-native/pull/24177/files
something goes wrong when nativeDriver: true
Снимок экрана 2019-04-15 в 17 11 32

@matpaul
Copy link

matpaul commented Apr 15, 2019

@msand i found issue (check only ios impletementation)
https://github.com/facebook/react-native/pull/24177/files#diff-91c488a182920b9c215c4ae83fae27dfR154

str = [NSString stringWithFormat:@"%1f", val];

we get something like:
...1.000000,10.000000,0.000000L0.000000,0.000000Z

fix:

NSNumber *numberValue = [NSNumber numberWithDouble:val];
str = [numberValue stringValue];

and it's work! =)

@msand
Copy link
Collaborator

msand commented Apr 15, 2019

@matpaul Thanks for the fix! There was also an issue on android. I've pushed a fix to my fork/pr: facebook/react-native@86052af

@msand
Copy link
Collaborator

msand commented Apr 16, 2019

@yodaheis I've added support for scientific notation to the string interpolation now, so the exponentialToFixedNotation function isn't needed anymore. facebook/react-native@19bba43
facebook/react-native#24177

@matpaul
Copy link

matpaul commented Apr 18, 2019

@msand testing some animation in my project - there is an issue when two path is not consistent between, for example
start path: 'M-92.57789989280825,-82.49443891219077A124,124,0,0,1,41.38463306159251,-116.89017129920438L41.38463306159257,-69.62982224849738A81,81,0,0,0,-69.81005309830067,-41.07987933785153Z'

end path: 'M-56.92694075333276,-106.64149118701941A4,4,0,0,1,-55.18772110863358,-112.15754740023375A125,125,0,0,1,-5.670941874406595,-124.87129541354611A4,4,0,0,1,-1.489471734425579,-120.87541396031263L-1.489471734425572,-84.82255419448853A4,4,0,0,1,-5.231143652805544,-80.83090458533613A81,81,0,0,0,-34.3564991377283,-73.35278431661115A4,4,0,0,1,-39.558343099273074,-75.04812933954311Z'

Error occure: invalid pattern

I think about function that take two path and create 0 for all missing points

What do you think?

@matpaul
Copy link

matpaul commented Apr 30, 2019

@msand found another issue when animate d using native driver, and then delete it - fatal issue
Снимок экрана 2019-04-30 в 17 12 38

@msand
Copy link
Collaborator

msand commented Apr 30, 2019

Can you post a minimal reproduction?

@matpaul
Copy link

matpaul commented Apr 30, 2019

@msand

import { arc } from 'd3-shape';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { Animated, Button, StyleSheet, View } from 'react-native';
import { Circle, G, Path, Svg } from 'react-native-svg';

const AnimatedPath = Animated.createAnimatedComponent(Path);

const arcGenerator = arc();

function ReproduceIssue() {
  const [isShown, setShown] = useState(true);

  const animatedValue = useMemo(() => new Animated.Value(0), []);

  const [startPath, endPath] = useMemo(
    () => [
      arcGenerator({
        innerRadius: 0,
        outerRadius: 10,
        startAngle: 0,
        endAngle: Math.PI / 2,
      }),
      arcGenerator({
        innerRadius: 0,
        outerRadius: 100,
        startAngle: 0,
        endAngle: Math.PI / 2,
      }),
    ],
    []
  );

  useEffect(() => {
    const loop = Animated.loop(
      Animated.sequence([
        Animated.timing(animatedValue, {
          toValue: 1,
          duration: 3000,
          useNativeDriver: true,
        }),
        Animated.timing(animatedValue, {
          toValue: 0,
          duration: 3000,
          useNativeDriver: true,
        }),
      ])
    );

    loop.start();

    return () => loop.stop();
  }, []);

  return (
    <View style={styles.container}>
      <Button
        title={'Hide/Show'}
        onPress={useCallback(() => setShown(!isShown), [isShown])}
      />
      {isShown && (
        <Svg
          width="50%"
          height="50%"
          viewBox="0 0 200 200"
          style={styles.svgContainer}
        >
          <G transform="translate(100, 100)">
            <Circle r="100" fill="none" stroke="black" />
            <AnimatedPath
              d={animatedValue.interpolate({
                inputRange: [0, 1],
                outputRange: [startPath, endPath],
              })}
              stroke="blue"
            />
          </G>
        </Svg>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#ecf0f1',
    justifyContent: 'center',
    alignItems: 'center',
    width: 500,
    height: 500,
  },
  svgContainer: { borderWidth: 1, borderColor: 'black', borderStyle: 'solid' },
});

export default memo(ReproduceIssue);

@matpaul
Copy link

matpaul commented Apr 30, 2019

@msand forgot to say, this is only on Android! - on iOS everything is ok

@matpaul
Copy link

matpaul commented May 2, 2019

@msand Hi!, I try to research this issue, so i found that restoreDefaultValues method make d: null

PropsAnimatedNode.java

 public void restoreDefaultValues() {
    ReadableMapKeySetIterator it = mPropMap.keySetIterator();
    while(it.hasNextKey()) {
       mPropMap.putNull(it.nextKey()); // < =====!
    }

I don't know how to properly fix it but simple if check in PathView works ))

PathView.java

 @ReactProp(name = "d")
    public void setD(String d) {
        if (d == null) {
            return;
        }
....

@headfire94
Copy link

headfire94 commented Jul 6, 2019

@msand forgot to say, this is only on Android! - on iOS everything is ok

Always get invalid pattern when trying to animated pie chart based on your example, but with angle transition. on IOS also. RN - 0.59.2. (UPD below, works with endAngle close to startAngle)

import React, { useMemo, useEffect } from 'react'

import { Animated } from 'react-native'

import { arc as arcShape, pie as pieShape } from 'd3-shape'

import Svg, { G, Path } from 'react-native-svg'

const AnimatedPath = Animated.createAnimatedComponent(Path)

function convertToFixed(m, integer, decimal = '', sign, power) {
  const fixed = integer + decimal
  return sign === '+'
    ? fixed + '0'.repeat(power - decimal.length)
    : '0.' + '0'.repeat(power - 1) + fixed
}

function exponentialToFixedNotation(num) {
  return num.replace(/(\d)(?:\.(\d+))?e([+-])(\d+)/g, convertToFixed)
}

const PieChart = ({
  data,
  innerRadius,
  outerRadius,
  padAngle,
  animate,
  animationDuration,
  valueAccessor,
  size,
  colors,
  id,
}) => {
  const animatedValue = useMemo(() => new Animated.Value(0), [])

  const arcGenerator = arcShape()
    .outerRadius(outerRadius)
    .innerRadius(innerRadius)
    .padAngle(padAngle)

  useEffect(() => {
    const anim = Animated.timing(animatedValue, {
      toValue: 1,
      duration: animationDuration,
      useNativeDriver: false, // also tried true
    })

    anim.start()

    return () => anim.stop()
  }, [])

  const pieSlices = pieShape().value((d) => valueAccessor(d))(data)

  return (
    <Svg style={{ height: size, width: size }}>
      <G x={size / 2} y={size / 2}>
        {pieSlices.map((slice, index) => {
          const startPath = arcGenerator({
            ...slice,
            startAngle: 0,
            endAngle: 0,
          })
          const endPath = arcGenerator(slice)
          const outputRange = [startPath, endPath].map(exponentialToFixedNotation)
          return (
            <AnimatedPath
              d={animatedValue.interpolate({
                inputRange: [0, 1],
                outputRange,
              })}
              fill={colors[index]}
            />
          )
        })}
      </G>
    </Svg>
  )
}


export default PieChart

@headfire94
Copy link

headfire94 commented Jul 6, 2019

It works (without native driver, with native driver app crashes without errors) when i change

const startPath = arcGenerator({
            ...slice,
            endAngle: slice.startAngle + 0.1,
          })

But animation looks strange. Interpolation in d3 library works great for example, but this one not -
error

My goal is to animate endAngle of arc from startAngle to endAngle, something like that -
result

P.S. app crashes when you useNativeDriver: true in this example

@matpaul
Copy link

matpaul commented Jul 15, 2019

@headfire94 hi, do you patch react-native with facebook/react-native#24177 ?

invalid pattern issue:
when you generate d path, you should make start d and end d same consist,
i used `'d3-interpolate-path' like: outputRange: [interpolatorActive(0), interpolatorActive(0.99)]

@headfire94
Copy link

@headfire94 hi, do you patch react-native with facebook/react-native#24177 ?

No, i will try. Thanks for response

@matpaul
Copy link

matpaul commented Jul 15, 2019

@headfire94 use patch-package, also note that ios patch is simple, android is not =)
a lot of manual work

@wcandillon
Copy link
Contributor

color animations with this package work great with Animated from RN but it doesn't seem to support color() node from reanimated.

@msand
Copy link
Collaborator

msand commented Sep 28, 2019

RN 0.61 includes my patches to Animated, allowing useNativeDriver animations of svg paths using only react-native and react-native-svg, no extra dependencies needed. It also includes some fixes for edge-cases and to make it easier to animate several properties at once per element, and share animations among several elements and styles. Closing this now.

@msand msand closed this as completed Sep 28, 2019
@msand
Copy link
Collaborator

msand commented Sep 28, 2019

@wcandillon Could you please open a new issue with a repro for the color compatibility with reanimated?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants