功能概述
基本流程:以不同的颜色分区作为依据,解析图片数据,帮助辅助选区


最终效果:

1.1 核心功能
-
图像追踪:将位图选区图片转换为 SVG 矢量路径
-
路径解析:按颜色分组解析 SVG 路径数据
-
交互选择:支持鼠标点击、悬停等交互操作
1.2 技术栈
-
Fabric.js:Canvas 图形库,用于路径渲染和交互
-
imageTracer.js:图像追踪库,用于位图转矢量
核心实现流程
主函数:test()
test() 函数是选区识别与交互功能的核心入口,完整流程如下:

初始化画布状态
ini
selectionLoading.value = true // 显示加载提示
fabricCanvas.skipTargetFind = false // 启用对象检测
fabricCanvas.isDrawingMode = false // 禁用绘制模式
fabricCanvas.renderAll() // 重新渲染
关键点:
skipTargetFind: false允许鼠标事件被路径对象捕获isDrawingMode: false确保不会触发自由绘制
加载选区图片
此处需要一张以不同颜色区分选区的图片,后续将基于这些颜色进行区域识别:

ini
const img = new Image()
img.crossOrigin = 'anonymous' // 跨域支持
img.src = props.selectionUrl
await new Promise((resolve) => {
img.onload = resolve
})
提取选区图片像素数据
ini
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0)
创建临时 Canvas 获取图片的像素数据
将图片canvas的数据导入进 imageTracer;将其转化为svg,
arduino
const svgString = imageTracer.imageDataToSVG(ctx?.getImageData(0, 0, canvas.width, canvas.height), {
ltres: 1, // 直线阈值
qtres: 1, // 二次曲线阈值
pathomit: 8, // 路径简化阈值
})
参数说明:
ltres:控制直线检测的精度,值越小越精确qtres:控制曲线检测的精度pathomit:路径简化参数,值越大简化程度越高
SVG 格式的字符串,包含多个 <path> 元素。这些path元素记录了我们需要的区域信息,比方说在这张图的那张位置,边界是如何巴拉巴拉。
<path> 元素提供了精确的区域信息,将其导入fabric的path中,利用fabric的交互监听,可以帮助进行处理
解析 SVG 并按颜色分组
typescript
const parser = new DOMParser()
const svgDoc = parser.parseFromString(svgString, 'image/svg+xml')
const pathGroupByColor = new Map<string, string>()
svgDoc.querySelectorAll('path').forEach((path) => {
const color = path.getAttribute('fill') || '#000000'
// 过滤黑色背景
if (color === 'rgb(0,0,0)' || color === '#000000') {
return
}
const pathData = path.getAttribute('d') || ''
const oriVal = pathGroupByColor.get(color) || ''
pathGroupByColor.set(color, oriVal + ' ' + pathData)
})
处理逻辑:
- 使用
DOMParser解析 SVG 字符串为 DOM 对象 - 遍历所有
<path>元素 - 按
fill颜色属性分组,相同颜色的路径数据合并 - 过滤掉黑色背景(
#000000或rgb(0,0,0))
为什么按颜色分组:
- 图像追踪可能产生多个不同颜色的路径区域
- 相同颜色的路径通常属于同一个选区区域
- 合并后可以减少路径对象数量,提高性能
创建可交互的 Fabric Path 对象
php
pathGroupByColor.forEach((pathData) => {
const fabricPath = new fabric.Path(pathData, {
fill: 'transparent',
opacity: 0.01,
stroke: 'transparent',
strokeWidth: 1,
perPixelTargetFind: true, // 像素级精确检测
})
// 绑定交互事件...
selectionPaths.value.push(fabricPath)
})
关键配置:
perPixelTargetFind: true:启用像素级命中检测,提高点击精度
必须使用opacity: 0.01,因为此处是使用perPixelTargetFind: true 进行像素检测,如果说完全透明+无背景,fabric无法监测这个path的交互事件
绑定鼠标交互事件
dart
const initPtah = {
fill: 'white',
opacity: 0.01,
stroke: 'transparent',
strokeWidth: 1,
};
const selectedPath = {
fill: pencilBrushColor.value,
globalCompositeOperation: 'xor',
stroke: pencilBrushColor.value,
opacity: 1,
};
// 点击
fabricPath.on('mousedown', (e: any) => {
const opacity = fabricPath.get('opacity')
const pathOption = opacity === 1 ? initPtah : selectedPath
fabricPath.set({
...pathOption,
})
fabricCanvas.renderAll()
})
// hover:
fabricPath.on('mouseover', (e: any) => {
const opacity = fabricPath.get('opacity')
if (opacity === 1) return // 已选中的不处理
fabricPath.set({
...selectedPath,
opacity: 0.75, // 半透明高亮
})
fabricCanvas.renderAll()
})
// leave
fabricPath.on('mouseout', (e: any) => {
const opacity = fabricPath.get('opacity')
if (opacity === 1) return // 已选中的保持选中状态
fabricPath.set({
...initPtah, // 恢复初始透明状态
})
fabricCanvas.renderAll()
})
组合路径并适配画布
arduino
const scaleX = canvasWidth.value / img.width!
const scaleY = canvasHeight.value / img.height!
const scale = Math.min(scaleX, scaleY) // 保持宽高比
const group = new fabric.Group(selectionPaths.value, {
evented: true, // 启用事件
hasControls: false, // 隐藏控制点
subTargetCheck: true, // 子对象检测
})
group.set({
scaleX: scale,
scaleY: scale,
left: (canvasWidth.value - img.width! * scale) / 2, // 水平居中
top: (canvasHeight.value - img.height! * scale) / 2, // 垂直居中
lockMovementX: true, // 锁定水平移动
lockMovementY: true, // 锁定垂直移动
})
缩放计算:
- 计算水平和垂直方向的缩放比例
- 取较小值确保图片完整显示且保持宽高比
Group 配置:
evented: true:允许接收鼠标事件subTargetCheck: true:检测子对象(路径),确保点击路径时能触发事件hasControls: false:隐藏变换控制点,防止用户误操作lockMovementX/Y: true:锁定位置,防止拖动