summaryrefslogtreecommitdiff
path: root/chromium/third_party/blink/renderer/core/animation/effect_input.cc
blob: c2e3d0c72476340d86faa61795599bc40be34f7d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
/*
 * 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