satgo1546’s ocean

Any sufficiently primitive magic is indistinguishable from technology.

慢报:CSS倒角、内圆角、无意义方圆角、无意义超椭圆角已于Chrome 139实装

什么是按钮?

GitHub Google Microsoft Apple

众所周知,按钮可以没有填充,可以没有描边,可以没有阴影,可以按不下去,但不能没有圆角!

border-radius将网页工程师从繁琐的切图中解放,却也消灭了其他形状的边框。设计师懒得画,程序员懒得实现,便利的圆角就此支配了整个互联网审美十余年。

好消息是,再等几年,倒角和内圆角也可以一样偷懒了。

corner-shape

CSS Borders and Box Decorations Module Level 4 § Corner Shaping: the corner-shape property目前已定稿,可供厂商实现了。Chrome是最先实装该功能的浏览器。

corner-shape属性指定圆角的超椭圆参数(?),即xn + yn = 1中的n

superellipse 4 2√2 2 2 1 1/√2 9/16 1/2 0

不过corner-shape: superellipse(⟨数值⟩)的语法指定的值并不是n,而是log2 n。据标准所述,这是为了扩大参数取值范围到全体实数,正值外凸,负值内凹,直线为0。

为什么CSSWG总喜欢搞奇奇怪怪的数学公式?套公式也就算了,可是套用的公式明显不符合直觉。我以前吐槽过CSS带中点的渐变插值不对称;这回,n = ½(内圆角)和n = 2(外圆角)还是不对称!

xn + yn = 1形式的超椭圆不可能在第一象限画出(x − 1)2 + (y − 1)2 = 1形式的内圆弧。将corner-shape定义的一个内圆角和一个外圆角拼在一起,会留下很大的缝隙:

½ 2

调整n,可以观察到在n = 9/16附近,超椭圆才与真正的内圆角近似重合,误差约为1‰。

9/16 2

可是,当我用Chrome测试表现时发现,scoop和round的形状之间并不会产生理论上的大缝隙,反倒是superellipse(-0.83)和round之间有缝隙。

这是你的浏览器渲染上图的效果:

这是Chrome的实现有问题吗?我翻了Chromium源码,找到了将超椭圆转换为近似的贝塞尔曲线的函数ApproximateSuperellipseHalfCornerAsBezierCurve,其中明确注释:

  // This formula only works with convex superellipses. To apply to a concave
  // superellipse, flip the center and the outer point and apply the
  // equivalent convex formula (1/curvature).
  DCHECK_GE(curvature, 1);

corner-shape相关的代码将超椭圆参数n误称为曲率(curvature);超椭圆上各点曲率并不相同。

稍向下,能看到实现确实是把n > 1和n < 1的情况特地当成对称来处理了。

