我给自己写了一个本地截图标注工具:安静、不上传、导出即走实录
目录
- 缘起:为什么我不想要任何云端截图工具
- 需求拆解:从"能截图"到"能标注"
- 技术选型:浏览器 + Canvas 就够了
- 核心实现一:截图粘贴与画布初始化
- 核心实现二:标注工具与撤销栈
- UI 定制:标注工具也是"自己的工作台"
- 三个让我卡了一阵的坑
- 结尾:截图工具的"即用即走"哲学
1. 缘起:为什么我不想要任何云端截图工具
我每天要截图几十次——bug 复现、设计标注、文档配图。截图本身不难,难的是截图之后:
- 想加个箭头指向重点,要打开一个重型编辑器;
- 想打个马赛克遮敏感信息,云端工具会先把图传上去;
- 标注完想导出,弹窗提示"登录会员才能去水印"。
我忽然意识到:截图标注是最高频的本地操作,凭什么要联网、要登录、要等上传。既然现成工具都在制造摩擦,那就自己写一个。
2. 需求拆解:从"能截图"到"能标注"
把"做一个截图标注工具"拆成可验收清单:
| 诉求(人话) | 拆解后(可验收) |
|---|---|
| 能导入图 | Ctrl+V 粘贴 / 拖入文件 / 选文件三种入口 |
| 能标注 | 箭头、矩形、椭圆、画笔、文字、马赛克六种工具 |
| 能撤销 | Ctrl+Z / Ctrl+Y 撤销重做,不限步数 |
| 能导出 | 保存为 PNG,可选复制到剪贴板 |
| 不上传 | 全程本地,不发起任何网络请求 |
| UI 我说了算 | 工具栏、配色、画布背景可自定义 |
这张表决定了工具的骨架。功能不多,但每一项都做到位——这是我对所有"自己用的工具"的一贯要求。
3. 技术选型:浏览器 + Canvas 就够了
我犹豫过要不要上 Electron,但仔细想需求——我不需要全局截图快捷键、不需要系统托盘,浏览器 + Canvas 完全够用:
| 能力 | 方案 |
|---|---|
| 粘贴截图 | clipboardData.items 读 image |
| 画布绘制 | Canvas 2D API |
| 标注交互 | 自实现工具状态机 |
| 撤销重做 | 操作栈 + 重绘 |
| 导出 | canvas.toDataURL + a[download] |
| 复制到剪贴板 | ClipboardItem + navigator.clipboard.write |
整个工具就是一个 HTML 文件,双击就能开。没有安装包、没有更新提示、没有后台进程——图片全程在内存里,关掉即消失,不留痕迹。
心得:选型时先问"我到底需要什么"。本地小工具的最高境界是双击即用、关掉即走。
4. 核心实现一:截图粘贴与画布初始化
粘贴是最高频的入口,必须做到"Ctrl+V 即进画布":
document.addEventListener('paste', e => {
for (const item of e.clipboardData.items) {
if (item.type.startsWith('image/')) {
const blob = item.getAsFile();
const img = new Image();
img.onload = () => initCanvas(img);
img.src = URL.createObjectURL(blob);
break;
}
}
});
画布初始化的关键:按图片真实尺寸建画布,不缩放。缩放会损失精度,标注时再按视口缩放显示即可:
function initCanvas(img) {
canvas.width = img.naturalWidth; // 真实分辨率
canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0);
fitToViewport(); // CSS 缩放显示
}
心得:截图工具的"专业感"来自一个细节——导出的图和原图同分辨率。任何"标注完画质变糊"的工具都是不合格的。
5. 核心实现二:标注工具与撤销栈
每个标注工具是一个状态机:idle → drawing → done。以箭头为例:
const tool = {
name: 'arrow',
onDown(x, y) { this.start = {x, y}; this.end = {x, y}; },
onMove(x, y) { this.end = {x, y}; redraw(); drawArrow(this.start, this.end); },
onUp() { stack.push({type:'arrow', ...this.start, ...this.end}); },
};
撤销栈是核心。每个标注是一个独立操作对象,重绘时按顺序回放:
function redraw() {
ctx.drawImage(original, 0, 0); // 重画底图
for (const op of stack) drawOp(op); // 回放所有标注
}
function undo() { stack.pop(); redoStack.push(...); redraw(); }
function redo() { stack.push(redoStack.pop()); redraw(); }
几个让体验"顺手"的细节:
- 马赛克是独立工具,不是滤镜。选中区域才打码,不污染整图;
- 标注时实时预览。
onMove里重绘 + 画当前正在拖的形状,松手才入栈; - 工具切换不清空栈。画完箭头切矩形,之前的箭头还在,撤销仍可回退。
心得:标注工具的核心指标是"手跟得上眼"。任何拖动延迟、松手跳变都会破坏手感,requestAnimationFrame 重绘是底线。
6. UI 定制:标注工具也是"自己的工作台"
本地工具最大的特权,是 UI 完全我说了算。我给自己定了几个商业软件不会这么做的设计选择:
- 画布占满中央,工具栏左侧细窄。标注时视觉焦点全在图上,工具不抢戏;
- 暗色画布背景。亮色背景会让浅色截图边缘融化,暗色让任何图都"浮"出来;
- 无导出水印、无登录提示。标注完按 Ctrl+S 即存,没有"升级会员"弹窗;
- 快捷键全覆盖。A 箭头、R 矩形、E 椭圆、B 画笔、T 文字、M 马赛克,左手不离键盘。
这些设计在商业产品里几乎不可能出现——它们太"个人化"了。但本地工具的意义,恰恰就是让软件贴合你的标注习惯,而不是让你迁就软件的功能预设。
7. 三个让我卡了一阵的坑
坑一:高 DPI 屏幕模糊。 画布 CSS 尺寸和实际像素不一致,标注线条发虚。解决办法是按 devicePixelRatio 放大画布像素,再 CSS 缩放回显示尺寸——所有 Canvas 工具都要过这一关。
坑二:马赛克要遮原图不能遮标注。 如果先画标注再打马赛克,标注也会被遮。正确顺序是马赛克直接改写底图层,而不是作为普通标注入栈。这是个反直觉的设计——有些"标注"本质是破坏性编辑,要单独处理。
坑三:复制到剪贴板要 ClipboardItem。 旧式 document.execCommand('copy') 复制不了图片。必须用 navigator.clipboard.write([new ClipboardItem({'image/png': blob})]),且要在用户手势里调用(不能异步延迟太久)。
8. 结尾:截图工具的"即用即走"哲学
做完这个工具,我对"截图标注"有了一种更深的体会:
- 它不抢你注意力。没有云空间、没有历史记录、没有"猜你想标注什么";
- 它尊重你的图片。全程本地,不发起任何网络请求,敏感信息放心打码;
- 它尊重你的时间。Ctrl+V 进图、标注、Ctrl+S 导出,三步完事。
在这个所有软件都在抢用户时长的时代,一个"打开就标注、关掉即消失"的本地工具,反而成了一种奢侈。
如果你也每天截图、也想要一个安静的标注工具,我强烈建议自己写一个。技术门槛比想象中低,一个 HTML 文件就够;真正难的是克制住加功能的冲动——不做云同步、不做历史记录、不做"AI 自动标注",就做一个老老实实的本地画板。
工具小而静,价值往往不在多,而在"它永远在那里,用完即走"。
- 点赞
- 收藏
- 关注作者
评论(0)