From 897cd88d2e9d2abd9e28cc6dab3caec9685eaf3a Mon Sep 17 00:00:00 2001 From: Alba Date: Thu, 18 Jul 2024 16:25:02 +0200 Subject: [PATCH 1/8] Refractor to different state Also organised imports, changed to updating properties, no optional chaining, no nullish coalescing or undefined values unless strictly necessary (will test this later). --- projects/demo-app/src/app/app.component.html | 55 +-- projects/demo-app/src/app/app.component.ts | 174 +++------ .../src/lib/component/cropper.state.spec.ts | 88 ----- .../src/lib/component/cropper.state.ts | 208 ----------- .../component/image-cropper.component.html | 20 +- .../lib/component/image-cropper.component.ts | 336 ++++++++---------- .../interfaces/cropper-options.interface.ts | 29 -- .../lib/interfaces/image-cropper-settings.ts | 49 +++ .../lib/interfaces/image-source-interface.ts | 33 ++ .../interfaces/image-transform.interface.ts | 14 +- .../src/lib/interfaces/index.ts | 17 +- .../src/lib/services/crop.service.ts | 130 +++---- .../src/lib/services/load-image.service.ts | 57 ++- .../src/lib/state/image-cropper-state.ts | 134 +++++++ .../state/init-values/cropper-init-value.ts | 12 + .../image-cropper-settings-init-value.ts | 41 +++ .../src/lib/state/init-values/index.ts | 3 + .../state/init-values/transform-init-value.ts | 15 + .../src/lib/utils/cropper-position.utils.ts | 296 +++++++-------- .../lib/utils/cropper-size-bounds.utils.ts | 49 +++ .../ngx-image-cropper/src/lib/utils/index.ts | 8 + projects/ngx-image-cropper/src/public-api.ts | 1 + 22 files changed, 825 insertions(+), 944 deletions(-) delete mode 100644 projects/ngx-image-cropper/src/lib/component/cropper.state.spec.ts delete mode 100644 projects/ngx-image-cropper/src/lib/component/cropper.state.ts delete mode 100644 projects/ngx-image-cropper/src/lib/interfaces/cropper-options.interface.ts create mode 100644 projects/ngx-image-cropper/src/lib/interfaces/image-cropper-settings.ts create mode 100644 projects/ngx-image-cropper/src/lib/interfaces/image-source-interface.ts create mode 100644 projects/ngx-image-cropper/src/lib/state/image-cropper-state.ts create mode 100644 projects/ngx-image-cropper/src/lib/state/init-values/cropper-init-value.ts create mode 100644 projects/ngx-image-cropper/src/lib/state/init-values/image-cropper-settings-init-value.ts create mode 100644 projects/ngx-image-cropper/src/lib/state/init-values/index.ts create mode 100644 projects/ngx-image-cropper/src/lib/state/init-values/transform-init-value.ts create mode 100644 projects/ngx-image-cropper/src/lib/utils/cropper-size-bounds.utils.ts create mode 100644 projects/ngx-image-cropper/src/lib/utils/index.ts diff --git a/projects/demo-app/src/app/app.component.html b/projects/demo-app/src/app/app.component.html index 07d54c9..5b3984d 100644 --- a/projects/demo-app/src/app/app.component.html +++ b/projects/demo-app/src/app/app.component.html @@ -1,4 +1,4 @@ - +

@@ -18,30 +18,30 @@
- - - + + +

- + - - - + + +

