TimingFunction.cpp 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. /*
  2. * Copyright (c) 2023, Ali Mohammad Pur <mpfard@serenityos.org>
  3. * Copyright (c) 2023, Matthew Olsson <mattco@serenityos.org>
  4. *
  5. * SPDX-License-Identifier: BSD-2-Clause
  6. */
  7. #include <AK/BinarySearch.h>
  8. #include <LibWeb/Animations/TimingFunction.h>
  9. #include <LibWeb/CSS/StyleValues/EasingStyleValue.h>
  10. #include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
  11. #include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
  12. #include <math.h>
  13. namespace Web::Animations {
  14. // https://www.w3.org/TR/css-easing-1/#linear-easing-function
  15. double LinearTimingFunction::operator()(double input_progress, bool) const
  16. {
  17. return input_progress;
  18. }
  19. static double cubic_bezier_at(double x1, double x2, double t)
  20. {
  21. auto a = 1.0 - 3.0 * x2 + 3.0 * x1;
  22. auto b = 3.0 * x2 - 6.0 * x1;
  23. auto c = 3.0 * x1;
  24. auto t2 = t * t;
  25. auto t3 = t2 * t;
  26. return (a * t3) + (b * t2) + (c * t);
  27. }
  28. // https://www.w3.org/TR/css-easing-1/#cubic-bezier-algo
  29. double CubicBezierTimingFunction::operator()(double input_progress, bool) const
  30. {
  31. // For input progress values outside the range [0, 1], the curve is extended infinitely using tangent of the curve
  32. // at the closest endpoint as follows:
  33. // - For input progress values less than zero,
  34. if (input_progress < 0.0) {
  35. // 1. If the x value of P1 is greater than zero, use a straight line that passes through P1 and P0 as the
  36. // tangent.
  37. if (x1 > 0.0)
  38. return y1 / x1 * input_progress;
  39. // 2. Otherwise, if the x value of P2 is greater than zero, use a straight line that passes through P2 and P0 as
  40. // the tangent.
  41. if (x2 > 0.0)
  42. return y2 / x2 * input_progress;
  43. // 3. Otherwise, let the output progress value be zero for all input progress values in the range [-∞, 0).
  44. return 0.0;
  45. }
  46. // - For input progress values greater than one,
  47. if (input_progress > 1.0) {
  48. // 1. If the x value of P2 is less than one, use a straight line that passes through P2 and P3 as the tangent.
  49. if (x2 < 1.0)
  50. return (1.0 - y2) / (1.0 - x2) * (input_progress - 1.0) + 1.0;
  51. // 2. Otherwise, if the x value of P1 is less than one, use a straight line that passes through P1 and P3 as the
  52. // tangent.
  53. if (x1 < 1.0)
  54. return (1.0 - y1) / (1.0 - x1) * (input_progress - 1.0) + 1.0;
  55. // 3. Otherwise, let the output progress value be one for all input progress values in the range (1, ∞].
  56. return 1.0;
  57. }
  58. // Note: The spec does not specify the precise algorithm for calculating values in the range [0, 1]:
  59. // "The evaluation of this curve is covered in many sources such as [FUND-COMP-GRAPHICS]."
  60. auto x = input_progress;
  61. auto solve = [&](auto t) {
  62. auto x = cubic_bezier_at(x1, x2, t);
  63. auto y = cubic_bezier_at(y1, y2, t);
  64. return CachedSample { x, y, t };
  65. };
  66. if (m_cached_x_samples.is_empty())
  67. m_cached_x_samples.append(solve(0.));
  68. size_t nearby_index = 0;
  69. if (auto found = binary_search(m_cached_x_samples, x, &nearby_index, [](auto x, auto& sample) {
  70. if (x > sample.x)
  71. return 1;
  72. if (x < sample.x)
  73. return -1;
  74. return 0;
  75. }))
  76. return found->y;
  77. if (nearby_index == m_cached_x_samples.size() || nearby_index + 1 == m_cached_x_samples.size()) {
  78. // Produce more samples until we have enough.
  79. auto last_t = m_cached_x_samples.is_empty() ? 0 : m_cached_x_samples.last().t;
  80. auto last_x = m_cached_x_samples.is_empty() ? 0 : m_cached_x_samples.last().x;
  81. while (last_x <= x) {
  82. last_t += 1. / 60.;
  83. auto solution = solve(last_t);
  84. m_cached_x_samples.append(solution);
  85. last_x = solution.x;
  86. }
  87. if (auto found = binary_search(m_cached_x_samples, x, &nearby_index, [](auto x, auto& sample) {
  88. if (x > sample.x)
  89. return 1;
  90. if (x < sample.x)
  91. return -1;
  92. return 0;
  93. }))
  94. return found->y;
  95. }
  96. // We have two samples on either side of the x value we want, so we can linearly interpolate between them.
  97. auto& sample1 = m_cached_x_samples[nearby_index];
  98. auto& sample2 = m_cached_x_samples[nearby_index + 1];
  99. auto factor = (x - sample1.x) / (sample2.x - sample1.x);
  100. return clamp(sample1.y + factor * (sample2.y - sample1.y), 0, 1);
  101. }
  102. // https://www.w3.org/TR/css-easing-1/#step-easing-algo
  103. double StepsTimingFunction::operator()(double input_progress, bool before_flag) const
  104. {
  105. // 1. Calculate the current step as floor(input progress value × steps).
  106. auto current_step = floor(input_progress * number_of_steps);
  107. // 2. If the step position property is one of:
  108. // - jump-start,
  109. // - jump-both,
  110. // increment current step by one.
  111. if (jump_at_start)
  112. current_step += 1;
  113. // 3. If both of the following conditions are true:
  114. // - the before flag is set, and
  115. // - input progress value × steps mod 1 equals zero (that is, if input progress value × steps is integral), then
  116. // decrement current step by one.
  117. auto step_progress = input_progress * number_of_steps;
  118. if (before_flag && trunc(step_progress) == step_progress)
  119. current_step -= 1;
  120. // 4. If input progress value ≥ 0 and current step < 0, let current step be zero.
  121. if (input_progress >= 0.0 && current_step < 0.0)
  122. current_step = 0.0;
  123. // 5. Calculate jumps based on the step position as follows:
  124. // jump-start or jump-end -> steps
  125. // jump-none -> steps - 1
  126. // jump-both -> steps + 1
  127. double jumps;
  128. if (jump_at_start ^ jump_at_end)
  129. jumps = number_of_steps;
  130. else if (jump_at_start && jump_at_end)
  131. jumps = number_of_steps + 1;
  132. else
  133. jumps = number_of_steps - 1;
  134. // 6. If input progress value ≤ 1 and current step > jumps, let current step be jumps.
  135. if (input_progress <= 1.0 && current_step > jumps)
  136. current_step = jumps;
  137. // 7. The output progress value is current step / jumps.
  138. return current_step / jumps;
  139. }
  140. TimingFunction TimingFunction::from_easing_style_value(CSS::EasingStyleValue const& easing_value)
  141. {
  142. switch (easing_value.easing_function()) {
  143. case CSS::EasingFunction::Linear:
  144. return Animations::linear_timing_function;
  145. case CSS::EasingFunction::Ease:
  146. return Animations::ease_timing_function;
  147. case CSS::EasingFunction::EaseIn:
  148. return Animations::ease_in_timing_function;
  149. case CSS::EasingFunction::EaseOut:
  150. return Animations::ease_out_timing_function;
  151. case CSS::EasingFunction::EaseInOut:
  152. return Animations::ease_in_out_timing_function;
  153. case CSS::EasingFunction::CubicBezier: {
  154. auto values = easing_value.values();
  155. return {
  156. Animations::CubicBezierTimingFunction {
  157. values[0]->as_number().number(),
  158. values[1]->as_number().number(),
  159. values[2]->as_number().number(),
  160. values[3]->as_number().number(),
  161. },
  162. };
  163. }
  164. case CSS::EasingFunction::Steps: {
  165. auto values = easing_value.values();
  166. auto jump_at_start = false;
  167. auto jump_at_end = true;
  168. if (values.size() > 1) {
  169. auto identifier = values[1]->to_identifier();
  170. switch (identifier) {
  171. case CSS::ValueID::JumpStart:
  172. case CSS::ValueID::Start:
  173. jump_at_start = true;
  174. jump_at_end = false;
  175. break;
  176. case CSS::ValueID::JumpEnd:
  177. case CSS::ValueID::End:
  178. jump_at_start = false;
  179. jump_at_end = true;
  180. break;
  181. case CSS::ValueID::JumpNone:
  182. jump_at_start = false;
  183. jump_at_end = false;
  184. break;
  185. default:
  186. break;
  187. }
  188. }
  189. return Animations::TimingFunction { Animations::StepsTimingFunction {
  190. .number_of_steps = static_cast<size_t>(max(values[0]->as_integer().integer(), !(jump_at_end && jump_at_start) ? 1 : 0)),
  191. .jump_at_start = jump_at_start,
  192. .jump_at_end = jump_at_end,
  193. } };
  194. }
  195. case CSS::EasingFunction::StepEnd:
  196. return Animations::TimingFunction { Animations::StepsTimingFunction {
  197. .number_of_steps = 1,
  198. .jump_at_start = false,
  199. .jump_at_end = true,
  200. } };
  201. case CSS::EasingFunction::StepStart:
  202. return Animations::TimingFunction { Animations::StepsTimingFunction {
  203. .number_of_steps = 1,
  204. .jump_at_start = true,
  205. .jump_at_end = false,
  206. } };
  207. default:
  208. return Animations::ease_timing_function;
  209. }
  210. }
  211. double TimingFunction::operator()(double input_progress, bool before_flag) const
  212. {
  213. return function.visit([&](auto const& f) { return f(input_progress, before_flag); });
  214. }
  215. }