<!DOCTYPE html>
<meta charset=utf-8>
<title>Setting the timeline of scroll animation</title>
<link rel="help"
      href="https://drafts.csswg.org/web-animations-1/#setting-the-timeline">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<script src="testcommon.js"></script>
<style>
  .scroller {
    overflow-x: hidden;
    overflow-y: auto;
    height: 200px;
    width: 100px;
    will-change: transform;
  }
  .contents {
    /* The height is set to align scrolling in pixels with logical time in ms */
    height: 1200px;
    width: 100%;
  }
  @keyframes anim {
    from { opacity: 0; }
    to { opacity: 1; }
  }
  .anim {
    animation: anim 1s paused linear;
  }
  #target {
    height:  100px;
    width:  100px;
    background-color: green;
    margin-top: -1000px;
  }
</style>
<body>
<script>
'use strict';

function createAnimation(t) {
  const elem = createDiv(t);
  const animation = elem.animate({ opacity: [1, 0] }, 1000);
  return animation;
}

function createPausedCssAnimation(t) {
  const elem = createDiv(t);
  elem.classList.add('anim');
  return elem.getAnimations()[0];
}

function updateScrollPosition(timeline, offset) {
  const scroller = timeline.source;
  assert_true(!!scroller, 'source is resolved');
  scroller.scrollTop = offset;
  // Wait for new animation frame which allows the timeline to compute new
  // current time.
  return waitForNextFrame();
}

function assert_timeline_current_time(animation, timeline_current_time) {
  if (animation.currentTime instanceof CSSUnitValue){
    assert_percents_equal(animation.timeline.currentTime, timeline_current_time,
                          `Timeline's currentTime aligns with the scroll ` +
                          `position even when paused`);
  }
  else {
    assert_times_equal(animation.timeline.currentTime, timeline_current_time,
                       `Timeline's currentTime aligns with the scroll ` +
                       `position even when paused`);
  }
}

function assert_scroll_synced_times(animation, timeline_current_time,
                                    animation_current_time) {
  assert_timeline_current_time(animation, timeline_current_time);
  if (animation.currentTime instanceof CSSUnitValue){
    assert_percents_equal(animation.currentTime, animation_current_time,
        `Animation's currentTime aligns with the scroll position`);
  }
  else {
    assert_times_equal(animation.currentTime, animation_current_time,
        `Animation's currentTime aligns with the scroll position`);
  }
}

function assert_paused_times(animation, timeline_current_time,
                             animation_current_time) {
  assert_timeline_current_time(animation, timeline_current_time);
  if (animation.currentTime instanceof CSSUnitValue){
    assert_percents_equal(animation.currentTime, animation_current_time,
                          `Animation's currentTime is fixed while paused`);
  }
  else {
    assert_times_equal(animation.currentTime, animation_current_time,
                       `Animation's currentTime is fixed while paused`);
  }
}

function createViewTimeline(t) {
  const parent = document.querySelector('.scroller');
  const elem = document.createElement('div');
  elem.id = 'target';
  t.add_cleanup(() => {
    elem.remove();
  });
  parent.appendChild(elem);
  return new ViewTimeline({ subject: elem });
}

promise_test(async t => {
  const scrollTimeline = createScrollTimeline(t);
  await updateScrollPosition(scrollTimeline, 100);

  const animation = createAnimation(t);
  animation.timeline = scrollTimeline;
  assert_true(animation.pending);
  await animation.ready;

  assert_equals(animation.playState, 'running');
  assert_scroll_synced_times(animation, 10, 10);
}, 'Setting a scroll timeline on a play-pending animation synchronizes ' +
   'currentTime of the animation with the scroll position.');

promise_test(async t => {
  const scrollTimeline = createScrollTimeline(t);
  await updateScrollPosition(scrollTimeline, 100);

  const animation = createAnimation(t);
  animation.pause();
  animation.timeline = scrollTimeline;
  assert_true(animation.pending);
  await animation.ready;

  assert_equals(animation.playState, 'paused');
  assert_paused_times(animation, 10, 0);

  await updateScrollPosition(animation.timeline, 200);

  assert_equals(animation.playState, 'paused');
  assert_paused_times(animation, 20, 0);

  animation.play();
  await animation.ready;

  assert_scroll_synced_times(animation, 20, 20);
}, 'Setting a scroll timeline on a pause-pending animation fixes the ' +
   'currentTime of the animation based on the scroll position once resumed');

promise_test(async t => {
  const scrollTimeline = createScrollTimeline(t);
  await updateScrollPosition(scrollTimeline, 100);

  const animation = createAnimation(t);
  animation.reverse();
  animation.timeline = scrollTimeline;
  await animation.ready;

  assert_equals(animation.playState, 'running');
  assert_scroll_synced_times(animation, 10, 90);
},  'Setting a scroll timeline on a reversed play-pending animation ' +
    'synchronizes the currentTime of the animation with the scroll ' +
    'position.');

