从0开始疫情3D地球 - 3D疫情地球VDEarth - 4 - 3D地球组件实现(2)
上一篇,实现到了地球的3D可视化的展示,本篇在之前的基础上增加一些数据标注
为了美观,先给地球加上一些点缀
1 创建星星
太空中的视角,星星就是一个个的两点,这里借助threejs的Geometry实现
修改material.js, 在材质类中新增一个星星的材质创建方法
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方法,新增调用星星创建的方法
// 创建模型 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 打开浏览器就可以看到动态创建的地球和散列的星星
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); } }
里面定义了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); ... }
这样柱形图标注就创建完成了
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]); } }
创建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); }
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); }
修改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); }); }
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连接,动态更新
相关链接