Jump to content
Search Community

Exporting timelines

Mr P test
Moderator Tag

Warning: Please note

This thread was started before GSAP 3 was released. Some information, especially the syntax, may be out of date for GSAP 3. Please see the GSAP 3 migration guide and release notes for more information about how to update the code to GSAP 3's syntax. 

Recommended Posts

Ok my question is sort of related to this so just put it out there for reference: 

https://greensock.com/forums/topic/14782-exportimport-timelinestweens-to-json/

 

 

 I have been using fabric.js with GSAP to create an app to make animations. I have run into a problem where i need to save animation in the db. I am saving my canvas as a JSON. Then the way i went about saving the saving the timeline is by just saving the whole timeline object. I think the saving is working (tell me if I am wrong?) but I think its the fact that the timeline doesn't see the reloaded canvas and its objects as the same. ?? If so what about the ideal solution here. 

 

 Btw I am very new to GSAP and I am using for my project and love it so far. The fact that I am new I could be missing something very obvious and so far the forums have been helpful with the answers already up and now I am hoping someone can help me with my mine. Thanks in Advance.

 

Regards,

Mr P

Link to comment
Share on other sites

Well, you probably can't store the target you are tweening inside your JSON file as that is most likely created at runtime. You need to find a way to store an id to the target in your JSON file. I'm really not familiar with the API for fabric, but this question on Stackoverflow looks like it shows how to create and select an object based on an id.

http://stackoverflow.com/questions/20096949/how-to-select-fabric-js-object-programmatically

 

 

.

  • Like 3
Link to comment
Share on other sites

I forgot to mention that you should NOT save the entire timeline. To safely export a timeline for reuse, you should only save relevant information about the tweens contained in a timeline, i.e. the target id, duration, and the vars (config) object. You can get an array of all the tweens in a timeline using the getChildren() method. 

 

Here's an example with some generic objects showing how you could export a timeline. The vars object may contain nested objects if you are using any plugins, which may require recursively looping through the object depending on how deep it goes. To simplify cloning a nested object, you could use something like lodash's cloneDeep method.

 

See the Pen 63adebe3c2865e9427bae98463b2368e by osublake (@osublake) on CodePen

 

  • Like 3
Link to comment
Share on other sites

  • 3 weeks later...

First of all thanks for the answers,

 

once I collected the children I noticed the target id for the tweens were undefined [as you predicted], so to resolve this I gave the fabric objects ids. Then when I collected the tweens up again the target ids were correctly matched with the id that I was just after creating (great how that just worked). Now my question is when I clear the canvas and then reload the canvas with the same obj (instantly) without any changes to the timeline, should it not simply work ? I checked the tweens and its still had the same target ids 

 

If not is the solution you are suggesting to iterate through the children and rerun the tweens on the fabric objects ? 

 

 

 

 

I made a simpler 

See the Pen EZbGQJ?editors=0010 by bhayla (@bhayla) on CodePen

 if its any help;

As you can see once you save and reload the canvas, the timeline doesn't work. However the timeline obj is unchanged

To reproduce: 

  1. Display timeline object
  2. Pause the timeline
  3. Save the canvas (it also reloads it after 1 sec)
  4. Try to play the timeline again (displaying the timeline obj you can see its unchanged)

 

Thanks in advance

Edited by Mr P
Link to comment
Share on other sites

I didn't have much time to look at this, but what jumped out to me was that you're trying to JSON-ify something with this:

onUpdate: function() { canvas.renderAll();}

But that's not really possible. JSON is basically strings/numbers, but you've got a function that's trying to call a very particular method of a particular object - those reference things in memory that cannot be JSON-ified. You'd probably need to build special logic in to accommodate things like that on both ends (encoding and decoding). 

Link to comment
Share on other sites

Ahh well spotted.. though I don't think this is the only issue in the above example as I am only using the json stringify on the timeline to display the object, not changging it. However you are right I am going to run into this exact issue later on with this approach. 

 

The way I see it, the easiest course of action for me here is to:

  1. save the timeline obj with stringfy
  2. clear the timeline
  3. re run the tween on the timline using the saved timeline data
Link to comment
Share on other sites

  • 2 weeks later...

The 2 libraries above weren't quite what I'm looking for but just found this earlier today.

 

https://www.npmjs.com/package/some-sql

 

Looks very promising but it doesn't handle circular structure conversion to JSON like circular-json-es6, which does a superb job of boiling GSAP Timelines down to their essence. But I'm only able to use that to save data to a file or put and take from an array. So, since the UNDO/REDO feature in some-sql looks pretty awesome, I think I'm gonna be forced to parse through and snag all the necessary Timeline data.

 

So, Blake, your

See the Pen 63adebe3c2865e9427bae98463b2368e?editors=0010 by osublake (@osublake) on CodePen

