Bug 1253476 - Implement Animation::Persist; r=boris,bzbarsky
authorBrian Birtles <birtles@gmail.com>
Mon, 20 May 2019 05:22:22 +0000
changeset 474491 16dbeeceab3854e49afa681ddffa0bfe8b943986
parent 474490 d540c77bd298cabf32ebfca673a47f2e34c9ed9f
child 474492 b74b84baf7176ceb375f0f891511e5687470493b
push id36040
push userrgurzau@mozilla.com
push dateMon, 20 May 2019 13:43:21 +0000
treeherdermozilla-central@319a369ccde4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersboris, bzbarsky
bugs1253476
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1253476 - Implement Animation::Persist; r=boris,bzbarsky https://drafts.csswg.org/web-animations-1/#dom-animation-persist Differential Revision: https://phabricator.services.mozilla.com/D30325
dom/animation/Animation.cpp
dom/animation/Animation.h
dom/animation/test/chrome/test_animation_observers_async.html
dom/webidl/Animation.webidl
testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-replaced-animations.html
testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations.html
testing/web-platform/tests/web-animations/interfaces/Animation/persist.html
testing/web-platform/tests/web-animations/interfaces/Animation/style-change-events.html
--- a/dom/animation/Animation.cpp
+++ b/dom/animation/Animation.cpp
@@ -591,16 +591,33 @@ void Animation::Reverse(ErrorResult& aRv
   if (aRv.Failed()) {
     mPendingPlaybackRate = originalPendingPlaybackRate;
   }
 
   // Play(), above, unconditionally calls PostUpdate so we don't need to do
   // it here.
 }
 
