Skip to content

181. 实战:全国人口柱状地图(三)

Published:

上节美化了一下地图:

2025-08-09 17.22.47.gif

这节我们继续来加功能。

首先要把人口的数量标注出来。

这个标注可以用 SpriteText,也可以用 CSS3DObject 来做。

这里我们还是用 SpriteText。

安装下:

pnpm install --save three-spritetext

在柱子上加一个标注:

image.png

const numText = new SpriteText(num + '万', 8);
numText.color = 'black';
numText.backgroundColor = 'white';
numText.strokeWidth = 0.5;
numText.strokeColor = 'blue';
numText.borderRadius = 5;
numText.borderWidth = 1;
numText.borderColor = '#000';
numText.padding = 2;
numText.position.set(0, 0, num/50/2  + 10);
bar.add(numText);

位置在柱子的高度上方 10 的位置。

看下效果:

2025-08-09 17.47.28.gif

我们给柱子加一个出现的动画,高度从 0 到现在的高度。

用 gsap 来做:

pnpm install --save gsap

image.png

bar.position.z = -num/50/2;
gsap.to(bar.position, {
    z: num/50/2,
    ease: 'bounce.out',
    duration: 2
});

加一个出现时的动画,先把柱子放到地图下面,然后运动到地图上面。

这里改 scale.z 也可以。

看下效果:

2025-08-09 18.58.33.gif

然后把省的名字也标出来:

image.png

const nameText = new SpriteText(feature.properties.name, 10);
nameText.color = 'white';
nameText.strokeWidth = 3;
nameText.strokeColor = 'black';
nameText.position.set(10, -10, -num/50/2 + 10);
bar.add(nameText);

2025-08-09 19.14.37.gif

这个名字不需要跟着做动画,等动画完再显示就行。

image.png

nameText.visible = false;

bar.position.z = -num/50/2;
gsap.to(bar.position, {
    z: num/50/2,
    ease: 'bounce.out',
    duration: 2,
    onComplete() {
        nameText.visible = true;
    }
});

2025-08-09 19.33.03.gif

这样,整体功能就差不多了。

去掉 AxesHelper,然后我们再加一些底部的装饰:

比如这种圆盘:

image.png

首先加一下背景:

image.png

你可以从仓库里找到这张图片

然后用 PlaneGeometry 来画一下:

image.png

const geometry = new THREE.PlaneGeometry(3000, 3000);

const loader = new THREE.TextureLoader();
const grid = loader.load('./grid.png');
grid.colorSpace = THREE.SRGBColorSpace;
grid.wrapS = grid.wrapT = THREE.RepeatWrapping;
grid.repeat.set(20, 20);

const material = new THREE.MeshPhongMaterial({
  map: grid,
  transparent: true,
  opacity: 0.15
});
const plane = new THREE.Mesh(geometry, material);
plane.rotateX(-Math.PI / 2);
plane.position.y = -11;
scene.add(plane);

加一个 PlaneGeometry,重复设置这张图片,透明度设置低一点。

2025-08-09 19.33.03.gif

然后是圆盘:

image.png

这个其实用图片是最简单的,没有图片就自己画。

我们自己用曲线 API 画一下:

image.png

const curve1 = new THREE.EllipseCurve(0, 0, 200 , 200, 0, Math.PI * 2);

const pointsArr = curve1.getPoints(50);
const geometry2 = new THREE.BufferGeometry();
geometry2.setFromPoints(pointsArr);

const material2 = new THREE.LineBasicMaterial({
    color: new THREE.Color('white')
});

const line = new THREE.Line(geometry2, material2);
line.rotateX(-Math.PI / 2);
line.position.y = -10;
scene.add(line);

画一条曲线,取点设置到 BufferGeometry,然后用 Line 画出来。

image.png

加个循环,多画几条:

image.png

循环设置不同的半径和透明度

for(let i = 0; i < 10;i ++) {
  const R = 100 + i * 50;
  const curve1 = new THREE.EllipseCurve(0, 0, R, R, 0, Math.PI * 2);

  const pointsArr = curve1.getPoints(50);
  const geometry2 = new THREE.BufferGeometry();
  geometry2.setFromPoints(pointsArr);

  const material2 = new THREE.LineBasicMaterial({
      color: new THREE.Color('white'),
      transparent: true,
      opacity: 0.8 - i * 0.05
  });

  const line = new THREE.Line(geometry2, material2);
  line.rotateX(-Math.PI / 2);
  line.position.y = -10;
  scene.add(line);
}

看下效果:

2025-08-09 20.12.40.gif

还可以画外面这种:

image.png

用 LineSegments 就行

BufferGeometry 存一堆点,然后用 LineSegments 两两连接

image.png

大概这样,先计算小一圈的 R2,

然后角度从 0 到 360 度,分别计算 cos、sin,的两个值

for(let i = 0; i < 10;i ++) {
  const R = 100 + i * 50;

  if(i % 2 === 0) {
    const curve1 = new THREE.EllipseCurve(0, 0, R, R, 0, Math.PI * 2);
  
    const pointsArr = curve1.getPoints(50);
    const geometry2 = new THREE.BufferGeometry();
    geometry2.setFromPoints(pointsArr);
  
    const material2 = new THREE.LineBasicMaterial({
        color: new THREE.Color('white'),
        transparent: true,
        opacity: 0.8 - i * 0.05
    });
  
    const line = new THREE.Line(geometry2, material2);
    line.rotateX(-Math.PI / 2);
    line.position.y = -10;
    scene.add(line);
  } else {
    const geometry2 = new THREE.BufferGeometry();

    const R2 = R - 10;
    const pointsArr =  [];

    for(let angle = 0; angle <= Math.PI * 2; angle += Math.PI  / 100) {
      pointsArr.push(
        new THREE.Vector3(R * Math.cos(angle), R * Math.sin(angle), 0),
        new THREE.Vector3(R2 * Math.cos(angle), R2 * Math.sin(angle), 0),
      )
    }

    geometry2.setFromPoints(pointsArr);
  
    const material2 = new THREE.LineBasicMaterial({
        color: new THREE.Color('white'),
        transparent: true,
        opacity: 0.8 - i * 0.05
    });
  
    const line = new THREE.LineSegments(geometry2, material2);
    line.rotateX(-Math.PI / 2);
    line.position.y = -10;
    scene.add(line);
  }
  
}

看下效果:

2025-08-09 20.33.23.gif

这样就好看多了。

案例代码上传了小册仓库

总结

这节我们给地图加上了人口和省市名的标注,用 SpriteText 来展示文字。

然后用 gsap 做了出现时的动画。

然后我们加了一层 PlaneGeometry 画背景,并且用 Line、LineSegments 配合 BufferGeometry 来画了一圈圈的装饰线。

至此,我们的全国人口柱状图就完成了。

评论