添加政府,银行大屏,修改政府前后端代码

This commit is contained in:
2025-09-30 17:48:03 +08:00
parent e9be0f9d98
commit deb005b88e
1409 changed files with 69541 additions and 520 deletions

View File

@@ -0,0 +1,223 @@
import * as THREE from 'three'
import TWEEN from '@tweenjs/tween.js'
import useCoord from '@/hooks/useCoord'
import { deepMerge, random } from '@/utils'
/**
*
* @param {object} {
* pointTextureUrl:标记点的图片url
* lightHaloTextureUrl:光圈的URL
* lightPillarUrl:光柱的URL
* scaleFactor:1 缩放系数,用来调整标记点和光圈的缩放大小
* }
*
* @returns
*/
export default function useMarkedLightPillar(options) {
const { geoSphereCoord } = useCoord()
// 默认参数
let defaultOptions = {
pointTextureUrl: './assets/texture/标注.png',
lightHaloTextureUrl: './assets/texture/标注光圈.png',
lightPillarUrl: './assets/texture/光柱.png',
scaleFactor: 1, // 缩放系数
}
defaultOptions = deepMerge(defaultOptions, options)
// 纹理加载器
const textureLoader = new THREE.TextureLoader()
// 射线拾取对象
const raycaster = new THREE.Raycaster()
let containerWidth = window.width
let containerHeight = window.height
// 对象属性
let getBoundingClientRect = null
/**
* 创建标记点
* @param {*} R 地球半径根据R来进行缩放
* @returns
*/
const createPointMesh = () => {
// 标记点:几何体,材质,
const geometry = new THREE.PlaneBufferGeometry(1, 1)
const material = new THREE.MeshBasicMaterial({
map: textureLoader.load(defaultOptions.pointTextureUrl),
color: 0x00ffff,
side: THREE.DoubleSide,
transparent: true,
depthWrite: false, //禁止写入深度缓冲区数据
})
let mesh = new THREE.Mesh(geometry, material)
mesh.name = 'createPointMesh'
// 缩放
const scale = 0.15 * defaultOptions.scaleFactor
mesh.scale.set(scale, scale, scale)
return mesh
}
/**
* 创建光圈
* @param {*} R 地球半径根据R来进行缩放
* @returns
*/
const createLightHalo = () => {
// 标记点:几何体,材质,
const geometry = new THREE.PlaneBufferGeometry(1, 1)
const material = new THREE.MeshBasicMaterial({
map: textureLoader.load(defaultOptions.lightHaloTextureUrl),
color: 0x00ffff,
side: THREE.DoubleSide,
opacity: 0,
transparent: true,
depthWrite: false, //禁止写入深度缓冲区数据
})
let mesh = new THREE.Mesh(geometry, material)
mesh.name = 'createLightHalo'
// 缩放
const scale = 0.3 * defaultOptions.scaleFactor
mesh.scale.set(scale, scale, scale)
// 动画延迟时间
const delay = random(0, 2000)
// 动画:透明度缩放动画
mesh.tween1 = new TWEEN.Tween({ scale: scale, opacity: 0 })
.to({ scale: scale * 1.5, opacity: 1 }, 1000)
.delay(delay)
.onUpdate(params => {
let { scale, opacity } = params
mesh.scale.set(scale, scale, scale)
mesh.material.opacity = opacity
})
mesh.tween2 = new TWEEN.Tween({ scale: scale * 1.5, opacity: 1 })
.to({ scale: scale * 2, opacity: 0 }, 1000)
.onUpdate(params => {
let { scale, opacity } = params
mesh.scale.set(scale, scale, scale)
mesh.material.opacity = opacity
})
mesh.tween1.chain(mesh.tween2)
mesh.tween2.chain(mesh.tween1)
mesh.tween1.start()
return mesh
}
/**
* 创建光柱
* @param {*} lon
* @param {*} lat
* @param {*} heightScaleFactor 光柱高度的缩放系数
* @returns
*/
const createLightPillar = (lon, lat, heightScaleFactor = 1) => {
let group = new THREE.Group()
// 柱体高度
const height = heightScaleFactor
// 柱体的geo,6.19=柱体图片高度/宽度的倍数
const geometry = new THREE.PlaneBufferGeometry(height / 6.219, height)
// 柱体旋转90度垂直于Y轴
geometry.rotateX(Math.PI / 2)
// 柱体的z轴移动高度一半对齐中心点
geometry.translate(0, 0, height / 2)
// 柱子材质
const material = new THREE.MeshBasicMaterial({
map: textureLoader.load(defaultOptions.lightPillarUrl),
color: 0x00ffff,
transparent: true,
depthWrite: false,
side: THREE.DoubleSide,
})
// 光柱01
let light01 = new THREE.Mesh(geometry, material)
light01.name = 'createLightPillar01'
// 光柱02复制光柱01
let light02 = light01.clone()
light02.name = 'createLightPillar02'
// 光柱02旋转90°跟 光柱01交叉
light02.rotateZ(Math.PI / 2)
// 创建底部标点
const bottomMesh = createPointMesh()
// 创建光圈
const lightHalo = createLightHalo()
// 将光柱和标点添加到组里
group.add(bottomMesh, lightHalo, light01, light02)
// 设置组对象的姿态
// group = setMeshQuaternion(group, R, lon, lat)
group.position.set(lon, lat, 0)
return group
}
/**
* 设置光柱颜色
* @param {*} group
* @param {*} color
*/
const setLightPillarColor = (group, color) => {
group.children.forEach(item => {
item.material.color = new THREE.Color(color)
})
}
/**
* 设置网格的位置及姿态
* @param {*} mesh
* @param {*} R
* @param {*} lon
* @param {*} lat
* @returns
*/
const setMeshQuaternion = (mesh, R, lon, lat) => {
const { x, y, z } = geoSphereCoord(R, lon, lat)
mesh.position.set(x, y, z)
// 姿态设置
// mesh在球面上的法线方向(球心和球面坐标构成的方向向量)
let meshVector = new THREE.Vector3(x, y, z).normalize()
// mesh默认在XOY平面上法线方向沿着z轴new THREE.Vector3(0, 0, 1)
let normal = new THREE.Vector3(0, 0, 1)
// 四元数属性.quaternion表示mesh的角度状态
//.setFromUnitVectors();计算两个向量之间构成的四元数值
mesh.quaternion.setFromUnitVectors(normal, meshVector)
return mesh
}
/**
* 射线拾取返回选中的mesh
* @param {*} event
* @param {*} container
* @param {*} camera
* @param {*} mesh // 光柱group
* @returns
*/
const getRaycasterObj = (event, container, camera, mesh) => {
//屏幕坐标转WebGL标准设备坐标
if (!getBoundingClientRect) {
getBoundingClientRect = container.getBoundingClientRect()
containerWidth = container.offsetWidth
containerHeight = container.offsetHeight
}
var x = ((event.clientX - getBoundingClientRect.left) / containerWidth) * 2 - 1
var y = -((event.clientY - getBoundingClientRect.top) / containerHeight) * 2 + 1
//通过鼠标单击位置标准设备坐标和相机参数计算射线投射器`Raycaster`的射线属性.ray
raycaster.setFromCamera(new THREE.Vector2(x, y), camera)
//返回.intersectObjects()参数中射线选中的网格模型对象
// 未选中对象返回空数组[],选中一个数组1个元素选中两个数组两个元素
var intersects = raycaster.intersectObjects(mesh)
return intersects
}
/**
* 选择光柱,返回选择的对象
* @param {*} event
* @param {*} container
* @param {*} camera
* @param {*} mesh
* @returns 返回选择的对象
*/
const chooseLightPillar = (event, container, camera, mesh) => {
event.preventDefault()
// 获取拾取的对象数组
let intersects = getRaycasterObj(event, container, camera, mesh)
// 大于0说明说明选中了mesh,返回对象
if (intersects.length > 0) {
return intersects[0]
}
return null
}
return {
createLightPillar,
setLightPillarColor,
chooseLightPillar,
}
}

