import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; import { Subject } from 'rxjs'; import { buffer, map, throttleTime } from 'rxjs/operators'; import { Image } from '../image'; import { Point, Svg, SvgControlPoint, SvgItem, SvgPoint } from '../svg'; /* eslint-disable @angular-eslint/component-selector */ @Component({ selector: '[app-canvas]', templateUrl: './canvas.component.html', styleUrls: ['./canvas.component.css'] }) export class CanvasComponent implements OnInit, OnChanges, AfterViewInit { get canvasWidth(): number { return this._canvasWidth; } set canvasWidth(canvasWidth: number) { this._canvasWidth = canvasWidth; this.canvasWidthChange.emit(this._canvasWidth); } get canvasHeight(): number { return this._canvasHeight; } set canvasHeight(canvasHeight: number) { this._canvasHeight = canvasHeight; this.canvasHeightChange.emit(this._canvasHeight); } get draggedPoint(): SvgPoint | null { return this._draggedPoint; } @Input() set draggedPoint(draggedPoint: SvgPoint| null ) { this._draggedPoint = draggedPoint; this.draggedPointChange.emit(this.draggedPoint); } get focusedItem(): SvgItem | null { return this._focusedItem; } @Input() set focusedItem(focusedItem: SvgItem | null) { this._focusedItem = focusedItem; this.focusedItemChange.emit(this.focusedItem); } get hoveredItem(): SvgItem | null { return this._hoveredItem; } @Input() set hoveredItem(hoveredItem: SvgItem | null ) { this._hoveredItem = hoveredItem; this.hoveredItemChange.emit(this.hoveredItem); } get wasCanvasDragged(): boolean { return this._wasCanvasDragged; } @Input() set wasCanvasDragged(wasCanvasDragged: boolean) { this._wasCanvasDragged = wasCanvasDragged; this.wasCanvasDraggedChange.emit(this._wasCanvasDragged); } get focusedImage(): Image | null { return this._focusedImage; } @Input() set focusedImage(focusedImage: Image | null) { this._focusedImage = focusedImage; this.focusedImageChange.emit(this.focusedImage); } constructor(public canvas: ElementRef) { } @Input() parsedPath?: Svg; @Input() targetPoints: SvgPoint[] = []; @Input() controlPoints: SvgControlPoint[] = []; @Input() decimals?: number; @Input() viewPortX: number = 0; @Input() viewPortY: number = 0; @Input() viewPortWidth: number = 0; @Input() viewPortHeight: number = 0; @Input() strokeWidth: number = 1; @Input() preview: boolean = false; @Input() filled: boolean = false; @Input() showTicks: boolean = false; @Input() tickInterval: number = 1; @Input() draggedIsNew = false; @Input() images: Image[] = []; @Input() editImages = true; @Output() afterModelChange = new EventEmitter<void>(); @Output() dragging = new EventEmitter<boolean>(); @Output() viewPort = new EventEmitter<{x: number, y: number, w: number, h: number | null, force?: boolean}>(); @Output() emptyCanvas = new EventEmitter<void>(); _canvasWidth: number = 0; @Output() canvasWidthChange = new EventEmitter<number>(); _canvasHeight: number = 0; @Output() canvasHeightChange = new EventEmitter<number>(); _draggedPoint: SvgPoint | null = null; @Output() draggedPointChange = new EventEmitter<SvgPoint | null>(); _focusedItem: SvgItem | null = null; @Output() focusedItemChange = new EventEmitter<SvgItem | null>(); _hoveredItem: SvgItem | null = null; @Output() hoveredItemChange = new EventEmitter<SvgItem | null>(); _wasCanvasDragged = false; @Output() wasCanvasDraggedChange = new EventEmitter<boolean>(); _focusedImage: Image | null = null; @Output() focusedImageChange = new EventEmitter<Image | null>(); draggedEvt: MouseEvent | TouchEvent | null = null; wheel$ = new Subject<WheelEvent>(); dragWithoutClick = true; draggedImage: Image | null = null; draggedImageType: number = 0; xGrid: number[] = []; yGrid: number[] = []; // Utility functions min = Math.min; abs = Math.abs; trackByIndex = (idx: number, _: any) => idx; ngOnChanges(changes: SimpleChanges): void { if (changes.viewPortX || changes.viewPortY || changes.viewPortWidth || changes.viewPortHeight) { this.refreshGrid(); } if (changes.draggedPoint && changes.draggedPoint.currentValue) { this.startDrag(changes.draggedPoint.currentValue); } } ngAfterViewInit() { setTimeout(() => { this.refreshCanvasSize(true); }); window.addEventListener('resize', () => { this.refreshCanvasSize(true); }); // Following line is a workaround for a bug in Safari preventing the Wheel events to be fired: window.addEventListener('wheel', () => {}); } ngOnInit(): void { const cap = (val:number, max:number) => val > max ? max : val < -max ? -max : val; const throttler = throttleTime(20, undefined, {leading: false, trailing: true}); this.wheel$ .pipe( buffer(this.wheel$.pipe(throttler)) ) .pipe( map(ev => ({ event: ev[0], deltaY: ev.reduce((acc, cur) => acc + cap(cur.deltaY, 50), 0) }))) .subscribe(this.mousewheel.bind(this)); } @HostListener('mousedown', ['$event']) onMouseDown($event: MouseEvent) { this.startDragCanvas($event); $event.stopPropagation(); } @HostListener('mousemove', ['$event']) onMouseMove($event: MouseEvent) { this.drag($event); } @HostListener('mouseup', ['$event']) onMouseUp($event: MouseEvent) { this.stopDrag(); } @HostListener('touchstart', ['$event']) onTouchStart($event: TouchEvent) { this.startDragCanvas($event); $event.preventDefault(); $event.stopPropagation(); } @HostListener('touchmove', ['$event']) onTouchMove($event: TouchEvent) { this.drag($event); } @HostListener('touchend', ['$event']) onTouchEnd($event: TouchEvent) { this.stopDrag(); } @HostListener('wheel', ['$event']) onWheel($event: WheelEvent) { this.wheel$.next($event); } @HostListener('click', ['$event']) onClick($event: MouseEvent) { this.hoveredItem = null; } refreshCanvasSize(emitEmptyCanvas = false) { const rect = this.canvas.nativeElement.parentNode.getBoundingClientRect(); if (rect.width === 0 && emitEmptyCanvas) { this.emptyCanvas.emit(); } this.canvasWidth = rect.width; this.canvasHeight = rect.height; this.viewPort.emit({ x: this.viewPortX, y: this.viewPortY, w: this.viewPortWidth, h: null, force: true }); } refreshGrid() { if (5 * this.viewPortWidth <= this.canvasWidth) { this.xGrid = Array(Math.ceil(this.viewPortWidth) + 1).fill(null).map((_, i) => Math.floor(this.viewPortX) + i); this.yGrid = Array(Math.ceil(this.viewPortHeight) + 1).fill(null).map((_, i) => Math.floor(this.viewPortY) + i); } else { this.xGrid = []; this.yGrid = []; } } eventToLocation(event: MouseEvent | TouchEvent, idx = 0): {x: number, y: number} { const rect = this.canvas.nativeElement.getBoundingClientRect(); const touch = event instanceof MouseEvent ? event : event.touches[idx]; const x = this.viewPortX + (touch.clientX - rect.left) * this.strokeWidth; const y = this.viewPortY + (touch.clientY - rect.top) * this.strokeWidth; return {x, y}; } pinchToZoom(previousEvent: MouseEvent | TouchEvent, event: MouseEvent | TouchEvent) { if ( window.TouchEvent && previousEvent instanceof TouchEvent && event instanceof TouchEvent && previousEvent.touches.length >= 2 && event.touches.length >= 2) { const pt = this.eventToLocation(event, 0); const pt2 = this.eventToLocation(event, 1); const oriPt = this.eventToLocation(previousEvent, 0); const oriPt2 = this.eventToLocation(previousEvent, 1); const ptm = {x: 0.5 * (pt.x + pt2.x) , y: 0.5 * (pt.y + pt2.y)}; const oriPtm = {x: 0.5 * (oriPt.x + oriPt2.x) , y: 0.5 * (oriPt.y + oriPt2.y)}; const delta = {x: oriPtm.x - ptm.x , y: oriPtm.y - ptm.y}; const k = Math.sqrt((oriPt.x - oriPt2.x) * (oriPt.x - oriPt2.x) + (oriPt.y - oriPt2.y) * (oriPt.y - oriPt2.y)) / Math.sqrt((pt.x - pt2.x) * (pt.x - pt2.x) + (pt.y - pt2.y) * (pt.y - pt2.y)); return {zoom: k, delta, center: ptm}; } return null; } mousewheel(event: {event: WheelEvent, deltaY: number}) { const scale = Math.pow(1.005, event.deltaY); const pt = this.eventToLocation(event.event); this.zoomViewPort(scale, pt); } zoomViewPort(scale: number, pt?: {x: number, y: number}) { if (!pt) { pt = {x: this.viewPortX + 0.5 * this.viewPortWidth, y: this.viewPortY + 0.5 * this.viewPortHeight}; } const w = scale * this.viewPortWidth; const h = scale * this.viewPortHeight; const x = this.viewPortX + ((pt.x - this.viewPortX) - scale * (pt.x - this.viewPortX)); const y = this.viewPortY + ((pt.y - this.viewPortY) - scale * (pt.y - this.viewPortY)); this.viewPort.emit({x, y, w, h}); } startDrag(item: SvgPoint) { if (item !== this.draggedPoint) { this.dragWithoutClick = false; } this.dragging.emit(true); if (item.itemReference.getType().toLowerCase() === 'z') { return; } this.focusedItem = item.itemReference; this.draggedPoint = item; } startDragCanvas(event: MouseEvent | TouchEvent) { this.draggedEvt = event; this.wasCanvasDragged = false; this.dragWithoutClick = false; } startDragImage(event: MouseEvent | TouchEvent, im: Image, type: number): void { this.dragging.emit(true); this.draggedEvt = event; this.draggedImage = im; this.draggedImageType = type; this.focusedImage = im; } stopDrag() { if (this.draggedPoint && this.draggedEvt) { this.drag(this.draggedEvt); } this.dragging.emit(false); if (!this.draggedPoint && !this.wasCanvasDragged) { // unselect action this.focusedItem = null; } if (!this.draggedImage && !this.wasCanvasDragged) { this.focusedImage = null; } this.draggedPoint = null; this.draggedEvt = null; this.dragWithoutClick = true; this.draggedImage = null; } drag(event: MouseEvent | TouchEvent) { if (this.draggedPoint || this.draggedEvt || this.draggedImage) { if (!this.dragWithoutClick && event instanceof MouseEvent && event.buttons === 0) { // Stop dragging if click is not maintained anymore. this.stopDrag(); return; } event.stopPropagation(); const pt = this.eventToLocation(event); if (this.draggedImage && this.draggedEvt) { const oriPt = this.eventToLocation(this.draggedEvt); /* eslint-disable no-bitwise */ if (this.draggedImageType & 0b0001) { this.draggedImage.x1 += (pt.x - oriPt.x); } if (this.draggedImageType & 0b0010) { this.draggedImage.y1 += (pt.y - oriPt.y); } if (this.draggedImageType & 0b0100) { this.draggedImage.x2 += (pt.x - oriPt.x); } if (this.draggedImageType & 0b1000) { this.draggedImage.y2 += (pt.y - oriPt.y); } /* eslint-enable no-bitwise */ this.draggedEvt = event; } else if (this.draggedPoint && this.parsedPath) { const decimals = event.ctrlKey ? (this.decimals ? 0 : 3) : this.decimals; pt.x = parseFloat(pt.x.toFixed(decimals)); pt.y = parseFloat(pt.y.toFixed(decimals)); this.parsedPath.setLocation(this.draggedPoint, pt as Point); if (this.draggedIsNew) { const previousIdx = this.parsedPath.path.indexOf(this.draggedPoint.itemReference) - 1; if (previousIdx >= 0) { this.draggedPoint.itemReference.resetControlPoints(this.parsedPath.path[previousIdx]); } } this.afterModelChange.emit(); this.draggedEvt = null; } else if(this.draggedEvt) { this.wasCanvasDragged = true; const pinchToZoom = this.pinchToZoom(this.draggedEvt, event); if (pinchToZoom !== null){ const w = pinchToZoom.zoom * this.viewPortWidth; const h = pinchToZoom.zoom * this.viewPortHeight; const x = this.viewPortX + pinchToZoom.delta.x + (pinchToZoom.center.x - this.viewPortX) * (1 - pinchToZoom.zoom); const y = this.viewPortY + pinchToZoom.delta.y + (pinchToZoom.center.y - this.viewPortY) * (1 - pinchToZoom.zoom); this.viewPort.emit({x, y, w, h}); } else { const oriPt = this.eventToLocation(this.draggedEvt); this.viewPort.emit({ x: this.viewPortX + (oriPt.x - pt.x), y: this.viewPortY + (oriPt.y - pt.y), w: this.viewPortWidth, h: this.viewPortHeight }); } this.draggedEvt = event; } } } }