Skip to content
223 changes: 138 additions & 85 deletions css-borders-4/Overview.bs
Original file line number Diff line number Diff line change
Expand Up @@ -1467,104 +1467,157 @@ The 'corner' shorthand</h4>
<h4 id=corner-shape-rendering>
Rendering 'corner-shape'</h4>

When rendering elements with shaped corners, the element's path needs to be offset,
When rendering elements with shaped corners, the element's [=border contour 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.

<figure>
<img src="images/corner-shape-adjusting.svg" width="600" height="537"
alt="Adjusting corner shapes"
>
<figcaption>Borders are aligned to the curve, shadows and clip are aligned to the axis.</figcaption>
</figure>

When rendering borders or outlines, the offset is aligned to the curve of the element's shape.
When rendering 'box-shadow' or offsetting for 'overflow-clip-margin', the offset is also aligned to the same curve in the other direction, and the curve continues to the outer edge.

An [=/element=] |element|'s <dfn>outer contour</dfn> is the [=border contour path=] given |element| and |element|'s [=border edge=].

An [=/element=] |element|'s <dfn>inner contour</dfn> is the [=border contour path=] given |element| and |element|'s [=padding edge=].
An [=/element=] |element|'s <dfn>inner contour</dfn> is the [=border contour path=] given |element| and |element|'s [=border width=].

An [=/element=]'s [=border=] is rendered in the area between its [=outer contour=] and its [=inner contour=].

An [=/element=]'s [=outline=] follows the [=outer contour=] with the [=used value|used=] 'outline-width' and 'outline-offset'.
The precise way in which it is rendered is implementation-defined.

An [=/element=]'s [=overflow=] area is shaped by its [=inner contour=].
An [=/element=]'s [=overflow clip edge=] is shaped by the [=border contour path=] given |element|, and |element|'s [=padding edge=], and |element|'s [=used value|used=] 'overflow-clip-margin'.

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 [=edge=] |targetEdge| and an optional number |spread| (default 0):
1. Let |outerLeft|, |outerTop|, |outerRight|, |outerBottom| be |element|'s [=unshaped edge|unshaped=] [=border edge=], outset by |spread|.
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=] and [=outset-adjusted border radius|outset-adjusted=].
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. [=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. Return |path|.
An [=/element=]'s [=overflow clip edge=] is shaped by the [=border contour path=] given |element|, and an [=uniform inset from an outset=] given |element|'s [=used value|used=] 'overflow-clip-margin'.

Each shadow of [=/element=]'s 'box-shadow' is shaped by the [=border contour path=] given |element|, and an [=uniform inset from an outset=] given the shadow's [=used value|used=] 'box-shadow-spread'.

An <dfn>uniform inset from an outset</dfn> given a number |outset| is an [=edge=] whose value is <code>-|outset|</code> in all directions.

<h5 id=vector-math-helpers>Vector Math Helpers</h5>

A <dfn>two-dimensional vector</dfn> is a pair of numbers (x, y).
<div algorithm="extend-point">
A <dfn>point extended by vectors</dfn> given a {{DOMPointReadOnly}} |p| and a [=/list=] of [=two-dimensional vector=]s |vectors|:
1. Let |x| be |p|'s {{DOMPointReadOnly/x}}.
1. Let |y| be |p|'s {{DOMPointReadOnly/y}}.
1. [=list/For each=] |v| in |vectors|:
1. Increment |x| by |v|[0];
1. Increment |y| by |v|[1];
1. Return a new {{DOMPointReadOnly}} whose {{DOMPointReadOnly/x}} is |x| and whose {{DOMPointReadOnly/y}} is |y|.
</div>

To <dfn for="two-dimensional vector">scale</dfn> a [=two-dimensional vector=] |v| by a number |factor|, return <code>(|v|[0] ⋅ |factor|, |v|[1] ⋅ |factor|)</code>.

<div algorithm="normalize-vector">
To <dfn for="two-dimensional vector">normalize</dfn> a [=two-dimensional vector=] |v|:
1. Let |l| be <code>hypot(|v|[0], |v|[1])</code>.
1. If |l| is 0, return |v|.
1. Return <code>(|v|[0] / |l|, |v|[1] / |l|)</code>.
</div>

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 ∞:
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 -∞:
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=]).
The <dfn for="two-dimensional vector">perpendicular</dfn> of a [=two-dimensional vector=] |v| is <code>(-|v|[1], |v|[0])</code>.

