1814 字
9 分钟
数学图像功能实现教程
功能预览
最终效果:
- ✅ 在 Markdown 中使用
mathgraph代码块 - ✅ 自动渲染交互式数学图形
- ✅ 滑动条实时调整参数
- ✅ 左图右参数布局
- ✅ 自动适配 Fuwari 主题
- ✅ 支持亮色/暗色模式
- ✅ 移动端响应式设计
NOTE所有代码都是AI敲的,所以相关问题不要问我
实现步骤
步骤 1: 下载 JSXGraph 库到本地
首先,我们需要将 JSXGraph 库下载到本地,避免远程调用可能遇到的网络问题。
# 创建目录mkdir -p public/jsxgraph
# 下载 JSXGraph 核心库curl -o public/jsxgraph/jsxgraphcore.js https://cdn.jsdelivr.net/npm/jsxgraph@1.8.0/distrib/jsxgraphcore.js
# 下载 JSXGraph 样式curl -o public/jsxgraph/jsxgraph.css https://cdn.jsdelivr.net/npm/jsxgraph@1.8.0/distrib/jsxgraph.css说明:
public/目录下的文件会被直接复制到构建输出- 使用本地文件避免依赖远程 CDN
- 确保部署到任何平台都能正常工作
步骤 2: 创建 Remark 插件
创建 src/plugins/remark-mathgraph.js 文件,用于解析 Markdown 中的 mathgraph 代码块。
import { visit } from "unist-util-visit";
/** * Remark plugin to detect and transform mathgraph code blocks. * Converts ```mathgraph code blocks to interactive math graph components. */export function remarkMathGraph() { return (tree) => { visit(tree, "code", (node) => { if (node.lang === "mathgraph") { const config = parseMathGraphConfig(node.value || ""); const configJson = JSON.stringify(config); const graphId = `mathgraph-${Math.random().toString(36).substr(2, 9)}`;
// 创建完整的 HTML 结构 node.data = { hName: "div", hProperties: { className: ["math-graph-container"], }, hChildren: [ { type: "element", tagName: "div", properties: { className: ["math-graph-wrapper"], }, children: [ { type: "element", tagName: "div", properties: { className: ["math-graph-canvas"], id: graphId, "data-config": configJson, }, children: [], }, ], }, ], }; } }); };}
function parseMathGraphConfig(content) { const config = { sliders: [], expressions: [], axis: true, grid: true, };
const lines = content.split("\n"); let currentSection = "";
for (const line of lines) { const trimmed = line.trim();
// 跳过空行和注释 if (!trimmed || trimmed.startsWith("#")) continue;
// 检测区块 if (trimmed === "sliders:") { currentSection = "sliders"; continue; } if (trimmed === "expressions:") { currentSection = "expressions"; continue; }
// 解析滑动条配置 if (currentSection === "sliders") { const sliderMatch = trimmed.match(/^(\w+):\s*\[([^\]]+)\]$/); if (sliderMatch) { const name = sliderMatch[1]; const values = sliderMatch[2].split(",").map((v) => parseFloat(v.trim())); if (values.length >= 3) { config.sliders.push({ name, min: values[0], value: values[1], max: values[2], step: values[3] || 0.1, }); } } }
// 解析表达式 if (currentSection === "expressions") { const exprMatch = trimmed.match(/^-\s*(.+)$/); if (exprMatch) { config.expressions.push(exprMatch[1].trim()); } }
// 解析其他配置 if (trimmed.startsWith("axis:")) { config.axis = trimmed.split(":")[1].trim() === "true"; } if (trimmed.startsWith("grid:")) { config.grid = trimmed.split(":")[1].trim() === "true"; } }
return config;}代码说明:
- 使用
unist-util-visit遍历 Markdown AST - 检测
lang === "mathgraph"的代码块 - 解析配置内容(滑动条、表达式等)
- 生成 HTML 结构,包含配置数据
步骤 3: 注册 Remark 插件
在 astro.config.mjs 中注册插件:
import { remarkMathGraph } from "./src/plugins/remark-mathgraph.js";
export default defineConfig({ // ... 其他配置 markdown: { remarkPlugins: [ // ... 其他插件 remarkMathGraph, ], },});步骤 4: 添加样式
创建 src/styles/mathgraph.css 文件:
/* MathGraph 样式 - 左图右参数布局 */
.math-graph-container { margin: 1.5rem 0; border-radius: 0.75rem; overflow: hidden; background: var(--card-bg); border: 1px solid var(--line-divider);}
/* 左图右参数布局 */.math-graph-layout { display: flex; flex-direction: row; gap: 0; min-height: 400px;}
.math-graph-canvas-wrapper { flex: 1; min-width: 0; position: relative;}
.math-graph-canvas { width: 100%; height: 400px; background: var(--page-bg);}
/* 参数控制面板 */.math-graph-controls { width: 280px; min-width: 280px; background: var(--btn-regular-bg); border-left: 1px solid var(--line-divider); display: flex; flex-direction: column;}
.math-graph-controls-title { padding: 1rem; font-size: 0.875rem; font-weight: 600; color: var(--btn-content); border-bottom: 1px solid var(--line-divider); text-align: center;}
/* 滑动条样式 */.math-slider-wrapper { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem; background: var(--card-bg); border-radius: 0.5rem; border: 1px solid var(--line-divider);}
.math-slider-label { font-size: 0.875rem; font-weight: 600; color: var(--btn-content); display: flex; justify-content: space-between;}
.math-slider-value { color: var(--primary);}
.math-slider-input { width: 100%; height: 6px; border-radius: 3px; background: var(--btn-plain-bg-hover); outline: none; -webkit-appearance: none; cursor: pointer;}
.math-slider-input::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--primary); cursor: pointer;}
/* 响应式设计 */@media (max-width: 768px) { .math-graph-layout { flex-direction: column; }
.math-graph-controls { width: 100%; border-left: none; border-top: 1px solid var(--line-divider); }}样式要点:
- 使用 Fuwari 的 CSS 变量(
--card-bg,--primary等) - 左图右参数布局(桌面端)
- 移动端自动切换为上下布局
步骤 5: 在 Layout 中引入样式和脚本
编辑 src/layouts/Layout.astro:
---// 在文件顶部引入样式import "../styles/mathgraph.css";---
<head> <!-- 其他 head 内容 -->
<!-- JSXGraph 库 --> <link rel="stylesheet" href="/jsxgraph/jsxgraph.css" /> <script src="/jsxgraph/jsxgraphcore.js" defer></script></head>步骤 6: 添加初始化脚本
在 src/layouts/Layout.astro 的 <script> 标签中添加初始化代码:
// ========== 数学图形初始化 ==========let mathGraphObserver = null;
function initMathGraphs() { const containers = document.querySelectorAll( '.math-graph-canvas[data-config]:not([data-initialized])' );
if (containers.length === 0) return;
// 检查 JSXGraph 是否已加载 if (!window.JXG) { waitForJSXGraph().then(initMathGraphs); return; }
containers.forEach((container) => { const configStr = container.getAttribute('data-config'); if (!configStr) return;
try { const config = JSON.parse(configStr); createMathGraph(container, config); container.setAttribute('data-initialized', 'true'); } catch (e) { console.error('MathGraph config parse error:', e); } });}
// 使用 MutationObserver 监听 DOM 变化function observeMathGraphs() { if (mathGraphObserver) { mathGraphObserver.disconnect(); }
initMathGraphs();
mathGraphObserver = new MutationObserver((mutations) => { let shouldInit = false;
mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node instanceof Element) { if (node.classList?.contains('math-graph-canvas') || node.querySelector?.('.math-graph-canvas')) { shouldInit = true; } } }); });
if (shouldInit) { setTimeout(initMathGraphs, 100); } });
mathGraphObserver.observe(document.body, { childList: true, subtree: true, });}
// 等待 JSXGraph 加载function waitForJSXGraph() { return new Promise((resolve) => { if (window.JXG) { resolve(); } else { const checkInterval = setInterval(() => { if (window.JXG) { clearInterval(checkInterval); resolve(); } }, 100);
setTimeout(() => { clearInterval(checkInterval); console.error('JSXGraph loading timeout'); }, 5000); } });}
// 创建数学图形async function createMathGraph(container, config) { const boardId = container.id; const JXG = window.JXG;
// 获取父容器 const wrapper = container.parentElement; if (!wrapper) return;
// 创建布局容器 const layoutContainer = document.createElement('div'); layoutContainer.className = 'math-graph-layout';
const graphContainer = document.createElement('div'); graphContainer.className = 'math-graph-canvas-wrapper';
const controlsContainer = document.createElement('div'); controlsContainer.className = 'math-graph-controls';
const controlsTitle = document.createElement('div'); controlsTitle.className = 'math-graph-controls-title'; controlsTitle.textContent = '参数调整'; controlsContainer.appendChild(controlsTitle);
const slidersContainer = document.createElement('div'); slidersContainer.className = 'math-graph-sliders-vertical'; controlsContainer.appendChild(slidersContainer);
// 移动容器到新布局 wrapper.removeChild(container); graphContainer.appendChild(container); layoutContainer.appendChild(graphContainer); layoutContainer.appendChild(controlsContainer); wrapper.appendChild(layoutContainer);
// 创建 JSXGraph 画板 const board = JXG.JSXGraph.initBoard(boardId, { boundingbox: [-10, 10, 10, -10], axis: config.axis !== false, grid: config.grid !== false, showCopyright: false, showNavigation: false, keepAspectRatio: false, });
const sliders = {};
// 创建滑动条 config.sliders.forEach((sliderConfig, index) => { const sliderWrapper = document.createElement('div'); sliderWrapper.className = 'math-slider-wrapper';
const labelRow = document.createElement('div'); labelRow.className = 'math-slider-label';
const labelText = document.createElement('span'); labelText.textContent = sliderConfig.name;
const valueDisplay = document.createElement('span'); valueDisplay.className = 'math-slider-value'; valueDisplay.textContent = sliderConfig.value.toFixed(2);
labelRow.appendChild(labelText); labelRow.appendChild(valueDisplay);
const slider = document.createElement('input'); slider.type = 'range'; slider.min = String(sliderConfig.min); slider.max = String(sliderConfig.max); slider.step = String(sliderConfig.step); slider.value = String(sliderConfig.value); slider.className = 'math-slider-input';
slider.addEventListener('input', () => { const val = parseFloat(slider.value); valueDisplay.textContent = val.toFixed(2); sliders[sliderConfig.name].setValue(val); board.update(); });
sliderWrapper.appendChild(labelRow); sliderWrapper.appendChild(slider); slidersContainer.appendChild(sliderWrapper);
// 创建 JSXGraph 内部滑动条 sliders[sliderConfig.name] = board.create('slider', [ [-8, -8 + index * 1.5], [-3, -8 + index * 1.5], [sliderConfig.min, sliderConfig.value, sliderConfig.max] ], { name: sliderConfig.name, visible: false, }); });
// 创建函数曲线 config.expressions.forEach((expr, index) => { try { const func = new Function( 'x', ...Object.keys(sliders), `with(Math) { return ${expr}; }` );
board.create('functiongraph', [ (x) => { const sliderValues = Object.keys(sliders).map(key => sliders[key].Value()); return func(x, ...sliderValues); } ], { strokeColor: getGraphColor(index), strokeWidth: 2, }); } catch (e) { console.error('Expression parse error:', expr, e); } });}
function getGraphColor(index) { const colors = [ '#3b82f6', '#e74c3c', '#27ae60', '#f39c12', '#9b59b6', '#1abc9c', ]; return colors[index % colors.length];}
// 初始化function initOnLoad() { waitForJSXGraph().then(() => { observeMathGraphs(); });}
document.addEventListener('DOMContentLoaded', initOnLoad);
if (document.readyState !== 'loading') { initOnLoad();}
// 支持 Swup 页面切换document.addEventListener('swup:contentReplaced', () => { setTimeout(initMathGraphs, 300);});
if (window?.swup?.hooks) { window.swup.hooks.on('page:view', () => { setTimeout(initMathGraphs, 300); });}关键点说明:
- MutationObserver: 监听 DOM 变化,自动初始化新添加的图形
- waitForJSXGraph: 等待 JSXGraph 库加载完成
- 防重复初始化: 使用
data-initialized标记 - Swup 兼容: 监听页面切换事件
使用方法
基本语法
在 Markdown 文件中使用:
```mathgraphsliders: 参数名: [最小值, 默认值, 最大值, 步长]expressions: - 数学表达式axis: true/falsegrid: true/false```支持的数学函数
- 基本运算:
+,-,*,/,^ - 三角函数:
sin,cos,tan,asin,acos,atan - 指数对数:
exp,log,log10,sqrt - 其他:
abs,floor,ceil,round
进阶定制
自定义颜色
修改 getGraphColor 函数:
function getGraphColor(index) { const colors = [ '#your-color-1', '#your-color-2', // ... ]; return colors[index % colors.length];}调整画板范围
修改 boundingbox 参数:
const board = JXG.JSXGraph.initBoard(boardId, { boundingbox: [-5, 5, 5, -5], // [x_min, y_max, x_max, y_min] // ...});添加更多配置选项
在 parseMathGraphConfig 函数中添加新的配置项:
if (trimmed.startsWith("boundingbox:")) { const values = trimmed.split(":")[1].trim().split(",").map(Number); config.boundingbox = values;}常见问题
Q: 图形不显示?
A: 检查以下几点:
- JSXGraph 文件是否正确下载到
public/jsxgraph/ - 浏览器控制台是否有错误
- Markdown 语法是否正确
Q: 需要手动刷新才显示?
A: 这是初始化时机问题,确保:
- 使用了 MutationObserver
- 等待 JSXGraph 加载完成
- 监听了 Swup 页面切换事件