1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 06:38:10 +00:00

LibWeb: Add preliminary support for CSS animations

This partially implements CSS-Animations-1 (though there are references
to CSS-Animations-2).
Current limitations:
- Multi-selector keyframes are not supported.
- Most animation properties are ignored.
- Timing functions are not applied.
- Non-absolute values are not interpolated unless the target is also of
  the same non-absolute type (e.g. 10% -> 25%, but not 10% -> 20px).
- The JavaScript interface is left as an exercise for the next poor soul
  looking at this code.

With those said, this commit implements:
- Interpolation for most common types
- Proper keyframe resolution (including the synthetic from-keyframe
  containing the initial state)
- Properly driven animations, and proper style invalidation

Co-Authored-By: Andreas Kling <kling@serenityos.org>
This commit is contained in:
Ali Mohammad Pur 2023-05-26 23:30:54 +03:30 committed by Andreas Kling
parent f07c4ffbc8
commit e90752cc21
31 changed files with 1062 additions and 12 deletions

View file

@ -986,6 +986,297 @@ static ErrorOr<void> cascade_custom_properties(DOM::Element& element, Optional<C
return {};
}
StyleComputer::AnimationStepTransition StyleComputer::Animation::step(CSS::Time const& time_step)
{
auto delay_ms = remaining_delay.to_milliseconds();
auto time_step_ms = time_step.to_milliseconds();
if (delay_ms > time_step_ms) {
remaining_delay = CSS::Time { static_cast<float>(delay_ms - time_step_ms), CSS::Time::Type::Ms };
return AnimationStepTransition::NoTransition;
}
remaining_delay = CSS::Time { 0, CSS::Time::Type::Ms };
time_step_ms -= delay_ms;
float added_progress = static_cast<float>(time_step_ms / duration.to_milliseconds());
auto new_progress = progress.as_fraction() + added_progress;
auto changed_iteration = false;
if (new_progress >= 1) {
if (iteration_count.has_value()) {
if (iteration_count.value() == 0) {
progress = CSS::Percentage(100);
return AnimationStepTransition::ActiveToAfter;
}
--iteration_count.value();
changed_iteration = true;
}
new_progress = 0;
}
progress = CSS::Percentage(new_progress * 100);
if (changed_iteration)
return AnimationStepTransition::ActiveToActiveChangingTheIteration;
return AnimationStepTransition::AfterToActive;
}
static ErrorOr<NonnullRefPtr<StyleValue>> interpolate_property(StyleValue const& from, StyleValue const& to, float delta)
{
if (from.type() != to.type()) {
if (delta > 0.999f)
return to;
return from;
}
auto interpolate_raw = [delta = static_cast<double>(delta)](auto from, auto to) {
return static_cast<RemoveCVReference<decltype(from)>>(static_cast<double>(from) + static_cast<double>(to - from) * delta);
};
switch (from.type()) {
case StyleValue::Type::Angle:
return AngleStyleValue::create(Angle::make_degrees(interpolate_raw(from.as_angle().angle().to_degrees(), to.as_angle().angle().to_degrees())));
case StyleValue::Type::Color: {
auto from_color = from.as_color().color();
auto to_color = to.as_color().color();
auto from_hsv = from_color.to_hsv();
auto to_hsv = to_color.to_hsv();
auto color = Color::from_hsv(
interpolate_raw(from_hsv.hue, to_hsv.hue),
interpolate_raw(from_hsv.saturation, to_hsv.saturation),
interpolate_raw(from_hsv.value, to_hsv.value));
color.set_alpha(interpolate_raw(from_color.alpha(), to_color.alpha()));
return ColorStyleValue::create(color);
}
case StyleValue::Type::Length: {
auto& from_length = from.as_length().length();
auto& to_length = to.as_length().length();
return LengthStyleValue::create(Length(interpolate_raw(from_length.raw_value(), to_length.raw_value()), from_length.type()));
}
case StyleValue::Type::Numeric:
return NumericStyleValue::create_float(interpolate_raw(from.as_numeric().number(), to.as_numeric().number()));
case StyleValue::Type::Percentage:
return PercentageStyleValue::create(Percentage(interpolate_raw(from.as_percentage().percentage().value(), to.as_percentage().percentage().value())));
case StyleValue::Type::Position: {
auto& from_position = from.as_position();
auto& to_position = to.as_position();
return PositionStyleValue::create(
TRY(interpolate_property(from_position.edge_x(), to_position.edge_x(), delta)),
TRY(interpolate_property(from_position.edge_y(), to_position.edge_y(), delta)));
}
case StyleValue::Type::Rect: {
auto from_rect = from.as_rect().rect();
auto to_rect = to.as_rect().rect();
return RectStyleValue::create({
Length(interpolate_raw(from_rect.top_edge.raw_value(), to_rect.top_edge.raw_value()), from_rect.top_edge.type()),
Length(interpolate_raw(from_rect.right_edge.raw_value(), to_rect.right_edge.raw_value()), from_rect.right_edge.type()),
Length(interpolate_raw(from_rect.bottom_edge.raw_value(), to_rect.bottom_edge.raw_value()), from_rect.bottom_edge.type()),
Length(interpolate_raw(from_rect.left_edge.raw_value(), to_rect.left_edge.raw_value()), from_rect.left_edge.type()),
});
}
case StyleValue::Type::Transformation: {
auto& from_transform = from.as_transformation();
auto& to_transform = to.as_transformation();
if (from_transform.transform_function() != to_transform.transform_function())
return from;
auto from_input_values = from_transform.values();
auto to_input_values = to_transform.values();
if (from_input_values.size() != to_input_values.size())
return from;
StyleValueVector interpolated_values;
interpolated_values.ensure_capacity(from_input_values.size());
for (size_t i = 0; i < from_input_values.size(); ++i)
interpolated_values.append(TRY(interpolate_property(*from_input_values[i], *to_input_values[i], delta)));
return TransformationStyleValue::create(from_transform.transform_function(), move(interpolated_values));
}
case StyleValue::Type::ValueList: {
auto& from_list = from.as_value_list();
auto& to_list = to.as_value_list();
if (from_list.size() != to_list.size())
return from;
StyleValueVector interpolated_values;
interpolated_values.ensure_capacity(from_list.size());
for (size_t i = 0; i < from_list.size(); ++i)
interpolated_values.append(TRY(interpolate_property(from_list.values()[i], to_list.values()[i], delta)));
return StyleValueList::create(move(interpolated_values), from_list.separator());
}
default:
return from;
}
}
ErrorOr<void> StyleComputer::Animation::collect_into(StyleProperties& style_properties, RuleCache const& rule_cache) const
{
if (remaining_delay.to_milliseconds() != 0)
return {};
auto matching_keyframes = rule_cache.rules_by_animation_keyframes.get(name);
if (!matching_keyframes.has_value())
return {};
auto& keyframes = matching_keyframes.value()->keyframes_by_key;
auto key = static_cast<u64>(progress.value() * AnimationKeyFrameKeyScaleFactor);
auto matching_keyframe_it = keyframes.find_largest_not_above_iterator(key);
if (matching_keyframe_it.is_end()) {
if constexpr (LIBWEB_CSS_ANIMATION_DEBUG) {
dbgln(" Did not find any start keyframe for the current state ({}) :(", key);
dbgln(" (have {} keyframes)", keyframes.size());
for (auto it = keyframes.begin(); it != keyframes.end(); ++it)
dbgln(" - {}", it.key());
}
return {};
}
auto keyframe_start = matching_keyframe_it.key();
auto keyframe_values = *matching_keyframe_it;
auto keyframe_end_it = ++matching_keyframe_it;
if (keyframe_end_it.is_end()) {
if constexpr (LIBWEB_CSS_ANIMATION_DEBUG) {
dbgln(" Did not find any end keyframe for the current state ({}) :(", key);
dbgln(" (have {} keyframes)", keyframes.size());
for (auto it = keyframes.begin(); it != keyframes.end(); ++it)
dbgln(" - {}", it.key());
}
return {};
}
auto keyframe_end = keyframe_end_it.key();
auto keyframe_end_values = *keyframe_end_it;
auto progress_in_keyframe = (progress.value() * AnimationKeyFrameKeyScaleFactor - keyframe_start) / (keyframe_end - keyframe_start);
auto valid_properties = 0;
for (auto const& property : keyframe_values.resolved_properties) {
if (property.has<Empty>())
continue;
valid_properties++;
}
dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "Animation {} contains {} properties to interpolate, progress = {}%", name, valid_properties, progress_in_keyframe * 100);
UnderlyingType<PropertyID> property_id_value = 0;
for (auto const& property : keyframe_values.resolved_properties) {
auto property_id = static_cast<PropertyID>(property_id_value++);
if (property.has<Empty>())
continue;
auto resolve_property = [&](auto& property) {
return property.visit(
[](Empty) -> RefPtr<StyleValue const> { VERIFY_NOT_REACHED(); },
[&](AnimationKeyFrameSet::ResolvedKeyFrame::UseInitial) {
if (auto value = initial_state[to_underlying(property_id)])
return value;
auto value = style_properties.maybe_null_property(property_id);
initial_state[to_underlying(property_id)] = value;
return value;
},
[&](RefPtr<StyleValue const> value) { return value; });
};
auto resolved_start_property = resolve_property(property);
auto const& end_property = keyframe_end_values.resolved_properties[to_underlying(property_id)];
if (end_property.has<Empty>()) {
if (resolved_start_property) {
style_properties.set_property(property_id, resolved_start_property.release_nonnull());
dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "No end property for property {}, using {}", string_from_property_id(property_id), resolved_start_property->to_string());
}
continue;
}
auto resolved_end_property = resolve_property(end_property);
if (!resolved_start_property || !resolved_end_property)
continue;
auto start = resolved_start_property.release_nonnull();
auto end = resolved_end_property.release_nonnull();
// FIXME: This should be a function of the animation-timing-function.
auto next_value = TRY(interpolate_property(*start, *end, progress_in_keyframe));
dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "Interpolated value for property {} at {}: {} -> {} = {}", string_from_property_id(property_id), progress_in_keyframe, start->to_string(), end->to_string(), next_value->to_string());
style_properties.set_property(property_id, next_value);
}
return {};
}
bool StyleComputer::Animation::is_done() const
{
return progress.as_fraction() >= 0.9999f && iteration_count.has_value() && iteration_count.value() == 0;
}
void StyleComputer::ensure_animation_timer() const
{
constexpr static auto timer_delay_ms = 1000 / 60;
if (!m_animation_driver_timer) {
m_animation_driver_timer = Platform::Timer::create_repeating(timer_delay_ms, [this] {
HashTable<AnimationKey> animations_to_remove;
HashTable<DOM::Element*> owning_elements_to_invalidate;
for (auto& it : m_active_animations) {
if (!it.value->owning_element) {
// The element disappeared since we last ran, just discard the animation.
animations_to_remove.set(it.key);
continue;
}
auto transition = it.value->step(CSS::Time { timer_delay_ms, CSS::Time::Type::Ms });
owning_elements_to_invalidate.set(it.value->owning_element);
switch (transition) {
case AnimationStepTransition::NoTransition:
break;
case AnimationStepTransition::IdleOrBeforeToActive:
// FIXME: Dispatch `animationstart`.
break;
case AnimationStepTransition::IdleOrBeforeToAfter:
// FIXME: Dispatch `animationstart` then `animationend`.
break;
case AnimationStepTransition::ActiveToBefore:
// FIXME: Dispatch `animationend`.
break;
case AnimationStepTransition::ActiveToActiveChangingTheIteration:
// FIXME: Dispatch `animationiteration`.
break;
case AnimationStepTransition::ActiveToAfter:
// FIXME: Dispatch `animationend`.
break;
case AnimationStepTransition::AfterToActive:
// FIXME: Dispatch `animationstart`.
break;
case AnimationStepTransition::AfterToBefore:
// FIXME: Dispatch `animationstart` then `animationend`.
break;
case AnimationStepTransition::Cancelled:
// FIXME: Dispatch `animationcancel`.
break;
}
if (it.value->is_done())
animations_to_remove.set(it.key);
}
for (auto key : animations_to_remove)
m_active_animations.remove(key);
for (auto* element : owning_elements_to_invalidate)
element->invalidate_style();
});
}
m_animation_driver_timer->start();
}
// https://www.w3.org/TR/css-cascade/#cascading
ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element& element, Optional<CSS::Selector::PseudoElement> pseudo_element, bool& did_match_any_pseudo_element_rules, ComputeStyleMode mode) const
{
@ -1036,7 +1327,56 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM
// Normal author declarations
cascade_declarations(style, element, pseudo_element, matching_rule_set.author_rules, CascadeOrigin::Author, Important::No);
// FIXME: Animation declarations [css-animations-1]
// Animation declarations [css-animations-2]
if (auto animation_name = style.maybe_null_property(PropertyID::AnimationName)) {
ensure_animation_timer();
if (auto source_declaration = style.property_source_declaration(PropertyID::AnimationName)) {
AnimationKey animation_key {
.source_declaration = source_declaration,
.element = &element,
};
if (auto name = TRY(animation_name->to_string()); !name.is_empty()) {
auto active_animation = m_active_animations.get(animation_key);
if (!active_animation.has_value()) {
// New animation!
CSS::Time duration { 0, CSS::Time::Type::S };
if (auto duration_value = style.maybe_null_property(PropertyID::AnimationDuration); duration_value && duration_value->is_time())
duration = duration_value->as_time().time();
CSS::Time delay { 0, CSS::Time::Type::S };
if (auto delay_value = style.maybe_null_property(PropertyID::AnimationDelay); delay_value && delay_value->is_time())
delay = delay_value->as_time().time();
Optional<size_t> iteration_count = 1;
if (auto iteration_count_value = style.maybe_null_property(PropertyID::AnimationIterationCount); iteration_count_value) {
if (iteration_count_value->is_identifier() && iteration_count_value->to_identifier() == ValueID::Infinite)
iteration_count = {};
else if (iteration_count_value->is_numeric())
iteration_count = static_cast<size_t>(iteration_count_value->as_numeric().number());
}
auto animation = make<Animation>(Animation {
.name = move(name),
.duration = duration,
.delay = delay,
.iteration_count = iteration_count,
.direction = Animation::Direction::Normal,
.fill_mode = Animation::FillMode::None,
.owning_element = TRY(element.try_make_weak_ptr<DOM::Element>()),
.progress = CSS::Percentage(0),
.remaining_delay = delay,
});
active_animation = animation;
m_active_animations.set(animation_key, move(animation));
}
TRY((*active_animation)->collect_into(style, rule_cache_for_cascade_origin(CascadeOrigin::Author)));
} else {
m_active_animations.remove(animation_key);
}
}
}
// Important author declarations
cascade_declarations(style, element, pseudo_element, matching_rule_set.author_rules, CascadeOrigin::Author, Important::Yes);
@ -1709,6 +2049,93 @@ NonnullOwnPtr<StyleComputer::RuleCache> StyleComputer::make_rule_cache_for_casca
}
++rule_index;
});
sheet.for_each_effective_keyframes_at_rule([&](CSSKeyframesRule const& rule) {
auto keyframe_set = make<AnimationKeyFrameSet>();
AnimationKeyFrameSet::ResolvedKeyFrame resolved_keyframe;
// Forwards pass, resolve all the user-specified keyframe properties.
for (auto const& keyframe : rule.keyframes()) {
auto key = static_cast<u64>(keyframe->key().value() * AnimationKeyFrameKeyScaleFactor);
auto keyframe_rule = keyframe->style();
if (!is<PropertyOwningCSSStyleDeclaration>(*keyframe_rule))
continue;
auto current_keyframe = resolved_keyframe;
auto& keyframe_style = static_cast<PropertyOwningCSSStyleDeclaration const&>(*keyframe_rule);
for (auto& property : keyframe_style.properties())
current_keyframe.resolved_properties[to_underlying(property.property_id)] = property.value;
resolved_keyframe = move(current_keyframe);
keyframe_set->keyframes_by_key.insert(key, resolved_keyframe);
}
// If there is no 'from' keyframe, make a synthetic one.
auto made_a_synthetic_from_keyframe = false;
if (!keyframe_set->keyframes_by_key.find(0)) {
keyframe_set->keyframes_by_key.insert(0, AnimationKeyFrameSet::ResolvedKeyFrame());
made_a_synthetic_from_keyframe = true;
}
// Backwards pass, resolve all the implied properties, go read <https://drafts.csswg.org/css-animations-2/#keyframe-processing> to see why.
auto first = true;
for (auto const& keyframe : rule.keyframes().in_reverse()) {
auto key = static_cast<u64>(keyframe->key().value() * AnimationKeyFrameKeyScaleFactor);
auto keyframe_rule = keyframe->style();
if (!is<PropertyOwningCSSStyleDeclaration>(*keyframe_rule))
continue;
// The last keyframe is already fully resolved.
if (first) {
first = false;
continue;
}
auto next_keyframe = resolved_keyframe;
auto& current_keyframes = *keyframe_set->keyframes_by_key.find(key);
for (auto it = next_keyframe.resolved_properties.begin(); !it.is_end(); ++it) {
auto& current_property = current_keyframes.resolved_properties[it.index()];
if (!current_property.has<Empty>() || it->has<Empty>())
continue;
if (key == 0)
current_property = AnimationKeyFrameSet::ResolvedKeyFrame::UseInitial();
else
current_property = *it;
}
resolved_keyframe = current_keyframes;
}
if (made_a_synthetic_from_keyframe && !first) {
auto next_keyframe = resolved_keyframe;
auto& current_keyframes = *keyframe_set->keyframes_by_key.find(0);
for (auto it = next_keyframe.resolved_properties.begin(); !it.is_end(); ++it) {
auto& current_property = current_keyframes.resolved_properties[it.index()];
if (!current_property.has<Empty>() || it->has<Empty>())
continue;
current_property = AnimationKeyFrameSet::ResolvedKeyFrame::UseInitial();
}
resolved_keyframe = current_keyframes;
}
if constexpr (LIBWEB_CSS_DEBUG) {
dbgln("Resolved keyframe set '{}' into {} keyframes:", rule.name(), keyframe_set->keyframes_by_key.size());
for (auto it = keyframe_set->keyframes_by_key.begin(); it != keyframe_set->keyframes_by_key.end(); ++it) {
size_t props = 0;
for (auto& entry : it->resolved_properties)
props += !entry.has<Empty>();
dbgln(" - keyframe {}: {} properties", it.key(), props);
}
}
rule_cache->rules_by_animation_keyframes.set(rule.name(), move(keyframe_set));
});
++style_sheet_index;
});