Hot questions for Using Lottie in bodymovin

Question:

I am using the react-bodymovin package (https://www.npmjs.com/package/react-bodymovin) to embed a Bodymovin animation, but I wanted to loop a section of the animation after it has played through once.

I can see is simple using the HTML version of Bodymovin, as you simply use the relevant methods to do this, for example, (assuming the div has ID bodymovin:

const params = {
    container: document.getElementById('bodymovin'),
    renderer: 'svg',
    loop: false,
    autoplay: false,
    animationData: animationData,
}

const anim = bodymovin.loadAnimation(params)

anim.playSegments([[0, 65]], true)

However, I am not sure how to access these same methods when using the React component. Here is my component:

import React from 'react'
import ReactBodymovin from 'react-bodymovin'
import animation from './animation.json'

const App = () => {
  const bodymovinOptions = {
    loop: true,
    autoplay: true,
    prerender: true,
    animationData: animation
  }

  return (
    <div>
      <ReactBodymovin options={bodymovinOptions} />
    </div>
  )
}

I have a feeling that due to the nature of the way this React wrapper works with Bodymovin, it may not be possible to access the methods within the file, but if anyone knows a way to do this, i would love to hear it. Thanks.


Answer:

Why don't you try react-lottie package instead? It has way more npm-downloads than react-bodymovin and you can achieve what you want and play segments like so:

import React from 'react'
import animation from './animation.json'

import Lottie from 'react-lottie';

const App = () => {
  const options= {
    loop: true,
    autoplay: true,
    prerender: true,
    animationData: animation
  }

  return (
    <div>
      <Lottie options={options} segments={[0, 65]} />
    </div>
  )
}

However, with both packges you won't have full control over the lottie object and thus can't call methods like loadAnimation or loadSegments manually. However you could use the lottie-web package like you would do in normal javascript, just as you have shown. Here is an example how you should do it in react with hooks:

import React, { useEffect, useRef } from "react";
import lottie from "lottie-web";
import animation from "./animation.json";

const App = () => {
 const animContainer = useRef<HTMLDivElement>(null);

 useEffect(() => {
    if (ref.current) {
      const anim = lottie.loadAnimation({
        container: animContainer.current,
        renderer: "svg",
        loop: true,
        autoplay: true,
        animationData: animation
      });

     anim.playSegments([[0, 65]], true);
    }
  });

  return <div ref={animContainer}></div>;
}

Question:

Using Lottie/BodyMoving, how can I trigger an event once the Lottie animation reaches a certain frame.

For example: Once anim1 reaches a certain frame, I want anim2 to play.

Thanks


Answer:

try enterFrame event

  const anim = lottie.loadAnimation({ ...options, path: 'URL_TO_JSON' });
anim.addEventListener('enterFrame', () => {
  console.log('enterFrame', anim.currentFrame);
  if(anim.currentFrame == 55)
  {
  //stop and play 2nd one
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.5.9/lottie.js"></script>
 <div style="width:1067px;height:600px"  class="lottie" data-animation-path="animation/" data-anim-loop="true" data-name="ninja"></div>

Question:

I am creating an animations using Adobe After Effects and React Lottie and I have many json files generated by Bodymovin extension. I am importing files in that way:

import * as initial from './white_bird.json';
import * as hoverOn from './white_bird_hover_on.json';
import * as hoverOff from './white_bird_hover_off.json';

But, when I have e.g. 6 other components that look identically but differ from each other only with imported files. How can I write those lines upper in something like this:

const data = {
    initial: import * as initial(`./${some_param}.json`),
  };

Note that I must import it like '* as' in another way this doesn't work


Answer:

You could use Dynamic import:

useEffect(() => {
  async loadData() {
    const data = await import(`./${some_param}.json`);
    setInitial(data);
  }

  loadData();
}, [])

and use Promise.all if you have multiple requests:

useEffect(() => {
  async loadData() {
    const [initalData, hoverOnData, hoverOffData] = await Promise.all([
      import(`./${bird}.json`),
      import(`./${bird}_hover_on.json`),
      import(`./${bird}_hover_off.json`)
    ]);

    setInitial(initalData);
    setHoverOn(hoverOnData);
    setHoverOff(hoverOffData);
  }

  loadData();
}, [])

Question:

I have an animation I'd like to use for a rollover effect with the following basic states:

  1. resting: continuously loop between frames 0 and 55
  2. mouse enter: play once frames 56 to 78, then pause on 78
  3. mouse out: play once frames 79-95, then return to resting

I almost have it working except for a continuous loop on the resting state. The loop plays only once and then sits static. My code sits as follows:

Codepen: https://codepen.io/anon/pen/pBXKeN

var animationContainer = document.getElementById("animation-container");

var animation = lottie.loadAnimation({
    wrapper: document.getElementById("animation-wrapper"),
    renderer: "svg",
    loop: false,
    autoplay: false,
    prerender: true,
    animationData: animationData,
});

animation.addEventListener('DOMLoaded',resting);
animationContainer.addEventListener("mouseenter", hoverStart);
animationContainer.addEventListener("mouseleave", hoverEnd);

function resting() {
    animation.removeEventListener("complete", resting);
    console.log('resting');
    animation.playSegments([0, 55], true);
}

function hoverStart() {
    console.log('hover started');
    animationContainer.removeEventListener("mouseenter", hoverStart);
    animation.playSegments([56, 78], true);
    animationContainer.addEventListener("mouseleave", hoverEnd);
}

function hoverEnd() {
    console.log('hover ended');
    animationContainer.removeEventListener("mouseleave", hoverEnd);
    animation.playSegments([79, 95], true);
    animation.addEventListener("complete", resting);
    animationContainer.addEventListener("mouseenter", hoverStart);
}

I have tried setting loop to true, but this causes all 3 states to loop, which is not the desired effect for the mouseenter and mouseleave effects. Is there a way to get a single section to loop?

Also I'm happy to switch to jQuery if it makes things simpler.


Answer:

Solved with a combination of setting loop to true, looping between 2 frames for the "pause" effect and more closely monitoring the hover state. I'm sure this could be improved further, but at this point I'm just glad to have it working.

Updated Codepen: https://codepen.io/matt3/pen/NVxWYJ

var hover = false;

animationContainer.addEventListener("mouseenter", hoverStart);
animation.addEventListener('DOMLoaded',resting);

function resting() {
  console.log('resting. hover status: ' + hover);
  animation.removeEventListener("loopComplete", resting);
  animation.playSegments([0, 55], true);
}

function hoverStart() {
  console.log('hover started. hover status: ' + hover);
  animationContainer.removeEventListener("mouseenter", hoverStart);
  animation.playSegments([56, 78], true);
  animation.addEventListener("loopComplete", hoverPause);
}

function hoverPause() {
  console.log('hover paused. hover status: ' + hover);
  animation.removeEventListener("loopComplete", hoverPause);
  animationContainer.addEventListener("mouseleave", hoverEnd);
  animation.playSegments([77, 78], true);
  console.log('hover pause complete. hover status: ' + hover);
  if (!hover) {
    animation.addEventListener("loopComplete", hoverEnd);
  }
}

function hoverEnd() {
  console.log('hover ended. hover status: ' + hover);
  animationContainer.removeEventListener("mouseleave", hoverEnd);
  animation.removeEventListener("loopComplete", hoverEnd);
  animation.playSegments([79, 95], true);
  animation.addEventListener("loopComplete", resting);
  animationContainer.addEventListener("mouseenter", hoverStart);
}

animationContainer.onmouseout = function () {
    hover = false;
}
animationContainer.onmouseover = function () {
    hover = true;
}

Question:

What I'm looking for here is a method to refer to a bound method from within that method for the purpose of removing an eventlistener from within the event listener.

I'd like to create a single method to handle the same action over a few different events.

I've got a function that handles rotating the elements called rotateActiveItem. It looks at a list of list items and activates one at a time.

In my constructor I'd like to set up a few, potentially many, events that trigger rotateLoadingCopy.

this.oneQuarter = genericDurationEvent.bind(this, animationDuration * .25);
this.half = genericDurationEvent.bind(this, animationDuration * .5);
this.threeQuarters = genericDurationEvent.bind(this, animationDuration * .75);

Each of these are added to the animation's events:

this.animation_.addEventListener('enterFrame', this.oneQuarter);
this.animation_.addEventListener('enterFrame', this.half);
this.animation_.addEventListener('enterFrame', this.threeQuarters);

And then the event checks for duration, executes the rotation, and then should remove itself from the eventListeners.

genericDurationEvent(duration, event) {
  if (event.currentTime >= duration) {
    // Activate the next loading text element.
    this.rotateActiveItem();

    // Stop listening for this event.
    this.animation_.removeEventListener('enterFrame', /*What goes here?*/);
  }
}

At first I thought maybe I could bind the bound method onto another method, but that's a rabbit hole of bound functions.

Then I thought arguments.callee would do this, but I'm in strict mode it its deprecated in strict mode.


Answer:

I would recommend going for a closure pattern, and generating the handlers dynamically, so you can keep a reference to the function.

genericDurationEvent(context, duration) {
  var handler = function (event) {
    if (event.currentTime >= duration) {
      // Activate the next loading text element.
      this.rotateActiveItem();

      // Stop listening for this event.
      this.animation_.removeEventListener('enterFrame', handler);
    }
  }.bind(context);
  return handler;
}

this.oneQuarter = genericDurationEvent(this, animationDuration * .25);
this.half = genericDurationEvent(this, animationDuration * .5);
this.threeQuarters = genericDurationEvent(this, animationDuration * .75);

If you wanna go the extra mile, you could isolate the logic in a way that the generator makes any function behave like "once" ala jQuery#one():

// definition
function once (what, eventName, originalHandler) {
  function onceHandler (...args) {
    what.removeEventListener(eventName, onceHandler);
    return originalHandler.apply(this, args);
  };
  what.addEventListener(eventName, onceHandler);
}

// usage
once(this.animation_, 'enterFrame', event => {
  if (event.currentTime >= animationDuration * .25) {
    // Activate the next loading text element.
    this.rotateActiveItem();
  }
});

Question:

I'm trying to use ScrollMagic to control a TimelineMax which moves the playhead of a Lottie animation.

So, as the user scrolls, the animation plays relative to the speed and direction of the scroll. I'm so close and need a little help to bring the effect home.

First, I include my Libraries

<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.5.9/lottie.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/ScrollMagic.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/plugins/animation.gsap.js"></script>

Then...

  // init scrollmagic
  var controller = new ScrollMagic.Controller();

Now, the animation setup...

  // Manage Animation
  var animation = bodymovin.loadAnimation({
    container: document.getElementById('lottie'), // Required
    path: '/static/animations/animation.json', // Required
    renderer: 'svg', // Required
    loop: false, // Optional
    autoplay: false, // Optional
    name: "Welcome to Awesomeness", // Name for future reference. Optional.
  });

This is where I'm struggling:

  // Setup Timeline
  var lottieControl = new TimelineMax({ repeat: -1, yoyo: true }); // <-- don't loop and let the timeline go back and forth
  lottieControl.to({ frame:0 }, 1, { // <-- is this right? I'm telling the timeline to start at frame 0
    onUpdate:function(){
      animation.goToAndStop(Math.round(this.target.frame), true) // <-- move the playback head of the animation to the target frame that has been rounded and use frames not time (true)
    },
    ease:Linear.easeNone
  })

Finally, bring it all back together...

  // Attach to scroll
  var lottieScene = new ScrollMagic.Scene({
        duration: '80%',
        offset: 1
    })
    .setPin("#header-scroll")
    .setTween(lottieControl)
    .addTo(controller);

For the life of me, I can't see if I'm using the goToAndStop method right or not. Thanks for your time.


Answer:

After hours of testing I found the answer. I was looking at the wrong thing. I needed to tie the timeslines progress to the frame to go to. In this instance, there are 300 frames, thus the multiplication by the number of frames in the animation.

Here's the revised code:

<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.5.9/lottie.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/ScrollMagic.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/plugins/animation.gsap.js"></script>

<script>
  // init scrollmagic
  var controller = new ScrollMagic.Controller();

  // Manage Animation
  var animation = bodymovin.loadAnimation({
    container: document.getElementById('lottie'), // Required
    path: '/static/animations/scroll_animation.json', // Required
    renderer: 'svg', // Required
    loop: false, // Optional
    autoplay: false, // Optional
    name: "Welcome to Awesomeness",
  });

  // Setup Timeline
  var tl = new TimelineMax();
  tl.to({frame:0}, 1, {
    frame: animation.totalFrames-1,
    onUpdate:function(){
      animation.goToAndStop((Math.round(this.progress() * 300)), true)
    },
    ease: Linear.easeNone
  })


  // Attach to scroll
  var lottieScene = new ScrollMagic.Scene({
        duration: '100%',
        offset: 1
    })
    .setPin("#header-scroll")
    .setTween(tl)
    .addTo(controller);

</script>

Question:

New to coding, trying to see what/if Tags would make this code work

So I'm a beginner with basic understanding and more of a Graphics/ Designer than code based. I found a codepen by WEDOO: (https://codepen.io/WEDOO/pen/EQrOxV)

that has exactly what I need and want to try to just swap my "animationData" to see if I can get it to work and then modify it as needed for my test (will be assign the button code to various objects for the SVG). I Can't seem to find the right "Tags" or determine if its referencing an external script...I'd image it just needs the right information to function...is that correct?

Thanks in advance!

```\var animation = bodymovin.loadAnimation({
container: targetAnim,
path: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/914929/data-testo4.json...```

My output from BodyMovin in a JSON file:

```var animationData = 
{"v":"5.4.2","fr":29.9700012207031,"ip":0,"op":149.000006068894...```

Does it make sense to think that I need replace the "var animation" with info that should be the "targetAnim" with the code in the JSON file? So far put the "var animationData" breaks things and does nothing (visually).


Answer:

The bodymovin.loadAnimation can be passed either a URL to a Bodymovin JSON via the path option OR you can pass the animation JSON inline by setting the animationData option instead.

In you case it would end up looking something like:

var animation = bodymovin.loadAnimation({
  container: targetAnim,
  animationData = {"v":"5.4.2","fr":29.9700012207031,"ip":0,"op":149.000006068894...
...
})