基于Fabric.js实现一个可以在图片上创建多个点击热区的组件

基于Fabric.js实现一个可以在图片上创建多个点击热区的组件

需求描述

前段时间接到一个需求,需要在管理后台的页面装修模块里增加一个组件。组件的主体就是一张图片,通过在图片中任意位置拖动鼠标可以创建一个矩形选区,该选区就是一个可点击的区域,所以还需要配置点击该区域后的页面跳转路径。可以创建多个选区,但选区不能重叠,也不能超出图片的边界。这个组件在用户端渲染出来后,用户可以通过点击该组件的不同区域触发不同的交互,一般是页面跳转。

需求分析

实现这个组件的思路比较简单。先创建一个 canvas,同时用自定义的图片填充这个 canvas,然后通过鼠标拖拽的方式创建选区。创建完选区后需要保存到服务端的主要数据包括图片的链接、每个选区的尺寸、选区左上角的坐标、选区的跳转链接。之后在用户端再根据这些数据在图片上创建透明的点击区域即可。

这里的重点在于如何通过拖动鼠标的方式创建可点击区域。

其实这个交互在一般的画板应用中都会看到,就是创建矩形的功能。原理也比较简单,就是监听鼠标事件。在 mouse:down 的时候记录下当前鼠标位置的坐标,然后在 mouse:up 的时候再获取一次当前坐标。有了这两组坐标就可以创建一个矩形了。当然,为了用户体验,一般还需要通过监听 mouse:move 事件在拖动鼠标的时候实时渲染一个过渡状态的选区,让用户感知到当前选区的实际位置与大小。

市面上有不少库实现了类似功能,我选用了 Fabric.js

最终结果

codesandbox.io/s/tapareacr...

需求实现

创建 canvas

既然是要在图片上创建点击区域,自然就要先创建一个与图片宽高相同的 canvas。但是由于运营实际上传的图片尺寸可能非常大,不可能在创建选区的时候直接按图片原尺寸创建 canvas,所以需要给定一个最大宽度,然后按图片的宽高比例进行缩放得到实际的 canvas 宽高。

一种实现方案是在页面里创建一个看不见的 img 元素并设置它的最大宽度和高度,加载完成后就可以通过查询这个 img 元素的宽高从而得到图片缩放后的实际宽高。也可以监听 img 元素的 load 事件,获取图片原始宽高然后根据设定的最大宽高计算得到缩放后的实际宽高。

js 复制代码
const BORDER_WIDTH = 3;

const bgImg = new fabric.Image(document.getElementById("bgImg"));
bgImg.scale(this.canvasWidth / this.imgWidth);

const canvas = new fabric.Canvas("canvas", {
  backgroundImage: bgImg,
  selectionColor: "rgba(255, 255, 255, 0)", // 鼠标框选区域背景颜色
  selectionLineWidth: BORDER_WIDTH, // 鼠标框选区域边框宽度
  selectionBorderColor: ACTIVE_COLOR, // 鼠标框选区域边框颜色
});

创建选区

创建选区主要依靠监听 canvas 对象上的 mouse:downmouse:up 事件。

mouse:down 的时候记录下当前坐标:

js 复制代码
canvas.on("mouse:down", (options) => {
  // options.e.offsetX -> 鼠标相对于 canvas 元素左上角的 x 轴距离
  // options.e.offsetY -> 鼠标相对于 canvas 元素左上角的 y 轴距离
  this.currentSelection.startX = options.e.offsetX;
  this.currentSelection.startY = options.e.offsetY;
});

mouse:up 的时候再获取当前坐标,结合 mouse:down 时候的坐标得出矩形的宽高,然后调用 fabric.Rect 构造函数创建矩形:

js 复制代码
// 获取矩形结束点的坐标
function getEndPoint(options) {
  const { x, y } = document
    .getElementById("canvas")
    ?.getBoundingClientRect() ?? { x: 0, y: 0 };
  const { pageX, pageY } = options;

  let endX = Math.min(this.canvasWidth, pageX - x) - BORDER_WIDTH; // 防止超出右边界
  endX = Math.max(endX, 0); // 防止超出左边界

  let endY = Math.min(this.canvasHeight, pageY - y) - BORDER_WIDTH; // 防止超出下边界
  endY = Math.max(endY, 0); // 防止超出上边界

  return {
    endX,
    endY,
  };
}

canvas.on("mouse:up", (options) => {
  const { endX, endY } = getEndPoint(options.e)
  
  // 创建矩形
  const rect = new fabric.Rect({
    top: this.currentSelection.startX,
    left: this.currentSelection.startY,
    width: endX - this.currentSelection.startX,
    height: endY - this.currentSelection.startY,
    fill: "rgba(255, 255, 255, 0)",
    stroke: ACTIVE_COLOR, // 边框颜色
    strokeWidth: BORDER_WIDTH, // 边框大小
    transparentCorners: false,
    lockRotation: true, // 不允许旋转
  })
});

注意上面在获取矩形选区结束点(矩形右下角)的时候,没有直接用 offsetXoffsetY 属性值,而是用 pageXpageY 属性值减去 canvas 元素自身的 xy 属性值。主要是为了处理拖动鼠标时候指针超出了 canvas 元素范围的边界情况,避免创建的选区超出 canvas 元素的可视范围。

另外需要注意选区的边框宽度对选区实际宽高的影响。我在创建 rect 的时候已经在 widthheight 里减去了边框宽度,所以通过 rect.widthrect.height 获取到的矩形的宽高不会包含边框宽度。但是如果通过 rect.getBoundingRect() 获取的 widthheight 是会包含边框宽度的。其实就跟 DOM 的 API 差不多。各位可以根据自己的需要选用 rect.width/rect.height 或者 rect.getBoundingRect()。 由于在后面计算两个选区是否有重叠以及选区是否有超出 canvas 可视范围的时候要把边框宽度也算在了选区的宽高中,所以我用 rect.getBoundingRect()。不过大家也可以不用边框来圈出选区,而是用背景颜色或者背景图,就不用考虑边框宽度对选区宽高的影响了。

获取当前选中的选区

如果想在点选了某个选区后获取对应选区的 Rect 实例,可以监听 canvas 对象的这几个事件:

  • selection:cleared:在已经选中了一个选区的情况下点击 canvas 元素的空白位置时触发,即不选中任何选区;
  • selection:updated:在已经选中了一个选区的情况下点击选中另一个选区时触发;
  • selection:created:本来没有选中选区,在选中一个选区时候触发;

在事件的回调函数中就能获取到当前选中的 Rect 实例。

处理一些边界情况

防止选区重叠

主要有三种情况可能会出现选区重叠:创建、移动、缩放选区。

这几种情况都可以在 mouse:up 事件的回调函数里处理。处理的思路就是获取所有 Rect 实例然后两两检测是否重叠,检测的方法可以参考 2D 图形的碰撞检测:

js 复制代码
function isTwoRectOverlap(rect1, rect2) {
  return !(
    rect1.left > rect2.left + rect2.width ||
    rect1.left + rect1.width < rect2.left ||
    rect1.top > rect2.top + rect2.height ||
    rect1.top + rect1.height < rect2.top
  );
}

防止选区超出 canvas 可视范围

上面在创建选区的时候就已经检查过新创建的选区是否会超出 canvas 元素的可视范围。在创建选区之后也需要防止移动、缩放选区的时候超出 canvas 元素可视范围的情况。核心思路与上面提到的差不多,这里不再赘述。详细的代码可以在文章开头的 codesandbox 链接中查看。

总结

本文主要介绍了如何利用 Fabric.js 实现一个可以在图片上创建点击区域的组件,本质上都是对 canvas 的操作。整体实现思路不难,主要卡点在于处理边界情况和 Fabric.js 的文档不够完善,需要自己慢慢调试或者看源码。

相关推荐
2601_9557600713 分钟前
如何用 Claude Opus 5 API 批量扩展长尾关键词和文章选题
前端·python·搜索引擎
KaMeidebaby18 分钟前
卡梅德生物技术快报|bli亲和力检测gst:告别批量跑胶:BLI实时酶切监测技术加速GST融合蛋白下游流程优化
前端·网络·数据库·人工智能·算法
bloglin9999936 分钟前
langchain 和 langgraph 和 react
javascript·react.js·langchain
触底反弹38 分钟前
🚀 删了数据刷新又回来?3 组件 × 4 回调 × 3 坑讲透 React 父子通信
前端·javascript·react.js
耳东小鹿2 小时前
对象常用方法
前端
程序员海军2 小时前
一个 AI 应用开发程序员的一天,都在屏幕前忙些什么?
前端·后端·程序员
程序员黑豆2 小时前
鸿蒙应用开发:Stack堆叠组件实战——实现微信消息角标效果
前端·harmonyos
夏幻灵2 小时前
浏览器原理深度解析:从输入 URL 到页面展现的全流程剖析
前端
会周易的程序员3 小时前
js-shm: 高性能 Node.js 共享内存模块
开发语言·javascript·c++·node.js·共享内存·shm
闲猫3 小时前
Agent工程实践:从WorkFlow到多Agent协作的落地笔记
java·服务器·前端