import 'hammerjs'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnChanges, Output, ViewChild, } from '@angular/core'; import { BorderUnit, CoordsInterface, DimensionsInterface, EnclosingQuadrilateral, MaskImageMapInterface, PIG_AREA_TYPE, PIGPrintGuideAreaInterface, } from './../../models/index'; import { CanvasUtilitiesService, CoordinateUtilitiesService, ImagePreloaderService, WindowResizeEventsService, } from './../../helpers/services/index'; import { CanvasImageOnMaskComponent, } from '../canvas-image-on-mask/canvas-image-on-mask.component'; import { flatten, getMid, } from './../../helpers/functions/index'; import { fadeInOutAnimation, } from './../../helpers/animations/index'; @Component({ animations: [ fadeInOutAnimation(), ], changeDetection: ChangeDetectionStrategy.OnPush, selector: 'image-editor-display-component', styleUrls: [ './image-editor-display.component.scss', ], templateUrl: './image-editor-display.component.template.pug', }) export class ImageEditorDisplayComponent implements OnChanges, AfterViewInit { @Input() public assetSize: DimensionsInterface; @Input() public dpi = 0; @Input() public maskImagesMap: MaskImageMapInterface[]; @Input() public backgroundUrl: string; @Input() public foregroundUrl: string; @Input() public foregroundBlendMode: string; @Input() public colorOverlay: string; @Input() public guideAreaColor: string; @Input() public showPrintArea: boolean; @Input() public showImageOverspill: boolean; @Input() public imageOverSpillBorderColor: string; @Input() public guideAreaStrokeWidth: number; @Input() public showGridLines = false; @Input() public gridLinesColor = '#CC0000'; @Input() public gridLinesOpacity = 0.3; @Input() public gridLineStrokeWidth = 2; @Input() public distanceToSnapToGrid = 0; @Input() public showRotationControl = false; @Input() public showRotationMeasurement = false; @Input() public distanceToSnapToRotation = 0; @Input() public showScaleControl = false; @Input() public showDeleteButton = false; @Output() public updateTranslations = new EventEmitter<[ MaskImageMapInterface, CoordsInterface ]>(); @Output() public onPinchRotateImage = new EventEmitter<[ MaskImageMapInterface, number ]>(); @Output() public onPinchScaleImage = new EventEmitter<[ MaskImageMapInterface, number ]>(); @Output() public onRotationControlRotate = new EventEmitter<[ MaskImageMapInterface, number ]>(); @Output() public onScaleControlScaleAndTranslate = new EventEmitter<[ MaskImageMapInterface, number, CoordsInterface ]>(); @Output() public onDeleteButtonClicked = new EventEmitter(); @ViewChild(CanvasImageOnMaskComponent) public canvasImageOnMaskComponent: CanvasImageOnMaskComponent; @ViewChild('imageEditorDisplay') public imageEditorDisplayMain: ElementRef; public imageEditorDisplayMainDimensions: DimensionsInterface; public imageEditorDisplayMainTopLeftCoords: CoordsInterface; public isLoading = true; public imageBeingDragged: MaskImageMapInterface; public lastClientX = 0; public lastClientY = 0; public lastRotation = 0; public imageBeingPinched: MaskImageMapInterface; public imageBeingRotated: MaskImageMapInterface; public mouseCoordinatesForRotation: CoordsInterface; public mouseCoordinatesForScale: CoordsInterface; public rotationOfRotationIcon: number; public imageBeingScaled: MaskImageMapInterface; public imageCornerBeingScaled: number; public imageCurrentScale: string; public loadedImages: HTMLImageElement[]; public previousImageUrls: string[] = []; public get maskScale(): number { return this.maskImagesMap[0].mask.bleed_area.maskScale || 1; } constructor( private _canvasUtilitiesService: CanvasUtilitiesService, private _imagePreloaderService: ImagePreloaderService, private _changeDetectorRef: ChangeDetectorRef, private _coordinateUtilitiesService: CoordinateUtilitiesService, private _windowResizeEventsService: WindowResizeEventsService, ) {} public ngOnChanges() { this.loadAllImages(); } public ngAfterViewInit() { this.recalculateImageEditorDisplayMainDimensions(); this._windowResizeEventsService.addEvent(() => ( this.recalculateImageEditorDisplayMainDimensions() )); } public recalculateImageEditorDisplayMainDimensions() { const boundingRectangle = this.imageEditorDisplayMain.nativeElement .getBoundingClientRect(); this.imageEditorDisplayMainDimensions = { height: boundingRectangle.height, width: boundingRectangle.width, }; this.imageEditorDisplayMainTopLeftCoords = { x: boundingRectangle.left, y: boundingRectangle.top, }; } public loadAllImages() { const allImageUrls = [ ...this.maskImagesMap.filter((maskImageMap) => ( maskImageMap.imageEditorImage )).map((maskImageMap) => ( maskImageMap.imageEditorImage.urlToRender )), ...this.maskImagesMap.map((maskImageMap) => ( maskImageMap.mask.mask )), this.backgroundUrl, this.foregroundUrl, ]; if (allImageUrls.some((url) => !this.previousImageUrls.includes(url))) { this.isLoading = true; this.previousImageUrls = allImageUrls; this._imagePreloaderService.load(allImageUrls).subscribe((resp) => { this.loadedImages = resp as HTMLImageElement[]; this.isLoading = false; this._changeDetectorRef.markForCheck(); }); } } public onMouseMove(event: MouseEvent | Touch) { if (this.imageBeingDragged) { const canvasDx = event.clientX - this.lastClientX; const canvasDy = event.clientY - this.lastClientY; const matchingImageBeingDragged = this.maskImagesMap .find((maskImage) => ( maskImage.imageEditorImage && maskImage.imageEditorImage.url_full === this.imageBeingDragged.imageEditorImage.url_full )); if (!matchingImageBeingDragged) { return; } // Convert canvas dx, dy to unscaled product image coordinate system const htmlImage = this._canvasUtilitiesService.getImageWithUrl( this.loadedImages, matchingImageBeingDragged.mask.mask, ); const productDx = this._canvasUtilitiesService .toPrintImageCoordinateSystem( this.canvasImageOnMaskComponent.canvas, canvasDx, matchingImageBeingDragged, htmlImage, this.assetSize, this.dpi, ); const productDy = this._canvasUtilitiesService .toPrintImageCoordinateSystem( this.canvasImageOnMaskComponent.canvas, canvasDy, matchingImageBeingDragged, htmlImage, this.assetSize, this.dpi, ); const newX = matchingImageBeingDragged.imageEditorImage.tx + productDx; const newY = matchingImageBeingDragged.imageEditorImage.ty + productDy; const snappedX = Math.abs(newX) <= this.distanceToSnapToGrid ? 0 : newX; const snappedY = Math.abs(newY) <= this.distanceToSnapToGrid ? 0 : newY; this.updateTranslations.emit([ matchingImageBeingDragged, { x: snappedX, y: snappedY, }, ]); this.lastClientX = event.clientX; this.lastClientY = event.clientY; } } public onMouseDown(event: MouseEvent | Touch) { const boundingRectangle = (event.target as HTMLElement) .getBoundingClientRect(); const productX = event.clientX - boundingRectangle.left; const productY = event.clientY - boundingRectangle.top; this.imageBeingDragged = this.getImageBeingDraggedOrFirstImage( productX, productY, ); this.lastClientX = event.clientX; this.lastClientY = event.clientY; } public onMouseUp() { this.imageBeingDragged = null; } public touchStart(touchEvent: TouchEvent) { this.onMouseDown(touchEvent.touches.item(0)); } public touchMove(touchEvent: TouchEvent) { this.onMouseMove(touchEvent.touches.item(0)); } public getImageBeingDraggedOrFirstImage( x: number, y: number, ) { const imageBeingDragged = this.getImageBeingDragged(x, y); return imageBeingDragged || this.maskImagesMap[0]; } public getImageBeingDragged( x: number, y: number, ) { const imagesBeingDragged = this.maskImagesMap.map((maskImageMap) => ({ maskImageMap, quad: this._canvasUtilitiesService .getScaledEnclosingQuad( this.canvasImageOnMaskComponent.canvas, this._canvasUtilitiesService.getImageWithUrl( this.loadedImages, maskImageMap.mask.mask, ), maskImageMap, this.assetSize, this.dpi, ), })).filter(({ quad, maskImageMap }) => ( x > quad.topLeft.x && x < quad.bottomRight.x && y > quad.topLeft.y && y < quad.bottomRight.y )); const lastImageMask = imagesBeingDragged[imagesBeingDragged.length - 1]; return lastImageMask ? lastImageMask.maskImageMap : null; } public pinchImageStart( touch: TouchInput, ) { this.imageBeingPinched = this._getSelectedImageForMultiTouchEvent( touch, ); this.lastRotation = touch.rotation; } public pinchImageMove( touch: TouchInput, ) { if (this.imageBeingPinched) { this.onPinchScaleImage.emit([ this.imageBeingPinched, this.imageBeingPinched.imageEditorImage.scale + touch.scale - 1, ]); this.onPinchRotateImage.emit([ this.imageBeingPinched, this.imageBeingPinched.imageEditorImage.rotate_degrees + -Math.round(touch.rotation - this.lastRotation), ]); } } public areGridLinesVisible() { return Boolean(this.showGridLines && this.imageBeingDragged); } public getGridLines() { return this.maskImagesMap.map((maskImage) => { const showGridX = Math.abs(maskImage.imageEditorImage.ty) <= this.distanceToSnapToGrid; const showGridY = Math.abs(maskImage.imageEditorImage.tx) <= this.distanceToSnapToGrid; const flattenedPoints = flatten( maskImage.mask.bleed_area.points, ); const xPoints = flattenedPoints.map(({ x }) => x); const yPoints = flattenedPoints.map(({ y }) => y); const midX = getMid(xPoints); const midY = getMid(yPoints); return { ...maskImage.mask.bleed_area, points: [ showGridX && [ { x: 0, y: midY, }, { x: this.assetSize.width, y: midY, }, ], showGridY && [ { x: midX, y: 0, }, { x: midX, y: this.assetSize.height, }, ], ].filter(Boolean), }; }, []); } public getImageQuadCoords(maskImageMap: MaskImageMapInterface) { const maskHtmlImage = this._canvasUtilitiesService.getImageWithUrl( this.loadedImages, maskImageMap.mask.mask, ); const userImage = this._canvasUtilitiesService.getImageWithUrl( this.loadedImages, maskImageMap.imageEditorImage.urlToRender, ); return this._canvasUtilitiesService.getImageQuadCoords( this.imageEditorDisplayMainDimensions, maskHtmlImage, userImage || { height: 1, width: 1 }, this.assetSize, maskImageMap, this.dpi, ); } public getConstrainedImageQuadCoords( maskImageMap: MaskImageMapInterface, ) { return this._coordinateUtilitiesService.constrainCoordinates( this.getImageQuadCoords(maskImageMap), { x: 0, y: 0, }, { x: this.imageEditorDisplayMainDimensions.width, y: this.imageEditorDisplayMainDimensions.height, }, ); } public getRotatedImageQuadCoords( maskImageMap, ) { return this._coordinateUtilitiesService .rotateCoordsAroundCenterOfCoords( this.getImageQuadCoords(maskImageMap), maskImageMap.imageEditorImage.rotate_degrees, ); } public getConstrainedRotatedImageQuadCoords( maskImageMap: MaskImageMapInterface, ) { return this._coordinateUtilitiesService.constrainCoordinates( this.getRotatedImageQuadCoords(maskImageMap), { x: 0, y: 0, }, { x: this.imageEditorDisplayMainDimensions.width, y: this.imageEditorDisplayMainDimensions.height, }, ); } public isImageBeingTransformed() { return Boolean( this.imageBeingRotated || this.imageBeingScaled || this.imageBeingDragged || this.imageBeingPinched, ); } public isStartRotationControlVisible() { return Boolean( this.showRotationControl && !this.isImageBeingTransformed(), ); } public mouseOverStartRotationControl( event: MouseEvent, maskImageMap: MaskImageMapInterface, ) { const elementRect = ( this.imageEditorDisplayMain.nativeElement as HTMLElement ).getBoundingClientRect(); const newCoordinates = { x: event.clientX - elementRect.left, y: event.clientY - elementRect.top, }; this.rotationOfRotationIcon = -maskImageMap.imageEditorImage.rotate_degrees; this.mouseCoordinatesForRotation = newCoordinates; } public getMouseCoordsTranslations() { const { x, y } = this.mouseCoordinatesForRotation; const rotateDegrees = this.rotationOfRotationIcon; return `translate(${x}px, ${y}px) rotate(${rotateDegrees}deg)`; } public getStartRotationControlCoords(maskImage: MaskImageMapInterface) { const rotatedCoords = this.getConstrainedRotatedImageQuadCoords(maskImage); const bottomRightCorner = rotatedCoords[2]; const bottomLeftCorner = rotatedCoords[3]; const rotationButtonWrapperHeight = 30; const midX = getMid([bottomRightCorner.x, bottomLeftCorner.x]); const maxY = getMid([bottomRightCorner.y, bottomLeftCorner.y]) - rotationButtonWrapperHeight / 2; return `translate(${midX}px, ${maxY}px) rotate(` + `${-(maskImage.imageEditorImage.rotate_degrees).toFixed(2)}deg)`; } public isRotatingControlVisible() { return Boolean( this.mouseCoordinatesForRotation && this.imageBeingRotated, ); } public getIsRotatingControlTransforms() { const { x, y } = this.mouseCoordinatesForRotation; return `translate(${x}px, ${y}px)`; } public getIsRotatingControlDegrees() { const degrees = this.maskImagesMap.find((maskImage) => ( maskImage.imageEditorImage.urlToRender === this.imageBeingRotated.imageEditorImage.urlToRender )).imageEditorImage.rotate_degrees; const degreesIn360Range = degrees % 360; const flippedDegrees = -degreesIn360Range; const result = flippedDegrees > 180 ? flippedDegrees - 360 : flippedDegrees; return result.toFixed(0); } public beginRotation(maskImageMap: MaskImageMapInterface) { this.imageBeingRotated = maskImageMap; } public applyRotation(event: MouseEvent) { if (this.imageBeingRotated) { const newCoordinates = { x: event.clientX - this.imageEditorDisplayMainTopLeftCoords.x, y: event.clientY - this.imageEditorDisplayMainTopLeftCoords.y, }; this.mouseCoordinatesForRotation = newCoordinates; const imageQuadCoords = this.getImageQuadCoords(this.imageBeingRotated); const xPoints = imageQuadCoords.map(({ x }) => x); const yPoints = imageQuadCoords.map(({ y }) => y); const midCoords = { x: getMid(xPoints), y: getMid(yPoints), }; const angleBetween = this._coordinateUtilitiesService.getAngleBetweenCoordinates( newCoordinates, midCoords, ) + 90; const nearest90 = this._coordinateUtilitiesService.getNearestDegrees( angleBetween, ); const angleToUse = Math.abs(nearest90 - angleBetween) < this.distanceToSnapToRotation ? nearest90 : angleBetween; this.rotationOfRotationIcon = angleToUse; this.onRotationControlRotate.emit([ this.imageBeingRotated, -angleToUse, ]); } } public endRotation() { this.imageBeingRotated = null; this.mouseCoordinatesForRotation = null; } public trackByUrl(maskImageMap: MaskImageMapInterface) { return maskImageMap.imageEditorImage && maskImageMap.imageEditorImage.urlToRender; } public isScaleControlVisible() { return Boolean( this.showScaleControl && !this.isImageBeingTransformed(), ); } public getStartScaleControlCoords( maskImage: MaskImageMapInterface, cornerIndex: number, ) { const rotatedCoords = this.getConstrainedRotatedImageQuadCoords(maskImage); const corner = rotatedCoords[cornerIndex]; return `translate(${corner.x}px, ${corner.y}px) rotate(` + `${-(maskImage.imageEditorImage.rotate_degrees).toFixed(2)}deg)`; } public mouseOverStartScaleControl( event: MouseEvent, ) { const elementRect = ( this.imageEditorDisplayMain.nativeElement as HTMLElement ).getBoundingClientRect(); const newCoordinates = { x: event.clientX - elementRect.left, y: event.clientY - elementRect.top, }; this.mouseCoordinatesForScale = newCoordinates; } public getIsScalingControlTransforms() { const { x, y } = this.mouseCoordinatesForScale; return `translate(${x}px, ${y}px)`; } public beginScaleFromCorner( maskImageMap: MaskImageMapInterface, imageCornerBeingScaled: number, event: MouseEvent, ) { this.imageBeingScaled = maskImageMap; this.imageCornerBeingScaled = imageCornerBeingScaled; this.lastClientX = event.clientX; this.lastClientY = event.clientY; } public applyScaleFromCorner( event: MouseEvent, ) { if (this.imageBeingScaled) { this.recalculateImageEditorDisplayMainDimensions(); const indexOfCornerBeingDragged = this.imageCornerBeingScaled; const indexOfOppositeCorner = (indexOfCornerBeingDragged + 2) % 4; const targetNewTopLeft = { x: event.clientX - this.imageEditorDisplayMainTopLeftCoords.x, y: event.clientY - this.imageEditorDisplayMainTopLeftCoords.y, }; this.mouseCoordinatesForScale = targetNewTopLeft; const imageCoords = this.getRotatedImageQuadCoords( this.imageBeingScaled, ); const oppositeCorner = imageCoords[indexOfOppositeCorner]; const distanceBetweenOriginalCoords = this._coordinateUtilitiesService.getDistanceBetweenCoords( { x: this.lastClientX - this.imageEditorDisplayMainTopLeftCoords.x, y: this.lastClientY - this.imageEditorDisplayMainTopLeftCoords.y, }, oppositeCorner, ); const distanceBetweenNewCoords = this._coordinateUtilitiesService.getDistanceBetweenCoords( { x: event.clientX - this.imageEditorDisplayMainTopLeftCoords.x, y: event.clientY - this.imageEditorDisplayMainTopLeftCoords.y, }, oppositeCorner, ); const newScale = this.imageBeingScaled.imageEditorImage.scale * (distanceBetweenNewCoords / distanceBetweenOriginalCoords); const newImageCoords = this.getRotatedImageQuadCoords( { ...this.imageBeingScaled, imageEditorImage: { ...this.imageBeingScaled.imageEditorImage, scale: newScale, }, }, ); const newOppositeCorner = newImageCoords[indexOfOppositeCorner]; const xDistance = oppositeCorner.x - newOppositeCorner.x; const yDistance = oppositeCorner.y - newOppositeCorner.y; const htmlImage = this._canvasUtilitiesService.getImageWithUrl( this.loadedImages, this.imageBeingScaled.mask.mask, ); const productDx = this._canvasUtilitiesService .toPrintImageCoordinateSystem( this.canvasImageOnMaskComponent.canvas, xDistance, this.imageBeingScaled, htmlImage, this.assetSize, this.dpi, ); const productDy = this._canvasUtilitiesService .toPrintImageCoordinateSystem( this.canvasImageOnMaskComponent.canvas, yDistance, this.imageBeingScaled, htmlImage, this.assetSize, this.dpi, ); this.onScaleControlScaleAndTranslate.emit([ this.imageBeingScaled, newScale, { x: productDx + this.imageBeingScaled.imageEditorImage.tx, y: productDy + this.imageBeingScaled.imageEditorImage.ty, }, ]); this.imageCurrentScale = (newScale * 100).toFixed(1); } } public endScaleFromCorner() { this.imageBeingScaled = null; } public getIsScalingNorthWestToSouthEastForCorner( imageCornerToEvaluate: number, maskImageMap: MaskImageMapInterface, ) { const nearestDegrees = this._coordinateUtilitiesService.getNearestDegrees( maskImageMap.imageEditorImage.rotate_degrees, ); return nearestDegrees % 180 === 0 ? imageCornerToEvaluate % 2 === 0 : (imageCornerToEvaluate + 1) % 2 === 0; } public isScalingNorthWestToSouthEast() { return this.getIsScalingNorthWestToSouthEastForCorner( this.imageCornerBeingScaled, this.imageBeingScaled, ); } public isDeleteControlVisible() { return this.showDeleteButton && !this.isImageBeingTransformed(); } public getDeleteButtonTransforms(maskImage: MaskImageMapInterface) { const rotatedCoords = this.getConstrainedRotatedImageQuadCoords(maskImage); const topLeftCorner = rotatedCoords[0]; const topRightCorner = rotatedCoords[1]; const rotationButtonWrapperHeight = 30; const midX = getMid([topRightCorner.x, topLeftCorner.x]); const minY = getMid([topRightCorner.y, topLeftCorner.y]) - rotationButtonWrapperHeight / 2; return `translate(${midX}px, ${minY}px) rotate(` + `${ -(maskImage.imageEditorImage.rotate_degrees + 180).toFixed(2) }deg)`; } public deleteButtonClicked(maskImageMap: MaskImageMapInterface) { this.onDeleteButtonClicked.emit(maskImageMap); } private _getSelectedImageForMultiTouchEvent( { center, target, }: TouchInput, ) { const boundingRectangle = (target as HTMLElement) .getBoundingClientRect(); return this.getImageBeingDraggedOrFirstImage( center.x - boundingRectangle.left, center.y - boundingRectangle.top, ); } public get printGuideArea(): PIGPrintGuideAreaInterface[] { return this.maskImagesMap.map((maskImageMap) => this._hasBorder(maskImageMap) ? this._getBleedAreaWithBorder(maskImageMap) : maskImageMap.mask.bleed_area, ); } private _hasBorder = (maskImageMap: MaskImageMapInterface): boolean => maskImageMap.imageEditorImage.borderWidth && maskImageMap.imageEditorImage.borderUnit !== BorderUnit.None && maskImageMap.mask.bleed_area.type === PIG_AREA_TYPE.RECTANGLE private _getBleedAreaWithBorder = (maskImageMap: MaskImageMapInterface): PIGPrintGuideAreaInterface => { const maskImageUrl = this._canvasUtilitiesService.getImageWithUrl(this.loadedImages, maskImageMap.mask.mask); const quad = this._canvasUtilitiesService.getScaledEnclosingQuad(maskImageUrl, maskImageUrl, maskImageMap, this.assetSize, this.dpi); return { ...maskImageMap.mask.bleed_area, points: [[quad.topLeft, quad.topRight, quad.bottomRight, quad.bottomLeft]], }; } }