works if you don't have nested timelines. Any thoughts on how to deconstruct and reconstruct a complex timeline with nested timelines and tweens? Every timeline could be different - have a different number of nested timelines and tweens, etc - so not sure how I would go about setting up a universal way of mapping a timeline to something that could then be reconstructed.

 

For now, I think I'll just store a reference to the array element that's storing the JSON converted by circular-json-es6 and add a timestamp to trigger the undo/redo listener. That should work in the meantime.

Link to comment
Share on other sites

The answer to this is in my comment about lodash. Recursion. I think a lot of people have probably heard about recursion before, but have probably never done any. It can be confusing at first, and might even seem like a bad idea, but it's used to figure out a lot problems. 

 

Fortunately, figuring out nested children isn't too hard. You start out with a function that loops through all the children of a timeline you pass to it. If the child is a tween, add it to your data. If it's a timeline, add it to your data, and then call the original function again, passing in that child timeline, starting a new loop.

 

That's why I said it might seem like a bad idea. You're calling the same function you're currently running while inside a loop, which will run another loop and possibly call the same function again. This process keeps repeating itself until it has gone through the entire tree or your computer has run out memory.  :-o

 

See the Pen 7d3275656ef955c5ea2eb121c2407ba6 by osublake (@osublake) on CodePen

 

 

Now what's the need for a circular structure about? That's sounds worse than recursion.  :unsure:

 

  • Like 1
Link to comment
Share on other sites

Thanks, Blake. As to the Circular thing, circular-json-es6 loops through and snags the data for timelines and doesn't drill down into any circular data. So, when it finds a reference back to a parent timeline, for example, it doesn't drill into it, but instead makes reference to it. That way, all the data can be saved and reconstructed. Some-sql wasn't doing that kind of thing so it was throwing that error when trying to save the data. That's because the data types available for collections (array and map) were both converted to JSON using a basic JSON stringify like:

"array": JSON.parse(JSON.stringify(val || [])),
"map": JSON.parse(JSON.stringify(val || {})),

which was causing the Circular data error. I heard from the developer though and he said he had added a blob type so I could probably give that another go. But, your approach is so much cleaner. Gonna give that a go. Again, thanks a million for your input. 

Link to comment
Share on other sites

Btw, Blake, that is an awesome codepen! You always seem to show me something I didn't know about GSAP Timeline's and Tweens. Like the ability to add an id to a timeline the way you did.

tl1.data = { id: "TIMELINE 1" };

You'd think I would have just known that - it is after all, an object, duh. Also, the simple use of instanceof TweenLite to know where to recurse. Thanks again!

Link to comment
Share on other sites

Yea, I've added data to every tween and timeline now like this:

tl.set(txt,{transformOrigin: '50% 50%', data:{tweenType: 'set'}});

or...

tl.to(txt,0.5,{scale: 0, ease: Elastic.easeIn, data: {tweenType: 'to'}});

so I can properly reconstruct the tweens.

 

Working on the best way to parse through the vars to reset props:vals there were a couple of things I noticed.

 

First, whenever an array is the target value for a tween, I'm not able to find the data that's so handy in _gsTransform like you see in single targets. Here's a single target...

 

single-target.png?dl=1

 

Here's from an array tween...

 

target-array.png?dl=1

 

 

Is there any convenient way to snag that data for target arrays? I could always store the settings in the data object but didn't know if there was a _gsTransform hiding somewhere for arrays I could tap into.

 

The other thing is the way eases are returned. Here's an example of a 'from' ease (not sure if that's why it's different from the 'to' ease that follows)..

from-ease.png?dl=1

 

 

 

Here's a 'to' ease...

to-ease.png?dl=1

 

Custom tweens are showing up with the actual name of the tween. As with _gsTransform, I could always store this info in the data object for later retrieval but wondering what the best way might be to reconstruct eases using the returned data from getChildren.

Link to comment
Share on other sites

Gonna pass the params in the data object and set the ease value as a string and then, when reconstructing the ease value, will parse it out to an object value.

 

Doesn't seem to hurt anything to pass data on each chained tween of a timeline like:

