Help with Synchronized Animations


#1

Is it possible to synchronize two or more animated elements? Just to demonstrate what I am asking for, I created calendar control. This control has a header element that shows the month/year and a content element that shows the days. When the user changes the month: both the header and the content should animate to the next month

Is it possible to make these two elements animate their transitions at the same time. Here is a simplified Codepen example that roughly demonstrates what I have so far: https://codepen.io/GooeyIdeas/pen/GemaKy


#2

What you showed is easily achievable with pure CSS. What do you have in mind that you think is trouble some to implement?


#3

For this example. I have more functionality to add, transitioning from yearly, monthly, weekly and daily views will change the used view/viewmodel and each will have an transition that is defined by how you changed the state. I will have several compose tags with animated changes that I need to trigger at the same time.


#4

I’m working on some details to make this sort of thing easier in general in vNext, but for your specific example you could consider using the EventAggregator to synchronously send a command to all components at the same time, instead of attempting to let the animation plugin do the syncing. It would work if all animations have equal length.

For example:

export class FooCustomElement {
  static inject = [EventAggregator];

  constructor(ea) {
    ea.subscribe('animate', (data) => {
      this.animate(data);
    });
  }

  animate(data) {
    // your magic
  }

  handleClick() {
    this.ea.publish('animate', { ... });
  }
}
export class BarCustomElement {
  static inject = [EventAggregator];

  constructor(ea) {
    ea.subscribe('animate', (data) => {
      this.animate(data);
    });
  }

  animate(data) {
    // your magic
  }

  handleClick() {
    this.ea.publish('animate', { ... });
  }
}

It’d be the same pattern in each element. An action in any of them will trigger the animation in all of them. Does that sound feasible?


#5

That was going to be my last resort option as it would require a lot of extra overhead. I was hoping to use a custom AureliaAnimator and rely on the compositionEngine’s built in swapping functionality to manage the creation, removal and even caching of views and view-models.

I think what I really need is a way to interact with the TaskQueue and pop off multiple tasks simultaneously…


#6

I think what I really need is a way to interact with the TaskQueue and pop off multiple tasks simultaneously…

That’s tricky in vCurrent. Tasks are executed synchronously in order once the queue starts processing. You could ‘trick’ the task queue by queueing promises, so it will think the task is done when really the promise has only started executing. I’m not knowledgeable enough about the internals of the animator to be able to tell you how, though. Maybe one of the others can chime in here @bigopon @EisenbergEffect?


#7

As @fkleuver mentions, this is something that we’re making easier in vNext. It’s a good example of the sort of design changes we’re making there so that this kind of thing is much more intuitive in the future. We’re all still learning here as we go.

Unfortunately, it’s not so easy in vCurrent. If it were me, I’d probably take the approach that @fkleuver gives on using the event aggregator. After you get into it a bit, you might find that you can extract something generalizable that lets you eliminate some of the boiler plate. Based on your description, I don’t think it would be too bad. Alternatively, you might look at the Velocity Animator. We’ve had it for a while but haven’t put much focus on it since it was originally written. Check this out: https://gooy.github.io/aurelia-animator-velocity/#/sequences

Side Note: We need to get those docs out of there and into our main site. I’ll follow up with the core team.


#8

@EisenbergEffect I think I found a reliable way to achieve the functionality I need. I extended the swapStrategies object to allow the swapping logic to be deferred. Then I used a custom binding behavior to point the swap-order to its deferred variant. By caching the promise returned by the swapping strategy on the view-slot, the binding behavior can properly mange binding updates that occur while the deferred animation is running. It is not the cleanest solution but so far it seems to work: https://codepen.io/GooeyIdeas/pen/jJGZmK?editors=1010

Would this approach, be safe to implement?

<compose view-model.bind="vmTitle & defer:vmChanged"  
         swap-order="with"></compose>
<compose view-model.bind="vmContent & defer:vmChanged"  
         swap-order="with"></compose>
const swapStrategies = ['with', 'after', 'before'];

swapStrategies.forEach((strategy) => {
  au.SwapStrategies[strategy + 'Deferred'] = function (
    viewSlot: any,
    previous: any,
    callback: any
  ) {
    let currentPromise = au.SwapStrategies[strategy](viewSlot, previous, callback);
    viewSlot.previousPromise = currentPromise;
    return Promise.resolve(false);
  };  
})

const interceptMethods = ['updateTarget', 'updateSource', 'callSource'];

function interceptor(method: any, update: any, intercept: any, value: any) {
  if (this.target.swapOrder && this.target.swapOrder.indexOf('Deferred') === -1) {
    this.target.swapOrder = this.target.swapOrder + 'Deferred';
  }
  Promise.resolve(this.target.viewSlot.previousPromise).then(() => {
    if (typeof intercept === 'function') {
      value = intercept.call(this.source.bindingContext, value) || value;
    }
    update(value);    
  });
}

@inject(au.BindingEngine)
class DeferBindingBehavior {
  constructor(BindingEngine:any) {
    this.bindingEngine = BindingEngine;
  }
  
  bind(binding:any, scope:any, intercept:any) {
    let i = interceptMethods.length;
    while (i--) {
      let method = interceptMethods[i];
      if (!binding[method]) {
        continue;
      }
      binding[`intercepted-${method}`] = binding[method];
      let update = binding[method].bind(binding);
      binding[method] = interceptor.bind(binding, method, update, intercept);
    }
  }

  unbind(binding:any, scope:any) {
    let i = interceptMethods.length;
    while (i--) {
      let method = interceptMethods[i];
      if (!binding[method]) {
        continue;
      }

      binding[method] = binding[`intercepted-${method}`];
      binding[`intercepted-${method}`] = null;
    }
  }
}

#9

That’s pretty dang clever, I have to say.