blob: c55ce89ee3a81485baa0d8605fa57b83fb7bf4f6 [file] [log] [blame]
// Copyright 2014 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 "core/inspector/InspectorAnimationAgent.h"
#include <memory>
#include "bindings/core/v8/V8BindingForCore.h"
#include "core/animation/Animation.h"
#include "core/animation/AnimationEffectReadOnly.h"
#include "core/animation/AnimationEffectTiming.h"
#include "core/animation/ComputedTimingProperties.h"
#include "core/animation/EffectModel.h"
#include "core/animation/ElementAnimation.h"
#include "core/animation/ElementAnimations.h"
#include "core/animation/KeyframeEffectModel.h"
#include "core/animation/KeyframeEffectReadOnly.h"
#include "core/animation/StringKeyframe.h"
#include "core/animation/css/CSSAnimations.h"
#include "core/css/CSSKeyframeRule.h"
#include "core/css/CSSKeyframesRule.h"
#include "core/css/CSSRuleList.h"
#include "core/css/CSSStyleRule.h"
#include "core/css/resolver/StyleResolver.h"
#include "core/dom/DOMNodeIds.h"
#include "core/frame/LocalFrame.h"
#include "core/inspector/AddStringToDigestor.h"
#include "core/inspector/InspectedFrames.h"
#include "core/inspector/InspectorCSSAgent.h"
#include "core/inspector/InspectorStyleSheet.h"
#include "core/inspector/V8InspectorString.h"
#include "platform/Decimal.h"
#include "platform/animation/TimingFunction.h"
#include "platform/wtf/text/Base64.h"
namespace AnimationAgentState {
static const char animationAgentEnabled[] = "animationAgentEnabled";
static const char animationAgentPlaybackRate[] = "animationAgentPlaybackRate";
}
namespace blink {
using protocol::Response;
InspectorAnimationAgent::InspectorAnimationAgent(
InspectedFrames* inspected_frames,
InspectorCSSAgent* css_agent,
v8_inspector::V8InspectorSession* v8_session)
: inspected_frames_(inspected_frames),
css_agent_(css_agent),
v8_session_(v8_session),
is_cloning_(false) {}
void InspectorAnimationAgent::Restore() {
if (state_->booleanProperty(AnimationAgentState::animationAgentEnabled,
false)) {
enable();
double playback_rate = 1;
state_->getDouble(AnimationAgentState::animationAgentPlaybackRate,
&playback_rate);
setPlaybackRate(playback_rate);
}
}
Response InspectorAnimationAgent::enable() {
state_->setBoolean(AnimationAgentState::animationAgentEnabled, true);
instrumenting_agents_->addInspectorAnimationAgent(this);
return Response::OK();
}
Response InspectorAnimationAgent::disable() {
setPlaybackRate(1);
for (const auto& clone : id_to_animation_clone_.Values())
clone->cancel();
state_->setBoolean(AnimationAgentState::animationAgentEnabled, false);
instrumenting_agents_->removeInspectorAnimationAgent(this);
id_to_animation_.clear();
id_to_animation_type_.clear();
id_to_animation_clone_.clear();
cleared_animations_.clear();
return Response::OK();
}
void InspectorAnimationAgent::DidCommitLoadForLocalFrame(LocalFrame* frame) {
if (frame == inspected_frames_->Root()) {
id_to_animation_.clear();
id_to_animation_type_.clear();
id_to_animation_clone_.clear();
cleared_animations_.clear();
}
double playback_rate = 1;
state_->getDouble(AnimationAgentState::animationAgentPlaybackRate,
&playback_rate);
setPlaybackRate(playback_rate);
}
static std::unique_ptr<protocol::Animation::AnimationEffect>
BuildObjectForAnimationEffect(KeyframeEffectReadOnly* effect,
bool is_transition) {
ComputedTimingProperties computed_timing = effect->getComputedTiming();
double delay = computed_timing.delay();
double duration = computed_timing.duration().GetAsUnrestrictedDouble();
String easing = effect->SpecifiedTiming().timing_function->ToString();
if (is_transition) {
// Obtain keyframes and convert keyframes back to delay
DCHECK(effect->Model()->IsKeyframeEffectModel());
const KeyframeVector& keyframes = effect->Model()->GetFrames();
if (keyframes.size() == 3) {
delay = keyframes.at(1)->CheckedOffset() * duration;
duration -= delay;
easing = keyframes.at(1)->Easing().ToString();
} else {
easing = keyframes.at(0)->Easing().ToString();
}
}
std::unique_ptr<protocol::Animation::AnimationEffect> animation_object =
protocol::Animation::AnimationEffect::create()
.setDelay(delay)
.setEndDelay(computed_timing.endDelay())
.setIterationStart(computed_timing.iterationStart())
.setIterations(computed_timing.iterations())
.setDuration(duration)
.setDirection(computed_timing.direction())
.setFill(computed_timing.fill())
.setEasing(easing)
.build();
if (effect->Target())
animation_object->setBackendNodeId(DOMNodeIds::IdForNode(effect->Target()));
return animation_object;
}
static std::unique_ptr<protocol::Animation::KeyframeStyle>
BuildObjectForStringKeyframe(const StringKeyframe* keyframe,
double computed_offset) {
String offset = String::NumberToStringECMAScript(computed_offset * 100);
offset.append('%');
std::unique_ptr<protocol::Animation::KeyframeStyle> keyframe_object =
protocol::Animation::KeyframeStyle::create()
.setOffset(offset)
.setEasing(keyframe->Easing().ToString())
.build();
return keyframe_object;
}
static std::unique_ptr<protocol::Animation::KeyframesRule>
BuildObjectForAnimationKeyframes(const KeyframeEffectReadOnly* effect) {
if (!effect || !effect->Model() || !effect->Model()->IsKeyframeEffectModel())
return nullptr;
const KeyframeEffectModelBase* model = effect->Model();
Vector<double> computed_offsets =
KeyframeEffectModelBase::GetComputedOffsets(model->GetFrames());
std::unique_ptr<protocol::Array<protocol::Animation::KeyframeStyle>>
keyframes = protocol::Array<protocol::Animation::KeyframeStyle>::create();
for (size_t i = 0; i < model->GetFrames().size(); i++) {
const Keyframe* keyframe = model->GetFrames().at(i).get();
// Ignore CSS Transitions
if (!keyframe->IsStringKeyframe())
continue;
const StringKeyframe* string_keyframe = ToStringKeyframe(keyframe);
keyframes->addItem(
BuildObjectForStringKeyframe(string_keyframe, computed_offsets.at(i)));
}
return protocol::Animation::KeyframesRule::create()
.setKeyframes(std::move(keyframes))
.build();
}
std::unique_ptr<protocol::Animation::Animation>
InspectorAnimationAgent::BuildObjectForAnimation(blink::Animation& animation) {
String animation_type;
std::unique_ptr<protocol::Animation::AnimationEffect> animation_effect_object;
if (!animation.effect()) {
animation_type = AnimationType::WebAnimation;
} else {
const Element* element =
ToKeyframeEffectReadOnly(animation.effect())->Target();
std::unique_ptr<protocol::Animation::KeyframesRule> keyframe_rule;
if (!element) {
animation_type = AnimationType::WebAnimation;
} else {
CSSAnimations& css_animations =
element->GetElementAnimations()->CssAnimations();
if (css_animations.IsTransitionAnimationForInspector(animation)) {
// CSS Transitions
animation_type = AnimationType::CSSTransition;
} else {
// Keyframe based animations
keyframe_rule = BuildObjectForAnimationKeyframes(
ToKeyframeEffectReadOnly(animation.effect()));
animation_type = css_animations.IsAnimationForInspector(animation)
? AnimationType::CSSAnimation
: AnimationType::WebAnimation;
}
}
animation_effect_object = BuildObjectForAnimationEffect(
ToKeyframeEffectReadOnly(animation.effect()),
animation_type == AnimationType::CSSTransition);
animation_effect_object->setKeyframesRule(std::move(keyframe_rule));
}
String id = String::Number(animation.SequenceNumber());
id_to_animation_.Set(id, &animation);
id_to_animation_type_.Set(id, animation_type);
std::unique_ptr<protocol::Animation::Animation> animation_object =
protocol::Animation::Animation::create()
.setId(id)
.setName(animation.id())
.setPausedState(animation.Paused())
.setPlayState(animation.playState())
.setPlaybackRate(animation.playbackRate())
.setStartTime(NormalizedStartTime(animation))
.setCurrentTime(animation.currentTime())
.setType(animation_type)
.build();
if (animation_type != AnimationType::WebAnimation)
animation_object->setCssId(CreateCSSId(animation));
if (animation_effect_object)
animation_object->setSource(std::move(animation_effect_object));
return animation_object;
}
Response InspectorAnimationAgent::getPlaybackRate(double* playback_rate) {
*playback_rate = ReferenceTimeline().PlaybackRate();
return Response::OK();
}
Response InspectorAnimationAgent::setPlaybackRate(double playback_rate) {
for (LocalFrame* frame : *inspected_frames_)
frame->GetDocument()->Timeline().SetPlaybackRate(playback_rate);
state_->setDouble(AnimationAgentState::animationAgentPlaybackRate,
playback_rate);
return Response::OK();
}
Response InspectorAnimationAgent::getCurrentTime(const String& id,
double* current_time) {
blink::Animation* animation = nullptr;
Response response = AssertAnimation(id, animation);
if (!response.isSuccess())
return response;
if (id_to_animation_clone_.at(id))
animation = id_to_animation_clone_.at(id);
if (animation->Paused()) {
*current_time = animation->currentTime();
} else {
// Use startTime where possible since currentTime is limited.
*current_time =
animation->TimelineInternal()->currentTime() - animation->startTime();
}
return Response::OK();
}
Response InspectorAnimationAgent::setPaused(
std::unique_ptr<protocol::Array<String>> animation_ids,
bool paused) {
for (size_t i = 0; i < animation_ids->length(); ++i) {
String animation_id = animation_ids->get(i);
blink::Animation* animation = nullptr;
Response response = AssertAnimation(animation_id, animation);
if (!response.isSuccess())
return response;
blink::Animation* clone = AnimationClone(animation);
if (!clone)
return Response::Error("Failed to clone detached animation");
if (paused && !clone->Paused()) {
// Ensure we restore a current time if the animation is limited.
double current_time =
clone->TimelineInternal()->currentTime() - clone->startTime();
clone->pause();
clone->setCurrentTime(current_time, false);
} else if (!paused && clone->Paused()) {
clone->Unpause();
}
}
return Response::OK();
}
blink::Animation* InspectorAnimationAgent::AnimationClone(
blink::Animation* animation) {
const String id = String::Number(animation->SequenceNumber());
if (!id_to_animation_clone_.at(id)) {
KeyframeEffectReadOnly* old_effect =
ToKeyframeEffectReadOnly(animation->effect());
DCHECK(old_effect->Model()->IsKeyframeEffectModel());
KeyframeEffectModelBase* old_model = old_effect->Model();
KeyframeEffectModelBase* new_model = nullptr;
// Clone EffectModel.
// TODO(samli): Determine if this is an animations bug.
if (old_model->IsStringKeyframeEffectModel()) {
StringKeyframeEffectModel* old_string_keyframe_model =
ToStringKeyframeEffectModel(old_model);
KeyframeVector old_keyframes = old_string_keyframe_model->GetFrames();
StringKeyframeVector new_keyframes;
for (auto& old_keyframe : old_keyframes)
new_keyframes.push_back(ToStringKeyframe(old_keyframe.get()));
new_model = StringKeyframeEffectModel::Create(new_keyframes);
} else if (old_model->IsTransitionKeyframeEffectModel()) {
TransitionKeyframeEffectModel* old_transition_keyframe_model =
ToTransitionKeyframeEffectModel(old_model);
KeyframeVector old_keyframes = old_transition_keyframe_model->GetFrames();
TransitionKeyframeVector new_keyframes;
for (auto& old_keyframe : old_keyframes)
new_keyframes.push_back(ToTransitionKeyframe(old_keyframe.get()));
new_model = TransitionKeyframeEffectModel::Create(new_keyframes);
}
KeyframeEffect* new_effect = KeyframeEffect::Create(
old_effect->Target(), new_model, old_effect->SpecifiedTiming());
is_cloning_ = true;
blink::Animation* clone =
blink::Animation::Create(new_effect, animation->timeline());
is_cloning_ = false;
id_to_animation_clone_.Set(id, clone);
id_to_animation_.Set(String::Number(clone->SequenceNumber()), clone);
clone->play();
clone->setStartTime(animation->startTime(), false);
animation->SetEffectSuppressed(true);
}
return id_to_animation_clone_.at(id);
}
Response InspectorAnimationAgent::seekAnimations(
std::unique_ptr<protocol::Array<String>> animation_ids,
double current_time) {
for (size_t i = 0; i < animation_ids->length(); ++i) {
String animation_id = animation_ids->get(i);
blink::Animation* animation = nullptr;
Response response = AssertAnimation(animation_id, animation);
if (!response.isSuccess())
return response;
blink::Animation* clone = AnimationClone(animation);
if (!clone)
return Response::Error("Failed to clone a detached animation.");
if (!clone->Paused())
clone->play();
clone->setCurrentTime(current_time, false);
}
return Response::OK();
}
Response InspectorAnimationAgent::releaseAnimations(
std::unique_ptr<protocol::Array<String>> animation_ids) {
for (size_t i = 0; i < animation_ids->length(); ++i) {
String animation_id = animation_ids->get(i);
blink::Animation* animation = id_to_animation_.at(animation_id);
if (animation)
animation->SetEffectSuppressed(false);
blink::Animation* clone = id_to_animation_clone_.at(animation_id);
if (clone)
clone->cancel();
id_to_animation_clone_.erase(animation_id);
id_to_animation_.erase(animation_id);
id_to_animation_type_.erase(animation_id);
cleared_animations_.insert(animation_id);
}
return Response::OK();
}
Response InspectorAnimationAgent::setTiming(const String& animation_id,
double duration,
double delay) {
blink::Animation* animation = nullptr;
Response response = AssertAnimation(animation_id, animation);
if (!response.isSuccess())
return response;
animation = AnimationClone(animation);
NonThrowableExceptionState exception_state;
String type = id_to_animation_type_.at(animation_id);
if (type == AnimationType::CSSTransition) {
KeyframeEffect* effect = ToKeyframeEffect(animation->effect());
const TransitionKeyframeEffectModel* old_model =
ToTransitionKeyframeEffectModel(effect->Model());
// Refer to CSSAnimations::calculateTransitionUpdateForProperty() for the
// structure of transitions.
const KeyframeVector& frames = old_model->GetFrames();
DCHECK(frames.size() == 3);
KeyframeVector new_frames;
for (int i = 0; i < 3; i++)
new_frames.push_back(ToTransitionKeyframe(frames[i]->Clone().get()));
// Update delay, represented by the distance between the first two
// keyframes.
new_frames[1]->SetOffset(delay / (delay + duration));
effect->Model()->SetFrames(new_frames);
AnimationEffectTiming* timing = effect->timing();
UnrestrictedDoubleOrString unrestricted_duration;
unrestricted_duration.SetUnrestrictedDouble(duration + delay);
timing->setDuration(unrestricted_duration, exception_state);
} else {
AnimationEffectTiming* timing =
ToAnimationEffectTiming(animation->effect()->timing());
UnrestrictedDoubleOrString unrestricted_duration;
unrestricted_duration.SetUnrestrictedDouble(duration);
timing->setDuration(unrestricted_duration, exception_state);
timing->setDelay(delay);
}
return Response::OK();
}
Response InspectorAnimationAgent::resolveAnimation(
const String& animation_id,
std::unique_ptr<v8_inspector::protocol::Runtime::API::RemoteObject>*
result) {
blink::Animation* animation = nullptr;
Response response = AssertAnimation(animation_id, animation);
if (!response.isSuccess())
return response;
if (id_to_animation_clone_.at(animation_id))
animation = id_to_animation_clone_.at(animation_id);
const Element* element =
ToKeyframeEffectReadOnly(animation->effect())->Target();
Document* document = element->ownerDocument();
LocalFrame* frame = document ? document->GetFrame() : nullptr;
ScriptState* script_state =
frame ? ToScriptStateForMainWorld(frame) : nullptr;
if (!script_state)
return Response::Error("Element not associated with a document.");
ScriptState::Scope scope(script_state);
static const char kAnimationObjectGroup[] = "animation";
v8_session_->releaseObjectGroup(
ToV8InspectorStringView(kAnimationObjectGroup));
*result = v8_session_->wrapObject(
script_state->GetContext(),
ToV8(animation, script_state->GetContext()->Global(),
script_state->GetIsolate()),
ToV8InspectorStringView(kAnimationObjectGroup),
false /* generatePreview */);
if (!*result)
return Response::Error("Element not associated with a document.");
return Response::OK();
}
String InspectorAnimationAgent::CreateCSSId(blink::Animation& animation) {
static const CSSProperty* g_animation_properties[] = {
&GetCSSPropertyAnimationDelay(),
&GetCSSPropertyAnimationDirection(),
&GetCSSPropertyAnimationDuration(),
&GetCSSPropertyAnimationFillMode(),
&GetCSSPropertyAnimationIterationCount(),
&GetCSSPropertyAnimationName(),
&GetCSSPropertyAnimationTimingFunction(),
};
static const CSSProperty* g_transition_properties[] = {
&GetCSSPropertyTransitionDelay(), &GetCSSPropertyTransitionDuration(),
&GetCSSPropertyTransitionProperty(),
&GetCSSPropertyTransitionTimingFunction(),
};
String type =
id_to_animation_type_.at(String::Number(animation.SequenceNumber()));
DCHECK_NE(type, AnimationType::WebAnimation);
KeyframeEffectReadOnly* effect = ToKeyframeEffectReadOnly(animation.effect());
Vector<const CSSProperty*> css_properties;
if (type == AnimationType::CSSAnimation) {
for (const CSSProperty* property : g_animation_properties)
css_properties.push_back(property);
} else {
for (const CSSProperty* property : g_transition_properties)
css_properties.push_back(property);
css_properties.push_back(&CSSProperty::Get(cssPropertyID(animation.id())));
}
Element* element = effect->Target();
HeapVector<Member<CSSStyleDeclaration>> styles =
css_agent_->MatchingStyles(element);
std::unique_ptr<WebCryptoDigestor> digestor =
CreateDigestor(kHashAlgorithmSha1);
AddStringToDigestor(digestor.get(), type);
AddStringToDigestor(digestor.get(), animation.id());
for (const CSSProperty* property : css_properties) {
CSSStyleDeclaration* style =
css_agent_->FindEffectiveDeclaration(*property, styles);
// Ignore inline styles.
if (!style || !style->ParentStyleSheet() || !style->parentRule() ||
style->parentRule()->type() != CSSRule::kStyleRule)
continue;
AddStringToDigestor(digestor.get(), property->GetPropertyNameString());
AddStringToDigestor(digestor.get(),
css_agent_->StyleSheetId(style->ParentStyleSheet()));
AddStringToDigestor(digestor.get(),
ToCSSStyleRule(style->parentRule())->selectorText());
}
DigestValue digest_result;
FinishDigestor(digestor.get(), digest_result);
return Base64Encode(reinterpret_cast<const char*>(digest_result.data()), 10);
}
void InspectorAnimationAgent::DidCreateAnimation(unsigned sequence_number) {
if (is_cloning_)
return;
GetFrontend()->animationCreated(String::Number(sequence_number));
}
void InspectorAnimationAgent::AnimationPlayStateChanged(
blink::Animation* animation,
blink::Animation::AnimationPlayState old_play_state,
blink::Animation::AnimationPlayState new_play_state) {
const String& animation_id = String::Number(animation->SequenceNumber());
// We no longer care about animations that have been released.
if (cleared_animations_.Contains(animation_id))
return;
// Record newly starting animations only once, as |buildObjectForAnimation|
// constructs and caches our internal representation of the given |animation|.
if ((new_play_state == blink::Animation::kRunning ||
new_play_state == blink::Animation::kFinished) &&
!id_to_animation_.Contains(animation_id))
GetFrontend()->animationStarted(BuildObjectForAnimation(*animation));
else if (new_play_state == blink::Animation::kIdle ||
new_play_state == blink::Animation::kPaused)
GetFrontend()->animationCanceled(animation_id);
}
void InspectorAnimationAgent::DidClearDocumentOfWindowObject(
LocalFrame* frame) {
if (!state_->booleanProperty(AnimationAgentState::animationAgentEnabled,
false))
return;
DCHECK(frame->GetDocument());
frame->GetDocument()->Timeline().SetPlaybackRate(
ReferenceTimeline().PlaybackRate());
}
Response InspectorAnimationAgent::AssertAnimation(const String& id,
blink::Animation*& result) {
result = id_to_animation_.at(id);
if (!result)
return Response::Error("Could not find animation with given id");
return Response::OK();
}
DocumentTimeline& InspectorAnimationAgent::ReferenceTimeline() {
return inspected_frames_->Root()->GetDocument()->Timeline();
}
double InspectorAnimationAgent::NormalizedStartTime(
blink::Animation& animation) {
if (ReferenceTimeline().PlaybackRate() == 0) {
return animation.startTime() + ReferenceTimeline().currentTime() -
animation.TimelineInternal()->currentTime();
}
return animation.startTime() + (animation.TimelineInternal()->ZeroTime() -
ReferenceTimeline().ZeroTime()) *
1000 * ReferenceTimeline().PlaybackRate();
}
void InspectorAnimationAgent::Trace(blink::Visitor* visitor) {
visitor->Trace(inspected_frames_);
visitor->Trace(css_agent_);
visitor->Trace(id_to_animation_);
visitor->Trace(id_to_animation_clone_);
InspectorBaseAgent::Trace(visitor);
}
} // namespace blink