Coverage Report

Created: 2018-09-25 14:53

/src/mozilla-central/dom/animation/KeyframeUtils.cpp
Line
Count
Source (jump to first uncovered line)
1
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
3
/* This Source Code Form is subject to the terms of the Mozilla Public
4
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5
 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7
#include "mozilla/KeyframeUtils.h"
8
9
#include "mozilla/ComputedStyle.h"
10
#include "mozilla/ErrorResult.h"
11
#include "mozilla/Move.h"
12
#include "mozilla/RangedArray.h"
13
#include "mozilla/ServoBindings.h"
14
#include "mozilla/ServoBindingTypes.h"
15
#include "mozilla/ServoCSSParser.h"
16
#include "mozilla/StyleAnimationValue.h"
17
#include "mozilla/StaticPrefs.h"
18
#include "mozilla/TimingParams.h"
19
#include "mozilla/dom/BaseKeyframeTypesBinding.h" // For FastBaseKeyframe etc.
20
#include "mozilla/dom/Element.h"
21
#include "mozilla/dom/KeyframeEffectBinding.h"
22
#include "mozilla/dom/KeyframeEffect.h" // For PropertyValuesPair etc.
23
#include "mozilla/dom/Nullable.h"
24
#include "jsapi.h" // For ForOfIterator etc.
25
#include "nsClassHashtable.h"
26
#include "nsContentUtils.h" // For GetContextForContent
27
#include "nsCSSPropertyIDSet.h"
28
#include "nsCSSProps.h"
29
#include "nsCSSPseudoElements.h" // For CSSPseudoElementType
30
#include "nsDocument.h" // For nsDocument::AreWebAnimationsImplicitKeyframesEnabled
31
#include "nsIScriptError.h"
32
#include "nsTArray.h"
33
#include <algorithm> // For std::stable_sort, std::min
34
35
using mozilla::dom::Nullable;
36
37
namespace mozilla {
38
39
// ------------------------------------------------------------------
40
//
41
// Internal data types
42
//
43
// ------------------------------------------------------------------
44
45
// For the aAllowList parameter of AppendStringOrStringSequence and
46
// GetPropertyValuesPairs.
47
enum class ListAllowance { eDisallow, eAllow };
48
49
/**
50
 * A property-values pair obtained from the open-ended properties
51
 * discovered on a regular keyframe or property-indexed keyframe object.
52
 *
53
 * Single values (as required by a regular keyframe, and as also supported
54
 * on property-indexed keyframes) are stored as the only element in
55
 * mValues.
56
 */
57
struct PropertyValuesPair
58
{
59
  nsCSSPropertyID mProperty;
60
  nsTArray<nsString> mValues;
61
};
62
63
/**
64
 * An additional property (for a property-values pair) found on a
65
 * BaseKeyframe or BasePropertyIndexedKeyframe object.
66
 */
67
struct AdditionalProperty
68
{
69
  nsCSSPropertyID mProperty;
70
  size_t mJsidIndex;        // Index into |ids| in GetPropertyValuesPairs.
71
72
  struct PropertyComparator
73
  {
74
    bool Equals(const AdditionalProperty& aLhs,
75
                const AdditionalProperty& aRhs) const
76
0
    {
77
0
      return aLhs.mProperty == aRhs.mProperty;
78
0
    }
79
    bool LessThan(const AdditionalProperty& aLhs,
80
                  const AdditionalProperty& aRhs) const
81
0
    {
82
0
      return nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) <
83
0
             nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
84
0
    }
85
  };
86
};
87
88
/**
89
 * Data for a segment in a keyframe animation of a given property
90
 * whose value is a StyleAnimationValue.
91
 *
92
 * KeyframeValueEntry is used in GetAnimationPropertiesFromKeyframes
93
 * to gather data for each individual segment.
94
 */
95
struct KeyframeValueEntry
96
{
97
  nsCSSPropertyID mProperty;
98
  AnimationValue mValue;
99
100
  float mOffset;
101
  Maybe<ComputedTimingFunction> mTimingFunction;
102
  dom::CompositeOperation mComposite;
103
104
  struct PropertyOffsetComparator
105
  {
106
    static bool Equals(const KeyframeValueEntry& aLhs,
107
                       const KeyframeValueEntry& aRhs)
108
0
    {
109
0
      return aLhs.mProperty == aRhs.mProperty &&
110
0
             aLhs.mOffset == aRhs.mOffset;
111
0
    }
112
    static bool LessThan(const KeyframeValueEntry& aLhs,
113
                         const KeyframeValueEntry& aRhs)
114
0
    {
115
0
      // First, sort by property IDL name.
116
0
      int32_t order = nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) -
117
0
                      nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
118
0
      if (order != 0) {
119
0
        return order < 0;
120
0
      }
121
0
122
0
      // Then, by offset.
123
0
      return aLhs.mOffset < aRhs.mOffset;
124
0
    }
125
  };
126
};
127
128
class ComputedOffsetComparator
129
{
130
public:
131
  static bool Equals(const Keyframe& aLhs, const Keyframe& aRhs)
132
0
  {
133
0
    return aLhs.mComputedOffset == aRhs.mComputedOffset;
134
0
  }
135
136
  static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs)
137
0
  {
138
0
    return aLhs.mComputedOffset < aRhs.mComputedOffset;
139
0
  }
140
};
141
142
// ------------------------------------------------------------------
143
//
144
// Internal helper method declarations
145
//
146
// ------------------------------------------------------------------
147
148
static void
149
GetKeyframeListFromKeyframeSequence(JSContext* aCx,
150
                                    nsIDocument* aDocument,
151
                                    JS::ForOfIterator& aIterator,
152
                                    nsTArray<Keyframe>& aResult,
153
                                    ErrorResult& aRv);
154
155
static bool
156
ConvertKeyframeSequence(JSContext* aCx,
157
                        nsIDocument* aDocument,
158
                        JS::ForOfIterator& aIterator,
159
                        nsTArray<Keyframe>& aResult);
160
161
static bool
162
GetPropertyValuesPairs(JSContext* aCx,
163
                       JS::Handle<JSObject*> aObject,
164
                       ListAllowance aAllowLists,
165
                       nsTArray<PropertyValuesPair>& aResult);
166
167
static bool
168
AppendStringOrStringSequenceToArray(JSContext* aCx,
169
                                    JS::Handle<JS::Value> aValue,
170
                                    ListAllowance aAllowLists,
171
                                    nsTArray<nsString>& aValues);
172
173
static bool
174
AppendValueAsString(JSContext* aCx,
175
                    nsTArray<nsString>& aValues,
176
                    JS::Handle<JS::Value> aValue);
177
178
static Maybe<PropertyValuePair>
179
MakePropertyValuePair(nsCSSPropertyID aProperty, const nsAString& aStringValue,
180
                      nsIDocument* aDocument);
181
182
static bool
183
HasValidOffsets(const nsTArray<Keyframe>& aKeyframes);
184
185
#ifdef DEBUG
186
static void
187
MarkAsComputeValuesFailureKey(PropertyValuePair& aPair);
188
189
#endif
190
191
192
static nsTArray<ComputedKeyframeValues>
193
GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
194
                          dom::Element* aElement,
195
                          const ComputedStyle* aComputedValues);
