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

参考资源

相关推荐
熊猫钓鱼>_>8 小时前
解决Web游戏Canvas内容在服务器部署时的显示问题
服务器·前端·游戏·canvas·cors·静态部署·资源路径
Lsx_2 天前
案例+图解带你一文读懂Svg、Canvas、Css、Js动画🔥🔥(4k+字)
前端·javascript·canvas
狂龙骄子2 天前
uniapp圆形时钟
小程序·uniapp·canvas·clock·圆盘时钟
若梦plus3 天前
Canvas基础
前端·canvas
若梦plus3 天前
Canvas的未来之AI绘图、生成式视觉与XR
前端·canvas
1024肥宅4 天前
综合项目实践:可视化技术核心实现与应用优化
svg·webgl·canvas
Just_Paranoid6 天前
【SystemUI】基于 Android R 实现下拉状态栏毛玻璃背景
android·canvas·systemui·renderscript
阿拉斯攀登8 天前
电子签名:SpringBoot + 汉王 ESP560 的考核签名项目实施方案
springboot·canvas·电子签名·电子签·汉王
Danny_FD11 天前
使用Taro实现微信小程序仪表盘:使用canvas实现仪表盘(有仪表盘背景,也可以用于Web等)
前端·taro·canvas
武清伯MVP13 天前
深入了解Canvas:HTML5时代的绘图利器(二)
前端·html5·canvas