【OpenHarmony/HarmonyOs 】函数图像绘制实践:ArkTS 表达式解析与 Canvas 曲线采样
【OpenHarmony/HarmonyOs 】函数图像绘制实践:ArkTS 表达式解析与 Canvas 曲线采样
项目类型:OpenHarmony / HarmonyOS ArkTS 数学学习应用
项目名称:数学视界
对应主题:端侧 AI、全新视觉与交互体验、禁止 AI 识图
关键词:函数图像、Canvas、表达式解析、端侧计算、ArkTS、数学可视化 📈
一、为什么函数图像适合写成一篇独立文章?
在数学学习 App 中,函数图像是一个非常典型的“端侧智能”场景。它不需要拍照、不需要 AI 识图、不需要上传数据,只要用户输入函数表达式,应用就可以在本地完成解析、采样和绘制。
数学视界的 CanvasBoard.ets 中已经支持函数图像绘制:
- 用户输入表达式,比如
sin(x)、x^2、sqrt(x); - 程序把表达式转换为可计算形式;
- 在当前坐标范围内对 x 进行采样;
- 计算每个采样点的 y;
- 把数学坐标转换成 Canvas 坐标;
- 连成平滑曲线。
这篇文章就围绕这个流程展开,重点写函数图像如何在 ArkTS 中本地绘制。
二、函数图像的数据结构
画板中函数图像和几何模型分开存储:
@State functionGraphs: FuncGraph[] = []
@State functionInput: string = ''
@State graphRange: DrawGraphRange = {
xMin: -10,
xMax: 10,
yMin: -10,
yMax: 10
}
这样设计有两个好处:
- 函数图像可以和圆、椭圆、双曲线同时存在;
- 函数表达式是结构化数据,可以收藏、重绘、分享。
对于数学学习场景来说,保存表达式比保存截图更有意义。截图只能看,表达式可以继续编辑。
三、绘制入口:遍历所有函数图像
画板中有一个统一绘制入口:
drawAllFunctionGraphs(ctx: CanvasRenderingContext2D): void {
for (let i: number = 0; i < this.functionGraphs.length; i++) {
const graph: FuncGraph = this.functionGraphs[i]
this.drawSingleFunction(
ctx,
graph.expr,
graph.color,
graph.label,
graph.lineWidth ?? 2
)
}
}
这里可以看到,每条函数图像至少需要:
expr:函数表达式;color:曲线颜色;label:图例标签;lineWidth:线宽。
这就让多个函数同屏对比成为可能,比如:
y = xy = x^2y = sin(x)y = log(x)
学生可以很直观地观察不同函数的形状差异。
四、核心绘制逻辑:采样 x,计算 y,再连线
单条函数图像的绘制逻辑如下:
drawSingleFunction(
ctx: CanvasRenderingContext2D,
expr: string,
color: string,
label: string,
lineWidth: number
): void {
if (expr === '') return
const exprLower: string = expr.replace(/\s+/g, '').toLowerCase()
ctx.strokeStyle = color
ctx.lineWidth = lineWidth
ctx.beginPath()
let started: boolean = false
const step: number =
(this.graphRange.xMax - this.graphRange.xMin) /
this.canvasWidth * 0.5
for (let mathX: number = this.graphRange.xMin; mathX <= this.graphRange.xMax; mathX += step) {
const mathY: number = this.evaluateExpr(exprLower, mathX)
if (isFinite(mathY)) {
const pt: DrawPoint = this.mathToCanvas(mathX, mathY)
if (!started) {
ctx.moveTo(pt.x, pt.y)
started = true
} else {
ctx.lineTo(pt.x, pt.y)
}
} else {
started = false
}
}
ctx.stroke()
}
这段代码的重点有三个:
step根据坐标范围和画布宽度动态计算;- 每个
mathX都调用evaluateExpr()得到mathY; - 如果结果不是有限数,就断开曲线,避免把不连续点硬连起来。
例如 log(x) 在 x <= 0 时没有实数结果,此时 isFinite(mathY) 会避免绘制错误线段。
五、表达式求值:把数学写法转换成 JS/ArkTS 可计算写法
用户输入的表达式通常是数学写法,比如:
x^2
sin(x)
sqrt(x)
ln(x)
abs(x)
程序需要把它转换成运行时能计算的形式:
evaluateExpr(expr: string, x: number): number {
try {
let e: string = expr.replace(/x/g, `(${x})`)
e = e.replace(/\^/g, '**')
e = e.replace(/pi/g, `${Math.PI}`)
e = e.replace(/e(?![x])/g, `${Math.E}`)
e = e.replace(/sin\(/g, `Math.sin(`)
e = e.replace(/cos\(/g, `Math.cos(`)
e = e.replace(/tan\(/g, `Math.tan(`)
e = e.replace(/sqrt\(/g, `Math.sqrt(`)
e = e.replace(/log\(/g, `Math.log10(`)
e = e.replace(/ln\(/g, `Math.log(`)
e = e.replace(/abs\(/g, `Math.abs(`)
e = e.replace(/exp\(/g, `Math.exp(`)
const fn: Function = new Function(`"use strict"; return (${e})`)
return fn() as number
} catch {
return NaN
}
}
这一段体现了函数绘制的核心思路:
x替换成当前采样点;^替换成幂运算**;sin/cos/tan映射到Math;log/ln/sqrt/abs/exp映射到标准数学函数;- 计算失败则返回
NaN。
注意:当前画板函数绘制使用
new Function来快速验证表达式。项目中的科学计算器普通算术部分则手写了解析器,没有使用new Function。如果未来要强化安全性,可以把画板表达式也改造成同一套白名单解析器。
六、为什么要在本地绘制,而不是 AI 识图?
函数学习有两种路线:
- 拍照识别题目,再让 AI 画图;
- 用户输入表达式,端侧直接绘图。
数学视界选择后者。原因很明确:
- 🔐 不需要相机权限;
- 🚫 不上传试卷图片;
- ⚡ 本地计算,响应快;
- 🧠 学生能理解表达式和图像之间的对应关系;
- 📦 表达式可以收藏和复用。
对学习类应用来说,“自己输入,自己观察变化”比“拍照等答案”更有学习价值。
七、图例显示:让多函数对比更清晰
当函数有标签时,会绘制一个小图例:
if (label !== '') {
ctx.fillStyle = color
ctx.fillRect(10, 10, 20, 3)
ctx.fillStyle = this.getColor('#333333', '#EEEEEE')
ctx.font = '11px sans-serif'
ctx.textAlign = 'left'
ctx.fillText(label, 36, 16)
}
这个小细节很适合多函数对比。例如学生同时画:
y = xy = 2xy = x + 2
图例可以帮助他们理解斜率、截距变化对图像的影响。
八、深色模式下的可读性
函数图像不是普通 UI,它有坐标轴、网格、标签、曲线。如果只是简单把背景变黑,很容易出现曲线或文字看不清的问题。
项目中通过 getColor() 处理深浅色:
getColor(lightColor: string, darkColor: string): string {
return this.isDarkMode ? darkColor : lightColor
}
绘制坐标轴时也会切换颜色:
ctx.strokeStyle = this.getColor('#333333', '#EEEEEE')
ctx.fillStyle = this.getColor('#555555', '#CCCCCC')
这样在深色背景下,坐标轴、刻度、标签仍然清晰。
九、可以继续优化的方向
当前函数图像绘制已经能满足基础学习需求,但还可以继续增强:
- 表达式解析改为安全白名单解析器;
- 支持隐式乘法,比如
2x自动识别为2*x; - 支持分段函数;
- 支持导数图像;
- 支持函数零点、极值点标注;
- 支持图像交点求解;
- 支持函数收藏后重新加载。
这些能力都不需要云端 AI,完全可以在端侧逐步实现。
十、总结
这篇文章围绕“函数图像绘制”展开,和另一个物理项目里的 Canvas 动画文章有明显区别。它更关注数学表达式、采样、坐标映射和本地计算。
核心实现包括:
- 📈 用
functionGraphs保存函数表达式; - 🧮 用
evaluateExpr()把表达式转换成本地可计算结果; - 🧭 用
mathToCanvas()将数学坐标映射到屏幕; - ✂️ 用
isFinite()处理不连续点; - 🌙 用深色模式颜色映射保证坐标轴和标签可读;
- 🔐 避免 AI 识图和图片上传,保护学习隐私。
数学学习应用的端侧能力,不一定非要接大模型。像函数图像绘制这种“输入表达式,立即生成可视化结果”的能力,本身就是非常实用的端侧智能。🚀

更多推荐


所有评论(0)