• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}