Browse Source

feat: 新增三维地球

奔跑的面条 3 years ago
parent
commit
5c4df5c824
35 changed files with 1438 additions and 6 deletions
  1. 3 0
      package.json
  2. 27 3
      pnpm-lock.yaml
  3. 6 0
      src/api/mock/index.ts
  4. 17 0
      src/api/mock/test.mock.ts
  5. BIN
      src/assets/images/chart/decorates/threeEarth01.png
  6. 1 1
      src/components/Pages/ChartItemSetting/GlobalSettingPosition.vue
  7. 236 0
      src/packages/components/Decorates/Three/ThreeEarth01/code/Utils/arc.ts
  8. 137 0
      src/packages/components/Decorates/Three/ThreeEarth01/code/Utils/common.ts
  9. 4 0
      src/packages/components/Decorates/Three/ThreeEarth01/code/interfaces/IEvents.ts
  10. 6 0
      src/packages/components/Decorates/Three/ThreeEarth01/code/interfaces/IWord.ts
  11. 23 0
      src/packages/components/Decorates/Three/ThreeEarth01/code/shaders/earth/fragment.fs
  12. 12 0
      src/packages/components/Decorates/Three/ThreeEarth01/code/shaders/earth/vertex.vs
  13. 34 0
      src/packages/components/Decorates/Three/ThreeEarth01/code/world/Assets.ts
  14. 62 0
      src/packages/components/Decorates/Three/ThreeEarth01/code/world/Basic.ts
  15. 496 0
      src/packages/components/Decorates/Three/ThreeEarth01/code/world/Earth.ts
  16. 54 0
      src/packages/components/Decorates/Three/ThreeEarth01/code/world/Resources.ts
  17. 104 0
      src/packages/components/Decorates/Three/ThreeEarth01/code/world/Word.ts
  18. 17 0
      src/packages/components/Decorates/Three/ThreeEarth01/config.ts
  19. 14 0
      src/packages/components/Decorates/Three/ThreeEarth01/config.vue
  20. 84 0
      src/packages/components/Decorates/Three/ThreeEarth01/data.json
  21. BIN
      src/packages/components/Decorates/Three/ThreeEarth01/images/earth/aircraft.png
  22. BIN
      src/packages/components/Decorates/Three/ThreeEarth01/images/earth/aperture.png
  23. BIN
      src/packages/components/Decorates/Three/ThreeEarth01/images/earth/earth.png
  24. BIN
      src/packages/components/Decorates/Three/ThreeEarth01/images/earth/glow.png
  25. BIN
      src/packages/components/Decorates/Three/ThreeEarth01/images/earth/gradient.png
  26. BIN
      src/packages/components/Decorates/Three/ThreeEarth01/images/earth/label-old.png
  27. BIN
      src/packages/components/Decorates/Three/ThreeEarth01/images/earth/label.png
  28. BIN
      src/packages/components/Decorates/Three/ThreeEarth01/images/earth/light_column.png
  29. BIN
      src/packages/components/Decorates/Three/ThreeEarth01/images/earth/redCircle.png
  30. 15 0
      src/packages/components/Decorates/Three/ThreeEarth01/index.ts
  31. 74 0
      src/packages/components/Decorates/Three/ThreeEarth01/index.vue
  32. 3 0
      src/packages/components/Decorates/Three/index.ts
  33. 2 0
      src/packages/components/Decorates/index.d.ts
  34. 2 1
      src/packages/components/Decorates/index.ts
  35. 5 1
      src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataRequest/components/RequestTargetConfig/index.vue

+ 3 - 0
package.json

@@ -22,6 +22,7 @@
     "echarts-liquidfill": "^3.1.0",
     "echarts-stat": "^1.2.0",
     "echarts-wordcloud": "^2.0.0",
+    "gsap": "^3.11.3",
     "highlight.js": "^11.5.0",
     "html2canvas": "^1.4.1",
     "keymaster": "^1.6.2",
@@ -29,6 +30,7 @@
     "naive-ui": "2.33.4",
     "pinia": "^2.0.13",
     "screenfull": "^6.0.1",
+    "three": "^0.145.0",
     "vue": "^3.2.31",
     "vue-demi": "^0.13.1",
     "vue-i18n": "9.1.9",
@@ -41,6 +43,7 @@
     "@commitlint/cli": "^17.0.2",
     "@commitlint/config-conventional": "^17.0.2",
     "@types/node": "^16.11.26",
+    "@types/three": "^0.144.0",
     "@typescript-eslint/eslint-plugin": "^5.18.0",
     "@typescript-eslint/parser": "^5.18.0",
     "@vicons/carbon": "^0.12.0",

+ 27 - 3
pnpm-lock.yaml

@@ -8,6 +8,7 @@ specifiers:
   '@types/keymaster': ^1.6.30
   '@types/lodash': ^4.14.184
   '@types/node': ^16.11.26
+  '@types/three': ^0.144.0
   '@typescript-eslint/eslint-plugin': ^5.18.0
   '@typescript-eslint/parser': ^5.18.0
   '@vicons/carbon': ^0.12.0
@@ -31,6 +32,7 @@ specifiers:
   eslint-plugin-import: ^2.26.0
   eslint-plugin-prettier: ^4.0.0
   eslint-plugin-vue: ^8.5.0
+  gsap: ^3.11.3
   highlight.js: ^11.5.0
   html2canvas: ^1.4.1
   husky: ^8.0.1
@@ -45,6 +47,7 @@ specifiers:
   sass: ^1.49.11
   sass-loader: ^12.6.0
   screenfull: ^6.0.1
+  three: ^0.145.0
   typescript: 4.6.3
   vite: 2.9.9
   vite-plugin-compression: ^0.5.1
@@ -73,6 +76,7 @@ dependencies:
   echarts-liquidfill: 3.1.0_echarts@5.3.3
   echarts-stat: 1.2.0
   echarts-wordcloud: 2.0.0_echarts@5.3.3
+  gsap: 3.11.3
   highlight.js: 11.5.1
   html2canvas: 1.4.1
   keymaster: 1.6.2
@@ -80,6 +84,7 @@ dependencies:
   naive-ui: 2.33.4_vue@3.2.37
   pinia: 2.0.14_ub5l46u3nefphax5x2tezui4oq
   screenfull: 6.0.1
+  three: 0.145.0
   vue: 3.2.37
   vue-demi: 0.13.1_vue@3.2.37
   vue-i18n: 9.1.9_vue@3.2.37
@@ -92,6 +97,7 @@ devDependencies:
   '@commitlint/cli': 17.0.2
   '@commitlint/config-conventional': 17.0.2
   '@types/node': 16.11.40
+  '@types/three': 0.144.0
   '@typescript-eslint/eslint-plugin': 5.28.0_evi7yu7wunhzwb24olrfvzynny
   '@typescript-eslint/parser': 5.28.0_sfmgizikprcxt7r54j7cnzjamu
   '@vicons/carbon': 0.12.0
@@ -905,7 +911,7 @@ packages:
     dev: true
 
   /@types/node/17.0.43:
