【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:booleanfalseconststep:number (this.graphRange.xMax-this.graphRange.xMin) /this.canvasWidth*0.5for(letmathX:numberthis.graphRange.xMin; mathX this.graphRange.xMax; mathX step) {constmathY:numberthis.evaluateExpr(exprLower, mathX)if(isFinite(mathY)) {constpt:DrawPointthis.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:FunctionnewFunction(use strict; return (${e}))returnfn()asnumber}catch{returnNaN } }这一段体现了函数绘制的核心思路x替换成当前采样点^替换成幂运算**sin/cos/tan映射到Mathlog/ln/sqrt/abs/exp映射到标准数学函数计算失败则返回NaN。注意当前画板函数绘制使用new Function来快速验证表达式。项目中的科学计算器普通算术部分则手写了解析器没有使用new Function。如果未来要强化安全性可以把画板表达式也改造成同一套白名单解析器。六、为什么要在本地绘制而不是 AI 识图函数学习有两种路线拍照识别题目再让 AI 画图用户输入表达式端侧直接绘图。数学视界选择后者。原因很明确 不需要相机权限 不上传试卷图片⚡ 本地计算响应快 学生能理解表达式和图像之间的对应关系 表达式可以收藏和复用。对学习类应用来说“自己输入自己观察变化”比“拍照等答案”更有学习价值。七、图例显示让多函数对比更清晰当函数有标签时会绘制一个小图例if(label!) { ctx.fillStylecolorctx.fillRect(10,10,20,3) ctx.fillStyle this.getColor(#333333,#EEEEEE) ctx.font11px sans-serifctx.textAlignleftctx.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 识图和图片上传保护学习隐私。数学学习应用的端侧能力不一定非要接大模型。像函数图像绘制这种“输入表达式立即生成可视化结果”的能力本身就是非常实用的端侧智能。