CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
POSTS / P-246 · 开发

开发一个函数绘图工具:从需求到单文件落地

开发一个函数绘图工具:从需求到单文件落地

站上有个工具:函数绘图室 →。一个单文件、零依赖、开箱即用的函数绘图器。这篇记一下它为什么存在、怎么做出来的。

起点:一个具体的场景

市面上绘图工具不少,为什么自己写?因为需求很具体:给孩子讲函数。这意味着:

  • 打开就能用,不注册、不装 App、不看广告
  • 小学到高中的函数都要画:从 y = 2xy = sin(x)/x
  • 输入方式要像作业本:^|x|、分段函数 { },而不是程序员的 Math.pow
  • 出错要好好说话,“第 3 个字符少个括号”比一屏红色堆栈有用得多

把这些列完,发现现成工具要么太重(专业数学软件),要么太轻(只能画多项式)。那就自己写。

技术决策:单文件,零依赖

整个工具是一个 index.html,没有构建步骤、没有框架、没有 CDN。这不是为了炫技,是三个务实的好处:

  1. 分发即复制 —— 丢到任何静态目录就能跑,我的博客 public/tools/ 下放一份就上线了
  2. 没有供应链 —— 零 node_modules,十年后打开还能跑
  3. 强制约束 —— 单文件逼着你把架构想清楚,什么东西该放哪

代价当然有:没有组件化、没有热更新、代码全靠自觉分段注释。但对一个两千行的工具,这个交换是划算的。项目的形态应该匹配它的生命周期——这工具大概会活很久,但不会长很大。

核心:手写表达式解析器

整个工具的心脏是一个手写的 tokenizer + 递归下降解析器,把用户输入编译成 AST,再求值绘图。为什么不直接 eval

  • 安全 —— eval 是把用户输入当代码执行,XSS 敞门
  • 体验 —— 解析器能定位错误:“括号不匹配”能标出是第几个字符,eval 只能抛一句 meaningless 的异常
  • 能力 —— 分段函数 {x<0: -x, 其他: x}、比较运算、绝对值 |x|、自定义函数复用,这些语法全要自己定义,eval 给不了

实现上是很教科书的三层:

输入字符串 → tokenize → 递归下降解析 → AST → 闭包求值

tokenize 负责把 × ÷ ² ³ π 这些数学符号统一成 ASCII;解析层处理运算符优先级(^ 右结合、隐式乘法 2x2(x+1));求值层把 AST 编译成原生闭包,比每次遍历 AST 快几倍——缩放绘图时每帧要算几千个点,这个优化是值得的。

绘图:Canvas 与”数学坐标系”

Canvas 原生是像素坐标系,y 轴向下。绘图层做的所有事就是一套干净的换算:

屏幕像素 ⇄ 数学坐标

围绕它构建:自适应网格(刻度永远是”好看”的 1-2-5 序列)、悬停读数(鼠标吸附最近的曲线点)、双指缩放与拖拽(移动端同样可用)。

一个容易忽略的细节是不连续函数的处理——tan(x)1/x 在渐近线附近会把两个不相连的分支连成一条竖线。解法很朴素:相邻两点 y 跳变超过阈值就断笔。朴素,但有效。

交互细节:让”解”看得见

工具里我个人最喜欢的功能是「解 · 浏览器」:画两条曲线,交点自动标出来;画一条,与 x 轴的交点(零点)自动标出来

这把”方程的解”从抽象概念变成了看得见的东西——方程 f(x) = g(x) 的解就是两线相交处的 x 坐标。数学老师讲一节课的事,鼠标悬停一下就懂了。

再加上数值表(列表描点,可导出 CSV)、小学/初中/高中分级示例库,一个”给孩子讲函数”的闭环就齐了。

复盘

  • 需求具体,方案才不会跑偏。 “给孩子讲函数”五个字,砍掉了 90% 的功能蔓延
  • 单文件不是反工程,是匹配生命周期的工程。 不是所有项目都值得一个脚手架
  • 手写解析器没有想象中难。 递归下降 + 运算符优先级,一个周末的量,换来完全可控的错误提示
  • 最费时间的不是核心算法,是交互。 缩放手感、悬停吸附、错误定位,这些”体感”占了代码的一半

工具在这里:函数绘图室 →,欢迎拿去给孩子讲一讲什么是函数。

← 返回文章列表