diff --git a/Base/res/html/misc/css-animations.html b/Base/res/html/misc/css-animations.html
index 99862f96c1..e431f66c5e 100644
--- a/Base/res/html/misc/css-animations.html
+++ b/Base/res/html/misc/css-animations.html
@@ -14,7 +14,7 @@
opacity: 0;
background: url(https://serenityos.org/buggie.png) no-repeat left center;
background-size: contain;
- animation: buggie 10s linear infinite;
+ animation: buggie 10s cubic-bezier(0.1, -0.6, 0.2, -0.2) infinite;
}
.offset-0 { animation-delay: 0.9s; }
.offset-1 { animation-delay: 1.7s; }
@@ -27,7 +27,7 @@
height: 50%;
background: url(https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/LadyBall-SerenityOS.png/240px-LadyBall-SerenityOS.png) no-repeat left center;
scale: 50%;
- animation: ladyball 9s linear infinite;
+ animation: ladyball 9s ease-in-out infinite;
}
@keyframes buggie {
0% { transform: translateX(0vw); opacity: 1; }
diff --git a/Userland/Libraries/LibWeb/CSS/Properties.json b/Userland/Libraries/LibWeb/CSS/Properties.json
index 363a233352..f9d28974ab 100644
--- a/Userland/Libraries/LibWeb/CSS/Properties.json
+++ b/Userland/Libraries/LibWeb/CSS/Properties.json
@@ -122,13 +122,8 @@
"affects-layout": true,
"inherited": false,
"initial": "ease",
- "__comment": "FIXME: This is like...wrong.",
- "valid-identifiers": [
- "ease",
- "linear",
- "ease-in-out",
- "ease-in",
- "ease-out"
+ "valid-types": [
+ "easing-function"
]
},
"appearance": {
diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
index 4d02d52fd4..9c6cd41d73 100644
--- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
+++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
@@ -6,6 +6,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
+#include
#include
#include
#include
@@ -35,6 +36,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -1341,27 +1343,139 @@ bool StyleComputer::Animation::is_done() const
return progress.as_fraction() >= 0.9999 && iteration_count.has_value() && iteration_count.value() == 0;
}
+// NOTE: Magic values from
+static auto ease_timing_function = StyleComputer::AnimationTiming::CubicBezier { 0.25, 0.1, 0.25, 1.0 };
+static auto ease_in_timing_function = StyleComputer::AnimationTiming::CubicBezier { 0.42, 0.0, 1.0, 1.0 };
+static auto ease_out_timing_function = StyleComputer::AnimationTiming::CubicBezier { 0.0, 0.0, 0.58, 1.0 };
+static auto ease_in_out_timing_function = StyleComputer::AnimationTiming::CubicBezier { 0.42, 0.0, 0.58, 1.0 };
+
float StyleComputer::Animation::compute_output_progress(float input_progress) const
{
auto output_progress = input_progress;
+ auto going_forwards = true;
switch (direction) {
case AnimationDirection::Alternate:
- if (current_iteration % 2 == 0)
+ if (current_iteration % 2 == 0) {
output_progress = 1.0f - output_progress;
+ going_forwards = false;
+ }
break;
case AnimationDirection::AlternateReverse:
- if (current_iteration % 2 == 1)
+ if (current_iteration % 2 == 1) {
output_progress = 1.0f - output_progress;
+ going_forwards = false;
+ }
break;
case AnimationDirection::Normal:
break;
case AnimationDirection::Reverse:
output_progress = 1.0f - output_progress;
+ going_forwards = false;
break;
}
- // FIXME: This should also be a function of the animation-timing-function, if not during the delay.
- return output_progress;
+ if (remaining_delay.to_milliseconds() != 0)
+ return output_progress;
+
+ return timing_function.timing_function.visit(
+ [&](AnimationTiming::Linear) { return output_progress; },
+ [&](AnimationTiming::Steps const& steps) {
+ auto before_flag = (current_state == AnimationState::Before && going_forwards) || (current_state == AnimationState::After && !going_forwards);
+ auto progress_step = output_progress * static_cast(steps.number_of_steps);
+ auto current_step = floorf(progress_step);
+ if (steps.jump_at_start)
+ current_step += 1;
+ if (before_flag && truncf(progress_step) == progress_step)
+ current_step -= 1;
+ if (output_progress >= 0 && current_step < 0)
+ current_step = 0;
+ size_t jumps;
+ if (steps.jump_at_start ^ steps.jump_at_end)
+ jumps = steps.number_of_steps;
+ else if (steps.jump_at_start && steps.jump_at_end)
+ jumps = steps.number_of_steps + 1;
+ else
+ jumps = steps.number_of_steps - 1;
+
+ if (output_progress <= 1 && current_step > static_cast(jumps))
+ current_step = static_cast(jumps);
+ return current_step / static_cast(steps.number_of_steps);
+ },
+ [&](AnimationTiming::CubicBezier const& bezier) {
+ // Special cases first:
+ if (bezier == AnimationTiming::CubicBezier { 0.0, 0.0, 1.0, 1.0 })
+ return output_progress;
+ // FIXME: This is quite inefficient on memory and CPU, find a better way to do this.
+ auto sample = bezier.sample_around(static_cast(output_progress));
+ return static_cast(sample.y);
+ });
+}
+
+static double cubic_bezier_at(double x1, double x2, double t)
+{
+ auto a = 1.0 - 3.0 * x2 + 3.0 * x1;
+ auto b = 3.0 * x2 - 6.0 * x1;
+ auto c = 3.0 * x1;
+
+ auto t2 = t * t;
+ auto t3 = t2 * t;
+
+ return (a * t3) + (b * t2) + (c * t);
+}
+
+StyleComputer::AnimationTiming::CubicBezier::CachedSample StyleComputer::AnimationTiming::CubicBezier::sample_around(double x) const
+{
+ x = clamp(x, 0, 1);
+
+ auto solve = [&](auto t) {
+ auto x = cubic_bezier_at(x1, x2, t);
+ auto y = cubic_bezier_at(y1, y2, t);
+ return CachedSample { x, y, t };
+ };
+
+ if (m_cached_x_samples.is_empty())
+ m_cached_x_samples.append(solve(0.));
+
+ size_t nearby_index = 0;
+ if (auto found = binary_search(m_cached_x_samples, x, &nearby_index, [](auto x, auto& sample) {
+ if (x > sample.x)
+ return 1;
+ if (x < sample.x)
+ return -1;
+ return 0;
+ }))
+ return *found;
+
+ if (nearby_index == m_cached_x_samples.size() || nearby_index + 1 == m_cached_x_samples.size()) {
+ // Produce more samples until we have enough.
+ auto last_t = m_cached_x_samples.is_empty() ? 0 : m_cached_x_samples.last().t;
+ auto last_x = m_cached_x_samples.is_empty() ? 0 : m_cached_x_samples.last().x;
+ while (last_x <= x) {
+ last_t += 1. / 60.;
+ auto solution = solve(last_t);
+ m_cached_x_samples.append(solution);
+ last_x = solution.x;
+ }
+
+ if (auto found = binary_search(m_cached_x_samples, x, &nearby_index, [](auto x, auto& sample) {
+ if (x > sample.x)
+ return 1;
+ if (x < sample.x)
+ return -1;
+ return 0;
+ }))
+ return *found;
+ }
+
+ // We have two samples on either side of the x value we want, so we can linearly interpolate between them.
+ auto& sample1 = m_cached_x_samples[nearby_index];
+ auto& sample2 = m_cached_x_samples[nearby_index + 1];
+ auto factor = (x - sample1.x) / (sample2.x - sample1.x);
+ return CachedSample {
+ x,
+ clamp(sample1.y + factor * (sample2.y - sample1.y), 0, 1),
+ sample1.t + factor * (sample2.t - sample1.t),
+ };
}
void StyleComputer::ensure_animation_timer() const
@@ -1543,11 +1657,94 @@ ErrorOr StyleComputer::compute_cascaded_values(StyleProperties& style, DOM
direction = *direction_value;
}
+ AnimationTiming timing_function { ease_timing_function };
+ if (auto timing_property = style.maybe_null_property(PropertyID::AnimationTimingFunction); timing_property && timing_property->is_easing()) {
+ auto& easing_value = timing_property->as_easing();
+ switch (easing_value.easing_function()) {
+ case EasingFunction::Linear:
+ timing_function = AnimationTiming { AnimationTiming::Linear {} };
+ break;
+ case EasingFunction::Ease:
+ timing_function = AnimationTiming { ease_timing_function };
+ break;
+ case EasingFunction::EaseIn:
+ timing_function = AnimationTiming { ease_in_timing_function };
+ break;
+ case EasingFunction::EaseOut:
+ timing_function = AnimationTiming { ease_out_timing_function };
+ break;
+ case EasingFunction::EaseInOut:
+ timing_function = AnimationTiming { ease_in_out_timing_function };
+ break;
+ case EasingFunction::CubicBezier: {
+ auto values = easing_value.values();
+ timing_function = AnimationTiming {
+ AnimationTiming::CubicBezier {
+ values[0]->as_number().number(),
+ values[1]->as_number().number(),
+ values[2]->as_number().number(),
+ values[3]->as_number().number(),
+ },
+ };
+ break;
+ }
+ case EasingFunction::Steps: {
+ auto values = easing_value.values();
+ auto jump_at_start = false;
+ auto jump_at_end = true;
+
+ if (values.size() > 1) {
+ auto identifier = values[1]->to_identifier();
+ switch (identifier) {
+ case ValueID::JumpStart:
+ case ValueID::Start:
+ jump_at_start = true;
+ jump_at_end = false;
+ break;
+ case ValueID::JumpEnd:
+ case ValueID::End:
+ jump_at_start = false;
+ jump_at_end = true;
+ break;
+ case ValueID::JumpNone:
+ jump_at_start = false;
+ jump_at_end = false;
+ break;
+ default:
+ break;
+ }
+ }
+
+ timing_function = AnimationTiming { AnimationTiming::Steps {
+ .number_of_steps = static_cast(max(values[0]->as_integer().integer(), !(jump_at_end && jump_at_start) ? 1 : 0)),
+ .jump_at_start = jump_at_start,
+ .jump_at_end = jump_at_end,
+ } };
+ break;
+ }
+ case EasingFunction::StepEnd:
+ timing_function = AnimationTiming { AnimationTiming::Steps {
+ .number_of_steps = 1,
+ .jump_at_start = false,
+ .jump_at_end = true,
+ } };
+ break;
+ case EasingFunction::StepStart:
+ timing_function = AnimationTiming { AnimationTiming::Steps {
+ .number_of_steps = 1,
+ .jump_at_start = true,
+ .jump_at_end = false,
+ } };
+ break;
+ }
+ }
+
auto animation = make(Animation {
.name = move(name),
.duration = duration,
.delay = delay,
.iteration_count = iteration_count,
+ .timing_function = timing_function,
.direction = direction,
.fill_mode = fill_mode,
.owning_element = TRY(element.try_make_weak_ptr()),
diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.h b/Userland/Libraries/LibWeb/CSS/StyleComputer.h
index 2cd1af44f1..1df1f3aa9b 100644
--- a/Userland/Libraries/LibWeb/CSS/StyleComputer.h
+++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.h
@@ -95,6 +95,37 @@ public:
DOM::Element const* element;
};
+ struct AnimationTiming {
+ struct Linear { };
+ struct CubicBezier {
+ // Regular parameters
+ double x1;
+ double y1;
+ double x2;
+ double y2;
+
+ struct CachedSample {
+ double x;
+ double y;
+ double t;
+ };
+ mutable Vector m_cached_x_samples = {};
+
+ CachedSample sample_around(double x) const;
+ bool operator==(CubicBezier const& other) const
+ {
+ return x1 == other.x1 && y1 == other.y1 && x2 == other.x2 && y2 == other.y2;
+ }
+ };
+ struct Steps {
+ size_t number_of_steps;
+ bool jump_at_start;
+ bool jump_at_end;
+ };
+
+ Variant timing_function;
+ };
+
private:
enum class ComputeStyleMode {
Normal,
@@ -202,6 +233,7 @@ private:
Optional duration; // "auto" if not set.
CSS::Time delay;
Optional iteration_count; // Infinite if not set.
+ AnimationTiming timing_function;
CSS::AnimationDirection direction;
CSS::AnimationFillMode fill_mode;
WeakPtr owning_element;