從0開始疫情3D地球 - 3D疫情地球VDEarth - 4 - 3D地球組件實現(2)

上一篇,實現到了地球的3D可視化的展現,本篇在以前的基礎上增長一些數據標註html

爲了美觀,先給地球加上一些點綴前端

1 建立星星

太空中的視角,星星就是一個個的兩點,這裏藉助threejs的Geometry實現npm

修改material.js, 在材質類中新增一個星星的材質建立方法json

class material {
  .
  .
  . 
  createStarMat() {
    var starsMaterial = new PointsMaterial({ color: 0x8e8e8e });
    return starsMaterial;
  }
  .
  .
  .
}

修改material.js, 在材質類中新增一個星星的模型建立方法,建立2000個geometry數組

class vdGeom {
  .
  .
  .
  createStarGeom() {
    let starsGeometry = new Geometry();
    for (let i = 0; i < 2000; i++) {
      let starVector = new Vector3(
        Math.randFloatSpread(2000),
        Math.randFloatSpread(2000),
        Math.randFloatSpread(2000)
      );
      starsGeometry.vertices.push(starVector);
    }
    return starsGeometry;
  }
  .
  .
  .
}

修改model.js ,新增建立星星模型的方法瀏覽器

class mesh {
  .
  .
  .
  createStars() {
    let vdGeom = this.vdGeom.createStarGeom();
    let vdMaterial = this.vdMaterial.createStarMat();
    return new Points(vdGeom, vdMaterial);
  }
  .
  .
  .
}

修改VDEarth.js ,修改以前的initObj方法,新增調用星星建立的方法socket

// 建立模型
function initObj() {
  let self = this;
  let fontloader = new FontLoader();
  // 建立地球模型組
  this.baseGroup = new Group();
  // 建立地球
  this.radius = minSize(this.contentWidth, this.contentHeight) * 0.2;
  let globalMesh = new model().createGlobe(this.radius, this.textrue);
  this.baseGroup.add(globalMesh);
  // 建立星點
  let stars = new model().createStars();
  this.baseGroup.add(stars);
  this.scene.add(this.baseGroup);
}

npm run start 打開瀏覽器就能夠看到動態建立的地球和散列的星星ide

2 建立柱形圖標註

有了基本的地球模型,咱們要在地球模型上實現數據的展現,這裏使用3d柱形圖進行數據的展現,以各個地區的疫情數據做爲基礎,這裏只涉及到前端展現,先定義一個數據格式以下:字體

定義一個數組對象,定義兩個國家的數據,包含名稱,經度,維度,累計確診總數動畫

[{
   name:'中國',
   lng:'116.46',
   lat: '39.92',
   total:'80000'
},
{
   name:'美國',
   lng:'-77.02',
   lat: '39.91',
   total:'890000'
}]

接下來修改以前的marker.js,建立一個marker類

import geometry from './geometry';
import material from './material';

class marker {
  constructor() {
    this.vdGeom = new geometry();
    this.vdMaterial = new material();
  }
}
export default marker;

在marker類中,新增建立柱形圖marker 的方法 

addBoxMarkers(group, radius, items) {
    for (let i = 0; i < items.length; i++) {
      // 獲取場景下組內已有的柱形圖標記模型
      let boxMarker = _.find(group.children, (model) => {
        return (
          model.userData.type == 'bar' && model.userData.name == items[i].name
        );
      });
      // 不存在就建立
      if (!boxMarker) {
        // 柱形圖的深度,即顯示出來的高度
        // 防止數據大小差異很大,這裏用log方法對數據進行歸一化處理
        let depth = window.Math.log2(items[i].total) * 5;
        let boxMarkerGeom = this.vdGeom.createBoxMarkerGeom(depth);
        let boxMarkerMat = this.vdMaterial.createBoxMakerMat();
        boxMarker = new Mesh(boxMarkerGeom, boxMarkerMat);
        // 定位
        let position = getPosition(
          parseFloat(items[i].lng),
          parseFloat(items[i].lat),
          radius
        );
        boxMarker.position.set(position.x, position.y, position.z);
        // 設置柱形圖的自定義數據爲遍歷的數據項
        boxMarker.userData = Object.assign({ type: 'bar' }, items[i]);
        // 標記垂直於圓心
        boxMarker.lookAt(new Vector3(0, 0, 0));
        group.add(boxMarker);
      }
      // 縮放0.1倍,爲後續柱形圖動畫預留
      boxMarker.scale.set(1, 1, 0.1);
      boxMarkAnimate(boxMarker);
    }
  }
View Code

裏面定義了mesh的自定義數據,數據爲前面定義的數據格式,

其中調用了獲取數據在場景中定位的方法getPosition,這個定義在utils.js文件

// 經緯度轉圖形position
export function getPosition(lng, lat, radius) {
    let phi = (90 - lat) * (window.Math.PI / 180),
      theta = (lng + 180) * (window.Math.PI / 180),
      x = -(radius * window.Math.sin(phi) * window.Math.cos(theta)),
      z = radius * window.Math.sin(phi) * window.Math.sin(theta),
      y = radius * window.Math.cos(phi);
    return { x: x, y: y, z: z };
  }

 能夠看到這裏建立的仍是一個mesh對象,響應的要建立一個geometry和material方法

修改 geomtry.js, 在類中新增一個建立柱形圖的geometry方法,createBoxMarkerGeom

class vdGeom {  
  ...

  createBoxMarkerGeom(depth) {
    let boxGeometry = new BoxGeometry(2, 2, depth);
    return boxGeometry;
  }

  ...
}

修改material.js , 在類中新增一個建立材質的方法createBoxMakerMat

class material {
  
  ...

  createBoxMakerMat() {
    var boxMarkerMaterial = new MeshPhongMaterial({
      color: '#6dc3ec',
      side: DoubleSide,
      depthTest: true,
    });
    return boxMarkerMaterial;
  }
  
  ...
}

修改VDEarth.js, 修改initObj 方法,新增調用建立柱形圖的方法

// 建立模型
function initObj() {

   ...   

   // 定義好的數據格式  
   var data = [{
   name:'中國',
   lng:'116.46',
   lat: '39.92',
   total:'80000'
   },{
   name:'美國',
   lng:'-77.02',
   lat: '39.91',
   total:'890000'
   }]
   var self = this  
   // 建立一個標記組
    self.markerGroup = new Group();
    // 建立標記
    var myMarkers = new marker();
    // 柱形
    myMarkers.addBoxMarkers(self.markerGroup, self.radius, data);
    // 添加到地球模型組裏
    self.baseGroup.add(self.markerGroup);
   ...
}
View Code

這樣柱形圖標註就建立完成了

3 建立名稱標註

除了柱形圖的標註的建立,繼續建立一個名稱標註,這裏用和柱形圖一樣的數據格式

同上一節,增長建立名稱標註的方法

addNameMarkers(group, radius, items, font) {
    for (let i = 0; i < items.length; i++) {
      let nameMarker = _.find(group.children, (model) => {
        return (
          model.userData.type == 'name' && model.userData.name == items[i].name
        );
      });
      if (!nameMarker) {
        let geometry = this.vdGeom.createNameMarkerGeom(
          items[i].name + ':' + items[i].total,
          font
        );
        let material = this.vdMaterial.createNameMarkerMat();
        nameMarker = new Mesh(geometry, material);
        // 定位
        let position = getPosition(
          parseFloat(items[i].lng),
          parseFloat(items[i].lat),
          radius + window.Math.log2(items[i].total) * 5
        );
        nameMarker.position.set(position.x, position.y, position.z);
        // 標記垂直於圓心
        nameMarker.lookAt(
          new Vector3(position.x * 1.1, position.y * 1.1, position.z * 1.1)
        );
        group.add(nameMarker);
      }
      nameMarker.visible = false;
      nameMarker.userData = Object.assign({ type: 'name' }, items[i]);
    }
  }
View Code

建立geometry

 createNameMarkerGeom(name, font) {
    let textGeo = new TextGeometry(name, {
      font: font,
      size: 4,
      height: 0.5,
      curveSegments: 0.1,
      bevelEnabled: false,
    });
    // 文字居中
    textGeo.center();
    return textGeo;
  }

建立材質

  createNameMarkerMat() {
    var nameMarkerMaterial = new MeshBasicMaterial({
      color: '#fff',
      transparent: true,
    });
    return nameMarkerMaterial;
  }

修改VDEarth.js, 修改initObj 方法,新增調用建立柱形圖的方法,這裏的字體使用了json文件的字體,具體能夠參考threejs官網字體的說明

// 建立模型
function initObj() {
  let self = this;
  let fontloader = new FontLoader();
  // 建立地球模型組
  this.baseGroup = new Group();
  // 建立地球
  this.radius = minSize(this.contentWidth, this.contentHeight) * 0.2;
  let globalMesh = new model().createGlobe(this.radius, this.textrue);
  this.baseGroup.add(globalMesh);
  // 建立星點
  let stars = new model().createStars();
  this.baseGroup.add(stars);
  var data = [{
   name:'中國',
   lng:'116.46',
   lat: '39.92',
   total:'80000'
  },
  {
   name:'美國',
   lng:'-77.02',
   lat: '39.91',
   total:'890000'
   }]
    // 建立一個標記組
    self.markerGroup = new Group();
    // 建立標記
    var myMarkers = new marker();
    // 柱形
    myMarkers.addBoxMarkers(self.markerGroup, self.radius, data);
    // 加載字體
    if (self.font) {
      myMarkers.addNameMarkers(self.markerGroup, self.radius, data, self.font);
    } else {
      fontloader.load('./fonts/SimHei_Regular.json', function (font) {
        self.font = font;
        myMarkers.addNameMarkers(
          self.markerGroup,
          self.radius,
          data,
          self.font
        );
      });
    }
    self.baseGroup.add(self.markerGroup);
  this.scene.add(this.baseGroup);
}
View Code

4 柱形圖交互

因爲名稱在地球上顯示,數據過多會相互遮擋,因此前面的名稱標註的建立都是隱藏的狀態,使用點擊柱形圖觸發名稱和數據的顯示

建立和修改eventLister.js

import {Vector2,Raycaster} from 'three'
// 獲取mesh
function getAllMeshes(obj, meshArr) {
    obj.children.forEach(element => {
      if (element.type == 'Mesh') {
        meshArr.push(element);
      } else if (element.children.length > 0) {
        getAllMeshes(element, meshArr);
      }
    });
}
// 柱狀標記點擊事件
export function createModelClick(){
    var self = this
    //點擊事件
    window.addEventListener('click',function (e) {
        // 重置選擇的柱形標註的顏色
        if(self.seledModel){
            self.seledModel.object.material.color.set('#6dc3ec');
        }
        // 重置名稱的可見
        if(self.nameModel ){
            self.nameModel.visible = false;
        }
        var mouse = new Vector2();
        mouse.x =((e.clientX - window.innerWidth + self.contentWidth) / self.contentWidth) * 2 - 1;
        mouse.y =-((e.clientY - window.innerHeight + self.contentHeight) / self.contentHeight) *2 + 1;
        
        var raycaster = new Raycaster();
        // update the picking ray with the camera and mouse position
        raycaster.setFromCamera(mouse, self.camera);
        
        var objects = [];
        // 獲取標記組下全部的mesh
        getAllMeshes(self.markerGroup, objects);

        //射線和模型求交,選中一系列直線
        var intersects = raycaster.intersectObjects(objects);
        if (intersects.length > 0) {
            for (var i = 0; i < intersects.length; i++) {
                var ele = intersects[i];
                if (ele.object.userData && ele.object.userData.type === 'bar') {
                    // 選中的柱形圖顏色修改
                    self.seledModel = ele;
                    ele.object.material.color.set('#e0ef08');
                    // 選中的柱形標註對應的文本的可見
                    self.nameModel = _.find(self.markerGroup.children,model=>{
                        return model.userData.type == 'name' && model.userData.name == ele.object.userData.name
                    })
                    if(self.nameModel){
                        self.nameModel.visible = true;
                    }
                    
                    return ;
                }
                self.seledModel = null
                self.nameModel = null
            }
        }else{
            self.seledModel = null
            self.nameModel = null
        }
    }, false);
}
View Code

修改VDEarth.js ,修改VDEarth類內的init方法

init(opt = {}) {
    var self = this;
    // 合併用戶配置屬性
    _.merge(this.options, opt);

    // 獲取容器的寬高
    this.contentWidth = this.options.container.offsetWidth;
    this.contentHeight = this.options.container.offsetHeight;
    // 加載貼圖
    let globeTextureLoader = new TextureLoader();
    globeTextureLoader.load('./images/world.jpg', function (textrue) {
      self.textrue = textrue;
      // 初始化渲染器
      initRenderer.call(self);
      // 初始化舞臺
      initScene.call(self);
      // 初始化相機
      initCamera.call(self);
      // 初始化燈光
      initLight.call(self);
      // 初始化控制器
      initControls.call(self);
      // 初始化模型
      initObj.call(self);
      // 初始化點擊事件
      createModelClick.call(self);
      // 初始化實時更新方法
      animate.call(self);
    });
  }
View Code

5 柱形圖動畫

柱形圖的動畫藉助tween.js進行實現,首先保證已經npm install @tweenjs/tween.js 組件

建立和修改animation.js, 定義一個加單的動畫效果,柱形圖在Z週上的縮放效果,具體以下

import TWEEN from '@tweenjs/tween.js'
export function boxMarkAnimate(model){
    let tw = new TWEEN.Tween(model.scale)
    tw.to({
        z:1
    },2000)
    tw.easing(TWEEN.Easing.Sinusoidal.InOut)
    tw.start()
}

修改markers.js,引入

import { boxMarkAnimate } from './animation';

在addBoxMarkers方法內增長

boxMarkAnimate(boxMarker);

至此就實現了3D地球的可視化,後續會在爬蟲完成後新增socket鏈接,動態更新

 

相關連接

從0開始疫情3D地球 - 3D疫情地球VDEarth - 1- 引言 

從0開始疫情3D地球 - 3D疫情地球VDEarth - 2 - 前端代碼構建 

從0開始疫情3D地球 - 3D疫情地球VDEarth - 3 - 3D地球組件實現(1) 

從0開始疫情3D地球 - 3D疫情地球VDEarth - 4 - 3D地球組件實現(2) 

從0開始疫情3D地球 - 3D疫情地球VDEarth - 5 - 疫情數據爬蟲 

從0開始疫情3D地球 - 3D疫情地球VDEarth - 6 - 數據推送  

相關文章
相關標籤/搜索