promise_test(async t => {
  const scrollTimeline = createScrollTimeline(t);
  await updateScrollPosition(scrollTimeline, 100);

  const animation = createAnimation(t);
  await animation.ready;

  animation.timeline = scrollTimeline;
  assert_true(animation.pending);
  assert_equals(animation.playState, 'running');
  await animation.ready;
  assert_scroll_synced_times(animation, 10, 10);
},  'Setting a scroll timeline on a running animation synchronizes the ' +
    'currentTime of the animation with the scroll position.');

promise_test(async t => {
  const scrollTimeline = createScrollTimeline(t);
  await updateScrollPosition(scrollTimeline, 100);

  const animation = createAnimation(t);
  animation.pause();
  await animation.ready;

  animation.timeline = scrollTimeline;
  assert_false(animation.pending);
  assert_equals(animation.playState, 'paused');
  assert_paused_times(animation, 10, 0);

  animation.play();
  await animation.ready;

  assert_scroll_synced_times(animation, 10, 10);
}, 'Setting a scroll timeline on a paused animation fixes the currentTime of ' +
   'the animation based on the scroll position when resumed');

promise_test(async t => {
  const scrollTimeline = createScrollTimeline(t);
  await updateScrollPosition(scrollTimeline, 100);

  const animation = createAnimation(t);
  animation.reverse();
  animation.pause();
  await animation.ready;

  animation.timeline = scrollTimeline;
  assert_false(animation.pending);
  assert_equals(animation.playState, 'paused');
  assert_paused_times(animation, 10, 100);

  animation.play();
  await animation.ready;

  assert_scroll_synced_times(animation, 10, 90);
}, 'Setting a scroll timeline on a reversed paused animation ' +
   'fixes the currentTime of the animation based on the scroll ' +
   'position when resumed');

promise_test(async t => {
  const animation = createAnimation(t);
  const scrollTimeline = createScrollTimeline(t);
  animation.timeline = scrollTimeline;
  await animation.ready;
  await updateScrollPosition(scrollTimeline, 100);

  animation.timeline = document.timeline;
  assert_times_equal(animation.currentTime, 100);
}, 'Transitioning from a scroll timeline to a document timeline on a running ' +
   'animation preserves currentTime');

promise_test(async t => {
  const animation = createAnimation(t);
  const scrollTimeline = createScrollTimeline(t);
  animation.timeline = scrollTimeline;
  await animation.ready;
  await updateScrollPosition(scrollTimeline, 100);

  animation.pause();
  animation.timeline = document.timeline;

  await animation.ready;
  assert_times_equal(animation.currentTime, 100);
}, 'Transitioning from a scroll timeline to a document timeline on a ' +
   'pause-pending animation preserves currentTime');

promise_test(async t => {
  const animation = createAnimation(t);
  const scrollTimeline = createScrollTimeline(t);
  animation.timeline = scrollTimeline;

  animation.reverse();
  await animation.ready;
  await updateScrollPosition(scrollTimeline, 100);

  animation.pause();
  await animation.ready;

  assert_scroll_synced_times(animation, 10, 90);

  animation.timeline = document.timeline;
  assert_false(animation.pending);
  assert_equals(animation.playState, 'paused');
  assert_times_equal(animation.currentTime, 900);
}, 'Transition from a scroll timeline to a document timeline on a reversed ' +
   'paused animation maintains correct currentTime');

promise_test(async t => {
  const animation = createAnimation(t);
  const scrollTimeline = createScrollTimeline(t);
  animation.timeline = scrollTimeline;
  await animation.ready;
  await updateScrollPosition(scrollTimeline, 100);

  const progress = animation.currentTime.value / 100;
  const duration = animation.effect.getTiming().duration;
  animation.timeline = null;

  const expectedCurrentTime = progress * duration;
  assert_times_equal(animation.currentTime, expectedCurrentTime);
}, 'Transitioning from a scroll timeline to a null timeline on a running ' +
    'animation preserves current progress.');

promise_test(async t => {
  const keyframeEfect = new KeyframeEffect(createDiv(t),
                                           { opacity: [0, 1] },
                                           1000);
  const animation = new Animation(keyframeEfect, null);
  animation.startTime = 0;
  assert_equals(animation.playState, 'running');

  const scrollTimeline = createScrollTimeline(t);
  await updateScrollPosition(scrollTimeline, 100);

  animation.timeline = scrollTimeline;
  assert_equals(animation.playState, 'running');
  await animation.ready;

  assert_percents_equal(animation.currentTime, 10);
}, 'Switching from a null timeline to a scroll timeline on an animation with ' +
   'a resolved start time preserved the play state');