tl.to(element1,1,{x: 100, ease: Back.easeOut, data: {type: 'to', params:{x: 100, ease: 'Back.easeOut'}})
  .to(element2,1,{y: 100, ease: Back.easeOut, data: {type: 'to', params:{y: 100, ease: 'Back.easeOut'}});

The data object ends up in vars. Anything wrong with doing it this way?

Link to comment
Share on other sites

You can use a string for ease. That's what I did in my examples above.

 

I'm don't know why you're not seeing the _gsTransform object on your array targets. Are you sure there is a transform on the element? You can always force one if there is isn't.

TweenLite.set(myElement, { x: "+=0" });

You can get the values you set from the vars object on a tween.

See the Pen 8654519ab7ecfd28156cd14738fe72d2 by osublake (@osublake) on CodePen

.

  • Like 4
Link to comment
Share on other sites

Have been successfully restoring animations. Have had to sort of cluge it together but one thing I noticed is you've gotta have a very specific selector. Lots of stuff isn't consistently in the same place, like there's not always a selector property. If there's not an id but instead a class selector, things don't always work the same. So, I used target.attributes to create a selector. As long as I have that, and can store the type of tween (to, fromTo, from, etc) in the data it works fine. 

 

I have so many different setups (and don't want to go back and change everything) I had to use something like the following to be sure to always snag the attributes. Attributes are always {id,class,style} and snagging the value for each gives you all you need to assemble a very specific selector.  

 

var attrs;


if( target.attributes !== undefined ) {
  attrs = target.attributes;
} else if( target[0].attributes !== undefined ) {
  attrs = target[0].attributes;
} else if( target.target.attributes !== undefined ) {
  attrs = target.target.attributes;
}

All the animations are in separate foreignObjects inside a master SVG because there could be multiple instances of the same animation type in the SVG. Just in case this helps anyone, my create selector script is:

 

function buildSelector(target,foreignObjId) {


  var attrs;


  if( target.attributes !== undefined ) {
    attrs = target.attributes;
  } else if( target[0].attributes !== undefined ) {
    attrs = target[0].attributes;
  } else if( target.target.attributes !== undefined ) {
    attrs = target.target.attributes;
  }


  var sel = '',
      id = null,
      cl = null; // class is a reserved word


  // check to see what's in there. Each should contain id, class, style
  if(attrs.id !== undefined ) {
    id = attrs.id.value !== undefined ? attrs.id.value : id;
  }
  
  if( attrs.class !== undefined ) {
    cl = attrs.class.value !== undefined ? attrs.class.value : cl;
  }
  // make sure it's not the actual foreignObj!
  if( foreignObjId == id ) {
    sel = id !== null ? `[id="${id}"]` : sel;
  } else {
    sel = id !== null ? `[id="${foreignObjId}"] [id="${id}"]` : sel;
  }
  
  var clArr = [];
  clArr = cl !== null ? cl.split(' ') : clArr;
  if(clArr.length > 0) {
    for ( let C of clArr ) {
      sel += `.${C}`;
    }
  }


  return sel; 
}
Link to comment
Share on other sites

Got a question regarding staggerTo and getChildren. It seems that staggerTo returns a TimelineLite child which doesn't have things like getLabelsArray() so when attempting to use it even inside a TimelineMax instance, it throws errors for anything called assuming a TimelineMax instance. I forked your Greensock staggerTo codepen, replaced the old version of TweenMax with the latest and used Blake's parsing script. You'll see this in the top script block...

tl.to(".box", 1, {rotation:360, x:600}, 0.5);

If you change that to...

tl.staggerTo(".box", 1, {rotation:360, x:600}, 0.5);

it breaks the parsing script. 

 

See the Pen bqqPrX by swampthang (@swampthang) on CodePen

 

Is there any way to get around this? Can you force staggerTo to use an instance of TimelineMax?

Link to comment
Share on other sites

I didn't notice anything in that script that required all those extra methods. Hm. If you really need them in the resulting TimelineLite, why not just wrap that in a TimelineMax yourself? I wouldn't recommend building your script around the assumption that all timelines will be TimelineMax instances. Is there a particular reason that'd be required? 

  • Like 1
Link to comment
Share on other sites

I was trying keep things short and sweet in an effort to minimize the time it takes to look at it. I'm rebuilding several different types of animations and each has several options as well as some randomness built in. So I have to be able to snag pretty much any animation, store the data and be able to rebuild it again from that data. For example, there are several things like labels that get set on timelines on various levels so I need to check any TimelineMax for the presence of labels.

 

I know this is beyond the normal use of GSAP so wasn't really asking for a solution to my unique issues. Just wanted to know "what's in there" and if there was a way to force all timelines returned by things like StaggerTo to be TimelineMax instead of Lite.

 

When you say, "why not just wrap that in a TimelineMax yourself?" are you saying as I'm going through the loop and detect a TimelineLite being returned by staggerTo, I can create a Max and add the Lite to it without breaking things?

 

What I've done at this point is convert any staggerTo to a series of "to's" like this...

 

See the Pen OpmLQv by swampthang (@swampthang) on CodePen

Link to comment
Share on other sites

Hm, I didn't see anything in that codepen that was getting tripped up due to the staggers being in a TimelineLite. Do you have a demo of something that's causing problems? I guess I'm just struggling to wrap my head around why you'd need the stagger methods to return a TimelineMax. 

 

If you're just saying that you wrote code that assumes everything is a TimelineMax, couldn't you just add a little conditional logic to sense when it's a TimelineLite instead and then skip looking for those Max-related methods/properties? 

  • Like 1
Link to comment
Share on other sites

What I finally realized was that staggerTo, staggerFrom, etc were being grouped with tween children. The reconstruction process for all other tweens worked as expected. I could store data in each of the tweens needed to reconstruct them. But, when a staggerTo was encountered, the child was a simple timeline that wrapped individual tweens. Those individual tweens were in an array each of which contained its own startTime. The good thing is that all the other data for every child tween of the stagger timeline was the same. So I just had to check in the loop for a type of "timeline" and reconstruct the stagger animation.

 

This is how I'm doing reconstructing the timelines - probably could be a lot more pithy but it's getting the job done. timelineData is the object that results from looping through the children using something like

See the Pen 7d3275656ef955c5ea2eb121c2407ba6?editors=0010 by osublake (@osublake) on CodePen

. A good bit of the other data was stored using the data object for timelines or tweens. For example, in a staggerTo I added the staggerTime value in the data object like:

tl.staggerTo(quoteSplit.words, 0.3, {autoAlpha:0, scale:0, ease:"magneticOut",data:{tweenType: 'staggerTo',staggerTime: 0.1, params:{autoAlpha:0, scale:0, ease:"magneticOut"}}}, 0.01, `+=${messageDisplayTime}`);

So in the reconstructing function....

var mainTL = new TimelineMax({startTime: timelineData[0].startTime});


mainTL.data = {id: timelineData[0].tlID};


console.log(timelineData);


timelineData[0].children.forEach(function(tlData,tlIndex){


// create the timeline instance and add the id and the startTime
var tl = new TimelineMax({data:{id: tlData.tlID}});
var tlStartTime = tlData.startTime;
// add labels if there are any
var labels = tlData.labels !== undefined ? labels : null;
if(labels != null) {
  if( labels.length > 0 ) {
    labels.forEach(function(lbl,lIndex){
      tl.add(lbl.name,lbl.time);
    });
  }
}


var tweenArr = tlData.children;


tweenArr.forEach(function(child,twIndex){


  var target,
      type,
      startTime = child.startTime;




  if(child.type == 'timeline') {
    // must be a staggerFrom, staggerTo or other animation with a timeline wrapper.
    // get the child data
    var stData = child.children[0].vars.data;
    type = stData.tweenType;
    var staggerTime = stData.staggerTime;
    var stDur = child.children[0].duration;
    var selectors = [];
    var delays = [];
    child.children.forEach(function(staggerChild,i){
      selectors.push(staggerChild.concatSelector);
      if( stData.params.cycle !== undefined ) {
        delays.push(staggerChild.delay);
      }
    });
    switch(type) {
      case 'staggerTo':
        if(delays.length>0) {
          stData.params.cycle = {
            delay: delays
          }
        }
        tl.staggerTo(selectors,stDur,stData.params,staggerTime,startTime);
      break;
      case 'staggerFrom':
        if(delays.length>0) {
          stData.params.cycle = {
            delay: delays
          }
        }
        tl.staggerFrom(selectors,stDur,stData.params,staggerTime,startTime);
      break;
      case 'staggerFromTo':
        tl.staggerFromTo(selectors,stDur,stData.params.from,stData.params.to,staggerTime,startTime);
      break;
    }
  } else {
    // must be an actual tween
    type = child.tweenType;


    var duration = child.duration,
        vars = child.vars,
        params = vars.data.params;


    if(child.isArray) {
      target = [];
      for( let sel of child.targetSelectors ) {
        target.push(sel);
      }
    } else {
      target = child.concatSelector;
    }


    switch(type) {
      case 'set':
        tl.set(target,params,startTime);
      break;
      case 'to':
        tl.to(target,duration,params,startTime);
      break;
      case 'from':
        tl.from(target,duration,params,startTime);
      break;
      case 'fromTo':
        tl.fromTo(target,duration,params.from,params.to,startTime);
      break;
    }
  }
});
 
tlStartTime = tlStartTime === undefined ? mainTL.duration() : tlStartTime;
mainTL.add(tl,tlStartTime);
I'm storing the timeline data using Some-SQL which is an awesome library. Well documented. It comes with an undo/redo setup which is allowing me to hook up an undo/redo for all the animations. Since there's a bunch of randomness in these animations, a user can click several times and then decide which one they want to keep. 
 
Hope that helps anyone else that might be wanting to do this. Feel free to message me if you have any questions that might not be answered here. Thanks, Blake and Jack, for all the help.
  • Like 1
Link to comment
Share on other sites

Updated the above to include a fix for a staggerFrom and staggerTo that had a cycle function with a delay parameter. Since there's not a good way to store a function in a cycle, I looped through the children and saved the delay value which is what I needed. This is, of course, not a complete set but it's all i needed.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...