高德地图+Three.js实现飞线、运动边界和炫酷标牌

2024-11-14 08:30   重庆  

点击关注公众号,“技术干货” 及时达!

地图经常使用标牌来标注地点位置,是时候来点炫酷的效果了!用高德地+Three.js,带你实现飞线、运动边界和炫酷标牌。

1.高德地图+Three.js

可以参考官方示例高德地图-自定义图层-GLCustomLayer 结合 THREE

  • 初始化高德3D地图
 this.map = new AMap.Map(this.container, {
      //缩放范围
      zooms: [220],
      zoom4.5,
      //倾斜角度
      pitch0,
      //隐藏标签
      showLabelfalse,
      //3D地图模式
      viewMode'3D',
      //初始化地图中心点
      centerthis.center,
      //暗色风格
      mapStyle'amap://styles/dark'
    });   
  • 自定义高德地图经纬度转px工具,即墨卡托投影坐标转换。
   // 数据转换工具
    this.customCoords = this.map.customCoords;
    //设置坐标转换中心
    this.customCoords.setCenter(this.center);
  • 高德地图添加WebGL自定义图层,初始化Three.js配置
 // 创建 GL 图层
    var gllayer = new AMap.GLCustomLayer({
      //zIndex: 10,
      // 初始化的操作,创建图层过程中执行一次。
      init(gl) => {
        //初始化Three相机
        this.camera = new THREE.PerspectiveCamera(
          60,
          this.container.offsetWidth / this.container.offsetHeight,
          1,
          1 << 30
        );
        //初始化Three渲染器
        this.renderer = new THREE.WebGLRenderer({
          context: gl // 地图的 gl 上下文
        });
        this.renderer.setPixelRatio(window.devicePixelRatio);
        // 自动清空画布这里必须设置为 false,否则地图底图将无法显示
        this.renderer.autoClear = false;
        //初始化场景
        this.scene = new THREE.Scene();  
        this.createChart();
      },
      render() => {
        // 这里必须执行!!重新设置 three 的 gl 上下文状态。
        this.renderer.resetState();
        //设置坐标转换中心
        this.customCoords.setCenter(this.center);
        var { near, far, fov, up, lookAt, position } = this.customCoords.getCameraParams();

        // 这里的顺序不能颠倒,否则可能会出现绘制卡顿的效果。
        this.camera.near = near;
        this.camera.far = far;
        this.camera.fov = fov;
        this.camera.position.set(...position);
        this.camera.up.set(...up);
        this.camera.lookAt(...lookAt);
        this.camera.updateProjectionMatrix();
        //渲染器渲染场景
        this.renderer.render(this.scene, this.camera);
        // 这里必须执行!!重新设置 three 的 gl 上下文状态。
        this.renderer.resetState();
      }
    });
    this.map.add(gllayer);

2.绘制运动边界

行政区边界可以通过地理小工具获取。

高德地图折线polyline经常使用,但是让折线运动起来,你可能很少其实只需要添加几行代码就能实现。

实现逻辑:添加边界折线,然后截取折线的一段,让它跟着时间往后移动,即开始索引递增,截取相同折线,即可让折线运动起来。

 //绘制中国大陆运动边界
        createChinaLine() {
          return new Promise((resolve) => {
            fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000.json')
              .then((res) => res.json())
              .then((res) => {
                let path = res.features[0].geometry.coordinates[0][0];
                //截取10%的线段
                let len = Math.floor(path.length * 0.1);
                //边界折线
                let polyline = new AMap.Polyline({
                  path: path,
                  strokeWeight4,
                  strokeColor'white',
                  lineJoin'round',
                  strokeOpacity1
                });
                this.map.add(polyline);
                //利用Tween创建动画
                new TWEEN.Tween({ start0 })
                  .to({ start: path.length }, 3000)
                  //无限循环动画
                  .repeat(Infinity)
                  .onUpdate((obj) => {
                    if (obj.start + len < path.length) {
                      polyline.setPath(path.slice(obj.start, obj.start + len));
                    } else {
                      const c = path.length - obj.start;
                      //头尾相接时截取尾部+头部各一段
                      polyline.setPath(
                        [].concat(path.slice(obj.start, path.length), path.slice(0, len - c))
                      );
                    }
                  })
                  .start();
                resolve();
              });
          });
        }
        //Tween动画
         animateAction() {
          if (TWEEN.getAll().length) {
            TWEEN.update();
          }
          }

