More paths, more React Art, and more animations. What else have you come to expect of me. A question was posed on Reactiflux about morphing one path to another with React Art. Of course I took this as a "challenge".

I say "challenge" because it took all of a few moments to check out the ART repo and see the Morph Demo which links to art/morph/path . That's of course what this all about, morphing a path. Sebastian Markbåge has thought of everything.

On Reactiflux the demo of morphing batman logos was proposed and linked to. You can read the orignal source with the linked SVG at http://tavmjong.free.fr/blog/?p=741.

What are we building

Concept

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.

Setup

If you're unsure how to get React Art running on React Native checkout my previous blog post here Getting react-art running on react-native.

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.

The SVG Paths

Thanks to the blog post I just parsed out the SVG paths, and tossed them into an array. For the sake of parsing, I map over each and convert them into native React Art paths.

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

render: function () { return ( < View style = {styles.container}> < Surface width = {width} height = {height}> < Shape x = { - 100 } y = { 100 } d = {this.state.transition} fill = "#000" /> </ Surface > </ View > ); }

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.

Initial Setup

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 it

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.

A lot of this is just boilerplate logic you can see here https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame.

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.

More than Batman Logos?

Of course. You can animate from anything to anything with Morph.Tween . See I animated Batman into a square at the end. But really you can do any sort of path to another path. If you're animating a complex path to another compelx path they not animate elegantly but they'll animate.

Clean it up

setState is hacky for animations, you could wrap this up just like Animated to make it all nice and performant with setNativeProps but you can take care of that yourself. Vjeux shows how to do that in his React Rally talk, seriously watch it https://www.youtube.com/watch?v=xtqUJVqpKNo. Also slides here https://speakerdeck.com/vjeux/react-rally-animated-react-performance-toolbox

Done

Okay, so now you know how to morph paths. Go make cool animated transitions on React Native now!

Final Code