blob: c2e3d0c72476340d86faa61795599bc40be34f7d [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/effect_input.h"
#include "third_party/blink/renderer/bindings/core/v8/array_value.h"
#include "third_party/blink/renderer/bindings/core/v8/dictionary.h"
#include "third_party/blink/renderer/bindings/core/v8/idl_types.h"
#include "third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h"
#include "third_party/blink/renderer/bindings/core/v8/script_iterator.h"
#include "third_party/blink/renderer/bindings/core/v8/string_or_string_sequence.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_base_keyframe.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_base_property_indexed_keyframe.h"
#include "third_party/blink/renderer/core/animation/animation_input_helpers.h"
#include "third_party/blink/renderer/core/animation/base_keyframe.h"
#include "third_party/blink/renderer/core/animation/base_property_indexed_keyframe.h"
#include "third_party/blink/renderer/core/animation/compositor_animations.h"
#include "third_party/blink/renderer/core/animation/css/css_animations.h"
#include "third_party/blink/renderer/core/animation/keyframe_effect_model.h"
#include "third_party/blink/renderer/core/animation/string_keyframe.h"
#include "third_party/blink/renderer/core/css/css_style_sheet.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/frame/frame_console.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/platform/wtf/ascii_ctype.h"
#include "third_party/blink/renderer/platform/wtf/hash_set.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "v8/include/v8.h"
namespace blink {
namespace {
// Converts the composite property of a BasePropertyIndexedKeyframe into a
// vector of base::Optional<EffectModel::CompositeOperation> enums.
Vector<base::Optional<EffectModel::CompositeOperation>> ParseCompositeProperty(
const BasePropertyIndexedKeyframe* keyframe) {
const CompositeOperationOrAutoOrCompositeOperationOrAutoSequence& composite =
keyframe->composite();
if (composite.IsCompositeOperationOrAuto()) {
return {EffectModel::StringToCompositeOperation(
composite.GetAsCompositeOperationOrAuto())};
}
Vector<base::Optional<EffectModel::CompositeOperation>> result;
for (const String& composite_operation_string :
composite.GetAsCompositeOperationOrAutoSequence()) {
result.push_back(
EffectModel::StringToCompositeOperation(composite_operation_string));
}
return result;
}
void SetKeyframeValue(Element* element,
Document& document,
StringKeyframe& keyframe,
const String& property,
const String& value,
ExecutionContext* execution_context) {
StyleSheetContents* style_sheet_contents = document.ElementSheet().Contents();
CSSPropertyID css_property =
AnimationInputHelpers::KeyframeAttributeToCSSProperty(property, document);
if (css_property != CSSPropertyInvalid) {
MutableCSSPropertyValueSet::SetResult set_result =
css_property == CSSPropertyVariable
? keyframe.SetCSSPropertyValue(
AtomicString(property), document.GetPropertyRegistry(), value,
document.GetSecureContextMode(), style_sheet_contents)
: keyframe.SetCSSPropertyValue(css_property, value,
document.GetSecureContextMode(),
style_sheet_contents);
if (!set_result.did_parse && 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(
CSSProperty::Get(css_property), value, document.GetSecureContextMode(),
style_sheet_contents);
return;
}
const QualifiedName* svg_attribute =
AnimationInputHelpers::KeyframeAttributeToSVGAttribute(property, element);
if (svg_attribute)
keyframe.SetSVGAttributeValue(*svg_attribute, value);
}
bool ValidatePartialKeyframes(const StringKeyframeVector& keyframes) {
// CSSAdditiveAnimationsEnabled guards both additive animations and allowing
// partial (implicit) keyframes.
if (RuntimeEnabledFeatures::CSSAdditiveAnimationsEnabled())
return true;
// An implicit keyframe is inserted in the below cases. Note that the 'first'
// keyframe is actually all keyframes with offset 0.0, and the 'last' keyframe
// is actually all keyframes with offset 1.0.
//
// 1. A given property is present somewhere in the full set of keyframes,
// but is either not present in the first keyframe (requiring an implicit
// start value for that property) or last keyframe (requiring an implicit
// end value for that property).
//
// 2. There is no first keyframe (requiring an implicit start keyframe), or
// no last keyframe (requiring an implicit end keyframe).
//
// We only care about CSS properties here; animating SVG elements is protected
// by a different runtime flag.
Vector<double> computed_offsets =
KeyframeEffectModelBase::GetComputedOffsets(keyframes);
PropertyHandleSet properties_with_offset_0;
PropertyHandleSet properties_with_offset_1;
for (wtf_size_t i = 0; i < keyframes.size(); i++) {
for (const PropertyHandle& property : keyframes[i]->Properties()) {
if (!property.IsCSSProperty())
continue;
if (computed_offsets[i] == 0.0) {
properties_with_offset_0.insert(property);
} else {
if (!properties_with_offset_0.Contains(property))
return false;
if (computed_offsets[i] == 1.0) {
properties_with_offset_1.insert(property);
}
}
}
}
// At this point we have compared all keyframes with offset > 0 against the
// properties contained in the first keyframe, and found that they match. Now
// we just need to make sure that there aren't any properties in the first
// keyframe that aren't in the last keyframe.
return properties_with_offset_0.size() == properties_with_offset_1.size();
}
// Ensures that a CompositeOperation is of an allowed value for a given
// StringKeyframe and the current runtime flags.
EffectModel::CompositeOperation ResolveCompositeOperationForKeyframe(
EffectModel::CompositeOperation composite,
StringKeyframe* keyframe) {
if (!RuntimeEnabledFeatures::CSSAdditiveAnimationsEnabled() &&
keyframe->HasCssProperty() && composite == EffectModel::kCompositeAdd) {
return EffectModel::kCompositeReplace;
}
return composite;
}
bool IsAnimatableKeyframeAttribute(const String& property,
Element* element,
const Document& document) {
CSSPropertyID css_property =
AnimationInputHelpers::KeyframeAttributeToCSSProperty(property, document);
if (css_property != CSSPropertyInvalid) {
return !CSSAnimations::IsAnimationAffectingProperty(
CSSProperty::Get(css_property));
}
css_property =
AnimationInputHelpers::KeyframeAttributeToPresentationAttribute(property,
element);
if (css_property != CSSPropertyInvalid)
return true;
return !!AnimationInputHelpers::KeyframeAttributeToSVGAttribute(property,
element);
}
void AddPropertyValuePairsForKeyframe(
v8::Isolate* isolate,
v8::Local<v8::Object> keyframe_obj,
Element* element,
const Document& document,
Vector<std::pair<String, String>>& property_value_pairs,
ExceptionState& exception_state) {
Vector<String> keyframe_properties =
GetOwnPropertyNames(isolate, keyframe_obj, exception_state);
if (exception_state.HadException())
return;
// By spec, we must sort the properties in "ascending order by the Unicode
// codepoints that define each property name."
std::sort(keyframe_properties.begin(), keyframe_properties.end(),
WTF::CodePointCompareLessThan);
v8::TryCatch try_catch(isolate);
for (const auto& property : keyframe_properties) {
if (property == "offset" || property == "composite" ||
property == "easing") {
continue;
}
// By spec, we are not allowed to access any non-animatable property.
if (!IsAnimatableKeyframeAttribute(property, element, document))
continue;
// By spec, we are only allowed to access a given (property, value) pair
// once. This is observable by the web client, so we take care to adhere
// to that.
v8::Local<v8::Value> v8_value;
if (!keyframe_obj
->Get(isolate->GetCurrentContext(), V8String(isolate, property))
.ToLocal(&v8_value)) {
exception_state.RethrowV8Exception(try_catch.Exception());
return;
}
if (v8_value->IsArray()) {
// Since allow-lists is false, array values should be ignored.
continue;
}
String string_value = NativeValueTraits<IDLString>::NativeValue(
isolate, v8_value, exception_state);
if (exception_state.HadException())
return;
property_value_pairs.push_back(std::make_pair(property, string_value));
}
}
StringKeyframeVector ConvertArrayForm(Element* element,
Document& document,
const v8::Local<v8::Object>& iterator_obj,
ScriptState* script_state,
ExceptionState& exception_state) {
v8::Isolate* isolate = script_state->GetIsolate();
ScriptIterator iterator(iterator_obj, isolate);
// This loop captures step 5 of the procedure to process a keyframes argument,
// in the case where the argument is iterable.
HeapVector<Member<const BaseKeyframe>> processed_base_keyframes;
Vector<Vector<std::pair<String, String>>> processed_properties;
ExecutionContext* execution_context = ExecutionContext::From(script_state);
while (iterator.Next(execution_context, exception_state)) {
if (exception_state.HadException())
return {};
// The value should already be non-empty, as guaranteed by the call to Next
// and the exception_state check above.
v8::Local<v8::Value> keyframe = iterator.GetValue().ToLocalChecked();
if (!keyframe->IsObject() && !keyframe->IsNullOrUndefined()) {
exception_state.ThrowTypeError(
"Keyframes must be objects, or null or undefined");
return {};
}
BaseKeyframe* base_keyframe = NativeValueTraits<BaseKeyframe>::NativeValue(
isolate, keyframe, exception_state);
Vector<std::pair<String, String>> property_value_pairs;
if (exception_state.HadException())
return {};
if (!keyframe->IsNullOrUndefined()) {
AddPropertyValuePairsForKeyframe(
isolate, v8::Local<v8::Object>::Cast(keyframe), element, document,
property_value_pairs, exception_state);
if (exception_state.HadException())
return {};
}
processed_base_keyframes.push_back(base_keyframe);
processed_properties.push_back(property_value_pairs);
}
// If the very first call to next() throws the above loop will never be
// entered, so we have to catch that here.
if (exception_state.HadException())
return {};
// 6. If processed keyframes is not loosely sorted by offset, throw a
// TypeError and abort these steps.
double previous_offset = -std::numeric_limits<double>::infinity();
const wtf_size_t num_processed_keyframes = processed_base_keyframes.size();
for (wtf_size_t i = 0; i < num_processed_keyframes; ++i) {
if (!processed_base_keyframes[i]->hasOffset())
continue;
double offset = processed_base_keyframes[i]->offset();
if (offset < previous_offset) {
exception_state.ThrowTypeError(
"Offsets must be montonically non-decreasing.");
return {};
}
previous_offset = offset;
}
// 7. If there exist any keyframe in processed keyframes whose keyframe
// offset is non-null and less than zero or greater than one, throw a
// TypeError and abort these steps.
for (wtf_size_t i = 0; i < num_processed_keyframes; ++i) {
if (!processed_base_keyframes[i]->hasOffset())
continue;
double offset = processed_base_keyframes[i]->offset();
if (offset < 0 || offset > 1) {
exception_state.ThrowTypeError(
"Offsets must be null or in the range [0,1].");
return {};
}
}
StringKeyframeVector keyframes;
for (wtf_size_t i = 0; i < num_processed_keyframes; ++i) {
// Now we create the actual Keyframe object. We start by assigning the
// offset and composite values; conceptually these were actually added in
// step 5 above but we didn't have a keyframe object then.
const BaseKeyframe* base_keyframe = processed_base_keyframes[i];
StringKeyframe* keyframe = StringKeyframe::Create();
if (base_keyframe->hasOffset()) {
keyframe->SetOffset(base_keyframe->offset());
}
// 8.1. For each property-value pair in frame, parse the property value
// using the syntax specified for that property.
for (const auto& pair : processed_properties[i]) {
// TODO(crbug.com/777971): Make parsing of property values spec-compliant.
SetKeyframeValue(element, document, *keyframe, pair.first, pair.second,
execution_context);
}
base::Optional<EffectModel::CompositeOperation> composite =
EffectModel::StringToCompositeOperation(base_keyframe->composite());
if (composite) {
keyframe->SetComposite(
ResolveCompositeOperationForKeyframe(composite.value(), keyframe));
}
// 8.2. Let the timing function of frame be the result of parsing the
// “easing” property on frame using the CSS syntax defined for the easing
// property of the AnimationEffectTimingReadOnly interface.
//
// If parsing the “easing” property fails, throw a TypeError and abort this
// procedure.
scoped_refptr<TimingFunction> timing_function =
AnimationInputHelpers::ParseTimingFunction(base_keyframe->easing(),
&document, exception_state);
if (!timing_function)
return {};
keyframe->SetEasing(timing_function);
keyframes.push_back(keyframe);
}
DCHECK(!exception_state.HadException());
return keyframes;
}
// Extracts the values for a given property in the input keyframes. As per the
// spec property values for the object-notation form have type (DOMString or
// sequence<DOMString>).
bool GetPropertyIndexedKeyframeValues(const v8::Local<v8::Object>& keyframe,
const String& property,
ScriptState* script_state,
ExceptionState& exception_state,
Vector<String>& result) {
DCHECK(result.IsEmpty());
// By spec, we are only allowed to access a given (property, value) pair once.
// This is observable by the web client, so we take care to adhere to that.
v8::Local<v8::Value> v8_value;
v8::TryCatch try_catch(script_state->GetIsolate());
v8::Local<v8::Context> context = script_state->GetContext();
v8::Isolate* isolate = script_state->GetIsolate();
if (!keyframe->Get(context, V8String(isolate, property)).ToLocal(&v8_value)) {
exception_state.RethrowV8Exception(try_catch.Exception());
return {};
}
StringOrStringSequence string_or_string_sequence;
V8StringOrStringSequence::ToImpl(
script_state->GetIsolate(), v8_value, string_or_string_sequence,
UnionTypeConversionMode::kNotNullable, exception_state);
if (exception_state.HadException())
return false;
if (string_or_string_sequence.IsString())
result.push_back(string_or_string_sequence.GetAsString());
else
result = string_or_string_sequence.GetAsStringSequence();
return true;
}
// Implements the procedure to "process a keyframes argument" from the
// web-animations spec for an object form keyframes argument.
//
// See https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
StringKeyframeVector ConvertObjectForm(Element* element,
Document& document,
const v8::Local<v8::Object>& keyframe,
ScriptState* script_state,
ExceptionState& exception_state) {
// We implement much of this procedure out of order from the way the spec is
// written, to avoid repeatedly going over the list of keyframes.
// The web-observable behavior should be the same as the spec.
// Extract the offset, easing, and composite as per step 1 of the 'procedure
// to process a keyframe-like object'.
BasePropertyIndexedKeyframe* property_indexed_keyframe =
NativeValueTraits<BasePropertyIndexedKeyframe>::NativeValue(
script_state->GetIsolate(), keyframe, exception_state);
if (exception_state.HadException())
return {};
Vector<base::Optional<double>> offsets;
if (property_indexed_keyframe->offset().IsNull())
offsets.push_back(base::nullopt);
else if (property_indexed_keyframe->offset().IsDouble())
offsets.push_back(property_indexed_keyframe->offset().GetAsDouble());
else
offsets = property_indexed_keyframe->offset().GetAsDoubleOrNullSequence();
// The web-animations spec explicitly states that easings should be kept as
// DOMStrings here and not parsed into timing functions until later.
Vector<String> easings;
if (property_indexed_keyframe->easing().IsString())
easings.push_back(property_indexed_keyframe->easing().GetAsString());
else
easings = property_indexed_keyframe->easing().GetAsStringSequence();
Vector<base::Optional<EffectModel::CompositeOperation>> composite_operations =
ParseCompositeProperty(property_indexed_keyframe);
// Next extract all animatable properties from the input argument and iterate
// through them, processing each as a list of values for that property. This
// implements both steps 2-7 of the 'procedure to process a keyframe-like
// object' and step 5.2 of the 'procedure to process a keyframes argument'.
Vector<String> keyframe_properties = GetOwnPropertyNames(
script_state->GetIsolate(), keyframe, exception_state);
if (exception_state.HadException())
return {};
// Steps 5.2 - 5.4 state that the user agent is to:
//
// * Create sets of 'property keyframes' with no offset.
// * Calculate computed offsets for each set of keyframes individually.
// * Join the sets together and merge those with identical computed offsets.
//
// This is equivalent to just keeping a hashmap from computed offset to a
// single keyframe, which simplifies the parsing logic.
HeapHashMap<double, Member<StringKeyframe>> keyframes;
// By spec, we must sort the properties in "ascending order by the Unicode
// codepoints that define each property name."
std::sort(keyframe_properties.begin(), keyframe_properties.end(),
WTF::CodePointCompareLessThan);
for (const auto& property : keyframe_properties) {
if (property == "offset" || property == "composite" ||
property == "easing") {
continue;
}
// By spec, we are not allowed to access any non-animatable property.
if (!IsAnimatableKeyframeAttribute(property, element, document))
continue;
Vector<String> values;
if (!GetPropertyIndexedKeyframeValues(keyframe, property, script_state,
exception_state, values)) {
return {};
}
// Now create a keyframe (or retrieve and augment an existing one) for each
// value this property maps to. As explained above, this loop performs both
// the initial creation and merging mentioned in the spec.
wtf_size_t num_keyframes = values.size();
ExecutionContext* execution_context = ExecutionContext::From(script_state);
for (wtf_size_t i = 0; i < num_keyframes; ++i) {
// As all offsets are null for these 'property keyframes', the computed
// offset is just the fractional position of each keyframe in the array.
//
// The only special case is that when there is only one keyframe the sole
// computed offset is defined as 1.
double computed_offset =
(num_keyframes == 1) ? 1 : i / double(num_keyframes - 1);
auto result = keyframes.insert(computed_offset, nullptr);
if (result.is_new_entry)
result.stored_value->value = StringKeyframe::Create();
SetKeyframeValue(element, document, *result.stored_value->value, property,
values[i], execution_context);
}
}
// 5.3 Sort processed keyframes by the computed keyframe offset of each
// keyframe in increasing order.
Vector<double> keys;
for (const auto& key : keyframes.Keys())
keys.push_back(key);
std::sort(keys.begin(), keys.end());
// Steps 5.5 - 5.12 deal with assigning the user-specified offset, easing, and
// composite properties to the keyframes.
//
// This loop also implements steps 6, 7, and 8 of the spec. Because nothing is
// user-observable at this point, we can operate out of order. Note that this
// may result in us throwing a different order of TypeErrors than other user
// agents[1], but as all exceptions are TypeErrors this is not observable by
// the web client.
//
// [1] E.g. if the offsets are [2, 0] we will throw due to the first offset
// being > 1 before we throw due to the offsets not being loosely ordered.
StringKeyframeVector results;
double previous_offset = 0.0;
for (wtf_size_t i = 0; i < keys.size(); i++) {
auto* keyframe = keyframes.at(keys[i]);
if (i < offsets.size()) {
base::Optional<double> offset = offsets[i];
// 6. If processed keyframes is not loosely sorted by offset, throw a
// TypeError and abort these steps.
if (offset.has_value()) {
if (offset.value() < previous_offset) {
exception_state.ThrowTypeError(
"Offsets must be montonically non-decreasing.");
return {};
}
previous_offset = offset.value();
}
// 7. If there exist any keyframe in processed keyframes whose keyframe
// offset is non-null and less than zero or greater than one, throw a
// TypeError and abort these steps.
if (offset.has_value() && (offset.value() < 0 || offset.value() > 1)) {
exception_state.ThrowTypeError(
"Offsets must be null or in the range [0,1].");
return {};
}
keyframe->SetOffset(offset);
}
// At this point in the code we have read all the properties we will read
// from the input object, so it is safe to parse the easing strings. See the
// note on step 8.2.
if (!easings.IsEmpty()) {
// 5.9 If easings has fewer items than property keyframes, repeat the
// elements in easings successively starting from the beginning of the
// list until easings has as many items as property keyframes.
const String& easing = easings[i % easings.size()];
// 8.2 Let the timing function of frame be the result of parsing the
// "easing" property on frame using the CSS syntax defined for the easing
// property of the AnimationEffectTimingReadOnly interface.
//
// If parsing the “easing” property fails, throw a TypeError and abort
// this procedure.
scoped_refptr<TimingFunction> timing_function =
AnimationInputHelpers::ParseTimingFunction(easing, &document,
exception_state);
if (!timing_function)
return {};
keyframe->SetEasing(timing_function);
}
if (!composite_operations.IsEmpty()) {
// 5.12.2 As with easings, if composite modes has fewer items than
// property keyframes, repeat the elements in composite modes successively
// starting from the beginning of the list until composite modes has as
// many items as property keyframes.
base::Optional<EffectModel::CompositeOperation> composite =
composite_operations[i % composite_operations.size()];
if (composite) {
keyframe->SetComposite(
ResolveCompositeOperationForKeyframe(composite.value(), keyframe));
}
}
results.push_back(keyframe);
}
// Step 8 of the spec is done above (or will be): parsing property values
// according to syntax for the property (discarding with console warning on
// fail) and parsing each easing property.
// TODO(crbug.com/777971): Fix parsing of property values to adhere to spec.
// 9. Parse each of the values in unused easings using the CSS syntax defined
// for easing property of the AnimationEffectTimingReadOnly interface, and if
// any of the values fail to parse, throw a TypeError and abort this
// procedure.
for (wtf_size_t i = results.size(); i < easings.size(); i++) {
scoped_refptr<TimingFunction> timing_function =
AnimationInputHelpers::ParseTimingFunction(easings[i], &document,
exception_state);
if (!timing_function)
return {};
}
DCHECK(!exception_state.HadException());
return results;
}
bool HasAdditiveCompositeCSSKeyframe(
const KeyframeEffectModelBase::KeyframeGroupMap& keyframe_groups) {
for (const auto& keyframe_group : keyframe_groups) {
PropertyHandle property = keyframe_group.key;
if (!property.IsCSSProperty())
continue;
for (const auto& keyframe : keyframe_group.value->Keyframes()) {
if (keyframe->Composite() == EffectModel::kCompositeAdd)
return true;
}
}
return false;
}
} // namespace
KeyframeEffectModelBase* EffectInput::Convert(
Element* element,
const ScriptValue& keyframes,
EffectModel::CompositeOperation composite,
ScriptState* script_state,
ExceptionState& exception_state) {
StringKeyframeVector parsed_keyframes =
ParseKeyframesArgument(element, keyframes, script_state, exception_state);
if (exception_state.HadException())
return nullptr;
composite = ResolveCompositeOperation(composite, parsed_keyframes);
StringKeyframeEffectModel* keyframe_effect_model =
StringKeyframeEffectModel::Create(parsed_keyframes, composite,
LinearTimingFunction::Shared());
if (!RuntimeEnabledFeatures::CSSAdditiveAnimationsEnabled()) {
// This should be enforced by the parsing code.
DCHECK(!HasAdditiveCompositeCSSKeyframe(
keyframe_effect_model->GetPropertySpecificKeyframeGroups()));
}
DCHECK(!exception_state.HadException());
return keyframe_effect_model;
}
StringKeyframeVector EffectInput::ParseKeyframesArgument(
Element* element,
const ScriptValue& keyframes,
ScriptState* script_state,
ExceptionState& exception_state) {
// Per the spec, a null keyframes object maps to a valid but empty sequence.
v8::Local<v8::Value> keyframes_value = keyframes.V8Value();
if (keyframes_value->IsNullOrUndefined())
return {};
v8::Local<v8::Object> keyframes_obj = keyframes_value.As<v8::Object>();
// 3. Let method be the result of GetMethod(object, @@iterator).
v8::Isolate* isolate = script_state->GetIsolate();
v8::Local<v8::Function> iterator_method =
GetEsIteratorMethod(isolate, keyframes_obj, exception_state);
if (exception_state.HadException())
return {};
// TODO(crbug.com/816934): Get spec to specify what parsing context to use.
Document& document =
element ? element->GetDocument()
: *To<Document>(ExecutionContext::From(script_state));
StringKeyframeVector parsed_keyframes;
if (iterator_method.IsEmpty()) {
parsed_keyframes = ConvertObjectForm(element, document, keyframes_obj,
script_state, exception_state);
} else {
v8::Local<v8::Object> iterator = GetEsIteratorWithMethod(
isolate, iterator_method, keyframes_obj, exception_state);
if (exception_state.HadException())
return {};
parsed_keyframes = ConvertArrayForm(element, document, iterator,
script_state, exception_state);
}
if (!ValidatePartialKeyframes(parsed_keyframes)) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Partial keyframes are not supported.");
return {};
}
return parsed_keyframes;
}
EffectModel::CompositeOperation EffectInput::ResolveCompositeOperation(
EffectModel::CompositeOperation composite,
const StringKeyframeVector& keyframes) {
EffectModel::CompositeOperation result = composite;
for (const Member<StringKeyframe>& keyframe : keyframes) {
// Replace is always supported, so we can early-exit if and when we have
// that as our composite value.
if (result == EffectModel::kCompositeReplace)
break;
result = ResolveCompositeOperationForKeyframe(result, keyframe);
}
return result;
}
} // namespace blink