diff --git a/package.json b/package.json index 8638b291..d9f61f7b 100644 --- a/package.json +++ b/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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b3b0354..c6c30227 100644 --- a/pnpm-lock.yaml +++ b/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 diff --git a/src/api/mock/index.ts b/src/api/mock/index.ts index 2e56cb1d..bf9b2f52 100644 --- a/src/api/mock/index.ts +++ b/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 diff --git a/src/api/mock/test.mock.ts b/src/api/mock/test.mock.ts index d2921f0a..65afb3d4 100644 --- a/src/api/mock/test.mock.ts +++ b/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)' } + ] + } + ] + } } diff --git a/src/assets/images/chart/decorates/threeEarth01.png b/src/assets/images/chart/decorates/threeEarth01.png new file mode 100644 index 00000000..84ab4ae1 Binary files /dev/null and b/src/assets/images/chart/decorates/threeEarth01.png differ diff --git a/src/components/Pages/ChartItemSetting/GlobalSettingPosition.vue b/src/components/Pages/ChartItemSetting/GlobalSettingPosition.vue index cf97a8bd..68a10eeb 100644 --- a/src/components/Pages/ChartItemSetting/GlobalSettingPosition.vue +++ b/src/components/Pages/ChartItemSetting/GlobalSettingPosition.vue @@ -1,5 +1,5 @@ diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/code/Utils/arc.ts b/src/packages/components/Decorates/Three/ThreeEarth01/code/Utils/arc.ts new file mode 100644 index 00000000..76db98fc --- /dev/null +++ b/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 } diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/code/Utils/common.ts b/src/packages/components/Decorates/Three/ThreeEarth01/code/Utils/common.ts new file mode 100644 index 00000000..ac2f05df --- /dev/null +++ b/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 + 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 = [] + option.pointList.forEach((e: Array) => 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) +} diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/code/interfaces/IEvents.ts b/src/packages/components/Decorates/Three/ThreeEarth01/code/interfaces/IEvents.ts new file mode 100644 index 00000000..a3cfe4b0 --- /dev/null +++ b/src/packages/components/Decorates/Three/ThreeEarth01/code/interfaces/IEvents.ts @@ -0,0 +1,4 @@ + +export interface IEvents { + resize: () => void +} diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/code/interfaces/IWord.ts b/src/packages/components/Decorates/Three/ThreeEarth01/code/interfaces/IWord.ts new file mode 100644 index 00000000..e0ba38f7 --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/code/shaders/earth/fragment.fs b/src/packages/components/Decorates/Three/ThreeEarth01/code/shaders/earth/fragment.fs new file mode 100644 index 00000000..d05e80d3 --- /dev/null +++ b/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 ); +} \ No newline at end of file diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/code/shaders/earth/vertex.vs b/src/packages/components/Decorates/Three/ThreeEarth01/code/shaders/earth/vertex.vs new file mode 100644 index 00000000..cd564037 --- /dev/null +++ b/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 ); +} \ No newline at end of file diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/code/world/Assets.ts b/src/packages/components/Decorates/Three/ThreeEarth01/code/world/Assets.ts new file mode 100644 index 00000000..bd8df6c4 --- /dev/null +++ b/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 } diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/code/world/Basic.ts b/src/packages/components/Decorates/Three/ThreeEarth01/code/world/Basic.ts new file mode 100644 index 00000000..bfe9d760 --- /dev/null +++ b/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 + } +} diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/code/world/Earth.ts b/src/packages/components/Decorates/Three/ThreeEarth01/code/world/Earth.ts new file mode 100644 index 00000000..0e1308d6 --- /dev/null +++ b/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 // 贴图 + 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 + + public options: options + public uniforms: uniforms + public timeValue: number + + public earth!: Mesh + 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 { + 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 + } + }) + } + } +} diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/code/world/Resources.ts b/src/packages/components/Decorates/Three/ThreeEarth01/code/world/Resources.ts new file mode 100644 index 00000000..71468dc2 --- /dev/null +++ b/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 + public textures: Record + 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 + }) + }) + } +} diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/code/world/Word.ts b/src/packages/components/Decorates/Three/ThreeEarth01/code/world/Word.ts new file mode 100644 index 00000000..cc03c907 --- /dev/null +++ b/src/packages/components/Decorates/Three/ThreeEarth01/code/world/Word.ts @@ -0,0 +1,112 @@ +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) + // 递归遍历组对象group释放所有后代网格模型绑定几何体占用内存 + this.earth.group.traverse((obj: any) => { + if (obj.type === 'Mesh') { + obj.geometry.dispose() + obj.material.dispose() + } + }) + // 重新创建 + this.createEarth(data) + } +} diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/config.ts b/src/packages/components/Decorates/Three/ThreeEarth01/config.ts new file mode 100644 index 00000000..fdc582cc --- /dev/null +++ b/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) +} diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/config.vue b/src/packages/components/Decorates/Three/ThreeEarth01/config.vue new file mode 100644 index 00000000..e4c619ed --- /dev/null +++ b/src/packages/components/Decorates/Three/ThreeEarth01/config.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/data.json b/src/packages/components/Decorates/Three/ThreeEarth01/data.json new file mode 100644 index 00000000..df862473 --- /dev/null +++ b/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 + } + ] + } +] diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/aircraft.png b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/aircraft.png new file mode 100644 index 00000000..9021ca22 Binary files /dev/null and b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/aircraft.png differ diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/aperture.png b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/aperture.png new file mode 100644 index 00000000..2edc9064 Binary files /dev/null and b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/aperture.png differ diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/earth.png b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/earth.png new file mode 100644 index 00000000..c5076ba1 Binary files /dev/null and b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/earth.png differ diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/glow.png b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/glow.png new file mode 100644 index 00000000..b0d8dc2d Binary files /dev/null and b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/glow.png differ diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/gradient.png b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/gradient.png new file mode 100644 index 00000000..df226794 Binary files /dev/null and b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/gradient.png differ diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/label-old.png b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/label-old.png new file mode 100644 index 00000000..de24165f Binary files /dev/null and b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/label-old.png differ diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/label.png b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/label.png new file mode 100644 index 00000000..2edc9064 Binary files /dev/null and b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/label.png differ diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/light_column.png b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/light_column.png new file mode 100644 index 00000000..b10b1a99 Binary files /dev/null and b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/light_column.png differ diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/redCircle.png b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/redCircle.png new file mode 100644 index 00000000..4b770a19 Binary files /dev/null and b/src/packages/components/Decorates/Three/ThreeEarth01/images/earth/redCircle.png differ diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/index.ts b/src/packages/components/Decorates/Three/ThreeEarth01/index.ts new file mode 100644 index 00000000..fbaf4b75 --- /dev/null +++ b/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 +} diff --git a/src/packages/components/Decorates/Three/ThreeEarth01/index.vue b/src/packages/components/Decorates/Three/ThreeEarth01/index.vue new file mode 100644 index 00000000..057dac40 --- /dev/null +++ b/src/packages/components/Decorates/Three/ThreeEarth01/index.vue @@ -0,0 +1,81 @@ + + + diff --git a/src/packages/components/Decorates/Three/index.ts b/src/packages/components/Decorates/Three/index.ts new file mode 100644 index 00000000..4dde0f6f --- /dev/null +++ b/src/packages/components/Decorates/Three/index.ts @@ -0,0 +1,3 @@ +import { ThreeEarth01Config } from './ThreeEarth01/index' + +export default [ThreeEarth01Config] diff --git a/src/packages/components/Decorates/index.d.ts b/src/packages/components/Decorates/index.d.ts index 67937226..d20047eb 100644 --- a/src/packages/components/Decorates/index.d.ts +++ b/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 = '更多' } \ No newline at end of file diff --git a/src/packages/components/Decorates/index.ts b/src/packages/components/Decorates/index.ts index ef13e4ee..17b64130 100644 --- a/src/packages/components/Decorates/index.ts +++ b/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] diff --git a/src/packages/components/Informations/Mores/WordCloud/index.vue b/src/packages/components/Informations/Mores/WordCloud/index.vue index beab5ebc..4c9626e3 100644 --- a/src/packages/components/Informations/Mores/WordCloud/index.vue +++ b/src/packages/components/Informations/Mores/WordCloud/index.vue @@ -47,9 +47,12 @@ const option = computed(() => { }) const dataSetHandle = (dataset: typeof dataJson) => { - dataset && (props.chartConfig.option.series[0].data = dataset) - - vChartRef.value && isPreview() && vChartRef.value.setOption(props.chartConfig.option) + try { + dataset && (props.chartConfig.option.series[0].data = dataset) + vChartRef.value && isPreview() && vChartRef.value.setOption(props.chartConfig.option) + } catch (error) { + console.log(error) + } } // dataset 无法变更条数的补丁 diff --git a/src/packages/components/Tables/Tables/TableList/index.vue b/src/packages/components/Tables/Tables/TableList/index.vue index f24001ed..5fd3e02d 100644 --- a/src/packages/components/Tables/Tables/TableList/index.vue +++ b/src/packages/components/Tables/Tables/TableList/index.vue @@ -10,19 +10,12 @@
No.{{ item.ranking }}
- {{ - status.mergedConfig.valueFormatter - ? status.mergedConfig.valueFormatter(item) - : item.value - }} + {{ status.mergedConfig.valueFormatter ? status.mergedConfig.valueFormatter(item) : item.value }} {{ unit }}
-
+
@@ -39,8 +32,8 @@ import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore const props = defineProps({ chartConfig: { type: Object as PropType, - required: true, - }, + required: true + } }) const { w, h } = toRefs(props.chartConfig.attr) const { rowNum, unit, color, textColor, borderColor, indexFontSize, leftFontSize, rightFontSize } = toRefs( @@ -50,13 +43,15 @@ const { rowNum, unit, color, textColor, borderColor, indexFontSize, leftFontSize const status = reactive({ mergedConfig: props.chartConfig.option, rowsData: [], - rows: [{ - scroll: 0, - ranking: 1, - name: '', - value: '', - percent: 0 - }], + rows: [ + { + scroll: 0, + ranking: 1, + name: '', + value: '', + percent: 0 + } + ], heights: [0], animationIndex: 0, animationHandler: 0, @@ -81,16 +76,16 @@ const calcRowsData = () => { // abs of max const maxAbs = Math.abs(max) const total = max + minAbs - dataset = dataset.map((row: any, i:number) => ({ + dataset = dataset.map((row: any, i: number) => ({ ...row, ranking: i + 1, - percent: ((row.value + minAbs) / total) * 100, + percent: ((row.value + minAbs) / total) * 100 })) const rowLength = dataset.length if (rowLength > rowNum && rowLength < 2 * rowNum) { dataset = [...dataset, ...dataset] } - dataset = dataset.map((d:any, i:number) => ({ ...d, scroll: i })) + dataset = dataset.map((d: any, i: number) => ({ ...d, scroll: i })) status.rowsData = dataset status.rows = dataset } @@ -134,11 +129,15 @@ const stopAnimation = () => { } const onRestart = async () => { - if (!status.mergedConfig) return - stopAnimation() - calcRowsData() - calcHeights(true) - animation(true) + try { + if (!status.mergedConfig) return + stopAnimation() + calcRowsData() + calcHeights(true) + animation(true) + } catch (error) { + console.log(error) + } } onRestart() diff --git a/src/packages/components/Tables/Tables/TableScrollBoard/index.vue b/src/packages/components/Tables/Tables/TableScrollBoard/index.vue index 3ea2a228..669be3ed 100644 --- a/src/packages/components/Tables/Tables/TableScrollBoard/index.vue +++ b/src/packages/components/Tables/Tables/TableScrollBoard/index.vue @@ -1,24 +1,47 @@