前言:啥也不說,先上效果:
思路:聽說要做一個仿微信截圖添加備注的效果,嚇得我趕緊做了個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: '', width: '', 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, width: 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{ width:100%; height:100%; .canvas_tool{ width: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{ width:100%; height:100%; box-sizing: border-box; padding: 0px 80px 0px 0px; .canvas_wrapper{ width: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去判斷。
修改文字大小和顏色的功能只需要將對應的參數用變量取代即可實現。
最后:為了方便今后回顧,注釋代碼都有,需要時便再來取,更多高級功能將來有機會再深入研究。