css-houdini-drafts icon indicating copy to clipboard operation
css-houdini-drafts copied to clipboard

[css-animationworklet] Mechanism to pause/play an animation from inside worklet

Open majido opened this issue 6 years ago • 8 comments

From @RByers on September 22, 2016 15:53

For example, in the spring example it should be possible for the Animator to somehow say "I'm done now, stop invoking my animate function".

Maybe it should be more powerful, eg. "sleep for 5 seconds"? Or a main-thread API to resume the Animator?

Copied from original issue: WICG/animation-worklet#8

majido avatar Aug 29 '18 18:08 majido

From @appsforartists on September 27, 2016 21:21

pause implies that there's still potential work waiting to be done. at rest (or CSS transition's end) are better concepts for a spring that's run out of frames.

If functions were side-effect-free (#14) returning a falsey value would be a good way to represent the animation has come to rest.

majido avatar Aug 29 '18 18:08 majido

From @flackr on April 18, 2017 6:15

Why does returning a falsey value require side-effect-free functions? My only concern with this model is how we know when to start invoking the animation again.

Alternate suggestion, could we allow pausing the input timelines: e.g.

static timelines() { return ['document']; }
animate(elementMap, timelines) {
  // do stuff
  if (done) {
    timelines[0].pause();
    // animate will no longer be called every frame, only when
    // some other input has changed at which point you can call
    // play() on the timeline to resume updating every frame.
  }
}

majido avatar Aug 29 '18 18:08 majido

Pausing timeline is interesting though I wouldn't probably call it pause as it implies any other animation attached to the same timeline will also get paused. It is more like "detach/attach".

majido avatar Aug 29 '18 18:08 majido

From @flackr on April 18, 2017 8:0

I think each instance has its own timelines, since the registered timeline list only specifies how to construct the timelines but doesn't contain actual constructed timelines.

Another idea I had was to create a mutable timeline list that animate can add to / remove from. An animator could then remove the document timeline and readd when it was needed again. e.g.

animate(elementMap, timelines) {
  // do stuff
  if (done) {
    timelines.pop();
    // animate will no longer be called every frame, only when
    // some other input has changed.
  } else if (timelines.length == 0) {
    timelines.push(new DocumentTimeline());
  }
}

majido avatar Aug 29 '18 18:08 majido

After some recent discussion with @flackr and @stephenmcgruer , we think this may be a key feature to enable animation effects that are driven both by input events and time.

Consider a simple swipe-to-dismiss effect, which follows the user swipe gesture and when finger lifts then continues to completion (e.g., dismiss or return to original) with a curve that matches the swipe gesture's velocity.

With Animation Worklet, this can be modeled as a stateful animation which consumes both time and pointer events and have the following state machines:

SwipeToCompletionAnimation

Here are the three main states:

  1. Animation is idle, where it is paused so that it is not actively ticking
  2. As soon as the user touches down, the animation moves the target to follow the user touchpoint while staying paused (optionally calculate the movement velocity, and overall delta).
  3. As soon as the user lift their finger the animation will the switch to 'playing' so that it is ticked by time until it reaches its finished state. The final state may be decided on overall delta and velocity and the animation curve adapts to the movement velocity.

Note that while in (3), if the user touches down we go back to (2) which ensures responsiveness to user touch input.

To make this more concrete, here is something like this can be coded assuming we have the proposed APIs for pause/play from worklet and also receiving input events. Note that all the state machine transitions and various state data (velocity, phase) and internal to the animator. Main thread only needs to provide appropriate keyframes that can used to translate the element on the scroll as appropriate (e.g., Keyframes(target, {transform: ['translateX(-100vw)', 'translateX(100vw)']})


registerAnimator('swipe-to-dismiss', class SwipeAnimator extends StatefulAnimator {
  constructor(options, state = {velocity:0, phase: 'idle'}) {
    this.velocity = state.velocity;
    this.phase = state.phase;

    if (phase == 'idle') {
      // pause until we receive pointer events.
      this.pause();
    }

    // Assume we have an API to receive pointer events for our target.
    this.addEventListener("eventtargetadded", (event) => {
     for (type of ["pointerdown", "pointermove", "pointerup"])  {
        event.target.addEventListener(type,onPointerEvent );
     }
    });
  }

  onpointerevent(event) {
    if (event.type == "pointerdown" || event.type == "pointermove") {
      this.phase = "follow_pointer";
    } else {
      this.phase = "animate_to_completion";
      // Also decide what is the completion phase (e.g., hide or show)
    }

    this.pointer_position = event.screenX;

    // Allow the animation to play for *one* frame to react to the pointer event.
    this.play();
  }

  animate(currentTime, effect) {
    if (this.phase == "follow_pointer") {
      effect.localTime = position_curve(this.pointer_position);
      update_velocity(currentTime, this.pointer_position);
      // Pause, no need to produce frames until next pointer event
      this.pause();
    } else if (this.phase = "animate_to_completion") {
      effect.localTime = time_curve(currentTime, velocity);

      if (effect.localTime == 0 || effect.localTime == effect.duration) {
        // The animation is complete. Pause and become idle until next user interaction.
        this.phase = "idle";
        this.pause();
      } else {
        // Continue producing frames based on time until we complete or the user interacts again.
        this.play();
      }
    }


  }

  position_curve(x) {
    // map finger position to local time so we follow user's touch.
  }

  time_curve(time, velocity) {
    // Map current time delta and given movement velocity to appropriate local time so that over 
    // time we animate to a final position.
  }

  update_velocity(time, x) {
    this.velocity = (x - last_x) / (time - last_time);
    this.last_time = time;
    this.last_x = x;
  }

  state() {
    return {
      phase: this.phase,
      velocity: this.velocity
    }
  }
});

majido avatar Mar 27 '19 15:03 majido

BTW this is an example of swipe-to-dismiss/action effect that I am referring to https://twitter.com/kzzzf/status/917444054887124992

majido avatar Mar 27 '19 18:03 majido

Sounds like a great use case to try prototyping. The example from twitter appears to have four states (animating to origin vs animating to completion) and there may be other subtleties that help inform the API (e.g. factoring in the position of a second pointer down event).

birtles avatar Apr 01 '19 23:04 birtles

@majido What's the distinction between css-animationworklet-1 and 2 labels? IMO this feature is MVP in making this a usable API.

From my current hacking around, even animations that are told to run for x milliseconds run indefinitely. This kinda makes sense, as passing duration to a spring simulation animation is redundant, and we want that kind of animation to run for as long as it needs to (but only as long as it needs to). Without an animation being able to end itself we'll just end up spawning more workers as animations are initialised without culling old ones.

IMO the swipe-to-dismiss example demonstrates a great API to have available.

mattgperry avatar Jan 16 '20 15:01 mattgperry