1/* 2 * Copyright (c) 2022 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import { Constants, Log } from '@ohos/common'; 17import { PhotoEditBase } from '../base/PhotoEditBase'; 18import { PhotoEditMode } from '../base/PhotoEditType'; 19import { Point } from '../base/Point'; 20import { RectF } from '../base/Rect'; 21import { CropAngle, CropRatioType, CropTouchState } from './CropType'; 22import { ImageFilterCrop } from './ImageFilterCrop'; 23import { CropShow } from './CropShow'; 24import { MathUtils } from './MathUtils'; 25import { DrawingUtils } from './DrawingUtils'; 26import type { ImageFilterBase } from '../base/ImageFilterBase'; 27import type { PixelMapWrapper } from '../base/PixelMapWrapper'; 28 29const TAG: string = 'editor_PhotoEditCrop'; 30 31export class PhotoEditCrop extends PhotoEditBase { 32 private static readonly BASE_SCALE_VALUE: number = 1.0; 33 private static readonly DEFAULT_MAX_SCALE_VALUE: number = 3.0; 34 private static readonly DEFAULT_IMAGE_RATIO: number = 1.0; 35 private static readonly DEFAULT_MIN_SIDE_LENGTH: number = 32; 36 private static readonly DEFAULT_MARGIN_LENGTH: number = 20; 37 private static readonly DEFAULT_TIMEOUT_MILLISECOND_1000: number = 1000; 38 private static readonly DEFAULT_SPLIT_FRACTION: number = 3; 39 private filter: ImageFilterCrop = undefined; 40 private input: PixelMapWrapper = undefined; 41 private isFlipHorizontal: boolean = false; 42 private isFlipVertically: boolean = false; 43 private rotationAngle: number = 0; 44 private sliderAngle: number = 0; 45 private cropRatio: CropRatioType = CropRatioType.RATIO_TYPE_FREE; 46 private cropShow: CropShow = undefined; 47 private isCropShowInitialized: boolean = false; 48 private ctx: CanvasRenderingContext2D = undefined; 49 private displayWidth: number = 0; 50 private displayHeight: number = 0; 51 private marginW: number = PhotoEditCrop.DEFAULT_MARGIN_LENGTH; 52 private marginH: number = PhotoEditCrop.DEFAULT_MARGIN_LENGTH; 53 private imageRatio: number = PhotoEditCrop.DEFAULT_IMAGE_RATIO; 54 private scale: number = PhotoEditCrop.BASE_SCALE_VALUE; 55 private timeoutId: number = 0; 56 private timeout: number = PhotoEditCrop.DEFAULT_TIMEOUT_MILLISECOND_1000; 57 private isWaitingRefresh: boolean = false; 58 private touchPoint: Point = undefined; 59 private pinchPoint: Point = undefined; 60 private state: CropTouchState = CropTouchState.NONE; 61 private splitFraction: number = PhotoEditCrop.DEFAULT_SPLIT_FRACTION; 62 private canvasReady: boolean = false; 63 64 constructor() { 65 super(PhotoEditMode.EDIT_MODE_CROP); 66 this.cropShow = new CropShow(); 67 this.touchPoint = new Point(0, 0); 68 this.pinchPoint = new Point(0, 0); 69 } 70 71 entry(pixelMap: PixelMapWrapper): void { 72 Log.info(TAG, `entry pixelMap: ${JSON.stringify(pixelMap)}`); 73 this.input = pixelMap; 74 this.filter = new ImageFilterCrop(); 75 this.initialize(); 76 if (this.isCropShowInitialized) { 77 let limit = this.calcNewLimit(); 78 this.cropShow.init(limit, this.imageRatio); 79 } 80 this.refresh(); 81 } 82 83 exit(): ImageFilterBase { 84 Log.info(TAG, 'exit'); 85 this.saveFinalOperation(); 86 this.isCropShowInitialized = false; 87 this.input = undefined; 88 if (this.couldReset()) { 89 this.clear(); 90 } else { 91 this.filter = undefined; 92 } 93 return this.filter; 94 } 95 96 setCanvasContext(context: CanvasRenderingContext2D): void { 97 Log.info(TAG, 'setCanvasContext'); 98 this.ctx = context; 99 this.refresh(); 100 } 101 102 setCanvasReady(state: boolean): void { 103 this.canvasReady = state; 104 } 105 106 setCanvasSize(width: number, height: number): void { 107 Log.info(TAG, `setCanvasSize: width[${width}], height[${height}]`); 108 this.displayWidth = width; 109 this.displayHeight = height; 110 let limit = this.calcNewLimit(); 111 if (this.isCropShowInitialized) { 112 this.cropShow.syncLimitRect(limit); 113 this.determineMaxScaleFactor(); 114 } else { 115 this.cropShow.init(limit, this.imageRatio); 116 this.isCropShowInitialized = true; 117 } 118 this.refresh(); 119 } 120 121 clearCanvas(): void { 122 if (this.ctx != undefined) { 123 this.ctx.clearRect(0, 0, this.displayWidth, this.displayHeight); 124 } 125 } 126 127 onMirrorChange(): void { 128 Log.debug(TAG, 'onMirrorChange'); 129 if (this.isWaitingRefresh) { 130 this.clearDelayRefresh(); 131 this.cropShow.enlargeCropArea(); 132 } else { 133 if (MathUtils.isOddRotation(this.rotationAngle)) { 134 this.isFlipVertically = !this.isFlipVertically; 135 } else { 136 this.isFlipHorizontal = !this.isFlipHorizontal; 137 } 138 this.cropShow.setFlip(this.isFlipHorizontal, this.isFlipVertically); 139 } 140 this.refresh(); 141 } 142 143 onRotationAngleChange(): void { 144 Log.debug(TAG, 'onRotationAngleChange'); 145 if (this.isWaitingRefresh) { 146 this.clearDelayRefresh(); 147 this.cropShow.enlargeCropArea(); 148 } else { 149 this.rotationAngle = (this.rotationAngle - CropAngle.ONE_QUARTER_CIRCLE_ANGLE) % CropAngle.CIRCLE_ANGLE; 150 this.cropShow.syncRotationAngle(this.rotationAngle); 151 } 152 this.refresh(); 153 } 154 155 onSliderAngleChange(angle: number): void { 156 Log.debug(TAG, `onSliderAngleChange: angle[${angle}]`); 157 if (this.isWaitingRefresh) { 158 this.clearDelayRefresh(); 159 this.cropShow.enlargeCropArea(); 160 this.refresh(); 161 } 162 this.sliderAngle = angle; 163 this.cropShow.syncHorizontalAngle(this.sliderAngle); 164 this.refresh(); 165 } 166 167 onFixedRatioChange(ratio: CropRatioType): void { 168 Log.debug(TAG, `onFixedRatioChange: ratio[${ratio}]`); 169 if (this.isWaitingRefresh) { 170 this.clearDelayRefresh(); 171 this.cropShow.enlargeCropArea(); 172 } 173 this.cropRatio = ratio; 174 this.cropShow.setRatio(ratio); 175 this.endImageDrag(); 176 this.refresh(); 177 } 178 179 onTouchStart(x: number, y: number): void { 180 if (this.state !== CropTouchState.NONE) { 181 Log.debug(TAG, 'onTouchStart: touch state is not none!'); 182 return; 183 } 184 185 if (this.isWaitingRefresh) { 186 this.clearDelayRefresh(); 187 } 188 189 Log.debug(TAG, `onTouchStart: [x: ${x}, y: ${y}]`); 190 if (this.cropShow.isCropRectTouch(x, y)) { 191 this.state = CropTouchState.CROP_MOVE; 192 } else { 193 this.state = CropTouchState.IMAGE_DRAG; 194 } 195 this.touchPoint.set(x, y); 196 } 197 198 onTouchMove(x: number, y: number): void { 199 Log.debug(TAG, `onTouchMove: [state: ${this.state}] [x: ${x}, y: ${y}]`); 200 let offsetX = x - this.touchPoint.x; 201 let offsetY = y - this.touchPoint.y; 202 if (this.state === CropTouchState.CROP_MOVE) { 203 this.cropShow.moveCropRect(offsetX, offsetY); 204 } else if (this.state === CropTouchState.IMAGE_DRAG) { 205 this.onImageDrag(offsetX, offsetY); 206 } else { 207 return; 208 } 209 this.refresh(); 210 this.touchPoint.set(x, y); 211 } 212 213 onTouchEnd(): void { 214 Log.debug(TAG, `onTouchEnd: [state: ${this.state}]`); 215 if (this.state === CropTouchState.CROP_MOVE) { 216 this.cropShow.endCropRectMove(); 217 } else if (this.state === CropTouchState.IMAGE_DRAG) { 218 this.endImageDrag(); 219 this.refresh(); 220 } else { 221 return; 222 } 223 this.state = CropTouchState.NONE; 224 if (this.isWaitingRefresh) { 225 this.clearDelayRefresh(); 226 } 227 this.delayRefresh(this.timeout); 228 } 229 230 onPinchStart(x: number, y: number, scale: number): void { 231 Log.debug(TAG, `onPinchStart: event[x: ${x}, y: ${y}]`); 232 this.state = CropTouchState.IMAGE_SCALE; 233 this.pinchPoint.set(x, y); 234 this.scale = scale; 235 } 236 237 onPinchUpdate(scale: number): void { 238 Log.debug(TAG, `onPinchUpdate: scale[${scale}]`); 239 if (this.state === CropTouchState.IMAGE_SCALE) { 240 let factor = scale / this.scale; 241 if (!this.cropShow.couldEnlargeImage()) { 242 factor = factor > PhotoEditCrop.BASE_SCALE_VALUE ? PhotoEditCrop.BASE_SCALE_VALUE : factor; 243 } 244 let image = this.cropShow.getImageRect(); 245 MathUtils.scaleRectBasedOnPoint(image, this.pinchPoint, factor); 246 this.cropShow.setImageRect(image); 247 this.refresh(); 248 this.scale *= factor; 249 } 250 } 251 252 onPinchEnd(): void { 253 Log.debug(TAG, 'onPinchEnd'); 254 let crop = this.cropShow.getCropRect(); 255 let points = MathUtils.rectToPoints(crop); 256 let tX = this.isFlipHorizontal ? -1 : 1; 257 let tY = this.isFlipVertically ? -1 : 1; 258 let angle = -(this.rotationAngle * tX * tY + this.sliderAngle); 259 let displayCenter = new Point(this.displayWidth / Constants.NUMBER_2, this.displayHeight / Constants.NUMBER_2); 260 let rotated = MathUtils.rotatePoints(points, angle, displayCenter); 261 262 let flipImage = this.cropShow.getCurrentFlipImage(); 263 let origin = new Point(crop.getCenterX(), crop.getCenterY()); 264 let centerOffsetX = origin.x - flipImage.getCenterX(); 265 let centerOffsetY = origin.y - flipImage.getCenterY(); 266 flipImage.move(centerOffsetX, centerOffsetY); 267 let scale = MathUtils.findSuitableScale(rotated, flipImage, origin); 268 flipImage.move(-centerOffsetX, -centerOffsetY); 269 270 MathUtils.scaleRectBasedOnPoint(flipImage, origin, scale); 271 let offsets = MathUtils.fixImageMove(rotated, flipImage); 272 273 let image = this.cropShow.getImageRect(); 274 MathUtils.scaleRectBasedOnPoint(image, origin, scale); 275 image.move(offsets[0] * tX, offsets[1] * tY); 276 this.cropShow.setImageRect(image); 277 this.refresh(); 278 this.state = CropTouchState.NONE; 279 this.delayRefresh(this.timeout); 280 this.scale = PhotoEditCrop.BASE_SCALE_VALUE; 281 } 282 283 couldReset(): boolean { 284 let crop = this.cropShow.getCropRect(); 285 MathUtils.roundOutRect(crop); 286 let image = this.cropShow.getImageRect(); 287 MathUtils.roundOutRect(image); 288 if ( 289 this.isFlipHorizontal !== false || 290 this.isFlipVertically !== false || 291 this.rotationAngle !== 0 || this.sliderAngle !== 0 || 292 this.cropRatio !== CropRatioType.RATIO_TYPE_FREE || 293 !MathUtils.areRectsSame(crop, image) 294 ) { 295 return true; 296 } 297 return false; 298 } 299 300 reset(): void { 301 Log.debug(TAG, 'reset'); 302 let limit = this.calcNewLimit(); 303 this.cropShow.init(limit, this.imageRatio); 304 this.initialize(); 305 this.refresh(); 306 } 307 308 private initialize(): void { 309 this.imageRatio = this.input.width / this.input.height; 310 this.determineMaxScaleFactor(); 311 this.clear(); 312 } 313 314 private calcNewLimit(): RectF { 315 let limit = new RectF(); 316 limit.set(this.marginW, this.marginH, this.displayWidth - this.marginW, this.displayHeight - this.marginH); 317 return limit; 318 } 319 320 private determineMaxScaleFactor(): void { 321 if (this.input == null) { 322 return; 323 } 324 let scaleFactorW = this.input.width / px2vp(PhotoEditCrop.DEFAULT_MIN_SIDE_LENGTH); 325 let scaleFactorH = this.input.height / px2vp(PhotoEditCrop.DEFAULT_MIN_SIDE_LENGTH); 326 this.cropShow.setMaxScaleFactor(scaleFactorW, scaleFactorH); 327 } 328 329 private saveFinalOperation(): void { 330 let crop = this.cropShow.getCropRect(); 331 let image = this.cropShow.getImageRect(); 332 crop.move(-image.left, -image.top); 333 MathUtils.normalizeRect(crop, image.getWidth(), image.getHeight()); 334 this.filter.setCropRect(crop); 335 this.filter.setRotationAngle(this.rotationAngle); 336 this.filter.setHorizontalAngle(this.sliderAngle); 337 this.filter.setFlipHorizontal(this.isFlipHorizontal); 338 this.filter.setFlipVertically(this.isFlipVertically); 339 } 340 341 private clear(): void { 342 this.cropRatio = CropRatioType.RATIO_TYPE_FREE; 343 this.isFlipHorizontal = false; 344 this.isFlipVertically = false; 345 this.rotationAngle = 0; 346 this.sliderAngle = 0; 347 } 348 349 private refresh(): void { 350 if (this.ctx !== undefined && this.input !== undefined && this.canvasReady) { 351 this.drawImage(); 352 this.drawCrop(); 353 } 354 } 355 356 private delayRefresh(delay: number): void { 357 this.isWaitingRefresh = true; 358 this.timeoutId = setTimeout(() => { 359 this.cropShow.enlargeCropArea(); 360 this.refresh(); 361 this.isWaitingRefresh = false; 362 }, delay); 363 } 364 365 private clearDelayRefresh(): void { 366 clearTimeout(this.timeoutId); 367 this.isWaitingRefresh = false; 368 } 369 370 private drawImage(): void { 371 this.ctx.save(); 372 this.clearCanvas(); 373 374 let x = this.displayWidth / Constants.NUMBER_2; 375 let y = this.displayHeight / Constants.NUMBER_2; 376 this.ctx.translate(this.isFlipHorizontal ? Constants.NUMBER_2 * x : 0, this.isFlipVertically ? Constants.NUMBER_2 * y : 0); 377 378 let tX = this.isFlipHorizontal ? -1 : 1; 379 let tY = this.isFlipVertically ? -1 : 1; 380 this.ctx.scale(tX, tY); 381 382 this.ctx.translate(x, y); 383 this.ctx.rotate(MathUtils.formulaAngle(this.rotationAngle * tX * tY + this.sliderAngle)); 384 this.ctx.translate(-x, -y); 385 386 let image = this.cropShow.getImageRect(); 387 MathUtils.roundOutRect(image); 388 this.ctx.drawImage(this.input.pixelMap, image.left, image.top, image.getWidth(), image.getHeight()); 389 this.ctx.restore(); 390 } 391 392 private drawCrop(): void { 393 let crop = this.cropShow.getCropRect(); 394 MathUtils.roundOutRect(crop); 395 let display = new RectF(); 396 display.set(0, 0, this.displayWidth, this.displayHeight); 397 DrawingUtils.drawMask(this.ctx, display, crop); 398 DrawingUtils.drawSplitLine(this.ctx, crop, this.splitFraction); 399 DrawingUtils.drawRect(this.ctx, crop); 400 DrawingUtils.drawCropButton(this.ctx, crop); 401 } 402 403 private onImageDrag(offsetX: number, offsetY: number): void { 404 let tX = this.isFlipHorizontal ? -1 : 1; 405 let tY = this.isFlipVertically ? -1 : 1; 406 let alpha = MathUtils.formulaAngle(this.rotationAngle * tX * tY + this.sliderAngle); 407 let x = Math.cos(alpha) * offsetX * tX + Math.sin(alpha) * offsetY * tY; 408 let y = -Math.sin(alpha) * offsetX * tX + Math.cos(alpha) * offsetY * tY; 409 let image = this.cropShow.getImageRect(); 410 image.move(x, y); 411 this.cropShow.setImageRect(image); 412 } 413 414 private endImageDrag(): void { 415 let crop = this.cropShow.getCropRect(); 416 let points = MathUtils.rectToPoints(crop); 417 let tX = this.isFlipHorizontal ? -1 : 1; 418 let tY = this.isFlipVertically ? -1 : 1; 419 let angle = -(this.rotationAngle * tX * tY + this.sliderAngle); 420 let displayCenter = new Point(this.displayWidth / Constants.NUMBER_2, this.displayHeight / Constants.NUMBER_2); 421 let rotated = MathUtils.rotatePoints(points, angle, displayCenter); 422 423 let flipImage = this.cropShow.getCurrentFlipImage(); 424 let offsets = MathUtils.fixImageMove(rotated, flipImage); 425 let image = this.cropShow.getImageRect(); 426 image.move(offsets[0] * tX, offsets[1] * tY); 427 this.cropShow.setImageRect(image); 428 } 429}