Three_3D_Map 中国多个省份的组合边界绘制,填充背景

需求

实现相邻省份组合边界线绘制

  • 基础:全国各地的geojson文件、中国边境线geojson文件
  • 多相邻省份组合需要用到turf这个插件,核心实现
  • 同组需要遍历好每一份数据(geojson的省份可能包含岛屿,边界线绘制不包含岛屿,因为是不相干的,无法连接)

填充对应的背景

  • 绘制底图层级需要对ExtrudeGeometry计算包围盒,不然背景无法正确安置
  • 指定每个组或者全局的图片背景的话,如果图片是复杂图片,需要正确安放(大小,角度)(本文章内不考虑这个需求,实际实现用的复杂渐变)
  • 指定每个组或者全局的背景为实色(本文章内不考虑这个需求,过于简单,自行AI)
  • 指定每个组或者全局的背景为复杂渐变,核心实现是通过canvas控制大小角度,以及颜色渐变的处理

实现(实现仅描述对应逻辑的实现,目前不会有完整实现案例)

  • 以函数中returnlayer可用的meshGroup为准,在layer中使用

省份组描述

  • 每个组内包含adcode数组,包含相邻省份的adcode(geojson内)
  • map是如果要实现每个组的不同地图背景,就开放这个参数,到时候遍历内使用
jsx 复制代码
// 省份组(目前只实现南方)(相邻省份为一组)
export const provinceGroups = {
  south: [
    {
      adcodes: [210000, 220000, 230000],
      // map: provinceGroupBg,
    },
    {
      adcodes: [110000, 120000],
    },
    {
      adcodes: [
        530000,
        510000,
        810000,
        820000,
        330000,
        360000,
        450000,
        320000,
        440000,
        410000,
        420000,
        370000,
        310000,
        520000,
        500000,
        340000,
        350000,
        430000,
      ],
      // map: provinceGroupBg,
    },
    {
      adcodes: [710000],
    },
    {
      adcodes: [460000],
    },
  ],
  land: [
    {
      adcodes: [710000],
    },
    {
      adcodes: [460000],
    },
  ],
};

geojson数据结构描述

数据量过大,自行检索获取

省份组json

json 复制代码
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "adcode": 110000,
        "name": "北京",
        "center": [116.405285, 39.904989],
        "centroid": [116.41995, 40.18994],
        "childrenNum": 16,
        "level": "province",
        "parent": { "adcode": 100000 },
        "subFeatureIndex": 0,
        "acroutes": [100000]
      },
      "geometry": {
        "type": "MultiPolygon",
        "coordinates": [
            [
                [
                      [117.348611, 40.581141],
                      [117.389879, 40.561593],
                      [117.429915, 40.576141],
                      [117.412669, 40.605226],
                      [117.467487, 40.649738],
                      [117.467487, 40.649738],
                ]
            ]
        ]
      }
    },
    {
        ...more features
    }
  ]
}

边境线的json跟省份的差不多,只是在coordinates中的内部数组里包含了所有的点位(组合后的可以理解为)

插件相关

turf官网

node 复制代码
npm i @turf/turf @turf/union

实现基础3D地图

  • meshGroup:
jsx 复制代码
  const mapGroup = new THREE.Group();
  • 3D地图绘制主函数
jsx 复制代码
import * as THREE from 'three';
/**
 * 绘制 3D 地图
 * @param {Object} topFaceMaterial 地图表面颜色材质
 * @param {'all' | 'south'} type 全部地区、南方地区(自定义地区)
 * @param {File} basicMap 全局的地图背景图片
 * @param {Boolean} isAnimateSide 是否开启边界(有一定高度后的边界面)的动画
 * @param {Boolean} isPrivinceMap 是否为渲染部分地区的map(type为准)
 * @param {Boolean} isProvinceStorke 是否为渲染部分地区的map(type为准)自定义省份的边界线
 * @returns {Object} mapGroup 3D地图
 */
 
/*
基础层级描述:
config.defaultOptions.depth(基准层级高度) + 0.001 底图背景 (可选)
+ 0.2 省市区分界线
+ 0.4 省名字
+ 0.42 边境滚动线 ( 可选
 */
 
