Hasan Setiawan

Write, write, write give your wings on code!

Follow me on GitHub

React Native Morphing SVG Paths with React Art

We’re going to take a path. In our case all of the Batman logos, and transform each SVG path into the next until we’re all out. Then we’re going to transform it into a square.

            
var React = require('react-native');
var ReactART = require('ReactNativeART');
var Dimensions = require('Dimensions');
var {
  width,
  height
} = Dimensions.get('window');
var {
  AppRegistry,
  StyleSheet,
  Text,
  View,
} = React;
var {
  Surface,
  Shape
} = ReactART;
var Morph = require('art/morph/path');
            
          

We bring in the usuals, but also require art/morph/path which will do our magic morphing.

            
var BatmanLogoSVGs = [
  'M 256,213 C 245,181 206,187 234,262 147,181 169,71.2 233,18   220,56   235,81   283,88   285,78.7 286,69.3 288,60   289,61.3 290,62.7 291,64   291,64   297,63   300,63   303,63   309,64   309,64   310,62.7 311,61.3 312,60   314,69.3 315,78.7 317,88   365,82   380,56   367,18   431,71   453,181 366,262 394,187 356,181 344,213 328,185 309,184 300,284 291,184 272,185 256,213 Z',
  'M 212,220 C 197,171 156,153 123,221 109,157 120,109  159,63.6 190,114  234,115  254,89.8 260,82.3 268,69.6 270,60.3 273,66.5 275,71.6 280,75.6 286,79.5 294,79.8 300,79.8 306,79.8 314,79.5 320,75.6 325,71.6 327,66.5 330,60.3 332,69.6 340,82.3 346,89.8 366,115  410,114  441,63.6 480,109  491,157 477,221 444,153 403,171 388,220 366,188 316,200 300,248 284,200 234,188 212,220 Z',
  'M 213,222 C 219,150 165,139 130,183 125,123 171,73.8 247,51.6 205,78   236,108  280,102  281,90.3 282,79   286,68.2 287,72   288,75.8 289,79.7 293,79.7 296,79.7 300,79.7 304,79.7 307,79.7 311,79.7 312,75.8 313,72   314,68.2 318,79   319,90.3 320,102  364,108  395,78   353,51.6 429,73.8 475,123 470,183 435,139 381,150 387,222 364,176 315,172 300,248 285,172 236,176 213,222 Z',
    // There are many more, truncated for blog reading purposes
];
var BatmanLogoPaths = BatmanLogoSVGs.map((svg) => Morph.Path(svg));
var square = Morph.Path()
  .move(100,0)
  .line(100,0)
  .line(0,100)
  .line(-100,0)
  .close();
BatmanLogoPaths.push(square);
            
          

Then we throw a square on the end.

Render

There is nothing special here. We just add a Surface the full width/height of the phone and instead of a string SVG path we give it the transition which just happens to be a MorphPath, which extends from Path which React Art knows what to do with.

            
render: function() {
    return (
    
    );
  }
            
          

The person that created the SVGs made the central point the start of the SVG so we just set it back -100 to center it-ish. I don’t know. We fill it with black. Batman likes black.

Intial

            
getInitialState: function() {
  return {
    transition: Morph.Tween(BatmanLogoPaths[0], BatmanLogoPaths[1])
  };
},
componentWillMount: function() {
  this._current = 1;
},
componentDidMount: function() {
  this.animate(null, this.nextAnimation)
},
            
          

We start the intial render with a Morph.Tween of the first and second Batman logos. We do a little setup in componentWillMount to say we’re currently animating to the second logo (it’s a 1 since we have 0 based array indexes).

Then once the component is mounted we kick off the animation with our this.animate call.

Animate

            
animate: function(start, cb) {
    requestAnimationFrame((timestamp) => {
      if (!start) start = timestamp;
      var delta = (timestamp - start) / 1000;
      if (delta > 1) return cb();
      this.state.transition.tween(delta);
      this.setState(this.state);
      this.animate(start, cb);
    })
  },
            
          

Our animate call takes a start, and a callback for when the animation is complete. Thanks to React Native with get a polyfilled requestAnimationFrame. If we don’t have a start, then we set it to the timestamp that requestAnimationFrame provides us. The start allows us to compute how far along in the animation we are.

The delta is the current timestamp which is some amount of time in the future, minus the start. The /1000 is the amount of time each animation will take. So each morph will take 1000ms to complete.

If our change is greater than 1 then we know our animation is complete and trigger are callback, and also return so we don’t keep animating a complete animation.

We tween our transition with the new delta progress, we trigger a setState to cause our UI to re-render, then we call ourself (aka this.animate), with our start and our callback so we can trigger the next animation frame.

Animate it Again

            
nextAnimation: function() {
    this._current += 1;
    if (this._current >= BatmanLogoPaths.length) return;
    this.setState({
      transition: Morph.Tween(BatmanLogoPaths[this._current - 1], BatmanLogoPaths[this._current])
    }, () => this.animate(null, this.nextAnimation))
  },
            
          

Okay so we need a little logic around keeping track of which logo is transitioning to which other shape. If this function is called it means an animation has completed and we need to trigger the next one.

We add one to the current to setup that we’re about to animate to the next logo path.

First we check if it’s equal to or somehow greater than the amount of logos we have. If it is we stop animating and just leave the current render as the last shape in the array.

If not we trigger a setState to adjust the this.state.transition (which we pass into the Shape). This just gets set to the this._current - 1 logo and then the this._current which is going to be the next logo. Because currently on screen is this._current - 1 and we do a setState, nothing will flash/jump since you’re rendering the same exact shape again.

setState also takes a success callback, meaning the UI has updated, we then kick off the animation. TahDah. Batman Animating.