广州市律师网站建设公司南昌seo
最终效果为上图。
实现该技术,需要一些技术,我分别罗列一下:
- canvas:需要使用canvas根据json来绘制地球,不懂的可以看这篇canvas绘制地球
- threejs:需要会使用threejs,这里并没有使用shader,不需要制作复杂的东西。
- Vue3:这个可选。不会也能实现。
需要使用的插件:
-
@surbowl/world-geo-json-zh :这个第三方包是简体中文 Geo JSON 世界地图,带有国家(地区)的 ISO 3166 代码、中文简称与全称。含中国南海海域十段线。
-
three :这个我就不用说了。
然后下面是具体实现的代码:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import * as TWEEN from 'three/examples/jsm/libs/tween.module.js';
import worldJSON from '@surbowl/world-geo-json-zh'
import earthquakeJSON from '../assets/json/earthquake.json'export default (domId) => {/* ------------------------------初始化三件套--------------------------------- */const dom = document.getElementById(domId);const { innerHeight, innerWidth } = windowconst scene = new THREE.Scene();const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 1, 2000);camera.position.set(0, 0, 10);camera.lookAt(scene.position);const renderer = new THREE.WebGLRenderer({antialias: true,// 抗锯齿alpha: false,// 透明度powerPreference: 'high-performance',// 性能logarithmicDepthBuffer: true,// 深度缓冲})// renderer.setClearColor(0x000000, 0);// 设置背景色// renderer.clear();// 清除渲染器renderer.shadowMap.enabled = true;// 开启阴影renderer.shadowMap.type = THREE.PCFSoftShadowMap;// 阴影类型renderer.outputEncoding = THREE.sRGBEncoding;// 输出编码renderer.toneMapping = THREE.ACESFilmicToneMapping;// 色调映射renderer.toneMappingExposure = 1;// 色调映射曝光renderer.physicallyCorrectLights = true;// 物理正确灯光renderer.setPixelRatio(devicePixelRatio);// 设置像素比renderer.setSize(innerWidth, innerHeight);// 设置渲染器大小dom.appendChild(renderer.domElement);// 重置大小window.addEventListener('resize', () => {const { innerHeight, innerWidth } = windowcamera.aspect = innerWidth / innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth, innerHeight);})/* ------------------------------初始化工具--------------------------------- */const controls = new OrbitControls(camera, renderer.domElement) // 相机轨道控制器controls.enableDamping = true // 是否开启阻尼controls.dampingFactor = 0.05// 阻尼系数controls.panSpeed = -1// 平移速度// const axesHelper = new THREE.AxesHelper(10);// scene.add(axesHelper);/* ------------------------------正题--------------------------------- */// 地图配置const mapOptions = {sphere: null, // 球体bg: 'rgb(10 ,20 ,28)',// 背景色borderColor: 'rgb(10 ,20 ,28)',// 边框颜色blurColor: '#000000',// 模糊颜色borderWidth: 1,// 边框宽度blurWidth: 5,// 模糊宽度fillColor: 'rgb(26, 35, 44)',// 填充颜色barHueStart: 0.7,// 柱体颜色起始值barHueEnd: 0.2,// 柱体颜色结束值barLightStart: 0.1,// 柱体亮度起始值barLightEnd: 1.0// 柱体亮度结束值}// 相机位置const cameraPos = {x: 0.27000767404584447,y: 1.0782003329514755,z: 3.8134631736522793}// 相机控制器位置const controlPos = {x: 0,y: 0,z: 0}// 柱状图信息const barInfo = {barMin: 0.01,barMax: 0.5,currentBarH: 0.01,// 柱体高度min: Number.MAX_SAFE_INTEGER,max: Number.MIN_SAFE_INTEGER,range: 0,mesh: null,// 柱体lonHelper: null, // 经度辅助线latHelper: null, // 纬度辅助线positionHelper: null,originHelper: null,}// 用于绑定整个地球的容器const objGroup = new THREE.Group();scene.add(objGroup);// 绘制地图const drawRegion = (ctx, c, geoInfo) => {ctx.beginPath();c.forEach((item, i) => {let pos = [(item[0] + 180) * 10, (-item[1] + 90) * 10];if (i == 0) {ctx.moveTo(pos[0], pos[1]);} else {ctx.lineTo(pos[0], pos[1]);}});ctx.closePath();ctx.fill();ctx.stroke();}// 创建地球const createMap = () => {const canvas = document.createElement('canvas');canvas.width = 3600;canvas.height = 1800;const ctx = canvas.getContext('2d');ctx.fillStyle = mapOptions.bg;ctx.rect(0, 0, canvas.width, canvas.height);ctx.fill();// 设置地图样式ctx.strokeStyle = mapOptions.borderColor;ctx.lineWidth = mapOptions.borderWidth;ctx.fillStyle = mapOptions.fillColor;if (mapOptions.blurWidth) {ctx.shadowColor = mapOptions.blurColor;ctx.shadowBlur = mapOptions.blurWidth;}// 遍历数据worldJSON.features.forEach(c1 => {// 判断是否为多边形if (c1.geometry.type == 'MultiPolygon') {c1.geometry.coordinates.forEach(c2 => {c2.forEach(c3 => {drawRegion(ctx, c3)})})}else {c1.geometry.coordinates.forEach(c2 => {drawRegion(ctx, c2)})}})const map = new THREE.CanvasTexture(canvas);// 创建纹理贴图map.wrapS = THREE.RepeatWrapping;// 水平方向重复map.wrapT = THREE.RepeatWrapping;// 垂直方向重复const geometry = new THREE.SphereGeometry(1, 32, 32);// 创建球体几何体const material = new THREE.MeshBasicMaterial({map: map,// 纹理贴图transparent: true// 透明});mapOptions.sphere = new THREE.Mesh(geometry, material);mapOptions.sphere.visible = false;// 隐藏地球objGroup.add(mapOptions.sphere);// 添加到场景中}// 创建柱体const createBar = (info, index) => {const amount = (info.mag - barInfo.min) / barInfo.range;// 根据值计算比例const hue = THREE.MathUtils.lerp(mapOptions.barHueStart, mapOptions.barHueEnd, amount);// 根据值计算颜色const saturation = 1;// 饱和度const lightness = THREE.MathUtils.lerp(mapOptions.barLightStart, mapOptions.barLightEnd, amount);// 根据值计算亮度const color = new THREE.Color();color.setHSL(hue, saturation, lightness);// 设置颜色barInfo.mesh.setColorAt(index, color);// 设置颜色barInfo.lonHelper.rotation.y = THREE.MathUtils.degToRad(info.lon) + Math.PI * 0.5;barInfo.latHelper.rotation.x = THREE.MathUtils.degToRad(-info.lat);barInfo.positionHelper.updateWorldMatrix(true, false);let h = THREE.MathUtils.lerp(0.01, 0.5, amount);barInfo.positionHelper.scale.set(0.01, 0.01, h <= barInfo.currentBarH ? h : barInfo.currentBarH);barInfo.originHelper.updateWorldMatrix(true, false);barInfo.mesh.setMatrixAt(index, barInfo.originHelper.matrixWorld);}// 创建柱体群const createBars = (list) => {list.forEach((info, index) => {createBar(info, index)})barInfo.mesh.instanceColor.needsUpdate = true;barInfo.mesh.instanceMatrix.needsUpdate = true;}// 创建全部柱体const createAllBars = () => {// 辅助对象barInfo.lonHelper = new THREE.Object3D();// 经度辅助对象scene.add(barInfo.lonHelper);barInfo.latHelper = new THREE.Object3D();// 纬度辅助对象barInfo.lonHelper.add(barInfo.latHelper);barInfo.positionHelper = new THREE.Object3D();// 位置辅助对象barInfo.positionHelper.position.z = 1;barInfo.latHelper.add(barInfo.positionHelper);barInfo.originHelper = new THREE.Object3D();// 原点barInfo.originHelper.position.z = 0.5;barInfo.positionHelper.add(barInfo.originHelper);const boxGeometry = new THREE.BoxGeometry(1, 1, 1);const boxMaterial = new THREE.MeshBasicMaterial({ color: '#FFFFFF' });earthquakeJSON.forEach(c1 => {if (barInfo.min > c1.mag) {barInfo.min = c1.mag;}if (barInfo.max < c1.mag) {barInfo.max = c1.mag;}})barInfo.range = barInfo.max - barInfo.min;barInfo.mesh = new THREE.InstancedMesh(boxGeometry, boxMaterial, earthquakeJSON.length);objGroup.add(barInfo.mesh);createBars(earthquakeJSON);objGroup.scale.set(0.1, 0.1, 0.1)mapOptions.sphere.visible = true;// 显示地球}// 播放动画const play = () => {const orgCamera = camera.position;const orgControl = controls.target;const tween = new TWEEN.Tween({scale: 0.1,rotate: 0,cameraX: orgCamera.x,cameraY: orgCamera.y,cameraZ: orgCamera.z,controlsX: orgControl.x,controlsY: orgControl.y,controlsZ: orgControl.z}).to({scale: 1,rotate: Math.PI,cameraX: cameraPos.x,cameraY: cameraPos.y,cameraZ: cameraPos.z,controlsX: controlPos.x,controlsY: controlPos.y,controlsZ: controlPos.z}, 2000).easing(TWEEN.Easing.Quadratic.Out).onUpdate((obj) => {objGroup.scale.set(obj.scale, obj.scale, obj.scale);objGroup.rotation.y = obj.rotate;camera.position.set(obj.cameraX, obj.cameraY, obj.cameraZ);controls.target.set(obj.controlsX, obj.controlsY, obj.controlsZ);}).chain(new TWEEN.Tween({ h: barInfo.barMin }).to({ h: barInfo.barMax }, 2000).easing(TWEEN.Easing.Quadratic.Out).onUpdate((obj) => {barInfo.currentBarH = obj.h;createBars(earthquakeJSON);})).start();TWEEN.add(tween);}// 初始化const init = () => {camera.near = 0.1;// 相机最近距离camera.updateProjectionMatrix();// 更新相机投影矩阵createMap();createAllBars();play();}init();/* ------------------------------动画函数--------------------------------- */const animation = () => {TWEEN.update();renderer.render(scene, camera);controls.update();requestAnimationFrame(animation);}animation();
}
在引入的地方有使用到一个文件 earthquake.json我也一块放在资源里面了。
最后在vue的onMounted生命周期里面调用就好了。