/*
// used:
  await onInit3DMap({
    isAnimateSide: true,
    isStorke: false,
    // basicMap: map3dBasicMap,
    isPrivinceMap: true,
    isProvinceStorke: true,
    type: mapType,
  });
*/
export async function onInit3DMap(params = {}) {
  const {
    isStorke = true,
    basicMap,
    topFaceMaterial = new THREE.MeshBasicMaterial({
      color: new THREE.Color('rgba(3,43,56,0.2)'),
      transparent: true,
      opacity: 1,
    }),
    isAnimateSide,
    type = 'all',
    isPrivinceMap = false,
    isProvinceStorke = false,
  } = params;
  
  // 创建边界的材质
  let sideMaterial = new THREE.MeshBasicMaterial({
    color: 0x07152b,
    transparent: true,
    opacity: 1,
  });

  if (isAnimateSide) {
  // 创建开启动画的材质
    sideMaterial = await createSideMaterial();
  }
  
  // 拿到所有组flat之后的Feature集合(详见下方函数描述)
  const res = await getProvinceGroupPoint(type);
  
  //整个3D地图的组,所有的层级都在这里
  const mapGroup = new THREE.Group();
  mapGroup.position.copy(new THREE.Vector3(0, 0, 0.06));
  const extrudeSettings = {
    depth: config.defaultOptions.depth // 基础层级,自行设定,
    bevelEnabled: true,
    bevelSegments: 1,
    bevelThickness: 0.1,
  };
  
  res.forEach((feature, groupIndex) => {
    let { name, center = [], adcode } = feature.properties;
    //相当于每一个省的地图集合
    const group = new THREE.Group();
    group.name = 'meshGroup' + groupIndex;
    group.userData = {
      index: groupIndex,
      name,
      center,
      centroid: feature.properties.centroid || feature.properties.center,
      adcode,
      childrenNum: feature.properties.childrenNum,
    };
    // 省份的边境线组
    let lineGroup = new THREE.Group();
    lineGroup.name = 'lineGroup' + groupIndex;
    lineGroup.userData.index = groupIndex;
    lineGroup.userData.adcode = adcode;
        
    // 表面材质以及side的材质组合
    let materials = [topFaceMaterial.clone(), sideMaterial];
    
    (['内蒙古'].includes(feature.properties.name)
      ? [feature.geometry.coordinates]
      : feature.geometry.coordinates
    ).forEach((multiPolygon) => {
      multiPolygon.forEach((polygon) => {
        const shape = new THREE.Shape();
        for (let i = 0; i < polygon.length; i++) {
          if (!polygon[i][0] || !polygon[i][1]) {
            return false;
          }
          // 根据geojson的经纬度计算投影点位映射返回的x,y点位
          const [x, y] = geoProjection({
            center: config.defaultOptions.pointCenter,
            args: polygon[i],
          });
          if (i === 0) {
            shape.moveTo(x, -y);
          }
          shape.lineTo(x, -y);
        }
        const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
        // 点位连线绘制完成之后创建3D网格对象
        const mesh = new THREE.Mesh(geometry, materials);
        mesh.userData.depth = config.defaultOptions.depth;
        mesh.userData.name = name;
        mesh.userData.adcode = adcode;
        group.add(mesh);
      });
      
      // 省边界线绘制
      const points = [];
      let line = null;
      // 只拿第一个组的点位绘制,像海南有群岛的情况会有多个组,只拿第一个也就是最大的点位组绘制
      multiPolygon[0].forEach((item) => {
        const [x, y] = geoProjection({ center: config.defaultOptions.pointCenter, args: item });
        points.push(new THREE.Vector3(x, -y, 0));

        const lineMaterial = new THREE.LineBasicMaterial({ color: 0x6cf9f9 });
        line = createLine(points, lineMaterial);
      });
      lineGroup.add(line);
    });
    
    // 边界线的组设定高度,添加到地图组
    lineGroup.position.set(0, 0, config.defaultOptions.depth + 0.2);
    group.add(lineGroup);
    // 基础板块地图绘制完毕
    mapGroup.add(group);
  })
}

这时候绘制出来的就是这么个东西:

箭头处是关于createSideMaterial函数开启之后的纵深影响位置

现在关于地图的合成,核心逻辑还是通过featrue的组合将省份依次拼接的,但是如果要绘制相邻省份组合之后的边界线,亦或者是给组合的这一大块地图设置一个背景色,图片的话,仅靠拼接就不太够了,这时候就需要用到turf这个插件来协助了:

实现相邻省份组合边界线绘制以及国境线绘制

边界线绘制的话,一般会使用new THREE.LineBasicMaterial方法。但是该方法并不支持设置宽度,也就是说宽度恒定为1

那么如何拓宽呢? 最终的实现方案还是需要依赖THREE.Mesh来用细长的矩形伪装成线来实现自定义宽度:

onInit3DMap函数内追加如下内容:

JSX 复制代码
  // 开启国境线绘制
  if (isStorke) {
    const meshs = await createStorke(type);
    mapGroup.add(...meshs);
  }
  // 开启省份边界线绘制
  if (isProvinceStorke) {
    const meshs = await createStorke(type, 'single');
    mapGroup.add(...meshs);
  }

核心实现(这里的全国边境线绘制使用了图片+动画,做出流线型效果):
createStorke

JS 复制代码
/**
 * 
 * @param {'all' | 'south' | '...待追加'} type 全国或者是指定的Group组
 * @param {'total' | 'single'} stokeType 线类型 total是全国边境线动画+图片,single为省份边界线(加粗)
 * @returns Mesh[]
 */
 
async function createStorke(type = 'all', stokeType = 'total') {
  let material;
  // 全局的话
  if (stokeType === 'total') {
    const texture = await config.onLoaderTexture(require('@/Fudge/assets/pathLine2.png'));
    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
    texture.repeat.set(1, 1);
    // 开启动画
    texture.tween1 = new TWEEN.Tween({ y: 0 })
      .to({ y: config.defaultOptions.depth }, 10000)
      .onUpdate((params) => {
        let { y } = params;
        texture.offset.x += 0.003;
      })
      .repeat(Infinity)
      .yoyo();
    texture.tween1.start();

    // 材质创建
    material = new THREE.MeshBasicMaterial({
      color: 0x92e5ff,
      map: texture,
      alphaMap: texture,
      fog: false,
      transparent: true,
      opacity: 1,
      blending: THREE.AdditiveBlending,
    });
  } else if (stokeType === 'single') {
    // 创建基础材质,纯色的
    material = new THREE.MeshBasicMaterial({ color: '#80c5f1' });
  }

  // 点位组记录Ars
  let pathPointGroups = [];
  if (type === 'all') {
    // 直接拿全国边境线geojson
    const res = getProvinceStorkeData();
    let pathPoint = [];
    res.features.forEach((path) => {
      path.geometry.coordinates.forEach((cord) => {
        cord[0].forEach((item) => {
          let [x, y] = geoProjection({ center: config.defaultOptions.pointCenter, args: item });
          pathPoint.push(new THREE.Vector3(x, -y, 0));
        });
      });
    });
    pathPointGroups = [pathPoint];
  } else {
    if (stokeType === 'single') {
      // 拿每一个省份的边界线信息
      const groupsJsonData = getProvinceGroupPoint(type);
      groupsJsonData.forEach((feature) => {
        const pathPoint = [];
        feature.geometry.coordinates[0].forEach((cord) => {
          cord.forEach((item) => {
            let [x, y] = geoProjection({ center: config.defaultOptions.pointCenter, args: item });
            pathPoint.push(new THREE.Vector3(x, -y, 0));
          });
        });
        pathPointGroups.push(pathPoint);
      });
    } else {
      // 获取到合并后的边境线组,会有多个
      const [_, __, points] = getOtherProvincesShape(type);
      pathPointGroups = points;
    }
  }
  
  const meshs = [];
  // 遍历组生成每一份mesh的线信息
  pathPointGroups.map((p) => {
    if (p) {
      const curve = new THREE.CatmullRomCurve3(p);
      const tubeGeometry = new THREE.TubeGeometry(curve, 256 * 10, 0.15, 4, false);
      const mesh = new THREE.Mesh(tubeGeometry, material);
      mesh.position.set(0, 0, config.defaultOptions.depth + 0.42);
      meshs.push(mesh);
    }
  });

  return meshs;
}

出来就是这样子的:

颜色,图片均可自定义。

填充相邻省份组合块的背景

onInit3DMap函数内追加如下内容:

JSX 复制代码
  if (isPrivinceMap) {
    const meshs = await createBasicMap({ mapUrl: false, hasGlobal: false, otherShapeType: type });
    mapGroup.add(...meshs);
  }
