地图与飞线

作者: HeyDelilah | 来源:发表于2016-08-16 18:05 被阅读808次

    2016-04-21

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

    一、绘制地图

    以绘制海南省为例:

    海南省.png

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

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

    • GeoJSON 是一种专门用于描述地理数据且基于 JSON 的公开标准。
    • 如海南省边界 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
    • 圆心 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. 为飞线添加一个亮色的头部

    飞线头部亮色.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: 麥卡托投影法

    相关文章

      网友评论

      • af0e48c9d233:p = $path.node().getPointAtLength(l * t)
        每次得到的坐标都是起点坐标,这是为啥?
      • c05bdf58f067:关于问一下循环多个path的时候怎么保证每条path 的marker都进行动画展示,我现在是第一次所有的path都会有marker在动,最后的是时候就只剩一个path在循环了,其他的都不循环了,是不是我的变量的生命周期控制出现了异常
      • HITMiner:可以参考源代码学习吗?
        hihl:同问

      本文标题:地图与飞线

      本文链接:https://www.haomeiwen.com/subject/ysipsttx.html