View File

@@ -0,0 +1,80 @@
import { CSS2DObject, CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer'
export default function useCSS2DRender() {
/**
* 初始化CSS2D渲染器
* @param {Object} options - 配置选项
* @param {HTMLElement} container - 容器元素
* @returns {CSS2DRenderer} CSS2D渲染器实例
*/
const initCSS2DRender = (options, container) => {
const { width, height } = options
// 确保尺寸有效避免Canvas尺寸为0的错误
const validWidth = Math.max(width || 800, 1)
const validHeight = Math.max(height || 600, 1)
let css2dRender = new CSS2DRenderer() // 实例化css2d渲染器
css2dRender.setSize(validWidth, validHeight) // 设置渲染器的尺寸
css2dRender.domElement.style.position = 'absolute' // 设置定位位置
css2dRender.domElement.style.left = '0px'
css2dRender.domElement.style.top = '0px'
css2dRender.domElement.style.width = validWidth + 'px'
css2dRender.domElement.style.height = validHeight + 'px'
css2dRender.domElement.style.zIndex = '10000' // 确保在WebGL canvas之上
css2dRender.domElement.style.pointerEvents = 'none' // 设置不能背选中
css2dRender.domElement.style.overflow = 'visible' // 确保内容可见
container.appendChild(css2dRender.domElement) // 插入到容器当中
console.log('CSS2D渲染器DOM元素已插入容器');
console.log('容器尺寸:', validWidth, 'x', validHeight);
console.log('DOM元素样式:', css2dRender.domElement.style.cssText);
return css2dRender
}
/**
* 创建2d标签
* @param {*} name 标签内容
* @param {*} className 标签class
* @returns
*/
const create2DTag = (name = '', className = '') => {
let tag = document.createElement('div')
tag.innerHTML = name
tag.className = className
tag.style.pointerEvents = 'none'
tag.style.visibility = 'hidden'
tag.style.position = 'absolute'
// 如果className不存在用以下样式
if (!className) {
tag.style.padding = '10px'
tag.style.color = '#fff'
tag.style.fontSize = '12px'
tag.style.textAlign = 'center'
tag.style.background = 'rgba(0,0,0,0.6)'
tag.style.borderRadius = '4px'
}
let label = new CSS2DObject(tag)
/**
* 标签显示,
* @param {*} name 显示内容
* @param {*} point 显示坐标
*/
label.show = (name, point) => {
label.element.innerHTML = name
label.element.style.visibility = 'visible'
label.position.copy(point)
}
/**
* 隐藏
*/
label.hide = () => {
label.element.style.visibility = 'hidden'
}
return label
}
return {
initCSS2DRender,
create2DTag
}
}

View File

@@ -0,0 +1,68 @@
export default function useConversionStandardData() {
/**
* 转换GeoJSON数据格式
* 将Polygon类型转换为MultiPolygon格式
* @param {Object} geoData - GeoJSON数据
* @returns {Object} 转换后的数据
*/
const transfromGeoJSON = (geoData) => {
if (!geoData || !geoData.features) {
return geoData;
}
const transformedFeatures = geoData.features.map(feature => {
if (feature.geometry && feature.geometry.type === 'Polygon') {
// 将Polygon转换为MultiPolygon格式
return {
...feature,
geometry: {
...feature.geometry,
coordinates: [feature.geometry.coordinates] // 包装在数组中
}
};
}
return feature;
});
return {
...geoData,
features: transformedFeatures
};
};
/**
* 转换道路GeoJSON数据格式
* 将LineString类型转换为MultiLineString格式
* @param {Object} geoData - GeoJSON道路数据
* @returns {Object} 转换后的数据
*/
const transformGeoRoad = (geoData) => {
if (!geoData || !geoData.features) {
return geoData;
}
const transformedFeatures = geoData.features.map(feature => {
if (feature.geometry && feature.geometry.type === 'LineString') {
// 将LineString转换为MultiLineString格式
return {
...feature,
geometry: {
...feature.geometry,
coordinates: [feature.geometry.coordinates] // 包装在数组中
}
};
}
return feature;
});
return {
...geoData,
features: transformedFeatures
};
};
return {
transfromGeoJSON,
transformGeoRoad
};
}

View File

@@ -0,0 +1,92 @@
import * as THREE from 'three'
const useCoord = () => {
/**
* 生成墨卡托坐标
* @param {*} longitude 经度
* @param {*} latitude 纬度
* @returns
*/
// 墨卡托坐标系展开地球赤道作为x轴向东为x轴正方本初子午线作为y轴向北为y轴正方向。
// 数字20037508.34是地球赤道周长的一半地球半径6378137米赤道周长2*PI*r = 2 * 20037508.3427892墨卡托坐标x轴区间[-20037508.3427892,20037508.3427892]
const geoMercatorCoord = (longitude, latitude) => {
var E = longitude
var N = latitude
var x = (E * 20037508.34) / 180
var y = Math.log(Math.tan(((90 + N) * Math.PI) / 360)) / (Math.PI / 180)
y = (y * 20037508.34) / 180
return {
x: x, //墨卡托x坐标——对应经度
y: y, //墨卡托y坐标——对应维度
}
}
/**
* 生成球面坐标
* @param {*} R 球半径
* @param {*} longitude 经度
* @param {*} latitude 纬度
* @returns
*/
const geoSphereCoord = (R, longitude, latitude) => {
var lon = (longitude * Math.PI) / 180 //转弧度值
var lat = (latitude * Math.PI) / 180 //转弧度值
lon = -lon // three.js坐标系z坐标轴对应经度-90度而不是90度
// 经纬度坐标转球面坐标计算公式
var x = R * Math.cos(lat) * Math.cos(lon)
var y = R * Math.sin(lat)
var z = R * Math.cos(lat) * Math.sin(lon)
// 返回球面坐标
return {
x: x,
y: y,
z: z,
}
}
/**
* 计算包围盒
* @param {*} group
* @returns
*/
const getBoundingBox = group => {
// 包围盒计算模型对象的大小和位置
var box3 = new THREE.Box3()
box3.expandByObject(group) // 计算模型包围盒
var size = new THREE.Vector3()
box3.getSize(size) // 计算包围盒尺寸
var center = new THREE.Vector3()
box3.getCenter(center) // 计算一个层级模型对应包围盒的几何体中心坐标
return {
box3,
center,
size,
}
}
/**
* 设置网格的位置及姿态
* @param {*} mesh
* @param {*} R
* @param {*} lon
* @param {*} lat
* @returns
*/
const setMeshQuaternion = (mesh, R, lon, lat) => {
const { x, y, z } = geoSphereCoord(R, lon, lat)
mesh.position.set(x, y, z)
// 姿态设置
// mesh在球面上的法线方向(球心和球面坐标构成的方向向量)
let meshVector = new THREE.Vector3(x, y, z).normalize()
// mesh默认在XOY平面上法线方向沿着z轴new THREE.Vector3(0, 0, 1)
let normal = new THREE.Vector3(0, 0, 1)
// 四元数属性.quaternion表示mesh的角度状态
//.setFromUnitVectors();计算两个向量之间构成的四元数值
mesh.quaternion.setFromUnitVectors(normal, meshVector)
return mesh
}
return {
geoMercatorCoord,
geoSphereCoord,
getBoundingBox,
setMeshQuaternion,
}
}
export default useCoord

View File

@@ -0,0 +1,143 @@
import * as THREE from 'three';
import { Line2 } from 'three/examples/jsm/lines/Line2.js';
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js';
export default function useCountry() {
/**
* 创建国家边界线
* @param {Object} data - GeoJSON数据
* @param {Object} options - 线条样式选项
* @param {string} lineType - 线条类型 ('Line', 'LineLoop', 'LineSegments', 'Line2')
* @returns {THREE.Object3D} 线条对象
*/
const createCountryFlatLine = (data, options = {}, lineType = 'Line') => {
const group = new THREE.Group();
if (!data || !data.features) {
return group;
}
data.features.forEach(feature => {
if (feature.geometry && feature.geometry.coordinates) {
const coordinates = feature.geometry.coordinates;
coordinates.forEach(multiPolygon => {
multiPolygon.forEach(polygon => {
const points = [];
// 提取坐标点
polygon.forEach(coord => {
if (Array.isArray(coord) && coord.length >= 2) {
points.push(new THREE.Vector3(coord[0], coord[1], 0));
}
});
if (points.length > 0) {
const line = createLine(points, options, lineType);
if (line) {
group.add(line);
}
}
});
});
}
});
return group;
};
/**
* 创建线条对象
* @param {Array<THREE.Vector3>} points - 点数组
* @param {Object} options - 样式选项
* @param {string} lineType - 线条类型
* @returns {THREE.Object3D} 线条对象
*/
const createLine = (points, options = {}, lineType = 'Line') => {
if (!points || points.length < 2) {
return null;
}
const defaultOptions = {
color: 0xffffff,
linewidth: 0.001,
transparent: true,
opacity: 1,
depthTest: true
};
const finalOptions = { ...defaultOptions, ...options };
try {
switch (lineType) {
case 'Line2': {
// 使用Line2创建更高质量的线条
const geometry = new LineGeometry();
const positions = [];
points.forEach(point => {
positions.push(point.x, point.y, point.z);
});
geometry.setPositions(positions);
const material = new LineMaterial({
color: finalOptions.color,
linewidth: finalOptions.linewidth,
transparent: finalOptions.transparent,
opacity: finalOptions.opacity,
depthTest: finalOptions.depthTest
});
// 设置分辨率
material.resolution.set(window.innerWidth, window.innerHeight);
return new Line2(geometry, material);
}
case 'LineLoop': {
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({
color: finalOptions.color,
transparent: finalOptions.transparent,
opacity: finalOptions.opacity,
depthTest: finalOptions.depthTest
});
return new THREE.LineLoop(geometry, material);
}
case 'LineSegments': {
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({
color: finalOptions.color,
transparent: finalOptions.transparent,
opacity: finalOptions.opacity,
depthTest: finalOptions.depthTest
});
return new THREE.LineSegments(geometry, material);
}
default: // 'Line'
case 'Line': {
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({
color: finalOptions.color,
transparent: finalOptions.transparent,
opacity: finalOptions.opacity,
depthTest: finalOptions.depthTest
});
return new THREE.Line(geometry, material);
}
}
} catch (error) {
console.error('Error creating line:', error);
return null;
}
};
return {
createCountryFlatLine,
createLine
};
}

View File

@@ -0,0 +1,60 @@
import { ref } from 'vue';
import * as THREE from 'three';
export default function useFileLoader() {
const loading = ref(false);
const progress = ref(0);
const error = ref(null);
const requestData = async (url) => {
loading.value = true;
error.value = null;
progress.value = 0;
try {
const loader = new THREE.FileLoader();
return new Promise((resolve, reject) => {
loader.load(
url,
// onLoad
(data) => {
try {
const jsonData = JSON.parse(data);
loading.value = false;
progress.value = 100;
resolve(jsonData);
} catch (parseError) {
error.value = parseError;
loading.value = false;
reject(parseError);
}
},
// onProgress
(progressEvent) => {
if (progressEvent.lengthComputable) {
progress.value = (progressEvent.loaded / progressEvent.total) * 100;
}
},
// onError
(err) => {
error.value = err;
loading.value = false;
reject(err);
}
);
});
} catch (err) {
error.value = err;
loading.value = false;
throw err;
}
};
return {
loading,
progress,
error,
requestData
};
}

View File

@@ -0,0 +1,234 @@
import * as THREE from 'three';
import TWEEN from '@tweenjs/tween.js';
export default function useMapMarkedLightPillar(options = {}) {
const defaultOptions = {
scaleFactor: 1.0,
color: 0x00ffff,
opacity: 0.8
};
const config = { ...defaultOptions, ...options };
/**
* 创建标记点网格
* @param {number} x - X坐标
* @param {number} y - Y坐标
* @param {number} scaleFactor - 缩放因子
* @returns {THREE.Mesh} 标记点网格
*/
const createPointMesh = (x, y, scaleFactor = 1) => {
const geometry = new THREE.SphereGeometry(0.05, 16, 16);
const material = new THREE.MeshBasicMaterial({
color: config.color,
transparent: true,
opacity: config.opacity
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, 0);
mesh.scale.set(scaleFactor, scaleFactor, scaleFactor);
return mesh;
};
/**
* 创建光晕效果
* @param {number} x - X坐标
* @param {number} y - Y坐标
* @param {number} scaleFactor - 缩放因子
* @returns {THREE.Mesh} 光晕网格
*/
const createLightHalo = (x, y, scaleFactor = 1) => {
// 创建光晕纹理 - 使用单色替代渐变
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d');
// 绘制圆形光晕,中心实色,边缘透明
ctx.fillStyle = 'rgba(0, 255, 255, 0.6)';
ctx.beginPath();
ctx.arc(128, 128, 64, 0, Math.PI * 2);
ctx.fill();
// 外圈较淡的圆
ctx.fillStyle = 'rgba(0, 255, 255, 0.2)';
ctx.beginPath();
ctx.arc(128, 128, 100, 0, Math.PI * 2);
ctx.fill();
const texture = new THREE.CanvasTexture(canvas);
const geometry = new THREE.PlaneGeometry(0.5, 0.5);
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0.6,
depthTest: false
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, 0.01);
mesh.scale.set(scaleFactor, scaleFactor, scaleFactor);
// 添加缩放动画
const animate = () => {
new TWEEN.Tween(mesh.scale)
.to({ x: scaleFactor * 1.5, y: scaleFactor * 1.5, z: scaleFactor * 1.5 }, 2000)
.easing(TWEEN.Easing.Sinusoidal.InOut)
.yoyo(true)
.repeat(Infinity)
.start();
};
animate();
return mesh;
};
/**
* 创建光柱
* @param {number} x - X坐标
* @param {number} y - Y坐标
* @param {number} heightScaleFactor - 高度缩放因子
* @returns {THREE.Group} 光柱组
*/
const createLightPillar = (x, y, heightScaleFactor = 1) => {
const group = new THREE.Group();
// 创建光柱纹理 - 使用单色替代渐变
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 256;
const ctx = canvas.getContext('2d');
// 绘制实色光柱,中部较亮
ctx.fillStyle = 'rgba(0, 255, 255, 0.8)';
ctx.fillRect(0, 25, 64, 206); // 中部主体
// 上下边缘稍淡
ctx.fillStyle = 'rgba(0, 255, 255, 0.4)';
ctx.fillRect(0, 0, 64, 25); // 顶部
ctx.fillRect(0, 231, 64, 25); // 底部
const texture = new THREE.CanvasTexture(canvas);
// 创建两个相交的平面形成光柱
const height = 2 * heightScaleFactor;
const width = 0.1;
// 第一个平面
const geometry1 = new THREE.PlaneGeometry(width, height);
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0.8,
depthTest: false,
side: THREE.DoubleSide
});
const plane1 = new THREE.Mesh(geometry1, material);
plane1.position.set(x, y, height / 2);
// 第二个平面旋转90度
const plane2 = plane1.clone();
plane2.rotation.z = Math.PI / 2;
// 底部光点
const bottomMesh = createPointMesh(x, y, config.scaleFactor);
// 光晕效果
const halo = createLightHalo(x, y, config.scaleFactor);
group.add(plane1);
group.add(plane2);
group.add(bottomMesh);
group.add(halo);
return group;
};
/**
* 设置光柱颜色
* @param {THREE.Group} lightPillar - 光柱组
* @param {number} color - 颜色值
*/
const setLightPillarColor = (lightPillar, color) => {
lightPillar.children.forEach(child => {
if (child.material) {
child.material.color.setHex(color);
}
});
};
/**
* 设置网格四元数(用于球面定位)
* @param {THREE.Object3D} mesh - 网格对象
* @param {number} lng - 经度
* @param {number} lat - 纬度
* @param {number} radius - 半径
*/
const setMeshQuaternion = (mesh, lng, lat, radius = 1) => {
const phi = (90 - lat) * (Math.PI / 180);
const theta = (lng + 180) * (Math.PI / 180);
const x = -(radius * Math.sin(phi) * Math.cos(theta));
const z = radius * Math.sin(phi) * Math.sin(theta);
const y = radius * Math.cos(phi);
mesh.position.set(x, y, z);
mesh.lookAt(0, 0, 0);
};
/**
* 获取射线检测对象
* @param {THREE.Camera} camera - 相机
* @param {THREE.Vector2} mouse - 鼠标位置
* @param {Array} objects - 检测对象数组
* @returns {Array} 相交对象数组
*/
const getRaycasterObj = (camera, mouse, objects) => {
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
return raycaster.intersectObjects(objects, true);
};
/**
* 选择光柱
* @param {Event} event - 鼠标事件
* @param {THREE.Camera} camera - 相机
* @param {Array} lightPillars - 光柱数组
* @param {HTMLElement} container - 容器元素
* @returns {Object|null} 选中的光柱信息
*/
const chooseLightPillar = (event, camera, lightPillars, container) => {
const rect = container.getBoundingClientRect();
const mouse = new THREE.Vector2();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
const intersects = getRaycasterObj(camera, mouse, lightPillars);
if (intersects.length > 0) {
return {
object: intersects[0].object,
point: intersects[0].point,
distance: intersects[0].distance
};
}
return null;
};
return {
createPointMesh,
createLightHalo,
createLightPillar,
setLightPillarColor,
setMeshQuaternion,
getRaycasterObj,
chooseLightPillar
};
}