createBasicMap函数核心实现:
JSX 复制代码
/**
 * 
 * @param {boolean} hasGlobal 是否为大陆设置背景底图
 * @param {'south'} otherShapeType 指定绘制区域的类型
 * @returns mesh[]
 */
async function createBasicMap(params = {}) {
  const { hasGlobal = true, otherShapeType } = params;
  let shapes = [],
    maps = [];
  if (hasGlobal) {
    const shape = new THREE.Shape();
    const res = getProvinceStorkeData();
    res.features.forEach((path) => {
      path.geometry.coordinates.forEach((cord) => {
        cord[0].forEach((item, index) => {
          let [x, y] = geoProjection({ center: config.defaultOptions.pointCenter, args: item });
          if (index === 0) {
            shape.moveTo(x, -y);
          }
          shape.lineTo(x, -y);
        });
      });
    });
    // 添加海南,台湾岛屿(边界限跟大陆划不在一块儿)
    const [otherShapes] = getOtherProvincesShape('land');
    shapes = [...shapes, shape, ...otherShapes];
  }

  if (otherShapeType) {
    const [otherShapes, otherMaps] = getOtherProvincesShape(otherShapeType);
    shapes = otherShapes;
    maps = otherMaps;
  }

  let meshs = [];

  for (let i = 0; i <= shapes.length - 1; i++) {
    const s = shapes[i];
    const extrudeSettings = getBasicMapGeometrySettings(s);
    const geometry = new THREE.ExtrudeGeometry(s, extrudeSettings);

    let texture;
    // 这里的图片就是从provinceGroups对象内提供的,可选
    if (maps[i]) {
      texture = await config.onLoaderTexture(maps[i]);
      texture.wrapS = THREE.RepeatWrapping;
      texture.wrapT = THREE.RepeatWrapping;
      texture.repeat.set(0.3, 0.7); // 缩小尺寸,自行设置
    } else {
      // 渐变色纹理
      const canvas = getCanvasColorBg();
      texture = new THREE.CanvasTexture(canvas);
    }
    // 声明纹理为 sRGB 颜色空间 重要!
    texture.encoding = THREE.sRGBEncoding;
    const material = new THREE.MeshBasicMaterial({
      map: texture,
      side: THREE.DoubleSide,
      transparent: true,
      opacity: 1,
    });

    const m = new THREE.Mesh(geometry, material);
    m.renderOrder = 999;
    m.position.z = config.defaultOptions.depth + 0.001;

    meshs.push(m);
  }

  return meshs;
}

实现之后的效果见下图(为了展示效果把先前的实现先藏起来):

可以看到是有五个组分别进行了颜色的设置,provinceGroups对象对应

相关引入函数,方法

