<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>无人机轨迹模拟 - A→B→C (带倾斜平板)</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a2a6c, #2c3e50);
color: #fff;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
display: block;
position: absolute;
top: 0;
left: 0;
}
#info-panel {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
border-radius: 15px;
padding: 20px;
max-width: 350px;
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
}
#info-panel h1 {
margin: 0 0 15px 0;
color: #4fc3f7;
font-size: 24px;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding-bottom: 10px;
}
.status-box {
background: rgba(30, 30, 46, 0.8);
border-radius: 10px;
padding: 15px;
margin: 10px 0;
}
.phase-container {
margin: 15px 0;
}
.phase {
display: flex;
align-items: center;
margin: 8px 0;
padding: 8px;
border-radius: 5px;
transition: all 0.3s;
background: rgba(50, 50, 70, 0.6);
}
.phase.active {
background: rgba(41, 98, 255, 0.4);
transform: translateX(5px);
}
.phase-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #555;
margin-right: 10px;
transition: all 0.3s;
}
.phase.active .phase-indicator {
background: #00e676;
box-shadow: 0 0 8px #00e676;
}
.progress-container {
height: 10px;
background: rgba(255, 255, 255, 0.1);
border-radius: 5px;
margin: 15px 0;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #00c853, #64ffda);
border-radius: 5px;
width: 0%;
transition: width 0.3s;
}
.data-panel {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 10px;
}
.data-box {
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
padding: 10px;
text-align: center;
}
.data-title {
font-size: 12px;
color: #aaa;
margin-bottom: 5px;
}
.data-value {
font-family: monospace;
font-size: 16px;
color: #64ffda;
}
.controls {
display: flex;
gap: 10px;
margin-top: 15px;
}
button {
background: rgba(76, 175, 80, 0.7);
border: none;
color: white;
padding: 12px 0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
flex: 1;
font-weight: bold;
letter-spacing: 0.5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
}
button:hover {
background: rgba(76, 175, 80, 1);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
button:active {
transform: translateY(1px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
#reset-btn {
background: rgba(244, 67, 54, 0.7);
}
#reset-btn:hover {
background: rgba(244, 67, 54, 1);
}
#speed-control {
display: flex;
align-items: center;
gap: 10px;
margin-top: 15px;
}
#speed-slider {
flex: 1;
height: 8px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.1);
outline: none;
-webkit-appearance: none;
}
#speed-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #64ffda;
cursor: pointer;
box-shadow: 0 0 5px rgba(100, 255, 218, 0.5);
}
#status-bar {
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.7);
border-radius: 10px;
padding: 15px;
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
}
.param-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
text-align: center;
}
.param-title {
font-size: 12px;
color: #aaa;
}
.param-value {
font-family: monospace;
font-size: 14px;
color: #64ffda;
}
.plate-tag {
position: absolute;
top: 5px;
right: 5px;
background: rgba(255, 100, 50, 0.8);
padding: 5px 10px;
border-radius: 10px;
font-size: 14px;
}
</style>
</head>
<body>
<div id="container">
<div id="info-panel">
<h1>无人机轨迹模拟</h1>
<div class="status-box">
<h3>飞行阶段状态</h3>
<div class="phase-container">
<div class="phase" id="phase1">
<div class="phase-indicator"></div>
<div>A→B上方 </div>
</div>
<div class="phase" id="phase2">
<div class="phase-indicator"></div>
<div>B上方→B点 (下降)</div>
</div>
<div class="phase" id="phase3">
<div class="phase-indicator"></div>
<div>B点停留</div>
</div>
<div class="phase" id="phase4">
<div class="phase-indicator"></div>
<div>B→C上方 </div>
</div>
<div class="phase" id="phase5">
<div class="phase-indicator"></div>
<div>C上方→C点 (下降)</div>
</div>
</div>
<div class="progress-container">
<div class="progress-bar" id="progress-bar"></div>
</div>
<div class="data-panel">
<div class="data-box">
<div class="data-title">当前位置</div>
<div class="data-value" id="position">(0.00, 0.00, 0.00)</div>
</div>
<div class="data-box">
<div class="data-title">飞行速度</div>
<div class="data-value" id="velocity">0.00 m/s</div>
</div>
</div>
</div>
<div class="controls">
<button id="start-btn">开始模拟</button>
<button id="pause-btn">暂停</button>
<button id="reset-btn">重置</button>
</div>
<div id="speed-control">
<span>速度: </span>
<input type="range" id="speed-slider" min="0.1" max="2" step="0.1" value="1">
<span id="speed-value">1.0x</span>
</div>
</div>
<div id="status-bar">
<div class="param-grid">
<div>
<div class="param-title">A→B上方</div>
<div class="param-value">8秒</div>
</div>
<div>
<div class="param-title">下降时间</div>
<div class="param-value">4秒</div>
</div>
<div>
<div class="param-title">停留时间</div>
<div class="param-value">5秒</div>
</div>
<div>
<div class="param-title">B→C上方</div>
<div class="param-value">10秒</div>
</div>
<div>
<div class="param-title">总时间</div>
<div class="param-value">31秒</div>
</div>
</div>
</div>
<div class="plate-tag">C点倾斜平台 (20×20m)</div>
</div>
<script>
// 定义关键点 - 高度调整
const A = { x: 0, y: 0, z: 0 };
const B = { x: 50, y: 1, z: 50 };
const C = { x: 100, y: 10, z: 100 };
const B_above = { x: 50, y: 10, z: 50 }; // 高度从3改为10
const C_above = { x: 100, y: 20, z: 100 }; // 高度从12改为20
// 抛物线最高点高度
const max_height = 25;
// 时间参数(秒)
const t_AB_parabola = 8.0;
const t_B_descent = 4.0;
const t_stay = 5.0;
const t_BC_linear = 10.0;
const t_C_descent = 4.0;
const total_time = t_AB_parabola + t_B_descent + t_stay + t_BC_linear + t_C_descent;
// 动画变量
let clock = new THREE.Clock();
let elapsedTime = 0;
let isPlaying = false;
let drone, scene, camera, renderer, controls;
let points = {}, paths = {};
let animationId;
let speedFactor = 1.0;
// 初始化场景
function init() {
// 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a1a);
scene.fog = new THREE.Fog(0x0a0a1a, 50, 200);
// 创建相机
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(70, 120, 70);
camera.lookAt(50, 0, 50);
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById('container').appendChild(renderer.domElement);
// 添加轨道控制
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 20;
controls.maxDistance = 300;
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040, 2);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 添加坐标轴
const axesHelper = new THREE.AxesHelper(20);
scene.add(axesHelper);
// 创建网格地面
const gridHelper = new THREE.GridHelper(200, 20, 0x444444, 0x222222);
scene.add(gridHelper);
// 创建高度标记
createHeightMarkers();
// 创建关键点标记
createPoint(A, 0xff5555, 'A (0,0,0)', 1.5);
createPoint(B, 0x55ff55, 'B (50,50,1)', 1.5);
createPoint(C, 0x5555ff, 'C (100,100,10)', 1.5);
createPoint(B_above, 0xffff55, `B上方 (50,50,${B_above.y})`, 1.0);
createPoint(C_above, 0xff55ff, `C上方 (100,100,${C_above.y})`, 1.0);
// 创建无人机
createDrone();
// 创建路径
createPath(A, B_above, 0xff5555, 'A→B上方 (抛物线)');
createPath(B_above, B, 0x55ff55, 'B上方→B点 (下降)');
createPath(B, C_above, 0x5555ff, 'B→C上方 (直线)');
createPath(C_above, C, 0xff55ff, 'C上方→C点 (下降)');
// 在C点创建倾斜平板模型
createTiltedPlateAtC();
// 添加窗口大小调整事件
window.addEventListener('resize', onWindowResize);
// 添加按钮事件
document.getElementById('start-btn').addEventListener('click', startSimulation);
document.getElementById('pause-btn').addEventListener('click', pauseSimulation);
document.getElementById('reset-btn').addEventListener('click', resetSimulation);
document.getElementById('speed-slider').addEventListener('input', updateSpeed);
// 开始动画
animate();
}
// 在C点创建倾斜平板模型
function createTiltedPlateAtC() {
const plateSize = 20; // 20×20米
// 创建平板几何体
const geometry = new THREE.PlaneGeometry(plateSize, plateSize, 20, 20);
// 创建材质 - 半透明的网格效果
const material = new THREE.MeshPhongMaterial({
color: 0x2980b9,
transparent: true,
opacity: 0.8,
wireframe: true,
side: THREE.DoubleSide
});
// 创建平板网格
const plate = new THREE.Mesh(geometry, material);
// 定位在C点上方 (平台中心与C点重合)
plate.position.set(C.x, C.y, C.z);
// 应用旋转使平板在xz平面倾斜
// 1. 首先使平板旋转到水平位置(默认是垂直的)
plate.rotateX(Math.PI / 2);
// 2. 再绕z轴旋转15度(在xy平面投影与x轴正方向呈15度夹角)
plate.rotateZ(15 * Math.PI / 180);
// 添加边框轮廓增强视觉效果
const edges = new THREE.EdgesGeometry(geometry);
const lineMaterial = new THREE.LineBasicMaterial({ color: 0x1abc9c, linewidth: 2 });
const edgeLines = new THREE.LineSegments(edges, lineMaterial);
edgeLines.position.set(C.x, C.y, C.z);
edgeLines.rotateX(Math.PI / 2);
edgeLines.rotateZ(15 * Math.PI / 180);
// 添加到场景
scene.add(plate);
scene.add(edgeLines);
// 添加标签
createMarker(C.x, C.y + 2, C.z, 0x3498db, "C点平台");
}
// 创建高度标记
function createHeightMarkers() {
// 抛物线顶点标记
const midX = (A.x + B_above.x) / 2;
const midZ = (A.z + B_above.z) / 2;
createMarker(midX, max_height, midZ, 0x00ff00, `顶点 (25m)`);
// 创建基准高度标记
createMarker(10, 0, -10, 0xffffff, "0m");
createMarker(10, 10, -10, 0xffff00, "10m");
createMarker(10, 20, -10, 0xff00ff, "20m");
createMarker(10, 25, -10, 0x00ff00, "25m");
}
function createMarker(x, y, z, color, label) {
const sphereGeometry = new THREE.SphereGeometry(0.8, 16, 16);
const sphereMaterial = new THREE.MeshBasicMaterial({ color: color });
const marker = new THREE.Mesh(sphereGeometry, sphereMaterial);
marker.position.set(x, y, z);
scene.add(marker);
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = 256;
canvas.height = 64;
context.fillStyle = 'rgba(0, 0, 0, 0.7)';
context.fillRect(0, 0, canvas.width, canvas.height);
context.font = '24px Arial';
context.fillStyle = 'white';
context.textAlign = 'center';
context.fillText(label, canvas.width/2, canvas.height/2 + 8);
const texture = new THREE.CanvasTexture(canvas);
const spriteMaterial = new THREE.SpriteMaterial({ map: texture });
const sprite = new THREE.Sprite(spriteMaterial);
sprite.position.set(x, y + 1.5, z);
sprite.scale.set(10, 2.5, 1);
scene.add(sprite);
}
// 创建无人机(方体机身,螺旋桨绕Y轴旋转)
function createDrone() {
const droneGroup = new THREE.Group();
// 方体机身(长方体)
const bodyGeometry = new THREE.BoxGeometry(2, 0.8, 2);
const bodyMaterial = new THREE.MeshPhongMaterial({
color: 0x00ffff,
emissive: 0x004444,
specular: 0xffffff,
shininess: 100,
wireframe: false
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
droneGroup.add(body);
// 添加无人机螺旋桨(绕Y轴旋转)
const propellerGroup = new THREE.Group();
// 四个螺旋桨的位置(正方形的四个角)
const propellerPositions = [
{ x: 1.5, y: 0, z: 1.5 },
{ x: -1.5, y: 0, z: 1.5 },
{ x: 1.5, y: 0, z: -1.5 },
{ x: -1.5, y: 0, z: -1.5 }
];
propellerPositions.forEach(pos => {
const propeller = new THREE.Mesh(
new THREE.CylinderGeometry(0.25, 0.25, 0.1, 16),
new THREE.MeshPhongMaterial({ color: 0xaaaaaa })
);
// 初始旋转使螺旋桨平行于XZ平面
propeller.rotation.x = Math.PI / 2;
propeller.position.set(pos.x, pos.y + 0.4, pos.z);
propellerGroup.add(propeller);
});
droneGroup.add(propellerGroup);
droneGroup.position.set(A.x, A.y, A.z);
scene.add(droneGroup);
drone = droneGroup;
}
// 创建关键点标记
function createPoint(position, color, label, size = 1) {
const geometry = new THREE.SphereGeometry(size, 16, 16);
const material = new THREE.MeshPhongMaterial({
color: color,
emissive: color,
emissiveIntensity: 0.2
});
const sphere = new THREE.Mesh(geometry, material);
sphere.position.set(position.x, position.y, position.z);
scene.add(sphere);
// 添加标签
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = 256;
canvas.height = 64;
context.fillStyle = 'rgba(0, 0, 0, 0.7)';
context.fillRect(0, 0, canvas.width, canvas.height);
context.font = '24px Arial';
context.fillStyle = 'white';
context.textAlign = 'center';
context.fillText(label, canvas.width/2, canvas.height/2 + 8);
const texture = new THREE.CanvasTexture(canvas);
const spriteMaterial = new THREE.SpriteMaterial({ map: texture });
const sprite = new THREE.Sprite(spriteMaterial);
sprite.position.set(position.x, position.y, position.z + size + 2);
sprite.scale.set(10, 2.5, 1);
scene.add(sprite);
// 保存点以便后续引用
points[label] = sphere;
}
// 创建路径
function createPath(start, end, color, name) {
const points = [];
const material = new THREE.LineBasicMaterial({
color: color,
linewidth: 2
});
if (name === 'A→B上方 (抛物线)') {
// 抛物线路径,最高点25米
for (let t = 0; t <= 1; t += 0.05) {
const x = start.x + t * (end.x - start.x);
const y = start.y + t * (end.y - start.y) + 4 * max_height * t * (1 - t);
const z = start.z + t * (end.z - start.z);
points.push(new THREE.Vector3(x, y, z));
}
} else {
// 直线路径
for (let t = 0; t <= 1; t += 0.05) {
const x = start.x + t * (end.x - start.x);
const y = start.y + t * (end.y - start.y);
const z = start.z + t * (end.z - start.z);
points.push(new THREE.Vector3(x, y, z));
}
}
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(geometry, material);
scene.add(line);
// 保存路径
paths[name] = line;
}
// 计算无人机位置
function getDronePosition(time) {
// A→B上方的抛物线运动
if (time < t_AB_parabola) {
setActivePhase(1);
const t = time / t_AB_parabola;
const x = A.x + t * (B_above.x - A.x);
const y = A.y + t * (B_above.y - A.y) + 4 * max_height * t * (1 - t);
const z = A.z + t * (B_above.z - A.z);
return { x, y, z };
}
// B上方→B点的缓慢下降
else if (time < t_AB_parabola + t_B_descent) {
setActivePhase(2);
const t = (time - t_AB_parabola) / t_B_descent;
const x = B_above.x + t * (B.x - B_above.x);
const y = B_above.y + t * (B.y - B_above.y);
const z = B_above.z + t * (B.z - B_above.z);
return { x, y, z };
}
// B点停留
else if (time < t_AB_parabola + t_B_descent + t_stay) {
setActivePhase(3);
return { x: B.x, y: B.y, z: B.z };
}
// B→C上方的直线运动
else if (time < t_AB_parabola + t_B_descent + t_stay + t_BC_linear) {
setActivePhase(4);
const t = (time - (t_AB_parabola + t_B_descent + t_stay)) / t_BC_linear;
const x = B.x + t * (C_above.x - B.x);
const y = B.y + t * (C_above.y - B.y);
const z = B.z + t * (C_above.z - B.z);
return { x, y, z };
}
// C上方→C点的缓慢下降
else {
setActivePhase(5);
const t = (time - (t_AB_parabola + t_B_descent + t_stay + t_BC_linear)) / t_C_descent;
const x = C_above.x + t * (C.x - C_above.x);
const y = C_above.y + t * (C.y - C_above.y);
const z = C_above.z + t * (C.z - C_above.z);
return { x, y, z };
}
}
// 设置活动阶段
function setActivePhase(phaseNum) {
// 重置所有阶段
for (let i = 1; i <= 5; i++) {
const phaseElement = document.getElementById(`phase${i}`);
phaseElement.classList.remove('active');
}
// 激活当前阶段
const currentPhase = document.getElementById(`phase${phaseNum}`);
if (currentPhase) {
currentPhase.classList.add('active');
}
}
// 开始模拟
function startSimulation() {
if (!isPlaying) {
isPlaying = true;
clock.start();
}
}
// 暂停模拟
function pauseSimulation() {
isPlaying = false;
clock.stop();
}
// 重置模拟
function resetSimulation() {
isPlaying = false;
elapsedTime = 0;
clock.stop();
clock = new THREE.Clock();
drone.position.set(A.x, A.y, A.z);
document.getElementById('progress-bar').style.width = '0%';
document.getElementById('position').textContent = '(0.00, 0.00, 0.00)';
document.getElementById('velocity').textContent = '0.00 m/s';
// 重置所有阶段
for (let i = 1; i <= 5; i++) {
const phaseElement = document.getElementById(`phase${i}`);
phaseElement.classList.remove('active');
}
}
// 更新速度
function updateSpeed() {
speedFactor = parseFloat(document.getElementById('speed-slider').value);
document.getElementById('speed-value').textContent = speedFactor.toFixed(1) + 'x';
}
// 窗口大小调整
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// 动画循环
function animate() {
animationId = requestAnimationFrame(animate);
if (isPlaying) {
const delta = Math.min(clock.getDelta(), 0.1) * speedFactor;
elapsedTime += delta;
// 更新无人机位置
if (elapsedTime <= total_time) {
const position = getDronePosition(elapsedTime);
drone.position.set(position.x, position.y, position.z);
// 更新UI
document.getElementById('progress-bar').style.width = `${(elapsedTime / total_time) * 100}%`;
document.getElementById('position').textContent =
`(${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)})`;
// 计算速度(简单近似)
const prevPosition = getDronePosition(Math.max(0, elapsedTime - delta));
const distance = Math.sqrt(
Math.pow(position.x - prevPosition.x, 2) +
Math.pow(position.y - prevPosition.y, 2) +
Math.pow(position.z - prevPosition.z, 2)
);
const speed = distance / delta;
document.getElementById('velocity').textContent = `${speed.toFixed(2)} m/s`;
// 旋转螺旋桨(绕Y轴)
if (drone.children[1]) {
drone.children[1].rotation.y += delta * 20;
}
} else {
// 模拟结束
isPlaying = false;
}
}
controls.update();
renderer.render(scene, camera);
}
// 初始化场景
init();
</script>
</body>
</html>
index.html
main.js
package.json
index.html