promise_test(async t => {
  const firstScrollTimeline = createScrollTimeline(t);
  await updateScrollPosition(firstScrollTimeline, 100);

  const secondScrollTimeline = createScrollTimeline(t);
  await updateScrollPosition(secondScrollTimeline, 200);

  const animation = createAnimation(t);
  animation.timeline = firstScrollTimeline;
  await animation.ready;
  assert_percents_equal(animation.currentTime, 10);

  animation.timeline = secondScrollTimeline;
  await animation.ready;

  assert_percents_equal(animation.currentTime, 20);
}, 'Switching from one scroll timeline to another updates currentTime');

promise_test(async t => {
  const scrollTimeline = createScrollTimeline(t);
  await updateScrollPosition(scrollTimeline, 100);

  const animation = createPausedCssAnimation(t);
  animation.timeline = scrollTimeline;
  await animation.ready;
  assert_equals(animation.playState, 'paused');
  assert_percents_equal(animation.currentTime, 0);

  const target = animation.effect.target;
  target.style.animationPlayState = 'running';
  await animation.ready;

  assert_percents_equal(animation.currentTime, 10);
}, 'Switching from a document timeline to a scroll timeline updates ' +
   'currentTime when unpaused via CSS.');

promise_test(async t => {
  const scrollTimeline = createScrollTimeline(t);
  await updateScrollPosition(scrollTimeline, 100);

  const animation = createAnimation(t);
  animation.pause();
  animation.currentTime = 500; // 50%
  animation.timeline = scrollTimeline;
  await animation.ready;
  assert_percents_equal(animation.currentTime, 50);

  animation.play();
  await animation.ready;
  assert_percents_equal(animation.currentTime, 10);
}, 'Switching from a document timeline to a scroll timeline and updating ' +
   'currentTime preserves the progress while paused.');

promise_test(async t => {
  const elem = createDiv(t);
  const animation = elem.animate(null, Infinity);
  await animation.ready;

  animation.timeline = new ScrollTimeline();
  let timing = animation.effect.getComputedTiming();
  assert_percents_equal(timing.endTime, 100);
  assert_percents_equal(timing.activeDuration, 100);
  assert_percents_equal(timing.duration, 100);

  animation.effect.updateTiming({ iterations: 2 });
  timing = animation.effect.getComputedTiming();
  assert_percents_equal(timing.endTime, 100);
  assert_percents_equal(timing.activeDuration, 100);
  assert_percents_equal(timing.duration, 50);

  // Blink implementation does not permit setting an infinite number of
  // iterations on a scroll-linked animation. Workaround by temporarily
  // switching back to a document timeline.
  animation.timeline = document.timeline;
  animation.effect.updateTiming({ iterations: Infinity });
  animation.timeline = new ScrollTimeline();
  timing = animation.effect.getComputedTiming();
  // Having an infinite number of iterations with a finite timeline results in
  // each iteration having zero duration.
  assert_percents_equal(timing.duration, 0);
  // If either the iteration duration or iteration count is zero, the active
  // duration is always zero.
  assert_percents_equal(timing.activeDuration, 0);
  assert_percents_equal(timing.endTime, 0);

}, 'Switching from a document timeline to a scroll timeline on an infinite ' +
   'duration animation.');


promise_test(async t => {
  const scrollTimeline = createScrollTimeline(t);
  const view_timeline = createViewTimeline(t);
  await updateScrollPosition(scrollTimeline, 100);
  const animation = createAnimation(t);
  animation.timeline = scrollTimeline;
  // Range name is ignored while attached to a non-view scroll-timeline.
  // Offsets are still applied to the scroll-timeline.
  animation.rangeStart = { rangeName: 'contain', offset: CSS.percent(10) };
  animation.rangeEnd = { rangeName: 'contain', offset: CSS.percent(90) };
  await animation.ready;

  assert_scroll_synced_times(animation, 10, 0);
  assert_percents_equal(animation.startTime, 10);

  animation.timeline = view_timeline;
  assert_true(animation.pending);
  await animation.ready;

  // Cover range is [0px, 300px]
  // Contain range is [100px, 200px]
  // start time = (contain 10% pos - cover start pos) / cover range * 100%
  const expected_start_time = 110 / 300 * 100;
  // timeline time = (scroll pos - cover start pos) / cover range * 100%
  const expected_timeline_time = 100 / 300 * 100;
  // current time = timeline time - start time.
  const expected_current_time = expected_timeline_time - expected_start_time;

  assert_percents_equal(animation.startTime, expected_start_time);
  assert_percents_equal(animation.timeline.currentTime, expected_timeline_time);
  assert_percents_equal(animation.currentTime, expected_current_time);
}, 'Changing from a scroll-timeline to a view-timeline updates start time.');

</script>
</body>
