import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Inject, Input, OnChanges, OnDestroy, SimpleChanges, ViewChild, } from '@angular/core'; import { DimensionsInterface, EnclosingQuadrilateral, ImageEditorTextInterface, MaskImageMapInterface, } from './../../models/index'; import { BrowserDetectorService, CanvasUtilitiesService, ImagePreloaderService, WindowResizeEventsService, } from './../../helpers/services/index'; import { DOCUMENT } from '@angular/common'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'canvas-image-on-mask-component', styleUrls: [ './canvas-image-on-mask.component.scss', ], templateUrl: './canvas-image-on-mask.component.template.pug', }) export class CanvasImageOnMaskComponent implements OnChanges, AfterViewInit, OnDestroy { @Input() public backgroundScale = 1; @Input() public backgroundUrl: string; @Input() public foregroundUrl: string; @Input() public foregroundBlendMode: string; @Input() public colorOverlay: string; @Input() public assetSize: DimensionsInterface; @Input() public maskImagesMap: MaskImageMapInterface[]; @Input() public showImageOverspill = false; @Input() public imageOverSpillBorderColor: string; @Input() public dpi: number; @ViewChild('imageCanvas') public imageCanvas: ElementRef; private _windowResizeEventId: number; public get canvas(): HTMLCanvasElement { return this.imageCanvas.nativeElement; } constructor( private _canvasUtilitiesService: CanvasUtilitiesService, private _imagePreloaderService: ImagePreloaderService, private _browserDetectorService: BrowserDetectorService, private _windowResizeEventsService: WindowResizeEventsService, @Inject(DOCUMENT) private _document: Document, ) {} public ngOnChanges(changes: SimpleChanges) { if (!changes.maskImagesMap || !changes.maskImagesMap.firstChange) { this.loadAndRender(); } } public ngAfterViewInit() { this.initCanvasSize(); this.loadAndRender(); this._windowResizeEventId = this._windowResizeEventsService.addEvent(() => { this.initCanvasSize(); this.loadAndRender(); }); if (this._browserDetectorService.isSafari()) { // Ensure canvas images are correctly initiated on Safari setTimeout(() => { this.initCanvasSize(); this.loadAndRender(); }, 500); } } public ngOnDestroy() { this._windowResizeEventsService.removeEvent(this._windowResizeEventId); } public loadAndRender() { this.loadAllImages().subscribe((loadedImages) => { if (Array.isArray(loadedImages)) { this.renderAllUserImages(loadedImages); } else { this.renderAllUserImages([loadedImages]); } }); } public initCanvasSize() { const canvas = this.canvas; canvas.height = canvas.offsetHeight; canvas.width = canvas.offsetWidth; } public loadAllImages() { const allImages = [ this.backgroundUrl, ...this.maskImagesMap.filter((maskImageMap) => ( maskImageMap.imageEditorImage )).map((maskImageMap) => ( maskImageMap.imageEditorImage.urlToRender )), ...this.maskImagesMap.map((maskImageMap) => ( maskImageMap.mask.mask )), this.foregroundUrl, ]; return this._imagePreloaderService.load(allImages); } public clearCanvas( canvas: HTMLCanvasElement, ) { canvas.getContext('2d').clearRect( 0, 0, canvas.width, canvas.height, ); } public renderBackgroundImageOnMask( canvas: HTMLCanvasElement, backgroundImageUrl: string, backgroundScale: number, loadedImages: HTMLImageElement[], colorOverlay?: string, ) { if ( backgroundImageUrl && /** * Blend mode doesn't work in IE 10 and 11. * As this is just a visual feature we'll skip drawing the overlay * on these versions of the browsers. */ !this._browserDetectorService.isIE() ) { const overlayImage = loadedImages.find((image) => image.src === backgroundImageUrl); const canvasCtx = canvas.getContext('2d'); this.drawOnCanvas( canvasCtx, overlayImage, true, 0, 0, canvas.width, canvas.height, backgroundScale, ); if (colorOverlay) { canvasCtx.globalCompositeOperation = 'source-in'; canvasCtx.fillStyle = colorOverlay; canvasCtx.fillRect( 0, 0, canvas.width, canvas.height, ); } } } public renderUserImageOnMask( canvas: HTMLCanvasElement, maskImageMap: MaskImageMapInterface, loadedImages: HTMLImageElement[], ) { const userImage = maskImageMap.imageEditorImage && maskImageMap.imageEditorImage.urlToRender && this._canvasUtilitiesService.getImageWithUrl( loadedImages, maskImageMap.imageEditorImage.urlToRender, ); const imageEditorText = maskImageMap.imageEditorImage as ImageEditorTextInterface; const userText = imageEditorText && imageEditorText.text; const maskImage = this._canvasUtilitiesService.getImageWithUrl( loadedImages, maskImageMap.mask.mask, ); if ((userImage || userText) && maskImage) { const tempCanvas = this.generateTemporaryCanvas(canvas); const tempCanvasCtx = tempCanvas.getContext('2d'); const quad = this._canvasUtilitiesService .getScaledEnclosingQuad( tempCanvas, maskImage, maskImageMap, this.assetSize, this.dpi, ); const tx = this._canvasUtilitiesService .toCanvasCoordinateSystem( tempCanvas, maskImageMap.imageEditorImage.tx, maskImageMap, maskImage, this.assetSize, this.dpi, ); const ty = this._canvasUtilitiesService .toCanvasCoordinateSystem( tempCanvas, maskImageMap.imageEditorImage.ty, maskImageMap, maskImage, this.assetSize, this.dpi, ); const userImageX = quad.topLeft.x + tx; const userImageY = quad.topLeft.y + ty; tempCanvasCtx.globalCompositeOperation = 'source-over'; this.drawOnCanvas( tempCanvasCtx, maskImage, true, 0, 0, tempCanvas.width, tempCanvas.height, maskImageMap.mask.bleed_area.maskScale, ); this.drawBorder(tempCanvasCtx, quad); tempCanvasCtx.globalCompositeOperation = 'source-in'; this.drawOnCanvas( tempCanvasCtx, userImage || maskImage, false, userImageX, userImageY, quad.size.width, quad.size.height, maskImageMap.imageEditorImage.scale, maskImageMap.imageEditorImage.mirror, maskImageMap.imageEditorImage.rotate_degrees, imageEditorText.text, imageEditorText.textColorHex, imageEditorText.textFontFamily, imageEditorText.textFontSize, ); if (this.showImageOverspill) { tempCanvasCtx.globalCompositeOperation = 'source-over'; this.drawOnCanvas( tempCanvasCtx, userImage || maskImage, false, userImageX, userImageY, quad.size.width, quad.size.height, maskImageMap.imageEditorImage.scale, maskImageMap.imageEditorImage.mirror, maskImageMap.imageEditorImage.rotate_degrees, imageEditorText.text, imageEditorText.textColorHex, imageEditorText.textFontFamily, imageEditorText.textFontSize, 0.1, this.imageOverSpillBorderColor, ); } const originalCanvasContext = canvas.getContext('2d'); originalCanvasContext.globalCompositeOperation = 'source-over'; const canvasWidth = canvas.width; if (canvasWidth) { originalCanvasContext.drawImage( tempCanvas, 0, 0, canvas.width, canvas.height, ); } } } public drawBorder(ctx: CanvasRenderingContext2D, quad: EnclosingQuadrilateral) { if (!quad.borderInPx) { return; } const opacity = this.showImageOverspill ? 0.9 : 1; ctx.strokeStyle = `rgba(255, 255, 255, ${opacity})`; ctx.globalCompositeOperation = 'destination-out'; ctx.lineWidth = quad.borderInPx + 1; ctx.strokeRect( quad.topLeft.x - Math.round(quad.borderInPx / 2), quad.topLeft.y - Math.round(quad.borderInPx / 2), quad.size.width + quad.borderInPx, quad.size.height + quad.borderInPx); } public renderMaskOverlay( canvas: HTMLCanvasElement, loadedImages: HTMLImageElement[], ) { if ( this.foregroundUrl && /** * Blend mode doesn't work in IE 10 and 11. * As this is just a visual feature we'll skip drawing the overlay * on these versions of the browsers. */ !this._browserDetectorService.isIE() ) { const overlayImage = loadedImages.find((image) => ( image.src === this.foregroundUrl )); const canvasCtx = canvas.getContext('2d'); canvasCtx.globalCompositeOperation = this.foregroundBlendMode && this.foregroundBlendMode !== 'normal' ? this.foregroundBlendMode : 'source-over'; this.drawOnCanvas( canvasCtx, overlayImage, true, 0, 0, canvas.width, canvas.height, this.backgroundScale, ); } } public renderAllUserImages( loadedImages: HTMLImageElement[], ) { const canvas = this.canvas; this.clearCanvas(canvas); this.renderBackgroundImageOnMask( canvas, this.backgroundUrl, this.backgroundScale, loadedImages, this.colorOverlay, ); this.maskImagesMap.forEach((maskImageMap) => { this.renderUserImageOnMask( canvas, maskImageMap, loadedImages, ); }); this.renderMaskOverlay( canvas, loadedImages, ); } public drawText( ctx: CanvasRenderingContext2D, text: string, textColorHex: string, textFontFamily: string, textFontSize: number, scale: number, widthToFitWithin: number, imageWidth: number, ) { ctx.fillStyle = textColorHex; const fontSize = textFontSize * scale * ( widthToFitWithin / imageWidth ); ctx.font = `${fontSize}px ${textFontFamily}`; const textWidth = ctx.measureText(text).width; const textHeight = fontSize; ctx.fillText( text, -textWidth / 2, -textHeight / 2, ); } public drawImage( ctx: CanvasRenderingContext2D, img: HTMLImageElement, w: number, h: number, ) { if (w > 0 && h > 0) { ctx.drawImage( img, -w / 2, -h / 2, w, h, ); } } public drawImageBorder( borderColor: string, ctx: CanvasRenderingContext2D, width: number, height: number, ) { const borderWidth = 0.5; ctx.globalAlpha = 1; ctx.lineWidth = borderWidth; ctx.strokeStyle = borderColor; ctx.strokeRect( (-width / 2) + borderWidth, (-height / 2) + borderWidth, width - borderWidth * 2, height - borderWidth * 2, ); } public drawOnCanvas( ctx: CanvasRenderingContext2D, img: HTMLImageElement, fitImage: boolean, x: number, y: number, fitWidth: number, fitHeight: number, scale: number = 1, flipHorizontal = false, rotationDegrees = 0, text?: string, textColorHex?: string, textFontFamily?: string, textFontSize?: number, opacity = 1, imageOverSpillBorderColor?: string, ) { ctx.save(); const hRatio = fitWidth / img.width; const vRatio = fitHeight / img.height; const ratio = fitImage ? Math.min(hRatio, vRatio) : Math.max(hRatio, vRatio); const w = img.width * ratio * scale; const h = img.height * ratio * scale; const xoff = Math.round(x + (fitWidth - w) / 2); const yoff = Math.round(y + (fitHeight - h) / 2); // Set the origin to the center of the image ctx.translate(xoff + w / 2, yoff + h / 2); if (flipHorizontal) { ctx.scale(-1, 1); } ctx.globalAlpha = opacity; ctx.rotate(-rotationDegrees * (Math.PI / 180)); if (text) { this.drawText( ctx, text, textColorHex, textFontFamily, textFontSize, scale, w, img.width, ); } else { this.drawImage( ctx, img, w, h, ); if (imageOverSpillBorderColor) { this.drawImageBorder( imageOverSpillBorderColor, ctx, w, h, ); } } ctx.restore(); } public trackByMaskImage(maskImage: MaskImageMapInterface) { return maskImage && maskImage.imageEditorImage ? maskImage.imageEditorImage.url_full : null; } public generateTemporaryCanvas(canvasToMirror: HTMLCanvasElement) { const tempCanvas = this._document.createElement('canvas'); tempCanvas.width = canvasToMirror.width; tempCanvas.height = canvasToMirror.height; return tempCanvas; } }