Quellcode durchsuchen

LibWeb: Partially implement the "process a keyframes argument" procedure

Keyframes can be given in two separate forms:

- As an array of separate keyframe objects, where the keys of each
  keyframe represent CSS properties, and their values represents the
  values that those CSS properties should take

  e.x.:
  [{ color: 'red', offset: 0.3 }, { color: 'blue', offset: 0.7 }]

- As a single monolithic keyframe object, where the keys of each
  keyframe represent CSS properties, and their values are arrays of
  values, where each index k represents the value of the given
  property at the k'th frame.

  e.x.:
  { color: ['red', 'blue'], offset: [0.3, 0.7] }

This commit only implements the first option, as it is much simpler. See
the next commit for the implementation of the second option.
Matthew Olsson vor 1 Jahr
Ursprung
Commit
7d69fa0ccf
1 geänderte Dateien mit 146 neuen und 1 gelöschten Zeilen
  1. 146 1
      Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp

+ 146 - 1
Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp

@@ -7,6 +7,7 @@
 #include <AK/QuickSort.h>
 #include <LibJS/Runtime/Iterator.h>
 #include <LibWeb/Animations/KeyframeEffect.h>
+#include <LibWeb/CSS/Parser/Parser.h>
 #include <LibWeb/WebIDL/ExceptionOr.h>
 
 namespace Web::Animations {
@@ -196,7 +197,7 @@ static WebIDL::ExceptionOr<KeyframeType<AL>> process_a_keyframe_like_object(JS::
 }
 
 // https://www.w3.org/TR/web-animations-1/#compute-missing-keyframe-offsets
-[[maybe_unused]] static void compute_missing_keyframe_offsets(Vector<BaseKeyframe>& keyframes)
+static void compute_missing_keyframe_offsets(Vector<BaseKeyframe>& keyframes)
 {
     // 1. For each keyframe, in keyframes, let the computed keyframe offset of the keyframe be equal to its keyframe
     //    offset value.
@@ -258,6 +259,150 @@ static WebIDL::ExceptionOr<KeyframeType<AL>> process_a_keyframe_like_object(JS::
     }
 }
 
+// https://www.w3.org/TR/web-animations-1/#loosely-sorted-by-offset
+static bool is_loosely_sorted_by_offset(Vector<BaseKeyframe> const& keyframes)
+{
+    // The list of keyframes for a keyframe effect must be loosely sorted by offset which means that for each keyframe
+    // in the list that has a keyframe offset that is not null, the offset is greater than or equal to the offset of the
+    // previous keyframe in the list with a keyframe offset that is not null, if any.
+
+    Optional<double> last_offset;
+    for (auto const& keyframe : keyframes) {
+        if (!keyframe.offset.has_value())
+            continue;
+
+        if (last_offset.has_value() && keyframe.offset.value() < last_offset.value())
+            return false;
+
+        last_offset = keyframe.offset;
+    }
+
+    return true;
+}
+
+// https://www.w3.org/TR/web-animations-1/#process-a-keyframes-argument
+[[maybe_unused]] static WebIDL::ExceptionOr<Vector<BaseKeyframe>> process_a_keyframes_argument(JS::Realm& realm, JS::GCPtr<JS::Object> object)
+{
+    auto& vm = realm.vm();
+
+    auto parse_easing_string = [&](auto& value) -> RefPtr<CSS::StyleValue const> {
+        auto maybe_parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext(realm), value);
+        if (maybe_parser.is_error())
+            return {};
+
+        if (auto style_value = maybe_parser.release_value().parse_as_css_value(CSS::PropertyID::AnimationTimingFunction)) {
+            if (style_value->is_easing())
+                return style_value;
+        }
+
+        return {};
+    };
+
+    // 1. If object is null, return an empty sequence of keyframes.
+    if (!object)
+        return Vector<BaseKeyframe> {};
+
+    // 2. Let processed keyframes be an empty sequence of keyframes.
+    Vector<BaseKeyframe> processed_keyframes;
+
+    // 3. Let method be the result of GetMethod(object, @@iterator).
+    // 4. Check the completion record of method.
+    auto method = TRY(JS::Value(object).get_method(vm, vm.well_known_symbol_iterator()));
+
+    // 5. Perform the steps corresponding to the first matching condition from below,
+
+    // -> If method is not undefined,
+    if (method) {
+        // 1. Let iter be GetIterator(object, method).
+        // 2. Check the completion record of iter.
+        auto iter = TRY(JS::get_iterator_from_method(vm, object, *method));
+
+        // 3. Repeat:
+        while (true) {
+            // 1. Let next be IteratorStep(iter).
+            // 2. Check the completion record of next.
+            auto next = TRY(JS::iterator_step(vm, iter));
+
+            // 3. If next is false abort this loop.
+            if (!next)
+                break;
+
+            // 4. Let nextItem be IteratorValue(next).
+            // 5. Check the completion record of nextItem.
+            auto next_item = TRY(JS::iterator_value(vm, *next));
+
+            // 6. If Type(nextItem) is not Undefined, Null or Object, then throw a TypeError and abort these steps.
+            if (!next_item.is_nullish() && !next_item.is_object())
+                return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOrNull, next_item.to_string_without_side_effects());
+
+            // 7. Append to processed keyframes the result of running the procedure to process a keyframe-like object
+            //    passing nextItem as the keyframe input and with the allow lists flag set to false.
+            processed_keyframes.append(TRY(process_a_keyframe_like_object<AllowLists::No>(realm, next_item.as_object())));
+        }
+    }
+    // -> Otherwise,
+    else {
+        // FIXME: Support processing a single keyframe-like object
+        TODO();
+    }
+
+    // 6. If processed keyframes is not loosely sorted by offset, throw a TypeError and abort these steps.
+    if (!is_loosely_sorted_by_offset(processed_keyframes))
+        return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Keyframes are not in ascending order based on offset"sv };
+
+    // 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 (size_t i = 0; i < processed_keyframes.size(); i++) {
+        auto const& keyframe = processed_keyframes[i];
+        if (!keyframe.offset.has_value())
+            continue;
+
+        auto offset = keyframe.offset.value();
+        if (offset < 0.0 || offset > 1.0)
+            return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Keyframe {} has invalid offset value {}"sv, i, offset)) };
+    }
+
+    // 8. For each frame in processed keyframes, perform the following steps:
+    for (auto& keyframe : processed_keyframes) {
+        // 1. For each property-value pair in frame, parse the property value using the syntax specified for that
+        //    property.
+        //
+        //    If the property value is invalid according to the syntax for the property, discard the property-value pair.
+        //    User agents that provide support for diagnosing errors in content SHOULD produce an appropriate warning
+        //    highlight
+        BaseKeyframe::ParsedProperties parsed_properties;
+        for (auto& [property_string, value_string] : keyframe.unparsed_properties()) {
+            if (auto property = CSS::property_id_from_camel_case_string(property_string); property.has_value()) {
+                auto maybe_parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext(realm), value_string);
+                if (maybe_parser.is_error())
+                    continue;
+
+                if (auto style_value = maybe_parser.release_value().parse_as_css_value(*property))
+                    parsed_properties.set(*property, *style_value);
+            }
+        }
+        keyframe.properties.set(move(parsed_properties));
+
+        // 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 member of the EffectTiming dictionary.
+        //
+        //    If parsing the "easing" property fails, throw a TypeError and abort this procedure.
+        auto easing_string = keyframe.easing.get<String>();
+        auto easing_value = parse_easing_string(easing_string);
+
+        if (!easing_value)
+            return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Invalid animation easing value: \"{}\"", easing_string)) };
+
+        keyframe.easing.set(NonnullRefPtr<CSS::StyleValue const> { *easing_value });
+    }
+
+    // FIXME:
+    // 9. Parse each of the values in unused easings using the CSS syntax defined for easing member of the EffectTiming
+    //    interface, and if any of the values fail to parse, throw a TypeError and abort this procedure.
+
+    return processed_keyframes;
+}
+
 JS::NonnullGCPtr<KeyframeEffect> KeyframeEffect::create(JS::Realm& realm)
 {
     return realm.heap().allocate<KeyframeEffect>(realm, realm);