Fabric.js 图片框选功能实现指南
概述
基于 Fabric.js 实现图片标注和区域选择功能,支持手动框选、预设区域、拖拽缩放等交互操作。
技术栈
- Vue 3 + TypeScript:响应式数据管理和类型安全
- Fabric.js:Canvas 操作核心库
- UUID:对象唯一标识
- 防抖函数:性能优化
核心功能模块
1. 画布初始化
typescript
const initCanvas = () => {
canvasInstance.value = new fabric.Canvas(canvas.value, {
width: containerWidth,
height: containerHeight,
selection: false, // 禁用默认多选
preserveObjectStacking: true, // 保持对象层级
imageSmoothingEnabled: false // 确保图片清晰度
})
setupEvents() // 设置事件监听
}
2. 图片加载

typescript
const loadImage = async (url: string) => {
loading.value = true
try {
const img = await FabricImage.fromURL(url)
img.set({
left: 0,
top: 0,
selectable: false,
evented: false
})
// 保存背景图尺寸用于边界限制
backgroundImageBounds.value = {
width: img.width || 0,
height: img.height || 0
}
canvasInstance.value.backgroundImage = img
canvasInstance.value.renderAll()
} catch (error) {
console.error('图片加载失败:', error)
} finally {
loading.value = false
}
}
3. 手动框选实现

typescript
let isDrawing = false
let startPoint: fabric.Point
let activeRect: fabric.Rect
// 鼠标按下开始框选
canvasInstance.value.on('mouse:down', (e) => {
if (e.e.ctrlKey) return // Ctrl键用于拖拽画布
isDrawing = true
startPoint = canvasInstance.value.getPointer(e.e)
activeRect = new fabric.Rect({
left: startPoint.x,
top: startPoint.y,
width: 0,
height: 0,
fill: 'transparent',
stroke: '#1890ff',
strokeWidth: 2
})
canvasInstance.value.add(activeRect)
})
// 鼠标移动更新矩形
canvasInstance.value.on('mouse:move', (e) => {
if (!isDrawing) return
const pointer = canvasInstance.value.getPointer(e.e)
const width = Math.abs(pointer.x - startPoint.x)
const height = Math.abs(pointer.y - startPoint.y)
activeRect.set({
left: Math.min(startPoint.x, pointer.x),
top: Math.min(startPoint.y, pointer.y),
width,
height
})
canvasInstance.value.renderAll()
})
// 鼠标抬起完成框选
canvasInstance.value.on('mouse:up', () => {
if (!isDrawing) return
isDrawing = false
// 验证最小尺寸
if (activeRect.width < 10 || activeRect.height < 10) {
//不满足条件,从画布上剔除
canvasInstance.value.remove(activeRect)
return
}
// 添加唯一标识
(activeRect as ChangeRect).uuid = generateUUID()
rectangles.value.push(activeRect as ChangeRect)
})
4. 边界约束

typescript
const constrainRectToBounds = (rect: fabric.Rect) => {
const bgImage = canvasInstance.value.backgroundImage as fabric.Image
if (!bgImage) return
const rectBounds = rect.getBoundingRect()
// 移动约束
const newLeft = Math.max(0, Math.min(
bgImage.width - rectBounds.width,
rectBounds.left
))
const newTop = Math.max(0, Math.min(
bgImage.height - rectBounds.height,
rectBounds.top
))
rect.set({ left: newLeft, top: newTop })
canvasInstance.value.renderAll()
}
// 监听对象移动和缩放
canvasInstance.value.on('object:moving', (e) => {
constrainRectToBounds(e.target as fabric.Rect)
})
canvasInstance.value.on('object:scaling', (e) => {
constrainRectToBounds(e.target as fabric.Rect)
})
5. 遮罩层效果

typescript
const showMaskOverlay = (rect: fabric.Rect) => {
const bgImage = canvasInstance.value.backgroundImage as fabric.Image
if (!bgImage) return
const canvasWidth = bgImage.width
const canvasHeight = bgImage.height
// 使用 SVG 路径创建镂空效果
const pathData = `
M 0 0 L ${canvasWidth} 0 L ${canvasWidth} ${canvasHeight} L 0 ${canvasHeight} Z
M ${rect.left} ${rect.top}
L ${rect.left + rect.width} ${rect.top}
L ${rect.left + rect.width} ${rect.top + rect.height}
L ${rect.left} ${rect.top + rect.height} Z
`
const maskPath = new fabric.Path(pathData, {
fill: 'rgba(0, 0, 0, 0.5)',
fillRule: 'evenodd', // 关键:实现镂空效果
selectable: false,
evented: false,
excludeFromExport: true
})
canvasInstance.value.add(maskPath)
maskOverlay.value = maskPath
}
6. 画布缩放和拖拽

typescript
// 滚轮缩放
canvasInstance.value.on('mouse:wheel', (opt) => {
const delta = opt.e.deltaY
let zoom = canvasInstance.value.getZoom()
zoom *= 0.999 ** delta
// 限制缩放范围
zoom = Math.max(0.1, Math.min(5, zoom))
canvasInstance.value.zoomToPoint(
{ x: opt.e.offsetX, y: opt.e.offsetY },
zoom
)
opt.e.preventDefault()
opt.e.stopPropagation()
})
// Ctrl + 拖拽平移画布
let isDragging = false
let lastPosX: number, lastPosY: number
canvasInstance.value.on('mouse:down', (opt) => {
const evt = opt.e
if (evt.ctrlKey) {
isDragging = true
canvasInstance.value.selection = false
lastPosX = evt.clientX
lastPosY = evt.clientY
}
})
canvasInstance.value.on('mouse:move', (opt) => {
if (isDragging) {
const e = opt.e
const vpt = canvasInstance.value.viewportTransform
vpt[4] += e.clientX - lastPosX
vpt[5] += e.clientY - lastPosY
canvasInstance.value.requestRenderAll()
lastPosX = e.clientX
lastPosY = e.clientY
}
})
7. 生命周期管理
typescript
// 组件挂载
onMounted(() => {
nextTick(() => {
initCanvas()
loadImage(imageUrl.value)
// 响应式尺寸监听
resizeObserver.value = new ResizeObserver(
useDebounceFn((entries) => {
updateCanvasSize(entries[0].target)
}, 100)
)
if (canvas.value?.parentElement) {
resizeObserver.value.observe(canvas.value.parentElement)
}
})
})
// 组件卸载清理
onUnmounted(() => {
removeMaskOverlay()
canvasInstance.value?.destroy()
resizeObserver.value?.disconnect()
})
类型定义
typescript
interface ChangeRect extends fabric.Rect {
uuid: string
//自定义属性
}
interface RectangleDimension {
left: number
top: number
width: number
height: number
uuid: string
}
关键技术要点
- 坐标系转换:Canvas 坐标与屏幕坐标的准确映射
- 事件处理:合理使用 Fabric.js 事件系统避免冲突
- 性能优化 :使用
requestRenderAll()
而非renderAll()
- 内存管理:及时清理事件监听器和画布实例
- 边界约束:确保标注区域在图片范围内
- 视觉反馈:遮罩层提供良好的用户体验