196
197
static void
198
BuildSegmentsFromValueEntries(nsTArray<KeyframeValueEntry>& aEntries,
199
                              nsTArray<AnimationProperty>& aResult);
200
201
static void
202
GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx,
203
                                           nsIDocument* aDocument,
204
                                           JS::Handle<JS::Value> aValue,
205
                                           nsTArray<Keyframe>& aResult,
206
                                           ErrorResult& aRv);
207
208
static bool
209
HasImplicitKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
210
                          nsIDocument* aDocument);
211
212
static void
213
DistributeRange(const Range<Keyframe>& aRange);
214
215
// ------------------------------------------------------------------
216
//
217
// Public API
218
//
219
// ------------------------------------------------------------------
220
221
/* static */ nsTArray<Keyframe>
222
KeyframeUtils::GetKeyframesFromObject(JSContext* aCx,
223
                                      nsIDocument* aDocument,
224
                                      JS::Handle<JSObject*> aFrames,
225
                                      ErrorResult& aRv)
226
0
{
227
0
  MOZ_ASSERT(!aRv.Failed());
228
0
229
0
  nsTArray<Keyframe> keyframes;
230
0
231
0
  if (!aFrames) {
232
0
    // The argument was explicitly null meaning no keyframes.
233
0
    return keyframes;
234
0
  }
235
0
236
0
  // At this point we know we have an object. We try to convert it to a
237
0
  // sequence of keyframes first, and if that fails due to not being iterable,
238
0
  // we try to convert it to a property-indexed keyframe.
239
0
  JS::Rooted<JS::Value> objectValue(aCx, JS::ObjectValue(*aFrames));
240
0
  JS::ForOfIterator iter(aCx);
241
0
  if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) {
242
0
    aRv.Throw(NS_ERROR_FAILURE);
243
0
    return keyframes;
244
0
  }
245
0
246
0
  if (iter.valueIsIterable()) {
247
0
    GetKeyframeListFromKeyframeSequence(aCx, aDocument, iter, keyframes, aRv);
248
0
  } else {
249
0
    GetKeyframeListFromPropertyIndexedKeyframe(aCx, aDocument, objectValue,
250
0
                                               keyframes, aRv);
251
0
  }
252
0
253
0
  if (aRv.Failed()) {
254
0
    MOZ_ASSERT(keyframes.IsEmpty(),
255
0
               "Should not set any keyframes when there is an error");
256
0
    return keyframes;
257
0
  }
258
0
259
0
  if (!nsDocument::AreWebAnimationsImplicitKeyframesEnabled(aCx, nullptr) &&
260
0
      HasImplicitKeyframeValues(keyframes, aDocument)) {
261
0
    keyframes.Clear();
262
0
    aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR);
263
0
  }
264
0
265
0
  return keyframes;
266
0
}
267
268
/* static */ void
269
KeyframeUtils::DistributeKeyframes(nsTArray<Keyframe>& aKeyframes)
270
0
{
271
0
  if (aKeyframes.IsEmpty()) {
272
0
    return;
273
0
  }
274
0
275
0
  // If the first keyframe has an unspecified offset, fill it in with 0%.
276
0
  // If there is only a single keyframe, then it gets 100%.
277
0
  if (aKeyframes.Length() > 1) {
278
0
    Keyframe& firstElement = aKeyframes[0];
279
0
    firstElement.mComputedOffset = firstElement.mOffset.valueOr(0.0);
280
0
    // We will fill in the last keyframe's offset below
281
0
  } else {
282
0
    Keyframe& lastElement = aKeyframes.LastElement();
283
0
    lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0);
284
0
  }
285
0
286
0
  // Fill in remaining missing offsets.
287
0
  const Keyframe* const last = &aKeyframes.LastElement();
288
0
  const RangedPtr<Keyframe> begin(aKeyframes.Elements(), aKeyframes.Length());
289
0
  RangedPtr<Keyframe> keyframeA = begin;
290
0
  while (keyframeA != last) {
291
0
    // Find keyframe A and keyframe B *between* which we will apply spacing.
292
0
    RangedPtr<Keyframe> keyframeB = keyframeA + 1;
293
0
    while (keyframeB->mOffset.isNothing() && keyframeB != last) {
294
0
      ++keyframeB;
295
0
    }
296
0
    keyframeB->mComputedOffset = keyframeB->mOffset.valueOr(1.0);
297
0
298
0
    // Fill computed offsets in (keyframe A, keyframe B).
299
0
    DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1));
300
0
    keyframeA = keyframeB;
301
0
  }
302
0
}
303
304
/* static */ nsTArray<AnimationProperty>
305
KeyframeUtils::GetAnimationPropertiesFromKeyframes(
306
  const nsTArray<Keyframe>& aKeyframes,
307
  dom::Element* aElement,
308
  const ComputedStyle* aStyle,
309
  dom::CompositeOperation aEffectComposite)
310
0
{
311
0
  nsTArray<AnimationProperty> result;
312
0
313
0
  const nsTArray<ComputedKeyframeValues> computedValues =
314
0
    GetComputedKeyframeValues(aKeyframes, aElement, aStyle);
315
0
  if (computedValues.IsEmpty()) {
316
0
    // In rare cases GetComputedKeyframeValues might fail and return an empty
317
0
    // array, in which case we likewise return an empty array from here.
318
0
    return result;
319
0
  }
320
0
321
0
  MOZ_ASSERT(aKeyframes.Length() == computedValues.Length(),
322
0
             "Array length mismatch");
323
0
324
0
  nsTArray<KeyframeValueEntry> entries(aKeyframes.Length());
325
0
326
0
  const size_t len = aKeyframes.Length();
327
0
  for (size_t i = 0; i < len; ++i) {
328
0
    const Keyframe& frame = aKeyframes[i];
329
0
    for (auto& value : computedValues[i]) {
330
0
      MOZ_ASSERT(frame.mComputedOffset != Keyframe::kComputedOffsetNotSet,
331
0
                 "Invalid computed offset");
332
0
      KeyframeValueEntry* entry = entries.AppendElement();
333
0
      entry->mOffset = frame.mComputedOffset;
334
0
      entry->mProperty = value.mProperty;
335
0
      entry->mValue = value.mValue;
336
0
      entry->mTimingFunction = frame.mTimingFunction;
337
0
      // The following assumes that CompositeOperation is a strict subset of
338
0
      // CompositeOperationOrAuto.
339
0
      entry->mComposite =
340
0
        frame.mComposite == dom::CompositeOperationOrAuto::Auto
341
0
        ? aEffectComposite
342
0
        : static_cast<dom::CompositeOperation>(frame.mComposite);
343
0
    }
344
0
  }
345
0
346
0
  BuildSegmentsFromValueEntries(entries, result);
347
0
  return result;
348
0
}
349
350
/* static */ bool
351
KeyframeUtils::IsAnimatableProperty(nsCSSPropertyID aProperty)
352
0
{
353
0
  // Regardless of the backend type, treat the 'display' property as not
354
0
  // animatable. (Servo will report it as being animatable, since it is
355
0
  // in fact animatable by SMIL.)
356
0
  if (aProperty == eCSSProperty_display) {
357
0
    return false;
358
0
  }
359
0
  return Servo_Property_IsAnimatable(aProperty);
360
0
}
361
362
// ------------------------------------------------------------------
363
//
364
// Internal helpers
365
//
366
// ------------------------------------------------------------------
367
368
/**
369
 * Converts a JS object to an IDL sequence<Keyframe>.
370
 *
371
 * @param aCx The JSContext corresponding to |aIterator|.
372
 * @param aDocument The document to use when parsing CSS properties.
373
 * @param aIterator An already-initialized ForOfIterator for the JS
374
 *   object to iterate over as a sequence.
375
 * @param aResult The array into which the resulting Keyframe objects will be
376
 *   appended.
377
 * @param aRv Out param to store any errors thrown by this function.
378
 */
