这是最近公司的一个项目。客户的需求是基于总公司和子公司的数据,开发一个数据展示大屏。 大屏两边都是一些图表展示数据,中间部分是一个三维中国地图,点击中国地图的某个省份,可以下钻到省份地图的展示。 地图上,会做一些数据的标注,信息标牌。 如下图所示:



本文将对一些技术原理进行分享。

2d图表

2d图表部分,主要通过echart图表进行开发,另外还会涉及到一些icon 文字的展示。 这个部分相信大部分前端人员都知道如何进行开发,可能需要的就是开发人员对于颜色,字体等有较好的敏感性,可以最大程度还原设计搞。

鉴于大家都比较熟知,不再详细说明。

三维地图的展示

对于中间的三维地图部分。 我们一般有几种方式来实现。

  1. 建模人员对地图部分进行建模
  2. 通过json数据生成三维模型
  3. 通过svg图片生产三维模型。

其中方式1能达到最好的效果,毕竟手动建模了,需要的效果都可以通过建模师智慧的双手进行调整。但是工作量相对来说较大,需要建立中国地图和各个省份的地图。 所以我们最终放弃了建模的这种思路。

通过json数据生成三维地图

首先要获取json数据。
通过datav可以获取中国地图的json数据,参考如下连接
http://datav.aliyun.com/portal/school/atlas/area_selector

获取数据之后,通过解析json数据,然后通过threejs的ExtrudeGeometry生成地图模型。代码如下所示:

 let jsonData = await (await fetch(jsonUrl)).json();  // console.log(jsonData);  let map = new dt.Group();  if (type && type === "world") {    jsonData.features = jsonData.features.filter(      (ele) => ele.properties.name === "China"    );  }  jsonData.features.forEach((elem, index) => {    if (filter && filter(elem) == false) {      return;    }    if (!elem.properties.name) {      return;    }    // 定一个省份3D对象    const province = new dt.Group();    // 每个的 坐标 数组    const coordinates = elem.geometry.coordinates;    const color = COLOR_ARR[index % COLOR_ARR.length];    // 循环坐标数组    coordinates.forEach((multiPolygon, index) => {      if (elem.properties.name == "海南省" && index > 0) {        return;      }      if (elem.properties.name == "台湾省" && index > 0) {        return;      }      if (elem.properties.name == "广东省" && index > 0) {        return;      }      multiPolygon.forEach((polygon) => {        const shape = new dt.Shape();        let positions = [];        for (let i = 0; i 

中国地图的json数据,实际包括的是每个省份的数据。
上述代码生成中国地图以及省之间的轮廓线。
其中projection 是投影函数,转换经纬度坐标未平面坐标,用的是d3这个库:

const projection = d3  .geoMercator()  .center([104.0, 37.5])  .scale(80)  .translate([0, 0]);

按照设计稿,还需生成整个中国地图的外轮廓。这种情况下,我们先获取world.json,然后只获取中国的部分,通过这个部分来生成轮廓线。

最终效果如下:

可以看出,通过json的方式生产地图,世界地图的json数据和中国地图的json数据,边缘的贴合度并不高,因此外边缘轮廓和地图块不能很好的融合在一块。

基于此,需要找新的方案。

通过svg数据生成三维地图