The <dfn>vector between two points</dfn>, given {{DOMPoint}}s |a| and |b|, is <code>(|b|'s {{DOMPointReadOnly/x}} - |a|'s {{DOMPointReadOnly/x}}, |b|'s {{DOMPointReadOnly/y}} - |a|'s {{DOMPointReadOnly/y}})</code>.

<h5 id=contour-path>Computing a contoured path</h5>

This algorithm describes how to compute a path with shaped corners (a path that is either inset or outset from the original).
To avoid the complexities of defining how superellipses intersect, the algorithm simplifies the process by specifying that each corner is "clipped out" of the path.
The specific implementation details of this clipping operation are left to implementations.

<div algorithm="border-aligned-contour-path">
To compute an [=/element=] |element|'s <dfn>border contour path</dfn> given numbers |topInset|, |rightInset|, |bottomInset|, |leftInset|:
1. Let |borderRect| be |element|'s [=border box=].
1. Let |unshapedTargetRect| be |borderRect|, inset by |topInset|, |rightInset|, |bottomInset|, |leftInset|.

Note: If this is a shadow or 'overflow-clip-margin', the insets would have negative values and |unshapedTargetRect| would become an outset of |borderRect|.

1. Let |path| be a path that contains |unshapedTargetRect|.

1. Let |scaleFactor| be the [=opposite corner scale factor=] given |element|.

1. Let |adjustedRadius| be the following steps given a property |P|, and numbers |insetX| and |insetY|:
1. Let |radius| be |element|'s [=used value=] of |P|.
1. If |insetX| and |insetY| are zero, return |radius|.
1. If |insetX| or |insetY| are positive, then return <code>(|radius|'s [=width=] ⋅ |scaleFactor|, |radius|'s [=height=] ⋅ |scaleFactor|)</code>.
1. Let |adjustedRadiusInOutsetCoordinates| be the [=outset-adjusted border radius=] given |borderRect|'s size, |radius|, and <code>(-|insetX|, -|insetY|)</code>.
1. Return <code>(|adjustedRadiusInOutsetCoordinates|'s [=width=] - |insetX|, |adjustedRadiusInOutsetCoordinates|'s [=height=] - |insetY|)</code>.

1. Let |adjustedTopRightRadius| be the |adjustedRadius| given 'border-top-right-radius', |insetRight|, and |insetTop|.
1. Let |adjustedBottomRightRadius| be the |adjustedRadius| given 'border-bottom-right-radius', |insetRight|, and |insetBottom|.
1. Let |adjustedBottomLeftRadius| be the |adjustedRadius| given 'border-bottom-left-radius', |insetLeft|, and |insetBottom|.
1. Let |adjustedTopLeftRadius| be the |adjustedRadius| given 'border-top-left-radius', |insetLeft|, and |insetTop|.

1. Clip out from |path|, the [=border-aligned corner clip-out path=] given
|borderRect|'s right, |borderRect|'s top,
<code>(-|adjustedTopRightRadius|[0], 0)</code>, <code>(0, |adjustedTopRightRadius|[1])</code>,
|element|'s [=computed value|computed=] 'corner-top-right-shape',
|topInset|, and |rightInset|.

1. Clip out from |path|, the [=border-aligned corner clip-out path=] given
|borderRect|'s right, |borderRect|'s bottom,
<code>(0, -|adjustedBottomRightRadius|[1])</code>, <code>(-|adjustedBottomRightRadius|[0], 0)</code>,
|element|'s [=computed value|computed=] 'corner-bottom-right-shape',
|rightInset|, and |bottomInset|.

