EasingStyleValue.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. /*
  2. * Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
  3. * Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
  4. * Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
  5. * Copyright (c) 2022-2023, MacDue <macdue@dueutil.tech>
  6. * Copyright (c) 2023, Ali Mohammad Pur <mpfard@serenityos.org>
  7. *
  8. * SPDX-License-Identifier: BSD-2-Clause
  9. */
  10. #include "EasingStyleValue.h"
  11. #include <AK/BinarySearch.h>
  12. #include <AK/StringBuilder.h>
  13. namespace Web::CSS {
  14. // NOTE: Magic cubic bezier values from https://www.w3.org/TR/css-easing-1/#valdef-cubic-bezier-easing-function-ease
  15. EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease()
  16. {
  17. static CubicBezier bezier { 0.25, 0.1, 0.25, 1.0 };
  18. return bezier;
  19. }
  20. EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_in()
  21. {
  22. static CubicBezier bezier { 0.42, 0.0, 1.0, 1.0 };
  23. return bezier;
  24. }
  25. EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_out()
  26. {
  27. static CubicBezier bezier { 0.0, 0.0, 0.58, 1.0 };
  28. return bezier;
  29. }
  30. EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_in_out()
  31. {
  32. static CubicBezier bezier { 0.42, 0.0, 0.58, 1.0 };
  33. return bezier;
  34. }
  35. EasingStyleValue::Steps EasingStyleValue::Steps::step_start()
  36. {
  37. static Steps steps { 1, Steps::Position::Start };
  38. return steps;
  39. }
  40. EasingStyleValue::Steps EasingStyleValue::Steps::step_end()
  41. {
  42. static Steps steps { 1, Steps::Position::End };
  43. return steps;
  44. }
  45. bool EasingStyleValue::CubicBezier::operator==(Web::CSS::EasingStyleValue::CubicBezier const& other) const
  46. {
  47. return x1 == other.x1 && y1 == other.y1 && x2 == other.x2 && y2 == other.y2;
  48. }
  49. double EasingStyleValue::Function::evaluate_at(double input_progress, bool before_flag) const
  50. {
  51. constexpr static auto cubic_bezier_at = [](double x1, double x2, double t) {
  52. auto a = 1.0 - 3.0 * x2 + 3.0 * x1;
  53. auto b = 3.0 * x2 - 6.0 * x1;
  54. auto c = 3.0 * x1;
  55. auto t2 = t * t;
  56. auto t3 = t2 * t;
  57. return (a * t3) + (b * t2) + (c * t);
  58. };
  59. return visit(
  60. [&](Linear const&) { return input_progress; },
  61. [&](CubicBezier const& bezier) {
  62. auto const& [x1, y1, x2, y2, cached_x_samples] = bezier;
  63. // https://www.w3.org/TR/css-easing-1/#cubic-bezier-algo
  64. // For input progress values outside the range [0, 1], the curve is extended infinitely using tangent of the curve
  65. // at the closest endpoint as follows:
  66. // - For input progress values less than zero,
  67. if (input_progress < 0.0) {
  68. // 1. If the x value of P1 is greater than zero, use a straight line that passes through P1 and P0 as the
  69. // tangent.
  70. if (x1 > 0.0)
  71. return y1 / x1 * input_progress;
  72. // 2. Otherwise, if the x value of P2 is greater than zero, use a straight line that passes through P2 and P0 as
  73. // the tangent.
  74. if (x2 > 0.0)
  75. return y2 / x2 * input_progress;
  76. // 3. Otherwise, let the output progress value be zero for all input progress values in the range [-∞, 0).
  77. return 0.0;
  78. }
  79. // - For input progress values greater than one,
  80. if (input_progress > 1.0) {
  81. // 1. If the x value of P2 is less than one, use a straight line that passes through P2 and P3 as the tangent.
  82. if (x2 < 1.0)
  83. return (1.0 - y2) / (1.0 - x2) * (input_progress - 1.0) + 1.0;
  84. // 2. Otherwise, if the x value of P1 is less than one, use a straight line that passes through P1 and P3 as the
  85. // tangent.
  86. if (x1 < 1.0)
  87. return (1.0 - y1) / (1.0 - x1) * (input_progress - 1.0) + 1.0;
  88. // 3. Otherwise, let the output progress value be one for all input progress values in the range (1, ∞].
  89. return 1.0;
  90. }
  91. // Note: The spec does not specify the precise algorithm for calculating values in the range [0, 1]:
  92. // "The evaluation of this curve is covered in many sources such as [FUND-COMP-GRAPHICS]."
  93. auto x = input_progress;
  94. auto solve = [&](auto t) {
  95. auto x = cubic_bezier_at(bezier.x1, bezier.x2, t);
  96. auto y = cubic_bezier_at(bezier.y1, bezier.y2, t);
  97. return CubicBezier::CachedSample { x, y, t };
  98. };
  99. if (cached_x_samples.is_empty())
  100. cached_x_samples.append(solve(0.));
  101. size_t nearby_index = 0;
  102. if (auto found = binary_search(cached_x_samples, x, &nearby_index, [](auto x, auto& sample) {
  103. if (x > sample.x)
  104. return 1;
  105. if (x < sample.x)
  106. return -1;
  107. return 0;
  108. }))
  109. return found->y;
  110. if (nearby_index == cached_x_samples.size() || nearby_index + 1 == cached_x_samples.size()) {
  111. // Produce more samples until we have enough.
  112. auto last_t = cached_x_samples.last().t;
  113. auto last_x = cached_x_samples.last().x;
  114. while (last_x <= x && last_t < 1.0) {
  115. last_t += 1. / 60.;
  116. auto solution = solve(last_t);
  117. cached_x_samples.append(solution);
  118. last_x = solution.x;
  119. }
  120. if (auto found = binary_search(cached_x_samples, x, &nearby_index, [](auto x, auto& sample) {
  121. if (x > sample.x)
  122. return 1;
  123. if (x < sample.x)
  124. return -1;
  125. return 0;
  126. }))
  127. return found->y;
  128. }
  129. // We have two samples on either side of the x value we want, so we can linearly interpolate between them.
  130. auto& sample1 = cached_x_samples[nearby_index];
  131. auto& sample2 = cached_x_samples[nearby_index + 1];
  132. auto factor = (x - sample1.x) / (sample2.x - sample1.x);
  133. return sample1.y + factor * (sample2.y - sample1.y);
  134. },
  135. [&](Steps const& steps) {
  136. // https://www.w3.org/TR/css-easing-1/#step-easing-algo
  137. // 1. Calculate the current step as floor(input progress value × steps).
  138. auto [number_of_steps, position] = steps;
  139. auto current_step = floor(input_progress * number_of_steps);
  140. // 2. If the step position property is one of:
  141. // - jump-start,
  142. // - jump-both,
  143. // increment current step by one.
  144. if (position == Steps::Position::JumpStart || position == Steps::Position::JumpBoth)
  145. current_step += 1;
  146. // 3. If both of the following conditions are true:
  147. // - the before flag is set, and
  148. // - input progress value × steps mod 1 equals zero (that is, if input progress value × steps is integral), then
  149. // decrement current step by one.
  150. auto step_progress = input_progress * number_of_steps;
  151. if (before_flag && trunc(step_progress) == step_progress)
  152. current_step -= 1;
  153. // 4. If input progress value ≥ 0 and current step < 0, let current step be zero.
  154. if (input_progress >= 0.0 && current_step < 0.0)
  155. current_step = 0.0;
  156. // 5. Calculate jumps based on the step position as follows:
  157. // jump-start or jump-end -> steps
  158. // jump-none -> steps - 1
  159. // jump-both -> steps + 1
  160. auto jumps = steps.number_of_intervals;
  161. if (position == Steps::Position::JumpNone) {
  162. jumps--;
  163. } else if (position == Steps::Position::JumpBoth) {
  164. jumps++;
  165. }
  166. // 6. If input progress value ≤ 1 and current step > jumps, let current step be jumps.
  167. if (input_progress <= 1.0 && current_step > jumps)
  168. current_step = jumps;
  169. // 7. The output progress value is current step / jumps.
  170. return current_step / jumps;
  171. });
  172. }
  173. String EasingStyleValue::Function::to_string() const
  174. {
  175. StringBuilder builder;
  176. visit(
  177. [&](Linear const& linear) {
  178. builder.append("linear"sv);
  179. if (!linear.stops.is_empty()) {
  180. builder.append('(');
  181. bool first = true;
  182. for (auto const& stop : linear.stops) {
  183. if (!first)
  184. builder.append(", "sv);
  185. first = false;
  186. builder.appendff("{}"sv, stop.offset);
  187. if (stop.position.has_value())
  188. builder.appendff(" {}"sv, stop.position.value());
  189. }
  190. builder.append(')');
  191. }
  192. },
  193. [&](CubicBezier const& bezier) {
  194. if (bezier == CubicBezier::ease()) {
  195. builder.append("ease"sv);
  196. } else if (bezier == CubicBezier::ease_in()) {
  197. builder.append("ease-in"sv);
  198. } else if (bezier == CubicBezier::ease_out()) {
  199. builder.append("ease-out"sv);
  200. } else if (bezier == CubicBezier::ease_in_out()) {
  201. builder.append("ease-in-out"sv);
  202. } else {
  203. builder.appendff("cubic-bezier({}, {}, {}, {})", bezier.x1, bezier.y1, bezier.x2, bezier.y2);
  204. }
  205. },
  206. [&](Steps const& steps) {
  207. if (steps == Steps::step_start()) {
  208. builder.append("step-start"sv);
  209. } else if (steps == Steps::step_end()) {
  210. builder.append("step-end"sv);
  211. } else {
  212. auto position = [&] -> Optional<StringView> {
  213. switch (steps.position) {
  214. case Steps::Position::JumpStart:
  215. return "jump-start"sv;
  216. case Steps::Position::JumpNone:
  217. return "jump-none"sv;
  218. case Steps::Position::JumpBoth:
  219. return "jump-both"sv;
  220. case Steps::Position::Start:
  221. return "start"sv;
  222. default:
  223. return {};
  224. }
  225. }();
  226. if (position.has_value()) {
  227. builder.appendff("steps({}, {})", steps.number_of_intervals, position.value());
  228. } else {
  229. builder.appendff("steps({})", steps.number_of_intervals);
  230. }
  231. }
  232. });
  233. return MUST(builder.to_string());
  234. }
  235. }