由于有设计师提供设计稿,所以设计师肯定可以提供中国地图的轮廓数据,以及内部的每个省份的轮廓数据。拿到设计的svg后,对svg路径进行解析,然后通过ExtrudeGeometry生成地图块对下,通过line生成轮廓线。

 let childNodes = svg.childNodes;  childNodes.forEach((child) => {    readSVGPath(child, graph, group);  });  if (svg.tagName == "path") {    const shape = getShapeBySvg(svg);    // let shape = $d3g.transformSVGPath(pathStr);    const extrudeSettings = {      depth: 15,      bevelEnabled: false,      bevelSegments: 5,      bevelThickness: 0.1,    };    const color = COLOR_ARR[parseInt(Math.random() * 3) % COLOR_ARR.length];    const geometry = new dt.ExtrudeGeometry(shape, extrudeSettings);    let center = new dt.Vec3();    // console.log(geometry.getBoundingBox().getCenter(center));    // geometry.translate(-center.x, -center.y, -center.z);    geometry.scale(1, -1, -1);    geometry.computeVertexNormals();    // console.log("geometry", geometry);    const material = new dt.StandardMaterial({      metalness: 1,      // color: color,      // visible: false,      map: window.texture,    });    let material1 = new dt.StandardMaterial({      polygonOffset: true,      polygonOffsetFactor: 1,      polygonOffsetUnits: 1,      metalness: 1,      roughness: 1,      color: color, //"#3abcbd",    });    material1 = createSideShaderMaterial(material1);    const mesh = new dt.Mesh(geometry, [material, material1]);    group.add(mesh);

其中解析svg路径的代码如下:

function getShapeBySvg(svg) {  let pathStr = svg.getAttribute("d");  let province = svg.getAttribute("province");  let commonds = new svgpathdata.SVGPathData(pathStr).commands;  const shape = new dt.Shape();  let lastC, cmd, c;  for (let i = 0; i < commonds.length; i++) {    cmd = commonds[i];    let relative = cmd.relative;    if (relative) {      c = copy(cmd);      let x = cmd.x || 0;      let y = cmd.y || 0;      let lx = lastC.x || 0;      let ly = lastC.y || 0;      c.x = x + lx;      c.y = y + ly;      c.x1 = c.x1 + lx;      c.x2 = c.x2 + lx;      c.y1 = c.y1 + ly;      c.y2 = c.y2 + ly;    } else {      c = cmd;    }    if (lastC) {      let lx = lastC.x,        ly = lastC.y;      if (        Math.hypot(lx - c.x, ly - c.y) < 0.2 &&        province == "内蒙" &&        [16, 32, 128, 64, 512, 4, 8].includes(c.type)      ) {        console.log(c.type);        continue;      }    }    if (c.type == 2) {      shape.moveTo(c.x, c.y);    } else if (c.type == 16) {      shape.lineTo(c.x, c.y);    } else if (c.type == 32) {      shape.bezierCurveTo(c.x1, c.y1, c.x2, c.y2, c.x, c.y);      // shape.lineTo(c.x, c.y);    } else if (c.type == 128 || c.type == 64) {      shape.quadraticCurveTo(c.x1 || c.x2, c.y1 || c.y2, c.x, c.y);      // shape.lineTo(c.x, c.y);    } else if (c.type == 512) {      // shape.absellipse(c.x, c.y, c.rX, c.rY, 0, Math.PI * 2, true);      shape.lineTo(c.x, c.y);    } else if (c.type == 4) {      c.y = lastC.y;      shape.lineTo(c.x, lastC.y);    } else if (c.type == 8) {      c.x = lastC.x;      shape.lineTo(lastC.x, c.y);    } else if (c.type == 1) {      // shape.closePath();    } else {      // console.log(c);    }    lastC = c;  }  return shape;}

其中里面涉及到相对定位的概念,一个cmd的坐标是相对于上一个坐标的,而不是绝对定位。这就需要我们在解析的时候,通过累加的方式获取绝对定位坐标。

另外cmd的type主要包括:

  //   ARC: 512  // CLOSE_PATH: 1  // CURVE_TO: 32  // DRAWING_COMMANDS: 1020  // HORIZ_LINE_TO: 4  // LINE_COMMANDS: 28  // LINE_TO: 16  // MOVE_TO: 2  // QUAD_TO: 128  // SMOOTH_CURVE_TO: 64  // SMOOTH_QUAD_TO: 256  // VERT_LINE_TO: 8

通过Shape的moveTo,lineTo,bezierCurveTo,quadraticCurveTo等等与之对应。
最终效果如下图:

可以看出线更加圆滑,外轮廓和地图块的贴合度更高。
这是我们项目最终采用的技术方案。

侧边渐变效果

上述两种方案的效果图,可以看出侧边地图的侧面都有渐变效果,这种是通过定制threejs的材质的shader来实现的。大致代码如下:

function createSideShaderMaterial(material) {  material.onBeforeCompile = function (shader, renderer) {    // console.log(shader.fragmentShader);    shader.vertexShader = shader.vertexShader.replace(      "void main() {",      "varying vec4 vPosition;\nvoid main() {"    );    shader.vertexShader = shader.vertexShader.replace(      "#include ",      "#include \nvPosition=modelMatrix * vec4( transformed, 1.0 );"    );    shader.fragmentShader = shader.fragmentShader.replace(      "void main() {",      "varying vec4 vPosition;\nvoid main() {"    );    shader.fragmentShader = shader.fragmentShader.replace(      "#include ",      `      #include       float z = vPosition.z;      float s = step(2.0,z);      vec3 bottomColor =  vec3(.0,1.,1.0);          diffuseColor.rgb = mix(bottomColor,diffuseColor.rgb,s);      // float r =  abs( 1.0 * (1.0 - s) + z  * (0.0  - s * 1.0) + s * 4.0) ;      float r =  abs(z  * (1.0  - s * 2.0) + s * 4.0) ;      diffuseColor.rgb *= pow(r, 0.5 + 2.0 * s);            // float c =     `    );  };  return material;}

通过material.onBeforeCompile方法实现材质的动态更改,然后通过z坐标的高度进行颜色的渐变差值运算。

三维地图的贴图

上面实现的效果,都是简单的颜色。没有贴图效果,而设计师提供的原型是有渐变效果的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j7BuKd9p-1667965040240)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5f0c6a260f3647b991609a440ae85002~tplv-k3u1fbpfcp-watermark.image" />
其中比较难的是中间三维地图的生成和效果优化方案,如果有类似需求的读者可以参考。

如果你有好的经验,也欢迎和我交流。关注公号“ITMan彪叔” 可以添加作者微信进行交流,及时收到更多有价值的文章。