| /* |
| * Copyright (C) 2013 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "third_party/blink/renderer/core/animation/animation.h" |
| |
| #include <limits> |
| #include <memory> |
| |
| #include "base/metrics/histogram_macros.h" |
| #include "third_party/blink/public/platform/platform.h" |
| #include "third_party/blink/public/platform/task_type.h" |
| #include "third_party/blink/renderer/core/animation/animation_timeline.h" |
| #include "third_party/blink/renderer/core/animation/css/css_animations.h" |
| #include "third_party/blink/renderer/core/animation/document_timeline.h" |
| #include "third_party/blink/renderer/core/animation/keyframe_effect.h" |
| #include "third_party/blink/renderer/core/animation/pending_animations.h" |
| #include "third_party/blink/renderer/core/css/style_change_reason.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/dom_node_ids.h" |
| #include "third_party/blink/renderer/core/events/animation_playback_event.h" |
| #include "third_party/blink/renderer/core/execution_context/execution_context.h" |
| #include "third_party/blink/renderer/core/frame/use_counter.h" |
| #include "third_party/blink/renderer/core/inspector/inspector_trace_events.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer.h" |
| #include "third_party/blink/renderer/core/probe/core_probes.h" |
| #include "third_party/blink/renderer/platform/animation/compositor_animation.h" |
| #include "third_party/blink/renderer/platform/bindings/script_forbidden_scope.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/heap/persistent.h" |
| #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h" |
| #include "third_party/blink/renderer/platform/wtf/math_extras.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| unsigned NextSequenceNumber() { |
| static unsigned next = 0; |
| return ++next; |
| } |
| |
| double ToMilliseconds(double seconds) { |
| return seconds * 1000; |
| } |
| |
| void RecordCompositorAnimationFailureReasons( |
| CompositorAnimations::FailureReasons failure_reasons) { |
| // UMA_HISTOGRAM_ENUMERATION requires that the enum_max must be strictly |
| // greater than the sample value. kFailureReasonCount doesn't include the |
| // kNoFailure value but the histograms do so adding the +1 is necessary. |
| // TODO(dcheng): Fix https://crbug.com/705169 so this isn't needed. |
| constexpr uint32_t kFailureReasonEnumMax = |
| CompositorAnimations::kFailureReasonCount + 1; |
| |
| if (failure_reasons == CompositorAnimations::kNoFailure) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "Blink.Animation.CompositedAnimationFailureReason", |
| CompositorAnimations::kNoFailure, kFailureReasonEnumMax); |
| return; |
| } |
| |
| for (uint32_t i = 0; i < CompositorAnimations::kFailureReasonCount; i++) { |
| unsigned val = 1 << i; |
| if (failure_reasons & val) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "Blink.Animation.CompositedAnimationFailureReason", i + 1, |
| kFailureReasonEnumMax); |
| } |
| } |
| } |
| } // namespace |
| |
| Animation* Animation::Create(AnimationEffect* effect, |
| AnimationTimeline* timeline, |
| ExceptionState& exception_state) { |
| if (!timeline || !timeline->IsDocumentTimeline()) { |
| // FIXME: Support creating animations without a timeline. |
| exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError, |
| "Animations can currently only be " |
| "created with a non-null " |
| "DocumentTimeline"); |
| return nullptr; |
| } |
| |
| DocumentTimeline* subtimeline = ToDocumentTimeline(timeline); |
| |
| Animation* animation = MakeGarbageCollected<Animation>( |
| subtimeline->GetDocument()->ContextDocument(), *subtimeline, effect); |
| |
| if (subtimeline) { |
| subtimeline->AnimationAttached(*animation); |
| animation->AttachCompositorTimeline(); |
| } |
| |
| return animation; |
| } |
| |
| Animation* Animation::Create(ExecutionContext* execution_context, |
| AnimationEffect* effect, |
| ExceptionState& exception_state) { |
| Document* document = To<Document>(execution_context); |
| return Create(effect, &document->Timeline(), exception_state); |
| } |
| |
| Animation* Animation::Create(ExecutionContext* execution_context, |
| AnimationEffect* effect, |
| AnimationTimeline* timeline, |
| ExceptionState& exception_state) { |
| if (!timeline) { |
| return Create(execution_context, effect, exception_state); |
| } |
| |
| return Create(effect, timeline, exception_state); |
| } |
| |
| Animation::Animation(ExecutionContext* execution_context, |
| DocumentTimeline& timeline, |
| AnimationEffect* content) |
| : ContextLifecycleObserver(execution_context), |
| internal_play_state_(kIdle), |
| animation_play_state_(kIdle), |
| playback_rate_(1), |
| start_time_(), |
| hold_time_(), |
| sequence_number_(NextSequenceNumber()), |
| content_(content), |
| timeline_(&timeline), |
| paused_(false), |
| is_paused_for_testing_(false), |
| is_composited_animation_disabled_for_testing_(false), |
| pending_pause_(false), |
| pending_play_(false), |
| outdated_(false), |
| finished_(true), |
| compositor_state_(nullptr), |
| compositor_pending_(false), |
| compositor_group_(0), |
| current_time_pending_(false), |
| state_is_being_updated_(false), |
| effect_suppressed_(false) { |
| if (content_) { |
| if (content_->GetAnimation()) { |
| content_->GetAnimation()->cancel(); |
| content_->GetAnimation()->setEffect(nullptr); |
| } |
| content_->Attach(this); |
| } |
| probe::DidCreateAnimation(timeline_->GetDocument(), sequence_number_); |
| } |
| |
| Animation::~Animation() { |
| // Verify that compositor_animation_ has been disposed of. |
| DCHECK(!compositor_animation_); |
| } |
| |
| void Animation::Dispose() { |
| DestroyCompositorAnimation(); |
| // If the DocumentTimeline and its Animation objects are |
| // finalized by the same GC, we have to eagerly clear out |
| // this Animation object's compositor animation registration. |
| DCHECK(!compositor_animation_); |
| } |
| |
| double Animation::EffectEnd() const { |
| return content_ ? content_->EndTimeInternal() : 0; |
| } |
| |
| bool Animation::Limited(double current_time) const { |
| return (playback_rate_ < 0 && current_time <= 0) || |
| (playback_rate_ > 0 && current_time >= EffectEnd()); |
| } |
| |
| void Animation::setCurrentTime(double new_current_time, |
| bool is_null, |
| ExceptionState& exception_state) { |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand); |
| |
| // Step 1. of the procedure to silently set the current time of an |
| // animation states that we abort if the new time is null. |
| if (is_null) { |
| // If the current time is resolved, then throw a TypeError. |
| if (!IsNull(CurrentTimeInternal())) { |
| exception_state.ThrowTypeError( |
| "currentTime may not be changed from resolved to unresolved"); |
| } |
| return; |
| } |
| |
| if (PlayStateInternal() == kIdle) |
| paused_ = true; |
| |
| current_time_pending_ = false; |
| internal_play_state_ = kUnset; |
| SetCurrentTimeInternal(new_current_time / 1000, kTimingUpdateOnDemand); |
| |
| if (CalculatePlayState() == kFinished) |
| start_time_ = CalculateStartTime(new_current_time); |
| } |
| |
| void Animation::SetCurrentTimeInternal(double new_current_time, |
| TimingUpdateReason reason) { |
| DCHECK(std::isfinite(new_current_time)); |
| |
| bool outdated = false; |
| bool is_limited = Limited(new_current_time); |
| bool is_held = paused_ || !playback_rate_ || is_limited || !start_time_; |
| if (is_held) { |
| // We only need to update the animation if the seek changes the hold time. |
| if (!hold_time_ || hold_time_ != new_current_time) |
| outdated = true; |
| hold_time_ = new_current_time; |
| if (paused_ || !playback_rate_) { |
| start_time_ = base::nullopt; |
| } else if (is_limited && !start_time_ && |
| reason == kTimingUpdateForAnimationFrame) { |
| start_time_ = CalculateStartTime(new_current_time); |
| } |
| } else { |
| hold_time_ = base::nullopt; |
| start_time_ = CalculateStartTime(new_current_time); |
| finished_ = false; |
| outdated = true; |
| } |
| |
| if (outdated) { |
| SetOutdated(); |
| } |
| } |
| |
| // Update timing to reflect updated animation clock due to tick |
| void Animation::UpdateCurrentTimingState(TimingUpdateReason reason) { |
| if (internal_play_state_ == kIdle) |
| return; |
| if (hold_time_) { |
| double new_current_time = hold_time_.value(); |
| if (internal_play_state_ == kFinished && start_time_ && timeline_) { |
| // Add hystersis due to floating point error accumulation |
| if (!Limited(CalculateCurrentTime() + 0.001 * playback_rate_)) { |
| // The current time became unlimited, eg. due to a backwards |
| // seek of the timeline. |
| new_current_time = CalculateCurrentTime(); |
| } else if (!Limited(hold_time_.value())) { |
| // The hold time became unlimited, eg. due to the effect |
| // becoming longer. |
| new_current_time = |
| clampTo<double>(CalculateCurrentTime(), 0, EffectEnd()); |
| } |
| } |
| SetCurrentTimeInternal(new_current_time, reason); |
| } else if (Limited(CalculateCurrentTime())) { |
| hold_time_ = playback_rate_ < 0 ? 0 : EffectEnd(); |
| } |
| } |
| |
| double Animation::startTime(bool& is_null) const { |
| base::Optional<double> result = startTime(); |
| is_null = !result; |
| return result.value_or(0); |
| } |
| |
| base::Optional<double> Animation::startTime() const { |
| return start_time_ ? base::make_optional(start_time_.value() * 1000) |
| : base::nullopt; |
| } |
| |
| double Animation::currentTime(bool& is_null) { |
| double result = currentTime(); |
| is_null = std::isnan(result); |
| return result; |
| } |
| |
| // https://drafts.csswg.org/web-animations/#the-current-time-of-an-animation |
| double Animation::currentTime() { |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand); |
| |
| // 1. If the animation’s hold time is resolved, |
| // The current time is the animation’s hold time. |
| if (hold_time_.has_value()) |
| return ToMilliseconds(hold_time_.value()); |
| |
| // 2. If any of the following are true: |
| // * the animation has no associated timeline, or |
| // * the associated timeline is inactive, or |
| // * the animation’s start time is unresolved. |
| // The current time is an unresolved time value. |
| if (!timeline_ || PlayStateInternal() == kIdle || !start_time_) |
| return NullValue(); |
| |
| // 3. Otherwise, |
| // current time = (timeline time - start time) × playback rate |
| double current_time = |
| (timeline_->EffectiveTime() - start_time_.value()) * playback_rate_; |
| return ToMilliseconds(current_time); |
| } |
| |
| double Animation::CurrentTimeInternal() const { |
| double result = hold_time_.value_or(CalculateCurrentTime()); |
| #if DCHECK_IS_ON() |
| // We can't enforce this check during Unset due to other assertions. |
| if (internal_play_state_ != kUnset) { |
| const_cast<Animation*>(this)->UpdateCurrentTimingState( |
| kTimingUpdateOnDemand); |
| double hold_or_current_time = hold_time_.value_or(CalculateCurrentTime()); |
| DCHECK((IsNull(result) && IsNull(hold_or_current_time)) || |
| result == hold_or_current_time); |
| } |
| #endif |
| return result; |
| } |
| |
| double Animation::UnlimitedCurrentTimeInternal() const { |
| #if DCHECK_IS_ON() |
| CurrentTimeInternal(); |
| #endif |
| return PlayStateInternal() == kPaused || !start_time_ |
| ? CurrentTimeInternal() |
| : CalculateCurrentTime(); |
| } |
| |
| bool Animation::PreCommit( |
| int compositor_group, |
| const PaintArtifactCompositor* paint_artifact_compositor, |
| bool start_on_compositor) { |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand, |
| kDoNotSetCompositorPending); |
| |
| bool soft_change = |
| compositor_state_ && |
| (Paused() || compositor_state_->playback_rate != playback_rate_); |
| bool hard_change = |
| compositor_state_ && (compositor_state_->effect_changed || |
| compositor_state_->start_time != start_time_ || |
| !compositor_state_->start_time || !start_time_); |
| |
| // FIXME: softChange && !hardChange should generate a Pause/ThenStart, |
| // not a Cancel, but we can't communicate these to the compositor yet. |
| |
| bool changed = soft_change || hard_change; |
| bool should_cancel = (!Playing() && compositor_state_) || changed; |
| bool should_start = Playing() && (!compositor_state_ || changed); |
| |
| if (start_on_compositor && should_cancel && should_start && |
| compositor_state_ && compositor_state_->pending_action == kStart) { |
| // Restarting but still waiting for a start time. |
| return false; |
| } |
| |
| if (should_cancel) { |
| CancelAnimationOnCompositor(); |
| compositor_state_ = nullptr; |
| } |
| |
| DCHECK(!compositor_state_ || compositor_state_->start_time); |
| |
| if (!should_start) { |
| current_time_pending_ = false; |
| } |
| |
| if (should_start) { |
| compositor_group_ = compositor_group; |
| if (start_on_compositor) { |
| CompositorAnimations::FailureReasons failure_reasons = |
| CheckCanStartAnimationOnCompositor(paint_artifact_compositor); |
| RecordCompositorAnimationFailureReasons(failure_reasons); |
| |
| if (failure_reasons == CompositorAnimations::kNoFailure) { |
| CreateCompositorAnimation(); |
| StartAnimationOnCompositor(paint_artifact_compositor); |
| compositor_state_ = std::make_unique<CompositorState>(*this); |
| } else { |
| CancelIncompatibleAnimationsOnCompositor(); |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| void Animation::PostCommit(double timeline_time) { |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand, |
| kDoNotSetCompositorPending); |
| |
| compositor_pending_ = false; |
| |
| if (!compositor_state_ || compositor_state_->pending_action == kNone) |
| return; |
| |
| switch (compositor_state_->pending_action) { |
| case kStart: |
| if (compositor_state_->start_time) { |
| DCHECK_EQ(start_time_.value(), compositor_state_->start_time.value()); |
| compositor_state_->pending_action = kNone; |
| } |
| break; |
| case kPause: |
| case kPauseThenStart: |
| DCHECK(!start_time_); |
| compositor_state_->pending_action = kNone; |
| SetCurrentTimeInternal( |
| (timeline_time - compositor_state_->start_time.value()) * |
| playback_rate_, |
| kTimingUpdateForAnimationFrame); |
| current_time_pending_ = false; |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| void Animation::NotifyCompositorStartTime(double timeline_time) { |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand, |
| kDoNotSetCompositorPending); |
| |
| if (compositor_state_) { |
| DCHECK_EQ(compositor_state_->pending_action, kStart); |
| DCHECK(!compositor_state_->start_time); |
| |
| double initial_compositor_hold_time = |
| compositor_state_->hold_time.value_or(NullValue()); |
| compositor_state_->pending_action = kNone; |
| // TODO(crbug.com/791086): Determine whether this can ever be null. |
| double start_time = timeline_time + CurrentTimeInternal() / -playback_rate_; |
| compositor_state_->start_time = |
| IsNull(start_time) ? base::nullopt : base::make_optional(start_time); |
| |
| if (start_time_ == timeline_time) { |
| // The start time was set to the incoming compositor start time. |
| // Unlikely, but possible. |
| // FIXME: Depending on what changed above this might still be pending. |
| // Maybe... |
| current_time_pending_ = false; |
| return; |
| } |
| |
| if (start_time_ || CurrentTimeInternal() != initial_compositor_hold_time) { |
| // A new start time or current time was set while starting. |
| SetCompositorPending(true); |
| return; |
| } |
| } |
| |
| NotifyStartTime(timeline_time); |
| } |
| |
| void Animation::NotifyStartTime(double timeline_time) { |
| if (Playing()) { |
| DCHECK(!start_time_); |
| DCHECK(hold_time_.has_value()); |
| |
| if (playback_rate_ == 0) { |
| SetStartTimeInternal(timeline_time); |
| } else { |
| SetStartTimeInternal(timeline_time + |
| CurrentTimeInternal() / -playback_rate_); |
| } |
| |
| // FIXME: This avoids marking this animation as outdated needlessly when a |
| // start time is notified, but we should refactor how outdating works to |
| // avoid this. |
| ClearOutdated(); |
| current_time_pending_ = false; |
| } |
| } |
| |
| bool Animation::Affects(const Element& element, |
| const CSSProperty& property) const { |
| if (!content_ || !content_->IsKeyframeEffect()) |
| return false; |
| |
| const KeyframeEffect* effect = ToKeyframeEffect(content_.Get()); |
| return (effect->target() == &element) && |
| effect->Affects(PropertyHandle(property)); |
| } |
| |
| base::Optional<double> Animation::CalculateStartTime( |
| double current_time) const { |
| base::Optional<double> start_time = |
| timeline_->EffectiveTime() - current_time / playback_rate_; |
| DCHECK(!IsNull(start_time.value())); |
| return start_time; |
| } |
| |
| double Animation::CalculateCurrentTime() const { |
| if (!start_time_ || !timeline_) |
| return NullValue(); |
| return (timeline_->EffectiveTime() - start_time_.value()) * playback_rate_; |
| } |
| |
| // TODO(crbug.com/771722): This doesn't handle anim.startTime = null; we just |
| // silently convert that to anim.startTime = 0. |
| void Animation::setStartTime(double start_time, bool is_null) { |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand); |
| |
| if (start_time == start_time_) |
| return; |
| |
| current_time_pending_ = false; |
| internal_play_state_ = kUnset; |
| paused_ = false; |
| SetStartTimeInternal(start_time / 1000); |
| } |
| |
| void Animation::SetStartTimeInternal(base::Optional<double> new_start_time) { |
| DCHECK(!paused_); |
| DCHECK(new_start_time.has_value()); |
| DCHECK(new_start_time != start_time_); |
| |
| bool had_start_time = start_time_.has_value(); |
| double previous_current_time = CurrentTimeInternal(); |
| start_time_ = new_start_time; |
| if (hold_time_ && playback_rate_) { |
| // If held, the start time would still be derived from the hold time. |
| // Force a new, limited, current time. |
| hold_time_ = base::nullopt; |
| double current_time = CalculateCurrentTime(); |
| if (playback_rate_ > 0 && current_time > EffectEnd()) { |
| current_time = EffectEnd(); |
| } else if (playback_rate_ < 0 && current_time < 0) { |
| current_time = 0; |
| } |
| SetCurrentTimeInternal(current_time, kTimingUpdateOnDemand); |
| } |
| UpdateCurrentTimingState(kTimingUpdateOnDemand); |
| double new_current_time = CurrentTimeInternal(); |
| |
| if (previous_current_time != new_current_time) { |
| SetOutdated(); |
| } else if (!had_start_time && timeline_) { |
| // Even though this animation is not outdated, time to effect change is |
| // infinity until start time is set. |
| ForceServiceOnNextFrame(); |
| } |
| } |
| |
| void Animation::setEffect(AnimationEffect* new_effect) { |
| if (content_ == new_effect) |
| return; |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand, |
| kSetCompositorPendingWithEffectChanged); |
| |
| double stored_current_time = CurrentTimeInternal(); |
| if (content_) |
| content_->Detach(); |
| content_ = new_effect; |
| if (new_effect) { |
| // FIXME: This logic needs to be updated once groups are implemented |
| if (new_effect->GetAnimation()) { |
| new_effect->GetAnimation()->cancel(); |
| new_effect->GetAnimation()->setEffect(nullptr); |
| } |
| new_effect->Attach(this); |
| SetOutdated(); |
| } |
| if (!IsNull(stored_current_time)) |
| SetCurrentTimeInternal(stored_current_time, kTimingUpdateOnDemand); |
| } |
| |
| const char* Animation::PlayStateString(AnimationPlayState play_state) { |
| switch (play_state) { |
| case kIdle: |
| return "idle"; |
| case kPending: |
| return "pending"; |
| case kRunning: |
| return "running"; |
| case kPaused: |
| return "paused"; |
| case kFinished: |
| return "finished"; |
| default: |
| NOTREACHED(); |
| return ""; |
| } |
| } |
| |
| Animation::AnimationPlayState Animation::PlayStateInternal() const { |
| DCHECK_NE(internal_play_state_, kUnset); |
| return internal_play_state_; |
| } |
| |
| Animation::AnimationPlayState Animation::CalculatePlayState() const { |
| if (paused_ && !current_time_pending_) |
| return kPaused; |
| if (internal_play_state_ == kIdle) |
| return kIdle; |
| if (current_time_pending_ || (!start_time_ && playback_rate_ != 0)) |
| return kPending; |
| if (Limited()) |
| return kFinished; |
| return kRunning; |
| } |
| |
| // https://drafts.csswg.org/web-animations/#play-states |
| Animation::AnimationPlayState Animation::CalculateAnimationPlayState() const { |
| // 1. All of the following conditions are true: |
| // * The current time of animation is unresolved, and |
| // * animation does not have either a pending play task or a pending pause |
| // task, |
| // then idle. |
| if (IsNull(CurrentTimeInternal()) && !pending()) |
| return kIdle; |
| |
| // 2. Either of the following conditions are true: |
| // * animation has a pending pause task, or |
| // * both the start time of animation is unresolved and it does not have a |
| // pending play task, |
| // then paused. |
| // TODO(crbug.com/958433): Presently using a paused_ flag that tracks an |
| // animation being in a paused state (including a pending pause). The above |
| // rules do not yet work verbatim due to subtle timing discrepancies on when |
| // start_time gets resolved. |
| if (paused_) |
| return kPaused; |
| |
| // 3. For animation, current time is resolved and either of the following |
| // conditions are true: |
| // * animation’s effective playback rate > 0 and current time ≥ target |
| // effect end; or |
| // * animation’s effective playback rate < 0 and current time ≤ 0, |
| // then finished. |
| if (Limited()) |
| return kFinished; |
| |
| // 4. Otherwise |
| return kRunning; |
| } |
| |
| bool Animation::pending() const { |
| return pending_pause_ || pending_play_; |
| } |
| |
| // https://drafts.csswg.org/web-animations-1/#reset-an-animations-pending-tasks. |
| void Animation::ResetPendingTasks() { |
| // We use an active playback rate instead of a pending playback rate to |
| // sidestep complications of maintaining correct sequencing for updating |
| // properties like current time and start time. Our implementation performs |
| // calculations based on what will happen rather than waiting on a scheduled |
| // task to commit the changes. |
| // TODO(crbug.com/960944): Bring implementation into closer alignment with the |
| // spec. |
| active_playback_rate_.reset(); |
| pending_pause_ = false; |
| pending_play_ = false; |
| } |
| |
| void Animation::pause(ExceptionState& exception_state) { |
| if (paused_) |
| return; |
| |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand); |
| |
| double new_current_time = CurrentTimeInternal(); |
| if (CalculatePlayState() == kIdle) { |
| if (playback_rate_ < 0 && |
| EffectEnd() == std::numeric_limits<double>::infinity()) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kInvalidStateError, |
| "Cannot pause, Animation has infinite target effect end."); |
| return; |
| } |
| new_current_time = playback_rate_ < 0 ? EffectEnd() : 0; |
| } |
| |
| internal_play_state_ = kUnset; |
| |
| // We use two paused flags to indicate that the play state is paused, but that |
| // the pause has not taken affect yet (pending). On the next async update, |
| // paused will remain in affect, but the pending_pause_ flag will reset. The |
| // pending pause can be interrupted via another change to the play state ahead |
| // of the asynchronous update. |
| // TODO(crbug.com/958433): We should not require the paused_ flag based on the |
| // algorithm for determining play state in the spec. Currently, timing issues |
| // prevent direct adoption of the algorithm in the spec. |
| // (https://drafts.csswg.org/web-animations/#play-states). |
| paused_ = true; |
| pending_pause_ = true; |
| |
| current_time_pending_ = true; |
| SetCurrentTimeInternal(new_current_time, kTimingUpdateOnDemand); |
| } |
| |
| void Animation::Unpause() { |
| if (!paused_) |
| return; |
| |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand); |
| |
| current_time_pending_ = true; |
| UnpauseInternal(); |
| } |
| |
| void Animation::UnpauseInternal() { |
| if (!paused_) |
| return; |
| paused_ = false; |
| SetCurrentTimeInternal(CurrentTimeInternal(), kTimingUpdateOnDemand); |
| } |
| |
| // https://drafts.csswg.org/web-animations/#playing-an-animation-section |
| void Animation::play(ExceptionState& exception_state) { |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand); |
| |
| double current_time = this->CurrentTimeInternal(); |
| if (playback_rate_ < 0 && (current_time <= 0 || IsNull(current_time)) && |
| EffectEnd() == std::numeric_limits<double>::infinity()) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kInvalidStateError, |
| "Cannot play reversed Animation with infinite target effect end."); |
| return; |
| } |
| |
| if (!Playing()) { |
| start_time_ = base::nullopt; |
| } |
| |
| if (PlayStateInternal() == kIdle) { |
| hold_time_ = 0; |
| } |
| |
| internal_play_state_ = kUnset; |
| pending_play_ = true; |
| finished_ = false; |
| UnpauseInternal(); |
| |
| if (playback_rate_ > 0 && (IsNull(current_time) || current_time < 0 || |
| current_time >= EffectEnd())) { |
| start_time_ = base::nullopt; |
| SetCurrentTimeInternal(0, kTimingUpdateOnDemand); |
| } else if (playback_rate_ < 0 && (IsNull(current_time) || current_time <= 0 || |
| current_time > EffectEnd())) { |
| start_time_ = base::nullopt; |
| SetCurrentTimeInternal(EffectEnd(), kTimingUpdateOnDemand); |
| } |
| } |
| |
| // https://drafts.csswg.org/web-animations/#reversing-an-animation-section |
| void Animation::reverse(ExceptionState& exception_state) { |
| if (!playback_rate_) { |
| return; |
| } |
| |
| if (!active_playback_rate_.has_value()) { |
| active_playback_rate_ = playback_rate_; |
| } |
| |
| double stored_playback_rate = playback_rate_; |
| SetPlaybackRateInternal(-playback_rate_); |
| play(exception_state); |
| |
| if (exception_state.HadException()) { |
| SetPlaybackRateInternal(stored_playback_rate); |
| } |
| } |
| |
| // https://drafts.csswg.org/web-animations/#finishing-an-animation-section |
| void Animation::finish(ExceptionState& exception_state) { |
| // Force resolution of PlayStateUpdateScope to enable immediate queuing of |
| // the finished event. |
| { |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand); |
| |
| if (!playback_rate_) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kInvalidStateError, |
| "Cannot finish Animation with a playbackRate of 0."); |
| return; |
| } |
| if (playback_rate_ > 0 && |
| EffectEnd() == std::numeric_limits<double>::infinity()) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kInvalidStateError, |
| "Cannot finish Animation with an infinite target effect end."); |
| return; |
| } |
| |
| // Avoid updating start time when already finished. |
| if (CalculatePlayState() == kFinished) |
| return; |
| |
| double new_current_time = playback_rate_ < 0 ? 0 : EffectEnd(); |
| SetCurrentTimeInternal(new_current_time, kTimingUpdateOnDemand); |
| paused_ = false; |
| current_time_pending_ = false; |
| start_time_ = CalculateStartTime(new_current_time); |
| internal_play_state_ = kFinished; |
| ResetPendingTasks(); |
| } |
| // Resolve finished event immediately. |
| QueueFinishedEvent(); |
| } |
| |
| // https://drafts.csswg.org/web-animations/#setting-the-playback-rate-of-an-animation |
| void Animation::updatePlaybackRate(double playback_rate) { |
| // The implementation differs from the spec; however, the end result is |
| // consistent. Whereas Animation.playbackRate updates the playback rate |
| // immediately, updatePlaybackRate is to take effect on the next async cycle. |
| // From an implementation perspective, the difference lies in what gets |
| // reported by the playbackRate getter ahead of the async update cycle, as the |
| // Blink implementation guards against a discontinuity in current time even |
| // when updating via the playbackRate setter. |
| double stored_playback_rate = active_playback_rate_.value_or(playback_rate_); |
| AnimationPlayState play_state = CalculateAnimationPlayState(); |
| if (play_state == kRunning) |
| pending_play_ = true; |
| |
| setPlaybackRate(playback_rate); |
| |
| if (pending()) |
| active_playback_rate_ = stored_playback_rate; |
| } |
| |
| ScriptPromise Animation::finished(ScriptState* script_state) { |
| if (!finished_promise_) { |
| finished_promise_ = MakeGarbageCollected<AnimationPromise>( |
| ExecutionContext::From(script_state), this, |
| AnimationPromise::kFinished); |
| if (PlayStateInternal() == kFinished) |
| finished_promise_->Resolve(this); |
| } |
| return finished_promise_->Promise(script_state->World()); |
| } |
| |
| ScriptPromise Animation::ready(ScriptState* script_state) { |
| if (!ready_promise_) { |
| ready_promise_ = MakeGarbageCollected<AnimationPromise>( |
| ExecutionContext::From(script_state), this, AnimationPromise::kReady); |
| if (PlayStateInternal() != kPending) |
| ready_promise_->Resolve(this); |
| } |
| return ready_promise_->Promise(script_state->World()); |
| } |
| |
| const AtomicString& Animation::InterfaceName() const { |
| return event_target_names::kAnimation; |
| } |
| |
| ExecutionContext* Animation::GetExecutionContext() const { |
| return ContextLifecycleObserver::GetExecutionContext(); |
| } |
| |
| bool Animation::HasPendingActivity() const { |
| bool has_pending_promise = |
| finished_promise_ && |
| finished_promise_->GetState() == ScriptPromisePropertyBase::kPending; |
| |
| return pending_finished_event_ || has_pending_promise || |
| (!finished_ && HasEventListeners(event_type_names::kFinish)); |
| } |
| |
| void Animation::ContextDestroyed(ExecutionContext*) { |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand); |
| |
| finished_ = true; |
| pending_finished_event_ = nullptr; |
| } |
| |
| DispatchEventResult Animation::DispatchEventInternal(Event& event) { |
| if (pending_finished_event_ == &event) |
| pending_finished_event_ = nullptr; |
| return EventTargetWithInlineData::DispatchEventInternal(event); |
| } |
| |
| double Animation::playbackRate() const { |
| // TODO(crbug.com/960944): Deviates from spec implementation, which instead |
| // uses an 'effective playback rate' to be forward looking and 'playback rate' |
| // for its current value. |
| return active_playback_rate_.value_or(playback_rate_); |
| } |
| |
| void Animation::setPlaybackRate(double playback_rate) { |
| active_playback_rate_.reset(); |
| if (playback_rate == playback_rate_) |
| return; |
| |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand); |
| |
| base::Optional<double> start_time_before = start_time_; |
| SetPlaybackRateInternal(playback_rate); |
| |
| // Adds a UseCounter to check if setting playbackRate causes a compensatory |
| // seek forcing a change in start_time_ |
| if (start_time_before && start_time_ != start_time_before && |
| internal_play_state_ != kFinished) { |
| UseCounter::Count(GetExecutionContext(), |
| WebFeature::kAnimationSetPlaybackRateCompensatorySeek); |
| } |
| } |
| |
| void Animation::SetPlaybackRateInternal(double playback_rate) { |
| DCHECK(std::isfinite(playback_rate)); |
| DCHECK_NE(playback_rate, playback_rate_); |
| |
| if (!Limited() && !Paused() && start_time_) |
| current_time_pending_ = true; |
| |
| double stored_current_time = CurrentTimeInternal(); |
| if ((playback_rate_ < 0 && playback_rate >= 0) || |
| (playback_rate_ > 0 && playback_rate <= 0)) |
| finished_ = false; |
| |
| playback_rate_ = playback_rate; |
| start_time_ = base::nullopt; |
| if (!IsNull(stored_current_time)) |
| SetCurrentTimeInternal(stored_current_time, kTimingUpdateOnDemand); |
| } |
| |
| void Animation::ClearOutdated() { |
| if (!outdated_) |
| return; |
| outdated_ = false; |
| if (timeline_) |
| timeline_->ClearOutdatedAnimation(this); |
| } |
| |
| void Animation::SetOutdated() { |
| if (outdated_) |
| return; |
| outdated_ = true; |
| if (timeline_) |
| timeline_->SetOutdatedAnimation(this); |
| } |
| |
| void Animation::ForceServiceOnNextFrame() { |
| timeline_->Wake(); |
| } |
| |
| CompositorAnimations::FailureReasons |
| Animation::CheckCanStartAnimationOnCompositor( |
| const PaintArtifactCompositor* paint_artifact_compositor) const { |
| CompositorAnimations::FailureReasons reasons = |
| CheckCanStartAnimationOnCompositorInternal(); |
| if (content_ && content_->IsKeyframeEffect()) { |
| reasons |= ToKeyframeEffect(content_.Get()) |
| ->CheckCanStartAnimationOnCompositor( |
| paint_artifact_compositor, playback_rate_); |
| } |
| return reasons; |
| } |
| |
| CompositorAnimations::FailureReasons |
| Animation::CheckCanStartAnimationOnCompositorInternal() const { |
| CompositorAnimations::FailureReasons reasons = |
| CompositorAnimations::kNoFailure; |
| |
| if (is_composited_animation_disabled_for_testing_) |
| reasons |= CompositorAnimations::kAcceleratedAnimationsDisabled; |
| |
| if (EffectSuppressed()) |
| reasons |= CompositorAnimations::kEffectSuppressedByDevtools; |
| |
| // An Animation with zero playback rate will produce no visual output, so |
| // there is no reason to composite it. |
| if (playback_rate_ == 0) |
| reasons |= CompositorAnimations::kInvalidAnimationOrEffect; |
| |
| // An infinite duration animation with a negative playback rate is essentially |
| // a static value, so there is no reason to composite it. |
| if (std::isinf(EffectEnd()) && playback_rate_ < 0) |
| reasons |= CompositorAnimations::kInvalidAnimationOrEffect; |
| |
| // An Animation without a timeline effectively isn't playing, so there is no |
| // reason to composite it. Additionally, mutating the timeline playback rate |
| // is a debug feature available via devtools; we don't support this on the |
| // compositor currently and there is no reason to do so. |
| if (!timeline_ || timeline_->PlaybackRate() != 1) |
| reasons |= CompositorAnimations::kInvalidAnimationOrEffect; |
| |
| // An Animation without an effect cannot produce a visual, so there is no |
| // reason to composite it. |
| if (!content_ || !content_->IsKeyframeEffect()) |
| reasons |= CompositorAnimations::kInvalidAnimationOrEffect; |
| |
| // An Animation that is not playing will not produce a visual, so there is no |
| // reason to composite it. |
| if (!Playing()) |
| reasons |= CompositorAnimations::kInvalidAnimationOrEffect; |
| |
| return reasons; |
| } |
| |
| void Animation::StartAnimationOnCompositor( |
| const PaintArtifactCompositor* paint_artifact_compositor) { |
| DCHECK_EQ(CheckCanStartAnimationOnCompositor(paint_artifact_compositor), |
| CompositorAnimations::kNoFailure); |
| |
| bool reversed = playback_rate_ < 0; |
| |
| base::Optional<double> start_time = base::nullopt; |
| double time_offset = 0; |
| if (start_time_) { |
| start_time = TimelineInternal()->ZeroTime().since_origin().InSecondsF() + |
| start_time_.value(); |
| if (reversed) |
| start_time = start_time.value() - (EffectEnd() / fabs(playback_rate_)); |
| } else { |
| time_offset = |
| reversed ? EffectEnd() - CurrentTimeInternal() : CurrentTimeInternal(); |
| time_offset = time_offset / fabs(playback_rate_); |
| } |
| |
| DCHECK(!start_time || !IsNull(start_time.value())); |
| DCHECK_NE(compositor_group_, 0); |
| DCHECK(ToKeyframeEffect(content_.Get())); |
| ToKeyframeEffect(content_.Get()) |
| ->StartAnimationOnCompositor(compositor_group_, start_time, time_offset, |
| playback_rate_); |
| } |
| |
| void Animation::SetCompositorPending(bool effect_changed) { |
| // FIXME: KeyframeEffect could notify this directly? |
| if (!HasActiveAnimationsOnCompositor()) { |
| DestroyCompositorAnimation(); |
| compositor_state_.reset(); |
| } |
| if (effect_changed && compositor_state_) { |
| compositor_state_->effect_changed = true; |
| } |
| if (compositor_pending_ || is_paused_for_testing_) { |
| return; |
| } |
| // In general, we need to update the compositor-side if anything has changed |
| // on the blink version of the animation. There is also an edge case; if |
| // neither the compositor nor blink side have a start time we still have to |
| // sync them. This can happen if the blink side animation was started, the |
| // compositor side hadn't started on its side yet, and then the blink side |
| // start time was cleared (e.g. by setting current time). |
| if (!compositor_state_ || compositor_state_->effect_changed || |
| compositor_state_->playback_rate != playback_rate_ || |
| compositor_state_->start_time != start_time_ || |
| !compositor_state_->start_time || !start_time_) { |
| compositor_pending_ = true; |
| TimelineInternal()->GetDocument()->GetPendingAnimations().Add(this); |
| } |
| } |
| |
| void Animation::CancelAnimationOnCompositor() { |
| if (HasActiveAnimationsOnCompositor()) { |
| ToKeyframeEffect(content_.Get()) |
| ->CancelAnimationOnCompositor(GetCompositorAnimation()); |
| } |
| |
| DestroyCompositorAnimation(); |
| } |
| |
| void Animation::RestartAnimationOnCompositor() { |
| if (!HasActiveAnimationsOnCompositor()) |
| return; |
| if (ToKeyframeEffect(content_.Get()) |
| ->CancelAnimationOnCompositor(GetCompositorAnimation())) |
| SetCompositorPending(true); |
| } |
| |
| void Animation::CancelIncompatibleAnimationsOnCompositor() { |
| if (content_ && content_->IsKeyframeEffect()) |
| ToKeyframeEffect(content_.Get()) |
| ->CancelIncompatibleAnimationsOnCompositor(); |
| } |
| |
| bool Animation::HasActiveAnimationsOnCompositor() { |
| if (!content_ || !content_->IsKeyframeEffect()) |
| return false; |
| |
| return ToKeyframeEffect(content_.Get())->HasActiveAnimationsOnCompositor(); |
| } |
| |
| bool Animation::Update(TimingUpdateReason reason) { |
| if (!timeline_) |
| return false; |
| |
| if (reason == kTimingUpdateForAnimationFrame) { |
| // Pending tasks are committed when the animation is 'ready'. This can be |
| // at the time of promise resolution or after a frame tick. Whereas the |
| // spec calls for creating scheduled tasks to commit pending changes, in the |
| // Blink implementation, this is an acknowledgment that the changes have |
| // taken affect. |
| // TODO(crbug.com/960944): Consider restructuring implementation to more |
| // closely align with the recommended algorithm in the spec. |
| ResetPendingTasks(); |
| } |
| |
| PlayStateUpdateScope update_scope(*this, reason, kDoNotSetCompositorPending); |
| |
| ClearOutdated(); |
| bool idle = PlayStateInternal() == kIdle; |
| |
| if (content_) { |
| double inherited_time = idle || IsNull(timeline_->CurrentTimeInternal()) |
| ? NullValue() |
| : CurrentTimeInternal(); |
| |
| // Special case for end-exclusivity when playing backwards. |
| if (inherited_time == 0 && playback_rate_ < 0) |
| inherited_time = -1; |
| content_->UpdateInheritedTime(inherited_time, reason); |
| |
| // After updating the animation time if the animation is no longer current |
| // blink will no longer composite the element (see |
| // CompositingReasonFinder::RequiresCompositingFor*Animation). We cancel any |
| // running compositor animation so that we don't try to animate the |
| // non-existent element on the compositor. |
| if (!content_->IsCurrent()) |
| CancelAnimationOnCompositor(); |
| } |
| |
| if ((idle || Limited()) && !finished_) { |
| if (reason == kTimingUpdateForAnimationFrame && (idle || start_time_)) { |
| if (idle) { |
| const AtomicString& event_type = event_type_names::kCancel; |
| if (GetExecutionContext() && HasEventListeners(event_type)) { |
| double event_current_time = NullValue(); |
| pending_cancelled_event_ = |
| MakeGarbageCollected<AnimationPlaybackEvent>( |
| event_type, event_current_time, |
| TimelineInternal()->currentTime()); |
| pending_cancelled_event_->SetTarget(this); |
| pending_cancelled_event_->SetCurrentTarget(this); |
| timeline_->GetDocument()->EnqueueAnimationFrameEvent( |
| pending_cancelled_event_); |
| } |
| } else { |
| QueueFinishedEvent(); |
| } |
| finished_ = true; |
| } |
| } |
| DCHECK(!outdated_); |
| return !finished_ || std::isfinite(TimeToEffectChange()); |
| } |
| |
| void Animation::QueueFinishedEvent() { |
| const AtomicString& event_type = event_type_names::kFinish; |
| if (GetExecutionContext() && HasEventListeners(event_type)) { |
| double event_current_time = CurrentTimeInternal() * 1000; |
| pending_finished_event_ = MakeGarbageCollected<AnimationPlaybackEvent>( |
| event_type, event_current_time, TimelineInternal()->currentTime()); |
| pending_finished_event_->SetTarget(this); |
| pending_finished_event_->SetCurrentTarget(this); |
| timeline_->GetDocument()->EnqueueAnimationFrameEvent( |
| pending_finished_event_); |
| } |
| } |
| |
| void Animation::UpdateIfNecessary() { |
| if (Outdated()) |
| Update(kTimingUpdateOnDemand); |
| DCHECK(!Outdated()); |
| } |
| |
| void Animation::EffectInvalidated() { |
| SetOutdated(); |
| // FIXME: Needs to consider groups when added. |
| SetCompositorPending(true); |
| } |
| |
| bool Animation::IsEventDispatchAllowed() const { |
| return Paused() || start_time_; |
| } |
| |
| double Animation::TimeToEffectChange() { |
| DCHECK(!outdated_); |
| if (!start_time_ || hold_time_) |
| return std::numeric_limits<double>::infinity(); |
| |
| if (!content_) |
| return -CurrentTimeInternal() / playback_rate_; |
| double result = playback_rate_ > 0 |
| ? content_->TimeToForwardsEffectChange() / playback_rate_ |
| : content_->TimeToReverseEffectChange() / -playback_rate_; |
| |
| return !HasActiveAnimationsOnCompositor() && |
| content_->GetPhase() == AnimationEffect::kPhaseActive |
| ? 0 |
| : result; |
| } |
| |
| void Animation::cancel() { |
| PlayStateUpdateScope update_scope(*this, kTimingUpdateOnDemand); |
| |
| if (PlayStateInternal() == kIdle) |
| return; |
| |
| hold_time_ = base::nullopt; |
| paused_ = false; |
| internal_play_state_ = kIdle; |
| start_time_ = base::nullopt; |
| current_time_pending_ = false; |
| ResetPendingTasks(); |
| ForceServiceOnNextFrame(); |
| } |
| |
| void Animation::BeginUpdatingState() { |
| // Nested calls are not allowed! |
| DCHECK(!state_is_being_updated_); |
| state_is_being_updated_ = true; |
| } |
| |
| void Animation::EndUpdatingState() { |
| DCHECK(state_is_being_updated_); |
| state_is_being_updated_ = false; |
| } |
| |
| void Animation::CreateCompositorAnimation() { |
| if (Platform::Current()->IsThreadedAnimationEnabled() && |
| !compositor_animation_) { |
| compositor_animation_ = CompositorAnimationHolder::Create(this); |
| DCHECK(compositor_animation_); |
| AttachCompositorTimeline(); |
| } |
| |
| AttachCompositedLayers(); |
| } |
| |
| void Animation::DestroyCompositorAnimation() { |
| DetachCompositedLayers(); |
| |
| if (compositor_animation_) { |
| DetachCompositorTimeline(); |
| compositor_animation_->Detach(); |
| compositor_animation_ = nullptr; |
| } |
| } |
| |
| void Animation::AttachCompositorTimeline() { |
| if (compositor_animation_) { |
| CompositorAnimationTimeline* timeline = |
| timeline_ ? timeline_->CompositorTimeline() : nullptr; |
| if (timeline) |
| timeline->AnimationAttached(*this); |
| } |
| } |
| |
| void Animation::DetachCompositorTimeline() { |
| if (compositor_animation_) { |
| CompositorAnimationTimeline* timeline = |
| timeline_ ? timeline_->CompositorTimeline() : nullptr; |
| if (timeline) |
| timeline->AnimationDestroyed(*this); |
| } |
| } |
| |
| void Animation::AttachCompositedLayers() { |
| if (!compositor_animation_) |
| return; |
| |
| DCHECK(content_); |
| DCHECK(content_->IsKeyframeEffect()); |
| |
| ToKeyframeEffect(content_.Get())->AttachCompositedLayers(); |
| } |
| |
| void Animation::DetachCompositedLayers() { |
| if (compositor_animation_ && |
| compositor_animation_->GetAnimation()->IsElementAttached()) |
| compositor_animation_->GetAnimation()->DetachElement(); |
| } |
| |
| void Animation::NotifyAnimationStarted(double monotonic_time, int group) { |
| TimelineInternal() |
| ->GetDocument() |
| ->GetPendingAnimations() |
| .NotifyCompositorAnimationStarted(monotonic_time, group); |
| } |
| |
| Animation::PlayStateUpdateScope::PlayStateUpdateScope( |
| Animation& animation, |
| TimingUpdateReason reason, |
| CompositorPendingChange compositor_pending_change) |
| : animation_(animation), |
| initial_play_state_(animation_->PlayStateInternal()), |
| compositor_pending_change_(compositor_pending_change) { |
| DCHECK_NE(initial_play_state_, kUnset); |
| animation_->BeginUpdatingState(); |
| animation_->UpdateCurrentTimingState(reason); |
| } |
| |
| Animation::PlayStateUpdateScope::~PlayStateUpdateScope() { |
| AnimationPlayState old_play_state = initial_play_state_; |
| AnimationPlayState new_play_state = animation_->CalculatePlayState(); |
| |
| // TODO(crbug.com/958433): Phase out internal_play_state_ in favor of the spec |
| // compliant version. At present, both are needed as the web exposed play |
| // state cannot simply be inferred from the internal play state. |
| animation_->internal_play_state_ = new_play_state; |
| animation_->animation_play_state_ = animation_->CalculateAnimationPlayState(); |
| if (old_play_state != new_play_state) { |
| bool was_active = old_play_state == kPending || old_play_state == kRunning; |
| bool is_active = new_play_state == kPending || new_play_state == kRunning; |
| if (!was_active && is_active) { |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN1( |
| "blink.animations,devtools.timeline,benchmark,rail", "Animation", |
| animation_, "data", inspector_animation_event::Data(*animation_)); |
| } else if (was_active && !is_active) { |
| TRACE_EVENT_NESTABLE_ASYNC_END1( |
| "blink.animations,devtools.timeline,benchmark,rail", "Animation", |
| animation_, "endData", |
| inspector_animation_state_event::Data(*animation_)); |
| } else { |
| TRACE_EVENT_NESTABLE_ASYNC_INSTANT1( |
| "blink.animations,devtools.timeline,benchmark,rail", "Animation", |
| animation_, "data", |
| inspector_animation_state_event::Data(*animation_)); |
| } |
| } |
| |
| // Ordering is important, the ready promise should resolve/reject before |
| // the finished promise. |
| if (animation_->ready_promise_ && new_play_state != old_play_state) { |
| if (new_play_state == kIdle) { |
| if (animation_->ready_promise_->GetState() == |
| AnimationPromise::kPending) { |
| animation_->RejectAndResetPromiseMaybeAsync( |
| animation_->ready_promise_.Get()); |
| } else { |
| animation_->ready_promise_->Reset(); |
| } |
| animation_->ResetPendingTasks(); |
| animation_->ResolvePromiseMaybeAsync(animation_->ready_promise_.Get()); |
| } else if (old_play_state == kPending) { |
| animation_->ResetPendingTasks(); |
| animation_->ResolvePromiseMaybeAsync(animation_->ready_promise_.Get()); |
| } else if (new_play_state == kPending) { |
| DCHECK_NE(animation_->ready_promise_->GetState(), |
| AnimationPromise::kPending); |
| animation_->ready_promise_->Reset(); |
| } |
| } |
| |
| if (animation_->finished_promise_ && new_play_state != old_play_state) { |
| if (new_play_state == kIdle) { |
| if (animation_->finished_promise_->GetState() == |
| AnimationPromise::kPending) { |
| animation_->RejectAndResetPromiseMaybeAsync( |
| animation_->finished_promise_.Get()); |
| } else { |
| animation_->finished_promise_->Reset(); |
| } |
| } else if (new_play_state == kFinished) { |
| animation_->ResetPendingTasks(); |
| animation_->ResolvePromiseMaybeAsync(animation_->finished_promise_.Get()); |
| } else if (old_play_state == kFinished) { |
| animation_->finished_promise_->Reset(); |
| } |
| } |
| |
| if (old_play_state != new_play_state && |
| (old_play_state == kIdle || new_play_state == kIdle)) { |
| animation_->SetOutdated(); |
| } |
| |
| #if DCHECK_IS_ON() |
| // Verify that current time is up to date. |
| animation_->CurrentTimeInternal(); |
| #endif |
| |
| switch (compositor_pending_change_) { |
| case kSetCompositorPending: |
| animation_->SetCompositorPending(); |
| break; |
| case kSetCompositorPendingWithEffectChanged: |
| animation_->SetCompositorPending(true); |
| break; |
| case kDoNotSetCompositorPending: |
| break; |
| default: |
| NOTREACHED(); |
| break; |
| } |
| animation_->EndUpdatingState(); |
| |
| if (old_play_state != new_play_state) { |
| probe::AnimationPlayStateChanged( |
| animation_->TimelineInternal()->GetDocument(), animation_, |
| old_play_state, new_play_state); |
| } |
| } |
| |
| void Animation::AddedEventListener( |
| const AtomicString& event_type, |
| RegisteredEventListener& registered_listener) { |
| EventTargetWithInlineData::AddedEventListener(event_type, |
| registered_listener); |
| if (event_type == event_type_names::kFinish) |
| UseCounter::Count(GetExecutionContext(), WebFeature::kAnimationFinishEvent); |
| } |
| |
| void Animation::PauseForTesting(double pause_time) { |
| SetCurrentTimeInternal(pause_time, kTimingUpdateOnDemand); |
| if (HasActiveAnimationsOnCompositor()) |
| ToKeyframeEffect(content_.Get()) |
| ->PauseAnimationForTestingOnCompositor(CurrentTimeInternal()); |
| is_paused_for_testing_ = true; |
| pause(); |
| } |
| |
| void Animation::SetEffectSuppressed(bool suppressed) { |
| effect_suppressed_ = suppressed; |
| if (suppressed) |
| CancelAnimationOnCompositor(); |
| } |
| |
| void Animation::DisableCompositedAnimationForTesting() { |
| is_composited_animation_disabled_for_testing_ = true; |
| CancelAnimationOnCompositor(); |
| } |
| |
| void Animation::InvalidateKeyframeEffect(const TreeScope& tree_scope) { |
| if (!content_ || !content_->IsKeyframeEffect()) |
| return; |
| |
| Element* target = ToKeyframeEffect(content_.Get())->target(); |
| |
| // TODO(alancutter): Remove dependency of this function on CSSAnimations. |
| // This function makes the incorrect assumption that the animation uses |
| // @keyframes for its effect model when it may instead be using JS provided |
| // keyframes. |
| if (target && |
| CSSAnimations::IsAffectedByKeyframesFromScope(*target, tree_scope)) { |
| target->SetNeedsStyleRecalc(kLocalStyleChange, |
| StyleChangeReasonForTracing::Create( |
| style_change_reason::kStyleSheetChange)); |
| } |
| } |
| |
| void Animation::ResolvePromiseMaybeAsync(AnimationPromise* promise) { |
| if (ScriptForbiddenScope::IsScriptForbidden()) { |
| GetExecutionContext() |
| ->GetTaskRunner(TaskType::kDOMManipulation) |
| ->PostTask(FROM_HERE, |
| WTF::Bind(&AnimationPromise::Resolve<Animation*>, |
| WrapPersistent(promise), WrapPersistent(this))); |
| } else { |
| promise->Resolve(this); |
| } |
| } |
| |
| void Animation::RejectAndResetPromise(AnimationPromise* promise) { |
| promise->Reject( |
| MakeGarbageCollected<DOMException>(DOMExceptionCode::kAbortError)); |
| promise->Reset(); |
| } |
| |
| void Animation::RejectAndResetPromiseMaybeAsync(AnimationPromise* promise) { |
| if (ScriptForbiddenScope::IsScriptForbidden()) { |
| GetExecutionContext() |
| ->GetTaskRunner(TaskType::kDOMManipulation) |
| ->PostTask(FROM_HERE, |
| WTF::Bind(&Animation::RejectAndResetPromise, |
| WrapPersistent(this), WrapPersistent(promise))); |
| } else { |
| RejectAndResetPromise(promise); |
| } |
| } |
| |
| void Animation::Trace(blink::Visitor* visitor) { |
| visitor->Trace(content_); |
| visitor->Trace(timeline_); |
| visitor->Trace(pending_finished_event_); |
| visitor->Trace(pending_cancelled_event_); |
| visitor->Trace(finished_promise_); |
| visitor->Trace(ready_promise_); |
| visitor->Trace(compositor_animation_); |
| EventTargetWithInlineData::Trace(visitor); |
| ContextLifecycleObserver::Trace(visitor); |
| } |
| |
| Animation::CompositorAnimationHolder* |
| Animation::CompositorAnimationHolder::Create(Animation* animation) { |
| return MakeGarbageCollected<CompositorAnimationHolder>(animation); |
| } |
| |
| Animation::CompositorAnimationHolder::CompositorAnimationHolder( |
| Animation* animation) |
| : animation_(animation) { |
| compositor_animation_ = CompositorAnimation::Create(); |
| compositor_animation_->SetAnimationDelegate(animation_); |
| } |
| |
| void Animation::CompositorAnimationHolder::Dispose() { |
| if (!animation_) |
| return; |
| animation_->Dispose(); |
| DCHECK(!animation_); |
| DCHECK(!compositor_animation_); |
| } |
| |
| void Animation::CompositorAnimationHolder::Detach() { |
| DCHECK(compositor_animation_); |
| compositor_animation_->SetAnimationDelegate(nullptr); |
| animation_ = nullptr; |
| compositor_animation_.reset(); |
| } |
| } // namespace blink |