379
static void
380
GetKeyframeListFromKeyframeSequence(JSContext* aCx,
381
                                    nsIDocument* aDocument,
382
                                    JS::ForOfIterator& aIterator,
383
                                    nsTArray<Keyframe>& aResult,
384
                                    ErrorResult& aRv)
385
0
{
386
0
  MOZ_ASSERT(!aRv.Failed());
387
0
  MOZ_ASSERT(aResult.IsEmpty());
388
0
389
0
  // Convert the object in aIterator to a sequence of keyframes producing
390
0
  // an array of Keyframe objects.
391
0
  if (!ConvertKeyframeSequence(aCx, aDocument, aIterator, aResult)) {
392
0
    aResult.Clear();
393
0
    aRv.Throw(NS_ERROR_FAILURE);
394
0
    return;
395
0
  }
396
0
397
0
  // If the sequence<> had zero elements, we won't generate any
398
0
  // keyframes.
399
0
  if (aResult.IsEmpty()) {
400
0
    return;
401
0
  }
402
0
403
0
  // Check that the keyframes are loosely sorted and with values all
404
0
  // between 0% and 100%.
405
0
  if (!HasValidOffsets(aResult)) {
406
0
    aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
407
0
    aResult.Clear();
408
0
    return;
409
0
  }
410
0
}
411
412
/**
413
 * Converts a JS object wrapped by the given JS::ForIfIterator to an
414
 * IDL sequence<Keyframe> and stores the resulting Keyframe objects in
415
 * aResult.
416
 */
417
static bool
418
ConvertKeyframeSequence(JSContext* aCx,
419
                        nsIDocument* aDocument,
420
                        JS::ForOfIterator& aIterator,
421
                        nsTArray<Keyframe>& aResult)
422
0
{
423
0
  JS::Rooted<JS::Value> value(aCx);
424
0
  ErrorResult parseEasingResult;
425
0
426
0
  for (;;) {
427
0
    bool done;
428
0
    if (!aIterator.next(&value, &done)) {
429
0
      return false;
430
0
    }
431
0
    if (done) {
432
0
      break;
433
0
    }
434
0
    // Each value found when iterating the object must be an object
435
0
    // or null/undefined (which gets treated as a default {} dictionary
436
0
    // value).
437
0
    if (!value.isObject() && !value.isNullOrUndefined()) {
438
0
      dom::ThrowErrorMessage(aCx, dom::MSG_NOT_OBJECT,
439
0
                             "Element of sequence<Keyframe> argument");
440
0
      return false;
441
0
    }
442
0
443
0
    // Convert the JS value into a BaseKeyframe dictionary value.
444
0
    dom::binding_detail::FastBaseKeyframe keyframeDict;
445
0
    if (!keyframeDict.Init(aCx, value,
446
0
                           "Element of sequence<Keyframe> argument")) {
447
0
      return false;
448
0
    }
449
0
450
0
    Keyframe* keyframe = aResult.AppendElement(fallible);
451
0
    if (!keyframe) {
452
0
      return false;
453
0
    }
454
0
    if (!keyframeDict.mOffset.IsNull()) {
455
0
      keyframe->mOffset.emplace(keyframeDict.mOffset.Value());
456
0
    }
457
0
458
0
    if (StaticPrefs::dom_animations_api_compositing_enabled()) {
459
0
      keyframe->mComposite = keyframeDict.mComposite;
460
0
    }
461
0
462
0
    // Look for additional property-values pairs on the object.
463
0
    nsTArray<PropertyValuesPair> propertyValuePairs;
464
0
    if (value.isObject()) {
465
0
      JS::Rooted<JSObject*> object(aCx, &value.toObject());
466
0
      if (!GetPropertyValuesPairs(aCx, object,
467
0
                                  ListAllowance::eDisallow,
468
0
                                  propertyValuePairs)) {
469
0
        return false;
470
0
      }
471
0
    }
472
0
473
0
    if (!parseEasingResult.Failed()) {
474
0
      keyframe->mTimingFunction =
475
0
        TimingParams::ParseEasing(keyframeDict.mEasing,
476
0
                                  aDocument,
477
0
                                  parseEasingResult);
478
0
      // Even if the above fails, we still need to continue reading off all the
479
0
      // properties since checking the validity of easing should be treated as
480
0
      // a separate step that happens *after* all the other processing in this
481
0
      // loop since (since it is never likely to be handled by WebIDL unlike the
482
0
      // rest of this loop).
483
0
    }
484
0
485
0
    for (PropertyValuesPair& pair : propertyValuePairs) {
486
0
      MOZ_ASSERT(pair.mValues.Length() == 1);
487
0
488
0
      Maybe<PropertyValuePair> valuePair =
489
0
        MakePropertyValuePair(pair.mProperty, pair.mValues[0], aDocument);
490
0
      if (!valuePair) {
491
0
        continue;
492
0
      }
493
0
      keyframe->mPropertyValues.AppendElement(std::move(valuePair.ref()));
494
0
495
#ifdef DEBUG
496
      // When we go to convert keyframes into arrays of property values we
497
      // call StyleAnimation::ComputeValues. This should normally return true
498
      // but in order to test the case where it does not, BaseKeyframeDict
499
      // includes a chrome-only member that can be set to indicate that
500
      // ComputeValues should fail for shorthand property values on that
501
      // keyframe.
502
      if (nsCSSProps::IsShorthand(pair.mProperty) &&
503
          keyframeDict.mSimulateComputeValuesFailure) {
504
        MarkAsComputeValuesFailureKey(keyframe->mPropertyValues.LastElement());
505
      }
506
#endif
507
    }
508
0
  }
509
0
510
0
  // Throw any errors we encountered while parsing 'easing' properties.
511
0
  if (parseEasingResult.MaybeSetPendingException(aCx)) {
512
0
    return false;
513
0
  }
514
0
515
0
  return true;
516
0
}
517
518
/**
519
 * Reads the property-values pairs from the specified JS object.
520
 *
521
 * @param aObject The JS object to look at.
522
 * @param aAllowLists If eAllow, values will be converted to
523
 *   (DOMString or sequence<DOMString); if eDisallow, values
524
 *   will be converted to DOMString.
525
 * @param aResult The array into which the enumerated property-values
526
 *   pairs will be stored.
527
 * @return false on failure or JS exception thrown while interacting
528
 *   with aObject; true otherwise.
529
 */
530
static bool
531
GetPropertyValuesPairs(JSContext* aCx,
532
                       JS::Handle<JSObject*> aObject,
533
                       ListAllowance aAllowLists,
534
                       nsTArray<PropertyValuesPair>& aResult)
