前言:啥也不说,先上效果:
思路:听说要做一个仿微信截图添加备注的效果,吓得我赶紧做了个demo,上图实现了文字备注,并修改文字颜色,修改文字大小的方法和修改颜色的实现差不多,就不赘述了。
工具:fabricjs 和 elementui
实现:
一、基本用法(本文不涉及elementui的用法,可自行到官网查看用法)
this.canvas = new fabric.Canvas('canvas') // 声明画布,与id为canvas的元素绑定
this.canvas.width = this.width // 动态设置画布宽(若画布宽高固定则不需要动态设置)
this.canvas.height = this.height // 动态设置画布高
new fabric.Image() // 设置图片,可使用变量接受实例,方便后面使用
new fabric.IText() // 设置可编辑文字(文字有好几种,这种是可以编辑的)
canvas.add(image) // fabric自带add方法,可将image示例添加到canvas画布上,添加文字同理
image.set('selectable', false) // 通过fabric实例的set方法给实例设置属性,有set方法自然也有get方法;
canvas.setActiveObject(text) // 将实例置为激活状态(也就是选中状态)
canvas.getActiveObject() // 获取当前激活对象
text.enterEditing() // 将文字置为编辑状态(可进行文字编辑)
canvas.remove(ctx) // 移除实例
text.on('selected',()=>{}) // 实例可通过on方法添加事件,selected为选中事件,当激活实例时执行
canvas.on('mouse:down',()=>{}) // 画布上鼠标按下事件
this.canvas.renderAll() // 手动执行canvas重绘
二、示例代码
Fabric.vue
<template> <div class="canvas_box"> <div class="canvas_container"> <div class="canvas_wrapper"> <canvas style="box-shadow:0px 0px 5px #ccc;" v-if="width!==''" id='canvas' :width='width' :height='height'></canvas> <img :src='url' id='img' ref='img' @load='init' style="position:absolute;top:0;left:0;opacity:0;z-index:-1;"/> </div> </div> <div class="canvas_tool"> <el-color-picker class="mt-15" v-model="color" circle @change="onChangeColor"></el-color-picker> <el-button class="mt-15" type="danger" icon="el-icon-close" circle @click="onDel"></el-button> <el-button class="mt-15" type="success" icon="el-icon-check" circle @click="save"></el-button> </div> </div> </template> <script> import { fabric } from 'fabric' export default { name: 'Fabric', props: { url: { type: String } }, data () { return { canvas: '', '', height: '', inputText: '', color: '#ff0000', fontSize: 18, ctxArr: {}, count: 0, activeIndex: null, isSelect: false, fileStearm: '', operation: 'text' } }, mounted () { }, methods: { // 图片加载完初始化画布 init () { // 获取画布宽高 this.width = this.$refs.img.offsetWidth // 宽 this.height = this.$refs.img.offsetHeight // 高 this.$nextTick(() => { console.log(this.width, this.height, this.$refs.img.src) this.canvas = new fabric.Canvas('canvas') // 声明画布 this.canvas.width = this.width // 设置画布宽 this.canvas.height = this.height // 设置画布高 const imgInstance = this.addOriginImage(this.canvas) // 添加背景图 imgInstance.set('selectable', false) // 背景图不可选择 this.onMouseDown(this.canvas) // 绑定点击新增文字事件 }) }, addOriginImage (canvas) { const imgInstance = new fabric.Image(this.$refs.img, {// 设置图片位置和样子 left: 0, top: 0, this.width, height: this.height, angle: 0 // 设置图形顺时针旋转角度 }) canvas.add(imgInstance) // 加入到canvas中 return imgInstance }, // 添加文字 addText (canvas, color, pos) { let text = new fabric.IText('', { borderColor: '#ff0000', // 激活状态时的边框颜色 editingBorderColor: '#ff0000', // 文本对象的边框颜色,当它处于编辑模式时 left: pos.x, top: pos.y - 10, transparentCorners: true, fontSize: 14, fill: color || '#ff0000', padding: 5, cornerSize: 5, // Size of object's controlling corners cornerColor: '#ff0000', rotatingPointOffset: 20, // Offset for object's controlling rotating point lockScalingFlip: true, // 不能通过缩放为负值来翻转对象 lockUniScaling: true // 对象非均匀缩放被锁定 }) text.id = this.count // 绑定选中事件 text.on('selected', () => { this.activeIndex = text.id this.isSelect = true }) canvas.add(text).setActiveObject(text) // 添加文字到画布上,并将文字置为激活状态 text.enterEditing() // 将文字置为编辑状态 this.activeIndex = text.id this.ctxArr[this.count] = text this.count++ }, delText (canvas, ctx) { canvas.remove(ctx) }, // 添加箭头 // addArrow (canvas) { // const sp = { // x: 0, // y: 30 // } // const ep = { // x: 200, // y: 30 // } // const p = `M ${sp.x} ${sp.y} ` // let path = new fabric.Path('M 0 20 L 30 0 L 27 10 L 200 20 L 27 30 L 30 40 z') // path.set({ left: 0, top: 0 }) // path.id = this.count // // 绑定选中事件 // path.on('selected', () => { // this.activeIndex = path.id // this.isSelect = true // }) // canvas.add(path).setActiveObject(path) // 添加文字到画布上,并将文字置为激活状态 // this.activeIndex = path.id // this.ctxArr[this.count] = path // this.count++ // }, /** * 点击事件: * 1.画布上无选中元素,点击空白处添加文字 * 2.画布上有选中元素,点击空白处,选中元素失去焦点 * 3.画布上有选中元素,点击选中元素,进行文字编辑 */ onMouseDown (canvas) { canvas.on('mouse:down', (opt) => { console.log('this.activeIndex', this.activeIndex) const pos = opt.absolutePointer // 执行文字操作 const isText = () => { if (this.activeIndex === null) { // 如果当前没有选中元素,点击空白处添加文字 this.addText(canvas, this.color, pos) } else { // 获取当前激活对象 const o = canvas.getActiveObject() if (!o) { this.activeIndex = null }; } } switch (this.operation) { case 'text': isText(); break default: isText() }; }) }, onDel () { this.delText(this.canvas, this.ctxArr[this.activeIndex]) delete this.ctxArr[this.activeIndex] }, onChangeColor () { // 获取当前激活对象 const o = this.canvas.getActiveObject() if (o) { console.log('this.color', this.color) o.set('fill', this.color) this.canvas.renderAll() }; }, save () { // const url = this.canvas.toDataURL({ // format: 'jpeg', // quality: 1 // }) const url = this.canvas.toDataURL() var blob = this.dataURLtoBlob(url) var file = this.blobToFile(blob, '截图.png') console.log(url) console.log(file) this.fileStearm = file // 组装a标签 let elink = document.createElement('a') // 设置下载文件名 elink.download = '截图.png' elink.style.display = 'none' elink.href = URL.createObjectURL(blob) document.body.appendChild(elink) elink.click() document.body.removeChild(elink) }, // 将base64转换为blob dataURLtoBlob: function (dataurl) { var arr = dataurl.split(',') var mime = arr[0].match(/:(.*?);/)[1] var bstr = atob(arr[1]) var n = bstr.length var u8arr = new Uint8Array(n) while (n--) { u8arr[n] = bstr.charCodeAt(n) } return new Blob([u8arr], { type: mime }) }, // 将blob转换为file blobToFile: function (theBlob, fileName) { theBlob.lastModifiedDate = new Date() theBlob.name = fileName return theBlob } } } </script> <style scoped lang="scss"> .canvas_box{ 100%; height:100%; .canvas_tool{ 80px; height:100%; box-sizing:border-box; padding:20px; display: flex; flex-direction: column; justify-content: flex-end; box-shadow: 0 0 10px #ccc; position:fixed; right:0; top:0; background:white; } .canvas_container{ 100%; height:100%; box-sizing: border-box; padding: 0px 80px 0px 0px; .canvas_wrapper{ 100%; height:100%; position:relative; display:flex; flex-direction:row; justify-content:center; align-items:center; overflow: scroll; } } .el-button+.el-button{ margin-left: 0 !important; } .mt-15{ margin-top:15px; } } </style>
三、思路
相对麻烦的功能是鼠标点击的事件,会有三种情况需要识别:
1、初次点击,画布上没有示例,这时需要添加文字示例;
2、再次点击
2.1、如点击到实例,我们不能添加实例,而是在点击上的示例上面继续操作;
2.2、如没有点击到实例,说明点击在了空白地方,这时画布的默认动作会取消掉激活对象;
上面三种情况可以通过this.activeIndex去判断。
修改文字大小和颜色的功能只需要将对应的参数用变量取代即可实现。
最后:为了方便今后回顾,注释代码都有,需要时便再来取,更多高级功能将来有机会再深入研究。