blob: b201b21ed684c337359e1e555674f6fae23185e9 [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 "core/animation/EffectInput.h"
#include "bindings/core/v8/Dictionary.h"
#include "bindings/core/v8/DictionaryHelperForBindings.h"
#include "bindings/core/v8/dictionary_sequence_or_dictionary.h"
#include "core/animation/AnimationInputHelpers.h"
#include "core/animation/CompositorAnimations.h"
#include "core/animation/KeyframeEffectModel.h"
#include "core/animation/StringKeyframe.h"
#include "core/css/CSSStyleSheet.h"
#include "core/dom/Document.h"
#include "core/dom/Element.h"
#include "core/dom/ExceptionCode.h"
#include "core/frame/FrameConsole.h"
#include "core/frame/LocalFrame.h"
#include "core/inspector/ConsoleMessage.h"
#include "platform/wtf/ASCIICType.h"
#include "platform/wtf/HashSet.h"
#include "platform/wtf/NonCopyingSort.h"
namespace blink {
namespace {
// Validates the value of |offset| and throws an exception if out of range.
bool CheckOffset(double offset,
double last_offset,
ExceptionState& exception_state) {
// Keyframes with offsets outside the range [0.0, 1.0] are an error.
if (std::isnan(offset)) {
exception_state.ThrowTypeError("Non numeric offset provided");
return false;
}
if (offset < 0 || offset > 1) {
exception_state.ThrowTypeError("Offsets provided outside the range [0, 1]");
return false;
}
if (offset < last_offset) {
exception_state.ThrowTypeError(
"Keyframes with specified offsets are not sorted");
return false;
}
return true;
}
void SetKeyframeValue(Element& element,
StringKeyframe& keyframe,
const String& property,
const String& value,
ExecutionContext* execution_context) {
StyleSheetContents* style_sheet_contents =
element.GetDocument().ElementSheet().Contents();
CSSPropertyID css_property =
AnimationInputHelpers::KeyframeAttributeToCSSProperty(
property, element.GetDocument());
if (css_property != CSSPropertyInvalid) {
MutableCSSPropertyValueSet::SetResult set_result =
css_property == CSSPropertyVariable
? keyframe.SetCSSPropertyValue(
AtomicString(property),
element.GetDocument().GetPropertyRegistry(), value,
element.GetDocument().SecureContextMode(),
style_sheet_contents)
: keyframe.SetCSSPropertyValue(
css_property, value,
element.GetDocument().SecureContextMode(),
style_sheet_contents);
if (!set_result.did_parse && execution_context) {
Document& document = ToDocument(*execution_context);
if (document.GetFrame()) {
document.GetFrame()->Console().AddMessage(ConsoleMessage::Create(
kJSMessageSource, kWarningMessageLevel,
"Invalid keyframe value for property " + property + ": " + value));
}
}
return;
}
css_property =
AnimationInputHelpers::KeyframeAttributeToPresentationAttribute(property,
element);
if (css_property != CSSPropertyInvalid) {
keyframe.SetPresentationAttributeValue(
css_property, value, element.GetDocument().SecureContextMode(),
style_sheet_contents);
return;
}
const QualifiedName* svg_attribute =
AnimationInputHelpers::KeyframeAttributeToSVGAttribute(property, element);
if (svg_attribute)
keyframe.SetSVGAttributeValue(*svg_attribute, value);
}
EffectModel* CreateEffectModelFromKeyframes(
Element& element,
const StringKeyframeVector& keyframes,
ExceptionState& exception_state) {
StringKeyframeEffectModel* keyframe_effect_model =
StringKeyframeEffectModel::Create(keyframes,
LinearTimingFunction::Shared());
if (!RuntimeEnabledFeatures::CSSAdditiveAnimationsEnabled()) {
for (const auto& keyframe_group :
keyframe_effect_model->GetPropertySpecificKeyframeGroups()) {
PropertyHandle property = keyframe_group.key;
if (!property.IsCSSProperty())
continue;
for (const auto& keyframe : keyframe_group.value->Keyframes()) {
if (keyframe->IsNeutral()) {
exception_state.ThrowDOMException(
kNotSupportedError, "Partial keyframes are not supported.");
return nullptr;
}
if (keyframe->Composite() != EffectModel::kCompositeReplace) {
exception_state.ThrowDOMException(
kNotSupportedError, "Additive animations are not supported.");
return nullptr;
}
}
}
}
DCHECK(!exception_state.HadException());
return keyframe_effect_model;
}
bool ExhaustDictionaryIterator(DictionaryIterator& iterator,
ExecutionContext* execution_context,
ExceptionState& exception_state,
Vector<Dictionary>& result) {
while (iterator.Next(execution_context, exception_state)) {
Dictionary dictionary;
if (!iterator.ValueAsDictionary(dictionary, exception_state)) {
exception_state.ThrowTypeError("Keyframes must be objects.");
return false;
}
result.push_back(dictionary);
}
return !exception_state.HadException();
}
} // namespace
// Spec: http://w3c.github.io/web-animations/#processing-a-keyframes-argument
EffectModel* EffectInput::Convert(
Element* element,
const DictionarySequenceOrDictionary& effect_input,
ExecutionContext* execution_context,
ExceptionState& exception_state) {
// TODO(crbug.com/772014): The element is allowed to be null; remove check.
if (effect_input.IsNull() || !element)
return nullptr;
if (effect_input.IsDictionarySequence()) {
return ConvertArrayForm(*element, effect_input.GetAsDictionarySequence(),
execution_context, exception_state);
}
const Dictionary& dictionary = effect_input.GetAsDictionary();
DictionaryIterator iterator = dictionary.GetIterator(execution_context);
if (!iterator.IsNull()) {
// TODO(alancutter): Convert keyframes during iteration rather than after to
// match spec.
Vector<Dictionary> keyframe_dictionaries;
if (ExhaustDictionaryIterator(iterator, execution_context, exception_state,
keyframe_dictionaries)) {
return ConvertArrayForm(*element, keyframe_dictionaries,
execution_context, exception_state);
}
return nullptr;
}
return ConvertObjectForm(*element, dictionary, execution_context,
exception_state);
}
EffectModel* EffectInput::ConvertArrayForm(
Element& element,
const Vector<Dictionary>& keyframe_dictionaries,
ExecutionContext* execution_context,
ExceptionState& exception_state) {
StringKeyframeVector keyframes;
double last_offset = 0;
for (const Dictionary& keyframe_dictionary : keyframe_dictionaries) {
scoped_refptr<StringKeyframe> keyframe = StringKeyframe::Create();
Nullable<double> offset;
if (DictionaryHelper::Get(keyframe_dictionary, "offset", offset) &&
!offset.IsNull()) {
if (!CheckOffset(offset.Get(), last_offset, exception_state))
return nullptr;
last_offset = offset.Get();
keyframe->SetOffset(offset.Get());
}
String composite_string;
DictionaryHelper::Get(keyframe_dictionary, "composite", composite_string);
if (composite_string == "add")
keyframe->SetComposite(EffectModel::kCompositeAdd);
// TODO(alancutter): Support "accumulate" keyframe composition.
String timing_function_string;
if (DictionaryHelper::Get(keyframe_dictionary, "easing",
timing_function_string)) {
scoped_refptr<TimingFunction> timing_function =
AnimationInputHelpers::ParseTimingFunction(
timing_function_string, &element.GetDocument(), exception_state);
if (!timing_function)
return nullptr;
keyframe->SetEasing(timing_function);
}
const Vector<String>& keyframe_properties =
keyframe_dictionary.GetPropertyNames(exception_state);
if (exception_state.HadException())
return nullptr;
for (const auto& property : keyframe_properties) {
if (property == "offset" || property == "composite" ||
property == "easing") {
continue;
}
Vector<String> values;
if (DictionaryHelper::Get(keyframe_dictionary, property, values)) {
exception_state.ThrowTypeError(
"Lists of values not permitted in array-form list of keyframes");
return nullptr;
}
String value;
DictionaryHelper::Get(keyframe_dictionary, property, value);
SetKeyframeValue(element, *keyframe.get(), property, value,
execution_context);
}
keyframes.push_back(keyframe);
}
DCHECK(!exception_state.HadException());
return CreateEffectModelFromKeyframes(element, keyframes, exception_state);
}
static bool GetPropertyIndexedKeyframeValues(
const Dictionary& keyframe_dictionary,
const String& property,
ExecutionContext* execution_context,
ExceptionState& exception_state,
Vector<String>& result) {
DCHECK(result.IsEmpty());
// Array of strings.
if (DictionaryHelper::Get(keyframe_dictionary, property, result))
return true;
Dictionary values_dictionary;
if (!keyframe_dictionary.Get(property, values_dictionary) ||
values_dictionary.IsUndefinedOrNull()) {
// Non-object.
String value;
DictionaryHelper::Get(keyframe_dictionary, property, value);
result.push_back(value);
return true;
}
DictionaryIterator iterator =
values_dictionary.GetIterator(execution_context);
if (iterator.IsNull()) {
// Non-iterable object.
String value;
DictionaryHelper::Get(keyframe_dictionary, property, value);
result.push_back(value);
return true;
}
// Iterable object.
while (iterator.Next(execution_context, exception_state)) {
String value;
if (!iterator.ValueAsString(value)) {
exception_state.ThrowTypeError(
"Unable to read keyframe value as string.");
return false;
}
result.push_back(value);
}
return !exception_state.HadException();
}
EffectModel* EffectInput::ConvertObjectForm(
Element& element,
const Dictionary& keyframe_dictionary,
ExecutionContext* execution_context,
ExceptionState& exception_state) {
StringKeyframeVector keyframes;
String timing_function_string;
scoped_refptr<TimingFunction> timing_function = nullptr;
if (DictionaryHelper::Get(keyframe_dictionary, "easing",
timing_function_string)) {
timing_function = AnimationInputHelpers::ParseTimingFunction(
timing_function_string, &element.GetDocument(), exception_state);
if (!timing_function)
return nullptr;
}
Nullable<double> offset;
if (DictionaryHelper::Get(keyframe_dictionary, "offset", offset) &&
!offset.IsNull()) {
if (!CheckOffset(offset.Get(), 0.0, exception_state))
return nullptr;
}
String composite_string;
DictionaryHelper::Get(keyframe_dictionary, "composite", composite_string);
const Vector<String>& keyframe_properties =
keyframe_dictionary.GetPropertyNames(exception_state);
if (exception_state.HadException())
return nullptr;
for (const auto& property : keyframe_properties) {
if (property == "offset" || property == "composite" ||
property == "easing") {
continue;
}
Vector<String> values;
if (!GetPropertyIndexedKeyframeValues(keyframe_dictionary, property,
execution_context, exception_state,
values))
return nullptr;
size_t num_keyframes = values.size();
for (size_t i = 0; i < num_keyframes; ++i) {
scoped_refptr<StringKeyframe> keyframe = StringKeyframe::Create();
if (!offset.IsNull())
keyframe->SetOffset(offset.Get());
else if (num_keyframes == 1)
keyframe->SetOffset(1.0);
else
keyframe->SetOffset(i / (num_keyframes - 1.0));
if (timing_function)
keyframe->SetEasing(timing_function);
if (composite_string == "add")
keyframe->SetComposite(EffectModel::kCompositeAdd);
// TODO(alancutter): Support "accumulate" keyframe composition.
SetKeyframeValue(element, *keyframe.get(), property, values[i],
execution_context);
keyframes.push_back(keyframe);
}
}
std::sort(keyframes.begin(), keyframes.end(), Keyframe::CompareOffsets);
DCHECK(!exception_state.HadException());
return CreateEffectModelFromKeyframes(element, keyframes, exception_state);
}
} // namespace blink