provinceGroups对象主要用于整合相邻省份组的信息,map的信息也是在这里进行添加
JSX 复制代码
// 省份组(目前只有南方)(相邻省份为一组)
export const provinceGroups = {
  south: [
    {
      adcodes: [210000, 220000, 230000],
      // map: provinceGroupBg,
    },
    {
      adcodes: [110000, 120000],
    },
    {
      adcodes: [
        530000,
        510000,
        810000,
        820000,
        330000,
        360000,
        450000,
        320000,
        440000,
        410000,
        420000,
        370000,
        310000,
        520000,
        500000,
        340000,
        350000,
        430000,
      ],
      // map: provinceGroupBg,
    },
    {
      adcodes: [710000],
    },
    {
      adcodes: [460000],
    },
  ],
  land: [
    {
      adcodes: [710000],
    },
    {
      adcodes: [460000],
    },
  ],
};
geoProjection
JS 复制代码
import { geoMercator } from 'd3-geo';
function geoProjection({ center = [108.55, 34.32], args }) {
  return geoMercator().center(center).scale(120).translate([0, 0])(args);
}
createLine
js 复制代码
export function createLine(points, lineMaterial) {
  const geometry = new THREE.BufferGeometry();
  geometry.setFromPoints(points);
  let line = new THREE.LineLoop(geometry, lineMaterial);
  line.renderOrder = 2;
  line.name = 'mapLine';
  return line;
}
createSideMaterial
jsx 复制代码
async function createSideMaterial() {
  const sideMap = await config.onLoaderTexture(require('@/assets/side.png'));
  // 设置大小,方位
  sideMap.wrapS = THREE.RepeatWrapping;
  sideMap.wrapT = THREE.RepeatWrapping;
  sideMap.repeat.set(1, 0.3);
  sideMap.offset.y += 0.01;
  let sideMaterial = new THREE.MeshStandardMaterial({
    // color: 0x62c3d1,
    color: 0xffffff,
    map: sideMap,
    fog: false,
    transparent: true,
    opacity: 1,
    side: THREE.DoubleSide,
  });
  sideMaterial.name = 'bgSideMaterial';
  // 动画逻辑:
  sideMaterial.onBeforeCompile = (shader) => {
    shader.uniforms = {
      ...shader.uniforms,
      uColor1: { value: new THREE.Color(0x6cf9f9) },
      uColor2: { value: new THREE.Color(0x6cf9f9) },
    };
    shader.vertexShader = shader.vertexShader.replace(
      'void main() {',
      `
        attribute float alpha;
        varying vec3 vPosition;
        varying float vAlpha;
        void main() {
          vAlpha = alpha;
          vPosition = position;
      `,
    );
    shader.fragmentShader = shader.fragmentShader.replace(
      'void main() {',
      `
        varying vec3 vPosition;
        varying float vAlpha;
        uniform vec3 uColor1;
        uniform vec3 uColor2;

        void main() {
      `,
    );
    shader.fragmentShader = shader.fragmentShader.replace(
      '#include <opaque_fragment>',
      /* glsl */ `
      #ifdef OPAQUE
      diffuseColor.a = 1.0;
      #endif

      // https://github.com/mrdoob/three.js/pull/22425
      #ifdef USE_TRANSMISSION
      diffuseColor.a *= transmissionAlpha + 0.1;
      #endif
      vec3 gradient = mix(uColor1, uColor2, vPosition.z/1.2);

      outgoingLight = outgoingLight*gradient;


      gl_FragColor = vec4( outgoingLight, diffuseColor.a  );
      `,
    );
  };
  sideMaterial.tween1 = new TWEEN.Tween({ y: 0 })
    .to({ y: config.defaultOptions.depth }, 10000)
    .onUpdate((params) => {
      let { y } = params;
      sideMaterial.map.offset.y = y;
    })
    .repeat(Infinity)
    .yoyo();
  sideMaterial.tween1.start();
  return sideMaterial;
}
getProvinceGroupPoint
jsx 复制代码
import { getProvinceData } from './utils';
/**
 * @description 获取指定类型的省份(组)点位
 * @param {provinceGroups的key} type
 * @param {boolean} isGroup 是否以 组 级区分
 * @returns Feature[]
 */
export const getProvinceGroupPoint = (type = 'all', isGroup = false) => {
  // 获取到中国各个省份的geojson点位信息
  const { features = [] } = getProvinceData();
  if (type === 'all') return features;
  const currentGroups = provinceGroups[type];
  if (!currentGroups?.length) return [];
  let groupJsonData = [];

  if (isGroup) {
    currentGroups.map((group, index) => {
      const findFeatures = features.filter((f) => group.adcodes.includes(f.properties.adcode));
      if (findFeatures) {
        groupJsonData[index] = groupJsonData[index] || [];
        groupJsonData[index].push(...findFeatures);
      }
    });
  } else {
    const adcodes = currentGroups.reduce((ls, cur) => [...ls, ...cur.adcodes], []);
    groupJsonData = features.filter((item) => adcodes.includes(item?.properties?.adcode));
  }

  return groupJsonData;
};

函数返回的结构就是下图这样,本质上就是筛选:

getOtherProvincesShape:
jsx 复制代码
// 获取指定组的边界线
/**
 *
 * @param {'all' | 'south'} type 选择区域
 * @returns [shapes:绘制好的二维图像组, maps:对应组的地图(若有), points:点位组,可用于绘制边界线, groupFeatures: 组内省份组合后的Feature点位组]
 */
