blob: 0c77450f875926ccf1f46598cdfeea659a796562 [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 "modules/animationworklet/WorkletAnimation.h"
#include "bindings/modules/v8/animation_effect_read_only_or_animation_effect_read_only_sequence.h"
#include "core/animation/ElementAnimations.h"
#include "core/animation/KeyframeEffectModel.h"
#include "core/animation/ScrollTimeline.h"
#include "core/animation/Timing.h"
#include "core/dom/Node.h"
#include "core/dom/NodeComputedStyle.h"
#include "core/layout/LayoutBox.h"
#include "platform/wtf/text/WTFString.h"
#include "public/platform/Platform.h"
#include "public/platform/WebCompositorSupport.h"
namespace blink {
namespace {
bool ConvertAnimationEffects(
const AnimationEffectReadOnlyOrAnimationEffectReadOnlySequence& effects,
HeapVector<Member<KeyframeEffectReadOnly>>& keyframe_effects,
String& error_string) {
DCHECK(keyframe_effects.IsEmpty());
// Currently we only support KeyframeEffectReadOnly (and its subclasses).
if (effects.IsAnimationEffectReadOnly()) {
const auto& effect = effects.GetAsAnimationEffectReadOnly();
if (!effect->IsKeyframeEffectReadOnly()) {
error_string = "Effect must be a KeyframeEffectReadOnly object";
return false;
}
keyframe_effects.push_back(ToKeyframeEffectReadOnly(effect));
} else {
const HeapVector<Member<AnimationEffectReadOnly>>& effect_sequence =
effects.GetAsAnimationEffectReadOnlySequence();
keyframe_effects.ReserveInitialCapacity(effect_sequence.size());
for (const auto& effect : effect_sequence) {
if (!effect->IsKeyframeEffectReadOnly()) {
error_string = "Effects must all be KeyframeEffectReadOnly objects";
return false;
}
keyframe_effects.push_back(ToKeyframeEffectReadOnly(effect));
}
}
if (keyframe_effects.IsEmpty()) {
error_string = "Effects array must be non-empty";
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;
}
bool CheckElementComposited(const Element& target) {
return target.GetLayoutObject() &&
target.GetLayoutObject()->GetCompositingState() ==
kPaintsIntoOwnBacking;
}
CompositorElementId GetCompositorScrollElementId(const Element& element) {
DCHECK(element.GetLayoutObject());
DCHECK(element.GetLayoutObject()->HasLayer());
return CompositorElementIdFromUniqueObjectId(
element.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(
const DocumentTimelineOrScrollTimeline& timeline) {
if (!timeline.IsScrollTimeline())
return nullptr;
ScrollTimeline* scroll_timeline = timeline.GetAsScrollTimeline();
Element* scroll_source = scroll_timeline->scrollSource();
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());
LayoutBox* box = scroll_source->GetLayoutBox();
DCHECK(box);
CompositorScrollTimeline::ScrollDirection orientation = ConvertOrientation(
scroll_timeline->GetOrientation(), box->IsHorizontalWritingMode());
return std::make_unique<CompositorScrollTimeline>(element_id, orientation,
time_range.GetAsDouble());
}
} // namespace
WorkletAnimation* WorkletAnimation::Create(
String animator_name,
const AnimationEffectReadOnlyOrAnimationEffectReadOnlySequence& effects,
DocumentTimelineOrScrollTimeline timeline,
scoped_refptr<SerializedScriptValue> options,
ExceptionState& exception_state) {
DCHECK(IsMainThread());
HeapVector<Member<KeyframeEffectReadOnly>> keyframe_effects;
String error_string;
if (!ConvertAnimationEffects(effects, keyframe_effects, error_string)) {
exception_state.ThrowDOMException(kNotSupportedError, error_string);
return nullptr;
}
if (!ValidateTimeline(timeline, error_string)) {
exception_state.ThrowDOMException(kNotSupportedError, error_string);
return nullptr;
}
Document& document = keyframe_effects.at(0)->Target()->GetDocument();
WorkletAnimation* animation = new WorkletAnimation(
animator_name, document, keyframe_effects, timeline, std::move(options));
return animation;
}
WorkletAnimation::WorkletAnimation(
const String& animator_name,
Document& document,
const HeapVector<Member<KeyframeEffectReadOnly>>& effects,
DocumentTimelineOrScrollTimeline timeline,
scoped_refptr<SerializedScriptValue> options)
: animator_name_(animator_name),
play_state_(Animation::kIdle),
document_(document),
effects_(effects),
timeline_(timeline),
options_(std::move(options)) {
DCHECK(IsMainThread());
DCHECK(Platform::Current()->IsThreadedAnimationEnabled());
DCHECK(Platform::Current()->CompositorSupport());
}
String WorkletAnimation::playState() {
DCHECK(IsMainThread());
return Animation::PlayStateString(play_state_);
}
void WorkletAnimation::play() {
DCHECK(IsMainThread());
if (play_state_ != Animation::kPending) {
document_->GetWorkletAnimationController().AttachAnimation(*this);
play_state_ = Animation::kPending;
}
}
void WorkletAnimation::cancel() {
DCHECK(IsMainThread());
if (play_state_ != Animation::kIdle) {
document_->GetWorkletAnimationController().DetachAnimation(*this);
play_state_ = Animation::kIdle;
}
}
bool WorkletAnimation::StartOnCompositor(String* failure_message) {
DCHECK(IsMainThread());
KeyframeEffectReadOnly* target_effect = effects_.at(0);
Element& target = *target_effect->Target();
// 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.
target_effect->Model()->SnapshotAllCompositorKeyframes(
target, target.ComputedStyleRef(), target.ParentComputedStyle());
if (!CheckElementComposited(target)) {
if (failure_message)
*failure_message = "The target element is not composited.";
return false;
}
if (timeline_.IsScrollTimeline() &&
!CheckElementComposited(
*timeline_.GetAsScrollTimeline()->scrollSource())) {
if (failure_message)
*failure_message = "The ScrollTimeline scrollSource is not composited.";
return false;
}
double playback_rate = 1;
CompositorAnimations::FailureCode failure_code =
target_effect->CheckCanStartAnimationOnCompositor(playback_rate);
if (!failure_code.Ok()) {
play_state_ = Animation::kIdle;
if (failure_message)
*failure_message = failure_code.reason;
return false;
}
if (!compositor_player_) {
compositor_player_ = CompositorAnimationPlayer::CreateWorkletPlayer(
animator_name_, ToCompositorScrollTimeline(timeline_));
compositor_player_->SetAnimationDelegate(this);
}
// Register ourselves on the compositor timeline. This will cause our cc-side
// animation player to be registered.
if (CompositorAnimationTimeline* compositor_timeline =
document_->Timeline().CompositorTimeline())
compositor_timeline->PlayerAttached(*this);
// TODO(smcgruer): Creating a WorkletAnimation should be a hint to blink
// compositing that the animated element should be promoted. Otherwise this
// fails. http://crbug.com/776533
CompositorAnimations::AttachCompositedLayers(target,
compositor_player_.get());
double start_time = std::numeric_limits<double>::quiet_NaN();
double time_offset = 0;
int group = 0;
// TODO(smcgruer): We need to start all of the effects, not just the first.
effects_.at(0)->StartAnimationOnCompositor(
group, start_time, time_offset, playback_rate, compositor_player_.get());
play_state_ = Animation::kRunning;
return true;
}
void WorkletAnimation::Dispose() {
DCHECK(IsMainThread());
if (CompositorAnimationTimeline* compositor_timeline =
document_->Timeline().CompositorTimeline())
compositor_timeline->PlayerDestroyed(*this);
if (compositor_player_) {
compositor_player_->SetAnimationDelegate(nullptr);
compositor_player_ = nullptr;
}
}
void WorkletAnimation::Trace(blink::Visitor* visitor) {
visitor->Trace(document_);
visitor->Trace(effects_);
visitor->Trace(timeline_);
WorkletAnimationBase::Trace(visitor);
}
} // namespace blink