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