2016-04-21
使用 d3.js 绘制地图和飞线动效。涉及内容:GeoJSON、地图投影、贝塞尔曲线、中间帧动画、蒙板等
一、绘制地图
以绘制海南省为例:
1. 拿到原始数据,比如构成海南省边界的一系列经纬点。
2. 转成适合 d3 识别的格式,如 GeoJSON
- GeoJSON 是一种专门用于描述地理数据且基于 JSON 的公开标准。
- 如海南省边界 GeoJSON 格式如下:
3. 将经纬度地理坐标(三维)转换成平面直角坐标(二维),即 “地图投影”。
为什么称之为 “投影”?
以圆柱投影为例,假想球中心有一处光源,球体的影子印在圆柱上,再把圆柱展开。
d3 中内置了多种地球投影函数,如 d3.geo.mercator()
等,调用非常方便。
4. 生成 SVG 路径
调用 d3 中的路径生成器 —— d3.geo.path()
,生成 SVG 路径。
// mercator 投影法,即正轴等角圆柱投影
var projection = d3.geo.mercator()
// 调用路径生成器,加入投影函数,生成路径。
d3.geo.path().projection(projection);
最后将生成的 SVG 路径数值,放入<path>
的 d
属性中,即可渲染出海南省
<path d="M1016.3165908260071,877.0020828012596L1016.7682629425233,878.4541870083519L1018.1190182452278,879.657674665422L1020.6713918274147,879.9425987991278L1021.8133553115683,885.0159656142133L1021.2168072186639,886.604367509897L1019.6572600974043,888.3863507038384L1018.7027831627181,888.1210805263548L1017.5821249651053,890.4022390262162L1016.5466879311009,890.6605304316585L1014.6405747770752,895.1029959335299L1014.1903230095061,897.3390762043034L1012.4858999118553,900.5456612724538L1012.5455547246386,902.4770156266966L1011.8083917196311,904.251242336848L1008.3256872046436,904.9648477276073L1006.1624902108458,906.8575129740502L1005.2776105490134,909.6023273911044L1002.8828961011761,909.1128379773108L1001.4881098764467,909.7482614499434L999.9285627377321,909.4975500969631L998.7269444582525,912.2707386591089L996.8208312867737,913.2240027380744L994.5851963222347,911.4637932711369L990.7133151839512,911.1571154411927L989.7446347249067,911.4847360484749L988.5060872681313,909.9997012838144L986.9195534470948,910.025891775339L983.3388445930832,907.8296269799064L981.5832887937775,907.4933903587646L981.5591428093464,903.5329747367365L980.40439614978,901.3944349544332L980.864590394872,899.8235323030736L980.1913432669389,896.4899343077275L981.02651057606,895.4811777242269L980.4782544869208,893.0774565320271L981.5719259673072,891.3381575865278L982.9042166988199,891.078418535463L985.8727535866233,888.8618187772439L987.1837390315939,887.2474817087502L989.6381082922107,886.351004192087L989.8184930620489,883.4614340271595L992.2075260966478,881.3048632571645L995.0226649143901,882.9314989438175L996.1049735857605,882.4135500915461L996.1049735857605,880.7541404207199L997.3037511673499,879.9781179011286L999.455585334679,879.6531398322504L1001.0023492805256,880.7541404207199L1002.3232771855689,880.1322809066637L1004.5986820252949,880.6400545755691L1005.9238709771737,880.2577212784149L1007.0999229232812,878.599357584266L1008.9151335179147,879.2351497824575L1011.0442420323056,878.3453043291311L1014.4885970559592,880.2274951763136L1014.2968494422012,878.3687446898831Z"></path>
二、绘制飞线
1. 找到城市
原始数据
[{
"from": {
"name":"拉萨",
"coordinate": [116.4551,40.2539]
},
"to": {
"name":"北京",
"coordinate": [91.1865,30.1465]
}
}]
三维转二维坐标
与绘制地图时相似,使用 projection()
,把经纬度转为直角坐标。
2. 绘制路径
二次贝塞尔曲线
// 起始点为(50,50),控制点在(50,100),结束点为(100,100)
<path d="M50,50 Q50,100 100,100" />
起始点拉萨坐标,结束点北京坐标,控制点由计算得出,如下:
三、飞线动画
使用 attrTween()
,插入中间帧函数,不断变更 <path>
中的 d
属性,呈现出线条在“一点点绘制出来”的效果。
// 过渡动画
flyline.transition()
// 动画时长
.duration(1800)
// 为属性 d ,设置中间帧过渡
.attrTween('d', function(d){
var l = $path.getTotalLength();
return function(t){
var p = $path.getPointAtLength(t * l)
return '最终返回的值'
}
});
说明:
- $path 变量为完整的飞线路径,即最终效果的飞线;
-
getTotalLength()
得出该<path>
的总长度; - 此时的 t,即是中间帧的时刻。值范围为[0, 1],总数量大概会有 100 帧左右(为何是 100 帧左右,而不是个确切的数?暂没搞懂..)
-
getPointAtLength()
传入路径上距离,返回该点的 x,y 坐标
新的控制点如何确定?
通过起始点和原控制点,求出新的控制点
取 p01 为新的控制点。
通过新控制点和终点(变量),起始点不变,动态一次次绘制飞线。
function valueTween(d){
var $path = d3.select(this.parentNode).select('.line-basic');
// 基路径
var coord = $path.attr('d').replace(/(M|Q)/g, '').match(/((\d|\.)+)/g);
var x1 = +coord[0], y1 = +coord[1], // 起点
x2 = +coord[2], y2 = +coord[3], // 控制点
x3 = +coord[4], y3 = +coord[5]; // 终点
var l = $path.node().getTotalLength();
return function(t){
// 新的终点
var p = $path.node().getPointAtLength(t * l);
// 新的控制点
var x = (1-t) * x1 + t * x2;
var y = (1-t) * y1 + t * y2;
return 'M'+x1+','+y1+' Q'+x+','+y+' '+p.x+','+p.y;
}
}
四、飞线样式
1. 使用 svg 蒙板,渲染飞线“头粗尾巴细”的效果
(1) 添加圆形蒙板
- 圆心 cy,cx 为飞线终点;
- 设置的半径即为可视区域;
- 蒙板动态跟随飞线变化。
(2) svg 中 <mask>
标签
<defs>
<mask id="Mask">
<circle r="100" fill="url(#grad)" />
</mask>
</defs>
(3) 为蒙板添加径向渐变,使得飞线有“头部深,尾部浅至透明”的效果
<radialGradient
id="grad"
cx="0.5"
cy="0.5"
r="0.5" >
<stop offset="0%" stop-color="#fff" stop-opacity='1' />
<stop offset="100%" stop-color="#fff" stop-opacity='0'/>
</radialGradient>
2. 为飞线添加一个亮色的头部
3. 优化
原因是蒙板半径没有自适应。当半径为一个固定数值时,将导致长度小于此值的飞线没掉了尾部渐变效果。如下图,白色圆圈为蒙板范围:
优化:使蒙板半径随着两点(起点与终点)的距离而变化
五、总结
整个流程如下:
- 加载地图数据,绘制出地图;
- 轮询飞线数据,保存在数据中心;
- 飞线池 FlylinePond 初始化 生成飞线实体;
- 启动飞线数据运输带 - 不断绘制(只要数据池中有数据)
- draw() -已知起点和终点,二次贝塞尔曲线
- 绘制飞线基本路线
- 飞线动画,不断改变 d 属性; attrTween
- 飞线头部
- 蒙板
- 结束圆圈
- 终点文字