Skip to content

Commit 65ab9cb

Browse files
authored
[css-borders-4] Define border/shadow rendering for corner-shape (#12175)
* [css-borders-4] Define border/shadow rendering * Add note * Update chart * Overhaul the algo * nit
1 parent 83c7bff commit 65ab9cb

File tree

5 files changed

+983
-11
lines changed

5 files changed

+983
-11
lines changed

css-borders-4/Overview.bs

Lines changed: 152 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ Warning: Not Ready
2020
spec:css-text-4; type:value; text:collapse
2121
spec:css-shapes-2; type:function; text:path()
2222
spec:css-shapes-2; type:property; text:shape-inside
23+
spec:geometry-1; type: dfn; text: width dimension
24+
spec:geometry-1; type: dfn; text: height dimension
25+
spec:geometry-1; type: dfn; text: x coordinate; for: rectangle
26+
spec:geometry-1; type: dfn; text: y coordinate; for: rectangle
27+
spec:geometry-1; type: dfn; text: rectangle
28+
spec:dom; type: dfn; text: element;
2329
</pre>
2430

2531
<link rel="stylesheet" href="style.css" />
@@ -358,10 +364,144 @@ Like 'border-radius', 'corner-shape' clips elements according to the [=overflow=
358364
Since stroking a superellipse accurately may be computationally intensive, user agents may approximate the path using bezier curves,
359365
as well as account for sharp edges and overlaps.
360366

361-
Issue: 'border-radius' already handles *adjacent* corners overlapping by shrinking the radiuses proportionally.
362-
A negative ''superellipse()'' parameter allows for *opposite* corners to sometimes overlap, and needs additional restrictions defined.
363-
364-
Issue <a href="https://github.com/w3c/csswg-drafts/issues/11610">#11610</a>: check if we need additional rendering restrictions.
367+
When rendering a [=border=] for a box that has a 'border-radius' and a 'corner-shape',
368+
the inner border follows the curve of the outer shape, with a nearly consistent distance from the outer path throughout,
369+
or a linearly increasing distance if the 'border-width' of the two edges of the corner is not uniform.
370+
371+
In contrast, when rendering a 'box shadow' or when extending the [=overflow clip edge=], the resulting path does not trace border contour.
372+
Instead, it preserves the original shape and scales it in an axis-aligned manner.
373+
374+
<figure>
375+
<img src="images/corner-shape-adjusting.svg"
376+
style="background: white;"
377+
alt="Adjusting corner shapes">
378+
<figcaption>Borders are aligned to the curve, shadows and clip are aligned to the axis.</figcaption>
379+
</figure>
380+
381+
382+
An [=/element=] |element|'s <dfn>outer contour</dfn> is the [=border contour path=] given |element| and |element|'s [=border edge=].
383+
384+
An [=/element=] |element|'s <dfn>inner contour</dfn> is the [=border contour path=] given |element| and |element|'s [=padding edge=].
385+
386+
An [=/element=]'s [=border=] is rendered in the area between its [=outer contour=] and its [=inner contour=].
387+
388+
An [=/element=]'s [=overflow=] area is shaped by its [=inner contour=].
389+
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'.
390+
391+
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'.
392+
393+
<div algorithm="adjust-border-inner-path-for-corner-shape">
394+
To compute an [=/element=] |element|'s <dfn>border contour path</dfn> given an an [=edge=] |targetEdge| and an optional number |spread| (default 0):
395+
1. Let |outerLeft|, |outerTop|, |outerRight|, |outerBottom| be |element|'s [=unshaped edge|unshaped=] [=border edge=].
396+
1. Let |topLeftHorizontalRadius|, |topLeftVericalRadius|, |topRightHorizontalRadius|, |topRightVerticalRadius|, |bottomRightHorizontalRadius|,
397+
|bottomRightVerticalRadius|, |bottomLeftHorizontalRadius|, and |bottomLeftVerticalRadius| be |element| [=border edge=]'s radii.
398+
1. Let |topLeftShape|, |topRightShape|, |bottomRightShape|, and |bottomLeftShape| be |element|'s [=computed value|computed=] 'corner-*-shape' values.
399+
1. Let |targetLeft|, |targetTop|, |targetRight|, |targetBottom| [=unshaped edge|unshaped=] |targetEdge|.
400+
1. Let |path| be a new path [[SVG2]].
401+
1. Compute a [=corner path=] given
402+
the [=rectangle=] <code>(|outerRight| - |topRightHorizontalRadius|, |outerTop|, |topRightHorizontalRadius|, |topRightVerticalRadius|)</code>,
403+
0, |targetTop| - |outerTop|, |outerRight| - |targetRight|, and |topRightShape|,
404+
and append it to |path|.
405+
1. Compute a [=corner path=] given
406+
the rectangle <code>(|outerRight| - |bottomRightHorizontalRadius|, |outerBottom| - |bottomRightVerticalRadius|, |bottomRightHorizontalRadius|, |bottomRightVerticalRadius|)</code>, |targetEdge|,
407+
1, |outerRight| - |targetRight|, |outerBottom| - |targetBottom|, and |bottomRightShape|,
408+
and append it to |path|.
409+
1. Compute a [=corner path=] given
410+
the rectangle <code>(|outerLeft|, |outerBottom| - |bottomLeftVerticalRadius|, |bottomLeftHorizontalRadius|, |bottomLeftVerticalRadius|)</code>, |targetEdge|,
411+
2, |outerBottom| - |targetBottom|, |targetLeft| - |outerLeft|, and |bottomLeftShape|,
412+
and append it to |path|.
413+
1. Compute a [=corner path=] given
414+
the rectangle <code>(|outerLeft|, |outerTop|, |topLeftHorizontalRadius|, |topLeftVericalRadius|)</code>, |targetEdge|,
415+
3, |targetLeft| - |outerLeft|, |targetTop| - |outerTop|, and |topLeftShape|,
416+
and append it to |path|.
417+
1. If |spread| is not 0, then:
418+
1. Scale |path| by <code>1 + (|spread| * 2) / (|targetRect|'s [=width dimension|width=]), 1 + (|spread| * 2) / (|targetEdge|'s [=height dimension|height=])</code>.
419+
1. Translate |path| by <code>-|spread|, -|spread|</code>.
420+
421+
Note: this creates an effect where the resulting path has the same shape as the original path, but scaled to fit the given spread.
422+
1. Return |path|.
423+
424+
To compute the <dfn>corner path</dfn> given a rectangle |cornerRect|, a rectangle |trimRect|, and numbers |startThickness|, |endThickness|, |orientation|, and |curvature|:
425+
1. Assert: |orientation| is 0, 1, 2, or 3.
426+
1. If |curvature| is less than zero, then:
427+
1. Set |curvature| to <code>-|curvature|</code>.
428+
1. Swap between |startThickness| and |endThickness|.
429+
1. Set |orientation| to (|orientation| + 2) % 4.
430+
1. Let |cornerPath| be a path that begins at <code>(0, 1)</code>.
431+
1. Switch on |curvature|:
432+
<dl class=switch>
433+
: 0
434+
:: Extend |cornerPath| by adding a straight line to <code>(1, 0)</code>.
435+
436+
: &infin;
437+
::
438+
1. Extend |cornerPath| by adding a straight line to <code>(1, 1)</code>.
439+
1. Extend |cornerPath| by adding a straight line to <code>(1, 0)</code>.
440+
441+
: Otherwise
442+
::
443+
1. Let |K| be <code>0.5<sup>|curvature|</sup></code>.
444+
1. For each |T| between 0 and 1, extend |cornerPath| through <code>(|T|<sup>|K|</sup>, (1−|T|)<sup>|K|</sup>)</code>.
445+
446+
User agents may approximate this path, for instance, by using concatenated Bezier curves, to balance between performance and rendering accuracy.
447+
</dl>
448+
449+
1. Let (|x|, |y|, |width|, |height|) be |targetRect|.
450+
1. Let |clockwiseRectQuad| be « (|x|, |y|), (|x| + |width|, |y|), (|x| + |width|, |y| + |height|), (|x|, |y| + height|) ».
451+
1. Let |curveStartPoint| be |clockwiseRectQuad|[|orientation|].
452+
1. Let |curveEndPoint| be |clockwiseRectQuad|[(|orientation| + 2) % 4].
453+
1. If either |startThickness| or |endThickness| is greater than 0, then:
454+
455+
Note: the following substeps compute a new |curveStartPoint| and |curveEndPoint|, based on the thickness and |curvature|.
456+
The start and end points are offset inwards, perpendicular to the direction of the curve, with the corresponding |startThickness| or |endThickness|.
457+
458+
1. Let |tangentUnitVector| be <code>(1, 0)</code>.
459+
460+
Note: |tangentUnitVector| is a unit vector (length of 1 pixel) that points along a curve with both positive X and Y components
461+
(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
462+
and scaled to match the required border thickness.
463+
For round curvatures, or for hyperellipses (|curvature| greater than 1), the tangent is a horizontal line to the right.
464+
465+
<figure>
466+
<img src="images/corner-shape-target-unit-vector-round.svg"
467+
style="background: white; padding: 8px;"
468+
alt="Tangent unit vector with round (s=1)">
469+
<figcaption>When the 'corner-shape' is ''corner-shape/round'' or more convex (<code>>= 1</code>), the unit vector is <code>1, 0</code>.
470+
</figcaption>
471+
</figure>
472+
473+
1. If |curvature| is less than 1:
474+
1. Let |halfCorner| be the [=normalized superellipse half corner=] given |curvature|.
475+
1. Let |offsetX| be <code>max(0, (|halfCorner| - 1) * 2 + &Sqrt;2)</code>.
476+
1. Let |offsetY| be <code>max(0, &Sqrt;2 - |halfCorner| * 2)</code>.
477+
478+
Note: This formula defines the tangent of a quadratic Bezier curve that's equivalent to a superellipse quadrant.
479+
Notably, convex hypoellipses (superellipses with a [=superellipse parameter|parameter=] between 0 and 1) can be very precisely represented by quadratic curves.
480+
481+
1. Let |length| be <code>hypot(|offsetX|, |offsetY|)</code>.
482+
1. Set |tangentUnitVector| to <code>(|offsetX| / |length|, |offsetY| / |length|)</code>.
483+
484+
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>.
485+
486+
<figure>
487+
<img src="images/corner-shape-target-unit-vector-bevel.svg"
488+
style="background: white; padding: 8px;"
489+
alt="Tangent unit vector with bevel (s=0)">
490+
<figcaption>When the 'corner-shape' is ''corner-shape/bevel'' (<code>0</code>), the unit vector is <code>&Sqrt;2/2, &Sqrt;2/2</code>.
491+
</figcaption>
492+
</figure>
493+
494+
1. Let |startOffset| be |tangentUnitVector|, scaled by |startThickness| and rotated <code>90° * ((|orientation| + 1) % 4)</code> clockwise.
495+
1. Let |endOffset| be |tangentUnitVector|, scaled by |endThickness| and rotated by <code>90° * ((|orientation| + 2) % 4)</code> clockwise.
496+
1. Translate |curveStartPoint| by |startOffset|.
497+
1. Translate |curveEndPoint| by |endOffset|.
498+
1. Set |cornerRect| to a rectangle that contains |curveStartPoint| and |curveEndPoint|.
499+
1. Rotate |cornerPath| by <code>90° * |orientation|</code>, with <code>(0.5, 0.5)</code> as the origin, as described [=transformation matrix|here=].
500+
1. Scale |cornerPath| by <code>|cornerRect|'s [=width dimension|width=], |cornerRect|'s [=width dimension|height=]</code>.
501+
1. translate |cornerPath| by<code> |cornerRect|'s [=x coordinate|x=], |cornerRect|'s [=y coordinate|y=]</code>.
502+
1. Trim |cornerPath| to |trimRect|.
503+
1. Return |cornerPath|.
504+
</div>
365505

366506
<h4 id=corner-shape-value>
367507
'corner-shape' values</h4>
@@ -396,7 +536,7 @@ Issue <a href="https://github.com/w3c/csswg-drafts/issues/11610">#11610</a>: che
396536
It is a number between <css>-infinity</css> and <css>infinity</css>, with <css>-infinity</css> corresponding to a straight concave corner,
397537
<css>infinity</css> corresponding to a square convex corner.
398538

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

402542
<pre>
@@ -415,10 +555,10 @@ Issue <a href="https://github.com/w3c/csswg-drafts/issues/11610">#11610</a>: che
415555

416556
<figure>
417557
<img src="images/superellipse-param.svg"
418-
width="320" height="240"
419-
style="background: white; padding: 8px;"
420-
title="rendering of different superellipse parameter values"
421-
alt="Rendering of different superellipse parameter values.">
558+
width="320" height="240"
559+
style="background: white; padding: 8px;"
560+
title="rendering of different superellipse parameter values"
561+
alt="Rendering of different superellipse parameter values.">
422562
<figcaption>
423563
Rendering examples of different ''superellipse()'' values.
424564
</figcaption>
@@ -504,7 +644,7 @@ Since it uses a <code>log2</code>, interpolating it linearly would result in an
504644
To balance that, the <dfn>superellipse interpolation</dfn> formula describes how a [=superellipse parameter=] is converted to a value between 0 and 1, and vice versa:
505645

506646
<div algorithm="superellipse-param-to-interpolation-value">
507-
To interpolate a <<number [-&infin;,&infin;]>> |s| to an interpolation value between 0 and 1, return the first matching statement, switch on |s|:
647+
To compute the <dfn>normalized superellipse half corner</dfn> given a [=superellipse parameter=] |s|, return the first matching statement, switching on |s|:
508648
<dl class=switch>
509649
: -&infin;
510650
:: Return 0.
@@ -518,9 +658,10 @@ To interpolate a <<number [-&infin;,&infin;]>> |s| to an interpolation value bet
518658
1. Let |convexHalfCorner| be <code>0.5<sup>|k|</sup></code>.
519659
1. If |param| is less than 0, return <code>1 - |convexHalfCorner|</code>.
520660
1. Return |convexHalfCorner|.
521-
522661
</dl>
523662

663+
To interpolate a [=superellipse parameter=] |s| to an interpolation value between 0 and 1, return the [=normalized superellipse half corner=] given |s|.
664+
524665
To convert a <<number [0,1]>> |interpolationValue| back to a [=superellipse parameter=], switch on |interpolationValue|:
525666
<dl class=switch>
526667
: 0

0 commit comments

Comments
 (0)