672 lines
23 KiB
HTML
672 lines
23 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>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
|
|
body {
|
|
background: linear-gradient(135deg, #1a2a6c, #2c3e50);
|
|
color: #fff;
|
|
overflow: hidden;
|
|
height: 100vh;
|
|
}
|
|
|
|
.container {
|
|
display: flex;
|
|
height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
#experiment-container {
|
|
flex: 1;
|
|
position: relative;
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.5);
|
|
background: rgba(10, 15, 30, 0.7);
|
|
}
|
|
|
|
#canvas-container {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.info-panel {
|
|
width: 350px;
|
|
padding: 25px;
|
|
background: rgba(0, 15, 30, 0.85);
|
|
border-left: 2px solid #00b4d8;
|
|
overflow-y: auto;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.info-panel h1 {
|
|
color: #00b4d8;
|
|
margin-bottom: 20px;
|
|
font-size: 28px;
|
|
text-align: center;
|
|
text-shadow: 0 0 10px rgba(0, 180, 216, 0.5);
|
|
}
|
|
|
|
.experiment-steps {
|
|
background: rgba(0, 30, 60, 0.6);
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
margin-bottom: 25px;
|
|
border: 1px solid #0077b6;
|
|
}
|
|
|
|
.experiment-steps h2 {
|
|
color: #90e0ef;
|
|
margin-bottom: 15px;
|
|
font-size: 20px;
|
|
}
|
|
|
|
.steps {
|
|
list-style-type: none;
|
|
counter-reset: step-counter;
|
|
}
|
|
|
|
.steps li {
|
|
position: relative;
|
|
padding-left: 35px;
|
|
margin-bottom: 15px;
|
|
line-height: 1.5;
|
|
font-size: 16px;
|
|
color: #caf0f8;
|
|
}
|
|
|
|
.steps li:before {
|
|
content: counter(step-counter);
|
|
counter-increment: step-counter;
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 24px;
|
|
height: 24px;
|
|
background: #0077b6;
|
|
border-radius: 50%;
|
|
text-align: center;
|
|
line-height: 24px;
|
|
font-weight: bold;
|
|
color: #fff;
|
|
}
|
|
|
|
.chemical-equation {
|
|
text-align: center;
|
|
font-size: 22px;
|
|
margin: 20px 0;
|
|
padding: 15px;
|
|
background: rgba(0, 60, 120, 0.3);
|
|
border-radius: 8px;
|
|
border: 1px solid #48cae4;
|
|
font-family: 'Cambria', serif;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.control-btn {
|
|
padding: 12px;
|
|
background: linear-gradient(45deg, #0077b6, #00b4d8);
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: white;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.control-btn:hover {
|
|
background: linear-gradient(45deg, #00b4d8, #0077b6);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 15px rgba(0, 180, 216, 0.4);
|
|
}
|
|
|
|
.control-btn:active {
|
|
transform: translateY(1px);
|
|
}
|
|
|
|
.control-btn i {
|
|
font-size: 20px;
|
|
}
|
|
|
|
.safety-note {
|
|
background: rgba(150, 0, 0, 0.2);
|
|
border-left: 4px solid #ff0000;
|
|
padding: 15px;
|
|
border-radius: 0 8px 8px 0;
|
|
margin-top: 25px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.safety-note h3 {
|
|
color: #ff9e00;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.status-bar {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
background: rgba(0, 30, 60, 0.8);
|
|
padding: 12px 20px;
|
|
border-radius: 30px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
border: 1px solid #00b4d8;
|
|
z-index: 10;
|
|
}
|
|
|
|
.status-indicator {
|
|
width: 15px;
|
|
height: 15px;
|
|
border-radius: 50%;
|
|
background: #4CAF50;
|
|
box-shadow: 0 0 10px #4CAF50;
|
|
}
|
|
|
|
.status-text {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.bubble {
|
|
position: absolute;
|
|
background: rgba(200, 230, 255, 0.7);
|
|
border-radius: 50%;
|
|
animation: float 4s infinite ease-in-out;
|
|
box-shadow: 0 0 10px rgba(173, 216, 230, 0.8);
|
|
}
|
|
|
|
@keyframes float {
|
|
0%, 100% { transform: translateY(0) translateX(0); }
|
|
25% { transform: translateY(-20px) translateX(5px); }
|
|
50% { transform: translateY(-40px) translateX(-5px); }
|
|
75% { transform: translateY(-20px) translateX(5px); }
|
|
}
|
|
|
|
.progress-container {
|
|
width: 100%;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 5px;
|
|
margin: 20px 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 8px;
|
|
background: linear-gradient(90deg, #0077b6, #00b4d8);
|
|
width: 0%;
|
|
transition: width 1s ease;
|
|
}
|
|
|
|
.logo {
|
|
text-align: center;
|
|
margin-top: 20px;
|
|
font-size: 18px;
|
|
color: #90e0ef;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.container {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.info-panel {
|
|
width: 100%;
|
|
height: 40%;
|
|
}
|
|
|
|
#experiment-container {
|
|
height: 60%;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div id="experiment-container">
|
|
<div id="canvas-container"></div>
|
|
<div class="status-bar">
|
|
<div class="status-indicator"></div>
|
|
<div class="status-text">实验准备就绪</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-panel">
|
|
<h1>氢气制取互动实验</h1>
|
|
|
|
<div class="experiment-steps">
|
|
<h2>实验步骤</h2>
|
|
<ol class="steps">
|
|
<li>将锌粒放入锥形瓶中</li>
|
|
<li>向锥形瓶中加入稀硫酸</li>
|
|
<li>连接气体发生装置</li>
|
|
<li>使用排水法收集氢气</li>
|
|
<li>检验氢气纯度</li>
|
|
</ol>
|
|
</div>
|
|
|
|
<div class="chemical-equation">
|
|
Zn + H<sub>2</sub>SO<sub>4</sub> → ZnSO<sub>4</sub> + H<sub>2</sub>↑
|
|
</div>
|
|
|
|
<div class="progress-container">
|
|
<div class="progress-bar" id="reaction-progress"></div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button class="control-btn" id="add-zinc">
|
|
<i>⚪</i> 添加锌粒
|
|
</button>
|
|
<button class="control-btn" id="add-acid">
|
|
<i>🧪</i> 加入稀硫酸
|
|
</button>
|
|
<button class="control-btn" id="start-reaction">
|
|
<i>🔥</i> 开始反应
|
|
</button>
|
|
<button class="control-btn" id="collect-gas">
|
|
<i>💧</i> 收集氢气
|
|
</button>
|
|
<button class="control-btn" id="reset-experiment">
|
|
<i>🔄</i> 重置实验
|
|
</button>
|
|
</div>
|
|
|
|
<div class="safety-note">
|
|
<h3>安全注意事项</h3>
|
|
<p>1. 氢气易燃易爆,实验需远离明火</p>
|
|
<p>2. 使用稀硫酸时需佩戴防护手套</p>
|
|
<p>3. 收集前必须检验氢气纯度</p>
|
|
<p>4. 实验应在通风环境下进行</p>
|
|
</div>
|
|
|
|
<div class="logo">
|
|
3D化学实验室 | 互动教学系统
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 初始化Three.js场景
|
|
let scene, camera, renderer, controls;
|
|
let experimentObjects = {};
|
|
let bubbles = [];
|
|
let reactionStarted = false;
|
|
|
|
init();
|
|
animate();
|
|
|
|
function init() {
|
|
// 创建场景
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x0a1a2a);
|
|
scene.fog = new THREE.Fog(0x0a1a2a, 10, 25);
|
|
|
|
// 创建相机
|
|
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
camera.position.set(0, 2, 5);
|
|
|
|
// 创建渲染器
|
|
const container = document.getElementById('canvas-container');
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
renderer.shadowMap.enabled = true;
|
|
container.appendChild(renderer.domElement);
|
|
|
|
// 添加轨道控制
|
|
controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
|
|
// 添加光源
|
|
const ambientLight = new THREE.AmbientLight(0x404040, 2);
|
|
scene.add(ambientLight);
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
|
directionalLight.position.set(5, 10, 7);
|
|
directionalLight.castShadow = true;
|
|
scene.add(directionalLight);
|
|
|
|
const pointLight = new THREE.PointLight(0x00aaff, 1, 20);
|
|
pointLight.position.set(0, 3, 0);
|
|
pointLight.castShadow = true;
|
|
scene.add(pointLight);
|
|
|
|
// 创建实验室环境
|
|
createLabEnvironment();
|
|
|
|
// 创建实验器材
|
|
createExperimentEquipment();
|
|
|
|
// 响应窗口大小变化
|
|
window.addEventListener('resize', onWindowResize);
|
|
|
|
// 添加事件监听器
|
|
setupEventListeners();
|
|
}
|
|
|
|
function createLabEnvironment() {
|
|
// 实验室地板
|
|
const floorGeometry = new THREE.PlaneGeometry(20, 20);
|
|
const floorMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x2c3e50,
|
|
roughness: 0.8,
|
|
metalness: 0.2
|
|
});
|
|
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
|
|
floor.rotation.x = -Math.PI / 2;
|
|
floor.receiveShadow = true;
|
|
scene.add(floor);
|
|
|
|
// 实验室墙壁
|
|
const wallGeometry = new THREE.BoxGeometry(20, 8, 0.2);
|
|
const wallMaterial = new THREE.MeshStandardMaterial({ color: 0x34495e });
|
|
|
|
const backWall = new THREE.Mesh(wallGeometry, wallMaterial);
|
|
backWall.position.z = -10;
|
|
backWall.position.y = 4;
|
|
scene.add(backWall);
|
|
|
|
// 实验台
|
|
const tableGeometry = new THREE.BoxGeometry(6, 0.5, 3);
|
|
const tableMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x7d5e3c,
|
|
roughness: 0.7
|
|
});
|
|
|
|
const table = new THREE.Mesh(tableGeometry, tableMaterial);
|
|
table.position.y = 0.25;
|
|
table.position.z = -1;
|
|
table.castShadow = true;
|
|
table.receiveShadow = true;
|
|
scene.add(table);
|
|
|
|
// 添加台面细节
|
|
const tableTopGeometry = new THREE.BoxGeometry(6.2, 0.1, 3.2);
|
|
const tableTopMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x8d6e4b,
|
|
roughness: 0.5,
|
|
metalness: 0.3
|
|
});
|
|
|
|
const tableTop = new THREE.Mesh(tableTopGeometry, tableTopMaterial);
|
|
tableTop.position.y = 0.55;
|
|
tableTop.position.z = -1;
|
|
tableTop.receiveShadow = true;
|
|
scene.add(tableTop);
|
|
}
|
|
|
|
function createExperimentEquipment() {
|
|
// 锥形瓶
|
|
const flaskGeometry = new THREE.CylinderGeometry(0.7, 0.5, 1.5, 32);
|
|
const flaskMaterial = new THREE.MeshPhysicalMaterial({
|
|
color: 0xd0e0f0,
|
|
transparent: true,
|
|
opacity: 0.7,
|
|
roughness: 0.1,
|
|
clearcoat: 1,
|
|
transmission: 0.9
|
|
});
|
|
|
|
const flask = new THREE.Mesh(flaskGeometry, flaskMaterial);
|
|
flask.position.set(0, 1.2, -1);
|
|
flask.castShadow = true;
|
|
flask.receiveShadow = true;
|
|
scene.add(flask);
|
|
experimentObjects.flask = flask;
|
|
|
|
// 锌粒
|
|
const zincGeometry = new THREE.SphereGeometry(0.1, 16, 16);
|
|
const zincMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x808080,
|
|
metalness: 0.8,
|
|
roughness: 0.3
|
|
});
|
|
|
|
experimentObjects.zincPieces = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
const zinc = new THREE.Mesh(zincGeometry, zincMaterial);
|
|
zinc.position.set(-1 + i * 0.1, 1, -2);
|
|
zinc.castShadow = true;
|
|
scene.add(zinc);
|
|
experimentObjects.zincPieces.push(zinc);
|
|
}
|
|
|
|
// 稀硫酸试剂瓶
|
|
const acidBottleGeometry = new THREE.CylinderGeometry(0.4, 0.4, 1.2, 32);
|
|
const acidBottleMaterial = new THREE.MeshPhysicalMaterial({
|
|
color: 0xa0f0a0,
|
|
transparent: true,
|
|
opacity: 0.8,
|
|
roughness: 0.1,
|
|
clearcoat: 1
|
|
});
|
|
|
|
const acidBottle = new THREE.Mesh(acidBottleGeometry, acidBottleMaterial);
|
|
acidBottle.position.set(-2, 1, -1);
|
|
acidBottle.castShadow = true;
|
|
scene.add(acidBottle);
|
|
experimentObjects.acidBottle = acidBottle;
|
|
|
|
// 导管
|
|
const tubeGeometry = new THREE.CylinderGeometry(0.05, 0.05, 1, 16);
|
|
const tubeMaterial = new THREE.MeshStandardMaterial({ color: 0x808080 });
|
|
|
|
const tube = new THREE.Mesh(tubeGeometry, tubeMaterial);
|
|
tube.position.set(0.5, 1.8, -1);
|
|
tube.rotation.z = Math.PI / 4;
|
|
scene.add(tube);
|
|
experimentObjects.tube = tube;
|
|
|
|
// 集气瓶
|
|
const gasJarGeometry = new THREE.CylinderGeometry(0.6, 0.6, 1.5, 32);
|
|
const gasJarMaterial = new THREE.MeshPhysicalMaterial({
|
|
color: 0xd0e0f0,
|
|
transparent: true,
|
|
opacity: 0.7,
|
|
roughness: 0.1,
|
|
clearcoat: 1,
|
|
transmission: 0.9
|
|
});
|
|
|
|
const gasJar = new THREE.Mesh(gasJarGeometry, gasJarMaterial);
|
|
gasJar.position.set(2, 1.5, -1);
|
|
scene.add(gasJar);
|
|
experimentObjects.gasJar = gasJar;
|
|
}
|
|
|
|
function startReaction() {
|
|
if (reactionStarted) return;
|
|
reactionStarted = true;
|
|
|
|
document.querySelector('.status-text').textContent = "反应进行中...";
|
|
|
|
// 移除锌粒
|
|
experimentObjects.zincPieces.forEach(zinc => {
|
|
scene.remove(zinc);
|
|
});
|
|
|
|
// 创建气泡
|
|
createBubbles();
|
|
|
|
// 更新进度条
|
|
const progressBar = document.getElementById('reaction-progress');
|
|
let progress = 0;
|
|
|
|
const interval = setInterval(() => {
|
|
progress += 1;
|
|
progressBar.style.width = progress + '%';
|
|
|
|
if (progress >= 100) {
|
|
clearInterval(interval);
|
|
document.querySelector('.status-text').textContent = "反应完成!";
|
|
document.getElementById('collect-gas').disabled = false;
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
function createBubbles() {
|
|
for (let i = 0; i < 50; i++) {
|
|
setTimeout(() => {
|
|
const bubble = {
|
|
mesh: null,
|
|
startTime: Date.now(),
|
|
position: new THREE.Vector3(
|
|
experimentObjects.flask.position.x + (Math.random() - 0.5) * 0.3,
|
|
experimentObjects.flask.position.y - 0.5,
|
|
experimentObjects.flask.position.z + (Math.random() - 0.5) * 0.3
|
|
),
|
|
velocity: new THREE.Vector3(
|
|
(Math.random() - 0.5) * 0.01,
|
|
Math.random() * 0.02 + 0.01,
|
|
(Math.random() - 0.5) * 0.01
|
|
)
|
|
};
|
|
|
|
const bubbleGeometry = new THREE.SphereGeometry(0.03 + Math.random() * 0.02, 8, 8);
|
|
const bubbleMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xffffff,
|
|
transparent: true,
|
|
opacity: 0.7
|
|
});
|
|
|
|
bubble.mesh = new THREE.Mesh(bubbleGeometry, bubbleMaterial);
|
|
bubble.mesh.position.copy(bubble.position);
|
|
scene.add(bubble.mesh);
|
|
|
|
bubbles.push(bubble);
|
|
}, i * 100);
|
|
}
|
|
}
|
|
|
|
function updateBubbles() {
|
|
const now = Date.now();
|
|
|
|
for (let i = bubbles.length - 1; i >= 0; i--) {
|
|
const bubble = bubbles[i];
|
|
const age = (now - bubble.startTime) / 1000;
|
|
|
|
if (age > 5) {
|
|
scene.remove(bubble.mesh);
|
|
bubbles.splice(i, 1);
|
|
continue;
|
|
}
|
|
|
|
bubble.position.add(bubble.velocity);
|
|
bubble.mesh.position.copy(bubble.position);
|
|
|
|
// 气泡上升时略微变大
|
|
bubble.mesh.scale.set(1 + age/10, 1 + age/10, 1 + age/10);
|
|
}
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
controls.update();
|
|
updateBubbles();
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
function onWindowResize() {
|
|
const container = document.getElementById('canvas-container');
|
|
camera.aspect = container.clientWidth / container.clientHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
document.getElementById('add-zinc').addEventListener('click', () => {
|
|
document.querySelector('.status-text').textContent = "已添加锌粒";
|
|
});
|
|
|
|
document.getElementById('add-acid').addEventListener('click', () => {
|
|
document.querySelector('.status-text').textContent = "已添加稀硫酸";
|
|
});
|
|
|
|
document.getElementById('start-reaction').addEventListener('click', startReaction);
|
|
|
|
document.getElementById('collect-gas').addEventListener('click', () => {
|
|
document.querySelector('.status-text').textContent = "正在收集氢气...";
|
|
|
|
// 模拟气体收集效果
|
|
setTimeout(() => {
|
|
document.querySelector('.status-text').textContent = "氢气收集完成!";
|
|
}, 2000);
|
|
});
|
|
|
|
document.getElementById('reset-experiment').addEventListener('click', () => {
|
|
// 移除所有气泡
|
|
bubbles.forEach(bubble => {
|
|
scene.remove(bubble.mesh);
|
|
});
|
|
bubbles = [];
|
|
|
|
// 重置进度条
|
|
document.getElementById('reaction-progress').style.width = '0%';
|
|
document.querySelector('.status-text').textContent = "实验已重置";
|
|
|
|
// 重新创建锌粒
|
|
experimentObjects.zincPieces.forEach(zinc => {
|
|
scene.add(zinc);
|
|
});
|
|
|
|
reactionStarted = false;
|
|
});
|
|
}
|
|
|
|
// 添加视觉气泡效果
|
|
function createVisualBubbles() {
|
|
const container = document.getElementById('experiment-container');
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
const bubble = document.createElement('div');
|
|
bubble.classList.add('bubble');
|
|
|
|
// 随机大小
|
|
const size = Math.random() * 30 + 10;
|
|
bubble.style.width = `${size}px`;
|
|
bubble.style.height = `${size}px`;
|
|
|
|
// 随机位置
|
|
bubble.style.left = `${Math.random() * 100}%`;
|
|
bubble.style.bottom = `-${size}px`;
|
|
|
|
// 随机动画延迟
|
|
bubble.style.animationDelay = `${Math.random() * 5}s`;
|
|
|
|
container.appendChild(bubble);
|
|
}
|
|
}
|
|
|
|
// 初始化视觉气泡
|
|
createVisualBubbles();
|
|
</script>
|
|
</body>
|
|
</html> |