blob: 148f14fe595f2562ab2477961fae49bba574493c [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/animationworklet/worklet_animation.h"
#include "base/optional.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value.h"
#include "third_party/blink/renderer/bindings/modules/v8/animation_effect_or_animation_effect_sequence.h"
#include "third_party/blink/renderer/core/animation/element_animations.h"
#include "third_party/blink/renderer/core/animation/keyframe_effect_model.h"
#include "third_party/blink/renderer/core/animation/scroll_timeline.h"
#include "third_party/blink/renderer/core/animation/timing.h"
#include "third_party/blink/renderer/core/animation/worklet_animation_controller.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/modules/animationworklet/window_animation_worklet.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
namespace blink {
namespace {
bool ConvertAnimationEffects(
const AnimationEffectOrAnimationEffectSequence& effects,
HeapVector<Member<KeyframeEffect>>& keyframe_effects,
String& error_string) {
DCHECK(keyframe_effects.IsEmpty());
// Currently we only support KeyframeEffect.
if (effects.IsAnimationEffect()) {
auto* const effect = effects.GetAsAnimationEffect();
if (!effect->IsKeyframeEffect()) {
error_string = "Effect must be a KeyframeEffect object";
return false;
}
keyframe_effects.push_back(ToKeyframeEffect(effect));
} else {
const HeapVector<Member<AnimationEffect>>& effect_sequence =
effects.GetAsAnimationEffectSequence();
keyframe_effects.ReserveInitialCapacity(effect_sequence.size());
for (const auto& effect : effect_sequence) {
if (!effect->IsKeyframeEffect()) {
error_string = "Effects must all be KeyframeEffect objects";
return false;
}
keyframe_effects.push_back(ToKeyframeEffect(effect));
}
}
if (keyframe_effects.IsEmpty()) {
error_string = "Effects array must be non-empty";
return false;
}
if (keyframe_effects.size() > 1) {
// TODO(yigu): We should allow group effects eventually by spec. See
// crbug.com/767043.
error_string = "Multiple effects are not currently supported";
return false;
}
// TODO(crbug.com/781816): Allow using effects with no target.
for (const auto& effect : keyframe_effects) {
if (!effect->target()) {
error_string = "All effect targets must exist";
return false;
}
}
Document& target_document = keyframe_effects.at(0)->target()->GetDocument();
for (const auto& effect : keyframe_effects) {
if (effect->target()->GetDocument() != target_document) {
error_string = "All effects must target elements in the same document";
return false;
}
}
return true;
}
bool ValidateTimeline(const DocumentTimelineOrScrollTimeline& timeline,
String& error_string) {
if (timeline.IsScrollTimeline()) {
DoubleOrScrollTimelineAutoKeyword time_range;
timeline.GetAsScrollTimeline()->timeRange(time_range);
if (time_range.IsScrollTimelineAutoKeyword()) {
error_string = "ScrollTimeline timeRange must have non-auto value";
return false;
}
}
return true;
}
AnimationTimeline* ConvertAnimationTimeline(
const Document& document,
const DocumentTimelineOrScrollTimeline& timeline) {
if (timeline.IsScrollTimeline())
return timeline.GetAsScrollTimeline();
if (timeline.IsDocumentTimeline())
return timeline.GetAsDocumentTimeline();
return &document.Timeline();
}
bool CheckElementComposited(const Node& target) {
return target.GetLayoutObject() &&
target.GetLayoutObject()->GetCompositingState() ==
kPaintsIntoOwnBacking;
}
base::Optional<CompositorElementId> GetCompositorScrollElementId(
const Node& node) {
if (!node.GetLayoutObject() || !node.GetLayoutObject()->UniqueId())
return base::nullopt;
return CompositorElementIdFromUniqueObjectId(
node.GetLayoutObject()->UniqueId(),
CompositorElementIdNamespace::kScroll);
}
// Convert the blink concept of a ScrollTimeline orientation into the cc one.
//
// The compositor does not know about writing modes, so we have to convert the
// web concepts of 'block' and 'inline' direction into absolute vertical or
// horizontal directions.
//
// TODO(smcgruer): If the writing mode of a scroller changes, we have to update
// any related cc::ScrollTimeline somehow.
CompositorScrollTimeline::ScrollDirection ConvertOrientation(
ScrollTimeline::ScrollDirection orientation,
bool is_horizontal_writing_mode) {
switch (orientation) {
case ScrollTimeline::Block:
return is_horizontal_writing_mode ? CompositorScrollTimeline::Vertical
: CompositorScrollTimeline::Horizontal;
case ScrollTimeline::Inline:
return is_horizontal_writing_mode ? CompositorScrollTimeline::Horizontal
: CompositorScrollTimeline::Vertical;
default:
NOTREACHED();
return CompositorScrollTimeline::Vertical;
}
}
// Converts a blink::ScrollTimeline into a cc::ScrollTimeline.
//
// If the timeline cannot be converted, returns nullptr.
std::unique_ptr<CompositorScrollTimeline> ToCompositorScrollTimeline(
AnimationTimeline* timeline) {
if (!timeline || timeline->IsDocumentTimeline())
return nullptr;
ScrollTimeline* scroll_timeline = ToScrollTimeline(timeline);
Node* scroll_source = scroll_timeline->ResolvedScrollSource();
base::Optional<CompositorElementId> element_id =
GetCompositorScrollElementId(*scroll_source);
DoubleOrScrollTimelineAutoKeyword time_range;
scroll_timeline->timeRange(time_range);
// TODO(smcgruer): Handle 'auto' time range value.
DCHECK(time_range.IsDouble());
// TODO(smcgruer): If the scroll source later gets a LayoutBox (e.g. was
// display:none and now isn't), we need to update the compositor with the
// writing mode to get the correct ScrollDirection conversion.
LayoutBox* box = scroll_source->GetLayoutBox();
CompositorScrollTimeline::ScrollDirection orientation =
ConvertOrientation(scroll_timeline->GetOrientation(),
box ? box->IsHorizontalWritingMode() : true);
return std::make_unique<CompositorScrollTimeline>(element_id, orientation,
time_range.GetAsDouble());
}
void StartEffectOnCompositor(CompositorAnimation* animation,
KeyframeEffect* effect) {
DCHECK(effect);
Element& target = *effect->target();
effect->Model()->SnapshotAllCompositorKeyframesIfNecessary(
target, target.ComputedStyleRef(), target.ParentComputedStyle());
int group = 0;
base::Optional<double> start_time = base::nullopt;
double time_offset = 0;
double playback_rate = 1;
effect->StartAnimationOnCompositor(group, start_time, time_offset,
playback_rate, animation);
}
unsigned NextSequenceNumber() {
// TODO(majidvp): This should actually come from the same source as other
// animation so that they have the correct ordering.
static unsigned next = 0;
return ++next;
}
} // namespace
WorkletAnimation* WorkletAnimation::Create(
ExecutionContext* context,
String animator_name,
const AnimationEffectOrAnimationEffectSequence& effects,
ExceptionState& exception_state) {
return Create(context, animator_name, effects,
DocumentTimelineOrScrollTimeline(), nullptr, exception_state);
}
WorkletAnimation* WorkletAnimation::Create(
ExecutionContext* context,
String animator_name,
const AnimationEffectOrAnimationEffectSequence& effects,
DocumentTimelineOrScrollTimeline timeline,
ExceptionState& exception_state) {
return Create(context, animator_name, effects, timeline, nullptr,
exception_state);
}
WorkletAnimation* WorkletAnimation::Create(
ExecutionContext* context,
String animator_name,
const AnimationEffectOrAnimationEffectSequence& effects,
DocumentTimelineOrScrollTimeline timeline,
scoped_refptr<SerializedScriptValue> options,
ExceptionState& exception_state) {
DCHECK(IsMainThread());
if (!Platform::Current()->IsThreadedAnimationEnabled()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"AnimationWorklet requires threaded animations to be enabled");
return nullptr;
}
HeapVector<Member<KeyframeEffect>> keyframe_effects;
String error_string;
if (!ConvertAnimationEffects(effects, keyframe_effects, error_string)) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
error_string);
return nullptr;
}
if (!ValidateTimeline(timeline, error_string)) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
error_string);
return nullptr;
}
AnimationWorklet* worklet =
WindowAnimationWorklet::animationWorklet(*context->ExecutingWindow());
WorkletAnimationId id = worklet->NextWorkletAnimationId();
Document& document = keyframe_effects.at(0)->target()->GetDocument();
AnimationTimeline* animation_timeline =
ConvertAnimationTimeline(document, timeline);
WorkletAnimation* animation =
new WorkletAnimation(id, animator_name, document, keyframe_effects,
animation_timeline, std::move(options));
return animation;
}
WorkletAnimation::WorkletAnimation(
WorkletAnimationId id,
const String& animator_name,
Document& document,
const HeapVector<Member<KeyframeEffect>>& effects,
AnimationTimeline* timeline,
scoped_refptr<SerializedScriptValue> options)
: sequence_number_(NextSequenceNumber()),
id_(id),
animator_name_(animator_name),
play_state_(Animation::kIdle),
document_(document),
effects_(effects),
timeline_(timeline),
options_(std::make_unique<WorkletAnimationOptions>(options)),
effect_needs_restart_(false) {
DCHECK(IsMainThread());
DCHECK(Platform::Current()->IsThreadedAnimationEnabled());
AnimationEffect* target_effect = effects_.at(0);
target_effect->Attach(this);
if (timeline_->IsScrollTimeline())
ToScrollTimeline(timeline_)->AttachAnimation();
}
String WorkletAnimation::playState() {
DCHECK(IsMainThread());
return Animation::PlayStateString(play_state_);
}
void WorkletAnimation::play() {
DCHECK(IsMainThread());
if (play_state_ == Animation::kPending)
return;
document_->GetWorkletAnimationController().AttachAnimation(*this);
play_state_ = Animation::kPending;
Element* target = GetEffect()->target();
if (!target)
return;
target->EnsureElementAnimations().GetWorkletAnimations().insert(this);
// TODO(majidvp): This should be removed once worklet animation correctly
// updates its effect timing. https://crbug.com/814851.
target->SetNeedsAnimationStyleRecalc();
}
void WorkletAnimation::cancel() {
DCHECK(IsMainThread());
if (play_state_ == Animation::kIdle)
return;
document_->GetWorkletAnimationController().DetachAnimation(*this);
if (compositor_animation_) {
GetEffect()->CancelAnimationOnCompositor(compositor_animation_.get());
DestroyCompositorAnimation();
}
play_state_ = Animation::kIdle;
Element* target = GetEffect()->target();
if (!target)
return;
target->EnsureElementAnimations().GetWorkletAnimations().erase(this);
// TODO(majidvp): This should be removed once worklet animation correctly
// updates its effect timing. https://crbug.com/814851.
target->SetNeedsAnimationStyleRecalc();
}
bool WorkletAnimation::Playing() const {
return play_state_ == Animation::kRunning;
}
void WorkletAnimation::UpdateIfNecessary() {
// TODO(crbug.com/833846): This is updating more often than necessary. This
// gets fixed once WorkletAnimation becomes a subclass of Animation.
Update(kTimingUpdateOnDemand);
}
void WorkletAnimation::EffectInvalidated() {
effect_needs_restart_ = true;
document_->GetWorkletAnimationController().InvalidateAnimation(*this);
}
void WorkletAnimation::Update(TimingUpdateReason reason) {
if (play_state_ != Animation::kRunning)
return;
if (!start_time_)
return;
// TODO(crbug.com/756359): For now we use 0 as inherited time in but we will
// need to get the inherited time from worklet context.
double inherited_time_seconds = 0;
GetEffect()->UpdateInheritedTime(inherited_time_seconds, reason);
}
bool WorkletAnimation::UpdateCompositingState() {
switch (play_state_) {
case Animation::kPending: {
String failure_message;
if (StartOnCompositor(&failure_message))
return true;
document_->AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kWarningMessageLevel, failure_message));
return false;
}
case Animation::kRunning: {
UpdateOnCompositor();
return false;
}
default:
return false;
}
}
bool WorkletAnimation::StartOnCompositor(String* failure_message) {
DCHECK(IsMainThread());
Element& target = *GetEffect()->target();
// TODO(crbug.com/836393): This should not be possible but it is currently
// happening and needs to be investigated/fixed.
if (!target.GetComputedStyle()) {
if (failure_message)
*failure_message = "The target element does not have style.";
return false;
}
// CheckCanStartAnimationOnCompositor requires that the property-specific
// keyframe groups have been created. To ensure this we manually snapshot the
// frames in the target effect.
// TODO(smcgruer): This shouldn't be necessary - Animation doesn't do this.
GetEffect()->Model()->SnapshotAllCompositorKeyframesIfNecessary(
target, target.ComputedStyleRef(), target.ParentComputedStyle());
if (!CheckElementComposited(target)) {
if (failure_message)
*failure_message = "The target element is not composited.";
return false;
}
double playback_rate = 1;
CompositorAnimations::FailureCode failure_code =
GetEffect()->CheckCanStartAnimationOnCompositor(playback_rate);
if (!failure_code.Ok()) {
play_state_ = Animation::kIdle;
if (failure_message)
*failure_message = failure_code.reason;
return false;
}
if (!compositor_animation_) {
compositor_animation_ = CompositorAnimation::CreateWorkletAnimation(
id_, animator_name_, ToCompositorScrollTimeline(timeline_),
std::move(options_));
compositor_animation_->SetAnimationDelegate(this);
}
// Register ourselves on the compositor timeline. This will cause our cc-side
// animation animation to be registered.
if (CompositorAnimationTimeline* compositor_timeline =
document_->Timeline().CompositorTimeline())
compositor_timeline->AnimationAttached(*this);
CompositorAnimations::AttachCompositedLayers(target,
compositor_animation_.get());
// TODO(smcgruer): We need to start all of the effects, not just the first.
StartEffectOnCompositor(compositor_animation_.get(), GetEffect());
play_state_ = Animation::kRunning;
bool is_null;
double time = timeline_->currentTime(is_null);
if (!is_null)
start_time_ = time;
return true;
}
void WorkletAnimation::UpdateOnCompositor() {
if (effect_needs_restart_) {
// We want to update the keyframe effect on compositor animation without
// destroying the compositor animation instance. This is achieved by
// canceling, and start the blink keyframe effect on compositor.
effect_needs_restart_ = false;
GetEffect()->CancelAnimationOnCompositor(compositor_animation_.get());
StartEffectOnCompositor(compositor_animation_.get(), GetEffect());
}
if (timeline_->IsScrollTimeline()) {
Element* scroll_source = ToScrollTimeline(timeline_)->scrollSource();
compositor_animation_->UpdateScrollTimelineId(
GetCompositorScrollElementId(*scroll_source));
}
}
void WorkletAnimation::DestroyCompositorAnimation() {
if (compositor_animation_ && compositor_animation_->IsElementAttached())
compositor_animation_->DetachElement();
if (CompositorAnimationTimeline* compositor_timeline =
document_->Timeline().CompositorTimeline())
compositor_timeline->AnimationDestroyed(*this);
if (compositor_animation_) {
compositor_animation_->SetAnimationDelegate(nullptr);
compositor_animation_ = nullptr;
}
}
KeyframeEffect* WorkletAnimation::GetEffect() const {
DCHECK(effects_.at(0));
return effects_.at(0);
}
void WorkletAnimation::Dispose() {
DCHECK(IsMainThread());
if (timeline_->IsScrollTimeline())
ToScrollTimeline(timeline_)->DetachAnimation();
DestroyCompositorAnimation();
}
void WorkletAnimation::Trace(blink::Visitor* visitor) {
visitor->Trace(document_);
visitor->Trace(effects_);
visitor->Trace(timeline_);
WorkletAnimationBase::Trace(visitor);
}
} // namespace blink