software-mansion / react-native-svg

SVG library for React Native, React Native Web, and plain React web projects.
MIT License
7.44k stars 1.12k forks source link

Translate animation #587

Closed NinjaXY closed 6 years ago

NinjaXY commented 6 years ago

I have drawn two rects and use rotateX:70deg to make them look three-dimentional, like the picture below. wechatimg12

Now I want to animate the rects and make them move vertically.

Here is the question, since there is no translateZ for Svg, if I use {translate:[0, 0, this.state.zAnim]} in Svg, it cames out an error Attempted to assign to readonly property.

Else if I wrapped each rect within a View component and use the translateY in View, the rect behind and be partially overlapped won't respond the onPress event.

Here is my code.

export default class App extends Component {
  constructor(){
      super();
      this.state = {
          currentAlpha: 1.0,
          zAnim: new Animated.Value(-300),
      }
  }

  moveV = ()=>{
      Animated.timing(this.state.zAnim, {
          toValue: 0,
      }).start();
  };

  render() {
    return (
      <View style={styles.container}>
        //<View style={{transform:[{translateY: this.state.fadeAnim}]}}>
        <AnimateSvg style = {{transform: [{rotateX: '70deg'},{rotateZ:'-45deg'},{translate:[0, 0, this.state.zAnim]}]}} width="400" height="400">
                <G onPressIn={() => this.moveV()}>
                  <Rect
                      x="5%"
                      y="5%"
                      width="90%"
                      height="90%"
                      fill="rgb(0,0,255)"
                      strokeWidth="3"
                      stroke="rgb(0,0,0)"
                  />
            </G>
        </AnimateSvg>
        //</View>
        <Svg style = {{transform: [{rotateX: '70deg'},{rotateZ:'-45deg'},{translate:[0, 0, 200]}]}} width="400" height="400">

                <G onPressIn={() => alert(1)}>
                  <Rect
                      x="5%"
                      y="5%"
                      width="90%"
                      height="90%"
                      fill="rgb(0,255,255)"
                      strokeWidth="3"
                      stroke="rgb(0,0,0)"
                  />
            </G>
        </Svg>
      </View>
    );
  }
}
msand commented 6 years ago

@NinjaXY The problem with the hit test is that you have them in separate svg roots. I guess the later one (higher z-index) will intersect with the touch event and override the earlier one. To get correct handling of 3d, we would have to send 3x3 matrices to the native implementation, currently we send a 2x3 matrix, as svg currently only supports 2d transforms, except on the root element. You can find some inspiration here https://medium.com/@youngchanje/implementing-cube-in-react-native-cb61b9a7e8c3

You could calculate path data yourself, and do the 3d transforms of the coordinates yourself before sending them to the native side, but then you have to handle z/buffering & occlusion culling yourself. Or use something like: http://seenjs.io/demo-multi-views.html or http://kovacsv.github.io/JSModeler/documentation/examples/svgto3d.html

Perhaps a better alternative would be for you to implement support for the upcoming svg 3d transforms: https://www.w3.org/TR/SVG-Transforms/

msand commented 6 years ago

Actually, for 3d content and transforms, opengl will be a much better match. https://www.npmjs.com/package/react-native-gl-model-view https://github.com/gre/gl-react-native-v2 Or, if you only need to rotate rectangles, then you can just use plain react-native views with a background color and the normal 3d transforms.

msand commented 6 years ago

@NinjaXY Here is an example of animating 2.5d(getting quite different results on android and ios, but something to start working/investigating from at least) and correct press handlers:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

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

import ZoomableSvg from 'zoomable-svg';

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

class SvgRoot extends Component {
  state = {
    toggle: false,
    zAnim: new Animated.Value(0),
    initAnim: new Animated.Value(0),
  };

  componentDidMount() {
    Animated.timing(
      // Animate over time
      this.state.initAnim,
      {
        toValue: 1,
        duration: 3000,
        useNativeDriver: false,
      },
    ).start();
  }
  moveV = () => {
    Animated.timing(this.state.zAnim, {
      toValue: this.state.zAnim._value > 0.5 ? 0 : 1,
      duration: 1000,
      useNativeDriver: false,
    }).start();
  };
  toggle = () => {
    this.setState(({ toggle }) => ({ toggle: !toggle }));
  };
  alert = () => alert(1);
  noop = () => {};

