1099 字 5 分钟
六维图生成器:一款纯前端的战力图制作工具
2026-08-14

六维图生成器:一款纯前端的战力图制作工具#

做了一个小工具:六维图生成器,一个纯前端的战力图 / 雷达图制作器,用于快速产出游戏角色战力图、能力评估图这类图片。这期博客聊聊它的技术设计,以及踩坑经历。

为什么做这个工具#

想搞一个生成六维图的工具,但市面上的大部分工具都不符合我的要求,懒得接着找了,所以直接vibe coding了一个,需求的功能有:

  • 拖动滑块实时调整七维数值,带平滑动画
  • 上传头像与背景图,图表在左、头像在右合成海报
  • 一键导出高清 PNG / JPEG
  • 数值与字母等级(无 E D C B A S)双模式切换

纯前端 + 零运行时依赖,npm run dev 即可本地使用,打开就是编辑器。

技术选型与结构#

Vite + TypeScript + 原生 Canvas 2D,不引入任何图表库——雷达图本质上就是多边形几何,自己画反而更可控。

src/
├── state.ts # 单一状态源 + localStorage 持久化 + 旧存档迁移
├── radar.ts # 雷达图绘制引擎(多边形/网格/标签/动画)
├── exporter.ts # 导出合成(背景 + 图表 + 头像)
├── main.ts # 控制面板 UI 与事件绑定
└── utils.ts # 图片加载与缩放工具

核心设计一:所见即所得#

预览和导出共用同一个 renderComposition(ctx, w, h, state, ...) 函数,只是画布尺寸与倍率不同。预览区显示的就是导出图的内容,用户在左侧面板调整的每一项配置都会实时反映在预览上,没有”预览一套、导出另一套”的割裂感。

核心设计二:单一状态源#

所有配置收敛进一个 AppState 对象:标签、数值、颜色、背景、头像、模式……任何修改都走 update(state, patch) → 持久化 + 通知。localStorage 自动保存,刷新后配置不丢;结构变更时(比如从六维改成固定七维),loadState 里做旧存档的补齐与迁移,老用户的存档平滑升级。

核心设计三:补间动画#

数值变化时不直接跳变,而是通过 requestAnimationFrame + easeOutCubic 补间过渡(约 480ms),拖动滑块时图表像”生长”一样变化。动画循环只在过渡期间运行,完成后自动停止,不浪费 CPU。

核心设计四:双模式数值#

数字模式下每维是 0~最大值的滑块;字母模式下是一组七档选择器(无 E D C B A S),多边形按档位比例绘制。两种模式切换时,数值按比例双向换算(例如 82/100 → A 档),回切不丢形。

踩坑记录#

改完分类名,输入框被”回滚”#

现象:修改分类名后图表正常更新,但参数面板的输入框又显示回旧名字。

这是典型的状态同步时序问题。当时 applyState 是这么写的:

state = update(state, patch)

而 update 内部:

const next = { ...prev, ...patch }
saveState(next)
notify(next) // ← 同步回调监听器
return next

JS 的赋值是先求值右侧再赋值,所以 notify 触发监听器时,模块级 state 还是旧值。监听器里的 syncAllRows() 读到旧 state,把输入框覆盖回旧名——图表渲染用的是新值(render 在赋值之后调用),于是出现了”图表对、输入框错”的诡异现象。滑块其实也有同样的隐患。

修复:监听器改为接收 notify 传入的新状态参数,不再依赖模块变量的赋值时机。

模式切换数值换算”原地不动”#

字母模式切换按钮第一次实现时,82 切换后还是 82,毫无换算效果。逐条拦截 localStorage 写入才看清:换算公式里当前值域和目标值域用反了。比例必须用当前值域计算(82/100),再乘目标值域(×6 → A 档),而不是反过来。修正后 82 ↔ 4.92 双向无损。

结语#

没什么好结语的,硬要说的话就是总结好需求,说清楚现象,问题就能迎刃而解了。

源码与使用说明见 Six-Dimensional Chart Maker,MIT 许可,欢迎取用和提意见。


发布日期:2026-08-14

许可协议:CC BY-NC-SA 4.0

六维图生成器:一款纯前端的战力图制作工具
https://peacedove2008.icu/posts/six-dimensional-chart-maker/
作者
PeaceDove2008
发布于
2026-08-14
许可协议
CC BY-NC-SA 4.0