Three.js 版本:0.152.0
地图Geojson获取方式:Datav.GeoAtlas
先看效果
代码拆分讲解
实例化并初始化地图
class CoreMap {
constructor(option, element) {
this.option = option
this.element = element
this.textureLoader = new THREE.TextureLoader();
this.mapGroup = new THREE.Group() // 地图
this.lineGroup = new THREE.Group() // 线条
this.regionGroup = new THREE.Group() // 区域
this.textLabelGroup = new THREE.Group() // 名称
this.barGroup = new THREE.Group() // 光柱
this.topTexture = this.textureLoader.load( './images/top-texture.png' );
this.topTexture.colorSpace = THREE.SRGBColorSpace;
}
init() {
this.initScene() // 初始化场景
this.initComposer() // 后期处理
this.drawMap() // 绘制地图
this.drawImage() // 绘制底图
this.setControls()
this.animate();
}
animate() {
requestAnimationFrame(this.animate.bind(this));
this.controls.update();
this.composer.render();
TWEEN.update()
this.labelRenderer.render( this.scene, this.camera );
}
....
}
const option = {
sideDepth: 8,
sideColor: '#175684',
topLineColor: '#fff',
edgeColor: '#00ae9d'
}
const coreMap = new CoreMap(option, document.querySelector('#container'))
coreMap.init()
初始化场景、相机等元素
initScene() {
this.scene = new THREE.Scene();
const width = this.element.offsetWidth, height = this.element.offsetHeight
// camera
this.camera = new THREE.PerspectiveCamera( 45, width / height, 1, 10000 );
this.camera.position.set(0, 0, 5);
// renderer
this.renderer = new THREE.WebGLRenderer({antialias: true});
this.renderer.setSize( width, height);
this.renderer.setClearColor(0xffffff, 0);
this.element.appendChild(this.renderer.domElement);
this.labelRenderer = new CSS2DRenderer(); // 绘制地名采用css2d的方式
this.labelRenderer.setSize( width, height );
this.labelRenderer.domElement.style.position = 'absolute';
this.labelRenderer.domElement.style.top = '0px';
this.element.appendChild( this.labelRenderer.domElement );
// light
const alight = new THREE.AmbientLight( 0xffffff, 4)
this.scene.add(alight);
}
绘制地图
async drawMap() {
const mapData = await this.loadMapData()
mapUtil.computeLngLat(mapData, 200) // 地图工具类,主要用来处理墨卡托或世界坐标转换
mapData.features.forEach(item => {
const {coordinates, type} = item.geometry
const shapeList = [] //多面数组
coordinates.forEach(coordinate => {
if (type === "MultiPolygon") {
coordinate.forEach(polyon => void this.createPolyon(polyon, shapeList))
} else if(type === "Polygon") {
this.createPolyon(coordinate, shapeList)
}
})
const mesh = this.getExtrudeMesh(shapeList)
if(item.properties.name) {
const textLabel = this.createTextLabel(item.properties)
this.textLabelGroup.add(textLabel)
}
this.regionGroup.add(mesh)
})
this.mapGroup.add(this.regionGroup)
this.mapGroup.add(this.lineGroup)
this.mapGroup.add(this.textLabelGroup)
this.updateCamera(this.mapGroup) // 根据地图大小自适应调整相机
this.scene.add(this.mapGroup)
}
loadMapData() {
const loader = new THREE.FileLoader()
loader.setResponseType('json')
return new Promise(rs => void loader.load('./city.json', data => rs(data)))
}
绘制区域多边形
createPolyon(shapeList) {
const vector2Arr = [], linePoints = []
for (let i = 0; i < points.length; i++) {
const [x, y] = mapUtil.toWorld(points[i]); // 经纬度转世界坐标
vector2Arr.push(new THREE.Vector2(x, -y))
linePoints.push(x, -y, 0);
}
shapeList.push(new THREE.Shape(vector2Arr))
this.lineGroup.add(this.getLineMesh(linePoints))
}
getExtrudeMesh(shapeList) {
// 挤压缓冲几何体,用这个几何体的原因是可以配置"depth"(侧边形状的深度)参数
const geometry = new THREE.ExtrudeGeometry( shapeList, {depth: this.option.sideDepth} );
// 顶面材质
const topMaterial = new THREE.MeshStandardMaterial({
map: this.topTexture // 纹理
});
// 侧面材质
const extrudeMaterial = new THREE.MeshStandardMaterial({color: this.option.sideColor})
const mesh = new THREE.Mesh( geometry, [topMaterial, extrudeMaterial] )
return mesh
}
绘制区域线
getLineMesh(linePoints) {
const geometry = new LineGeometry();
geometry.setPositions( linePoints );
const color = new THREE.Color(this.option.topLineColor);
const uplineMaterial = new LineMaterial({
transparent: true,
opacity: 0.6,
color,
linewidth: 1
});
uplineMaterial.resolution.set( this.element.offsetWidth, this.element.offsetHeight)
const line = new Line2(geometry, uplineMaterial);
line.position.z = this.option.sideDepth + 0.3; // 侧边深度 + 0.3
return line
}
绘制地名
createTextLabel(properties) {
const z = this.option.sideDepth + 0.021
const {name, centroid, center} = properties
const element = document.createElement( 'div' );
const textNode = document.createElement('div');
const imgNode = document.createElement('div');
textNode.textContent = name;
textNode.style.color = '#fff';
textNode.style.fontSize = '14px';
textNode.style.opacity = 1
imgNode.style.backgroundImage = 'url(./images/bg1.svg)'
imgNode.style.height = '22px'
imgNode.style.backgroundRepeat = 'no-repeat'
imgNode.style.backgroundPosition = 'center'
element.appendChild( textNode );
element.appendChild( imgNode );
const label = new CSS2DObject(element);
const [x, y] = mapUtil.toWorld(centroid || center);
label.position.set(x, -y, z);
return label
}
相机自适应
updateCamera(obj) {
const mapBox = new THREE.Box3().setFromObject(obj);
const mapBoxSize = mapBox.getSize(new THREE.Vector3()).length();
const boxCenter = mapBox.getCenter(new THREE.Vector3());
// 这个函数官方案例有,可以自己去案例里搜索,简单讲就是根据物体大小计算相机与物体的距离,调整相机到适合观察物体的距离
this.frameArea(mapBoxSize * 0.9, mapBoxSize, boxCenter);
obj.rotation.x = - Math.PI / 180 * 45 // 这里为了初次加载好看,绕x轴旋转-45度
this.controls.maxDistance = mapBoxSize * 2 // 向外移动
this.controls.maxAzimuthAngle = Math.PI / 180 * 90 // 水平旋转的角度上限
this.controls.minAzimuthAngle = -Math.PI / 180 * 90 // 水平旋转的角度下限
this.controls.update();
}
frameArea(sizeToFitOnScreen, boxSize, boxCenter) {
const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
const halfFovY = THREE.MathUtils.degToRad(this.camera.fov * .5);
const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
// compute a unit vector that points in the direction the camera is now
// in the xz plane from the center of the box
const direction = (new THREE.Vector3())
.subVectors(this.camera.position, boxCenter)
.multiply(new THREE.Vector3(0, 0, 1))
.normalize();
// move the camera to a position distance units way from the center
// in whatever direction the camera was from the center already
this.camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
// pick some near and far values for the frustum that
// will contain the box.
this.camera.near = boxSize / 100;
this.camera.far = boxSize * 100;
this.camera.updateProjectionMatrix();
// point the camera to look at the center of the box
this.camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
}
至此,地图区域和相机自适应大小实现完成~
底座动画
底座绘制没什么特别高深的东西,就是创建几个平面几何体,材质贴上自己想要的纹理图片即可;动画效果是通过tween.js实现的,参照下列代码.
drawImage() {
const imgGroup = new THREE.Group()
const bgTexture = this.textureLoader.load( './images/bg-texture.png' );
bgTexture.colorSpace = THREE.SRGBColorSpace;
const bgTexture1 = this.textureLoader.load( './images/t1.png' );
bgTexture1.colorSpace = THREE.SRGBColorSpace;
const bgTexture2 = this.textureLoader.load( './images/t2.png' );
bgTexture2.colorSpace = THREE.SRGBColorSpace;
const geometry = new THREE.PlaneGeometry( 400,400 );
const material = new THREE.MeshBasicMaterial( { map: bgTexture, depthWrite: false, transparent: true} );
const plane = new THREE.Mesh( geometry, material );
const geometry1 = new THREE.PlaneGeometry( 200,200 );
const material1 = new THREE.MeshBasicMaterial( { map: bgTexture1, depthWrite: false, transparent: true} );
const plane1 = new THREE.Mesh( geometry1, material1 );
const geometry2 = new THREE.PlaneGeometry( 180,180 );
const material2 = new THREE.MeshBasicMaterial( { map: bgTexture2, depthWrite: false, transparent: true} );
const plane2 = new THREE.Mesh( geometry2, material2 );
imgGroup.add(plane, plane1, plane2)
imgGroup.rotation.x = -Math.PI / 180 * 45
const coords = {z: 0}
const tween = new TWEEN.Tween(coords)
.to({z: 360}, 25000)
.onUpdate(() => {
plane1.rotation.z = Math.PI / 180 * coords.z
plane2.rotation.z = -Math.PI / 180 * coords.z
})
.start()
.repeat(Infinity)
console.log(tween)
this.scene.add( imgGroup );
}
总结
以上代码趁着节假日时间充裕整理了一下,分享给大家,最后文章有哪里写的不对或不理解的地方,欢迎指正交流~