【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 中本地绘制。
二、函数图像的数据结构
画板中函数图像和几何模型分开存储:
@StatefunctionGraphs: FuncGraph[] = []@StatefunctionInput: string =''@StategraphRange: 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 ==='')returnconstexprLower:string= expr.replace(/\s+/g,'').toLowerCase() ctx.strokeStyle= color ctx.lineWidth= lineWidth ctx.beginPath()letstarted:boolean=falseconststep:number= (this.graphRange.xMax-this.graphRange.xMin) /this.canvasWidth*0.5for(letmathX:number=this.graphRange.xMin; mathX <=this.graphRange.xMax; mathX += step) {constmathY:number=this.evaluateExpr(exprLower, mathX)if(isFinite(mathY)) {constpt: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^2sin(x)sqrt(x)ln(x)abs(x)程序需要把它转换成运行时能计算的形式:
evaluateExpr(expr:string,x:number):number{try{lete: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(`)constfn:Function=newFunction(`"use strict"; return (${e})`)returnfn()asnumber}catch{returnNaN } }这一段体现了函数绘制的核心思路:
x替换成当前采样点;^替换成幂运算**;sin/cos/tan映射到Math;log/ln/sqrt/abs/exp映射到标准数学函数;- 计算失败则返回
NaN。
注意:当前画板函数绘制使用
new Function来快速验证表达式。项目中的科学计算器普通算术部分则手写了解析器,没有使用new Function。如果未来要强化安全性,可以把画板表达式也改造成同一套白名单解析器。
六、为什么要在本地绘制,而不是 AI 识图?
函数学习有两种路线:
- 拍照识别题目,再让 AI 画图;
- 用户输入表达式,端侧直接绘图。
数学视界选择后者。原因很明确:
- 🔐 不需要相机权限;
- 🚫 不上传试卷图片;
- ⚡ 本地计算,响应快;
- 🧠 学生能理解表达式和图像之间的对应关系;
- 📦 表达式可以收藏和复用。
对学习类应用来说,“自己输入,自己观察变化”比“拍照等答案”更有学习价值。
七、图例显示:让多函数对比更清晰
当函数有标签时,会绘制一个小图例:
if(label!=='') { ctx.fillStyle=colorctx.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 识图和图片上传,保护学习隐私。
数学学习应用的端侧能力,不一定非要接大模型。像函数图像绘制这种“输入表达式,立即生成可视化结果”的能力,本身就是非常实用的端侧智能。🚀
