有的时候我们需要在地图上画一些飞线,类似这样:



如何实现这种效果呢?
地图绘制我们学了,曲线的绘制我们也学了,其实组合一下就可以实现这种飞线效果。
我们来试一下:
npx create-vite map-flyline

创建项目,进入项目,安装依赖:
pnpm install
pnpm install --save three
pnpm install --save-dev @types/three
改下 src/main.js
import './style.css';
import * as THREE from 'three';
import {
OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mesh from './mesh.js';
const scene = new THREE.Scene();
scene.add(mesh);
const light = new THREE.DirectionalLight(0xffffff);
light.position.set(500, 300, 600);
scene.add(light);
const light2 = new THREE.AmbientLight();
scene.add(light2);
const axesHelper = new THREE.AxesHelper(1000);
scene.add(axesHelper);
const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(60, width / height, 1, 10000);
camera.position.set(0, 200, 600);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(width, height)
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
document.body.append(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
创建 Scene、Light、Camera、Renderer
改下 style.css
body {
margin: 0;
}
然后创建 mesh.js
import * as THREE from 'three';
import { geoMercator } from 'd3-geo';
const chinaMap = new THREE.Group();
const mercator = geoMercator()
.center([105,34]).translate([0, 0]).scale(600)
const loader = new THREE.FileLoader();
loader.load('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json', function (data) {
const geojson = JSON.parse(data);
console.log(geojson);
geojson.features.forEach(feature => {
const province = new THREE.Group();
if (feature.geometry.type === 'Polygon') {
const polygon = createPolygon(feature.geometry.coordinates);
province.add(polygon);
} else if (feature.geometry.type === 'MultiPolygon') {
feature.geometry.coordinates.forEach(polygonCoords => {
const polygon = createPolygon(polygonCoords);
province.add(polygon);
});
}
chinaMap.add(province);
});
});
function createPolygon(coordinates) {
const group = new THREE.Group();
coordinates.forEach(item => {
const bufferGeometry = new THREE.BufferGeometry();
const vertices = [];
item.forEach(point => {
const [x, y] = mercator(point);
vertices.push(x, -y, 0);
});
const attribute = new THREE.Float32BufferAttribute(vertices, 3);;
bufferGeometry.attributes.position = attribute;
const lineMaterial = new THREE.LineBasicMaterial({
color: 'white'
});
const line = new THREE.Line(bufferGeometry, lineMaterial);
group.add(line);
});
return group;
}
export default chinaMap;
用 Line + BufferGeometry 把中国地图轮廓画出来。
安装下 d3-geo
pnpm install --save d3-geo
pnpm install --save-dev @types/d3-geo
跑一下:
npm run dev


我们画一条从北京到上海的飞线,怎么画呢?
用样条曲线 SplineCurve 的的话,至少需要三个点:
起点、终点、中间穿过的点
起点和终点就是北京、上海的经纬度转换的坐标。
我们先随便找一个中间的点画一下:
首先把 geojson 的结构转换一下:

key 为城市名,value 为中心点坐标:

const cityCenterMap = new Map();
geojson.features.forEach(feature => {
cityCenterMap.set(feature.properties.name, feature.properties.center);
})
console.log(cityCenterMap);
转换后的结构是这样的:

然后从中取出北京市和上海市的经纬度来画线:

const beijingPos = mercator(cityCenterMap.get('北京市'));
const shanghaiPos = mercator(cityCenterMap.get('上海市'));
const start = new THREE.Vector3( beijingPos[0], -beijingPos[1], 0 );
const end = new THREE.Vector3( shanghaiPos[0], -shanghaiPos[1], 0 );
const curve = new THREE.CatmullRomCurve3([
start,
end
]);
const pointsArr = curve.getPoints(20);
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(pointsArr);
const material = new THREE.LineBasicMaterial({
color: new THREE.Color('orange')
});
const line = new THREE.Line( geometry, material );
chinaMap.add(line);
拿到北京、上海的经纬度,用墨卡托投影转换成平面坐标。
用三维样条曲线 CatmullRomCurve3 画穿过两点的曲线。
这里要注意 y 的坐标是负的,因为画地图的时候 y 就是负的:

从曲线取点,用 BufferGeometry + Line 画出来。
看下效果:

现在就是一条从北京到上海的线了。
但还缺少中间一个点
如何计算这个点呢?
很简单,两点坐标的和除以 2

const middle = start.clone().add(end).divideScalar(2);
middle.z = 100;
const curve = new THREE.CatmullRomCurve3([
start,
middle,
end
]);
用 Vector3 自带的 add、divideScalar 来计算。

这样,曲线就画出来了。
只不过有点细,我们换成 LineGeometry 设置下 lineWidth

const geometry = new LineGeometry();
geometry.setFromPoints(pointsArr);
const material = new LineMaterial({
color: new THREE.Color('orange'),
linewidth: 3
});
const line = new Line2( geometry, material );
chinaMap.add(line);

现在两点之间的飞线画出来了,但离最终的目标还有点差距:

下节我们继续完善
案例代码上传了小册仓库
总结
这节我们画了下地图飞线。
用 CatmullRomCurve3 三维样条曲线画了穿过三个点的曲线。
起始点的坐标通过经纬度转换得到,中间的点就是起始点的坐标的和除于二。
我们用 LineGeometry + Line2 画的线,这样可以设置 lineWidth
现在只是画出了基本的曲线,下节我们继续完善飞线效果。