electron_smooth_round_rect.cc 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. // Copyright (c) 2024 Salesforce, Inc.
  2. // Use of this source code is governed by the MIT license that can be
  3. // found in the LICENSE file.
  4. #include "electron/shell/renderer/electron_smooth_round_rect.h"
  5. #include <numbers>
  6. #include "base/check.h"
  7. namespace electron {
  8. namespace {
  9. // Applies quarter rotations (n * 90°) to a point relative to the origin.
  10. constexpr SkPoint QuarterRotate(const SkPoint& p,
  11. unsigned int quarter_rotations) {
  12. switch (quarter_rotations % 4) {
  13. case 0:
  14. return p;
  15. case 1:
  16. return {-p.y(), p.x()};
  17. case 2:
  18. return {-p.x(), -p.y()};
  19. // case 3:
  20. default:
  21. return {p.y(), -p.x()};
  22. }
  23. }
  24. // Edge length consumed for a given smoothness and corner radius.
  25. constexpr float LengthForCornerSmoothness(float smoothness, float radius) {
  26. return (1.0f + smoothness) * radius;
  27. }
  28. // The smoothness value when consuming an edge length for a corner with a given
  29. // radius.
  30. //
  31. // This function complements `LengthForCornerSmoothness`:
  32. // SmoothnessForCornerLength(LengthForCornerSmoothness(s, r), r) = s
  33. constexpr float SmoothnessForCornerLength(float length, float radius) {
  34. return (length / radius) - 1.0f;
  35. }
  36. // Geometric measurements for constructing the curves of smooth round corners on
  37. // a rectangle.
  38. //
  39. // Each measurement's value is relative to the rectangle's natural corner point.
  40. // An "offset" measurement is a one-dimensional length and a "vector"
  41. // measurement is a two-dimensional pair of lengths.
  42. //
  43. // Each measurement's direction is relative to the direction of an edge towards
  44. // the corner. Offsets are in the same direction as the edge toward the corner.
  45. // For vectors, the X direction is parallel and the Y direction is
  46. // perpendicular.
  47. struct CurveGeometry {
  48. constexpr CurveGeometry(float radius, float smoothness);
  49. constexpr SkVector edge_connecting_vector() const {
  50. return {edge_connecting_offset, 0.0f};
  51. }
  52. constexpr SkVector edge_curve_vector() const {
  53. return {edge_curve_offset, 0.0f};
  54. }
  55. constexpr SkVector arc_curve_vector() const {
  56. return {arc_curve_offset, 0.0f};
  57. }
  58. constexpr SkVector arc_connecting_vector_transposed() const {
  59. return {arc_connecting_vector.y(), arc_connecting_vector.x()};
  60. }
  61. // The point where the edge connects to the curve.
  62. float edge_connecting_offset;
  63. // The control point for the curvature where the edge connects to the curve.
  64. float edge_curve_offset;
  65. // The control point for the curvature where the arc connects to the curve.
  66. float arc_curve_offset;
  67. // The point where the arc connects to the curve.
  68. SkVector arc_connecting_vector;
  69. };
  70. constexpr CurveGeometry::CurveGeometry(float radius, float smoothness) {
  71. edge_connecting_offset = LengthForCornerSmoothness(smoothness, radius);
  72. float arc_angle = (std::numbers::pi / 4.0f) * smoothness;
  73. arc_connecting_vector =
  74. SkVector::Make(1.0f - std::sin(arc_angle), 1.0f - std::cos(arc_angle)) *
  75. radius;
  76. arc_curve_offset = (1.0f - std::tan(arc_angle / 2.0f)) * radius;
  77. constexpr float EDGE_CURVE_POINT_RATIO = 2.0f / 3.0f;
  78. edge_curve_offset =
  79. edge_connecting_offset -
  80. ((edge_connecting_offset - arc_curve_offset) * EDGE_CURVE_POINT_RATIO);
  81. }
  82. void DrawCorner(SkPath& path,
  83. float radius,
  84. const CurveGeometry& curve1,
  85. const CurveGeometry& curve2,
  86. const SkPoint& corner,
  87. unsigned int quarter_rotations) {
  88. // Move/Line to the edge connecting point
  89. {
  90. SkPoint edge_connecting_point =
  91. corner +
  92. QuarterRotate(curve1.edge_connecting_vector(), quarter_rotations + 1);
  93. if (quarter_rotations == 0) {
  94. path.moveTo(edge_connecting_point);
  95. } else {
  96. path.lineTo(edge_connecting_point);
  97. }
  98. }
  99. // Draw the first smoothing curve
  100. {
  101. SkPoint edge_curve_point =
  102. corner +
  103. QuarterRotate(curve1.edge_curve_vector(), quarter_rotations + 1);
  104. SkPoint arc_curve_point = corner + QuarterRotate(curve1.arc_curve_vector(),
  105. quarter_rotations + 1);
  106. SkPoint arc_connecting_point =
  107. corner + QuarterRotate(curve1.arc_connecting_vector_transposed(),
  108. quarter_rotations);
  109. path.cubicTo(edge_curve_point, arc_curve_point, arc_connecting_point);
  110. }
  111. // Draw the arc
  112. {
  113. SkPoint arc_connecting_point =
  114. corner + QuarterRotate(curve2.arc_connecting_vector, quarter_rotations);
  115. path.arcTo(SkPoint::Make(radius, radius), 0.0f, SkPath::kSmall_ArcSize,
  116. SkPathDirection::kCW, arc_connecting_point);
  117. }
  118. // Draw the second smoothing curve
  119. {
  120. SkPoint arc_curve_point =
  121. corner + QuarterRotate(curve2.arc_curve_vector(), quarter_rotations);
  122. SkPoint edge_curve_point =
  123. corner + QuarterRotate(curve2.edge_curve_vector(), quarter_rotations);
  124. SkPoint edge_connecting_point =
  125. corner +
  126. QuarterRotate(curve2.edge_connecting_vector(), quarter_rotations);
  127. path.cubicTo(arc_curve_point, edge_curve_point, edge_connecting_point);
  128. }
  129. }
  130. // Constrains the smoothness of two corners along the same edge.
  131. //
  132. // If the smoothness value needs to be constrained, it will try to keep the
  133. // ratio of the smoothness values the same as the ratio of the radii
  134. // (`s1/s2 = r1/r2`).
  135. constexpr std::pair<float, float> ConstrainSmoothness(float size,
  136. float smoothness,
  137. float radius1,
  138. float radius2) {
  139. float edge_consumed1 = LengthForCornerSmoothness(smoothness, radius1);
  140. float edge_consumed2 = LengthForCornerSmoothness(smoothness, radius2);
  141. // If both corners fit within the edge size then keep the smoothness
  142. if (edge_consumed1 + edge_consumed2 <= size) {
  143. return {smoothness, smoothness};
  144. }
  145. float ratio = radius1 / (radius1 + radius2);
  146. float length1 = size * ratio;
  147. float length2 = size - length1;
  148. float smoothness1 =
  149. std::max(SmoothnessForCornerLength(length1, radius1), 0.0f);
  150. float smoothness2 =
  151. std::max(SmoothnessForCornerLength(length2, radius2), 0.0f);
  152. return {smoothness1, smoothness2};
  153. }
  154. } // namespace
  155. // The algorithm for drawing this shape is based on the article
  156. // "Desperately seeking squircles" by Daniel Furse. A brief summary:
  157. //
  158. // In a simple round rectangle, each corner of a plain rectangle is replaced
  159. // with a quarter circle and connected to each edge of the corner.
  160. //
  161. // Edge
  162. // ←------→ ↖
  163. // ----------o--__ `、 Corner (Quarter Circle)
  164. // `、 `、
  165. // | ↘
  166. // |
  167. // o
  168. // | ↑
  169. // | | Edge
  170. // | ↓
  171. //
  172. // This creates sharp changes in the curvature at the points where the edge
  173. // transitions to the corner, suddenly curving at a constant rate. Our primary
  174. // goal is to smooth out that curvature profile, slowly ramping up and back
  175. // down, like turning a car with the steering wheel.
  176. //
  177. // To achieve this, we "expand" that point where the circular corner meets the
  178. // straight edge in both directions. We use this extra space to construct a
  179. // small curved path that eases the curvature from the edge to the corner
  180. // circle.
  181. //
  182. // Edge Curve
  183. // ←--→ ←-----→
  184. // -----o----___o ↖、 Corner (Circular Arc)
  185. // `、 `↘
  186. // o
  187. // | ↑
  188. // | | Curve
  189. // | ↓
  190. // o
  191. // | ↕ Edge
  192. //
  193. // Each curve is implemented as a cubic Bézier curve, composed of four control
  194. // points:
  195. //
  196. // * The first control point connects to the straight edge.
  197. // * The fourth (last) control point connects to the circular arc.
  198. // * The second & third control points both lie on the infinite line extending
  199. // from the straight edge.
  200. // * The third control point (only) also lies on the infinite line tangent to
  201. // the circular arc at the fourth control point.
  202. //
  203. // The first and fourth (last) control points are firmly fixed by attaching to
  204. // the straight edge and circular arc, respectively. The third control point is
  205. // fixed at the intersection between the edge and tangent lines. The second
  206. // control point, however, is only constrained to the infinite edge line, but
  207. // we may choose where.
  208. SkPath DrawSmoothRoundRect(float x,
  209. float y,
  210. float width,
  211. float height,
  212. float smoothness,
  213. float top_left_radius,
  214. float top_right_radius,
  215. float bottom_right_radius,
  216. float bottom_left_radius) {
  217. DCHECK(0.0f <= smoothness && smoothness <= 1.0f);
  218. // Assume the radii are already constrained within the rectangle size
  219. DCHECK(top_left_radius + top_right_radius <= width);
  220. DCHECK(bottom_left_radius + bottom_right_radius <= width);
  221. DCHECK(top_left_radius + bottom_left_radius <= height);
  222. DCHECK(top_right_radius + bottom_right_radius <= height);
  223. if (width <= 0.0f || height <= 0.0f) {
  224. return SkPath();
  225. }
  226. // Constrain the smoothness for each curve on each edge
  227. auto [top_left_smoothness, top_right_smoothness] =
  228. ConstrainSmoothness(width, smoothness, top_left_radius, top_right_radius);
  229. auto [right_top_smoothness, right_bottom_smoothness] = ConstrainSmoothness(
  230. height, smoothness, top_right_radius, bottom_right_radius);
  231. auto [bottom_left_smoothness, bottom_right_smoothness] = ConstrainSmoothness(
  232. width, smoothness, bottom_left_radius, bottom_right_radius);
  233. auto [left_top_smoothness, left_bottom_smoothness] = ConstrainSmoothness(
  234. height, smoothness, top_left_radius, bottom_left_radius);
  235. SkPath path;
  236. // Top left corner
  237. DrawCorner(path, top_left_radius,
  238. CurveGeometry(top_left_radius, left_top_smoothness),
  239. CurveGeometry(top_left_radius, top_left_smoothness),
  240. SkPoint::Make(x, y), 0);
  241. // Top right corner
  242. DrawCorner(path, top_right_radius,
  243. CurveGeometry(top_right_radius, top_right_smoothness),
  244. CurveGeometry(top_right_radius, right_top_smoothness),
  245. SkPoint::Make(x + width, y), 1);
  246. // Bottom right corner
  247. DrawCorner(path, bottom_right_radius,
  248. CurveGeometry(bottom_right_radius, right_bottom_smoothness),
  249. CurveGeometry(bottom_right_radius, bottom_right_smoothness),
  250. SkPoint::Make(x + width, y + height), 2);
  251. // Bottom left corner
  252. DrawCorner(path, bottom_left_radius,
  253. CurveGeometry(bottom_left_radius, bottom_left_smoothness),
  254. CurveGeometry(bottom_left_radius, left_bottom_smoothness),
  255. SkPoint::Make(x, y + height), 3);
  256. path.close();
  257. return path;
  258. }
  259. } // namespace electron