首页 前端知识 fabric.js 实现元素拖拽、引入图片、标注交互

fabric.js 实现元素拖拽、引入图片、标注交互

2024-08-21 10:08:53 前端知识 前端哥 614 338 我要收藏

by:垃圾程序员

零、前言

特别鸣谢:拿只键盘出来绣花的德育处主任,他的系列文章给了我很大的帮助。该说不说,站在前人的肩膀上就是得劲。

德育处主任 - 知乎拿只键盘出来绣花 回答数 7,获得 143 次赞同icon-default.png?t=N7T8https://www.zhihu.com/people/rabbit-svip

fabric.js 是一个用于创建可交互式的 HTML5 canvas 应用程序的开源 JavaScript 库,它提供了一套简单、易用的 API,可以快速地实现各种图形操作和动画效果。使用 fabric.js,你可以轻松地创建文本、图像、形状、路径等多种元素,并对它们进行缩放、旋转、位移、剪切、合并等操作。

fabric.js 具有以下特点:

  1. 简单易用:fabric.js 的 API 非常简单、易于理解和使用,即使是初学者也能够快速上手。

  2. 功能强大:fabric.js 提供了各种基本图形元素的创建和操作方法,同时还支持高级功能,如复合对象、滤镜、选区、事件处理、动画等。

  3. 跨浏览器兼容:fabric.js 支持多种主流浏览器,包括 Chrome、Firefox、Safari、Edge 和 IE 等。

  4. 开源免费:fabric.js 是完全开源的,你可以自由地使用、修改和分发它。

最终实现效果:

一、引入 fabric.js 库

1.1npm 安装

npm install fabric --save

1.2全局引入

import { fabric } from 'fabric'
Vue.use(fabric);

二、页面布局

2.1创建左右结构的布局

<template>
  <div class="mainDiv rowflex">
    <!-- 左侧元素区 -->
    <div class="leftFiv columnflex" style="justify-content: start;">
      <!-- 循环所有图标 -->
      <div class="element columnflex" v-for="(item, index) in elementList" :key="index">
        <img class="element_item" :src="item.address" @dragstart="handleDragStart(index)" />
      </div>
    </div>
    <!-- 画布区 -->
    <div id="rightDiv" class="rightDiv">
      <canvas id="fabric"></canvas>
    </div>
    <!-- 右键菜单 -->
    <div id="menu" class="menu-x" v-show="menuDisplay">
      <div class="menu-li" @click="bindingIdentifier()">绑定唯一标识</div>
      <div class="menu-li" @click="deleteElement()">删除</div>
    </div>
  </div>
</template>

2.2画布初始化并加载底图

//初始化画布
      initCanvas() {
        // 获取容器元素,得到父容器的宽和高
        const container = document.getElementById('rightDiv');
        // 创建一个与容器宽高一致的 canvas 对象
        this.canvas = new fabric.Canvas("fabric", {
          width: container.clientWidth,
          height: container.clientHeight,
          fireRightClick: true, // 启用右键,button的数字为3
          stopContextMenu: true, // 禁止默认右键菜单
        })
        // 设置画布的背景图片
        this.setBackground(container.clientWidth, container.clientHeight)
        //监听元素是否被下放到画布上
        this.elementPlacement()
        //监听画布拖拽,包含三个监听事件
        this.canvasDragAndDrop()
        //监听画布缩放
        this.canvasZoom()
      },
      //设置画布的背景图片
      setBackground(clientWidth, clientHeight) {
        fabric.Image.fromURL(this.canvasBackgroundImage, img => {
          // 缩放背景图片以适应画布
          // 如果背景图片宽度或高度大于画布宽度或高度
          if (img.width > clientWidth || img.height > clientHeight) {
            // 计算缩放比例
            let scaleX = clientWidth / img.width;
            let scaleY = clientHeight / img.height;
            let scale = Math.min(scaleX, scaleY);
            // 缩放背景图片
            img.scaleToWidth(img.width * scale);
            img.scaleToHeight(img.height * scale);
          } else { // 如果背景图片宽度和高度都小于或等于画布宽度和高度
            // 计算背景图片在画布中的位置
            img.left = (clientWidth - img.width) / 2;
            img.top = (clientHeight - img.height) / 2;
          }
          this.canvas.setBackgroundImage(img, this.canvas.renderAll.bind(this.canvas));
        });
      },

