UNPKG

vue-drawing-canvas

Version:
683 lines (611 loc) 20.9 kB
import { defineComponent, isVue2, h } from 'vue-demi'; /* eslint-disable no-debugger, no-console */ var VueDrawingCanvas = /*#__PURE__*/defineComponent({ name: 'VueDrawingCanvas', props: { strokeType: { type: String, validator: value => { return ['dash', 'line', 'square', 'circle', 'triangle', 'half_triangle'].indexOf(value) !== -1; }, default: () => 'dash' }, fillShape: { type: Boolean, default: () => false }, width: { type: [String, Number], default: () => 600 }, height: { type: [String, Number], default: () => 400 }, image: { type: String, default: () => '' }, eraser: { type: Boolean, default: () => false }, color: { type: String, default: () => '#000000' }, lineWidth: { type: Number, default: () => 5 }, lineCap: { type: String, validator: value => { return ['round', 'square', 'butt'].indexOf(value) !== -1; }, default: () => 'round' }, lineJoin: { type: String, validator: value => { return ['miter', 'round', 'bevel'].indexOf(value) !== -1; }, default: () => 'miter' }, lock: { type: Boolean, default: () => false }, styles: { type: [Array, String, Object] }, classes: { type: [Array, String, Object] }, backgroundColor: { type: String, default: () => '#FFFFFF' }, backgroundImage: { type: String, default: () => null }, watermark: { type: Object, default: () => null }, saveAs: { type: String, validator: value => { return ['jpeg', 'png'].indexOf(value) !== -1; }, default: () => 'png' }, canvasId: { type: String, default: () => 'VueDrawingCanvas' }, initialImage: { type: Array, default: () => [] }, additionalImages: { type: Array, default: () => [] }, outputWidth: { type: Number }, outputHeight: { type: Number } }, data() { return { loadedImage: null, drawing: false, context: null, images: [], strokes: { type: '', from: { x: 0, y: 0 }, coordinates: [], color: '', width: '', fill: false, lineCap: '', lineJoin: '' }, guides: [], trash: [] }; }, mounted() { this.setContext(); this.$nextTick(() => { this.drawInitialImage(); this.drawAdditionalImages(); }); }, watch: { backgroundImage: function () { this.loadedImage = null; } }, methods: { async setContext() { let canvas = document.querySelector('#' + this.canvasId); this.context = this.context ? this.context : canvas.getContext('2d'); await this.setBackground(); }, drawInitialImage() { if (this.initialImage && this.initialImage.length > 0) { // @ts-ignore this.images = [].concat(this.images, this.initialImage); this.redraw(true); } }, drawAdditionalImages() { if (this.additionalImages && this.additionalImages.length > 0) { let canvas = document.querySelector('#' + this.canvasId); this.additionalImages.forEach(watermarkObject => { this.drawWatermark(canvas, this.context, watermarkObject); }); } }, clear() { this.context.clearRect(0, 0, Number(this.width), Number(this.height)); }, async setBackground() { this.clear(); this.context.fillStyle = this.backgroundColor; this.context.fillRect(0, 0, Number(this.width), Number(this.height)); await this.$nextTick(async () => { await this.drawBackgroundImage(); }); this.save(); }, async drawBackgroundImage() { if (!this.loadedImage) { return new Promise(resolve => { if (!this.backgroundImage) { resolve(); return; } const image = new Image(); image.src = this.backgroundImage; image.onload = () => { this.context.drawImage(image, 0, 0, Number(this.width), Number(this.height)); this.loadedImage = image; resolve(); }; }); } else { this.context.drawImage(this.loadedImage, 0, 0, Number(this.width), Number(this.height)); } }, getCoordinates(event) { let x, y; if (event.touches && event.touches.length > 0) { let canvas = document.querySelector('#' + this.canvasId); let rect = canvas.getBoundingClientRect(); x = event.touches[0].clientX - rect.left; y = event.touches[0].clientY - rect.top; } else { x = event.offsetX; y = event.offsetY; } return { x: x, y: y }; }, startDraw(event) { if (!this.lock) { this.drawing = true; let coordinate = this.getCoordinates(event); this.strokes = { type: this.eraser ? 'eraser' : this.strokeType, from: coordinate, coordinates: [], color: this.eraser ? this.backgroundColor : this.color, width: this.lineWidth, fill: this.eraser || this.strokeType === 'dash' || this.strokeType === 'line' ? false : this.fillShape, lineCap: this.lineCap, lineJoin: this.lineJoin }; this.guides = []; } }, draw(event) { if (this.drawing) { if (!this.context) { this.setContext(); } let coordinate = this.getCoordinates(event); if (this.eraser || this.strokeType === 'dash') { this.strokes.coordinates.push(coordinate); this.drawShape(this.context, this.strokes, false); } else { switch (this.strokeType) { case 'line': this.guides = [{ x: coordinate.x, y: coordinate.y }]; break; case 'square': this.guides = [{ x: coordinate.x, y: this.strokes.from.y }, { x: coordinate.x, y: coordinate.y }, { x: this.strokes.from.x, y: coordinate.y }, { x: this.strokes.from.x, y: this.strokes.from.y }]; break; case 'triangle': let center = Math.floor((coordinate.x - this.strokes.from.x) / 2) < 0 ? Math.floor((coordinate.x - this.strokes.from.x) / 2) * -1 : Math.floor((coordinate.x - this.strokes.from.x) / 2); let width = this.strokes.from.x < coordinate.x ? this.strokes.from.x + center : this.strokes.from.x - center; this.guides = [{ x: coordinate.x, y: this.strokes.from.y }, { x: width, y: coordinate.y }, { x: this.strokes.from.x, y: this.strokes.from.y }]; break; case 'half_triangle': this.guides = [{ x: coordinate.x, y: this.strokes.from.y }, { x: this.strokes.from.x, y: coordinate.y }, { x: this.strokes.from.x, y: this.strokes.from.y }]; break; case 'circle': let radiusX = this.strokes.from.x - coordinate.x < 0 ? (this.strokes.from.x - coordinate.x) * -1 : this.strokes.from.x - coordinate.x; this.guides = [{ x: this.strokes.from.x > coordinate.x ? this.strokes.from.x - radiusX : this.strokes.from.x + radiusX, y: this.strokes.from.y }, { x: radiusX, y: radiusX }]; break; } this.drawGuide(true); } } }, drawGuide(closingPath) { this.redraw(true); this.$nextTick(() => { this.context.strokeStyle = this.color; this.context.lineWidth = 1; this.context.lineJoin = this.lineJoin; this.context.lineCap = this.lineCap; this.context.beginPath(); this.context.setLineDash([15, 15]); if (this.strokes.type === 'circle') { this.context.ellipse(this.guides[0].x, this.guides[0].y, this.guides[1].x, this.guides[1].y, 0, 0, Math.PI * 2); } else { this.context.moveTo(this.strokes.from.x, this.strokes.from.y); this.guides.forEach(coordinate => { this.context.lineTo(coordinate.x, coordinate.y); }); if (closingPath) { this.context.closePath(); } } this.context.stroke(); }); }, drawShape(context, strokes, closingPath) { context.strokeStyle = strokes.color; context.fillStyle = strokes.color; context.lineWidth = strokes.width; context.lineJoin = strokes.lineJoin === undefined ? this.lineJoin : strokes.lineJoin; context.lineCap = strokes.lineCap === undefined ? this.lineCap : strokes.lineCap; context.beginPath(); context.setLineDash([]); if (strokes.type === 'circle') { context.ellipse(strokes.coordinates[0].x, strokes.coordinates[0].y, strokes.coordinates[1].x, strokes.coordinates[1].y, 0, 0, Math.PI * 2); } else { context.moveTo(strokes.from.x, strokes.from.y); strokes.coordinates.forEach(stroke => { context.lineTo(stroke.x, stroke.y); }); if (closingPath) { context.closePath(); } } if (strokes.fill) { context.fill(); } else { context.stroke(); } }, stopDraw() { if (this.drawing) { this.strokes.coordinates = this.guides.length > 0 ? this.guides : this.strokes.coordinates; this.images.push(this.strokes); this.redraw(true); this.drawing = false; this.trash = []; } }, reset() { if (!this.lock) { this.images = []; this.strokes = { type: '', coordinates: [], color: '', width: '', fill: false, lineCap: '', lineJoin: '' }; this.guides = []; this.trash = []; this.redraw(true); } }, undo() { if (!this.lock) { let strokes = this.images.pop(); if (strokes) { this.trash.push(strokes); this.redraw(true); } } }, redo() { if (!this.lock) { let strokes = this.trash.pop(); if (strokes) { this.images.push(strokes); this.redraw(true); } } }, async redraw(output) { output = typeof output !== 'undefined' ? output : true; await this.setBackground().then(() => { this.drawAdditionalImages(); }).then(() => { let baseCanvas = document.createElement('canvas'); let baseCanvasContext = baseCanvas.getContext('2d'); baseCanvas.width = Number(this.width); baseCanvas.height = Number(this.height); if (baseCanvasContext) { this.images.forEach(stroke => { if (baseCanvasContext) { baseCanvasContext.globalCompositeOperation = stroke.type === 'eraser' ? 'destination-out' : 'source-over'; if (stroke.type !== 'circle' || stroke.type === 'circle' && stroke.coordinates.length > 0) { this.drawShape(baseCanvasContext, stroke, stroke.type === 'eraser' || stroke.type === 'dash' || stroke.type === 'line' ? false : true); } } }); this.context.drawImage(baseCanvas, 0, 0, Number(this.width), Number(this.height)); } }).then(() => { if (output) { this.save(); } }); }, wrapText(context, text, x, y, maxWidth, lineHeight) { const newLineRegex = /(\r\n|\n\r|\n|\r)+/g; const whitespaceRegex = /\s+/g; var lines = text.split(newLineRegex).filter(word => word.length > 0); for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) { var words = lines[lineNumber].split(whitespaceRegex).filter(word => word.length > 0); var line = ''; for (var n = 0; n < words.length; n++) { var testLine = line + words[n] + ' '; var metrics = context.measureText(testLine); var testWidth = metrics.width; if (testWidth > maxWidth && n > 0) { if (this.watermark && this.watermark.fontStyle && this.watermark.fontStyle.drawType && this.watermark.fontStyle.drawType === 'stroke') { context.strokeText(line, x, y); } else { context.fillText(line, x, y); } line = words[n] + ' '; y += lineHeight; } else { line = testLine; } } if (this.watermark && this.watermark.fontStyle && this.watermark.fontStyle.drawType && this.watermark.fontStyle.drawType === 'stroke') { context.strokeText(line, x, y); } else { context.fillText(line, x, y); } y += words.length > 0 ? lineHeight : 0; } }, save() { let canvas = document.querySelector('#' + this.canvasId); if (this.watermark) { let temp = document.createElement('canvas'); let ctx = temp.getContext('2d'); if (ctx) { temp.width = Number(this.width); temp.height = Number(this.height); ctx.drawImage(canvas, 0, 0, Number(this.width), Number(this.height)); this.drawWatermark(temp, ctx, this.watermark); } } else { let temp = document.createElement('canvas'); let tempCtx = temp.getContext('2d'); let tempWidth = this.outputWidth === undefined ? this.width : this.outputWidth; let tempHeight = this.outputHeight === undefined ? this.height : this.outputHeight; temp.width = Number(tempWidth); temp.height = Number(tempHeight); if (tempCtx) { tempCtx.drawImage(canvas, 0, 0, Number(tempWidth), Number(tempHeight)); this.$emit('update:image', temp.toDataURL('image/' + this.saveAs, 1)); return temp.toDataURL('image/' + this.saveAs, 1); } } }, drawWatermark(canvas, ctx, watermark) { if (watermark.type === 'Image') { let imageWidth = watermark.imageStyle ? watermark.imageStyle.width ? watermark.imageStyle.width : Number(this.width) : Number(this.width); let imageHeight = watermark.imageStyle ? watermark.imageStyle.height ? watermark.imageStyle.height : Number(this.height) : Number(this.height); const image = new Image(); image.src = watermark.source; image.onload = () => { if (watermark && ctx) { ctx.drawImage(image, watermark.x, watermark.y, Number(imageWidth), Number(imageHeight)); } let temp = document.createElement('canvas'); let tempCtx = temp.getContext('2d'); let tempWidth = this.outputWidth === undefined ? this.width : this.outputWidth; let tempHeight = this.outputHeight === undefined ? this.height : this.outputHeight; temp.width = Number(tempWidth); temp.height = Number(tempHeight); if (tempCtx) { tempCtx.drawImage(canvas, 0, 0, Number(tempWidth), Number(tempHeight)); this.$emit('update:image', temp.toDataURL('image/' + this.saveAs, 1)); return temp.toDataURL('image/' + this.saveAs, 1); } }; } else if (watermark.type === 'Text') { let font = watermark.fontStyle ? watermark.fontStyle.font ? watermark.fontStyle.font : '20px serif' : '20px serif'; let align = watermark.fontStyle ? watermark.fontStyle.textAlign ? watermark.fontStyle.textAlign : 'start' : 'start'; let baseline = watermark.fontStyle ? watermark.fontStyle.textBaseline ? watermark.fontStyle.textBaseline : 'alphabetic' : 'alphabetic'; let color = watermark.fontStyle ? watermark.fontStyle.color ? watermark.fontStyle.color : '#000000' : '#000000'; ctx.font = font; ctx.textAlign = align; ctx.textBaseline = baseline; if (watermark.fontStyle && watermark.fontStyle.rotate) { let centerX, centerY; if (watermark.fontStyle && watermark.fontStyle.width) { centerX = watermark.x + Math.floor(watermark.fontStyle.width / 2); } else { centerX = watermark.x; } if (watermark.fontStyle && watermark.fontStyle.lineHeight) { centerY = watermark.y + Math.floor(watermark.fontStyle.lineHeight / 2); } else { centerY = watermark.y; } ctx.translate(centerX, centerY); ctx.rotate(watermark.fontStyle.rotate * Math.PI / 180); ctx.translate(centerX * -1, centerY * -1); } if (watermark.fontStyle && watermark.fontStyle.drawType && watermark.fontStyle.drawType === 'stroke') { ctx.strokeStyle = watermark.fontStyle.color; if (watermark.fontStyle && watermark.fontStyle.width) { this.wrapText(ctx, watermark.source, watermark.x, watermark.y, watermark.fontStyle.width, watermark.fontStyle.lineHeight); } else { ctx.strokeText(watermark.source, watermark.x, watermark.y); } } else { ctx.fillStyle = color; if (watermark.fontStyle && watermark.fontStyle.width) { this.wrapText(ctx, watermark.source, watermark.x, watermark.y, watermark.fontStyle.width, watermark.fontStyle.lineHeight); } else { ctx.fillText(watermark.source, watermark.x, watermark.y); } } let temp = document.createElement('canvas'); let tempCtx = temp.getContext('2d'); let tempWidth = this.outputWidth === undefined ? this.width : this.outputWidth; let tempHeight = this.outputHeight === undefined ? this.height : this.outputHeight; temp.width = Number(tempWidth); temp.height = Number(tempHeight); if (tempCtx) { tempCtx.drawImage(canvas, 0, 0, Number(tempWidth), Number(tempHeight)); this.$emit('update:image', temp.toDataURL('image/' + this.saveAs, 1)); return temp.toDataURL('image/' + this.saveAs, 1); } } }, isEmpty() { return this.images.length > 0 ? false : true; }, getAllStrokes() { return this.images; } }, render() { if (isVue2) { return h('canvas', { attrs: { id: this.canvasId, width: Number(this.width), height: Number(this.height) }, style: { 'touchAction': 'none', // @ts-ignore ...this.styles }, class: this.classes, on: { mousedown: event => this.startDraw(event), mousemove: event => this.draw(event), mouseup: () => this.stopDraw(), mouseleave: () => this.stopDraw(), touchstart: event => this.startDraw(event), touchmove: event => this.draw(event), touchend: () => this.stopDraw(), touchleave: () => this.stopDraw(), touchcancel: () => this.stopDraw(), pointerdown: event => this.startDraw(event), pointermove: event => this.draw(event), pointerup: () => this.stopDraw(), pointerleave: () => this.stopDraw(), pointercancel: () => this.stopDraw() }, ...this.$props }); } return h('canvas', { id: this.canvasId, height: Number(this.height), width: Number(this.width), style: { 'touchAction': 'none', // @ts-ignore ...this.styles }, class: this.classes, onMousedown: $event => this.startDraw($event), onMousemove: $event => this.draw($event), onMouseup: () => this.stopDraw(), onMouseleave: () => this.stopDraw(), onTouchstart: $event => this.startDraw($event), onTouchmove: $event => this.draw($event), onTouchend: () => this.stopDraw(), onTouchleave: () => this.stopDraw(), onTouchcancel: () => this.stopDraw(), onPointerdown: $event => this.startDraw($event), onPointermove: $event => this.draw($event), onPointerup: () => this.stopDraw(), onPointerleave: () => this.stopDraw(), onPointercancel: () => this.stopDraw() }); } }); export { VueDrawingCanvas as default };