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 (this.isFlipHorizontal && this.isFlipVertically && MathUtils.areRectsSame(crop, image)) { 289 return false; 290 } 291 if ( 292 this.isFlipHorizontal !== false || 293 this.isFlipVertically !== false || 294 this.rotationAngle !== 0 || this.sliderAngle !== 0 || 295 this.cropRatio !== CropRatioType.RATIO_TYPE_FREE || 296 !MathUtils.areRectsSame(crop, image) 297 ) { 298 return true; 299 } 300 return false; 301 } 302 303 reset(): void { 304 Log.debug(TAG, 'reset'); 305 let limit = this.calcNewLimit(); 306 this.cropShow.init(limit, this.imageRatio); 307 this.initialize(); 308 this.refresh(); 309 } 310 311 private initialize(): void { 312 this.imageRatio = this.input.width / this.input.height; 313 this.determineMaxScaleFactor(); 314 this.clear(); 315 } 316 317 private calcNewLimit(): RectF { 318 let limit = new RectF(); 319 limit.set(this.marginW, this.marginH, this.displayWidth - this.marginW, this.displayHeight - this.marginH); 320 return limit; 321 } 322 323 private determineMaxScaleFactor(): void { 324 if (this.input == null) { 325 return; 326 } 327 let scaleFactorW = this.input.width / px2vp(PhotoEditCrop.DEFAULT_MIN_SIDE_LENGTH); 328 let scaleFactorH = this.input.height / px2vp(PhotoEditCrop.DEFAULT_MIN_SIDE_LENGTH); 329 this.cropShow.setMaxScaleFactor(scaleFactorW, scaleFactorH); 330 } 331 332 private saveFinalOperation(): void { 333 let crop = this.cropShow.getCropRect(); 334 let image = this.cropShow.getImageRect(); 335 crop.move(-image.left, -image.top); 336 MathUtils.normalizeRect(crop, image.getWidth(), image.getHeight()); 337 this.filter.setCropRect(crop); 338 this.filter.setRotationAngle(this.rotationAngle); 339 this.filter.setHorizontalAngle(this.sliderAngle); 340 this.filter.setFlipHorizontal(this.isFlipHorizontal); 341 this.filter.setFlipVertically(this.isFlipVertically); 342 } 343 344 private clear(): void { 345 this.cropRatio = CropRatioType.RATIO_TYPE_FREE; 346 this.isFlipHorizontal = false; 347 this.isFlipVertically = false; 348 this.rotationAngle = 0; 349 this.sliderAngle = 0; 350 } 351 352 private refresh(): void { 353 if (this.ctx !== undefined && this.input !== undefined && this.canvasReady) { 354 this.drawImage(); 355 this.drawCrop(); 356 } 357 } 358 359 private delayRefresh(delay: number): void { 360 this.isWaitingRefresh = true; 361 this.timeoutId = setTimeout(() => { 362 this.cropShow.enlargeCropArea(); 363 this.refresh(); 364 this.isWaitingRefresh = false; 365 }, delay); 366 } 367 368 private clearDelayRefresh(): void { 369 clearTimeout(this.timeoutId); 370 this.isWaitingRefresh = false; 371 } 372 373 private drawImage(): void { 374 this.ctx.save(); 375 this.clearCanvas(); 376 377 let x = this.displayWidth / Constants.NUMBER_2; 378 let y = this.displayHeight / Constants.NUMBER_2; 379 this.ctx.translate(this.isFlipHorizontal ? Constants.NUMBER_2 * x : 0, this.isFlipVertically ? Constants.NUMBER_2 * y : 0); 380 381 let tX = this.isFlipHorizontal ? -1 : 1; 382 let tY = this.isFlipVertically ? -1 : 1; 383 this.ctx.scale(tX, tY); 384 385 this.ctx.translate(x, y); 386 this.ctx.rotate(MathUtils.formulaAngle(this.rotationAngle * tX * tY + this.sliderAngle)); 387 this.ctx.translate(-x, -y); 388 389 let image = this.cropShow.getImageRect(); 390 MathUtils.roundOutRect(image); 391 this.ctx.drawImage(this.input.pixelMap, image.left, image.top, image.getWidth(), image.getHeight()); 392 this.ctx.restore(); 393 } 394 395 private drawCrop(): void { 396 let crop = this.cropShow.getCropRect(); 397 MathUtils.roundOutRect(crop); 398 let display = new RectF(); 399 display.set(0, 0, this.displayWidth, this.displayHeight); 400 DrawingUtils.drawMask(this.ctx, display, crop); 401 DrawingUtils.drawSplitLine(this.ctx, crop, this.splitFraction); 402 DrawingUtils.drawRect(this.ctx, crop); 403 DrawingUtils.drawCropButton(this.ctx, crop); 404 } 405 406 private onImageDrag(offsetX: number, offsetY: number): void { 407 let tX = this.isFlipHorizontal ? -1 : 1; 408 let tY = this.isFlipVertically ? -1 : 1; 409 let alpha = MathUtils.formulaAngle(this.rotationAngle * tX * tY + this.sliderAngle); 410 let x = Math.cos(alpha) * offsetX * tX + Math.sin(alpha) * offsetY * tY; 411 let y = -Math.sin(alpha) * offsetX * tX + Math.cos(alpha) * offsetY * tY; 412 let image = this.cropShow.getImageRect(); 413 image.move(x, y); 414 this.cropShow.setImageRect(image); 415 } 416 417 private endImageDrag(): void { 418 let crop = this.cropShow.getCropRect(); 419 let points = MathUtils.rectToPoints(crop); 420 let tX = this.isFlipHorizontal ? -1 : 1; 421 let tY = this.isFlipVertically ? -1 : 1; 422 let angle = -(this.rotationAngle * tX * tY + this.sliderAngle); 423 let displayCenter = new Point(this.displayWidth / Constants.NUMBER_2, this.displayHeight / Constants.NUMBER_2); 424 let rotated = MathUtils.rotatePoints(points, angle, displayCenter); 425 426 let flipImage = this.cropShow.getCurrentFlipImage(); 427 let offsets = MathUtils.fixImageMove(rotated, flipImage); 428 let image = this.cropShow.getImageRect(); 429 image.move(offsets[0] * tX, offsets[1] * tY); 430 this.cropShow.setImageRect(image); 431 } 432}