-    resolution: {integrity: sha512-jnUpgw8fL9kP2iszfIDyBQtw5Mf4/XSqy0Loc1J9pI14ejL83XcCEvSf50Gs/4ET0I9VCCDoOfufQysj0S66xA==, registry: https://registry.npm.taobao.org/}
+    resolution: {integrity: sha512-jnUpgw8fL9kP2iszfIDyBQtw5Mf4/XSqy0Loc1J9pI14ejL83XcCEvSf50Gs/4ET0I9VCCDoOfufQysj0S66xA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/@types/node/-/node-17.0.43.tgz}
 
   /@types/normalize-package-data/2.4.1:
     resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
@@ -921,12 +927,22 @@ packages:
       '@types/node': 17.0.43
     dev: true
 
+  /@types/three/0.144.0:
+    resolution: {integrity: sha512-psvEs6q5rLN50jUYZ3D4pZMfxTbdt3A243blt0my7/NcL6chaCZpHe2csbCtx0SOD9fI/XnF3wnVUAYZGqCSYg==}
+    dependencies:
+      '@types/webxr': 0.5.0
+    dev: true
+
   /@types/through/0.0.30:
     resolution: {integrity: sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==}
     dependencies:
       '@types/node': 17.0.43
     dev: true
 
+  /@types/webxr/0.5.0:
+    resolution: {integrity: sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA==}
+    dev: true
+
   /@typescript-eslint/eslint-plugin/5.28.0_evi7yu7wunhzwb24olrfvzynny:
     resolution: {integrity: sha512-DXVU6Cg29H2M6EybqSg2A+x8DgO9TCUBRp4QEXQHJceLS7ogVDP0g3Lkg/SZCqcvkAP/RruuQqK0gdlkgmhSUA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -1884,7 +1900,7 @@ packages:
     dev: true
 
   /csstype/2.6.20:
-    resolution: {integrity: sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==, registry: https://registry.npm.taobao.org/}
+    resolution: {integrity: sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/csstype/-/csstype-2.6.20.tgz}
     dev: false
 
   /csstype/3.0.11:
@@ -2091,7 +2107,7 @@ packages:
     dev: false
 
   /echarts-wordcloud/2.0.0_echarts@5.3.3:
-    resolution: {integrity: sha512-K7l6pTklqdW7ZWzT/1CS0KhBSINr/cd7c5N1fVMzZMwLQHEwT7x+nivK7g5hkVh7WNcAv4Dn6/ZS5zMKRozC1g==, registry: https://registry.npm.taobao.org/}
+    resolution: {integrity: sha512-K7l6pTklqdW7ZWzT/1CS0KhBSINr/cd7c5N1fVMzZMwLQHEwT7x+nivK7g5hkVh7WNcAv4Dn6/ZS5zMKRozC1g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/echarts-wordcloud/-/echarts-wordcloud-2.0.0.tgz}
     peerDependencies:
       echarts: ^5.0.1
     dependencies:
@@ -3032,6 +3048,10 @@ packages:
     resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
     dev: true
 
+  /gsap/3.11.3:
+    resolution: {integrity: sha512-xc/iIJy+LWiMbRa4IdMtdnnKa/7PXEK6NNzV71gdOYUVeTZN7UWnLU0fB7Hi1iwiz4ZZoYkBZPPYGg+2+zzFHA==}
+    dev: false
+
   /handlebars/4.7.7:
     resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==}
     engines: {node: '>=0.4.7'}
@@ -4825,6 +4845,10 @@ packages:
     resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
     dev: true
 
+  /three/0.145.0:
+    resolution: {integrity: sha512-EKoHQEtEJ4CB6b2BGMBgLZrfwLjXcSUfoI/MiIXUuRpeYsfK5aPWbYhdtIVWOH+x6X0TouldHKHBuc/LAiFzAw==}
+    dev: false
+
   /through/2.3.8:
     resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
     dev: true

+ 6 - 0
src/api/mock/index.ts

@@ -17,6 +17,7 @@ export const scatterBasicUrl = '/mock/scatterBasic'
 export const mapUrl = '/mock/map'
 export const wordCloudUrl = '/mock/wordCloud'
 export const treemapUrl = '/mock/treemap'
+export const threeEarth01Url = '/mock/threeEarth01Data'
 
 const mockObject: MockMethod[] = [
   {
@@ -91,6 +92,11 @@ const mockObject: MockMethod[] = [
     method: RequestHttpEnum.GET,
     response: () => test.fetchTreemap
   },
+  {
+    url: threeEarth01Url,
+    method: RequestHttpEnum.GET,
+    response: () => test.threeEarth01Data
+  },
 ]
 
 export default mockObject

+ 17 - 0
src/api/mock/test.mock.ts

@@ -254,4 +254,21 @@ export default {
     msg: '请求成功',
     data: tTreemapJson
   },
+  // 三维地球
+  threeEarth01Data: {
+    code: 0,
+    status: 200,
+    msg: '请求成功',
+    data: [
+      {
+        startArray: { name: '@name', N: '@integer(10, 100)', E: '@integer(10, 100)' },
+        endArray: [
+          { name: '@name', N: '@integer(10, 100)', E: '@integer(10, 100)' },
+          { name: '@name', N: '@integer(10, 100)', E: '@integer(10, 100)' },
+          { name: '@name', N: '@integer(10, 100)', E: '@integer(10, 100)' },
+          { name: '@name', N: '@integer(10, 100)', E: '@integer(10, 100)' }
+        ]
+      }
+    ]
+  }
 }

BIN
src/assets/images/chart/decorates/threeEarth01.png


+ 1 - 1
src/components/Pages/ChartItemSetting/GlobalSettingPosition.vue

@@ -1,5 +1,5 @@
 <template>
-  <setting-item-box name="位置">
+  <setting-item-box v-if="targetData" name="位置">
     <setting-item :name="`偏移 X:${targetData.left || 0}px`">
       <n-input-number v-model:value="targetData.left" size="small" step="10"></n-input-number>
     </setting-item>

+ 236 - 0
src/packages/components/Decorates/Three/ThreeEarth01/code/Utils/arc.ts

@@ -0,0 +1,236 @@
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-nocheck
+import {
+  ArcCurve,
+  BufferAttribute,
+  BufferGeometry,
+  Color,
+  Line,
+  LineBasicMaterial,
+  Points,
+  PointsMaterial,
+  Quaternion,
+  Vector3
+} from 'three'
+import { lon2xyz } from './common'
+
+/*
+ * 绘制一条圆弧飞线
+ * 5个参数含义:( 飞线圆弧轨迹半径, 开始角度, 结束角度)
+ */
+function createFlyLine(radius, startAngle, endAngle, color) {
+  const geometry = new BufferGeometry() //声明一个几何体对象BufferGeometry
+  //  ArcCurve创建圆弧曲线
+  const arc = new ArcCurve(0, 0, radius, startAngle, endAngle, false)
+  //getSpacedPoints是基类Curve的方法,返回一个vector2对象作为元素组成的数组
+  const pointsArr = arc.getSpacedPoints(100) //分段数80,返回81个顶点
+  geometry.setFromPoints(pointsArr) // setFromPoints方法从pointsArr中提取数据改变几何体的顶点属性vertices
+  // 每个顶点对应一个百分比数据attributes.percent 用于控制点的渲染大小
+  const percentArr = [] //attributes.percent的数据
+  for (let i = 0; i < pointsArr.length; i++) {
+    percentArr.push(i / pointsArr.length)
+  }
+  const percentAttribue = new BufferAttribute(new Float32Array(percentArr), 1)
+  // 通过顶点数据percent点模型从大到小变化,产生小蝌蚪形状飞线
+  geometry.attributes.percent = percentAttribue
+  // 批量计算所有顶点颜色数据
+  const colorArr = []
+  for (let i = 0; i < pointsArr.length; i++) {
+    const color1 = new Color(0xec8f43) //轨迹线颜色 青色
+    const color2 = new Color(0xf3ae76) //黄色
+    const color = color1.lerp(color2, i / pointsArr.length)
+    colorArr.push(color.r, color.g, color.b)
+  }
+  // 设置几何体顶点颜色数据
+  geometry.attributes.color = new BufferAttribute(new Float32Array(colorArr), 3)
+  const size = 1.3
+  // 点模型渲染几何体每个顶点
+  const material = new PointsMaterial({
+    size, //点大小
+    // vertexColors: VertexColors, //使用顶点颜色渲染
+    transparent: true,
+    depthWrite: false
+  })
+  // 修改点材质的着色器源码(注意:不同版本细节可能会稍微会有区别,不过整体思路是一样的)
+  material.onBeforeCompile = function (shader) {
+    // 顶点着色器中声明一个attribute变量:百分比
+    shader.vertexShader = shader.vertexShader.replace(
+      'void main() {',
+      [
+        'attribute float percent;', //顶点大小百分比变量,控制点渲染大小
+        'void main() {'
+      ].join('\n') // .join()把数组元素合成字符串
+    )
+    // 调整点渲染大小计算方式
+    shader.vertexShader = shader.vertexShader.replace(
+      'gl_PointSize = size;',
+      ['gl_PointSize = percent * size;'].join('\n') // .join()把数组元素合成字符串
+    )
+  }
+  const FlyLine = new Points(geometry, material)
+  material.color = new Color(color)
+  FlyLine.name = '飞行线'
+
+  return FlyLine
+}
+
+/**输入地球上任意两点的经纬度坐标,通过函数flyArc可以绘制一个飞线圆弧轨迹
+ * lon1,lat1:轨迹线起点经纬度坐标
+ * lon2,lat2:轨迹线结束点经纬度坐标
+ */
+function flyArc(radius, lon1, lat1, lon2, lat2, options) {
+  const sphereCoord1 = lon2xyz(radius, lon1, lat1) //经纬度坐标转球面坐标
+  // startSphereCoord:轨迹线起点球面坐标
+  const startSphereCoord = new Vector3(sphereCoord1.x, sphereCoord1.y, sphereCoord1.z)
+  const sphereCoord2 = lon2xyz(radius, lon2, lat2)
+  // startSphereCoord:轨迹线结束点球面坐标
+  const endSphereCoord = new Vector3(sphereCoord2.x, sphereCoord2.y, sphereCoord2.z)
+
+  //计算绘制圆弧需要的关于y轴对称的起点、结束点和旋转四元数
+  const startEndQua = _3Dto2D(startSphereCoord, endSphereCoord)
+  // 调用arcXOY函数绘制一条圆弧飞线轨迹
+  const arcline = arcXOY(radius, startEndQua.startPoint, startEndQua.endPoint, options)
+  arcline.quaternion.multiply(startEndQua.quaternion)
+  return arcline
+}
+/*
+ * 把3D球面上任意的两个飞线起点和结束点绕球心旋转到到XOY平面上,
+ * 同时保持关于y轴对称,借助旋转得到的新起点和新结束点绘制
+ * 一个圆弧,最后把绘制的圆弧反向旋转到原来的起点和结束点即可
+ */
+function _3Dto2D(startSphere, endSphere) {
+  /*计算第一次旋转的四元数:表示从一个平面如何旋转到另一个平面*/
+  const origin = new Vector3(0, 0, 0) //球心坐标
+  const startDir = startSphere.clone().sub(origin) //飞线起点与球心构成方向向量
+  const endDir = endSphere.clone().sub(origin) //飞线结束点与球心构成方向向量
+  // dir1和dir2构成一个三角形,.cross()叉乘计算该三角形法线normal
+  const normal = startDir.clone().cross(endDir).normalize()
+  const xoyNormal = new Vector3(0, 0, 1) //XOY平面的法线
+  //.setFromUnitVectors()计算从normal向量旋转达到xoyNormal向量所需要的四元数
+  // quaternion表示把球面飞线旋转到XOY平面上需要的四元数
+  const quaternion3D_XOY = new Quaternion().setFromUnitVectors(normal, xoyNormal)
+  /*第一次旋转:飞线起点、结束点从3D空间第一次旋转到XOY平面*/
+  const startSphereXOY = startSphere.clone().applyQuaternion(quaternion3D_XOY)
+  const endSphereXOY = endSphere.clone().applyQuaternion(quaternion3D_XOY)
+
+  /*计算第二次旋转的四元数*/
+  // middleV3:startSphereXOY和endSphereXOY的中点
+  const middleV3 = startSphereXOY.clone().add(endSphereXOY).multiplyScalar(0.5)
+  const midDir = middleV3.clone().sub(origin).normalize() // 旋转前向量midDir,中点middleV3和球心构成的方向向量
+  const yDir = new Vector3(0, 1, 0) // 旋转后向量yDir,即y轴
+  // .setFromUnitVectors()计算从midDir向量旋转达到yDir向量所需要的四元数
+  // quaternion2表示让第一次旋转到XOY平面的起点和结束点关于y轴对称需要的四元数
+  const quaternionXOY_Y = new Quaternion().setFromUnitVectors(midDir, yDir)
+
+  /*第二次旋转:使旋转到XOY平面的点再次旋转,实现关于Y轴对称*/
+  const startSpherXOY_Y = startSphereXOY.clone().applyQuaternion(quaternionXOY_Y)
+  const endSphereXOY_Y = endSphereXOY.clone().applyQuaternion(quaternionXOY_Y)
+
+  /**一个四元数表示一个旋转过程
+   *.invert()方法表示四元数的逆,简单说就是把旋转过程倒过来
+   * 两次旋转的四元数执行.invert()求逆,然后执行.multiply()相乘
+   *新版本.invert()对应旧版本.invert()
+   */
+  const quaternionInverse = quaternion3D_XOY.clone().invert().multiply(quaternionXOY_Y.clone().invert())
+  return {
+    // 返回两次旋转四元数的逆四元数
+    quaternion: quaternionInverse,
+    // 范围两次旋转后在XOY平面上关于y轴对称的圆弧起点和结束点坐标
+    startPoint: startSpherXOY_Y,
+    endPoint: endSphereXOY_Y
+  }
+}
+/**通过函数arcXOY()可以在XOY平面上绘制一个关于y轴对称的圆弧曲线
+ * startPoint, endPoint:表示圆弧曲线的起点和结束点坐标值,起点和结束点关于y轴对称
+ * 同时在圆弧轨迹的基础上绘制一段飞线*/
+function arcXOY(radius, startPoint, endPoint, options) {
+  // 计算两点的中点
+  const middleV3 = new Vector3().addVectors(startPoint, endPoint).multiplyScalar(0.5)
+  // 弦垂线的方向dir(弦的中点和圆心构成的向量)
+  const dir = middleV3.clone().normalize()
+  // 计算球面飞线的起点、结束点和球心构成夹角的弧度值
+  const earthRadianAngle = radianAOB(startPoint, endPoint, new Vector3(0, 0, 0))
+  /*设置飞线轨迹圆弧的中间点坐标
+  弧度值 * radius * 0.2:表示飞线轨迹圆弧顶部距离地球球面的距离
+  起点、结束点相聚越远,构成的弧线顶部距离球面越高*/
+  const arcTopCoord = dir.multiplyScalar(radius + earthRadianAngle * radius * 0.2) // 黄色飞行线的高度
+  //求三个点的外接圆圆心(飞线圆弧轨迹的圆心坐标)
+  const flyArcCenter = threePointCenter(startPoint, endPoint, arcTopCoord)
+  // 飞线圆弧轨迹半径flyArcR
+  const flyArcR = Math.abs(flyArcCenter.y - arcTopCoord.y)
+  /*坐标原点和飞线起点构成直线和y轴负半轴夹角弧度值
+  参数分别是:飞线圆弧起点、y轴负半轴上一点、飞线圆弧圆心*/
+  const flyRadianAngle = radianAOB(startPoint, new Vector3(0, -1, 0), flyArcCenter)
+  const startAngle = -Math.PI / 2 + flyRadianAngle //飞线圆弧开始角度
+  const endAngle = Math.PI - startAngle //飞线圆弧结束角度
+  // 调用圆弧线模型的绘制函数
+  const arcline = circleLine(flyArcCenter.x, flyArcCenter.y, flyArcR, startAngle, endAngle, options.color)
+  // const arcline = new  Group();// 不绘制轨迹线,使用 Group替换circleLine()即可
+  arcline.center = flyArcCenter //飞线圆弧自定一个属性表示飞线圆弧的圆心
+  arcline.topCoord = arcTopCoord //飞线圆弧自定一个属性表示飞线圆弧中间也就是顶部坐标
+
+  // const flyAngle = Math.PI/ 10; //飞线圆弧固定弧度
+  const flyAngle = (endAngle - startAngle) / 7 //飞线圆弧的弧度和轨迹线弧度相关
+  // 绘制一段飞线,圆心做坐标原点
+  const flyLine = createFlyLine(flyArcR, startAngle, startAngle + flyAngle, options.flyLineColor)
+  flyLine.position.y = flyArcCenter.y //平移飞线圆弧和飞线轨迹圆弧重合
+  //飞线段flyLine作为飞线轨迹arcLine子对象,继承飞线轨迹平移旋转等变换
+  arcline.add(flyLine)
+  //飞线段运动范围startAngle~flyEndAngle
+  flyLine.flyEndAngle = endAngle - startAngle - flyAngle
+  flyLine.startAngle = startAngle
+  // arcline.flyEndAngle:飞线段当前角度位置,这里设置了一个随机值用于演示
+  flyLine.AngleZ = arcline.flyEndAngle * Math.random()
+  // flyLine.rotation.z = arcline.AngleZ;
+  // arcline.flyLine指向飞线段,便于设置动画是访问飞线段
+  arcline.userData['flyLine'] = flyLine
+
+  return arcline
+}
+/*计算球面上两点和球心构成夹角的弧度值
+参数point1, point2:表示地球球面上两点坐标Vector3
+计算A、B两点和顶点O构成的AOB夹角弧度值*/
+function radianAOB(A, B, O) {
+  // dir1、dir2:球面上两个点和球心构成的方向向量
+  const dir1 = A.clone().sub(O).normalize()
+  const dir2 = B.clone().sub(O).normalize()
+  //点乘.dot()计算夹角余弦值
+  const cosAngle = dir1.clone().dot(dir2)
+  const radianAngle = Math.acos(cosAngle) //余弦值转夹角弧度值,通过余弦值可以计算夹角范围是0~180度
+  return radianAngle
+}
+/*绘制一条圆弧曲线模型Line
+5个参数含义:(圆心横坐标, 圆心纵坐标, 飞线圆弧轨迹半径, 开始角度, 结束角度)*/
+function circleLine(x, y, r, startAngle, endAngle, color) {
+  const geometry = new BufferGeometry() //声明一个几何体对象Geometry
+  //  ArcCurve创建圆弧曲线
+  const arc = new ArcCurve(x, y, r, startAngle, endAngle, false)
+  //getSpacedPoints是基类Curve的方法,返回一个vector2对象作为元素组成的数组
+  const points = arc.getSpacedPoints(80) //分段数50,返回51个顶点
+  geometry.setFromPoints(points) // setFromPoints方法从points中提取数据改变几何体的顶点属性vertices
+  const material = new LineBasicMaterial({
+    color: color || 0xd18547
+  }) //线条材质
+  const line = new Line(geometry, material) //线条模型对象
+  return line
+}
+//求三个点的外接圆圆心,p1, p2, p3表示三个点的坐标Vector3。
+function threePointCenter(p1, p2, p3) {
+  const L1 = p1.lengthSq() //p1到坐标原点距离的平方
+  const L2 = p2.lengthSq()
+  const L3 = p3.lengthSq()
+  const x1 = p1.x,
+    y1 = p1.y,
+    x2 = p2.x,
+    y2 = p2.y,
+    x3 = p3.x,
+    y3 = p3.y
+  const S = x1 * y2 + x2 * y3 + x3 * y1 - x1 * y3 - x2 * y1 - x3 * y2
+  const x = (L2 * y3 + L1 * y2 + L3 * y1 - L2 * y1 - L3 * y2 - L1 * y3) / S / 2
+  const y = (L3 * x2 + L2 * x1 + L1 * x3 - L1 * x2 - L2 * x3 - L3 * x1) / S / 2
+  // 三点外接圆圆心坐标
+  const center = new Vector3(x, y, 0)
+  return center
+}
+
+export { arcXOY, flyArc }

+ 137 - 0
src/packages/components/Decorates/Three/ThreeEarth01/code/Utils/common.ts

@@ -0,0 +1,137 @@
+import {
+  CatmullRomCurve3,
+  DoubleSide,
+  Group,
+  Mesh,
+  MeshBasicMaterial,
+  PlaneGeometry,
+  Texture,
+  TubeGeometry,
+  Vector3
+} from 'three'
+import { punctuation } from '../world/Earth'
+
+/**
+ * 经纬度坐标转球面坐标
+ * @param {地球半径} R
+ * @param {经度(角度值)} longitude
+ * @param {维度(角度值)} latitude
+ */
+export const lon2xyz = (R: number, longitude: number, latitude: number): Vector3 => {
+  let lon = (longitude * Math.PI) / 180 // 转弧度值
+  const lat = (latitude * Math.PI) / 180 // 转弧度值
+  lon = -lon // js坐标系z坐标轴对应经度-90度,而不是90度
+
+  // 经纬度坐标转球面坐标计算公式
+  const x = R * Math.cos(lat) * Math.cos(lon)
+  const y = R * Math.sin(lat)
+  const z = R * Math.cos(lat) * Math.sin(lon)
+  // 返回球面坐标
+  return new Vector3(x, y, z)
+}
+
+// 创建波动光圈
+export const createWaveMesh = (options: { radius: number; lon: number; lat: number; textures: any }) => {
+  const geometry = new PlaneGeometry(1, 1) //默认在XOY平面上
+  const texture = options.textures.aperture
+
+  const material = new MeshBasicMaterial({
+    color: 0xe99f68,
+    map: texture,
+    transparent: true, //使用背景透明的png贴图,注意开启透明计算
+    opacity: 1.0,
+    depthWrite: false //禁止写入深度缓冲区数据
+  })
+  const mesh = new Mesh(geometry, material)
+  // 经纬度转球面坐标
+  const coord = lon2xyz(options.radius * 1.001, options.lon, options.lat)
+  const size = options.radius * 0.12 //矩形平面Mesh的尺寸
+  mesh.scale.set(size, size, size) //设置mesh大小
+  mesh.userData['size'] = size //自顶一个属性,表示mesh静态大小
+  mesh.userData['scale'] = Math.random() * 1.0 //自定义属性._s表示mesh在原始大小基础上放大倍数  光圈在原来mesh.size基础上1~2倍之间变化
+  mesh.position.set(coord.x, coord.y, coord.z)
+  const coordVec3 = new Vector3(coord.x, coord.y, coord.z).normalize()
+  const meshNormal = new Vector3(0, 0, 1)
+  mesh.quaternion.setFromUnitVectors(meshNormal, coordVec3)
+  return mesh
+}
+
+// 创建柱状
+export const createLightPillar = (options: {
+  radius: number
+  lon: number
+  lat: number
+  index: number
+  textures: Record<string, Texture>
+  punctuation: punctuation
+}) => {
+  const height = options.radius * 0.3
+  const geometry = new PlaneGeometry(options.radius * 0.05, height)
+  geometry.rotateX(Math.PI / 2)
+  geometry.translate(0, 0, height / 2)
+  const material = new MeshBasicMaterial({
+    map: options.textures.light_column,
+    color: options.index == 0 ? options.punctuation.lightColumn.startColor : options.punctuation.lightColumn.endColor,
+    transparent: true,
+    side: DoubleSide,
+    depthWrite: false //是否对深度缓冲区有任何的影响
+  })
+  const mesh = new Mesh(geometry, material)
+  const group = new Group()
+  // 两个光柱交叉叠加
+  group.add(mesh, mesh.clone().rotateZ(Math.PI / 2)) //几何体绕x轴旋转了,所以mesh旋转轴变为z
+  // 经纬度转球面坐标
+  const SphereCoord = lon2xyz(options.radius, options.lon, options.lat) //SphereCoord球面坐标
+  group.position.set(SphereCoord.x, SphereCoord.y, SphereCoord.z) //设置mesh位置
+  const coordVec3 = new Vector3(SphereCoord.x, SphereCoord.y, SphereCoord.z).normalize()
+  const meshNormal = new Vector3(0, 0, 1)
+  group.quaternion.setFromUnitVectors(meshNormal, coordVec3)
+  return group
+}
+
+// 光柱底座矩形平面
+export const createPointMesh = (options: { radius: number; lon: number; lat: number; material: MeshBasicMaterial }) => {
+  const geometry = new PlaneGeometry(1, 1) //默认在XOY平面上
+  const mesh = new Mesh(geometry, options.material)
+  // 经纬度转球面坐标
+  const coord = lon2xyz(options.radius * 1.001, options.lon, options.lat)
+  const size = options.radius * 0.05 // 矩形平面Mesh的尺寸
+  mesh.scale.set(size, size, size) // 设置mesh大小
+
+  // 设置mesh位置
+  mesh.position.set(coord.x, coord.y, coord.z)
+  const coordVec3 = new Vector3(coord.x, coord.y, coord.z).normalize()
+  const meshNormal = new Vector3(0, 0, 1)
+  mesh.quaternion.setFromUnitVectors(meshNormal, coordVec3)
+  return mesh
+}
+
+// 获取点
+export const getCirclePoints = (option: any) => {
+  const list = []
+  for (let j = 0; j < 2 * Math.PI - 0.1; j += (2 * Math.PI) / (option.number || 100)) {
+    list.push([
+      parseFloat((Math.cos(j) * (option.radius || 10)).toFixed(2)),
+      0,
+      parseFloat((Math.sin(j) * (option.radius || 10)).toFixed(2))
+    ])
+  }
+  if (option.closed) list.push(list[0])
+  return list
+}
+
+// 创建线
+
+/**
+ * 创建动态的线
+ */
+export const createAnimateLine = (option: any) => {
+  // 由多个点数组构成的曲线 通常用于道路
+  const l: Array<any> = []
+  option.pointList.forEach((e: Array<any>) => l.push(new Vector3(e[0], e[1], e[2])))
+  const curve = new CatmullRomCurve3(l) // 曲线路径
+
+  // 管道体
+  const tubeGeometry = new TubeGeometry(curve, option.number || 50, option.radius || 1, option.radialSegments)
+  return new Mesh(tubeGeometry, option.material)
+}

+ 4 - 0
src/packages/components/Decorates/Three/ThreeEarth01/code/interfaces/IEvents.ts

@@ -0,0 +1,4 @@
+
+export interface IEvents {
+  resize: () => void
+}

+ 6 - 0
src/packages/components/Decorates/Three/ThreeEarth01/code/interfaces/IWord.ts

@@ -0,0 +1,6 @@
+export interface IWord {
+  dom: HTMLElement
+  data: any
+  width: number
+  height: number
+}

+ 23 - 0
src/packages/components/Decorates/Three/ThreeEarth01/code/shaders/earth/fragment.fs

@@ -0,0 +1,23 @@
+uniform vec3 glowColor;
+uniform float bias;
+uniform float power;
+uniform float time;
+varying vec3 vp;
+varying vec3 vNormal;
+varying vec3 vPositionNormal;
+uniform float scale;
+// 获取纹理
+uniform sampler2D map;
+// 纹理坐标
+varying vec2 vUv;
+
+void main(void){
+  float a = pow( bias + scale * abs(dot(vNormal, vPositionNormal)), power );
+  if(vp.y > time && vp.y < time + 20.0) {
+    float t =  smoothstep(0.0, 0.8,  (1.0 - abs(0.5 - (vp.y - time) / 20.0)) / 3.0  );
+    gl_FragColor = mix(gl_FragColor, vec4(glowColor, 1.0), t * t );
+  }
+  gl_FragColor = mix(gl_FragColor, vec4( glowColor, 1.0 ), a);
+  float b = 0.8;
+  gl_FragColor = gl_FragColor + texture2D( map, vUv );
+}

+ 12 - 0
src/packages/components/Decorates/Three/ThreeEarth01/code/shaders/earth/vertex.vs

@@ -0,0 +1,12 @@
+
+varying vec2 vUv;
+varying vec3 vNormal;
+varying vec3 vp;
+varying vec3 vPositionNormal;
+void main(void){
+  vUv = uv;
+  vNormal = normalize( normalMatrix * normal ); // 转换到视图空间
+  vp = position;
+  vPositionNormal = normalize(( modelViewMatrix * vec4(position, 1.0) ).xyz);
+  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+}

+ 34 - 0
src/packages/components/Decorates/Three/ThreeEarth01/code/world/Assets.ts

@@ -0,0 +1,34 @@
+/**
+ * 资源文件
+ * 把模型和图片分开进行加载
+ */
+
+interface ITextures {
+  name: string
+  url: string
+}
+
+export interface IResources {
+  textures?: ITextures[]
+}
+
+const fileSuffix = ['earth', 'gradient', 'redCircle', 'label', 'aperture', 'glow', 'light_column', 'aircraft']
+const textures: ITextures[] = []
+
+const modules = import.meta.globEager("../../images/earth/*");
+
+for(let item in modules) {
+  const n = item.split('/').pop()
+  if(n) {
+    textures.push({
+      name: n.split('.')[0],
+      url: modules[item].default
+    })
+  }
+}
+
+const resources: IResources = {
+  textures
+}
+
+export { resources }

+ 62 - 0
src/packages/components/Decorates/Three/ThreeEarth01/code/world/Basic.ts

@@ -0,0 +1,62 @@
+/**
+ * 创建 threejs 四大天王
+ * 场景、相机、渲染器、控制器
+ */
+
+import * as THREE from 'three'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
+
+export class Basic {
+  public scene!: THREE.Scene
+  public camera!: THREE.PerspectiveCamera
+  public renderer!: THREE.WebGLRenderer
+  public controls!: OrbitControls
+  public dom: HTMLElement
+
+  constructor(dom: HTMLElement) {
+    this.dom = dom
+    this.initScenes()
+    this.setControls()
+  }
+
+  /**
+   * 初始化场景
+   */
+  initScenes() {
+    this.scene = new THREE.Scene()
+
+    this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 100000)
+    this.camera.position.set(0, 30, -250)
+
+    this.renderer = new THREE.WebGLRenderer({
+      // canvas: this.dom,
+      alpha: true, // 透明
+      antialias: true // 抗锯齿
+    })
+    this.renderer.setPixelRatio(window.devicePixelRatio) // 设置屏幕像素比
+    this.renderer.setSize(window.innerWidth, window.innerHeight) // 设置渲染器宽高
+    this.dom.appendChild(this.renderer.domElement) // 添加到dom中
+  }
+
+  /**
+   * 设置控制器
+   */
+  setControls() {
+    // 鼠标控制      相机,渲染dom
+    this.controls = new OrbitControls(this.camera, this.renderer.domElement)
+
+    this.controls.autoRotateSpeed = 3
+    // 使动画循环使用时阻尼或自转 意思是否有惯性
+    this.controls.enableDamping = true
+    // 动态阻尼系数 就是鼠标拖拽旋转灵敏度
+    this.controls.dampingFactor = 0.05
+    // 是否可以缩放
+    this.controls.enableZoom = true
+    // 设置相机距离原点的最远距离
+    this.controls.minDistance = 100
+    // 设置相机距离原点的最远距离
+    this.controls.maxDistance = 300
+    // 是否开启右键拖拽
+    this.controls.enablePan = false
+  }
+}

+ 496 - 0
src/packages/components/Decorates/Three/ThreeEarth01/code/world/Earth.ts

@@ -0,0 +1,496 @@
+import {
+  BufferAttribute,
+  BufferGeometry,
+  Color,
+  DoubleSide,
+  Group,
+  Material,
+  Mesh,
+  MeshBasicMaterial,
+  NormalBlending,
+  Object3D,
+  Points,
+  PointsMaterial,
+  ShaderMaterial,
+  SphereGeometry,
+  Sprite,
+  SpriteMaterial,
+  Texture,
+  TextureLoader,
+  Vector3
+} from 'three'
+
+import {
+  createAnimateLine,
+  createLightPillar,
+  createPointMesh,
+  createWaveMesh,
+  getCirclePoints,
+  lon2xyz
+} from '../Utils/common'
+import gsap from 'gsap'
+import { flyArc } from '../Utils/arc'
+import earthVertex from '../shaders/earth/vertex.vs?raw'
+import earthFragment from '../shaders/earth/fragment.fs?raw'
+
+export type punctuation = {
+  circleColor: number
+  lightColumn: {
+    startColor: number // 起点颜色
+    endColor: number // 终点颜色
+  }
+}
+
+type options = {
+  data: {
+    startArray: {
+      name: string
+      E: number // 经度
+      N: number // 维度
+    }
+    endArray: {
+      name: string
+      E: number // 经度
+      N: number // 维度
+    }[]
+  }[]
+  dom: HTMLElement
+  textures: Record<string, Texture> // 贴图
+  earth: {
+    radius: number // 地球半径
+    rotateSpeed: number // 地球旋转速度
+    isRotation: boolean // 地球组是否自转
+  }
+  satellite: {
+    show: boolean // 是否显示卫星
+    rotateSpeed: number // 旋转速度
+    size: number // 卫星大小
+    number: number // 一个圆环几个球
+  }
+  punctuation: punctuation
+  flyLine: {
+    color: number // 飞线的颜色
+    speed: number // 飞机拖尾线速度
+    flyLineColor: number // 飞行线的颜色
+  }
+}
+type uniforms = {
+  glowColor: { value: Color }
+  scale: { type: string; value: number }
+  bias: { type: string; value: number }
+  power: { type: string; value: number }
+  time: { type: string; value: any }
+  isHover: { value: boolean }
+  map: { value?: Texture }
+}
+
+export default class earth {
+  public group: Group
+  public earthGroup: Group
+
+  public around!: BufferGeometry
+  public aroundPoints!: Points<BufferGeometry, PointsMaterial>
+
+  public options: options
+  public uniforms: uniforms
+  public timeValue: number
+
+  public earth!: Mesh<SphereGeometry, ShaderMaterial>
+  public punctuationMaterial!: MeshBasicMaterial
+  public markupPoint: Group
+  public waveMeshArr: Object3D[]
+
+  public circleLineList: any[]
+  public circleList: any[]
+  public x: number
+  public n: number
+  public isRotation: boolean
+  public flyLineArcGroup!: Group
+
+  constructor(options: options) {
+    this.options = options
+
+    this.group = new Group()
+    this.group.name = 'group'
+    this.group.scale.set(0, 0, 0)
+    this.earthGroup = new Group()
+    this.group.add(this.earthGroup)
+    this.earthGroup.name = 'EarthGroup'
+
+    // 标注点效果
+    this.markupPoint = new Group()
+    this.markupPoint.name = 'markupPoint'
+    this.waveMeshArr = []
+
+    // 卫星和标签
+    this.circleLineList = []
+    this.circleList = []
+    this.x = 0
+    this.n = 0
+
+    // 地球自转
+    this.isRotation = this.options.earth.isRotation
+
+    // 扫光动画 shader
+    this.timeValue = 200
+
+    this.uniforms = {
+      glowColor: {
+        value: new Color(0x0cd1eb)
+      },
+      scale: {
+        type: 'f',
+        value: -1.0
+      },
+      bias: {
+        type: 'f',
+        value: 1.0
+      },
+      power: {
+        type: 'f',
+        value: 3.3
+      },
+      time: {
+        type: 'f',
+        value: this.timeValue
+      },
+      isHover: {
+        value: false
+      },
+      map: {
+        value: undefined
+      }
+    }
+  }
+
+  async init(): Promise<void> {
+    return new Promise(resolve => {
+      const init = async () => {
+        this.createEarth() // 创建地球
+        this.createEarthGlow() // 创建地球辉光
+        this.createEarthAperture() // 创建地球的大气层
+        await this.createMarkupPoint() // 创建柱状点位
+        this.createAnimateCircle() // 创建环绕卫星
+        this.createFlyLine() // 创建飞线
+        this.show()
+        resolve()
+      }
+      init()
+    })
+  }
+
+  createEarth() {
+    const earth_geometry = new SphereGeometry(this.options.earth.radius, 50, 50)
+    const earth_border = new SphereGeometry(this.options.earth.radius + 10, 60, 60)
+
+    const pointMaterial = new PointsMaterial({
+      color: 0x81ffff, //设置颜色,默认 0xFFFFFF
+      transparent: true,
+      sizeAttenuation: true,
+      opacity: 0.1,
+      vertexColors: false, //定义材料是否使用顶点颜色,默认false ---如果该选项设置为true,则color属性失效
+      size: 0.2 //定义粒子的大小。默认为1.0
+    })
+    const points = new Points(earth_border, pointMaterial) //将模型添加到场景
+
+    this.earthGroup.add(points)
+
+    this.uniforms.map.value = this.options.textures.earth
+
+    const earth_material = new ShaderMaterial({
+      // wireframe:true, // 显示模型线条
+      uniforms: this.uniforms as any,
+      vertexShader: earthVertex,
+      fragmentShader: earthFragment
+    })
+
+    earth_material.needsUpdate = true
+    this.earth = new Mesh(earth_geometry, earth_material)
+    this.earth.name = 'earth'
+    this.earthGroup.add(this.earth)
+  }
+
+  createEarthGlow() {
+    const R = this.options.earth.radius //地球半径
+
+    // TextureLoader创建一个纹理加载器对象,可以加载图片作为纹理贴图
+    const texture = this.options.textures.glow // 加载纹理贴图
+
+    // 创建精灵材质对象SpriteMaterial
+    const spriteMaterial = new SpriteMaterial({
+      map: texture, // 设置精灵纹理贴图
+      color: 0x4390d1,
+      transparent: true, //开启透明
+      opacity: 0.7, // 可以通过透明度整体调节光圈
+      depthWrite: false //禁止写入深度缓冲区数据
+    })
+
+    // 创建表示地球光圈的精灵模型
+    const sprite = new Sprite(spriteMaterial)
+    sprite.scale.set(R * 3.0, R * 3.0, 1) //适当缩放精灵
+    this.earthGroup.add(sprite)
+  }
+
+  createEarthAperture() {
+    const vertexShader = [
+      'varying vec3	vVertexWorldPosition;',
+      'varying vec3	vVertexNormal;',
+      'varying vec4	vFragColor;',
+      'void main(){',
+      '	vVertexNormal	= normalize(normalMatrix * normal);', //将法线转换到视图坐标系中
+      '	vVertexWorldPosition	= (modelMatrix * vec4(position, 1.0)).xyz;', //将顶点转换到世界坐标系中
+      '	// set gl_Position',
+      '	gl_Position	= projectionMatrix * modelViewMatrix * vec4(position, 1.0);',
+      '}'
+    ].join('\n')
+
+    //大气层效果
+    const AeroSphere = {
+      uniforms: {
+        coeficient: {
+          type: 'f',
+          value: 1.0
+        },
+        power: {
+          type: 'f',
+          value: 3
+        },
+        glowColor: {
+          type: 'c',
+          value: new Color(0x4390d1)
+        }
+      },
+      vertexShader: vertexShader,
+      fragmentShader: [
+        'uniform vec3	glowColor;',
+        'uniform float	coeficient;',
+        'uniform float	power;',
+
+        'varying vec3	vVertexNormal;',
+        'varying vec3	vVertexWorldPosition;',
+
+        'varying vec4	vFragColor;',
+
+        'void main(){',
+        '	vec3 worldCameraToVertex = vVertexWorldPosition - cameraPosition;', //世界坐标系中从相机位置到顶点位置的距离
+        '	vec3 viewCameraToVertex	= (viewMatrix * vec4(worldCameraToVertex, 0.0)).xyz;', //视图坐标系中从相机位置到顶点位置的距离
+        '	viewCameraToVertex= normalize(viewCameraToVertex);', //规一化
+        '	float intensity	= pow(coeficient + dot(vVertexNormal, viewCameraToVertex), power);',
+        '	gl_FragColor = vec4(glowColor, intensity);',
+        '}'
+      ].join('\n')
+    }
+    //球体 辉光 大气层
+    const material1 = new ShaderMaterial({
+      uniforms: AeroSphere.uniforms,
+      vertexShader: AeroSphere.vertexShader,
+      fragmentShader: AeroSphere.fragmentShader,
+      blending: NormalBlending,
+      transparent: true,
+      depthWrite: false
+    })
+    const sphere = new SphereGeometry(this.options.earth.radius + 0, 50, 50)
+    const mesh = new Mesh(sphere, material1)
+    this.earthGroup.add(mesh)
+  }
+
+  async createMarkupPoint() {
+    await Promise.all(
+      this.options.data.map(async item => {
+        const radius = this.options.earth.radius
+        const lon = item.startArray.E //经度
+        const lat = item.startArray.N //纬度
+
+        this.punctuationMaterial = new MeshBasicMaterial({
+          color: this.options.punctuation.circleColor,
+          map: this.options.textures.label,
+          transparent: true, //使用背景透明的png贴图,注意开启透明计算
+          depthWrite: false //禁止写入深度缓冲区数据
+        })
+
+        const mesh = createPointMesh({ radius, lon, lat, material: this.punctuationMaterial }) //光柱底座矩形平面
+        this.markupPoint.add(mesh)
+        const LightPillar = createLightPillar({
+          radius: this.options.earth.radius,
+          lon,
+          lat,
+          index: 0,
+          textures: this.options.textures,
+          punctuation: this.options.punctuation
+        }) //光柱
+        this.markupPoint.add(LightPillar)
+        const WaveMesh = createWaveMesh({ radius, lon, lat, textures: this.options.textures }) //波动光圈
+        this.markupPoint.add(WaveMesh)
+        this.waveMeshArr.push(WaveMesh)
+
+        await Promise.all(
+          item.endArray.map(obj => {
+            const lon = obj.E //经度
+            const lat = obj.N //纬度
+            const mesh = createPointMesh({ radius, lon, lat, material: this.punctuationMaterial }) //光柱底座矩形平面
+            this.markupPoint.add(mesh)
+            const LightPillar = createLightPillar({
+              radius: this.options.earth.radius,
+              lon,
+              lat,
+              index: 1,
+              textures: this.options.textures,
+              punctuation: this.options.punctuation
+            }) //光柱
+            this.markupPoint.add(LightPillar)
+            const WaveMesh = createWaveMesh({ radius, lon, lat, textures: this.options.textures }) //波动光圈
+            this.markupPoint.add(WaveMesh)
+            this.waveMeshArr.push(WaveMesh)
+          })
+        )
+        this.earthGroup.add(this.markupPoint)
+      })
+    )
+  }
+
+  createAnimateCircle() {
+    // 创建 圆环 点
+    const list = getCirclePoints({
+      radius: this.options.earth.radius + 15,
+      number: 150, //切割数
+      closed: true // 闭合
+    })
+    const mat = new MeshBasicMaterial({
+      color: '#0c3172',
+      transparent: true,
+      opacity: 0.4,
+      side: DoubleSide
+    })
+    const line = createAnimateLine({
+      pointList: list,
+      material: mat,
+      number: 100,
+      radius: 0.1
+    })
+    this.earthGroup.add(line)
+
+    // 在clone两条线出来
+    const l2 = line.clone()
+    l2.scale.set(1.2, 1.2, 1.2)
+    l2.rotateZ(Math.PI / 6)
+    this.earthGroup.add(l2)
+
+    const l3 = line.clone()
+    l3.scale.set(0.8, 0.8, 0.8)
+    l3.rotateZ(-Math.PI / 6)
+    this.earthGroup.add(l3)
+
+    /**
+     * 旋转的球
+     */
+    const ball = new Mesh(
+      new SphereGeometry(this.options.satellite.size, 32, 32),
+      new MeshBasicMaterial({
+        color: '#e0b187' // 745F4D
+      })
+    )
+
+    const ball2 = new Mesh(
+      new SphereGeometry(this.options.satellite.size, 32, 32),
+      new MeshBasicMaterial({
+        color: '#628fbb' // 324A62
+      })
+    )
+
+    const ball3 = new Mesh(
+      new SphereGeometry(this.options.satellite.size, 32, 32),
+      new MeshBasicMaterial({
+        color: '#806bdf' //6D5AC4
+      })
+    )
+
+    this.circleLineList.push(line, l2, l3)
+    ball.name = ball2.name = ball3.name = '卫星'
+
+    for (let i = 0; i < this.options.satellite.number; i++) {
+      const ball01 = ball.clone()
+      // 一根线上总共有几个球,根据数量平均分布一下
+      const num = Math.floor(list.length / this.options.satellite.number)
+      ball01.position.set(list[num * (i + 1)][0] * 1, list[num * (i + 1)][1] * 1, list[num * (i + 1)][2] * 1)
+      line.add(ball01)
+
+      const ball02 = ball2.clone()
+      const num02 = Math.floor(list.length / this.options.satellite.number)
+      ball02.position.set(list[num02 * (i + 1)][0] * 1, list[num02 * (i + 1)][1] * 1, list[num02 * (i + 1)][2] * 1)
+      l2.add(ball02)
+
+      const ball03 = ball2.clone()
+      const num03 = Math.floor(list.length / this.options.satellite.number)
+      ball03.position.set(list[num03 * (i + 1)][0] * 1, list[num03 * (i + 1)][1] * 1, list[num03 * (i + 1)][2] * 1)
+      l3.add(ball03)
+    }
+  }
+
+  createFlyLine() {
+    this.flyLineArcGroup = new Group()
+    this.flyLineArcGroup.userData['flyLineArray'] = []
+    this.earthGroup.add(this.flyLineArcGroup)
+    this.options.data.forEach(cities => {
+      cities.endArray.forEach(item => {
+        // 调用函数flyArc绘制球面上任意两点之间飞线圆弧轨迹
+        const arcline = flyArc(
+          this.options.earth.radius,
+          cities.startArray.E,
+          cities.startArray.N,
+          item.E,
+          item.N,
+          this.options.flyLine
+        )
+
+        this.flyLineArcGroup.add(arcline) // 飞线插入flyArcGroup中
+        this.flyLineArcGroup.userData['flyLineArray'].push(arcline.userData['flyLine'])
+      })
+    })
+  }
+
+  show() {
+    gsap.to(this.group.scale, {
+      x: 1,
+      y: 1,
+      z: 1,
+      duration: 2,
+      ease: 'Quadratic'
+    })
+  }
+
+  render() {
+    this.flyLineArcGroup?.userData['flyLineArray']?.forEach((fly: any) => {
+      fly.rotation.z += this.options.flyLine.speed // 调节飞线速度
+      if (fly.rotation.z >= fly.flyEndAngle) fly.rotation.z = 0
+    })
+
+    if (this.isRotation) {
+      this.earthGroup.rotation.y += this.options.earth.rotateSpeed
+    }
+
+    this.circleLineList.forEach(e => {
+      e.rotateY(this.options.satellite.rotateSpeed)
+    })
+
+    this.uniforms.time.value =
+      this.uniforms.time.value < -this.timeValue ? this.timeValue : this.uniforms.time.value - 1
+
+    if (this.waveMeshArr.length) {
+      this.waveMeshArr.forEach((mesh: any) => {
+        mesh.userData['scale'] += 0.007
+        mesh.scale.set(
+          mesh.userData['size'] * mesh.userData['scale'],
+          mesh.userData['size'] * mesh.userData['scale'],
+          mesh.userData['size'] * mesh.userData['scale']
+        )
+        if (mesh.userData['scale'] <= 1.5) {
+          (mesh.material as Material).opacity = (mesh.userData['scale'] - 1) * 2 //2等于1/(1.5-1.0),保证透明度在0~1之间变化
+        } else if (mesh.userData['scale'] > 1.5 && mesh.userData['scale'] <= 2) {
+          (mesh.material as Material).opacity = 1 - (mesh.userData['scale'] - 1.5) * 2 //2等于1/(2.0-1.5) mesh缩放2倍对应0 缩放1.5被对应1
+        } else {
+          mesh.userData['scale'] = 1
+        }
+      })
+    }
+  }
+}

+ 54 - 0
src/packages/components/Decorates/Three/ThreeEarth01/code/world/Resources.ts

@@ -0,0 +1,54 @@
+/**
+ * 资源管理和加载
+ */
+import { LoadingManager, Texture, TextureLoader } from 'three'
+import { loadingStart, loadingFinish, loadingError } from '@/utils'
+import { resources } from './Assets'
+export class Resources {
+  private manager!: LoadingManager
+  private callback: () => void
+  private textureLoader!: InstanceType<typeof TextureLoader>
+  public textures: Record<string, Texture>
+  constructor(callback: () => void) {
+    this.callback = callback // 资源加载完成的回调
+    this.textures = {} // 贴图对象
+    this.setLoadingManager()
+    this.loadResources()
+  }
+
+  /**
+   * 管理加载状态
+   */
+  private setLoadingManager() {
+    this.manager = new LoadingManager()
+    // 开始加载
+    this.manager.onStart = () => {
+      loadingStart()
+    }
+    // 加载完成
+    this.manager.onLoad = () => {
+      this.callback()
+    }
+    // 正在进行中
+    this.manager.onProgress = url => {
+      loadingFinish()
+    }
+
+    this.manager.onError = url => {
+      loadingError()
+      window['$message'].error('数据加载失败,请刷新重试!')
+    }
+  }
+
+  /**
+   * 加载资源
+   */
+  private loadResources(): void {
+    this.textureLoader = new TextureLoader(this.manager)
+    resources.textures?.forEach(item => {
+      this.textureLoader.load(item.url, t => {
+        this.textures[item.name] = t
+      })
+    })
+  }
+}

+ 104 - 0
src/packages/components/Decorates/Three/ThreeEarth01/code/world/Word.ts

@@ -0,0 +1,104 @@
+import { MeshBasicMaterial, PerspectiveCamera, Scene, ShaderMaterial, WebGLRenderer } from 'three'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
+// interfaces
+import { IWord } from '../interfaces/IWord'
+import { Basic } from './Basic'
+import { Resources } from './Resources'
+// earth
+import Earth from './Earth'
+
+export default class World {
+  public basic: Basic
+  public scene: Scene
+  public camera: PerspectiveCamera
+  public renderer: WebGLRenderer
+  public controls: OrbitControls
+  public material!: ShaderMaterial | MeshBasicMaterial
+  public resources: Resources
+  public option: IWord
+  public earth!: Earth
+
+  constructor(option: IWord) {
+    /**
+     * 加载资源
+     */
+    this.option = option
+    this.basic = new Basic(option.dom)
+    this.scene = this.basic.scene
+    this.renderer = this.basic.renderer
+    this.controls = this.basic.controls
+    this.camera = this.basic.camera
+    this.updateSize()
+    this.resources = new Resources(async () => {
+      await this.createEarth()
+      // 开始渲染
+      this.render()
+    })
+  }
+
+  async createEarth(data?: any) {
+    // 资源加载完成,开始制作地球,注释在new Earth()类型里面
+    this.earth = new Earth({
+      data: data || this.option.data,
+      dom: this.option.dom,
+      textures: this.resources.textures,
+      earth: {
+        radius: 50,
+        rotateSpeed: 0.002,
+        isRotation: true
+      },
+      satellite: {
+        show: true,
+        rotateSpeed: -0.01,
+        size: 1,
+        number: 2
+      },
+      punctuation: {
+        circleColor: 0x3892ff,
+        lightColumn: {
+          startColor: 0xe4007f, // 起点颜色
+          endColor: 0xffffff // 终点颜色
+        }
+      },
+      flyLine: {
+        color: 0xf3ae76, // 飞线的颜色
+        flyLineColor: 0xff7714, // 飞行线的颜色
+        speed: 0.004 // 拖尾飞线的速度
+      }
+    })
+
+    this.scene.add(this.earth.group)
+    await this.earth.init()
+  }
+
+  /**
+   * 渲染函数
+   */
+  public render() {
+    requestAnimationFrame(this.render.bind(this))
+    this.renderer.render(this.scene, this.camera)
+    this.controls && this.controls.update()
+    this.earth && this.earth.render()
+  }
+
+  // 更新
+  public updateSize(width?: number, height?: number) {
+    let w = width || this.option.width
+    let h = height || this.option.height
+    // 取小值
+    if (w < h) h = w
+    else w = h
+
+    this.renderer.setSize(w, h)
+    this.camera.aspect = w / h
+    this.camera.updateProjectionMatrix()
+  }
+
+  // 数据更新重新渲染
+  public updateData(data?: any) {
+    if(!this.earth.group) return
+    // 先删除旧的
+    this.scene.remove(this.earth.group)
+    this.createEarth(data)
+  }
+}

+ 17 - 0
src/packages/components/Decorates/Three/ThreeEarth01/config.ts

@@ -0,0 +1,17 @@
+import { PublicConfigClass } from '@/packages/public'
+import { CreateComponentType } from '@/packages/index.d'
+import { chartInitConfig } from '@/settings/designSetting'
+import { ThreeEarth01Config } from './index'
+import dataJson from './data.json'
+import cloneDeep from 'lodash/cloneDeep'
+
+export const option = {
+  dataset: dataJson
+}
+
+export default class Config extends PublicConfigClass implements CreateComponentType {
+  public key = ThreeEarth01Config.key
+  public attr = { ...chartInitConfig, w: 800, h: 800, zIndex: -1 }
+  public chartConfig = cloneDeep(ThreeEarth01Config)
+  public option = cloneDeep(option)
+}

+ 14 - 0
src/packages/components/Decorates/Three/ThreeEarth01/config.vue

@@ -0,0 +1,14 @@
+<template></template>
+
+<script setup lang="ts">
+import { PropType } from 'vue'
+import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
+import { option } from './config'
+
+const props = defineProps({
+  optionData: {
+    type: Object as PropType<typeof option>,
+    required: true
+  }
+})
+</script>

+ 84 - 0
src/packages/components/Decorates/Three/ThreeEarth01/data.json

@@ -0,0 +1,84 @@
+[
+  {
+    "startArray": {
+      "name": "杭州",
+      "N": 30.246026,
+      "E": 120.210792
+    },
+    "endArray": [
+      {
+        "name": "曼谷",
+        "N": 22,
+        "E": 100.49074172973633
+      },
+      {
+        "name": "澳大利亚",
+        "N": -23.68477416688374,
+        "E": 133.857421875
+      },
+
+      {
+        "name": "新疆维吾尔自治区",
+        "N": 41.748,
+        "E": 84.9023
+      },
+
+      {
+        "name": "德黑兰",
+        "N": 35,
+        "E": 51
+      },
+      {
+        "name": "德黑兰",
+        "N": 35,
+        "E": 51
+      },
+      {
+        "name": "美国",
+        "N": 34.125447565116126,
+        "E": 241.7431640625
+      },
+      {
+        "name": "英国",
+        "N": 51.508742458803326,
+        "E": 359.82421875
+      },
+      {
+        "name": "巴西",
+        "N": -9.96885060854611,
+        "E": 668.1445312499999
+      }
+    ]
+  },
+  {
+    "startArray": {
+      "name": "北京",
+      "N": 39.89491,
+      "E": 116.322056
+    },
+    "endArray": [
+      {
+        "name": "西藏",
+        "N": 29.660361,
+        "E": 91.132212
+      },
+      {
+        "name": "广西",
+        "N": 22.830824,
+        "E": 108.30616
+      },
+
+      {
+        "name": "江西",
+        "N": 28.676493,
+        "E": 115.892151
+      },
+
+      {
+        "name": "贵阳",
+        "N": 26.647661,
+        "E": 106.630153
+      }
+    ]
+  }
+]

BIN
src/packages/components/Decorates/Three/ThreeEarth01/images/earth/aircraft.png


BIN
src/packages/components/Decorates/Three/ThreeEarth01/images/earth/aperture.png


BIN
src/packages/components/Decorates/Three/ThreeEarth01/images/earth/earth.png


BIN
src/packages/components/Decorates/Three/ThreeEarth01/images/earth/glow.png


BIN
src/packages/components/Decorates/Three/ThreeEarth01/images/earth/gradient.png


BIN
src/packages/components/Decorates/Three/ThreeEarth01/images/earth/label-old.png


BIN
src/packages/components/Decorates/Three/ThreeEarth01/images/earth/label.png


BIN
src/packages/components/Decorates/Three/ThreeEarth01/images/earth/light_column.png


BIN
src/packages/components/Decorates/Three/ThreeEarth01/images/earth/redCircle.png


+ 15 - 0
src/packages/components/Decorates/Three/ThreeEarth01/index.ts

@@ -0,0 +1,15 @@
+import image from '@/assets/images/chart/decorates/threeEarth01.png'
+import { ConfigType, PackagesCategoryEnum, ChartFrameEnum } from '@/packages/index.d'
+import { ChatCategoryEnum, ChatCategoryEnumName } from '../../index.d'
+
+export const ThreeEarth01Config: ConfigType = {
+  key: 'ThreeEarth01',
+  chartKey: 'VThreeEarth01',
+  conKey: 'VCThreeEarth01',
+  title: '时钟',
+  category: ChatCategoryEnum.THREE,
+  categoryName: ChatCategoryEnumName.THREE,
+  package: PackagesCategoryEnum.DECORATES,
+  chartFrame: ChartFrameEnum.STATIC,
+  image
+}

+ 74 - 0
src/packages/components/Decorates/Three/ThreeEarth01/index.vue

@@ -0,0 +1,74 @@
+<template>
+  <div ref="chartRef"></div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, PropType, ref, toRefs, watch } from 'vue'
+import { CreateComponentType } from '@/packages/index.d'
+import { useChartDataFetch } from '@/hooks'
+import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
+import { option } from './config'
+import World from './code/world/Word'
+import throttle from 'lodash/throttle'
+
+const props = defineProps({
+  chartConfig: {
+    type: Object as PropType<CreateComponentType & typeof option>,
+    required: true
+  }
+})
+
+const chartRef = ref<HTMLElement>()
+const { w, h } = toRefs(props.chartConfig.attr)
+let threeClassInstance: World
+
+// 初始化
+const init = () => {
+  const dom: HTMLElement | undefined = chartRef.value
+  if (dom) {
+    threeClassInstance = new World({
+      dom: dom,
+      data: props.chartConfig.option.dataset,
+      width: w.value,
+      height: h.value
+    })
+  }
+}
+
+const updateData = (data: any) => {
+  try {
+    threeClassInstance.updateData(data)
+  } catch (error) {
+    console.log(error)
+  }
+}
+
+// 改变大小
+watch(
+  () => [w.value, h.value],
+  throttle(([newWidth], [newHeight]) => {
+    threeClassInstance.updateSize(newWidth, newHeight)
+  }, 100)
+)
+
+watch(
+  () => props.chartConfig.option.dataset,
+  (newData: any) => {
+    updateData(newData)
+  },
+  {
+    deep: false
+  }
+)
+
+// DOM 渲染之后进行初始化
+onMounted(() => {
+  try {
+    init()
+  } catch (error) {
+    console.log(error)
+  }
+})
+
+useChartDataFetch(props.chartConfig, useChartEditStore, updateData)
+</script>

+ 3 - 0
src/packages/components/Decorates/Three/index.ts

@@ -0,0 +1,3 @@
+import { ThreeEarth01Config } from './ThreeEarth01/index'
+
+export default [ThreeEarth01Config]

+ 2 - 0
src/packages/components/Decorates/index.d.ts

@@ -1,11 +1,13 @@
 export enum ChatCategoryEnum {
   BORDER = 'Borders',
   DECORATE = 'Decorates',
+  THREE = 'Three',
   MORE = 'Mores'
 }
 
 export enum ChatCategoryEnumName {
   BORDER = '边框',
   DECORATE = '装饰',
+  THREE = '三维',
   MORE = '更多'
 }

+ 2 - 1
src/packages/components/Decorates/index.ts

@@ -1,5 +1,6 @@
 import Borders from './Borders'
 import Decorates from './Decorates'
+import Three from './Three'
 import Mores from './Mores'
 
-export const DecorateList = [...Borders, ...Decorates, ...Mores]
+export const DecorateList = [...Borders, ...Decorates, ...Three, ...Mores]

+ 5 - 1
src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataRequest/components/RequestTargetConfig/index.vue

@@ -76,7 +76,8 @@ import {
   scatterBasicUrl,
   mapUrl,
   wordCloudUrl,
-  treemapUrl
+  treemapUrl,
+  threeEarth01Url
 } from '@/api/mock'
 
 const { HelpOutlineIcon } = icon.ionicons5
@@ -127,6 +128,9 @@ const apiList = [
   {
     value: `【树图】${treemapUrl}`
   },
+  {
+    value: `【三维地球】${threeEarth01Url}`
+  },
 ]
 </script>