基于threejs实现中国地图轮廓动画

2022-05-06 17:24:27 浏览数 (1)

背景


目前项目的中国地图是echarts画的,现在这想再次基础上增加一个中国地图描边动画。

分析


因为echart 使用geo 坐标画上去的,我们可以根绝中国地图坐标画点,然后定时去移动这些点。

这里使用threejs 的点材质去帧动画移动。

geojson版本


threejs 基础场景不过多介绍,具体看代码,只写下核心部分。

步骤:

  1. 中国地图轮廓geojson 获取点坐标。(百度和阿里都有提供,可以自己搜很多。)
  2. 使用卡墨托投影方法,将经纬坐标转成平面
  3. 根绝点轮廓图采样出亮点
  4. 控制亮点亮度和移动

核心代码:

代码语言: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版本


设计思路:

  1. 加载svg 取所有的点
  2. 根绝点来创建threejs 亮光点
  3. 移动动画

核心代码:

代码语言: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;
svg

0 人点赞