blob: 3a4339b10244866c3f008c41aee1c5d788edd232 [file] [log] [blame]
/*
* 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/keyframe_effect.h"
#include "third_party/blink/renderer/bindings/core/v8/unrestricted_double_or_keyframe_effect_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_object_builder.h"
#include "third_party/blink/renderer/core/animation/effect_input.h"
#include "third_party/blink/renderer/core/animation/element_animations.h"
#include "third_party/blink/renderer/core/animation/keyframe_effect_options.h"
#include "third_party/blink/renderer/core/animation/sampled_effect.h"
#include "third_party/blink/renderer/core/animation/timing_input.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/frame/use_counter.h"
#include "third_party/blink/renderer/core/paint/paint_layer.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
#include "third_party/blink/renderer/core/svg/svg_element.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
namespace blink {
KeyframeEffect* KeyframeEffect::Create(Element* target,
KeyframeEffectModelBase* model,
const Timing& timing,
Priority priority,
EventDelegate* event_delegate) {
return MakeGarbageCollected<KeyframeEffect>(target, model, timing, priority,
event_delegate);
}
KeyframeEffect* KeyframeEffect::Create(
ScriptState* script_state,
Element* element,
const ScriptValue& keyframes,
const UnrestrictedDoubleOrKeyframeEffectOptions& options,
ExceptionState& exception_state) {
DCHECK(RuntimeEnabledFeatures::WebAnimationsAPIEnabled());
if (element) {
UseCounter::Count(
element->GetDocument(),
WebFeature::kAnimationConstructorKeyframeListEffectObjectTiming);
}
Document* document = element ? &element->GetDocument() : nullptr;
Timing timing = TimingInput::Convert(options, document, exception_state);
if (exception_state.HadException())
return nullptr;
EffectModel::CompositeOperation composite = EffectModel::kCompositeReplace;
if (options.IsKeyframeEffectOptions()) {
composite = EffectModel::StringToCompositeOperation(
options.GetAsKeyframeEffectOptions()->composite())
.value();
}
KeyframeEffectModelBase* model = EffectInput::Convert(
element, keyframes, composite, script_state, exception_state);
if (exception_state.HadException())
return nullptr;
return Create(element, model, timing);
}
KeyframeEffect* KeyframeEffect::Create(ScriptState* script_state,
Element* element,
const ScriptValue& keyframes,
ExceptionState& exception_state) {
DCHECK(RuntimeEnabledFeatures::WebAnimationsAPIEnabled());
if (element) {
UseCounter::Count(
element->GetDocument(),
WebFeature::kAnimationConstructorKeyframeListEffectNoTiming);
}
KeyframeEffectModelBase* model =
EffectInput::Convert(element, keyframes, EffectModel::kCompositeReplace,
script_state, exception_state);
if (exception_state.HadException())
return nullptr;
return Create(element, model, Timing());
}
KeyframeEffect* KeyframeEffect::Create(ScriptState* script_state,
KeyframeEffect* source,
ExceptionState& exception_state) {
Timing new_timing = source->SpecifiedTiming();
KeyframeEffectModelBase* model = source->Model()->Clone();
return MakeGarbageCollected<KeyframeEffect>(source->target(), model,
new_timing, source->GetPriority(),
source->GetEventDelegate());
}
KeyframeEffect::KeyframeEffect(Element* target,
KeyframeEffectModelBase* model,
const Timing& timing,
Priority priority,
EventDelegate* event_delegate)
: AnimationEffect(timing, event_delegate),
target_(target),
model_(model),
sampled_effect_(nullptr),
priority_(priority) {
DCHECK(model_);
}
KeyframeEffect::~KeyframeEffect() = default;
void KeyframeEffect::setTarget(Element* target) {
if (target_ == target)
return;
DetachTarget(GetAnimation());
target_ = target;
AttachTarget(GetAnimation());
InvalidateAndNotifyOwner();
}
String KeyframeEffect::composite() const {
return EffectModel::CompositeOperationToString(CompositeInternal());
}
void KeyframeEffect::setComposite(String composite_string) {
Model()->SetComposite(
EffectModel::StringToCompositeOperation(composite_string).value());
}
Vector<ScriptValue> KeyframeEffect::getKeyframes(ScriptState* script_state) {
Vector<ScriptValue> computed_keyframes;
if (!model_->HasFrames())
return computed_keyframes;
// getKeyframes() returns a list of 'ComputedKeyframes'. A ComputedKeyframe
// consists of the normal keyframe data combined with the computed offset for
// the given keyframe.
//
// https://w3c.github.io/web-animations/#dom-keyframeeffectreadonly-getkeyframes
const KeyframeVector& keyframes = model_->GetFrames();
Vector<double> computed_offsets =
KeyframeEffectModelBase::GetComputedOffsets(keyframes);
computed_keyframes.ReserveInitialCapacity(keyframes.size());
ScriptState::Scope scope(script_state);
for (wtf_size_t i = 0; i < keyframes.size(); i++) {
V8ObjectBuilder object_builder(script_state);
keyframes[i]->AddKeyframePropertiesToV8Object(object_builder);
object_builder.Add("computedOffset", computed_offsets[i]);
computed_keyframes.push_back(object_builder.GetScriptValue());
}
return computed_keyframes;
}
void KeyframeEffect::setKeyframes(ScriptState* script_state,
const ScriptValue& keyframes,
ExceptionState& exception_state) {
// TODO(crbug.com/799061): Support TransitionKeyframeEffectModel. This will
// require a lot of work as the setKeyframes API can mutate a transition
// Animation into a 'normal' one with multiple properties.
if (!Model()->IsStringKeyframeEffectModel()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
"Calling setKeyframes on CSS Transitions is not yet supported");
return;
}
StringKeyframeVector new_keyframes = EffectInput::ParseKeyframesArgument(
target(), keyframes, script_state, exception_state);
if (exception_state.HadException())
return;
SetKeyframes(new_keyframes);
}
void KeyframeEffect::SetKeyframes(StringKeyframeVector keyframes) {
Model()->SetComposite(
EffectInput::ResolveCompositeOperation(Model()->Composite(), keyframes));
ToStringKeyframeEffectModel(Model())->SetFrames(keyframes);
// Changing the keyframes will invalidate any sampled effect, as well as
// potentially affect the effect owner.
ClearEffects();
InvalidateAndNotifyOwner();
}
bool KeyframeEffect::Affects(const PropertyHandle& property) const {
return model_->Affects(property);
}
void KeyframeEffect::NotifySampledEffectRemovedFromEffectStack() {
sampled_effect_ = nullptr;
}
CompositorAnimations::FailureCode
KeyframeEffect::CheckCanStartAnimationOnCompositor(
const base::Optional<CompositorElementIdSet>& composited_element_ids,
double animation_playback_rate) const {
if (!model_->HasFrames()) {
return CompositorAnimations::FailureCode::Actionable(
"Animation effect has no keyframes");
}
if (!target_) {
return CompositorAnimations::FailureCode::Actionable(
"Animation effect has no target element");
}
if (target_->GetComputedStyle() && target_->GetComputedStyle()->HasOffset()) {
return CompositorAnimations::FailureCode::Actionable(
"Accelerated animations do not support elements with offset-position "
"or offset-path CSS properties");
}
// Do not put transforms on compositor if more than one of them are defined
// in computed style because they need to be explicitly ordered
if (HasMultipleTransformProperties()) {
return CompositorAnimations::FailureCode::Actionable(
"Animation effect applies to multiple transform-related properties");
}
return CompositorAnimations::CheckCanStartAnimationOnCompositor(
SpecifiedTiming(), *target_, GetAnimation(), *Model(),
composited_element_ids, animation_playback_rate);
}
void KeyframeEffect::StartAnimationOnCompositor(
int group,
base::Optional<double> start_time,
double current_time,
double animation_playback_rate,
CompositorAnimation* compositor_animation) {
DCHECK(!HasActiveAnimationsOnCompositor());
// TODO(petermayo): Maybe we should recheck that we can start on the
// compositor if we have the compositable IDs somewhere.
if (!compositor_animation)
compositor_animation = GetAnimation()->GetCompositorAnimation();
DCHECK(compositor_animation);
DCHECK(target_);
DCHECK(Model());
CompositorAnimations::StartAnimationOnCompositor(
*target_, group, start_time, current_time, SpecifiedTiming(),
GetAnimation(), *compositor_animation, *Model(),
compositor_keyframe_model_ids_, animation_playback_rate);
DCHECK(!compositor_keyframe_model_ids_.IsEmpty());
}
bool KeyframeEffect::HasActiveAnimationsOnCompositor() const {
return !compositor_keyframe_model_ids_.IsEmpty();
}
bool KeyframeEffect::HasActiveAnimationsOnCompositor(
const PropertyHandle& property) const {
return HasActiveAnimationsOnCompositor() && Affects(property);
}
bool KeyframeEffect::CancelAnimationOnCompositor(
CompositorAnimation* compositor_animation) {
// FIXME: cancelAnimationOnCompositor is called from withins style recalc.
// This queries compositingState, which is not necessarily up to date.
// https://code.google.com/p/chromium/issues/detail?id=339847
DisableCompositingQueryAsserts disabler;
if (!HasActiveAnimationsOnCompositor())
return false;
if (!target_ || !target_->GetLayoutObject())
return false;
for (const auto& compositor_keyframe_model_id :
compositor_keyframe_model_ids_) {
CompositorAnimations::CancelAnimationOnCompositor(
*target_, compositor_animation, compositor_keyframe_model_id);
}
compositor_keyframe_model_ids_.clear();
return true;
}
void KeyframeEffect::CancelIncompatibleAnimationsOnCompositor() {
if (target_ && GetAnimation() && model_->HasFrames()) {
CompositorAnimations::CancelIncompatibleAnimationsOnCompositor(
*target_, *GetAnimation(), *Model());
}
}
void KeyframeEffect::PauseAnimationForTestingOnCompositor(double pause_time) {
DCHECK(HasActiveAnimationsOnCompositor());
if (!target_ || !target_->GetLayoutObject())
return;
DCHECK(GetAnimation());
for (const auto& compositor_keyframe_model_id :
compositor_keyframe_model_ids_) {
CompositorAnimations::PauseAnimationForTestingOnCompositor(
*target_, *GetAnimation(), compositor_keyframe_model_id, pause_time);
}
}
void KeyframeEffect::AttachCompositedLayers() {
DCHECK(target_);
DCHECK(GetAnimation());
CompositorAnimations::AttachCompositedLayers(
*target_, GetAnimation()->GetCompositorAnimation());
}
bool KeyframeEffect::HasAnimation() const {
return !!owner_;
}
bool KeyframeEffect::HasPlayingAnimation() const {
return owner_ && owner_->Playing();
}
void KeyframeEffect::Trace(blink::Visitor* visitor) {
visitor->Trace(target_);
visitor->Trace(model_);
visitor->Trace(sampled_effect_);
AnimationEffect::Trace(visitor);
}
EffectModel::CompositeOperation KeyframeEffect::CompositeInternal() const {
return model_->Composite();
}
void KeyframeEffect::ApplyEffects() {
DCHECK(IsInEffect());
if (!target_ || !model_->HasFrames())
return;
if (GetAnimation() && HasIncompatibleStyle()) {
GetAnimation()->CancelAnimationOnCompositor();
}
double iteration = CurrentIteration();
DCHECK_GE(iteration, 0);
bool changed = false;
if (sampled_effect_) {
changed = model_->Sample(clampTo<int>(iteration, 0), Progress().value(),
IterationDuration(),
sampled_effect_->MutableInterpolations());
} else {
HeapVector<Member<Interpolation>> interpolations;
model_->Sample(clampTo<int>(iteration, 0), Progress().value(),
IterationDuration(), interpolations);
if (!interpolations.IsEmpty()) {
SampledEffect* sampled_effect =
SampledEffect::Create(this, owner_->SequenceNumber());
sampled_effect->MutableInterpolations().swap(interpolations);
sampled_effect_ = sampled_effect;
target_->EnsureElementAnimations().GetEffectStack().Add(sampled_effect);
changed = true;
} else {
return;
}
}
if (changed) {
target_->SetNeedsAnimationStyleRecalc();
if (RuntimeEnabledFeatures::WebAnimationsSVGEnabled() &&
target_->IsSVGElement())
ToSVGElement(*target_).SetWebAnimationsPending();
}
}
void KeyframeEffect::ClearEffects() {
if (!sampled_effect_)
return;
sampled_effect_->Clear();
sampled_effect_ = nullptr;
if (GetAnimation())
GetAnimation()->RestartAnimationOnCompositor();
target_->SetNeedsAnimationStyleRecalc();
if (RuntimeEnabledFeatures::WebAnimationsSVGEnabled() &&
target_->IsSVGElement())
ToSVGElement(*target_).ClearWebAnimatedAttributes();
Invalidate();
}
void KeyframeEffect::UpdateChildrenAndEffects() const {
if (!model_->HasFrames())
return;
DCHECK(owner_);
if (IsInEffect() && !owner_->EffectSuppressed())
const_cast<KeyframeEffect*>(this)->ApplyEffects();
else
const_cast<KeyframeEffect*>(this)->ClearEffects();
}
void KeyframeEffect::Attach(AnimationEffectOwner* owner) {
AttachTarget(owner->GetAnimation());
AnimationEffect::Attach(owner);
}
void KeyframeEffect::Detach() {
DetachTarget(GetAnimation());
AnimationEffect::Detach();
}
void KeyframeEffect::AttachTarget(Animation* animation) {
if (!target_ || !animation)
return;
target_->EnsureElementAnimations().Animations().insert(animation);
target_->SetNeedsAnimationStyleRecalc();
if (RuntimeEnabledFeatures::WebAnimationsSVGEnabled() &&
target_->IsSVGElement())
ToSVGElement(target_)->SetWebAnimationsPending();
}
void KeyframeEffect::DetachTarget(Animation* animation) {
if (target_ && animation)
target_->GetElementAnimations()->Animations().erase(animation);
// If we have sampled this effect previously, we need to purge that state.
// ClearEffects takes care of clearing the cached sampled effect, informing
// the target that it needs to refresh its style, and doing any necessary
// update on the compositor.
ClearEffects();
}
double KeyframeEffect::CalculateTimeToEffectChange(
bool forwards,
double local_time,
double time_to_next_iteration) const {
const double start_time = SpecifiedTiming().start_delay;
const double end_time_minus_end_delay = start_time + RepeatedDuration();
const double end_time =
end_time_minus_end_delay + SpecifiedTiming().end_delay;
const double after_time = std::min(end_time_minus_end_delay, end_time);
switch (GetPhase()) {
case kPhaseNone:
return std::numeric_limits<double>::infinity();
case kPhaseBefore:
DCHECK_GE(start_time, local_time);
return forwards ? start_time - local_time
: std::numeric_limits<double>::infinity();
case kPhaseActive:
if (forwards) {
// Need service to apply fill / fire events.
const double time_to_end = after_time - local_time;
if (RequiresIterationEvents()) {
return std::min(time_to_end, time_to_next_iteration);
}
return time_to_end;
}
return 0;
case kPhaseAfter:
DCHECK_GE(local_time, after_time);
// If this KeyframeEffect is still in effect then it will need to update
// when its parent goes out of effect. We have no way of knowing when
// that will be, however, so the parent will need to supply it.
return forwards ? std::numeric_limits<double>::infinity()
: local_time - after_time;
default:
NOTREACHED();
return std::numeric_limits<double>::infinity();
}
}
// Returns true if transform, translate, rotate or scale is composited
// and a motion path or other transform properties
// has been introduced on the element
bool KeyframeEffect::HasIncompatibleStyle() const {
if (!target_->GetComputedStyle())
return false;
bool affects_transform = Affects(PropertyHandle(GetCSSPropertyTransform())) ||
Affects(PropertyHandle(GetCSSPropertyScale())) ||
Affects(PropertyHandle(GetCSSPropertyRotate())) ||
Affects(PropertyHandle(GetCSSPropertyTranslate()));
if (HasActiveAnimationsOnCompositor()) {
if (target_->GetComputedStyle()->HasOffset() && affects_transform)
return true;
return HasMultipleTransformProperties();
}
return false;
}
bool KeyframeEffect::HasMultipleTransformProperties() const {
if (!target_->GetComputedStyle())
return false;
unsigned transform_property_count = 0;
if (target_->GetComputedStyle()->HasTransformOperations())
transform_property_count++;
if (target_->GetComputedStyle()->Rotate())
transform_property_count++;
if (target_->GetComputedStyle()->Scale())
transform_property_count++;
if (target_->GetComputedStyle()->Translate())
transform_property_count++;
return transform_property_count > 1;
}
} // namespace blink