  render() {
    const { toggle, zAnim, initAnim } = this.state;
    let rotZ = zAnim.interpolate({
      inputRange: [0, 1],
      outputRange: ['0deg', '-45deg'],
    });
    let translateRootX = initAnim.interpolate({
      inputRange: [0, 1],
      outputRange: [0, 50],
    });
    let translateRectY = initAnim.interpolate({
      inputRange: [0, 1],
      outputRange: ['0', '50'],
    });
    const { transform } = this.props;
    return (
      <AnimatedSvg
        width={width}
        height={height}
        style={{
          transform: [
            { perspective: 850 },
            { rotateX: '70deg' },
            { rotateZ: rotZ },
            { translateX: translateRootX },
          ],
        }}
      >
        <G transform={transform}>
          <AnimatedRect
            onPressIn={this.moveV}
            y={translateRectY}
            x="5"
            width="90"
            height="90"
            fill="rgb(0,0,255)"
            strokeWidth="3"
            stroke="rgb(0,0,0)"
          />
          <Rect
            x="5"
            y="5"
            width="55"
            height="55"
            fill="white"
            onPressIn={this.alert}
          />
          <Circle
            cx="32"
            cy="32"
            r="4.167"
            fill={toggle ? 'red' : 'blue'}
            onPress={this.toggle}
          />
          <Path
            d="M55.192 27.87l-5.825-1.092a17.98 17.98 0 0 0-1.392-3.37l3.37-4.928c.312-.456.248-1.142-.143-1.532l-4.155-4.156c-.39-.39-1.076-.454-1.532-.143l-4.928 3.37a18.023 18.023 0 0 0-3.473-1.42l-1.086-5.793c-.103-.543-.632-.983-1.185-.983h-5.877c-.553 0-1.082.44-1.185.983l-1.096 5.85a17.96 17.96 0 0 0-3.334 1.393l-4.866-3.33c-.456-.31-1.142-.247-1.532.144l-4.156 4.156c-.39.39-.454 1.076-.143 1.532l3.35 4.896a18.055 18.055 0 0 0-1.37 3.33L8.807 27.87c-.542.103-.982.632-.982 1.185v5.877c0 .553.44 1.082.982 1.185l5.82 1.09a18.013 18.013 0 0 0 1.4 3.4l-3.31 4.842c-.313.455-.25 1.14.142 1.53l4.155 4.157c.39.39 1.076.454 1.532.143l4.84-3.313c1.04.563 2.146 1.02 3.3 1.375l1.096 5.852c.103.542.632.982 1.185.982h5.877c.553 0 1.082-.44 1.185-.982l1.086-5.796c1.2-.354 2.354-.82 3.438-1.4l4.902 3.353c.456.313 1.142.25 1.532-.142l4.155-4.154c.39-.39.454-1.076.143-1.532l-3.335-4.874a18.016 18.016 0 0 0 1.424-3.44l5.82-1.09c.54-.104.98-.633.98-1.186v-5.877c0-.553-.44-1.082-.982-1.185zM32 42.085c-5.568 0-10.083-4.515-10.083-10.086 0-5.568 4.515-10.084 10.083-10.084 5.57 0 10.086 4.516 10.086 10.083 0 5.57-4.517 10.085-10.086 10.085z"
            fill="blue"
            onPress={this.noop}
          />
        </G>
      </AnimatedSvg>
    );
  }
}

export default class App extends Component {
  render() {
    return (
      <View style={styles.container}>
        <ZoomableSvg
          align="mid"
          vbWidth={100}
          vbHeight={100}
          width={width}
          height={height}
          meetOrSlice="slice"
          svgRoot={SvgRoot}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#ecf0f1',
  },
});
msand commented 6 years ago

Seems the perspective values are interpreted differently on android and ios.

NinjaXY commented 6 years ago

@msand Really thanks for your detailed reply.

In your last example code, actually I want the rects move vertically along the screen side separatelly. To achieve this, especially after I rotated the rects with X axis, I have to use translateZ or wrap each of them into a View and then use translateY.

All I want is to do some 3d transforms with several simple svg images, is there any easy way to achieve this, or I have to do with the matrix things?

msand commented 6 years ago

Well, if you add another dimension to your information and make your rendering dependent on it, then you have to do the required math to have the correct results ;) It doesn't have to be matrices, but the mathematically equivalent computation has to be done somehow.

The native rendering stack allows for 3x3 matrices. if the z-order (what objects are in front of what other objects, and thus occluding them, as they will be rendered in DOM order rather than furthest away first) doesn't change over time/interaction, then it would be enough to add support for sending 3x3 matrices using e.g. setNativeProps, allowing you to just calculate a single transform matrix per element/group you want to transform independently in 3d.

NinjaXY commented 6 years ago

@msand OK I'll do some investigating on it, thanks again, really.

wcandillon commented 4 years ago

@msand I'm struggling to find out the amount of perspective applied on Android, do you know? on iOS it seems that we have full control over the perspective but not on Android.

msand commented 4 years ago

@wcandillon There is a matrix decomposition done in the ViewManagers here: https://github.com/react-native-community/react-native-svg/blob/3e3ad13b65ed70f606f6826947cbcfb4f7ce2c4b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java#L211-L379

The logic has been copied from react-native and adapted: https://github.com/facebook/react-native/blob/3b31e69e284074da72108edfb11e41ee74d7100e/ReactAndroid/src/main/java/com/facebook/react/uimanager/MatrixMathHelper.java#L102-L215

There might have been changes in the react-native part which haven't been accounted for in the react-native-svg part, I haven't checked that recently.

You can set breakpoints in android studio and inspect the values there. If you have some sample code that renders differently on ios and android which is representative of the issue, then I'll gladly take a look and see if I can help to isolate and resolve the issue.

msand commented 4 years ago

It seems this part has changed in react-native: https://github.com/facebook/react-native/blob/f2d58483c2aec689d7065eb68766a5aec7c96e97/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java#L44

private static final float CAMERA_DISTANCE_NORMALIZATION_MULTIPLIER = (float) Math.sqrt(5);

vs this in react-native-svg: https://github.com/react-native-community/react-native-svg/blob/3e3ad13b65ed70f606f6826947cbcfb4f7ce2c4b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java#L224