<!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>
body {
font-family: 'Microsoft YaHei', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
background-color: #f4f6f9;
margin: 0;
}
h1 {
color: #2c3e50;
text-align: center;
}
.container {
width: 90%;
max-width: 800px;
display: flex;
flex-direction: column;
gap: 15px;
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
background-color: #ecf0f1;
padding: 15px;
border-radius: 8px;
}
label {
font-weight: bold;
color: #34495e;
}
input[type="range"],
input[type="number"],
select {
width: 100%;
padding: 8px;
border-radius: 5px;
border: 1px solid #bdc3c7;
font-size: 14px;
}
canvas {
border: 1px solid #ccc;
background-color: #ffffff;
margin-top: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
.info {
margin-top: 15px;
padding: 12px;
background-color: #d5f5e3;
border-left: 5px solid #2ecc71;
font-size: 15px;
color: #27ae60;
white-space: pre-line;
}
</style>
</head>
<body>
<h1>✨ 光的折射模拟器(斯涅尔定律演示)</h1>
<div class="container">
<div class="controls">
<div>
<label>入射角 θ₁ (度):</label>
<input type="range" id="angleSlider" min="0" max="90" value="30" step="0.1" />
<span id="angleValue">30.0°</span>
</div>
<div>
<label>介质1折射率 n₁:</label>
<input type="number" id="n1Input" step="0.01" min="1.00" max="2.50" value="1.00" />
</div>
<div>
<label>介质2折射率 n₂:</label>
<input type="number" id="n2Input" step="0.01" min="1.00" max="2.50" value="1.50" />
</div>
<div>
<label>光线颜色:</label>
<select id="colorSelect">
<option value="red">🔴 红色</option>
<option value="green">🟢 绿色</option>
<option value="blue">🔵 蓝色</option>
<option value="yellow">🟡 黄色</option>
<option value="magenta">🟣 品红</option>
</select>
</div>
</div>
<canvas id="refractionCanvas" width="800" height="500"></canvas>
<div class="info" id="angleDisplay">
入射角 θ₁ = 30.0°
折射角 θ₂ = 19.5°
验证斯涅尔定律:n₁·sinθ₁ ≈ n₂·sinθ₂
</div>
</div>
<!-- 使用国内 CDN 加载 jQuery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$(function () {
const canvas = document.getElementById('refractionCanvas');
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// 中间分界线 y 坐标
const boundaryY = height / 2;
function draw() {
const theta1Deg = parseFloat($('#angleSlider').val());
const n1 = parseFloat($('#n1Input').val());
const n2 = parseFloat($('#n2Input').val());
const color = $('#colorSelect').val();
const theta1Rad = (theta1Deg * Math.PI) / 180;
let theta2Rad = 0;
let theta2Deg = 0;
// 斯涅尔定律:n1*sin(θ1) = n2*sin(θ2)
const sinTheta2 = (n1 * Math.sin(theta1Rad)) / n2;
// 判断是否全反射
if (Math.abs(sinTheta2) <= 1) {
theta2Rad = Math.asin(sinTheta2);
theta2Deg = (theta2Rad * 180) / Math.PI;
} else {
theta2Deg = "全反射";
}
// 更新显示角度和公式
$('#angleValue').text(`${theta1Deg.toFixed(1)}°`);
$('#angleDisplay').text(
`入射角 θ₁ = ${theta1Deg.toFixed(1)}°\n` +
`折射角 θ₂ = ${typeof theta2Deg === 'number' ? theta2Deg.toFixed(1) : theta2Deg}°\n` +
`验证斯涅尔定律:n₁·sinθ₁ = n₂·sinθ₂?\n` +
(typeof theta2Deg === 'number'
? `→ ${n1.toFixed(2)} × sin(${theta1Deg.toFixed(1)}°) ≈ ${(n1 * Math.sin(theta1Rad)).toFixed(4)}\n → ${n2.toFixed(2)} × sin(${theta2Deg.toFixed(1)}°) ≈ ${(n2 * Math.sin(theta2Rad)).toFixed(4)}`
: `→ |sin(θ₂)| > 1,发生全反射`)
);
// 清空画布
ctx.clearRect(0, 0, width, height);
// 绘制上下区域
ctx.fillStyle = '#e3f2fd'; // 介质1:浅蓝
ctx.fillRect(0, 0, width, boundaryY);
ctx.fillStyle = '#f3e5f5'; // 介质2:浅紫
ctx.fillRect(0, boundaryY, width, boundaryY);
// 绘制分界线
ctx.beginPath();
ctx.moveTo(0, boundaryY);
ctx.lineTo(width, boundaryY);
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.stroke();
// 绘制法线(虚线)
ctx.setLineDash([6, 4]);
ctx.beginPath();
ctx.moveTo(width / 2, 0);
ctx.lineTo(width / 2, height);
ctx.strokeStyle = '#555';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.setLineDash([]);
// 设置光源位置(在画布左侧上方)
const originX = width / 2;
const originY = boundaryY;
// 绘制入射光线
const incidentLength = 140;
const incidentX = originX - incidentLength * Math.sin(theta1Rad);
const incidentY = originY - incidentLength * Math.cos(theta1Rad);
drawArrow(ctx, incidentX, incidentY, originX, originY, color, 3);
// 如果没有全反射,绘制折射光线;否则绘制反射光线
if (typeof theta2Deg === 'number') {
const refractedLength = 140;
const refractedX = originX + refractedLength * Math.sin(theta2Rad);
const refractedY = originY + refractedLength * Math.cos(theta2Rad);
drawArrow(ctx, originX, originY, refractedX, refractedY, color, 3);
} else {
// 全反射:向上反射
const reflectedLength = 140;
const reflectedX = originX + reflectedLength * Math.sin(theta1Rad);
const reflectedY = originY - reflectedLength * Math.cos(theta1Rad);
drawArrow(ctx, originX, originY, reflectedX, reflectedY, color, 3, true);
}
}
// 绘制带箭头的线段
function drawArrow(context, fromX, fromY, toX, toY, color, lineWidth, isReflection = false) {
context.strokeStyle = color;
context.fillStyle = color;
context.lineWidth = lineWidth;
context.beginPath();
context.moveTo(fromX, fromY);
context.lineTo(toX, toY);
context.stroke();
// 绘制箭头
const headLen = 10;
const angle = Math.atan2(toY - fromY, toX - fromX);
context.beginPath();
context.moveTo(toX, toY);
context.lineTo(
toX - headLen * Math.cos(angle - Math.PI / 6),
toY - headLen * Math.sin(angle - Math.PI / 6)
);
context.lineTo(
toX - headLen * Math.cos(angle + Math.PI / 6),
toY - headLen * Math.sin(angle + Math.PI / 6)
);
context.closePath();
context.fill();
// 反射标记
if (isReflection) {
context.font = '14px Microsoft YaHei';
context.fillStyle = 'red';
context.fillText('反射!', toX + 12, toY - 10);
}
}
// 初始化绘制
draw();
// 绑定所有控件事件
$('#angleSlider, #n1Input, #n2Input, #colorSelect')
.on('input change', draw);
});
</script>
</body>
</html>