博客导航栏里的 Calculator(/calculators/standard/)是一个纯浏览器端运行的进阶计算器。它最初是四个选项卡挤在一个页面里,后来随着工具页体系(见 Tools)的建立被拆成了独立子页面——每个工具一个网址,更好分享、更好搜索:
- Calculator → /calculators/standard/(科学计算器)
- Graph → /calculators/graph/(函数绘图),另有 /calculators/graph3d/(三维曲面,同样只用 Canvas 2D)
- Units → 独立成 11 个换算页 /converters/length/ 等(长度、重量、温度、面积、体积、速度、时间、数据大小、压强、功率、能量)
- Stats → 并入 /calculators/descriptive-statistics/(描述统计与线性回归)
拆分前的旧地址 /calculator/ 现已下线。
一个注册表,135 个工具
这次拆分顺带建起了整套工具页体系(入口在导航栏的 Tools,已发展为 135 个工具、12 个分类):
| 分类 | 内容 |
|---|---|
| Calculators(32) | 科学计算器、函数绘图、三维曲面、描述统计、置信区间、假设检验、方差分析,及整套计量经济学工具(线性回归/稳健标准误、回归诊断、分位数回归、混合效应、Logit/Probit、计数回归、平稳性检验、ARIMA 预测、VAR 协整、卡尔曼滤波) |
| Converters(13) | 长度、重量、温度、面积、体积、速度、时间、数据、压强、功率、能量,及新增的燃油消耗与角度——双向换算、一键交换 |
| Finance(24) | 复利(含月定投、逐年明细)、等额本息月供与摊销表、房贷(含税险 HOA)、提前还款、真实年化与分期 IRR、通胀购买力、储蓄目标、车贷、FIRE、个税、年薪换算、ROI、折扣、买房 vs 租房 |
| Daily(4) | 日期计算、年龄、BMI、时区换算 |
| Dev Tools(28) | 密码/UUID/随机数生成器、二维码、进制转换、Unix 时间戳、Cron 表达式、正则测试、文本 diff、SHA-256 哈希、字数与字符统计、JSON/SQL/YAML/XML 格式化、JWT 解码、URL 解析、cURL 转代码、Markdown 预览、Base64 |
| Text(13) | 大小写转换、字符/字数统计、CSV↔JSON、HTML 实体、JSON diff、行整理、Markdown 表格对齐与预览、罗马数字、文本 diff、网址邮箱提取、UA 解析 |
| Office(3) | PDF 工具箱(合并/拆分/水印等)、Excel 分析、文档 OCR |
| Security(5) | 浏览器指纹、CIDR 计算、哈希生成、JWT 解码、密码生成 |
| Media(3) | 图片滤镜实验室、无损校验、视频信息 |
| Color(5) | 颜色转换、调色板、CSS clamp、px↔rem、WCAG 对比度 |
| SEO(4) | robots.txt、sitemap、meta 标签、slug 生成 |
| Fun(1) | Mandelbrot 探索器 |
几个值得一提的细节:
- JSON 格式化报错不只说“Invalid JSON”,还给出精确的行列号——引擎的
SyntaxError各版本文案不一,所以定位逻辑写成两级降级(先找position,再找line/column),另有一篇专门拆解 - 三个生成器(密码/UUID/随机数)全部用
crypto.getRandomValues并做拒绝采样,保证每个字符均匀分布——Math.random做密码是不合格的 - 二维码生成器的编码器是手写的(下文展开),支持版本 1–10、四档纠错、PNG 下载
- Markdown 预览的解析器同样是手写的,公式排版则用打包进产物的 KaTeX 按需加载(这条界线为什么这么划)
- 所有计算在浏览器本地完成,没有任何数据离开你的设备
功能总览
计算器
支持完整的数学表达式,而不是普通计算器的“一步一步”输入:
2 + 3 * 4 → 14
sin(pi/2) → 1 (弧度制)
a = 5 → 定义变量
a^2 + 1 → 26
ans * 2 → 上一轮结果
- 标准 / 科学键盘:右上角可切换。标准键盘是 4 列的日常布局;科学键盘多出
sin cos tan √ ln log π e x^y n!等按键 - DEG / RAD:科学键盘上可切换角度制与弧度制。注意与实体计算器一致的语义——角度制下
sin(pi/2)会把 π/2 ≈ 1.57 当作 1.57° 来算 - 变量:
名字 = 表达式赋值(常量名和函数名不可占用),变量以 chips 形式列在输入框下方,点一下插入、点 × 删除。变量存进 localStorage,在绘图页里可以直接引用(如a*x) - 历史记录:每次按 = 的算式都进历史,点击任意一条可回填
函数绘图
最多同时画 5 条曲线(固定五色,前四色色盲友好),支持 sin(x)、x^2 - 3、a*x(引用计算器变量)等:
- 滚轮缩放(以鼠标位置为中心)、拖拽平移、触屏双指 pinch
- 悬停显示十字线与各函数在该 x 处的取值
- 自动网格与坐标轴刻度,
1/x这类渐近线会自动断笔 - 修改科学计算器页里的变量,图形实时联动重绘
- 暗色主题下画布配色自动跟随重绘(颜色从 CSS 变量读取,MutationObserver 监听主题切换)
三维曲面
/calculators/graph3d/ 画 z = f(x, y),用的仍然是 Canvas 2D——没有 three.js,也没有 WebGL。一个曲面图需要的东西只有正交投影加一个从后往前的绘制顺序,一个软件渲染器就能覆盖(而这个顺序不用排序也能拿到,见下):
- 拖拽旋转、滚轮缩放、键盘也能开(画布
tabindex=0,方向键转视角、+/-缩放、0复位) - 采样网格带缓存:旋转和缩放只重新投影已有网格,改公式、改定义域、改分辨率才重新采样。同一层缓存还挡住了着色——格子颜色只依赖高度场和固定光向,与相机无关
1/(x²+y²)这类有极点的函数会把真实值域拉到无穷大,让其余部分挤成一个颜色。所以当尾部极端时,高度与配色的映射改用 1–99 百分位,尖峰按裁剪画出来
之所以不上 WebGL:一帧的钱几乎全花在 canvas 的颜色状态上,而不是光栅化。实测 7,744 个四边形里,真正的填充只值 15~21ms,fillStyle 的 CSS 颜色解析值 123ms——换渲染后端解决不了一个字符串解析问题,在 JS 里就能解决,改完一帧从 166ms 降到 59ms。全部实测与推导。
实现原理
轻量工具坚持原生 TypeScript 与浏览器 API——计算器没有 math.js,页面没有 React,二维码编码管线也没有交给现成包。第三方依赖只用在重复实现成本与规范风险都明显更高的边界:Markdown 数学排版用 KaTeX;Excel 分析用 JSZip;PDF 结构/渲染用自托管的 pdf-lib 与 pdf.js;文档 OCR 用自托管的 Tesseract.js。它们全部版本锁定并由本站提供,不走 CDN、数据不出浏览器。KaTeX 的划线见Markdown 解析器那篇,Office 重库的划线见浏览器 Office 三件套。
手写表达式引擎
核心是一条经典的编译流水线,位于 src/scripts/calculator/engine/:
tokenizer → parser(递归下降) → AST → compile 为闭包
-
tokenizer:把
"2+3*4"切成数字、运算符、函数名、括号 token -
parser:按文法递归下降,处理优先级和结合性:
expr := term (('+'|'-') term)* term := unary (('*'|'/'|'%') unary | 隐式乘法)* unary := ('-'|'+') unary | power power := postfix ('^' unary)? // 右结合 postfix := primary ('!' | '%')* -
compile:把 AST 编译成一个
(scope) => number闭包。绘图时每帧要采样上千个点,解释执行 AST 太慢,预编译成闭包后绘图热路径就是纯函数调用
% 有一个刻意设计的歧义处理:跟在操作数后面且下一个 token 不是操作数时是百分号(50 + 10% = 55),否则是取模(10 % 3 = 1),与主流计算器一致。
Canvas 绘图
- DPR 适配:画布物理尺寸 = CSS 尺寸 ×
devicePixelRatio,保证 2K/4K 屏不糊 ResizeObserver监听容器尺寸变化自动重绘- 每像素列采样一个点;相邻两点屏幕跳跃超过两倍画布高且函数值变号时断笔(渐近线检测)
- Pointer Events 统一处理鼠标/触摸的拖拽与 pinch
手写二维码编码器
二维码生成器是工具集里最硬核的一个——整条编码管线都是手写的,不到 400 行:
UTF-8 字节 → 位流(模式+长度+数据+填充)
→ Reed–Solomon 纠错码(GF(256),生成多项式 0x11D)
→ 分块交织 → 矩阵摆放(之字形扫描)
→ 8 种掩码按 ISO 罚分规则选最优 → 写入格式信息
几个关键点:
- Reed–Solomon:在 GF(256) 上用对数/反对数表做乘法,移位寄存器式求余算出纠错码字,块结构按版本 × 纠错等级查表
- 掩码:数据模块要 XOR 一个掩码图案避免出现大面积同色或类似定位图案的结构;8 种候选逐一评估四条罚分规则(连续同色、2×2 同色块、类定位图案、明暗比例),取罚分最低者
- 验证方式:与 python qrcode 库强制相同掩码后逐模块 diff(128 例全部一致),再把输出交给 opencv 的解码器做解码往返(16/16 还原出原文)——证明生成的码是真实可扫的,不只是“看起来像”
拆分后的工程组织
工具页共用一套注册表驱动的架构:src/tools/registry.ts 里每个工具是一条数据(路径、名称、字段定义、compute 纯函数),四个动态路由 src/pages/{calculators,converters,finance,tools}/[slug].astro 据此静态生成全部子页面,客户端按 data-tool-kind 分发到对应的渲染器(表单、换算器、文本工具……)。新增一个计算器只需在注册表里加一条配置,路由、目录页、sitemap 全部自动跟上。
性能哲学
没有用 React 之类的框架:这类工具页交互密度低、无状态共享需求,vanilla TS 在加载(无框架运行时)与运行(无虚拟 DOM diff)两端都更快,打包体积只有几 KB。
技术栈小结
| 部分 | 方案 |
|---|---|
| 表达式求值 | 手写 tokenizer + 递归下降 parser + 闭包编译 |
| 二维绘图 | 原生 Canvas 2D + Pointer Events |
| 三维曲面 | 同一块 Canvas 2D:正交投影 + 画家算法,采样网格带缓存 |
| 二维码 | 手写编码器:RS 纠错 + 8 掩码 ISO 罚分选择 |
| 金融计算 | 等额本息/复利闭式解,摊销表按年分组 |
| 随机性 | crypto.getRandomValues + 拒绝采样 |
| 数学排版 | KaTeX,版本锁死、字体 vendored、按需加载 |
| 状态持久化 | localStorage(历史、变量、主题) |
| 框架依赖 | 0 |