光的反射折射原理演示
2025-10-07 09:37:58
发布于:安徽
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>光的反射与折射定律交互式演示</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- GSAP 动画库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#8B5CF6',
dark: '#0F172A',
light: '#E0E7FF',
accent: '#10B981'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
animation: {
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.text-gradient {
@apply bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-500;
}
.bg-glass {
@apply bg-white/10 backdrop-blur-md border border-white/20;
}
.shadow-glow {
@apply shadow-lg shadow-blue-500/20;
}
.transition-all-300 {
@apply transition-all duration-300 ease-in-out;
}
}
body {
background-color: #0F172A;
background-image:
radial-gradient(circle at 25% 25%, rgba(59, 130, 246, 0.1) 0%, transparent 50%),
radial-gradient(circle at 75% 75%, rgba(139, 92, 246, 0.1) 0%, transparent 50%);
min-height: 100vh;
overflow-x: hidden;
}
.canvas-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
#lightCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#particlesCanvas { pointer-events: none; }
.control-panel {
position: static;
z-index: auto;
}
.slider-container {
position: relative;
height: 40px;
}
.slider-container::before {
content: attr(data-min);
position: absolute;
left: 0;
bottom: -20px;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
}
.slider-container::after {
content: attr(data-max);
position: absolute;
right: 0;
bottom: -20px;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 6px;
background: linear-gradient(to right, #3B82F6, #8B5CF6);
border-radius: 3px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: #FFFFFF;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.2);
transition: all 0.2s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.preset-btn {
transition: all 0.2s ease;
}
.preset-btn:hover {
transform: translateY(-2px);
}
.preset-btn.active {
border-color: #3B82F6;
background-color: rgba(59, 130, 246, 0.1);
}
.formula-container {
font-family: 'Times New Roman', serif;
}
.formula {
font-style: italic;
}
.particle {
position: absolute;
width: 3px;
height: 3px;
border-radius: 50%;
pointer-events: none;
}
.media-boundary {
position: absolute;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(to right, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.1));
z-index: 1;
}
.normal-line {
position: absolute;
width: 1px;
background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent);
z-index: 1;
}
.start-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #0F172A;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 100;
transition: opacity 0.8s ease, visibility 0.8s ease;
}
.start-screen.hidden {
opacity: 0;
visibility: hidden;
}
.start-screen-content {
text-align: center;
max-width: 600px;
padding: 2rem;
border-radius: 16px;
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.37);
}
.start-button {
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.start-button::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s ease, height 0.6s ease;
}
.start-button:hover::after {
width: 300px;
height: 300px;
}
.start-button:active {
transform: scale(0.95);
}
.loading-bar {
width: 0;
height: 4px;
background: linear-gradient(to right, #3B82F6, #8B5CF6);
border-radius: 2px;
transition: width 1.5s ease;
}
@keyframes float {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-15px);
}
100% {
transform: translateY(0px);
}
}
.float-animation {
animation: float 3s ease-in-out infinite;
}
.tooltip {
position: absolute;
font-size: 12px;
padding: 6px 10px;
border-radius: 4px;
background-color: rgba(15, 23, 42, 0.9);
border: 1px solid rgba(255, 255, 255, 0.2);
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 20;
}
.tooltip.visible {
opacity: 1;
}
.info-card {
transition: all 0.3s ease;
}
.info-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.3);
}
.ray-path {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: dash 2s linear forwards;
}
@keyframes dash {
to {
stroke-dashoffset: 0;
}
}
.angle-arc {
fill: none;
stroke-width: 2;
stroke-linecap: round;
opacity: 0.8;
}
.angle-text {
font-size: 14px;
font-weight: 500;
text-anchor: middle;
dominant-baseline: middle;
}
.media-label {
position: absolute;
font-size: 14px;
font-weight: 500;
padding: 4px 8px;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.2);
pointer-events: none;
z-index: 2;
}
.media-label.top {
top: 10px;
left: 10px;
}
.media-label.bottom {
top: 50%;
left: 10px;
transform: none;
}
</style>
</head>
<body class="text-light">
<!-- 粒子背景 -->
<canvas id="particlesCanvas" class="fixed top-0 left-0 w-full h-full z-0"></canvas>
<div class="max-w-6xl mx-auto px-6 pt-6 pb-2 space-y-2 relative z-10 text-center">
<h1 class="text-4xl font-bold text-white tracking-wide">光学的折射和反射原理演示</h1>
<p class="text-gray-300 text-sm md:text-base">探索光线在不同介质界面的传播规律</p>
</div>
<div class="container mx-auto px-6 py-4 flex flex-col gap-6 max-w-6xl relative z-10">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
<!-- 左侧:演示区域 -->
<div class="w-full md:col-span-2 md:row-start-1 bg-glass rounded-xl shadow-glow overflow-hidden h-[420px]">
<div class="canvas-container h-full">
<canvas id="lightCanvas"></canvas>
<div class="media-boundary" id="mediaBoundary"></div>
<div class="normal-line" id="normalLine"></div>
<div class="media-label top" id="mediaTopLabel">介质 1: 空气 (n₁ = 1.0)</div>
<div class="media-label bottom" id="mediaBottomLabel">介质 2: 水 (n₂ = 1.33)</div>
</div>
</div>
<!-- 右侧:控制面板(上) -->
<!-- 控制区作为单独网格项:右列第1行 -->
<div class="bg-glass rounded-xl shadow-glow p-5 control-panel md:col-start-3 md:row-start-1 h-[420px] flex flex-col overflow-hidden">
<h2 class="text-lg font-semibold mb-3">参数控制</h2>
<div class="flex-1 overflow-y-auto pr-1">
<!-- 入射角控制 -->
<div class="mb-5">
<label for="incidentAngle" class="block text-xs font-medium mb-1">入射角 (°)</label>
<div class="slider-container" data-min="0" data-max="90">
<input type="range" id="incidentAngle" min="0" max="90" value="45" class="w-full">
</div>
<div class="mt-1 flex justify-center">
<span id="incidentAngleValue" class="text-xs font-semibold text-blue-400">45°</span>
</div>
</div>
<!-- 介质折射率控制 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="n1" class="block text-xs font-medium mb-1">介质 1 折射率 (n₁)</label>
<div class="slider-container" data-min="1.0" data-max="2.4">
<input type="range" id="n1" min="1.0" max="2.4" step="0.01" value="1.0" class="w-full">
</div>
<div class="mt-1 flex justify-center">
<span id="n1Value" class="text-xs font-semibold text-blue-400">1.0</span>
</div>
</div>
<div>
<label for="n2" class="block text-xs font-medium mb-1">介质 2 折射率 (n₂)</label>
<div class="slider-container" data-min="1.0" data-max="2.4">
<input type="range" id="n2" min="1.0" max="2.4" step="0.01" value="1.33" class="w-full">
</div>
<div class="mt-1 flex justify-center">
<span id="n2Value" class="text-xs font-semibold text-purple-400">1.33</span>
</div>
</div>
</div>
<!-- 预设介质 -->
<div class="mt-4">
<h3 class="text-xs font-medium mb-2">预设介质</h3>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
<button class="preset-btn px-3 py-2 bg-white/5 border border-white/20 rounded-lg text-xs hover:bg-white/10 transition-all-300" data-n1="1.0" data-n2="1.0">空气-空气</button>
<button class="preset-btn px-3 py-2 bg-white/5 border border-white/20 rounded-lg text-xs hover:bg-white/10 transition-all-300" data-n1="1.0" data-n2="1.33">空气-水</button>
<button class="preset-btn px-3 py-2 bg-white/5 border border-white/20 rounded-lg text-xs hover:bg-white/10 transition-all-300" data-n1="1.0" data-n2="1.52">空气-玻璃</button>
<button class="preset-btn px-3 py-2 bg-white/5 border border-white/20 rounded-lg text-xs hover:bg-white/10 transition-all-300" data-n1="1.0" data-n2="2.42">空气-钻石</button>
<button class="preset-btn px-3 py-2 bg-white/5 border border-white/20 rounded-lg text-xs hover:bg-white/10 transition-all-300" data-n1="1.33" data-n2="1.52">水-玻璃</button>
<button class="preset-btn px-3 py-2 bg-white/5 border border-white/20 rounded-lg text-xs hover:bg-white/10 transition-all-300" data-n1="1.52" data-n2="2.42">玻璃-钻石</button>
<button class="preset-btn px-3 py-2 bg-white/5 border border-white/20 rounded-lg text-xs hover:bg-white/10 transition-all-300" data-n1="1.33" data-n2="1.0">水-空气</button>
<button class="preset-btn px-3 py-2 bg-white/5 border border-white/20 rounded-lg text-xs hover:bg-white/10 transition-all-300" data-n1="1.52" data-n2="1.0">玻璃-空气</button>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="mt-4 flex flex-wrap gap-3 flex-shrink-0">
<button id="playButton" class="flex items-center gap-2 px-4 py-2 text-sm bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-700 hover:to-blue-600 rounded-lg shadow-md hover:shadow-lg transition-all-300">
<i class="fa fa-play"></i> 播放动画
</button>
<button id="resetButton" class="flex items-center gap-2 px-4 py-2 text-sm bg-gradient-to-r from-gray-600 to-gray-500 hover:from-gray-700 hover:to-gray-600 rounded-lg shadow-md hover:shadow-lg transition-all-300">
<i class="fa fa-refresh"></i> 重置
</button>
<button id="screenshotButton" class="flex items-center gap-2 px-4 py-2 text-sm bg-gradient-to-r from-purple-600 to-purple-500 hover:from-purple-700 hover:to-purple-600 rounded-lg shadow-md hover:shadow-lg transition-all-300">
<i class="fa fa-camera"></i> 截图
</button>
</div>
</div>
<!-- 数据显示区:右列第2行 -->
<div class="bg-glass rounded-xl shadow-glow p-5 control-panel md:col-start-3 md:row-start-2 flex-none min-h-[220px]">
<h2 class="text-lg font-semibold mb-3">实时数据</h2>
<div class="space-y-3 text-sm">
<div class="flex justify-between items-center">
<span class="text-gray-300 text-xs">入射角:</span>
<span id="displayIncidentAngle" class="text-base font-semibold text-blue-400">45°</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-300 text-xs">反射角:</span>
<span id="displayReflectionAngle" class="text-base font-semibold text-purple-400">45°</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-300 text-xs">折射角:</span>
<span id="displayRefractionAngle" class="text-base font-semibold text-green-400">32.0°</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-300 text-xs">临界角:</span>
<span id="displayCriticalAngle" class="text-base font-semibold text-amber-400">48.7°</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-300 text-xs">现象:</span>
<span id="displayPhenomenon" class="text-base font-semibold text-teal-400">折射</span>
</div>
</div>
</div>
<!-- 底部:公式和说明(左列第2行,与实时数据顶部对齐) -->
<div class="bg-glass rounded-xl shadow-glow p-5 mt-0 w-full md:col-span-2 md:row-start-2 relative z-10 min-h-[220px]">
<div class="grid grid-cols-1 gap-6">
<div class="formula-container">
<h2 class="text-lg font-semibold mb-3">物理公式</h2>
<div class="space-y-3">
<div>
<h3 class="text-base font-medium">反射定律</h3>
<p class="formula text-lg">θᵢ = θᵣ</p>
<p class="text-xs text-gray-400">入射角等于反射角</p>
</div>
<div>
<h3 class="text-base font-medium">折射定律 (斯奈尔定律)</h3>
<p class="formula text-lg">n₁·sin(θᵢ) = n₂·sin(θₜ)</p>
<p class="text-xs text-gray-400">入射角的正弦与折射角的正弦之比等于两种介质折射率之比</p>
</div>
<div>
<h3 class="text-base font-medium">临界角</h3>
<p class="formula text-lg">sin(θc) = n₂/n₁ (n₁ > n₂)</p>
<p class="text-xs text-gray-400">当光从光密介质射向光疏介质时,折射角为90°时的入射角</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 工具提示 -->
<div id="tooltip" class="tooltip">提示信息</div>
<script>
// 全局变量
let canvas, ctx;
let canvasWidth, canvasHeight;
let centerX, centerY;
let incidentAngle = 45; // 入射角(度)
let n1 = 1.0; // 介质1折射率
let n2 = 1.33; // 介质2折射率
let isPlaying = false;
let animationId;
let particles = [];
let rayAnimationProgress = 0;
let rayAnimationSpeed = 0.02;
let incidentPoint = { x: 0, y: 0 };
let mediaBoundaryY;
let normalLineX;
let isDragging = false;
let isMobile = false;
let currentAngleLabels = {
incident: '入射角 θᵢ = 45.0°',
reflection: '反射角 θᵣ = 45.0°',
refraction: '折射角 θₜ = 32.0°',
hasRefraction: true
};
// 介质名称映射
const mediumNames = {
1.0: "空气",
1.33: "水",
1.52: "玻璃",
2.42: "钻石"
};
// 颜色配置
const colors = {
incidentRay: '#60A5FA', // 入射光 - 蓝色
reflectionRay: '#A78BFA', // 反射光 - 紫色
refractionRay: '#34D399', // 折射光 - 绿色
normalLine: 'rgba(255, 255, 255, 0.5)', // 法线 - 白色半透明
mediaBoundary: 'rgba(255, 255, 255, 0.3)', // 介质边界 - 白色半透明
angleArcIncident: '#60A5FA', // 入射角弧线 - 蓝色
angleArcReflection: '#A78BFA', // 反射角弧线 - 紫色
angleArcRefraction: '#34D399', // 折射角弧线 - 绿色
particleIncident: '#60A5FA', // 入射光粒子 - 蓝色
particleReflection: '#A78BFA', // 反射光粒子 - 紫色
particleRefraction: '#34D399', // 折射光粒子 - 绿色
};
// 初始化函数
function init() {
// 检查是否为移动设备
isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
// 获取Canvas元素
canvas = document.getElementById('lightCanvas');
ctx = canvas.getContext('2d');
// 设置Canvas尺寸
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// 初始化UI事件
initUIEvents();
// 初始化介质边界和法线
mediaBoundaryY = centerY;
normalLineX = centerX;
incidentPoint = { x: normalLineX, y: mediaBoundaryY };
// 更新介质标签
updateMediaLabels();
// 初始化粒子系统
initParticleSystem();
// 开始动画循环
animate();
// 重置模拟
resetSimulation();
}
// 粒子系统类
class ParticleSystem {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.particles = [];
this.maxParticles = 150; // 增加粒子数量
this.isRunning = false;
this.mouseX = null;
this.mouseY = null;
this.mouseRadius = 150; // 鼠标影响半径
// 设置Canvas尺寸
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
// 添加鼠标移动事件
this.canvas.addEventListener('mousemove', (e) => {
const rect = this.canvas.getBoundingClientRect();
this.mouseX = e.clientX - rect.left;
this.mouseY = e.clientY - rect.top;
});
// 初始化粒子
this.initParticles();
}
resizeCanvas() {
const container = this.canvas.parentElement;
this.canvas.width = container.clientWidth;
this.canvas.height = container.clientHeight;
}
initParticles() {
this.particles = [];
for (let i = 0; i < this.maxParticles; i++) {
this.particles.push(this.createParticle());
}
}
createParticle() {
// 随机位置
const x = Math.random() * this.canvas.width;
const y = Math.random() * this.canvas.height;
// 随机速度
const speed = Math.random() * 0.8 + 0.2;
const angle = Math.random() * Math.PI * 2;
const vx = Math.cos(angle) * speed;
const vy = Math.sin(angle) * speed;
// 随机颜色 - 使用渐变色彩
const colorType = Math.floor(Math.random() * 3);
let color;
switch(colorType) {
case 0: // 蓝色渐变
color = `hsl(210, ${Math.random() * 30 + 70}%, ${Math.random() * 20 + 40}%)`;
break;
case 1: // 紫色渐变
color = `hsl(270, ${Math.random() * 30 + 70}%, ${Math.random() * 20 + 40}%)`;
break;
case 2: // 青色渐变
color = `hsl(180, ${Math.random() * 30 + 70}%, ${Math.random() * 20 + 40}%)`;
break;
}
// 随机大小
const size = Math.random() * 4 + 1;
// 随机透明度
const alpha = Math.random() * 0.7 + 0.3;
// 随机脉冲速度
const pulseSpeed = Math.random() * 0.02 + 0.01;
// 随机轨迹长度
const trailLength = Math.floor(Math.random() * 20 + 5);
return {
x,
y,
vx,
vy,
color,
size,
baseSize: size,
alpha,
life: Math.random() * 3000 + 2000, // 延长粒子生命周期
bornTime: Date.now(),
pulseSpeed,
pulseDirection: 1,
trail: [],
trailLength
};
}
update() {
if (!this.isRunning) return;
const currentTime = Date.now();
for (let i = this.particles.length - 1; i >= 0; i--) {
const particle = this.particles[i];
// 记录轨迹
particle.trail.push({x: particle.x, y: particle.y});
if (particle.trail.length > particle.trailLength) {
particle.trail.shift();
}
// 粒子脉冲效果
particle.size += particle.pulseSpeed * particle.pulseDirection;
if (particle.size > particle.baseSize * 1.5) {
particle.pulseDirection = -1;
} else if (particle.size < particle.baseSize * 0.5) {
particle.pulseDirection = 1;
}
// 鼠标交互 - 粒子被鼠标吸引
if (this.mouseX !== null && this.mouseY !== null) {
const dx = this.mouseX - particle.x;
const dy = this.mouseY - particle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < this.mouseRadius) {
// 计算吸引力
const force = (this.mouseRadius - distance) / this.mouseRadius;
const angle = Math.atan2(dy, dx);
particle.vx += Math.cos(angle) * force * 0.05;
particle.vy += Math.sin(angle) * force * 0.05;
// 限制速度
const speed = Math.sqrt(particle.vx * particle.vx + particle.vy * particle.vy);
if (speed > 2) {
particle.vx = (particle.vx / speed) * 2;
particle.vy = (particle.vy / speed) * 2;
}
}
}
// 更新位置
particle.x += particle.vx;
particle.y += particle.vy;
// 边界反弹效果
if (particle.x < 0 || particle.x > this.canvas.width) {
particle.vx *= -0.8; // 反弹时损失一些能量
}
if (particle.y < 0 || particle.y > this.canvas.height) {
particle.vy *= -0.8; // 反弹时损失一些能量
}
// 保持粒子在画布内
particle.x = Math.max(0, Math.min(this.canvas.width, particle.x));
particle.y = Math.max(0, Math.min(this.canvas.height, particle.y));
// 检查生命周期
if (currentTime > particle.bornTime + particle.life) {
this.particles[i] = this.createParticle();
}
}
}
draw() {
if (!this.isRunning) return;
// 创建渐变背景
const gradient = this.ctx.createLinearGradient(0, 0, 0, this.canvas.height);
gradient.addColorStop(0, 'rgba(15, 23, 42, 0.8)');
gradient.addColorStop(1, 'rgba(30, 41, 59, 0.8)');
this.ctx.fillStyle = gradient;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制粒子连线
this.particles.forEach((particle, i) => {
this.particles.slice(i + 1).forEach(otherParticle => {
const dx = particle.x - otherParticle.x;
const dy = particle.y - otherParticle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 120) {
// 连线颜色随距离变化
const opacity = 0.2 - distance / 1000;
const gradient = this.ctx.createLinearGradient(
particle.x, particle.y,
otherParticle.x, otherParticle.y
);
gradient.addColorStop(0, particle.color);
gradient.addColorStop(1, otherParticle.color);
this.ctx.beginPath();
this.ctx.moveTo(particle.x, particle.y);
this.ctx.lineTo(otherParticle.x, otherParticle.y);
this.ctx.strokeStyle = `${particle.color.replace(')', `, ${opacity})`).replace('hsl', 'hsla')}`;
this.ctx.lineWidth = 0.8;
this.ctx.stroke();
}
});
});
// 绘制粒子轨迹
this.particles.forEach(particle => {
if (particle.trail.length < 2) return;
this.ctx.beginPath();
this.ctx.moveTo(particle.trail[0].x, particle.trail[0].y);
for (let i = 1; i < particle.trail.length; i++) {
this.ctx.lineTo(particle.trail[i].x, particle.trail[i].y);
}
// 轨迹颜色随时间变化
const opacity = particle.alpha * 0.5;
this.ctx.strokeStyle = `${particle.color.replace(')', `, ${opacity})`).replace('hsl', 'hsla')}`;
this.ctx.lineWidth = particle.size * 0.5;
this.ctx.lineCap = 'round';
this.ctx.stroke();
});
// 绘制粒子
this.particles.forEach(particle => {
// 绘制发光效果
this.ctx.beginPath();
this.ctx.arc(particle.x, particle.y, particle.size * 2, 0, Math.PI * 2);
const glowGradient = this.ctx.createRadialGradient(
particle.x, particle.y, 0,
particle.x, particle.y, particle.size * 2
);
glowGradient.addColorStop(0, `${particle.color.replace(')', `, ${particle.alpha * 0.5})`).replace('hsl', 'hsla')}`);
glowGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
this.ctx.fillStyle = glowGradient;
this.ctx.fill();
// 绘制粒子主体
this.ctx.beginPath();
this.ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
// 创建渐变
const particleGradient = this.ctx.createRadialGradient(
particle.x, particle.y, 0,
particle.x, particle.y, particle.size
);
particleGradient.addColorStop(0, particle.color);
particleGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
this.ctx.fillStyle = particleGradient;
this.ctx.globalAlpha = particle.alpha;
this.ctx.fill();
this.ctx.globalAlpha = 1;
});
// 绘制鼠标影响区域(可选)
if (this.mouseX !== null && this.mouseY !== null) {
this.ctx.beginPath();
this.ctx.arc(this.mouseX, this.mouseY, this.mouseRadius, 0, Math.PI * 2);
this.ctx.fillStyle = 'rgba(255, 255, 255, 0.01)';
this.ctx.fill();
}
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.lastTime = Date.now();
// 设置粒子出生时间
this.particles.forEach(particle => {
particle.bornTime = Date.now();
});
// 开始动画循环
const animate = () => {
const currentTime = Date.now();
const deltaTime = currentTime - this.lastTime;
if (deltaTime > 16) { // 限制帧率约为60fps
this.update();
this.draw();
this.lastTime = currentTime;
}
if (this.isRunning) {
requestAnimationFrame(animate);
}
};
animate();
}
stop() {
this.isRunning = false;
}
}
// 初始化粒子系统
function initParticleSystem() {
// 创建粒子系统
const particleSystem = new ParticleSystem('particlesCanvas');
particleSystem.start();
}
// 调整Canvas尺寸
function resizeCanvas() {
const container = canvas.parentElement;
canvasWidth = container.clientWidth;
canvasHeight = container.clientHeight;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
centerX = canvasWidth / 2;
centerY = canvasHeight / 2;
// 更新介质边界和法线位置
if (mediaBoundaryY === undefined) {
mediaBoundaryY = centerY;
}
if (normalLineX === undefined) {
normalLineX = centerX;
}
// 更新DOM元素位置
updateDOMElementPositions();
// 重新绘制
draw();
}
// 更新DOM元素位置
function updateDOMElementPositions() {
const mediaBoundary = document.getElementById('mediaBoundary');
const normalLine = document.getElementById('normalLine');
const mediaBottomLabel = document.getElementById('mediaBottomLabel');
if (mediaBoundary) {
mediaBoundary.style.top = `${mediaBoundaryY}px`;
}
if (normalLine) {
normalLine.style.left = `${normalLineX}px`;
normalLine.style.top = `${mediaBoundaryY - 100}px`;
normalLine.style.height = '200px';
}
// 更新介质2标签位置
if (mediaBottomLabel) {
mediaBottomLabel.style.left = '10px';
mediaBottomLabel.style.top = `${mediaBoundaryY + 100}px`;
}
}
// 初始化UI事件
function initUIEvents() {
// 入射角滑块
const incidentAngleSlider = document.getElementById('incidentAngle');
const incidentAngleValue = document.getElementById('incidentAngleValue');
incidentAngleSlider.addEventListener('input', (e) => {
incidentAngle = parseInt(e.target.value);
incidentAngleValue.textContent = `${incidentAngle}°`;
updateSimulation();
});
// 介质1折射率滑块
const n1Slider = document.getElementById('n1');
const n1Value = document.getElementById('n1Value');
n1Slider.addEventListener('input', (e) => {
n1 = parseFloat(e.target.value);
n1Value.textContent = n1.toFixed(2);
updateMediaLabels();
updateSimulation();
});
// 介质2折射率滑块
const n2Slider = document.getElementById('n2');
const n2Value = document.getElementById('n2Value');
n2Slider.addEventListener('input', (e) => {
n2 = parseFloat(e.target.value);
n2Value.textContent = n2.toFixed(2);
updateMediaLabels();
updateSimulation();
});
// 预设介质按钮
const presetButtons = document.querySelectorAll('.preset-btn');
presetButtons.forEach(button => {
button.addEventListener('click', () => {
// 移除所有按钮的active类
presetButtons.forEach(btn => btn.classList.remove('active'));
// 添加当前按钮的active类
button.classList.add('active');
// 获取预设值
n1 = parseFloat(button.dataset.n1);
n2 = parseFloat(button.dataset.n2);
// 更新滑块和显示值
n1Slider.value = n1;
n1Value.textContent = n1.toFixed(2);
n2Slider.value = n2;
n2Value.textContent = n2.toFixed(2);
// 更新介质标签
updateMediaLabels();
// 更新模拟
updateSimulation();
});
});
// 播放按钮
const playButton = document.getElementById('playButton');
playButton.addEventListener('click', togglePlay);
// 重置按钮
const resetButton = document.getElementById('resetButton');
resetButton.addEventListener('click', resetSimulation);
// 截图按钮
const screenshotButton = document.getElementById('screenshotButton');
screenshotButton.addEventListener('click', takeScreenshot);
// Canvas交互
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('mouseleave', handleMouseUp);
// 触摸事件
canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
canvas.addEventListener('touchend', handleTouchEnd);
canvas.addEventListener('touchcancel', handleTouchEnd);
// 工具提示
const tooltip = document.getElementById('tooltip');
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 检查是否在入射点附近
const distance = Math.sqrt(Math.pow(x - incidentPoint.x, 2) + Math.pow(y - incidentPoint.y, 2));
if (distance < 20) {
tooltip.textContent = '拖动可改变入射点位置';
tooltip.style.left = `${e.clientX + 10}px`;
tooltip.style.top = `${e.clientY + 10}px`;
tooltip.classList.add('visible');
} else {
tooltip.classList.remove('visible');
}
});
canvas.addEventListener('mouseleave', () => {
tooltip.classList.remove('visible');
});
}
// 处理鼠标按下事件
function handleMouseDown(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 检查是否点击在入射点附近
const distance = Math.sqrt(Math.pow(x - incidentPoint.x, 2) + Math.pow(y - incidentPoint.y, 2));
if (distance < 20) {
isDragging = true;
canvas.style.cursor = 'grabbing';
}
}
// 处理鼠标移动事件
function handleMouseMove(e) {
if (!isDragging) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 更新入射点位置
incidentPoint.x = x;
incidentPoint.y = y;
// 更新介质边界和法线位置
mediaBoundaryY = incidentPoint.y;
normalLineX = incidentPoint.x;
// 更新DOM元素位置
updateDOMElementPositions();
// 重新绘制
draw();
}
// 处理鼠标释放事件
function handleMouseUp() {
isDragging = false;
canvas.style.cursor = 'grab';
}
// 处理触摸开始事件
function handleTouchStart(e) {
e.preventDefault();
if (e.touches.length === 1) {
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousedown', {
clientX: touch.clientX,
clientY: touch.clientY
});
handleMouseDown(mouseEvent);
}
}
// 处理触摸移动事件
function handleTouchMove(e) {
e.preventDefault();
if (e.touches.length === 1) {
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousemove', {
clientX: touch.clientX,
clientY: touch.clientY
});
handleMouseMove(mouseEvent);
}
}
// 处理触摸结束事件
function handleTouchEnd() {
handleMouseUp();
}
// 更新介质标签
function updateMediaLabels() {
const mediaTopLabel = document.getElementById('mediaTopLabel');
const mediaBottomLabel = document.getElementById('mediaBottomLabel');
// 获取介质名称
const medium1Name = mediumNames[n1.toFixed(2)] || `介质 ${n1.toFixed(2)}`;
const medium2Name = mediumNames[n2.toFixed(2)] || `介质 ${n2.toFixed(2)}`;
// 更新标签
if (mediaTopLabel) {
mediaTopLabel.textContent = `介质 1: ${medium1Name} (n₁ = ${n1.toFixed(2)})`;
}
if (mediaBottomLabel) {
mediaBottomLabel.textContent = `介质 2: ${medium2Name} (n₂ = ${n2.toFixed(2)})`;
}
}
// 切换播放状态
function togglePlay() {
isPlaying = !isPlaying;
const playButton = document.getElementById('playButton');
if (isPlaying) {
playButton.innerHTML = '<i class="fa fa-pause"></i> 暂停动画';
// 如果动画已被取消,重新启动动画循环
if (!animationId) {
animate();
}
} else {
playButton.innerHTML = '<i class="fa fa-play"></i> 继续动画';
}
}
// 重置模拟
function resetSimulation() {
// 重置参数
incidentAngle = 45;
n1 = 1.0;
n2 = 1.33;
// 更新滑块和显示值
document.getElementById('incidentAngle').value = incidentAngle;
document.getElementById('incidentAngleValue').textContent = `${incidentAngle}°`;
document.getElementById('n1').value = n1;
document.getElementById('n1Value').textContent = n1.toFixed(2);
document.getElementById('n2').value = n2;
document.getElementById('n2Value').textContent = n2.toFixed(2);
// 重置入射点位置
incidentPoint = { x: centerX, y: centerY };
mediaBoundaryY = centerY;
normalLineX = centerX;
// 更新DOM元素位置
updateDOMElementPositions();
// 更新介质标签
updateMediaLabels();
// 重置动画状态
isPlaying = false;
document.getElementById('playButton').innerHTML = '<i class="fa fa-play"></i> 播放动画';
rayAnimationProgress = 0;
// 清除粒子
particles = [];
// 更新模拟
updateSimulation();
}
// 截图功能
function takeScreenshot() {
// 创建一个临时的Canvas元素
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
// 设置临时Canvas尺寸
tempCanvas.width = canvasWidth;
tempCanvas.height = canvasHeight;
// 绘制背景
tempCtx.fillStyle = '#0F172A';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// 绘制介质边界
tempCtx.beginPath();
tempCtx.moveTo(0, mediaBoundaryY);
tempCtx.lineTo(tempCanvas.width, mediaBoundaryY);
tempCtx.strokeStyle = colors.mediaBoundary;
tempCtx.lineWidth = 2;
tempCtx.stroke();
// 绘制法线
tempCtx.beginPath();
tempCtx.moveTo(normalLineX, mediaBoundaryY - 100);
tempCtx.lineTo(normalLineX, mediaBoundaryY + 100);
tempCtx.setLineDash([5, 5]);
tempCtx.strokeStyle = colors.normalLine;
tempCtx.lineWidth = 1;
tempCtx.stroke();
tempCtx.setLineDash([]);
// 计算光线
const { incidentRay, reflectionRay, refractionRay, isRefracted, isTotalReflection } = calculateRays();
// 绘制入射光线
drawRay(tempCtx, incidentRay, colors.incidentRay, 2, false);
// 绘制反射光线
drawRay(tempCtx, reflectionRay, colors.reflectionRay, 2, false);
// 绘制折射光线(如果存在)
if (isRefracted && !isTotalReflection) {
drawRay(tempCtx, refractionRay, colors.refractionRay, 2, false);
}
// 绘制角度标注
drawAngleAnnotation(tempCtx, incidentPoint.x, incidentPoint.y, incidentRay.angle, 30, colors.angleArcIncident, currentAngleLabels.incident, 'incident');
drawAngleAnnotation(tempCtx, incidentPoint.x, incidentPoint.y, reflectionRay.angle, 30, colors.angleArcReflection, currentAngleLabels.reflection, 'reflection');
if (isRefracted && !isTotalReflection && currentAngleLabels.hasRefraction) {
drawAngleAnnotation(tempCtx, incidentPoint.x, incidentPoint.y, Math.abs(refractionRay.angle), 30, colors.angleArcRefraction, currentAngleLabels.refraction, 'refraction');
} else if (isTotalReflection) {
// 绘制全反射提示
tempCtx.fillStyle = '#F59E0B';
tempCtx.font = 'bold 16px Inter, sans-serif';
tempCtx.textAlign = 'center';
tempCtx.fillText('全反射', incidentPoint.x + 50, incidentPoint.y - 20);
}
// 绘制介质标签
tempCtx.fillStyle = 'rgba(255, 255, 255, 0.8)';
tempCtx.font = '14px Inter, sans-serif';
tempCtx.textAlign = 'left';
// 获取介质名称
const medium1Name = mediumNames[n1.toFixed(2)] || `介质 ${n1.toFixed(2)}`;
const medium2Name = mediumNames[n2.toFixed(2)] || `介质 ${n2.toFixed(2)}`;
tempCtx.fillText(`介质 1: ${medium1Name} (n₁ = ${n1.toFixed(2)})`, 10, 30);
tempCtx.fillText(`介质 2: ${medium2Name} (n₂ = ${n2.toFixed(2)})`, 10, tempCanvas.height - 20);
// 创建下载链接
const link = document.createElement('a');
link.download = `光的反射折射_${new Date().toISOString().slice(0, 10)}.png`;
link.href = tempCanvas.toDataURL('image/png');
link.click();
}
// 计算光线
function calculateRays() {
// 入射角(弧度)
const incidentAngleRad = incidentAngle * Math.PI / 180;
// 入射光线起点
const incidentStartX = incidentPoint.x - Math.sin(incidentAngleRad) * 200;
const incidentStartY = incidentPoint.y - Math.cos(incidentAngleRad) * 200;
// 入射光线
const incidentRay = {
start: { x: incidentStartX, y: incidentStartY },
end: { x: incidentPoint.x, y: incidentPoint.y },
angle: incidentAngl
全部评论 3
太多了,写不下去。
2天前 来自 安徽
1}; // 反射光线 const reflectionAngleRad = incidentAngleRad; const reflectionEndX = incidentPoint.x + Math.sin(reflectionAngleRad) * 200; const reflectionEndY = incidentPoint.y - Math.cos(reflectionAngleRad) * 200; const reflectionRay = { start: { x: incidentPoint.x, y: incidentPoint.y }, end: { x: reflectionEndX, y: reflectionEndY }, angle: reflectionAngleRad }; // 折射光线 let refractionRay = null; let isRefracted = true; let isTotalReflection = false; // 使用斯奈尔定律计算折射角 const sinRefraction = (n1 / n2) * Math.sin(incidentAngleRad); // 检查是否发生全反射 if (Math.abs(sinRefraction) > 1 && n1 > n2) { isRefracted = false; isTotalReflection = true; } else { const refractionAngleRad = Math.asin(sinRefraction); const refractionEndX = incidentPoint.x + Math.sin(refractionAngleRad) * 200; const refractionEndY = incidentPoint.y + Math.cos(refractionAngleRad) * 200; refractionRay = { start: { x: incidentPoint.x, y: incidentPoint.y }, end: { x: refractionEndX, y: refractionEndY }, angle: refractionAngleRad }; } return { incidentRay, reflectionRay, refractionRay, isRefracted, isTotalReflection }; } // 绘制光线 function drawRay(ctx, ray, color, lineWidth, dashed = false) { ctx.beginPath(); ctx.moveTo(ray.start.x, ray.start.y); ctx.lineTo(ray.end.x, ray.end.y); if (dashed) { ctx.setL
昨天 来自 安徽
0ber,狠入啊
2天前 来自 浙江
0
有帮助,赞一个