使用svg绘制一张漂亮的地铁图(基础篇)

作者: 张阳君 分类: 前端技术

最近上级派下来一个任务,做一张上海地铁线路图用于大屏展示(webkit内核),要求按照客户给的概念图绘制,还需要一些交互和酷炫的动效,这张概念图的原型见下方(竟然还是CAD格式的)。

使用svg绘制一张漂亮的地铁图

接到这个任务时,心里是千万匹草泥马奔腾。大概分析一下就知道,Echars之类的可视化库根本无法定制这种概念图、而客户还要加一些酷炫狂拽吊炸天的特效,内心不免流过一丝蛋蛋的忧伤。

抗拒归抗拒,项目还是要完成的,认真分析之后,得出几个结论:

  1. 需要自定义一个坐标系统,基于客户给的概念图取坐标,最后映射到大屏上。
  2. 因为需要映射坐标,整个地铁线路肯定要进行缩放,若要高保真的画质,svg是首选。
  3. JSON数据需要包含线路、站点,线路和站点为多对多关系(存在中转站)。
  4. 客户很懒,站点没表现在CAD图里面,而是丢了个其他网站里的站点作参考,这就需要人工搬运了。

基于上述结论,我最终选择d3(5.5.0版)作为项目框架。公司的小胡同学负责采集坐标,非常到位,这里先感谢一波。

基本工作:

考虑封装成一个库,所以代码一上来写成了这种形式:

(function(window, d3) {
  var Metro = function(info) {
  };
  window.Metro = Metro;
})(window, d3);

在window对象里暴露了Metro类(备注:这个库没有考虑es6的语法,引入script即可使用)。

接下来是地铁的数据格式:

{
  // 线路数组
  lines: [{
    id: 'line1', // 线路id
    name: '1号线',  // 线路名
    color: '#e999c0', //线路颜色
    width: 8, //线路宽度
    running: true, // 该线路是否运行
    points: [
      [{
        x: 1260,
        y: 571
      }, {
        x: 1488,
        y: 571
      },...]
    ]
  },...],
  // 站点数组
  stations: [
    {
      id: 'station1', // 站点id
      name: '金运路', // 站点名
      type: 1, // 站点类型
      x: 1260, // 站点的x坐标
      y: 571, // 站点的y坐标
      status: 'normal', // 当前站点状态
      rotation: 0 // 站点是否旋转角度(备用)
    },
    ...
  ]
}

注释应该一目了然了吧,lines中的points对象是一个二维数组,这是因为每条地铁线可能会有分支。另外上述线路的坐标都取自客户给的底图,通过简单的算法,可以将其映射到实际的svg容器中。

构思Metro对象必需的属性和方法:

思考一下,实例化Metro对象需要传什么参数?

换个角度来想,假设这个库已经完成,我们如何调用这个库呢?我需要知道在哪个svg中实例化,需要知道客户底图的尺寸,需要传入地铁的JSON数据,因此很容易得到这么一个写法:

var metro = new Metro({
    id: '#app', // svg的id
    // 底图尺寸
    origin: {   
      width: 3840,
      height: 2160
    },
    // 传入的JSON数据
    data: jsonData
});

这样入参的格式就定了,我们补全一下Metro的function方法:

var Metro = function(info) {
    var self = this;
    // 容器id
    this.containerId = info.id;
    // 容器对象
    this.container = d3.select(info.id);
    // 对象组
    this.group = this.container.append("g");
    // 特效对象组
    this.effectGroup = this.group.append("g");
    // 原始底图参数
    this.origin = info.origin;
    // 地铁数据
    this.data = info.data;
    // 当前容器宽高
    this.containerWidth = this.container.node().getBoundingClientRect().width;
    this.containerHeight = this.container.node().getBoundingClientRect().height;
    // 当前地图的缩放参数
    this.scale = 1;
    this.offsetX = 0;
    this.offsetY = 0;
};

