赞
踩
by:垃圾程序员
特别鸣谢:拿只键盘出来绣花的德育处主任,他的系列文章给了我很大的帮助。该说不说,站在前人的肩膀上就是得劲。
德育处主任 - 知乎拿只键盘出来绣花 回答数 7,获得 143 次赞同https://www.zhihu.com/people/rabbit-svip
fabric.js 是一个用于创建可交互式的 HTML5 canvas 应用程序的开源 JavaScript 库,它提供了一套简单、易用的 API,可以快速地实现各种图形操作和动画效果。使用 fabric.js,你可以轻松地创建文本、图像、形状、路径等多种元素,并对它们进行缩放、旋转、位移、剪切、合并等操作。
fabric.js 具有以下特点:
简单易用:fabric.js 的 API 非常简单、易于理解和使用,即使是初学者也能够快速上手。
功能强大:fabric.js 提供了各种基本图形元素的创建和操作方法,同时还支持高级功能,如复合对象、滤镜、选区、事件处理、动画等。
跨浏览器兼容:fabric.js 支持多种主流浏览器,包括 Chrome、Firefox、Safari、Edge 和 IE 等。
开源免费:fabric.js 是完全开源的,你可以自由地使用、修改和分发它。
最终实现效果:
npm install fabric --save
- import { fabric } from 'fabric'
- Vue.use(fabric);
- <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>
- //初始化画布
- 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));
- });
- },
效果如下:
- //监听画布缩放
- 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;
- });
- },
- //监听元素是否被下放到画布上
- 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)
- })
- },
- //删除元素
- 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
- }
完整代码:
- <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>
一户炊烟煮黄昏
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。