击上方蓝字关注“小郑搞码事”,每天都能学到知识,搞懂一个问题!
Canvas是HTML5提供的新标签,通过JavaScript可以在Canvas元素上绘制图片并实现动画效果,今天展示一下Canvas绘制一个简单饼图的基本过程。
实现上面图中的饼图效果,首先在HTML引入Canvas标签,代码如下:
1、在JavaScript文件,创建PieChart类,并在其构造函数中获取Canvas的Context环境。
2、添加PieChart类原型方法load用于载入饼图所使用的数据,并计算饼图的数据总量,用于之后渲染饼图时分配每个数据所对应的扇形比例。
3、添加PieChart类原型方法render用于对饼图进行渲染,_generateLegend内部函数用于创建饼图对应的图例,当存在legend参数调用_generateLegend生成饼图图例。
4、最后,引入需要绘制的数据创建饼图对象即可完成饼图绘制。
总结一下:
在目前来看在移动端利用2d放射变换来实现的动画比较实惠的还是canvas 2d了。在大部分机型上canvas都能获得更好的渲染性能(在硬件加速情况下,浏览器会将绘图命令切换成gpu硬件来执行),并且现在的硬件加速支持程度也比较好。所以如果需要运动的物体多的话建议用canvas。
注:需要源代码运行的可以私信哟!!
言
25年过去了,Brooks博士著名的“没有银弹”的论断依旧没有被打破。HTML5也是一样。但这并不妨碍HTML5是一个越来越有威力的“炸蛋”:发展迅速、势不可挡。随着HTML5技术的普及,用HTML5做可视化呈现的项目越来越多了。HTML5的优势明显:网页上直接运行无需插件、手机平板方便兼容、代码开发和维护相对容易,等等。一大波一大波的做Java、.NET甚至C++桌面的程序老手们都纷纷开始研究javascript了,而初出茅庐的新一代程序猿更是义无反顾的直奔HTML5这个技术大热点而来。
HTML5涵盖的技术点很多,甚至延伸到了前端、后端、通讯等各个层面。前端的canvas绘图这块无疑是它的核心内容。Canvas的API虽然不是很复杂很强大,但是做一般的2d绘图基本都够用了。基于这些API,一大堆的2d绘图组件纷纷出炉。Echarts、d3.js都是很不错的项目。 Echarts主要是chart组件,而d3相对杂一些,很多呈现方式很有创意,值得研究。
概述
研究d3的起因是最近有一个项目,用户截了一张效果图让我们用HTML5做一下:
看着很眼熟,搜了一下,感觉就是d3例子中的sunburst效果,程序在这里:
http://bl.ocks.org/mbostock/4063423
看上去似乎也不难,就是一圈一圈的饼图,把树状结构数据按占比一层一层绘制上去就行了。所以引起了自己动手做一个的兴趣。“sunburst”英文里应该是“云开日出”的意思,类似强烈的光芒从云层背后透射出来,不知为何中文里大多把它翻译成“日落”。比如这把Fender Telecaster吉他型号是Brown Sunburst款,就会被大家翻译成“日落色”。
关于日出和日落更喜欢哪一个的问题,网上还真有这样的调查。有意思的是,选择喜欢日落的远多于选择日出的。日出代表希望,日落代表成熟,都是一种美,哪个更美要看你个人的心境,因为它的美丽是由心生。为了不在这个问题上站错对,我们还是给他重新起一个更加响亮霸气的中文名字:“彩虹爆炸图”,怎么样?
仔细研究一下彩虹爆炸图的结构,无非就是一个树形结构,并采用发射状的布局。根节点在中间(也可以认为没有唯一的根,而是一堆根节点围绕在第一圈),一次向外发散排列。每一个节点有名称、数值。节点可以按照自身数值在扇区所占比例进行绘制,这样就不用管节点具体数值有多大多小了。
这种图最先是由布朗大学教授John T. Stasko设计。
http://www.cc.gatech.edu/~john.stasko/
经过一天的折腾,终于做出了一个还算过得去的“彩虹爆炸图”。先上个图看看:
主要功能包括了:
可以通过json来定义数据和样式(类似百度的echarts那样);
颜色可以固定,也可以自动彩虹色;
自动计算数值及角度占比;
动态显示导航路径;
鼠标动态高亮显示路径;
动画飞入、展开导航路径;
文字显示及角度控制;
全矢量,可鼠标缩放、平移,不失真;
下面重点码一下折腾过程中的几个重点:
一、定义节点对象
首先定义每一个小扇片节点。每个扇片可以用一段饼图来绘制。为了简单方便,这里用了最简单高效偷懒的方法:用一个半径很粗的线画一段角度的arc,即可。如下图:
另外还有文字等内容。所以定义它的json结构大概如下:
var item = {name: '名称', color: 'red', angle: '45', …};
此外,下一圈的数据,可直接定义为这个节点的“孩子节点”,直接在item中定义一个data的子节点数据:
var item = {name: '名称', color: 'red', angle: '45', data:[
{name:’孩子一’, color:’green’,…},
{name:’孩子二’, color:’yellow’,…},
]};
这样就可以组成一个树状结构。接下来要在canvas上绘制图形了。为了方便,这里直接使用了矢量图进行定义:
twaver.Util.registerImage('node', {
v: [{
shape: 'circle',
r: ...
lineColor: function(data,view){return data.getClient("lineColor");},
lineWidth: ...
startAngle: ...
endAngle: ...
},{
shape: 'text',
textBaseline: 'middle',
textAlign: ...
text: ...
x: ...
y: ...
font: ...
fill: ...
rotate: ...
visible: ...
shadow: ...
}],
});
矢量图中定义了2个图形元素:一个arc弧线、一个文字对象,分别用于画node和绘制其文字。颜色、字体、是否可见、阴影、对齐、位置、线宽、角度…等等均在上面的定义中用一个function动态获取。例如,这个节点的半径,通过下面的方法,就可以在图形的lineColor属性中保存并驱动,需要修改,直接修改lineColor这个client属性即可,而不用去修改绘图参数,非常方便:
r:function(data,view){return data.getClient("lineColor");}
这里有一个比较啰嗦的地方是:每个扇片的角度需要根据每个item定义的原始值进行计算角度占比。而且,对于太小的扇片,可以给一定的最小值(例如 1度),保证能视觉上看到它。否则,显示10000和1两个数值,由于对比过大,可能就杯具了,因为1连1度都占不到,显示效果会非常差。还有,每个扇片之间应该尽量留有一定的空隙。如果连续绘制,就会连成一片,没有“分片”感。这些可以在代码中进行简单控制。
二、文字控制
文字控制也比较啰嗦。首先是对齐方式。最简单的方式当然是让文字在所在扇片处,直接居中、旋转。这样文字会在径向的中间位置,如下图:
但这样显示感觉并不是很完美。对于中文来说,如果能统一靠近圆心方向的位置对齐,会更好看一些。这样,即使文字过长,也会向外延伸,不会和里面的重叠。如下图:
还有,当文字在左半圆时,如果不做特殊处理,文字旋转会导致文字大头朝下,阅读起来有把脖子歪断的风险。所以应该动态判断,如果文字在左侧,应该文字再增加旋转180度。同时左侧的文字对齐也要特殊考虑,应该变成右对齐,才能保持径向的整齐一致。
文字还有一个细节就是颜色和阴影的问题。不使用阴影,单纯的使用颜色(例如白色),则在一些方向上的节点的文字会看不清楚,因为我们做的是彩虹爆炸图,各个方向颜色都不一样,而且还会随着圈数增加而变淡颜色,所以几乎不可能用一个固定的颜色(例如白色或黑色)能保证文字在所有地方都能和node颜色搭配并看清楚。所以思来想去还是使用了阴影效果。
联想了一下我们看美剧时候的字幕,似乎也是同样的问题。视频字幕要显示在千变万化的视频场景里面,视频场景的颜色完全随机出现无从知晓,要想让字幕看清楚,必然也会想一些办法解决。我们仔细观察一下视频字幕:
仔细观察,字幕是白色文字加了一圈黑色外框,这样就不怕任何场景了。我们在文字定义时也模拟一下,设置阴影和阴影偏移试一试:
fill:'white',
shadow: {
offsetX: 2,
offsetY: 2,
blur: 4,
color: 'black',
},
看一下使用前和使用后的效果对比:
使用阴影后不但文字更清晰了,而且也增加了立体感,效果还是不错的。下面图显示在应用在节点上的效果:
可见不论什么颜色,都能比较好的勾勒出文字轮廓,保持清晰可读。
三、生成彩虹颜色
关于颜色,是一个有趣的话题。对于广大程序猿来说,颜色是一个既简单又困难的东西。我们随手就能写下’red’, ‘green’, ‘orange’, ‘yellow’这样的色彩斑斓的颜色,还能保证没有语法错误;我们还会写’#FF55AA’、’#0c0’、’RGB(0,204,0)’、’ RGB(0%,80%,0%)’这样的各种颜色写法;我们也明白RGBA的含义和用途。但是,我们很少能把一个demo写的颜色很好看、很搭配。关于颜色和配色以后再专门讨论。这里我们只想自动生成一圈彩虹一样的颜色。用我们熟悉的RGB方法好像比较困难了。于是想起了那个HSV的颜色定义方法,它貌似很适合解决这个问题。
HSV颜色模型定义了色调H、饱和度S和亮度V,由A. R. Smith在1978年创建的一种颜色空间。其中H用一圈360度表示所有颜色,从红色开始按逆时针方向计算,红色为0度。饱和度S从0到1,越大越饱和。亮度V从0到255(也可以转换为从0到1,方便使用),越大越明亮,越小越暗淡。
Js里面并没有直接处理HSV颜色的函数。不过用下面的代码很方便可以从hsv转为rgb:
写一个对应的js函数也很简单:
/* h, s, v (0 ~ 1) */
function getHSVColor(h, s, v) {
var r, g, b, i, f, p, q, t;
if (h && s === undefined && v === undefined) {
s = h.s, v = h.v, h = h.h;
}
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
var rgb='#'+toHex(r * 255)+toHex(g * 255)+toHex(b * 255);
return rgb;
}
再回到我们的彩虹爆炸图。每一个节点对应的所在角度(中心角度)决定了它自己的颜色值。所以,我们可以直接根据这个角度得到颜色的h。然后,为了让彩虹逐渐一圈一圈变淡,再把s饱和度从1逐圈递减(例如0.1),产生变淡的效果。为了防止圈太多最后看不清,减到0.2到0.3左右可以停止递减。
var fromAngle=node.getClient(‘fromAngle’);
var toAngle=node.getClient(‘toAngle’);
var level=node.getClient(‘level’);//节点在第几圈
var h = (fromAngle+to)/2 % 360 /360; //中心角度,并转换为弧度
var s = Math.max(0.2, 1-level*0.1);//每圈s递减0.1,直到0.2为止
var v=1;
var color=getHSVColor(h, s, v);
这样就获得了一圈颜色。实验效果如下:
如果相对某个节点的颜色做特殊处理,例如强制为橙色来凸显,我们可以在数据中定义时加个标记,设置颜色时候直接使用而不用计算即可。
{name:'浦东新区', value: 2600, color: '#FE9A2E'}
接下来要实现鼠标划过节点,自动计算路径、高亮路径节点、暗淡非路径节点。为了方便路径寻找,程序把每个节点的下一圈子数据定义为子节点,子节点通过getParent函数可以直接获得父对象。这样,通过不断getParent就可以获得整个路径上的节点,并修改其颜色为预设颜色,实现高亮效果:
var node=highlightedNode;
while(node){
node.setClient(‘color’, node.getClient(‘color.original’));
node=node.getParent;
}
对于非路径节点的颜色,可以设置为预设颜色但饱和度为0.1的淡颜色 ,让它变淡,以便突出高亮路径:
var color = getHSVColor(h, 0.1, v);
node.setClient(‘color’, color);
四、动画效果
最后,为了图形更生动,使用了一些动画效果。首先想到的就是图形出来时候,用动画从小到大发散开来,会很动感。这样做需要用动画函数来驱动每一个节点的半径位置,从0增加到所在的半径位置,如果大家一起设置,整个图就会动起来。这里用了一个动画函数来驱动,并使用了网上常用的easing函数来控制,避免线性的动画太死板:
new Animate({
from: 0,
to: 1,
dur: 3000+level*100,
easing: 'elasticOut',
onUpdate: function (value) {
node.setLocation('pie.location’, value);
},
}).play;
上面定义的动画,用3秒钟跑完,用'elasticOut'的easing方式。每一帧,修改node的位置信息。这样就完成了橡皮筋一样的环形弹出散开效果。
另外,导航条的出来也比较突兀,这里也使用一下动画,让它从左到右慢慢伸出:
new Animate({
from: {x:x1, y:y1},
to: {x:x2, y:y2},
delay:50,
type: 'point',
dur: 1000,
easing: 'bounceOut',
onUpdate: function (value) {
node.setCenterLocation(value.x, value.y);
和上一个动画的不同之处在于这里使用了{x、y}的point结构,每一帧直接更新节点位置。同时设置了50毫秒的delay,让动画有一点点粘性停滞,不至于太突兀。效果不错。
至此,彩虹爆炸图基本上就做的差不多了。使用起来也很简单,只要准备一些json数据就可以了,下面是一些有趣的数据做出来的效果。感兴趣的同学可以到这里索取代码。
实际应用在项目中的示意图。如果你也希望项目中用一下彩虹爆炸图,欢迎给我发邮件索取:info@servasoft.com
扫推荐微信:中国大数据
推荐理由:一手新鲜,绝对干货
ath 是 SVG 基本形状中最强大的一个,不仅可以实现我们上一篇《HTML5(七)——SVG基础入门》中,介绍的 SVG 预定义的 line、rect、circle、ellipse、line、polyline、polygon 六种基本形状。还可以实现更复杂的效果,我们就开始学习 path 如何应用。
1.1、path 命令
path 用于定义一个路径,其中命令就是控制这条路径的,以下命令就是可用于路径数据:
注:以上所有命令大小写都可以,区别是大写命令表示绝对定位,小写表示相对定位。
1.2、path 使用
使用语法:<path d=" M x1 y1 L x2 y2 H x3.... " stroke="red"></path>
d:引出路径,d 中的值由多条命令组合而成。
圆弧在实际场景中应用非常广泛,A 命令中参数较多,large-arc 和 sweep 较难理解,详细介绍下。
使用语法:<path d="M x y A rx ry x-axis-rotation large-arc sweep x y"></path>
如上图所示,A 到 B 两个点再加半径,可以绘制出 4 条弧线,具体选哪一条呢?就是由 large-arc (是否是大弧)和 sweep(是否逆时针旋转) 两个参数决定。
large-arc = 1 表示弧度大于等于180,反之就是小于180。
sweep = 0 表示逆时针旋转,反正顺时针旋转。
所以上述 4 条弧线分别对应的两个参数为:
eg:使用 path 绘制一条直线、半圆、直线,连接起来形成一个拱桥,代码如下:
<svg version="1.1" height="400" width="550">
<path d="
M 0 100 //(0,100)是起点
L 100 100 // 画一条直接到 (100,100)
A 100 100 0 1 1 300 100 // 画一段圆弧
L 400 100 //画一条直线到 (400,100)
" stroke="black" stroke-width="1" fill="none"></path>
</svg>
运行结果如下:
可以自己修改上述 A 中 参数观察半圆的变化。
1.3、js 操作path
我们经常使用js动态添加、移除元素等,可以实现更炫酷的特效,那js能动态操作path吗?如何操作呢?
我们使用js动态绘制一个与上边案例eg1一样的path。
<svg version="1.1" height="400" width="550" id="svg"></svg>
<script >
window.onload = function(){
let svg = document.getElementById("svg")
let path = document.createElement("path")
path.setAttribute('d',"M 0 100 L 100 100 A 100 100 0 1 1 300 100 L 400 100")
svg.appendChild(path)
}
</script>
运行代码,我们发现没有报错,也没有显示任何结果。
添加:alert(path),打印出 [object HTMLUnknownElement]。
说明 html 把 path 当作普通的 html 标签解析了,发现并不认识该标签,所以页面没有任何效果,此时我们需要介绍一个新的创建元素方法。
createElementNS介绍
createElementNS 是创建一个具有指定的命名空间URI和限定名称的元素。
使用语法:document.createElementNS(namespaceURI, qualifiedName[, options]);
生成path元素代码:
let path = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
)
js操作属性时,html元素与SVG元素区别:
普通html元素可以使用两种方法:
SVG操作方法只有一种
eg3:图表中饼图是特别常见的,我们就先使用 js 动态地绘制一个扇形圆弧。
<svg version="1.1" height="400" width="550" id="svg"></svg>
<script >
function d2a(n){
return Math.PI*n/180
}
function pie(ang1,ang2,r,cx,cy){
let svg = document.getElementById("svg")
let path = document.createElementNS("http://www.w3.org/2000/svg","path")
let arr = []
let x1 = cx + Math.sin(d2a(ang1))*r
let y1 = cy - Math.cos(d2a(ang1))*r
let x2 = cx + Math.sin(d2a(ang2))*r
let y2 = cy - Math.cos(d2a(ang2))*r
arr.push(`M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 0 1 ${x2} ${y2} `)
arr.push("Z")
path.setAttribute('d',arr.join(' '))
svg.appendChild(path)
}
window.onload = function(){
pie(20,180,200,200,200)
}
</script>
运行结果如图所示:
如果有兴趣,可以自己修改开始角度和结束角度,可以绘制出很多种不同效果的圆弧。
上述代码
<path d="
M 0 100 //(0,100)是起点
L 100 100 // 画一条直接到 (100,100)
" stroke="black" stroke-width="1" fill="none"></path>
上述属性可以分为两类:
如 troke、fill等是控制视觉上的效果,这样的属性可以放入 style 样式中。所以上述代码可以改写为:
<path d="
M 0 100 //(0,100)是起点
L 100 100 // 画一条直接到 (100,100)
" style="stroke:black;stroke-width:1;fill:none"></path>
该样式可以放到 head 的 style 中,代码为:
path{
fill:none;
stroke:black;
stroke-width:1
}
还可以通过 class、id、标签等添加样式,他们的优先级分别为:
属性< * < 标签 < class < id < 行间
path 的样式控制同样适用于 SVG 预定义的 rect、circle、ellipse 等元素。
如果觉得还不错!
点个关注,下篇解密 SVG 动画 !
*请认真填写需求信息,我们会在24小时内与您取得联系。