1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 21:17:44 +00:00

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.
This commit is contained in:
Matthew Olsson 2023-11-04 13:26:43 -07:00 committed by Andreas Kling
parent 9f404ed9c1
commit 7d69fa0ccf

View file

@ -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);