Skip to content

182. 地图飞线

Published:

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

image.png

08e087c594d4f3b046bbf7fc3a94e2bf.png

9cb00b3e43736b8c367d8388fbc29fc7.png

如何实现这种效果呢?

地图绘制我们学了,曲线的绘制我们也学了,其实组合一下就可以实现这种飞线效果。

我们来试一下:

npx create-vite map-flyline

image.png

创建项目,进入项目,安装依赖:

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

image.png

2025-08-17 12.17.39.gif

我们画一条从北京到上海的飞线,怎么画呢?

用样条曲线 SplineCurve 的的话,至少需要三个点:

起点、终点、中间穿过的点

起点和终点就是北京、上海的经纬度转换的坐标。

我们先随便找一个中间的点画一下:

首先把 geojson 的结构转换一下:

image.png

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

image.png

const cityCenterMap = new Map();
geojson.features.forEach(feature => {
    cityCenterMap.set(feature.properties.name, feature.properties.center);
})
console.log(cityCenterMap);

转换后的结构是这样的:

image.png

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

image.png

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 就是负的:

image.png

从曲线取点,用 BufferGeometry + Line 画出来。

看下效果:

2025-08-17 12.51.07.gif

现在就是一条从北京到上海的线了。

但还缺少中间一个点

如何计算这个点呢?

很简单,两点坐标的和除以 2

image.png

const middle = start.clone().add(end).divideScalar(2);
middle.z = 100;

const curve = new THREE.CatmullRomCurve3([
    start,
    middle,
    end
]);

用 Vector3 自带的 add、divideScalar 来计算。

2025-08-17 13.00.07.gif

这样,曲线就画出来了。

只不过有点细,我们换成 LineGeometry 设置下 lineWidth

image.png

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);

2025-08-17 13.02.20.gif

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

9cb00b3e43736b8c367d8388fbc29fc7.png

下节我们继续完善

案例代码上传了小册仓库

总结

这节我们画了下地图飞线。

用 CatmullRomCurve3 三维样条曲线画了穿过三个点的曲线。

起始点的坐标通过经纬度转换得到,中间的点就是起始点的坐标的和除于二。

我们用 LineGeometry + Line2 画的线,这样可以设置 lineWidth

现在只是画出了基本的曲线,下节我们继续完善飞线效果。

评论