535
0
{
536
0
  nsTArray<AdditionalProperty> properties;
537
0
538
0
  // Iterate over all the properties on aObject and append an
539
0
  // entry to properties for them.
540
0
  //
541
0
  // We don't compare the jsids that we encounter with those for
542
0
  // the explicit dictionary members, since we know that none
543
0
  // of the CSS property IDL names clash with them.
544
0
  JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
545
0
  if (!JS_Enumerate(aCx, aObject, &ids)) {
546
0
    return false;
547
0
  }
548
0
  for (size_t i = 0, n = ids.length(); i < n; i++) {
549
0
    nsAutoJSString propName;
550
0
    if (!propName.init(aCx, ids[i])) {
551
0
      return false;
552
0
    }
553
0
    nsCSSPropertyID property =
554
0
      nsCSSProps::LookupPropertyByIDLName(propName,
555
0
                                          CSSEnabledState::eForAllContent);
556
0
    if (KeyframeUtils::IsAnimatableProperty(property)) {
557
0
      AdditionalProperty* p = properties.AppendElement();
558
0
      p->mProperty = property;
559
0
      p->mJsidIndex = i;
560
0
    }
561
0
  }
562
0
563
0
  // Sort the entries by IDL name and then get each value and
564
0
  // convert it either to a DOMString or to a
565
0
  // (DOMString or sequence<DOMString>), depending on aAllowLists,
566
0
  // and build up aResult.
567
0
  properties.Sort(AdditionalProperty::PropertyComparator());
568
0
569
0
  for (AdditionalProperty& p : properties) {
570
0
    JS::Rooted<JS::Value> value(aCx);
571
0
    if (!JS_GetPropertyById(aCx, aObject, ids[p.mJsidIndex], &value)) {
572
0
      return false;
573
0
    }
574
0
    PropertyValuesPair* pair = aResult.AppendElement();
575
0
    pair->mProperty = p.mProperty;
576
0
    if (!AppendStringOrStringSequenceToArray(aCx, value, aAllowLists,
577
0
                                             pair->mValues)) {
578
0
      return false;
579
0
    }
580
0
  }
581
0
582
0
  return true;
583
0
}
584
585
/**
586
 * Converts aValue to DOMString, if aAllowLists is eDisallow, or
587
 * to (DOMString or sequence<DOMString>) if aAllowLists is aAllow.
588
 * The resulting strings are appended to aValues.
589
 */
590
static bool
591
AppendStringOrStringSequenceToArray(JSContext* aCx,
592
                                    JS::Handle<JS::Value> aValue,
593
                                    ListAllowance aAllowLists,
594
                                    nsTArray<nsString>& aValues)
595
0
{
596
0
  if (aAllowLists == ListAllowance::eAllow && aValue.isObject()) {
597
0
    // The value is an object, and we want to allow lists; convert
598
0
    // aValue to (DOMString or sequence<DOMString>).
599
0
    JS::ForOfIterator iter(aCx);
600
0
    if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) {
601
0
      return false;
602
0
    }
603
0
    if (iter.valueIsIterable()) {
604
0
      // If the object is iterable, convert it to sequence<DOMString>.
605
0
      JS::Rooted<JS::Value> element(aCx);
606
0
      for (;;) {
607
0
        bool done;
608
0
        if (!iter.next(&element, &done)) {
609
0
          return false;
610
0
        }
611
0
        if (done) {
612
0
          break;
613
0
        }
614
0
        if (!AppendValueAsString(aCx, aValues, element)) {
615
0
          return false;
616
0
        }
617
0
      }
618
0
      return true;
619
0
    }
620
0
  }
621
0
622
0
  // Either the object is not iterable, or aAllowLists doesn't want
623
0
  // a list; convert it to DOMString.
624
0
  if (!AppendValueAsString(aCx, aValues, aValue)) {
625
0
    return false;
626
0
  }
627
0
628
0
  return true;
629
0
}
630
631
/**
632
 * Converts aValue to DOMString and appends it to aValues.
633
 */
634
static bool
635
AppendValueAsString(JSContext* aCx,
636
                    nsTArray<nsString>& aValues,
637
                    JS::Handle<JS::Value> aValue)
638
0
{
639
0
  return ConvertJSValueToString(aCx, aValue, dom::eStringify, dom::eStringify,
640
0
                                *aValues.AppendElement());
641
0
}
642
643
static void
644
ReportInvalidPropertyValueToConsole(nsCSSPropertyID aProperty,
645
                                    const nsAString& aInvalidPropertyValue,
646
                                    nsIDocument* aDoc)
647
0
{
648
0
  const nsString& invalidValue = PromiseFlatString(aInvalidPropertyValue);
649
0
  const NS_ConvertASCIItoUTF16 propertyName(
650
0
    nsCSSProps::GetStringValue(aProperty));
651
0
  const char16_t* params[] = { invalidValue.get(), propertyName.get() };
652
0
  nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
653
0
                                  NS_LITERAL_CSTRING("Animation"),
654
0
                                  aDoc,
655
0
                                  nsContentUtils::eDOM_PROPERTIES,
656
0
                                  "InvalidKeyframePropertyValue",
657
0
                                  params, ArrayLength(params));
658
0
}
659
660
/**
661
 * Construct a PropertyValuePair parsing the given string into a suitable
662
 * nsCSSValue object.
663
 *
664
 * @param aProperty The CSS property.
665
 * @param aStringValue The property value to parse.
666
 * @param aDocument The document to use when parsing.
667
 * @return The constructed PropertyValuePair, or Nothing() if |aStringValue| is
668
 *   an invalid property value.
669
 */
670
static Maybe<PropertyValuePair>
671
MakePropertyValuePair(nsCSSPropertyID aProperty, const nsAString& aStringValue,
672
                      nsIDocument* aDocument)
673
0
{
674
0
  MOZ_ASSERT(aDocument);
675
0
  Maybe<PropertyValuePair> result;
676
0
677
0
  ServoCSSParser::ParsingEnvironment env =
678
0
    ServoCSSParser::GetParsingEnvironment(aDocument);
679
0
  RefPtr<RawServoDeclarationBlock> servoDeclarationBlock =
680
0
    ServoCSSParser::ParseProperty(aProperty, aStringValue, env);
681
0
682
0
  if (servoDeclarationBlock) {
683
0
    result.emplace(aProperty, std::move(servoDeclarationBlock));
684
0
  } else {
685
0
    ReportInvalidPropertyValueToConsole(aProperty, aStringValue, aDocument);
686
0
  }
687
0
  return result;
688
0
}
689
690
/**
691
 * Checks that the given keyframes are loosely ordered (each keyframe's
692
 * offset that is not null is greater than or equal to the previous
693
 * non-null offset) and that all values are within the range [0.0, 1.0].
694
 *
695
 * @return true if the keyframes' offsets are correctly ordered and
696
 *   within range; false otherwise.
697
 */
698
static bool
699
HasValidOffsets(const nsTArray<Keyframe>& aKeyframes)
700
0
{
701
0
  double offset = 0.0;
702
0
  for (const Keyframe& keyframe : aKeyframes) {
703
0
    if (keyframe.mOffset) {
704
0
      double thisOffset = keyframe.mOffset.value();
705
0
      if (thisOffset < offset || thisOffset > 1.0f) {
706
0
        return false;
707
0
      }
708
0
      offset = thisOffset;
709
0
    }
710
0
  }
711
0
  return true;
712
0
}
713
714
#ifdef DEBUG
715
/**
716
 * Takes a property-value pair for a shorthand property and modifies the
717
 * value to indicate that when we call StyleAnimationValue::ComputeValues on
718
 * that value we should behave as if that function had failed.
719
 *
720
 * @param aPair The PropertyValuePair to modify. |aPair.mProperty| must be
721
 *              a shorthand property.
722
 */