+void Animation::Persist() {
+  if (mReplaceState == AnimationReplaceState::Persisted) {
+    return;
+  }
+
+  bool wasRemoved = mReplaceState == AnimationReplaceState::Removed;
+
+  mReplaceState = AnimationReplaceState::Persisted;
+
+  // If the animation is not (yet) removed, there should be no side effects of
+  // persisting it.
+  if (wasRemoved) {
+    UpdateEffect(PostRestyleMode::IfNeeded);
+    PostUpdate();
+  }
+}
+
 // ---------------------------------------------------------------------------
 //
 // JS wrappers for Animation interface:
 //
 // ---------------------------------------------------------------------------
 
 Nullable<double> Animation::GetStartTimeAsDouble() const {
   return AnimationUtils::TimeDurationToDouble(mStartTime);
--- a/dom/animation/Animation.h
+++ b/dom/animation/Animation.h
@@ -136,16 +136,18 @@ class Animation : public DOMEventTargetH
    * in future we will likely have to flush style in
    * CSSAnimation::PauseFromJS so we leave it for now.
    */
   void PauseFromJS(ErrorResult& aRv) { Pause(aRv); }
 
   void UpdatePlaybackRate(double aPlaybackRate);
   void Reverse(ErrorResult& aRv);
 
+  void Persist();
+
   bool IsRunningOnCompositor() const;
 
   virtual void Tick();
   bool NeedsTicks() const {
     return Pending() ||
            (PlayState() == AnimationPlayState::Running &&
             // An animation with a zero playback rate doesn't need ticks even if
             // it is running since it effectively behaves as if it is paused.
--- a/dom/animation/test/chrome/test_animation_observers_async.html
+++ b/dom/animation/test/chrome/test_animation_observers_async.html
@@ -616,22 +616,36 @@ promise_test(async t => {
     [
       { added: [], changed: [animA], removed: [] },
       { added: [], changed: [animB], removed: [] },
       { added: [], changed: [], removed: [animA] },
     ],
     'records after finishing'
   );
 
+  // Restore animA.
+  animA.persist();
+
+  // Wait for the MutationRecord corresponding to the re-addition of animA.
+  await waitForNextFrame();
+
+  assert_records(
+    [{ added: [animA], changed: [], removed: [] }],
+    'records after persisting'
+  );
+
   // Tidy up
   animA.cancel();
   animB.cancel();
 
   await waitForNextFrame();
 
   assert_records(
-    [{ added: [], changed: [], removed: [animB] }],
+    [
+      { added: [], changed: [], removed: [animA] },
+      { added: [], changed: [], removed: [animB] },
+    ],
     'records after tidying up end'
   );
 }, 'Animations automatically removed are reported');
 
 runTest();
 </script>
--- a/dom/webidl/Animation.webidl
+++ b/dom/webidl/Animation.webidl
@@ -47,14 +47,16 @@ interface Animation : EventTarget {
   void finish ();
   [Throws, BinaryName="playFromJS"]
   void play ();
   [Throws, BinaryName="pauseFromJS"]
   void pause ();
   void updatePlaybackRate (double playbackRate);
   [Throws]
   void reverse ();
+  [Pref="dom.animations-api.autoremove.enabled"]
+  void persist ();
 };
 
 // Non-standard extensions
 partial interface Animation {
   [ChromeOnly] readonly attribute boolean isRunningOnCompositor;
 };
--- a/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-replaced-animations.html
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-replaced-animations.html
@@ -69,10 +69,93 @@ promise_test(async t => {
   // (If animA were not removed it would be 0.2 + 0.3 = 0.5.)
   assert_opacity_value(
     getComputedStyle(div).opacity,
     0.4,
     'Opacity value should not include the removed animation'
   );
 }, 'Removed animations do not contribute to the effect stack');
 
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.opacity = '0.1';
+
+  const animA = div.animate(
+    { opacity: 0.2 },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { opacity: 0.3 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  await animA.finished;
+
+  animA.persist();
+
+  animB.cancel();
+  assert_opacity_value(
+    getComputedStyle(div).opacity,
+    0.2,
+    "Opacity should be the persisted animation's value"
+  );
+}, 'Persisted animations contribute to animated style');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.opacity = '0.1';
+
+  const animA = div.animate(
+    { opacity: 0.2 },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { opacity: 0.3, composite: 'add' },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  await animA.finished;
+
+  assert_opacity_value(
+    getComputedStyle(div).opacity,
+    0.4,
+    'Opacity value should NOT include the contribution of the removed animation'
+  );
+
+  animA.persist();
+
+  assert_opacity_value(
+    getComputedStyle(div).opacity,
+    0.5,
+    'Opacity value should include the contribution of the persisted animation'
+  );
+}, 'Persisted animations contribute to the effect stack');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.opacity = '0.1';
+
+  const animA = div.animate(
+    { opacity: 0.2 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  // Persist the animation before it finishes (and before it would otherwise be
+  // removed).
+  animA.persist();
+
+  const animB = div.animate(
+    { opacity: 0.3, composite: 'add' },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  await animA.finished;
+
+  assert_opacity_value(
+    getComputedStyle(div).opacity,
+    0.5,
+    'Opacity value should include the contribution of the persisted animation'
+  );
+}, 'Animations persisted before they would be removed contribute to the'
+   + ' effect stack');
+
 </script>
 </body>
--- a/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations.html
+++ b/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations.html
@@ -216,16 +216,28 @@ promise_test(async t => {
   const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
   await animA.finished;
 
   assert_array_equals(div.getAnimations(), [animB]);
 }, 'Does not return an animation that has been removed');
 
 promise_test(async t => {
   const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animA.finished;
+
+  animA.persist();
+
+  assert_array_equals(div.getAnimations(), [animA, animB]);
+}, 'Returns an animation that has been persisted');
+
+promise_test(async t => {
+  const div = createDiv(t);
   const watcher = EventWatcher(t, div, 'transitionrun');
 
   // Create a covering animation to prevent transitions from firing after
   // calling getAnimations().
   const coveringAnimation = new Animation(
     new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC)
   );
 
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/persist.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Animation.persist</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-persist">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+async_test(t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+  animA.onremove = t.step_func_done(() => {
+    assert_equals(animA.replaceState, 'removed');
+    animA.persist();
+    assert_equals(animA.replaceState, 'persisted');
+  });
+}, 'Allows an animation to be persisted after being removed');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+  animA.persist();
+
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'persisted');
+}, 'Allows an animation to be persisted before being removed');
+
+</script>
+</body>
--- a/testing/web-platform/tests/web-animations/interfaces/Animation/style-change-events.html
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/style-change-events.html
@@ -228,16 +228,36 @@ const tests = {
       const animation = elem.animate({ opacity: [0.5, 1] }, 100 * MS_PER_SEC);
       animation.finish();
       return animation;
     },
     test: animation => {
       animation.reverse();
     },
   }),
+  persist: PlayAnimationTest({
+    setup: async elem => {
+      // Create an animation whose replaceState is 'removed'.
+      const animA = elem.animate(
+        { opacity: 1 },
+        { duration: 1, fill: 'forwards' }
+      );
+      const animB = elem.animate(
+        { opacity: 1 },
+        { duration: 1, fill: 'forwards' }
+      );
+      await animA.finished;
+      animB.cancel();
+
+      return animA;
+    },
+    test: animation => {
+      animation.persist();
+    },
+  }),
   get ['Animation constructor']() {
     let originalElem;
     return UsePropertyTest({
       setup: elem => {
         originalElem = elem;
         // Return a dummy animation so the caller has something to wait on
         return elem.animate(null);
       },
@@ -279,17 +299,17 @@ for (const key of properties) {
     // Setup target element
     const div = createDiv(t);
     let gotTransition = false;
     div.addEventListener('transitionrun', () => {
       gotTransition = true;
     });
 
     // Setup animation
-    const animation = setup(div);
+    const animation = await setup(div);
 
     // Setup transition start point
     div.style.transition = 'opacity 100s';
     getComputedStyle(div).opacity;
 
     // Update specified style but don't flush
     div.style.opacity = '0.5';