只用CSS绘制雷达图

最近做游戏资料经常要描述六围,现在比较流行的做法就是用六围图,但是资料库系统限制导致外挂JS库或者JS代码比较麻烦,何况秉承着能用CSS绝不用JS的思路,于是想着看能不能用CSS做六围图…

就不告诉你是什么游戏

做出来差不多就是这样了:

CodePen上查看纯CSS六边形雷达图(@hikarievo)。

顺带一提,这玩意儿的学名叫雷达图…我搜什么六边形图标啊六围图啊都搜不到快急死我了……

基本思路

六围图通常是一个不规则六边形(6V选手一边去),我们已知的值就是顶点到中心的距离,而将所有的顶点与中心点连接起来之后,我们就得到了6个已知的三角形(已知两个邻边长度及其夹角就可以确定一个三角形…如果背过三角形相似/全等规则的话大概会想起SAS)。

CSS画三角本身并不困难,使用一个DOM元素加上一些CSS代码就可以画出任意三角形。但是这些三角形要么无法固定夹角度数,要么无法满足邻边长度的要求(简单来说,就是无法画出钝角三角形),所以我将目光转移到了CSS transform。如果先画出一个邻边长度满足要求的直角三角形,再把直角扭成所需圆心角的度数不就OK了嘛。一开始我想用skew()来解决这个问题,但是skew()是斜切,也就是说,在保持DOM面积不变(高度不变)的情况下,倾斜元素。但是我需要的是保持两边边长不变,倾斜元素…所以已知的CSS transform简易方法都阵亡了。于是只好祭出transform的最终杀器——matrix()黑客帝国矩阵变换),理论上matrix()可以完成任何线性变换,非常适合这种场合。

基本操作思路如下:

  1. 按照给定边长绘制直角三角形。
  2. 使用matrix()将直角三角形的直角扭成所需的圆心角。
  3. 依次旋转所得的三角形。

计算矩阵

…思路就是要跳跃,画直角三角形随便摆弄一下上面的生成器就能搞定了。问题在于矩阵要怎么办…矩阵变换听起来挺复杂的,其实只要搞明白写法就OK了。

matrix()需要提供6个参数,从a到f,转换成矩阵如下:
[acebdf001][xy1]=[ax+cy+ebx+dy+f1]\begin{bmatrix} a & c & e \\ b & d & f \\ 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} ax + cy + e \\ bx + dy + f \\ 1 \end{bmatrix}
转换成我们熟悉的方式写成方程组的话,就是:

{x=ax+cy+ey=bx+dy+f\left\{ \begin{aligned} x^{'} & = & ax + cy + e \\ y^{'} & = & bx + dy + f \end{aligned} \right.

然后我们的变换是这样的(画的平行四边形,三角形的部分是实线画起来的部分…从红色的矩形变成蓝色的平行四边形):

在这里我出了点小小的思路上的问题,在准备这篇文章的时候才发现…主要是我还保留着skew()的思路,而且当时心想着反正都是转,转几圈都是转…导致多转了一次,这个回头说。

根据上面的图解出来(解方程的时候发生了一场惨剧…按习惯画坐标轴都是右上为正,左下为负,可是我忘了!忘了!CSS渲染下的Y轴方向是向下的!向下的!!导致我对着一个方程算了俩钟头,怎么算都是反的!!):

{x=xsinθyy=cosθy\left\{ \begin{aligned} x^{'} & = & x - \sin\theta \cdot y \\ y^{'} & = & \cos\theta\cdot y \end{aligned} \right.

代入a~f得到
[1sinθ00cosθ0001]\begin{bmatrix} 1 & - \sin\theta & 0 \\ 0 & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix}

因为六边形的圆心角是60°,所以此时的θ是30°。代入得到matrix(1,0,0.5,32,0,01, 0, -0.5, \frac{\sqrt{3}}{2}, 0, 0)

旋转代入

CSS transform有一个transform-origin属性,可以规定元素变换的原点。对于前面所示的情况,显然是以我们的已知角——左下角为原点代入最合适。CSS旋转本身可以用rotate(),但是我直接测试的结果是它总转不到该转的地方,一不做二不休,干脆直接乘进去算了…

旋转的矩阵是这样的,角度为逆时针:
[cosθsinθsinθcosθ][xy]=[cosθxsinθysinθx+cosθy]\begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix} \cdot \begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} \cos\theta\cdot x - \sin\theta\cdot y \\ \sin\theta\cdot x + \cos\theta\cdot y \end{bmatrix}

因此对应六围图右上角那个三角的位置,旋转角度应该是30°,接下来的5个三角形再依次旋转60°就可以了。最终算出来的参数是

{x=cosθxcosθ+3sinθ2yy=sinθx+3cosθsinθ2y\left\{ \begin{aligned} x^{'} & = & \cos\theta\cdot x - \frac{\cos\theta + \sqrt{3}\sin\theta}{2} \cdot y \\ y^{'} & = & \sin\theta\cdot x + \frac{\sqrt{3}\cos\theta - \sin\theta}{2}\cdot y \end{aligned} \right.

代入a~f得到matrix(cosθ,sinθ,(cosθ+3sinθ)/2,(3cosθsinθ)/2,0,0)\cos\theta, \sin\theta, -(\cos\theta + \sqrt{3}\sin\theta)/2, (\sqrt{3}\cos\theta - \sin\theta)/2, 0 ,0)(这里的θ对应最终的旋转角,即30deg、90deg、150deg…)

源码可以上我的CodePen查看。

另外背景的圆环思路来自牛逼的A Single Div

进一步思考

我发现这个玩意儿不叫六围图而是叫雷达图之后…我才认识到它不光是6个角的…还有可能是5个角、7个角的,按理说是都可以实现的…因为此时对应的圆心角应该是360/n,之前算的只是n=6的情况而已。如果我一开始改变一下斜切的方向(如下图),最后就可以少算一步。

差不多就这样,理论上CSS雷达图可以达到和canvas一样的效果,唯一的遗憾就是背景是个圆…虽然可以用filter做出投影,但是想Single Div画出这些网格就相当困难了(如果想到了好办法再来update)。


UPDATE

说好的UPDATE来了…这两天写完了之后茶不思饭不想,总觉得哪里不太对………今天趁着摸鱼又仔细重算了一遍…放假的时候果然不适合干活。

原点和长度

之前我是以三角形的直角为原点建立的坐标系,这样可以简化运算…但是当原点变化的时候…伪斜切变换(姑且这么命名吧)的公式也会跟着变,以上文最后一张图(橙、黑)为例,假设坐标系原点为(a,b)(a, b),那么这个伪斜切变换的矩阵应该是
[cosθ0a(cosθ+1)sinθ1sinθa001]\begin{bmatrix} \cos\theta & 0 & -a(\cos\theta+1) \\ - \sin\theta & 1 & \sin\theta a \\ 0 & 0 & 1 \end{bmatrix}
…如果把这个斜切拆解来看,其实它是一个横坐标绕原点旋转,而纵坐标线性平移的两个不同的变换叠加而来的…这个新的矩阵只是重新移动了一下位置,旋转横坐标的部分实际上并没有变…

transform的叠加及其顺序

我前面提到,用伪斜切切完之后,应该直接用一个rotate()就可以完成拼图了,但是不知道为什么总是转不对位。这里涉及到两个问题:一个是变换的顺序,另一个是矩阵的计算规则。

我在前文中说matrix()是大杀器,是因为它是CSS变换的本质,所有的CSS变换都可以写成矩阵形式,而矩阵也是线性变换的标准数学表达方式,CSS提供的变换方法只不过是语法糖罢了。

transform本身是支持多重变换的,比如

1
<div style="transform:translate(-10px,-20px) scale(2) rotate(45deg) translate(5px,10px)"/>

这段代码,直觉上感觉应该是先向左上方移动(translate(-10px,-20px)),然后放大scale(2)),然后旋转(rotate(45deg)),最后再向右下方移动(translate(5px,10px)),但是规范上说,它在功能上等于

1
2
3
4
5
6
7
8
<div style="transform:translate(-10px,-20px)">
<div style="transform:scale(2)">
<div style="transform:rotate(45deg)">
<div style="transform:translate(5px,10px)">
</div>
</div>
</div>
</div>

也就是说,如果在同一个元素上应用了一串变换,它的变换过程是反过来的:先向右下移动,然后旋转,然后放大,最后向左上移动。这又有什么不同呢…下面是一个例子

CodePen上查看transform顺序对效果的影响(@hikarievo)。

蓝色的是先放大再旋转,黄色的是先旋转再放大…会出现这样的区别,是因为每次变换操作之间,坐标轴是不会发生变化的。

回到它的数学本质上来解释:多次线性变换,相当于是多个变换矩阵相乘,而矩阵的乘法是不满足交换律的(简单来说也就是所谓的abba|a|\cdot |b| \neq |b| \cdot |a|)。

所以我之前想当然的先伪斜切,再旋转,于是写成了transform: matrix() rotate();,按照文档规范的顺序,就变成了先旋转、再伪斜切…顺序反了,自然结果就不对了……

本部落格采用DISQUS评论系统,如果您无法见到留言框,可前往我的GitHub微博提Issue(留言)。
为您带来了不便我也很尴尬╮(╯_╰)╭