723
static void
724
MarkAsComputeValuesFailureKey(PropertyValuePair& aPair)
725
{
726
  MOZ_ASSERT(nsCSSProps::IsShorthand(aPair.mProperty),
727
             "Only shorthand property values can be marked as failure values");
728
729
  aPair.mSimulateComputeValuesFailure = true;
730
}
731
732
#endif
733
734
735
/**
736
 * The variation of the above function. This is for Servo backend.
737
 */
738
static nsTArray<ComputedKeyframeValues>
739
GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
740
                          dom::Element* aElement,
741
                          const ComputedStyle* aComputedStyle)
742
0
{
743
0
  MOZ_ASSERT(aElement);
744
0
745
0
  nsTArray<ComputedKeyframeValues> result;
746
0
747
0
  nsPresContext* presContext = nsContentUtils::GetContextForContent(aElement);
748
0
  if (!presContext) {
749
0
    // This has been reported to happen with some combinations of content
750
0
    // (particularly involving resize events and layout flushes? See bug 1407898
751
0
    // and bug 1408420) but no reproducible steps have been found.
752
0
    // For now we just return an empty array.
753
0
    return result;
754
0
  }
755
0
756
0
  result = presContext->StyleSet()->
757
0
    GetComputedKeyframeValuesFor(aKeyframes, aElement, aComputedStyle);
758
0
  return result;
759
0
}
760
761
static void
762
AppendInitialSegment(AnimationProperty* aAnimationProperty,
763
                     const KeyframeValueEntry& aFirstEntry)
764
0
{
765
0
  AnimationPropertySegment* segment =
766
0
    aAnimationProperty->mSegments.AppendElement();
767
0
  segment->mFromKey        = 0.0f;
768
0
  segment->mToKey          = aFirstEntry.mOffset;
769
0
  segment->mToValue        = aFirstEntry.mValue;
770
0
  segment->mToComposite    = aFirstEntry.mComposite;
771
0
}
772
773
static void
774
AppendFinalSegment(AnimationProperty* aAnimationProperty,
775
                   const KeyframeValueEntry& aLastEntry)
776
0
{
777
0
  AnimationPropertySegment* segment =
778
0
    aAnimationProperty->mSegments.AppendElement();
779
0
  segment->mFromKey        = aLastEntry.mOffset;
780
0
  segment->mFromValue      = aLastEntry.mValue;
781
0
  segment->mFromComposite  = aLastEntry.mComposite;
782
0
  segment->mToKey          = 1.0f;
783
0
  segment->mTimingFunction = aLastEntry.mTimingFunction;
784
0
}
785
786
// Returns a newly created AnimationProperty if one was created to fill-in the
787
// missing keyframe, nullptr otherwise (if we decided not to fill the keyframe
788
// becase we don't support implicit keyframes).
789
static AnimationProperty*
790
HandleMissingInitialKeyframe(nsTArray<AnimationProperty>& aResult,
791
                             const KeyframeValueEntry& aEntry)
792
0
{
793
0
  MOZ_ASSERT(aEntry.mOffset != 0.0f,
794
0
             "The offset of the entry should not be 0.0");
795
0
796
0
  // If the preference for implicit keyframes is not enabled, don't fill in the
797
0
  // missing keyframe.
798
0
  if (!StaticPrefs::dom_animations_api_implicit_keyframes_enabled()) {
799
0
    return nullptr;
800
0
  }
801
0
802
0
  AnimationProperty* result = aResult.AppendElement();
803
0
  result->mProperty = aEntry.mProperty;
804
0
805
0
  AppendInitialSegment(result, aEntry);
806
0
807
0
  return result;
808
0
}
809
810
static void
811
HandleMissingFinalKeyframe(nsTArray<AnimationProperty>& aResult,
812
                           const KeyframeValueEntry& aEntry,
813
                           AnimationProperty* aCurrentAnimationProperty)
814
0
{
815
0
  MOZ_ASSERT(aEntry.mOffset != 1.0f,
816
0
             "The offset of the entry should not be 1.0");
817
0
818
0
  // If the preference for implicit keyframes is not enabled, don't fill
819
0
  // in the missing keyframe.
820
0
  if (!StaticPrefs::dom_animations_api_implicit_keyframes_enabled()) {
821
0
    // If we have already appended a new entry for the property so we have to
822
0
    // remove it.
823
0
    if (aCurrentAnimationProperty) {
824
0
      aResult.RemoveLastElement();
825
0
    }
826
0
    return;
827
0
  }
828
0
829
0
  // If |aCurrentAnimationProperty| is nullptr, that means this is the first
830
0
  // entry for the property, we have to append a new AnimationProperty for this
831
0
  // property.
832
0
  if (!aCurrentAnimationProperty) {
833
0
    aCurrentAnimationProperty = aResult.AppendElement();
834
0
    aCurrentAnimationProperty->mProperty = aEntry.mProperty;
835
0
836
0
    // If we have only one entry whose offset is neither 1 nor 0 for this
837
0
    // property, we need to append the initial segment as well.
838
0
    if (aEntry.mOffset != 0.0f) {
839
0
      AppendInitialSegment(aCurrentAnimationProperty, aEntry);
840
0
    }
841
0
  }
842
0
  AppendFinalSegment(aCurrentAnimationProperty, aEntry);
843
0
}
844
845
/**
846
 * Builds an array of AnimationProperty objects to represent the keyframe
847
 * animation segments in aEntries.
848
 */
849
static void
850
BuildSegmentsFromValueEntries(nsTArray<KeyframeValueEntry>& aEntries,
851
                              nsTArray<AnimationProperty>& aResult)