运动边界实现简单,但效果一级棒!并且2D和3D地图都能用,优秀!

3.绘制升起山峰

在对应的坐标点上升起一座尖尖的山峰,这里使用Plane平面实现

  • 顶点着色器
precision mediump float;
uniform float uTime;
uniform float uHeight;
varying float vD;
float PI = acos(-1.0);
vec2 center = vec2(0.5);
void main(void) {
//离中线的距离
    float d = length(uv - center) * 2.0;
    //沿中心点往外减少
    vD = pow(1.0 - d, 3.0);
    //山峰高度,随着uTime变化
    float h = vD * uHeight * uTime;
    vec3 pos = vec3(position.x * 0.5, position.y * 0.5, h);  
    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
  • 片元着色器
precision mediump float;
uniform vec3 uColor;
varying float vD;
void main(void) {
    if(vD < 0.01)//透明度太小则不渲染颜色
        discard;
    else//透明度随着距离中心点的变化
        gl_FragColor = vec4(uColor, vD * 2.0);
}
  • 添加shaderMaterial的平面
 const material = new THREE.ShaderMaterial({
            uniforms: {
            //随时间变化
              uTime: { value0 },
              //高度
              uHeight: { valuethis.size },
              //颜色
              uColor: { valuenew THREE.Color(data.color) }
            },
            //开启透明度
            transparenttrue,
            vertexShader``,
            fragmentShader``
          });
          this.material = material;
           //平面形状,方便复用
          if (!this.ageometry) {         
          //平面的面数一定要足够才能形成山峰
            const geometry = new THREE.PlaneGeometry(this.size, this.size, 500500);
            this.ageometry = geometry;
          }
          const plane = new THREE.Mesh(this.ageometry, material);
          //转换经纬度作为px
          const d = this.customCoords.lngLatToCoord(data.pos);
          plane.position.set(d[0], d[1], 0);
          this.scene.add(plane);

「注意:」

  1. 平面的面数一定要足够才能形成山峰
  2. 平面的大小和高度要足够大,在中国地图上才能看到,这里的this.size=500000,500k。在小的地图范围内,大小也要对应缩小。
  3. 高德地图坐标与three.js的坐标有些许不同,经纬度lng,lat对应xy,而高度对应z坐标。
  • 添加山峰升起动画
 addAnimate(start, end, time, update) {
          return new Promise((resolve) => {
            const tween = new TWEEN.Tween(start)
              .to(end, time)
              .onUpdate(update)
              .easing(TWEEN.Easing.Quadratic.In)
              .onComplete(() => {
                resolve(tween);
              })
              .start();
          });
        }
        
        const tw = await this.addAnimate({ time0 }, { time1 }, 1000, (obj) => {
              this.material.uniforms.uTime.value = obj.time;
            });
         //播放完删除动画
            TWEEN.remove(tw);

4.绘制浮动四棱锥

//转换经纬度坐标
 const d = this.customCoords.lngLatToCoord(data.pos); 
          const r = this.size * 0.1;
          //四棱锥图形,方便复用
          if (!this.cgeometry) {
            const geometry = new THREE.ConeGeometry(r, r * 241);
            this.cgeometry = geometry;
          }
          
          const material = new THREE.MeshLambertMaterial({ colornew THREE.Color(data.color) });
          //创建四棱锥网格
          const cone = new THREE.Mesh(this.cgeometry, material);
          //旋转90度,让四棱锥倒立
          cone.rotateX(-Math.PI * 0.5);
          //设置位置
          cone.position.set(d[0], d[1], this.size * 1.1);
          
          this.scene.add(cone);
          //收集四棱锥
          this.cones.push({ obj: cone, stepthis.speed });
  • 设置垂直高度坐标,让四棱锥上下浮动:遇到最大或最小高度时改变速度speed方向
 animateAction() {          
          if (this.cones.length) {
            this.cones.forEach((c) => {
            //高低浮动
              if (c.obj.position.z >= this.maxHeight) {//最大高度
                c.step = -this.speed;
              } else if (c.obj.position.z <= this.minHeight) {//最小高度
                c.step = this.speed;
              }

              c.obj.position.z += c.step;
            });
          }
        }

5.绘制文本标牌

原本尝试用Marker的自定义内容content来实现html标签的,但文档上说Marker高度属性height只有在2.1版本生效,但目前只有2.0版本,暂时无法使用高度属性

通过咨询高德地图官方,官方推荐用Loca数据可视化里面的ZMarkerLayer

以下代码来源于高德地图-Loca API标牌点-某片区房价信息

      var triangleZMarker = new Loca.ZMarkerLayer({
          loca: loca,
          zIndex119,
          depthfalse,
      });
      triangleZMarker.setSource(geo);//设置数据集
      triangleZMarker.setStyle({
          content(i, feat) => {//html自定义内容
              return (
                  '<div style="width: 120px; height: 120px; background: url(https://a.amap.com/Loca/static/loca-v2/demos/images/triangle_'
                  + (feat.properties.price < 60000 ? 'blue' : 'yellow')
                  + '.png);"></div>'
              );
          },
          unit'meter',
          rotation0,
          alwaysFronttrue,
          size: [6060],
          altitude15,//高度
      });
      triangleZMarker.addAnimate({//动画
          key'altitude',
          value: [01],
          randomtrue,
          transform1000,
          delay2000,
          yoyotrue,
          repeat999999,
      });
 

ZMarker添加整一个图层Layer生成一批Marker,官方的示例很漂亮。因为我想控制一个个的Marker,故改用Three.js的CSS2DRender来实现。

  • 在GLCustomLayer的init中添加初始化CSS2DRender
 let labelRenderer = new CSS2DRenderer();
        labelRenderer.setSize(container.offsetWidth, container.offsetHeight);
        labelRenderer.domElement.style.position = 'absolute';
        labelRenderer.domElement.style.top = '0px';
        //不妨碍界面上点击冲突
        labelRenderer.domElement.style.pointerEvents = 'none';
        this.container.appendChild(labelRenderer.domElement);
        this.labelRenderer = labelRenderer;
  • 在在GLCustomLayer的init中添加CSS2DRender渲染
this.labelRenderer.render(this.scene, this.camera);
  • 添加Label标牌
 addLabel(dom, pos) { 
  //label的dom可以触发事件
  dom.style.pointerEvents = 'auto';
    const label = new CSS2DObject(dom);
    label.position.set(...pos);
    this.scene.add(label);
    return label;
  }
  
addALabel(data) {
          const div = document.createElement('div');
          div.innerHTML = `<div class="tip-box" style="background:${data.bg};--base-color:${data.color}"><span class="circle" ></span><span class="text">${data.name}</span></div>`;
          //坐标转换
          const d = this.customCoords.lngLatToCoord(data.pos);
          const label = this.addLabel(div, [d[0], d[1], this.size * 1.5]);
          }
  • 标牌动画和样式直接用css
//变大弹出
@keyframes big {
        0% {
          transformscale(0);
        }
        100% {
          transformscale(1);
        }
      }
      //闪烁
      @keyframes flash {
        0% {
          opacity0.3;
        }
        100% {
          opacity1;
        }
      }

      .tip-box {
        --base-color: dodgerblue;
        border: solid 1px var(--base-color);
        background-colorrgba(301442550.3);
        color: white;
        white-space: nowrap;
        padding-left8px;
        padding-right16px;
        height32px;
        animation: big 1s ease-in;
        border-radius16px;
        display: flex;
        align-items: center;
        box-shadow0 0 8px var(--base-color);
      }
      .tip-box .circle {
        background-colorvar(--base-color);
        height16px;
        width16px;
        border-radius50%;
        animation: flash 0.5s ease-in alternate infinite;
      }
      .tip-box .text {
        margin0 8px;
      }

「注意」:DOM对象上如果有动画css请在外部包裹一层,避免css样式冲突,导致动画失效。

6.绘制飞线

飞线由管道形状绘制,通过着色器来改成头大尾小的形状。

  • 顶点着色器
float PI = acos(-1.0);
varying float vT;
varying float vS;
uniform float uTime;
uniform float uSize;
uniform float uLen;
void main(void) {
    //取模循环
    float d = mod(uv.x - uTime, 1.0);
    //截取uLen长度
    vS = smoothstep(0.0, uLen, d);
    //不在范围内不渲染
    if(vS < 0.01 || d > uLen)
        return;
    //头大尾小的飞线坐标点
    vec3 pos = position + normal * sin(PI * 0.5 * (vS - 0.6)) * uSize;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
  • 片元着色器
varying float vS; 
uniform vec3 uColor; 
void main(void) {
//透明度随着飞线头尾变小
    gl_FragColor = vec4(uColor, vS);
}
  • 添加管道飞线,用贝塞尔曲线形成弯曲
addLine(posList, color) {
//经纬度坐标点统一转换
          const d = this.customCoords.lngLatsToCoords(posList);
          //贝塞尔曲线形成弯曲
          const curve = new THREE.QuadraticBezierCurve3(
            new THREE.Vector3(d[0][0], d[0][1], 0),
            //取中间点
            new THREE.Vector3((d[0][0] + d[1][0]) * 0.5, (d[0][1] + d[1][1]) * 0.5this.size),
            new THREE.Vector3(d[1][0], d[1][1], 0)
          );
          const geometry = new THREE.TubeGeometry(curve, 32100008false);
          const material = new THREE.ShaderMaterial({
            uniforms: {
            //随时间变化
              uTime: { value0.0 },
              //飞线长度
              uLen: { value0.6 },
              //飞线宽度
              uSize: { value10000 },
              //飞线颜色
              uColor: { valuenew THREE.Color(color) }
            },
            //开启透明度
            transparenttrue,

            vertexShader``,
            fragmentShader``
          });

          const line = new THREE.Mesh(geometry, material);
          this.scene.add(line);
        }
  • 添加飞线动画,让飞线动起来
  new TWEEN.Tween({ time0 })
            .to({ time1.0 }, 1000)
            //重复动画
            .repeat(Infinity)
            .onUpdate((obj) => {
              material.uniforms.uTime.value = obj.time;
            })
            .start();

7.动画连续起来

最终效果:地图绘制运动边界,镜头放大倾斜,对应地点长出尖尖的山峰,然后弹出一个上下浮动四棱锥和文本标牌,随即生出一条飞线,镜头跟随,跳到下一个景点,再次弹出四棱锥和文本标牌,重复,走过所有地点后,定格到最终视角。

//运动边界线
          await this.createChinaLine();
          //绘制山峰
          await this.createA(this.tags[0]);
          //视角变化
          this.map.setPitch(68false3000);
          this.map.setRotation(24false3000);
          await this.sleep(2000);
          {
             //升起山峰
            const tw = await this.addAnimate({ time0 }, { time1 }, 1000, (obj) => {
              this.material.uniforms.uTime.value = obj.time;
            });            
            TWEEN.remove(tw);
            //添加标牌
            this.addALabel(this.tags[0]);
          }
          await this.sleep(2000);
          //绘制飞线
          this.addLine([this.tags[0].pos, this.tags[1].pos], this.tags[0].color);
          for (let i = 1; i < this.tags.length; i++) {
            const data = this.tags[i];
            //视角跟随到新地点
            this.map.setCenter(data.pos, false1000);
            this.map.setZoom(6false1000);
            await this.sleep(1000);
            //绘制山峰
            await this.createA(data);
            //升起山峰
            const tw = await this.addAnimate({ time0 }, { time1 }, 1000, (obj) => {
              this.material.uniforms.uTime.value = obj.time;
            });
            TWEEN.remove(tw);
            this.addALabel(data);
            await this.sleep(1000);
            //添加飞线
            if (i < this.tags.length - 1) {
              this.addLine([data.pos, this.tags[i + 1].pos], data.color);
            } else {
              //最终视角
              this.map.setPitch(73.2false3000);
              this.map.setZoom(5false1000);
              this.map.setRotation(58.7false3000);
              this.map.setCenter([101.635.6], false1000);
            }
          }

高德地图3D地图视角设置真的很方便,还自带动画功能,效果真的很棒!

  • setZoom设置缩放大小
  • setPitch设置倾斜角度
  • setCenter设置地图中心经纬度
  • setRotation设置旋转角度

8.GitHub地址

https://github.com/xiaolidan00/my-earth

点击关注公众号,“技术干货” 及时达!

稀土掘金技术社区
掘金,一个帮助开发者成长的技术社区
 最新文章