这里没有对入参作过滤,大家将就着看,下面容器的宽高和地图的拖放参数都是交互必须的参数,自适应算法里会涉及到。

思考第二个问题,怎么用d3实现内部元素的拖拽和缩放?

由于d3版本更新,网上很多拖放的代码都失效了,这里我就直接列出相关方法,供大家参考:

var Metro = function(info) {
    ...
    // 拖放代码
    this.zoom = d3.zoom().scaleExtent([0.1, 100]).on("zoom", function() {
      self.group.attr("transform", d3.event.transform);
      // 将当前的缩放参数保存到属性中,后续有用
      self.scale = d3.event.transform.k;
      self.offsetX = d3.event.transform.x;
      self.offsetY = d3.event.transform.y;
    });
    // 缩放事件对象
    this.groupEvent = this.container.call(this.zoom);
};

这里通过scaleExtent控制svg的缩放阈值为0.1到100之间。this.groupEvent是缩放事件对象,后续有用。

我们下面要开始写最关键的render渲染函数了,有了这个函数,地铁线路才会被绘制到页面中:

var Metro = function(info) {
    ...
    // 渲染线路
    this.render();
};

// 渲染函数
Metro.prototype.render = function() {
    ...
    var stationLines = this.data.lines;
    // 渲染线路
    stationLines.forEach(function(item) {
      var groupPath = self.group.append("path");
      // 根据关键点,绘制折线
      var path = d3.path()
      var points = item.points
      points.forEach(function(line) {
        line.forEach(function(p, index) {
          if (index === 0) {
            path.moveTo(p.x, p.y)
          } else {
            path.lineTo(p.x, p.y)
          }
        })
      });
      // 将折线路径写入group
      groupPath
        .attr('stroke-width', item.width)
        .attr('fill', 'transparent')
        .attr('stroke', item.color)
        .attr("d", path.toString())
        .attr("stroke-linecap", "round");
    });
};

由于篇幅问题,这里的渲染函数只列出了比较关键的方法,其实在渲染函数头部还有清除上一次渲染,渲染次序调整,加粗线路等功能,就不一一列出了。上面的函数本质上是在svg中append了一个path,path的路径通过forEach方法计算得到。站点渲染的实现方案请各位读者自己YY。

渲染完成之后,你会发现绘制到页面上的线路要么偏大,要么偏小,位置也不居中。原因在于,我们还没有做映射和自适应,因此下面来完成Metro的自适应功能:

var Metro = function(info) {
    ...
    // 自适应
    this.fit();
};

// 自适应算法
Metro.prototype.fit = function() {
  var scaleW = this.containerWidth / this.origin.width;
  var scaleH = this.containerHeight / this.origin.height;
  var scale = (scaleW <= scaleH) ? scaleW : scaleH;
  this.scaleTo(scale);
};

// 缩放
Metro.prototype.scaleTo = function(scale) {
  this.scale = scale;
  this.offsetX = (this.containerWidth - this.origin.width * scale) * 0.5;
  this.offsetY = (this.containerHeight - this.origin.height * scale) * 0.5;
  this.groupEvent.transition().duration(400).call(this.zoom.transform,d3.zoomIdentity.translate(this.offsetX, this.offsetY).scale(scale));
};

自适应算法看上去有点复杂,实际上是让客户给的底图经过缩放,能够最大化地显示在svg中,然后对缩放后的svg元素进行上下左右边距的调整。

到这里,我们就能把一张地铁线路完整地显示在网页中了。可是,可是,可是客户要的酷炫狂拽吊炸天的特效呢?别急,本文只是基础篇,在后续的进阶篇中,我会教你如何巧用渐变,如何完成一个svg的粒子发射器,如何实现像彗星一样的拖尾效果。完整的demo和源码,请关注进阶篇文章。

(全文完)

0 条评论
回复
支持 Markdown 语法
暂无评论,来抢个沙发吧!