
<template> <div class="wraper" id="sketchpad-template" ref="wraper"> <div class="canvas-wraper"> <canvas id="canvas" ref="canvas"></canvas> </div> <div class="controlPanel"> <div class="text"> <p>視點塗鴉工具</p> </div> <div class="tool"> <div :class="[initIdx==idx ? 'contro-item active' : 'contro-item']" v-for="(item,idx) in toolsArr" :key="idx" @click="handleTools(item, idx)"> <i :class="'iconfont' + item.icon" :title="item.title"></i> </div> <colorPicker class="color-list" v-model="drawColor" v-on:change="headleChangeColor"/> <Slider v-model="drawWidth" :min=0 :max=50 style="width:150px;padding:0px 10px"></Slider> </div> </div> <div class="download"> <img :src="imageBase64" v-show="imageBase64!=''" alt=""> </div> </div> </template> <script> import {fabric} from 'fabric';
import {Slider} from 'element-ui'; import { setTimeout } from 'timers'; export default { name:'Sketchpad', props: { imgSrc: { type: String }, canvasHeight:{ type:Number } }, data() { return { onRemoveVaule:false, onStartvalue:false, currentTool: '', done: false, fabricObj: null, initIdx: 0, toolsArr: [{ name: 'pencil', icon: ' iconhuabi', title:'鉛筆' }, { name: 'line', icon: ' iconline', title:'直線' }, { name: 'arrow', icon: ' iconhj2', title:'箭頭' }, { name: 'xuxian', icon: ' icon-xiaoxuxian', title:'虛線' }, { name: 'text', icon: ' iconwenzi1', title:'文本' }, { name: 'juxing', icon: ' iconjuxing', title:'矩形' }, { name: 'cricle', icon: ' iconyuanxingxuankuang', title:'圓形' }, { name: 'ellipse', icon: ' icontuoyuanxing', title:'橢圓形' }, { name: 'equilateral', //三角形 icon: ' iconxingzhuang-sanjiaoxing', title:'三角形' }, { name: 'remove', icon: ' iconqingchu1', title:'清除' }, { name: 'undo', icon: ' iconzhixiang-houtui', title:'上一步' }, { name: 'redo', icon: ' iconzhixiang-qianjin', title:'下一步' }, { name: 'reset', icon: ' iconzhongzhi', title:'重置' }, { name: 'test', icon: ' iconicon-test', title:'選中' }, { name: 'baocun', icon: ' iconbaocun', title:'保存' }], mouseFrom:{}, mouseTo:{}, moveCount: 1, doDrawing: false, fabricHistoryJson: [], mods: 0, drawingObject: null, //繪制對象 // drawColor: '#E34F51', drawWidth: 4, drawColor:'#E34F51', imageBase64: '', zoom: window.zoom ? window.zoom : 1, } }, components:{ Slider }, mounted() { //初始化canvas setTimeout(()=>{ this.initCanvas() },500) }, watch:{ 'drawWidth': function(newVal){ this.drawWidth = newVal; this.fabricObj.freeDrawingBrush.width = newVal }, }, computed:{ canvasWidth() { return window.innerWidth } }, methods:{ initCanvas() { // let _this = this; this.fabricObj = new fabric.Canvas('canvas',{ isDrawingMode: true, // 開啟畫筆 selectable: false, selection: false, // perPixelTargetFind: true, devicePixelRatio:true, //Retina 高清屏 屏幕支持 }) this.fabricObj.setBackgroundImage(this.imgSrc, this.fabricObj.renderAll.bind(this.fabricObj), { backgroundImageOpacity: 0.5, // should the image be resized to fit the container? backgroundImageStretch: false }); // console.log(this.$parent.canvasHeight) this.fabricObj.freeDrawingBrush.color = '#E34F51' this.fabricObj.freeDrawingBrush.width = this.drawWidth this.fabricObj.setWidth(this.canvasWidth) this.fabricObj.setHeight(this.$parent.canvasHeight) console.log(this.canvasWidth,this.$parent.canvasHeight) this.fabricObj.add( // new fabric.Rect({ top: 100, left: 100, width: 50, height: 50, fill: '#f55' }), // new fabric.Circle({ top: 140, left: 230, radius: 75, fill: 'green' }), // new fabric.Triangle({ top: 300, left: 210, width: 100, height: 100, fill: 'blue' }), ); //綁定畫板事件 this.fabricObjAddEvent() // console.log(this.fabricObj._onSelected) }, //事件監聽 fabricObjAddEvent() { this.fabricObj.on({ 'mouse:down': (o)=> { this.mouseFrom.x = o.pointer.x; this.mouseFrom.y = o.pointer.y; this.doDrawing = true; if(this.currentTool=='text') { this.drawText() } }, 'mouse:up': (o)=> { this.mouseTo.x = o.pointer.x; this.mouseTo.y = o.pointer.y; this.drawingObject = null; this.moveCount = 1; this.doDrawing = false; this.updateModifications(true); }, 'mouse:move': (o)=> { if (this.moveCount % 2 && !this.doDrawing) { //減少繪制頻率 return; } this.moveCount++; this.mouseTo.x = o.pointer.x; this.mouseTo.y = o.pointer.y; if(this.onStartvalue){ this.drawing(); } }, //對象移動事件 'object:moving': (e)=> { e.target.opacity = 0.5; }, 'object:added':(e)=>{ console.log(e) }, //增加對象 'object:added': ()=>{ // debugger }, 'object:modified':(e)=> { e.target.opacity = 1; let object = e.target; this.updateModifications(true) }, 'selection:created': (e)=>{ console.log(e) if(this.onRemoveVaule){ if (e.target._objects) { //多選刪除 var etCount = e.target._objects.length; for (var etindex = 0; etindex < etCount; etindex++) { this.fabricObj.remove(e.target._objects[etindex]); } } else { //單選刪除 this.fabricObj.remove(e.target); } this.fabricObj.discardActiveObject(); //清除選中框 this.updateModifications(true) }else { // this.onRemoveVaule = false; // this.fabricObj.isDrawingMode = false; // this.fabricObj.skipTargetFind = false; } }, }); }, //儲存歷史記錄 updateModifications(savehistory) { if(savehistory==true) { this.fabricHistoryJson.push(JSON.stringify(this.fabricObj)) } }, //canvas 歷史后退 undo() { let state = this.fabricHistoryJson if(this.mods < state.length) { this.fabricObj.clear().renderAll(); this.fabricObj.loadFromJSON(state[state.length - 1 - this.mods - 1]); this.fabricObj.renderAll(); this.mods += 1; if(this.mods == state.length){ this.fabricObj.setBackgroundImage(this.imgSrc, this.fabricObj.renderAll.bind(this.fabricObj), { // Optionally add an opacity lvl to the image backgroundImageOpacity: 0.5, // should the image be resized to fit the container? backgroundImageStretch: false }); } } }, //前進 redo() { let state = this.fabricHistoryJson if (this.mods > 0) { this.fabricObj.clear().renderAll(); this.fabricObj.loadFromJSON(state[state.length - 1 - this.mods + 1]); this.fabricObj.renderAll(); this.mods -= 1; } }, transformMouse(mouseX, mouseY) { return { x: mouseX / this.zoom, y: mouseY / this.zoom }; }, resetObj() { this.fabricObj.selectable = false this.fabricObj.selection = false this.fabricObj.skipTargetFind = true //清除文字對象 if(this.textboxObj) { this.textboxObj.exitEditing(); this.textboxObj = null; } }, handleTools(tools, idx) { console.log(tools.name) this.initIdx = idx; this.currentTool = tools.name; this.fabricObj.isDrawingMode = false; this.resetObj() switch(tools.name) { case 'pencil': this.fabricObj.isDrawingMode = true; break; case 'remove': this.fabricObj.discardActiveObject().renderAll(); //清除畫布選中元素 this.onRemoveVaule = true; this.fabricObj.selection = true this.fabricObj.skipTargetFind = false this.fabricObj.selectable = true break; case 'reset': this.onStartvalue = true; this.fabricObj.clear(); this.fabricObj.setBackgroundImage(this.imgSrc, this.fabricObj.renderAll.bind(this.fabricObj), { // Optionally add an opacity lvl to the image backgroundImageOpacity: 0.5, // should the image be resized to fit the container? backgroundImageStretch: false }); break; case 'redo': this.onStartvalue = true; this.redo(); break; case 'undo': this.onStartvalue = true; this.undo(); break; case 'xuxian': this.onStartvalue = true; break; case 'arrow': this.onStartvalue = true; break; case 'line': this.onStartvalue = true; break; case 'juxing': this.onStartvalue = true; break; case 'cricle': this.onStartvalue = true; break; case 'ellipse': this.onStartvalue = true; break; case 'equilateral': this.onStartvalue = true; break; case 'test': this.onSelect() break; case 'baocun': this.downLoadImage() break; default: break; } }, //繪制文字對象 drawText() { this.textboxObj = new fabric.Textbox(" ", { left: this.mouseFrom.x, top: this.mouseFrom.y, width: 220, fontSize: 22, fill: this.drawColor, hasControls: true }); this.fabricObj.add(this.textboxObj); this.textboxObj.enterEditing(); this.textboxObj.hiddenTextarea.focus(); this.updateModifications(true) }, drawing() { // let _this = this; if(this.drawingObject) { this.fabricObj.remove(this.drawingObject) } let fabricObject = null switch (this.currentTool) { case 'pencil': this.fabricObj.isDrawingMode = true break; case 'line': fabricObject = new fabric.Line([this.mouseFrom.x,this.mouseFrom.y,this.mouseTo.x,this.mouseTo.y],{ stroke: this.drawColor, strokeWidth: this.drawWidth }) break; case 'arrow': fabricObject = new fabric.Path(this.drawArrow(this.mouseFrom.x, this.mouseFrom.y, this.mouseTo.x, this.mouseTo.y, 17.5, 17.5), { stroke: this.drawColor, fill: "rgba(255,255,255,0)", strokeWidth: this.drawWidth }); break; case 'xuxian': //doshed line fabricObject = new fabric.Line([this.mouseFrom.x, this.mouseFrom.y, this.mouseTo.x, this.mouseTo.y],{ strokeDashArray: [10, 3], stroke: this.drawColor, strokeWidth: this.drawWidth }) break; case 'juxing': //矩形 var path = "M " + this.mouseFrom.x + " " + this.mouseFrom.y + " L " + this.mouseTo.x + " " + this.mouseFrom.y + " L " + this.mouseTo.x + " " + this.mouseTo.y + " L " + this.mouseFrom.x + " " + this.mouseTo.y + " L " + this.mouseFrom.x + " " + this.mouseFrom.y + " z"; fabricObject = new fabric.Path(path, { left: this.mouseFrom.x, top: this.mouseFrom.y, stroke: this.drawColor, strokeWidth: this.drawWidth, fill: "rgba(255, 255, 255, 0)" }); break; case "cricle": //正圓 var radius = Math.sqrt((this.mouseTo.x - this.mouseFrom.x) * (this.mouseTo.x - this.mouseFrom.x) + (this.mouseTo.y - this.mouseFrom.y) * (this.mouseTo.y - this.mouseFrom.y)) / 2; fabricObject = new fabric.Circle({ left: this.mouseFrom.x, top: this.mouseFrom.y, stroke: this.drawColor, fill: "rgba(255, 255, 255, 0)", radius: radius, strokeWidth: this.drawWidth }); break; case "ellipse": //橢圓 var left = this.mouseFrom.x; var top = this.mouseFrom.y; var ellipse = Math.sqrt((this.mouseTo.x - left) * (this.mouseTo.x - left) + (this.mouseTo.y - top) * (this.mouseTo.y - top)) / 2; fabricObject = new fabric.Ellipse({ left: left, top: top, stroke: this.drawColor, fill: "rgba(255, 255, 255, 0)", originX: "center", originY: "center", rx: Math.abs(left - this.mouseTo.x), ry: Math.abs(top - this.mouseTo.y), strokeWidth: this.drawWidth }); break; case "equilateral": //等邊三角形 var height = this.mouseTo.y - this.mouseFrom.y; fabricObject = new fabric.Triangle({ top: this.mouseFrom.y, left: this.mouseFrom.x, width: Math.sqrt(Math.pow(height, 2) + Math.pow(height / 2.0, 2)), height: height, stroke: this.drawColor, strokeWidth: this.drawWidth, fill: "rgba(255,255,255,0)" }); break; case 'remove': break; default: // statements_def' break; } if(fabricObject) { this.fabricObj.add(fabricObject) this.drawingObject = fabricObject; } }, //書寫箭頭的方法 drawArrow(fromX, fromY, toX, toY, theta, headlen) { theta = typeof theta != "undefined" ? theta : 30; headlen = typeof theta != "undefined" ? headlen : 10; // 計算各角度和對應的P2,P3坐標 let angle = Math.atan2(fromY - toY, fromX - toX) * 180 / Math.PI, angle1 = (angle + theta) * Math.PI / 180, angle2 = (angle - theta) * Math.PI / 180, topX = headlen * Math.cos(angle1), topY = headlen * Math.sin(angle1), botX = headlen * Math.cos(angle2), botY = headlen * Math.sin(angle2); let arrowX = fromX - topX, arrowY = fromY - topY; let path = " M " + fromX + " " + fromY; path += " L " + toX + " " + toY; arrowX = toX + topX; arrowY = toY + topY; path += " M " + arrowX + " " + arrowY; path += " L " + toX + " " + toY; arrowX = toX + botX; arrowY = toY + botY; path += " L " + arrowX + " " + arrowY; return path; }, downLoadImage() { // this.done = true; //生成雙倍像素比的圖片 let base64URl = this.fabricObj.toDataURL({ formart: 'png', multiplier: 2 }); this.imageBase64 = base64URl; this.$emit("onChang", { type: base64URl }); // this.done = false; }, onSelect(){ this.onRemoveVaule = false; this.onStartvalue = false; this.fabricObj.isDrawingMode = false; this.fabricObj.skipTargetFind = false; }, headleChangeColor(value){ this.drawColor = value; console.log(value) this.fabricObj.freeDrawingBrush.color = value } }, } </script> <style lang="scss" scoped> .wraper{ position: relative; width: 100%; height: 100%; .canvas-wraper{ // height: 50%; height: 100%; width: 100%; // margin-bottom: 10px; overflow: hidden; } .controlPanel{ position: absolute; width: 800px; bottom: 30px; // width: 100%; // height: 32px; // background: #ddd; left: 50%; transform: translateX(-50%); box-shadow: 1px 3px 10px 0 rgba(150, 150, 150, 0.4); background: rgba(255, 255, 255, 1); border-radius: 5px; display: flex; flex-direction: column; box-sizing: border-box; // justify-content: center; // align-items: center; // margin-bottom: 15px; .text { display: inline-block; padding-left: 15px; font-size: 12px; letter-spacing: 1px; color: #aaaaaa; line-height: 20px; border-bottom: 1px solid #eeeeee; } .tool { display: flex; } .contro-item{ width: 38px; // flex-basis: 100px; // border-right: 1px solid #dad7d9; text-align: center; cursor: pointer; background: #fefefe; i{ height: 100%; font-size: 22px; line-height: 25px; display: flex; justify-content: center; align-items: center; } &.active{ // background: #e34f51; color: #00BFFF; border-radius: 3px; i{ font-size: 26px; } } } .color-list { width: 38px; display: flex; justify-content: center; align-items: center; } } .spn { display: block; width: 20px; height: 20px; } .download{ img{ width: 100%; } } } </style> <style> .controlPanel .box { bottom: 0px; } .wraper .el-slider__button-wrapper { z-index: 0 !important; } </style>