前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于threejs实现中国地图轮廓动画

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

作者头像
星宇大前端
发布2022-05-06 17:24:27
3K0
发布2022-05-06 17:24:27
举报
文章被收录于专栏:大宇笔记
请添加图片描述
请添加图片描述

背景


目前项目的中国地图是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;
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-04-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 分析
  • geojson版本
  • 遇到问题
  • SVG版本
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档