blob: ff36b3f6c61ff0fcf2334caf9f8b9b2e99d02f84 [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 "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/KeyframeEffectModel.h"
#include "core/animation/KeyframeEffectReadOnly.h"
#include "core/animation/StringKeyframe.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/dom/NodeComputedStyle.h"
#include "core/frame/LocalFrame.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 "wtf/text/Base64.h"
#include <memory>
namespace AnimationAgentState {
static const char animationAgentEnabled[] = "animationAgentEnabled";
static const char animationAgentPlaybackRate[] = "animationAgentPlaybackRate";
}
namespace blink {
InspectorAnimationAgent::InspectorAnimationAgent(
InspectedFrames* inspectedFrames,
InspectorCSSAgent* cssAgent,
v8_inspector::V8InspectorSession* v8Session)
: m_inspectedFrames(inspectedFrames),
m_cssAgent(cssAgent),
m_v8Session(v8Session),
m_isCloning(false) {}
void InspectorAnimationAgent::restore() {
if (m_state->booleanProperty(AnimationAgentState::animationAgentEnabled,
false)) {
enable();
double playbackRate = 1;
m_state->getDouble(AnimationAgentState::animationAgentPlaybackRate,
&playbackRate);
setPlaybackRate(playbackRate);
}
}
Response InspectorAnimationAgent::enable() {
m_state->setBoolean(AnimationAgentState::animationAgentEnabled, true);
m_instrumentingAgents->addInspectorAnimationAgent(this);
return Response::OK();
}
Response InspectorAnimationAgent::disable() {
setPlaybackRate(1);
for (const auto& clone : m_idToAnimationClone.values())
clone->cancel();
m_state->setBoolean(AnimationAgentState::animationAgentEnabled, false);
m_instrumentingAgents->removeInspectorAnimationAgent(this);
m_idToAnimation.clear();
m_idToAnimationType.clear();
m_idToAnimationClone.clear();
m_clearedAnimations.clear();
return Response::OK();
}
void InspectorAnimationAgent::didCommitLoadForLocalFrame(LocalFrame* frame) {
if (frame == m_inspectedFrames->root()) {
m_idToAnimation.clear();
m_idToAnimationType.clear();
m_idToAnimationClone.clear();
m_clearedAnimations.clear();
}
double playbackRate = 1;
m_state->getDouble(AnimationAgentState::animationAgentPlaybackRate,
&playbackRate);
setPlaybackRate(playbackRate);
}
static std::unique_ptr<protocol::Animation::AnimationEffect>
buildObjectForAnimationEffect(KeyframeEffectReadOnly* effect,
bool isTransition) {
ComputedTimingProperties computedTiming = effect->getComputedTiming();
double delay = computedTiming.delay();
double duration = computedTiming.duration().getAsUnrestrictedDouble();
String easing = effect->specifiedTiming().timingFunction->toString();
if (isTransition) {
// Obtain keyframes and convert keyframes back to delay
ASSERT(effect->model()->isKeyframeEffectModel());
const KeyframeEffectModelBase* model =
toKeyframeEffectModelBase(effect->model());
Vector<RefPtr<Keyframe>> keyframes =
KeyframeEffectModelBase::normalizedKeyframesForInspector(
model->getFrames());
if (keyframes.size() == 3) {
delay = keyframes.at(1)->offset() * duration;
duration -= delay;
easing = keyframes.at(1)->easing().toString();
} else {
easing = keyframes.at(0)->easing().toString();
}
}
std::unique_ptr<protocol::Animation::AnimationEffect> animationObject =
protocol::Animation::AnimationEffect::create()
.setDelay(delay)
.setEndDelay(computedTiming.endDelay())
.setIterationStart(computedTiming.iterationStart())
.setIterations(computedTiming.iterations())
.setDuration(duration)
.setDirection(computedTiming.direction())
.setFill(computedTiming.fill())
.setBackendNodeId(DOMNodeIds::idForNode(effect->target()))
.setEasing(easing)
.build();
return animationObject;
}
static std::unique_ptr<protocol::Animation::KeyframeStyle>
buildObjectForStringKeyframe(const StringKeyframe* keyframe) {
Decimal decimal = Decimal::fromDouble(keyframe->offset() * 100);
String offset = decimal.toString();
offset.append('%');
std::unique_ptr<protocol::Animation::KeyframeStyle> keyframeObject =
protocol::Animation::KeyframeStyle::create()
.setOffset(offset)
.setEasing(keyframe->easing().toString())
.build();
return keyframeObject;
}
static std::unique_ptr<protocol::Animation::KeyframesRule>
buildObjectForAnimationKeyframes(const KeyframeEffectReadOnly* effect) {
if (!effect || !effect->model() || !effect->model()->isKeyframeEffectModel())
return nullptr;
const KeyframeEffectModelBase* model =
toKeyframeEffectModelBase(effect->model());
Vector<RefPtr<Keyframe>> normalizedKeyframes =
KeyframeEffectModelBase::normalizedKeyframesForInspector(
model->getFrames());
std::unique_ptr<protocol::Array<protocol::Animation::KeyframeStyle>>
keyframes = protocol::Array<protocol::Animation::KeyframeStyle>::create();
for (const auto& keyframe : normalizedKeyframes) {
// Ignore CSS Transitions
if (!keyframe.get()->isStringKeyframe())
continue;
const StringKeyframe* stringKeyframe = toStringKeyframe(keyframe.get());
keyframes->addItem(buildObjectForStringKeyframe(stringKeyframe));
}
return protocol::Animation::KeyframesRule::create()
.setKeyframes(std::move(keyframes))
.build();
}
std::unique_ptr<protocol::Animation::Animation>
InspectorAnimationAgent::buildObjectForAnimation(blink::Animation& animation) {
const Element* element =
toKeyframeEffectReadOnly(animation.effect())->target();
CSSAnimations& cssAnimations = element->elementAnimations()->cssAnimations();
std::unique_ptr<protocol::Animation::KeyframesRule> keyframeRule = nullptr;
String animationType;
if (cssAnimations.isTransitionAnimationForInspector(animation)) {
// CSS Transitions
animationType = AnimationType::CSSTransition;
} else {
// Keyframe based animations
keyframeRule = buildObjectForAnimationKeyframes(
toKeyframeEffectReadOnly(animation.effect()));
animationType = cssAnimations.isAnimationForInspector(animation)
? AnimationType::CSSAnimation
: AnimationType::WebAnimation;
}
String id = String::number(animation.sequenceNumber());
m_idToAnimation.set(id, &animation);
m_idToAnimationType.set(id, animationType);
std::unique_ptr<protocol::Animation::AnimationEffect> animationEffectObject =
buildObjectForAnimationEffect(
toKeyframeEffectReadOnly(animation.effect()),
animationType == AnimationType::CSSTransition);
animationEffectObject->setKeyframesRule(std::move(keyframeRule));
std::unique_ptr<protocol::Animation::Animation> animationObject =
protocol::Animation::Animation::create()
.setId(id)
.setName(animation.id())
.setPausedState(animation.paused())
.setPlayState(animation.playState())
.setPlaybackRate(animation.playbackRate())
.setStartTime(normalizedStartTime(animation))
.setCurrentTime(animation.currentTime())
.setSource(std::move(animationEffectObject))
.setType(animationType)
.build();
if (animationType != AnimationType::WebAnimation)
animationObject->setCssId(createCSSId(animation));
return animationObject;
}
Response InspectorAnimationAgent::getPlaybackRate(double* playbackRate) {
*playbackRate = referenceTimeline().playbackRate();
return Response::OK();
}
Response InspectorAnimationAgent::setPlaybackRate(double playbackRate) {
for (LocalFrame* frame : *m_inspectedFrames)
frame->document()->timeline().setPlaybackRate(playbackRate);
m_state->setDouble(AnimationAgentState::animationAgentPlaybackRate,
playbackRate);
return Response::OK();
}
Response InspectorAnimationAgent::getCurrentTime(const String& id,
double* currentTime) {
blink::Animation* animation = nullptr;
Response response = assertAnimation(id, animation);
if (!response.isSuccess())
return response;
if (m_idToAnimationClone.get(id))
animation = m_idToAnimationClone.get(id);
if (animation->paused()) {
*currentTime = animation->currentTime();
} else {
// Use startTime where possible since currentTime is limited.
*currentTime =
animation->timeline()->currentTime() - animation->startTime();
}
return Response::OK();
}
Response InspectorAnimationAgent::setPaused(
std::unique_ptr<protocol::Array<String>> animationIds,
bool paused) {
for (size_t i = 0; i < animationIds->length(); ++i) {
String animationId = animationIds->get(i);
blink::Animation* animation = nullptr;
Response response = assertAnimation(animationId, 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 currentTime =
clone->timeline()->currentTime() - clone->startTime();
clone->pause();
clone->setCurrentTime(currentTime);
} 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 (!m_idToAnimationClone.get(id)) {
KeyframeEffectReadOnly* oldEffect =
toKeyframeEffectReadOnly(animation->effect());
ASSERT(oldEffect->model()->isKeyframeEffectModel());
KeyframeEffectModelBase* oldModel =
toKeyframeEffectModelBase(oldEffect->model());
EffectModel* newModel = nullptr;
// Clone EffectModel.
// TODO(samli): Determine if this is an animations bug.
if (oldModel->isStringKeyframeEffectModel()) {
StringKeyframeEffectModel* oldStringKeyframeModel =
toStringKeyframeEffectModel(oldModel);
KeyframeVector oldKeyframes = oldStringKeyframeModel->getFrames();
StringKeyframeVector newKeyframes;
for (auto& oldKeyframe : oldKeyframes)
newKeyframes.append(toStringKeyframe(oldKeyframe.get()));
newModel = StringKeyframeEffectModel::create(newKeyframes);
} else if (oldModel->isAnimatableValueKeyframeEffectModel()) {
AnimatableValueKeyframeEffectModel* oldAnimatableValueKeyframeModel =
toAnimatableValueKeyframeEffectModel(oldModel);
KeyframeVector oldKeyframes =
oldAnimatableValueKeyframeModel->getFrames();
AnimatableValueKeyframeVector newKeyframes;
for (auto& oldKeyframe : oldKeyframes)
newKeyframes.append(toAnimatableValueKeyframe(oldKeyframe.get()));
newModel = AnimatableValueKeyframeEffectModel::create(newKeyframes);
}
KeyframeEffect* newEffect = KeyframeEffect::create(
oldEffect->target(), newModel, oldEffect->specifiedTiming());
m_isCloning = true;
blink::Animation* clone =
blink::Animation::create(newEffect, animation->timeline());
m_isCloning = false;
m_idToAnimationClone.set(id, clone);
m_idToAnimation.set(String::number(clone->sequenceNumber()), clone);
clone->play();
clone->setStartTime(animation->startTime());
animation->setEffectSuppressed(true);
}
return m_idToAnimationClone.get(id);
}
Response InspectorAnimationAgent::seekAnimations(
std::unique_ptr<protocol::Array<String>> animationIds,
double currentTime) {
for (size_t i = 0; i < animationIds->length(); ++i) {
String animationId = animationIds->get(i);
blink::Animation* animation = nullptr;
Response response = assertAnimation(animationId, 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(currentTime);
}
return Response::OK();
}
Response InspectorAnimationAgent::releaseAnimations(
std::unique_ptr<protocol::Array<String>> animationIds) {
for (size_t i = 0; i < animationIds->length(); ++i) {
String animationId = animationIds->get(i);
blink::Animation* animation = m_idToAnimation.get(animationId);
if (animation)
animation->setEffectSuppressed(false);
blink::Animation* clone = m_idToAnimationClone.get(animationId);
if (clone)
clone->cancel();
m_idToAnimationClone.remove(animationId);
m_idToAnimation.remove(animationId);
m_idToAnimationType.remove(animationId);
m_clearedAnimations.add(animationId);
}
return Response::OK();
}
Response InspectorAnimationAgent::setTiming(const String& animationId,
double duration,
double delay) {
blink::Animation* animation = nullptr;
Response response = assertAnimation(animationId, animation);
if (!response.isSuccess())
return response;
animation = animationClone(animation);
NonThrowableExceptionState exceptionState;
String type = m_idToAnimationType.get(animationId);
if (type == AnimationType::CSSTransition) {
KeyframeEffect* effect = toKeyframeEffect(animation->effect());
KeyframeEffectModelBase* model = toKeyframeEffectModelBase(effect->model());
const AnimatableValueKeyframeEffectModel* oldModel =
toAnimatableValueKeyframeEffectModel(model);
// Refer to CSSAnimations::calculateTransitionUpdateForProperty() for the
// structure of transitions.
const KeyframeVector& frames = oldModel->getFrames();
ASSERT(frames.size() == 3);
KeyframeVector newFrames;
for (int i = 0; i < 3; i++)
newFrames.append(toAnimatableValueKeyframe(frames[i]->clone().get()));
// Update delay, represented by the distance between the first two
// keyframes.
newFrames[1]->setOffset(delay / (delay + duration));
model->setFrames(newFrames);
AnimationEffectTiming* timing = effect->timing();
UnrestrictedDoubleOrString unrestrictedDuration;
unrestrictedDuration.setUnrestrictedDouble(duration + delay);
timing->setDuration(unrestrictedDuration, exceptionState);
} else {
AnimationEffectTiming* timing =
toAnimationEffectTiming(animation->effect()->timing());
UnrestrictedDoubleOrString unrestrictedDuration;
unrestrictedDuration.setUnrestrictedDouble(duration);
timing->setDuration(unrestrictedDuration, exceptionState);
timing->setDelay(delay);
}
return Response::OK();
}
Response InspectorAnimationAgent::resolveAnimation(
const String& animationId,
std::unique_ptr<v8_inspector::protocol::Runtime::API::RemoteObject>*
result) {
blink::Animation* animation = nullptr;
Response response = assertAnimation(animationId, animation);
if (!response.isSuccess())
return response;
if (m_idToAnimationClone.get(animationId))
animation = m_idToAnimationClone.get(animationId);
const Element* element =
toKeyframeEffectReadOnly(animation->effect())->target();
Document* document = element->ownerDocument();
LocalFrame* frame = document ? document->frame() : nullptr;
ScriptState* scriptState = frame ? ScriptState::forMainWorld(frame) : nullptr;
if (!scriptState)
return Response::Error("Element not associated with a document.");
ScriptState::Scope scope(scriptState);
static const char kAnimationObjectGroup[] = "animation";
m_v8Session->releaseObjectGroup(
toV8InspectorStringView(kAnimationObjectGroup));
*result = m_v8Session->wrapObject(
scriptState->context(),
toV8(animation, scriptState->context()->Global(), scriptState->isolate()),
toV8InspectorStringView(kAnimationObjectGroup));
if (!*result)
return Response::Error("Element not associated with a document.");
return Response::OK();
}
static CSSPropertyID animationProperties[] = {
CSSPropertyAnimationDelay, CSSPropertyAnimationDirection,
CSSPropertyAnimationDuration, CSSPropertyAnimationFillMode,
CSSPropertyAnimationIterationCount, CSSPropertyAnimationName,
CSSPropertyAnimationTimingFunction};
static CSSPropertyID transitionProperties[] = {
CSSPropertyTransitionDelay, CSSPropertyTransitionDuration,
CSSPropertyTransitionProperty, CSSPropertyTransitionTimingFunction,
};
static void addStringToDigestor(WebCryptoDigestor* digestor,
const String& string) {
digestor->consume(
reinterpret_cast<const unsigned char*>(string.ascii().data()),
string.length());
}
String InspectorAnimationAgent::createCSSId(blink::Animation& animation) {
String type =
m_idToAnimationType.get(String::number(animation.sequenceNumber()));
ASSERT(type != AnimationType::WebAnimation);
KeyframeEffectReadOnly* effect = toKeyframeEffectReadOnly(animation.effect());
Vector<CSSPropertyID> cssProperties;
if (type == AnimationType::CSSAnimation) {
for (CSSPropertyID property : animationProperties)
cssProperties.append(property);
} else {
for (CSSPropertyID property : transitionProperties)
cssProperties.append(property);
cssProperties.append(cssPropertyID(animation.id()));
}
Element* element = effect->target();
HeapVector<Member<CSSStyleDeclaration>> styles =
m_cssAgent->matchingStyles(element);
std::unique_ptr<WebCryptoDigestor> digestor =
createDigestor(HashAlgorithmSha1);
addStringToDigestor(digestor.get(), type);
addStringToDigestor(digestor.get(), animation.id());
for (CSSPropertyID property : cssProperties) {
CSSStyleDeclaration* style =
m_cssAgent->findEffectiveDeclaration(property, styles);
// Ignore inline styles.
if (!style || !style->parentStyleSheet() || !style->parentRule() ||
style->parentRule()->type() != CSSRule::kStyleRule)
continue;
addStringToDigestor(digestor.get(), getPropertyNameString(property));
addStringToDigestor(digestor.get(),
m_cssAgent->styleSheetId(style->parentStyleSheet()));
addStringToDigestor(digestor.get(),
toCSSStyleRule(style->parentRule())->selectorText());
}
DigestValue digestResult;
finishDigestor(digestor.get(), digestResult);
return base64Encode(reinterpret_cast<const char*>(digestResult.data()), 10);
}
void InspectorAnimationAgent::didCreateAnimation(unsigned sequenceNumber) {
if (m_isCloning)
return;
frontend()->animationCreated(String::number(sequenceNumber));
}
void InspectorAnimationAgent::animationPlayStateChanged(
blink::Animation* animation,
blink::Animation::AnimationPlayState oldPlayState,
blink::Animation::AnimationPlayState newPlayState) {
const String& animationId = String::number(animation->sequenceNumber());
if (m_idToAnimation.get(animationId) ||
m_clearedAnimations.contains(animationId))
return;
if (newPlayState == blink::Animation::Running ||
newPlayState == blink::Animation::Finished)
frontend()->animationStarted(buildObjectForAnimation(*animation));
else if (newPlayState == blink::Animation::Idle ||
newPlayState == blink::Animation::Paused)
frontend()->animationCanceled(animationId);
}
void InspectorAnimationAgent::didClearDocumentOfWindowObject(
LocalFrame* frame) {
if (!m_state->booleanProperty(AnimationAgentState::animationAgentEnabled,
false))
return;
ASSERT(frame->document());
frame->document()->timeline().setPlaybackRate(
referenceTimeline().playbackRate());
}
Response InspectorAnimationAgent::assertAnimation(const String& id,
blink::Animation*& result) {
result = m_idToAnimation.get(id);
if (!result)
return Response::Error("Could not find animation with given id");
return Response::OK();
}
AnimationTimeline& InspectorAnimationAgent::referenceTimeline() {
return m_inspectedFrames->root()->document()->timeline();
}
double InspectorAnimationAgent::normalizedStartTime(
blink::Animation& animation) {
if (referenceTimeline().playbackRate() == 0)
return animation.startTime() + referenceTimeline().currentTime() -
animation.timeline()->currentTime();
return animation.startTime() +
(animation.timeline()->zeroTime() - referenceTimeline().zeroTime()) *
1000 * referenceTimeline().playbackRate();
}
DEFINE_TRACE(InspectorAnimationAgent) {
visitor->trace(m_inspectedFrames);
visitor->trace(m_cssAgent);
visitor->trace(m_idToAnimation);
visitor->trace(m_idToAnimationClone);
InspectorBaseAgent::trace(visitor);
}
} // namespace blink