export function getOtherProvincesShape(type) {
  const featuresGroup = getProvinceGroupPoint(type, true);

  const provinceDataGroup = provinceGroups[type];

  const shapes = [],
    maps = [],
    points = [],
    groupFeatures = [];
  featuresGroup.map((features, index) => {
    maps[index] = provinceDataGroup[index].map;
    const shape = new THREE.Shape();
    let cord;

    // 只有一个的话就直接拿json的点位,多个就组合起来
    if (features.length === 1) {
      cord = features[0].geometry.coordinates?.[0]?.[0] || [];
    } else {
      const stokes = getProvinceMergeStoke(features);
      cord = stokes?.geometry?.coordinates[0] || [];
      /*
      // 如果组内出现多岛屿情况,且无法分离(这时候点位成了MultiPolygon),那么就用这个逻辑,让其使用数组多的points,主动变成单组(Polygon)
      if(cord[0].length > 2){
        cord = cord[0]
      }
      */
    }
    groupFeatures.push(cord);

    cord.map((item, i) => {
      const [x, y] = geoProjection({ center: config.defaultOptions.pointCenter, args: item });

      // 单独省份不会划线(暂时)
      if (features.length > 1) {
        points[index] = points[index] || [];
        points[index].push(new THREE.Vector3(x, -y, 0));
      }
      if (i === 0) {
        shape.moveTo(x, -y);
      }
      shape.lineTo(x, -y);
    });
    shapes.push(shape);
  });
  return [shapes, maps, points, groupFeatures];
}
getProvinceMergeStoke:

根据得到的features信息,组合成featureCollection类型,然后进行合并:

jsx 复制代码
/**
 *
 * @description 合并相邻省份的边境界(必须相邻,否则返回MultiPolygon无法绘制(现有逻辑))
 * @param {} provinceGroup Feature[]
 * @param {} adcodes number[]
 * @description 参数二选一
 * @returns point[]
 */
export const getProvinceMergeStoke = (provinceGroup, adcodes) => {
  let groups = provinceGroup;
  if (!provinceGroup && adcodes) {
    groups = getProvinceData().filter((item) => adcodes.includes(item.properties.adcode));
  }
  if (groups.length === 0) return [];
  const polygons = groups.map((it) => polygon(it.geometry.coordinates[0]));
  const u = union(featureCollection(polygons));
  return u;
};
getBasicMapGeometrySettings(计算包围盒)

建立复杂三维图像的时候,必须要计算包围盒拿到setting数据,否则无法展示

jsx 复制代码
// 计算包围盒
export function getBasicMapGeometrySettings(shape) {
  const boundingBox = new THREE.Box2().setFromPoints(shape.getPoints());
  const min = boundingBox.min;
  const max = boundingBox.max;
  const customUVGenerator = {
    generateTopUV: function (geometry, vertices, a, b, c) {
      const ax = vertices[a * 3],
        ay = vertices[a * 3 + 1];
      const bx = vertices[b * 3],
        by = vertices[b * 3 + 1];
      const cx = vertices[c * 3],
        cy = vertices[c * 3 + 1];

      const ua = (ax - min.x) / (max.x - min.x);
      const va = 1 - (ay - min.y) / (max.y - min.y);
      const ub = (bx - min.x) / (max.x - min.x);
      const vb = 1 - (by - min.y) / (max.y - min.y);
      const uc = (cx - min.x) / (max.x - min.x);
      const vc = 1 - (cy - min.y) / (max.y - min.y);

      return [new THREE.Vector2(ua, va), new THREE.Vector2(ub, vb), new THREE.Vector2(uc, vc)];
    },
    generateSideUV: function () {
      return [
        new THREE.Vector2(0, 0),
        new THREE.Vector2(1, 0),
        new THREE.Vector2(1, 1),
        new THREE.Vector2(0, 1),
      ];
    },
    generateSideWallUV: function (geometry, vertices, idxA, idxB, idxC, idxD) {
      /*
      参数说明:
      - geometry: 几何体对象
      - vertices: 顶点数组(平面坐标)
      - idxA, idxB, idxC, idxD: 构成侧面四边形的四个顶点索引
      
      四边形顶点顺序:
      A ---- B
      |      |
      D ---- C
      */
      // 简单实现:将侧面展开为矩形纹理映射
      return [
        new THREE.Vector2(0, 0), // A
        new THREE.Vector2(1, 0), // B
        new THREE.Vector2(1, 1), // C
        new THREE.Vector2(0, 1), // D
      ];
    },
  };
  // 创建挤压几何体
  const extrudeSettings = {
    depth: 0.1,
    bevelEnabled: false,
    UVGenerator: customUVGenerator,
  };

  return extrudeSettings;
}
getCanvasColorBg

