Skip to content

Commit a3a4c3c

Browse files
authored
[css-borders-4] Tighten and improve corner-shape algorithm (#12313)
This reduces a lot of steps, and uses a couple of helper algorithms. Also fixed a few minor bugs, and removed the "canonical superellipse formula" which is no longer needed.
1 parent 53ae2ac commit a3a4c3c

File tree

1 file changed

+72
-124
lines changed

1 file changed

+72
-124
lines changed

css-borders-4/Overview.bs

Lines changed: 72 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -609,33 +609,18 @@ The 'corner-*-shape' Shorthands And Longhands
609609

610610
Rendering 'corner-shape'
611611

612+
When rendering elements with shaped corners, the element's path needs to be offset,
613+
based on [=border=], [=outline=], 'box-shadow', 'overflow-clip-margin' and more.
612614

615+
When rendering borders or outlines, the offset is aligned to the curve of the element's shape,
616+
while when rendering 'box-shadow' or offsetting for 'overflow-clip-margin', the offset is aligned to the axis.
613617

614-
The canonical superellipse formula can be described in Cartesian coordinates, as follows,
615-
where s is the [=superellipse parameter=]:
616-
617-
618-
k = 2abs(|s|)
619-
xk + yk = 1
620-
621-
622-
The resulting |x| and |y| are later projected to CSS coordinates by scaling based on the 'border-radius' properties,
623-
inversed if the [=superellipse parameter=] is negative. This creates symmetry between convex and concave shapes of the same absolute
624-
[=superellipse parameter=].
625-
626-
627-
628-
629-
Since stroking a superellipse accurately may be computationally intensive,
630-
user agents may approximate the path using bezier curves,
631-
as well as account for sharp edges and overlaps.
632-
633-
634-
635-
style="background: white;"
636-
alt="Adjusting corner shapes">
637-
Borders are aligned to the curve, shadows and clip are aligned to the axis.
638-
618+
619+
620+
style="background: white;"
621+
alt="Adjusting corner shapes">
622+
Borders are aligned to the curve, shadows and clip are aligned to the axis.
623+
639624

640625

641626
An [=/element=] |element|'s outer contour is the [=border contour path=] given |element| and |element|'s [=border edge=].
@@ -653,117 +638,80 @@ An [=/element=]'s [=overflow clip edge=] is shaped by the [=border contour path=
653638
Each shadow of [=/element=]'s 'box shadow' is shaped by the [=border contour path=] given |element|, and |element|'s [=border edge=], and the shadow's [=used value|used=] 'box-shadow-spread'.
654639

655640
656-
To compute an [=/element=] |element|'s border contour path given an an [=edge=] |targetEdge| and an optional number |spread| (default 0):
641+
To compute an [=/element=] |element|'s border contour path given an [=edge=] |targetEdge| and an optional number |spread| (default 0):
657642
1. Let |outerLeft|, |outerTop|, |outerRight|, |outerBottom| be |element|'s [=unshaped edge|unshaped=] [=border edge=].
658643
1. Let |topLeftHorizontalRadius|, |topLeftVericalRadius|, |topRightHorizontalRadius|, |topRightVerticalRadius|, |bottomRightHorizontalRadius|,
659644
|bottomRightVerticalRadius|, |bottomLeftHorizontalRadius|, and |bottomLeftVerticalRadius| be |element| [=border edge=]'s radii,
660645
scaled by |element|'s [=opposite corner scale factor=].
661646
1. Let |topLeftShape|, |topRightShape|, |bottomRightShape|, and |bottomLeftShape| be |element|'s [=computed value|computed=] 'corner-*-shape' values.
662647
1. Let |targetLeft|, |targetTop|, |targetRight|, |targetBottom| [=unshaped edge|unshaped=] |targetEdge|.
663648
1. Let |path| be a new path [[SVG2]].
664-
1. Compute a [=corner path=] given
665-
the [=rectangle=] (|outerRight| - |topRightHorizontalRadius|, |outerTop|, |topRightHorizontalRadius|, |topRightVerticalRadius|),
666-
0, |targetTop| - |outerTop|, |outerRight| - |targetRight|, and |topRightShape|,
667-
and append it to |path|.
668-
1. Compute a [=corner path=] given
669-
the rectangle (|outerRight| - |bottomRightHorizontalRadius|, |outerBottom| - |bottomRightVerticalRadius|, |bottomRightHorizontalRadius|, |bottomRightVerticalRadius|), |targetEdge|,
670-
1, |outerRight| - |targetRight|, |outerBottom| - |targetBottom|, and |bottomRightShape|,
671-
and append it to |path|.
672-
1. Compute a [=corner path=] given
673-
the rectangle (|outerLeft|, |outerBottom| - |bottomLeftVerticalRadius|, |bottomLeftHorizontalRadius|, |bottomLeftVerticalRadius|), |targetEdge|,
674-
2, |outerBottom| - |targetBottom|, |targetLeft| - |outerLeft|, and |bottomLeftShape|,
675-
and append it to |path|.
676-
1. Compute a [=corner path=] given
677-
the rectangle (|outerLeft|, |outerTop|, |topLeftHorizontalRadius|, |topLeftVericalRadius|), |targetEdge|,
678-
3, |targetLeft| - |outerLeft|, |targetTop| - |outerTop|, and |topLeftShape|,
679-
and append it to |path|.
649+
1. [=Add corner to path=] given |path|,
650+
the [=rectangle=] (|outerRight| - |topRightHorizontalRadius|, |outerTop|, |topRightHorizontalRadius|, |topRightVerticalRadius|), |targetEdge|,
651+
0, |targetTop| - |outerTop|, |outerRight| - |targetRight|, and |topRightShape|.
652+
1. [=Add corner to path=] given |path|,
653+
the [=rectangle=] (|outerRight| - |bottomRightHorizontalRadius|, |outerBottom| - |bottomRightVerticalRadius|, |bottomRightHorizontalRadius|, |bottomRightVerticalRadius|), |targetEdge|,
654+
1, |outerRight| - |targetRight|, |outerBottom| - |targetBottom|, and |bottomRightShape|.
655+
1. [=Add corner to path=] given |path|,
656+
the [=rectangle=] (|outerLeft|, |outerBottom| - |bottomLeftVerticalRadius|, |bottomLeftHorizontalRadius|, |bottomLeftVerticalRadius|), |targetEdge|,
657+
2, |outerBottom| - |targetBottom|, |targetLeft| - |outerLeft|, and |bottomLeftShape|.
658+
1. [=Add corner to path=] given |path|,
659+
the [=rectangle=] (|outerLeft|, |outerTop|, |topLeftHorizontalRadius|, |topLeftVericalRadius|), |targetEdge|,
660+
3, |targetLeft| - |outerLeft|, |targetTop| - |outerTop|, and |topLeftShape|.
680661
1. If |spread| is not 0, then:
681-
1. Scale |path| by 1 + (|spread| * 2) / (|targetRect|'s [=width dimension|width=]), 1 + (|spread| * 2) / (|targetEdge|'s [=height dimension|height=]).
662+
1. Scale |path| by 1 + (|spread| * 2) / (|targetEdge|'s [=width dimension|width=]), 1 + (|spread| * 2) / (|targetEdge|'s [=height dimension|height=]).
682663
1. Translate |path| by -|spread|, -|spread|.
683664

684665
Note: this creates an effect where the resulting path has the same shape as the original path, but scaled to fit the given spread.
685666
1. Return |path|.
686667

687-
To compute the corner path given a rectangle |cornerRect|, a rectangle |trimRect|, and numbers |startThickness|, |endThickness|, |orientation|, and |curvature|:
688-
1. Assert: |orientation| is 0, 1, 2, or 3.
689-
1. If |curvature| is less than zero, then:
690-
1. Set |curvature| to -|curvature|.
691-
1. Swap between |startThickness| and |endThickness|.
692-
1. Set |orientation| to (|orientation| + 2) % 4.
693-
1. Let |cornerPath| be a path that begins at (0, 1).
694-
1. Switch on |curvature|:
695-
696-
: 0
697-
:: Extend |cornerPath| by adding a straight line to (1, 0).
698-
699-
: ∞
700-
::
701-
1. Extend |cornerPath| by adding a straight line to (1, 1).
702-
1. Extend |cornerPath| by adding a straight line to (1, 0).
703-
704-
: Otherwise
705-
::
706-
1. Let |K| be 0.5|curvature|.
707-
1. For each |T| between 0 and 1, extend |cornerPath| through (|T||K|, (1−|T|)|K|).
708-
709-
User agents may approximate this path, for instance, by using concatenated Bezier curves, to balance between performance and rendering accuracy.
710-
711-
712-
1. Let (|x|, |y|, |width|, |height|) be |targetRect|.
713-
1. Let |clockwiseRectQuad| be « (|x|, |y|), (|x| + |width|, |y|), (|x| + |width|, |y| + |height|), (|x|, |y| + height|) ».
714-
1. Let |curveStartPoint| be |clockwiseRectQuad|[|orientation|].
715-
1. Let |curveEndPoint| be |clockwiseRectQuad|[(|orientation| + 2) % 4].
716-
1. If either |startThickness| or |endThickness| is greater than 0, then:
717-
718-
Note: the following substeps compute a new |curveStartPoint| and |curveEndPoint|, based on the thickness and |curvature|.
719-
The start and end points are offset inwards, perpendicular to the direction of the curve, with the corresponding |startThickness| or |endThickness|.
720-
721-
1. Let |tangentUnitVector| be (1, 0).
722-
723-
Note: |tangentUnitVector| is a unit vector (length of 1 pixel) that points along a curve with both positive X and Y components
724-
(like a top-right corner) and reflects the given |curvature|. This base vector can then be rotated to align with the specific corner's orientation
725-
and scaled to match the required border thickness.
726-
For round curvatures, or for hyperellipses (|curvature| greater than 1), the tangent is a horizontal line to the right.
727-
728-
729-
730-
style="background: white; padding: 8px;"
731-
alt="Tangent unit vector with round (s=1)">
732-
When the 'corner-shape' is ''corner-shape/round'' or more convex (>= 1), the unit vector is 1, 0.
733-
734-
735-
736-
1. If |curvature| is less than 1:
737-
1. Let |halfCorner| be the [=normalized superellipse half corner=] given |curvature|.
738-
1. Let |offsetX| be max(0, (|halfCorner| - 1) * 2 + √2).
739-
1. Let |offsetY| be max(0, √2 - |halfCorner| * 2).
740-
741-
Note: This formula defines the tangent of a quadratic Bezier curve that's equivalent to a superellipse quadrant.
742-
Notably, convex hypoellipses (superellipses with a [=superellipse parameter|parameter=] between 0 and 1) can be very precisely represented by quadratic curves.
743-
744-
1. Let |length| be hypot(|offsetX|, |offsetY|).
745-
1. Set |tangentUnitVector| to (|offsetX| / |length|, |offsetY| / |length|).
746-
747-
At this point |curvature| is guaranteed to be convex (>=1), so ther resulting |tangentUnitVector| would be in the range between (1, 0) and (√2/2, √2/2).
748-
749-
750-
751-
style="background: white; padding: 8px;"
752-
alt="Tangent unit vector with bevel (s=0)">
753-
When the 'corner-shape' is ''corner-shape/bevel'' (0), the unit vector is √2/2, √2/2.
754-
755-
756-
757-
1. Let |startOffset| be |tangentUnitVector|, scaled by |startThickness| and rotated 90° * ((|orientation| + 1) % 4) clockwise.
758-
1. Let |endOffset| be |tangentUnitVector|, scaled by |endThickness| and rotated by 90° * ((|orientation| + 2) % 4) clockwise.
759-
1. Translate |curveStartPoint| by |startOffset|.
760-
1. Translate |curveEndPoint| by |endOffset|.
761-
1. Set |cornerRect| to a rectangle that contains |curveStartPoint| and |curveEndPoint|.
762-
1. Rotate |cornerPath| by 90° * |orientation|, with (0.5, 0.5) as the origin, as described [=transformation matrix|here=].
763-
1. Scale |cornerPath| by |cornerRect|'s [=width dimension|width=], |cornerRect|'s [=width dimension|height=].
764-
1. translate |cornerPath| by |cornerRect|'s [=x coordinate|x=], |cornerRect|'s [=y coordinate|y=].
765-
1. Trim |cornerPath| to |trimRect|.
766-
1. Return |cornerPath|.
668+
To add corner to path given a path |path|, a rectangle |cornerRect|, a rectangle |trimRect|,
669+
and numbers |orientation|, |startThickness|, |endThickness|, |curvature|:
670+
671+
1. If |cornerRect| is empty, or if |curvature| is ∞:
672+
1. Let |innerQuad| be |trimRect|'s [=clockwise quad=] .
673+
1. Extend |path| by drawing a line to |innerQuad|[(|orienation| + 1) % 4].
674+
1. Return.
675+
676+
1. Let |cornerQuad| be |cornerRect|'s [=clockwise quad=].
677+
1. If |curvature| is -∞:
678+
1. Extend |path| by drawing a line from |cornerQuad|[0] to |cornerQuad|[3], trimmed by |trimRect|.
679+
1. Extend |path| by drawing a line from |cornerQuad|[3] to |cornerQuad|[2], trimmed by |trimRect|.
680+
1. Return.
681+
682+
1. Let |clampedNormalizedHalfCorner| be the [=normalized superellipse half corner=] given clamp(|curvature|, -1, 1).
683+
1. Let |equivalentQuadraticControlPointX| be |clampedNormalizedHalfCorner| * 2 - 0.5.
684+
1. Let |curveStartPoint| be the [=aligned corner point=] given |cornerQuad|[|orienation|], the vector (|equivalentQuadraticControlPointX|, 1 - |equivalentQuadraticControlPointX|), |startThickness|, and |orientation| + 1.
685+
1. Let |curveEndPoint| by the [=aligned corner point=] given |cornerQuad|[(|orientation| + 2) % 4], the vector (|equivalentQuadraticControlPointX| - 1, -|equivalentQuadraticControlPointX|), |endThickness|, and |orientation| + 3.
686+
1. Let |alignedCornerRect| be a [=rectangle=] that includes the points |curveStartPoint| and |curveEndPoint|.
687+
1. Let |projectionToCornerRect| be a [=transformation matrix=],
688+
translated by (|alignedCornerRect|'s [=x coordinate=], |alignedCornerRect|'s [=y coordinate=]),
689+
scaled by (|alignedCornerRect|'s [=width dimension=], |alignedCornerRect|'s [=height dimension=]),
690+
translated by (0.5, 0.5),
691+
rotated by 90deg * orientation,
692+
and translated by (-0.5, -0.5).
693+
694+
1. Let |K| be 0.5abs(|curvature|).
695+
1. For each |T| between 0 and 1:
696+
1. Let |A| be |T||K|.
697+
1. Let |B| be 1 - (1 - |T|)|K|.
698+
1. Let |normalizedPoint| be (|A|, |B|) if |curvature| is positive, otherwise (|B|, |A|).
699+
1. Let |absolutePoint| be |normalizedPoint|, transformed by |projectionToCornerRect|.
700+
1. If |absolutePoint| is within |trimRect|, extend |path| through |absolutePoint|.
701+
702+
Note: User agents may approximate this algorithm, for instance, by using concatenated Bezier curves, to balance between performance and rendering accuracy.
703+
704+
To compute the aligned corner point given a point |originalPoint|, a two-component vector |offsetFromControlPoint|, a number |thickness|, and a number |orientation|:
705+
1. Let |length| be hypot(|offsetFromControlPoint|.x, |offsetFromControlPoint|.y).
706+
1. Rotate |offsetFromControlPoint| by 90deg * |orientation|, and scale by |thickness|.
707+
1. Translate |originalPoint| by |offsetFromControlPoint|.x / |length|, |offsetFromControlPoint|.y / |length|, and return the result.
708+
709+
The clockwise quad given a [=rectangle=] |rect|, is a [=quadrilateral=] with the points
710+
(|rect|'s [=x coordinate=], |rect|'s [=y coordinate=]),
711+
(|rect|'s [=x coordinate=] + |rect|'s [=width dimension=], |rect|'s [=y coordinate=]),
712+
(|rect|'s [=x coordinate=] + |rect|'s [=width dimension=], |rect|'s [=y coordinate=] + |rect|'s [=height dimension=]),
713+
(|rect|'s [=x coordinate=], |rect|'s [=y coordinate=] + |rect|'s [=height dimension=]).
714+
767715
768716

769717

@@ -829,7 +777,7 @@ To compute the normalized superellipse half corner given a [=superell
829777
::
830778
1. Let |k| be 0.5abs(|s|).
831779
1. Let |convexHalfCorner| be 0.5|k|.
832-
1. If |param| is less than 0, return 1 - |convexHalfCorner|.
780+
1. If |s| is less than 0, return 1 - |convexHalfCorner|.
833781
1. Return |convexHalfCorner|.
834782
835783

0 commit comments

Comments
 (0)