mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 14:07:45 +00:00
LibWeb: Fully implement CSS animation-timing-function
This implements all the timing functions, and hopefully all their quirks. Also changes the animation demo to use some funny cubic timing functions.
This commit is contained in:
parent
efa55673cd
commit
0c14698028
4 changed files with 237 additions and 13 deletions
|
@ -14,7 +14,7 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
background: url(https://serenityos.org/buggie.png) no-repeat left center;
|
background: url(https://serenityos.org/buggie.png) no-repeat left center;
|
||||||
background-size: contain;
|
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-0 { animation-delay: 0.9s; }
|
||||||
.offset-1 { animation-delay: 1.7s; }
|
.offset-1 { animation-delay: 1.7s; }
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
height: 50%;
|
height: 50%;
|
||||||
background: url(https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/LadyBall-SerenityOS.png/240px-LadyBall-SerenityOS.png) no-repeat left center;
|
background: url(https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/LadyBall-SerenityOS.png/240px-LadyBall-SerenityOS.png) no-repeat left center;
|
||||||
scale: 50%;
|
scale: 50%;
|
||||||
animation: ladyball 9s linear infinite;
|
animation: ladyball 9s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@keyframes buggie {
|
@keyframes buggie {
|
||||||
0% { transform: translateX(0vw); opacity: 1; }
|
0% { transform: translateX(0vw); opacity: 1; }
|
||||||
|
|
|
@ -122,13 +122,8 @@
|
||||||
"affects-layout": true,
|
"affects-layout": true,
|
||||||
"inherited": false,
|
"inherited": false,
|
||||||
"initial": "ease",
|
"initial": "ease",
|
||||||
"__comment": "FIXME: This is like...wrong.",
|
"valid-types": [
|
||||||
"valid-identifiers": [
|
"easing-function"
|
||||||
"ease",
|
|
||||||
"linear",
|
|
||||||
"ease-in-out",
|
|
||||||
"ease-in",
|
|
||||||
"ease-out"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <AK/BinarySearch.h>
|
||||||
#include <AK/Debug.h>
|
#include <AK/Debug.h>
|
||||||
#include <AK/Error.h>
|
#include <AK/Error.h>
|
||||||
#include <AK/Find.h>
|
#include <AK/Find.h>
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
#include <LibWeb/CSS/StyleValues/ColorStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/ColorStyleValue.h>
|
||||||
#include <LibWeb/CSS/StyleValues/CompositeStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/CompositeStyleValue.h>
|
||||||
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
|
||||||
|
#include <LibWeb/CSS/StyleValues/EasingStyleValue.h>
|
||||||
#include <LibWeb/CSS/StyleValues/FilterValueListStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/FilterValueListStyleValue.h>
|
||||||
#include <LibWeb/CSS/StyleValues/FlexFlowStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/FlexFlowStyleValue.h>
|
||||||
#include <LibWeb/CSS/StyleValues/FlexStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/FlexStyleValue.h>
|
||||||
|
@ -1341,27 +1343,139 @@ bool StyleComputer::Animation::is_done() const
|
||||||
return progress.as_fraction() >= 0.9999 && iteration_count.has_value() && iteration_count.value() == 0;
|
return progress.as_fraction() >= 0.9999 && iteration_count.has_value() && iteration_count.value() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Magic values from <https://www.w3.org/TR/css-easing-1/#valdef-cubic-bezier-easing-function-ease>
|
||||||
|
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
|
float StyleComputer::Animation::compute_output_progress(float input_progress) const
|
||||||
{
|
{
|
||||||
auto output_progress = input_progress;
|
auto output_progress = input_progress;
|
||||||
|
auto going_forwards = true;
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case AnimationDirection::Alternate:
|
case AnimationDirection::Alternate:
|
||||||
if (current_iteration % 2 == 0)
|
if (current_iteration % 2 == 0) {
|
||||||
output_progress = 1.0f - output_progress;
|
output_progress = 1.0f - output_progress;
|
||||||
|
going_forwards = false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case AnimationDirection::AlternateReverse:
|
case AnimationDirection::AlternateReverse:
|
||||||
if (current_iteration % 2 == 1)
|
if (current_iteration % 2 == 1) {
|
||||||
output_progress = 1.0f - output_progress;
|
output_progress = 1.0f - output_progress;
|
||||||
|
going_forwards = false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case AnimationDirection::Normal:
|
case AnimationDirection::Normal:
|
||||||
break;
|
break;
|
||||||
case AnimationDirection::Reverse:
|
case AnimationDirection::Reverse:
|
||||||
output_progress = 1.0f - output_progress;
|
output_progress = 1.0f - output_progress;
|
||||||
|
going_forwards = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: This should also be a function of the animation-timing-function, if not during the delay.
|
if (remaining_delay.to_milliseconds() != 0)
|
||||||
return output_progress;
|
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<float>(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<float>(jumps))
|
||||||
|
current_step = static_cast<float>(jumps);
|
||||||
|
return current_step / static_cast<float>(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<double>(output_progress));
|
||||||
|
return static_cast<float>(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
|
void StyleComputer::ensure_animation_timer() const
|
||||||
|
@ -1543,11 +1657,94 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM
|
||||||
direction = *direction_value;
|
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<size_t>(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>(Animation {
|
auto animation = make<Animation>(Animation {
|
||||||
.name = move(name),
|
.name = move(name),
|
||||||
.duration = duration,
|
.duration = duration,
|
||||||
.delay = delay,
|
.delay = delay,
|
||||||
.iteration_count = iteration_count,
|
.iteration_count = iteration_count,
|
||||||
|
.timing_function = timing_function,
|
||||||
.direction = direction,
|
.direction = direction,
|
||||||
.fill_mode = fill_mode,
|
.fill_mode = fill_mode,
|
||||||
.owning_element = TRY(element.try_make_weak_ptr<DOM::Element>()),
|
.owning_element = TRY(element.try_make_weak_ptr<DOM::Element>()),
|
||||||
|
|
|
@ -95,6 +95,37 @@ public:
|
||||||
DOM::Element const* element;
|
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<CachedSample, 64> 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<Linear, CubicBezier, Steps> timing_function;
|
||||||
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class ComputeStyleMode {
|
enum class ComputeStyleMode {
|
||||||
Normal,
|
Normal,
|
||||||
|
@ -202,6 +233,7 @@ private:
|
||||||
Optional<CSS::Time> duration; // "auto" if not set.
|
Optional<CSS::Time> duration; // "auto" if not set.
|
||||||
CSS::Time delay;
|
CSS::Time delay;
|
||||||
Optional<size_t> iteration_count; // Infinite if not set.
|
Optional<size_t> iteration_count; // Infinite if not set.
|
||||||
|
AnimationTiming timing_function;
|
||||||
CSS::AnimationDirection direction;
|
CSS::AnimationDirection direction;
|
||||||
CSS::AnimationFillMode fill_mode;
|
CSS::AnimationFillMode fill_mode;
|
||||||
WeakPtr<DOM::Element> owning_element;
|
WeakPtr<DOM::Element> owning_element;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue