Fabric.js从0到1实现图片框选功能

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
}

关键技术要点

  1. 坐标系转换:Canvas 坐标与屏幕坐标的准确映射
  2. 事件处理:合理使用 Fabric.js 事件系统避免冲突
  3. 性能优化 :使用 requestRenderAll() 而非 renderAll()
  4. 内存管理:及时清理事件监听器和画布实例
  5. 边界约束:确保标注区域在图片范围内
  6. 视觉反馈:遮罩层提供良好的用户体验

参考资源

相关推荐
wayhome在哪2 天前
用 fabric.js 搞定电子签名拖拽合成图片
javascript·产品·canvas
德育处主任2 天前
p5.js 掌握圆锥体 cone
前端·数据可视化·canvas
德育处主任3 天前
p5.js 3D 形状 "预制工厂"——buildGeometry ()
前端·javascript·canvas
德育处主任5 天前
p5.js 3D盒子的基础用法
前端·数据可视化·canvas
掘金安东尼5 天前
2分钟创建一个“不依赖任何外部库”的粒子动画背景
前端·面试·canvas
百万蹄蹄向前冲6 天前
让AI写2D格斗游戏,坏了我成测试了
前端·canvas·trae
用户2519162427118 天前
Canvas之画图板
前端·javascript·canvas
FogLetter11 天前
玩转Canvas:从静态图像到动态动画的奇妙之旅
前端·canvas
用户25191624271112 天前
Canvas之贪吃蛇
前端·javascript·canvas