平滑轨迹点动画网页


平滑轨迹点动画

功能介绍

此网页可以研究函数曲线在经过坐标变换后的轨迹,暂时没有画上平面直角坐标系或极坐标系,因此使用时需要自行添加。在文章结尾会奉上源代码,欢迎大家修改。
下面是页面展示:
在这里插入图片描述

支持的数学函数

  • 基本函数:sincostansqrtpow
  • 常量:pie

优化内容

在原有代码基础上进行了以下优化:

绘图优化
  1. 使用 二次贝塞尔曲线quadraticCurveTo)代替直线。
  2. 增加采样点数量(从 100 增加到 200)。
  3. 优化线条连接处的显示效果。
动画效果优化
  1. 使用 进度控制 替代帧计数。
  2. 增加点的 光晕效果
  3. 添加平滑的 轨迹渐现效果
交互改进
  1. 添加 清除画布 按钮。
  2. 优化动画重启逻辑。
  3. 改进错误提示显示。
视觉效果提升
  1. 线条使用圆角端点(lineCap: 'round')。
  2. 运动点添加发光效果。
  3. 轨迹线带有透明度,视觉效果更柔和。

使用方法

  1. 输入轨迹方程:如 [sin(t), cos(t)]
  2. 输入变换方程:如 [x + 1, y * 2]
  3. 点击 “开始动画” 观看平滑动画效果。
  4. 可随时点击 “清除画布” 重新开始。

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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值