效果如下:

三、实现画布缩放和移动

3.1画布缩放

//监听画布缩放
      canvasZoom() {
        this.canvas.on('mouse:wheel', opt => {
          const delta = opt.e.deltaY // 滚轮,向上滚一下是 -100,向下滚一下是 100
          let zoom = this.canvas.getZoom() // 获取画布当前缩放值
          zoom *= 0.999 ** delta
          if (zoom > 20) zoom = 20 // 限制最大缩放级别
          if (zoom < 0.01) zoom = 0.01 // 限制最小缩放级别
          // 以鼠标所在位置为原点缩放
          this.canvas.zoomToPoint({ // 关键点
              x: opt.e.offsetX,
              y: opt.e.offsetY
            },
            zoom // 传入修改后的缩放级别
          )
        })
      },

3.2画布移动

// 监听画布拖拽,同时也监听了右键菜单
      canvasDragAndDrop() {
        // 按下鼠标事件
        this.canvas.on("mouse:down", opt => {
          var evt = opt.e
          // 判断:右键,且在元素上右键
          // opt.button: 1-左键;2-中键;3-右键
          // 在画布上点击:opt.target 为 null
          if (opt.button === 3 && opt.target) {
            this.lastMenu = opt.target
            let menu = document.getElementById('menu');
            // 禁止在菜单上的默认右键事件
            menu.oncontextmenu = function(e) {
              e.preventDefault()
            }
            // 显示菜单,设置右键菜单位置
            // 获取菜单组件的宽高
            //这个地方是我自己设置的宽和高计算的,如果你之后复制过去需要修改一下
            const menuWidth = 120
            const menuHeight = menu.childNodes.length * 40
            // 当前鼠标位置
            let pointX = opt.pointer.x
            let pointY = opt.pointer.y
            // 计算菜单出现的位置
            // 如果鼠标靠近画布底部,菜单就出现在鼠标指针上方
            if (this.canvas.height - pointY <= menuHeight) {
              pointY -= menuHeight
            }
            menu.style = `
                  visibility: visible;
                  left: ${pointX}px;
                  top: ${pointY}px;
                  z-index: 100;
                `
            // 将菜单展示
            this.menuDisplay = true
          } else {
            // 将菜单隐藏
            this.menuDisplay = false
          }
          //拖拽
          if (evt.shiftKey === true) {
            this.isDragging = true
            this.canvas.selection = false;
          }
        });
        // 移动鼠标事件
        this.canvas.on("mouse:move", opt => {
          if (this.isDragging && opt && opt.e) {
            var delta = new fabric.Point(opt.e.movementX, opt.e.movementY);
            this.canvas.relativePan(delta);
          }
        });
        // 松开鼠标事件
        this.canvas.on("mouse:up", opt => {
          this.isDragging = false;
          this.canvas.selection = true;
        });
      },

四、实现拖拽元素到画布

4.1监听拖到画布上

 //监听元素是否被下放到画布上
      elementPlacement() {
        this.canvas.on('drop', elt => {
          // 画布元素距离浏览器左侧和顶部的距离
          let offset = {
            left: this.canvas.getSelectionElement().getBoundingClientRect().left,
            top: this.canvas.getSelectionElement().getBoundingClientRect().top
          }
          // 鼠标坐标转换成画布的坐标(未经过缩放和平移的坐标)
          let point = {
            x: elt.e.x - offset.left,
            y: elt.e.y - offset.top,
          }
          // 转换后的坐标,restorePointerVpt 不受视窗变换的影响
          let pointerVpt = this.canvas.restorePointerVpt(point)
          //创建元素
          this.createElement(this.imageAddress, pointerVpt)
        });
      },

