数学题目动画生成器
概述
将用户输入的数学题目(文本描述或图片)转化为一个完整的项目包,包含:
index.html— LaTeX 题目描述 + JSXGraph 交互动画 + 详细解答animation.js— 独立的动画逻辑脚本css/和js/、imgs/— 从math-animation-skills项目中的assets文件夹中复制到本地依赖文件
所有文件使用本地相对路径引用(css/、js/、、imgs/、animation.js),不依赖 CDN(MathJax LaTeX 渲染除外)。
工作流程
Step 1: 题目分析
从用户输入中提取:
- 题目文字(如果是图片,读取图片内容提取文字和数学表达式)
- 题目类型:轨迹问题 / 切线问题 / 最值问题 / 几何构造 / 函数变换 / 圆锥曲线 / 数列 / 其他
- 已知条件:坐标系、方程、点的坐标、几何约束等
- 求解目标
Step 2: 数学求解
对题目进行完整求解:
- 写出详细的推导步骤
- 计算关键数值(坐标、斜率、方程参数等)
- 得到最终答案
- 所有数学表达式使用 LaTeX 格式(
$...$行内,\\(...\\)行内,$$...$$块级)
Step 3: 动画设计
根据题目类型设计动画步骤(3-6 步为宜):
动画设计原则:
- 每步对应解题过程中的一个关键操作
- 步骤之间通过
setTimeout自动链式触发 - 每步开始时显示该步骤的文字说明(红色文字,
col2) - 最终步展示结论
注意:动画中呈现的文字不能重叠。
典型动画步骤模板:
| 题型 | 步骤示例 | |------|---------| | 轨迹问题 | 1.展示基本图形 + 关键点 → 2.放置动点 → 3.移动动点展示变化 → 4.展示轨迹曲线和结论 | | 几何构造 | 1.已知图形元素 → 2.逐步构造辅助线/点 → 3.展示结论关系 | | 函数图像 | 1.坐标系 + 函数图像 → 2.参数 slider 变化 → 3.标注特殊点(极值/零点) | | 切线问题 | 1.曲线 + 外点 → 2.切线的形成 → 3.切点标注 → 4.展示几何关系 | | 圆锥曲线 | 1.椭圆/双曲线/抛物线 → 2.焦点/准线标注 → 3.参数变化展示 |
Step 4: 编写动画脚本(animation.js)
生成独立的 animation.js 文件,遵循 references/animation-patterns.md 中的模式。
4.1 animation.js 基本结构
// animation.js - 由 math-animation-generator 自动生成
var board, col = "black", col2 = "red", col3 = "#1d04e1ff", col4 = "#9605e3ff";
var time1 = 1, finish = 1, play = 1;
var text, P, A, B, angle, Interval, cartoon_set;
$(document).ready(function() {
initial();
window.addEventListener('orientationchange', function(event) {
if (window.orientation == 180 || window.orientation == 0) {
var width = $(window).width();
if (board) board.resizeContainer(width - 20);
if (typeof reset === 'function') reset();
}
if (window.orientation == 90 || window.orientation == -90) {
var width = $(window).width();
if (board) board.resizeContainer(width - 20);
}
});
});
$(document).on("click", "#jxgbox", function(e) { e.stopPropagation(); });
// ====== 初始化画板 ======
function initial() {
board = JXG.JSXGraph.initBoard('jxgbox', {
boundingbox: [x_min, y_max, x_max, y_min],
keepaspectratio: true,
axis: false,
Grids:false,
showNavigation: false
});
board.options.line.strokeColor = 'black';
// 坐标轴标记
var box_range = board.getBoundingBox();
board.create('text', [function() { return box_range[2] - 0.8; }, 0.3, 'x'], { fixed: true, fontsize: 14, strokeColor: col });
board.create('text', [0.2, function() { return box_range[1] - 0.2; }, 'y'], { fixed: true, fontsize: 14, strokeColor: col });
X= board.create('arrow',[[-2.2,0],[18.2,0]],{strokeColor:col,strokeWidth:2,fixed:true});
Y= board.create('arrow',[[0,-10.8],[0,15]],{strokeColor:col,strokeWidth:2,fixed:true});
// ★★★ 在此创建初始图形元素(曲线、点、线段等) ★★★
// 隐藏的 slider 驱动动画
a = board.create('slider', [[sx1, sy], [sx2, sy], [start, start, end]], {
size: 2, withLabel: true, name: 'a'
});
a.hideElement();
}
// ====== 动画函数(每个步骤对应一个 AnimationN) ======
function Animation1() {
play = 0;
$("#catroong_config1").attr("playing_cartoon", "1");
cartoon_time = $("#catroong_config1").attr("cartoon_1");
playing_time = $("#catroong_config1").attr("playing_time");
animation_time = cartoon_time - parseInt(playing_time);
a.moveTo([target_value, slider_y], animation_time, {
callback: function() {
clearInterval(Interval);
$("#catroong_config1").attr("is_clear2", "0");
$("#catroong_config1").attr("playing_time", "0");
// 显示文字说明
CheckTXT();
text = board.create('text', [tx, ty, function() { return "步骤1的说明"; }], {
fixed: true, fontsize: 16, strokeColor: col2
});
$("#catroong_config1").attr("is_text", "0");
// ★★★ 创建/更新图形元素 ★★★
// 链式触发下一步
$("#catroong_config1").attr("playing_cartoon", "2");
$("#catroong_config1").attr("is_clear", "1");
cartoon_set = setTimeout(function() {
clearTimeout(cartoon_set);
$("#catroong_config1").attr("is_clear", "0");
Animation2();
CountTime();
}, 2500);
}
});
}
function Animation2() { /* 类似结构 */ }
function Animation3() { /* 类似结构 */ }
function Animation4() {
// 最后一步...
a.moveTo([final_value, slider_y], animation_time, {
callback: function() {
// ... 清理和显示结论 ...
finish = 0;
$("#catroong_config1").attr("playing_cartoon", "1");
$("#catroong_config1").attr("is_text", "1");
$("#catroong_config1").attr("is_clear", "0");
$("#start").css("display", "");
$("#stop").css("display", "none");
}
});
}
// ====== 播放控制 ======
$(document).on("click", "#start", function() {
var pc = $("#catroong_config1").attr("playing_cartoon");
var fn = "Animation" + pc + "()";
if (finish == "0") {
JXG.JSXGraph.freeBoard(board);
initial();
board.update();
finish = 1;
angle = "";
}
if (time1 == "1") {
$("#loading").css("display", "");
$("#jxgbox").css("display", "none");
$("#regist").attr("disabled", "true");
setTimeout(function() {
$("#loading").hide();
$("#jxgbox").show();
time1 = 2;
$("#regist").removeAttr("disabled");
}, 1000);
cartoon_set = setTimeout(function() {
clearTimeout(cartoon_set);
$("#catroong_config1").attr("is_clear", "0");
eval(fn);
if (play == 0) CountTime();
}, 2000);
} else {
eval(fn);
if (play == 0) CountTime();
}
$("#start").css("display", "none");
$("#stop").css("display", "");
});
$(document).on("click", "#stop", function() {
$("#start").css("display", "");
$("#stop").css("display", "none");
if ($("#catroong_config1").attr("is_clear") == "1") {
clearTimeout(cartoon_set);
$("#catroong_config1").attr("is_clear", "0");
}
if ($("#catroong_config1").attr("is_clear2") == "1") {
clearInterval(Interval);
$("#catroong_config1").attr("is_clear2", "0");
}
board.stopAllAnimation();
});
$(document).on("click", "#regist", function() {
if (typeof($("#regist").attr("disabled")) == "undefined") {
reset();
}
angle = "";
});
// ====== 辅助函数 ======
function CountTime() {
$("#catroong_config1").attr("is_clear2", "1");
Interval = setInterval(function() {
var pt = $("#catroong_config1").attr("playing_time");
$("#catroong_config1").attr("playing_time", parseInt(pt) + 100);
}, 100);
}
function CheckTXT() {
if ($("#catroong_config1").attr("is_text") == "0") {
text.remove();
$("#catroong_config1").attr("is_text", "1");
}
}
function Show_Answer() { $("#ShowAnswer").hide(); $("#HideAnswer").show(); $("#answer").show(); }
function Hide_Answer() { $("#ShowAnswer").show(); $("#HideAnswer").hide(); $("#answer").hide(); }
function getLength(a, b) {
return Math.abs(Math.sqrt(Math.pow(a.X() - b.X(), 2) + Math.pow(a.Y() - b.Y(), 2)));
}
function getAngle(a, b, c) {
return Math.acos((Math.pow(getLength(a, b), 2) + Math.pow(getLength(b, c), 2) - Math.pow(getLength(a, c), 2)) / (2 * getLength(a, b) * getLength(b, c))) / Math.PI * 180;
}
function changeTwoDecimal(x) {
var f_x = parseFloat(x);
if (isNaN(f_x)) return "";
return Math.round(x * 100) / 100;
}
function reset() {
if ($("#catroong_config1").attr("is_clear") == "1") {
clearTimeout(cartoon_set);
$("#catroong_config1").attr("is_clear", "0");
}
if ($("#catroong_config1").attr("is_clear2") == "1") {
clearInterval(Interval);
$("#catroong_config1").attr("is_clear2", "0");
}
board.stopAllAnimation();
$("#catroong_config1").attr("playing_cartoon", "1");
$("#catroong_config1").attr("is_text", "1");
$("#catroong_config1").attr("is_clear", "0");
$("#catroong_config1").attr("playing_time", "0");
JXG.JSXGraph.freeBoard(board);
initial();
board.update();
finish = 1;
$("#start").css("display", "");
$("#stop").css("display", "none");
}
Step 5: 组装项目包
生成的项目包结构如下:
project-name/
├── index.html # 主页面
├── animation.js # 动画脚本
├── imgs/ # 从项目复制的图片资源
│ ├──start.png
│ ├──stop.png
│ ├──reset.png
├── css/ # 从项目复制的 CSS
│ ├── bootstrap.min.css
│ ├── font-awesome.min.css
│ ├── ionicons.min.css
│ ├── AdminLTE2.css
│ ├── font-awesome.min.css
│ └── Math.css
└── js/ # 从项目复制的 JS
├── jquery.min.js
├── bootstrap.min.js
├── Math.js
├── jsxgraphcore.js
└── AdminLTE/
└── apps.js
操作步骤:
-
创建输出目录(在工作目录下创建,如
E:\AI-agent\math-animation-skills\generated\<题目名>\) -
复制依赖文件 — 运行脚本:
python scripts/copy_deps.py <输出目录路径>或手动从
E:\AI-agent\math-animation-skills\css\和E:\AI-agent\math-animation-skills\js\复制所需文件到输出目录的对应位置。 -
生成 animation.js — 写入独立的动画 JavaScript 文件
-
生成 index.html — 读取
assets/template.html,替换占位符:| 占位符 | 替换内容 | 示例 | |--------|---------|------| |
{{TITLE}}| 页面标题 |《椭圆中的定值与轨迹方程问题》| |{{PROBLEM_STATEMENT}}| Tab1 题目描述(LaTeX,<p>包裹) |<p>已知椭圆\(C:\frac{x^2}{9}+\frac{y^2}{4}=1\)...</p>| |{{CONCLUSION}}| 动画结论(LaTeX) |<p>\(P\)轨迹是半径为\(\sqrt{13}\)的圆,方程为\(x^2+y^2=13\)</p>| |{{FULL_PROBLEM}}| Tab2 完整题目 | 同 PROBLEM_STATEMENT 或更详细版本 | |{{SOLUTION}}| 详细解答(LaTeX,<p>分步) |<p>解:(Ⅰ)可知\(c=\sqrt{5}\)...</p>| |{{CARTOON_TIMES}}| 动画时长配置属性 |cartoon_1="800" cartoon_2="400" cartoon_3="2000" cartoon_4="11000"| -
验证目录结构 — 确认所有文件就位
Step 6: 预览
- 确认所有 LaTeX 表达式语法正确
- 确认 JSXGraph 代码逻辑正确
- 确认动画步骤数量与
cartoon_N属性数量匹配 - 用
preview_url预览生成的 index.html
重要注意事项
坐标系设置
boundingbox: [x_min, y_max, x_max, y_min] 决定了可见区域。根据题目需要合理设置,确保所有图形元素都在可视范围内。
Slider 作为动画引擎
- Slider 始终隐藏(
a.hideElement()) - Slider 值变化驱动所有关联点坐标变化
- 动画持续时间合理(300ms-15000ms)
颜色约定
| 用途 | 颜色 |
|------|------|
| 坐标轴 | col = "black" |
| 文字说明/高亮 | col2 = "red" |
| 主图形(椭圆/曲线等) | col3 = "#1d04e1ff" |
| 辅助线 | col4 = "#9605e3ff" |
LaTeX 转义
- 在 HTML body 中使用
$...$ - 不能使用
\(...\)单反斜杠(会被 HTML 解析破坏)
数学求解准确性
- 解析几何题目必须正确计算交点、切点、斜率等
- 轨迹方程推导要有清晰的数学逻辑
- 特殊情形(垂直于 x 轴、k 不存在等)必须考虑
依赖文件路径
- HTML 使用
css/和js/引用(因为 index.html 在项目根目录,css/js 在平级目录) - animation.js 使用
animation.js(同一目录) - AdminLTE JS 文件名为
apps.js(不是app.js),HTML 中引用js/AdminLTE/apps.js
icon/ 缺失处理
原项目 icon/ 目录不存在。模板使用内联 SVG 图标替代播放/停止/重置按钮:
- 播放按钮 → 蓝色圆形 + 白色三角形
- 停止按钮 → 红色圆形 + 白色矩形
- 重置按钮 → 灰色圆形 + 白色弧形箭头
参考资源
references/animation-patterns.md:JSXGraph 动画模式和 API 详解references/html-structure.md:HTML 页面结构与依赖说明assets/template.html:完整 HTML 模板(本地引用版本)assets/css.. 、assets/js 、assets/imgs:本地的css和js、imgs(本地引用)scripts/copy_deps.py:自动复制 CSS/JS 依赖到输出目录的 Python 脚本
示例用法
用户输入 1(文本):
已知椭圆 C: x²/9 + y²/4 = 1,若动点 P(x₀, y₀) 为椭圆 C 外一点,且点 P 到椭圆 C 的两条切线相互垂直,求点 P 的轨迹。
输出项目包:
generated/ellipse-tangent-trajectory/
├── index.html ← 双Tab页面(动画解析 + 题目解答)
├── animation.js ← 4步动画(取P点→做垂线→相切→轨迹圆)
├── css/ ← 5个CSS文件(bootstrap, font-awesome, ionicons, AdminLTE2, Math.css)
└── js/ ← 5个JS文件(jquery, bootstrap, Math.js, jsxgraphcore, AdminLTE/apps.js)
用户输入 2(图片):
[一张包含数学题目的截图]
处理: 先用视觉能力读取图片中的题目文字和数学公式,然后按上述流程生成完整项目包。
微信扫一扫