| // 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 |