void AddCurvedCorner(SkPath& path, const Corner& corner) {
  if (corner.IsConcave()) {
    AddCurvedCorner(path, corner.Inverse());
    return;
  }

我在w3c/csswg-drafts发了个议题指出这一问题。@tabatkins回复道,标准确实规定要将内凹角翻成外凸角来绘制,只是这一点没有在文章中说明,而是由规定的执行步骤确定。

  1. For each T between 0 and 1:

    1. Let A be TK.
    2. Let B be 1 - (1 - T)K.
    3. Let normalizedPoint be (A, B) if curvature is positive, otherwise (B, A).
    4. Let absolutePoint be normalizedPoint, transformed by projectionToCornerRect.
    5. If absolutePoint is within trimRect, extend path through absolutePoint.

(t1/n, (1 − t)1/n) (0 ≤ t ≤ 1)是超椭圆的另一个参数方程形式。

所以,可以说corner-shape为了参数对称性,只支持n > 1的超椭圆。下面才是目前corner-shape实际能绘制的圆角形状:

superellipse() ∞ = square 2 = squircle 1.5 1 = round 0.5 0 = bevel −0.5 −1 = scoop −2 −∞ = notch

超椭圆……?

话说回来,到底为什么选择超椭圆?超椭圆泛用性较低,对策性较低,总的来说属于小杯。看上去参数化可定制,实际只能产生直线段和形状确定且怪异的弧状曲线,有个性的边角依然遥远。面对设计师的异想天灾,程序员总是无计可施。

面对这样的边角,贴图是永远的幻神

我翻阅了提议添加corner-shape: superellipse()议题解说另一份解说。该功能的主要目的在于在兼容目前的border-radius的前提下支持方圆形(squircle)——一种在原生平台已广泛使用(“Native platforms have had different versions of them for a long time”)、比正圆弧更美观的圆角画法(“Being able to design beautiful websites”),以解决设计师的诉求(“Some folks in the design community have been asking for this persistently”)。个性化边角将由border-shape负责,不在corner-shape处理范围内。

根据MathWorld上没有标注参考文献的定义,方圆形是超椭圆指数n = 4的特殊情况。如此一来,超椭圆是圆角的严格推广,覆盖方圆形的同时也支持直角、倒角、外圆角、切角,通过人造对称也支持了内圆角,单一参数还方便了动画插值。既然要提供方圆形,不如提供参数化的超椭圆。

……是不是有哪里不对?

设计师怎么可能指定一种怪诞的数学曲线?

方圆形到底是什么?

让我们回看设计师的初衷。正圆角有什么问题,以至于要换成别的曲线?

这是平凡的圆角矩形:

即便没有辅助,仅凭肉眼感觉,就能定位直线段和圆弧的相接点。四个圆角就像缝上去的一样,明明拼合处切线方向一致,却又好像凸出来一般,分外扎眼。

这是因为直线段与圆弧之间存在曲率突变,人眼能捕捉到曲率突变带来的不和谐。

下面是从Apple Design Resources页面获取的适用于iOS 26和iPad OS 26的应用图标模板。

同样是圆角矩形,它就浑然一体,直线段和圆弧之间过渡自然,很难说直线段到某个点结束,圆弧从某个点开始。

直线段和圆弧之间的过渡带缓和了拼接带来的曲率突变,消除了拼接处的不协调感。

iOS应用图标这种打磨过的圆角矩形的形状被设计师称为方圆形。虽然曲线路径公开,其背后的原理我们仍无从得知。自从iOS 7用方圆形换掉了标准圆角矩形以来,有许多破解其中奥秘的尝试,其中不得不提的是在corner-shape议题和解说中频繁提到的一篇来自Figma的Daniel Furse的博文Desperately seeking squircles

既然目的是避免曲率突变,作者便计算了iOS图标圆角处各点的曲率。绘图可见分为曲率上升-保持-下降三段,曲率保持不变的部分就是原先的圆角,曲率变化的部分是打磨产物。

Desperately seeking squircles § Squircles under the scalpel

iOS 7刚发布时,Marc Edwards最早尝试了用超椭圆拟合iOS图标,用叠图的方法得出了超椭圆指数n = 5的结论。这是打磨过的圆角矩形被传为方圆形的原因。

然而,图标的形状从根本上就不可能是超椭圆,因为超椭圆的曲率只有上升-下降,虽然解决了曲率突变,但原先的平顶部分没有了。换句话说,超椭圆不够圆。

Figma最终也没有搞出iOS图标那样的圆角,而是出于泛用性考虑实现了另一种同样缓和了曲率突变,同时保留圆角印象的曲线,通过调整参数能实现与iOS足够近似的效果。

主流平台中只有苹果的使用方圆形。苹果以外的平台秉持着能用就行的态度,绝不可能打磨圆角的细节。Android文档中唯一提到方圆形的页面Adaptive icons只是将其作为一种厂商自定义的可能性,例图也与圆角矩形相去甚远。Flutter提供方圆形支持(议题),是为了匹配原生应用样式,仅用于iOS。corner-shape解说中提到的所谓“复数原生平台早已支持”,指的大概是macOS、iOS、watchOS、tvOS这几个平台吧。

结论

corner-shape提供的方圆形与设计师所追求的方圆形没有任何关系。

除了预定义值(round、square、bevel、scoop、notch,除去squircle),其他superellipse()没有应用场景。因为square、bevel、notch基于superellipse()定义,它们之间的动画插值会经过怪异的弧线。

我的内心有一万匹草泥马奔腾而过。太幽默了这CSS,闹半天实现的又是虚空需求 😾

在GitHub上查看和发表评论