• 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.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}