# 一、绘制地球 关于绘制地球,一开始是想找一张地球图片当做纹理绘制在球体上,之后想做用户通过鼠标选中国家并产生高光,想让纹理的一部分发光想不到怎么做,后面发现了 geojson。可以获取地球的 geojson 数据绘制一个地球。可以使用 echarts 绘制一个地球,然后使用 canvasTexture 贴在球上。至于如何选中国家,可以让用户的鼠标射出一条射线,然后与球体做碰撞检测,获取碰撞点,将点的空间坐标转化为经纬度,然后根据经纬度在 echarts 中获取对应的国家,在 echarts 中高亮,然后监听 echarts 渲染完成事件,再将地球的纹理换成 echarts 渲染完成的图片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import mapData from './world.json' const canvas = document.getElementById('world') const chart = echarts.init(canvas) echarts.registerMap('world',{geoJSON:mapData}) const option ={ geo:[{ type: 'map', map: 'world', }] } chart.setOption(option) this.object.material.map = new THREE.CanvasTexture(canvas) this.object.material.needsUpdate = true //如果不加这个就没有效果,我猜是echarts是先生成canvas,然后再绘制,绘制是异步的,此时获取的canvas没有数据,所以需要这个设置为true this.renderer.render(this.scene, this.camera)
但是绘制效果不尽如人意。后面再了解到了 echarts 在 5.3 版本后支持不同的投影方式,geojson 数据是经纬度,可以通过不同的转化绘制出不同形状的地图。其中有一种投影方式是 geoOrthographic。也就是将地球投影到一个平面的方法。假设使投影面与地球相切,然后将地球投影到平面上,d3-geo 提供了不同的投影方式。
1 2 3 4 5 6 7 8 9 10 11 12 import { geoOrthographic } from 'd3-geo' const projection = geoOrthographic() const option ={ geo:[{ type: 'map', map: 'world', projection: { project: (point) =>projection(point), unproject: (point) => projection.invert(point) } }] }
效果不错,或许不需要球。毕竟用户最终看到的就是圆面。好消息是成功将地球可视化中移除了地球。接下来就是如何让这个平面旋转了。
# 二、让地球旋转 上面的地球还有一个问题就是国家与国家间会连在一起。
1 2 3 4 5 6 7 8 9 geo:[{ type: 'map', map: 'world', projection: { project: (point) =>projection(point), unproject: (point) => projection.invert(point), stream:projection.stream //估计是上面那个是把所有国家都投影了,但其实能看到的之后的国家范围在180经度。 } }]
1 2 3 4 5 6 7 8 9 10 function changerotate(){ let [x,y,z] = projection.rotate() x+=1 y+=2 z+=3 projection.rotate([x,y,z]) chart.setOption(option) requestAnimationFrame(changerotate) } requestAnimationFrame(changerotate)
分别测试让地球绕着 xyz 轴旋转,结果发现这个 xy 轴与一般认为的 xy 轴不一样,反过来了。接下来就是监听用户鼠标变化左右移动就是旋转 x 轴,上下移动就是旋转 y 轴。然后在调用 echarts 的 setOption 时出现了问题。 setOption should not be called during main process. 这个函数无法在主进程中调用。大概是渲染没结束就继续渲染,然后出错了。可以用 requestAnimation 或者 echarts 提供了一个渲染完成后的事件,在渲染完成后再继续渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 startRotateChange(e){ this.x = e.clientX this.y = e.clientY if(this.earthRotateChange) return this.earthRotateChange = true }, rotateChange(e){ if(!this.earthRotateChange) return this.rotateX = e.clientX this.rotateY = e.clientY if(!this.rotateAnimation) requestAnimationFrame(this.changerotate) this.rotateAnimation = true }, stopRotateChange(){ if(!this.earthRotateChange) return this.earthRotateChange = false this.rotateAnimation = false cancelAnimationFrame(this.animationId) }, changerotate(){ let [x,y,z] = this.projection.rotate() let dx = this.rotateX - this.x //鼠标移动后的x坐标 let dy = this.rotateY - this.y //鼠标移动后的y坐标 this.x = this.rotateX this.y = this.rotateY x +=dx y -=dy this.projection.rotate([x,y,z]) this.chart.setOption(this.option) this.animationId = requestAnimationFrame(this.changerotate) }
# 三、给地球上颜色 给陆地加上陆地的颜色,给海洋加上海洋的颜色。陆地还好说,直接给绘制的区域加上颜色即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 geo:[{ type: 'map', map: 'world', projection: { project: (point) =>projection(point), unproject: (point) => projection.invert(point), stream:projection.stream }, nameMap:{ 'China':'中国' }, itemStyle:{ color:'#c26e00' } }]
至于海洋吗,地球除了陆地就是海洋,将 canvas 画布变成原型,然后添加一个海洋色的背景颜色即可。
添加的时候发现这个绘制的区域怎么比实际的区域小这么多。使用 zoom 放大绘制区域发现当 zoom 等于 1.25 时,绘制区域与画布一样大。1.25,让我想起了半年前做截图功能的时候遇到了 window.devicePixelRatio,我显示器的 dpr 正好就是 1.25 难怪。简单的讲画布展示的像素是绘制的 1.25 倍,所以最后展示的区域便是绘制的 1.25 倍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const devicePixelRatio = window.devicePixelRatio const option ={ backgroundColor:'#4f42b5', geo:[{ type: 'map', map: 'world', projection: { project: (point) =>projection(point), unproject: (point) => projection.invert(point), stream:projection.stream }, nameMap:{ 'China':'中国' }, itemStyle:{ color:'#c26e00' }, zoom:devicePixelRatio }] }