ladybird/Libraries/LibWeb/CSS/StyleValues/EasingStyleValue.cpp

431 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2022-2023, MacDue <macdue@dueutil.tech>
* Copyright (c) 2023, Ali Mohammad Pur <mpfard@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "EasingStyleValue.h"
#include <AK/BinarySearch.h>
#include <AK/StringBuilder.h>
namespace Web::CSS {
// https://drafts.csswg.org/css-easing-1/#valdef-easing-function-linear
EasingStyleValue::Linear EasingStyleValue::Linear::identity()
{
static Linear linear { { { 0, {}, false }, { 1, {}, false } } };
return linear;
}
// NOTE: Magic cubic bezier values from https://www.w3.org/TR/css-easing-1/#valdef-cubic-bezier-easing-function-ease
EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease()
{
static CubicBezier bezier { 0.25, 0.1, 0.25, 1.0 };
return bezier;
}
EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_in()
{
static CubicBezier bezier { 0.42, 0.0, 1.0, 1.0 };
return bezier;
}
EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_out()
{
static CubicBezier bezier { 0.0, 0.0, 0.58, 1.0 };
return bezier;
}
EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_in_out()
{
static CubicBezier bezier { 0.42, 0.0, 0.58, 1.0 };
return bezier;
}
EasingStyleValue::Steps EasingStyleValue::Steps::step_start()
{
static Steps steps { 1, Steps::Position::Start };
return steps;
}
EasingStyleValue::Steps EasingStyleValue::Steps::step_end()
{
static Steps steps { 1, Steps::Position::End };
return steps;
}
bool EasingStyleValue::CubicBezier::operator==(Web::CSS::EasingStyleValue::CubicBezier const& other) const
{
return x1 == other.x1 && y1 == other.y1 && x2 == other.x2 && y2 == other.y2;
}
// https://drafts.csswg.org/css-easing/#linear-canonicalization
EasingStyleValue::Linear::Linear(Vector<EasingStyleValue::Linear::Stop> stops)
{
// To canonicalize a linear() functions control points, perform the following:
// 1. If the first control point lacks an input progress value, set its input progress value to 0.
if (!stops.first().input.has_value())
stops.first().input = 0;
// 2. If the last control point lacks an input progress value, set its input progress value to 1.
if (!stops.last().input.has_value())
stops.last().input = 1;
// 3. If any control point has an input progress value that is less than
// the input progress value of any preceding control point,
// set its input progress value to the largest input progress value of any preceding control point.
double largest_input = 0;
for (auto stop : stops) {
if (stop.input.has_value()) {
if (stop.input.value() < largest_input) {
stop.input = largest_input;
} else {
largest_input = stop.input.value();
}
}
}
// 4. If any control point still lacks an input progress value,
// then for each contiguous run of such control points,
// set their input progress values so that they are evenly spaced
// between the preceding and following control points with input progress values.
Optional<size_t> run_start_idx;
for (size_t idx = 0; idx < stops.size(); idx++) {
auto stop = stops[idx];
if (stop.input.has_value() && run_start_idx.has_value()) {
// Note: this stop is immediately after a run
// set inputs of [start, idx-1] stops to be evenly spaced between start-1 and idx
auto start_input = stops[run_start_idx.value() - 1].input.value();
auto end_input = stops[idx].input.value();
auto run_stop_count = idx - run_start_idx.value() + 1;
auto delta = (end_input - start_input) / run_stop_count;
for (size_t run_idx = 0; run_idx < run_stop_count; run_idx++) {
stops[run_idx + run_start_idx.value() - 1].input = start_input + delta * run_idx;
}
run_start_idx = {};
} else if (!stop.input.has_value() && !run_start_idx.has_value()) {
// Note: this stop is the start of a run
run_start_idx = idx;
}
}
this->stops = move(stops);
}
// https://drafts.csswg.org/css-easing/#linear-easing-function-output
double EasingStyleValue::Linear::evaluate_at(double input_progress, bool before_flag) const
{
// To calculate linear easing output progress for a given linear easing function func,
// an input progress value inputProgress, and an optional before flag (defaulting to false),
// perform the following:
// 1. Let points be funcs control points.
// 2. If points holds only a single item, return the output progress value of that item.
if (stops.size() == 1)
return stops[0].output;
// 3. If inputProgress matches the input progress value of the first point in points,
// and the before flag is true, return the first points output progress value.
if (input_progress == stops[0].input.value() && before_flag)
return stops[0].output;
// 4. If inputProgress matches the input progress value of at least one point in points,
// return the output progress value of the last such point.
auto maybe_match = stops.last_matching([&](auto& stop) { return input_progress == stop.input.value(); });
if (maybe_match.has_value())
return maybe_match->output;
// 5. Otherwise, find two control points in points, A and B, which will be used for interpolation:
Stop A;
Stop B;
if (input_progress < stops[0].input.value()) {
// 1. If inputProgress is smaller than any input progress value in points,
// let A and B be the first two items in points.
// If A and B have the same input progress value, return As output progress value.
A = stops[0];
B = stops[1];
if (A.input == B.input)
return A.output;
} else if (input_progress > stops.last().input.value()) {
// 2. If inputProgress is larger than any input progress value in points,
// let A and B be the last two items in points.
// If A and B have the same input progress value, return Bs output progress value.
A = stops[stops.size() - 2];
B = stops[stops.size() - 1];
if (A.input == B.input)
return B.output;
} else {
// 3. Otherwise, let A be the last control point whose input progress value is smaller than inputProgress,
// and let B be the first control point whose input progress value is larger than inputProgress.
A = stops.last_matching([&](auto& stop) { return stop.input.value() < input_progress; }).value();
B = stops.first_matching([&](auto& stop) { return stop.input.value() > input_progress; }).value();
}
// 6. Linearly interpolate (or extrapolate) inputProgress along the line defined by A and B, and return the result.
auto factor = (input_progress - A.input.value()) / (B.input.value() - A.input.value());
return A.output + factor * (B.output - A.output);
}
// https://drafts.csswg.org/css-easing/#linear-easing-function-serializing
String EasingStyleValue::Linear::to_string() const
{
// The linear keyword is serialized as itself.
if (*this == identity())
return "linear"_string;
// To serialize a linear() function:
// 1. Let s be the string "linear(".
StringBuilder builder;
builder.append("linear("sv);
// 2. Serialize each control point of the function,
// concatenate the results using the separator ", ",
// and append the result to s.
bool first = true;
for (auto stop : stops) {
if (first) {
first = false;
} else {
builder.append(", "sv);
}
// To serialize a linear() control point:
// 1. Let s be the serialization, as a <number>, of the control points output progress value.
builder.appendff("{}", stop.output);
// 2. If the control point originally lacked an input progress value, return s.
// 3. Otherwise, append " " (U+0020 SPACE) to s,
// then serialize the control points input progress value as a <percentage> and append it to s.
if (stop.had_explicit_input) {
builder.appendff(" {}%", stop.input.value() * 100);
}
// 4. Return s.
}
// 4. Append ")" to s, and return it.
builder.append(')');
return MUST(builder.to_string());
}
double EasingStyleValue::CubicBezier::evaluate_at(double input_progress, bool) const
{
constexpr static auto 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);
};
// https://www.w3.org/TR/css-easing-1/#cubic-bezier-algo
// For input progress values outside the range [0, 1], the curve is extended infinitely using tangent of the curve
// at the closest endpoint as follows:
// - For input progress values less than zero,
if (input_progress < 0.0) {
// 1. If the x value of P1 is greater than zero, use a straight line that passes through P1 and P0 as the
// tangent.
if (x1 > 0.0)
return y1 / x1 * input_progress;
// 2. Otherwise, if the x value of P2 is greater than zero, use a straight line that passes through P2 and P0 as
// the tangent.
if (x2 > 0.0)
return y2 / x2 * input_progress;
// 3. Otherwise, let the output progress value be zero for all input progress values in the range [-∞, 0).
return 0.0;
}
// - For input progress values greater than one,
if (input_progress > 1.0) {
// 1. If the x value of P2 is less than one, use a straight line that passes through P2 and P3 as the tangent.
if (x2 < 1.0)
return (1.0 - y2) / (1.0 - x2) * (input_progress - 1.0) + 1.0;
// 2. Otherwise, if the x value of P1 is less than one, use a straight line that passes through P1 and P3 as the
// tangent.
if (x1 < 1.0)
return (1.0 - y1) / (1.0 - x1) * (input_progress - 1.0) + 1.0;
// 3. Otherwise, let the output progress value be one for all input progress values in the range (1, ∞].
return 1.0;
}
// Note: The spec does not specify the precise algorithm for calculating values in the range [0, 1]:
// "The evaluation of this curve is covered in many sources such as [FUND-COMP-GRAPHICS]."
auto x = input_progress;
auto solve = [&](auto t) {
auto x = cubic_bezier_at(x1, x2, t);
auto y = cubic_bezier_at(y1, y2, t);
return CubicBezier::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 >= NumericLimits<double>::epsilon())
return 1;
if (x - sample.x <= NumericLimits<double>::epsilon())
return -1;
return 0;
}))
return found->y;
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.last().t;
auto last_x = m_cached_x_samples.last().x;
while (last_x <= x && last_t < 1.0) {
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 >= NumericLimits<double>::epsilon())
return 1;
if (x - sample.x <= NumericLimits<double>::epsilon())
return -1;
return 0;
}))
return found->y;
}
// 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 sample1.y + factor * (sample2.y - sample1.y);
}
// https://drafts.csswg.org/css-easing/#bezier-serialization
String EasingStyleValue::CubicBezier::to_string() const
{
StringBuilder builder;
if (*this == CubicBezier::ease()) {
builder.append("ease"sv);
} else if (*this == CubicBezier::ease_in()) {
builder.append("ease-in"sv);
} else if (*this == CubicBezier::ease_out()) {
builder.append("ease-out"sv);
} else if (*this == CubicBezier::ease_in_out()) {
builder.append("ease-in-out"sv);
} else {
builder.appendff("cubic-bezier({}, {}, {}, {})", x1, y1, x2, y2);
}
return MUST(builder.to_string());
}
double EasingStyleValue::Steps::evaluate_at(double input_progress, bool before_flag) const
{
// https://www.w3.org/TR/css-easing-1/#step-easing-algo
// 1. Calculate the current step as floor(input progress value × steps).
auto current_step = floor(input_progress * number_of_intervals);
// 2. If the step position property is one of:
// - jump-start,
// - jump-both,
// increment current step by one.
if (position == Steps::Position::JumpStart || position == Steps::Position::Start || position == Steps::Position::JumpBoth)
current_step += 1;
// 3. If both of the following conditions are true:
// - the before flag is set, and
// - input progress value × steps mod 1 equals zero (that is, if input progress value × steps is integral), then
// decrement current step by one.
auto step_progress = input_progress * number_of_intervals;
if (before_flag && trunc(step_progress) == step_progress)
current_step -= 1;
// 4. If input progress value ≥ 0 and current step < 0, let current step be zero.
if (input_progress >= 0.0 && current_step < 0.0)
current_step = 0.0;
// 5. Calculate jumps based on the step position as follows:
// jump-start or jump-end -> steps
// jump-none -> steps - 1
// jump-both -> steps + 1
auto jumps = number_of_intervals;
if (position == Steps::Position::JumpNone) {
jumps--;
} else if (position == Steps::Position::JumpBoth) {
jumps++;
}
// 6. If input progress value ≤ 1 and current step > jumps, let current step be jumps.
if (input_progress <= 1.0 && current_step > jumps)
current_step = jumps;
// 7. The output progress value is current step / jumps.
return current_step / jumps;
}
// https://drafts.csswg.org/css-easing/#steps-serialization
String EasingStyleValue::Steps::to_string() const
{
StringBuilder builder;
// Unlike the other easing function keywords, step-start and step-end do not serialize as themselves.
// Instead, they serialize as "steps(1, start)" and "steps(1)", respectively.
if (*this == Steps::step_start()) {
builder.append("steps(1, start)"sv);
} else if (*this == Steps::step_end()) {
builder.append("steps(1)"sv);
} else {
auto position = [&] -> Optional<StringView> {
switch (this->position) {
case Steps::Position::JumpStart:
return "jump-start"sv;
case Steps::Position::JumpNone:
return "jump-none"sv;
case Steps::Position::JumpBoth:
return "jump-both"sv;
case Steps::Position::Start:
return "start"sv;
default:
return {};
}
}();
if (position.has_value()) {
builder.appendff("steps({}, {})", number_of_intervals, position.value());
} else {
builder.appendff("steps({})", number_of_intervals);
}
}
return MUST(builder.to_string());
}
double EasingStyleValue::Function::evaluate_at(double input_progress, bool before_flag) const
{
return visit(
[&](auto const& curve) {
return curve.evaluate_at(input_progress, before_flag);
});
}
String EasingStyleValue::Function::to_string() const
{
return visit(
[&](auto const& curve) {
return curve.to_string();
});
}
}