- - - - - + + + + +
@@ -49,37 +49,12 @@
Loading...
diff --git a/projects/demo-app/src/app/app.component.ts b/projects/demo-app/src/app/app.component.ts index b0860cc..2e5c719 100644 --- a/projects/demo-app/src/app/app.component.ts +++ b/projects/demo-app/src/app/app.component.ts @@ -1,10 +1,13 @@ import { Component } from '@angular/core'; import { - CropperPosition, Dimensions, + getCropperInitValue, + getTransformInitValue, + getImageCropperSettingsInitValue, ImageCroppedEvent, ImageCropperComponent, - ImageTransform + ImageCropperSettings, + PartialImageCropperSettings, } from 'ngx-image-cropper'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { NgIf } from '@angular/common'; @@ -22,39 +25,11 @@ export class AppComponent { loading = false; croppedImage: SafeUrl = ''; - imageChangedEvent: Event | null = null; - imageURL?: string; - hidden = false; - disabled = false; - alignImage = 'center' as const; - roundCropper = false; - backgroundColor = 'red'; - allowMoveImage = false; - hideResizeSquares = false; - canvasRotation = 0; - aspectRatio = 4 / 3; - containWithinAspectRatio = false; - maintainAspectRatio = false; - cropperStaticWidth = 0; - cropperStaticHeight = 0; - cropperMinWidth = 0; - cropperMinHeight = 0; - cropperMaxWidth = 0; - cropperMaxHeight = 0; - resetCropOnAspectRatioChange = true; - cropper?: CropperPosition; - transform: ImageTransform = { - translateUnit: 'px', - scale: 1, - rotate: 0, - flipH: false, - flipV: false, - translateH: 0, - translateV: 0 - }; - timeout: any; eventList = {}; + + settings: ImageCropperSettings = getImageCropperSettingsInitValue(); + settingsToUpdate: PartialImageCropperSettings = {}; constructor( private sanitizer: DomSanitizer @@ -63,12 +38,7 @@ export class AppComponent { fileChangeEvent(event: Event): void { this.loading = true; - this.imageChangedEvent = event; - } - - imageCropped(event: ImageCroppedEvent) { - this.croppedImage = this.sanitizer.bypassSecurityTrustUrl(event.objectUrl || event.base64 || ''); - console.log('CROPPED', event); + this.settingsToUpdate = { imageSource: { imageChangedEvent : event } } } imageLoaded() { @@ -76,140 +46,113 @@ export class AppComponent { console.log('Image loaded'); } + loadImageFailed() { + console.error('Load image failed'); + } + + settingsUpdated(settings: ImageCropperSettings){ + this.settings = settings; + } + cropperReady(sourceImageDimensions: Dimensions) { console.log('Cropper ready', sourceImageDimensions); this.loading = false; } - loadImageFailed() { - console.error('Load image failed'); - } - - transformChange(transform: ImageTransform) { - console.log('transform changed', transform); + imageCropped(event: ImageCroppedEvent) { + this.croppedImage = this.sanitizer.bypassSecurityTrustUrl(event.objectUrl || event.base64 || ''); + console.log('CROPPED', event); } rotateLeft() { this.loading = true; setTimeout(() => { // Use timeout because rotating image is a heavy operation and will block the ui thread - this.canvasRotation--; - this.flipAfterRotate(); + this.settingsToUpdate = { canvasRotation: --this.settings.canvasRotation, ...this.flipAfterRotate() }; }); } rotateRight() { this.loading = true; - setTimeout(() => { - this.canvasRotation++; - this.flipAfterRotate(); + setTimeout(() => { + this.settingsToUpdate = { canvasRotation: ++this.settings.canvasRotation, ...this.flipAfterRotate() }; }); } moveLeft() { - this.transform = { - ...this.transform, - translateH: this.transform.translateH! - 1 - }; + this.settingsToUpdate = { transform: { translateX: --this.settings.transform.translateX } }; } moveRight() { - this.transform = { - ...this.transform, - translateH: this.transform.translateH! + 1 - }; + this.settingsToUpdate = { transform: { translateX: this.settings.transform.translateX + 1 } }; } - moveDown() { - this.transform = { - ...this.transform, - translateV: this.transform.translateV! + 1 - }; + moveUp() { + this.settingsToUpdate = { transform: { translateY: --this.settings.transform.translateY } }; } - moveUp() { - this.transform = { - ...this.transform, - translateV: this.transform.translateV! - 1 - }; + moveDown() { + this.settingsToUpdate = { transform: { translateY: ++this.settings.transform.translateY } }; } private flipAfterRotate() { - const flippedH = this.transform.flipH; - const flippedV = this.transform.flipV; - this.transform = { - ...this.transform, - flipH: flippedV, - flipV: flippedH, - translateH: 0, - translateV: 0 + return { + transform: { + flipX: this.settings.transform.flipY, + flipY: this.settings.transform.flipX, + translateX: 0, + translateY: 0 + } }; } flipHorizontal() { - this.transform = { - ...this.transform, - flipH: !this.transform.flipH - }; + this.settingsToUpdate = { transform: { flipX: !this.settings.transform.flipX } }; } flipVertical() { - this.transform = { - ...this.transform, - flipV: !this.transform.flipV - }; + this.settingsToUpdate = { transform: { flipY: !this.settings.transform.flipY } }; } resetImage() { - this.canvasRotation = 0; - this.cropper = undefined; - this.maintainAspectRatio = false; - this.transform = { - translateUnit: 'px', - scale: 1, - rotate: 0, - flipH: false, - flipV: false, - translateH: 0, - translateV: 0 + this.settingsToUpdate = { + canvasRotation: 0, + cropper: getCropperInitValue(), + maintainAspectRatio: false, + transform: getTransformInitValue(), }; } zoomOut() { - this.transform = { - ...this.transform, - scale: this.transform.scale! - .1 - }; + this.settingsToUpdate = { transform: { scale: this.settings.transform.scale - .1 } }; } zoomIn() { - this.transform = { - ...this.transform, - scale: this.transform.scale! + .1 - }; + this.settingsToUpdate = { transform: { scale: this.settings.transform.scale + .1 } }; } updateRotation(rotate: number) { - this.transform = { - ...this.transform, - rotate - }; + this.settingsToUpdate = { transform: { scale: this.settings.transform.rotate } }; } toggleAspectRatio() { - this.aspectRatio = this.aspectRatio === 4 / 3 ? 16 / 5 : 4 / 3; + this.settingsToUpdate = { aspectRatio: this.settings.aspectRatio === 4 / 3 ? 16 / 5 : 4 / 3 }; } toggleBackgroundColor() { - this.backgroundColor = this.backgroundColor === 'red' ? 'blue' : 'red'; + this.settingsToUpdate = { backgroundColor: this.settings.backgroundColor === 'red' ? 'blue' : 'red' }; } // prevent over triggering app when typing debounce(event: any) { clearTimeout(this.timeout); - (this.eventList as any)[event.target!.id] = event.target.value; + (this.eventList as any)[event.target.id] = event.target.value; this.timeout = setTimeout(() => { for (const [key, value] of Object.entries(this.eventList)) { - (this as any)[key] = Number(value); + if (key === 'imageURL') { + this.settingsToUpdate = { imageSource: { [key]: value as string }}; + } else { + this.settingsToUpdate = { [key]: Number(value) }; + } } this.eventList = {}; }, 500); @@ -220,11 +163,10 @@ export class AppComponent { use it to test whatever you want */ test() { - this.canvasRotation = 3; - this.transform = { - ...this.transform, - scale: 2 + this.settingsToUpdate = { + canvasRotation: 3, + transform: {scale: 2}, + cropper: { x1: 190, y1: 221.5, x2: 583, y2: 344.3125 } // has 16/5 aspect ratio }; - this.cropper = {x1: 190, y1: 221.5, x2: 583, y2: 344.3125}; // has 16/5 aspect ratio } } diff --git a/projects/ngx-image-cropper/src/lib/component/cropper.state.spec.ts b/projects/ngx-image-cropper/src/lib/component/cropper.state.spec.ts deleted file mode 100644 index 13ef21c..0000000 --- a/projects/ngx-image-cropper/src/lib/component/cropper.state.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { CropperPosition, ImageTransform, LoadedImage } from '../interfaces'; -import { SimpleChange, SimpleChanges } from '@angular/core'; -import { CropperState } from './cropper.state'; - -describe('CropperState', () => { - let cropperState: CropperState; - - beforeEach(() => { - cropperState = new CropperState(); - }); - - it('should initialize with default options', () => { - expect(cropperState.options.format).toBe('png'); - expect(cropperState.options.output).toBe('blob'); - expect(cropperState.options.autoCrop).toBe(true); - expect(cropperState.options.maintainAspectRatio).toBe(true); - expect(cropperState.options.aspectRatio).toBe(1); - }); - - it('should set options from changes', () => { - const changes: SimpleChanges = { - format: new SimpleChange('png', 'jpeg', true), - output: new SimpleChange('blob', 'base64', true), - maintainAspectRatio: new SimpleChange(false, true, true) - }; - cropperState.setOptionsFromChanges(changes); - expect(cropperState.options.format).toBe('jpeg'); - expect(cropperState.options.output).toBe('base64'); - expect(cropperState.options.maintainAspectRatio).toBe(true); - }); - - it('should validate options and throw error if aspectRatio is not set when maintainAspectRatio is enabled', () => { - expect(() => { - cropperState.setOptions({maintainAspectRatio: true, aspectRatio: 0}); - }).toThrowError('`aspectRatio` should > 0 when `maintainAspectRatio` is enabled'); - }); - - it('should set max size and update cropper scaled min and max sizes', () => { - cropperState.loadedImage = { - transformed: { - image: new Image(), - size: {width: 200, height: 200} - } - } as LoadedImage; - - cropperState.setMaxSize(100, 100); - expect(cropperState.maxSize).toEqual({width: 100, height: 100}); - expect(cropperState.cropperScaledMinWidth).toBe(20); - expect(cropperState.cropperScaledMinHeight).toBe(20); - expect(cropperState.cropperScaledMaxWidth).toBe(100); - expect(cropperState.cropperScaledMaxHeight).toBe(100); - }); - - it('should resize cropper position based on new max size', () => { - cropperState.maxSize = {width: 200, height: 200}; - cropperState.cropper = {x1: 25, x2: 75, y1: 25, y2: 75}; - cropperState.resizeCropperPosition({width: 100, height: 100}); - expect(cropperState.cropper).toEqual({x1: 50, x2: 150, y1: 50, y2: 150}); - }); - - it('should correctly determine if aspect ratio is correct', () => { - cropperState.cropper = {x1: 0, x2: 100, y1: 0, y2: 100}; - cropperState.options.aspectRatio = 1; - expect(cropperState.aspectRatioIsCorrect()).toBe(true); - - cropperState.cropper = {x1: 0, x2: 100, y1: 0, y2: 50}; - cropperState.options.aspectRatio = 2; - expect(cropperState.aspectRatioIsCorrect()).toBe(true); - }); - - it('should correctly compare cropper positions', () => { - const position: CropperPosition = {x1: 0, x2: 100, y1: 0, y2: 100}; - cropperState.cropper = {x1: 0, x2: 100, y1: 0, y2: 100}; - expect(cropperState.equalsCropperPosition(position)).toBe(true); - - cropperState.cropper = {x1: 0, x2: 100, y1: 0, y2: 50}; - expect(cropperState.equalsCropperPosition(position)).toBe(false); - }); - - it('should correctly compare transforms', () => { - const transform: ImageTransform = {scale: 1, rotate: 0, translateH: 0, translateV: 0, flipH: false, flipV: false}; - cropperState.transform = {scale: 1, rotate: 0, translateH: 0, translateV: 0, flipH: false, flipV: false}; - expect(cropperState.equalsTransform(transform)).toBe(true); - - cropperState.transform = {scale: 1.5, rotate: 0, translateH: 0, translateV: 0, flipH: false, flipV: false}; - expect(cropperState.equalsTransform(transform)).toBe(false); - }); -}); diff --git a/projects/ngx-image-cropper/src/lib/component/cropper.state.ts b/projects/ngx-image-cropper/src/lib/component/cropper.state.ts deleted file mode 100644 index a19ccf1..0000000 --- a/projects/ngx-image-cropper/src/lib/component/cropper.state.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { CropperOptions } from '../interfaces/cropper-options.interface'; -import { CropperPosition, Dimensions, ImageTransform, LoadedImage } from '../interfaces'; -import { SimpleChanges } from '@angular/core'; -import { checkCropperPosition } from '../utils/cropper-position.utils'; - -export class CropperState { - - options: CropperOptions = { - format: 'png', - output: 'blob', - autoCrop: true, - maintainAspectRatio: true, - aspectRatio: 1, - resetCropOnAspectRatioChange: true, - resizeToWidth: 0, - resizeToHeight: 0, - cropperMinWidth: 0, - cropperMinHeight: 0, - cropperMaxHeight: 0, - cropperMaxWidth: 0, - cropperStaticWidth: 0, - cropperStaticHeight: 0, - canvasRotation: 0, - roundCropper: false, - onlyScaleDown: false, - imageQuality: 92, - backgroundColor: null, - containWithinAspectRatio: false, - hideResizeSquares: false, - alignImage: 'center', - cropperFrameAriaLabel: undefined, - checkImageType: true - }; - - loadedImage?: LoadedImage; - maxSize?: Dimensions; - cropper: CropperPosition = {x1: 0, x2: 0, y1: 0, y2: 0}; - transform: ImageTransform = {}; - - // Internal - cropperScaledMinWidth = 20; - cropperScaledMinHeight = 20; - cropperScaledMaxWidth = 20; - cropperScaledMaxHeight = 20; - stepSize = 3; - - setOptionsFromChanges(changes: SimpleChanges): void { - if (changes['options']?.currentValue) { - this.setOptions(changes['options'].currentValue); - } - const options = Object.entries(changes) - .filter(([key]) => key in this.options) - .reduce((acc, [key, change]) => ({ - ...acc, - [key]: change.currentValue - }), {} as Partial); - if (Object.keys(options).length > 0) { - this.setOptions(options); - } - } - - setOptions(options: Partial): void { - this.options = { - ...this.options, - ...(options || {}) - }; - this.validateOptions(); - - if (!this.loadedImage?.transformed.image.complete || !this.maxSize) { - return; - } - - let positionPossiblyChanged = false; - if ((this.options.maintainAspectRatio && options['aspectRatio']) || options['maintainAspectRatio']) { - this.setCropperScaledMinSize(); - this.setCropperScaledMaxSize(); - if (this.options.maintainAspectRatio && (this.options.resetCropOnAspectRatioChange || !this.aspectRatioIsCorrect())) { - this.cropper = this.maxSizeCropperPosition(); - positionPossiblyChanged = true; - } - } else { - if (options['cropperMinWidth'] || options['cropperMinHeight']) { - this.setCropperScaledMinSize(); - positionPossiblyChanged = true; - } - if (options['cropperMaxWidth'] || options['cropperMaxHeight']) { - this.setCropperScaledMaxSize(); - positionPossiblyChanged = true; - } - if (options['cropperStaticWidth'] || options['cropperStaticHeight']) { - positionPossiblyChanged = true; - } - } - - if (positionPossiblyChanged) { - this.cropper = checkCropperPosition(this.cropper, this, false); - } - } - - private validateOptions(): void { - if (this.options.maintainAspectRatio && !this.options.aspectRatio) { - throw new Error('`aspectRatio` should > 0 when `maintainAspectRatio` is enabled'); - } - } - - setMaxSize(width: number, height: number): void { - this.maxSize = {width, height}; - this.setCropperScaledMinSize(); - this.setCropperScaledMaxSize(); - } - - setCropperScaledMinSize(): void { - if (this.loadedImage?.transformed.size) { - this.setCropperScaledMinWidth(); - this.setCropperScaledMinHeight(); - } else { - this.cropperScaledMinWidth = 20; - this.cropperScaledMinHeight = 20; - } - } - - setCropperScaledMinWidth(): void { - this.cropperScaledMinWidth = this.options.cropperMinWidth > 0 - ? Math.max(20, this.options.cropperMinWidth / this.loadedImage!.transformed.size.width * this.maxSize!.width) - : 20; - } - - setCropperScaledMinHeight(): void { - if (this.options.maintainAspectRatio) { - this.cropperScaledMinHeight = Math.max(20, this.cropperScaledMinWidth / this.options.aspectRatio); - } else if (this.options.cropperMinHeight > 0) { - this.cropperScaledMinHeight = Math.max( - 20, - this.options.cropperMinHeight / this.loadedImage!.transformed.size.height * this.maxSize!.height - ); - } else { - this.cropperScaledMinHeight = 20; - } - } - - setCropperScaledMaxSize(): void { - if (this.loadedImage?.transformed.size) { - const ratio = this.loadedImage.transformed.size.width / this.maxSize!.width; - this.cropperScaledMaxWidth = this.options.cropperMaxWidth > 20 ? this.options.cropperMaxWidth / ratio : this.maxSize!.width; - this.cropperScaledMaxHeight = this.options.cropperMaxHeight > 20 ? this.options.cropperMaxHeight / ratio : this.maxSize!.height; - if (this.options.maintainAspectRatio) { - if (this.cropperScaledMaxWidth > this.cropperScaledMaxHeight * this.options.aspectRatio) { - this.cropperScaledMaxWidth = this.cropperScaledMaxHeight * this.options.aspectRatio; - } else if (this.cropperScaledMaxWidth < this.cropperScaledMaxHeight * this.options.aspectRatio) { - this.cropperScaledMaxHeight = this.cropperScaledMaxWidth / this.options.aspectRatio; - } - } - } else { - this.cropperScaledMaxWidth = this.maxSize!.width; - this.cropperScaledMaxHeight = this.maxSize!.height; - } - } - - equalsCropperPosition(cropper?: CropperPosition): boolean { - return this.cropper == null && cropper == null - || this.cropper != null && cropper != null - && this.cropper.x1.toFixed(3) === cropper.x1.toFixed(3) - && this.cropper.y1.toFixed(3) === cropper.y1.toFixed(3) - && this.cropper.x2.toFixed(3) === cropper.x2.toFixed(3) - && this.cropper.y2.toFixed(3) === cropper.y2.toFixed(3); - } - - equalsTransformTranslate(transform: ImageTransform): boolean { - return (this.transform.translateH ?? 0) === (transform.translateH ?? 0) - && (this.transform.translateV ?? 0) === (transform.translateV ?? 0); - } - - equalsTransform(transform: ImageTransform): boolean { - return this.equalsTransformTranslate(transform) - && (this.transform.scale ?? 1) === (transform.scale ?? 1) - && (this.transform.rotate ?? 0) === (transform.rotate ?? 0) - && (this.transform.flipH ?? false) === (transform.flipH ?? false) - && (this.transform.flipV ?? false) === (transform.flipV ?? false); - } - - aspectRatioIsCorrect(): boolean { - const currentCropAspectRatio = (this.cropper.x2 - this.cropper.x1) / (this.cropper.y2 - this.cropper.y1); - return currentCropAspectRatio === this.options.aspectRatio; - } - - resizeCropperPosition(oldMaxSize: Dimensions): void { - if (!this.cropper) { - return; - } - if (oldMaxSize.width !== this.maxSize!.width || oldMaxSize.height !== this.maxSize!.height) { - this.cropper = { - x1: this.cropper.x1 * this.maxSize!.width / oldMaxSize.width, - x2: this.cropper.x2 * this.maxSize!.width / oldMaxSize.width, - y1: this.cropper.y1 * this.maxSize!.height / oldMaxSize.height, - y2: this.cropper.y2 * this.maxSize!.height / oldMaxSize.height - }; - } - } - - maxSizeCropperPosition(): CropperPosition { - return { - x1: 0, - y1: 0, - x2: this.maxSize!.width, - y2: this.maxSize!.height - }; - } -} diff --git a/projects/ngx-image-cropper/src/lib/component/image-cropper.component.html b/projects/ngx-image-cropper/src/lib/component/image-cropper.component.html index 58767b0..e2a2ae0 100644 --- a/projects/ngx-image-cropper/src/lib/component/image-cropper.component.html +++ b/projects/ngx-image-cropper/src/lib/component/image-cropper.component.html @@ -1,5 +1,5 @@
- + ; @ViewChild('sourceImage', {static: false}) sourceImage!: ElementRef; - @Input() imageChangedEvent?: Event | null; - @Input() imageURL?: string; - @Input() imageBase64?: string; - @Input() imageFile?: File; - @Input() imageAltText?: string; - - @Input() options?: Partial; - @Input() cropperFrameAriaLabel?: string; - @Input() output?: 'blob' | 'base64'; - @Input() format?: OutputFormat; - @Input() autoCrop?: boolean; - @Input() cropper?: CropperPosition; - @Input() transform?: ImageTransform; - @Input() maintainAspectRatio?: boolean; - @Input() aspectRatio?: number; - @Input() resetCropOnAspectRatioChange?: boolean; - @Input() resizeToWidth?: number; - @Input() resizeToHeight?: number; - @Input() cropperMinWidth?: number; - @Input() cropperMinHeight?: number; - @Input() cropperMaxHeight?: number; - @Input() cropperMaxWidth?: number; - @Input() cropperStaticWidth?: number; - @Input() cropperStaticHeight?: number; - @Input() canvasRotation?: number; - @Input() initialStepSize?: number; - @Input() roundCropper?: boolean; - @Input() onlyScaleDown?: boolean; - @Input() imageQuality?: number; - @Input() backgroundColor?: string; - @Input() containWithinAspectRatio?: boolean; - @Input() hideResizeSquares?: boolean; - @Input() allowMoveImage = false; - @Input() checkImageType = true; - @Input() alignImage?: 'left' | 'center'; - - @HostBinding('class.disabled') - @Input() disabled = false; - @HostBinding('class.ngx-ic-hidden') - @Input() hidden = false; + @Input() settingsToUpdate?: PartialImageCropperSettings; @Output() imageCropped = new EventEmitter(); @Output() startCropImage = new EventEmitter(); @Output() imageLoaded = new EventEmitter(); @Output() cropperReady = new EventEmitter(); @Output() loadImageFailed = new EventEmitter(); - @Output() transformChange = new EventEmitter(); - @Output() cropperChange = new EventEmitter(); + @Output() settingsUpdated = new EventEmitter(true); - @HostBinding('style.text-align') - get alignImageStyle() { - return this.state.options.alignImage; - } + @HostBinding('class.disabled') get disbaled() { return this.state.disabled }; + @HostBinding('class.ngx-ic-hidden') get hidden() { return this.state.hidden }; + @HostBinding('style.text-align') get alignImageStyle() { return this.state.alignImage }; constructor( private cropService: CropService, @@ -138,49 +88,77 @@ export class ImageCropperComponent implements OnChanges, OnInit { } ngOnInit(): void { - this.state.stepSize = this.initialStepSize || this.state.stepSize; void this.activatePinchGesture(); } - ngOnChanges(changes: SimpleChanges): void { - const previousCropperPosition = this.state.cropper; - const previousTransform = this.state.transform; - const previousBackgroundColor = this.state.options.backgroundColor; - - this.state.setOptionsFromChanges(changes); - this.onChangesInputImage(changes); - - if (changes['transform'] && this.transform) { - this.state.transform = this.transform; - this.setCssTransform(); - } - - if (!this.state.loadedImage?.transformed.image.complete || !this.state.maxSize) { + ngOnChanges(simpleChanges: SimpleChanges): void { + const previousCropper = { ...this.state.cropper }; + + let changes: PartialImageCropperSettings = simpleChanges["settingsToUpdate"].currentValue; + changes = this.state.getChangesAndUpdateSettings(changes); + + if (changes.imageSource) { + this.onChangesInputImage(this.state); return; - } - - if ((this.containWithinAspectRatio && changes['aspectRatio']) || changes['containWithinAspectRatio'] || changes['canvasRotation']) { + }; + + if (!this.state.loadedImage?.transformed.image.complete) return; + + if ((this.state.containWithinAspectRatio && changes.aspectRatio) || changes.containWithinAspectRatio || changes.canvasRotation) { this.loadImageService .transformLoadedImage(this.state.loadedImage, this.state) .then((res) => this.setLoadedImage(res)) .catch((err) => this.loadImageError(err)); - return; + return; } - if (changes['cropper'] && this.cropper) { - this.state.cropper = checkCropperPosition(this.cropper, this.state, true); + let checkCropperWithinBounds = false; + let resetCropper = false; + let crop = false; + + if ((this.state.maintainAspectRatio && changes.aspectRatio) || changes.maintainAspectRatio) { + cropperSizeBounds.setCropperScaledMinSize(this.state); + cropperSizeBounds.setCropperScaledMaxSize(this.state); + if (this.state.maintainAspectRatio && (this.state.resetCropOnAspectRatioChange || !this.aspectRatioIsCorrect())) { + checkCropperWithinBounds = true; + resetCropper = true; + } + } else { + if (changes.cropperMinWidth || changes.cropperMinHeight) { + cropperSizeBounds.setCropperScaledMinSize(this.state); + checkCropperWithinBounds = true; + } + if (changes.cropperMaxWidth || changes.cropperMaxHeight) { + cropperSizeBounds.setCropperScaledMaxSize(this.state); + checkCropperWithinBounds = true; + } + if (changes.cropperStaticWidth || changes.cropperStaticHeight) { + checkCropperWithinBounds = true; + } } - const cropperChanged = !this.state.equalsCropperPosition(previousCropperPosition); - if (cropperChanged && (!this.cropper || !this.state.equalsCropperPosition(this.cropper))) { - this.cropperChange.emit(this.state.cropper); + + if (changes.cropper) checkCropperWithinBounds = true; + + if (checkCropperWithinBounds) { + cropperPosition.checkWithinCropperSizeBounds(this.state, resetCropper); + cropperPosition.checkWithinMaxSizeBounds(this.state, true); + crop = this.state.equalsCropper(previousCropper); } - if (cropperChanged - || !this.state.equalsTransform(previousTransform) - || this.state.options.backgroundColor !== previousBackgroundColor) { + + if (changes.transform) { + this.setCssTransform(); + crop = true; + } + + if (Object.keys(changes).length > 0 && !changes.hidden) { + this.settingsUpdated.emit(this.state.getDeepCopyOfSettings()); + } + + if (crop || changes.backgroundColor) { this.doAutoCrop(); } - if (changes['hidden'] && this.resizedWhileHidden && !this.hidden) { + if (changes.hidden && this.resizedWhileHidden && !this.state.hidden) { setTimeout(() => { this.onResize(); this.resizedWhileHidden = false; @@ -188,31 +166,18 @@ export class ImageCropperComponent implements OnChanges, OnInit { } } - private onChangesInputImage(changes: SimpleChanges): void { - if (changes['imageChangedEvent'] || changes['imageURL'] || changes['imageBase64'] || changes['imageFile']) { - this.reset(); - } - if (changes['imageChangedEvent'] && this.isValidImageChangedEvent()) { - this.loadImageFile(this.imageChangedEvent.target.files[0]); - } - if (changes['imageURL'] && this.imageURL) { - this.loadImageFromURL(this.imageURL); - } - if (changes['imageBase64'] && this.imageBase64) { - this.loadBase64Image(this.imageBase64); - } - if (changes['imageFile'] && this.imageFile) { - this.loadImageFile(this.imageFile); - } - } - - private isValidImageChangedEvent(): this is { - imageChangedEvent: Event & { - target: { files: FileList }; + private onChangesInputImage(state: ImageCropperState): void { + this.reset(); + if (state.imageSource.imageChangedEvent) { + const target = state.imageSource.imageChangedEvent.target as HTMLInputElement; + if (!!target.files && target.files.length > 0) this.loadImageFile(target.files![0]); + } else if (state.imageSource.imageURL) { + this.loadImageFromURL(state.imageSource.imageURL); + } else if (state.imageSource.imageBase64) { + this.loadBase64Image(state.imageSource.imageBase64); + } else if (state.imageSource.imageFile) { + this.loadImageFile(state.imageSource.imageFile); } - } { - const files = (this.imageChangedEvent as any)?.target?.files; - return files instanceof FileList && files.length > 0; } private reset(): void { @@ -220,7 +185,7 @@ export class ImageCropperComponent implements OnChanges, OnInit { + 'oAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAU' + 'AAarVyFEAAAAASUVORK5CYII='; this.state.loadedImage = undefined; - this.state.maxSize = undefined; + this.state.maxSize = { width: 0, height: 0 }; this.imageVisible = false; } @@ -257,12 +222,12 @@ export class ImageCropperComponent implements OnChanges, OnInit { } private setCssTransform(): void { - const translateUnit = this.state.transform?.translateUnit || '%'; + const translateUnit = this.state.transform.translateUnit; this.safeTransformStyle = this.sanitizer.bypassSecurityTrustStyle( - `translate(${this.state.transform.translateH || 0}${translateUnit}, ${this.state.transform.translateV || 0}${translateUnit})` + - ' scaleX(' + (this.state.transform.scale || 1) * (this.state.transform.flipH ? -1 : 1) + ')' + - ' scaleY(' + (this.state.transform.scale || 1) * (this.state.transform.flipV ? -1 : 1) + ')' + - ' rotate(' + (this.state.transform.rotate || 0) + 'deg)' + `translate(${this.state.transform.translateX}${translateUnit}, ${this.state.transform.translateY}${translateUnit})` + + ' scaleX(' + this.state.transform.scale * (this.state.transform.flipX ? -1 : 1) + ')' + + ' scaleY(' + this.state.transform.scale * (this.state.transform.flipY ? -1 : 1) + ')' + + ' rotate(' + this.state.transform.rotate + 'deg)' ); } @@ -279,16 +244,16 @@ export class ImageCropperComponent implements OnChanges, OnInit { this.loadImageFailed.emit(); } else if (this.sourceImageLoaded()) { this.setMaxSize(); - if (this.cropper && (!this.maintainAspectRatio || this.state.aspectRatioIsCorrect())) { - this.state.cropper = checkCropperPosition(this.cropper, this.state, true); - this.emitCropperPositionChange(this.cropper) - } else { - this.state.cropper = checkCropperPosition(this.state.maxSizeCropperPosition(), this.state, true); - this.cropperChange.emit(this.state.cropper); - } + cropperSizeBounds.setCropperScaledMinSize(this.state); + cropperSizeBounds.setCropperScaledMaxSize(this.state); + cropperPosition.checkWithinCropperSizeBounds(this.state, true); + //TODO(loiddy): add checkCropperWithinMaxSizeBounds when resetCropper and x2=0 is fully implemented. + this.setCssTransform(); this.imageVisible = true; - this.cropperReady.emit({...this.state.maxSize!}); this.doAutoCrop(); + this.cropperReady.emit({...this.state.maxSize}); + console.log('UPDATING SETTINGS IN CHECK IMAGE SIZE RECURISVELY'); + this.settingsUpdated.emit(this.state.getDeepCopyOfSettings()); this.cd.markForCheck(); } else { this.setImageMaxSizeRetries++; @@ -297,7 +262,7 @@ export class ImageCropperComponent implements OnChanges, OnInit { } private sourceImageLoaded(): boolean { - return this.sourceImage?.nativeElement?.offsetWidth > 1; + return this.sourceImage.nativeElement.offsetWidth > 1; } @HostListener('window:resize') @@ -308,9 +273,12 @@ export class ImageCropperComponent implements OnChanges, OnInit { if (this.hidden) { this.resizedWhileHidden = true; } else { - const oldMaxSize = {...this.state.maxSize!}; + const oldMaxSize = {...this.state.maxSize}; this.setMaxSize(); - this.state.resizeCropperPosition(oldMaxSize); + this.resizeCropperPosition(oldMaxSize); + cropperSizeBounds.setCropperScaledMinSize(this.state); + cropperSizeBounds.setCropperScaledMaxSize(this.state); + this.settingsUpdated.emit(this.state.getDeepCopyOfSettings()); this.cd.markForCheck(); } } @@ -340,7 +308,7 @@ export class ImageCropperComponent implements OnChanges, OnInit { private changeKeyboardStepSize(event: KeyboardEvent): void { const key = +event.key; if (key >= 1 && key <= 9) { - this.state.stepSize = key; + this.state.initialStepSize = key; } } @@ -349,9 +317,9 @@ export class ImageCropperComponent implements OnChanges, OnInit { if (!(keyboardWhiteList.includes(event.key))) { return; } - const moveType = event.shiftKey ? MoveTypes.Resize : MoveTypes.Move; + const moveType = event.shiftKey ? MoveTypes.Resize : MoveTypes.Move; //TODO: MoveTypes.Drag? const position = event.altKey ? getInvertedPositionForKey(event.key) : getPositionForKey(event.key); - const moveEvent = getEventForKey(event.key, this.state.stepSize); + const moveEvent = getEventForKey(event.key, this.state.initialStepSize); event.preventDefault(); event.stopPropagation(); this.moveStart = { @@ -360,17 +328,17 @@ export class ImageCropperComponent implements OnChanges, OnInit { position, clientX: 0, clientY: 0, - transform: this.state.transform, - cropper: this.state.cropper + transform: { ...this.state.transform }, + cropper: { ...this.state.cropper } }; this.handleMouseMove(moveEvent); this.handleMouseUp(); } startMove(event: Event | BasicEvent, moveType: MoveTypes, position: Position | null = null): void { - if (this.disabled + if (this.state.disabled || this.moveStart?.active && this.moveStart?.type === MoveTypes.Pinch - || moveType === MoveTypes.Drag && !this.allowMoveImage) { + || moveType === MoveTypes.Drag && !this.state.allowMoveImage) { return; } if ('preventDefault' in event) { @@ -380,10 +348,10 @@ export class ImageCropperComponent implements OnChanges, OnInit { active: true, type: moveType, position, - clientX: getClientX(event), - clientY: getClientY(event), - transform: this.state.transform, - cropper: this.state.cropper + clientX: cropperPosition.getClientX(event), + clientY: cropperPosition.getClientY(event), + transform: { ...this.state.transform }, + cropper: { ...this.state.cropper } }; this.initMouseMove(); } @@ -411,7 +379,7 @@ export class ImageCropperComponent implements OnChanges, OnInit { } startPinch(event: HammerInput) { - if (this.disabled || !this.sourceImageLoaded()) { + if (this.state.disabled || !this.sourceImageLoaded()) { return; } if (event.preventDefault) { @@ -423,7 +391,7 @@ export class ImageCropperComponent implements OnChanges, OnInit { position: 'center', clientX: this.state.cropper.x1 + (this.state.cropper.x2 - this.state.cropper.x1) / 2, clientY: this.state.cropper.y1 + (this.state.cropper.y2 - this.state.cropper.y1) / 2, - cropper: this.state.cropper + cropper: { ...this.state.cropper } }; } @@ -436,26 +404,20 @@ export class ImageCropperComponent implements OnChanges, OnInit { event.preventDefault(); } if (this.moveStart!.type === MoveTypes.Move) { - this.state.cropper = checkCropperWithinMaxSizeBounds( - moveCropper(event, this.moveStart!), - this.state, - true - ); + cropperPosition.move(event, this.moveStart!, this.state.cropper); + cropperPosition.checkWithinMaxSizeBounds(this.state, true); } else if (this.moveStart!.type === MoveTypes.Resize) { - if (!this.cropperStaticWidth && !this.cropperStaticHeight) { - this.state.cropper = checkCropperWithinMaxSizeBounds( - resizeCropper(event, this.moveStart!, this.state), - this.state, - false - ); + if (!this.state.cropperStaticWidth && !this.state.cropperStaticHeight) { + cropperPosition.resize(event, this.moveStart!, this.state); + cropperPosition.checkWithinMaxSizeBounds(this.state, false); } } else if (this.moveStart!.type === MoveTypes.Drag) { - const diffX = getClientX(event) - this.moveStart!.clientX; - const diffY = getClientY(event) - this.moveStart!.clientY; + const diffX = cropperPosition.getClientX(event) - this.moveStart!.clientX; + const diffY = cropperPosition.getClientY(event) - this.moveStart!.clientY; this.state.transform = { ...this.state.transform, - translateH: (this.moveStart!.transform?.translateH || 0) + diffX, - translateV: (this.moveStart!.transform?.translateV || 0) + diffY + translateX: (this.moveStart!.transform?.translateX || 0) + diffX, + translateY: (this.moveStart!.transform?.translateY || 0) + diffY }; this.setCssTransform(); } @@ -468,12 +430,9 @@ export class ImageCropperComponent implements OnChanges, OnInit { event.preventDefault(); } if (this.moveStart!.type === MoveTypes.Pinch) { - if (!this.cropperStaticWidth && !this.cropperStaticHeight) { - this.state.cropper = checkCropperWithinMaxSizeBounds( - resizeCropper(event, this.moveStart!, this.state), - this.state, - false - ); + if (!this.state.cropperStaticWidth && !this.state.cropperStaticHeight) { + cropperPosition.resize(event, this.moveStart!, this.state); + cropperPosition.checkWithinMaxSizeBounds(this.state, false); } } this.cd.markForCheck(); @@ -483,43 +442,48 @@ export class ImageCropperComponent implements OnChanges, OnInit { private setMaxSize(): void { if (this.sourceImage) { const sourceImageStyle = getComputedStyle(this.sourceImage.nativeElement); - this.state.setMaxSize(parseFloat(sourceImageStyle.width), parseFloat(sourceImageStyle.height)); - this.marginLeft = this.sanitizer.bypassSecurityTrustStyle('calc(50% - ' + this.state.maxSize!.width / 2 + 'px)'); + this.state.maxSize.width = parseFloat(sourceImageStyle.width); + this.state.maxSize.height = parseFloat(sourceImageStyle.height); + this.marginLeft = this.sanitizer.bypassSecurityTrustStyle('calc(50% - ' + this.state.maxSize.width / 2 + 'px)'); } } private handleMouseUp(): void { if (this.moveStart?.active) { - if (!this.state.equalsCropperPosition(this.moveStart.cropper) || this.moveStart.transform && !this.state.equalsTransform(this.moveStart.transform)) { - if (this.moveStart.type === MoveTypes.Drag) { - this.transformChange.emit(this.state.transform); - } else { - this.cropperChange.emit(this.state.cropper); - } + this.moveStart.active = false; + if (this.state.equalsCropper(this.moveStart.cropper) || this.state.equalsTransformTranslate(this.moveStart.transform!)) { + this.settingsUpdated.emit(this.state.getDeepCopyOfSettings()); this.doAutoCrop(); } - this.moveStart = undefined; } } pinchStop(): void { if (this.moveStart?.active) { - this.emitCropperPositionChange(this.moveStart.cropper) this.moveStart!.active = false; - if (!this.state.equalsCropperPosition(this.moveStart.cropper)) { - this.doAutoCrop(); - } + if (this.state.equalsCropper(this.moveStart.cropper) || this.state.equalsTransformTranslate(this.moveStart.transform!)) { + this.settingsUpdated.emit(this.state.getDeepCopyOfSettings()); + this.doAutoCrop() + }; } } - private emitCropperPositionChange(previousPosition: CropperPosition): void { - if (!this.state.equalsCropperPosition(previousPosition)) { - this.cropperChange.emit(this.state.cropper); + private aspectRatioIsCorrect(): boolean { + const currentCropAspectRatio = (this.state.cropper.x2 - this.state.cropper.x1) / (this.state.cropper.y2 - this.state.cropper.y1); + return currentCropAspectRatio === this.state.aspectRatio; + } + + private resizeCropperPosition(oldMaxSize: Dimensions): void { + if (oldMaxSize.width !== this.state.maxSize.width || oldMaxSize.height !== this.state.maxSize.height) { + this.state.cropper.x1 = this.state.cropper.x1 * this.state.maxSize.width / oldMaxSize.width; + this.state.cropper.x2 = this.state.cropper.x2 * this.state.maxSize.width / oldMaxSize.width; + this.state.cropper.y1 = this.state.cropper.y1 * this.state.maxSize.height / oldMaxSize.height; + this.state.cropper.y2 = this.state.cropper.y2 * this.state.maxSize.height / oldMaxSize.height; } } private doAutoCrop(): void { - if (this.state.options.autoCrop) { + if (this.state.autoCrop) { void this.crop(); } } @@ -527,7 +491,7 @@ export class ImageCropperComponent implements OnChanges, OnInit { crop(): ImageCroppedEvent | null; crop(output: 'base64'): ImageCroppedEvent | null; crop(output: 'blob'): Promise | null; - crop(output: OutputType = this.state.options.output): Promise | ImageCroppedEvent | null { + crop(output: OutputType = this.state.output): Promise | ImageCroppedEvent | null { if (this.state.loadedImage?.transformed?.image != null) { this.startCropImage.emit(); if (output === 'blob') { diff --git a/projects/ngx-image-cropper/src/lib/interfaces/cropper-options.interface.ts b/projects/ngx-image-cropper/src/lib/interfaces/cropper-options.interface.ts deleted file mode 100644 index 4a788ef..0000000 --- a/projects/ngx-image-cropper/src/lib/interfaces/cropper-options.interface.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface CropperOptions { - format: OutputFormat; - output: OutputType; - autoCrop: boolean; - maintainAspectRatio: boolean; - resetCropOnAspectRatioChange: boolean; - aspectRatio: number; - resizeToWidth: number; - resizeToHeight: number; - cropperMinWidth: number; - cropperMinHeight: number; - cropperMaxHeight: number; - cropperMaxWidth: number; - cropperStaticWidth: number; - cropperStaticHeight: number; - canvasRotation: number; - roundCropper: boolean; - onlyScaleDown: boolean; - imageQuality: number; - backgroundColor: string | null; - containWithinAspectRatio: boolean; - hideResizeSquares: boolean; - alignImage: 'left' | 'center'; - cropperFrameAriaLabel: string | undefined; - checkImageType: boolean; -} - -export type OutputFormat = 'png' | 'jpeg' | 'bmp' | 'webp' | 'ico'; -export type OutputType = 'base64' | 'blob'; diff --git a/projects/ngx-image-cropper/src/lib/interfaces/image-cropper-settings.ts b/projects/ngx-image-cropper/src/lib/interfaces/image-cropper-settings.ts new file mode 100644 index 0000000..e6945e3 --- /dev/null +++ b/projects/ngx-image-cropper/src/lib/interfaces/image-cropper-settings.ts @@ -0,0 +1,49 @@ +import { ImageTransform, CropperPosition, ImageSource } from "./../interfaces"; + +export type AlignImage = "left" | "center"; +export type OutputFormat = "png" | "jpeg" | "bmp" | "webp" | "ico"; +export type OutputType = "base64" | "blob"; + +export interface ImageCropperSettings { + alignImage: AlignImage; + allowMoveImage: boolean; + aspectRatio: number; + autoCrop: boolean; + backgroundColor: string | null; + canvasRotation: number; + checkImageType: boolean; + containWithinAspectRatio: boolean; + cropper: CropperPosition; + cropperFrameAriaLabel: string | null; + cropperMaxHeight: number; + cropperMaxWidth: number; + cropperMinHeight: number; + cropperMinWidth: number; + cropperStaticHeight: number; + cropperStaticWidth: number; + disabled: boolean; + format: OutputFormat; + hidden: boolean; + hideResizeSquares: boolean; + imageAltText: string | null; + imageQuality: number; + imageSource: ImageSource; + initialStepSize: number; + maintainAspectRatio: boolean; + onlyScaleDown: boolean; + output: OutputType; + resetCropOnAspectRatioChange: boolean; + resizeToHeight: number; + resizeToWidth: number; + roundCropper: boolean; + transform: ImageTransform; +} + +interface ImageCropperSettingsWithOptionalPropertyKeys + extends Omit { + cropper: Partial; + transform: Partial; +} + +export type PartialImageCropperSettings = + Partial; diff --git a/projects/ngx-image-cropper/src/lib/interfaces/image-source-interface.ts b/projects/ngx-image-cropper/src/lib/interfaces/image-source-interface.ts new file mode 100644 index 0000000..98f5a28 --- /dev/null +++ b/projects/ngx-image-cropper/src/lib/interfaces/image-source-interface.ts @@ -0,0 +1,33 @@ +interface NoImageSource { + imageBase64: never; + imageChangedEvent: never; + imageFile: never; + imageURL: never; +} +interface ImageBase64 { + imageBase64?: string; + imageChangedEvent?: never; + imageFile?: never; + imageURL?: never; +} +interface ImageChangedEvent { + imageBase64?: never; + imageChangedEvent?: Event; + imageFile?: never; + imageURL?: never; +} +interface ImageFile { + imageBase64?: never; + imageChangedEvent?: never; + imageFile?: File; + imageURL?: never; +} +interface ImageUrl { + imageBase64?: never; + imageChangedEvent?: never; + imageFile?: never; + imageURL?: string; +} + +export type ImageSource = ImageBase64 | ImageChangedEvent | ImageFile | ImageUrl | NoImageSource; + diff --git a/projects/ngx-image-cropper/src/lib/interfaces/image-transform.interface.ts b/projects/ngx-image-cropper/src/lib/interfaces/image-transform.interface.ts index 23a10a6..d077e3e 100644 --- a/projects/ngx-image-cropper/src/lib/interfaces/image-transform.interface.ts +++ b/projects/ngx-image-cropper/src/lib/interfaces/image-transform.interface.ts @@ -1,9 +1,9 @@ export interface ImageTransform { - scale?: number; - rotate?: number; - flipH?: boolean; - flipV?: boolean; - translateH?: number; - translateV?: number; - translateUnit?: '%' | 'px'; + flipX: boolean; + flipY: boolean; + rotate: number; + scale: number; + translateUnit: '%' | 'px'; + translateX: number; + translateY: number; } diff --git a/projects/ngx-image-cropper/src/lib/interfaces/index.ts b/projects/ngx-image-cropper/src/lib/interfaces/index.ts index 183808d..cf34f9f 100644 --- a/projects/ngx-image-cropper/src/lib/interfaces/index.ts +++ b/projects/ngx-image-cropper/src/lib/interfaces/index.ts @@ -1,7 +1,10 @@ -export { CropperOptions, OutputFormat } from './cropper-options.interface'; -export { CropperPosition } from './cropper-position.interface'; -export { Dimensions } from './dimensions.interface'; -export { ImageCroppedEvent } from './image-cropped-event.interface'; -export { MoveStart } from './move-start.interface'; -export { ImageTransform } from './image-transform.interface'; -export { LoadedImage } from './loaded-image.interface'; +export * from './basic-event.interface'; +export * from './cropper-position.interface'; +export * from './dimensions.interface'; +export * from './exif-transform.interface'; +export * from './image-cropped-event.interface'; +export * from './image-cropper-settings'; +export * from './image-source-interface'; +export * from './image-transform.interface'; +export * from './loaded-image.interface'; +export * from './move-start.interface'; \ No newline at end of file diff --git a/projects/ngx-image-cropper/src/lib/services/crop.service.ts b/projects/ngx-image-cropper/src/lib/services/crop.service.ts index d13e2f6..5cec420 100644 --- a/projects/ngx-image-cropper/src/lib/services/crop.service.ts +++ b/projects/ngx-image-cropper/src/lib/services/crop.service.ts @@ -1,17 +1,17 @@ import { Injectable } from '@angular/core'; -import { CropperOptions, CropperPosition, ImageCroppedEvent } from '../interfaces'; -import { CropperState } from '../component/cropper.state'; +import { CropperPosition, ImageCroppedEvent } from '../interfaces'; import { resizeCanvas } from '../utils/resize.utils'; import { percentage } from '../utils/percentage.utils'; -import { OutputType } from '../interfaces/cropper-options.interface'; +import { OutputType } from '../interfaces/image-cropper-settings'; +import { ImageCropperState } from '../state/image-cropper-state'; @Injectable({providedIn: 'root'}) export class CropService { - crop(cropperState: CropperState, output: 'blob'): Promise | null; - crop(cropperState: CropperState, output: 'base64'): ImageCroppedEvent | null; - crop(cropperState: CropperState, output: OutputType): Promise | ImageCroppedEvent | null { - const imagePosition = this.getImagePosition(cropperState); + crop(state: ImageCropperState, output: 'blob'): Promise | null; + crop(state: ImageCropperState, output: 'base64'): ImageCroppedEvent | null; + crop(state: ImageCropperState, output: OutputType): Promise | ImageCroppedEvent | null { + const imagePosition = this.getImagePosition(state); const width = imagePosition.x2 - imagePosition.x1; const height = imagePosition.y2 - imagePosition.y1; const cropCanvas = document.createElement('canvas') as HTMLCanvasElement; @@ -22,19 +22,19 @@ export class CropService { if (!ctx) { return null; } - if (cropperState.options.backgroundColor != null) { - ctx.fillStyle = cropperState.options.backgroundColor; + if (state.backgroundColor != null) { + ctx.fillStyle = state.backgroundColor; ctx.fillRect(0, 0, width, height); } - const scaleX = (cropperState.transform.scale || 1) * (cropperState.transform.flipH ? -1 : 1); - const scaleY = (cropperState.transform.scale || 1) * (cropperState.transform.flipV ? -1 : 1); - const {translateH, translateV} = this.getCanvasTranslate(cropperState); + const scaleX = state.transform.scale * (state.transform.flipX ? -1 : 1); + const scaleY = state.transform.scale * (state.transform.flipY ? -1 : 1); + const {translateX, translateY} = this.getCanvasTranslate(state); - const transformedImage = cropperState.loadedImage!.transformed; - ctx.setTransform(scaleX, 0, 0, scaleY, transformedImage.size.width / 2 + translateH, transformedImage.size.height / 2 + translateV); + const transformedImage = state.loadedImage!.transformed; + ctx.setTransform(scaleX, 0, 0, scaleY, transformedImage.size.width / 2 + translateX, transformedImage.size.height / 2 + translateY); ctx.translate(-imagePosition.x1 / scaleX, -imagePosition.y1 / scaleY); - ctx.rotate((cropperState.transform.rotate || 0) * Math.PI / 180); + ctx.rotate(state.transform.rotate * Math.PI / 180); ctx.drawImage( transformedImage.image, @@ -45,125 +45,125 @@ export class CropService { const result: ImageCroppedEvent = { width, height, imagePosition, - cropperPosition: {...cropperState.cropper} + cropperPosition: {...state.cropper} }; - if (cropperState.options.containWithinAspectRatio) { - result.offsetImagePosition = this.getOffsetImagePosition(cropperState); + if (state.containWithinAspectRatio) { + result.offsetImagePosition = this.getOffsetImagePosition(state); } - const resizeRatio = this.getResizeRatio(width, height, cropperState.options); + const resizeRatio = this.getResizeRatio(width, height, state); if (resizeRatio !== 1) { result.width = Math.round(width * resizeRatio); - result.height = cropperState.options.maintainAspectRatio - ? Math.round(result.width / cropperState.options.aspectRatio) + result.height = state.maintainAspectRatio + ? Math.round(result.width / state.aspectRatio) : Math.round(height * resizeRatio); resizeCanvas(cropCanvas, result.width, result.height); } if (output === 'blob') { - return this.cropToBlob(result, cropCanvas, cropperState); + return this.cropToBlob(result, cropCanvas, state); } else { - result.base64 = cropCanvas.toDataURL('image/' + cropperState.options.format, this.getQuality(cropperState.options)); + result.base64 = cropCanvas.toDataURL('image/' + state.format, this.getQuality(state)); return result; } } - private async cropToBlob(output: ImageCroppedEvent, cropCanvas: HTMLCanvasElement, cropperState: CropperState): Promise { - output.blob = await new Promise(resolve => cropCanvas.toBlob(resolve, 'image/' + cropperState.options.format, this.getQuality(cropperState.options))); + private async cropToBlob(output: ImageCroppedEvent, cropCanvas: HTMLCanvasElement, state: ImageCropperState): Promise { + output.blob = await new Promise(resolve => cropCanvas.toBlob(resolve, 'image/' + state.format, this.getQuality(state))); if (output.blob) { output.objectUrl = URL.createObjectURL(output.blob); } return output; } - private getCanvasTranslate(cropperState: CropperState): { translateH: number, translateV: number } { - if (cropperState.transform.translateUnit === 'px') { - const ratio = this.getRatio(cropperState); + private getCanvasTranslate(state: ImageCropperState): { translateX: number, translateY: number } { + if (state.transform.translateUnit === 'px') { + const ratio = this.getRatio(state); return { - translateH: (cropperState.transform.translateH || 0) * ratio, - translateV: (cropperState.transform.translateV || 0) * ratio + translateX: state.transform.translateX * ratio, + translateY: state.transform.translateY * ratio }; } else { return { - translateH: cropperState.transform.translateH ? percentage(cropperState.transform.translateH, cropperState.loadedImage!.transformed.size.width) : 0, - translateV: cropperState.transform.translateV ? percentage(cropperState.transform.translateV, cropperState.loadedImage!.transformed.size.height) : 0 + translateX: state.transform.translateX ? percentage(state.transform.translateX, state.loadedImage!.transformed.size.width) : 0, + translateY: state.transform.translateY ? percentage(state.transform.translateY, state.loadedImage!.transformed.size.height) : 0 }; } } - private getRatio(cropperState: CropperState): number { - return cropperState.loadedImage!.transformed.size.width / cropperState.maxSize!.width; + private getRatio(state: ImageCropperState): number { + return state.loadedImage!.transformed.size.width / state.maxSize.width; } - private getImagePosition(cropperState: CropperState): CropperPosition { - const ratio = this.getRatio(cropperState); + private getImagePosition(state: ImageCropperState): CropperPosition { + const ratio = this.getRatio(state); const out: CropperPosition = { - x1: Math.round(cropperState.cropper.x1 * ratio), - y1: Math.round(cropperState.cropper.y1 * ratio), - x2: Math.round(cropperState.cropper.x2 * ratio), - y2: Math.round(cropperState.cropper.y2 * ratio) + x1: Math.round(state.cropper.x1 * ratio), + y1: Math.round(state.cropper.y1 * ratio), + x2: Math.round(state.cropper.x2 * ratio), + y2: Math.round(state.cropper.y2 * ratio) }; - if (!cropperState.options.containWithinAspectRatio) { + if (!state.containWithinAspectRatio) { out.x1 = Math.max(out.x1, 0); out.y1 = Math.max(out.y1, 0); - out.x2 = Math.min(out.x2, cropperState.loadedImage!.transformed.size.width); - out.y2 = Math.min(out.y2, cropperState.loadedImage!.transformed.size.height); + out.x2 = Math.min(out.x2, state.loadedImage!.transformed.size.width); + out.y2 = Math.min(out.y2, state.loadedImage!.transformed.size.height); } return out; } - private getOffsetImagePosition(cropperState: CropperState): CropperPosition { - const canvasRotation = cropperState.options.canvasRotation + cropperState.loadedImage!.exifTransform.rotate; - const ratio = this.getRatio(cropperState); + private getOffsetImagePosition(state: ImageCropperState): CropperPosition { + const canvasRotation = state.canvasRotation + state.loadedImage!.exifTransform.rotate; + const ratio = this.getRatio(state); let offsetX: number; let offsetY: number; if (canvasRotation % 2) { - offsetX = (cropperState.loadedImage!.transformed.size.width - cropperState.loadedImage!.original.size.height) / 2; - offsetY = (cropperState.loadedImage!.transformed.size.height - cropperState.loadedImage!.original.size.width) / 2; + offsetX = (state.loadedImage!.transformed.size.width - state.loadedImage!.original.size.height) / 2; + offsetY = (state.loadedImage!.transformed.size.height - state.loadedImage!.original.size.width) / 2; } else { - offsetX = (cropperState.loadedImage!.transformed.size.width - cropperState.loadedImage!.original.size.width) / 2; - offsetY = (cropperState.loadedImage!.transformed.size.height - cropperState.loadedImage!.original.size.height) / 2; + offsetX = (state.loadedImage!.transformed.size.width - state.loadedImage!.original.size.width) / 2; + offsetY = (state.loadedImage!.transformed.size.height - state.loadedImage!.original.size.height) / 2; } const out: CropperPosition = { - x1: Math.round(cropperState.cropper.x1 * ratio) - offsetX, - y1: Math.round(cropperState.cropper.y1 * ratio) - offsetY, - x2: Math.round(cropperState.cropper.x2 * ratio) - offsetX, - y2: Math.round(cropperState.cropper.y2 * ratio) - offsetY + x1: Math.round(state.cropper.x1 * ratio) - offsetX, + y1: Math.round(state.cropper.y1 * ratio) - offsetY, + x2: Math.round(state.cropper.x2 * ratio) - offsetX, + y2: Math.round(state.cropper.y2 * ratio) - offsetY }; - if (!cropperState.options.containWithinAspectRatio) { + if (!state.containWithinAspectRatio) { out.x1 = Math.max(out.x1, 0); out.y1 = Math.max(out.y1, 0); - out.x2 = Math.min(out.x2, cropperState.loadedImage!.transformed.size.width); - out.y2 = Math.min(out.y2, cropperState.loadedImage!.transformed.size.height); + out.x2 = Math.min(out.x2, state.loadedImage!.transformed.size.width); + out.y2 = Math.min(out.y2, state.loadedImage!.transformed.size.height); } return out; } - getResizeRatio(width: number, height: number, options: CropperOptions): number { - const ratioWidth = options.resizeToWidth / width; - const ratioHeight = options.resizeToHeight / height; + getResizeRatio(width: number, height: number, state: ImageCropperState): number { + const ratioWidth = state.resizeToWidth / width; + const ratioHeight = state.resizeToHeight / height; const ratios = new Array(); - if (options.resizeToWidth > 0) { + if (state.resizeToWidth > 0) { ratios.push(ratioWidth); } - if (options.resizeToHeight > 0) { + if (state.resizeToHeight > 0) { ratios.push(ratioHeight); } const result = ratios.length === 0 ? 1 : Math.min(...ratios); - if (result > 1 && !options.onlyScaleDown) { + if (result > 1 && !state.onlyScaleDown) { return result; } return Math.min(result, 1); } - getQuality(options: CropperOptions): number { - return Math.min(1, Math.max(0, options.imageQuality / 100)); + getQuality(state: ImageCropperState): number { + return Math.min(1, Math.max(0, state.imageQuality / 100)); } } diff --git a/projects/ngx-image-cropper/src/lib/services/load-image.service.ts b/projects/ngx-image-cropper/src/lib/services/load-image.service.ts index 3346786..3f9947b 100644 --- a/projects/ngx-image-cropper/src/lib/services/load-image.service.ts +++ b/projects/ngx-image-cropper/src/lib/services/load-image.service.ts @@ -1,8 +1,7 @@ import { Injectable } from '@angular/core'; -import { Dimensions, LoadedImage } from '../interfaces'; -import { CropperState } from '../component/cropper.state'; -import { ExifTransform } from '../interfaces/exif-transform.interface'; +import { Dimensions, LoadedImage, ExifTransform } from '../interfaces'; import { getTransformationsFromExifData, supportsAutomaticRotation } from '../utils/exif.utils'; +import { ImageCropperState } from '../state/image-cropper-state'; interface LoadImageArrayBuffer { originalImage: HTMLImageElement; @@ -16,35 +15,35 @@ export class LoadImageService { private autoRotateSupported: Promise = supportsAutomaticRotation(); - async loadImageFile(file: File, cropperSettings: CropperState): Promise { + async loadImageFile(file: File, state: ImageCropperState): Promise { const arrayBuffer = await file.arrayBuffer(); - if (cropperSettings.options.checkImageType) { - return await this.checkImageTypeAndLoadImageFromArrayBuffer(arrayBuffer, file.type, cropperSettings); + if (state.checkImageType) { + return await this.checkImageTypeAndLoadImageFromArrayBuffer(arrayBuffer, file.type, state); } - return await this.loadImageFromArrayBuffer(arrayBuffer, cropperSettings); + return await this.loadImageFromArrayBuffer(arrayBuffer, state); } - private checkImageTypeAndLoadImageFromArrayBuffer(arrayBuffer: ArrayBufferLike, imageType: string, cropperSettings: CropperState): Promise { + private checkImageTypeAndLoadImageFromArrayBuffer(arrayBuffer: ArrayBufferLike, imageType: string, state: ImageCropperState): Promise { if (!this.isValidImageType(imageType)) { return Promise.reject(new Error('Invalid image type')); } - return this.loadImageFromArrayBuffer(arrayBuffer, cropperSettings, imageType); + return this.loadImageFromArrayBuffer(arrayBuffer, state, imageType); } private isValidImageType(type: string): boolean { return /image\/(png|jpg|jpeg|heic|bmp|gif|tiff|svg|webp|x-icon|vnd.microsoft.icon)/.test(type); } - async loadImageFromURL(url: string, cropperSettings: CropperState): Promise { + async loadImageFromURL(url: string, state: ImageCropperState): Promise { const res = await fetch(url); const blob = await res.blob(); const buffer = await blob.arrayBuffer(); - return await this.loadImageFromArrayBuffer(buffer, cropperSettings, blob.type); + return await this.loadImageFromArrayBuffer(buffer, state, blob.type); } - loadBase64Image(imageBase64: string, cropperSettings: CropperState): Promise { + loadBase64Image(imageBase64: string, state: ImageCropperState): Promise { const arrayBuffer = this.base64ToArrayBuffer(imageBase64); - return this.loadImageFromArrayBuffer(arrayBuffer, cropperSettings); + return this.loadImageFromArrayBuffer(arrayBuffer, state); } private base64ToArrayBuffer(imageBase64: string): ArrayBufferLike { @@ -58,7 +57,7 @@ export class LoadImageService { return bytes.buffer; } - private async loadImageFromArrayBuffer(arrayBuffer: ArrayBufferLike, cropperState: CropperState, imageType?: string): Promise { + private async loadImageFromArrayBuffer(arrayBuffer: ArrayBufferLike, state: ImageCropperState, imageType?: string): Promise { const res = await new Promise(async (resolve, reject) => { try { const blob = new Blob([arrayBuffer], imageType ? {type: imageType} : undefined); @@ -78,7 +77,7 @@ export class LoadImageService { reject(e); } }); - return await this.transformImageFromArrayBuffer(res, cropperState, res.originalImageSize != null); + return await this.transformImageFromArrayBuffer(res, state, res.originalImageSize != null); } private async getSvgImageSize(blob: Blob): Promise<{ width: number; height: number; } | null> { @@ -105,7 +104,7 @@ export class LoadImageService { throw Error('Failed to load SVG image. SVG must have width + height or viewBox definition.'); } - private async transformImageFromArrayBuffer(res: LoadImageArrayBuffer, cropperSettings: CropperState, forceTransform = false): Promise { + private async transformImageFromArrayBuffer(res: LoadImageArrayBuffer, state: ImageCropperState, forceTransform = false): Promise { const autoRotate = await this.autoRotateSupported; const exifTransform = getTransformationsFromExifData(autoRotate ? -1 : res.originalArrayBuffer); if (!res.originalImage || !res.originalImage.complete) { @@ -122,13 +121,13 @@ export class LoadImageService { }, exifTransform }; - return this.transformLoadedImage(loadedImage, cropperSettings, forceTransform); + return this.transformLoadedImage(loadedImage, state, forceTransform); } - async transformLoadedImage(loadedImage: Partial, cropperState: CropperState, forceTransform = false): Promise { - const canvasRotation = cropperState.options.canvasRotation + loadedImage.exifTransform!.rotate; + async transformLoadedImage(loadedImage: Partial, state: ImageCropperState, forceTransform = false): Promise { + const canvasRotation = state.canvasRotation + loadedImage.exifTransform!.rotate; const originalSize = loadedImage.original!.size; - if (!forceTransform && canvasRotation === 0 && !loadedImage.exifTransform!.flip && !cropperState.options.containWithinAspectRatio) { + if (!forceTransform && canvasRotation === 0 && !loadedImage.exifTransform!.flip && !state.containWithinAspectRatio) { return { original: { objectUrl: loadedImage.original!.objectUrl, @@ -144,7 +143,7 @@ export class LoadImageService { }; } - const transformedSize = this.getTransformedSize(originalSize, loadedImage.exifTransform!, cropperState); + const transformedSize = this.getTransformedSize(originalSize, loadedImage.exifTransform!, state); const canvas = document.createElement('canvas'); canvas.width = transformedSize.width; canvas.height = transformedSize.height; @@ -163,7 +162,7 @@ export class LoadImageService { -originalSize.width / 2, -originalSize.height / 2 ); - const blob = await new Promise(resolve => canvas.toBlob(resolve, cropperState.options.format)); + const blob = await new Promise(resolve => canvas.toBlob(resolve, state.format)); if (!blob) { throw new Error('Failed to get Blob for transformed image.'); } @@ -199,20 +198,20 @@ export class LoadImageService { private getTransformedSize( originalSize: { width: number, height: number }, exifTransform: ExifTransform, - cropperState: CropperState + state: ImageCropperState ): Dimensions { - const canvasRotation = cropperState.options.canvasRotation + exifTransform.rotate; - if (cropperState.options.containWithinAspectRatio) { + const canvasRotation = state.canvasRotation + exifTransform.rotate; + if (state.containWithinAspectRatio) { if (canvasRotation % 2) { - const minWidthToContain = originalSize.width * cropperState.options.aspectRatio; - const minHeightToContain = originalSize.height / cropperState.options.aspectRatio; + const minWidthToContain = originalSize.width * state.aspectRatio; + const minHeightToContain = originalSize.height / state.aspectRatio; return { width: Math.max(originalSize.height, minWidthToContain), height: Math.max(originalSize.width, minHeightToContain) }; } else { - const minWidthToContain = originalSize.height * cropperState.options.aspectRatio; - const minHeightToContain = originalSize.width / cropperState.options.aspectRatio; + const minWidthToContain = originalSize.height * state.aspectRatio; + const minHeightToContain = originalSize.width / state.aspectRatio; return { width: Math.max(originalSize.width, minWidthToContain), height: Math.max(originalSize.height, minHeightToContain) diff --git a/projects/ngx-image-cropper/src/lib/state/image-cropper-state.ts b/projects/ngx-image-cropper/src/lib/state/image-cropper-state.ts new file mode 100644 index 0000000..4e9b83a --- /dev/null +++ b/projects/ngx-image-cropper/src/lib/state/image-cropper-state.ts @@ -0,0 +1,134 @@ +import { + CropperPosition, + Dimensions, + ImageTransform, + LoadedImage, +} from "../interfaces"; +import { + ImageCropperSettings, + PartialImageCropperSettings, +} from "../interfaces/image-cropper-settings"; +import { imageCropperSettingsInitValue } from "./init-values"; + +export class ImageCropperState implements ImageCropperSettings { + // Image Cropper Settings + alignImage = imageCropperSettingsInitValue.alignImage; + allowMoveImage = imageCropperSettingsInitValue.allowMoveImage; + aspectRatio = imageCropperSettingsInitValue.aspectRatio; + autoCrop = imageCropperSettingsInitValue.autoCrop; + backgroundColor = imageCropperSettingsInitValue.backgroundColor; + canvasRotation = imageCropperSettingsInitValue.canvasRotation; + checkImageType = imageCropperSettingsInitValue.checkImageType; + containWithinAspectRatio = imageCropperSettingsInitValue.containWithinAspectRatio; + cropper = imageCropperSettingsInitValue.cropper; + cropperFrameAriaLabel = imageCropperSettingsInitValue.cropperFrameAriaLabel; + cropperMaxHeight = imageCropperSettingsInitValue.cropperMaxHeight; + cropperMaxWidth = imageCropperSettingsInitValue.cropperMaxWidth; + cropperMinHeight = imageCropperSettingsInitValue.cropperMinHeight; + cropperMinWidth = imageCropperSettingsInitValue.cropperMinWidth; + cropperStaticHeight = imageCropperSettingsInitValue.cropperStaticHeight; + cropperStaticWidth = imageCropperSettingsInitValue.cropperStaticWidth; + disabled = imageCropperSettingsInitValue.disabled; + format = imageCropperSettingsInitValue.format; + hidden = imageCropperSettingsInitValue.hidden; + hideResizeSquares = imageCropperSettingsInitValue.hideResizeSquares; + imageAltText = imageCropperSettingsInitValue.imageAltText; + imageQuality = imageCropperSettingsInitValue.imageQuality; + imageSource = imageCropperSettingsInitValue.imageSource; + initialStepSize = imageCropperSettingsInitValue.initialStepSize; + maintainAspectRatio = imageCropperSettingsInitValue.maintainAspectRatio; + onlyScaleDown = imageCropperSettingsInitValue.onlyScaleDown; + output = imageCropperSettingsInitValue.output; + resetCropOnAspectRatioChange = imageCropperSettingsInitValue.resetCropOnAspectRatioChange; + resizeToHeight = imageCropperSettingsInitValue.resizeToHeight; + resizeToWidth = imageCropperSettingsInitValue.resizeToWidth; + roundCropper = imageCropperSettingsInitValue.roundCropper; + transform = imageCropperSettingsInitValue.transform; + // Internal + cropperScaledMinHeight = 20; + cropperScaledMinWidth = 20; + cropperScaledMaxHeight = 20; + cropperScaledMaxWidth = 20; + loadedImage?: LoadedImage; + maxSize: Dimensions = { width: 0, height: 0 }; + + getChangesAndUpdateSettings( input: PartialImageCropperSettings ): PartialImageCropperSettings { + console.log("settings before", this.getDeepCopyOfSettings()); + console.log("input", { ...input }); + const changes: PartialImageCropperSettings = {}; + for (let k in input) { + if (!(k in imageCropperSettingsInitValue)) continue; + if (!!(this as any)[k] && (this as any)[k].constructor === Object) { + (changes as any)[k] = {}; + for (let j in (input as any)[k]) { + if ((this as any)[k][j] !== (input as any)[k][j]) { + console.log("old", (this as any)[k][j], " new", (input as any)[k][j]); + if (k === 'imageSource') this.imageSource = {}; + (this as any)[k][j] = (input as any)[k][j]; + (changes as any)[k][j] = true; + } + } + if (k !== 'imageSource' && !Object.keys((changes as any)[k]).length) delete (changes as any)[k]; + } else { + if ((this as any)[k] !== (input as any)[k]) { + console.log("old", (this as any)[k], " new", (input as any)[k]); + (this as any)[k] = (input as any)[k]; + (changes as any)[k] = true; + } + } + } + if (changes.imageSource) this.imageSource = { ...input.imageSource }; + console.log("changes", { ...changes }); + console.log("settings after", this.getDeepCopyOfSettings()); + this.validateOptions(); + return changes; + } + + private validateOptions(): void { + if (this.maintainAspectRatio && !this.aspectRatio) { + throw new Error( + "`aspectRatio` should > 0 when `maintainAspectRatio` is enabled" + ); + } + } + + getDeepCopyOfSettings(): ImageCropperSettings { + let settings = {}; + for (let k in imageCropperSettingsInitValue) { + if (k in (this as any)) { + if (!!(this as any)[k] && (this as any)[k].constructor === Object) { + (settings as any)[k] = { ...(this as any)[k] }; + } else { + (settings as any)[k] = (this as any)[k]; + } + } + } + return settings as ImageCropperSettings; + } + + equalsCropper(cropper: CropperPosition): boolean { //TODO + return ( + this.cropper.x1.toFixed(3) === cropper.x1.toFixed(3) && + this.cropper.y1.toFixed(3) === cropper.y1.toFixed(3) && + this.cropper.x2.toFixed(3) === cropper.x2.toFixed(3) && + this.cropper.y2.toFixed(3) === cropper.y2.toFixed(3) + ); + } + + equalsTransformTranslate(transform: ImageTransform): boolean { + return ( + this.transform.translateX === transform.translateX && + this.transform.translateY === transform.translateY + ); + } + + equalsTransform(transform: ImageTransform): boolean { + return ( + this.equalsTransformTranslate(transform) && + this.transform.scale === transform.scale && + this.transform.rotate === transform.rotate && + this.transform.flipX === transform.flipX && + this.transform.flipY === transform.flipY + ); + } +} diff --git a/projects/ngx-image-cropper/src/lib/state/init-values/cropper-init-value.ts b/projects/ngx-image-cropper/src/lib/state/init-values/cropper-init-value.ts new file mode 100644 index 0000000..c4e9325 --- /dev/null +++ b/projects/ngx-image-cropper/src/lib/state/init-values/cropper-init-value.ts @@ -0,0 +1,12 @@ +import { CropperPosition } from './../../interfaces'; + +export function getCropperInitValue() { + return { ...cropperInitValue }; +} + +const cropperInitValue: CropperPosition = { + x1: 0, + y1: 0, + x2: 0, + y2: 0, +} \ No newline at end of file diff --git a/projects/ngx-image-cropper/src/lib/state/init-values/image-cropper-settings-init-value.ts b/projects/ngx-image-cropper/src/lib/state/init-values/image-cropper-settings-init-value.ts new file mode 100644 index 0000000..00c79c4 --- /dev/null +++ b/projects/ngx-image-cropper/src/lib/state/init-values/image-cropper-settings-init-value.ts @@ -0,0 +1,41 @@ +import { ImageCropperSettings } from '../../interfaces'; +import { getCropperInitValue, getTransformInitValue } from './'; + +export function getImageCropperSettingsInitValue(){ + return structuredClone(imageCropperSettingsInitValue); +} + +export const imageCropperSettingsInitValue: ImageCropperSettings = { + alignImage: 'center', + allowMoveImage: false, + aspectRatio: 1, + autoCrop: true, + backgroundColor: null, + canvasRotation: 0, + checkImageType: true, + containWithinAspectRatio: false, + cropper: getCropperInitValue(), + cropperFrameAriaLabel: null, + cropperMaxHeight: 0, + cropperMaxWidth: 0, + cropperMinHeight: 0, + cropperMinWidth: 0, + cropperStaticHeight: 0, + cropperStaticWidth: 0, + disabled: false, + format: 'png', + hidden: false, + hideResizeSquares: false, + imageAltText: null, + imageQuality: 92, + imageSource: {}, + initialStepSize: 3, + maintainAspectRatio: true, + onlyScaleDown: false, + output: 'blob', + resetCropOnAspectRatioChange: true, + resizeToHeight: 0, + resizeToWidth: 0, + roundCropper: false, + transform: getTransformInitValue(), +} \ No newline at end of file diff --git a/projects/ngx-image-cropper/src/lib/state/init-values/index.ts b/projects/ngx-image-cropper/src/lib/state/init-values/index.ts new file mode 100644 index 0000000..5aa6a03 --- /dev/null +++ b/projects/ngx-image-cropper/src/lib/state/init-values/index.ts @@ -0,0 +1,3 @@ +export * from './cropper-init-value'; +export * from './transform-init-value'; +export * from './image-cropper-settings-init-value'; \ No newline at end of file diff --git a/projects/ngx-image-cropper/src/lib/state/init-values/transform-init-value.ts b/projects/ngx-image-cropper/src/lib/state/init-values/transform-init-value.ts new file mode 100644 index 0000000..debbae1 --- /dev/null +++ b/projects/ngx-image-cropper/src/lib/state/init-values/transform-init-value.ts @@ -0,0 +1,15 @@ +import { ImageTransform } from './../../interfaces'; + +const transformInitValueToCopy: ImageTransform = { + flipX: false, + flipY: false, + rotate: 0, + scale: 1, + translateUnit: 'px', + translateX: 0, + translateY: 0, +}; + +export function getTransformInitValue() { + return { ...transformInitValueToCopy }; +}; diff --git a/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts b/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts index 5e4513d..a7ffad2 100644 --- a/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts +++ b/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts @@ -1,247 +1,225 @@ -import { CropperPosition, MoveStart } from '../interfaces'; -import { CropperState } from '../component/cropper.state'; -import { BasicEvent } from '../interfaces/basic-event.interface'; +import { CropperPosition, MoveStart, BasicEvent } from '../interfaces'; import { HammerInput } from './hammer.utils'; +import { ImageCropperState } from '../state/image-cropper-state'; -export function checkCropperPosition(cropperPosition: CropperPosition, cropperState: CropperState, maintainSize: boolean): CropperPosition { - cropperPosition = checkCropperSizeRestriction(cropperPosition, cropperState); - return checkCropperWithinMaxSizeBounds(cropperPosition, cropperState, maintainSize); -} +export function checkWithinCropperSizeBounds(state: ImageCropperState, resetCropper: boolean): void { + + if (resetCropper || state.cropper.x2 === 0) { + state.cropper.x1 = 0; + state.cropper.y1 = 0; + state.cropper.x2 = state.maxSize.width; + state.cropper.y2 = state.maxSize.height; + }; -export function checkCropperSizeRestriction(cropperPosition: CropperPosition, cropperState: CropperState): CropperPosition { - let cropperWidth = cropperPosition.x2 - cropperPosition.x1; - let cropperHeight = cropperPosition.y2 - cropperPosition.y1; - const centerX = cropperPosition.x1 + cropperWidth / 2; - const centerY = cropperPosition.y1 + cropperHeight / 2; + let cropperWidth = state.cropper.x2 - state.cropper.x1; + let cropperHeight = state.cropper.y2 - state.cropper.y1; + const centerX = state.cropper.x1 + cropperWidth / 2; + const centerY = state.cropper.y1 + cropperHeight / 2; - if (cropperState.options.cropperStaticHeight && cropperState.options.cropperStaticWidth) { - cropperWidth = cropperState.maxSize!.width > cropperState.options.cropperStaticWidth - ? cropperState.options.cropperStaticWidth - : cropperState.maxSize!.width; - cropperHeight = cropperState.maxSize!.height > cropperState.options.cropperStaticHeight - ? cropperState.options.cropperStaticHeight - : cropperState.maxSize!.height; + if (state.cropperStaticHeight && state.cropperStaticWidth) { + cropperWidth = state.maxSize.width > state.cropperStaticWidth ? + state.cropperStaticWidth : state.maxSize.width; + cropperHeight = state.maxSize.height > state.cropperStaticHeight ? + state.cropperStaticHeight : state.maxSize.height; } else { - cropperWidth = Math.max(cropperState.cropperScaledMinWidth, Math.min(cropperWidth, cropperState.cropperScaledMaxWidth, cropperState.maxSize!.width)); - cropperHeight = Math.max(cropperState.cropperScaledMinHeight, Math.min(cropperHeight, cropperState.cropperScaledMaxHeight, cropperState.maxSize!.height)); - if (cropperState.options.maintainAspectRatio) { - if (cropperState.maxSize!.width / cropperState.options.aspectRatio < cropperState.maxSize!.height) { - cropperHeight = cropperWidth / cropperState.options.aspectRatio; - } else { - cropperWidth = cropperHeight * cropperState.options.aspectRatio; - } + cropperWidth = Math.max(state.cropperScaledMinWidth, Math.min(cropperWidth, state.cropperScaledMaxWidth, state.maxSize.width)); + cropperHeight = Math.max(state.cropperScaledMinHeight, Math.min(cropperHeight, state.cropperScaledMaxHeight, state.maxSize.height)); + if (state.maintainAspectRatio) { + state.maxSize.width / state.aspectRatio < state.maxSize.height + ? cropperHeight = cropperWidth / state.aspectRatio + : cropperWidth = cropperHeight * state.aspectRatio; } } - const x1 = centerX - cropperWidth / 2; - const x2 = x1 + cropperWidth; - const y1 = centerY - cropperHeight / 2; - const y2 = y1 + cropperHeight; - return {x1, x2, y1, y2}; + state.cropper.x1 = centerX - cropperWidth / 2; + state.cropper.x2 = state.cropper.x1 + cropperWidth; + state.cropper.y1 = centerY - cropperHeight / 2; + state.cropper.y2 = state.cropper.y1 + cropperHeight; } -export function checkCropperWithinMaxSizeBounds(position: CropperPosition, cropperState: CropperState, maintainSize = false): CropperPosition { - if (position.x1 < 0) { - position = { - ...position, - x1: 0, - x2: position.x2 - (maintainSize ? position.x1 : 0) - }; +export function checkWithinMaxSizeBounds(state: ImageCropperState, maintainSize = false): void { + if (state.cropper.x1 < 0) { + state.cropper.x2 -= maintainSize ? state.cropper.x1 : 0; + state.cropper.x1 = 0; } - if (position.y1 < 0) { - position = { - ...position, - y2: position.y2 - (maintainSize ? position.y1 : 0), - y1: 0 - }; + if (state.cropper.y1 < 0) { + state.cropper.y2 -= maintainSize ? state.cropper.y1 : 0; + state.cropper.y1 = 0; } - if (position.x2 > cropperState.maxSize!.width) { - position = { - ...position, - x1: position.x1 - (maintainSize ? (position.x2 - cropperState.maxSize!.width) : 0), - x2: cropperState.maxSize!.width - }; + if (state.cropper.x2 > state.maxSize.width) { + state.cropper.x1 -= maintainSize ? (state.cropper.x2 - state.maxSize.width) : 0; + state.cropper.x2 = state.maxSize.width; } - if (position.y2 > cropperState.maxSize!.height) { - position = { - ...position, - y1: position.y1 - (maintainSize ? (position.y2 - cropperState.maxSize!.height) : 0), - y2: cropperState.maxSize!.height - }; + if (state.cropper.y2 > state.maxSize.height) { + state.cropper.y1 -= maintainSize ? (state.cropper.y2 - state.maxSize.height) : 0; + state.cropper.y2 = state.maxSize.height; } - return position; } -export function moveCropper(event: Event | BasicEvent, moveStart: MoveStart): CropperPosition { +export function move(event: Event | BasicEvent, moveStart: MoveStart, cropper: CropperPosition) { const diffX = getClientX(event) - moveStart.clientX; const diffY = getClientY(event) - moveStart.clientY; - return { - x1: moveStart.cropper.x1 + diffX, - y1: moveStart.cropper.y1 + diffY, - x2: moveStart.cropper.x2 + diffX, - y2: moveStart.cropper.y2 + diffY - }; + cropper.x1 = moveStart.cropper.x1 + diffX; + cropper.y1 = moveStart.cropper.y1 + diffY; + cropper.x2 = moveStart.cropper.x2 + diffX; + cropper.y2 = moveStart.cropper.y2 + diffY; } -export function resizeCropper(event: Event | BasicEvent | HammerInput, moveStart: MoveStart, cropperState: CropperState): CropperPosition { - const cropperPosition = {...cropperState.cropper}; +export function resize(event: Event | BasicEvent | HammerInput, moveStart: MoveStart, state: ImageCropperState): void { const moveX = getClientX(event) - moveStart.clientX; const moveY = getClientY(event) - moveStart.clientY; switch (moveStart.position) { case 'left': - cropperPosition.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, cropperPosition.x2 - cropperState.cropperScaledMaxWidth), - cropperPosition.x2 - cropperState.cropperScaledMinWidth); + state.cropper.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, state.cropper.x2 - state.cropperScaledMaxWidth), + state.cropper.x2 - state.cropperScaledMinWidth); break; case 'topleft': - cropperPosition.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, cropperPosition.x2 - cropperState.cropperScaledMaxWidth), - cropperPosition.x2 - cropperState.cropperScaledMinWidth); - cropperPosition.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, cropperPosition.y2 - cropperState.cropperScaledMaxHeight), - cropperPosition.y2 - cropperState.cropperScaledMinHeight); + state.cropper.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, state.cropper.x2 - state.cropperScaledMaxWidth), + state.cropper.x2 - state.cropperScaledMinWidth); + state.cropper.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, state.cropper.y2 - state.cropperScaledMaxHeight), + state.cropper.y2 - state.cropperScaledMinHeight); break; case 'top': - cropperPosition.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, cropperPosition.y2 - cropperState.cropperScaledMaxHeight), - cropperPosition.y2 - cropperState.cropperScaledMinHeight); + state.cropper.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, state.cropper.y2 - state.cropperScaledMaxHeight), + state.cropper.y2 - state.cropperScaledMinHeight); break; case 'topright': - cropperPosition.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, cropperPosition.x1 + cropperState.cropperScaledMaxWidth), - cropperPosition.x1 + cropperState.cropperScaledMinWidth); - cropperPosition.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, cropperPosition.y2 - cropperState.cropperScaledMaxHeight), - cropperPosition.y2 - cropperState.cropperScaledMinHeight); + state.cropper.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, state.cropper.x1 + state.cropperScaledMaxWidth), + state.cropper.x1 + state.cropperScaledMinWidth); + state.cropper.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, state.cropper.y2 - state.cropperScaledMaxHeight), + state.cropper.y2 - state.cropperScaledMinHeight); break; case 'right': - cropperPosition.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, cropperPosition.x1 + cropperState.cropperScaledMaxWidth), - cropperPosition.x1 + cropperState.cropperScaledMinWidth); + state.cropper.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, state.cropper.x1 + state.cropperScaledMaxWidth), + state.cropper.x1 + state.cropperScaledMinWidth); break; case 'bottomright': - cropperPosition.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, cropperPosition.x1 + cropperState.cropperScaledMaxWidth), - cropperPosition.x1 + cropperState.cropperScaledMinWidth); - cropperPosition.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, cropperPosition.y1 + cropperState.cropperScaledMaxHeight), - cropperPosition.y1 + cropperState.cropperScaledMinHeight); + state.cropper.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, state.cropper.x1 + state.cropperScaledMaxWidth), + state.cropper.x1 + state.cropperScaledMinWidth); + state.cropper.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, state.cropper.y1 + state.cropperScaledMaxHeight), + state.cropper.y1 + state.cropperScaledMinHeight); break; case 'bottom': - cropperPosition.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, cropperPosition.y1 + cropperState.cropperScaledMaxHeight), - cropperPosition.y1 + cropperState.cropperScaledMinHeight); + state.cropper.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, state.cropper.y1 + state.cropperScaledMaxHeight), + state.cropper.y1 + state.cropperScaledMinHeight); break; case 'bottomleft': - cropperPosition.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, cropperPosition.x2 - cropperState.cropperScaledMaxWidth), - cropperPosition.x2 - cropperState.cropperScaledMinWidth); - cropperPosition.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, cropperPosition.y1 + cropperState.cropperScaledMaxHeight), - cropperPosition.y1 + cropperState.cropperScaledMinHeight); + state.cropper.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, state.cropper.x2 - state.cropperScaledMaxWidth), + state.cropper.x2 - state.cropperScaledMinWidth); + state.cropper.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, state.cropper.y1 + state.cropperScaledMaxHeight), + state.cropper.y1 + state.cropperScaledMinHeight); break; case 'center': const scale = 'scale' in event ? event.scale : 1; const newWidth = Math.min( - Math.max(cropperState.cropperScaledMinWidth, (Math.abs(moveStart.cropper.x2 - moveStart.cropper.x1)) * scale), - cropperState.cropperScaledMaxWidth); + Math.max(state.cropperScaledMinWidth, (Math.abs(moveStart.cropper.x2 - moveStart.cropper.x1)) * scale), + state.cropperScaledMaxWidth); const newHeight = Math.min( - Math.max(cropperState.cropperScaledMinHeight, (Math.abs(moveStart.cropper.y2 - moveStart.cropper.y1)) * scale), - cropperState.cropperScaledMaxHeight); - cropperPosition.x1 = moveStart.clientX - newWidth / 2; - cropperPosition.x2 = moveStart.clientX + newWidth / 2; - cropperPosition.y1 = moveStart.clientY - newHeight / 2; - cropperPosition.y2 = moveStart.clientY + newHeight / 2; - if (cropperPosition.x1 < 0) { - cropperPosition.x2 -= cropperPosition.x1; - cropperPosition.x1 = 0; - } else if (cropperPosition.x2 > cropperState.maxSize!.width) { - cropperPosition.x1 -= (cropperPosition.x2 - cropperState.maxSize!.width); - cropperPosition.x2 = cropperState.maxSize!.width; + Math.max(state.cropperScaledMinHeight, (Math.abs(moveStart.cropper.y2 - moveStart.cropper.y1)) * scale), + state.cropperScaledMaxHeight); + state.cropper.x1 = moveStart.clientX - newWidth / 2; + state.cropper.x2 = moveStart.clientX + newWidth / 2; + state.cropper.y1 = moveStart.clientY - newHeight / 2; + state.cropper.y2 = moveStart.clientY + newHeight / 2; + if (state.cropper.x1 < 0) { + state.cropper.x2 -= state.cropper.x1; + state.cropper.x1 = 0; + } else if (state.cropper.x2 > state.maxSize.width) { + state.cropper.x1 -= (state.cropper.x2 - state.maxSize.width); + state.cropper.x2 = state.maxSize.width; } - if (cropperPosition.y1 < 0) { - cropperPosition.y2 -= cropperPosition.y1; - cropperPosition.y1 = 0; - } else if (cropperPosition.y2 > cropperState.maxSize!.height) { - cropperPosition.y1 -= (cropperPosition.y2 - cropperState.maxSize!.height); - cropperPosition.y2 = cropperState.maxSize!.height; + if (state.cropper.y1 < 0) { + state.cropper.y2 -= state.cropper.y1; + state.cropper.y1 = 0; + } else if (state.cropper.y2 > state.maxSize.height) { + state.cropper.y1 -= (state.cropper.y2 - state.maxSize.height); + state.cropper.y2 = state.maxSize.height; } break; } - if (cropperState.options.maintainAspectRatio) { - return checkAspectRatio(moveStart.position!, cropperPosition, cropperState); - } else { - return cropperPosition; + if (state.maintainAspectRatio) { + checkAspectRatio(moveStart.position!, state); } } -export function checkAspectRatio(position: string, cropperPosition: CropperPosition, cropperState: CropperState): CropperPosition { - cropperPosition = {...cropperPosition}; +export function checkAspectRatio(position: string, state: ImageCropperState): void { let overflowX = 0; let overflowY = 0; + switch (position) { case 'top': - cropperPosition.x2 = cropperPosition.x1 + (cropperPosition.y2 - cropperPosition.y1) * cropperState.options.aspectRatio; - overflowX = Math.max(cropperPosition.x2 - cropperState.maxSize!.width, 0); - overflowY = Math.max(0 - cropperPosition.y1, 0); + state.cropper.x2 = state.cropper.x1 + (state.cropper.y2 - state.cropper.y1) * state.aspectRatio; + overflowX = Math.max(state.cropper.x2 - state.maxSize.width, 0); + overflowY = Math.max(0 - state.cropper.y1, 0); if (overflowX > 0 || overflowY > 0) { - cropperPosition.x2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? (overflowY * cropperState.options.aspectRatio) : overflowX; - cropperPosition.y1 += (overflowY * cropperState.options.aspectRatio) > overflowX ? overflowY : overflowX / cropperState.options.aspectRatio; + state.cropper.x2 -= (overflowY * state.aspectRatio) > overflowX ? (overflowY * state.aspectRatio) : overflowX; + state.cropper.y1 += (overflowY * state.aspectRatio) > overflowX ? overflowY : overflowX / state.aspectRatio; } break; case 'bottom': - cropperPosition.x2 = cropperPosition.x1 + (cropperPosition.y2 - cropperPosition.y1) * cropperState.options.aspectRatio; - overflowX = Math.max(cropperPosition.x2 - cropperState.maxSize!.width, 0); - overflowY = Math.max(cropperPosition.y2 - cropperState.maxSize!.height, 0); + state.cropper.x2 = state.cropper.x1 + (state.cropper.y2 - state.cropper.y1) * state.aspectRatio; + overflowX = Math.max(state.cropper.x2 - state.maxSize.width, 0); + overflowY = Math.max(state.cropper.y2 - state.maxSize.height, 0); if (overflowX > 0 || overflowY > 0) { - cropperPosition.x2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? (overflowY * cropperState.options.aspectRatio) : overflowX; - cropperPosition.y2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? overflowY : (overflowX / cropperState.options.aspectRatio); + state.cropper.x2 -= (overflowY * state.aspectRatio) > overflowX ? (overflowY * state.aspectRatio) : overflowX; + state.cropper.y2 -= (overflowY * state.aspectRatio) > overflowX ? overflowY : (overflowX / state.aspectRatio); } break; case 'topleft': - cropperPosition.y1 = cropperPosition.y2 - (cropperPosition.x2 - cropperPosition.x1) / cropperState.options.aspectRatio; - overflowX = Math.max(0 - cropperPosition.x1, 0); - overflowY = Math.max(0 - cropperPosition.y1, 0); + state.cropper.y1 = state.cropper.y2 - (state.cropper.x2 - state.cropper.x1) / state.aspectRatio; + overflowX = Math.max(0 - state.cropper.x1, 0); + overflowY = Math.max(0 - state.cropper.y1, 0); if (overflowX > 0 || overflowY > 0) { - cropperPosition.x1 += (overflowY * cropperState.options.aspectRatio) > overflowX ? (overflowY * cropperState.options.aspectRatio) : overflowX; - cropperPosition.y1 += (overflowY * cropperState.options.aspectRatio) > overflowX ? overflowY : overflowX / cropperState.options.aspectRatio; + state.cropper.x1 += (overflowY * state.aspectRatio) > overflowX ? (overflowY * state.aspectRatio) : overflowX; + state.cropper.y1 += (overflowY * state.aspectRatio) > overflowX ? overflowY : overflowX / state.aspectRatio; } break; case 'topright': - cropperPosition.y1 = cropperPosition.y2 - (cropperPosition.x2 - cropperPosition.x1) / cropperState.options.aspectRatio; - overflowX = Math.max(cropperPosition.x2 - cropperState.maxSize!.width, 0); - overflowY = Math.max(0 - cropperPosition.y1, 0); + state.cropper.y1 = state.cropper.y2 - (state.cropper.x2 - state.cropper.x1) / state.aspectRatio; + overflowX = Math.max(state.cropper.x2 - state.maxSize.width, 0); + overflowY = Math.max(0 - state.cropper.y1, 0); if (overflowX > 0 || overflowY > 0) { - cropperPosition.x2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? (overflowY * cropperState.options.aspectRatio) : overflowX; - cropperPosition.y1 += (overflowY * cropperState.options.aspectRatio) > overflowX ? overflowY : overflowX / cropperState.options.aspectRatio; + state.cropper.x2 -= (overflowY * state.aspectRatio) > overflowX ? (overflowY * state.aspectRatio) : overflowX; + state.cropper.y1 += (overflowY * state.aspectRatio) > overflowX ? overflowY : overflowX / state.aspectRatio; } break; case 'right': case 'bottomright': - cropperPosition.y2 = cropperPosition.y1 + (cropperPosition.x2 - cropperPosition.x1) / cropperState.options.aspectRatio; - overflowX = Math.max(cropperPosition.x2 - cropperState.maxSize!.width, 0); - overflowY = Math.max(cropperPosition.y2 - cropperState.maxSize!.height, 0); + state.cropper.y2 = state.cropper.y1 + (state.cropper.x2 - state.cropper.x1) / state.aspectRatio; + overflowX = Math.max(state.cropper.x2 - state.maxSize.width, 0); + overflowY = Math.max(state.cropper.y2 - state.maxSize.height, 0); if (overflowX > 0 || overflowY > 0) { - cropperPosition.x2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? (overflowY * cropperState.options.aspectRatio) : overflowX; - cropperPosition.y2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? overflowY : overflowX / cropperState.options.aspectRatio; + state.cropper.x2 -= (overflowY * state.aspectRatio) > overflowX ? (overflowY * state.aspectRatio) : overflowX; + state.cropper.y2 -= (overflowY * state.aspectRatio) > overflowX ? overflowY : overflowX / state.aspectRatio; } break; case 'left': case 'bottomleft': - cropperPosition.y2 = cropperPosition.y1 + (cropperPosition.x2 - cropperPosition.x1) / cropperState.options.aspectRatio; - overflowX = Math.max(0 - cropperPosition.x1, 0); - overflowY = Math.max(cropperPosition.y2 - cropperState.maxSize!.height, 0); + state.cropper.y2 = state.cropper.y1 + (state.cropper.x2 - state.cropper.x1) / state.aspectRatio; + overflowX = Math.max(0 - state.cropper.x1, 0); + overflowY = Math.max(state.cropper.y2 - state.maxSize.height, 0); if (overflowX > 0 || overflowY > 0) { - cropperPosition.x1 += (overflowY * cropperState.options.aspectRatio) > overflowX ? (overflowY * cropperState.options.aspectRatio) : overflowX; - cropperPosition.y2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? overflowY : overflowX / cropperState.options.aspectRatio; + state.cropper.x1 += (overflowY * state.aspectRatio) > overflowX ? (overflowY * state.aspectRatio) : overflowX; + state.cropper.y2 -= (overflowY * state.aspectRatio) > overflowX ? overflowY : overflowX / state.aspectRatio; } break; case 'center': - cropperPosition.x2 = cropperPosition.x1 + (cropperPosition.y2 - cropperPosition.y1) * cropperState.options.aspectRatio; - cropperPosition.y2 = cropperPosition.y1 + (cropperPosition.x2 - cropperPosition.x1) / cropperState.options.aspectRatio; - const overflowX1 = Math.max(0 - cropperPosition.x1, 0); - const overflowX2 = Math.max(cropperPosition.x2 - cropperState.maxSize!.width, 0); - const overflowY1 = Math.max(cropperPosition.y2 - cropperState.maxSize!.height, 0); - const overflowY2 = Math.max(0 - cropperPosition.y1, 0); + state.cropper.x2 = state.cropper.x1 + (state.cropper.y2 - state.cropper.y1) * state.aspectRatio; + state.cropper.y2 = state.cropper.y1 + (state.cropper.x2 - state.cropper.x1) / state.aspectRatio; + const overflowX1 = Math.max(0 - state.cropper.x1, 0); + const overflowX2 = Math.max(state.cropper.x2 - state.maxSize.width, 0); + const overflowY1 = Math.max(state.cropper.y2 - state.maxSize.height, 0); + const overflowY2 = Math.max(0 - state.cropper.y1, 0); if (overflowX1 > 0 || overflowX2 > 0 || overflowY1 > 0 || overflowY2 > 0) { - cropperPosition.x1 += (overflowY1 * cropperState.options.aspectRatio) > overflowX1 ? (overflowY1 * cropperState.options.aspectRatio) : overflowX1; - cropperPosition.x2 -= (overflowY2 * cropperState.options.aspectRatio) > overflowX2 ? (overflowY2 * cropperState.options.aspectRatio) : overflowX2; - cropperPosition.y1 += (overflowY2 * cropperState.options.aspectRatio) > overflowX2 ? overflowY2 : overflowX2 / cropperState.options.aspectRatio; - cropperPosition.y2 -= (overflowY1 * cropperState.options.aspectRatio) > overflowX1 ? overflowY1 : overflowX1 / cropperState.options.aspectRatio; + state.cropper.x1 += (overflowY1 * state.aspectRatio) > overflowX1 ? (overflowY1 * state.aspectRatio) : overflowX1; + state.cropper.x2 -= (overflowY2 * state.aspectRatio) > overflowX2 ? (overflowY2 * state.aspectRatio) : overflowX2; + state.cropper.y1 += (overflowY2 * state.aspectRatio) > overflowX2 ? overflowY2 : overflowX2 / state.aspectRatio; + state.cropper.y2 -= (overflowY1 * state.aspectRatio) > overflowX1 ? overflowY1 : overflowX1 / state.aspectRatio; } break; } - return cropperPosition; } export function getClientX(event: Event | BasicEvent | TouchEvent | HammerInput): number { @@ -262,4 +240,4 @@ export function getClientY(event: Event | BasicEvent | TouchEvent | HammerInput) } return 0; -} +} \ No newline at end of file diff --git a/projects/ngx-image-cropper/src/lib/utils/cropper-size-bounds.utils.ts b/projects/ngx-image-cropper/src/lib/utils/cropper-size-bounds.utils.ts new file mode 100644 index 0000000..a36f7bb --- /dev/null +++ b/projects/ngx-image-cropper/src/lib/utils/cropper-size-bounds.utils.ts @@ -0,0 +1,49 @@ +import { ImageCropperState } from "../state/image-cropper-state"; + +export function setCropperScaledMinSize(state: ImageCropperState): void { + if (state.loadedImage?.transformed.size) { + setCropperScaledMinWidth(state); + setCropperScaledMinHeight(state); + } else { + state.cropperScaledMinWidth = 20; + state.cropperScaledMinHeight = 20; + } +} + +export function setCropperScaledMinWidth(state: ImageCropperState): void { + state.cropperScaledMinWidth = state.cropperMinWidth > 0 + ? Math.max(20, (state.cropperMinWidth / state.loadedImage!.transformed.size.width) * state.maxSize.width) + : 20; +} + +export function setCropperScaledMinHeight(state: ImageCropperState): void { + if (state.maintainAspectRatio) { + state.cropperScaledMinHeight = Math.max(20, state.cropperScaledMinWidth / state.aspectRatio); + } else if (state.cropperMinHeight > 0) { + state.cropperScaledMinHeight = Math.max(20, (state.cropperMinHeight / state.loadedImage!.transformed.size.height) * state.maxSize.height); + } else { + state.cropperScaledMinHeight = 20; + } +} + +export function setCropperScaledMaxSize(state: ImageCropperState): void { + if (state.loadedImage?.transformed.size) { + const ratio = state.loadedImage.transformed.size.width / state.maxSize.width; + state.cropperScaledMaxWidth = state.cropperMaxWidth > 20 + ? state.cropperMaxWidth / ratio + : state.maxSize.width; + state.cropperScaledMaxHeight = state.cropperMaxHeight > 20 + ? state.cropperMaxHeight / ratio + : state.maxSize.height; + if (state.maintainAspectRatio) { + if (state.cropperScaledMaxWidth > state.cropperScaledMaxHeight * state.aspectRatio) { + state.cropperScaledMaxWidth = state.cropperScaledMaxHeight * state.aspectRatio; + } else if (state.cropperScaledMaxWidth < state.cropperScaledMaxHeight * state.aspectRatio) { + state.cropperScaledMaxHeight = state.cropperScaledMaxWidth / state.aspectRatio; + } + } + } else { + state.cropperScaledMaxWidth = state.maxSize.width; + state.cropperScaledMaxHeight = state.maxSize.height; + } +} diff --git a/projects/ngx-image-cropper/src/lib/utils/index.ts b/projects/ngx-image-cropper/src/lib/utils/index.ts new file mode 100644 index 0000000..77125e8 --- /dev/null +++ b/projects/ngx-image-cropper/src/lib/utils/index.ts @@ -0,0 +1,8 @@ +export * from './blob.utils'; +export * as cropperPosition from './cropper-position.utils'; +export * as cropperSizeBounds from './cropper-size-bounds.utils'; +export * from './exif.utils'; +export * from './hammer.utils'; +export * from './keyboard.utils'; +export * from './percentage.utils'; +export * from './resize.utils'; \ No newline at end of file diff --git a/projects/ngx-image-cropper/src/public-api.ts b/projects/ngx-image-cropper/src/public-api.ts index 753c4d8..4d3bfe5 100644 --- a/projects/ngx-image-cropper/src/public-api.ts +++ b/projects/ngx-image-cropper/src/public-api.ts @@ -2,5 +2,6 @@ export * from './lib/component/image-cropper.component'; export * from './lib/services/crop.service'; export * from './lib/services/load-image.service'; export * from './lib/interfaces'; +export { getCropperInitValue, getTransformInitValue, getImageCropperSettingsInitValue } from './lib/state/init-values'; export { base64ToFile } from './lib/utils/blob.utils'; export { resizeCanvas } from './lib/utils/resize.utils'; From 3e7925f18a6880606d322be434f707faf41ea59c Mon Sep 17 00:00:00 2001 From: Alba Date: Wed, 24 Jul 2024 13:55:29 +0200 Subject: [PATCH 2/8] Move functions out of main --- .../lib/component/image-cropper.component.ts | 63 +++---------------- .../src/lib/services/load-image.service.ts | 15 +++++ .../src/lib/utils/cropper-position.utils.ts | 16 ++++- 3 files changed, 40 insertions(+), 54 deletions(-) diff --git a/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts b/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts index ff0d73a..bea6e62 100644 --- a/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts +++ b/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts @@ -98,8 +98,14 @@ export class ImageCropperComponent implements OnChanges, OnInit { changes = this.state.getChangesAndUpdateSettings(changes); if (changes.imageSource) { - this.onChangesInputImage(this.state); - return; + this.reset(); + if (Object.keys(changes.imageSource).length) { + this.loadImageService + .loadNewImage(this.state) + .then((res) => this.setLoadedImage(res)) + .catch((err) => this.loadImageError(err)); + return; + } }; if (!this.state.loadedImage?.transformed.image.complete) return; @@ -119,7 +125,7 @@ export class ImageCropperComponent implements OnChanges, OnInit { if ((this.state.maintainAspectRatio && changes.aspectRatio) || changes.maintainAspectRatio) { cropperSizeBounds.setCropperScaledMinSize(this.state); cropperSizeBounds.setCropperScaledMaxSize(this.state); - if (this.state.maintainAspectRatio && (this.state.resetCropOnAspectRatioChange || !this.aspectRatioIsCorrect())) { + if (this.state.maintainAspectRatio && (this.state.resetCropOnAspectRatioChange || !cropperPosition.aspectRatioIsCorrect(this.state))) { checkCropperWithinBounds = true; resetCropper = true; } @@ -166,20 +172,6 @@ export class ImageCropperComponent implements OnChanges, OnInit { } } - private onChangesInputImage(state: ImageCropperState): void { - this.reset(); - if (state.imageSource.imageChangedEvent) { - const target = state.imageSource.imageChangedEvent.target as HTMLInputElement; - if (!!target.files && target.files.length > 0) this.loadImageFile(target.files![0]); - } else if (state.imageSource.imageURL) { - this.loadImageFromURL(state.imageSource.imageURL); - } else if (state.imageSource.imageBase64) { - this.loadBase64Image(state.imageSource.imageBase64); - } else if (state.imageSource.imageFile) { - this.loadImageFile(state.imageSource.imageFile); - } - } - private reset(): void { this.safeImgDataUrl = 'data:image/png;base64,iVBORw0KGg' + 'oAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAU' @@ -189,27 +181,6 @@ export class ImageCropperComponent implements OnChanges, OnInit { this.imageVisible = false; } - private loadImageFile(file: File): void { - this.loadImageService - .loadImageFile(file, this.state) - .then((res) => this.setLoadedImage(res)) - .catch((err) => this.loadImageError(err)); - } - - private loadBase64Image(imageBase64: string): void { - this.loadImageService - .loadBase64Image(imageBase64, this.state) - .then((res) => this.setLoadedImage(res)) - .catch((err) => this.loadImageError(err)); - } - - private loadImageFromURL(url: string): void { - this.loadImageService - .loadImageFromURL(url, this.state) - .then((res) => this.setLoadedImage(res)) - .catch((err) => this.loadImageError(err)); - } - private setLoadedImage(loadedImage: LoadedImage): void { this.state.loadedImage = loadedImage; this.safeImgDataUrl = this.sanitizer.bypassSecurityTrustResourceUrl(loadedImage.transformed.objectUrl); @@ -275,7 +246,7 @@ export class ImageCropperComponent implements OnChanges, OnInit { } else { const oldMaxSize = {...this.state.maxSize}; this.setMaxSize(); - this.resizeCropperPosition(oldMaxSize); + cropperPosition.resizeCropperAccordingToNewMaxSize(this.state, oldMaxSize); cropperSizeBounds.setCropperScaledMinSize(this.state); cropperSizeBounds.setCropperScaledMaxSize(this.state); this.settingsUpdated.emit(this.state.getDeepCopyOfSettings()); @@ -468,20 +439,6 @@ export class ImageCropperComponent implements OnChanges, OnInit { } } - private aspectRatioIsCorrect(): boolean { - const currentCropAspectRatio = (this.state.cropper.x2 - this.state.cropper.x1) / (this.state.cropper.y2 - this.state.cropper.y1); - return currentCropAspectRatio === this.state.aspectRatio; - } - - private resizeCropperPosition(oldMaxSize: Dimensions): void { - if (oldMaxSize.width !== this.state.maxSize.width || oldMaxSize.height !== this.state.maxSize.height) { - this.state.cropper.x1 = this.state.cropper.x1 * this.state.maxSize.width / oldMaxSize.width; - this.state.cropper.x2 = this.state.cropper.x2 * this.state.maxSize.width / oldMaxSize.width; - this.state.cropper.y1 = this.state.cropper.y1 * this.state.maxSize.height / oldMaxSize.height; - this.state.cropper.y2 = this.state.cropper.y2 * this.state.maxSize.height / oldMaxSize.height; - } - } - private doAutoCrop(): void { if (this.state.autoCrop) { void this.crop(); diff --git a/projects/ngx-image-cropper/src/lib/services/load-image.service.ts b/projects/ngx-image-cropper/src/lib/services/load-image.service.ts index 3f9947b..e232c6f 100644 --- a/projects/ngx-image-cropper/src/lib/services/load-image.service.ts +++ b/projects/ngx-image-cropper/src/lib/services/load-image.service.ts @@ -15,6 +15,21 @@ export class LoadImageService { private autoRotateSupported: Promise = supportsAutomaticRotation(); + loadNewImage(state: ImageCropperState): Promise { + if (state.imageSource.imageChangedEvent) { + const target = state.imageSource.imageChangedEvent.target as HTMLInputElement; + if (!!target.files && target.files.length > 0) { + return this.loadImageFile(target.files![0], state)}; + } else if (state.imageSource.imageURL) { + return this.loadImageFromURL(state.imageSource.imageURL, state); + } else if (state.imageSource.imageBase64) { + return this.loadBase64Image(state.imageSource.imageBase64, state); + } else if (state.imageSource.imageFile) { + return this.loadImageFile(state.imageSource.imageFile, state); + } + return Promise.reject(new Error('Invalid image source')); + } + async loadImageFile(file: File, state: ImageCropperState): Promise { const arrayBuffer = await file.arrayBuffer(); if (state.checkImageType) { diff --git a/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts b/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts index a7ffad2..c69a328 100644 --- a/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts +++ b/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts @@ -1,4 +1,4 @@ -import { CropperPosition, MoveStart, BasicEvent } from '../interfaces'; +import { CropperPosition, MoveStart, BasicEvent, Dimensions } from '../interfaces'; import { HammerInput } from './hammer.utils'; import { ImageCropperState } from '../state/image-cropper-state'; @@ -222,6 +222,20 @@ export function checkAspectRatio(position: string, state: ImageCropperState): vo } } +export function aspectRatioIsCorrect(state: ImageCropperState): boolean { + const currentCropAspectRatio = (state.cropper.x2 - state.cropper.x1) / (state.cropper.y2 - state.cropper.y1); + return currentCropAspectRatio === state.aspectRatio; +} + +export function resizeCropperAccordingToNewMaxSize(state: ImageCropperState, oldMaxSize: Dimensions): void { + if (oldMaxSize.width !== state.maxSize.width || oldMaxSize.height !== state.maxSize.height) { + state.cropper.x1 = state.cropper.x1 * state.maxSize.width / oldMaxSize.width; + state.cropper.x2 = state.cropper.x2 * state.maxSize.width / oldMaxSize.width; + state.cropper.y1 = state.cropper.y1 * state.maxSize.height / oldMaxSize.height; + state.cropper.y2 = state.cropper.y2 * state.maxSize.height / oldMaxSize.height; + } +} + export function getClientX(event: Event | BasicEvent | TouchEvent | HammerInput): number { if ('touches' in event && event.touches[0]) { return event.touches[0].clientX; From 27873a60362c7973baec9ef2baae6975187638da Mon Sep 17 00:00:00 2001 From: Alba Date: Sat, 27 Jul 2024 18:35:10 +0200 Subject: [PATCH 3/8] Fix: initialise aspect ratio in demo so it matches logic --- projects/demo-app/src/app/app.component.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/projects/demo-app/src/app/app.component.ts b/projects/demo-app/src/app/app.component.ts index 2e5c719..0df440f 100644 --- a/projects/demo-app/src/app/app.component.ts +++ b/projects/demo-app/src/app/app.component.ts @@ -29,7 +29,7 @@ export class AppComponent { eventList = {}; settings: ImageCropperSettings = getImageCropperSettingsInitValue(); - settingsToUpdate: PartialImageCropperSettings = {}; + settingsToUpdate: PartialImageCropperSettings = { aspectRatio: 16/5 }; constructor( private sanitizer: DomSanitizer @@ -164,9 +164,7 @@ export class AppComponent { */ test() { this.settingsToUpdate = { - canvasRotation: 3, - transform: {scale: 2}, - cropper: { x1: 190, y1: 221.5, x2: 583, y2: 344.3125 } // has 16/5 aspect ratio + cropper: { x1: 50, y1: 50, x2: 250, y2: 112.5 } // has 16/5 aspect ratio }; } } From 9a64ec580cb8a35f51a3c450d6c6a8345ca3f594 Mon Sep 17 00:00:00 2001 From: Alba Date: Sat, 27 Jul 2024 18:45:13 +0200 Subject: [PATCH 4/8] Fix: emit settingsUpdated when there's no image loaded but settings has changed. Parent needs to know that the settings were updated to update view. --- .../src/lib/component/image-cropper.component.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts b/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts index bea6e62..efd5246 100644 --- a/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts +++ b/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts @@ -96,6 +96,8 @@ export class ImageCropperComponent implements OnChanges, OnInit { let changes: PartialImageCropperSettings = simpleChanges["settingsToUpdate"].currentValue; changes = this.state.getChangesAndUpdateSettings(changes); + + if (Object.keys(changes).length === 0) return; if (changes.imageSource) { this.reset(); @@ -108,7 +110,10 @@ export class ImageCropperComponent implements OnChanges, OnInit { } }; - if (!this.state.loadedImage?.transformed.image.complete) return; + if (!this.state.loadedImage?.transformed.image.complete) { + this.settingsUpdated.emit(this.state.getDeepCopyOfSettings()); + return + }; if ((this.state.containWithinAspectRatio && changes.aspectRatio) || changes.containWithinAspectRatio || changes.canvasRotation) { this.loadImageService @@ -156,7 +161,7 @@ export class ImageCropperComponent implements OnChanges, OnInit { crop = true; } - if (Object.keys(changes).length > 0 && !changes.hidden) { + if (!changes.hidden) { this.settingsUpdated.emit(this.state.getDeepCopyOfSettings()); } From dbd76d97c0ab7dcb0c18822b2ebe3d4150682fa0 Mon Sep 17 00:00:00 2001 From: Alba Date: Sat, 27 Jul 2024 18:47:49 +0200 Subject: [PATCH 5/8] Improve setting cropper size bounds and checkSizeAndPosition (previously checkWithinCropperSizeBounds) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed how cropper size bounds are calculated. - Changed naming of scaled cropper state values to internal, as we now check all of them –static too– and they follow different rules. - Added hiding resize squares when static side and changed cursor in non hidden squares. - Deleted resetCropOnAspectRatio change (explained in the rules section of cropperPosition.utils in changes) - Change naming of checkWithinCropperSizeBounds to checkSizeAndPosition, and updated it to cover new behaviour explained in rules (see changes, cropperPosition.utils). - See rules in cropper position and cropper size bounds utils. --- projects/demo-app/src/app/app.component.html | 1 - .../component/image-cropper.component.html | 4 +- .../component/image-cropper.component.scss | 24 ++ .../lib/component/image-cropper.component.ts | 32 +- .../lib/interfaces/image-cropper-settings.ts | 1 - .../src/lib/state/image-cropper-state.ts | 12 +- .../image-cropper-settings-init-value.ts | 1 - .../src/lib/utils/cropper-position.utils.ts | 185 +++++++--- .../lib/utils/cropper-size-bounds.utils.ts | 328 ++++++++++++++++-- 9 files changed, 479 insertions(+), 109 deletions(-) diff --git a/projects/demo-app/src/app/app.component.html b/projects/demo-app/src/app/app.component.html index 5b3984d..bcce441 100644 --- a/projects/demo-app/src/app/app.component.html +++ b/projects/demo-app/src/app/app.component.html @@ -40,7 +40,6 @@ - diff --git a/projects/ngx-image-cropper/src/lib/component/image-cropper.component.html b/projects/ngx-image-cropper/src/lib/component/image-cropper.component.html index e2a2ae0..1cd21a1 100644 --- a/projects/ngx-image-cropper/src/lib/component/image-cropper.component.html +++ b/projects/ngx-image-cropper/src/lib/component/image-cropper.component.html @@ -26,6 +26,8 @@ - + state.cropperStaticWidth ? - state.cropperStaticWidth : state.maxSize.width; - cropperHeight = state.maxSize.height > state.cropperStaticHeight ? - state.cropperStaticHeight : state.maxSize.height; - } else { - cropperWidth = Math.max(state.cropperScaledMinWidth, Math.min(cropperWidth, state.cropperScaledMaxWidth, state.maxSize.width)); - cropperHeight = Math.max(state.cropperScaledMinHeight, Math.min(cropperHeight, state.cropperScaledMaxHeight, state.maxSize.height)); - if (state.maintainAspectRatio) { - state.maxSize.width / state.aspectRatio < state.maxSize.height - ? cropperHeight = cropperWidth / state.aspectRatio - : cropperWidth = cropperHeight * state.aspectRatio; + if (newCropper) { + cropperWidth = state.internalStaticWidth + ? state.internalStaticWidth + : Math.min(cropperWidth, state.internalMaxWidth); + cropperHeight = state.internalStaticHeight + ? state.internalStaticHeight + : Math.min(cropperHeight, state.internalMaxHeight); + } + else if (state.internalStaticWidth && state.internalStaticHeight) { + cropperWidth = state.internalStaticWidth; + cropperHeight = state.internalStaticHeight; + } + else if (state.internalStaticWidth) { + cropperWidth = state.internalStaticWidth; + cropperHeight = state.maintainAspectRatio + ? cropperWidth / state.aspectRatio + : Math.max(state.internalMinHeight, Math.min(cropperHeight, state.internalMaxHeight)); + } + else if (state.internalStaticHeight) { + cropperHeight = state.internalStaticHeight; + cropperWidth = state.maintainAspectRatio + ? cropperHeight * state.aspectRatio + : Math.max(state.internalMinWidth, Math.min(cropperWidth, state.internalMaxWidth)); + } + else if (!state.maintainAspectRatio) { + cropperWidth = Math.max(state.internalMinWidth, Math.min(cropperWidth, state.internalMaxWidth)); + cropperHeight = Math.max(state.internalMinHeight, Math.min(cropperHeight, state.internalMaxHeight)); + } + else { + // const start = {cropperWidth, cropperHeight}; // + const prevAspectRatio = prevWidth / prevHeight; + const maxSizeAspectRatio = state.maxSize.width / state.maxSize.height; + if (prevAspectRatio > maxSizeAspectRatio) { + cropperWidth = prevWidth; + cropperHeight = cropperWidth / maxSizeAspectRatio; + } else { + cropperHeight = prevHeight; + cropperWidth = cropperHeight * maxSizeAspectRatio; } + // const bbox = {cropperWidth, cropperHeight}; // + if (state.aspectRatio > maxSizeAspectRatio) { + cropperWidth = Math.max(state.internalMinWidth, Math.min(cropperWidth, state.internalMaxWidth)); + cropperHeight = cropperWidth / state.aspectRatio; + } else { + cropperHeight = Math.max(state.internalMinHeight, Math.min(cropperHeight, state.internalMaxHeight)); + cropperWidth = cropperHeight * state.aspectRatio; + } + /* + console.log('\n fit to aspect ratio in checkWithinCropperSizeBounds', + '\n cropperStatic ', state.internalStaticWidth, state.internalStaticHeight, + '\n cropperMin ', state.internalMinWidth, state.internalMinHeight, + '\n cropperMax ', state.internalMaxWidth, state.internalMaxHeight, + '\n ', + '\n prev and max ratios ', prevAspectRatio, maxSizeAspectRatio, + '\n state and end ratios ', state.aspectRatio, cropperWidth / cropperHeight, + '\n ', + '\n prev ', prevWidth, prevHeight, + '\n start ', start.cropperWidth, start.cropperHeight, + '\n bbox ', bbox.cropperWidth, bbox.cropperHeight, + '\n end ', cropperWidth, cropperHeight, + '\n ' + ); + */ } - state.cropper.x1 = centerX - cropperWidth / 2; + state.cropper.x1 = prevCenterX - cropperWidth / 2; state.cropper.x2 = state.cropper.x1 + cropperWidth; - state.cropper.y1 = centerY - cropperHeight / 2; + state.cropper.y1 = prevCenterY - cropperHeight / 2; state.cropper.y2 = state.cropper.y1 + cropperHeight; + + if (!newCropper) { checkWithinMaxSizeBounds(state, true) }; + + /* + console.log('\n checkWithinBounds', + '\n ', + '\n cropper static ', state.internalStaticWidth, state.internalStaticHeight, + '\n cropper min ', state.internalMinWidth, state.internalMinHeight, + '\n cropper max ', state.internalMaxWidth, state.internalMaxHeight, + '\n ', + '\n prev cropper ', prevWidth, prevHeight, + '\n prev center ', prevCenterX, prevCenterY, + '\n ', + '\n cropper size ', cropperWidth, cropperHeight, + '\n cropper aspect ratio ', cropperWidth / cropperHeight, + '\n cropper center ', state.cropper.x1 + cropperWidth / 2, state.cropper.y1 + cropperHeight / 2, + '\n ', + ); + */ } export function checkWithinMaxSizeBounds(state: ImageCropperState, maintainSize = false): void { @@ -71,53 +164,53 @@ export function resize(event: Event | BasicEvent | HammerInput, moveStart: MoveS const moveY = getClientY(event) - moveStart.clientY; switch (moveStart.position) { case 'left': - state.cropper.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, state.cropper.x2 - state.cropperScaledMaxWidth), - state.cropper.x2 - state.cropperScaledMinWidth); + state.cropper.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, state.cropper.x2 - state.internalMaxWidth), + state.cropper.x2 - state.internalMinWidth); break; case 'topleft': - state.cropper.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, state.cropper.x2 - state.cropperScaledMaxWidth), - state.cropper.x2 - state.cropperScaledMinWidth); - state.cropper.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, state.cropper.y2 - state.cropperScaledMaxHeight), - state.cropper.y2 - state.cropperScaledMinHeight); + state.cropper.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, state.cropper.x2 - state.internalMaxWidth), + state.cropper.x2 - state.internalMinWidth); + state.cropper.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, state.cropper.y2 - state.internalMaxHeight), + state.cropper.y2 - state.internalMinHeight); break; case 'top': - state.cropper.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, state.cropper.y2 - state.cropperScaledMaxHeight), - state.cropper.y2 - state.cropperScaledMinHeight); + state.cropper.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, state.cropper.y2 - state.internalMaxHeight), + state.cropper.y2 - state.internalMinHeight); break; case 'topright': - state.cropper.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, state.cropper.x1 + state.cropperScaledMaxWidth), - state.cropper.x1 + state.cropperScaledMinWidth); - state.cropper.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, state.cropper.y2 - state.cropperScaledMaxHeight), - state.cropper.y2 - state.cropperScaledMinHeight); + state.cropper.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, state.cropper.x1 + state.internalMaxWidth), + state.cropper.x1 + state.internalMinWidth); + state.cropper.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, state.cropper.y2 - state.internalMaxHeight), + state.cropper.y2 - state.internalMinHeight); break; case 'right': - state.cropper.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, state.cropper.x1 + state.cropperScaledMaxWidth), - state.cropper.x1 + state.cropperScaledMinWidth); + state.cropper.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, state.cropper.x1 + state.internalMaxWidth), + state.cropper.x1 + state.internalMinWidth); break; case 'bottomright': - state.cropper.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, state.cropper.x1 + state.cropperScaledMaxWidth), - state.cropper.x1 + state.cropperScaledMinWidth); - state.cropper.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, state.cropper.y1 + state.cropperScaledMaxHeight), - state.cropper.y1 + state.cropperScaledMinHeight); + state.cropper.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, state.cropper.x1 + state.internalMaxWidth), + state.cropper.x1 + state.internalMinWidth); + state.cropper.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, state.cropper.y1 + state.internalMaxHeight), + state.cropper.y1 + state.internalMinHeight); break; case 'bottom': - state.cropper.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, state.cropper.y1 + state.cropperScaledMaxHeight), - state.cropper.y1 + state.cropperScaledMinHeight); + state.cropper.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, state.cropper.y1 + state.internalMaxHeight), + state.cropper.y1 + state.internalMinHeight); break; case 'bottomleft': - state.cropper.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, state.cropper.x2 - state.cropperScaledMaxWidth), - state.cropper.x2 - state.cropperScaledMinWidth); - state.cropper.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, state.cropper.y1 + state.cropperScaledMaxHeight), - state.cropper.y1 + state.cropperScaledMinHeight); + state.cropper.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, state.cropper.x2 - state.internalMaxWidth), + state.cropper.x2 - state.internalMinWidth); + state.cropper.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, state.cropper.y1 + state.internalMaxHeight), + state.cropper.y1 + state.internalMinHeight); break; case 'center': const scale = 'scale' in event ? event.scale : 1; const newWidth = Math.min( - Math.max(state.cropperScaledMinWidth, (Math.abs(moveStart.cropper.x2 - moveStart.cropper.x1)) * scale), - state.cropperScaledMaxWidth); + Math.max(state.internalMinWidth, (Math.abs(moveStart.cropper.x2 - moveStart.cropper.x1)) * scale), + state.internalMaxWidth); const newHeight = Math.min( - Math.max(state.cropperScaledMinHeight, (Math.abs(moveStart.cropper.y2 - moveStart.cropper.y1)) * scale), - state.cropperScaledMaxHeight); + Math.max(state.internalMinHeight, (Math.abs(moveStart.cropper.y2 - moveStart.cropper.y1)) * scale), + state.internalMaxHeight); state.cropper.x1 = moveStart.clientX - newWidth / 2; state.cropper.x2 = moveStart.clientX + newWidth / 2; state.cropper.y1 = moveStart.clientY - newHeight / 2; diff --git a/projects/ngx-image-cropper/src/lib/utils/cropper-size-bounds.utils.ts b/projects/ngx-image-cropper/src/lib/utils/cropper-size-bounds.utils.ts index a36f7bb..22d7e8f 100644 --- a/projects/ngx-image-cropper/src/lib/utils/cropper-size-bounds.utils.ts +++ b/projects/ngx-image-cropper/src/lib/utils/cropper-size-bounds.utils.ts @@ -1,49 +1,303 @@ +import { CropperPosition } from "../interfaces"; import { ImageCropperState } from "../state/image-cropper-state"; -export function setCropperScaledMinSize(state: ImageCropperState): void { - if (state.loadedImage?.transformed.size) { - setCropperScaledMinWidth(state); - setCropperScaledMinHeight(state); +/* +RULES FOR THESE FUNCTIONS +- They set the internal values for cropper static, min and max. +- They compute these values accoriding to some rules (mentioned below). +- There's a new minForAccessibility value in state, to prevent unclickable buttons when resizing (someone opened + an issue about this). +- Static sides are created by inputs AND when internal min > internal max. + * Weird, but it can happen. +- Static side leads to hidden resize squares and cursor type updated in visible ones. + * Still debating whether this is a good idea. Some sort of disabled indication through css styles might + be more communicative for users. + * This happens in image-cropper.ts and image-cropper.scss just writing the rule here so it's in one place. +- Biggest possible internal min, max and static values are bound by maxSize. +- Smallest internal min is bound by minForAccessibility BUT smallest internal max and static can be smaller. If + so, the side becomes static. + * Making the side static is hopefully more communicative for the user and resize is stopped + from being triggerd unnecessarily. +- If two static sides and mantain aspect ratio is applied, mantain aspect ratio is ignored. +- Always check static/min/max width and height together (as in min width and height, max width and height...). A + previous state could've changed the internal value of one of the sides. +- Always set internal statics after setting internal min and max sides, cos remember that if internal min > + internal max, the side becomes static. The rule might need to be applied or unapplied. Also check this when mantainAspectRatio is true. The same rule applied there too of course. +- Never update non-internal state values of cropper size bounds. We need them clean so we can use them again when + state changes. +*/ + +export function setInternalMinWidthAndHeight( + state: ImageCropperState +): void { + state.internalMinWidth = state.cropperMinWidth + ? Math.min( + state.maxSize.width, + Math.max( + state.cropperMinSizeForAccessibility, + (state.cropperMinWidth / state.loadedImage!.transformed.image.width) * state.maxSize.width + ) + ) + : state.cropperMinSizeForAccessibility; + state.internalMinHeight = state.cropperMinHeight + ? Math.min( + state.maxSize.height, + Math.max( + state.cropperMinSizeForAccessibility, + (state.cropperMinHeight / + state.loadedImage!.transformed.image.height) * state.maxSize.height + ) + ) + : state.cropperMinSizeForAccessibility; +} + +export function setInternalMaxWidthAndHeight(state: ImageCropperState): void { + state.internalMaxWidth = state.cropperMaxWidth + ? Math.min( + state.maxSize.width, + (state.cropperMaxWidth / state.loadedImage!.transformed.image.width) * state.maxSize.width + ) + : state.maxSize.width; + state.internalMaxHeight = state.cropperMaxHeight + ? Math.min( + state.maxSize.height, + (state.cropperMaxHeight / state.loadedImage!.transformed.image.height) * state.maxSize.height + ) + : state.maxSize.height; +} + +export function setInternalStaticWidthAndHeight(state: ImageCropperState): void { + if (state.cropperStaticWidth) { + state.internalStaticWidth = Math.min( + state.maxSize.width, + state.cropperStaticWidth + ); + } else if (state.internalMinWidth >= state.internalMaxWidth) { + state.internalStaticWidth = state.internalMaxWidth; } else { - state.cropperScaledMinWidth = 20; - state.cropperScaledMinHeight = 20; + state.internalStaticWidth = 0; + } + + if (state.cropperStaticHeight) { + state.internalStaticHeight = Math.min( + state.maxSize.height, + state.cropperStaticHeight + ); + } else if (state.internalMinHeight >= state.internalMaxHeight) { + state.internalStaticHeight = state.internalMaxHeight; + } else { + state.internalStaticHeight = 0; } } -export function setCropperScaledMinWidth(state: ImageCropperState): void { - state.cropperScaledMinWidth = state.cropperMinWidth > 0 - ? Math.max(20, (state.cropperMinWidth / state.loadedImage!.transformed.size.width) * state.maxSize.width) - : 20; +export function checkBoundsFollowAspectRatio(state: ImageCropperState): void { + // logging here cos this fn is always triggered when there are changes to cropper size bounds (min, max, static). + + /* + const prevStaticWidth = state.internalStaticWidth; // + const prevStaticHeight = state.internalStaticHeight; // + const prevMinWidth = state.internalMinWidth; // + const prevMinHeight = state.internalMinHeight; // + const prevMaxWidth = state.internalMaxWidth; // + const prevMaxHeight = state.internalMaxHeight; // + + if (!state.maintainAspectRatio || (state.internalStaticWidth && state.internalStaticHeight)) { // + console.log('\n setting cropper bounds', + '\n ' , + '\n state before', + '\n cropperStatic ', state.cropperStaticWidth, state.cropperStaticHeight, + '\n cropperMin ', state.cropperMinWidth, state.cropperMinHeight, + '\n cropperMax ', state.cropperMaxWidth, state.cropperMaxHeight, + '\n ' , + '\n local after', + '\n cropperStatic ', state.internalStaticWidth, state.internalStaticHeight, + '\n cropperMin ', state.internalMinWidth, state.internalMinHeight, + '\n cropperMax ', state.internalMaxWidth, state.internalMaxHeight, + '\n ' , + ); + } + */ + + if (!state.maintainAspectRatio) return; + if (state.internalStaticWidth && state.internalStaticHeight) return; + + state.internalStaticWidth || state.internalStaticHeight + ? followAspectRatioWhenOneStaticSide(state) + : followAspectRatioWhenNoStaticSide(state); + + /* + console.log('\n setting cropper bounds', + '\n ' , + '\n state beofre', + '\n cropperStatic ', state.cropperStaticWidth, state.cropperStaticHeight, + '\n cropperMin ', state.cropperMinWidth, state.cropperMinHeight, + '\n cropperMax ', state.cropperMaxWidth, state.cropperMaxHeight, + '\n ' , + '\n local after', + '\n cropperStatic ', prevStaticWidth, prevStaticHeight, + '\n cropperMin ', prevMinWidth, prevMinHeight, + '\n cropperMax ', prevMaxWidth, prevMaxHeight, + '\n ' , + '\n local after mantain aspect ratio', + '\n cropperStatic ', state.internalStaticWidth, state.internalStaticHeight, + '\n cropperMin ', state.internalMinWidth, state.internalMinHeight, + '\n cropperMax ', state.internalMaxWidth, state.internalMaxHeight, + '\n ' , + ); + */ } -export function setCropperScaledMinHeight(state: ImageCropperState): void { - if (state.maintainAspectRatio) { - state.cropperScaledMinHeight = Math.max(20, state.cropperScaledMinWidth / state.aspectRatio); - } else if (state.cropperMinHeight > 0) { - state.cropperScaledMinHeight = Math.max(20, (state.cropperMinHeight / state.loadedImage!.transformed.size.height) * state.maxSize.height); - } else { - state.cropperScaledMinHeight = 20; - } -} - -export function setCropperScaledMaxSize(state: ImageCropperState): void { - if (state.loadedImage?.transformed.size) { - const ratio = state.loadedImage.transformed.size.width / state.maxSize.width; - state.cropperScaledMaxWidth = state.cropperMaxWidth > 20 - ? state.cropperMaxWidth / ratio - : state.maxSize.width; - state.cropperScaledMaxHeight = state.cropperMaxHeight > 20 - ? state.cropperMaxHeight / ratio - : state.maxSize.height; - if (state.maintainAspectRatio) { - if (state.cropperScaledMaxWidth > state.cropperScaledMaxHeight * state.aspectRatio) { - state.cropperScaledMaxWidth = state.cropperScaledMaxHeight * state.aspectRatio; - } else if (state.cropperScaledMaxWidth < state.cropperScaledMaxHeight * state.aspectRatio) { - state.cropperScaledMaxHeight = state.cropperScaledMaxWidth / state.aspectRatio; - } +function followAspectRatioWhenOneStaticSide(state: ImageCropperState) { + if (state.internalStaticWidth) { + state.internalStaticHeight = state.internalStaticWidth / state.aspectRatio; + if (state.internalStaticHeight > state.maxSize.height) { + state.internalStaticHeight = state.maxSize.height; + state.internalStaticWidth = state.maxSize.height * state.aspectRatio; } + } else if (state.internalStaticHeight) { + state.internalStaticWidth = state.internalStaticHeight * state.aspectRatio; + if (state.internalStaticWidth > state.maxSize.width) { + state.internalStaticWidth = state.maxSize.width; + state.internalStaticHeight = state.maxSize.width / state.aspectRatio; + } + } +} + +function followAspectRatioWhenNoStaticSide(state: ImageCropperState) { + if (state.internalMinWidth <= state.internalMinHeight * state.aspectRatio) { + state.internalMinWidth = state.internalMinHeight * state.aspectRatio; } else { - state.cropperScaledMaxWidth = state.maxSize.width; - state.cropperScaledMaxHeight = state.maxSize.height; + state.internalMinHeight = state.internalMinWidth / state.aspectRatio; + } // res will always respect min values BUT could be bigger than max values (including maxSize) + + if (state.internalMaxWidth >= state.internalMaxHeight * state.aspectRatio) { + state.internalMaxWidth = state.internalMaxHeight * state.aspectRatio; + } else { + state.internalMaxHeight = state.internalMaxWidth / state.aspectRatio; + } // res will always respect max values BUT could be smaller than min values (including minForAccesiblity BUT only if the img is of a smaller dimension) + + if (state.internalMinWidth >= state.internalMaxWidth + || state.internalMinHeight >= state.internalMaxHeight + ) { + state.internalStaticWidth = state.internalMaxWidth; + state.internalStaticHeight = state.internalMaxHeight; + } // res will always respect max values BUT could be smaller than min values if smaller img. But this is okay as the cropper becomes static (no unclickable buttons problem). +} + +export function setAllInternalSizes( + state: ImageCropperState +): void { + setInternalMinWidthAndHeight(state); + setInternalMaxWidthAndHeight(state); + setInternalStaticWidthAndHeight(state); + checkBoundsFollowAspectRatio(state); +} + +// delete +function isOK(cropper: CropperPosition, state: ImageCropperState) { + const width = cropper.x2 - cropper.x1; + const height = cropper.y2 - cropper.y1; + + const roundWidth = round(width); + const roundHeight = round(height); + const roundMinWidth = round(state.internalMinWidth); + const roundMinHeight = round(state.internalMinHeight); + const roundMaxWidth = round(state.internalMaxWidth); + const roundMaxHeight = round(state.internalMaxHeight); + const roundStaticWidth = round(state.internalStaticWidth); + const roundStaticHeight = round(state.internalStaticHeight); + + if (roundStaticWidth) { + if (roundWidth !== roundStaticWidth) { + console.warn( + "\n isOk", + "\n cropperWidth ", + roundWidth, + width, + "\n cropperStaticWidth ", + roundMaxWidth, + state.internalMaxWidth + ); + } + } else if (roundWidth < roundMinWidth || roundWidth > roundMaxWidth) { + console.warn( + "\n isOk", + "\n cropperWidth ", + roundWidth, + width, + "\n cropperMinWidth ", + roundMinWidth, + state.internalMinWidth, + "\n cropperMaxWidth ", + roundMaxWidth, + state.internalMaxWidth + ); + } + + if (roundStaticHeight) { + if (roundHeight !== roundStaticHeight) { + console.warn( + "\n isOk", + "\n cropperHeight ", + roundHeight, + height, + "\n cropperStaticHeight ", + roundMaxHeight, + state.internalMaxHeight + ); + } + } else if (roundHeight < roundMinHeight || roundHeight > roundMaxHeight) { + console.warn( + "\n isOk", + "\n cropperHeight ", + roundHeight, + height, + "\n cropperMinHeight ", + roundMinHeight, + state.internalMinHeight, + "\n cropperMaxHeight ", + roundMaxHeight, + state.internalMaxHeight + ); + } + + if (cropper.x1 < 0) { + console.log("\n isOk", "\n cropper.x1 < 0", cropper.x1, "<", "0"); + } + + if (cropper.y1 < 0) { + console.log("\n isOk", "\n cropper.y1 < 0", cropper.y1, "<", "0"); + } + + if (cropper.x2 > state.maxSize.width) { + console.log( + "\n isOk", + "\n cropper.x2 > maxWidth", + cropper.x2, + ">", + state.maxSize.width + ); + } + + if (cropper.y2 > state.maxSize.height) { + console.log( + "\n isOk", + "\n cropper.y2 > maxHeight", + cropper.y2, + ">", + state.maxSize.height + ); } } + +// delete +function round(x: number) { + // return Math.round(x * 1000) / 1000 + return x; +} + +/* TODO + - internalStatic sizes is not scaled but the min and max are. was that on purpose? + - the lib is not fully covering the relationship between transform, min, max, static cropper sizes and + onlyScaleDown. The user could go into the "scaled down zone" and crop "rejects the move". +*/ From 7bf39eb4ffa82343501a22f8d664295af2136c2a Mon Sep 17 00:00:00 2001 From: Alba Date: Sat, 27 Jul 2024 19:43:38 +0200 Subject: [PATCH 6/8] Fix: Prevent code from running unless there's a change in img element size on resize window --- .../src/lib/component/image-cropper.component.ts | 15 ++++++++++----- .../src/lib/utils/cropper-position.utils.ts | 14 ++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts b/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts index 6fc4c97..44d1477 100644 --- a/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts +++ b/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts @@ -245,15 +245,20 @@ export class ImageCropperComponent implements OnChanges, OnInit { if (!this.state.loadedImage) { return; } - if (this.hidden) { + if (this.state.hidden) { this.resizedWhileHidden = true; } else { const oldMaxSize = {...this.state.maxSize}; this.setMaxSize(); - cropperPosition.resizeCropperAccordingToNewMaxSize(this.state, oldMaxSize); - cropperSizeBounds.setAllInternalSizes(this.state); - this.settingsUpdated.emit(this.state.getDeepCopyOfSettings()); - this.cd.markForCheck(); + if (oldMaxSize.width !== this.state.maxSize.width) { + cropperPosition.resizeCropperAccordingToNewMaxSize( + this.state, + this.state.maxSize.width / oldMaxSize.width + ); + cropperSizeBounds.setAllInternalSizes(this.state); + this.settingsUpdated.emit(this.state.getDeepCopyOfSettings()); + this.cd.markForCheck(); + } } } diff --git a/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts b/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts index 116a757..0c9edf6 100644 --- a/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts +++ b/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts @@ -1,4 +1,4 @@ -import { CropperPosition, MoveStart, BasicEvent, Dimensions } from '../interfaces'; +import { CropperPosition, MoveStart, BasicEvent } from '../interfaces'; import { HammerInput } from './hammer.utils'; import { ImageCropperState } from '../state/image-cropper-state'; @@ -320,13 +320,11 @@ export function aspectRatioIsCorrect(state: ImageCropperState): boolean { return currentCropAspectRatio === state.aspectRatio; } -export function resizeCropperAccordingToNewMaxSize(state: ImageCropperState, oldMaxSize: Dimensions): void { - if (oldMaxSize.width !== state.maxSize.width || oldMaxSize.height !== state.maxSize.height) { - state.cropper.x1 = state.cropper.x1 * state.maxSize.width / oldMaxSize.width; - state.cropper.x2 = state.cropper.x2 * state.maxSize.width / oldMaxSize.width; - state.cropper.y1 = state.cropper.y1 * state.maxSize.height / oldMaxSize.height; - state.cropper.y2 = state.cropper.y2 * state.maxSize.height / oldMaxSize.height; - } +export function resizeCropperAccordingToNewMaxSize(state: ImageCropperState, ratio: number): void { + state.cropper.x1 *= ratio; + state.cropper.x2 *= ratio; + state.cropper.y1 *= ratio; + state.cropper.y2 *= ratio; } export function getClientX(event: Event | BasicEvent | TouchEvent | HammerInput): number { From cad2d0d11dda20d1b6c8061f5fe0327c7c89b3e6 Mon Sep 17 00:00:00 2001 From: Alba Date: Sun, 28 Jul 2024 00:06:18 +0200 Subject: [PATCH 7/8] Improve how cropper resizes - How it moves when resizing when maintain aspect ratio is true - Can resize when one internal static side is applied --- .../lib/component/image-cropper.component.ts | 12 +- .../src/lib/utils/cropper-position.utils.ts | 266 +++++++++--------- 2 files changed, 134 insertions(+), 144 deletions(-) diff --git a/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts b/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts index 44d1477..5aacf74 100644 --- a/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts +++ b/projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts @@ -384,12 +384,9 @@ export class ImageCropperComponent implements OnChanges, OnInit { } if (this.moveStart!.type === MoveTypes.Move) { cropperPosition.move(event, this.moveStart!, this.state.cropper); - cropperPosition.checkWithinMaxSizeBounds(this.state, true); + cropperPosition.checkWithinMaxSizeBounds(this.state); } else if (this.moveStart!.type === MoveTypes.Resize) { - if (!this.state.cropperStaticWidth && !this.state.cropperStaticHeight) { - cropperPosition.resize(event, this.moveStart!, this.state); - cropperPosition.checkWithinMaxSizeBounds(this.state, false); - } + cropperPosition.resize(event, this.moveStart!, this.state); } else if (this.moveStart!.type === MoveTypes.Drag) { const diffX = cropperPosition.getClientX(event) - this.moveStart!.clientX; const diffY = cropperPosition.getClientY(event) - this.moveStart!.clientY; @@ -409,10 +406,7 @@ export class ImageCropperComponent implements OnChanges, OnInit { event.preventDefault(); } if (this.moveStart!.type === MoveTypes.Pinch) { - if (!this.state.cropperStaticWidth && !this.state.cropperStaticHeight) { - cropperPosition.resize(event, this.moveStart!, this.state); - cropperPosition.checkWithinMaxSizeBounds(this.state, false); - } + cropperPosition.resize(event, this.moveStart!, this.state); } this.cd.markForCheck(); } diff --git a/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts b/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts index 0c9edf6..a30adf1 100644 --- a/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts +++ b/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts @@ -18,6 +18,17 @@ RULES FOR THESE FUNCTIONS there's a new imageSource). * The logic in checkSizeAndPosition works because of the logic in cropper-size-bounds.utils.ts. The internal bounding sizes already consider aspect ratio and static sides. That's why, for example, if it's a new cropper, we don't need to check mantain aspect ratio. internalMaxSize and internalStaticSide already consider it. + - resize: + - The cropper can be resized as long as at least one of the internal static side values is false (0). Remeber + both internal static sides will be true if one static side is set by parent and mantain aspect ratio is true. + - When mantain aspect ratio is true and resizing from one side, it's perpendicular sides will change too. So, + for example, if you move the left side, top and bottom also change to keep the aspect ratio. Before it would resize from a corner. + - When resizing from a corner, both x and y pointer movements are accounted for. There are still some "blind + spots", I'll try and send a video, but it's good enough for now. I've noticed google photos has the same blind spots :) Before it only resized with x pointer movements. + * The logic in resize works because of the logic in cropper-size-bounds.utils.ts. The internal min, max and + static cropper bounding sizes account for maxSize and mantain aspect ratio. It also deals with weird values for min and max that a user could have sent in accidentally, making the cropper impossible to resize, but turning the side/s to static. + * As resizing the cropper will always be within maxSize, the maintainSize value in checkWithinMaxSizeBounds + is no longer necessary. */ @@ -110,7 +121,7 @@ export function checkSizeAndPosition(state: ImageCropperState): void { state.cropper.y1 = prevCenterY - cropperHeight / 2; state.cropper.y2 = state.cropper.y1 + cropperHeight; - if (!newCropper) { checkWithinMaxSizeBounds(state, true) }; + if (!newCropper) { checkWithinMaxSizeBounds(state) }; /* console.log('\n checkWithinBounds', @@ -130,21 +141,21 @@ export function checkSizeAndPosition(state: ImageCropperState): void { */ } -export function checkWithinMaxSizeBounds(state: ImageCropperState, maintainSize = false): void { +export function checkWithinMaxSizeBounds(state: ImageCropperState): void { if (state.cropper.x1 < 0) { - state.cropper.x2 -= maintainSize ? state.cropper.x1 : 0; + state.cropper.x2 -= state.cropper.x1; state.cropper.x1 = 0; } if (state.cropper.y1 < 0) { - state.cropper.y2 -= maintainSize ? state.cropper.y1 : 0; + state.cropper.y2 -= state.cropper.y1; state.cropper.y1 = 0; } if (state.cropper.x2 > state.maxSize.width) { - state.cropper.x1 -= maintainSize ? (state.cropper.x2 - state.maxSize.width) : 0; + state.cropper.x1 -= (state.cropper.x2 - state.maxSize.width); state.cropper.x2 = state.maxSize.width; } if (state.cropper.y2 > state.maxSize.height) { - state.cropper.y1 -= maintainSize ? (state.cropper.y2 - state.maxSize.height) : 0; + state.cropper.y1 -= (state.cropper.y2 - state.maxSize.height); state.cropper.y2 = state.maxSize.height; } } @@ -160,158 +171,143 @@ export function move(event: Event | BasicEvent, moveStart: MoveStart, cropper: C } export function resize(event: Event | BasicEvent | HammerInput, moveStart: MoveStart, state: ImageCropperState): void { + + console.log('HERE', state.internalStaticWidth, state.internalStaticHeight) + //if (state.internalStaticWidth && state.internalStaticHeight) console.log('no resize, both static') // + if (state.internalStaticWidth && state.internalStaticHeight) return; + const moveX = getClientX(event) - moveStart.clientX; const moveY = getClientY(event) - moveStart.clientY; - switch (moveStart.position) { - case 'left': - state.cropper.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, state.cropper.x2 - state.internalMaxWidth), - state.cropper.x2 - state.internalMinWidth); - break; - case 'topleft': - state.cropper.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, state.cropper.x2 - state.internalMaxWidth), - state.cropper.x2 - state.internalMinWidth); - state.cropper.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, state.cropper.y2 - state.internalMaxHeight), - state.cropper.y2 - state.internalMinHeight); - break; - case 'top': - state.cropper.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, state.cropper.y2 - state.internalMaxHeight), - state.cropper.y2 - state.internalMinHeight); - break; - case 'topright': - state.cropper.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, state.cropper.x1 + state.internalMaxWidth), - state.cropper.x1 + state.internalMinWidth); - state.cropper.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, state.cropper.y2 - state.internalMaxHeight), - state.cropper.y2 - state.internalMinHeight); - break; - case 'right': - state.cropper.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, state.cropper.x1 + state.internalMaxWidth), - state.cropper.x1 + state.internalMinWidth); - break; - case 'bottomright': - state.cropper.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, state.cropper.x1 + state.internalMaxWidth), - state.cropper.x1 + state.internalMinWidth); - state.cropper.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, state.cropper.y1 + state.internalMaxHeight), - state.cropper.y1 + state.internalMinHeight); - break; - case 'bottom': - state.cropper.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, state.cropper.y1 + state.internalMaxHeight), - state.cropper.y1 + state.internalMinHeight); - break; - case 'bottomleft': - state.cropper.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, state.cropper.x2 - state.internalMaxWidth), - state.cropper.x2 - state.internalMinWidth); - state.cropper.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, state.cropper.y1 + state.internalMaxHeight), - state.cropper.y1 + state.internalMinHeight); - break; - case 'center': - const scale = 'scale' in event ? event.scale : 1; + + if (!state.internalStaticWidth) { + if (moveStart.position!.endsWith('left')) { + state.cropper.x1 = Math.min( + state.cropper.x2 - state.internalMinWidth, + Math.max( + 0, + moveStart.cropper.x1 + moveX, + state.cropper.x2 - state.internalMaxWidth + ) + ); + } else if (moveStart.position!.endsWith('right')) { + state.cropper.x2 = Math.max( + state.cropper.x1 + state.internalMinWidth, + Math.min( + state.maxSize.width, + moveStart.cropper.x2 + moveX, + state.cropper.x1 + state.internalMaxWidth + ) + ); + } else if (moveStart.position! === "center") { const newWidth = Math.min( - Math.max(state.internalMinWidth, (Math.abs(moveStart.cropper.x2 - moveStart.cropper.x1)) * scale), - state.internalMaxWidth); - const newHeight = Math.min( - Math.max(state.internalMinHeight, (Math.abs(moveStart.cropper.y2 - moveStart.cropper.y1)) * scale), - state.internalMaxHeight); + state.internalMaxWidth, + Math.max( + state.internalMinWidth, + (moveStart.cropper.x2 - moveStart.cropper.x1) * ('scale' in event ? event.scale : 1) // always positive + ) + ); state.cropper.x1 = moveStart.clientX - newWidth / 2; state.cropper.x2 = moveStart.clientX + newWidth / 2; - state.cropper.y1 = moveStart.clientY - newHeight / 2; - state.cropper.y2 = moveStart.clientY + newHeight / 2; if (state.cropper.x1 < 0) { state.cropper.x2 -= state.cropper.x1; state.cropper.x1 = 0; } else if (state.cropper.x2 > state.maxSize.width) { - state.cropper.x1 -= (state.cropper.x2 - state.maxSize.width); + state.cropper.x1 -= state.cropper.x2 - state.maxSize.width; state.cropper.x2 = state.maxSize.width; - } + } + } + } + + if (!state.internalStaticHeight) { + if (moveStart.position!.startsWith('top')) { + state.cropper.y1 = Math.min( + state.cropper.y2 - state.internalMinHeight, + Math.max( + 0, + moveStart.cropper.y1 + moveY, + state.cropper.y2 - state.internalMaxHeight + ) + ); + } else if (moveStart.position!.startsWith('bottom')) { + state.cropper.y2 = Math.max( + state.cropper.y1 + state.internalMinHeight, + Math.min( + state.maxSize.height, + moveStart.cropper.y2 + moveY, + state.cropper.y1 + state.internalMaxHeight + ) + ); + } else if (moveStart.position! === "center") { + const newHeight = Math.min( + state.internalMaxHeight, + Math.max( + state.internalMinHeight, + (moveStart.cropper.y2 - moveStart.cropper.y1) * ('scale' in event ? event.scale : 1) // always positive + ) + ); + state.cropper.y1 = moveStart.clientY - newHeight / 2; + state.cropper.y2 = moveStart.clientY + newHeight / 2; if (state.cropper.y1 < 0) { state.cropper.y2 -= state.cropper.y1; state.cropper.y1 = 0; } else if (state.cropper.y2 > state.maxSize.height) { - state.cropper.y1 -= (state.cropper.y2 - state.maxSize.height); + state.cropper.y1 -= state.cropper.y2 - state.maxSize.height; state.cropper.y2 = state.maxSize.height; } - break; + } } - if (state.maintainAspectRatio) { - checkAspectRatio(moveStart.position!, state); + if (state.maintainAspectRatio && moveStart.position! !== 'center') { // center already keeps aspect ratio + checkAspectRatioOnResize(moveStart.position!, state); } } -export function checkAspectRatio(position: string, state: ImageCropperState): void { - let overflowX = 0; - let overflowY = 0; +function checkAspectRatioOnResize(position: string, state: ImageCropperState): void { + const newWidth = (state.cropper.y2 - state.cropper.y1) * state.aspectRatio; + const newHeight = (state.cropper.x2 - state.cropper.x1) / state.aspectRatio; - switch (position) { - case 'top': - state.cropper.x2 = state.cropper.x1 + (state.cropper.y2 - state.cropper.y1) * state.aspectRatio; - overflowX = Math.max(state.cropper.x2 - state.maxSize.width, 0); - overflowY = Math.max(0 - state.cropper.y1, 0); - if (overflowX > 0 || overflowY > 0) { - state.cropper.x2 -= (overflowY * state.aspectRatio) > overflowX ? (overflowY * state.aspectRatio) : overflowX; - state.cropper.y1 += (overflowY * state.aspectRatio) > overflowX ? overflowY : overflowX / state.aspectRatio; - } - break; - case 'bottom': - state.cropper.x2 = state.cropper.x1 + (state.cropper.y2 - state.cropper.y1) * state.aspectRatio; - overflowX = Math.max(state.cropper.x2 - state.maxSize.width, 0); - overflowY = Math.max(state.cropper.y2 - state.maxSize.height, 0); - if (overflowX > 0 || overflowY > 0) { - state.cropper.x2 -= (overflowY * state.aspectRatio) > overflowX ? (overflowY * state.aspectRatio) : overflowX; - state.cropper.y2 -= (overflowY * state.aspectRatio) > overflowX ? overflowY : (overflowX / state.aspectRatio); - } - break; - case 'topleft': - state.cropper.y1 = state.cropper.y2 - (state.cropper.x2 - state.cropper.x1) / state.aspectRatio; - overflowX = Math.max(0 - state.cropper.x1, 0); - overflowY = Math.max(0 - state.cropper.y1, 0); - if (overflowX > 0 || overflowY > 0) { - state.cropper.x1 += (overflowY * state.aspectRatio) > overflowX ? (overflowY * state.aspectRatio) : overflowX; - state.cropper.y1 += (overflowY * state.aspectRatio) > overflowX ? overflowY : overflowX / state.aspectRatio; - } - break; - case 'topright': - state.cropper.y1 = state.cropper.y2 - (state.cropper.x2 - state.cropper.x1) / state.aspectRatio; - overflowX = Math.max(state.cropper.x2 - state.maxSize.width, 0); - overflowY = Math.max(0 - state.cropper.y1, 0); - if (overflowX > 0 || overflowY > 0) { - state.cropper.x2 -= (overflowY * state.aspectRatio) > overflowX ? (overflowY * state.aspectRatio) : overflowX; - state.cropper.y1 += (overflowY * state.aspectRatio) > overflowX ? overflowY : overflowX / state.aspectRatio; - } - break; - case 'right': - case 'bottomright': - state.cropper.y2 = state.cropper.y1 + (state.cropper.x2 - state.cropper.x1) / state.aspectRatio; - overflowX = Math.max(state.cropper.x2 - state.maxSize.width, 0); - overflowY = Math.max(state.cropper.y2 - state.maxSize.height, 0); - if (overflowX > 0 || overflowY > 0) { - state.cropper.x2 -= (overflowY * state.aspectRatio) > overflowX ? (overflowY * state.aspectRatio) : overflowX; - state.cropper.y2 -= (overflowY * state.aspectRatio) > overflowX ? overflowY : overflowX / state.aspectRatio; - } - break; - case 'left': - case 'bottomleft': - state.cropper.y2 = state.cropper.y1 + (state.cropper.x2 - state.cropper.x1) / state.aspectRatio; - overflowX = Math.max(0 - state.cropper.x1, 0); - overflowY = Math.max(state.cropper.y2 - state.maxSize.height, 0); - if (overflowX > 0 || overflowY > 0) { - state.cropper.x1 += (overflowY * state.aspectRatio) > overflowX ? (overflowY * state.aspectRatio) : overflowX; - state.cropper.y2 -= (overflowY * state.aspectRatio) > overflowX ? overflowY : overflowX / state.aspectRatio; - } - break; - case 'center': - state.cropper.x2 = state.cropper.x1 + (state.cropper.y2 - state.cropper.y1) * state.aspectRatio; - state.cropper.y2 = state.cropper.y1 + (state.cropper.x2 - state.cropper.x1) / state.aspectRatio; - const overflowX1 = Math.max(0 - state.cropper.x1, 0); - const overflowX2 = Math.max(state.cropper.x2 - state.maxSize.width, 0); - const overflowY1 = Math.max(state.cropper.y2 - state.maxSize.height, 0); - const overflowY2 = Math.max(0 - state.cropper.y1, 0); - if (overflowX1 > 0 || overflowX2 > 0 || overflowY1 > 0 || overflowY2 > 0) { - state.cropper.x1 += (overflowY1 * state.aspectRatio) > overflowX1 ? (overflowY1 * state.aspectRatio) : overflowX1; - state.cropper.x2 -= (overflowY2 * state.aspectRatio) > overflowX2 ? (overflowY2 * state.aspectRatio) : overflowX2; - state.cropper.y1 += (overflowY2 * state.aspectRatio) > overflowX2 ? overflowY2 : overflowX2 / state.aspectRatio; - state.cropper.y2 -= (overflowY1 * state.aspectRatio) > overflowX1 ? overflowY1 : overflowX1 / state.aspectRatio; + if (position === 'left' || position === 'right') { + let diff = (state.cropper.y2 - (state.cropper.y1 + newHeight)) / 2; + state.cropper.y1 += diff; + state.cropper.y2 -= diff; + if (state.cropper.y1 < 0 || state.cropper.y2 > state.maxSize.height) { + diff = state.cropper.y1 < state.maxSize.height - state.cropper.y2 + ? -state.cropper.y1 + : state.cropper.y2 - state.maxSize.height; + state.cropper.y1 += diff; + state.cropper.y2 -= diff; + position === 'left' + ? state.cropper.x1 += diff * 2 * state.aspectRatio + : state.cropper.x2 -= diff * 2 * state.aspectRatio; + } + return; + } + + if (position === 'top' || position === 'bottom') { + let diff = (state.cropper.x2 - (state.cropper.x1 + newWidth)) / 2; + state.cropper.x1 += diff; + state.cropper.x2 -= diff; + if (state.cropper.x1 < 0 || state.cropper.x2 > state.maxSize.width) { + diff = state.cropper.x1 < state.maxSize.width - state.cropper.x2 + ? state.cropper.x1 + : state.maxSize.width - state.cropper.x2; + state.cropper.x1 -= diff; + state.cropper.x2 += diff; + position === 'top' + ? state.cropper.y1 -= diff * 2 / state.aspectRatio + : state.cropper.y2 += diff * 2 / state.aspectRatio; } - break; + return; + } + + if (state.aspectRatio >= (state.cropper.x2 - state.cropper.x1) / (state.cropper.y2 - state.cropper.y1)) { + position.startsWith('top') + ? state.cropper.y1 = state.cropper.y2 - newHeight + : state.cropper.y2 = state.cropper.y1 + newHeight; + } else { + position.endsWith('left') + ? state.cropper.x1 = state.cropper.x2 - newWidth + : state.cropper.x2 = state.cropper.x1 + newWidth; } } From f7b0e7f6696b730518bd21e60bfdf1eb280a7bfa Mon Sep 17 00:00:00 2001 From: Alba Date: Sun, 28 Jul 2024 00:37:23 +0200 Subject: [PATCH 8/8] Delete no longer used things --- .../src/lib/utils/cropper-position.utils.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts b/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts index a30adf1..1719e1c 100644 --- a/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts +++ b/projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts @@ -172,7 +172,6 @@ export function move(event: Event | BasicEvent, moveStart: MoveStart, cropper: C export function resize(event: Event | BasicEvent | HammerInput, moveStart: MoveStart, state: ImageCropperState): void { - console.log('HERE', state.internalStaticWidth, state.internalStaticHeight) //if (state.internalStaticWidth && state.internalStaticHeight) console.log('no resize, both static') // if (state.internalStaticWidth && state.internalStaticHeight) return; @@ -311,11 +310,6 @@ function checkAspectRatioOnResize(position: string, state: ImageCropperState): v } } -export function aspectRatioIsCorrect(state: ImageCropperState): boolean { - const currentCropAspectRatio = (state.cropper.x2 - state.cropper.x1) / (state.cropper.y2 - state.cropper.y1); - return currentCropAspectRatio === state.aspectRatio; -} - export function resizeCropperAccordingToNewMaxSize(state: ImageCropperState, ratio: number): void { state.cropper.x1 *= ratio; state.cropper.x2 *= ratio;