1. Clip out from |path|, the [=border-aligned corner clip-out path=] given
|borderRect|'s left, |borderRect|'s bottom,
<code>(|adjustedBottomLeftRadius|[0], 0)</code>, <code>(0, -|adjustedBottomLeftRadius|[1])</code>,
|element|'s [=computed value|computed=] 'corner-bottom-left-shape',
|bottomInset|, and |leftInset|.

1. Clip out from |path|, the [=border-aligned corner clip-out path=] given
|borderRect|'s left, |borderRect|'s top,
<code>(0, |adjustedTopLeftRadius|[1])</code>, <code>(|adjustedTopLeftRadius|[0], 0)</code>,
|element|'s [=computed value|computed=] 'corner-top-left-shape',
|leftInset|, and |topInset|.

1. Return |path|.

To get the <dfn>border-aligned corner clip-out path</dfn> given a {{DOMPointReadOnly}} |originalCornerOuter|, a [=two-dimensional vector=] |vectorTowardsStart|, a [=two-dimensional vector=] |vectorTowardsEnd|, a [=superellipse parameter=] |curvature|, and numbers |startInset| and |endInset|:
1. If |curvature| is ∞, then return an empty path.
1. Let |clampedHalfCorner| be the [=normalized superellipse half corner=] given <code>clamp(|curvature|, -1, 1)</code>.
1. Let |originalCornerStart| be |originalCornerOuter|, [=point extended by vectors|extended by=] « |vectorTowardsStart| ».
1. Let |originalCornerEnd| be |originalCornerOuter|, [=point extended by vectors|extended by=] « |vectorTowardsEnd| ».
1. Let |originalCornerCenter| be |originalCornerOuter|, [=point extended by vectors|extended by=] « |vectorTowardsStart|, |vectorTowardsEnd| ».
1. Let |extendStart| be a [=vector between two points|vector between=] |originalCornerStart| and |originalCornerCenter|, [=two-dimensional vector/normalize|normalized=] and [=two-dimensional vector/scale|scaled by=] |startInset|.
1. Let |extendEnd| be a [=vector between two points|vector between=] |originalCornerEnd| and |originalCornerCenter|, [=two-dimensional vector/normalize|normalized=] and [=two-dimensional vector/scale|scaled by=] |endInset|.
1. Let |clipStart| be |originalCornerStart|, [=point extended by vectors|extended by=] « |extendStart| ».
1. Let |clipEnd| be |originalCornerEnd|, [=point extended by vectors|extended by=] « |extendEnd| ».
1. Let |clipOuter| be |originalCornerOuter|, [=point extended by vectors|extended by=] « |extendStart|, |extendEnd| ».
1. Let |vectorFromStartToControlPoint| be the the [=two-dimensional vector=] <code>(2 ⋅ |clampedHalfCorner| - 0.5, 1.5 - 2 ⋅ |clampedHalfCorner|)</code>.
1. Let |singlePixelVectorFromStartToControlPoint| be the |vectorFromStartToControlPoint|, [=two-dimensional vector/normalize|normalized=].
1. Let <code>|strokeA|, |strokeB|</code> be the [=two-dimensional vector/perpendicular=] of |singlePixelVectorFromStartToControlPoint|.
1. Let |offset1| be [=vector between two points|the vector between=] |originalCornerStart| and |outerCorner|, [=two-dimensional vector/normalize|normalized=] and [=two-dimensional vector/scale|scaled by=] <code>|startInset| ⋅ |strokeA|</code>.
1. Let |offset2| be [=vector between two points|the vector between=] |outerCorner| and |originalCornerEnd|, [=two-dimensional vector/normalize|normalized=] and [=two-dimensional vector/scale|scaled by=] <code>|startInset| ⋅ |strokeB|</code>.
1. Let |offset3| be [=vector between two points|the vector between=] |originalCornerEnd| and |originalCornerCenter|, [=two-dimensional vector/normalize|normalized=] and [=two-dimensional vector/scale|scaled by=] <code>|endInset| ⋅ |strokeB|</code>.
1. Let |offset4| be [=vector between two points|the vector between=] |originalCornerCenter| and |originalCornerStart|, [=two-dimensional vector/normalize|normalized=] and [=two-dimensional vector/scale|scaled by=] <code>|endInset| ⋅ |strokeA|</code>.
1. Let |adjustedCornerStart| be |originalCornerStart|, [=point extended by vectors|extended by=] « |offset1|, |offset2| ».
1. Let |adjustedCornerEnd| be |originalCornerEnd|, [=point extended by vectors|extended by=] « |offset3|, |offset4| ».
1. Let |adjustedCornerCenter| be |originalCornerCenter|, [=point extended by vectors|extended by=] « |offset4|, |offset1| ».
1. Let |adjustedCornerOuter| be |originalCornerOuter|, [=point extended by vectors|extended by=] « |offset2|, |offset3| ».
1. Let |curveCenter| be |adjustedCornerOuter| if |curvature| is less than 0, |adjustedCornerCenter| otherwise.
1. Let |mapPointToCorner| be the following steps: given numbers |x| and |y|:
1. Let |v1| be [=vector between two points|the vector between=] |curveCenter| and |adjustedCornerEnd|, [=two-dimensional vector/scale|scaled by=] |x|.
1. Let |v2| be [=vector between two points|the vector between=] |curveCenter| and |adjustedCornerStart|, [=two-dimensional vector/scale|scaled by=] |y|.
1. Return the {{DOMPointReadOnly}} at <code>(|x|, |y|)</code>, [=point extended by vectors|extended by=] « |v1|, |v2| ».