852
0
{
853
0
  if (aEntries.IsEmpty()) {
854
0
    return;
855
0
  }
856
0
857
0
  // Sort the KeyframeValueEntry objects so that all entries for a given
858
0
  // property are together, and the entries are sorted by offset otherwise.
859
0
  std::stable_sort(aEntries.begin(), aEntries.end(),
860
0
                   &KeyframeValueEntry::PropertyOffsetComparator::LessThan);
861
0
862
0
  // For a given index i, we want to generate a segment from aEntries[i]
863
0
  // to aEntries[j], if:
864
0
  //
865
0
  //   * j > i,
866
0
  //   * aEntries[i + 1]'s offset/property is different from aEntries[i]'s, and
867
0
  //   * aEntries[j - 1]'s offset/property is different from aEntries[j]'s.
868
0
  //
869
0
  // That will eliminate runs of same offset/property values where there's no
870
0
  // point generating zero length segments in the middle of the animation.
871
0
  //
872
0
  // Additionally we need to generate a zero length segment at offset 0 and at
873
0
  // offset 1, if we have multiple values for a given property at that offset,
874
0
  // since we need to retain the very first and very last value so they can
875
0
  // be used for reverse and forward filling.
876
0
  //
877
0
  // Typically, for each property in |aEntries|, we expect there to be at least
878
0
  // one KeyframeValueEntry with offset 0.0, and at least one with offset 1.0.
879
0
  // However, since it is possible that when building |aEntries|, the call to
880
0
  // StyleAnimationValue::ComputeValues might fail, this can't be guaranteed.
881
0
  // Furthermore, if additive animation is disabled, the following loop takes
882
0
  // care to identify properties that lack a value at offset 0.0/1.0 and drops
883
0
  // those properties from |aResult|.
884
0
885
0
  nsCSSPropertyID lastProperty = eCSSProperty_UNKNOWN;
886
0
  AnimationProperty* animationProperty = nullptr;
887
0
888
0
  size_t i = 0, n = aEntries.Length();
889
0
890
0
  while (i < n) {
891
0
    // If we've reached the end of the array of entries, synthesize a final (and
892
0
    // initial) segment if necessary.
893
0
    if (i + 1 == n) {
894
0
      if (aEntries[i].mOffset != 1.0f) {
895
0
        HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
896
0
      } else if (aEntries[i].mOffset == 1.0f && !animationProperty) {
897
0
        // If the last entry with offset 1 and no animation property, that means
898
0
        // it is the only entry for this property so append a single segment
899
0
        // from 0 offset to |aEntry[i].offset|.
900
0
        Unused << HandleMissingInitialKeyframe(aResult, aEntries[i]);
901
0
      }
902
0
      animationProperty = nullptr;
903
0
      break;
904
0
    }
905
0
906
0
    MOZ_ASSERT(aEntries[i].mProperty != eCSSProperty_UNKNOWN &&
907
0
               aEntries[i + 1].mProperty != eCSSProperty_UNKNOWN,
908
0
               "Each entry should specify a valid property");
909
0
910
0
    // No keyframe for this property at offset 0.
911
0
    if (aEntries[i].mProperty != lastProperty &&
912
0
        aEntries[i].mOffset != 0.0f) {
913
0
      // If we don't support additive animation we can't fill in the missing
914
0
      // keyframes and we should just skip this property altogether. Since the
915
0
      // entries are sorted by offset for a given property, and since we don't
916
0
      // update |lastProperty|, we will keep hitting this condition until we
917
0
      // change property.
918
0
      animationProperty = HandleMissingInitialKeyframe(aResult, aEntries[i]);
919
0
      if (animationProperty) {
920
0
        lastProperty = aEntries[i].mProperty;
921
0
      } else {
922
0
        // Skip this entry if we did not handle the missing entry.
923
0
        ++i;
924
0
        continue;
925
0
      }
926
0
    }
927
0
928
0
    // Skip this entry if the next entry has the same offset except for initial
929
0
    // and final ones. We will handle missing keyframe in the next loop
930
0
    // if the property is changed on the next entry.
931
0
    if (aEntries[i].mProperty == aEntries[i + 1].mProperty &&
932
0
        aEntries[i].mOffset == aEntries[i + 1].mOffset &&
933
0
        aEntries[i].mOffset != 1.0f && aEntries[i].mOffset != 0.0f) {
934
0
      ++i;
935
0
      continue;
936
0
    }
937
0
938
0
    // No keyframe for this property at offset 1.
939
0
    if (aEntries[i].mProperty != aEntries[i + 1].mProperty &&
940
0
        aEntries[i].mOffset != 1.0f) {
941
0
      HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
942
0
      // Move on to new property.
943
0
      animationProperty = nullptr;
944
0
      ++i;
945
0
      continue;
946
0
    }
947
0
948
0
    // Starting from i + 1, determine the next [i, j] interval from which to
949
0
    // generate a segment. Basically, j is i + 1, but there are some special
950
0
    // cases for offset 0 and 1, so we need to handle them specifically.
951
0
    // Note: From this moment, we make sure [i + 1] is valid and
952
0
    //       there must be an initial entry (i.e. mOffset = 0.0) and
953
0
    //       a final entry (i.e. mOffset = 1.0). Besides, all the entries
954
0
    //       with the same offsets except for initial/final ones are filtered
955
0
    //       out already.
956
0
    size_t j = i + 1;
957
0
    if (aEntries[i].mOffset == 0.0f && aEntries[i + 1].mOffset == 0.0f) {
958
0
      // We need to generate an initial zero-length segment.
959
0
      MOZ_ASSERT(aEntries[i].mProperty == aEntries[i + 1].mProperty);
960
0
      while (j + 1 < n &&
961
0
             aEntries[j + 1].mOffset == 0.0f &&
962
0
             aEntries[j + 1].mProperty == aEntries[j].mProperty) {
963
0
        ++j;
964
0
      }
965
0
    } else if (aEntries[i].mOffset == 1.0f) {
966
0
      if (aEntries[i + 1].mOffset == 1.0f &&
967
0
          aEntries[i + 1].mProperty == aEntries[i].mProperty) {
968
0
        // We need to generate a final zero-length segment.
969
0
        while (j + 1 < n &&
970
0
               aEntries[j + 1].mOffset == 1.0f &&
971
0
               aEntries[j + 1].mProperty == aEntries[j].mProperty) {
972
0
          ++j;
973
0
        }
974
0
      } else {
975
0
        // New property.
976
0
        MOZ_ASSERT(aEntries[i].mProperty != aEntries[i + 1].mProperty);
977
0
        animationProperty = nullptr;
978
0
        ++i;
979
0
        continue;
980
0
      }
981
0
    }
982
0
983
0
    // If we've moved on to a new property, create a new AnimationProperty
984
0
    // to insert segments into.
985
0
    if (aEntries[i].mProperty != lastProperty) {
986
0
      MOZ_ASSERT(aEntries[i].mOffset == 0.0f);
987
0
      MOZ_ASSERT(!animationProperty);
988
0
      animationProperty = aResult.AppendElement();
989
0
      animationProperty->mProperty = aEntries[i].mProperty;
990
0
      lastProperty = aEntries[i].mProperty;
991
0
    }
992
0
993
0
    MOZ_ASSERT(animationProperty, "animationProperty should be valid pointer.");
994
0
995
0
    // Now generate the segment.
996
0
    AnimationPropertySegment* segment =
997
0
      animationProperty->mSegments.AppendElement();
998
0
    segment->mFromKey        = aEntries[i].mOffset;
999
0
    segment->mToKey          = aEntries[j].mOffset;
1000
0
    segment->mFromValue      = aEntries[i].mValue;
1001
0
    segment->mToValue        = aEntries[j].mValue;
1002
0
    segment->mTimingFunction = aEntries[i].mTimingFunction;
1003
0
    segment->mFromComposite  = aEntries[i].mComposite;
1004
0
    segment->mToComposite    = aEntries[j].mComposite;
1005
0
1006
0
    i = j;
1007
0
  }
1008
0
}
1009
1010
/**
1011
 * Converts a JS object representing a property-indexed keyframe into
1012
 * an array of Keyframe objects.
1013
 *
1014
 * @param aCx The JSContext for |aValue|.
1015
 * @param aDocument The document to use when parsing CSS properties.
1016
 * @param aValue The JS object.
1017
 * @param aResult The array into which the resulting AnimationProperty
1018
 *   objects will be appended.
1019
 * @param aRv Out param to store any errors thrown by this function.
1020
 */
1021
static void
1022
GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx,
1023
                                           nsIDocument* aDocument,