View File

@@ -0,0 +1,182 @@
import * as THREE from 'three';
export default function useSequenceFrameAnimate() {
/**
* 创建序列帧动画
* @param {Object} options - 配置选项
* @param {string} options.image - 图片路径
* @param {number} options.width - 图片宽度
* @param {number} options.height - 图片高度
* @param {number} options.frame - 总帧数
* @param {number} options.column - 列数
* @param {number} options.row - 行数
* @param {number} options.speed - 播放速度
* @returns {THREE.Mesh} 序列帧网格对象
*/
const createSequenceFrame = (options = {}) => {
const {
image,
width = 256,
height = 256,
frame = 16,
column = 4,
row = 4,
speed = 1
} = options;
// 创建几何体
const geometry = new THREE.PlaneGeometry(1, 1);
// 加载纹理
const texture = new THREE.TextureLoader().load(image);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
// 计算单帧的UV偏移
const frameWidth = 1 / column;
const frameHeight = 1 / row;
// 设置初始UV偏移
texture.repeat.set(frameWidth, frameHeight);
texture.offset.set(0, 1 - frameHeight); // 从顶部开始
// 创建材质
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
alphaTest: 0.1,
side: THREE.DoubleSide
});
// 创建网格
const mesh = new THREE.Mesh(geometry, material);
// 添加动画属性
mesh.currentFrame = 0;
mesh.frameCount = frame;
mesh.column = column;
mesh.row = row;
mesh.speed = speed;
mesh.frameWidth = frameWidth;
mesh.frameHeight = frameHeight;
mesh.frameTimer = 0;
// 更新序列帧的方法
mesh.updateSequenceFrame = function() {
this.frameTimer += this.speed;
if (this.frameTimer >= 60 / this.frameCount) { // 假设60FPS
this.frameTimer = 0;
this.currentFrame = (this.currentFrame + 1) % this.frameCount;
// 计算当前帧在纹理中的位置
const col = this.currentFrame % this.column;
const row = Math.floor(this.currentFrame / this.column);
// 更新UV偏移
this.material.map.offset.set(
col * this.frameWidth,
1 - (row + 1) * this.frameHeight // 从顶部开始
);
}
};
// 设置帧的方法
mesh.setFrame = function(frameIndex) {
if (frameIndex >= 0 && frameIndex < this.frameCount) {
this.currentFrame = frameIndex;
const col = frameIndex % this.column;
const row = Math.floor(frameIndex / this.column);
this.material.map.offset.set(
col * this.frameWidth,
1 - (row + 1) * this.frameHeight
);
}
};
// 播放动画的方法
mesh.play = function() {
this.isPlaying = true;
};
// 暂停动画的方法
mesh.pause = function() {
this.isPlaying = false;
};
// 停止动画的方法
mesh.stop = function() {
this.isPlaying = false;
this.currentFrame = 0;
this.setFrame(0);
};
// 设置播放速度的方法
mesh.setSpeed = function(newSpeed) {
this.speed = newSpeed;
};
// 默认开始播放
mesh.isPlaying = true;
return mesh;
};
/**
* 创建简单的粒子序列帧动画
* @param {Object} options - 配置选项
* @returns {THREE.Mesh} 粒子网格对象
*/
const createParticleSequenceFrame = (options = {}) => {
const {
color = 0x00ffff,
size = 0.1,
opacity = 0.8
} = options;
// 创建简单的粒子几何体
const geometry = new THREE.SphereGeometry(size, 8, 8);
// 创建材质
const material = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: opacity
});
// 创建网格
const mesh = new THREE.Mesh(geometry, material);
// 添加简单的闪烁动画
mesh.animationTimer = 0;
mesh.baseOpacity = opacity;
mesh.updateSequenceFrame = function() {
this.animationTimer += 0.1;
const opacity = this.baseOpacity * (0.5 + 0.5 * Math.sin(this.animationTimer));
this.material.opacity = opacity;
};
return mesh;
};
/**
* 批量更新序列帧动画
* @param {Array<THREE.Mesh>} meshes - 网格数组
*/
const updateSequenceFrames = (meshes) => {
meshes.forEach(mesh => {
if (mesh.updateSequenceFrame && mesh.isPlaying !== false) {
mesh.updateSequenceFrame();
}
});
};
return {
createSequenceFrame,
createParticleSequenceFrame,
updateSequenceFrames
};
}