1. Let |controlPoint| be the result of calling |mapPointToCorner| given |vectorFromStartToControlPoint|'s {{DOMPointReadOnly/y}} and <code>1 - |unitVectorFromStartToControlPoint|'s {{DOMPointReadOnly/x}}</code>.
1. Let |axisAlignedCornerStart| be the intersection between the lines <code>(|adjustedCornerStart|, |controlPoint|)</code> and <code>(|clipStart|, |clipOuter|)</code>. If the lines are parallel, let it be <code>adjustedCornerStart</code>.
1. Let |axisAlignedCornerEnd| be the intersection between the lines <code>(|adjustedCornerEnd|, |controlPoint|)</code> and <code>(|clipEnd|, |clipOuter|)</code>. If the lines are parallel, let it be <code>adjustedCornerEnd</code>.

Note: |axisAlignedCornerStart| and |axisAlignedCornerEnd| act as "miters" when rendering an outset.
They are a straight extension of the curve's tangent, intersecting with the unshaped target rect.

1. Let |path| be a path, starting at |axisAlignedCornerStart|.
1. If |curvature| is -∞:
1. Extend |path| to |adjustedCornerStart|.
1. Extend |path| to |adjustedCornerCenter|.
1. Extend |path| to |adjustedCornerEnd|.
1. Otherwise:
1. Let |K| be <code>0.5<sup>-abs(|curvature|)</sup></code>.
1. For every |T| between 0 and 1, in an [=implementation-approximated=] manner,
extend |path| to the result of calling |mapPointToCorner| given <code>|T|<sup>|K|</sup></code> and <code>(1 - |T|)<sup>|K|</sup></code>.

1. Extend |path| to |axisAlignedCornerEnd|.
1. Extend |path| to |clipOuter|.
1. Return |path|.
</div>

<wpt>
Expand Down Expand Up @@ -1635,7 +1688,7 @@ To compute the <dfn>normalized superellipse half corner</dfn> given a [=superell
: Otherwise
::
1. Let |k| be <code>0.5<sup>abs(|s|)</sup></code>.
1. Let |convexHalfCorner| be <code>0.5<sup>|k|</sup></code>.
1. Let |convexHalfCorner| be <code>0.5<sup>1/|k|</sup></code>.
1. If |s| is less than 0, return <code>1 - |convexHalfCorner|</code>.
1. Return |convexHalfCorner|.
</dl>
Expand Down