1024
                                           JS::Handle<JS::Value> aValue,
1025
                                           nsTArray<Keyframe>& aResult,
1026
                                           ErrorResult& aRv)
1027
0
{
1028
0
  MOZ_ASSERT(aValue.isObject());
1029
0
  MOZ_ASSERT(aResult.IsEmpty());
1030
0
  MOZ_ASSERT(!aRv.Failed());
1031
0
1032
0
  // Convert the object to a property-indexed keyframe dictionary to
1033
0
  // get its explicit dictionary members.
1034
0
  dom::binding_detail::FastBasePropertyIndexedKeyframe keyframeDict;
1035
0
  if (!keyframeDict.Init(aCx, aValue, "BasePropertyIndexedKeyframe argument",
1036
0
                         false)) {
1037
0
    aRv.Throw(NS_ERROR_FAILURE);
1038
0
    return;
1039
0
  }
1040
0
1041
0
  // Get all the property--value-list pairs off the object.
1042
0
  JS::Rooted<JSObject*> object(aCx, &aValue.toObject());
1043
0
  nsTArray<PropertyValuesPair> propertyValuesPairs;
1044
0
  if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eAllow,
1045
0
                              propertyValuesPairs)) {
1046
0
    aRv.Throw(NS_ERROR_FAILURE);
1047
0
    return;
1048
0
  }
1049
0
1050
0
  // Create a set of keyframes for each property.
1051
0
  nsClassHashtable<nsFloatHashKey, Keyframe> processedKeyframes;
1052
0
  for (const PropertyValuesPair& pair : propertyValuesPairs) {
1053
0
    size_t count = pair.mValues.Length();
1054
0
    if (count == 0) {
1055
0
      // No animation values for this property.
1056
0
      continue;
1057
0
    }
1058
0
1059
0
    // If we only have one value, we should animate from the underlying value
1060
0
    // but not if the pref for supporting implicit keyframes is disabled.
1061
0
    if (!StaticPrefs::dom_animations_api_implicit_keyframes_enabled() &&
1062
0
        count == 1) {
1063
0
      aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR);
1064
0
      return;
1065
0
    }
1066
0
1067
0
    size_t n = pair.mValues.Length() - 1;
1068
0
    size_t i = 0;
1069
0
1070
0
    for (const nsString& stringValue : pair.mValues) {
1071
0
      // For single-valued lists, the single value should be added to a
1072
0
      // keyframe with offset 1.
1073
0
      double offset = n ? i++ / double(n) : 1;
1074
0
      Keyframe* keyframe = processedKeyframes.LookupOrAdd(offset);
1075
0
      if (keyframe->mPropertyValues.IsEmpty()) {
1076
0
        keyframe->mComputedOffset = offset;
1077
0
      }
1078
0
1079
0
      Maybe<PropertyValuePair> valuePair =
1080
0
        MakePropertyValuePair(pair.mProperty, stringValue, aDocument);
1081
0
      if (!valuePair) {
1082
0
        continue;
1083
0
      }
1084
0
      keyframe->mPropertyValues.AppendElement(std::move(valuePair.ref()));
1085
0
    }
1086
0
  }
1087
0
1088
0
  aResult.SetCapacity(processedKeyframes.Count());
1089
0
  for (auto iter = processedKeyframes.Iter(); !iter.Done(); iter.Next()) {
1090
0
    aResult.AppendElement(std::move(*iter.UserData()));
1091
0
  }
1092
0
1093
0
  aResult.Sort(ComputedOffsetComparator());
1094
0
1095
0
  // Fill in any specified offsets
1096
0
  //
1097
0
  // This corresponds to step 5, "Otherwise," branch, substeps 5-6 of
1098
0
  // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
1099
0
  const FallibleTArray<Nullable<double>>* offsets = nullptr;
1100
0
  AutoTArray<Nullable<double>, 1> singleOffset;
1101
0
  auto& offset = keyframeDict.mOffset;
1102
0
  if (offset.IsDouble()) {
1103
0
    singleOffset.AppendElement(offset.GetAsDouble());
1104
0
    // dom::Sequence is a fallible but AutoTArray is infallible and we need to
1105
0
    // point to one or the other. Fortunately, fallible and infallible array
1106
0
    // types can be implicitly converted provided they are const.
1107
0
    const FallibleTArray<Nullable<double>>& asFallibleArray = singleOffset;
1108
0
    offsets = &asFallibleArray;
1109
0
  } else if (offset.IsDoubleOrNullSequence()) {
1110
0
    offsets = &offset.GetAsDoubleOrNullSequence();
1111
0
  }
1112
0
  // If offset.IsNull() is true, then we want to leave the mOffset member of
1113
0
  // each keyframe with its initialized value of null. By leaving |offsets|
1114
0
  // as nullptr here, we skip updating mOffset below.
1115
0
1116
0
  size_t offsetsToFill =
1117
0
    offsets ? std::min(offsets->Length(), aResult.Length()) : 0;
1118
0
  for (size_t i = 0; i < offsetsToFill; i++) {
1119
0
    if (!offsets->ElementAt(i).IsNull()) {
1120
0
      aResult[i].mOffset.emplace(offsets->ElementAt(i).Value());
1121
0
    }
1122
0
  }
1123
0
1124
0
  // Check that the keyframes are loosely sorted and that any specified offsets
1125
0
  // are between 0.0 and 1.0 inclusive.
1126
0
  //
1127
0
  // This corresponds to steps 6-7 of
1128
0
  // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
1129
0
  //
1130
0
  // In the spec, TypeErrors arising from invalid offsets and easings are thrown
1131
0
  // at the end of the procedure since it assumes we initially store easing
1132
0
  // values as strings and then later parse them.
1133
0
  //
1134
0
  // However, we will parse easing members immediately when we process them
1135
0
  // below. In order to maintain the relative order in which TypeErrors are
1136
0
  // thrown according to the spec, namely exceptions arising from invalid
1137
0
  // offsets are thrown before exceptions arising from invalid easings, we check
1138
0
  // the offsets here.
1139
0
  if (!HasValidOffsets(aResult)) {
1140
0
    aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
1141
0
    aResult.Clear();
1142
0
    return;
1143
0
  }
1144
0
1145
0
  // Fill in any easings.
1146
0
  //
1147
0
  // This corresponds to step 5, "Otherwise," branch, substeps 7-11 of
1148
0
  // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
1149
0
  FallibleTArray<Maybe<ComputedTimingFunction>> easings;
1150
0
  auto parseAndAppendEasing = [&](const nsString& easingString,
1151
0
                                  ErrorResult& aRv) {
1152
0
    auto easing = TimingParams::ParseEasing(easingString, aDocument, aRv);
1153
0
    if (!aRv.Failed() && !easings.AppendElement(std::move(easing), fallible)) {
1154
0
      aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
1155
0
    }
1156
0
  };
1157
0
1158
0
  auto& easing = keyframeDict.mEasing;
1159
0
  if (easing.IsString()) {
1160
0
    parseAndAppendEasing(easing.GetAsString(), aRv);
1161
0
    if (aRv.Failed()) {
1162
0
      aResult.Clear();
1163
0
      return;
1164
0
    }
1165
0
  } else {
1166
0
    for (const nsString& easingString : easing.GetAsStringSequence()) {
1167
0
      parseAndAppendEasing(easingString, aRv);
1168
0
      if (aRv.Failed()) {
1169
0
        aResult.Clear();
1170
0
        return;
1171
0
      }
1172
0
    }
1173
0
  }
