背景
目前项目的中国地图是echarts画的,现在这想再次基础上增加一个中国地图描边动画。
分析
因为echart 使用geo 坐标画上去的,我们可以根绝中国地图坐标画点,然后定时去移动这些点。
这里使用threejs 的点材质去帧动画移动。
geojson版本
threejs 基础场景不过多介绍,具体看代码,只写下核心部分。
步骤:
- 中国地图轮廓geojson 获取点坐标。(百度和阿里都有提供,可以自己搜很多。)
- 使用卡墨托投影方法,将经纬坐标转成平面
- 根绝点轮廓图采样出亮点
- 控制亮点亮度和移动
核心代码:
代码语言:javascript复制import {
BufferGeometry,
Object3D,
FileLoader,
BufferAttribute,
ShaderMaterial,
Color,
Points,
LineBasicMaterial,
Line,
Vector3,
ColorRepresentation
} from "three";
import * as d3 from "d3-geo";
import coordinates from "./data/china";
const projection = d3
.geoMercator()
.center([116.412318, 39.909843])
.translate([0, 0]);
const linePinots:any[] = [];
const countryLine = (tintColor:ColorRepresentation,outLineColor?:ColorRepresentation)=>{
let positions:Float32Array;
let opacitys:Float32Array;
const opacityGeometry = new BufferGeometry();
// 中国边界
const chinaLines = new Object3D();
// 点数据
coordinates.forEach((coordinate:any) => {
// coordinate 多边形数据
coordinate.forEach((rows:any) => {
const line = lineDraw(rows, outLineColor ?? '0xffffff');
chinaLines.add(line);
});
});
positions = new Float32Array(linePinots.flat(1));
// 设置顶点
opacityGeometry.setAttribute("position", new BufferAttribute(positions, 3));
// 设置 粒子透明度为 0
opacitys = new Float32Array(positions.length).map(() => 0);
opacityGeometry.setAttribute("aOpacity", new BufferAttribute(opacitys, 1));
// 控制 颜色和粒子大小
const params = {
pointSize: 2.0,
pointColor: tintColor
}
const vertexShader = `
attribute float aOpacity;
uniform float uSize;
varying float vOpacity;
void main(){
gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0);
gl_PointSize = uSize;
vOpacity=aOpacity;
}
`
const fragmentShader = `
varying float vOpacity;
uniform vec3 uColor;
float invert(float n){
return 1.-n;
}
void main(){
if(vOpacity <=0.2){
discard;
}
vec2 uv=vec2(gl_PointCoord.x,invert(gl_PointCoord.y));
vec2 cUv=2.*uv-1.;
vec4 color=vec4(1./length(cUv));
color*=vOpacity;
color.rgb*=uColor;
gl_FragColor=color;
}
`
const material = new ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true, // 设置透明
uniforms: {
uSize: {
value: params.pointSize
},
uColor: {
value: new Color(params.pointColor)
}
}
})
const opacityPoints = new Points(opacityGeometry, material)
return {
chinaLines,
opacityPoints,
opacitys,
linePinots,
opacityGeometry
}
}
// let indexBol = true
/**
* 边框 图形绘制
* @param polygon 多边形 点数组
* @param color 材质颜色
* */
function lineDraw(polygon:any, color:ColorRepresentation) {
const lineGeometry = new BufferGeometry();
const pointsArray = new Array();
polygon.forEach((row:any) => {
const projectionRow = projection(row);
if (!projectionRow) {
return
}
let x = projectionRow[0]
let y = projectionRow[1]
// 创建三维点
pointsArray.push(new Vector3(x, -y, 0));
linePinots.push([x, -y, 0]);
});
// 放入多个点
lineGeometry.setFromPoints(pointsArray);
const lineMaterial = new LineBasicMaterial({
color: color,
});
return new Line(lineGeometry, lineMaterial);
}
export { countryLine };
全部代码: demo
遇到问题
geojson 版本我们需要将提供的经纬度坐标点转场成平面,各平台算法不同,投影失真情况不同,所以一些情况地图会失真无法重合。
我们刚才使用卡墨托投影转换,也会失真并且和echarts 地图轮廓对不上,所以想起其他方案。
我们利用svg路径来取点,UI提供的svg地图轮廓肯定是一致的。
SVG版本
设计思路:
- 加载svg 取所有的点
- 根绝点来创建threejs 亮光点
- 移动动画
核心代码:
代码语言:javascript复制import * as THREE from 'three';
import { initRender } from './render';
import { initScene } from './scene';
import { initCamera } from './camera';
// import { countryLine } from "./countryPolygon";
import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js';
export interface OutLineConfig {
outline: boolean;
outlineColor?: THREE.ColorRepresentation;
tintColor: THREE.ColorRepresentation;
speed: number;
tintLength?: number;
tintPointSize?: number;
}
class MapOutline {
private parentDom: HTMLElement;
private width: number;
private height: number;
private renderer: THREE.WebGLRenderer;
private scene: THREE.Scene;
private camera: THREE.PerspectiveCamera;
private opacitys: Float32Array | null = null;
private linePinots: any[] = [];
private opacityGeometry: THREE.BufferGeometry | null = null;
private currentPos = 0;
public constructor(
containerId: string,
public config: OutLineConfig = { outline: false, speed: 3, tintColor: '#008792' },
) {
this.parentDom = document.getElementById(containerId)!;
this.width = this.parentDom.offsetWidth;
this.height = this.parentDom.offsetHeight;
this.renderer = initRender(this.width, this.height);
this.parentDom?.appendChild(this.renderer.domElement);
this.scene = initScene();
this.camera = initCamera(this.width, this.height);
}
public render = () => {
const loader = new SVGLoader();
loader.load('./chinaLine.svg', (data) => {
const paths = data.paths;
const group = new THREE.Group();
group.scale.multiplyScalar(0.34);
group.position.x = -117;
group.position.y = 90;
group.scale.y *= -1;
let allPoints: any[] = [];
let pointsMesh: THREE.Points | null = null;
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < paths.length; i ) {
const path = paths[i];
const strokeColor = path.userData?.style.stroke;
const material = new THREE.MeshBasicMaterial({
color: 'red',
opacity: path.userData?.style.strokeOpacity,
transparent: path.userData?.style.strokeOpacity < 1,
side: THREE.DoubleSide,
depthWrite: false,
});
for (let j = 0, jl = path.subPaths.length; j < jl; j ) {
const subPath = path.subPaths[j];
let subPoints = subPath.getPoints();
allPoints = allPoints.concat(subPoints);
const geometry = SVGLoader.pointsToStroke(subPath.getPoints(), path.userData?.style);
if (geometry) {
const mesh = new THREE.Mesh(geometry, material);
group.add(mesh);
}
}
allPoints = allPoints.map((item) => {
item.z = 0;
return item;
});
for (const point of allPoints) {
this.linePinots.push(point.x * 0.34 - 117, -point.y * 0.34 90, point.z);
}
this.opacityGeometry = new THREE.BufferGeometry();
this.opacitys = new Float32Array(this.linePinots.length).map(() => 0);
this.opacityGeometry.setAttribute('position', new THREE.Float32BufferAttribute(this.linePinots, 3));
this.opacityGeometry.setAttribute('aOpacity', new THREE.BufferAttribute(this.opacitys, 1));
// 控制 颜色和粒子大小
const params = {
pointSize: 5.0,
pointColor: 'DarkOrange',
};
const vertexShader = `
attribute float aOpacity;
uniform float uSize;
varying float vOpacity;
void main(){
gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0);
gl_PointSize = uSize;
vOpacity=aOpacity;
}
`;
const fragmentShader = `
varying float vOpacity;
uniform vec3 uColor;
float invert(float n){
return 1.-n;
}
void main(){
if(vOpacity <=0.2){
discard;
}
vec2 uv=vec2(gl_PointCoord.x,invert(gl_PointCoord.y));
vec2 cUv=2.*uv-1.;
vec4 color=vec4(1./length(cUv));
color*=vOpacity;
color.rgb*=uColor;
gl_FragColor=color;
}
`;
const material = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true, // 设置透明
uniforms: {
uSize: {
value: params.pointSize,
},
uColor: {
value: new THREE.Color(params.pointColor),
},
},
});
let opacityPoints = new THREE.Points(this.opacityGeometry, material);
this.scene.add(opacityPoints);
// this.scene.add(group);
});
this.animate();
};
private animate = () => {
if (this.linePinots && this.opacitys) {
// console.log(this.currentPos);
if (this.currentPos > 1600) {
this.currentPos = 0;
}
this.currentPos = this.config.speed;
for (let i = 0; i < this.config.speed; i ) {
this.opacitys[(this.currentPos - i) % this.linePinots.length] = 0;
}
for (let i = 0; i < 100; i ) {
// console.log((this.currentPos i) % this.linePinots.length);
this.opacitys[(this.currentPos i) % this.linePinots.length] = i / 50 > 2 ? 2 : i / 50;
}
if (this.opacityGeometry) {
this.opacityGeometry.attributes.aOpacity.needsUpdate = true;
}
}
this.renderer.render(this.scene, this.camera);
requestAnimationFrame(this.animate);
};
}
export default MapOutline;