svg可视化可视化

地图与飞线

2016-08-16  本文已影响808人  HeyDelilah

2016-04-21

使用 d3.js 绘制地图和飞线动效。涉及内容:GeoJSON、地图投影、贝塞尔曲线、中间帧动画、蒙板等

一、绘制地图

以绘制海南省为例:

海南省.png

1. 拿到原始数据,比如构成海南省边界的一系列经纬点。

2. 转成适合 d3 识别的格式,如 GeoJSON

海南省数据.png

3. 将经纬度地理坐标(三维)转换成平面直角坐标(二维),即 “地图投影”。

为什么称之为 “投影”?

以圆柱投影为例,假想球中心有一处光源,球体的影子印在圆柱上,再把圆柱展开。

圆柱投影.png

图片来源自 wikipedia

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" />
飞线.png

起始点拉萨坐标,结束点北京坐标,控制点由计算得出,如下:

求控制点坐标.png

三、飞线动画

使用 attrTween(),插入中间帧函数,不断变更 <path> 中的 d 属性,呈现出线条在“一点点绘制出来”的效果。

飞线动画.png
// 过渡动画
flyline.transition()

    // 动画时长
    .duration(1800)

    // 为属性 d ,设置中间帧过渡
    .attrTween('d', function(d){

        var l = $path.getTotalLength();

        return function(t){

            var p = $path.getPointAtLength(t * l)

            return '最终返回的值'
        }
    });

说明:

  1. $path 变量为完整的飞线路径,即最终效果的飞线;
  2. getTotalLength() 得出该 <path> 的总长度;
  3. 此时的 t,即是中间帧的时刻。值范围为[0, 1],总数量大概会有 100 帧左右(为何是 100 帧左右,而不是个确切的数?暂没搞懂..)
  4. getPointAtLength() 传入路径上距离,返回该点的 x,y 坐标

新的控制点如何确定?

通过起始点和原控制点,求出新的控制点

新的控制点.gif 新的控制点计算公式.jpg

图片来源 cnblogs

取 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) 添加圆形蒙板

蒙板.png

(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. 为飞线添加一个亮色的头部

飞线头部亮色.png

3. 优化

蒙板半径-before.png

原因是蒙板半径没有自适应。当半径为一个固定数值时,将导致长度小于此值的飞线没掉了尾部渐变效果。如下图,白色圆圈为蒙板范围:

mask-radius.png

优化:使蒙板半径随着两点(起点与终点)的距离而变化

mask-radius2.png

五、总结

整个流程如下:

  1. 加载地图数据,绘制出地图;
  2. 轮询飞线数据,保存在数据中心;
  3. 飞线池 FlylinePond 初始化 生成飞线实体;
  4. 启动飞线数据运输带 - 不断绘制(只要数据池中有数据)
  5. draw() -已知起点和终点,二次贝塞尔曲线
    • 绘制飞线基本路线
    • 飞线动画,不断改变 d 属性; attrTween
    • 飞线头部
    • 蒙板
    • 结束圆圈
    • 终点文字

效果图:

效果图.gif

六、资料:

  1. eChart(百度)-地图-模拟迁徙
  2. 贝塞尔曲线原理
  3. D3.js 入门系列 — 地图的制作
  4. Marker animation along SVG path element with D3.js
  5. D3.js 官网 Tutorials
  6. wikipedia: 麥卡托投影法
上一篇 下一篇

猜你喜欢

热点阅读