4.2生成元素

 //在画布上生成拖拽过来的元素
      createElement(imageAddress, pointerVpt) {
        fabric.Image.fromURL(imageAddress, oImg => {
          //这个地方做了一下偏移,让鼠标位置为图标的中心,真实的位置信息要还原回去
          oImg.top = pointerVpt.y - 24
          oImg.left = pointerVpt.x - 24
          this.canvas.add(oImg)
        })
      },

五、实现元素右键交互

5.2删除画布元素

//删除元素
      deleteElement() {
        this.canvas.remove(this.lastMenu)
        // 将菜单隐藏
        this.menuDisplay = false
      },

5.3弹窗输入编码

      //设定唯一标识
      bindingIdentifier() {
        this.$prompt('请输入唯一编码', '编辑', {
          confirmButtonText: '确定',
          cancelButtonText: '取消'
        }).then(({
          value
        }) => {
          this.$message({
            type: 'success',
            message: '你的唯一编码是: ' + value
          });
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '取消输入'
          });
        });
        // 将菜单隐藏
        this.menuDisplay = false
      }

完整代码:

<template>
  <div class="mainDiv rowflex">
    <!-- 左侧元素区 -->
    <div class="leftFiv columnflex" style="justify-content: start;">
      <!-- 循环所有图标 -->
      <div class="element columnflex" v-for="(item, index) in elementList" :key="index">
        <img class="element_item" :src="item.address" @dragstart="handleDragStart(index)" />
      </div>
    </div>
    <!-- 画布区 -->
    <div id="rightDiv" class="rightDiv">
      <canvas id="fabric"></canvas>
    </div>
    <!-- 右键菜单 -->
    <div id="menu" class="menu-x" v-show="menuDisplay">
      <div class="menu-li" @click="bindingIdentifier()">绑定唯一标识</div>
      <div class="menu-li" @click="deleteElement()">删除</div>
    </div>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        // 图标列表
        elementList: [{
            key: 'ancientTrees',
            address: require('@/assets/icons/fabric/ancientTrees.png')
          },
          {
            key: 'camera',
            address: require('@/assets/icons/fabric/camera.png')
          },
          {
            key: 'factory',
            address: require('@/assets/icons/fabric/factory.png')
          },
          {
            key: 'fireFighting',
            address: require('@/assets/icons/fabric/fireFighting.png')
          },
          {
            key: 'house',
            address: require('@/assets/icons/fabric/house.png')
          },
          {
            key: 'hydrology',
            address: require('@/assets/icons/fabric/hydrology.png')
          },
        ],
        // 画布背景图片
        canvasBackgroundImage: require('@/assets/icons/fabric/canvasImg.jpg'),
        // 画布
        canvas: null,
        //最后一次拖动的图片
        imageAddress: '',
        //是否拖动中
        isDragging: false,
        //最后的x轴方向的位置
        lastPosX: 0,
        //最后的y轴方向的位置
        lastPosY: 0,
        //菜单是否显示
        menuDisplay: false,
        //最后一次右键选中的元素
        lastMenu: null
      }
    },
    mounted() {
      this.initCanvas();
    },
    methods: {
      //初始化画布
      initCanvas() {
        // 获取容器元素,得到父容器的宽和高
        const container = document.getElementById('rightDiv');
        // 创建一个与容器宽高一致的 canvas 对象
        this.canvas = new fabric.Canvas("fabric", {
          width: container.clientWidth,
          height: container.clientHeight,
          fireRightClick: true, // 启用右键,button的数字为3
          stopContextMenu: true, // 禁止默认右键菜单
        })
        // 设置画布的背景图片
        this.setBackground(container.clientWidth, container.clientHeight)
        //监听元素是否被下放到画布上
        this.elementPlacement()
        //监听画布拖拽,包含三个监听事件
        this.canvasDragAndDrop()
        //监听画布缩放
        this.canvasZoom()
      },
      //设置画布的背景图片
      setBackground(clientWidth, clientHeight) {
        fabric.Image.fromURL(this.canvasBackgroundImage, img => {
          // 缩放背景图片以适应画布
          // 如果背景图片宽度或高度大于画布宽度或高度
          if (img.width > clientWidth || img.height > clientHeight) {
            // 计算缩放比例
            let scaleX = clientWidth / img.width;
            let scaleY = clientHeight / img.height;
            let scale = Math.min(scaleX, scaleY);
            // 缩放背景图片
            img.scaleToWidth(img.width * scale);
            img.scaleToHeight(img.height * scale);
          } else { // 如果背景图片宽度和高度都小于或等于画布宽度和高度
            // 计算背景图片在画布中的位置
            img.left = (clientWidth - img.width) / 2;
            img.top = (clientHeight - img.height) / 2;
          }
          this.canvas.setBackgroundImage(img, this.canvas.renderAll.bind(this.canvas));
        });
      },
      //监听被拖动元素的图片
      handleDragStart(event) {
        this.imageAddress = this.elementList[event].address
      },
      //监听元素是否被下放到画布上
      elementPlacement() {
        this.canvas.on('drop', elt => {
          // 画布元素距离浏览器左侧和顶部的距离
          let offset = {
            left: this.canvas.getSelectionElement().getBoundingClientRect().left,
            top: this.canvas.getSelectionElement().getBoundingClientRect().top
          }
          // 鼠标坐标转换成画布的坐标(未经过缩放和平移的坐标)
          let point = {
            x: elt.e.x - offset.left,
            y: elt.e.y - offset.top,
          }
          // 转换后的坐标,restorePointerVpt 不受视窗变换的影响
          let pointerVpt = this.canvas.restorePointerVpt(point)
          //创建元素
          this.createElement(this.imageAddress, pointerVpt)
        });
      },
      //在画布上生成拖拽过来的元素
      createElement(imageAddress, pointerVpt) {
        fabric.Image.fromURL(imageAddress, oImg => {
          //这个地方做了一下偏移,让鼠标位置为图标的中心,真实的位置信息要还原回去
          oImg.top = pointerVpt.y - 24
          oImg.left = pointerVpt.x - 24
          this.canvas.add(oImg)
        })
      },
      //监听画布缩放
      canvasZoom() {
        this.canvas.on('mouse:wheel', opt => {
          const delta = opt.e.deltaY // 滚轮,向上滚一下是 -100,向下滚一下是 100
          let zoom = this.canvas.getZoom() // 获取画布当前缩放值
          zoom *= 0.999 ** delta
          if (zoom > 20) zoom = 20 // 限制最大缩放级别
          if (zoom < 0.01) zoom = 0.01 // 限制最小缩放级别
          // 以鼠标所在位置为原点缩放
          this.canvas.zoomToPoint({ // 关键点
              x: opt.e.offsetX,
              y: opt.e.offsetY
            },
            zoom // 传入修改后的缩放级别
          )
        })
      },
      // 监听画布拖拽,同时也监听了右键菜单
      canvasDragAndDrop() {
        // 按下鼠标事件
        this.canvas.on("mouse:down", opt => {
          var evt = opt.e
          // 判断:右键,且在元素上右键
          // opt.button: 1-左键;2-中键;3-右键
          // 在画布上点击:opt.target 为 null
          if (opt.button === 3 && opt.target) {
            this.lastMenu = opt.target
            let menu = document.getElementById('menu');
            // 禁止在菜单上的默认右键事件
            menu.oncontextmenu = function(e) {
              e.preventDefault()
            }
            // 显示菜单,设置右键菜单位置
            // 获取菜单组件的宽高
            //这个地方是我自己设置的宽和高计算的,如果你之后复制过去需要修改一下
            const menuWidth = 120
            const menuHeight = menu.childNodes.length * 40
            // 当前鼠标位置
            let pointX = opt.pointer.x
            let pointY = opt.pointer.y
            // 计算菜单出现的位置
            // 如果鼠标靠近画布底部,菜单就出现在鼠标指针上方
            if (this.canvas.height - pointY <= menuHeight) {
              pointY -= menuHeight
            }
            menu.style = `
                  visibility: visible;
                  left: ${pointX}px;
                  top: ${pointY}px;
                  z-index: 100;
                `
            // 将菜单展示
            this.menuDisplay = true
          } else {
            // 将菜单隐藏
            this.menuDisplay = false
          }
          //拖拽
          if (evt.shiftKey === true) {
            this.isDragging = true
            this.canvas.selection = false;
          }
        });
        // 移动鼠标事件
        this.canvas.on("mouse:move", opt => {
          if (this.isDragging && opt && opt.e) {
            var delta = new fabric.Point(opt.e.movementX, opt.e.movementY);
            this.canvas.relativePan(delta);
          }
        });
        // 松开鼠标事件
        this.canvas.on("mouse:up", opt => {
          this.isDragging = false;
          this.canvas.selection = true;
        });
      },
      //删除元素
      deleteElement() {
        this.canvas.remove(this.lastMenu)
        // 将菜单隐藏
        this.menuDisplay = false
      },
      //设定唯一标识
      bindingIdentifier() {
        this.$prompt('请输入唯一编码', '编辑', {
          confirmButtonText: '确定',
          cancelButtonText: '取消'
        }).then(({
          value
        }) => {
          this.$message({
            type: 'success',
            message: '你的唯一编码是: ' + value
          });
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '取消输入'
          });
        });
        // 将菜单隐藏
        this.menuDisplay = false
      }
    }
  }
