项目概述
随着教育技术的不断发展,交互式模拟工具已成为生物学教学中的重要手段。今天我分享一个创新的"猪笼草生长环境模拟器",它将气象数据可视化与植物生长状态模拟相结合,为学生和植物爱好者提供了一个直观的学习平台。
项目特色
核心功能
-
动态数据交互:用户可直接在图表上拖动修改气温和降水数据
-
实时植物响应:猪笼草根据环境条件实时调整生长状态
-
多感官体验:结合视觉、听觉反馈,增强学习沉浸感
-
数据量化分析:自动计算环境评分和适宜度指标
-
跨平台适配:响应式设计,支持桌面和移动设备
教育价值
-
直观展示环境因素对植物生长的影响
-
理解热带植物(猪笼草)的特殊生态需求
-
培养数据分析和环境评估能力
技术实现详解
1. 数据可视化系统
使用Chart.js库创建双Y轴图表,同时展示气温和降水数据:
javascript
// 创建混合图表(柱状图+折线图)
const weatherChart = new Chart(ctx, {
type: 'bar',
data: {
datasets: [
{
label: '降水量 (mm)',
data: currentData.rainfall,
backgroundColor: 'rgba(54, 162, 235, 0.7)',
type: 'bar', // 降水用柱状图
yAxisID: 'y'
},
{
label: '气温 (°C)',
data: currentData.temperatures,
type: 'line', // 气温用折线图
borderColor: 'rgba(255, 99, 132, 1)',
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
interaction: {
mode: 'index', // 支持多数据系列交互
intersect: false,
}
}
});
2. 交互式图表控制
实现用户直接在图表上拖动修改数据的功能:
javascript
function setupChartInteractivity() {
const chart = weatherChart;
let activePoint = null;
ctx.canvas.onmousedown = function(e) {
const points = chart.getElementsAtEventForMode(e, 'nearest', { intersect: true }, true);
if (points.length) {
activePoint = points[0];
handleDrag(e);
}
};
function handleDrag(e) {
const chartArea = chart.chartArea;
if (activePoint.datasetIndex === 0) { // 降水数据
const yAxis = chart.scales.y;
const mouseY = e.offsetY;
const yValue = yAxis.getValueForPixel(mouseY);
currentData.rainfall[activePoint.index] = Math.max(0, Math.round(yValue));
} else { // 气温数据
const yAxis = chart.scales.y1;
const mouseY = e.offsetY;
const yValue = yAxis.getValueForPixel(mouseY);
currentData.temperatures[activePoint.index] = parseFloat(yValue.toFixed(1));
}
chart.update();
updatePlantStatus(); // 更新植物状态
}
}
3. 猪笼草生长状态模拟
使用CSS几何图形动态构建猪笼草模型:
javascript
function updatePlantStatus() {
// 计算环境适宜度
let goodMonths = 0;
for (let i = 0; i < 12; i++) {
const temp = currentData.temperatures[i];
const rain = currentData.rainfall[i];
if (temp >= 20 && temp <= 30 && rain >= 100) goodMonths++;
}
// 计算环境评分
const avgTemp = (currentData.temperatures.reduce((a, b) => a + b, 0) / 12).toFixed(1);
const totalRain = currentData.rainfall.reduce((a, b) => a + b, 0);
let envScore = Math.min(100, Math.round(avgTemp * 2 + totalRain / 15));
// 更新植物视觉状态
const statusText = document.getElementById('plantStatus');
const stem = document.getElementById('stem');
if (goodMonths >= 10 && envScore >= 80) {
// 健康状态:茎高叶茂,多个捕虫笼
stem.style.height = "320px";
statusText.textContent = "猪笼草状态:健康生长 🌿";
statusText.style.color = "#7eff7e";
// 显示所有捕虫笼
for (let i = 0; i < 5; i++) {
const pitcher = document.getElementById(`pitcher-${i}`);
pitcher.style.height = `${120 + Math.random() * 80}px`;
pitcher.style.opacity = "1";
}
} else if (goodMonths >= 6) {
// 中等状态:茎较短,部分捕虫笼
stem.style.height = "240px";
statusText.textContent = "猪笼草状态:生长缓慢 ⚠️";
statusText.style.color = "#ffcc66";
// 显示部分捕虫笼
for (let i = 0; i < 5; i++) {
const pitcher = document.getElementById(`pitcher-${i}`);
if (i < 3) {
pitcher.style.height = `${80 + Math.random() * 60}px`;
pitcher.style.opacity = "0.7";
} else {
pitcher.style.opacity = "0";
}
}
} else {
// 不良状态:茎矮,无捕虫笼
stem.style.height = "160px";
statusText.textContent = "猪笼草状态:枯萎状态 ❗";
statusText.style.color = "#ff6666";
// 隐藏所有捕虫笼
for (let i = 0; i < 5; i++) {
const pitcher = document.getElementById(`pitcher-${i}`);
pitcher.style.opacity = "0";
}
}
}
4. 音频反馈系统
根据植物状态播放相应背景音乐:
javascript
// 音频元素初始化
const healthyMusic = new Audio("./mp3/1.爱的纪念.mp3");
const mediumMusic = new Audio("./mp3/2.卡农.mp3");
const poorMusic = new Audio("./mp3/3.悲伤.mp3");
// 智能音乐切换
let previousState = null;
let currentMusic = null;
function updatePlantStatus() {
// ... 计算当前状态 ...
let currentState = determineState(); // 返回 'healthy'、'medium' 或 'poor'
// 只在状态改变时切换音乐
if (currentState !== previousState) {
if (currentMusic) {
currentMusic.pause();
currentMusic.currentTime = 0;
}
if (currentState === 'healthy') {
currentMusic = healthyMusic;
} else if (currentState === 'medium') {
currentMusic = mediumMusic;
} else {
currentMusic = poorMusic;
}
if (currentMusic) {
currentMusic.play().catch(e => console.log('音频播放失败:', e));
}
previousState = currentState;
}
}
5. CSS几何图形构建
使用纯CSS创建猪笼草几何图形:
css
/* 猪笼草茎 */
.stem {
width: 12px;
height: 320px; /* 根据状态动态调整 */
background: linear-gradient(to top, #2c5530, #4a7c59, #2c5530);
border-radius: 6px;
transition: height 1.5s ease;
}
/* 叶子 */
.leaf {
width: 30px;
height: 180px; /* 动态调整 */
background: linear-gradient(to top, #4a7c59, #2c5530);
border-radius: 15px;
transition: all 1.5s ease;
}
/* 捕虫笼 */
.pitcher {
width: 50px;
height: 160px; /* 动态调整 */
background: linear-gradient(to bottom, #d4af37, #8b4513, #228b22);
border-radius: 0 0 25px 25px;
transition: all 2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
opacity: 1; /* 动态调整 */
}
/* 捕虫笼盖子 */
.pitcher-lid {
height: 25px;
background: linear-gradient(to bottom, #a0522d, #8b4513);
border-radius: 50% 50% 0 0;
}
6. 响应式设计
确保在多种设备上都有良好体验:
css
/* 桌面端布局 */
.container {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 3fr 2fr; /* 图表3份,植物2份 */
gap: 30px;
}
/* 平板适配 */
@media (max-width: 900px) {
.container {
grid-template-columns: 1fr; /* 单列布局 */
}
.plant-img-container {
width: 200px;
height: 300px;
}
}
/* 手机适配 */
@media (max-width: 480px) {
.stats {
grid-template-columns: repeat(2, 1fr); /* 2列统计 */
}
h1 {
font-size: 1.8rem;
}
}
教育应用场景
课堂教学应用
-
生物学教学:展示植物与环境的关系
-
地理学整合:联系气候与植被分布
-
数据科学启蒙:学习数据可视化与统计分析
-
环境教育:理解生态保护的重要性
自主学习工具
-
学生可自主探索不同气候条件对植物的影响
-
通过实验理解植物生长的限制因素
-
培养科学探究和假设验证能力
项目扩展方向
1. 功能增强
-
添加更多环境变量(湿度、光照强度、土壤pH值)
-
引入不同猪笼草品种的对比
-
增加时间序列模拟(多年生长周期)
2. 技术优化
-
使用WebGL进行更精细的3D植物渲染
-
集成机器学习模型预测生长趋势
-
添加数据导入/导出功能
3. 教育功能
-
添加测验和评估模块
-
创建教学案例库
-
支持小组协作功能
4. 多语言支持
javascript
// 多语言配置示例
const translations = {
'zh-CN': {
title: '猪笼草生长环境模拟器',
statusHealthy: '猪笼草状态:健康生长',
// ... 其他翻译
},
'en-US': {
title: 'Nepenthes Growth Environment Simulator',
statusHealthy: 'Nepenthes Status: Healthy Growth',
// ... other translations
}
};
部署与使用
快速部署
-
下载完整代码文件
-
准备音频资源(或使用在线资源)
-
上传到任意Web服务器
-
通过浏览器访问即可使用
-
也可以基于python+pyinstaller+webview打包成桌面应用(exe),具体打包方法请自行琢磨或者联系小编获取(.py、.spec文件)
自定义配置
修改initialData对象中的初始值:
javascript
const initialData = {
labels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
temperatures: [12.5, 14.2, 18.7, 23.5, 26.8, 28.3, 29.5, 29.1, 26.7, 22.4, 17.9, 13.8],
rainfall: [85, 90, 110, 135, 180, 220, 240, 210, 160, 120, 95, 80]
};
教学建议
课堂活动设计
-
气候探索任务:让学生调整数据,找到猪笼草最佳生长条件
-
对比实验:比较不同城市的气候数据对猪笼草的影响
-
预测挑战:根据部分数据预测植物状态,然后验证
-
创意设计:设计适合猪笼草生长的理想气候模式
跨学科连接
-
数学:计算平均值、百分比和趋势线
-
艺术:设计植物视觉表现
-
计算机科学:理解交互式网页开发
-
环境科学:探讨气候变化的影响
完整代码如下:
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="./chart.js"></script>
<style>
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
color: #fff;
padding: 20px;
margin: 0;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 3fr 2fr;
gap: 30px;
}
header {
grid-column: 1 / -1;
text-align: center;
padding: 20px;
background: rgba(0, 0, 0, 0.3);
border-radius: 15px;
margin-bottom: 20px;
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
h1 {
font-size: 2.5rem;
margin: 0;
text-shadow: 0 0 10px rgba(0, 255, 100, 0.7);
color: #7eff7e;
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
margin-top: 10px;
}
.chart-container {
background: rgba(10, 20, 30, 0.85);
padding: 25px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(100, 200, 100, 0.3);
position: relative;
}
.plant-container {
background: rgba(10, 30, 20, 0.85);
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
border: 1px solid rgba(100, 200, 100, 0.3);
position: relative;
overflow: hidden;
}
.plant-health {
font-size: 1.4rem;
margin-bottom: 20px;
text-align: center;
font-weight: bold;
z-index: 10;
}
.plant-img-container {
position: relative;
width: 280px;
height: 420px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: flex-end;
}
/* 猪笼草几何图形样式 */
.plant-geometric {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: flex-end;
}
.stem {
position: absolute;
bottom: 0;
width: 12px;
height: 0;
background: linear-gradient(to top, #2c5530, #4a7c59, #2c5530);
border-radius: 6px;
transition: height 1.5s ease;
z-index: 1;
}
.leaves {
position: absolute;
bottom: 0;
width: 100%;
height: 60%;
display: flex;
justify-content: center;
align-items: flex-end;
}
.leaf {
position: absolute;
width: 30px;
height: 0;
background: linear-gradient(to top, #4a7c59, #2c5530);
border-radius: 15px;
transition: all 1.5s ease;
transform-origin: bottom center;
}
.pitchers {
position: absolute;
bottom: 0;
width: 100%;
height: 60%;
display: flex;
justify-content: space-around;
align-items: flex-end;
}
.pitcher {
width: 0;
height: 0;
background: linear-gradient(to bottom, #d4af37, #8b4513, #228b22);
border-radius: 0 0 25px 25px;
transition: all 2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transform-origin: bottom;
position: relative;
opacity: 0;
}
.pitcher-lid {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 0;
background: linear-gradient(to bottom, #a0522d, #8b4513);
border-radius: 50% 50% 0 0;
transition: height 0.5s ease;
}
.music-controls {
position: absolute;
bottom: 15px;
right: 15px;
background: rgba(0, 0, 0, 0.5);
border-radius: 50px;
padding: 8px;
display: flex;
align-items: center;
gap: 10px;
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.music-btn {
background: transparent;
border: none;
color: #7eff7e;
font-size: 1.5rem;
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.music-btn:hover {
background: rgba(126, 255, 126, 0.2);
}
.volume-slider {
width: 80px;
-webkit-appearance: none;
height: 5px;
border-radius: 5px;
background: rgba(255, 255, 255, 0.2);
outline: none;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 15px;
height: 15px;
border-radius: 50%;
background: #7eff7e;
cursor: pointer;
}
.stats {
grid-column: 1 / -1;
background: rgba(0, 0, 0, 0.3);
padding: 20px;
border-radius: 15px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-box {
background: rgba(30, 60, 50, 0.7);
padding: 15px;
border-radius: 10px;
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #7eff7e;
margin: 10px 0;
}
.instructions {
grid-column: 1 / -1;
background: rgba(0, 20, 30, 0.7);
padding: 20px;
border-radius: 15px;
margin-top: 20px;
font-size: 1.1rem;
line-height: 1.6;
border: 1px solid rgba(100, 200, 255, 0.3);
}
.instructions h2 {
color: #7eff7e;
margin-top: 0;
}
.instructions ul {
padding-left: 20px;
}
.instructions li {
margin-bottom: 10px;
}
.data-table {
grid-column: 1 / -1;
background: rgba(0, 10, 20, 0.7);
padding: 20px;
border-radius: 15px;
margin-top: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px 15px;
text-align: center;
border-bottom: 1px solid rgba(100, 200, 100, 0.3);
}
th {
background: rgba(20, 60, 40, 0.5);
color: #7eff7e;
}
tr:hover {
background: rgba(30, 70, 50, 0.4);
}
.reset-btn {
grid-column: 1 / -1;
background: linear-gradient(to right, #ff416c, #ff4b2b);
color: white;
border: none;
padding: 15px 30px;
font-size: 1.2rem;
border-radius: 50px;
cursor: pointer;
margin: 30px auto;
display: block;
width: 250px;
font-weight: bold;
letter-spacing: 1px;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(255, 65, 108, 0.4);
}
.reset-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(255, 65, 108, 0.6);
}
@media (max-width: 900px) {
.container {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<!-- 在body标签开始后添加 -->
<div id="interactionOverlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); display: flex; justify-content: center; align-items: center; z-index: 9999;">
<div style="background: rgba(30, 60, 50, 0.9); padding: 30px; border-radius: 15px; text-align: center; border: 2px solid #7eff7e;">
<h2 style="color: #7eff7e; margin-bottom: 20px;">欢迎使用猪笼草生长环境模拟器</h2>
<p style="margin-bottom: 25px; font-size: 1.1rem;">点击下方按钮开始体验并启用背景音乐</p>
<button id="startBtn" style="background: linear-gradient(to right, #7eff7e, #4CAF50); color: #1a2a6c; border: none; padding: 12px 30px; font-size: 1.1rem; border-radius: 25px; cursor: pointer; font-weight: bold;">
开始体验
</button>
</div>
</div>
<div class="container">
<header>
<h1>A城市猪笼草生长环境模拟器</h1>
<div class="subtitle">调整每月气温和降水数据,观察猪笼草生长状态变化</div>
</header>
<div class="chart-container">
<canvas id="weatherChart"></canvas>
</div>
<div class="plant-container">
<div class="plant-health" id="plantStatus">猪笼草状态:健康生长</div>
<div class="plant-img-container">
<div class="plant-geometric">
<div class="stem" id="stem"></div>
<div class="leaves" id="leavesContainer"></div>
<div class="pitchers" id="pitchersContainer"></div>
</div>
</div>
<div class="music-controls">
<button class="music-btn" id="playBtn">▶</button>
<button class="music-btn" id="pauseBtn">❚❚</button>
<input type="range" min="0" max="1" step="0.1" value="0.5" class="volume-slider" id="volumeSlider">
</div>
</div>
<div class="stats">
<div class="stat-box">
<div>平均气温</div>
<div class="stat-value" id="avgTemp">23.5°C</div>
<div>猪笼草适宜: 20-30°C</div>
</div>
<div class="stat-box">
<div>总降水量</div>
<div class="stat-value" id="totalRain">1460mm</div>
<div>猪笼草适宜: >1200mm</div>
</div>
<div class="stat-box">
<div>适宜月份</div>
<div class="stat-value" id="goodMonths">10个月</div>
<div>健康生长需要≥10个月适宜</div>
</div>
<div class="stat-box">
<div>环境评分</div>
<div class="stat-value" id="envScore">92/100</div>
<div>≥80分表示环境良好</div>
</div>
</div>
<div class="instructions">
<h2>猪笼草生长环境要求</h2>
<ul>
<li><strong>气温适宜范围</strong>:20-30°C(最适生长温度25°C左右)</li>
<li><strong>降水要求</strong>:月降水量≥100mm(年降水量≥1200mm)</li>
<li><strong>高湿度环境</strong>:相对湿度需维持在70%以上</li>
<li><strong>光照需求</strong>:明亮散射光,避免强烈直射阳光</li>
<li><strong>土壤条件</strong>:贫瘠酸性土壤(pH值3-5),排水良好</li>
</ul>
<p>当环境条件适宜时,猪笼草会生长出捕虫笼;长期不适宜条件会导致植株枯萎。</p>
</div>
<div class="data-table">
<table>
<thead>
<tr>
<th>月份</th>
<th>气温 (°C)</th>
<th>降水 (mm)</th>
<th>环境状态</th>
<th>猪笼草生长</th>
</tr>
</thead>
<tbody id="dataTableBody"></tbody>
</table>
</div>
<button class="reset-btn" id="resetBtn">重置数据</button>
</div>
<script>
// 在脚本开头添加这些变量
let userInteracted = false;
let audioContextInitialized = false;
let previousState = null; // 跟踪上一次的生长状态
// 在脚本中添加初始化函数
function initializeAudio() {
if (!userInteracted) return;
// 设置音乐音量
const volume = document.getElementById('volumeSlider').value;
healthyMusic.volume = volume;
mediumMusic.volume = volume;
poorMusic.volume = volume;
audioContextInitialized = true;
updatePlantStatus(); // 重新更新状态以播放音乐
}
// 添加开始按钮事件监听
document.getElementById('startBtn').addEventListener('click', function() {
userInteracted = true;
document.getElementById('interactionOverlay').style.display = 'none';
initializeAudio();
// 初始化后立即更新植物状态,这会设置初始的previousState
updatePlantStatus();
});
// 初始气象数据
const initialData = {
labels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
temperatures: [12.5, 14.2, 18.7, 23.5, 26.8, 28.3, 29.5, 29.1, 26.7, 22.4, 17.9, 13.8],
rainfall: [85, 90, 110, 135, 180, 220, 240, 210, 160, 120, 95, 80]
};
// 当前气象数据
let currentData = JSON.parse(JSON.stringify(initialData));
// 背景音乐 - 使用公共领域的音乐
// 修改这部分代码,使用相对路径
const healthyMusic = new Audio("./mp3/1.爱的纪念.mp3");
const mediumMusic = new Audio("./mp3/2.卡农.mp3");
const poorMusic = new Audio("./mp3/3.悲伤.mp3");
/*
const healthyMusic = new Audio("https://m10.music.126.net/20251109103630/84e2c096c6aa2c0649b5ae5c1572dcfb/ymusic/1c87/5dcb/e7cc/f9f18902a84dbdb0a374b2e910c981a8.mp3?vuutv=0PPpHP0Vyc2v4vDkPhsZHGpYSctzNbHEfMo+lA4J8BiGTP4ga3gPQxHO48tZ3A17LAh8NwgoXm8d5vjIy5fV8/drHS/ijIjDo/2LA/d+so8=&cdntag=bWFyaz1vc193ZWIscXVhbGl0eV9leGhpZ2g");
const mediumMusic = new Audio("https://m701.music.126.net/20251109103259/652bb105bb2ffc1ea79b5c221b685e08/jdymusic/obj/wo3DlMOGwrbDjj7DisKw/17608050162/e386/96b6/927e/1d486ced39809dfe0eb5bb3f50f06029.mp3?vuutv=HLL+svVOfMj72uzm3eeNfm62/ZY4suyZVb27CVOKu3Cbt0w9oUqrgLfBBscdzHNwuULnB1h641xXKTlgOILV5ajysk6Wm38yDg+wre4KXu4=&cdntag=bWFyaz1vc193ZWIscXVhbGl0eV9leGhpZ2g");
const poorMusic = new Audio("https://m801.music.126.net/20251109102811/d65fedd79c08106a4e4a9049981bbfed/jdymusic/obj/wo3DlMOGwrbDjj7DisKw/14520382396/6ab6/68f5/03da/d70bb0bc8dc011a2de8bd1ba9d3d2f58.mp3?vuutv=plJbf1rSCMJtcMPElLjWnbSU33tRJ44dStUzGJRb+Ffluz1Mdeare8HhC3omwO2Kv2CmOJYtvOw27+Qe3F443zgQ6jaUKj9dMVO5Zn7/ARI=&cdntag=bWFyaz1vc193ZWIscXVhbGl0eV9leGhpZ2g");
*/
// 设置音乐循环
healthyMusic.loop = true;
mediumMusic.loop = true;
poorMusic.loop = true;
// 当前播放的音乐
let currentMusic = null;
// 创建图表
const ctx = document.getElementById('weatherChart').getContext('2d');
const weatherChart = new Chart(ctx, {
type: 'bar',
data: {
labels: currentData.labels,
datasets: [
{
label: '降水量 (mm)',
data: currentData.rainfall,
backgroundColor: 'rgba(54, 162, 235, 0.7)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1,
borderRadius: 5,
order: 2,
yAxisID: 'y'
},
{
label: '气温 (°C)',
data: currentData.temperatures,
type: 'line',
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderWidth: 3,
pointRadius: 6,
pointBackgroundColor: 'rgba(255, 99, 132, 1)',
pointBorderColor: '#fff',
pointHoverRadius: 8,
tension: 0.3,
fill: false,
order: 1,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: true,
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
title: {
display: true,
text: 'A城市2025年月平均气温与降水量',
font: {
size: 18
},
color: '#fff'
},
tooltip: {
mode: 'index',
intersect: false,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#7eff7e',
bodyColor: '#fff',
padding: 12,
displayColors: true
},
legend: {
labels: {
color: '#fff',
font: {
size: 14
}
}
}
},
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: '降水量 (mm)',
color: '#fff'
},
min: 0,
max: 300,
ticks: {
color: '#ccc'
},
grid: {
color: 'rgba(255, 255, 255, 0.1)'
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: '气温 (°C)',
color: '#fff'
},
min: 0,
max: 40,
ticks: {
color: '#ccc'
},
grid: {
drawOnChartArea: false,
},
},
x: {
ticks: {
color: '#ccc'
},
grid: {
color: 'rgba(255, 255, 255, 0.1)'
}
}
}
}
});
// 创建猪笼草DOM元素
function createPlantElements() {
const leavesContainer = document.getElementById('leavesContainer');
leavesContainer.innerHTML = '';
// 创建叶子
for (let i = 0; i < 5; i++) {
const leaf = document.createElement('div');
leaf.className = 'leaf';
leaf.id = `leaf-${i}`;
leavesContainer.appendChild(leaf);
}
const pitchersContainer = document.getElementById('pitchersContainer');
pitchersContainer.innerHTML = '';
// 创建捕虫笼
for (let i = 0; i < 5; i++) {
const pitcher = document.createElement('div');
pitcher.className = 'pitcher';
pitcher.id = `pitcher-${i}`;
const lid = document.createElement('div');
lid.className = 'pitcher-lid';
pitcher.appendChild(lid);
pitchersContainer.appendChild(pitcher);
}
updatePlantStatus();
}
// 更新猪笼草状态
function updatePlantStatus() {
let goodMonths = 0;
const monthlyStatus = [];
// 计算适宜月份
for (let i = 0; i < 12; i++) {
const temp = currentData.temperatures[i];
const rain = currentData.rainfall[i];
const isTempGood = temp >= 20 && temp <= 30;
const isRainGood = rain >= 100;
const isGood = isTempGood && isRainGood;
if (isGood) goodMonths++;
monthlyStatus.push({
temp,
rain,
isGood
});
}
// 计算统计数据
const avgTemp = (currentData.temperatures.reduce((a, b) => a + b, 0) / 12).toFixed(1);
const totalRain = currentData.rainfall.reduce((a, b) => a + b, 0);
// 环境评分 (0-100)
let envScore = 0;
envScore += Math.min(100, Math.max(0, avgTemp - 10) * 10); // 温度因素
envScore += Math.min(100, totalRain / 15); // 降水因素
envScore += goodMonths * 5; // 适宜月份因素
envScore = Math.min(100, Math.round(envScore));
// 更新统计显示
document.getElementById('avgTemp').textContent = `${avgTemp}°C`;
document.getElementById('totalRain').textContent = `${totalRain}mm`;
document.getElementById('goodMonths').textContent = `${goodMonths}个月`;
document.getElementById('envScore').textContent = `${envScore}/100`;
// 更新表格数据
updateTable(monthlyStatus);
// 更新猪笼草状态
const statusText = document.getElementById('plantStatus');
const stem = document.getElementById('stem');
// 确定当前状态
let currentState;
if (goodMonths >= 10 && envScore >= 80) {
currentState = 'healthy';
} else if (goodMonths >= 6 && envScore >= 60) {
currentState = 'medium';
} else {
currentState = 'poor';
}
// 只在用户已交互且音频已初始化时处理音乐
if (userInteracted && audioContextInitialized) {
// 只有在状态改变时才切换音乐
if (currentState !== previousState) {
// 停止当前音乐
if (currentMusic) {
currentMusic.pause();
currentMusic.currentTime = 0;
}
// 根据条件选择新音乐
if (currentState === 'healthy') {
currentMusic = healthyMusic;
} else if (currentState === 'medium') {
currentMusic = mediumMusic;
} else {
currentMusic = poorMusic;
}
// 播放新音乐
if (currentMusic) {
currentMusic.play().catch(e => {
console.log('音频播放失败:', e);
});
}
// 更新状态记录
previousState = currentState;
}
// 如果状态没变,音乐继续播放,不做任何操作
}
// 更新视觉状态(无论状态是否改变都需要更新)
if (currentState === 'healthy') {
statusText.textContent = "猪笼草状态:健康生长 🌿";
statusText.style.color = "#7eff7e";
// 更新茎叶
stem.style.height = "320px";
// 更新叶子
for (let i = 0; i < 5; i++) {
const leaf = document.getElementById(`leaf-${i}`);
const angle = (i / 5) * 360;
const length = 150 + Math.random() * 50;
leaf.style.height = `${length}px`;
leaf.style.transform = `rotate(${angle}deg) translateX(${40 + i*10}px)`;
leaf.style.opacity = "1";
leaf.style.backgroundColor = "linear-gradient(to top, #4a7c59, #2c5530)";
}
// 显示所有捕虫笼
for (let i = 0; i < 5; i++) {
const pitcher = document.getElementById(`pitcher-${i}`);
const lid = pitcher.querySelector('.pitcher-lid');
const height = 120 + Math.random() * 80;
const width = 40 + Math.random() * 20;
pitcher.style.height = `${height}px`;
pitcher.style.width = `${width}px`;
pitcher.style.opacity = "1";
pitcher.style.transform = `translateY(${-height/2}px)`;
pitcher.style.background = "linear-gradient(to bottom, #d4af37, #8b4513, #228b22)";
// 添加盖子
lid.style.height = `${width/2}px`;
lid.style.background = "linear-gradient(to bottom, #a0522d, #8b4513)";
// 添加随机延迟使生长更自然
pitcher.style.transitionDelay = `${i * 0.2}s`;
}
}
else if (currentState === 'medium') {
statusText.textContent = "猪笼草状态:生长缓慢 ⚠️";
statusText.style.color = "#ffcc66";
// 更新茎叶
stem.style.height = "240px";
// 更新叶子
for (let i = 0; i < 5; i++) {
const leaf = document.getElementById(`leaf-${i}`);
const angle = (i / 5) * 360;
const length = 100 + Math.random() * 40;
leaf.style.height = `${length}px`;
leaf.style.transform = `rotate(${angle}deg) translateX(${30 + i*8}px)`;
leaf.style.opacity = "0.8";
leaf.style.backgroundColor = "linear-gradient(to top, #8a9a5b, #556b2f)";
}
// 显示部分捕虫笼
for (let i = 0; i < 5; i++) {
const pitcher = document.getElementById(`pitcher-${i}`);
const lid = pitcher.querySelector('.pitcher-lid');
if (i < 3) {
const height = 80 + Math.random() * 60;
const width = 30 + Math.random() * 15;
pitcher.style.height = `${height}px`;
pitcher.style.width = `${width}px`;
pitcher.style.opacity = "0.7";
pitcher.style.transform = `translateY(${-height/2}px)`;
pitcher.style.background = "linear-gradient(to bottom, #b8860b, #8b4513, #3cb371)";
// 添加盖子
lid.style.height = `${width/2}px`;
lid.style.background = "linear-gradient(to bottom, #8b4513, #654321)";
// 添加随机延迟使生长更自然
pitcher.style.transitionDelay = `${i * 0.2}s`;
} else {
pitcher.style.height = "0";
pitcher.style.width = "0";
pitcher.style.opacity = "0";
pitcher.style.transform = "translateY(0)";
}
}
}
else {
statusText.textContent = "猪笼草状态:枯萎状态 ❗";
statusText.style.color = "#ff6666";
// 更新茎叶
stem.style.height = "160px";
// 更新叶子
for (let i = 0; i < 5; i++) {
const leaf = document.getElementById(`leaf-${i}`);
const angle = (i / 5) * 360;
const length = 60 + Math.random() * 30;
leaf.style.height = `${length}px`;
leaf.style.transform = `rotate(${angle}deg) translateX(${20 + i*5}px)`;
leaf.style.opacity = "0.5";
leaf.style.backgroundColor = "linear-gradient(to top, #a9a9a9, #696969)";
}
// 隐藏所有捕虫笼
for (let i = 0; i < 5; i++) {
const pitcher = document.getElementById(`pitcher-${i}`);
pitcher.style.height = "0";
pitcher.style.width = "0";
pitcher.style.opacity = "0";
pitcher.style.transform = "translateY(0)";
}
}
}
// 更新表格数据
function updateTable(monthlyStatus) {
const tableBody = document.getElementById('dataTableBody');
tableBody.innerHTML = '';
for (let i = 0; i < 12; i++) {
const status = monthlyStatus[i];
const row = document.createElement('tr');
row.innerHTML = `
<td>${currentData.labels[i]}</td>
<td>${status.temp.toFixed(1)}°C</td>
<td>${status.rain}mm</td>
<td style="color: ${status.isGood ? '#7eff7e' : '#ff6666'}">
${status.isGood ? '适宜' : '不适宜'}
</td>
<td>
<div style="width: 20px; height: 20px; border-radius: 50%;
background: ${status.isGood ? '#7eff7e' : '#ff6666'};
margin: 0 auto;"></div>
</td>
`;
tableBody.appendChild(row);
}
}
// 添加图表交互功能
function setupChartInteractivity() {
const chart = weatherChart;
let activePoint = null;
// 鼠标按下事件
ctx.canvas.onmousedown = function(e) {
const points = chart.getElementsAtEventForMode(e, 'nearest', { intersect: true }, true);
if (points.length) {
activePoint = points[0];
handleDrag(e);
}
};
// 鼠标移动事件
ctx.canvas.onmousemove = function(e) {
if (activePoint) {
handleDrag(e);
}
};
// 鼠标释放事件
ctx.canvas.onmouseup = function() {
activePoint = null;
};
ctx.canvas.onmouseleave = function() {
activePoint = null;
};
// 处理拖动
function handleDrag(e) {
const chartArea = chart.chartArea;
let yValue;
// 根据拖动的是气温还是降水调整数据
if (activePoint.datasetIndex === 0) { // 降水
const yAxis = chart.scales.y;
const mouseY = e.offsetY;
yValue = yAxis.getValueForPixel(mouseY);
// 确保降水值不为负
currentData.rainfall[activePoint.index] = Math.max(0, Math.round(yValue));
} else { // 气温
const yAxis = chart.scales.y1;
const mouseY = e.offsetY;
yValue = yAxis.getValueForPixel(mouseY);
currentData.temperatures[activePoint.index] = parseFloat(yValue.toFixed(1));
}
// 更新图表
chart.data.datasets[0].data = currentData.rainfall;
chart.data.datasets[1].data = currentData.temperatures;
chart.update();
// 更新猪笼草状态
updatePlantStatus();
}
}
// 重置按钮事件
document.getElementById('resetBtn').addEventListener('click', function() {
currentData = JSON.parse(JSON.stringify(initialData));
// 更新图表
weatherChart.data.datasets[0].data = currentData.rainfall;
weatherChart.data.datasets[1].data = currentData.temperatures;
weatherChart.update();
// 更新猪笼草状态
updatePlantStatus();
});
// 音乐控制
document.getElementById('playBtn').addEventListener('click', function() {
if (currentMusic) {
currentMusic.play();
}
});
document.getElementById('pauseBtn').addEventListener('click', function() {
if (currentMusic) {
currentMusic.pause();
}
});
document.getElementById('volumeSlider').addEventListener('input', function() {
const volume = this.value;
if (currentMusic) {
currentMusic.volume = volume;
}
// 设置所有音乐的默认音量
healthyMusic.volume = volume;
mediumMusic.volume = volume;
poorMusic.volume = volume;
});
// 初始化猪笼草
createPlantElements();
// 设置图表交互
setTimeout(setupChartInteractivity, 1000);
// 初始状态更新
updatePlantStatus();
</script>
</body>
</html>
总结
这个猪笼草生长环境模拟器展示了如何将复杂科学概念通过交互式可视化呈现,使学习变得直观有趣。它结合了数据可视化、动态图形和多媒体反馈,为学生提供了一个探索生物学和气候科学的有力工具。
项目的模块化设计和清晰的代码结构也使其成为学习前端开发(特别是Canvas操作、CSS动画和响应式设计)的优秀案例。
无论您是教育工作者寻找教学工具,还是开发者学习交互式网页开发技术,这个项目都提供了宝贵的参考价值。
温馨提示:
-
确保音频文件路径正确,或使用可靠的在线资源
-
在移动设备上测试触摸交互功能
-
考虑添加加载提示,改善用户体验
-
定期备份用户的数据调整,支持保存/加载功能
如果您在实现过程中遇到任何问题,或有改进建议,欢迎在评论区交流讨论!