|
@@ -98,6 +98,7 @@ export default {
|
|
|
waterParticleSystem: null,
|
|
|
pathPositions: [],
|
|
|
simulationStep: 0,
|
|
|
+ flameIntensity: 1.0, // 火焰强度,初始为1
|
|
|
};
|
|
|
},
|
|
|
mounted() {
|
|
@@ -198,7 +199,8 @@ export default {
|
|
|
polyline: {
|
|
|
positions: pathPoints,
|
|
|
width: 5,
|
|
|
- material: SkyScenery.Color.RED.withAlpha(0.7),
|
|
|
+ // 修改路径颜色为蓝色系,降低透明度
|
|
|
+ material: SkyScenery.Color.fromCssColorString("#409EFF").withAlpha(0.6),
|
|
|
clampToGround: true,
|
|
|
},
|
|
|
});
|
|
@@ -221,6 +223,7 @@ export default {
|
|
|
this.showPathModal = false;
|
|
|
});
|
|
|
},
|
|
|
+
|
|
|
// 清除路径规划
|
|
|
clearPath() {
|
|
|
if (this.pathEntity) {
|
|
@@ -247,7 +250,7 @@ export default {
|
|
|
// 加载消防车模型
|
|
|
this.loadVehicleModel()
|
|
|
.then(() => this.createFlameEffect())
|
|
|
- // .then(() => this.animateVehicleAlongPath())
|
|
|
+ .then(() => this.animateVehicleAlongPath())
|
|
|
.catch((error) => {
|
|
|
console.error("模拟启动失败:", error);
|
|
|
this.showToast("模拟启动失败,请重试", { type: "error" });
|
|
@@ -269,11 +272,22 @@ export default {
|
|
|
return;
|
|
|
}
|
|
|
const glbUrl = "/static/gltf/消防车.gltf";
|
|
|
- if (this.vehicleModel) {
|
|
|
- this.vehicleModel.remove();
|
|
|
+ if (this.vehicleModel && viewer && viewer.entities) {
|
|
|
+ viewer.entities.remove(this.vehicleModel);
|
|
|
+ this.vehicleModel = null; // 清空引用
|
|
|
}
|
|
|
const position = this.pathPositions[0];
|
|
|
- const heading = SkyScenery.Math.toRadians(135); // 航向角(绕Y轴旋转角度): 135°
|
|
|
+ // 转换为方向角
|
|
|
+ const direction = SkyScenery.Cartesian3.subtract(
|
|
|
+ this.pathPositions[1],
|
|
|
+ this.pathPositions[0],
|
|
|
+ new SkyScenery.Cartesian3()
|
|
|
+ );
|
|
|
+ // console.log(
|
|
|
+ // Math.atan2(direction.y, direction.x),
|
|
|
+ // SkyScenery.Math.toRadians(direction)
|
|
|
+ // );
|
|
|
+ const heading = Math.atan2(direction.y, direction.x); // 航向角(绕Y轴旋转角度)
|
|
|
const pitch = 0; // 俯仰角(绕X轴旋转角度): 0°
|
|
|
const roll = 0; // 翻滚角(绕Z轴旋转角度): 0°
|
|
|
const hpr = new SkyScenery.HeadingPitchRoll(heading, pitch, roll);
|
|
@@ -298,140 +312,316 @@ export default {
|
|
|
resolve();
|
|
|
});
|
|
|
},
|
|
|
+
|
|
|
// 创建火焰粒子效果
|
|
|
createFlameEffect() {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
- const destinationPosition = SkyScenery.Cartesian3.fromDegrees(
|
|
|
- this.endPoint.longitude, // 经度
|
|
|
- this.endPoint.latitude, // 纬度
|
|
|
- 50 // 高度(米)
|
|
|
- );
|
|
|
- if (!destinationPosition) {
|
|
|
- reject(new Error("终点位置不存在"));
|
|
|
- return;
|
|
|
- }
|
|
|
- // 定义火焰粒子系统的选项
|
|
|
- const particleSystemOptions = {
|
|
|
- // 粒子发射器:圆形区域发射
|
|
|
- emitter: new SkyScenery.CircleEmitter(100.0), // 发射半径
|
|
|
- image: "/static/image/point.png", // 添加此行(需提供实际纹理图片路径)
|
|
|
- // 粒子生命周期
|
|
|
- life: 10.0, // 粒子存活时间(秒)
|
|
|
- lifeVariance: 0.5, // 生命周期变化范围
|
|
|
- // 粒子速度
|
|
|
- speed: 3.0, // 基础速度
|
|
|
- speedVariance: 1.0, // 速度变化范围
|
|
|
- // 粒子大小
|
|
|
-
|
|
|
- startScale: 1,
|
|
|
- endScale: 5,
|
|
|
-
|
|
|
- minimumParticleLife: 0.5,
|
|
|
- maximumParticleLife: 0.5,
|
|
|
-
|
|
|
- minimumSpeed: 1,
|
|
|
- maximumSpeed: 4,
|
|
|
-
|
|
|
- imageSize: new Cesium.Cartesian2(25, 25),
|
|
|
-
|
|
|
- emissionRate: viewModel.emissionRate,
|
|
|
-
|
|
|
- // 粒子方向:向上发射为主
|
|
|
- direction: new SkyScenery.Cartesian3(0.0, 0.0, 1.0), // Z轴向上
|
|
|
- spread: SkyScenery.Math.toRadians(30.0), // 扩散角度
|
|
|
-
|
|
|
- // 粒子颜色:火焰颜色渐变(红->橙->黄->透明)
|
|
|
- startColor: new SkyScenery.Color(1.0, 0.3, 0.0, 1.0), // 初始颜色(红色)
|
|
|
- endColor: new SkyScenery.Color(1.0, 0.8, 0.0, 1.0), // 结束颜色(透明黄色)
|
|
|
- // 粒子数量
|
|
|
- particleCount: 500, // 最大粒子数
|
|
|
-
|
|
|
- // 发射率:每秒发射的粒子数
|
|
|
- emissionRate: 100,
|
|
|
-
|
|
|
- // 粒子更新函数:模拟火焰的随机飘动
|
|
|
- updateCallback: function (particle, dt) {
|
|
|
- // 添加随机横向运动,模拟火焰飘动
|
|
|
- const wind = 0.5; // 风力系数
|
|
|
- particle.velocity.x += (Math.random() - 0.5) * wind * dt;
|
|
|
- particle.velocity.y += (Math.random() - 0.5) * wind * dt;
|
|
|
-
|
|
|
- // 随时间增加粒子透明度衰减
|
|
|
- particle.color.alpha = SkyScenery.Math.lerp(
|
|
|
- particle.startColor.alpha,
|
|
|
- particle.endColor.alpha,
|
|
|
- particle.life / particle.startLife
|
|
|
+ try {
|
|
|
+ let endPoint = this.endPoint;
|
|
|
+ const vm = this;
|
|
|
+ // 创建Canvas元素
|
|
|
+ const canvas = document.createElement("canvas");
|
|
|
+ canvas.id = "fireCanvas";
|
|
|
+ canvas.style.position = "absolute";
|
|
|
+ canvas.style.pointerEvents = "none"; // 让鼠标事件穿透canvas
|
|
|
+ document.body.appendChild(canvas);
|
|
|
+
|
|
|
+ // 设置Canvas尺寸,适当增大以避免溢出
|
|
|
+ canvas.width = 80;
|
|
|
+ canvas.height = 160;
|
|
|
+
|
|
|
+ const ctx = canvas.getContext("2d");
|
|
|
+
|
|
|
+ // 火焰粒子数组
|
|
|
+ let particles = [];
|
|
|
+ // 火焰强度控制
|
|
|
+ let intensity = 100;
|
|
|
+
|
|
|
+ // 添加火焰强度衰减控制
|
|
|
+ this.flameIntensity = 1.0;
|
|
|
+
|
|
|
+ // 粒子类 - 优化火焰效果
|
|
|
+ class Particle {
|
|
|
+ constructor(x, y) {
|
|
|
+ this.x = x;
|
|
|
+ this.y = y;
|
|
|
+ // 减小粒子大小,从原来的5-20改为2-8
|
|
|
+ this.size = Math.random() * 6 + 2;
|
|
|
+ // 调整X方向速度范围,减少水平扩散
|
|
|
+ this.speedX = Math.random() * 3 - 1.5;
|
|
|
+ // 调整Y方向速度,使火焰更向上集中
|
|
|
+ this.speedY = Math.random() * -2 - 2;
|
|
|
+ // 优化火焰颜色,使用更真实的火焰色调范围
|
|
|
+ const hue = Math.random() * 30 + 15; // 色调范围:15-45
|
|
|
+ const lightness = Math.random() * 20 + 40; // 亮度范围:40-60
|
|
|
+ this.color = `hsl(${hue}, 100%, ${lightness}%)`;
|
|
|
+ this.alpha = Math.random() * 0.5 + 0.5; // 透明度范围:0.5-1.0
|
|
|
+ this.decay = Math.random() * 0.01 + 0.005; // 透明度衰减速度
|
|
|
+ this.originalSize = this.size; // 保存初始大小用于动画
|
|
|
+ }
|
|
|
+
|
|
|
+ update() {
|
|
|
+ // 应用火焰强度衰减
|
|
|
+ const effectiveSize = this.size * this.originalSize * vm.flameIntensity;
|
|
|
+
|
|
|
+ // 向上运动时逐渐减速
|
|
|
+ this.speedY += 0.05; // 重力效果,使粒子上升变慢
|
|
|
+ this.x += this.speedX * (1 - this.y / canvas.height); // 上部水平速度减慢
|
|
|
+ this.y += this.speedY;
|
|
|
+
|
|
|
+ // 透明度衰减
|
|
|
+ this.alpha -= this.decay * (1 - this.y / canvas.height);
|
|
|
+
|
|
|
+ // 大小变化:先增大后减小,更像火焰
|
|
|
+ const lifeProgress = 1 - this.alpha;
|
|
|
+ this.size =
|
|
|
+ this.originalSize *
|
|
|
+ vm.flameIntensity *
|
|
|
+ (lifeProgress < 0.3
|
|
|
+ ? 1 + lifeProgress * 2
|
|
|
+ : 1 - (lifeProgress - 0.3) * 3);
|
|
|
+
|
|
|
+ // 添加更自然的随机性,上部随机性减小
|
|
|
+ const randomFactor = 1 - this.y / canvas.height;
|
|
|
+ this.speedX += (Math.random() * 0.6 - 0.3) * randomFactor;
|
|
|
+ }
|
|
|
+
|
|
|
+ draw() {
|
|
|
+ ctx.globalAlpha = this.alpha;
|
|
|
+ ctx.fillStyle = this.color;
|
|
|
+ ctx.beginPath();
|
|
|
+ // 使用椭圆代替圆形,使火焰更扁平
|
|
|
+ ctx.ellipse(this.x, this.y, this.size * 0.8, this.size, 0, 0, Math.PI * 2);
|
|
|
+ ctx.fill();
|
|
|
+ }
|
|
|
+
|
|
|
+ isDead() {
|
|
|
+ return this.alpha <= 0 || this.size <= 0.5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建粒子 - 增加粒子数量以弥补大小减小
|
|
|
+ function createParticles() {
|
|
|
+ const particleCount = (intensity / 100) * 10; // 从5增加到10
|
|
|
+ // 从底部中心区域发射粒子,更集中
|
|
|
+ for (let i = 0; i < particleCount; i++) {
|
|
|
+ const x =
|
|
|
+ canvas.width / 2 + (Math.random() * 30 - 15) * (1 - Math.random() * 0.5);
|
|
|
+ const y = canvas.height - 10;
|
|
|
+ particles.push(new Particle(x, y));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理粒子
|
|
|
+ function handleParticles() {
|
|
|
+ for (let i = 0; i < particles.length; i++) {
|
|
|
+ particles[i].update();
|
|
|
+ particles[i].draw();
|
|
|
+ if (particles[i].isDead()) {
|
|
|
+ particles.splice(i, 1);
|
|
|
+ i--;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制火焰底部 - 更集中的火焰源头
|
|
|
+ function drawFireBase() {
|
|
|
+ const gradient = ctx.createRadialGradient(
|
|
|
+ canvas.width / 2,
|
|
|
+ canvas.height - 10,
|
|
|
+ 5,
|
|
|
+ canvas.width / 2,
|
|
|
+ canvas.height - 10,
|
|
|
+ 40
|
|
|
);
|
|
|
- },
|
|
|
+ gradient.addColorStop(0, `rgba(255, 100, 0, ${0.6 * vm.flameIntensity})`);
|
|
|
+ gradient.addColorStop(1, `rgba(255, 50, 0, 0)`);
|
|
|
+
|
|
|
+ ctx.globalAlpha = 0.6;
|
|
|
+ ctx.fillStyle = gradient;
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.arc(canvas.width / 2, canvas.height - 10, 40, 0, Math.PI * 2);
|
|
|
+ ctx.fill();
|
|
|
+ }
|
|
|
|
|
|
- // 粒子位置
|
|
|
- modelMatrix: SkyScenery.Transforms.eastNorthUpToFixedFrame(destinationPosition),
|
|
|
- };
|
|
|
+ // 更新火焰位置到地图终点
|
|
|
+ function updateFlamePosition() {
|
|
|
+ if (!endPoint || !viewer) return;
|
|
|
|
|
|
- // 创建粒子系统
|
|
|
- const particleSystem = new SkyScenery.ParticleSystem(particleSystemOptions);
|
|
|
+ // 将地理坐标转换为屏幕坐标
|
|
|
+ const destinationCartesian = SkyScenery.Cartesian3.fromDegrees(
|
|
|
+ parseFloat(endPoint.longitude),
|
|
|
+ parseFloat(endPoint.latitude),
|
|
|
+ 0 // 高度偏移,让火焰悬浮在地面上方
|
|
|
+ );
|
|
|
|
|
|
- // 验证粒子系统是否创建成功
|
|
|
- if (!particleSystem) {
|
|
|
- reject(new Error("火焰粒子系统创建失败"));
|
|
|
- return;
|
|
|
- } else {
|
|
|
- // 添加到场景
|
|
|
- console.log("火焰粒子系统创建成功", particleSystem);
|
|
|
- this.flameParticleSystem = viewer.scene.primitives.add(particleSystem);
|
|
|
- // viewer.imageryLayers.removeAll(); // 移除影像图层
|
|
|
- // 确保相机飞到粒子位置
|
|
|
- viewer.camera.flyTo({
|
|
|
- destination: SkyScenery.Cartesian3.fromDegrees(
|
|
|
- this.endPoint.longitude, // 经度
|
|
|
- this.endPoint.latitude, // 纬度
|
|
|
- 500 // 高度(米)
|
|
|
- ),
|
|
|
- duration: 2,
|
|
|
- });
|
|
|
+ const windowPosition = SkyScenery.SceneTransforms.wgs84ToWindowCoordinates(
|
|
|
+ viewer.scene,
|
|
|
+ destinationCartesian
|
|
|
+ );
|
|
|
+
|
|
|
+ if (windowPosition) {
|
|
|
+ // 设置canvas位置,使其中心点对准终点
|
|
|
+ canvas.style.left = `${windowPosition.x - canvas.width / 2}px`;
|
|
|
+ canvas.style.top = `${windowPosition.y - canvas.height * 0.8}px`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 动画循环
|
|
|
+ function animate() {
|
|
|
+ // 清除画布
|
|
|
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
+
|
|
|
+ // 更新火焰位置
|
|
|
+ updateFlamePosition();
|
|
|
+
|
|
|
+ drawFireBase();
|
|
|
+ createParticles();
|
|
|
+ handleParticles();
|
|
|
+
|
|
|
+ requestAnimationFrame(animate);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化
|
|
|
+ function init() {
|
|
|
+ // 监听地图渲染事件,更新火焰位置
|
|
|
+ viewer.scene.postRender.addEventListener(updateFlamePosition);
|
|
|
+
|
|
|
+ // 开始动画
|
|
|
+ animate();
|
|
|
+ resolve(canvas); // 初始化完成,返回canvas元素
|
|
|
+ }
|
|
|
+
|
|
|
+ // 启动初始化
|
|
|
+ init();
|
|
|
+ } catch (error) {
|
|
|
+ reject(error);
|
|
|
}
|
|
|
- resolve();
|
|
|
});
|
|
|
- },
|
|
|
- // 车辆沿路径动画
|
|
|
+ }, // 车辆沿路径动画
|
|
|
animateVehicleAlongPath() {
|
|
|
const totalDuration = 10000; // 10秒完成路径
|
|
|
- const stepDuration = totalDuration / this.pathPositions.length;
|
|
|
- let currentStep = 0;
|
|
|
+ const startTime = performance.now();
|
|
|
+ const pathLength = this.pathPositions.length;
|
|
|
+ // 计算路径总长度用于速度控制
|
|
|
+ let totalPathDistance = 0;
|
|
|
+ for (let i = 0; i < pathLength - 1; i++) {
|
|
|
+ totalPathDistance += SkyScenery.Cartesian3.distance(
|
|
|
+ this.pathPositions[i],
|
|
|
+ this.pathPositions[i + 1]
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ let animateStep = (currentTime) => {
|
|
|
+ const elapsed = currentTime - startTime;
|
|
|
+ const progress = Math.min(elapsed / totalDuration, 1.0);
|
|
|
+
|
|
|
+ // 根据进度计算当前位置
|
|
|
+ const targetDistance = progress * totalPathDistance;
|
|
|
+ let accumulatedDistance = 0;
|
|
|
+ // 找到当前所在路径段
|
|
|
+ for (let i = 0; i < pathLength - 1; i++) {
|
|
|
+ const segmentDistance = SkyScenery.Cartesian3.distance(
|
|
|
+ this.pathPositions[i],
|
|
|
+ this.pathPositions[i + 1]
|
|
|
+ );
|
|
|
+
|
|
|
+ if (accumulatedDistance + segmentDistance >= targetDistance) {
|
|
|
+ const segmentProgress =
|
|
|
+ (targetDistance - accumulatedDistance) / segmentDistance;
|
|
|
+ // 线性插值计算当前位置
|
|
|
+ this.vehicleModel.position = SkyScenery.Cartesian3.lerp(
|
|
|
+ this.pathPositions[i],
|
|
|
+ this.pathPositions[i + 1],
|
|
|
+ segmentProgress,
|
|
|
+ new SkyScenery.Cartesian3()
|
|
|
+ );
|
|
|
|
|
|
- const animateStep = () => {
|
|
|
- if (currentStep < this.pathPositions.length) {
|
|
|
- this.vehicleModel.position = this.pathPositions[currentStep];
|
|
|
- currentStep++;
|
|
|
+ // 计算方向向量并转向
|
|
|
+ if (i < pathLength - 1) {
|
|
|
+ const direction = SkyScenery.Cartesian3.subtract(
|
|
|
+ this.pathPositions[i + 1],
|
|
|
+ this.pathPositions[i],
|
|
|
+ new SkyScenery.Cartesian3()
|
|
|
+ );
|
|
|
+ // 转换为方向角
|
|
|
+ const heading = Math.atan2(direction.y, direction.x);
|
|
|
+ const position = this.vehicleModel.position;
|
|
|
+ const orientation = SkyScenery.Transforms.headingPitchRollQuaternion(
|
|
|
+ position,
|
|
|
+ new SkyScenery.HeadingPitchRoll(heading, 0, 0)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ accumulatedDistance += segmentDistance;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (progress < 1.0) {
|
|
|
requestAnimationFrame(animateStep);
|
|
|
} else {
|
|
|
- // 车辆到达终点,开始灭火作业
|
|
|
+ // 消防车到达终点前的位置偏移(距离终点5米)
|
|
|
+ if (pathLength >= 2) {
|
|
|
+ // 获取最后一段路径向量
|
|
|
+ const lastSegment = SkyScenery.Cartesian3.subtract(
|
|
|
+ this.pathPositions[pathLength - 1],
|
|
|
+ this.pathPositions[pathLength - 2],
|
|
|
+ new SkyScenery.Cartesian3()
|
|
|
+ );
|
|
|
+ // 标准化向量
|
|
|
+ SkyScenery.Cartesian3.normalize(lastSegment, lastSegment);
|
|
|
+ // 反向移动5米(根据实际场景调整距离)
|
|
|
+ SkyScenery.Cartesian3.multiplyByScalar(lastSegment, -5, lastSegment);
|
|
|
+
|
|
|
+ // 设置最终位置(终点前5米)
|
|
|
+ this.vehicleModel.position = SkyScenery.Cartesian3.add(
|
|
|
+ this.pathPositions[pathLength - 1],
|
|
|
+ lastSegment,
|
|
|
+ new SkyScenery.Cartesian3()
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ // 路径点不足时直接使用终点
|
|
|
+ this.vehicleModel.position = this.pathPositions[pathLength - 1];
|
|
|
+ }
|
|
|
this.startExtinguishing();
|
|
|
}
|
|
|
};
|
|
|
-
|
|
|
- animateStep();
|
|
|
+ requestAnimationFrame(animateStep);
|
|
|
},
|
|
|
// 开始灭火作业
|
|
|
startExtinguishing() {
|
|
|
this.showToast("消防车到达目的地,开始灭火作业...");
|
|
|
|
|
|
- // 创建水流粒子效果
|
|
|
- this.createWaterEffect();
|
|
|
+ // 创建水流粒子效果(添加Promise链式调用)
|
|
|
+ this.createWaterEffect().catch((error) => {
|
|
|
+ console.error("水流效果创建失败:", error);
|
|
|
+ });
|
|
|
|
|
|
// 3秒后开始减少火焰
|
|
|
setTimeout(() => {
|
|
|
- if (this.flameParticleSystem) {
|
|
|
- this.flameParticleSystem.emissionRate = 10; // 减少粒子发射量
|
|
|
- }
|
|
|
+ // 火焰强度逐渐减弱
|
|
|
+ let interval = setInterval(() => {
|
|
|
+ if (this.flameIntensity > 0) {
|
|
|
+ this.flameIntensity -= 0.02;
|
|
|
+ } else {
|
|
|
+ clearInterval(interval);
|
|
|
+ }
|
|
|
+ }, 50);
|
|
|
}, 3000);
|
|
|
|
|
|
// 5秒后完全移除火焰
|
|
|
setTimeout(() => {
|
|
|
- if (this.flameParticleSystem) {
|
|
|
- viewer.scene.primitives.remove(this.flameParticleSystem);
|
|
|
- this.flameParticleSystem = null;
|
|
|
- }
|
|
|
+ const canvas = document.getElementById("fireCanvas");
|
|
|
+ if (canvas) canvas.remove();
|
|
|
+
|
|
|
+ // 显示完成弹窗
|
|
|
+ this.showToast("作业完成,起火点已熄灭!");
|
|
|
+
|
|
|
+ // 1秒后显示模拟完成
|
|
|
+ setTimeout(() => {
|
|
|
+ this.showToast("模拟完成");
|
|
|
+ this.isSimulating = false;
|
|
|
+ }, 1000);
|
|
|
}, 5000);
|
|
|
|
|
|
// 6秒后完成作业
|
|
@@ -441,74 +631,219 @@ export default {
|
|
|
},
|
|
|
// 创建水流粒子效果
|
|
|
createWaterEffect() {
|
|
|
- try {
|
|
|
- const vehiclePosition = this.vehicleModel.position;
|
|
|
- const destinationPosition = this.pathPositions[this.pathPositions.length - 1];
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ try {
|
|
|
+ const vm = this;
|
|
|
+ const endPoint = this.endPoint;
|
|
|
+
|
|
|
+ // 创建Canvas元素
|
|
|
+ const canvas = document.createElement("canvas");
|
|
|
+ canvas.id = "waterCanvas";
|
|
|
+ canvas.style.position = "absolute";
|
|
|
+ canvas.style.pointerEvents = "none"; // 让鼠标事件穿透canvas
|
|
|
+ document.body.appendChild(canvas);
|
|
|
+
|
|
|
+ // 设置Canvas尺寸
|
|
|
+ canvas.width = 120; // 较宽的画布以覆盖水流范围
|
|
|
+ canvas.height = 120;
|
|
|
+
|
|
|
+ const ctx = canvas.getContext("2d");
|
|
|
+
|
|
|
+ // 水流粒子数组
|
|
|
+ let particles = [];
|
|
|
+ // 水流强度控制
|
|
|
+ let waterIntensity = 1.0;
|
|
|
+
|
|
|
+ // 粒子类 - 水流效果
|
|
|
+ class WaterParticle {
|
|
|
+ constructor(x, y, targetX, targetY) {
|
|
|
+ this.x = x;
|
|
|
+ this.y = y;
|
|
|
+ this.targetX = targetX; // 目标X坐标(火焰位置)
|
|
|
+ this.targetY = targetY; // 目标Y坐标(火焰位置)
|
|
|
+ this.size = Math.random() * 4 + 2; // 水滴大小(2-6)
|
|
|
+
|
|
|
+ // 计算指向火焰的初始速度
|
|
|
+ const angle = Math.atan2(targetY - y, targetX - x);
|
|
|
+ const speed = Math.random() * 4 + 6; // 速度(6-10)
|
|
|
+ this.speedX = Math.cos(angle) * speed;
|
|
|
+ this.speedY = Math.sin(angle) * speed;
|
|
|
+
|
|
|
+ // 水滴颜色(蓝色系)
|
|
|
+ const hue = Math.random() * 20 + 180; // 色调范围:180-200(蓝色)
|
|
|
+ const lightness = Math.random() * 30 + 60; // 亮度范围:60-90
|
|
|
+ this.color = `hsl(${hue}, 80%, ${lightness}%)`;
|
|
|
+
|
|
|
+ this.alpha = Math.random() * 0.5 + 0.5; // 透明度(0.5-1.0)
|
|
|
+ this.decay = Math.random() * 0.015 + 0.005; // 衰减速度
|
|
|
+ this.gravity = 0.15; // 重力影响
|
|
|
+ }
|
|
|
|
|
|
- if (!vehiclePosition || !destinationPosition) {
|
|
|
- throw new Error("车辆或终点位置未定义");
|
|
|
- }
|
|
|
+ update() {
|
|
|
+ // 应用重力
|
|
|
+ this.speedY += this.gravity;
|
|
|
|
|
|
- this.waterParticleSystem = viewer.scene.primitives.add(
|
|
|
- new SkyScenery.ParticleSystem({
|
|
|
- image: require("@static/image/point.png"), // 使用本地水资源图片
|
|
|
- startColor: new SkyScenery.Color(0.1, 0.5, 1.0, 0.8),
|
|
|
- endColor: new SkyScenery.Color(0.1, 0.5, 1.0, 0.0),
|
|
|
- startScale: 5.0,
|
|
|
- endScale: 15.0,
|
|
|
- particleLife: 1.0,
|
|
|
- emissionRate: 30.0,
|
|
|
- speed: 10.0,
|
|
|
- width: 10,
|
|
|
- height: 10,
|
|
|
- lifeTime: 3.0,
|
|
|
- position: vehiclePosition,
|
|
|
- emitter: new SkyScenery.CircleEmitter(2.0),
|
|
|
- velocity: SkyScenery.Cartesian3.subtract(
|
|
|
- destinationPosition,
|
|
|
- vehiclePosition,
|
|
|
- new SkyScenery.Cartesian3()
|
|
|
- ),
|
|
|
- })
|
|
|
- );
|
|
|
- } catch (error) {
|
|
|
- console.error("水流粒子效果创建失败:", error);
|
|
|
- this.showToast("水流效果加载失败");
|
|
|
- }
|
|
|
+ // 更新位置
|
|
|
+ this.x += this.speedX * waterIntensity;
|
|
|
+ this.y += this.speedY * waterIntensity;
|
|
|
+
|
|
|
+ // 透明度衰减
|
|
|
+ this.alpha -= this.decay;
|
|
|
+
|
|
|
+ // 水滴下落时轻微摇摆
|
|
|
+ this.speedX += (Math.random() - 0.5) * 0.3;
|
|
|
+ }
|
|
|
+
|
|
|
+ draw() {
|
|
|
+ ctx.globalAlpha = this.alpha;
|
|
|
+ ctx.fillStyle = this.color;
|
|
|
+ ctx.beginPath();
|
|
|
+ // 确保半径为正数
|
|
|
+ const radiusX = Math.max(0.1, this.size * 0.8);
|
|
|
+ const radiusY = Math.max(0.1, this.size);
|
|
|
+ // 水滴形状(略微扁圆)
|
|
|
+ ctx.ellipse(this.x, this.y, radiusX, radiusY, 0, 0, Math.PI * 2);
|
|
|
+ ctx.fill();
|
|
|
+ }
|
|
|
+
|
|
|
+ isDead() {
|
|
|
+ return this.alpha <= 0 || this.size <= 0.5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建水流粒子
|
|
|
+ function createWaterParticles(truckX, truckY, flameX, flameY) {
|
|
|
+ const particleCount = 8; // 每次发射粒子数量
|
|
|
+ for (let i = 0; i < particleCount; i++) {
|
|
|
+ // 从消防车位置附近随机发射
|
|
|
+ const offsetX = (Math.random() - 0.5) * 15;
|
|
|
+ const offsetY = (Math.random() - 0.5) * 10;
|
|
|
+ particles.push(
|
|
|
+ new WaterParticle(
|
|
|
+ truckX + offsetX,
|
|
|
+ truckY + offsetY,
|
|
|
+ flameX, // 目标火焰X
|
|
|
+ flameY // 目标火焰Y
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理水流粒子
|
|
|
+ function handleWaterParticles() {
|
|
|
+ for (let i = 0; i < particles.length; i++) {
|
|
|
+ particles[i].update();
|
|
|
+ particles[i].draw();
|
|
|
+ if (particles[i].isDead()) {
|
|
|
+ particles.splice(i, 1);
|
|
|
+ i--;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新水流位置(消防车 -> 火焰)
|
|
|
+ function updateWaterPosition() {
|
|
|
+ if (!vm.vehicleModel || !endPoint || !viewer) return;
|
|
|
+
|
|
|
+ // 获取消防车屏幕坐标 - 修复位置克隆问题
|
|
|
+ const currentTime = viewer.clock.currentTime;
|
|
|
+ // 获取当前位置的Cartesian3值再克隆
|
|
|
+ const truckPosition = vm.vehicleModel.position.getValue(currentTime);
|
|
|
+ const truckCartesian = truckPosition.clone(); // 现在可以安全调用clone()
|
|
|
+
|
|
|
+ const truckWindowPos = SkyScenery.SceneTransforms.wgs84ToWindowCoordinates(
|
|
|
+ viewer.scene,
|
|
|
+ truckCartesian
|
|
|
+ );
|
|
|
+
|
|
|
+ // 获取火焰屏幕坐标
|
|
|
+ const flameCartesian = SkyScenery.Cartesian3.fromDegrees(
|
|
|
+ parseFloat(endPoint.longitude),
|
|
|
+ parseFloat(endPoint.latitude),
|
|
|
+ 0
|
|
|
+ );
|
|
|
+ const flameWindowPos = SkyScenery.SceneTransforms.wgs84ToWindowCoordinates(
|
|
|
+ viewer.scene,
|
|
|
+ flameCartesian
|
|
|
+ );
|
|
|
+
|
|
|
+ if (truckWindowPos && flameWindowPos) {
|
|
|
+ // 设置Canvas位置(消防车位置)
|
|
|
+ canvas.style.left = `${truckWindowPos.x - canvas.width / 2}px`;
|
|
|
+ canvas.style.top = `${truckWindowPos.y - canvas.height / 2}px`;
|
|
|
+
|
|
|
+ // 创建水流粒子(从消防车到火焰)
|
|
|
+ createWaterParticles(
|
|
|
+ canvas.width / 2, // 消防车在Canvas内的X
|
|
|
+ canvas.height / 2, // 消防车在Canvas内的Y
|
|
|
+ flameWindowPos.x - truckWindowPos.x + canvas.width / 2, // 火焰相对X
|
|
|
+ flameWindowPos.y - truckWindowPos.y + canvas.height / 2 // 火焰相对Y
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 动画循环
|
|
|
+ function animate() {
|
|
|
+ // // 清除画布(保留轻微拖尾效果)
|
|
|
+ // ctx.fillStyle = "rgba(0, 0, 0, 0.05)";
|
|
|
+ // ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
+
|
|
|
+ // 修复:使用完全透明的背景,而不是半透明黑色
|
|
|
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
+
|
|
|
+ // 更新位置并创建粒子
|
|
|
+ updateWaterPosition();
|
|
|
+
|
|
|
+ // 绘制粒子
|
|
|
+ handleWaterParticles();
|
|
|
+
|
|
|
+ requestAnimationFrame(animate);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化
|
|
|
+ function init() {
|
|
|
+ // 监听地图渲染事件,更新水流位置
|
|
|
+ viewer.scene.postRender.addEventListener(updateWaterPosition);
|
|
|
+
|
|
|
+ // 开始动画
|
|
|
+ animate();
|
|
|
+
|
|
|
+ // 3秒后停止水流
|
|
|
+ setTimeout(() => {
|
|
|
+ waterIntensity = 0;
|
|
|
+ // 等待剩余粒子消失后移除Canvas
|
|
|
+ setTimeout(() => {
|
|
|
+ canvas.remove();
|
|
|
+ }, 1500);
|
|
|
+ }, 3000);
|
|
|
+
|
|
|
+ resolve(canvas);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 启动初始化
|
|
|
+ init();
|
|
|
+ } catch (error) {
|
|
|
+ reject(error);
|
|
|
+ }
|
|
|
+ });
|
|
|
},
|
|
|
// 完成模拟
|
|
|
completeSimulation() {
|
|
|
// 移除车辆和粒子效果
|
|
|
- if (this.vehicleModel) {
|
|
|
- viewer.entities.remove(this.vehicleModel);
|
|
|
- this.vehicleModel = null;
|
|
|
- }
|
|
|
- if (this.waterParticleSystem) {
|
|
|
- viewer.scene.primitives.remove(this.waterParticleSystem);
|
|
|
- this.waterParticleSystem = null;
|
|
|
- }
|
|
|
+ // if (this.vehicleModel) {
|
|
|
+ // viewer.entities.remove(this.vehicleModel);
|
|
|
+ // this.vehicleModel = null;
|
|
|
+ // }
|
|
|
|
|
|
// 显示完成弹窗
|
|
|
- this.showCompletionModal("作业完成,起火点已熄灭!");
|
|
|
+ this.showToast("作业完成,起火点已熄灭!");
|
|
|
|
|
|
// 1秒后显示模拟完成
|
|
|
setTimeout(() => {
|
|
|
- this.showCompletionModal("模拟完成");
|
|
|
+ this.showToast("模拟完成");
|
|
|
this.isSimulating = false;
|
|
|
}, 1000);
|
|
|
},
|
|
|
- // 显示完成弹窗
|
|
|
- showCompletionModal(message) {
|
|
|
- const modal = document.createElement("div");
|
|
|
- modal.className = "simulation-completion-modal";
|
|
|
- modal.innerHTML = `
|
|
|
- <div class="modal-content">
|
|
|
- <h3>${message}</h3>
|
|
|
- <button class="ok-btn" onclick="document.querySelector('.simulation-completion-modal').remove()">确定</button>
|
|
|
- </div>
|
|
|
- `;
|
|
|
- document.body.appendChild(modal);
|
|
|
- },
|
|
|
showToast(message) {
|
|
|
// 创建自定义提示框
|
|
|
let toast = document.createElement("div");
|
|
@@ -684,7 +1019,10 @@ export default {
|
|
|
this.removeMapMarker(type);
|
|
|
|
|
|
// 创建新标记
|
|
|
- const color = type === "start" ? SkyScenery.Color.BLUE : SkyScenery.Color.RED;
|
|
|
+ const color =
|
|
|
+ type === "start"
|
|
|
+ ? SkyScenery.Color.fromCssColorString("#409EFF") // 柔和蓝色
|
|
|
+ : SkyScenery.Color.fromCssColorString("#FF9F40"); // 柔和橙色
|
|
|
const labelText = type === "start" ? "起点" : type === "end" ? "终点" : "选点";
|
|
|
|
|
|
viewer.entities.add({
|
|
@@ -694,17 +1032,20 @@ export default {
|
|
|
point: {
|
|
|
show: true,
|
|
|
color: color,
|
|
|
- pixelSize: 20,
|
|
|
- outlineColor: SkyScenery.Color.YELLOW,
|
|
|
- outlineWidth: 3,
|
|
|
+ pixelSize: 12,
|
|
|
+ outlineColor: SkyScenery.Color.WHITE,
|
|
|
+ outlineWidth: 2,
|
|
|
},
|
|
|
label: {
|
|
|
text: labelText,
|
|
|
- font: "normal 18px 楷体",
|
|
|
+ font: "bold 16px 楷体", // 增大字号并加粗
|
|
|
fillColor: color,
|
|
|
horizontalOrigin: SkyScenery.HorizontalOrigin.CENTER,
|
|
|
verticalOrigin: SkyScenery.VerticalOrigin.BOTTOM,
|
|
|
- pixelOffset: new SkyScenery.Cartesian2(0, -20),
|
|
|
+ pixelOffset: new SkyScenery.Cartesian2(0, -15),
|
|
|
+ // 添加文字描边效果
|
|
|
+ outlineColor: SkyScenery.Color.BLACK,
|
|
|
+ outlineWidth: 2,
|
|
|
},
|
|
|
});
|
|
|
},
|
|
@@ -774,7 +1115,7 @@ export default {
|
|
|
viewer.imageryLayers.addImageryProvider(
|
|
|
new SkyScenery.ArcGisMapServerImageryProvider({
|
|
|
url:
|
|
|
- "https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer",
|
|
|
+ "https://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer",
|
|
|
})
|
|
|
);
|
|
|
|
|
@@ -993,39 +1334,4 @@ export default {
|
|
|
.select-btn:hover:not(.active) {
|
|
|
background-color: #66b1ff;
|
|
|
}
|
|
|
-/* 模拟完成弹窗样式 */
|
|
|
-.simulation-completion-modal {
|
|
|
- position: fixed;
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- background: rgba(0, 0, 0, 0.5);
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- align-items: center;
|
|
|
- z-index: 2000;
|
|
|
-}
|
|
|
-
|
|
|
-.simulation-completion-modal .modal-content {
|
|
|
- background: white;
|
|
|
- padding: 30px;
|
|
|
- border-radius: 8px;
|
|
|
- text-align: center;
|
|
|
- min-width: 300px;
|
|
|
-}
|
|
|
-
|
|
|
-.simulation-completion-modal h3 {
|
|
|
- color: #1f2d3d;
|
|
|
- margin-bottom: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.simulation-completion-modal .ok-btn {
|
|
|
- background-color: #1890ff;
|
|
|
- color: white;
|
|
|
- border: none;
|
|
|
- padding: 8px 16px;
|
|
|
- border-radius: 4px;
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
</style>
|