EasingStyleValue.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  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. // https://drafts.csswg.org/css-easing-1/#valdef-easing-function-linear
  15. EasingStyleValue::Linear EasingStyleValue::Linear::identity()
  16. {
  17. static Linear linear { { { 0, {}, false }, { 1, {}, false } } };
  18. return linear;
  19. }
  20. // NOTE: Magic cubic bezier values from https://www.w3.org/TR/css-easing-1/#valdef-cubic-bezier-easing-function-ease
  21. EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease()
  22. {
  23. static CubicBezier bezier { 0.25, 0.1, 0.25, 1.0 };
  24. return bezier;
  25. }
  26. EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_in()
  27. {
  28. static CubicBezier bezier { 0.42, 0.0, 1.0, 1.0 };
  29. return bezier;
  30. }
  31. EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_out()
  32. {
  33. static CubicBezier bezier { 0.0, 0.0, 0.58, 1.0 };
  34. return bezier;
  35. }
  36. EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_in_out()
  37. {
  38. static CubicBezier bezier { 0.42, 0.0, 0.58, 1.0 };
  39. return bezier;
  40. }
  41. EasingStyleValue::Steps EasingStyleValue::Steps::step_start()
  42. {
  43. static Steps steps { 1, Steps::Position::Start };
  44. return steps;
  45. }
  46. EasingStyleValue::Steps EasingStyleValue::Steps::step_end()
  47. {
  48. static Steps steps { 1, Steps::Position::End };
  49. return steps;
  50. }
  51. bool EasingStyleValue::CubicBezier::operator==(Web::CSS::EasingStyleValue::CubicBezier const& other) const
  52. {
  53. return x1 == other.x1 && y1 == other.y1 && x2 == other.x2 && y2 == other.y2;
  54. }
  55. // https://drafts.csswg.org/css-easing/#linear-canonicalization
  56. EasingStyleValue::Linear::Linear(Vector<EasingStyleValue::Linear::Stop> stops)
  57. {
  58. // To canonicalize a linear() function’s control points, perform the following:
  59. // 1. If the first control point lacks an input progress value, set its input progress value to 0.
  60. if (!stops.first().input.has_value())
  61. stops.first().input = 0;
  62. // 2. If the last control point lacks an input progress value, set its input progress value to 1.
  63. if (!stops.last().input.has_value())
  64. stops.last().input = 1;
  65. // 3. If any control point has an input progress value that is less than
  66. // the input progress value of any preceding control point,
  67. // set its input progress value to the largest input progress value of any preceding control point.
  68. double largest_input = 0;
  69. for (auto stop : stops) {
  70. if (stop.input.has_value()) {
  71. if (stop.input.value() < largest_input) {
  72. stop.input = largest_input;
  73. } else {
  74. largest_input = stop.input.value();
  75. }
  76. }
  77. }
  78. // 4. If any control point still lacks an input progress value,
  79. // then for each contiguous run of such control points,
  80. // set their input progress values so that they are evenly spaced
  81. // between the preceding and following control points with input progress values.
  82. Optional<size_t> run_start_idx;
  83. for (size_t idx = 0; idx < stops.size(); idx++) {
  84. auto stop = stops[idx];
  85. if (stop.input.has_value() && run_start_idx.has_value()) {
  86. // Note: this stop is immediately after a run
  87. // set inputs of [start, idx-1] stops to be evenly spaced between start-1 and idx
  88. auto start_input = stops[run_start_idx.value() - 1].input.value();
  89. auto end_input = stops[idx].input.value();
  90. auto run_stop_count = idx - run_start_idx.value() + 1;
  91. auto delta = (end_input - start_input) / run_stop_count;
  92. for (size_t run_idx = 0; run_idx < run_stop_count; run_idx++) {
  93. stops[run_idx + run_start_idx.value() - 1].input = start_input + delta * run_idx;
  94. }
  95. run_start_idx = {};
  96. } else if (!stop.input.has_value() && !run_start_idx.has_value()) {
  97. // Note: this stop is the start of a run
  98. run_start_idx = idx;
  99. }
  100. }
  101. this->stops = move(stops);
  102. }
  103. // https://drafts.csswg.org/css-easing/#linear-easing-function-output
  104. double EasingStyleValue::Linear::evaluate_at(double input_progress, bool before_flag) const
  105. {
  106. // To calculate linear easing output progress for a given linear easing function func,
  107. // an input progress value inputProgress, and an optional before flag (defaulting to false),
  108. // perform the following:
  109. // 1. Let points be func’s control points.
  110. // 2. If points holds only a single item, return the output progress value of that item.
  111. if (stops.size() == 1)
  112. return stops[0].output;
  113. // 3. If inputProgress matches the input progress value of the first point in points,
  114. // and the before flag is true, return the first point’s output progress value.
  115. if (input_progress == stops[0].input.value() && before_flag)
  116. return stops[0].output;
  117. // 4. If inputProgress matches the input progress value of at least one point in points,
  118. // return the output progress value of the last such point.
  119. auto maybe_match = stops.last_matching([&](auto& stop) { return input_progress == stop.input.value(); });
  120. if (maybe_match.has_value())
  121. return maybe_match->output;
  122. // 5. Otherwise, find two control points in points, A and B, which will be used for interpolation:
  123. Stop A;
  124. Stop B;
  125. if (input_progress < stops[0].input.value()) {
  126. // 1. If inputProgress is smaller than any input progress value in points,
  127. // let A and B be the first two items in points.
  128. // If A and B have the same input progress value, return A’s output progress value.
  129. A = stops[0];
  130. B = stops[1];
  131. if (A.input == B.input)
  132. return A.output;
  133. } else if (input_progress > stops.last().input.value()) {
  134. // 2. If inputProgress is larger than any input progress value in points,
  135. // let A and B be the last two items in points.
  136. // If A and B have the same input progress value, return B’s output progress value.
  137. A = stops[stops.size() - 2];
  138. B = stops[stops.size() - 1];
  139. if (A.input == B.input)
  140. return B.output;
  141. } else {
  142. // 3. Otherwise, let A be the last control point whose input progress value is smaller than inputProgress,
  143. // and let B be the first control point whose input progress value is larger than inputProgress.
  144. A = stops.last_matching([&](auto& stop) { return stop.input.value() < input_progress; }).value();
  145. B = stops.first_matching([&](auto& stop) { return stop.input.value() > input_progress; }).value();
  146. }
  147. // 6. Linearly interpolate (or extrapolate) inputProgress along the line defined by A and B, and return the result.
  148. auto factor = (input_progress - A.input.value()) / (B.input.value() - A.input.value());
  149. return A.output + factor * (B.output - A.output);
  150. }
  151. // https://drafts.csswg.org/css-easing/#linear-easing-function-serializing
  152. String EasingStyleValue::Linear::to_string() const
  153. {
  154. // The linear keyword is serialized as itself.
  155. if (*this == identity())
  156. return "linear"_string;
  157. // To serialize a linear() function:
  158. // 1. Let s be the string "linear(".
  159. StringBuilder builder;
  160. builder.append("linear("sv);
  161. // 2. Serialize each control point of the function,
  162. // concatenate the results using the separator ", ",
  163. // and append the result to s.
  164. bool first = true;
  165. for (auto stop : stops) {
  166. if (first) {
  167. first = false;
  168. } else {
  169. builder.append(", "sv);
  170. }
  171. // To serialize a linear() control point:
  172. // 1. Let s be the serialization, as a <number>, of the control point’s output progress value.
  173. builder.appendff("{}", stop.output);
  174. // 2. If the control point originally lacked an input progress value, return s.
  175. // 3. Otherwise, append " " (U+0020 SPACE) to s,
  176. // then serialize the control point’s input progress value as a <percentage> and append it to s.
  177. if (stop.had_explicit_input) {
  178. builder.appendff(" {}%", stop.input.value() * 100);
  179. }
  180. // 4. Return s.
  181. }
  182. // 4. Append ")" to s, and return it.
  183. builder.append(')');
  184. return MUST(builder.to_string());
  185. }
  186. double EasingStyleValue::CubicBezier::evaluate_at(double input_progress, bool) const
  187. {
  188. constexpr static auto cubic_bezier_at = [](double x1, double x2, double t) {
  189. auto a = 1.0 - 3.0 * x2 + 3.0 * x1;
  190. auto b = 3.0 * x2 - 6.0 * x1;
  191. auto c = 3.0 * x1;
  192. auto t2 = t * t;
  193. auto t3 = t2 * t;
  194. return (a * t3) + (b * t2) + (c * t);
  195. };
  196. // https://www.w3.org/TR/css-easing-1/#cubic-bezier-algo
  197. // For input progress values outside the range [0, 1], the curve is extended infinitely using tangent of the curve
  198. // at the closest endpoint as follows:
  199. // - For input progress values less than zero,
  200. if (input_progress < 0.0) {
  201. // 1. If the x value of P1 is greater than zero, use a straight line that passes through P1 and P0 as the
  202. // tangent.
  203. if (x1 > 0.0)
  204. return y1 / x1 * input_progress;
  205. // 2. Otherwise, if the x value of P2 is greater than zero, use a straight line that passes through P2 and P0 as
  206. // the tangent.
  207. if (x2 > 0.0)
  208. return y2 / x2 * input_progress;
  209. // 3. Otherwise, let the output progress value be zero for all input progress values in the range [-∞, 0).
  210. return 0.0;
  211. }
  212. // - For input progress values greater than one,
  213. if (input_progress > 1.0) {
  214. // 1. If the x value of P2 is less than one, use a straight line that passes through P2 and P3 as the tangent.
  215. if (x2 < 1.0)
  216. return (1.0 - y2) / (1.0 - x2) * (input_progress - 1.0) + 1.0;
  217. // 2. Otherwise, if the x value of P1 is less than one, use a straight line that passes through P1 and P3 as the
  218. // tangent.
  219. if (x1 < 1.0)
  220. return (1.0 - y1) / (1.0 - x1) * (input_progress - 1.0) + 1.0;
  221. // 3. Otherwise, let the output progress value be one for all input progress values in the range (1, ∞].
  222. return 1.0;
  223. }
  224. // Note: The spec does not specify the precise algorithm for calculating values in the range [0, 1]:
  225. // "The evaluation of this curve is covered in many sources such as [FUND-COMP-GRAPHICS]."
  226. auto x = input_progress;
  227. auto solve = [&](auto t) {
  228. auto x = cubic_bezier_at(x1, x2, t);
  229. auto y = cubic_bezier_at(y1, y2, t);
  230. return CubicBezier::CachedSample { x, y, t };
  231. };
  232. if (m_cached_x_samples.is_empty())
  233. m_cached_x_samples.append(solve(0.));
  234. size_t nearby_index = 0;
  235. if (auto found = binary_search(m_cached_x_samples, x, &nearby_index, [](auto x, auto& sample) {
  236. if (x - sample.x >= NumericLimits<double>::epsilon())
  237. return 1;
  238. if (x - sample.x <= NumericLimits<double>::epsilon())
  239. return -1;
  240. return 0;
  241. }))
  242. return found->y;
  243. if (nearby_index == m_cached_x_samples.size() || nearby_index + 1 == m_cached_x_samples.size()) {
  244. // Produce more samples until we have enough.
  245. auto last_t = m_cached_x_samples.last().t;
  246. auto last_x = m_cached_x_samples.last().x;
  247. while (last_x <= x && last_t < 1.0) {
  248. last_t += 1. / 60.;
  249. auto solution = solve(last_t);
  250. m_cached_x_samples.append(solution);
  251. last_x = solution.x;
  252. }
  253. if (auto found = binary_search(m_cached_x_samples, x, &nearby_index, [](auto x, auto& sample) {
  254. if (x - sample.x >= NumericLimits<double>::epsilon())
  255. return 1;
  256. if (x - sample.x <= NumericLimits<double>::epsilon())
  257. return -1;
  258. return 0;
  259. }))
  260. return found->y;
  261. }
  262. // We have two samples on either side of the x value we want, so we can linearly interpolate between them.
  263. auto& sample1 = m_cached_x_samples[nearby_index];
  264. auto& sample2 = m_cached_x_samples[nearby_index + 1];
  265. auto factor = (x - sample1.x) / (sample2.x - sample1.x);
  266. return sample1.y + factor * (sample2.y - sample1.y);
  267. }
  268. String EasingStyleValue::CubicBezier::to_string() const
  269. {
  270. StringBuilder builder;
  271. if (*this == CubicBezier::ease()) {
  272. builder.append("ease"sv);
  273. } else if (*this == CubicBezier::ease_in()) {
  274. builder.append("ease-in"sv);
  275. } else if (*this == CubicBezier::ease_out()) {
  276. builder.append("ease-out"sv);
  277. } else if (*this == CubicBezier::ease_in_out()) {
  278. builder.append("ease-in-out"sv);
  279. } else {
  280. builder.appendff("cubic-bezier({}, {}, {}, {})", x1, y1, x2, y2);
  281. }
  282. return MUST(builder.to_string());
  283. }
  284. double EasingStyleValue::Steps::evaluate_at(double input_progress, bool before_flag) const
  285. {
  286. // https://www.w3.org/TR/css-easing-1/#step-easing-algo
  287. // 1. Calculate the current step as floor(input progress value × steps).
  288. auto current_step = floor(input_progress * number_of_intervals);
  289. // 2. If the step position property is one of:
  290. // - jump-start,
  291. // - jump-both,
  292. // increment current step by one.
  293. if (position == Steps::Position::JumpStart || position == Steps::Position::Start || position == Steps::Position::JumpBoth)
  294. current_step += 1;
  295. // 3. If both of the following conditions are true:
  296. // - the before flag is set, and
  297. // - input progress value × steps mod 1 equals zero (that is, if input progress value × steps is integral), then
  298. // decrement current step by one.
  299. auto step_progress = input_progress * number_of_intervals;
  300. if (before_flag && trunc(step_progress) == step_progress)
  301. current_step -= 1;
  302. // 4. If input progress value ≥ 0 and current step < 0, let current step be zero.
  303. if (input_progress >= 0.0 && current_step < 0.0)
  304. current_step = 0.0;
  305. // 5. Calculate jumps based on the step position as follows:
  306. // jump-start or jump-end -> steps
  307. // jump-none -> steps - 1
  308. // jump-both -> steps + 1
  309. auto jumps = number_of_intervals;
  310. if (position == Steps::Position::JumpNone) {
  311. jumps--;
  312. } else if (position == Steps::Position::JumpBoth) {
  313. jumps++;
  314. }
  315. // 6. If input progress value ≤ 1 and current step > jumps, let current step be jumps.
  316. if (input_progress <= 1.0 && current_step > jumps)
  317. current_step = jumps;
  318. // 7. The output progress value is current step / jumps.
  319. return current_step / jumps;
  320. }
  321. String EasingStyleValue::Steps::to_string() const
  322. {
  323. StringBuilder builder;
  324. if (*this == Steps::step_start()) {
  325. builder.append("step-start"sv);
  326. } else if (*this == Steps::step_end()) {
  327. builder.append("step-end"sv);
  328. } else {
  329. auto position = [&] -> Optional<StringView> {
  330. switch (this->position) {
  331. case Steps::Position::JumpStart:
  332. return "jump-start"sv;
  333. case Steps::Position::JumpNone:
  334. return "jump-none"sv;
  335. case Steps::Position::JumpBoth:
  336. return "jump-both"sv;
  337. case Steps::Position::Start:
  338. return "start"sv;
  339. default:
  340. return {};
  341. }
  342. }();
  343. if (position.has_value()) {
  344. builder.appendff("steps({}, {})", number_of_intervals, position.value());
  345. } else {
  346. builder.appendff("steps({})", number_of_intervals);
  347. }
  348. }
  349. return MUST(builder.to_string());
  350. }
  351. double EasingStyleValue::Function::evaluate_at(double input_progress, bool before_flag) const
  352. {
  353. return visit(
  354. [&](auto const& curve) {
  355. return curve.evaluate_at(input_progress, before_flag);
  356. });
  357. }
  358. String EasingStyleValue::Function::to_string() const
  359. {
  360. return visit(
  361. [&](auto const& curve) {
  362. return curve.to_string();
  363. });
  364. }
  365. }