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。

相关推荐
Mike_jia30 分钟前
Memos:知识工作者的理想开源笔记系统
前端
前端大白话30 分钟前
前端崩溃瞬间救星!10 个 JavaScript 实战技巧大揭秘
前端·javascript
loveoobaby31 分钟前
Shadertoy着色器移植到Three.js经验总结
前端
蓝易云34 分钟前
在Linux、CentOS7中设置shell脚本开机自启动服务
前端·后端·centos
浩龙不eMo34 分钟前
前端获取环境变量方式区分(Vite)
前端·vite
一千柯橘40 分钟前
Nestjs 解决 request entity too large
javascript·后端
土豆骑士1 小时前
monorepo 实战练习
前端
土豆骑士1 小时前
monorepo最佳实践
前端
见青..1 小时前
【学习笔记】文件包含漏洞--本地远程包含、伪协议、加密编码
前端·笔记·学习·web安全·文件包含
举个栗子dhy1 小时前
如何处理动态地址栏参数,以及Object.entries() 、Object.fromEntries()和URLSearchParams.entries()使用
javascript