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 {
end_scroll_offset_, time_range_);
}
double ScrollTimeline::CurrentTime(const ScrollTree& scroll_tree,
bool is_active_tree) const {
base::Optional<base::TimeTicks> ScrollTimeline::CurrentTime(
const ScrollTree& scroll_tree,
bool is_active_tree) const {
// 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.
// if it is no longer composited). In these cases the best we can do is to
// return an unresolved time value.
if ((is_active_tree && !active_id_) || (!is_active_tree && !pending_id_))
return std::numeric_limits<double>::quiet_NaN();
return base::nullopt;
ElementId scroller_id =
is_active_tree ? active_id_.value() : pending_id_.value();
......@@ -60,7 +61,7 @@ double ScrollTimeline::CurrentTime(const ScrollTree& scroll_tree,
const ScrollNode* scroll_node =
scroll_tree.FindNodeFromElementId(scroller_id);
if (!scroll_node)
return std::numeric_limits<double>::quiet_NaN();
return base::nullopt;
gfx::ScrollOffset offset =
scroll_tree.GetPixelSnappedScrollOffset(scroll_node->id);
......@@ -87,7 +88,7 @@ double ScrollTimeline::CurrentTime(const ScrollTree& scroll_tree,
// unresolved time value if fill is none or forwards, or 0 otherwise.
// TODO(smcgruer): Implement |fill|.
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:
......@@ -97,24 +98,26 @@ double ScrollTimeline::CurrentTime(const ScrollTree& scroll_tree,
// value.
// TODO(smcgruer): Implement |fill|.
if (resolved_end_scroll_offset < max_offset)
return std::numeric_limits<double>::quiet_NaN();
return base::nullopt;
// 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.
// See https://github.com/WICG/scroll-animations/issues/20
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:
// ((current scroll offset - startScrollOffset) /
// (endScrollOffset - startScrollOffset)) * effective time range
return ((current_offset - resolved_start_scroll_offset) /
(resolved_end_scroll_offset - resolved_start_scroll_offset)) *
time_range_;
return base::TimeTicks() +
base::TimeDelta::FromMillisecondsD(
((current_offset - resolved_start_scroll_offset) /
(resolved_end_scroll_offset - resolved_start_scroll_offset)) *
time_range_);
}
void ScrollTimeline::PushPropertiesTo(ScrollTimeline* impl_timeline) {
......
......@@ -6,6 +6,7 @@
#define CC_ANIMATION_SCROLL_TIMELINE_H_
#include "base/optional.h"
#include "base/time/time.h"
#include "cc/animation/animation_export.h"
#include "cc/trees/element_id.h"
......@@ -38,11 +39,13 @@ class CC_ANIMATION_EXPORT ScrollTimeline {
// compositor.
std::unique_ptr<ScrollTimeline> CreateImplInstance() const;
// Calculate the current time of the ScrollTimeline. This is either a double
// value or std::numeric_limits<double>::quiet_NaN() if the current time is
// unresolved.
virtual double CurrentTime(const ScrollTree& scroll_tree,
bool is_active_tree) const;
// Calculate the current time of the ScrollTimeline. This is either a
// base::TimeTicks value or base::nullopt if the current time is unresolved.
// The internal calculations are performed using doubles and the result is
// converted to base::TimeTicks. This limits the precision to 1us.
virtual base::Optional<base::TimeTicks> CurrentTime(
const ScrollTree& scroll_tree,
bool is_active_tree) const;
void SetScrollerId(base::Optional<ElementId> scroller_id);
void UpdateStartAndEndScrollOffsets(
......
......@@ -13,6 +13,13 @@ namespace cc {
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,
ElementId scroller_id,
gfx::ScrollOffset offset) {
......@@ -61,6 +68,15 @@ double CalculateCurrentTime(double current_scroll_offset,
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
class ScrollTimelineTest : public ::testing::Test {
......@@ -105,16 +121,20 @@ TEST_F(ScrollTimelineTest, BasicCurrentTimeCalculations) {
// Unscrolled, both timelines should read a current time of 0.
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset());
EXPECT_FLOAT_EQ(0, vertical_timeline.CurrentTime(scroll_tree(), false));
EXPECT_FLOAT_EQ(0, horizontal_timeline.CurrentTime(scroll_tree(), false));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
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.
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(75, 50));
// As noted above, we have mapped the time range such that current time should
// just be the scroll offset.
EXPECT_FLOAT_EQ(50, vertical_timeline.CurrentTime(scroll_tree(), false));
EXPECT_FLOAT_EQ(75, horizontal_timeline.CurrentTime(scroll_tree(), false));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
50, vertical_timeline.CurrentTime(scroll_tree(), false));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
75, horizontal_timeline.CurrentTime(scroll_tree(), false));
}
TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForTimeRange) {
......@@ -127,7 +147,8 @@ TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForTimeRange) {
SetScrollOffset(&property_trees(), scroller_id(),
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
......@@ -162,20 +183,20 @@ TEST_F(ScrollTimelineTest, ActiveTimeIsSetOnlyAfterPromotion) {
std::unique_ptr<ScrollTimeline> impl_timeline =
main_timeline.CreateImplInstance();
EXPECT_TRUE(
std::isnan(impl_timeline->CurrentTime(active_tree.scroll_tree, true)));
EXPECT_FLOAT_EQ(50,
impl_timeline->CurrentTime(pending_tree.scroll_tree, false));
EXPECT_TRUE(std::isnan(
ToDouble(impl_timeline->CurrentTime(active_tree.scroll_tree, true))));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
50, impl_timeline->CurrentTime(pending_tree.scroll_tree, false));
// 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
// and just claim it is the active tree; this avoids needing to properly
// implement tree swapping just for the test.
impl_timeline->PromoteScrollTimelinePendingToActive();
EXPECT_FLOAT_EQ(50,
impl_timeline->CurrentTime(pending_tree.scroll_tree, true));
EXPECT_FLOAT_EQ(50,
impl_timeline->CurrentTime(pending_tree.scroll_tree, false));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
50, impl_timeline->CurrentTime(pending_tree.scroll_tree, true));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
50, impl_timeline->CurrentTime(pending_tree.scroll_tree, false));
}
TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForPixelSnapping) {
......@@ -191,7 +212,8 @@ TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForPixelSnapping) {
property_trees().transform_tree.FindNodeFromElementId(scroller_id());
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) {
......@@ -203,18 +225,19 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesStartScrollOffset) {
// Unscrolled, the timeline should read a current time of unresolved, since
// the current offset (0) will be less than the startScrollOffset.
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));
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));
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));
EXPECT_FLOAT_EQ(
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(50, start_scroll_offset, time_range, time_range),
timeline.CurrentTime(scroll_tree(), false));
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),
timeline.CurrentTime(scroll_tree(), false));
}
......@@ -227,18 +250,18 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesEndScrollOffset) {
SetScrollOffset(&property_trees(), scroller_id(),
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(),
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(),
gfx::ScrollOffset(0, time_range - 50));
EXPECT_FLOAT_EQ(
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(time_range - 50, 0, end_scroll_offset, time_range),
timeline.CurrentTime(scroll_tree(), false));
SetScrollOffset(&property_trees(), scroller_id(),
gfx::ScrollOffset(0, time_range - 200));
EXPECT_FLOAT_EQ(
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(time_range - 200, 0, end_scroll_offset, time_range),
timeline.CurrentTime(scroll_tree(), false));
}
......@@ -253,7 +276,7 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesEndScrollOffsetInclusive) {
const double current_offset = end_scroll_offset;
SetScrollOffset(&property_trees(), scroller_id(),
gfx::ScrollOffset(0, current_offset));
EXPECT_FLOAT_EQ(
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(current_offset, 0, end_scroll_offset, time_range),
timeline.CurrentTime(scroll_tree(), false));
}
......@@ -266,9 +289,10 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesCombinedStartAndEndScrollOffset) {
start_scroll_offset, end_scroll_offset, time_range);
SetScrollOffset(&property_trees(), scroller_id(),
gfx::ScrollOffset(0, time_range - 150));
EXPECT_FLOAT_EQ(CalculateCurrentTime(time_range - 150, start_scroll_offset,
end_scroll_offset, time_range),
timeline.CurrentTime(scroll_tree(), false));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(time_range - 150, start_scroll_offset,
end_scroll_offset, time_range),
timeline.CurrentTime(scroll_tree(), false));
}
TEST_F(ScrollTimelineTest, CurrentTimeHandlesEqualStartAndEndScrollOffset) {
......@@ -276,7 +300,7 @@ TEST_F(ScrollTimelineTest, CurrentTimeHandlesEqualStartAndEndScrollOffset) {
ScrollTimeline timeline(scroller_id(), ScrollTimeline::ScrollDown, 20, 20,
time_range);
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,
......@@ -285,7 +309,7 @@ TEST_F(ScrollTimelineTest,
ScrollTimeline timeline(scroller_id(), ScrollTimeline::ScrollDown, 50, 10,
time_range);
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
......@@ -105,8 +105,10 @@ void WorkletAnimation::UpdateInputState(MutatorInputState* input_state,
bool is_active_tree) {
// 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.
// 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())
start_time_ = monotonic_time;
start_time_ = scroll_timeline_ ? base::TimeTicks() : monotonic_time;
// Skip running worklet animations with unchanged input time and reuse
// their value from the previous animation call.
......@@ -151,9 +153,9 @@ void WorkletAnimation::SetPlaybackRate(double playback_rate) {
if (playback_rate == playback_rate_)
return;
// Setting playback rate is rejected in the blink side if any of the
// conditions below is false.
DCHECK(playback_rate_ && !scroll_timeline_);
// Setting playback rate is rejected in the blink side if playback_rate_ is
// zero.
DCHECK(playback_rate_);
if (start_time_ && last_current_time_) {
// Update startTime in order to maintain previous currentTime and,
......@@ -173,16 +175,20 @@ void WorkletAnimation::UpdatePlaybackRate(double playback_rate) {
SetNeedsPushProperties();
}
// TODO(gerchiko): Implement support playback_rate for scroll-linked
// animations. http://crbug.com/852475.
double WorkletAnimation::CurrentTime(base::TimeTicks monotonic_time,
const ScrollTree& scroll_tree,
bool is_active_tree) {
// Note that we have intentionally decided not to offset the scroll timeline
// by the start time. See: https://github.com/w3c/csswg-drafts/issues/2075
if (scroll_timeline_)
return scroll_timeline_->CurrentTime(scroll_tree, is_active_tree);
return (monotonic_time - start_time_.value()).InMillisecondsF() *
base::TimeTicks timeline_time;
if (scroll_timeline_) {
base::Optional<base::TimeTicks> scroll_monotonic_time =
scroll_timeline_->CurrentTime(scroll_tree, is_active_tree);
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_;
}
......@@ -191,7 +197,7 @@ bool WorkletAnimation::NeedsUpdate(base::TimeTicks monotonic_time,
bool is_active_tree) {
// If we don't have a start time it means that an update was never sent to
// the worklet therefore we need one.
if (!scroll_timeline_ && !start_time_.has_value())
if (!start_time_.has_value())
return true;
DCHECK(state_ == State::PENDING || last_current_time_.has_value());
......
......@@ -55,7 +55,8 @@ class MockScrollTimeline : public ScrollTimeline {
base::nullopt,
base::nullopt,
0) {}
MOCK_CONST_METHOD2(CurrentTime, double(const ScrollTree&, bool));
MOCK_CONST_METHOD2(CurrentTime,
base::Optional<base::TimeTicks>(const ScrollTree&, bool));
};
TEST_F(WorkletAnimationTest, NonImplInstanceDoesNotTickKeyframe) {
......@@ -109,7 +110,9 @@ TEST_F(WorkletAnimationTest, LocalTimeIsUsedWhenTicking) {
TEST_F(WorkletAnimationTest, CurrentTimeCorrectlyUsesScrollTimeline) {
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 =
WorkletAnimation::Create(worklet_animation_id_, "test_name", 1,
std::move(scroll_timeline), nullptr);
......@@ -208,6 +211,56 @@ TEST_F(WorkletAnimationTest, DocumentTimelineSetPlaybackRate) {
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.
TEST_F(WorkletAnimationTest, UpdateInputStateProducesCorrectState) {
AttachWorkletAnimation();
......
......@@ -164,31 +164,58 @@ double ToMilliseconds(base::Optional<base::TimeDelta> time) {
// Calculates start time backwards from the current time and
// 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::TimeDelta current_time,
double playback_rate,
AnimationTimeline& timeline) {
if (timeline.IsScrollTimeline())
return base::TimeDelta();
bool is_null;
double 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)
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;
}
auto timeline_time = base::TimeDelta::FromMillisecondsD(time_ms);
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
WorkletAnimation* WorkletAnimation::Create(
......@@ -318,7 +345,7 @@ void WorkletAnimation::play(ExceptionState& exception_state) {
// While animation is pending, it hold time at Zero, see:
// https://drafts.csswg.org/web-animations-1/#playing-an-animation-section
SetPlayState(Animation::kPending);
SetCurrentTime(base::TimeDelta());
SetCurrentTime(GetInitialCurrentTime(playback_rate_, *timeline_));
has_started_ = true;
for (auto& effect : effects_) {
......@@ -360,7 +387,8 @@ void WorkletAnimation::pause(ExceptionState& exception_state) {
// If animation is playing then we should hold the current time
// otherwise hold zero.
base::TimeDelta new_current_time =
Playing() ? CurrentTime().value() : base::TimeDelta();
Playing() ? CurrentTime().value()
: GetInitialCurrentTime(playback_rate_, *timeline_);
SetPlayState(Animation::kPaused);
SetCurrentTime(new_current_time);
}
......@@ -432,17 +460,6 @@ void WorkletAnimation::setPlaybackRate(ScriptState* script_state,
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);
}
......@@ -450,7 +467,6 @@ void WorkletAnimation::SetPlaybackRateInternal(double playback_rate) {
DCHECK(std::isfinite(playback_rate));
DCHECK_NE(playback_rate, playback_rate_);
DCHECK(playback_rate);
DCHECK(!timeline_->IsScrollTimeline());
base::Optional<base::TimeDelta> previous_current_time = CurrentTime();
playback_rate_ = playback_rate;
......@@ -543,7 +559,8 @@ void WorkletAnimation::InvalidateCompositingState() {
void WorkletAnimation::StartOnMain() {
running_on_main_thread_ = true;
// 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);
SetCurrentTime(current_time);
}
......@@ -604,7 +621,7 @@ bool WorkletAnimation::StartOnCompositor() {
// TODO(smcgruer): We need to start all of the effects, not just the first.
StartEffectOnCompositor(compositor_animation_.get(), GetEffect());
SetPlayState(Animation::kRunning);
SetCurrentTime(base::TimeDelta());
SetCurrentTime(GetInitialCurrentTime(playback_rate_, *timeline_));
return true;
}
......
......@@ -258,7 +258,7 @@ TEST_F(WorkletAnimationTest, DocumentTimelineSetPlaybackRate) {
double playback_rate = 2.0;
SimulateFrame(111.0);
worklet_animation_->setPlaybackRate(nullptr, playback_rate);
worklet_animation_->setPlaybackRate(GetScriptState(), playback_rate);
worklet_animation_->play(ASSERT_NO_EXCEPTION);
worklet_animation_->UpdateCompositingState();
// Zero current time is not impacted by playback rate.
......@@ -283,7 +283,7 @@ TEST_F(WorkletAnimationTest, DocumentTimelineSetPlaybackRateWhilePlaying) {
worklet_animation_->UpdateCompositingState();
// Update playback rate after second tick.
SimulateFrame(111.0 + 123.4);
worklet_animation_->setPlaybackRate(nullptr, playback_rate);
worklet_animation_->setPlaybackRate(GetScriptState(), playback_rate);
// Verify current time after third tick.
SimulateFrame(111.0 + 123.4 + 200.0);
EXPECT_TIME_NEAR(123.4 + 200.0 * playback_rate,
......@@ -322,4 +322,101 @@ TEST_F(WorkletAnimationTest, PausePlay) {
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
<!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>
......@@ -15,126 +15,318 @@ window.assert_times_equal = (actual, expected, description) => {
</script>
<script src="/web-animations/testcommon.js"></script>
<script src="common.js"></script>
<style>
.scroller {
overflow: auto;
height: 100px;
width: 100px;
}
.contents {
height: 1000px;
width: 100%;
}
</style>
<body>
<div id="log"></div>
<script>
'use strict';
function InstantiateWorkletAnimation(test) {
function createWorkletAnimation(test) {
const DURATION = 10000; // ms
const KEYFRAMES = { height : ['100px', '50px'] };
const KEYFRAMES = { transform: ['translateY(100px)', 'translateY(200px)'] };
return new WorkletAnimation('passthrough', new KeyframeEffect(createDiv(test),
KEYFRAMES, DURATION), document.timeline);
}
promise_test(async t => {
await registerPassthroughAnimator();
const animation = InstantiateWorkletAnimation(t);
function createScroller(test) {
var scroller = createDiv(test);
scroller.innerHTML = "<div class='contents'></div>";
scroller.classList.add('scroller');
return scroller;
}
function createScrollLinkedWorkletAnimation(test) {
const timeline = new ScrollTimeline({
scrollSource: createScroller(test),
timeRange: 1000
});
const DURATION = 10000; // ms
const KEYFRAMES = { transform: ['translateY(100px)', 'translateY(200px)'] };
return new WorkletAnimation('passthrough', new KeyframeEffect(createDiv(test),
KEYFRAMES, DURATION), timeline);
}
setup(setupAndRegisterTests, {explicit_done: true});
function setupAndRegisterTests() {
registerPassthroughAnimator().then(() => {
promise_test(async t => {
const animation = createWorkletAnimation(t);
animation.playbackRate = 0.5;
animation.play();
assert_equals(animation.currentTime, 0,
'Zero current time is not affected by playbackRate.');
}, 'Zero current time is not affected by playbackRate set while the ' +
'animation is in idle state.');
promise_test(async t => {
const animation = createWorkletAnimation(t);
animation.play();
animation.playbackRate = 0.5;
assert_equals(animation.currentTime, 0,
'Zero current time is not affected by playbackRate.');
}, 'Zero current time is not affected by playbackRate set while the ' +
'animation is in play-pending state.');
promise_test(async t => {
const animation = createWorkletAnimation(t);
const playbackRate = 2;
animation.playbackRate = 0.5;
animation.play();
assert_equals(animation.currentTime, 0,
'Zero current time is not affected by playbackRate.');
}, 'Zero current time is not affected by playbackRate set while the animation is in idle state.');
animation.play();
promise_test(async t => {
await registerPassthroughAnimator();
const animation = InstantiateWorkletAnimation(t);
waitForAnimationFrameWithCondition(_=> {
return animation.playState == "running"
});
// Make sure the current time is not Zero.
await waitForNextFrame();
animation.play();
animation.playbackRate = 0.5;
assert_equals(animation.currentTime, 0,
'Zero current time is not affected by playbackRate.');
}, 'Zero current time is not affected by playbackRate set while the animation is in play-pending state.');
// Set playback rate while the animation is playing.
const prevCurrentTime = animation.currentTime;
animation.playbackRate = playbackRate;
promise_test(async t => {
await registerPassthroughAnimator();
const animation = InstantiateWorkletAnimation(t);
const playbackRate = 2;
assert_times_equal(animation.currentTime, prevCurrentTime,
'The current time should stay unaffected by setting playback rate.');
}, 'Non zero current time is not affected by playbackRate set while the ' +
'animation is in play state.');
animation.play();
promise_test(async t => {
const animation = createWorkletAnimation(t);
const playbackRate = 2;
await waitForNextFrame();
animation.play();
// Set playback rate while the animation is playing.
const prevCurrentTime = animation.currentTime;
animation.playbackRate = playbackRate;
waitForAnimationFrameWithCondition(_=> {
return animation.playState == "running"
});
assert_times_equal(animation.currentTime, prevCurrentTime,
'The current time should stay unaffected by setting playback rate.');
}, 'Non zero current time is not affected by playbackRate set while the animation is in play state.');
// Set playback rate while the animation is playing.
const prevCurrentTime = animation.currentTime;
const prevTimelineTime = document.timeline.currentTime;
animation.playbackRate = playbackRate;
promise_test(async t => {
await registerPassthroughAnimator();
const animation = InstantiateWorkletAnimation(t);
const playbackRate = 2;
// Play the animation some more.
await waitForNextFrame();
animation.play();
const currentTime = animation.currentTime;
const currentTimelineTime = document.timeline.currentTime;
await waitForNextFrame();
assert_times_equal(
currentTime - prevCurrentTime,
(currentTimelineTime - prevTimelineTime) * playbackRate,
'The current time should increase two times faster than timeline.');
}, 'The playback rate affects the rate of progress of the current time.');
// Set playback rate while the animation is playing
const prevCurrentTime = animation.currentTime;
const prevTimelineTime = document.timeline.currentTime;
animation.playbackRate = playbackRate;
promise_test(async t => {
const animation = createWorkletAnimation(t);
const playbackRate = 2;
// Play the animation some more.
await waitForNextFrame();
// Set playback rate while the animation is in 'idle' state.
animation.playbackRate = playbackRate;
animation.play();
waitForAnimationFrameWithCondition(_=> {
return animation.playState == "running"
});
const prevTimelineTime = document.timeline.currentTime;
const currentTime = animation.currentTime;
const currentTimelineTime = document.timeline.currentTime;
await waitForNextFrame();
assert_times_equal(currentTime - prevCurrentTime, (currentTimelineTime - prevTimelineTime) * playbackRate,
'The current time should increase two times faster than timeline.');
const currentTime = animation.currentTime;
const timelineTime = document.timeline.currentTime;
assert_times_equal(
currentTime,
(timelineTime - prevTimelineTime) * playbackRate,
'The current time should increase two times faster than timeline.');
}, 'The playback rate set before the animation started playing affects ' +
'the rate of progress of the current time');
}, 'The playback rate affects the rate of progress of the current time.');
promise_test(async t => {
const timing = { duration: 100,
easing: 'linear',
fill: 'none',
iterations: 1
};
const target = createDiv(t);
const keyframeEffect = new KeyframeEffect(
target, { opacity: [0, 1] }, timing);
const animation = new WorkletAnimation(
'passthrough', keyframeEffect, document.timeline);
const playbackRate = 2;
promise_test(async t => {
await registerPassthroughAnimator();
const animation = InstantiateWorkletAnimation(t);;
const playbackRate = 2;
animation.play();
waitForAnimationFrameWithCondition(_=> {
return animation.playState == "running"
});
animation.playbackRate = playbackRate;
// Set playback rate while the animation is in 'idle' state.
animation.playbackRate = playbackRate;
animation.play();
const prevTimelineTime = document.timeline.currentTime;
await waitForNextFrame();
await waitForNextFrame();
assert_times_equal(
keyframeEffect.getComputedTiming().localTime, animation.currentTime,
'When playback rate is set on WorkletAnimation, the underlying ' +
'effect\'s timing should be properly updated.');
const currentTime = animation.currentTime;
const timelineTime = document.timeline.currentTime;
assert_times_equal(currentTime, (timelineTime - prevTimelineTime) * playbackRate,
'The current time should increase two times faster than timeline.');
}, 'The playback rate set before the animation started playing affects the ' +
'rate of progress of the current time');
assert_approx_equals(Number(getComputedStyle(target).opacity),
animation.currentTime / 100, 0.001,
'When playback rate is set on WorkletAnimation, the underlying effect' +
' should produce correct visual result.');
}, 'When playback rate is updated, the underlying effect is properly ' +
'updated with the current time of its WorkletAnimation and produces ' +
'correct visual result.');
promise_test(async t => {
await registerPassthroughAnimator();
const timing = { duration: 100,
easing: 'linear',
fill: 'none',
iterations: 1
};
const target = createDiv(t);
const keyframeEffect = new KeyframeEffect(target, { opacity: [0, 1] }, timing);
const animation = new WorkletAnimation('passthrough', keyframeEffect, document.timeline);
const playbackRate = 2;
promise_test(async t => {
const animation = createScrollLinkedWorkletAnimation(t);
const scroller = animation.timeline.scrollSource;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
const timeRange = animation.timeline.timeRange;
scroller.scrollTop = 0.2 * maxScroll;
animation.play();
animation.playbackRate = playbackRate;
animation.playbackRate = 0.5;
animation.play();
waitForAnimationFrameWithCondition(_=> {
return animation.playState == "running"
});
assert_equals(animation.currentTime, 0.2 * timeRange * 0.5,
'Initial current time is scaled by playbackRate.');
}, 'Initial current time is scaled by playbackRate set while ' +
'scroll-linked animation is in idle state.');
await waitForNextFrame();
promise_test(async t => {
const animation = createScrollLinkedWorkletAnimation(t);
const scroller = animation.timeline.scrollSource;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
const timeRange = animation.timeline.timeRange;
scroller.scrollTop = 0.2 * maxScroll;
assert_times_equal(keyframeEffect.getComputedTiming().localTime, animation.currentTime,
'When playback rate is set on WorkletAnimation, the underlying effect\'s timing should be properly updated.');
animation.play();
animation.playbackRate = 0.5;
waitForAnimationFrameWithCondition(_=> {
return animation.playState == "running"
});
assert_equals(animation.currentTime, 0.2 * timeRange,
'Initial current time is not affected by playbackRate.');
}, 'Initial current time is not affected by playbackRate set while '+
'scroll-linked animation is in play-pending state.');
assert_approx_equals(Number(getComputedStyle(target).opacity),
animation.currentTime / 100, 0.001,
'When playback rate is set on WorkletAnimation, the underlying effect should produce correct visual result.');
promise_test(async t => {
const animation = createScrollLinkedWorkletAnimation(t);
const scroller = animation.timeline.scrollSource;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
const timeRange = animation.timeline.timeRange;
const playbackRate = 2;
}, 'When playback rate is updated, the underlying effect is properly updated ' +
'with the current time of its WorkletAnimation and produces correct ' +
'visual result.');
animation.play();
scroller.scrollTop = 0.2 * maxScroll;
waitForAnimationFrameWithCondition(_=> {
return animation.playState == "running"
});
// Set playback rate while the animation is playing.
animation.playbackRate = playbackRate;
assert_times_equal(animation.currentTime, 0.2 * timeRange,
'The current time should stay unaffected by setting playback rate.');
}, 'The current time is not affected by playbackRate set while the ' +
'scroll-linked animation is in play state.');
promise_test(async t => {
const animation = createScrollLinkedWorkletAnimation(t);
const scroller = animation.timeline.scrollSource;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
const playbackRate = 2;
const timeRange = animation.timeline.timeRange;
animation.play();
waitForAnimationFrameWithCondition(_=> {
return animation.playState == "running"
});
scroller.scrollTop = 0.1 * maxScroll;
// Set playback rate while the animation is playing.
animation.playbackRate = playbackRate;
scroller.scrollTop = 0.2 * maxScroll;
assert_times_equal(
animation.currentTime - 0.1 * timeRange, 0.1 * timeRange * playbackRate,
'The current time should increase twice faster than scroll timeline.');
}, 'Scroll-linked animation playback rate affects the rate of progress ' +
'of the current time.');
promise_test(async t => {
const animation = createScrollLinkedWorkletAnimation(t);
const scroller = animation.timeline.scrollSource;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
const timeRange = animation.timeline.timeRange;
const playbackRate = 2;
// Set playback rate while the animation is in 'idle' state.
animation.playbackRate = playbackRate;
animation.play();
waitForAnimationFrameWithCondition(_=> {
return animation.playState == "running"
});
scroller.scrollTop = 0.2 * maxScroll;
assert_times_equal(animation.currentTime, 0.2 * timeRange * playbackRate,
'The current time should increase two times faster than timeline.');
}, 'The playback rate set before scroll-linked animation started playing ' +
'affects the rate of progress of the current time');
promise_test(async t => {
const scroller = createScroller(t);
const timeline = new ScrollTimeline({
scrollSource: scroller,
timeRange: 1000
});
const timing = { duration: 1000,
easing: 'linear',
fill: 'none',
iterations: 1
};
const target = createDiv(t);
const keyframeEffect = new KeyframeEffect(
target, { opacity: [0, 1] }, timing);
const animation = new WorkletAnimation(
'passthrough', keyframeEffect, timeline);
const playbackRate = 2;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
const timeRange = timeline.timeRange;
animation.play();
animation.playbackRate = playbackRate;
waitForAnimationFrameWithCondition(_=> {
return animation.playState == "running"
});
scroller.scrollTop = 0.2 * maxScroll;
await waitForNextFrame();
assert_times_equal(
keyframeEffect.getComputedTiming().localTime,
0.2 * timeRange * playbackRate,
'When playback rate is set on WorkletAnimation, the underlying ' +
'effect\'s timing should be properly updated.');
assert_approx_equals(
Number(getComputedStyle(target).opacity),
0.2 * timeRange * playbackRate / 1000, 0.001,
'When playback rate is set on WorkletAnimation, the underlying ' +
'effect should produce correct visual result.');
}, 'When playback rate is updated, the underlying effect is properly ' +
'updated with the current time of its scroll-linked WorkletAnimation ' +
'and produces correct visual result.');
done();
});
}
</script>
</body>
\ No newline at end of file
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