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. 视觉反馈:遮罩层提供良好的用户体验

参考资源

相关推荐
鱼钓猫1 天前
H5 电子签名组件
vue.js·canvas
小鱼儿亮亮2 天前
canvas中常见问题的解决方法及分析,踩坑填坑经历
前端·canvas
普兰店拉马努金3 天前
【Canvas与图标】古铜色“HTML”图标
html·canvas·图标
wayhome在哪13 天前
Cropper.js 轻松拿捏前端裁剪🤞
javascript·canvas·设计
PineappleCoder18 天前
SVG 适合静态图,Canvas 适合大数据?图表库的场景选择
前端·面试·canvas
德育处主任19 天前
p5.js 用 cylinder() 绘制 3D 圆柱体
前端·数据可视化·canvas
wayhome在哪22 天前
用 fabric.js 搞定电子签名拖拽合成图片
javascript·产品·canvas
德育处主任22 天前
p5.js 掌握圆锥体 cone
前端·数据可视化·canvas
德育处主任23 天前
p5.js 3D 形状 "预制工厂"——buildGeometry ()
前端·javascript·canvas