添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
火星上的蚂蚁  ·  An Optimized ...·  4 月前    · 
腹黑的槟榔  ·  Invalid header ...·  4 月前    · 
淡定的枇杷  ·  Git:git-rev-parse ...·  1 年前    · 

超越chrome?深入css的圆角绘制

妈妈说文章标题不起的醒目点就没有人看,于是就夸大其词地写了这个。
在某些极端情况下,karas确实比chrome好那么一丢丢,但许多其它情况还是有不如的。
感谢@织艺 共建贝塞尔拟合椭圆弧的公式、证明和求值方法,@子舆 共建夹边几何交点计算公式。
本文为纯web图形学技术,最初发于2020-07-25语雀,现转载知乎。

前言

圆角是ui视觉中很常见的一种设计,对应css的属性是border-radius。它的使用非常简单,只要赋予一个像素数值或百分比,即可让矩形4个角中的某些角圆化。



div {
  width: 200px;
  height: 100px;
  border-radius: 10px;
  background: #C90;
}

它的设计和使用是如此得简单直观,以至于日常开发完全没有考虑过背后的故事,这里面蕴藏着数学和工程之美。

场景

上述情况有一个大前提,就是使用css在html页面绘制,浏览器渲染引擎已经完全封装好了。然而在富交互/动画/小游戏场景中,视觉如果使用了圆角该怎么办?

换句话说,即在canvas/svg中,圆角要怎么实现?



还是以这个微动画为例,抽大奖文字有红色的背景,且是圆角。

熟悉canvas的人肯定能想出来:用arc()方法画弧线部分,用lineTo()绘制直线部分,整体用路径即可。



// 伪代码,移动到h点开始 ctx.moveTo(h);
// 画h到a的直线 ctx.lineTo(a);
// 假设以o为中心点画a到b的1/4圆弧 ctx.arc(o, a, b);
// 画b到c的直线 ctx.lineTo(c); // 其他3个角类似省略

限制

这只是个特例,假定了圆角一定是个正1/4圆,事实上椭圆才是更常见的。然而椭圆并不能直接绘制出来,更不能只画其中的某一部分(如1/4椭圆、椭圆的外圈)。这时候问题就没那么简单了。



不止是背景色,border宽度不为0时,边框本身也要绘制出来。甚至边框还带虚线样式,被划分为多个不规则的弧块,这个场景能让人抓狂。



如果用过类似sketch的软件,你会发现,里面所有的圆弧都有2个贝塞尔控制点。

以常见的圆形为例,它有4段三阶贝塞尔曲线,每段控制1/4圆。



事实上,圆弧并不能直接被绘制出,只能通过计算近似值来达到,然后用贝塞尔模拟画。

这个计算方式叫做拟合。

浏览器中svg的矢量圆就是用的这种方法。

拟合

这个解法的关键点在于求得2个控制点的坐标,即每个控制点到其端点的长度h和半径r以及角度θ的关系。



具体推导过程在这里: blog.csdn.net/nibiewuxu

最终得出,r视作单位1时h和θ的关系为:



在1/4正圆弧夹角90°时,它等于0.5522847498307936,搜一搜就会发现,很多开源库中用了这个魔法数字。

扩展论文阅读:
tinaja.com/glib/ellipse 更好的魔法数字 0.551784
spencermortensen.com/ar 另一个更好的0.55191502449
math.stackexchange.com/ 首尾不为0,1的误差更小

误差

这种方法其实是有误差的,在0度、45度和90度为最小误差0.000000,在19.3度和70.7度达到最大误差为0.000273,基本上非常接近1/4圆弧了。

来源: jianshu.com/p/5bd843e8d

在超过1/4圆弧后误差会越来越大,这也是为什么一个圆要分为4份来绘制。

当然划分越多越精细,一般都采用4分法或8分法。



椭圆

上面的公式还是针对正圆,那么椭圆呢?

数学中椭圆可以用伸缩变换(仿射变换)转化为圆,通俗点的说法就是,椭圆可以视作一个压扁的圆,伸缩一个轴使得和另外一个轴保持比例即可变成正圆。

那么,只要先将椭圆变化为正圆,求得贝塞尔曲线控制点后,再反向伸缩变换回来不就解决了?



扩展阅读:椭圆真的是个被压扁的圆吗?——伸缩变换
bilibili.com/read/cv317

规范

回到正题,参阅css的border-radius规范: drafts.csswg.org/css-ba





总结一下,就是圆心确定,角度在90°以内,半径不同的2段圆弧组成边框的内外边,然后端点直线相连。



一段边框的绘制:知道border-radius为r2(外边,内边需减去border-width得r1),这段边框形成的夹角θ(dashed/dotted算法规范并未规定,可以自己实现,solid则为特殊情况90°),便可求出4个端点a、b、c、d。

其中ab为一段圆弧,cd为一段圆弧,各自用贝塞尔曲线拟合,再相连端点ac和bd,即组成图中的一块不规则弧块。

交点

知道方法后,关键就是求交点abcd,这比较简单,从最初没有radius的状态出发, 此时是个矩形(首尾比较特殊是个四边形下面夹边讲)。

连接端点和圆心,和圆弧相交的点即是要求的交点。



夹边

考虑这样一种情况,两条边相交的这条夹边radius化会怎么样?



截图对比不难发现,夹边和原本的斜率一致,延长线是重合的。这也符合规范要求的相邻两条边均分。

它不能用上述方法求,因为上述是个特例,两条邻边宽度相等且xy方向的半径也相等。

一旦不等,延长线并不会和圆心相交。这需要一个公式,求解夹边延长线和圆边的交点。



已知夹边的夹角θ,想求x/y的坐标,实际就是求α角,然后交点连线圆心知道半径,通过三角函数就能得解。




通过对tanθ的化简最终得出上述式子,此时面临开根。



由于角度范围确定,所以根号正负值也确定,最终得出上述角度关系。如此交点x/y坐标便得出了。

工程

以上即是理论部分,工程实现部分非常繁杂,各种情况让人崩溃。

当圆角碰上虚线时,假如边界恰好落在一块虚线上该怎么办?

简单办法就是拆分成2个部分,只对圆弧内做圆化。



另外相邻边相交的地方,因边宽不一致有可能出现下图情况,此时绿色点其实只有3个,并不是4边形的情况,此时可以认为右上角的绿点有2个,只是重合了,实际还是个4边形,这样还是求外边和内边的三阶贝塞尔,程序写起来保证一致性。



对比

说这么多,chrome和karas在渲染对比上到底有啥不同呢?

来看个例子:

div {
  width: 100px;
  height: 100px;
  border: 20px dashed #000;
  border-right-color: #00F;
  border-top-color: #0F0;
  border-top-left-radius: 30px;
  border-left-color: #F00;
}



左图为chrome,右图为karas。

虚线的样式因没有标准定义,划分情况各自实现。chrome图中分为3块,而karas则分为2块。

chrome的渲染引擎blink在红色出现了怪异的情况。这是因为边界考虑不足,估计规范制定者都没想到会有那么多恶心的极端情况。

div {
  width: 100px;
  height: 100px;