TEST(AnimationEffectInputTest, LooslySorted) { V8TestingScope scope; Vector<Dictionary> jsKeyframes; v8::Local<v8::Object> keyframe1 = v8::Object::New(scope.isolate()); v8::Local<v8::Object> keyframe2 = v8::Object::New(scope.isolate()); v8::Local<v8::Object> keyframe3 = v8::Object::New(scope.isolate()); setV8ObjectPropertyAsString(scope.isolate(), keyframe1, "width", "100px"); setV8ObjectPropertyAsString(scope.isolate(), keyframe1, "offset", "0"); setV8ObjectPropertyAsString(scope.isolate(), keyframe2, "width", "200px"); setV8ObjectPropertyAsString(scope.isolate(), keyframe3, "width", "0px"); setV8ObjectPropertyAsString(scope.isolate(), keyframe3, "offset", "1"); jsKeyframes.push_back( Dictionary(scope.isolate(), keyframe1, scope.getExceptionState())); jsKeyframes.push_back( Dictionary(scope.isolate(), keyframe2, scope.getExceptionState())); jsKeyframes.push_back( Dictionary(scope.isolate(), keyframe3, scope.getExceptionState())); Element* element = appendElement(scope.document()); EffectModel* animationEffect = EffectInput::convert( element, DictionarySequenceOrDictionary::fromDictionarySequence(jsKeyframes), nullptr, scope.getExceptionState()); EXPECT_FALSE(scope.getExceptionState().hadException()); const KeyframeEffectModelBase& keyframeEffect = *toKeyframeEffectModelBase(animationEffect); EXPECT_EQ(1, keyframeEffect.getFrames()[2]->offset()); }
void InspectorAnimationAgent::setTiming(ErrorString* errorString, const String& animationId, double duration, double delay) { Animation* animation = assertAnimation(errorString, animationId); if (!animation) return; AnimationType 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 = animation->effect()->timing(); UnrestrictedDoubleOrString unrestrictedDuration; unrestrictedDuration.setUnrestrictedDouble(duration + delay); timing->setDuration(unrestrictedDuration); } else if (type == AnimationType::WebAnimation) { AnimationEffectTiming* timing = animation->effect()->timing(); UnrestrictedDoubleOrString unrestrictedDuration; unrestrictedDuration.setUnrestrictedDouble(duration); timing->setDuration(unrestrictedDuration); timing->setDelay(delay); } }
bool CompositorAnimations::startAnimationOnCompositor(const Element& element, int group, double startTime, double timeOffset, const Timing& timing, const AnimationPlayer& player, const AnimationEffect& effect, Vector<int>& startedAnimationIds, double playerPlaybackRate) { ASSERT(startedAnimationIds.isEmpty()); ASSERT(isCandidateForAnimationOnCompositor(timing, element, &player, effect, playerPlaybackRate)); ASSERT(canStartAnimationOnCompositor(element)); const KeyframeEffectModelBase& keyframeEffect = toKeyframeEffectModelBase(effect); DeprecatedPaintLayer* layer = toLayoutBoxModelObject(element.layoutObject())->layer(); ASSERT(layer); Vector<OwnPtr<WebCompositorAnimation>> animations; CompositorAnimationsImpl::getAnimationOnCompositor(timing, group, startTime, timeOffset, keyframeEffect, animations, playerPlaybackRate); ASSERT(!animations.isEmpty()); for (auto& animation : animations) { int id = animation->id(); if (RuntimeEnabledFeatures::compositorAnimationTimelinesEnabled()) { WebCompositorAnimationPlayer* compositorPlayer = player.compositorPlayer(); ASSERT(compositorPlayer); compositorPlayer->addAnimation(animation.leakPtr()); } else if (!layer->compositedDeprecatedPaintLayerMapping()->mainGraphicsLayer()->addAnimation(animation.release())) { // FIXME: We should know ahead of time whether these animations can be started. for (int startedAnimationId : startedAnimationIds) cancelAnimationOnCompositor(element, player, startedAnimationId); startedAnimationIds.clear(); return false; } startedAnimationIds.append(id); } ASSERT(!startedAnimationIds.isEmpty()); return true; }
bool CompositorAnimations::getAnimatedBoundingBox(FloatBox& box, const AnimationEffect& effect, double minValue, double maxValue) const { const KeyframeEffectModelBase& keyframeEffect = toKeyframeEffectModelBase(effect); PropertySet properties = keyframeEffect.properties(); if (properties.isEmpty()) return true; minValue = std::min(minValue, 0.0); maxValue = std::max(maxValue, 1.0); for (const auto& property : properties) { // TODO: Add the ability to get expanded bounds for filters as well. if (property != CSSPropertyTransform && property != CSSPropertyWebkitTransform) continue; const PropertySpecificKeyframeVector& frames = keyframeEffect.getPropertySpecificKeyframes(property); if (frames.isEmpty() || frames.size() < 2) continue; FloatBox originalBox(box); for (size_t j = 0; j < frames.size() - 1; ++j) { const AnimatableTransform* startTransform = toAnimatableTransform(frames[j]->getAnimatableValue().get()); const AnimatableTransform* endTransform = toAnimatableTransform(frames[j+1]->getAnimatableValue().get()); if (!startTransform || !endTransform) return false; // TODO: Add support for inflating modes other than Replace. if (frames[j]->composite() != AnimationEffect::CompositeReplace) return false; const TimingFunction& timing = frames[j]->easing(); double min = 0; double max = 1; if (j == 0) { float frameLength = frames[j+1]->offset(); if (frameLength > 0) { min = minValue / frameLength; } } if (j == frames.size() - 2) { float frameLength = frames[j+1]->offset() - frames[j]->offset(); if (frameLength > 0) { max = 1 + (maxValue - 1) / frameLength; } } FloatBox bounds; timing.range(&min, &max); if (!endTransform->transformOperations().blendedBoundsForBox(originalBox, startTransform->transformOperations(), min, max, &bounds)) return false; box.expandTo(bounds); } } return true; }
void CSSAnimations::calculateCompositorAnimationUpdate(CSSAnimationUpdate& update, const Element* animatingElement, Element& element, const ComputedStyle& style) { ElementAnimations* elementAnimations = animatingElement ? animatingElement->elementAnimations() : nullptr; // We only update compositor animations in response to changes in the base style. if (!elementAnimations || elementAnimations->isAnimationStyleChange()) return; if (!animatingElement->layoutObject() || !animatingElement->layoutObject()->style()) return; const ComputedStyle& oldStyle = *animatingElement->layoutObject()->style(); if (!oldStyle.shouldCompositeForCurrentAnimations()) return; CSSAnimations& cssAnimations = elementAnimations->cssAnimations(); for (auto& runningAnimation : cssAnimations.m_animations.values()) { Animation& animation = *runningAnimation->animation; if (animation.effect() && animation.effect()->isKeyframeEffect()) { EffectModel* model = toKeyframeEffect(animation.effect())->model(); if (model && model->isKeyframeEffectModel()) { KeyframeEffectModelBase* keyframeEffect = toKeyframeEffectModelBase(model); if (keyframeEffect->hasSyntheticKeyframes() && keyframeEffect->snapshotNeutralCompositorKeyframes(element, oldStyle, style)) update.updateCompositorKeyframes(&animation); } } } if (oldStyle.hasCurrentTransformAnimation() && oldStyle.effectiveZoom() != style.effectiveZoom()) { for (auto& entry : elementAnimations->animations()) { Animation& animation = *entry.key; if (animation.effect() && animation.effect()->isKeyframeEffect()) { EffectModel* model = toKeyframeEffect(animation.effect())->model(); if (model && model->isKeyframeEffectModel()) { KeyframeEffectModelBase* keyframeEffect = toKeyframeEffectModelBase(model); if (keyframeEffect->affects(PropertyHandle(CSSPropertyTransform)) && keyframeEffect->snapshotAllCompositorKeyframes(element, &style)) update.updateCompositorKeyframes(&animation); } } } } }
bool CompositorAnimations::isCandidateForAnimationOnCompositor(const Timing& timing, const Element& targetElement, const AnimationPlayer* playerToAdd, const AnimationEffect& effect, double playerPlaybackRate) { const KeyframeEffectModelBase& keyframeEffect = toKeyframeEffectModelBase(effect); PropertySet properties = keyframeEffect.properties(); if (properties.isEmpty()) return false; for (const auto& property : properties) { const PropertySpecificKeyframeVector& keyframes = keyframeEffect.getPropertySpecificKeyframes(property); ASSERT(keyframes.size() >= 2); for (const auto& keyframe : keyframes) { // FIXME: Determine candidacy based on the CSSValue instead of a snapshot AnimatableValue. bool isNeutralKeyframe = keyframe->isStringPropertySpecificKeyframe() && !toStringPropertySpecificKeyframe(keyframe.get())->value() && keyframe->composite() == AnimationEffect::CompositeAdd; if ((keyframe->composite() != AnimationEffect::CompositeReplace && !isNeutralKeyframe) || !keyframe->getAnimatableValue()) return false; switch (property) { case CSSPropertyOpacity: break; case CSSPropertyTransform: if (toAnimatableTransform(keyframe->getAnimatableValue().get())->transformOperations().dependsOnBoxSize()) return false; break; case CSSPropertyWebkitFilter: { const FilterOperations& operations = toAnimatableFilterOperations(keyframe->getAnimatableValue().get())->operations(); if (operations.hasFilterThatMovesPixels()) return false; break; } default: // any other types are not allowed to run on compositor. return false; } } } if (playerToAdd && hasIncompatibleAnimations(targetElement, *playerToAdd, effect)) return false; CompositorAnimationsImpl::CompositorTiming out; if (!CompositorAnimationsImpl::convertTimingForCompositor(timing, 0, out, playerPlaybackRate)) return false; return true; }
TEST_F(AnimationAnimationV8Test, CanCreateAnAnimation) { Vector<Dictionary> jsKeyframes; v8::Handle<v8::Object> keyframe1 = v8::Object::New(m_isolate); v8::Handle<v8::Object> keyframe2 = v8::Object::New(m_isolate); setV8ObjectPropertyAsString(keyframe1, "width", "100px"); setV8ObjectPropertyAsString(keyframe1, "offset", "0"); setV8ObjectPropertyAsString(keyframe1, "easing", "ease-in-out"); setV8ObjectPropertyAsString(keyframe2, "width", "0px"); setV8ObjectPropertyAsString(keyframe2, "offset", "1"); setV8ObjectPropertyAsString(keyframe2, "easing", "cubic-bezier(1, 1, 0.3, 0.3)"); jsKeyframes.append(Dictionary(keyframe1, m_isolate)); jsKeyframes.append(Dictionary(keyframe2, m_isolate)); String value1; ASSERT_TRUE(jsKeyframes[0].get("width", value1)); ASSERT_EQ("100px", value1); String value2; ASSERT_TRUE(jsKeyframes[1].get("width", value2)); ASSERT_EQ("0px", value2); RefPtrWillBeRawPtr<Animation> animation = createAnimation(element.get(), jsKeyframes, 0, exceptionState); Element* target = animation->target(); EXPECT_EQ(*element.get(), *target); const KeyframeVector keyframes = toKeyframeEffectModelBase(animation->effect())->getFrames(); EXPECT_EQ(0, keyframes[0]->offset()); EXPECT_EQ(1, keyframes[1]->offset()); const CSSValue* keyframe1Width = toStringKeyframe(keyframes[0].get())->propertyValue(CSSPropertyWidth); const CSSValue* keyframe2Width = toStringKeyframe(keyframes[1].get())->propertyValue(CSSPropertyWidth); ASSERT(keyframe1Width); ASSERT(keyframe2Width); EXPECT_EQ("100px", keyframe1Width->cssText()); EXPECT_EQ("0px", keyframe2Width->cssText()); EXPECT_EQ(*(CubicBezierTimingFunction::preset(CubicBezierTimingFunction::EaseInOut)), *keyframes[0]->easing()); EXPECT_EQ(*(CubicBezierTimingFunction::create(1, 1, 0.3, 0.3).get()), *keyframes[1]->easing()); }
blink::Animation* InspectorAnimationAgent::animationClone( blink::Animation* animation) { const String id = String::number(animation->sequenceNumber()); if (!m_idToAnimationClone.get(id)) { KeyframeEffect* oldEffect = toKeyframeEffect(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); }
static PassRefPtr<TypeBuilder::Animation::KeyframesRule> buildObjectForAnimationKeyframes(const KeyframeEffect* effect) { if (!effect || !effect->model() || !effect->model()->isKeyframeEffectModel()) return nullptr; const KeyframeEffectModelBase* model = toKeyframeEffectModelBase(effect->model()); Vector<RefPtr<Keyframe>> normalizedKeyframes = KeyframeEffectModelBase::normalizedKeyframesForInspector(model->getFrames()); RefPtr<TypeBuilder::Array<TypeBuilder::Animation::KeyframeStyle> > keyframes = TypeBuilder::Array<TypeBuilder::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)); } RefPtr<TypeBuilder::Animation::KeyframesRule> keyframesObject = TypeBuilder::Animation::KeyframesRule::create() .setKeyframes(keyframes); return keyframesObject.release(); }
TEST_F(AnimationEffectInputTest, SortedOffsets) { Vector<Dictionary> jsKeyframes; v8::Local<v8::Object> keyframe1 = v8::Object::New(m_isolate); v8::Local<v8::Object> keyframe2 = v8::Object::New(m_isolate); setV8ObjectPropertyAsString(m_isolate, keyframe1, "width", "100px"); setV8ObjectPropertyAsString(m_isolate, keyframe1, "offset", "0"); setV8ObjectPropertyAsString(m_isolate, keyframe2, "width", "0px"); setV8ObjectPropertyAsString(m_isolate, keyframe2, "offset", "1"); jsKeyframes.append(Dictionary(keyframe1, m_isolate, exceptionState)); jsKeyframes.append(Dictionary(keyframe2, m_isolate, exceptionState)); RefPtrWillBeRawPtr<EffectModel> animationEffect = EffectInput::convert(element.get(), jsKeyframes, exceptionState); EXPECT_FALSE(exceptionState.hadException()); const KeyframeEffectModelBase& keyframeEffect = *toKeyframeEffectModelBase(animationEffect.get()); EXPECT_EQ(1.0, keyframeEffect.getFrames()[1]->offset()); }
TEST_F(AnimationEffectInputTest, LooslySorted) { Vector<Dictionary> jsKeyframes; v8::Local<v8::Object> keyframe1 = v8::Object::New(m_isolate); v8::Local<v8::Object> keyframe2 = v8::Object::New(m_isolate); v8::Local<v8::Object> keyframe3 = v8::Object::New(m_isolate); setV8ObjectPropertyAsString(m_isolate, keyframe1, "width", "100px"); setV8ObjectPropertyAsString(m_isolate, keyframe1, "offset", "0"); setV8ObjectPropertyAsString(m_isolate, keyframe2, "width", "200px"); setV8ObjectPropertyAsString(m_isolate, keyframe3, "width", "0px"); setV8ObjectPropertyAsString(m_isolate, keyframe3, "offset", "1"); jsKeyframes.append(Dictionary(keyframe1, m_isolate, exceptionState)); jsKeyframes.append(Dictionary(keyframe2, m_isolate, exceptionState)); jsKeyframes.append(Dictionary(keyframe3, m_isolate, exceptionState)); EffectModel* animationEffect = EffectInput::convert(element.get(), EffectModelOrDictionarySequenceOrDictionary::fromDictionarySequence(jsKeyframes), nullptr, exceptionState); EXPECT_FALSE(exceptionState.hadException()); const KeyframeEffectModelBase& keyframeEffect = *toKeyframeEffectModelBase(animationEffect); EXPECT_EQ(1, keyframeEffect.getFrames()[2]->offset()); }
static std::unique_ptr<protocol::Animation::AnimationEffect> buildObjectForAnimationEffect(KeyframeEffect* 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::KeyframesRule> buildObjectForAnimationKeyframes(const KeyframeEffect* 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(); }
bool CompositorAnimations::isCandidateForAnimationOnCompositor(const Timing& timing, const Element& targetElement, const Animation* animationToAdd, const EffectModel& effect, double animationPlaybackRate) { const KeyframeEffectModelBase& keyframeEffect = toKeyframeEffectModelBase(effect); PropertyHandleSet properties = keyframeEffect.properties(); if (properties.isEmpty()) return false; unsigned transformPropertyCount = 0; for (const auto& property : properties) { if (!property.isCSSProperty()) return false; if (isTransformRelatedCSSProperty(property)) { if (targetElement.layoutObject() && targetElement.layoutObject()->isInline()) { return false; } transformPropertyCount++; } const PropertySpecificKeyframeVector& keyframes = keyframeEffect.getPropertySpecificKeyframes(property); ASSERT(keyframes.size() >= 2); for (const auto& keyframe : keyframes) { // FIXME: Determine candidacy based on the CSSValue instead of a snapshot AnimatableValue. bool isNeutralKeyframe = keyframe->isCSSPropertySpecificKeyframe() && !toCSSPropertySpecificKeyframe(keyframe.get())->value() && keyframe->composite() == EffectModel::CompositeAdd; if ((keyframe->composite() != EffectModel::CompositeReplace && !isNeutralKeyframe) || !keyframe->getAnimatableValue()) return false; switch (property.cssProperty()) { case CSSPropertyOpacity: break; case CSSPropertyRotate: case CSSPropertyScale: case CSSPropertyTranslate: case CSSPropertyTransform: if (toAnimatableTransform(keyframe->getAnimatableValue().get())->transformOperations().dependsOnBoxSize()) return false; break; case CSSPropertyWebkitFilter: case CSSPropertyBackdropFilter: { const FilterOperations& operations = toAnimatableFilterOperations(keyframe->getAnimatableValue().get())->operations(); if (operations.hasFilterThatMovesPixels()) return false; break; } default: // any other types are not allowed to run on compositor. return false; } } } // TODO: Support multiple transform property animations on the compositor if (transformPropertyCount > 1) return false; if (animationToAdd && hasIncompatibleAnimations(targetElement, *animationToAdd, effect)) return false; CompositorAnimationsImpl::CompositorTiming out; if (!CompositorAnimationsImpl::convertTimingForCompositor(timing, 0, out, animationPlaybackRate)) return false; return true; }