UNPKG

ngx-image-drawing

Version:
549 lines (543 loc) 23.7 kB
import { __decorate, __metadata } from 'tslib'; import { CommonModule } from '@angular/common'; import { EventEmitter, Input, TemplateRef, Output, Component, NgModule } from '@angular/core'; import { fabric } from 'fabric'; import { Observable, of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; const I18nFr = { saveBtn: 'Enregistrer', cancelBtn: 'Annuler', loadImage: 'Charger une image depuis votre PC', loadImageUrl: 'Charger une image depuis une URL', loading: 'Chargement', loadError: 'Erreur de chargement %@', removeImage: 'Supprimer l\'image', sizes: { small: 'Petit', medium: 'Moyen', large: 'Gros' }, undo: 'Annuler', redo: 'Répter', clear: 'Effacer', colors: { black: 'Noir', white: 'Blanc', yellow: 'Jaune', red: 'Rouge', green: 'Vert', blue: 'Blue', purple: 'Violet' }, tools: { brush: 'Pinceau' } }; const I18nEn = { saveBtn: 'Save', cancelBtn: 'Cancel', loadImage: 'Load image', loadImageUrl: 'Load image from URL', loading: 'Loading', loadError: 'Error loading %@', removeImage: 'Remove image', sizes: { small: 'Small', medium: 'Medium', large: 'Large' }, undo: 'Undo', redo: 'Redo', clear: 'Clear', colors: { black: 'Black', white: 'White', yellow: 'Yellow', red: 'Red', green: 'Green', blue: 'Blue', purple: 'Purple', }, tools: { brush: 'Brush' } }; const i18nLanguages = { fr: I18nFr, en: I18nEn }; let ImageDrawingComponent = class ImageDrawingComponent { constructor() { this.forceSizeCanvas = true; this.forceSizeExport = false; this.enableRemoveImage = false; this.enableLoadAnotherImage = false; this.enableTooltip = true; this.showCancelButton = true; this.locale = 'en'; /* @deprecated Use i18n.saveBtn */ this.saveBtnText = 'Save'; /* @deprecated Use i18n.cancelBtn */ this.cancelBtnText = 'Cancel'; /* @deprecated Use i18n.loading */ this.loadingText = 'Loading…'; /* @deprecated Use i18n.loadError */ this.errorText = 'Error loading %@'; this.outputMimeType = 'image/jpeg'; this.outputQuality = 0.8; this.borderCss = 'none'; this.drawingSizes = { small: 5, medium: 10, large: 25, }; this.colors = { black: '#000', white: '#fff', yellow: '#ffeb3b', red: '#f44336', blue: '#2196f3', green: '#4caf50', purple: '#7a08af', }; this.save = new EventEmitter(); this.cancel = new EventEmitter(); this.currentTool = 'brush'; this.currentSize = 'medium'; this.currentColor = 'black'; this.i18n = I18nEn; this.canUndo = false; this.canRedo = false; this.isLoading = false; this.hasError = false; this.errorMessage = ''; this.stack = []; this.colorsName = []; this.drawingSizesName = []; } ngOnInit() { this.colorsName = Object.keys(this.colors); this.drawingSizesName = Object.keys(this.drawingSizes); this.canvas = new fabric.Canvas('canvas', { hoverCursor: 'pointer', isDrawingMode: true, }); this.canvas.backgroundColor = 'white'; if (this.src) { this.importPhotoFromSrc(this.src); } else { if (!this.width || !this.height) { throw new Error('No width or hight given !'); } this.canvas.setWidth(this.width); this.canvas.setHeight(this.height); } this.canvas.on('path:created', () => { this.stack = []; this.setUndoRedo(); }); this.selectTool(this.currentTool); this.selectColor(this.currentColor); this.selectDrawingSize(this.currentSize); if (this.locale && i18nLanguages[this.locale.toLowerCase()]) { this.i18n = i18nLanguages[this.locale.toLowerCase()]; } // FIXME remove after a while because properties are now deprecated if (this.saveBtnText) { this.i18n.saveBtn = this.saveBtnText; } if (this.cancelBtnText) { this.i18n.cancelBtn = this.cancelBtnText; } if (this.loadingText) { this.i18n.loading = this.loadingText; } if (this.errorText) { this.i18n.loadError = this.errorText; } } // Tools selectTool(tool) { this.currentTool = tool; } selectDrawingSize(size) { this.currentSize = size; if (this.canvas) { this.canvas.freeDrawingBrush.width = this.drawingSizes[size]; } } selectColor(color) { this.currentColor = color; if (this.canvas) { this.canvas.freeDrawingBrush.color = this.colors[color]; } } // Actions undo() { if (this.canUndo) { const lastId = this.canvas.getObjects().length - 1; const lastObj = this.canvas.getObjects()[lastId]; this.stack.push(lastObj); this.canvas.remove(lastObj); this.setUndoRedo(); } } redo() { if (this.canRedo) { const firstInStack = this.stack.splice(-1, 1)[0]; if (firstInStack) { this.canvas.insertAt(firstInStack, this.canvas.getObjects().length - 1, false); } this.setUndoRedo(); } } clearCanvas() { if (this.canvas) { this.canvas.remove(...this.canvas.getObjects()); this.setUndoRedo(); } } saveImage() { if (!this.forceSizeExport || (this.forceSizeExport && this.width && this.height)) { const canvasScaledElement = document.createElement('canvas'); const canvasScaled = new fabric.Canvas(canvasScaledElement); canvasScaled.backgroundColor = 'white'; new Observable(observer => { if (this.imageUsed) { if (this.forceSizeExport) { canvasScaled.setWidth(this.width); canvasScaled.setHeight(this.height); this.imageUsed.cloneAsImage(imageCloned => { imageCloned.scaleToWidth(this.width, false); imageCloned.scaleToHeight(this.height, false); canvasScaled.setBackgroundImage(imageCloned, (img) => { if (!img) { observer.error(new Error('Impossible to draw the image on the temporary canvas')); } observer.next(canvasScaled); observer.complete(); }, { crossOrigin: 'anonymous', originX: 'left', originY: 'top' }); }); } else { canvasScaled.setBackgroundImage(this.imageUsed, (img) => { if (!img) { observer.error(new Error('Impossible to draw the image on the temporary canvas')); } canvasScaled.setWidth(img.width); canvasScaled.setHeight(img.height); observer.next(canvasScaled); observer.complete(); }, { crossOrigin: 'anonymous', originX: 'left', originY: 'top' }); } } else { canvasScaled.setWidth(this.width); canvasScaled.setHeight(this.height); } }).pipe(switchMap(() => { let process = of(canvasScaled); if (this.canvas.getObjects().length > 0) { const ratioX = canvasScaled.getWidth() / this.canvas.getWidth(); const ratioY = canvasScaled.getHeight() / this.canvas.getHeight(); this.canvas.getObjects().forEach((originalObject, i) => { process = process.pipe(switchMap(() => { return new Observable(observerObject => { originalObject.clone((clonedObject) => { clonedObject.set('left', originalObject.left * ratioX); clonedObject.set('top', originalObject.top * ratioY); clonedObject.scaleToWidth(originalObject.width * ratioX); clonedObject.scaleToHeight(originalObject.height * ratioY); canvasScaled.insertAt(clonedObject, i, false); canvasScaled.renderAll(); observerObject.next(canvasScaled); observerObject.complete(); }); }); })); }); } return process; })).subscribe(() => { canvasScaled.renderAll(); canvasScaled.getElement().toBlob((data) => { this.save.emit(data); }, this.outputMimeType, this.outputQuality); }); } else { this.canvas.getElement().toBlob((data) => { this.save.emit(data); }, this.outputMimeType, this.outputQuality); } } cancelAction() { this.cancel.emit(); } getTextTranslated(name) { let strOk = name.split('.').reduce((o, i) => o[i], this.i18n); if (this.i18nUser) { try { const str = name.split('.').reduce((o, i) => o[i], this.i18nUser); if (str) { strOk = str; } } catch (e) { // if we pass here, ignored } } if (!strOk) { console.error(name + ' translation not found !'); } return strOk; } getTooltipTranslated(name) { if (this.enableTooltip) { return this.getTextTranslated(name); } else { return ''; } } setUndoRedo() { this.canUndo = this.canvas.getObjects().length > 0; this.canRedo = this.stack.length > 0; this.canvas.renderAll(); } importPhotoFromFile(event) { if (event.target.files && event.target.files.length > 0) { const file = event.target.files[0]; if (file.type.match('image.*')) { this.importPhotoFromBlob(file); } else { throw new Error('Not an image !'); } } } removeImage() { if (this.imageUsed) { this.imageUsed.dispose(); this.imageUsed = null; } this.canvas.backgroundImage = null; if (this.width && this.height) { this.canvas.setWidth(this.width); this.canvas.setHeight(this.height); } this.canvas.renderAll(); } get hasImage() { return !!this.canvas.backgroundImage; } importPhotoFromSrc(src) { this.isLoading = true; let isFirstTry = true; const imgEl = new Image(); imgEl.setAttribute('crossOrigin', 'anonymous'); imgEl.src = src; imgEl.onerror = () => { // Retry with cors proxy if (isFirstTry) { imgEl.src = 'https://cors-anywhere.herokuapp.com/' + this.src; isFirstTry = false; } else { this.isLoading = false; this.hasError = true; this.errorMessage = this.getTextTranslated('loadError').replace('%@', this.src); } }; imgEl.onload = () => { this.isLoading = false; this.imageUsed = new fabric.Image(imgEl); this.imageUsed.cloneAsImage(image => { let width = imgEl.width; let height = imgEl.height; if (this.width) { width = this.width; } if (this.height) { height = this.height; } image.scaleToWidth(width, false); image.scaleToHeight(height, false); this.canvas.setBackgroundImage(image, ((img) => { if (img) { if (this.forceSizeCanvas) { this.canvas.setWidth(width); this.canvas.setHeight(height); } else { this.canvas.setWidth(image.getScaledWidth()); this.canvas.setHeight(image.getScaledHeight()); } } }), { crossOrigin: 'anonymous', originX: 'left', originY: 'top' }); }); }; } importPhotoFromBlob(file) { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = (evtReader) => { if (evtReader.target.readyState == FileReader.DONE) { this.importPhotoFromSrc(evtReader.target.result); } }; } importPhotoFromUrl() { const url = prompt(this.getTooltipTranslated('loadImageUrl')); if (url) { this.importPhotoFromSrc(url); } } ngOnChanges(changes) { if (changes.src && !changes.src.firstChange && changes.src.currentValue) { if (typeof changes.src.currentValue === 'string') { this.importPhotoFromSrc(changes.src.currentValue); } else if (changes.src.currentValue instanceof Blob) { this.importPhotoFromBlob(changes.src.currentValue); } } } }; __decorate([ Input(), __metadata("design:type", String) ], ImageDrawingComponent.prototype, "src", void 0); __decorate([ Input(), __metadata("design:type", Number) ], ImageDrawingComponent.prototype, "width", void 0); __decorate([ Input(), __metadata("design:type", Number) ], ImageDrawingComponent.prototype, "height", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "forceSizeCanvas", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "forceSizeExport", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "enableRemoveImage", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "enableLoadAnotherImage", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "enableTooltip", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "showCancelButton", void 0); __decorate([ Input('i18n'), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "i18nUser", void 0); __decorate([ Input(), __metadata("design:type", String) ], ImageDrawingComponent.prototype, "locale", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "saveBtnText", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "cancelBtnText", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "loadingText", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "errorText", void 0); __decorate([ Input(), __metadata("design:type", TemplateRef) ], ImageDrawingComponent.prototype, "loadingTemplate", void 0); __decorate([ Input(), __metadata("design:type", TemplateRef) ], ImageDrawingComponent.prototype, "errorTemplate", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "outputMimeType", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "outputQuality", void 0); __decorate([ Input(), __metadata("design:type", String) ], ImageDrawingComponent.prototype, "borderCss", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "drawingSizes", void 0); __decorate([ Input(), __metadata("design:type", Object) ], ImageDrawingComponent.prototype, "colors", void 0); __decorate([ Output(), __metadata("design:type", EventEmitter) ], ImageDrawingComponent.prototype, "save", void 0); __decorate([ Output(), __metadata("design:type", EventEmitter) ], ImageDrawingComponent.prototype, "cancel", void 0); ImageDrawingComponent = __decorate([ Component({ selector: 'image-drawing', template: "<link href=\"https://fonts.googleapis.com/icon?family=Material+Icons\" rel=\"stylesheet\" />\n\n<div class=\"loading\" *ngIf=\"isLoading\">\n <ng-container *ngTemplateOutlet=\"loadingTemplate ? loadingTemplate : defaultLoading\"></ng-container>\n</div>\n<div class=\"error\" *ngIf=\"hasError\">\n <ng-container *ngTemplateOutlet=\"errorTemplate ? errorTemplate : defaultError\"></ng-container>\n</div>\n\n<ng-template #defaultLoading><p>{{ getTextTranslated('loading') }}</p></ng-template>\n<ng-template #defaultError> <p>{{ errorMessage }}</p> </ng-template>\n\n<div [ngStyle]=\"{ border: borderCss }\">\n <canvas id=\"canvas\"></canvas>\n</div>\n<div class=\"toolbar\" *ngIf=\"!isLoading\">\n <div class=\"tools\">\n <div class=\"row\">\n <i class=\"material-icons btn\" [class.selected]=\"currentTool == 'brush'\" (click)=\"selectTool('brush')\"\n [title]=\"getTooltipTranslated('tools.brush')\">brush</i>\n <span *ngFor=\"let drawingSizeName of drawingSizesName\" class=\"size btn\"\n [style.width.px]=\"drawingSizes[drawingSizeName] * 0.8 + 8\"\n [style.height.px]=\"drawingSizes[drawingSizeName] * 0.8 + 8\"\n [style.borderRadius.px]=\"drawingSizes[drawingSizeName] * 0.4 + 4\"\n [class.selected]=\"currentSize == drawingSizeName\"\n [title]=\"getTooltipTranslated('sizes.' + drawingSizeName)\"\n (click)=\"selectDrawingSize(drawingSizeName)\">\n </span>\n\n <input style=\"display: none\" type=\"file\" #fileInput (change)=\"importPhotoFromFile($event)\"\n accept=\"image/*\"/>\n <i class=\"material-icons btn\" *ngIf=\"enableLoadAnotherImage && !hasImage\" (click)=\"fileInput.click();\"\n [title]=\"getTooltipTranslated('loadImage')\">attach_file</i>\n <i class=\"material-icons btn\" *ngIf=\"enableLoadAnotherImage && !hasImage\" (click)=\"importPhotoFromUrl()\"\n [title]=\"getTooltipTranslated('loadImageUrl')\">insert_drive_file</i>\n <i class=\"material-icons btn\" *ngIf=\"enableRemoveImage && hasImage\" (click)=\"removeImage()\"\n [title]=\"getTooltipTranslated('removeImage')\">clear</i>\n\n <i class=\"material-icons btn\" [class.disabled]=\"!canUndo\" (click)=\"undo()\"\n [title]=\"getTooltipTranslated('undo')\">undo</i>\n <i class=\"material-icons btn\" [class.disabled]=\"!canRedo\" (click)=\"redo()\"\n [title]=\"getTooltipTranslated('redo')\">redo</i>\n <i class=\"material-icons btn\" (click)=\"clearCanvas()\" [title]=\"getTooltipTranslated('clear')\">delete</i>\n </div>\n <div class=\"row\">\n <div *ngFor=\"let colorName of colorsName\" [class.selected]=\"currentColor == colorName\" class=\"color\"\n [ngClass]=\"colorName\"\n [style.background]=\"colors[colorName]\" [title]=\"getTooltipTranslated('colors.' + colorName)\"\n (click)=\"selectColor(colorName)\">\n </div>\n </div>\n </div>\n <div class=\"buttons\">\n <a href=\"#\" class=\"button btn-primary\"\n (click)=\"saveImage(); $event.preventDefault()\">{{ getTextTranslated('saveBtn') }}</a>\n <a href=\"#\" class=\"button btn-light\" *ngIf=\"showCancelButton\"\n (click)=\"cancelAction(); $event.preventDefault()\">{{ getTextTranslated('cancelBtn') }}</a>\n </div>\n <!-- Any additional toolbar buttons to be projected by the consuming app -->\n <ng-content></ng-content>\n</div>\n", styles: [":host{display:flex;flex-direction:column;align-items:center}:host .toolbar{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap}:host .tools{display:inline-flex;flex-direction:column;padding:20px;margin:10px;background:#fff;border-radius:6px;box-shadow:0 3px 10px rgba(0,0,0,.4)}:host .row{display:flex;width:300px;justify-content:space-around;align-items:center}:host .row:first-child{margin-bottom:10px}:host .btn{cursor:pointer}:host .btn.selected{color:#bdbdbd}:host .btn.disabled{cursor:initial;color:#bdbdbd}:host .size{background-color:#000}:host .size.selected{background-color:#bdbdbd}:host .color{width:28px;height:28px;border-radius:14px;cursor:pointer;display:flex;align-items:center;justify-content:center}:host .color.selected::after{content:\"\";width:10px;height:10px;background:#000;display:flex;border-radius:5px}:host .color.black{background-color:#000}:host .color.black.selected::after{background:#fff}:host .color.white{border:1px solid #a7a7a7}:host .buttons{margin:10px;display:flex;flex-direction:column}:host .button{cursor:pointer;outline:0;border:none;white-space:nowrap;text-decoration:none;vertical-align:baseline;text-align:center;min-width:64px;line-height:36px;padding:3px 16px;border-radius:4px;overflow:visible;transition:background .4s cubic-bezier(.25,.8,.25,1),box-shadow 280ms cubic-bezier(.4,0,.2,1);margin:10px}:host .button:hover{text-decoration:none!important}:host .button.btn-primary{background-color:#ef5f27;color:#fff}:host .button.btn-primary:hover{background-color:rgba(239,95,39,.8)}:host .button.btn-light{color:#ef5f27}:host .button.btn-light:hover{background-color:rgba(239,95,39,.1)}"] }), __metadata("design:paramtypes", []) ], ImageDrawingComponent); let ImageDrawingModule = class ImageDrawingModule { }; ImageDrawingModule = __decorate([ NgModule({ declarations: [ ImageDrawingComponent ], exports: [ ImageDrawingComponent, ], imports: [ CommonModule ] }) ], ImageDrawingModule); /** * Generated bundle index. Do not edit. */ export { ImageDrawingComponent, ImageDrawingModule }; //# sourceMappingURL=ngx-image-drawing.js.map