Skip to content

[css-borders-4] Tighten and improve corner-shape algorithm #12313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 11, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 72 additions & 124 deletions css-borders-4/Overview.bs
Original file line number Diff line number Diff line change
Expand Up @@ -609,33 +609,18 @@ The 'corner-*-shape' Shorthands And Longhands</h4>
<h4 id=corner-shape-rendering>
Rendering 'corner-shape'</h4>

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

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

The <dfn export>canonical superellipse formula</dfn> can be described in Cartesian coordinates, as follows,
where <code>s</code> is the [=superellipse parameter=]:

<pre>
k = 2<sup>abs(|s|)</sup>
x<sup>k</sup> + y<sup>k</sup> = 1
</pre>

The resulting |x| and |y| are later projected to CSS coordinates by scaling based on the 'border-radius' properties,
inversed if the [=superellipse parameter=] is negative. This creates symmetry between convex and concave shapes of the same absolute
[=superellipse parameter=].




Since stroking a superellipse accurately may be computationally intensive,
user agents may approximate the path using bezier curves,
as well as account for sharp edges and overlaps.

<figure>
<img src="images/corner-shape-adjusting.svg"
style="background: white;"
alt="Adjusting corner shapes">
<figcaption>Borders are aligned to the curve, shadows and clip are aligned to the axis.</figcaption>
</figure>
<figure>
<img src="images/corner-shape-adjusting.svg"
style="background: white;"
alt="Adjusting corner shapes">
<figcaption>Borders are aligned to the curve, shadows and clip are aligned to the axis.</figcaption>
</figure>


An [=/element=] |element|'s <dfn>outer contour</dfn> is the [=border contour path=] given |element| and |element|'s [=border edge=].
Expand All @@ -653,117 +638,80 @@ An [=/element=]'s [=overflow clip edge=] is shaped by the [=border contour path=
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'.

<div algorithm="adjust-border-inner-path-for-corner-shape">
To compute an [=/element=] |element|'s <dfn>border contour path</dfn> given an an [=edge=] |targetEdge| and an optional number |spread| (default 0):
To compute an [=/element=] |element|'s <dfn>border contour path</dfn> given an [=edge=] |targetEdge| and an optional number |spread| (default 0):
1. Let |outerLeft|, |outerTop|, |outerRight|, |outerBottom| be |element|'s [=unshaped edge|unshaped=] [=border edge=].
1. Let |topLeftHorizontalRadius|, |topLeftVericalRadius|, |topRightHorizontalRadius|, |topRightVerticalRadius|, |bottomRightHorizontalRadius|,
|bottomRightVerticalRadius|, |bottomLeftHorizontalRadius|, and |bottomLeftVerticalRadius| be |element| [=border edge=]'s radii,
scaled by |element|'s [=opposite corner scale factor=].
1. Let |topLeftShape|, |topRightShape|, |bottomRightShape|, and |bottomLeftShape| be |element|'s [=computed value|computed=] 'corner-*-shape' values.
1. Let |targetLeft|, |targetTop|, |targetRight|, |targetBottom| [=unshaped edge|unshaped=] |targetEdge|.
1. Let |path| be a new path [[SVG2]].
1. Compute a [=corner path=] given
the [=rectangle=] <code>(|outerRight| - |topRightHorizontalRadius|, |outerTop|, |topRightHorizontalRadius|, |topRightVerticalRadius|)</code>,
0, |targetTop| - |outerTop|, |outerRight| - |targetRight|, and |topRightShape|,
and append it to |path|.
1. Compute a [=corner path=] given
the rectangle <code>(|outerRight| - |bottomRightHorizontalRadius|, |outerBottom| - |bottomRightVerticalRadius|, |bottomRightHorizontalRadius|, |bottomRightVerticalRadius|)</code>, |targetEdge|,
1, |outerRight| - |targetRight|, |outerBottom| - |targetBottom|, and |bottomRightShape|,
and append it to |path|.
1. Compute a [=corner path=] given
the rectangle <code>(|outerLeft|, |outerBottom| - |bottomLeftVerticalRadius|, |bottomLeftHorizontalRadius|, |bottomLeftVerticalRadius|)</code>, |targetEdge|,
2, |outerBottom| - |targetBottom|, |targetLeft| - |outerLeft|, and |bottomLeftShape|,
and append it to |path|.
1. Compute a [=corner path=] given
the rectangle <code>(|outerLeft|, |outerTop|, |topLeftHorizontalRadius|, |topLeftVericalRadius|)</code>, |targetEdge|,
3, |targetLeft| - |outerLeft|, |targetTop| - |outerTop|, and |topLeftShape|,
and append it to |path|.
1. [=Add corner to path=] given |path|,
the [=rectangle=] <code>(|outerRight| - |topRightHorizontalRadius|, |outerTop|, |topRightHorizontalRadius|, |topRightVerticalRadius|)</code>, |targetEdge|,
0, |targetTop| - |outerTop|, |outerRight| - |targetRight|, and |topRightShape|.
1. [=Add corner to path=] given |path|,
the [=rectangle=] <code>(|outerRight| - |bottomRightHorizontalRadius|, |outerBottom| - |bottomRightVerticalRadius|, |bottomRightHorizontalRadius|, |bottomRightVerticalRadius|)</code>, |targetEdge|,
1, |outerRight| - |targetRight|, |outerBottom| - |targetBottom|, and |bottomRightShape|.
1. [=Add corner to path=] given |path|,
the [=rectangle=] <code>(|outerLeft|, |outerBottom| - |bottomLeftVerticalRadius|, |bottomLeftHorizontalRadius|, |bottomLeftVerticalRadius|)</code>, |targetEdge|,
2, |outerBottom| - |targetBottom|, |targetLeft| - |outerLeft|, and |bottomLeftShape|.
1. [=Add corner to path=] given |path|,
the [=rectangle=] <code>(|outerLeft|, |outerTop|, |topLeftHorizontalRadius|, |topLeftVericalRadius|)</code>, |targetEdge|,
3, |targetLeft| - |outerLeft|, |targetTop| - |outerTop|, and |topLeftShape|.
1. If |spread| is not 0, then:
1. Scale |path| by <code>1 + (|spread| * 2) / (|targetRect|'s [=width dimension|width=]), 1 + (|spread| * 2) / (|targetEdge|'s [=height dimension|height=])</code>.
1. Scale |path| by <code>1 + (|spread| * 2) / (|targetEdge|'s [=width dimension|width=]), 1 + (|spread| * 2) / (|targetEdge|'s [=height dimension|height=])</code>.
1. Translate |path| by <code>-|spread|, -|spread|</code>.

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

To compute the <dfn>corner path</dfn> given a rectangle |cornerRect|, a rectangle |trimRect|, and numbers |startThickness|, |endThickness|, |orientation|, and |curvature|:
1. Assert: |orientation| is 0, 1, 2, or 3.
1. If |curvature| is less than zero, then:
1. Set |curvature| to <code>-|curvature|</code>.
1. Swap between |startThickness| and |endThickness|.
1. Set |orientation| to (|orientation| + 2) % 4.
1. Let |cornerPath| be a path that begins at <code>(0, 1)</code>.
1. Switch on |curvature|:
<dl class=switch>
: 0
:: Extend |cornerPath| by adding a straight line to <code>(1, 0)</code>.

: &infin;
::
1. Extend |cornerPath| by adding a straight line to <code>(1, 1)</code>.
1. Extend |cornerPath| by adding a straight line to <code>(1, 0)</code>.

: Otherwise
::
1. Let |K| be <code>0.5<sup>|curvature|</sup></code>.
1. For each |T| between 0 and 1, extend |cornerPath| through <code>(|T|<sup>|K|</sup>, (1−|T|)<sup>|K|</sup>)</code>.

User agents may approximate this path, for instance, by using concatenated Bezier curves, to balance between performance and rendering accuracy.
</dl>

1. Let (|x|, |y|, |width|, |height|) be |targetRect|.
1. Let |clockwiseRectQuad| be « (|x|, |y|), (|x| + |width|, |y|), (|x| + |width|, |y| + |height|), (|x|, |y| + height|) ».
1. Let |curveStartPoint| be |clockwiseRectQuad|[|orientation|].
1. Let |curveEndPoint| be |clockwiseRectQuad|[(|orientation| + 2) % 4].
1. If either |startThickness| or |endThickness| is greater than 0, then:

Note: the following substeps compute a new |curveStartPoint| and |curveEndPoint|, based on the thickness and |curvature|.
The start and end points are offset inwards, perpendicular to the direction of the curve, with the corresponding |startThickness| or |endThickness|.

1. Let |tangentUnitVector| be <code>(1, 0)</code>.

Note: |tangentUnitVector| is a unit vector (length of 1 pixel) that points along a curve with both positive X and Y components
(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
and scaled to match the required border thickness.
For round curvatures, or for hyperellipses (|curvature| greater than 1), the tangent is a horizontal line to the right.

<figure>
<img src="images/corner-shape-target-unit-vector-round.svg"
style="background: white; padding: 8px;"
alt="Tangent unit vector with round (s=1)">
<figcaption>When the 'corner-shape' is ''corner-shape/round'' or more convex (<code>>= 1</code>), the unit vector is <code>1, 0</code>.
</figcaption>
</figure>

1. If |curvature| is less than 1:
1. Let |halfCorner| be the [=normalized superellipse half corner=] given |curvature|.
1. Let |offsetX| be <code>max(0, (|halfCorner| - 1) * 2 + &Sqrt;2)</code>.
1. Let |offsetY| be <code>max(0, &Sqrt;2 - |halfCorner| * 2)</code>.

Note: This formula defines the tangent of a quadratic Bezier curve that's equivalent to a superellipse quadrant.
Notably, convex hypoellipses (superellipses with a [=superellipse parameter|parameter=] between 0 and 1) can be very precisely represented by quadratic curves.

1. Let |length| be <code>hypot(|offsetX|, |offsetY|)</code>.
1. Set |tangentUnitVector| to <code>(|offsetX| / |length|, |offsetY| / |length|)</code>.

At this point |curvature| is guaranteed to be convex (>=1), so ther resulting |tangentUnitVector| would be in the range between <code>(1, 0)</code> and <code>(&Sqrt;2/2, &Sqrt;2/2)</code>.

<figure>
<img src="images/corner-shape-target-unit-vector-bevel.svg"
style="background: white; padding: 8px;"
alt="Tangent unit vector with bevel (s=0)">
<figcaption>When the 'corner-shape' is ''corner-shape/bevel'' (<code>0</code>), the unit vector is <code>&Sqrt;2/2, &Sqrt;2/2</code>.
</figcaption>
</figure>

1. Let |startOffset| be |tangentUnitVector|, scaled by |startThickness| and rotated <code>90° * ((|orientation| + 1) % 4)</code> clockwise.
1. Let |endOffset| be |tangentUnitVector|, scaled by |endThickness| and rotated by <code>90° * ((|orientation| + 2) % 4)</code> clockwise.
1. Translate |curveStartPoint| by |startOffset|.
1. Translate |curveEndPoint| by |endOffset|.
1. Set |cornerRect| to a rectangle that contains |curveStartPoint| and |curveEndPoint|.
1. Rotate |cornerPath| by <code>90° * |orientation|</code>, with <code>(0.5, 0.5)</code> as the origin, as described [=transformation matrix|here=].
1. Scale |cornerPath| by <code>|cornerRect|'s [=width dimension|width=], |cornerRect|'s [=width dimension|height=]</code>.
1. translate |cornerPath| by<code> |cornerRect|'s [=x coordinate|x=], |cornerRect|'s [=y coordinate|y=]</code>.
1. Trim |cornerPath| to |trimRect|.
1. Return |cornerPath|.
To <dfn>add corner to path</dfn> given a path |path|, a rectangle |cornerRect|, a rectangle |trimRect|,
and numbers |orientation|, |startThickness|, |endThickness|, |curvature|:

1. If |cornerRect| is empty, or if |curvature| is &infin;:
1. Let |innerQuad| be |trimRect|'s [=clockwise quad=] .
1. Extend |path| by drawing a line to |innerQuad|[<code>(|orienation| + 1) % 4</code>].
1. Return.

1. Let |cornerQuad| be |cornerRect|'s [=clockwise quad=].
1. If |curvature| is -&infin;:
1. Extend |path| by drawing a line from |cornerQuad|[0] to |cornerQuad|[3], trimmed by |trimRect|.
1. Extend |path| by drawing a line from |cornerQuad|[3] to |cornerQuad|[2], trimmed by |trimRect|.
1. Return.

1. Let |clampedNormalizedHalfCorner| be the [=normalized superellipse half corner=] given <code>clamp(|curvature|, -1, 1)</code>.
1. Let |equivalentQuadraticControlPointX| be <code>|clampedNormalizedHalfCorner| * 2 - 0.5</code>.
1. Let |curveStartPoint| be the [=aligned corner point=] given |cornerQuad|[|orienation|], the vector (|equivalentQuadraticControlPointX|, <code>1 - |equivalentQuadraticControlPointX|</code>), |startThickness|, and |orientation| + 1.
1. Let |curveEndPoint| by the [=aligned corner point=] given |cornerQuad|[(|orientation| + 2) % 4], the vector (<code>|equivalentQuadraticControlPointX| - 1</code>, <code>-|equivalentQuadraticControlPointX|</code>), |endThickness|, and |orientation| + 3.
1. Let |alignedCornerRect| be a [=rectangle=] that includes the points |curveStartPoint| and |curveEndPoint|.
1. Let |projectionToCornerRect| be a [=transformation matrix=],
translated by <code>(|alignedCornerRect|'s [=x coordinate=], |alignedCornerRect|'s [=y coordinate=])</code>,
scaled by <code>(|alignedCornerRect|'s [=width dimension=], |alignedCornerRect|'s [=height dimension=])</code>,
translated by <code>(0.5, 0.5)</code>,
rotated by <code>90deg * orientation</code>,
and translated by <code>(-0.5, -0.5)</code>.

1. Let |K| be <code>0.5<sup>abs(|curvature|)</sup></code>.
1. For each |T| between 0 and 1:
1. Let |A| be <code>|T|<sup>|K|</sup></code>.
1. Let |B| be <code>1 - (1 - |T|)<sup>|K|</sup></code>.
1. Let |normalizedPoint| be <code>(|A|, |B|)</code> if |curvature| is positive, otherwise <code>(|B|, |A|)</code>.
1. Let |absolutePoint| be |normalizedPoint|, transformed by |projectionToCornerRect|.
1. If |absolutePoint| is within |trimRect|, extend |path| through |absolutePoint|.

Note: User agents may approximate this algorithm, for instance, by using concatenated Bezier curves, to balance between performance and rendering accuracy.

To compute the <dfn>aligned corner point</dfn> given a point |originalPoint|, a two-component vector |offsetFromControlPoint|, a number |thickness|, and a number |orientation|:
1. Let |length| be <code>hypot(|offsetFromControlPoint|.x, |offsetFromControlPoint|.y)</code>.
1. Rotate |offsetFromControlPoint| by <code>90deg * |orientation|</code>, and scale by |thickness|.
1. Translate |originalPoint| by <code>|offsetFromControlPoint|.x / |length|, |offsetFromControlPoint|.y / |length|</code>, and return the result.

The <dfn>clockwise quad</dfn> given a [=rectangle=] |rect|, is a [=quadrilateral=] with the points
(|rect|'s [=x coordinate=], |rect|'s [=y coordinate=]),
(|rect|'s [=x coordinate=] + |rect|'s [=width dimension=], |rect|'s [=y coordinate=]),
(|rect|'s [=x coordinate=] + |rect|'s [=width dimension=], |rect|'s [=y coordinate=] + |rect|'s [=height dimension=]),
(|rect|'s [=x coordinate=], |rect|'s [=y coordinate=] + |rect|'s [=height dimension=]).

</div>

<h4 id=corner-shape-constrain-radii>
Expand Down Expand Up @@ -829,7 +777,7 @@ To compute the <dfn>normalized superellipse half corner</dfn> given a [=superell
::
1. Let |k| be <code>0.5<sup>abs(|s|)</sup></code>.
1. Let |convexHalfCorner| be <code>0.5<sup>|k|</sup></code>.
1. If |param| is less than 0, return <code>1 - |convexHalfCorner|</code>.
1. If |s| is less than 0, return <code>1 - |convexHalfCorner|</code>.
1. Return |convexHalfCorner|.
</dl>
</div>
Expand Down