Commit 682affef authored by Olga Gerchikov's avatar Olga Gerchikov Committed by Commit Bot

Implemented setPlaybackRate for scroll-linked Worklet Animations.

• Updated logic of calculating initial current time of non-composited scroll-linked animations to
  ensure that the start time is always initialized to zero.
• Changed signature of cc::ScrollTimeline::CurrentTime to return base::Optional<base::TimeTicks>.
  This is to enforce cc: WorkletAnimation::CurrentTime operating in the same units of
  base::TimeTicks and correctly handling NaN values.
• Adjusted start time of scroll-linked animations when playback rate is updated.


Bug: 852475
Change-Id: Iffd966c1b3ef6d821eef7cc2f6f79dae790a69f3
Reviewed-on: https://chromium-review.googlesource.com/c/1461513
Commit-Queue: Olga Gerchikov <gerchiko@microsoft.com>
Reviewed-by: default avatarStephen McGruer <smcgruer@chromium.org>
Reviewed-by: default avatarMajid Valipour <majidvp@chromium.org>
Cr-Commit-Position: refs/heads/master@{#636836}
parent 0fe77068
...@@ -43,14 +43,15 @@ std::unique_ptr<ScrollTimeline> ScrollTimeline::CreateImplInstance() const { ...@@ -43,14 +43,15 @@ std::unique_ptr<ScrollTimeline> ScrollTimeline::CreateImplInstance() const {
end_scroll_offset_, time_range_); end_scroll_offset_, time_range_);
} }
double ScrollTimeline::CurrentTime(const ScrollTree& scroll_tree, base::Optional<base::TimeTicks> ScrollTimeline::CurrentTime(
bool is_active_tree) const { const ScrollTree& scroll_tree,
bool is_active_tree) const {
// We may be asked for the CurrentTime before the pending tree with our // We may be asked for the CurrentTime before the pending tree with our
// scroller has been activated, or after the scroller has been removed (e.g. // scroller has been activated, or after the scroller has been removed (e.g.
// if it is no longer composited). In these cases the best we can do is to // if it is no longer composited). In these cases the best we can do is to
// return an unresolved time value. // return an unresolved time value.
if ((is_active_tree && !active_id_) || (!is_active_tree && !pending_id_)) if ((is_active_tree && !active_id_) || (!is_active_tree && !pending_id_))
return std::numeric_limits<double>::quiet_NaN(); return base::nullopt;
ElementId scroller_id = ElementId scroller_id =
is_active_tree ? active_id_.value() : pending_id_.value(); is_active_tree ? active_id_.value() : pending_id_.value();
...@@ -60,7 +61,7 @@ double ScrollTimeline::CurrentTime(const ScrollTree& scroll_tree, ...@@ -60,7 +61,7 @@ double ScrollTimeline::CurrentTime(const ScrollTree& scroll_tree,
const ScrollNode* scroll_node = const ScrollNode* scroll_node =
scroll_tree.FindNodeFromElementId(scroller_id); scroll_tree.FindNodeFromElementId(scroller_id);
if (!scroll_node) if (!scroll_node)
return std::numeric_limits<double>::quiet_NaN(); return base::nullopt;
gfx::ScrollOffset offset = gfx::ScrollOffset offset =
scroll_tree.GetPixelSnappedScrollOffset(scroll_node->id); scroll_tree.GetPixelSnappedScrollOffset(scroll_node->id);
...@@ -87,7 +88,7 @@ double ScrollTimeline::CurrentTime(const ScrollTree& scroll_tree, ...@@ -87,7 +88,7 @@ double ScrollTimeline::CurrentTime(const ScrollTree& scroll_tree,
// unresolved time value if fill is none or forwards, or 0 otherwise. // unresolved time value if fill is none or forwards, or 0 otherwise.
// TODO(smcgruer): Implement |fill|. // TODO(smcgruer): Implement |fill|.
if (current_offset < resolved_start_scroll_offset) { if (current_offset < resolved_start_scroll_offset) {
return std::numeric_limits<double>::quiet_NaN(); return base::nullopt;
} }
// 4. If current scroll offset is greater than or equal to endScrollOffset: // 4. If current scroll offset is greater than or equal to endScrollOffset:
...@@ -97,24 +98,26 @@ double ScrollTimeline::CurrentTime(const ScrollTree& scroll_tree, ...@@ -97,24 +98,26 @@ double ScrollTimeline::CurrentTime(const ScrollTree& scroll_tree,
// value. // value.
// TODO(smcgruer): Implement |fill|. // TODO(smcgruer): Implement |fill|.
if (resolved_end_scroll_offset < max_offset) if (resolved_end_scroll_offset < max_offset)
return std::numeric_limits<double>::quiet_NaN(); return base::nullopt;
// Otherwise, return the effective time range. // Otherwise, return the effective time range.
return time_range_; return base::TimeTicks() + base::TimeDelta::FromMillisecondsD(time_range_);
} }
// This is not by the spec, but avoids a negative current time. // This is not by the spec, but avoids a negative current time.
// See https://github.com/WICG/scroll-animations/issues/20 // See https://github.com/WICG/scroll-animations/issues/20
if (resolved_start_scroll_offset >= resolved_end_scroll_offset) { if (resolved_start_scroll_offset >= resolved_end_scroll_offset) {
return std::numeric_limits<double>::quiet_NaN(); return base::nullopt;
} }
// 5. Return the result of evaluating the following expression: // 5. Return the result of evaluating the following expression:
// ((current scroll offset - startScrollOffset) / // ((current scroll offset - startScrollOffset) /
// (endScrollOffset - startScrollOffset)) * effective time range // (endScrollOffset - startScrollOffset)) * effective time range
return ((current_offset - resolved_start_scroll_offset) / return base::TimeTicks() +
(resolved_end_scroll_offset - resolved_start_scroll_offset)) * base::TimeDelta::FromMillisecondsD(
time_range_; ((current_offset - resolved_start_scroll_offset) /
(resolved_end_scroll_offset - resolved_start_scroll_offset)) *
time_range_);
} }
void ScrollTimeline::PushPropertiesTo(ScrollTimeline* impl_timeline) { void ScrollTimeline::PushPropertiesTo(ScrollTimeline* impl_timeline) {
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#define CC_ANIMATION_SCROLL_TIMELINE_H_ #define CC_ANIMATION_SCROLL_TIMELINE_H_
#include "base/optional.h" #include "base/optional.h"
#include "base/time/time.h"
#include "cc/animation/animation_export.h" #include "cc/animation/animation_export.h"
#include "cc/trees/element_id.h" #include "cc/trees/element_id.h"
...@@ -38,11 +39,13 @@ class CC_ANIMATION_EXPORT ScrollTimeline { ...@@ -38,11 +39,13 @@ class CC_ANIMATION_EXPORT ScrollTimeline {
// compositor. // compositor.
std::unique_ptr<ScrollTimeline> CreateImplInstance() const; std::unique_ptr<ScrollTimeline> CreateImplInstance() const;
// Calculate the current time of the ScrollTimeline. This is either a double // Calculate the current time of the ScrollTimeline. This is either a
// value or std::numeric_limits<double>::quiet_NaN() if the current time is // base::TimeTicks value or base::nullopt if the current time is unresolved.
// unresolved. // The internal calculations are performed using doubles and the result is
virtual double CurrentTime(const ScrollTree& scroll_tree, // converted to base::TimeTicks. This limits the precision to 1us.
bool is_active_tree) const; virtual base::Optional<base::TimeTicks> CurrentTime(
const ScrollTree& scroll_tree,
bool is_active_tree) const;
void SetScrollerId(base::Optional<ElementId> scroller_id); void SetScrollerId(base::Optional<ElementId> scroller_id);
void UpdateStartAndEndScrollOffsets( void UpdateStartAndEndScrollOffsets(
......
...@@ -13,6 +13,13 @@ namespace cc { ...@@ -13,6 +13,13 @@ namespace cc {
namespace { namespace {
// Only expect precision up to 1 microsecond for double conversion error (mainly
// due to timeline time getting converted between double and TimeTick).
static constexpr double time_error_ms = 0.001;
#define EXPECT_SCROLL_TIMELINE_TIME_NEAR(expected, value) \
EXPECT_NEAR(expected, ToDouble(value), time_error_ms)
void SetScrollOffset(PropertyTrees* property_trees, void SetScrollOffset(PropertyTrees* property_trees,
ElementId scroller_id, ElementId scroller_id,
gfx::ScrollOffset offset) { gfx::ScrollOffset offset) {
...@@ -61,6 +68,15 @@ double CalculateCurrentTime(double current_scroll_offset, ...@@ -61,6 +68,15 @@ double CalculateCurrentTime(double current_scroll_offset,
effective_time_range; effective_time_range;
} }
// Helper method to convert base::TimeTicks to double.
// Returns double milliseconds if the input value is resolved or
// std::numeric_limits<double>::quiet_NaN() otherwise.
double ToDouble(base::Optional<base::TimeTicks> time_ticks) {
if (time_ticks)
return (time_ticks.value() - base::TimeTicks()).InMillisecondsF();
return std::numeric_limits<double>::quiet_NaN();
}
} // namespace } // namespace
class ScrollTimelineTest : public ::testing::Test { class ScrollTimelineTest : public ::testing::Test {
...@@ -105,16 +121,20 @@ TEST_F(ScrollTimelineTest, BasicCurrentTimeCalculations) { ...@@ -105,16 +121,20 @@ TEST_F(ScrollTimelineTest, BasicCurrentTimeCalculations) {
// Unscrolled, both timelines should read a current time of 0. // Unscrolled, both timelines should read a current time of 0.
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset()); SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset());
EXPECT_FLOAT_EQ(0, vertical_timeline.CurrentTime(scroll_tree(), false)); EXPECT_SCROLL_TIMELINE_TIME_NEAR(
EXPECT_FLOAT_EQ(0, horizontal_timeline.CurrentTime(scroll_tree(), false)); 0, vertical_timeline.CurrentTime(scroll_tree(), false));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
0, horizontal_timeline.CurrentTime(scroll_tree(), false));
// Now do some scrolling and make sure that the ScrollTimelines update. // Now do some scrolling and make sure that the ScrollTimelines update.
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(75, 50)); SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(75, 50));
// As noted above, we have mapped the time range such that current time should // As noted above, we have mapped the time range such that current time should
// just be the scroll offset. // just be the scroll offset.
EXPECT_FLOAT_EQ(50, vertical_timeline.CurrentTime(scroll_tree(), false)); EXPECT_SCROLL_TIMELINE_TIME_NEAR(
EXPECT_FLOAT_EQ(75, horizontal_timeline.CurrentTime(scroll_tree(), false)); 50, vertical_timeline.CurrentTime(scroll_tree(), false));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
75, horizontal_timeline.CurrentTime(scroll_tree(), false));
} }
TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForTimeRange) { TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForTimeRange) {
...@@ -127,7 +147,8 @@ TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForTimeRange) { ...@@ -127,7 +147,8 @@ TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForTimeRange) {
SetScrollOffset(&property_trees(), scroller_id(), SetScrollOffset(&property_trees(), scroller_id(),
gfx::ScrollOffset(0, halfwayY)); gfx::ScrollOffset(0, halfwayY));
EXPECT_FLOAT_EQ(50, timeline.CurrentTime(scroll_tree(), false)); EXPECT_SCROLL_TIMELINE_TIME_NEAR(50,
timeline.CurrentTime(scroll_tree(), false));
} }
// This test ensures that the ScrollTimeline's active scroller id is correct. We // This test ensures that the ScrollTimeline's active scroller id is correct. We
...@@ -162,20 +183,20 @@ TEST_F(ScrollTimelineTest, ActiveTimeIsSetOnlyAfterPromotion) { ...@@ -162,20 +183,20 @@ TEST_F(ScrollTimelineTest, ActiveTimeIsSetOnlyAfterPromotion) {
std::unique_ptr<ScrollTimeline> impl_timeline = std::unique_ptr<ScrollTimeline> impl_timeline =
main_timeline.CreateImplInstance(); main_timeline.CreateImplInstance();
EXPECT_TRUE( EXPECT_TRUE(std::isnan(
std::isnan(impl_timeline->CurrentTime(active_tree.scroll_tree, true))); ToDouble(impl_timeline->CurrentTime(active_tree.scroll_tree, true))));
EXPECT_FLOAT_EQ(50, EXPECT_SCROLL_TIMELINE_TIME_NEAR(
impl_timeline->CurrentTime(pending_tree.scroll_tree, false)); 50, impl_timeline->CurrentTime(pending_tree.scroll_tree, false));
// Now fake a tree activation; this should cause the ScrollTimeline to update // Now fake a tree activation; this should cause the ScrollTimeline to update
// its active scroller id. Note that we deliberately pass in the pending_tree // its active scroller id. Note that we deliberately pass in the pending_tree
// and just claim it is the active tree; this avoids needing to properly // and just claim it is the active tree; this avoids needing to properly
// implement tree swapping just for the test. // implement tree swapping just for the test.
impl_timeline->PromoteScrollTimelinePendingToActive(); impl_timeline->PromoteScrollTimelinePendingToActive();
EXPECT_FLOAT_EQ(50, EXPECT_SCROLL_TIMELINE_TIME_NEAR(
impl_timeline->CurrentTime(pending_tree.scroll_tree, true)); 50, impl_timeline->CurrentTime(pending_tree.scroll_tree, true));
EXPECT_FLOAT_EQ(50, EXPECT_SCROLL_TIMELINE_TIME_NEAR(
impl_timeline->CurrentTime(pending_tree.scroll_tree, false)); 50, impl_timeline->CurrentTime(pending_tree.scroll_tree, false));
} }
TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForPixelSnapping) { TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForPixelSnapping) {
...@@ -191,7 +212,8 @@ TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForPixelSnapping) { ...@@ -191,7 +212,8 @@ TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForPixelSnapping) {
property_trees().transform_tree.FindNodeFromElementId(scroller_id()); property_trees().transform_tree.FindNodeFromElementId(scroller_id());
transform_node->snap_amount = gfx::Vector2dF(0, 0.5); transform_node->snap_amount = gfx::Vector2dF(0, 0.5);
EXPECT_FLOAT_EQ(49.5, timeline.CurrentTime(scroll_tree(), false)); EXPECT_SCROLL_TIMELINE_TIME_NEAR(49.5,
timeline.CurrentTime(scroll_tree(), false));
} }
TEST_F(ScrollTimelineTest, CurrentTimeHandlesStartScrollOffset) { TEST_F(ScrollTimelineTest, CurrentTimeHandlesStartScrollOffset) {
...@@ -203,18 +225,19 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesStartScrollOffset) { ...@@ -203,18 +225,19 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesStartScrollOffset) {
// Unscrolled, the timeline should read a current time of unresolved, since // Unscrolled, the timeline should read a current time of unresolved, since
// the current offset (0) will be less than the startScrollOffset. // the current offset (0) will be less than the startScrollOffset.
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset()); SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset());
EXPECT_TRUE(std::isnan(timeline.CurrentTime(scroll_tree(), false))); EXPECT_TRUE(std::isnan(ToDouble(timeline.CurrentTime(scroll_tree(), false))));
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 19)); SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 19));
EXPECT_TRUE(std::isnan(timeline.CurrentTime(scroll_tree(), false))); EXPECT_TRUE(std::isnan(ToDouble(timeline.CurrentTime(scroll_tree(), false))));
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 20)); SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 20));
EXPECT_FLOAT_EQ(0, timeline.CurrentTime(scroll_tree(), false)); EXPECT_SCROLL_TIMELINE_TIME_NEAR(0,
timeline.CurrentTime(scroll_tree(), false));
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 50)); SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 50));
EXPECT_FLOAT_EQ( EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(50, start_scroll_offset, time_range, time_range), CalculateCurrentTime(50, start_scroll_offset, time_range, time_range),
timeline.CurrentTime(scroll_tree(), false)); timeline.CurrentTime(scroll_tree(), false));
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 200)); SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 200));
EXPECT_FLOAT_EQ( EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(200, start_scroll_offset, time_range, time_range), CalculateCurrentTime(200, start_scroll_offset, time_range, time_range),
timeline.CurrentTime(scroll_tree(), false)); timeline.CurrentTime(scroll_tree(), false));
} }
...@@ -227,18 +250,18 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesEndScrollOffset) { ...@@ -227,18 +250,18 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesEndScrollOffset) {
SetScrollOffset(&property_trees(), scroller_id(), SetScrollOffset(&property_trees(), scroller_id(),
gfx::ScrollOffset(0, time_range)); gfx::ScrollOffset(0, time_range));
EXPECT_TRUE(std::isnan(timeline.CurrentTime(scroll_tree(), false))); EXPECT_TRUE(std::isnan(ToDouble(timeline.CurrentTime(scroll_tree(), false))));
SetScrollOffset(&property_trees(), scroller_id(), SetScrollOffset(&property_trees(), scroller_id(),
gfx::ScrollOffset(0, time_range - 20)); gfx::ScrollOffset(0, time_range - 20));
EXPECT_TRUE(std::isnan(timeline.CurrentTime(scroll_tree(), false))); EXPECT_TRUE(std::isnan(ToDouble(timeline.CurrentTime(scroll_tree(), false))));
SetScrollOffset(&property_trees(), scroller_id(), SetScrollOffset(&property_trees(), scroller_id(),
gfx::ScrollOffset(0, time_range - 50)); gfx::ScrollOffset(0, time_range - 50));
EXPECT_FLOAT_EQ( EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(time_range - 50, 0, end_scroll_offset, time_range), CalculateCurrentTime(time_range - 50, 0, end_scroll_offset, time_range),
timeline.CurrentTime(scroll_tree(), false)); timeline.CurrentTime(scroll_tree(), false));
SetScrollOffset(&property_trees(), scroller_id(), SetScrollOffset(&property_trees(), scroller_id(),
gfx::ScrollOffset(0, time_range - 200)); gfx::ScrollOffset(0, time_range - 200));
EXPECT_FLOAT_EQ( EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(time_range - 200, 0, end_scroll_offset, time_range), CalculateCurrentTime(time_range - 200, 0, end_scroll_offset, time_range),
timeline.CurrentTime(scroll_tree(), false)); timeline.CurrentTime(scroll_tree(), false));
} }
...@@ -253,7 +276,7 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesEndScrollOffsetInclusive) { ...@@ -253,7 +276,7 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesEndScrollOffsetInclusive) {
const double current_offset = end_scroll_offset; const double current_offset = end_scroll_offset;
SetScrollOffset(&property_trees(), scroller_id(), SetScrollOffset(&property_trees(), scroller_id(),
gfx::ScrollOffset(0, current_offset)); gfx::ScrollOffset(0, current_offset));
EXPECT_FLOAT_EQ( EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(current_offset, 0, end_scroll_offset, time_range), CalculateCurrentTime(current_offset, 0, end_scroll_offset, time_range),
timeline.CurrentTime(scroll_tree(), false)); timeline.CurrentTime(scroll_tree(), false));
} }
...@@ -266,9 +289,10 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesCombinedStartAndEndScrollOffset) { ...@@ -266,9 +289,10 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesCombinedStartAndEndScrollOffset) {
start_scroll_offset, end_scroll_offset, time_range); start_scroll_offset, end_scroll_offset, time_range);
SetScrollOffset(&property_trees(), scroller_id(), SetScrollOffset(&property_trees(), scroller_id(),
gfx::ScrollOffset(0, time_range - 150)); gfx::ScrollOffset(0, time_range - 150));
EXPECT_FLOAT_EQ(CalculateCurrentTime(time_range - 150, start_scroll_offset, EXPECT_SCROLL_TIMELINE_TIME_NEAR(
end_scroll_offset, time_range), CalculateCurrentTime(time_range - 150, start_scroll_offset,
timeline.CurrentTime(scroll_tree(), false)); end_scroll_offset, time_range),
timeline.CurrentTime(scroll_tree(), false));
} }
TEST_F(ScrollTimelineTest, CurrentTimeHandlesEqualStartAndEndScrollOffset) { TEST_F(ScrollTimelineTest, CurrentTimeHandlesEqualStartAndEndScrollOffset) {
...@@ -276,7 +300,7 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesEqualStartAndEndScrollOffset) { ...@@ -276,7 +300,7 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesEqualStartAndEndScrollOffset) {
ScrollTimeline timeline(scroller_id(), ScrollTimeline::ScrollDown, 20, 20, ScrollTimeline timeline(scroller_id(), ScrollTimeline::ScrollDown, 20, 20,
time_range); time_range);
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 150)); SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 150));
EXPECT_TRUE(std::isnan(timeline.CurrentTime(scroll_tree(), false))); EXPECT_TRUE(std::isnan(ToDouble(timeline.CurrentTime(scroll_tree(), false))));
} }
TEST_F(ScrollTimelineTest, TEST_F(ScrollTimelineTest,
...@@ -285,7 +309,7 @@ TEST_F(ScrollTimelineTest, ...@@ -285,7 +309,7 @@ TEST_F(ScrollTimelineTest,
ScrollTimeline timeline(scroller_id(), ScrollTimeline::ScrollDown, 50, 10, ScrollTimeline timeline(scroller_id(), ScrollTimeline::ScrollDown, 50, 10,
time_range); time_range);
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 150)); SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 150));
EXPECT_TRUE(std::isnan(timeline.CurrentTime(scroll_tree(), false))); EXPECT_TRUE(std::isnan(ToDouble(timeline.CurrentTime(scroll_tree(), false))));
} }
} // namespace cc } // namespace cc
...@@ -105,8 +105,10 @@ void WorkletAnimation::UpdateInputState(MutatorInputState* input_state, ...@@ -105,8 +105,10 @@ void WorkletAnimation::UpdateInputState(MutatorInputState* input_state,
bool is_active_tree) { bool is_active_tree) {
// Record the monotonic time to be the start time first time state is // Record the monotonic time to be the start time first time state is
// generated. This time is used as the origin for computing the current time. // generated. This time is used as the origin for computing the current time.
// The start time of scroll-linked animations is always initialized to zero.
// See: https://github.com/w3c/csswg-drafts/issues/2075
if (!start_time_.has_value()) if (!start_time_.has_value())
start_time_ = monotonic_time; start_time_ = scroll_timeline_ ? base::TimeTicks() : monotonic_time;
// Skip running worklet animations with unchanged input time and reuse // Skip running worklet animations with unchanged input time and reuse
// their value from the previous animation call. // their value from the previous animation call.
...@@ -151,9 +153,9 @@ void WorkletAnimation::SetPlaybackRate(double playback_rate) { ...@@ -151,9 +153,9 @@ void WorkletAnimation::SetPlaybackRate(double playback_rate) {
if (playback_rate == playback_rate_) if (playback_rate == playback_rate_)
return; return;
// Setting playback rate is rejected in the blink side if any of the // Setting playback rate is rejected in the blink side if playback_rate_ is
// conditions below is false. // zero.
DCHECK(playback_rate_ && !scroll_timeline_); DCHECK(playback_rate_);
if (start_time_ && last_current_time_) { if (start_time_ && last_current_time_) {
// Update startTime in order to maintain previous currentTime and, // Update startTime in order to maintain previous currentTime and,
...@@ -173,16 +175,20 @@ void WorkletAnimation::UpdatePlaybackRate(double playback_rate) { ...@@ -173,16 +175,20 @@ void WorkletAnimation::UpdatePlaybackRate(double playback_rate) {
SetNeedsPushProperties(); SetNeedsPushProperties();
} }
// TODO(gerchiko): Implement support playback_rate for scroll-linked
// animations. http://crbug.com/852475.
double WorkletAnimation::CurrentTime(base::TimeTicks monotonic_time, double WorkletAnimation::CurrentTime(base::TimeTicks monotonic_time,
const ScrollTree& scroll_tree, const ScrollTree& scroll_tree,
bool is_active_tree) { bool is_active_tree) {
// Note that we have intentionally decided not to offset the scroll timeline base::TimeTicks timeline_time;
// by the start time. See: https://github.com/w3c/csswg-drafts/issues/2075 if (scroll_timeline_) {
if (scroll_timeline_) base::Optional<base::TimeTicks> scroll_monotonic_time =
return scroll_timeline_->CurrentTime(scroll_tree, is_active_tree); scroll_timeline_->CurrentTime(scroll_tree, is_active_tree);
return (monotonic_time - start_time_.value()).InMillisecondsF() * if (!scroll_monotonic_time)
return std::numeric_limits<double>::quiet_NaN();
timeline_time = scroll_monotonic_time.value();
} else {
timeline_time = monotonic_time;
}
return (timeline_time - start_time_.value()).InMillisecondsF() *
playback_rate_; playback_rate_;
} }
...@@ -191,7 +197,7 @@ bool WorkletAnimation::NeedsUpdate(base::TimeTicks monotonic_time, ...@@ -191,7 +197,7 @@ bool WorkletAnimation::NeedsUpdate(base::TimeTicks monotonic_time,
bool is_active_tree) { bool is_active_tree) {
// If we don't have a start time it means that an update was never sent to // If we don't have a start time it means that an update was never sent to
// the worklet therefore we need one. // the worklet therefore we need one.
if (!scroll_timeline_ && !start_time_.has_value()) if (!start_time_.has_value())
return true; return true;
DCHECK(state_ == State::PENDING || last_current_time_.has_value()); DCHECK(state_ == State::PENDING || last_current_time_.has_value());
......
...@@ -55,7 +55,8 @@ class MockScrollTimeline : public ScrollTimeline { ...@@ -55,7 +55,8 @@ class MockScrollTimeline : public ScrollTimeline {
base::nullopt, base::nullopt,
base::nullopt, base::nullopt,
0) {} 0) {}
MOCK_CONST_METHOD2(CurrentTime, double(const ScrollTree&, bool)); MOCK_CONST_METHOD2(CurrentTime,
base::Optional<base::TimeTicks>(const ScrollTree&, bool));
}; };
TEST_F(WorkletAnimationTest, NonImplInstanceDoesNotTickKeyframe) { TEST_F(WorkletAnimationTest, NonImplInstanceDoesNotTickKeyframe) {
...@@ -109,7 +110,9 @@ TEST_F(WorkletAnimationTest, LocalTimeIsUsedWhenTicking) { ...@@ -109,7 +110,9 @@ TEST_F(WorkletAnimationTest, LocalTimeIsUsedWhenTicking) {
TEST_F(WorkletAnimationTest, CurrentTimeCorrectlyUsesScrollTimeline) { TEST_F(WorkletAnimationTest, CurrentTimeCorrectlyUsesScrollTimeline) {
auto scroll_timeline = std::make_unique<MockScrollTimeline>(); auto scroll_timeline = std::make_unique<MockScrollTimeline>();
EXPECT_CALL(*scroll_timeline, CurrentTime(_, _)).WillRepeatedly(Return(1234)); EXPECT_CALL(*scroll_timeline, CurrentTime(_, _))
.WillRepeatedly(Return(
(base::TimeTicks() + base::TimeDelta::FromMilliseconds(1234))));
scoped_refptr<WorkletAnimation> worklet_animation = scoped_refptr<WorkletAnimation> worklet_animation =
WorkletAnimation::Create(worklet_animation_id_, "test_name", 1, WorkletAnimation::Create(worklet_animation_id_, "test_name", 1,
std::move(scroll_timeline), nullptr); std::move(scroll_timeline), nullptr);
...@@ -208,6 +211,56 @@ TEST_F(WorkletAnimationTest, DocumentTimelineSetPlaybackRate) { ...@@ -208,6 +211,56 @@ TEST_F(WorkletAnimationTest, DocumentTimelineSetPlaybackRate) {
input->updated_animations[0].current_time); input->updated_animations[0].current_time);
} }
// Verifies correctness of current time when playback rate is set on
// initializing the scroll-linked animation and while the animation is playing.
TEST_F(WorkletAnimationTest, ScrollTimelineSetPlaybackRate) {
const double playback_rate_double = 2;
const double playback_rate_half = 0.5;
auto scroll_timeline = std::make_unique<MockScrollTimeline>();
EXPECT_CALL(*scroll_timeline, CurrentTime(_, _))
// First UpdateInputState call.
.WillOnce(
Return(base::TimeTicks() + base::TimeDelta::FromMilliseconds(50)))
// First UpdateInputState call.
.WillOnce(
Return(base::TimeTicks() + base::TimeDelta::FromMilliseconds(50)))
// Second UpdateInputState call.
.WillRepeatedly(
Return(base::TimeTicks() + base::TimeDelta::FromMilliseconds(100)));
scoped_refptr<WorkletAnimation> worklet_animation = WorkletAnimation::Create(
worklet_animation_id_, "test_name", playback_rate_double,
std::move(scroll_timeline), nullptr);
ScrollTree scroll_tree;
std::unique_ptr<MutatorInputState> state =
std::make_unique<MutatorInputState>();
// Start the animation.
worklet_animation->UpdateInputState(state.get(), base::TimeTicks(),
scroll_tree, true);
std::unique_ptr<AnimationWorkletInput> input =
state->TakeWorkletState(worklet_animation_id_.worklet_id);
// Verify that the current time is updated twice faster than the timeline
// time.
EXPECT_EQ(50 * playback_rate_double,
input->added_and_updated_animations[0].current_time);
// Update the playback rate.
worklet_animation->SetPlaybackRateForTesting(playback_rate_half);
state.reset(new MutatorInputState());
// Continue playing the animation.
worklet_animation->UpdateInputState(state.get(), base::TimeTicks(),
scroll_tree, true);
input = state->TakeWorkletState(worklet_animation_id_.worklet_id);
// Verify that the current time is updated half as fast as the timeline time.
EXPECT_EQ(50 * playback_rate_double + 50 * playback_rate_half,
input->updated_animations[0].current_time);
}
// This test verifies that worklet animation state is properly updated. // This test verifies that worklet animation state is properly updated.
TEST_F(WorkletAnimationTest, UpdateInputStateProducesCorrectState) { TEST_F(WorkletAnimationTest, UpdateInputStateProducesCorrectState) {
AttachWorkletAnimation(); AttachWorkletAnimation();
......
...@@ -164,31 +164,58 @@ double ToMilliseconds(base::Optional<base::TimeDelta> time) { ...@@ -164,31 +164,58 @@ double ToMilliseconds(base::Optional<base::TimeDelta> time) {
// Calculates start time backwards from the current time and // Calculates start time backwards from the current time and
// timeline.currentTime. // timeline.currentTime.
//
// If this is a scroll-linked animation, we always consider start time to be
// zero (i.e., scroll origin). This means the computed start time post this
// calculation may not match the expected current time that was given as input.
//
// Changing this is under consideration here:
// https://github.com/w3c/csswg-drafts/issues/2075
base::Optional<base::TimeDelta> CalculateStartTime( base::Optional<base::TimeDelta> CalculateStartTime(
base::TimeDelta current_time, base::TimeDelta current_time,
double playback_rate, double playback_rate,
AnimationTimeline& timeline) { AnimationTimeline& timeline) {
if (timeline.IsScrollTimeline())
return base::TimeDelta();
bool is_null; bool is_null;
double time_ms = timeline.currentTime(is_null); double time_ms = timeline.currentTime(is_null);
// TODO(majidvp): Make it so that inactive timelines do not reach here // TODO(majidvp): Make it so that inactive timelines do not reach here
// i.e., we should instead "hold" when timeline is inactive. // i.e., we should instead "hold" when timeline is inactive.
// https://crbug.com/924159 // https://crbug.com/924159
if (is_null) if (is_null) {
if (timeline.IsScrollTimeline()) {
// Currently start_time_ of scroll-linked animations must always be
// initialized, whether the timeline is active or not.
// Without the start_time_ being initialized, when the scroll-timeline
// becomes active, no logic kicks in to initialize the start_time_ and,
// as a result, the animation doesn't run.
// This is a temporary measure until https://crbug.com/924159
// is implemented.
return base::TimeDelta();
}
return base::nullopt; return base::nullopt;
}
auto timeline_time = base::TimeDelta::FromMillisecondsD(time_ms); auto timeline_time = base::TimeDelta::FromMillisecondsD(time_ms);
return timeline_time - (current_time / playback_rate); return timeline_time - (current_time / playback_rate);
} }
// Returns initial current time of an animation. This method is called when
// calculating initial start time.
// Document-linked animations are initialized with the current time of zero
// and start time of the document timeline current time.
// Scroll-linked animations are initialized with the start time of
// zero (i.e., scroll origin) and the current time corresponding to the current
// scroll position adjusted by the playback rate.
//
// Changing scroll-linked animation start_time initialization is under
// consideration here: https://github.com/w3c/csswg-drafts/issues/2075.
base::TimeDelta GetInitialCurrentTime(double playback_rate,
AnimationTimeline& timeline) {
if (timeline.IsScrollTimeline()) {
bool is_null;
double timeline_time_ms = timeline.currentTime(is_null);
// TODO(majidvp): Make it so that inactive timelines do not reach here
// i.e., we should instead "hold" when timeline is inactive.
// https://crbug.com/924159
if (is_null)
return base::TimeDelta();
return base::TimeDelta::FromMillisecondsD(timeline_time_ms) * playback_rate;
}
return base::TimeDelta();
}
} // namespace } // namespace
WorkletAnimation* WorkletAnimation::Create( WorkletAnimation* WorkletAnimation::Create(
...@@ -318,7 +345,7 @@ void WorkletAnimation::play(ExceptionState& exception_state) { ...@@ -318,7 +345,7 @@ void WorkletAnimation::play(ExceptionState& exception_state) {
// While animation is pending, it hold time at Zero, see: // While animation is pending, it hold time at Zero, see:
// https://drafts.csswg.org/web-animations-1/#playing-an-animation-section // https://drafts.csswg.org/web-animations-1/#playing-an-animation-section
SetPlayState(Animation::kPending); SetPlayState(Animation::kPending);
SetCurrentTime(base::TimeDelta()); SetCurrentTime(GetInitialCurrentTime(playback_rate_, *timeline_));
has_started_ = true; has_started_ = true;
for (auto& effect : effects_) { for (auto& effect : effects_) {
...@@ -360,7 +387,8 @@ void WorkletAnimation::pause(ExceptionState& exception_state) { ...@@ -360,7 +387,8 @@ void WorkletAnimation::pause(ExceptionState& exception_state) {
// If animation is playing then we should hold the current time // If animation is playing then we should hold the current time
// otherwise hold zero. // otherwise hold zero.
base::TimeDelta new_current_time = base::TimeDelta new_current_time =
Playing() ? CurrentTime().value() : base::TimeDelta(); Playing() ? CurrentTime().value()
: GetInitialCurrentTime(playback_rate_, *timeline_);
SetPlayState(Animation::kPaused); SetPlayState(Animation::kPaused);
SetCurrentTime(new_current_time); SetCurrentTime(new_current_time);
} }
...@@ -432,17 +460,6 @@ void WorkletAnimation::setPlaybackRate(ScriptState* script_state, ...@@ -432,17 +460,6 @@ void WorkletAnimation::setPlaybackRate(ScriptState* script_state,
return; return;
} }
// TODO(gerchiko): Implement support playback_rate for scroll-linked
// animations. http://crbug.com/852475.
if (timeline_->IsScrollTimeline()) {
if (document_->GetFrame() && ExecutionContext::From(script_state)) {
document_->GetFrame()->Console().AddMessage(
ConsoleMessage::Create(kJSMessageSource, kWarningMessageLevel,
"Scroll-linked WorkletAnimation currently "
"does not support setting playback rate."));
}
return;
}
SetPlaybackRateInternal(playback_rate); SetPlaybackRateInternal(playback_rate);
} }
...@@ -450,7 +467,6 @@ void WorkletAnimation::SetPlaybackRateInternal(double playback_rate) { ...@@ -450,7 +467,6 @@ void WorkletAnimation::SetPlaybackRateInternal(double playback_rate) {
DCHECK(std::isfinite(playback_rate)); DCHECK(std::isfinite(playback_rate));
DCHECK_NE(playback_rate, playback_rate_); DCHECK_NE(playback_rate, playback_rate_);
DCHECK(playback_rate); DCHECK(playback_rate);
DCHECK(!timeline_->IsScrollTimeline());
base::Optional<base::TimeDelta> previous_current_time = CurrentTime(); base::Optional<base::TimeDelta> previous_current_time = CurrentTime();
playback_rate_ = playback_rate; playback_rate_ = playback_rate;
...@@ -543,7 +559,8 @@ void WorkletAnimation::InvalidateCompositingState() { ...@@ -543,7 +559,8 @@ void WorkletAnimation::InvalidateCompositingState() {
void WorkletAnimation::StartOnMain() { void WorkletAnimation::StartOnMain() {
running_on_main_thread_ = true; running_on_main_thread_ = true;
// Start from existing current time in case one exists or zero. // Start from existing current time in case one exists or zero.
base::TimeDelta current_time = CurrentTime().value_or(base::TimeDelta()); base::TimeDelta current_time =
CurrentTime().value_or(GetInitialCurrentTime(playback_rate_, *timeline_));
SetPlayState(Animation::kRunning); SetPlayState(Animation::kRunning);
SetCurrentTime(current_time); SetCurrentTime(current_time);
} }
...@@ -604,7 +621,7 @@ bool WorkletAnimation::StartOnCompositor() { ...@@ -604,7 +621,7 @@ bool WorkletAnimation::StartOnCompositor() {
// TODO(smcgruer): We need to start all of the effects, not just the first. // TODO(smcgruer): We need to start all of the effects, not just the first.
StartEffectOnCompositor(compositor_animation_.get(), GetEffect()); StartEffectOnCompositor(compositor_animation_.get(), GetEffect());
SetPlayState(Animation::kRunning); SetPlayState(Animation::kRunning);
SetCurrentTime(base::TimeDelta()); SetCurrentTime(GetInitialCurrentTime(playback_rate_, *timeline_));
return true; return true;
} }
......
...@@ -258,7 +258,7 @@ TEST_F(WorkletAnimationTest, DocumentTimelineSetPlaybackRate) { ...@@ -258,7 +258,7 @@ TEST_F(WorkletAnimationTest, DocumentTimelineSetPlaybackRate) {
double playback_rate = 2.0; double playback_rate = 2.0;
SimulateFrame(111.0); SimulateFrame(111.0);
worklet_animation_->setPlaybackRate(nullptr, playback_rate); worklet_animation_->setPlaybackRate(GetScriptState(), playback_rate);
worklet_animation_->play(ASSERT_NO_EXCEPTION); worklet_animation_->play(ASSERT_NO_EXCEPTION);
worklet_animation_->UpdateCompositingState(); worklet_animation_->UpdateCompositingState();
// Zero current time is not impacted by playback rate. // Zero current time is not impacted by playback rate.
...@@ -283,7 +283,7 @@ TEST_F(WorkletAnimationTest, DocumentTimelineSetPlaybackRateWhilePlaying) { ...@@ -283,7 +283,7 @@ TEST_F(WorkletAnimationTest, DocumentTimelineSetPlaybackRateWhilePlaying) {
worklet_animation_->UpdateCompositingState(); worklet_animation_->UpdateCompositingState();
// Update playback rate after second tick. // Update playback rate after second tick.
SimulateFrame(111.0 + 123.4); SimulateFrame(111.0 + 123.4);
worklet_animation_->setPlaybackRate(nullptr, playback_rate); worklet_animation_->setPlaybackRate(GetScriptState(), playback_rate);
// Verify current time after third tick. // Verify current time after third tick.
SimulateFrame(111.0 + 123.4 + 200.0); SimulateFrame(111.0 + 123.4 + 200.0);
EXPECT_TIME_NEAR(123.4 + 200.0 * playback_rate, EXPECT_TIME_NEAR(123.4 + 200.0 * playback_rate,
...@@ -322,4 +322,101 @@ TEST_F(WorkletAnimationTest, PausePlay) { ...@@ -322,4 +322,101 @@ TEST_F(WorkletAnimationTest, PausePlay) {
worklet_animation_->CurrentTime().value().InMillisecondsF()); worklet_animation_->CurrentTime().value().InMillisecondsF());
} }
// Verifies correctness of current time when playback rate is set while
// scroll-linked animation is in idle state.
TEST_F(WorkletAnimationTest, ScrollTimelineSetPlaybackRate) {
SetBodyInnerHTML(R"HTML(
<style>
#scroller { overflow: scroll; width: 100px; height: 100px; }
#spacer { width: 200px; height: 200px; }
</style>
<div id='scroller'>
<div id='spacer'></div>
</div>
)HTML");
LayoutBoxModelObject* scroller =
ToLayoutBoxModelObject(GetLayoutObjectByElementId("scroller"));
ASSERT_TRUE(scroller);
ASSERT_TRUE(scroller->HasOverflowClip());
PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea();
ASSERT_TRUE(scrollable_area);
scrollable_area->SetScrollOffset(ScrollOffset(0, 20), kProgrammaticScroll);
ScrollTimelineOptions* options = ScrollTimelineOptions::Create();
DoubleOrScrollTimelineAutoKeyword time_range =
DoubleOrScrollTimelineAutoKeyword::FromDouble(100);
options->setTimeRange(time_range);
options->setScrollSource(GetElementById("scroller"));
ScrollTimeline* scroll_timeline =
ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION);
WorkletAnimation* worklet_animation = CreateWorkletAnimation(
GetScriptState(), element_, animator_name_, scroll_timeline);
DummyExceptionStateForTesting exception_state;
double playback_rate = 2.0;
// Set playback rate while the animation is in 'idle' state.
worklet_animation->setPlaybackRate(GetScriptState(), playback_rate);
worklet_animation->play(exception_state);
worklet_animation->UpdateCompositingState();
// Initial current time increased by playback rate.
EXPECT_TIME_NEAR(40,
worklet_animation->CurrentTime().value().InMillisecondsF());
// Update scroll offset.
scrollable_area->SetScrollOffset(ScrollOffset(0, 40), kProgrammaticScroll);
// Verify that the current time is updated playback_rate faster than the
// timeline time.
EXPECT_TIME_NEAR(40 + 20 * playback_rate,
worklet_animation->CurrentTime().value().InMillisecondsF());
}
// Verifies correctness of current time when playback rate is set while the
// scroll-linked animation is playing.
TEST_F(WorkletAnimationTest, ScrollTimelineSetPlaybackRateWhilePlaying) {
SetBodyInnerHTML(R"HTML(
<style>
#scroller { overflow: scroll; width: 100px; height: 100px; }
#spacer { width: 200px; height: 200px; }
</style>
<div id='scroller'>
<div id='spacer'></div>
</div>
)HTML");
LayoutBoxModelObject* scroller =
ToLayoutBoxModelObject(GetLayoutObjectByElementId("scroller"));
ASSERT_TRUE(scroller);
ASSERT_TRUE(scroller->HasOverflowClip());
PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea();
ASSERT_TRUE(scrollable_area);
ScrollTimelineOptions* options = ScrollTimelineOptions::Create();
DoubleOrScrollTimelineAutoKeyword time_range =
DoubleOrScrollTimelineAutoKeyword::FromDouble(100);
options->setTimeRange(time_range);
options->setScrollSource(GetElementById("scroller"));
ScrollTimeline* scroll_timeline =
ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION);
WorkletAnimation* worklet_animation = CreateWorkletAnimation(
GetScriptState(), element_, animator_name_, scroll_timeline);
double playback_rate = 0.5;
// Start the animation.
DummyExceptionStateForTesting exception_state;
worklet_animation->play(exception_state);
worklet_animation->UpdateCompositingState();
// Update scroll offset and playback rate.
scrollable_area->SetScrollOffset(ScrollOffset(0, 40), kProgrammaticScroll);
worklet_animation->setPlaybackRate(GetScriptState(), playback_rate);
// Verify the current time after another scroll offset update.
scrollable_area->SetScrollOffset(ScrollOffset(0, 80), kProgrammaticScroll);
EXPECT_TIME_NEAR(40 + 40 * playback_rate,
worklet_animation->CurrentTime().value().InMillisecondsF());
}
} // namespace blink } // namespace blink
<!DOCTYPE html>
<style>
#box {
width: 100px;
height: 100px;
background-color: #00ff00;
transform: translate(0, 100px);
will-change: transform; /* force compositing */
}
#covered {
width: 100px;
height: 100px;
background-color: #ff8080;
}
#scroller {
overflow: auto;
height: 100px;
width: 100px;
will-change: transform; /* force compositing */
}
#contents {
height: 1000px;
width: 100%;
}
</style>
<div id="box"></div>
<div id="covered"></div>
<div id="scroller">
<div id="contents"></div>
</div>
<script>
window.addEventListener('load', function() {
// Move the scroller to quarter way.
const scroller = document.getElementById("scroller");
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
scroller.scrollTop = 0.25 * maxScroll;
});
</script>
<!DOCTYPE html>
<title>When playback rate of scroll-linked WorkletAnimation is updated,
the underlying effect produces correct visual result.</title>
<style>
#box {
width: 100px;
height: 100px;
background-color: #00ff00;
}
#covered {
width: 100px;
height: 100px;
background-color: #ff8080;
}
#scroller {
overflow: auto;
height: 100px;
width: 100px;
}
#contents {
height: 1000px;
width: 100%;
}
</style>
<div id="box"></div>
<div id="covered"></div>
<div id="scroller">
<div id="contents"></div>
</div>
<script id="passthrough" type="text/worklet">
registerAnimator("passthrough_animator", class {
animate(currentTime, effect) {
effect.localTime = currentTime;
}
});
</script>
<script src="resources/animation-worklet-tests.js"></script>
<script>
if (window.testRunner) {
testRunner.waitUntilDone();
}
runInAnimationWorklet(
document.getElementById('passthrough').textContent
).then(()=>{
const box = document.getElementById('box');
const effect = new KeyframeEffect(box,
[
{ transform: 'translateY(0)'},
{ transform: 'translateY(200px)'}
], {
duration: 1000,
}
);
const scroller = document.getElementById('scroller');
const timeline = new ScrollTimeline({ scrollSource: scroller, timeRange: 1000, orientation: 'block' });
const animation = new WorkletAnimation('passthrough_animator', effect, timeline);
animation.play();
animation.playbackRate = 2;
// Move the scroller to the quarter way point.
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
scroller.scrollTop = 0.25 * maxScroll;
if (window.testRunner) {
waitTwoAnimationFrames(_ => {
testRunner.notifyDone();
});
}
});
</script>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment