vue 数据可视化大屏 项目开发中,会添加一些echarts地图效果,但是一些大屏项目中对地图的效果要求比较高,一般的echarts无法满足客户需要了,所以我们需要3d效果的地图,这里我们使用threejs来实现3d地图效果。今天我们要实现的是3d地图加标签效果。效果类似echarts的label标签。
效果演示地址
threejs版本
因为threejs不同版本会有差异,为了避免出现代码无法其他版本下正常运行,我们事先指定好版本,这样可以避免因为版本不同导致的问题。
"three": "0.126.0",
vue内代码
threejs是要在vue实现效果,所以框架也是vue脚手架,vue内html
<template>
<div class="canvasGLTFBody" ref="canvasGLTFBody">
<div ref='canvasGLTF' class="canvasGLTF" id="canvasGLTF">
</div>
<div id="LoadingInfo" class="LoadingInfo"></div>
</div>
</template>
<script>
监听窗口变化
因为数据大屏项目一般时要求适应屏幕宽高的,所以我们需要监听窗口变化,当窗口有所变化是,我们需要重新调整three窗口的大小进行适配。
监听窗口大小
mounted() {
var that = this;
const viewElem = document.body;
setTimeout(() => {
this.getData()
}, 300)
// 监听窗口变化
const resizeObserver = new ResizeObserver(() => {
setTimeout(() => {
that.getResize();
}, 300)
});
resizeObserver.observe(viewElem);
},
调整threejs窗口
这里为了让threejs项目能够快速应用到vue项目中,threejs项目的宽高动态获取指定div宽度,而这个指定div又是随着父div的大小变化而变化。
getResize() {
var that = this;
this.width = this.$refs.canvasGLTF.offsetWidth
this.height = this.$refs.canvasGLTF.offsetHeight
that.renderer.setSize(that.width, that.height);
that.labelRenderer.setSize(this.width, this.height)
},
加载地图数据
首先我们需要引入地图数据,然后遍历地图边界数据,绘制地图上的边线以及区域。
import data from './data/china.json'
function getJson() {
return new Promise(resolve => {
data.features.forEach(function (area) {
if (area.geometry.type === "Polygon") {
area.geometry.coordinates = [area.geometry.coordinates];
}
var listItem = {
title: area.properties.name,
center: area.properties.center,
}
tagList.push(listItem)
lineGroup.add(borderLine(area.geometry.coordinates));
var height = TensileHeight;//TensileHeight
var mesh = ExtrudeMesh(area.geometry.coordinates, height)
mesh.name = area.properties.name
meshGroup.add(mesh);
});
var lineGroup2 = lineGroup.clone();
mapGroup.add(lineGroup2);
lineGroup2.position.z = -TensileHeight * 0.1;
resolve('data')
})
}
地图边线方法
我们在获取边界数据后,通过THREE.LineBasicMaterial
来绘制地图上的边线,这里我们可以指定边界线的颜色。
import * as THREE from 'three';
function borderLine(pointsArrs) {
var group = new THREE.Group();
pointsArrs.forEach(polygon => {
var pointArr = [];
polygon[0].forEach(elem => {
pointArr.push(elem[0], elem[1], 0);
});
group.add(outline(pointArr));
});
return group;
}
function outline(pointArr) {
/**
* 通过BufferGeometry构建一个几何体,传入顶点数据
* 通过Line模型渲染几何体,连点成线
* LineLoop和Line功能一样,区别在于首尾顶点相连,轮廓闭合
*/
var geometry = new THREE.BufferGeometry();
var vertices = new Float32Array(pointArr);
// 创建属性缓冲区对象
var attribue = new THREE.BufferAttribute(vertices, 3);
geometry.attributes.position = attribue;
var material = new THREE.LineBasicMaterial({
color: 0xa9dbff
});
var line = new THREE.LineLoop(geometry, material);
return line;
}
export { borderLine };
地图区域绘制方法
我们通过threejs的MeshLambertMaterial
来绘制地图区域,这里我们可以指定地图的颜色,以及高度。
import * as THREE from 'three';
function ExtrudeMesh(pointsArrs, height) {
var shapeArr = [];
pointsArrs.forEach(pointsArr => {
var vector2Arr = [];
pointsArr[0].forEach(elem => {
vector2Arr.push(new THREE.Vector2(elem[0], elem[1]))
});
var shape = new THREE.Shape(vector2Arr);
shapeArr.push(shape);
});
var material = new THREE.MeshLambertMaterial({
color: 0x6793e0,
}); //材质对象
var sidebarMaterial = new THREE.MeshLambertMaterial({
color: 0x174ca3,
}); //材质对象
var geometry = new THREE.ExtrudeBufferGeometry( //拉伸造型
shapeArr,
{
depth: height,
bevelEnabled: false
}
);
var mesh = new THREE.Mesh(geometry, [material, sidebarMaterial]); //网格模型对象
return mesh;
}
export {ExtrudeMesh};
绘制光源
要能看到地图,我们还需要光源。
// 光源
var directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
directionalLight.position.set(400, 200, 300);
scene.add(directionalLight);
var directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.6);
directionalLight2.position.set(-400, -200, -300);
scene.add(directionalLight2);
var ambient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambient);
// 光源
添加css2d标签
要添加标签,还需要添加css2d渲染器
that.labelRenderer = new CSS2DRenderer();
that.labelRenderer.setSize(that.width, that.height);
that.labelRenderer.domElement.style.position = 'absolute';
// // 避免renderer.domElement影响HTMl标签定位,设置top为0px
that.labelRenderer.domElement.style.top = '0px';
that.labelRenderer.domElement.style.left = '0px';
// //设置.pointerEvents=none,以免模型标签HTML元素遮挡鼠标选择场景模型
that.labelRenderer.domElement.style.pointerEvents = 'none';
将渲染器插入到div
threejs的两个渲染器需要都插入到同一个div内。
that.$refs.canvasGLTF.appendChild(that.labelRenderer.domElement)
that.$refs.canvasGLTF.appendChild(that.renderer.domElement)
地图居中处理
因为地图的多样性,比如我们除了渲染中国地图,还可能渲染某个省的,某个市的地图,这时候我们就需要染地图无论渲染什么样的地图,都是处在中间的。我们可以通过获取地图的中心点,地图的大小,来实现地图居中的效果。
import * as THREE from 'three';
function centerCamera(mapGroup, camera, k) {
// 地图mapGroup的包围盒计算
var box3 = new THREE.Box3(); //创建一个包围盒
box3.expandByObject(mapGroup); // .expandByObject()方法:计算层级模型group包围盒
var center = new THREE.Vector3(); //scaleV3表示包围盒的几何体中心
box3.getCenter(center); // .getCenter()计算一个层级模型对应包围盒的几何体中心
// console.log('查看几何中心', center);
// 重新设置模型的位置
mapGroup.position.x = mapGroup.position.x - center.x;
mapGroup.position.y = mapGroup.position.y - center.y;
mapGroup.position.z = mapGroup.position.z - center.z;
/*可以根据中国地图mapGroup的包围盒尺寸设置相机参数s */
var scaleV3 = new THREE.Vector3(); //scaleV3表示包围盒长宽高尺寸
box3.getSize(scaleV3) // .getSize()计算包围盒长宽高尺寸
console.log('查看包围盒尺寸', scaleV3)
// frame.js文件中var s = 150; 150更改为scaleV3.x/2
var maxL = maxLFun(scaleV3);
//重新设置s值 乘以0.5适当缩小显示范围,地图占canvas画布比例更大,自然渲染范围更大
var s = maxL / 2 * 0.5;
camera.left = -s * k;
camera.right = s * k;
camera.top = s;
camera.bottom = -s;
//更新相机视图矩阵
camera.updateProjectionMatrix();
return maxL
}
// 获取地图的中心点
function getBoxCenter(mapGroup) {
// 地图mapGroup的包围盒计算
var box3 = new THREE.Box3(); //创建一个包围盒
box3.expandByObject(mapGroup); // .expandByObject()方法:计算层级模型group包围盒
var center = new THREE.Vector3(); //scaleV3表示包围盒的几何体中心
box3.getCenter(center); // .getCenter()计算一个层级模型对应包围盒的几何体中心
return center
}
// 获取地图的大小
function getBoxSize(mapGroup) {
// 地图mapGroup的包围盒计算
var box3 = new THREE.Box3(); //创建一个包围盒
box3.expandByObject(mapGroup); // .expandByObject()方法:计算层级模型group包围盒
var scaleV3 = new THREE.Vector3(); //scaleV3表示包围盒长宽高尺寸
box3.getSize(scaleV3) // .getSize()计算包围盒长宽高尺寸
return scaleV3
}
// 计算包围盒的最大边长
function maxLFun(v3) {
var max;
if (v3.x > v3.y) {
max = v3.x
} else {
max = v3.y
}
if (max > v3.z) {
} else {
max = v3.z
}
return max;
}
export {centerCamera, getBoxCenter, getBoxSize};
完整实例代码
getData() {
var that = this
this.width = this.$refs.canvasGLTF.offsetWidth
this.height = this.$refs.canvasGLTF.offsetHeight
var color1 = new THREE.Color(0xedc551);
var tagList = []
var scene = new THREE.Scene();
var mapGroup = new THREE.Group();
scene.add(mapGroup);
var lineGroup = new THREE.Group();
mapGroup.add(lineGroup);
var meshGroup = new THREE.Group();
var center = mapGroup.add(meshGroup);
var TensileHeight = 1;// 拉伸高度
lineGroup.position.z = TensileHeight + TensileHeight * 0.1;
// 光源
var directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
directionalLight.position.set(400, 200, 300);
scene.add(directionalLight);
var directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.6);
directionalLight2.position.set(-400, -200, -300);
scene.add(directionalLight2);
var ambient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambient);
// 光源
var k = that.width / that.height;
var s = 0;
that.labelRenderer = new CSS2DRenderer();
that.labelRenderer.setSize(that.width, that.height);
that.labelRenderer.domElement.style.position = 'absolute';
// // 避免renderer.domElement影响HTMl标签定位,设置top为0px
that.labelRenderer.domElement.style.top = '0px';
that.labelRenderer.domElement.style.left = '0px';
// //设置.pointerEvents=none,以免模型标签HTML元素遮挡鼠标选择场景模型
that.labelRenderer.domElement.style.pointerEvents = 'none';
getJson();
// 添加tag标签
tagList.forEach((type) => {
scene.add(tagName(type.title, type.center[0], type.center[1], TensileHeight + 0.01));
});
s = (getBoxSize(mapGroup).x + getBoxSize(mapGroup).y) / 3
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(104, -105, 200);
camera.lookAt(scene.position);
that.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
// 设置背景颜色
that.renderer.setClearColor(0x000000, 0)
that.renderer.setSize(that.width, that.height);
function render() {
that.labelRenderer.render(scene, camera);
that.renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
var controls = new OrbitControls(camera, that.renderer.domElement);
// 获取中心点
var getBoxCenterData = getBoxCenter(mapGroup)
// 设置中心
controls.target.set(getBoxCenterData.x, getBoxCenterData.y, 0);
controls.update();
// 获取Json数据
function getJson() {
return new Promise(resolve => {
data.features.forEach(function (area) {
if (area.geometry.type === "Polygon") {
area.geometry.coordinates = [area.geometry.coordinates];
}
var listItem = {
title: area.properties.name,
center: area.properties.center,
}
tagList.push(listItem)
lineGroup.add(borderLine(area.geometry.coordinates));
var height = TensileHeight;//TensileHeight
var mesh = ExtrudeMesh(area.geometry.coordinates, height)
mesh.name = area.properties.name
meshGroup.add(mesh);
});
var lineGroup2 = lineGroup.clone();
mapGroup.add(lineGroup2);
lineGroup2.position.z = -TensileHeight * 0.1;
resolve('data')
})
}
that.$refs.canvasGLTF.appendChild(that.labelRenderer.domElement)
that.$refs.canvasGLTF.appendChild(that.renderer.domElement)
}
源码实例下载
当前完整演示实例代码下载
项目基于Vue3 vite js实现