通过canvas得到渐变色背景用于填充

CSS 复制代码
background: radial-gradient(44.21% 39.63% at 49.89% 55.39%, #0C3E4D 19.82%, #0DAFE1 100%);

下方对象canvasGradientParams的参数是对上方css渐变处理的,可以对应替换

jsx 复制代码
// canvas渐变背景参数
const canvasGradientParams = {
  shape: 'ellipse', // 椭圆形(CSS 默认)
  size: { w: 44.21, h: 39.63 },
  position: { x: 49.89, y: 55.39 },
  stops: [
    { color: '#0C3E4D', offset: 0.1982 }, // 19.82%
    { color: '#0D9AE1', offset: 1.0 }, // 100%
  ],
};
export const getCanvasColorBg = () => {
  const canvas = document.createElement('canvas');
  canvas.width = 2048; // 高分辨率避免锯齿
  canvas.height = 2048;
  const ctx = canvas.getContext('2d');
  // 计算实际像素坐标
  const centerX = (canvasGradientParams.position.x / 100) * canvas.width;
  const centerY = (canvasGradientParams.position.y / 100) * canvas.height;
  const radiusX = (canvasGradientParams.size.w / 100) * canvas.width;
  const radiusY = (canvasGradientParams.size.h / 100) * canvas.height;

  // 创建径向渐变
  const gradient = ctx.createRadialGradient(
    centerX,
    centerY,
    0, // 起始圆(中心点,半径0)
    centerX,
    centerY,
    Math.max(radiusX, radiusY), // 结束圆(相同中心,最大半径)
  );

  // 添加色标
  canvasGradientParams.stops.forEach((stop) => {
    gradient.addColorStop(stop.offset, stop.color);
  });

  // 绘制渐变
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  return canvas;
};

彩头:如何判断目标经纬度是否在组合块(或者是目标省份adcode)的范围内?

核心实现在于 booleanPointInPolygon方法,判断是否在区域内

jsx 复制代码
import { booleanPointInPolygon, point } from '@turf/turf';
// 获取指定区域内的人员(组合的区域)
export const getCurrentProvinceGroupPerson = (personLs, type = 'all') => {
  if (type === 'all') return personLs;

  let [_, __, ___, groupFeatures] = getOtherProvincesShape(type);

  // 包装成features
  groupFeatures = groupFeatures.map((points) => ({
    type: 'Feature',
    geometry: {
      type: 'Polygon',
      coordinates: [[...points]],
    },
  }));

  const filterPersonLs = personLs.filter((person) => {
    return groupFeatures.some((feature) => {
      const { longitude, latitude } = person;
      const p = point([longitude, latitude]);
      const isInside = booleanPointInPolygon(p, feature);
      return isInside
    });
  });

  return filterPersonLs
};

小结(吐槽)

正所谓学中干,干中学。three.js这玩意儿一个月之前还是一点都不了解,到现在也是站在前人的肩膀上,去试图了解一些,然后用刚学到的皮毛去做一些有趣的东西,真是让人感到兴(痛)奋(苦)啊,不出意外的话以后还要再长期接触three,只得希望未来产品可以手下留情一些吧hhh。

相关推荐
BillKu5 分钟前
Axios中POST、PUT、PATCH用法区别
前端·vue.js
好奇的菜鸟1 小时前
掌握 npm 核心操作:从安装到管理依赖的完整指南
前端·npm·node.js
STUPID MAN2 小时前
arcgis js统计FeatureLayer的椭球面积、平面面积
javascript·arcgis·椭球面积·平面面积
肥肠可耐的西西公主2 小时前
前端(小程序)学习笔记(CLASS 2):WXML模板语法与WXSS模板样式
前端·学习·小程序
JAVA学习通3 小时前
JavaScript网页开发设计(轮播图)
javascript
逆袭的菜鸟X3 小时前
RxJS 高阶映射操作符详解:map、mergeMap 和 switchMap
前端
bubiyoushang8883 小时前
HTML5的新语义化标签
前端·html·html5
会飞的鱼先生4 小时前
vue3自定义指令来实现 v-copy 功能
前端·javascript·vue.js
陈天伟教授4 小时前
Web前端开发 - 制作简单的焦点图效果
java·开发语言·前端·前端开发·visual studio
_殊途4 小时前
前端三件套之html详解
前端·html