</script>

<style lang="scss">
  // flex横向布局
  .rowflex {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
  }

  // flex纵向布局
  .columnflex {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }

  // 主div
  .mainDiv {
    height: calc(100vh - 50px) !important;
    width: 100%;
    min-width: 1000px;
  }

  /* 左侧元素区 */
  .leftFiv {
    height: 100%;
    width: 6%;
    min-width: 60px;
    border-right: solid 1px #eee;

    // 元素块
    .element {
      height: 6.5%;
      width: 60%;
      min-height: 48px;
      min-width: 48px;
      margin-top: 20px;

      .element_item {
        height: 90%;
        width: 90%;
      }
    }
  }

  /* 右侧画布区 */
  .rightDiv {
    height: 100%;
    width: 94%;
    min-width: 940px;
  }

  .menu-x {
    z-index: -100;
    position: absolute;
    top: 0;
    left: 0;
    box-sizing: border-box;
    border-radius: 4px;
    box-shadow: 0 0 4px rgba(0, 0, 0, 0.3);
    background-color: #fff;
  }

  /* 菜单每个选项 */
  .menu-li {
    box-sizing: border-box;
    padding: 4px 8px;
    border-bottom: 1px solid #ccc;
    cursor: pointer;
    line-height: 30px;
    height: 40px;
    width: 120px;
  }

  /* 鼠标经过的选项,更改背景色 */
  .menu-li:hover {
    background-color: antiquewhite;
  }

  /* 第一个选项,顶部两角是圆角 */
  .menu-li:first-child {
    border-top-left-radius: 4px;
    border-top-right-radius: 4px;
  }

  /* 最后一个选项,底部两角是圆角,底部不需要边框 */
  .menu-li:last-child {
    border-bottom: none;
    border-bottom-left-radius: 4px;
    border-bottom-right-radius: 4px;
  }
</style>

一户炊烟煮黄昏

转载请注明出处或者链接地址:https://www.qianduange.cn//article/16350.html
评论
发布的文章

CSS3 动画

2024-04-17 21:04:24

JWT(JSON Web Token)

2024-08-30 03:08:56

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!