[
  {
    "path": ".commitlintrc.js",
    "content": "module.exports = {\n  extends: [\"@commitlint/config-conventional\"],\n  rules: {\n    \"type-enum\": [\n      2,\n      \"always\",\n      [\n        \"feat\", // 功能\n        \"fix\", // bug\n        \"test\", // 测试\n        \"perf\", // 优化\n        \"refactor\", // 重构\n        \"docs\", // 文档\n        \"chore\", // 辅助工具配置\n        \"style\", // 格式 （适合lint fix...）\n        \"revert\", // 回滚\n        \"merge\", // 合并\n        \"sync\", // 同步（同步主线或分支上的fix修复等）\n      ],\n    ],\n    \"type-case\": [2, \"always\", \"lower-case\"],\n    \"type-empty\": [2, \"never\"],\n    \"scope-empty\": [0],\n    \"scope-case\": [0],\n    \"subject-full-stop\": [0, \"never\"],\n    \"subject-case\": [0, \"never\"],\n    \"header-max-length\": [0, \"always\", 72],\n  },\n};\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  \"plugins\": ['@typescript-eslint'],\n  extends: [\"eslint:recommended\", \"plugin:@typescript-eslint/recommended\"],\n  rules: {\n    eqeqeq: 0, // 必须使用全等\n    'no-unused-vars': 1, // 不能有声明后未被使用的变量或参数\n    'no-throw-literal': 0, // 0可以/2不可以 抛出字面量错误 throw \"error\";\n    'no-sparse-arrays': 2, // 数组中不允许出现空位置\n    'no-empty': 0, // 禁止出现空语句块\n    'no-console': ['error', { allow: ['warn', 'error', 'info', \"log\"] }],\n    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',\n    'no-useless-escape': 0,\n    \"@typescript-eslint/no-explicit-any\": \"off\",\n    \"no-async-promise-executor\": 0\n  },\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "*.js linguist-detectable=false\n"
  },
  {
    "path": ".gitignore",
    "content": ".vscode\nnode_modules\ndist\nbuild"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpx lint-staged\n\n"
  },
  {
    "path": ".prettierrc.js",
    "content": "export default {\n  \"printWidth\": 80,\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"trailingComma\": \"es5\",\n  \"bracketSpacing\": true,\n  \"spaceBeforeFunctionParen\": true\n}"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 GhostCat\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.en.md",
    "content": "# Webpack 5 + Typescript 4 + Three.js 基础模板\n\n- Webpack 5\n- Typescript 4\n- Three.js 130\n- lodash"
  },
  {
    "path": "README.md",
    "content": "# 3d-earth\n本项目使用 [three-ts-webpack](https://github.com/GhostCatcg/three-ts-webpack) 构建\n\n[Live Demo](https://gcat.cc/demo/earth)\n\n![alt](./3d-earth.png)\n## Todolist\n1. - [x] 加载效果[loading...]\n2. - [x] 地球、以及星空背景🌏\n3. - [x] 辉光以及大气层✨\n4. - [x] 地球标点以及城市标签🇨🇳\n5. - [x] 卫星环绕旋转🛰\n6. - [x] 国家/城市之前的飞线🪐\n7. - [ ] 飞机沿飞线飞行🛫"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"GhostCat\",\n  \"version\": \"1.0.0\",\n  \"description\": \"3d-earth\",\n  \"keywords\": [\n    \"GhostCat\",\n    \"3d\",\n    \"threejs\",\n    \"typescript\"\n  ],\n  \"author\": \"GhostCat\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"dev\": \"webpack serve --mode development\",\n    \"build\": \"webpack --mode production\",\n    \"lint\": \"eslint\",\n    \"prepare\": \"husky install\"\n  },\n  \"lint-staged\": {\n    \"src/*.{js,jsx,tsx,ts}\": \"eslint --fix\"\n  },\n  \"homepage\": \"https://gcat.cc\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.12.10\",\n    \"@babel/preset-env\": \"^7.12.11\",\n    \"@commitlint/config-conventional\": \"^15.0.0\",\n    \"@size-limit/preset-small-lib\": \"^7.0.3\",\n    \"@types/lodash\": \"^4.14.172\",\n    \"@types/three\": \"^0.131.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.13.0\",\n    \"@typescript-eslint/parser\": \"^5.13.0\",\n    \"babel-loader\": \"^8.2.2\",\n    \"clean-webpack-plugin\": \"^3.0.0\",\n    \"copy-webpack-plugin\": \"^9.0.1\",\n    \"core-js\": \"^3.8.1\",\n    \"css-loader\": \"^6.2.0\",\n    \"eslint\": \"^8.10.0\",\n    \"html-webpack-plugin\": \"^4.5.0\",\n    \"husky\": \"^7.0.0\",\n    \"lint-staged\": \"^12.1.2\",\n    \"style-loader\": \"^3.2.1\",\n    \"ts-loader\": \"^8.0.12\",\n    \"ts-shader-loader\": \"^1.0.6\",\n    \"typescript\": \"^4.1.3\",\n    \"webpack\": \"^5.11.0\",\n    \"webpack-cli\": \"^4.2.0\",\n    \"webpack-dev-server\": \"^3.11.0\"\n  },\n  \"dependencies\": {\n    \"@tweakpane/core\": \"^1.0.6\",\n    \"eslint-webpack-plugin\": \"^3.1.1\",\n    \"gsap\": \"^3.7.1\",\n    \"html2canvas\": \"^1.4.1\",\n    \"lodash\": \"^4.17.21\",\n    \"pietile-eventemitter\": \"^1.0.1\",\n    \"three\": \"^0.143\"\n  }\n}\n"
  },
  {
    "path": "src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n  <html>\n\n<head>\n\t<title>GhostCat</title>\n  <meta charset=\"utf-8\">\n</head>\n<style>\n  html,body{\n    height:100%;\n    width: 100%;\n    padding:0;\n    margin:0;\n  }\n  body{\n    overflow: hidden;\n    position: relative;\n  }\n  h1,h2{\n    position: absolute;\n    color:#fff;\n    width: 100%;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    padding-top:20vh;\n    pointer-events: none;\n  }\n  h2{\n    pointer-events: all;\n    padding-top:25vh;\n  }\n  h2 a{\n    color:rgb(19, 22, 226);\n    font-size: 30px;\n  }\n\n  #earth-canvas{\n    height: 100%;\n    width: 100%;\n    background: #010826;\n  }\n  \n  #html2canvas {\n    position: absolute;\n    z-index: -1;\n    left: 0;\n    top:0;\n    background: rgba(0, 0, 0, 0);\n  }\n  .css3d-wapper {\n    pointer-events: none;\n    color: #fff;\n  }\n  \n  .css3d-wapper .fire-div {\n    font-size: 20px;\n    font-weight: 600;\n    border-top: 3px solid #0cd1eb;\n    padding: 6px 8px;\n    min-width: 50px;\n    background: rgba(40, 108, 181, 0.5);\n    display: flex;\n    justify-content: center;\n    align-items: center;\n  }\n  #loading{\n    position:absolute;\n    left:0;\n    top:0;\n    right:0;\n    bottom: 0;\n    width: 100vw;\n    height:100vh;\n    z-index: 999;\n    background:#010826;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    flex-direction: column;\n    color:#409EFF;\n    font-size: 15px;\n    letter-spacing: 2px;\n    overflow: hidden;\n  }\n  @keyframes zoomOut {\n    0%{\n      opacity:1\n    }\n    50%{\n      opacity:0;\n      transform:scale3d(1.3,1.3,1.3)\n    }\n    to{\n      opacity:0\n    }\n  }\n  #loading.out{\n    animation:zoomOut 0.5s  linear forwards;\n    pointer-events: none;\n  }\n  #loading.out .sk-chase-dot,\n  #loading.out .sk-chase{\n    animation: null;\n  }\n  .sk-chase {\n    margin-bottom: 20px;\n    width: 40px;\n    height: 40px;\n    position: relative;\n    animation: sk-chase 2.5s infinite linear both;\n  }\n\n  .sk-chase-dot {\n    width: 100%;\n    height: 100%;\n    position: absolute;\n    left: 0;\n    top: 0;\n    animation: sk-chase-dot 2s infinite ease-in-out both;\n  }\n\n  .sk-chase-dot::before {\n    content: '';\n    display: block;\n    width: 20%;\n    height: 20%;\n    background-color: #409EFF;\n    border-radius: 100%;\n    animation: sk-chase-dot-before 2s infinite ease-in-out both;\n  }\n  .sk-chase-dot:nth-child(1) {\n    animation-delay: -1.1s;\n  }\n  .sk-chase-dot:nth-child(2) {\n    animation-delay: -1s;\n  }\n  .sk-chase-dot:nth-child(3) {\n    animation-delay: -0.9s;\n  }\n  .sk-chase-dot:nth-child(4) {\n    animation-delay: -0.8s;\n  }\n  .sk-chase-dot:nth-child(5) {\n    animation-delay: -0.7s;\n  }\n  .sk-chase-dot:nth-child(6) {\n    animation-delay: -0.6s;\n  }\n  .sk-chase-dot:nth-child(1):before {\n    animation-delay: -1.1s;\n  }\n  .sk-chase-dot:nth-child(2):before {\n    animation-delay: -1s;\n  }\n  .sk-chase-dot:nth-child(3):before {\n    animation-delay: -0.9s;\n  }\n  .sk-chase-dot:nth-child(4):before {\n    animation-delay: -0.8s;\n  }\n  .sk-chase-dot:nth-child(5):before {\n    animation-delay: -0.7s;\n  }\n  .sk-chase-dot:nth-child(6):before {\n    animation-delay: -0.6s;\n  }\n\n  .sk-chase-dot .sk-chase-dot:nth-child(2) {\n    animation-delay: -1s;\n  }\n  .sk-chase-dot:nth-child(3) {\n    animation-delay: -0.9s;\n  }\n  .sk-chase-dot:nth-child(4) {\n    animation-delay: -0.8s;\n  }\n  .sk-chase-dot:nth-child(5) {\n    animation-delay: -0.7s;\n  }\n  .sk-chase-dot:nth-child(6) {\n    animation-delay: -0.6s;\n  }\n  .sk-chase-dot:nth-child(1):before {\n    animation-delay: -1.1s;\n  }\n  .sk-chase-dot:nth-child(2):before {\n    animation-delay: -1s;\n  }\n  .sk-chase-dot:nth-child(3):before {\n    animation-delay: -0.9s;\n  }\n  .sk-chase-dot:nth-child(4):before {\n    animation-delay: -0.8s;\n  }\n  .sk-chase-dot:nth-child(5):before {\n    animation-delay: -0.7s;\n  }\n  .sk-chase-dot:nth-child(6):before {\n    animation-delay: -0.6s;\n  }\n\n  @keyframes sk-chase {\n    100% {\n      transform: rotate(360deg);\n    }\n  }\n\n  @keyframes sk-chase-dot {\n    80%,\n    100% {\n      transform: rotate(360deg);\n    }\n  }\n\n  @keyframes sk-chase-dot-before {\n    50% {\n      transform: scale(0.4);\n    }\n    100%,\n    0% {\n      transform: scale(1);\n    }\n  }\n</style>\n\n<body>\n  <div id=\"loading\">\n    <div class=\"sk-chase\">\n      <div class=\"sk-chase-dot\"></div>\n      <div class=\"sk-chase-dot\"></div>\n      <div class=\"sk-chase-dot\"></div>\n      <div class=\"sk-chase-dot\"></div>\n      <div class=\"sk-chase-dot\"></div>\n      <div class=\"sk-chase-dot\"></div>\n    </div>\n    <div>加载资源中...</div>\n  </div>\n  <div id=\"html2canvas\" class=\"css3d-wapper\">\n    <div class=\"fire-div\"></div>\n  </div>\n  <div id=\"earth-canvas\"></div>\n</body>\n\n</html>"
  },
  {
    "path": "src/shaders/earth/fragment.fs",
    "content": "uniform vec3 glowColor;\nuniform float bias;\nuniform float power;\nuniform float time;\nvarying vec3 vp;\nvarying vec3 vNormal;\nvarying vec3 vPositionNormal;\nuniform float scale;\n// 获取纹理\nuniform sampler2D map;\n// 纹理坐标\nvarying vec2 vUv;\n\nvoid main(void){\n  float a = pow( bias + scale * abs(dot(vNormal, vPositionNormal)), power );\n  if(vp.y > time && vp.y < time + 20.0) {\n    float t =  smoothstep(0.0, 0.8,  (1.0 - abs(0.5 - (vp.y - time) / 20.0)) / 3.0  );\n    gl_FragColor = mix(gl_FragColor, vec4(glowColor, 1.0), t * t );\n  }\n  gl_FragColor = mix(gl_FragColor, vec4( glowColor, 1.0 ), a);\n  float b = 0.8;\n  gl_FragColor = gl_FragColor + texture2D( map, vUv );\n}"
  },
  {
    "path": "src/shaders/earth/vertex.vs",
    "content": "\nvarying vec2 vUv;\nvarying vec3 vNormal;\nvarying vec3 vp;\nvarying vec3 vPositionNormal;\nvoid main(void){\n  vUv = uv;\n  vNormal = normalize( normalMatrix * normal ); // 转换到视图空间\n  vp = position;\n  vPositionNormal = normalize(( modelViewMatrix * vec4(position, 1.0) ).xyz);\n  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n}"
  },
  {
    "path": "src/ts/Utils/Sizes.ts",
    "content": "/**\n * 屏幕尺寸\n*/\nimport { EventEmitter } from 'pietile-eventemitter';\nimport { IEvents } from '../interfaces/IEvents';\n\n\ntype options = { dom: HTMLElement }\n\nexport default class Sizes {\n  public width: number\n  public height: number\n  public viewport: {\n    width: number,\n    height: number\n  }\n  public $sizeViewport: HTMLElement\n  public emitter: EventEmitter<IEvents>;\n\n  /**\n   * Constructor\n   */\n  constructor(options: options) {\n\n    this.emitter = new EventEmitter<IEvents>()\n\n    // Viewport size\n    this.$sizeViewport = options.dom\n\n    this.viewport = {\n      width: 0,\n      height: 0\n    }\n\n    // Resize event\n    this.resize = this.resize.bind(this)\n    window.addEventListener('resize', this.resize)\n\n    this.resize()\n  }\n\n  /**\n   * 目前用于监听历史记录执行 historyChange\n   * @param event 事件\n   * @param fun 执行\n   */\n  $on<T extends keyof IEvents>(event: T, fun: () => void) {\n    this.emitter.on(\n      event,\n      () => {\n        fun()\n      }\n    )\n  }\n\n  /**\n   * Resize\n   */\n  resize() {\n    // 可视区域大小\n    this.viewport.width = this.$sizeViewport.offsetWidth\n    this.viewport.height = this.$sizeViewport.offsetHeight\n\n    this.emitter.emit('resize')\n  }\n}\n"
  },
  {
    "path": "src/ts/Utils/arc.ts",
    "content": "\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { ArcCurve, BufferAttribute, BufferGeometry,\nColor, Line, LineBasicMaterial, Points, PointsMaterial, \nQuaternion, Vector3 } from 'three';\nimport { lon2xyz } from './common';\n\n/*\n * 绘制一条圆弧飞线\n * 5个参数含义：( 飞线圆弧轨迹半径, 开始角度, 结束角度)\n */\nfunction createFlyLine(radius, startAngle, endAngle,color) {\n  const geometry = new BufferGeometry(); //声明一个几何体对象BufferGeometry\n  //  ArcCurve创建圆弧曲线\n  const arc = new ArcCurve(0, 0, radius, startAngle, endAngle, false);\n  //getSpacedPoints是基类Curve的方法，返回一个vector2对象作为元素组成的数组\n  const pointsArr = arc.getSpacedPoints(100); //分段数80，返回81个顶点\n  geometry.setFromPoints(pointsArr); // setFromPoints方法从pointsArr中提取数据改变几何体的顶点属性vertices\n  // 每个顶点对应一个百分比数据attributes.percent 用于控制点的渲染大小\n  const percentArr = []; //attributes.percent的数据\n  for (let i = 0; i < pointsArr.length; i++) {\n    percentArr.push(i / pointsArr.length);\n  }\n  const percentAttribue = new BufferAttribute(\n    new Float32Array(percentArr),\n    1\n  );\n  // 通过顶点数据percent点模型从大到小变化，产生小蝌蚪形状飞线\n  geometry.attributes.percent = percentAttribue;\n  // 批量计算所有顶点颜色数据\n  const colorArr = [];\n  for (let i = 0; i < pointsArr.length; i++) {\n    const color1 = new Color(0xec8f43); //轨迹线颜色 青色\n    const color2 = new Color(0xf3ae76); //黄色\n    const color = color1.lerp(color2, i / pointsArr.length);\n    colorArr.push(color.r, color.g, color.b);\n  }\n  // 设置几何体顶点颜色数据\n  geometry.attributes.color = new BufferAttribute(\n    new Float32Array(colorArr),\n    3\n  );\n  const size = 1.3;\n  // 点模型渲染几何体每个顶点\n  const material = new PointsMaterial({\n    size, //点大小\n    // vertexColors: VertexColors, //使用顶点颜色渲染\n    transparent: true,\n    depthWrite: false,\n  });\n  // 修改点材质的着色器源码(注意：不同版本细节可能会稍微会有区别，不过整体思路是一样的)\n  material.onBeforeCompile = function (shader) {\n    // 顶点着色器中声明一个attribute变量:百分比\n    shader.vertexShader = shader.vertexShader.replace(\n      \"void main() {\",\n      [\n        \"attribute float percent;\", //顶点大小百分比变量，控制点渲染大小\n        \"void main() {\",\n      ].join(\"\\n\") // .join()把数组元素合成字符串\n    );\n    // 调整点渲染大小计算方式\n    shader.vertexShader = shader.vertexShader.replace(\n      \"gl_PointSize = size;\",\n      [\"gl_PointSize = percent * size;\"].join(\"\\n\") // .join()把数组元素合成字符串\n    );\n  };\n  const FlyLine = new Points(geometry, material);\n  material.color = new Color(color)\n  FlyLine.name = \"飞行线\";\n\n  return FlyLine;\n}\n\n\n/**输入地球上任意两点的经纬度坐标，通过函数flyArc可以绘制一个飞线圆弧轨迹\n * lon1,lat1:轨迹线起点经纬度坐标\n * lon2,lat2：轨迹线结束点经纬度坐标\n */\nfunction flyArc(radius, lon1, lat1, lon2, lat2,options) {\n  const sphereCoord1 = lon2xyz(radius, lon1, lat1); //经纬度坐标转球面坐标\n  // startSphereCoord：轨迹线起点球面坐标\n  const startSphereCoord = new Vector3(sphereCoord1.x, sphereCoord1.y, sphereCoord1.z);\n  const sphereCoord2 = lon2xyz(radius, lon2, lat2);\n  // startSphereCoord：轨迹线结束点球面坐标\n  const endSphereCoord = new Vector3(sphereCoord2.x, sphereCoord2.y, sphereCoord2.z);\n\n  //计算绘制圆弧需要的关于y轴对称的起点、结束点和旋转四元数\n  const startEndQua = _3Dto2D(startSphereCoord, endSphereCoord)\n  // 调用arcXOY函数绘制一条圆弧飞线轨迹\n  const arcline = arcXOY(radius, startEndQua.startPoint, startEndQua.endPoint,options);\n  arcline.quaternion.multiply(startEndQua.quaternion)\n  return arcline;\n}\n/*\n* 把3D球面上任意的两个飞线起点和结束点绕球心旋转到到XOY平面上，\n* 同时保持关于y轴对称，借助旋转得到的新起点和新结束点绘制\n* 一个圆弧，最后把绘制的圆弧反向旋转到原来的起点和结束点即可\n*/\nfunction _3Dto2D(startSphere, endSphere) {\n  /*计算第一次旋转的四元数：表示从一个平面如何旋转到另一个平面*/\n  const origin = new Vector3(0, 0, 0); //球心坐标\n  const startDir = startSphere.clone().sub(origin); //飞线起点与球心构成方向向量\n  const endDir = endSphere.clone().sub(origin); //飞线结束点与球心构成方向向量\n  // dir1和dir2构成一个三角形，.cross()叉乘计算该三角形法线normal\n  const normal = startDir.clone().cross(endDir).normalize();\n  const xoyNormal = new Vector3(0, 0, 1); //XOY平面的法线\n  //.setFromUnitVectors()计算从normal向量旋转达到xoyNormal向量所需要的四元数\n  // quaternion表示把球面飞线旋转到XOY平面上需要的四元数\n  const quaternion3D_XOY = new Quaternion().setFromUnitVectors(normal, xoyNormal);\n  /*第一次旋转：飞线起点、结束点从3D空间第一次旋转到XOY平面*/\n  const startSphereXOY = startSphere.clone().applyQuaternion(quaternion3D_XOY);\n  const endSphereXOY = endSphere.clone().applyQuaternion(quaternion3D_XOY);\n\n  /*计算第二次旋转的四元数*/\n  // middleV3：startSphereXOY和endSphereXOY的中点\n  const middleV3 = startSphereXOY.clone().add(endSphereXOY).multiplyScalar(0.5);\n  const midDir = middleV3.clone().sub(origin).normalize(); // 旋转前向量midDir，中点middleV3和球心构成的方向向量\n  const yDir = new Vector3(0, 1, 0); // 旋转后向量yDir，即y轴\n  // .setFromUnitVectors()计算从midDir向量旋转达到yDir向量所需要的四元数\n  // quaternion2表示让第一次旋转到XOY平面的起点和结束点关于y轴对称需要的四元数\n  const quaternionXOY_Y = new Quaternion().setFromUnitVectors(midDir, yDir);\n\n  /*第二次旋转：使旋转到XOY平面的点再次旋转，实现关于Y轴对称*/\n  const startSpherXOY_Y = startSphereXOY.clone().applyQuaternion(quaternionXOY_Y);\n  const endSphereXOY_Y = endSphereXOY.clone().applyQuaternion(quaternionXOY_Y);\n\n  /**一个四元数表示一个旋转过程\n   *.invert()方法表示四元数的逆，简单说就是把旋转过程倒过来\n   * 两次旋转的四元数执行.invert()求逆，然后执行.multiply()相乘\n   *新版本.invert()对应旧版本.invert()\n   */\n  const quaternionInverse = quaternion3D_XOY.clone().invert().multiply(quaternionXOY_Y.clone().invert())\n  return {\n    // 返回两次旋转四元数的逆四元数\n    quaternion: quaternionInverse,\n    // 范围两次旋转后在XOY平面上关于y轴对称的圆弧起点和结束点坐标\n    startPoint: startSpherXOY_Y,\n    endPoint: endSphereXOY_Y,\n  }\n}\n/**通过函数arcXOY()可以在XOY平面上绘制一个关于y轴对称的圆弧曲线\n * startPoint, endPoint：表示圆弧曲线的起点和结束点坐标值，起点和结束点关于y轴对称\n * 同时在圆弧轨迹的基础上绘制一段飞线*/\nfunction arcXOY(radius,startPoint, endPoint,options) {\n  // 计算两点的中点\n  const middleV3 = new Vector3().addVectors(startPoint, endPoint).multiplyScalar(0.5);\n  // 弦垂线的方向dir(弦的中点和圆心构成的向量)\n  const dir = middleV3.clone().normalize()\n  // 计算球面飞线的起点、结束点和球心构成夹角的弧度值\n  const earthRadianAngle = radianAOB(startPoint, endPoint, new Vector3(0, 0, 0))\n  /*设置飞线轨迹圆弧的中间点坐标\n  弧度值 * radius * 0.2：表示飞线轨迹圆弧顶部距离地球球面的距离\n  起点、结束点相聚越远，构成的弧线顶部距离球面越高*/\n  const arcTopCoord = dir.multiplyScalar(radius + earthRadianAngle * radius * 0.2) // 黄色飞行线的高度\n  //求三个点的外接圆圆心(飞线圆弧轨迹的圆心坐标)\n  const flyArcCenter = threePointCenter(startPoint, endPoint, arcTopCoord)\n  // 飞线圆弧轨迹半径flyArcR\n  const flyArcR = Math.abs(flyArcCenter.y - arcTopCoord.y);\n  /*坐标原点和飞线起点构成直线和y轴负半轴夹角弧度值\n  参数分别是：飞线圆弧起点、y轴负半轴上一点、飞线圆弧圆心*/\n  const flyRadianAngle = radianAOB(startPoint, new Vector3(0, -1, 0), flyArcCenter);\n  const startAngle = -Math.PI / 2 + flyRadianAngle; //飞线圆弧开始角度\n  const endAngle = Math.PI - startAngle; //飞线圆弧结束角度\n  // 调用圆弧线模型的绘制函数\n  const arcline = circleLine(flyArcCenter.x, flyArcCenter.y, flyArcR, startAngle, endAngle, options.color)\n  // const arcline = new  Group();// 不绘制轨迹线，使用 Group替换circleLine()即可\n  arcline.center = flyArcCenter; //飞线圆弧自定一个属性表示飞线圆弧的圆心\n  arcline.topCoord = arcTopCoord; //飞线圆弧自定一个属性表示飞线圆弧中间也就是顶部坐标\n\n  // const flyAngle = Math.PI/ 10; //飞线圆弧固定弧度\n  const flyAngle = (endAngle - startAngle) / 7; //飞线圆弧的弧度和轨迹线弧度相关\n  // 绘制一段飞线，圆心做坐标原点\n  const flyLine = createFlyLine(flyArcR, startAngle, startAngle + flyAngle, options.flyLineColor);\n  flyLine.position.y = flyArcCenter.y; //平移飞线圆弧和飞线轨迹圆弧重合\n  //飞线段flyLine作为飞线轨迹arcLine子对象，继承飞线轨迹平移旋转等变换\n  arcline.add(flyLine);\n  //飞线段运动范围startAngle~flyEndAngle\n  flyLine.flyEndAngle = endAngle - startAngle - flyAngle;\n  flyLine.startAngle = startAngle;\n  // arcline.flyEndAngle：飞线段当前角度位置，这里设置了一个随机值用于演示\n  flyLine.AngleZ = arcline.flyEndAngle * Math.random();\n  // flyLine.rotation.z = arcline.AngleZ;\n  // arcline.flyLine指向飞线段,便于设置动画是访问飞线段\n  arcline.userData['flyLine'] = flyLine;\n\n  return arcline\n}\n/*计算球面上两点和球心构成夹角的弧度值\n参数point1, point2:表示地球球面上两点坐标Vector3\n计算A、B两点和顶点O构成的AOB夹角弧度值*/\nfunction radianAOB(A, B, O) {\n  // dir1、dir2：球面上两个点和球心构成的方向向量\n  const dir1 = A.clone().sub(O).normalize();\n  const dir2 = B.clone().sub(O).normalize();\n  //点乘.dot()计算夹角余弦值\n  const cosAngle = dir1.clone().dot(dir2);\n  const radianAngle = Math.acos(cosAngle); //余弦值转夹角弧度值,通过余弦值可以计算夹角范围是0~180度\n  return radianAngle\n}\n/*绘制一条圆弧曲线模型Line\n5个参数含义：(圆心横坐标, 圆心纵坐标, 飞线圆弧轨迹半径, 开始角度, 结束角度)*/\nfunction circleLine(x, y, r, startAngle, endAngle,color) {\n  const geometry = new BufferGeometry(); //声明一个几何体对象Geometry\n  //  ArcCurve创建圆弧曲线\n  const arc = new ArcCurve(x, y, r, startAngle, endAngle, false);\n  //getSpacedPoints是基类Curve的方法，返回一个vector2对象作为元素组成的数组\n  const points = arc.getSpacedPoints(80); //分段数50，返回51个顶点\n  geometry.setFromPoints(points); // setFromPoints方法从points中提取数据改变几何体的顶点属性vertices\n  const material = new LineBasicMaterial({\n    color:color || 0xd18547,\n  }); //线条材质\n  const line = new Line(geometry, material); //线条模型对象\n  return line;\n}\n//求三个点的外接圆圆心，p1, p2, p3表示三个点的坐标Vector3。\nfunction threePointCenter(p1, p2, p3) {\n  const L1 = p1.lengthSq(); //p1到坐标原点距离的平方\n  const L2 = p2.lengthSq();\n  const L3 = p3.lengthSq();\n  const x1 = p1.x,\n    y1 = p1.y,\n    x2 = p2.x,\n    y2 = p2.y,\n    x3 = p3.x,\n    y3 = p3.y;\n  const S = x1 * y2 + x2 * y3 + x3 * y1 - x1 * y3 - x2 * y1 - x3 * y2;\n  const x = (L2 * y3 + L1 * y2 + L3 * y1 - L2 * y1 - L3 * y2 - L1 * y3) / S / 2;\n  const y = (L3 * x2 + L2 * x1 + L1 * x3 - L1 * x2 - L2 * x3 - L3 * x1) / S / 2;\n  // 三点外接圆圆心坐标\n  const center = new Vector3(x, y, 0);\n  return center\n}\n\n\nexport {\n  arcXOY,\n  flyArc\n}\n"
  },
  {
    "path": "src/ts/Utils/common.ts",
    "content": "import { CatmullRomCurve3, DoubleSide, Group, Mesh, MeshBasicMaterial, PlaneBufferGeometry, Texture, TubeGeometry, Vector3 } from \"three\";\nimport { punctuation } from \"../world/Earth\";\n\n\n/**\n * 经纬度坐标转球面坐标  \n * @param {地球半径} R  \n * @param {经度(角度值)} longitude \n * @param {维度(角度值)} latitude\n */\nexport const lon2xyz = (R:number, longitude:number, latitude:number): Vector3 => {\n  let lon = longitude * Math.PI / 180; // 转弧度值\n  const lat = latitude * Math.PI / 180; // 转弧度值\n  lon = -lon; // js坐标系z坐标轴对应经度-90度，而不是90度\n\n  // 经纬度坐标转球面坐标计算公式\n  const x = R * Math.cos(lat) * Math.cos(lon);\n  const y = R * Math.sin(lat);\n  const z = R * Math.cos(lat) * Math.sin(lon);\n  // 返回球面坐标\n  return new Vector3(x, y, z);\n}\n\n// 创建波动光圈\nexport const createWaveMesh = (options: { radius, lon, lat, textures }) => {\n  const geometry = new PlaneBufferGeometry(1, 1); //默认在XOY平面上\n  const texture = options.textures.aperture;\n\n  const material = new MeshBasicMaterial({\n    color: 0xe99f68,\n    map: texture,\n    transparent: true, //使用背景透明的png贴图，注意开启透明计算\n    opacity: 1.0,\n    depthWrite: false, //禁止写入深度缓冲区数据\n  });\n  const mesh = new Mesh(geometry, material);\n  // 经纬度转球面坐标\n  const coord = lon2xyz(options.radius * 1.001, options.lon, options.lat);\n  const size = options.radius * 0.12; //矩形平面Mesh的尺寸\n  mesh.scale.set(size, size, size); //设置mesh大小\n  mesh.userData['size'] = size; //自顶一个属性，表示mesh静态大小\n  mesh.userData['scale'] = Math.random() * 1.0; //自定义属性._s表示mesh在原始大小基础上放大倍数  光圈在原来mesh.size基础上1~2倍之间变化\n  mesh.position.set(coord.x, coord.y, coord.z);\n  const coordVec3 = new Vector3(coord.x, coord.y, coord.z).normalize();\n  const meshNormal = new Vector3(0, 0, 1);\n  mesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);\n  return mesh;\n}\n\n// 创建柱状\nexport const createLightPillar = (options: { radius: number, lon: number, lat: number, index: number, textures: Record<string, Texture>, punctuation: punctuation }) => {\n  const height = options.radius * 0.3;\n  const geometry = new PlaneBufferGeometry(options.radius * 0.05, height);\n  geometry.rotateX(Math.PI / 2);\n  geometry.translate(0, 0, height / 2);\n  const material = new MeshBasicMaterial({\n    map: options.textures.light_column,\n    color:\n      options.index == 0\n        ? options.punctuation.lightColumn.startColor\n        : options.punctuation.lightColumn.endColor,\n    transparent: true,\n    side: DoubleSide,\n    depthWrite: false, //是否对深度缓冲区有任何的影响\n  });\n  const mesh = new Mesh(geometry, material);\n  const group = new Group();\n  // 两个光柱交叉叠加\n  group.add(mesh, mesh.clone().rotateZ(Math.PI / 2)); //几何体绕x轴旋转了，所以mesh旋转轴变为z\n  // 经纬度转球面坐标\n  const SphereCoord = lon2xyz(options.radius, options.lon, options.lat); //SphereCoord球面坐标\n  group.position.set(SphereCoord.x, SphereCoord.y, SphereCoord.z); //设置mesh位置\n  const coordVec3 = new Vector3(\n    SphereCoord.x,\n    SphereCoord.y,\n    SphereCoord.z\n  ).normalize();\n  const meshNormal = new Vector3(0, 0, 1);\n  group.quaternion.setFromUnitVectors(meshNormal, coordVec3);\n  return group;\n}\n\n// 光柱底座矩形平面\nexport const createPointMesh = (options: {\n  radius: number, lon: number,\n  lat: number, material: MeshBasicMaterial\n}) => {\n\n  const geometry = new PlaneBufferGeometry(1, 1); //默认在XOY平面上\n  const mesh = new Mesh(geometry, options.material);\n  // 经纬度转球面坐标\n  const coord = lon2xyz(options.radius * 1.001, options.lon, options.lat);\n  const size = options.radius * 0.05; // 矩形平面Mesh的尺寸\n  mesh.scale.set(size, size, size); // 设置mesh大小\n\n  // 设置mesh位置\n  mesh.position.set(coord.x, coord.y, coord.z);\n  const coordVec3 = new Vector3(coord.x, coord.y, coord.z).normalize();\n  const meshNormal = new Vector3(0, 0, 1);\n  mesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);\n  return mesh;\n\n}\n\n// 获取点\nexport const getCirclePoints = (option) => {\n  const list = [];\n  for (\n    let j = 0;\n    j < 2 * Math.PI - 0.1;\n    j += (2 * Math.PI) / (option.number || 100)\n  ) {\n    list.push([\n      parseFloat((Math.cos(j) * (option.radius || 10)).toFixed(2)),\n      0,\n      parseFloat((Math.sin(j) * (option.radius || 10)).toFixed(2)),\n    ]);\n  }\n  if (option.closed) list.push(list[0]);\n  return list;\n}\n\n// 创建线\n\n/**\n * 创建动态的线\n */\nexport const createAnimateLine = (option) => {\n  // 由多个点数组构成的曲线 通常用于道路\n  const l = [];\n  option.pointList.forEach((e) =>\n    l.push(new Vector3(e[0], e[1], e[2]))\n  );\n  const curve = new CatmullRomCurve3(l); // 曲线路径\n\n  // 管道体\n  const tubeGeometry = new TubeGeometry(\n    curve,\n    option.number || 50,\n    option.radius || 1,\n    option.radialSegments\n  );\n  return new Mesh(tubeGeometry, option.material);\n}"
  },
  {
    "path": "src/ts/global.d.ts",
    "content": "// Definitions to let TS understand .vs, .fs, .glsl shader files\n/**\n * 声明着色器\n */\n\ndeclare module '*.fs' {\n\tconst value: string\n\texport default value\n}\ndeclare module '*.vs' {\n\tconst value: string\n\texport default value\n}\ndeclare module '*.glsl' {\n\tconst value: string\n\texport default value\n}"
  },
  {
    "path": "src/ts/index.ts",
    "content": "import World  from './world/Word'\n\n// earth-canvas\nconst dom: HTMLElement = document.querySelector('#earth-canvas')\nnew World({\n  dom,\n})"
  },
  {
    "path": "src/ts/interfaces/IEvents.ts",
    "content": "\nexport interface IEvents {\n  resize: () => void\n}\n"
  },
  {
    "path": "src/ts/interfaces/IWord.ts",
    "content": "export interface IWord {\n  dom: HTMLElement\n}"
  },
  {
    "path": "src/ts/world/Assets.ts",
    "content": "/**\n * 资源文件\n * 把模型和图片分开进行加载\n */\n\ninterface ITextures {\n  name: string\n  url: string\n}\n\nexport interface IResources {\n  textures?: ITextures[],\n}\n\nconst filePath = './images/earth/'\nconst fileSuffix = [\n  'gradient',\n  'redCircle',\n  \"label\",\n  \"aperture\",\n  'glow',\n  'light_column',\n  'aircraft'\n]\n\nconst textures = fileSuffix.map(item => {\n  return {\n    name: item,\n    url: filePath + item + '.png'\n  }\n})\n\ntextures.push({\n  name: 'earth',\n  url: filePath + 'earth.jpg'\n})\n\nconst resources: IResources = {\n  textures\n}\n\n\nexport {\n  resources\n}"
  },
  {
    "path": "src/ts/world/Basic.ts",
    "content": "/**\n * 创建 threejs 四大天王\n * 场景、相机、渲染器、控制器\n */\n\nimport * as THREE from 'three';\nimport {\n  OrbitControls\n} from \"three/examples/jsm/controls/OrbitControls\";\n\nexport class Basic {\n  public scene: THREE.Scene;\n  public camera: THREE.PerspectiveCamera;\n  public renderer: THREE.WebGLRenderer\n  public controls: OrbitControls;\n  public dom: HTMLElement;\n\n  constructor(dom: HTMLElement) {\n    this.dom = dom\n    this.initScenes()\n    this.setControls()\n  }\n\n  /**\n   * 初始化场景\n   */\n  initScenes() {\n    this.scene = new THREE.Scene();\n\n    this.camera = new THREE.PerspectiveCamera(\n      45,\n      window.innerWidth / window.innerHeight,\n      1,\n      100000\n    );\n    this.camera.position.set(0, 30, -250)\n\n\n    this.renderer = new THREE.WebGLRenderer({\n      alpha: true, // 透明\n      antialias: true, // 抗锯齿\n    });\n    this.renderer.setPixelRatio(window.devicePixelRatio); // 设置屏幕像素比\n    this.renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染器宽高\n    this.dom.appendChild(this.renderer.domElement); // 添加到dom中\n  }\n\n  /**\n   * 设置控制器\n   */\n  setControls() {\n    // 鼠标控制      相机，渲染dom\n    this.controls = new OrbitControls(this.camera, this.renderer.domElement);\n    \n    this.controls.autoRotateSpeed = 3\n    // 使动画循环使用时阻尼或自转 意思是否有惯性\n    this.controls.enableDamping = true;\n    // 动态阻尼系数 就是鼠标拖拽旋转灵敏度\n    this.controls.dampingFactor = 0.05;\n    // 是否可以缩放\n    this.controls.enableZoom = true;\n    // 设置相机距离原点的最远距离\n    this.controls.minDistance = 100;\n    // 设置相机距离原点的最远距离\n    this.controls.maxDistance = 300;\n    // 是否开启右键拖拽\n    this.controls.enablePan = false;\n  }\n}"
  },
  {
    "path": "src/ts/world/Data.ts",
    "content": "export default [{\n  startArray: {\n    name: '杭州',\n    N: 30.246026,\n    E: 120.210792,\n  },\n  endArray: [{\n      name: '曼谷',\n      N: 22, //维度\n      E: 100.49074172973633, //经度\n    },\n    {\n      name: '澳大利亚',\n      N: -23.68477416688374,\n      E: 133.857421875,\n    },\n\n    {\n      name: '新疆维吾尔自治区',\n      N: 41.748,\n      E: 84.9023,\n    },\n\n    {\n      name: '德黑兰',\n      N: 35,\n      E: 51,\n    },\n    {\n      name: '德黑兰',\n      N: 35,\n      E: 51,\n    },\n    {\n      name: '美国',\n      N: 34.125447565116126,\n      E: 241.7431640625,\n    },\n    {\n      name: '英国',\n      N: 51.508742458803326,\n      E: 359.82421875,\n    },\n    {\n      name: '巴西',\n      N:  -9.96885060854611,\n      E: 668.1445312499999,\n    },\n  ]\n},\n{\n  startArray: {\n    name: '北京',\n    N: 39.89491,\n    E: 116.322056,\n  },\n  endArray: [{\n      name: '西藏',\n      N: 29.660361, //维度\n      E: 91.132212 //经度\n    },\n    {\n      name: '广西',\n      N: 22.830824,\n      E: 108.30616\n    },\n\n    {\n      name: '江西',\n      N: 28.676493,\n      E: 115.892151\n    },\n\n    {\n      name: '贵阳',\n      N: 26.647661,\n      E: 106.630153\n    }\n  ]\n}\n\n]\n"
  },
  {
    "path": "src/ts/world/Earth.ts",
    "content": "import {\n  BufferAttribute, BufferGeometry, Color, DoubleSide, Group, Material, Mesh, MeshBasicMaterial, NormalBlending,\n  Object3D,\n  Points, PointsMaterial, ShaderMaterial,\n  SphereBufferGeometry, Sprite, SpriteMaterial, Texture, TextureLoader, Vector3\n} from \"three\";\n\nimport html2canvas from \"html2canvas\";\n\nimport earthVertex from '../../shaders/earth/vertex.vs';\nimport earthFragment from '../../shaders/earth/fragment.fs';\nimport { createAnimateLine, createLightPillar, createPointMesh, createWaveMesh, getCirclePoints, lon2xyz } from \"../Utils/common\";\nimport gsap from \"gsap\";\nimport { flyArc } from \"../Utils/arc\";\n\nexport type punctuation = {\n  circleColor: number,\n  lightColumn: {\n    startColor: number, // 起点颜色\n    endColor: number, // 终点颜色\n  },\n}\n\ntype options = {\n  data: {\n    startArray: {\n      name: string,\n      E: number, // 经度\n      N: number, // 维度\n    },\n    endArray: {\n      name: string,\n      E: number, // 经度\n      N: number, // 维度\n    }[]\n  }[]\n  dom: HTMLElement,\n  textures: Record<string, Texture>, // 贴图\n  earth: {\n    radius: number, // 地球半径\n    rotateSpeed: number, // 地球旋转速度\n    isRotation: boolean // 地球组是否自转\n  }\n  satellite: {\n    show: boolean, // 是否显示卫星\n    rotateSpeed: number, // 旋转速度\n    size: number, // 卫星大小\n    number: number, // 一个圆环几个球\n  },\n  punctuation: punctuation,\n  flyLine: {\n    color: number, // 飞线的颜色\n    speed: number, // 飞机拖尾线速度\n    flyLineColor: number // 飞行线的颜色\n  },\n}\ntype uniforms = {\n  glowColor: { value: Color; }\n  scale: { type: string; value: number; }\n  bias: { type: string; value: number; }\n  power: { type: string; value: number; }\n  time: { type: string; value: any; }\n  isHover: { value: boolean; };\n  map: { value: Texture }\n}\n\nexport default class earth {\n\n  public group: Group;\n  public earthGroup: Group;\n\n  public around: BufferGeometry\n  public aroundPoints: Points<BufferGeometry, PointsMaterial>;\n\n  public options: options;\n  public uniforms: uniforms\n  public timeValue: number;\n\n  public earth: Mesh<SphereBufferGeometry, ShaderMaterial>;\n  public punctuationMaterial: MeshBasicMaterial;\n  public markupPoint: Group;\n  public waveMeshArr: Object3D[];\n\n  public circleLineList: any[];\n  public circleList: any[];\n  public x: number;\n  public n: number;\n  public isRotation: boolean;\n  public flyLineArcGroup: Group;\n\n  constructor(options: options) {\n\n    this.options = options;\n\n    this.group = new Group()\n    this.group.name = \"group\";\n    this.group.scale.set(0, 0, 0)\n    this.earthGroup = new Group()\n    this.group.add(this.earthGroup)\n    this.earthGroup.name = \"EarthGroup\";\n\n    // 标注点效果\n    this.markupPoint = new Group()\n    this.markupPoint.name = \"markupPoint\"\n    this.waveMeshArr = []\n\n    // 卫星和标签\n    this.circleLineList = []\n    this.circleList = [];\n    this.x = 0;\n    this.n = 0;\n\n    // 地球自转\n    this.isRotation = this.options.earth.isRotation\n\n    // 扫光动画 shader\n    this.timeValue = 100\n    this.uniforms = {\n      glowColor: {\n        value: new Color(0x0cd1eb),\n      },\n      scale: {\n        type: \"f\",\n        value: -1.0,\n      },\n      bias: {\n        type: \"f\",\n        value: 1.0,\n      },\n      power: {\n        type: \"f\",\n        value: 3.3,\n      },\n      time: {\n        type: \"f\",\n        value: this.timeValue,\n      },\n      isHover: {\n        value: false,\n      },\n      map: {\n        value: null,\n      },\n    };\n\n  }\n\n  async init(): Promise<void> {\n    return new Promise(async (resolve) => {\n\n      this.createEarth(); // 创建地球\n      this.createStars(); // 添加星星\n      this.createEarthGlow() // 创建地球辉光\n      this.createEarthAperture() // 创建地球的大气层\n      await this.createMarkupPoint() // 创建柱状点位\n      await this.createSpriteLabel() // 创建标签\n      this.createAnimateCircle() // 创建环绕卫星\n      this.createFlyLine() // 创建飞线\n\n      this.show()\n      resolve()\n    })\n  }\n\n  createEarth() {\n    const earth_geometry = new SphereBufferGeometry(\n      this.options.earth.radius,\n      50,\n      50\n    );\n\n    const earth_border = new SphereBufferGeometry(\n      this.options.earth.radius + 10,\n      60,\n      60\n    );\n\n    const pointMaterial = new PointsMaterial({\n      color: 0x81ffff, //设置颜色，默认 0xFFFFFF\n      transparent: true,\n      sizeAttenuation: true,\n      opacity: 0.1,\n      vertexColors: false, //定义材料是否使用顶点颜色，默认false ---如果该选项设置为true，则color属性失效\n      size: 0.01, //定义粒子的大小。默认为1.0\n    })\n    const points = new Points(earth_border, pointMaterial); //将模型添加到场景\n\n    this.earthGroup.add(points);\n\n    this.uniforms.map.value = this.options.textures.earth;\n\n    const earth_material = new ShaderMaterial({\n      // wireframe:true, // 显示模型线条\n      uniforms: this.uniforms,\n      vertexShader: earthVertex,\n      fragmentShader: earthFragment,\n    });\n\n    earth_material.needsUpdate = true;\n    this.earth = new Mesh(earth_geometry, earth_material);\n    this.earth.name = \"earth\";\n    this.earthGroup.add(this.earth);\n\n  }\n\n  createStars() {\n\n    const vertices = []\n    const colors = []\n    for (let i = 0; i < 500; i++) {\n      const vertex = new Vector3();\n      vertex.x = 800 * Math.random() - 300;\n      vertex.y = 800 * Math.random() - 300;\n      vertex.z = 800 * Math.random() - 300;\n      vertices.push(vertex.x, vertex.y, vertex.z);\n      colors.push(new Color(1, 1, 1));\n    }\n\n    // 星空效果\n    this.around = new BufferGeometry()\n    this.around.setAttribute(\"position\", new BufferAttribute(new Float32Array(vertices), 3));\n    this.around.setAttribute(\"color\", new BufferAttribute(new Float32Array(colors), 3));\n\n    const aroundMaterial = new PointsMaterial({\n      size: 2,\n      sizeAttenuation: true, // 尺寸衰减\n      color: 0x4d76cf,\n      transparent: true,\n      opacity: 1,\n      map: this.options.textures.gradient,\n    });\n\n    this.aroundPoints = new Points(this.around, aroundMaterial);\n    this.aroundPoints.name = \"星空\";\n    this.aroundPoints.scale.set(1, 1, 1);\n    this.group.add(this.aroundPoints);\n  }\n\n  createEarthGlow() {\n    const R = this.options.earth.radius; //地球半径\n\n    // TextureLoader创建一个纹理加载器对象，可以加载图片作为纹理贴图\n    const texture = this.options.textures.glow; // 加载纹理贴图\n\n    // 创建精灵材质对象SpriteMaterial\n    const spriteMaterial = new SpriteMaterial({\n      map: texture, // 设置精灵纹理贴图\n      color: 0x4390d1,\n      transparent: true, //开启透明\n      opacity: 0.7, // 可以通过透明度整体调节光圈\n      depthWrite: false, //禁止写入深度缓冲区数据\n    });\n\n    // 创建表示地球光圈的精灵模型\n    const sprite = new Sprite(spriteMaterial);\n    sprite.scale.set(R * 3.0, R * 3.0, 1); //适当缩放精灵\n    this.earthGroup.add(sprite);\n  }\n\n  createEarthAperture() {\n\n    const vertexShader = [\n      \"varying vec3\tvVertexWorldPosition;\",\n      \"varying vec3\tvVertexNormal;\",\n      \"varying vec4\tvFragColor;\",\n      \"void main(){\",\n      \"\tvVertexNormal\t= normalize(normalMatrix * normal);\", //将法线转换到视图坐标系中\n      \"\tvVertexWorldPosition\t= (modelMatrix * vec4(position, 1.0)).xyz;\", //将顶点转换到世界坐标系中\n      \"\t// set gl_Position\",\n      \"\tgl_Position\t= projectionMatrix * modelViewMatrix * vec4(position, 1.0);\",\n      \"}\",\n    ].join(\"\\n\");\n\n    //大气层效果\n    const AeroSphere = {\n      uniforms: {\n        coeficient: {\n          type: \"f\",\n          value: 1.0,\n        },\n        power: {\n          type: \"f\",\n          value: 3,\n        },\n        glowColor: {\n          type: \"c\",\n          value: new Color(0x4390d1),\n        },\n      },\n      vertexShader: vertexShader,\n      fragmentShader: [\n        \"uniform vec3\tglowColor;\",\n        \"uniform float\tcoeficient;\",\n        \"uniform float\tpower;\",\n\n        \"varying vec3\tvVertexNormal;\",\n        \"varying vec3\tvVertexWorldPosition;\",\n\n        \"varying vec4\tvFragColor;\",\n\n        \"void main(){\",\n        \"\tvec3 worldCameraToVertex = vVertexWorldPosition - cameraPosition;\", //世界坐标系中从相机位置到顶点位置的距离\n        \"\tvec3 viewCameraToVertex\t= (viewMatrix * vec4(worldCameraToVertex, 0.0)).xyz;\", //视图坐标系中从相机位置到顶点位置的距离\n        \"\tviewCameraToVertex= normalize(viewCameraToVertex);\", //规一化\n        \"\tfloat intensity\t= pow(coeficient + dot(vVertexNormal, viewCameraToVertex), power);\",\n        \"\tgl_FragColor = vec4(glowColor, intensity);\",\n        \"}\",\n      ].join(\"\\n\"),\n    };\n    //球体 辉光 大气层\n    const material1 = new ShaderMaterial({\n      uniforms: AeroSphere.uniforms,\n      vertexShader: AeroSphere.vertexShader,\n      fragmentShader: AeroSphere.fragmentShader,\n      blending: NormalBlending,\n      transparent: true,\n      depthWrite: false,\n    });\n    const sphere = new SphereBufferGeometry(\n      this.options.earth.radius + 0,\n      50,\n      50\n    );\n    const mesh = new Mesh(sphere, material1);\n    this.earthGroup.add(mesh);\n  }\n\n  async createMarkupPoint() {\n\n    await Promise.all(this.options.data.map(async (item) => {\n\n      const radius = this.options.earth.radius;\n      const lon = item.startArray.E; //经度\n      const lat = item.startArray.N; //纬度\n\n      this.punctuationMaterial = new MeshBasicMaterial({\n        color: this.options.punctuation.circleColor,\n        map: this.options.textures.label,\n        transparent: true, //使用背景透明的png贴图，注意开启透明计算\n        depthWrite: false, //禁止写入深度缓冲区数据\n      });\n\n      const mesh = createPointMesh({ radius, lon, lat, material: this.punctuationMaterial }); //光柱底座矩形平面\n      this.markupPoint.add(mesh);\n      const LightPillar = createLightPillar({\n        radius: this.options.earth.radius,\n        lon,\n        lat,\n        index: 0,\n        textures: this.options.textures,\n        punctuation: this.options.punctuation,\n      }); //光柱\n      this.markupPoint.add(LightPillar);\n      const WaveMesh = createWaveMesh({ radius, lon, lat, textures: this.options.textures }); //波动光圈\n      this.markupPoint.add(WaveMesh);\n      this.waveMeshArr.push(WaveMesh);\n\n      await Promise.all(item.endArray.map((obj) => {\n        const lon = obj.E; //经度\n        const lat = obj.N; //纬度\n        const mesh = createPointMesh({ radius, lon, lat, material: this.punctuationMaterial }); //光柱底座矩形平面\n        this.markupPoint.add(mesh);\n        const LightPillar = createLightPillar({\n          radius: this.options.earth.radius,\n          lon,\n          lat,\n          index: 1,\n          textures: this.options.textures,\n          punctuation: this.options.punctuation\n        }); //光柱\n        this.markupPoint.add(LightPillar);\n        const WaveMesh = createWaveMesh({ radius, lon, lat, textures: this.options.textures }); //波动光圈\n        this.markupPoint.add(WaveMesh);\n        this.waveMeshArr.push(WaveMesh);\n      }))\n      this.earthGroup.add(this.markupPoint)\n    }))\n  }\n\n  async createSpriteLabel() {\n    await Promise.all(this.options.data.map(async item => {\n      let cityArry = [];\n      cityArry.push(item.startArray);\n      cityArry = cityArry.concat(...item.endArray);\n      await Promise.all(cityArry.map(async e => {\n        const p = lon2xyz(this.options.earth.radius * 1.001, e.E, e.N);\n        const div = `<div class=\"fire-div\">${e.name}</div>`;\n        const shareContent = document.getElementById(\"html2canvas\");\n        shareContent.innerHTML = div;\n        const opts = {\n          backgroundColor: null, // 背景透明\n          scale: 6,\n          dpi: window.devicePixelRatio,\n        };\n        const canvas = await html2canvas(document.getElementById(\"html2canvas\"), opts)\n        const dataURL = canvas.toDataURL(\"image/png\");\n        const map = new TextureLoader().load(dataURL);\n        const material = new SpriteMaterial({\n          map: map,\n          transparent: true,\n        });\n        const sprite = new Sprite(material);\n        const len = 5 + (e.name.length - 2) * 2;\n        sprite.scale.set(len, 3, 1);\n        sprite.position.set(p.x * 1.1, p.y * 1.1, p.z * 1.1);\n        this.earth.add(sprite);\n      }))\n    }))\n  }\n\n  createAnimateCircle() {\n    // 创建 圆环 点\n    const list = getCirclePoints({\n      radius: this.options.earth.radius + 15,\n      number: 150, //切割数\n      closed: true, // 闭合\n    });\n    const mat = new MeshBasicMaterial({\n      color: \"#0c3172\",\n      transparent: true,\n      opacity: 0.4,\n      side: DoubleSide,\n    });\n    const line = createAnimateLine({\n      pointList: list,\n      material: mat,\n      number: 100,\n      radius: 0.1,\n    });\n    this.earthGroup.add(line);\n\n    // 在clone两条线出来\n    const l2 = line.clone();\n    l2.scale.set(1.2, 1.2, 1.2);\n    l2.rotateZ(Math.PI / 6);\n    this.earthGroup.add(l2);\n\n    const l3 = line.clone();\n    l3.scale.set(0.8, 0.8, 0.8);\n    l3.rotateZ(-Math.PI / 6);\n    this.earthGroup.add(l3);\n\n    /**\n     * 旋转的球\n     */\n    const ball = new Mesh(\n      new SphereBufferGeometry(this.options.satellite.size, 32, 32),\n      new MeshBasicMaterial({\n        color: \"#e0b187\", // 745F4D\n      })\n    );\n\n    const ball2 = new Mesh(\n      new SphereBufferGeometry(this.options.satellite.size, 32, 32),\n      new MeshBasicMaterial({\n        color: \"#628fbb\", // 324A62\n      })\n    );\n\n    const ball3 = new Mesh(\n      new SphereBufferGeometry(this.options.satellite.size, 32, 32),\n      new MeshBasicMaterial({\n        color: \"#806bdf\", //6D5AC4\n      })\n    );\n\n    this.circleLineList.push(line, l2, l3);\n    ball.name = ball2.name = ball3.name = \"卫星\";\n\n    for (let i = 0; i < this.options.satellite.number; i++) {\n      const ball01 = ball.clone();\n      // 一根线上总共有几个球，根据数量平均分布一下\n      const num = Math.floor(list.length / this.options.satellite.number)\n      ball01.position.set(\n        list[num * (i + 1)][0] * 1,\n        list[num * (i + 1)][1] * 1,\n        list[num * (i + 1)][2] * 1\n      );\n      line.add(ball01);\n\n      const ball02 = ball2.clone();\n      const num02 = Math.floor(list.length / this.options.satellite.number)\n      ball02.position.set(\n        list[num02 * (i + 1)][0] * 1,\n        list[num02 * (i + 1)][1] * 1,\n        list[num02 * (i + 1)][2] * 1\n      );\n      l2.add(ball02);\n\n      const ball03 = ball2.clone();\n      const num03 = Math.floor(list.length / this.options.satellite.number)\n      ball03.position.set(\n        list[num03 * (i + 1)][0] * 1,\n        list[num03 * (i + 1)][1] * 1,\n        list[num03 * (i + 1)][2] * 1\n      );\n      l3.add(ball03);\n    }\n  }\n\n  createFlyLine() {\n\n    this.flyLineArcGroup = new Group();\n    this.flyLineArcGroup.userData['flyLineArray'] = []\n    this.earthGroup.add(this.flyLineArcGroup)\n\n    this.options.data.forEach((cities) => {\n      cities.endArray.forEach(item => {\n\n        // 调用函数flyArc绘制球面上任意两点之间飞线圆弧轨迹\n        const arcline = flyArc(\n          this.options.earth.radius,\n          cities.startArray.E,\n          cities.startArray.N,\n          item.E,\n          item.N,\n          this.options.flyLine\n        );\n\n        this.flyLineArcGroup.add(arcline); // 飞线插入flyArcGroup中\n        this.flyLineArcGroup.userData['flyLineArray'].push(arcline.userData['flyLine'])\n      });\n\n    })\n\n  }\n\n  show() {\n    gsap.to(this.group.scale, {\n      x: 1,\n      y: 1,\n      z: 1,\n      duration: 2,\n      ease: \"Quadratic\",\n    })\n  }\n\n  render() {\n\n    this.flyLineArcGroup?.userData['flyLineArray']?.forEach(fly => {\n      fly.rotation.z += this.options.flyLine.speed; // 调节飞线速度\n      if (fly.rotation.z >= fly.flyEndAngle) fly.rotation.z = 0;\n    })\n\n    if (this.isRotation) {\n      this.earthGroup.rotation.y += this.options.earth.rotateSpeed;\n    }\n\n    this.circleLineList.forEach((e) => {\n      e.rotateY(this.options.satellite.rotateSpeed);\n    });\n\n    this.uniforms.time.value =\n      this.uniforms.time.value < -this.timeValue\n        ? this.timeValue\n        : this.uniforms.time.value - 1;\n\n    if (this.waveMeshArr.length) {\n      this.waveMeshArr.forEach((mesh: Mesh) => {\n        mesh.userData['scale'] += 0.007;\n        mesh.scale.set(\n          mesh.userData['size'] * mesh.userData['scale'],\n          mesh.userData['size'] * mesh.userData['scale'],\n          mesh.userData['size'] * mesh.userData['scale']\n        );\n        if (mesh.userData['scale'] <= 1.5) {\n          (mesh.material as Material).opacity = (mesh.userData['scale'] - 1) * 2; //2等于1/(1.5-1.0)，保证透明度在0~1之间变化\n        } else if (mesh.userData['scale'] > 1.5 && mesh.userData['scale'] <= 2) {\n          (mesh.material as Material).opacity = 1 - (mesh.userData['scale'] - 1.5) * 2; //2等于1/(2.0-1.5) mesh缩放2倍对应0 缩放1.5被对应1\n        } else {\n          mesh.userData['scale'] = 1;\n        }\n      });\n    }\n\n  }\n\n}"
  },
  {
    "path": "src/ts/world/Resources.ts",
    "content": "/**\n * 资源管理和加载\n */\nimport { LoadingManager, Texture, TextureLoader } from 'three';\nimport { resources } from './Assets'\nexport class Resources {\n  private manager: LoadingManager\n  private callback: () => void;\n  private textureLoader!: InstanceType<typeof TextureLoader>;\n  public textures: Record<string, Texture>;\n  constructor(callback: () => void) {\n    this.callback = callback // 资源加载完成的回调\n\n    this.textures = {} // 贴图对象\n\n    this.setLoadingManager()\n    this.loadResources()\n  }\n\n  /**\n   * 管理加载状态\n   */\n  private setLoadingManager() {\n\n    this.manager = new LoadingManager()\n    // 开始加载\n    this.manager.onStart = () => {\n      console.log('开始加载资源文件')\n    }\n    // 加载完成\n    this.manager.onLoad = () => {\n      this.callback()\n    }\n    // 正在进行中\n    this.manager.onProgress = (url) => {\n      console.log(`正在加载：${url}`)\n    }\n\n    this.manager.onError = url => {\n      console.log('加载失败：' + url)\n    }\n\n  }\n\n  /**\n   * 加载资源\n   */\n  private loadResources(): void {\n    this.textureLoader = new TextureLoader(this.manager)\n    resources.textures?.forEach((item) => {\n      this.textureLoader.load(item.url, (t) => {\n        this.textures[item.name] = t\n      })\n    })\n  }\n}\n"
  },
  {
    "path": "src/ts/world/Word.ts",
    "content": "import {\n  MeshBasicMaterial, PerspectiveCamera,\n  Scene, ShaderMaterial, WebGLRenderer\n} from \"three\";\nimport {\n  OrbitControls\n} from \"three/examples/jsm/controls/OrbitControls\";\n\n// interfaces\nimport { IWord } from '../interfaces/IWord'\n\nimport { Basic } from './Basic'\nimport Sizes from '../Utils/Sizes'\nimport { Resources } from './Resources';\n\n// earth \nimport Earth from './Earth'\nimport Data from './Data'\n\nexport default class World {\n  public basic: Basic;\n  public scene: Scene;\n  public camera: PerspectiveCamera;\n  public renderer: WebGLRenderer\n  public controls: OrbitControls;\n  public sizes: Sizes;\n  public material: ShaderMaterial | MeshBasicMaterial;\n  public resources: Resources;\n  public option: IWord;\n  public earth: Earth;\n\n\n  constructor(option: IWord) {\n    /**\n     * 加载资源\n     */\n    this.option = option\n\n    this.basic = new Basic(option.dom)\n    this.scene = this.basic.scene\n    this.renderer = this.basic.renderer\n    this.controls = this.basic.controls\n    this.camera = this.basic.camera\n\n    this.sizes = new Sizes({ dom: option.dom })\n\n    this.sizes.$on('resize', () => {\n      this.renderer.setSize(Number(this.sizes.viewport.width), Number(this.sizes.viewport.height))\n      this.camera.aspect = Number(this.sizes.viewport.width) / Number(this.sizes.viewport.height)\n      this.camera.updateProjectionMatrix()\n    })\n\n    this.resources = new Resources(async () => {\n      await this.createEarth()\n      // 开始渲染\n      this.render()\n    })\n  }\n\n  async createEarth() {\n\n    // 资源加载完成，开始制作地球，注释在new Earth()类型里面\n    this.earth = new Earth({\n      data: Data,\n      dom: this.option.dom,\n      textures: this.resources.textures,\n      earth: {\n        radius: 50,\n        rotateSpeed: 0.002,\n        isRotation: true\n      },\n      satellite: {\n        show: true,\n        rotateSpeed: -0.01,\n        size: 1,\n        number: 2\n      },\n      punctuation: {\n        circleColor: 0x3892ff,\n        lightColumn: {\n          startColor: 0xe4007f, // 起点颜色\n          endColor: 0xffffff, // 终点颜色\n        },\n      },\n      flyLine: {\n        color: 0xf3ae76, // 飞线的颜色\n        flyLineColor: 0xff7714, // 飞行线的颜色\n        speed: 0.004, // 拖尾飞线的速度\n      }\n    })\n\n    this.scene.add(this.earth.group)\n\n    await this.earth.init()\n\n    // 隐藏dom\n    const loading = document.querySelector('#loading')\n    loading.classList.add('out')\n\n  }\n\n  /**\n   * 渲染函数\n   */\n  public render() {\n    requestAnimationFrame(this.render.bind(this))\n    this.renderer.render(this.scene, this.camera)\n    this.controls && this.controls.update()\n    this.earth && this.earth.render()\n  }\n}"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"include\": [\n      \"src/ts/**/*\"\n  ],\n  \"compilerOptions\": {\n      \"module\": \"ESNext\",\n      \"target\": \"ESNext\",\n      \"moduleResolution\": \"node\",\n      \"declaration\": true,\n      \"declarationDir\": \"build/types\",\n      \"baseUrl\": \".\",\n      \"noImplicitAny\": false\n  }\n}"
  },
  {
    "path": "tslint.json",
    "content": "{\n    \"defaultSeverity\": \"error\",\n    \"extends\": [\n        \"tslint:recommended\"\n    ],\n    \"jsRules\": {},\n    \"rules\": {\n\t\t\"one-line\": false,\n\t\t\"no-empty\": false,\n\t\t\"no-console\": false,\n\t\t\"indent\": [true, \"tabs\"],\n\t\t\"object-literal-shorthand\": false,\n        \"quotemark\": [true, \"single\"],\n        \"prefer-const\": false,\n        \"eofline\": false,\n        \"max-line-length\": false,\n        \"no-trailing-whitespace\": false,\n        \"ordered-imports\": false,\n        \"import-spacing\": false,\n        \"trailing-comma\": false,\n        \"curly\": false,\n        \"object-literal-sort-keys\": false,\n        \"object-literal-key-quotes\": false,\n        \"variable-name\": false,\n        \"typedef\": [\n            true,\n            \"call-signature\",\n            \"parameter\",\n            \"property-declaration\",\n            \"variable-declaration-ignore-function\",\n            \"member-variable-declaration\",\n            \"object-destructuring\",\n            \"array-destructuring\"\n          ]\n    },\n    \"linterOptions\": {\n        \"exclude\": [\n            \"build\"\n        ]\n    },\n    \"rulesDirectory\": []\n}"
  },
  {
    "path": "webpack.config.js",
    "content": "const path = require('path')\n// 引入html插件\nconst HTMLWebpackPlugin = require('html-webpack-plugin')\n// 把整个目录copy过去\nconst CopyWebpackPlugin = require('copy-webpack-plugin')\n// 引入clean插件\nconst { CleanWebpackPlugin } = require('clean-webpack-plugin')\n\nconst ESLintPlugin = require('eslint-webpack-plugin')\n\n// webpack中的所有的配置信息都应该写在module.exports中\nmodule.exports = {\n  devServer: {\n    port: '8088'\n  },\n  // 指定入口文件\n  entry: './src/ts/index.ts',\n  // 指定打包文件所在目录\n  output: {\n    // 指定打包文件的目录\n    path: path.resolve(__dirname, 'dist'),\n    // 打包后文件的文件\n    filename: 'bundle.js',\n    libraryTarget: 'umd',\n    // 告诉webpack不使用箭头\n    // 默认打包后是一个立即执行的箭头函数，在IE 11中也是无法执行的！\n    // 加上下面的配置，可以在webpack打包时，最外层不再是箭头函数\n    // webpack新版本已经不想兼容IE了！233\n    environment: {\n      arrowFunction: false,\n    },\n  },\n  // 指定webpack打包时要使用模块\n  module: {\n    // 指定要加载的规则\n    rules: [\n      {\n        // test指定的是规则生效的文件\n        test: /\\.ts$/,\n        // 要使用的loader\n        // Webpack在加载时是\"从后向前\"加载！\n        use: [\n          // 配置babel\n          {\n            // 指定加载器\n            loader: 'babel-loader',\n            // 设置babel\n            options: {\n              // 设置预定义的环境\n              presets: [\n                [\n                  // 指定环境的插件\n                  '@babel/preset-env',\n                  // 配置信息\n                  {\n                    // 要兼容的目标浏览器\n                    targets: {\n                      chrome: '58',\n                      ie: '11',\n                    },\n                    // 指定corejs的版本\n                    // package.json中的版本为3.8.1\n                    corejs: '3',\n                    // 使用corejs的方式，\"usage\" 表示按需加载\n                    useBuiltIns: 'usage',\n                  },\n                ],\n              ],\n            },\n          },\n          'ts-loader',\n        ],\n        // 要排除的文件\n        exclude: /node-modules/,\n      },\n      {\n        test: /\\.(css|scss|sass)$/i,\n        use: ['style-loader', 'css-loader'],\n      },\n      // Shaders\n      {\n        test: /\\.(glsl|vs|fs)$/,\n        loader: 'ts-shader-loader',\n      },\n    ],\n  },\n\n  // 配置Webpack插件\n  plugins: [\n    // new CleanWebpackPlugin(),\n    new HTMLWebpackPlugin({\n      template: './src/index.html',\n    }),\n    // 把整个目录copy过去\n    new CopyWebpackPlugin({\n      patterns: [{ from: path.resolve(__dirname, './static') }],\n    }),\n\n    new ESLintPlugin({\n      context: './src', // 检查目录\n      extensions: ['js', 'jsx', 'ts', 'tsx'], // 文件扩展\n    }),\n  ],\n  // 用来设置引用模块\n  resolve: {\n    extensions: ['.tsx', '.ts', '.js'],\n  },\n  // 包太大会提示你\n  performance: {\n    hints: false,\n  },\n}\n"
  }
]