平滑轨迹点动画
功能介绍
此网页可以研究函数曲线在经过坐标变换后的轨迹,暂时没有画上平面直角坐标系或极坐标系,因此使用时需要自行添加。在文章结尾会奉上源代码,欢迎大家修改。
下面是页面展示:
支持的数学函数
- 基本函数:
sin
、cos
、tan
、sqrt
、pow
等 - 常量:
pi
和e
优化内容
在原有代码基础上进行了以下优化:
绘图优化
- 使用 二次贝塞尔曲线(
quadraticCurveTo
)代替直线。 - 增加采样点数量(从 100 增加到 200)。
- 优化线条连接处的显示效果。
动画效果优化
- 使用 进度控制 替代帧计数。
- 增加点的 光晕效果。
- 添加平滑的 轨迹渐现效果。
交互改进
- 添加 清除画布 按钮。
- 优化动画重启逻辑。
- 改进错误提示显示。
视觉效果提升
- 线条使用圆角端点(
lineCap: 'round'
)。 - 运动点添加发光效果。
- 轨迹线带有透明度,视觉效果更柔和。
使用方法
- 输入轨迹方程:如
[sin(t), cos(t)]
。 - 输入变换方程:如
[x + 1, y * 2]
。 - 点击 “开始动画” 观看平滑动画效果。
- 可随时点击 “清除画布” 重新开始。
HTML 源代码
以下是完整的 HTML 源代码(部分AI生成):
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>平滑轨迹点动画</title>
<script src="https://cdn.jsdelivr.net/npm/mathjs/lib/browser/math.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f2f5;
display: flex;
flex-direction: column;
align-items: center;
}
.container {
max-width: 800px;
width: 100%;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #1a73e8;
margin-bottom: 20px;
text-align: center;
}
.input-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
}
input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.controls {
display: flex;
gap: 10px;
margin: 10px 0;
}
button {
background-color: #1a73e8;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
flex: 1;
}
button:hover {
background-color: #1557b0;
}
#error {
color: #d93025;
margin-top: 10px;
padding: 10px;
border-radius: 4px;
display: none;
}
canvas {
border: 1px solid #ddd;
border-radius: 4px;
margin-top: 20px;
background-color: #fff;
}
</style>
</head>
<body>
<div class="container">
<h1>平滑轨迹点动画</h1>
<div class="input-group">
<label>轨迹方程 (参数:t)</label>
<input type="text" id="trajectory" value="[sin(t), cos(t)]" placeholder="例如:[sin(t), cos(t)]">
</div>
<div class="input-group">
<label>变换方程 (参数:x, y)</label>
<input type="text" id="transform" value="[x + 1, y * 2]" placeholder="例如:[x + 1, y * 2]">
</div>
<div class="controls">
<button id="start">开始动画</button>
<button id="clear">清除画布</button>
</div>
<div id="error"></div>
<canvas id="canvas" width="600" height="600"></canvas>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const errorDiv = document.getElementById('error');
let animationId = null;
// 数学函数环境
const mathContext = {
sin: Math.sin,
cos: Math.cos,
tan: Math.tan,
sqrt: Math.sqrt,
pow: Math.pow,
pi: Math.PI,
e: Math.E
};
function showError(message) {
errorDiv.style.display = 'block';
errorDiv.textContent = message;
}
function hideError() {
errorDiv.style.display = 'none';
}
function parseExpression(expr, isTrajectory) {
try {
const content = expr.slice(1, -1).split(',').map(e => e.trim());
if (content.length !== 2) {
throw new Error(`表达式必须包含两个分量,格式:[表达式1, 表达式2]`);
}
return content.map(expr => math.compile(expr));
} catch (e) {
throw new Error(`${isTrajectory ? '轨迹' : '变换'}方程格式错误: ${e.message}`);
}
}
function evaluatePoint(expressions, params) {
try {
return expressions.map(expr => expr.evaluate(params));
} catch (e) {
throw new Error(`计算出错: ${e.message}`);
}
}
// 绘制平滑曲线
function drawSmoothLine(points, color, width = 2, alpha = 1) {
if (points.length < 2) return;
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
// 使用贝塞尔曲线使线条更平滑
for (let i = 1; i < points.length - 2; i++) {
const xc = (points[i][0] + points[i + 1][0]) / 2;
const yc = (points[i][1] + points[i + 1][1]) / 2;
ctx.quadraticCurveTo(points[i][0], points[i][1], xc, yc);
}
// 处理最后两个点
if (points.length > 2) {
ctx.quadraticCurveTo(
points[points.length - 2][0],
points[points.length - 2][1],
points[points.length - 1][0],
points[points.length - 1][1]
);
}
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalAlpha = alpha;
ctx.stroke();
ctx.globalAlpha = 1.0;
}
function drawPoint(x, y, color = 'blue', size = 6) {
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
// 添加光晕效果
ctx.beginPath();
ctx.arc(x, y, size + 4, 0, Math.PI * 2);
ctx.fillStyle = color.replace(')', ', 0.3)');
ctx.fill();
}
// 清除画布
document.getElementById('clear').addEventListener('click', () => {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
hideError();
});
document.getElementById('start').addEventListener('click', () => {
if (animationId) {
cancelAnimationFrame(animationId);
}
hideError();
ctx.clearRect(0, 0, canvas.width, canvas.height);
try {
const trajectoryExpr = document.getElementById('trajectory').value.trim();
const transformExpr = document.getElementById('transform').value.trim();
if (!trajectoryExpr.startsWith('[') || !trajectoryExpr.endsWith(']')) {
throw new Error('轨迹方程必须用方括号包围');
}
if (!transformExpr.startsWith('[') || !transformExpr.endsWith(']')) {
throw new Error('变换方程必须用方括号包围');
}
const trajectoryFns = parseExpression(trajectoryExpr, true);
const transformFns = parseExpression(transformExpr, false);
// 增加采样点数量,使曲线更平滑
const steps = 200;
const points = [];
const transformedPoints = [];
for (let i = 0; i <= steps; i++) {
const t = (i / steps) * Math.PI * 2;
const [x, y] = evaluatePoint(trajectoryFns, { ...mathContext, t });
const canvasX = x * 100 + canvas.width / 2;
const canvasY = y * 100 + canvas.height / 2;
points.push([canvasX, canvasY]);
const [tx, ty] = evaluatePoint(transformFns, {
...mathContext,
x: x,
y: y
});
transformedPoints.push([
tx * 100 + canvas.width / 2,
ty * 100 + canvas.height / 2
]);
}
let progress = 0;
const animate = (timestamp) => {
const pointCount = Math.floor(progress * points.length);
if (pointCount > 1) {
// 绘制已完成的轨迹部分(带透明度渐变)
const currentPoints = points.slice(0, pointCount);
const currentTransformed = transformedPoints.slice(0, pointCount);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制平滑曲线
drawSmoothLine(currentPoints, 'rgba(0,0,255,0.5)', 2);
drawSmoothLine(currentTransformed, 'rgba(255,0,0,0.5)', 2);
// 绘制当前运动点
const lastPoint = currentPoints[currentPoints.length - 1];
const lastTransformed = currentTransformed[currentTransformed.length - 1];
drawPoint(lastPoint[0], lastPoint[1], 'rgba(0,0,255,1)');
drawPoint(lastTransformed[0], lastTransformed[1], 'rgba(255,0,0,1)');
}
progress += 0.005; // 控制动画速度
if (progress <= 1) {
animationId = requestAnimationFrame(animate);
}
};
animationId = requestAnimationFrame(animate);
} catch (e) {
showError(e.message);
}
});
</script>
</body>
</html>