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.filter && 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 if (this.ctx != undefined) { 180 this.ctx.clearRect(0, 0, this.displayWidth, this.displayHeight); 181 } 182 } 183 184 private drawImage() { 185 this.ctx.save(); 186 this.clearCanvas(); 187 188 let x = this.displayWidth / 2; 189 let y = this.displayHeight / 2; 190 this.ctx.translate(this.isFlipHorizontal ? 2 * x : 0, this.isFlipVertically ? 2 * y : 0); 191 192 let tX = this.isFlipHorizontal ? -1 : 1; 193 let tY = this.isFlipVertically ? -1 : 1; 194 this.ctx.scale(tX, tY); 195 196 this.ctx.translate(x, y); 197 this.ctx.rotate(MathUtils.formulaAngle(this.rotationAngle * tX * tY + this.sliderAngle)); 198 this.ctx.translate(-x, -y); 199 200 let image = this.cropShow.getImageRect(); 201 MathUtils.roundRect(image); 202 this.ctx.drawImage(this.input.pixelMap, image.left, image.top, image.getWidth(), image.getHeight()); 203 this.ctx.restore(); 204 } 205 206 private drawCrop() { 207 let crop = this.cropShow.getCropRect(); 208 MathUtils.roundRect(crop); 209 let display = new RectF(); 210 display.set(0, 0, this.displayWidth, this.displayHeight); 211 DrawingUtils.drawMask(this.ctx, display, crop); 212 DrawingUtils.drawSplitLine(this.ctx, crop, this.splitFraction); 213 DrawingUtils.drawRect(this.ctx, crop); 214 DrawingUtils.drawCropButton(this.ctx, crop); 215 } 216 217 onMirrorChange() { 218 Log.debug(this.TAG, 'onMirrorChange'); 219 if (this.isWaitingRefresh) { 220 this.clearDelayRefresh(); 221 this.cropShow.enlargeCropArea(); 222 } else { 223 if (MathUtils.isOddRotation(this.rotationAngle)) { 224 this.isFlipVertically = !this.isFlipVertically; 225 } else { 226 this.isFlipHorizontal = !this.isFlipHorizontal; 227 } 228 this.cropShow.setFlip(this.isFlipHorizontal, this.isFlipVertically); 229 } 230 this.refresh(); 231 } 232 233 onRotationAngleChange() { 234 Log.debug(this.TAG, 'onRotationAngleChange'); 235 if (this.isWaitingRefresh) { 236 this.clearDelayRefresh(); 237 this.cropShow.enlargeCropArea(); 238 } else { 239 this.rotationAngle = (this.rotationAngle - CropAngle.ONE_QUARTER_CIRCLE_ANGLE) % CropAngle.CIRCLE_ANGLE; 240 this.cropShow.syncRotationAngle(this.rotationAngle); 241 } 242 this.refresh(); 243 } 244 245 onSliderAngleChange(angle: number) { 246 Log.debug(this.TAG, `onSliderAngleChange: angle[${angle}]`); 247 if (this.isWaitingRefresh) { 248 this.clearDelayRefresh(); 249 this.cropShow.enlargeCropArea(); 250 this.refresh(); 251 } 252 this.sliderAngle = angle; 253 this.cropShow.syncHorizontalAngle(this.sliderAngle); 254 this.refresh(); 255 } 256 257 onFixedRatioChange(ratio: CropRatioType) { 258 Log.debug(this.TAG, `onFixedRatioChange: ratio[${ratio}]`); 259 if (this.isWaitingRefresh) { 260 this.clearDelayRefresh(); 261 this.cropShow.enlargeCropArea(); 262 } 263 this.cropRatio = ratio; 264 this.cropShow.setRatio(ratio); 265 this.endImageDrag(); 266 this.refresh(); 267 } 268 269 onTouchStart(x: number, y: number) { 270 if (this.state != CropTouchState.NONE) { 271 Log.debug(this.TAG, `onTouchStart: touch state is not none!`); 272 return; 273 } 274 275 if (this.isWaitingRefresh) { 276 this.clearDelayRefresh(); 277 } 278 279 Log.debug(this.TAG, `onTouchStart: [x: ${x}, y: ${y}]`); 280 if (this.cropShow.isCropRectTouch(x, y)) { 281 this.state = CropTouchState.CROP_MOVE; 282 } else { 283 this.state = CropTouchState.IMAGE_DRAG; 284 } 285 this.touchPoint.set(x, y); 286 } 287 288 onTouchMove(x: number, y: number) { 289 Log.debug(this.TAG, `onTouchMove: [state: ${this.state}] [x: ${x}, y: ${y}]`); 290 let offsetX = x - this.touchPoint.x; 291 let offsetY = y - this.touchPoint.y; 292 if (this.state == CropTouchState.CROP_MOVE) { 293 this.cropShow.moveCropRect(offsetX, offsetY); 294 } else if (this.state == CropTouchState.IMAGE_DRAG) { 295 this.onImageDrag(offsetX, offsetY); 296 } else { 297 return; 298 } 299 this.refresh(); 300 this.touchPoint.set(x, y); 301 } 302 303 onTouchEnd() { 304 Log.debug(this.TAG, `onTouchEnd: [state: ${this.state}]`); 305 if (this.state == CropTouchState.CROP_MOVE) { 306 this.cropShow.endCropRectMove(); 307 } else if (this.state == CropTouchState.IMAGE_DRAG) { 308 this.endImageDrag(); 309 this.refresh(); 310 } else { 311 return; 312 } 313 this.state = CropTouchState.NONE; 314 if (this.isWaitingRefresh) { 315 this.clearDelayRefresh(); 316 } 317 this.delayRefresh(this.timeout); 318 } 319 320 private onImageDrag(offsetX: number, offsetY: number) { 321 let tX = this.isFlipHorizontal ? -1 : 1; 322 let tY = this.isFlipVertically ? -1 : 1; 323 let alpha = MathUtils.formulaAngle(this.rotationAngle * tX * tY + this.sliderAngle); 324 let x = Math.cos(alpha) * offsetX * tX + Math.sin(alpha) * offsetY * tY; 325 let y = -Math.sin(alpha) * offsetX * tX + Math.cos(alpha) * offsetY * tY; 326 let image = this.cropShow.getImageRect(); 327 image.move(x, y); 328 this.cropShow.setImageRect(image); 329 } 330 331 private endImageDrag() { 332 let crop = this.cropShow.getCropRect(); 333 let points = MathUtils.rectToPoints(crop); 334 let tX = this.isFlipHorizontal ? -1 : 1; 335 let tY = this.isFlipVertically ? -1 : 1; 336 let angle = -(this.rotationAngle * tX * tY + this.sliderAngle); 337 let displayCenter = new Point(this.displayWidth / 2, this.displayHeight / 2); 338 let rotated = MathUtils.rotatePoints(points, angle, displayCenter); 339 340 let flipImage = this.cropShow.getCurrentFlipImage(); 341 let offsets = MathUtils.fixImageMove(rotated, flipImage); 342 let image = this.cropShow.getImageRect(); 343 image.move(offsets[0] * tX, offsets[1] * tY); 344 this.cropShow.setImageRect(image); 345 } 346 347 onPinchStart(x: number, y: number, scale: number) { 348 Log.debug(this.TAG, `onPinchStart: event[x: ${x}, y: ${y}]`); 349 this.state = CropTouchState.IMAGE_SCALE; 350 this.pinchPoint.set(x, y); 351 this.scale = scale; 352 } 353 354 onPinchUpdate(scale: number) { 355 Log.debug(this.TAG, `onPinchUpdate: scale[${scale}]`); 356 if (this.state == CropTouchState.IMAGE_SCALE) { 357 let factor = scale / this.scale; 358 if (!this.cropShow.couldEnlargeImage()) { 359 factor = factor > PhotoEditCrop.BASE_SCALE_VALUE ? PhotoEditCrop.BASE_SCALE_VALUE : factor; 360 } 361 let image = this.cropShow.getImageRect(); 362 MathUtils.scaleRectBasedOnPoint(image, this.pinchPoint, factor); 363 this.cropShow.setImageRect(image); 364 this.refresh(); 365 this.scale *= factor; 366 } 367 } 368 369 onPinchEnd() { 370 Log.debug(this.TAG, 'onPinchEnd'); 371 let crop = this.cropShow.getCropRect(); 372 let points = MathUtils.rectToPoints(crop); 373 let tX = this.isFlipHorizontal ? -1 : 1; 374 let tY = this.isFlipVertically ? -1 : 1; 375 let angle = -(this.rotationAngle * tX * tY + this.sliderAngle); 376 let displayCenter = new Point(this.displayWidth / 2, this.displayHeight / 2); 377 let rotated = MathUtils.rotatePoints(points, angle, displayCenter); 378 379 let flipImage = this.cropShow.getCurrentFlipImage(); 380 let origin = new Point(crop.getCenterX(), crop.getCenterY()); 381 let centerOffsetX = origin.x - flipImage.getCenterX(); 382 let centerOffsetY = origin.y - flipImage.getCenterY(); 383 flipImage.move(centerOffsetX, centerOffsetY); 384 let scale = MathUtils.findSuitableScale(rotated, flipImage, origin); 385 flipImage.move(-centerOffsetX, -centerOffsetY); 386 387 MathUtils.scaleRectBasedOnPoint(flipImage, origin, scale); 388 let offsets = MathUtils.fixImageMove(rotated, flipImage); 389 390 let image = this.cropShow.getImageRect(); 391 MathUtils.scaleRectBasedOnPoint(image, origin, scale); 392 image.move(offsets[0] * tX, offsets[1] * tY); 393 this.cropShow.setImageRect(image); 394 this.refresh(); 395 this.state = CropTouchState.NONE; 396 this.delayRefresh(this.timeout); 397 this.scale = PhotoEditCrop.BASE_SCALE_VALUE; 398 } 399 400 couldReset(): boolean { 401 let crop = this.cropShow.getCropRect(); 402 MathUtils.roundRect(crop); 403 let image = this.cropShow.getImageRect(); 404 MathUtils.roundRect(image); 405 if (this.isFlipHorizontal != false || this.isFlipVertically != false 406 || this.rotationAngle != 0 || this.sliderAngle != 0 407 || this.cropRatio != CropRatioType.RATIO_TYPE_FREE 408 || !MathUtils.areRectSame(crop, image)) { 409 return true; 410 } 411 return false; 412 } 413 414 reset() { 415 Log.debug(this.TAG, 'reset'); 416 let limit = this.calcNewLimit(); 417 this.cropShow.init(limit, this.imageRatio); 418 this.initialize(this.input); 419 this.refresh(); 420 } 421}