1174
0
1175
0
  // If |easings| is empty, then we are supposed to fill it in with the value
1176
0
  // "linear" and then repeat the list as necessary.
1177
0
  //
1178
0
  // However, for Keyframe.mTimingFunction we represent "linear" as a None
1179
0
  // value. Since we have not assigned 'mTimingFunction' for any of the
1180
0
  // keyframes in |aResult| they will already have their initial None value
1181
0
  // (i.e. linear). As a result, if |easings| is empty, we don't need to do
1182
0
  // anything.
1183
0
  if (!easings.IsEmpty()) {
1184
0
    for (size_t i = 0; i < aResult.Length(); i++) {
1185
0
      aResult[i].mTimingFunction = easings[i % easings.Length()];
1186
0
    }
1187
0
  }
1188
0
1189
0
  // Fill in any composite operations.
1190
0
  //
1191
0
  // This corresponds to step 5, "Otherwise," branch, substep 12 of
1192
0
  // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
1193
0
  if (StaticPrefs::dom_animations_api_compositing_enabled()) {
1194
0
    const FallibleTArray<dom::CompositeOperationOrAuto>* compositeOps = nullptr;
1195
0
    AutoTArray<dom::CompositeOperationOrAuto, 1> singleCompositeOp;
1196
0
    auto& composite = keyframeDict.mComposite;
1197
0
    if (composite.IsCompositeOperationOrAuto()) {
1198
0
      singleCompositeOp.AppendElement(
1199
0
        composite.GetAsCompositeOperationOrAuto());
1200
0
      const FallibleTArray<dom::CompositeOperationOrAuto>& asFallibleArray =
1201
0
        singleCompositeOp;
1202
0
      compositeOps = &asFallibleArray;
1203
0
    } else if (composite.IsCompositeOperationOrAutoSequence()) {
1204
0
      compositeOps = &composite.GetAsCompositeOperationOrAutoSequence();
1205
0
    }
1206
0
1207
0
    // Fill in and repeat as needed.
1208
0
    if (compositeOps && !compositeOps->IsEmpty()) {
1209
0
      size_t length = compositeOps->Length();
1210
0
      for (size_t i = 0; i < aResult.Length(); i++) {
1211
0
        aResult[i].mComposite = compositeOps->ElementAt(i % length);
1212
0
      }
1213
0
    }
1214
0
  }
1215
0
}
1216
1217
/**
1218
 * Returns true if the supplied set of keyframes has keyframe values for
1219
 * any property for which it does not also supply a value for the 0% and 100%
1220
 * offsets. The check is not entirely accurate but should detect most common
1221
 * cases.
1222
 *
1223
 * @param aKeyframes The set of keyframes to analyze.
1224
 * @param aDocument The document to use when parsing keyframes so we can
1225
 *   try to detect where we have an invalid value at 0%/100%.
1226
 */
1227
static bool
1228
HasImplicitKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
1229
                          nsIDocument* aDocument)
1230
0
{
1231
0
  // We are looking to see if that every property referenced in |aKeyframes|
1232
0
  // has a valid property at offset 0.0 and 1.0. The check as to whether a
1233
0
  // property is valid or not, however, is not precise. We only check if the
1234
0
  // property can be parsed, NOT whether it can also be converted to a
1235
0
  // StyleAnimationValue since doing that requires a target element bound to
1236
0
  // a document which we might not always have at the point where we want to
1237
0
  // perform this check.
1238
0
  //
1239
0
  // This is only a temporary measure until we ship implicit keyframes and
1240
0
  // remove the corresponding pref.
1241
0
  // So as long as this check catches most cases, and we don't do anything
1242
0
  // horrible in one of the cases we can't detect, it should be sufficient.
1243
0
1244
0
  nsCSSPropertyIDSet properties;              // All properties encountered.
1245
0
  nsCSSPropertyIDSet propertiesWithFromValue; // Those with a defined 0% value.
1246
0
  nsCSSPropertyIDSet propertiesWithToValue;   // Those with a defined 100% value.
1247
0
1248
0
  auto addToPropertySets = [&](nsCSSPropertyID aProperty, double aOffset) {
1249
0
    properties.AddProperty(aProperty);
1250
0
    if (aOffset == 0.0) {
1251
0
      propertiesWithFromValue.AddProperty(aProperty);
1252
0
    } else if (aOffset == 1.0) {
1253
0
      propertiesWithToValue.AddProperty(aProperty);
1254
0
    }
1255
0
  };
1256
0
1257
0
  for (size_t i = 0, len = aKeyframes.Length(); i < len; i++) {
1258
0
    const Keyframe& frame = aKeyframes[i];
1259
0
1260
0
    // We won't have called DistributeKeyframes when this is called so
1261
0
    // we can't use frame.mComputedOffset. Instead we do a rough version
1262
0
    // of that algorithm that substitutes null offsets with 0.0 for the first
1263
0
    // frame, 1.0 for the last frame, and 0.5 for everything else.
1264
0
    double computedOffset = i == len - 1
1265
0
                            ? 1.0
1266
0
                            : i == 0 ? 0.0 : 0.5;
1267
0
    double offsetToUse = frame.mOffset
1268
0
                         ? frame.mOffset.value()
1269
0
                         : computedOffset;
1270
0
1271
0
    for (const PropertyValuePair& pair : frame.mPropertyValues) {
1272
0
      if (nsCSSProps::IsShorthand(pair.mProperty)) {
1273
0
        MOZ_ASSERT(pair.mServoDeclarationBlock);
1274
0
        CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(
1275
0
            prop, pair.mProperty, CSSEnabledState::eForAllContent) {
1276
0
          addToPropertySets(*prop, offsetToUse);
1277
0
        }
1278
0
      } else {
1279
0
        addToPropertySets(pair.mProperty, offsetToUse);
1280
0
      }
1281
0
    }
1282
0
  }
1283
0
1284
0
  return !propertiesWithFromValue.Equals(properties) ||
1285
0
         !propertiesWithToValue.Equals(properties);
1286
0
}
1287
1288
/**
1289
 * Distribute the offsets of all keyframes in between the endpoints of the
1290
 * given range.
1291
 *
1292
 * @param aRange The sequence of keyframes between whose endpoints we should
1293
 * distribute offsets.
1294
 */
1295
static void
1296
DistributeRange(const Range<Keyframe>& aRange)
1297
0
{
1298
0
  const Range<Keyframe> rangeToAdjust = Range<Keyframe>(aRange.begin() + 1,
1299
0
                                                        aRange.end() - 1);
1300
0
  const size_t n = aRange.length() - 1;
1301
0
  const double startOffset = aRange[0].mComputedOffset;
1302
0
  const double diffOffset = aRange[n].mComputedOffset - startOffset;
1303
0
  for (auto iter = rangeToAdjust.begin(); iter != rangeToAdjust.end(); ++iter) {
1304
0
    size_t index = iter - aRange.begin();
1305
0
    iter->mComputedOffset = startOffset + double(index) / n * diffOffset;
1306
0
  }
1307
0
}
1308
1309
1310
} // namespace mozilla