最近做游戏资料经常要描述六围,现在比较流行的做法就是用六围图,但是资料库系统限制导致外挂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()
可以完成任何线性变换,非常适合这种场合。
基本操作思路如下:
- 按照给定边长绘制直角三角形。
- 使用
matrix()
将直角三角形的直角扭成所需的圆心角。 - 依次旋转所得的三角形。
计算矩阵
…思路就是要跳跃,画直角三角形随便摆弄一下上面的生成器就能搞定了。问题在于矩阵要怎么办…矩阵变换听起来挺复杂的,其实只要搞明白写法就OK了。
matrix()
需要提供6个参数,从a到f,转换成矩阵如下:
转换成我们熟悉的方式写成方程组的话,就是:
然后我们的变换是这样的(画的平行四边形,三角形的部分是实线画起来的部分…从红色的矩形变成蓝色的平行四边形):
在这里我出了点小小的思路上的问题,在准备这篇文章的时候才发现…主要是我还保留着skew()
的思路,而且当时心想着反正都是转,转几圈都是转…导致多转了一次,这个回头说。
根据上面的图解出来(解方程的时候发生了一场惨剧…按习惯画坐标轴都是右上为正,左下为负,可是我忘了!忘了!CSS渲染下的Y轴方向是向下的!向下的!!导致我对着一个方程算了俩钟头,怎么算都是反的!!):
代入a~f得到
因为六边形的圆心角是60°,所以此时的θ是30°。代入得到matrix()
旋转代入
CSS transform
有一个transform-origin
属性,可以规定元素变换的原点。对于前面所示的情况,显然是以我们的已知角——左下角为原点代入最合适。CSS旋转本身可以用rotate()
,但是我直接测试的结果是它总转不到该转的地方,一不做二不休,干脆直接乘进去算了…
旋转的矩阵是这样的,角度为逆时针:
因此对应六围图右上角那个三角的位置,旋转角度应该是30°,接下来的5个三角形再依次旋转60°就可以了。最终算出来的参数是
代入a~f得到matrix(
(这里的θ对应最终的旋转角,即30deg、90deg、150deg…)
源码可以上我的CodePen查看。
另外背景的圆环思路来自牛逼的A Single Div。
进一步思考
我发现这个玩意儿不叫六围图而是叫雷达图之后…我才认识到它不光是6个角的…还有可能是5个角、7个角的,按理说是都可以实现的…因为此时对应的圆心角应该是360/n
,之前算的只是n=6的情况而已。如果我一开始改变一下斜切的方向(如下图),最后就可以少算一步。
差不多就这样,理论上CSS雷达图可以达到和canvas一样的效果,唯一的遗憾就是背景是个圆…虽然可以用filter
做出投影,但是想Single Div画出这些网格就相当困难了(如果想到了好办法再来update)。
UPDATE
说好的UPDATE来了…这两天写完了之后茶不思饭不想,总觉得哪里不太对………今天趁着摸鱼又仔细重算了一遍…放假的时候果然不适合干活。
原点和长度
之前我是以三角形的直角为原点建立的坐标系,这样可以简化运算…但是当原点变化的时候…伪斜切变换(姑且这么命名吧)的公式也会跟着变,以上文最后一张图(橙、黑)为例,假设坐标系原点为,那么这个伪斜切变换的矩阵应该是
…如果把这个斜切拆解来看,其实它是一个横坐标绕原点旋转,而纵坐标线性平移的两个不同的变换叠加而来的…这个新的矩阵只是重新移动了一下位置,旋转横坐标的部分实际上并没有变…
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 | <div style="transform:translate(-10px,-20px)"> |
也就是说,如果在同一个元素上应用了一串变换,它的变换过程是反过来的:先向右下移动,然后旋转,然后放大,最后向左上移动。这又有什么不同呢…下面是一个例子
在CodePen上查看transform顺序对效果的影响(@hikarievo)。
蓝色的是先放大再旋转,黄色的是先旋转再放大…会出现这样的区别,是因为每次变换操作之间,坐标轴是不会发生变化的。
回到它的数学本质上来解释:多次线性变换,相当于是多个变换矩阵相乘,而矩阵的乘法是不满足交换律的(简单来说也就是所谓的)。
所以我之前想当然的先伪斜切,再旋转,于是写成了transform: matrix() rotate();
,按照文档规范的顺序,就变成了先旋转、再伪斜切…顺序反了,自然结果就不对了……
为您带来了不便我也很尴尬╮(╯_╰)╭