From d3f1a75170496a888ad898a94015d81821feeb21 Mon Sep 17 00:00:00 2001 From: Max Vogler Date: Fri, 27 May 2022 10:48:01 +0200 Subject: [PATCH] Implement cropping in preprocess tab. --- src/app/preprocess/preprocess.component.html | 35 ++++- src/app/preprocess/preprocess.component.scss | 61 ++++++++- src/app/preprocess/preprocess.component.ts | 134 +++++++++++++++---- 3 files changed, 194 insertions(+), 36 deletions(-) diff --git a/src/app/preprocess/preprocess.component.html b/src/app/preprocess/preprocess.component.html index 468fbc0..d55fcac 100644 --- a/src/app/preprocess/preprocess.component.html +++ b/src/app/preprocess/preprocess.component.html @@ -4,12 +4,12 @@ Size Colors @@ -27,8 +27,8 @@
- {{ targetWidth }} x - {{ targetHeight }} + {{ croppedWidth }} x + {{ croppedHeight }}
{{ realWidth | number: "1.0-0" }} x {{ realHeight | number: "1.0-0" }} cm @@ -37,5 +37,28 @@
- +
+
+ + +
+ +
diff --git a/src/app/preprocess/preprocess.component.scss b/src/app/preprocess/preprocess.component.scss index e40b20a..7fcf8d8 100644 --- a/src/app/preprocess/preprocess.component.scss +++ b/src/app/preprocess/preprocess.component.scss @@ -16,12 +16,71 @@ mat-toolbar.vertical { } } +$button-size: 36px; + .scroll-container { flex: 1; display: flex; + align-items: center; overflow: auto; - & > * { + .canvas-buttons { margin: 0 auto; + position: relative; + + canvas { + border: 1px solid var(--accent-border); + } + + .expand-buttons { + display: flex; + position: absolute; + + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: center; + + &.expand-bottom { + top: initial; + bottom: -$button-size + 6px; + } + + &.expand-top { + bottom: initial; + top: -$button-size; + transform: scale(-1, -1); + } + + &.expand-left, + &.expand-right { + flex-direction: column; + + .mat-button { + min-height: 64px; + line-height: 24px; + min-width: 0; + width: 36px; + padding: 16px 0; + + mat-icon { + transform: rotate(90deg); + } + } + } + + &.expand-left { + right: initial; + left: -$button-size; + } + + &.expand-right { + left: initial; + right: -$button-size; + transform: scale(-1, -1); + } + } } } diff --git a/src/app/preprocess/preprocess.component.ts b/src/app/preprocess/preprocess.component.ts index 6523309..a97554d 100644 --- a/src/app/preprocess/preprocess.component.ts +++ b/src/app/preprocess/preprocess.component.ts @@ -29,6 +29,13 @@ import { import { downscale, EditableImageData, getContext2D } from '../image'; import { requireNonNull } from '../state'; +enum Edge { + TOP = 'top', + RIGHT = 'right', + BOTTOM = 'bottom', + LEFT = 'left', +} + @Component({ selector: 'app-preprocess', templateUrl: './preprocess.component.html', @@ -37,15 +44,17 @@ import { requireNonNull } from '../state'; export class PreprocessComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy { + readonly Edge = Edge; + @ViewChild('canvas') canvas!: ElementRef; private ctx!: CanvasRenderingContext2D; - readonly minTargetWidth = 8; + readonly minTargetScale = 8; targetColors = 20; - targetWidth = this.minTargetWidth; + targetScale = this.minTargetScale; @Input() imageData!: EditableImageData; @@ -53,7 +62,7 @@ export class PreprocessComponent @Output() readonly imageDataOnDestroy = new EventEmitter(); - get maxTargetWidth() { + get maxTargetScale() { return Math.min(80, this.imageData?.width ?? 80); } @@ -61,36 +70,55 @@ export class PreprocessComponent return Math.min(20, this.imageData?.count().size ?? 20); } - get targetHeight() { + get scaledHeight() { return Math.round( - (this.imageData.height / this.imageData.width) * this.targetWidth + (this.imageData.height / this.imageData.width) * this.scaledWidth ); } + get scaledWidth() { + return this.targetScale; + } + + get croppedWidth() { + return this.scaledWidth - this.crops.left - this.crops.right; + } + + get croppedHeight() { + return this.scaledHeight - this.crops.top - this.crops.bottom; + } + get realWidth() { - return this.targetWidth * 7.6; + return this.croppedWidth * 7.6; } get realHeight() { - return this.targetHeight * 7.6; + return this.croppedHeight * 7.6; } + crops = { + [Edge.LEFT]: 0, + [Edge.RIGHT]: 0, + [Edge.TOP]: 0, + [Edge.BOTTOM]: 0, + }; + ngOnInit(): void { requireNonNull(this.imageData); - let targetWidth = this.imageData.width; + let targetScale = this.imageData.width; - if (targetWidth === 0) { - throw new Error('targetWidth is 0'); + if (targetScale === 0) { + throw new Error('targetScale is 0'); } - while (targetWidth > this.maxTargetWidth) { - targetWidth /= 2; + while (targetScale > this.maxTargetScale) { + targetScale /= 2; } - while (targetWidth < this.minTargetWidth) { - targetWidth *= 2; + while (targetScale < this.minTargetScale) { + targetScale *= 2; } - this.targetWidth = Math.round(targetWidth); + this.targetScale = Math.round(targetScale); } ngAfterViewInit(): void { @@ -107,36 +135,84 @@ export class PreprocessComponent Math.max( 1, Math.min( - (window.innerWidth / this.imageData.width) * 0.9, - (window.innerHeight / this.imageData.height) * 0.9, + (window.innerWidth - 200) / this.croppedWidth, + (window.innerHeight - 200) / this.croppedHeight, 25 ) ) ); - this.canvas.nativeElement.style.width = `${this.imageData.width * zoom}px`; - this.canvas.nativeElement.style.height = `${ - this.imageData.height * zoom - }px`; - - const targetHeight = this.targetHeight; - this.ctx.canvas.width = this.targetWidth; - this.ctx.canvas.height = targetHeight; + this.canvas.nativeElement.style.width = `${this.croppedWidth * zoom}px`; + this.canvas.nativeElement.style.height = `${this.croppedHeight * zoom}px`; + + this.ctx.canvas.width = this.croppedWidth; + this.ctx.canvas.height = this.croppedHeight; this.ctx.fillStyle = '#ffffff'; - this.ctx.fillRect(0, 0, this.targetWidth, targetHeight); + this.ctx.fillRect(0, 0, this.croppedWidth, this.croppedHeight); // TODO: To reduce GC and improve performance, reuse one ImageData and change // the its size. const scaledImageData = new EditableImageData( - this.ctx.getImageData(0, 0, this.targetWidth, targetHeight) + new ImageData(this.scaledWidth, this.scaledHeight) ); + downscale(this.imageData, scaledImageData, this.targetColors); - this.ctx.putImageData(scaledImageData.imageData, 0, 0); + + this.ctx.putImageData( + scaledImageData.imageData, + -this.crops[Edge.LEFT], + -this.crops[Edge.TOP], + 0, + 0, + this.scaledWidth, + this.scaledHeight + ); + } + + expand(edge: Edge) { + if (this.canExpand(edge)) { + this.crops[edge]--; + this.ngOnChanges(); + } + } + + shrink(edge: Edge) { + if (this.canShrink(edge)) { + this.crops[edge]++; + this.ngOnChanges(); + } + } + + canExpand(edge: Edge) { + return this.crops[edge] > 0; + } + + canShrink(edge: Edge) { + if (edge === Edge.TOP || edge === Edge.BOTTOM) { + return ( + this.crops[Edge.TOP] + this.crops[Edge.BOTTOM] < this.scaledHeight - 1 + ); + } else { + return ( + this.crops[Edge.LEFT] + this.crops[Edge.RIGHT] < this.scaledWidth - 1 + ); + } + } + + resizeTo(scaledWidth: number) { + this.targetScale = scaledWidth; + this.crops = { + [Edge.LEFT]: 0, + [Edge.RIGHT]: 0, + [Edge.TOP]: 0, + [Edge.BOTTOM]: 0, + }; + this.ngOnChanges(); } ngOnDestroy(): void { this.imageDataOnDestroy.next( new EditableImageData( - this.ctx.getImageData(0, 0, this.targetWidth, this.targetHeight) + this.ctx.getImageData(0, 0, this.croppedWidth, this.croppedHeight) ) ); }