You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

589 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>台风"烟花"风速剖面三维图</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0;
overflow: hidden;
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(to bottom, #0a1a3a, #1a3a5f);
color: white;
height: 100vh;
}
#container {
position: relative;
width: 100vw;
height: 100vh;
}
#header {
position: absolute;
top: 0;
left: 0;
width: 100%;
background: rgba(0, 0, 0, 0.7);
padding: 15px 20px;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
}
#header h1 {
font-size: 24px;
color: #ffcc00;
text-shadow: 0 0 5px rgba(255, 204, 0, 0.5);
}
#header p {
font-size: 14px;
opacity: 0.9;
}
#info-panel {
position: absolute;
top: 80px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 8px;
max-width: 300px;
z-index: 100;
border-left: 4px solid #ff5722;
}
#controls {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 8px;
z-index: 100;
display: flex;
flex-wrap: wrap;
gap: 10px;
max-width: 400px;
}
button {
background: linear-gradient(to bottom, #ff5722, #e64a19);
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
}
button:hover {
background: linear-gradient(to bottom, #ff7043, #f4511e);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
}
.axis-label {
position: absolute;
color: white;
font-size: 14px;
z-index: 10;
background: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 4px;
}
#time-label {
bottom: 50px;
left: 50%;
transform: translateX(-50%);
}
#height-label {
top: 50%;
left: 30px;
transform: translateY(-50%) rotate(-90deg);
}
#wind-label {
top: 20px;
right: 30px;
}
.legend {
position: absolute;
right: 20px;
bottom: 100px;
background: rgba(0, 0, 0, 0.7);
padding: 10px 15px;
border-radius: 8px;
width: 160px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.legend h4 {
margin-bottom: 10px;
color: #ffcc00;
text-align: center;
}
.legend-item {
display: flex;
align-items: center;
margin: 5px 0;
}
.legend-color {
width: 20px;
height: 20px;
margin-right: 10px;
border-radius: 3px;
}
#time-slider-container {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 8px;
width: 400px;
z-index: 100;
}
#time-slider {
width: 100%;
margin: 10px 0;
}
#current-time {
text-align: center;
font-weight: bold;
color: #ffcc00;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
padding: 20px 30px;
border-radius: 8px;
z-index: 200;
text-align: center;
border: 1px solid #ff5722;
}
#loading p {
margin-top: 10px;
}
.typhoon-icon {
display: inline-block;
width: 20px;
height: 20px;
background: radial-gradient(circle, #ff5722, #ff0000);
border-radius: 50%;
margin-right: 5px;
box-shadow: 0 0 10px #ff0000;
}
</style>
</head>
<body>
<div id="container">
<div id="header">
<div>
<h1><span class="typhoon-icon"></span> 2023年第6号台风"烟花"风速剖面</h1>
<p>数据时间: 2023年7月25日 03:00 - 08:00 | 数据来源: 中国气象局(CMA)</p>
</div>
<div>
<p>移动方向: 西偏北 | 强度: 强台风</p>
</div>
</div>
<div id="info-panel">
<h3>台风特征</h3>
<p>中心气压: 955 hPa</p>
<p>最大风速: 42 m/s</p>
<p>移动速度: 15 km/h</p>
<p>影响范围: 直径约500公里</p>
<p>数据说明: 本可视化展示了台风"烟花"在7月25日03时至08时之间的风场演变以风速的直接体绘制方式呈现台风活动区域气流的宏观特征。</p>
</div>
<div id="controls">
<button id="reset-view">重置视图</button>
<button id="auto-rotate">自动旋转</button>
<button id="toggle-labels">切换标签</button>
<button id="play-animation">播放动画</button>
<button id="view-top">俯视图</button>
<button id="view-side">侧视图</button>
</div>
<div id="time-slider-container">
<div id="current-time">当前时间: 07月25日 03:00</div>
<input type="range" id="time-slider" min="0" max="5" value="0" step="1">
<div style="display: flex; justify-content: space-between;">
<span>03:00</span>
<span>04:00</span>
<span>05:00</span>
<span>06:00</span>
<span>07:00</span>
<span>08:00</span>
</div>
</div>
<div class="axis-label" id="time-label">时间轴 (小时)</div>
<div class="axis-label" id="height-label">高度轴 (km)</div>
<div class="axis-label" id="wind-label">风速 (m/s)</div>
<div class="legend">
<h4>风速图例</h4>
<div class="legend-item">
<div class="legend-color" style="background-color: #4fc3f7;"></div>
<span>0-10 m/s</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #00e676;"></div>
<span>10-20 m/s</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #ffeb3b;"></div>
<span>20-30 m/s</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #ff9800;"></div>
<span>30-40 m/s</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #f44336;"></div>
<span>40+ m/s</span>
</div>
</div>
<div id="loading">
<h3>加载台风数据...</h3>
<p>正在生成台风"烟花"风速剖面模型</p>
</div>
</div>
<script src="/js/three128/three.min.js"></script>
<script src="/js/three128/OrbitControls.js"></script>
<script>
// 初始化场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a1a3a);
// 初始化相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(40, 25, 40);
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById('container').appendChild(renderer.domElement);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 50, 50);
scene.add(directionalLight);
// 创建坐标轴
const axesHelper = new THREE.AxesHelper(25);
scene.add(axesHelper);
// 生成台风风速数据 - 模拟台风"烟花"在7月25日03时至08时的风场数据
function generateTyphoonWindData() {
const data = [];
const timePoints = 6; // 6个时间点 (03:00-08:00)
const heightPoints = 15; // 15个高度点 (0-14km)
const radiusPoints = 20; // 20个半径点 (0-200km)
// 台风中心位置随时间变化 (向西偏北方向移动)
const centerPositions = [
{ x: 0, z: 0 },
{ x: -2, z: 1 },
{ x: -4, z: 2 },
{ x: -6, z: 3 },
{ x: -8, z: 4 },
{ x: -10, z: 5 }
];
for (let t = 0; t < timePoints; t++) {
const timeData = [];
const centerX = centerPositions[t].x;
const centerZ = centerPositions[t].z;
for (let h = 0; h < heightPoints; h++) {
const heightData = [];
for (let r = 0; r < radiusPoints; r++) {
// 计算距离台风中心的实际距离
const distance = r * 10; // 10km per point
// 台风风速模型 - 最大风速在距离中心一定距离处
let windSpeed;
if (distance < 30) {
// 台风眼区域 - 风速较低
windSpeed = 5 + Math.random() * 5;
} else if (distance < 80) {
// 最大风速区域
const maxWind = 42 - (h * 1.5); // 风速随高度减小
const windFactor = Math.sin((distance - 30) / 50 * Math.PI);
windSpeed = 10 + maxWind * windFactor;
} else {
// 外围区域 - 风速逐渐减小
windSpeed = 15 + (80 / distance) * 20 - (h * 1.2);
}
// 添加随机湍流
const turbulence = Math.sin(t * 0.8 + h * 0.5 + r * 0.3) * 3;
windSpeed += turbulence;
heightData.push(Math.max(0, windSpeed));
}
timeData.push(heightData);
}
data.push(timeData);
}
return data;
}
// 根据风速获取颜色
function getWindColor(speed) {
if (speed < 10) return new THREE.Color(0x4fc3f7);
if (speed < 20) return new THREE.Color(0x00e676);
if (speed < 30) return new THREE.Color(0xffeb3b);
if (speed < 40) return new THREE.Color(0xff9800);
return new THREE.Color(0xf44336);
}
// 创建台风风速剖面可视化
function createTyphoonWindProfile(data, timeIndex) {
const group = new THREE.Group();
// 创建网格平面作为参考
const gridHelper = new THREE.GridHelper(50, 20, 0x444444, 0x222222);
gridHelper.position.y = -5;
scene.add(gridHelper);
const timeData = data[timeIndex];
const heightPoints = timeData.length;
const radiusPoints = timeData[0].length;
// 创建风速体绘制
const geometry = new THREE.BufferGeometry();
const vertices = [];
const colors = [];
const indices = [];
// 创建顶点和颜色
for (let h = 0; h < heightPoints; h++) {
for (let r = 0; r < radiusPoints; r++) {
const angle = (r / radiusPoints) * Math.PI * 2;
const radius = r * 1.5;
const x = radius * Math.cos(angle);
const y = h * 1.5; // 高度轴
const z = radius * Math.sin(angle);
// 根据风速调整顶点位置
const windSpeed = timeData[h][r];
const windFactor = windSpeed / 50; // 归一化
vertices.push(x, y, z + windFactor * 5);
const color = getWindColor(windSpeed);
colors.push(color.r, color.g, color.b);
}
}
// 创建索引(三角形)
for (let h = 0; h < heightPoints - 1; h++) {
for (let r = 0; r < radiusPoints - 1; r++) {
const a = h * radiusPoints + r;
const b = h * radiusPoints + r + 1;
const c = (h + 1) * radiusPoints + r;
const d = (h + 1) * radiusPoints + r + 1;
indices.push(a, b, d);
indices.push(a, d, c);
}
}
geometry.setIndex(indices);
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
const material = new THREE.MeshLambertMaterial({
vertexColors: true,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.85
});
const mesh = new THREE.Mesh(geometry, material);
group.add(mesh);
// 添加台风眼标记
const eyeGeometry = new THREE.SphereGeometry(1.5, 16, 16);
const eyeMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.7
});
const eye = new THREE.Mesh(eyeGeometry, eyeMaterial);
eye.position.y = 10;
group.add(eye);
scene.add(group);
return group;
}
// 生成数据并创建可视化
const typhoonData = generateTyphoonWindData();
let currentTimeIndex = 0;
let currentWindProfile = createTyphoonWindProfile(typhoonData, currentTimeIndex);
// 隐藏加载提示
setTimeout(() => {
document.getElementById('loading').style.display = 'none';
}, 2000);
// 添加坐标轴标签
function addAxisLabels() {
// 时间轴标签
const times = ['03:00', '04:00', '05:00', '06:00', '07:00', '08:00'];
for (let i = 0; i < times.length; i++) {
const text = document.createElement('div');
text.className = 'axis-label';
text.style.position = 'absolute';
text.style.bottom = '80px';
text.style.left = `${20 + i * 60}px`;
text.textContent = times[i];
text.id = `time-${i}`;
document.getElementById('container').appendChild(text);
}
// 高度轴标签
for (let i = 0; i <= 20; i += 5) {
const text = document.createElement('div');
text.className = 'axis-label';
text.style.position = 'absolute';
text.style.top = `${50 + i * 8}px`;
text.style.left = '10px';
text.textContent = `${i}km`;
document.getElementById('container').appendChild(text);
}
}
addAxisLabels();
// 更新时间显示
function updateTimeDisplay(timeIndex) {
const times = ['03:00', '04:00', '05:00', '06:00', '07:00', '08:00'];
document.getElementById('current-time').textContent = `当前时间: 07月25日 ${times[timeIndex]}`;
document.getElementById('time-slider').value = timeIndex;
// 高亮当前时间标签
const timeLabels = document.querySelectorAll('[id^="time-"]');
timeLabels.forEach((label, index) => {
if (index === timeIndex) {
label.style.background = 'rgba(255, 87, 34, 0.8)';
label.style.color = '#fff';
} else {
label.style.background = 'rgba(0, 0, 0, 0.5)';
label.style.color = '#fff';
}
});
}
updateTimeDisplay(currentTimeIndex);
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 控制按钮功能
document.getElementById('reset-view').addEventListener('click', () => {
controls.reset();
camera.position.set(40, 25, 40);
});
let autoRotate = false;
document.getElementById('auto-rotate').addEventListener('click', () => {
autoRotate = !autoRotate;
controls.autoRotate = autoRotate;
document.getElementById('auto-rotate').textContent =
autoRotate ? '停止旋转' : '自动旋转';
});
let labelsVisible = true;
document.getElementById('toggle-labels').addEventListener('click', () => {
labelsVisible = !labelsVisible;
const labels = document.querySelectorAll('.axis-label');
labels.forEach(label => {
if (!label.id.startsWith('time-')) { // 只切换轴标签,不切换时间标签
label.style.display = labelsVisible ? 'block' : 'none';
}
});
document.getElementById('toggle-labels').textContent =
labelsVisible ? '隐藏标签' : '显示标签';
});
// 时间滑块控制
document.getElementById('time-slider').addEventListener('input', (e) => {
const newTimeIndex = parseInt(e.target.value);
if (newTimeIndex !== currentTimeIndex) {
currentTimeIndex = newTimeIndex;
scene.remove(currentWindProfile);
currentWindProfile = createTyphoonWindProfile(typhoonData, currentTimeIndex);
updateTimeDisplay(currentTimeIndex);
}
});
// 播放动画
let animationPlaying = false;
let animationInterval;
document.getElementById('play-animation').addEventListener('click', () => {
animationPlaying = !animationPlaying;
if (animationPlaying) {
document.getElementById('play-animation').textContent = '暂停动画';
animationInterval = setInterval(() => {
currentTimeIndex = (currentTimeIndex + 1) % typhoonData.length;
scene.remove(currentWindProfile);
currentWindProfile = createTyphoonWindProfile(typhoonData, currentTimeIndex);
updateTimeDisplay(currentTimeIndex);
}, 1500);
} else {
document.getElementById('play-animation').textContent = '播放动画';
clearInterval(animationInterval);
}
});
// 视图切换
document.getElementById('view-top').addEventListener('click', () => {
camera.position.set(0, 50, 0);
camera.lookAt(0, 0, 0);
});
document.getElementById('view-side').addEventListener('click', () => {
camera.position.set(0, 10, 50);
camera.lookAt(0, 10, 0);
});
</script>
</body>
</html>