前言
做课堂演示的时候,你有没有遇到过这样的场景:想在屏幕上圈一个重点,画个箭头指一下关键数据,却发现手头没有趁手的工具?市面上的屏幕标注软件要么体积臃肿、要么功能繁杂、要么夹带私货(广告和遥测),我一直没找到一个「开箱即用、足够轻量、隐私安全」的方案。
于是我决定自己造一个。
MarkerOn 是一款轻量级桌面屏幕标注工具——按下快捷键,一层透明画布覆盖全屏,在桌面上自由绘画、标注,松手即走。适用于课堂演示、会议讲解、录屏批注等场景。
GitHub:github.com/ifer47/mark… Microsoft Store:MarkerOn 协议:MIT
一、它能做什么
8 种标注工具,覆盖日常场景
| 按键 | 工具 | 说明 |
|---|---|---|
1 | 画笔 | 自由绘画,贝塞尔曲线平滑 |
2 | 荧光笔 | 半透明高亮标记 |
3 | 箭头 | 带箭头的指示线 |
4 | 矩形 | 矩形边框 |
5 | 椭圆 | 椭圆边框 |
6 | 直线 | 直线段 |
7 | 橡皮擦 | 实时擦除,效果跟随元素拖拽 |
T | 文字 | 双击放置文字,滚轮调字号 |
按数字键即时切换工具,不需要打开任何面板——键盘党的福音。
修饰键 + 鼠标拖动 = 快速绘制几何图形
这是我最喜欢的交互设计:不需要切换工具,用修饰键组合就能画出不同图形:
| Windows / Linux | macOS | 图形 |
|---|---|---|
| Ctrl + 拖动 | ⌘ Command + 拖动 | 矩形 |
| Ctrl + Alt + 拖动 | ⌘ + ⌥ Option + 拖动 | 正方形 |
| Shift + 拖动 | ⇧ Shift + 拖动 | 椭圆 |
| Shift + Alt + 拖动 | ⇧ + ⌥ Option + 拖动 | 正圆 |
| Ctrl + Shift + 拖动 | ⌘ + ⇧ Shift + 拖动 | 箭头 |
也就是说,你在画笔模式下,按住 Ctrl(Mac 上是 ⌘ Command)一拖就是矩形,完全不打断绘画节奏。
全键盘操作,效率翻倍
除了工具切换,颜色切换(Q / E)、撤销重做(Ctrl+Z / Ctrl+Y,Mac 上为 ⌘+Z / ⌘+Y)、呼出设置面板(Space)、复制屏幕到剪贴板(Ctrl+C,Mac 上为 ⌘+C)、清除全部标注(Delete)、退出标注模式(Esc)全部有快捷键,覆盖标注模式全流程,手不用离开键盘。
还有这些细节
- 彩色光标:光标颜色实时反映当前画笔色,切换颜色后底部短暂提示颜色名称
- 右键快速选色盘:鼠标右键弹出调色盘,选完即画
- 元素拖拽:可在设置中开启「允许拖拽已有元素」,绘制完的图形、文字都能拖动调整位置
- 文字编辑:双击已有文字重新进入编辑模式,滚轮调整字号,
Ctrl+Enter(Mac 上为⌘+Enter)确认 - 开机自启:设置中一键开启,系统启动后静默驻留托盘
按 Space 呼出设置面板,工具、颜色、线宽一目了然:
二、技术栈
| 技术 | 角色 |
|---|---|
| Tauri v2 | 桌面应用框架 — Rust 后端负责窗口管理、系统托盘、全局快捷键 |
| Vue3 + TypeScript | 前端 UI 框架 |
| Vite6 | 构建与热更新 |
| Tailwind CSS v4 | 样式 |
| HTML5 Canvas | 绘图引擎核心 |
为什么选 Tauri 而不是 Electron?两个字:轻量。MarkerOn 安装包不到 3MB,运行内存占用极低,作为一个常驻托盘的工具,这一点至关重要。Tauri2 的 Rust 后端提供了出色的系统级能力——透明置顶窗口、全局快捷键注册、系统托盘交互——这些恰好是屏幕标注工具的刚需。
三、架构设计
整个应用的架构可以概括为:Rust 管生命周期,Canvas 管渲染,Vue 管交互。
3.1 Rust 后端:窗口管理与系统交互
Rust 端的核心职责是管理一个全屏透明置顶窗口。这个窗口在 tauri.conf.json 中配置为无装饰、跳过任务栏、透明背景、始终置顶,默认隐藏。
当用户按下全局快捷键时,Rust 端会:
- 检测光标所在显示器(多显示器支持),通过 Windows API
MonitorFromPoint+GetMonitorInfoW获取该显示器的坐标和尺寸 - 调整窗口大小和位置到对应显示器——这里有一个有趣的 trick:窗口高度减 1 像素,避免 Windows 将其识别为全屏独占应用,否则任务栏会丢失 Mica 透明效果
- 显示窗口并聚焦,同时通过
set_ignore_cursor_events切换鼠标事件穿透
// 高度减 1 像素,防止 Windows 认为是全屏独占应用
window
.set_size(tauri::PhysicalSize::new(w, h.saturating_sub(1)))
.ok();
截屏功能也在 Rust 端实现,针对不同平台做了适配:
- Windows:直接调用 GDI
BitBlt→GetDIBits→ 写入CF_DIB剪贴板,跳过 PNG 编码,速度最快 - macOS:调用系统
screencapture -c -x -R命令 - Linux:使用
xcap+arboard库
3.2 Canvas 绘图引擎:性能导向的渲染方案
绘图引擎是整个项目最核心的部分,全部封装在 useDrawing.ts 这个 Vue composable 中(约 1400 行)。几个关键的技术决策:
贝塞尔曲线平滑
画笔工具不是简单的 lineTo 连线,而是使用二次贝塞尔曲线(quadratic mid-point smoothing)对采样点做平滑插值。在 bakeIncrementalStroke 中,每次新增采样点时只绘制增量部分到缓存 Canvas,避免整条曲线重绘。
非破坏性橡皮擦
这是一个有意思的设计:橡皮擦不会真正删除历史记录中的绘制动作,而是将擦除笔画作为 attachedErasers 附加到被擦除的目标上。渲染时,使用离屏 Canvas + destination-out 合成模式来「挖去」擦除区域。
interface DrawAction {
tool: Tool
color: string
lineWidth: number
points: Point[]
attachedErasers?: DrawAction[] // 附加的擦除笔画
bbox?: { x1: number, y1: number, x2: number, y2: number }
// ...
}
这样做的好处:擦除操作可以完美撤销和重做。Undo 时只需把 attachedErasers 回滚到之前的状态即可,不需要重建被擦除的绘制数据。而且当元素被拖拽移动时,擦除效果会跟着走。
空间网格加速碰撞检测
拖拽和擦除都需要做元素命中检测(hit testing),对每个采样点遍历所有历史绘制动作的开销太大。引擎维护了一个空间哈希网格(HIT_GRID_SIZE = 192),将绘制动作按 bounding box 注册到对应网格中,碰撞检测时只查询光标所在网格的元素。
分层缓存 + RAF 批量渲染
引擎维护了主画布的位图缓存(cache canvas),绘制新笔画时采用增量写入缓存的方式,避免每帧重绘全部历史。拖拽时有独立的 drag canvas 层。渲染调度使用 requestAnimationFrame 批量合并,避免同一帧多次重绘。
3.3 前端路由:一个 WebView 两个界面
整个前端只有一个 index.html,通过 URL hash 区分两个界面:
// App.vue
onMounted(() => {
if (window.location.hash === '#settings') {
mode.value = 'settings' // 设置窗口
}
// 否则默认是绘图覆盖层
})
设置窗口由 Rust 端通过 WebviewWindowBuilder 用 index.html#settings 创建,复用同一套前端代码,免去了多页面构建的复杂度。
四、隐私设计
这一点我想特别强调:MarkerOn 不联网、不追踪、不收集任何数据。
- 不创建任何唯一标识符或追踪器
- 不收集使用统计、性能指标或任何用户行为信息
- 不检查更新、不发送遥测数据、不连接任何远程服务器
- 绘画数据只存在于内存中,退出标注模式即自动清除
- 唯一落盘的数据是用户主动修改的配置文件(快捷键、是否开机自启等)
这是一个完全离线的应用。详见项目中的 PRIVACY.md。
五、项目结构
markeron/
├── src-tauri/
│ ├── src/
│ │ └── lib.rs # Rust 后端 — 窗口管理、快捷键、托盘、截屏
│ └── tauri.conf.json # Tauri 配置
│
├── src/
│ ├── components/
│ │ ├── DrawingOverlay.vue # 绘图覆盖层(Canvas + 指针事件 + Tauri 通信)
│ │ ├── SettingsPanel.vue # 标注模式内的工具面板
│ │ ├── SettingsView.vue # 独立设置窗口
│ │ └── TextBox.vue # 内联文字输入框
│ ├── composables/
│ │ └── useDrawing.ts # 绘图引擎(~1400 行)
│ ├── App.vue # 根组件(hash 路由)
│ └── main.ts # 入口
│
├── package.json
└── vite.config.ts
整个项目代码量不大,但每一行都经过精心设计。Rust 端约 700 行,前端核心引擎约 1400 行。
六、本地开发
# 克隆仓库
git clone https://github.com/ifer47/markeron.git
cd markeron
# 安装依赖
npm install
# 启动开发模式(Tauri + Vite 热更新)
npm run dev
# 打包构建
npm run build
前置要求:需要安装 Rust 和 Node.js 环境,以及 Tauri2 的系统依赖。
七、下载安装
如果你只是想用,不需要从源码构建:
| 平台 | 安装包 | 说明 |
|---|---|---|
| Windows x64 | EXE 安装包 | NSIS 安装程序(推荐) |
| Windows x64 | MSI 安装包 | MSI 安装程序 |
| macOS x64 | DMG | Apple 芯片需开启 Rosetta |
也可以直接在 Microsoft Store 搜索「Marker On」安装。
八、一些踩坑记录
开发过程中遇到不少有意思的问题,分享几个:
全屏窗口导致任务栏失去透明效果
在 Windows 上,如果一个窗口恰好覆盖整个显示器(宽高完全等于分辨率),系统会将其视为「全屏独占」应用,导致任务栏丢失 Mica/Acrylic 透明效果。解决方案很简单但不好排查:窗口高度减 1 像素。
橡皮擦的撤销难题
最初橡皮擦是直接从历史中删除被擦除的路径段,但这让撤销变得极其复杂——你需要精确重建被部分擦除的笔画。后来改用「附加擦除层」的方案:原始绘制数据不动,擦除笔画作为 attachment 挂在目标上,渲染时用 Canvas 合成模式实现视觉效果。撤销时只需操作 attachment 数组,干净利落。
Canvas desynchronized 模式
在创建 Canvas 2D context 时启用了 { alpha: true, desynchronized: true },desynchronized 会让 Canvas 的绘制脱离浏览器的主合成流程,减少一帧延迟。对于屏幕标注这种对实时性有要求的场景,这个优化很有价值。
写在最后
MarkerOn 是一个很小的项目,但它解决了一个真实的痛点。如果你是老师、讲师,或者经常做技术分享和录屏,希望它能帮到你。
项目完全开源(MIT),欢迎 Star、提 Issue 或贡献代码:
GitHub:github.com/ifer47/mark…
如果觉得有用,点个赞支持一下吧 👍