热区与图片标注
标注工具的真相是 底图世界坐标上的几何 + JSON。SVG 负责可点矩形/多边形和无障碍名;Canvas 不是默认命中层。不要在 <img> 上盖一张透明 canvas 当热区——那是退回像素拾取,读屏和缩放都会坏。
1. 场景怎么切
世界:图像自然像素(0..naturalWidth × 0..naturalHeight)
相机:fit 进 viewport 的 viewBox;用户缩放改 scale,锚点在光标
<image>:底图(光栅,可以)
g.hotspots:rect / polygon(必须是节点)
g.chrome:正在拉的框、hover(不进业务 JSON)
DOM:侧栏列表、label 输入
必须是节点:每个热区。tabindex、title、列表与图形共用 id。命中走 DOM 或 isPointInFill,坐标已是世界。
可以光栅:底图本身、导出 burn-in PNG、打印预览。训练/审核后台要的是 JSON,不是烧进图的像素。
export type Hotspot =
| { id: string; type: "rect"; x: number; y: number; w: number; h: number; label: string }
| { id: string; type: "polygon"; points: { x: number; y: number }[]; label: string };
export interface AnnotatorDoc {
imageHref: string;
naturalWidth: number;
naturalHeight: number;
hotspots: Hotspot[];
}
export function fitViewBox(doc: AnnotatorDoc, cssW: number, cssH: number): string {
const s = Math.min(cssW / doc.naturalWidth, cssH / doc.naturalHeight);
const w = cssW / s;
const h = cssH / s;
const x = (doc.naturalWidth - w) / 2;
const y = (doc.naturalHeight - h) / 2;
return `${x} ${y} ${w} ${h}`;
}
x/y/w/h 相对自然像素,不是 CSS、不是当前缩放。换 DPR、换窗口,几何不变。这和 Canvas 标注篇同一世界,只是投影换成 SVG 节点。
创建矩形:pointerdown 空处 → 拖 → viewToWorld → 松手写入 hotspot。最小尺寸(如 4 图像像素)以下当误触丢弃。指针坐标必须经 getScreenCTM,不要 offsetX。
多边形命中用 isPointInFill,点已在元素用户空间。列表与图形共用 selection id,侧栏是 DOM overlay,见 无障碍。
2. 导出
业务:JSON.stringify(doc)。分享图:按自然分辨率离屏画底图 + 描框,见 转位图。两套导出不要一个按钮。
底图跨源:inline <image href> 能显示,序列化后别人打开可能丢图。编辑器内存里用 blob URL,导出再打包或 data URL。相机缩放见 锚点缩放。
正在拉的框画在 chrome,松手才写入 hotspots。最小尺寸过滤后才 History.execute。
适用:审核框、图片上的可点区域、无障碍热区。HTML map/area 没有相机,响应式会漂,生产不要用。
3. 不适用
- 热区只存在 CSS 绝对定位
div:和响应式 object-fit 会脱节,除非你完全不用 SVG 相机。 - 把标注画进底图像素还当可编辑:下一轮打不开对象。
- 用户 SVG 当底图还 inline 脚本:底图走
<img>或消毒,见 注入。
4. 失败形态
| 症状 | 原因 |
|---|---|
| 缩放后框滑走 | 框存在了 view / CSS 坐标 |
| 点不准 | 忘了 letterbox,用 client 当图像像素 |
| 列表高亮和图形不一致 | 两份 selection |
| 导出框粗细不对 | 按屏幕 px 描边去烧自然分辨率 |
| 读屏点不到 | 热区是 canvas 像素,没有节点 |
权威资料
核对日期:2026-08-26