<repo_name>jayt08/cropper<file_sep>/src/manager/crop-image.ts
import {Crop} from "../CropManager";

function cropImage(ctx: CanvasRenderingContext2D, crop: Crop) {
    const horizontalMargin = 24;
    const topMargin = 152;

    let canvas: HTMLCanvasElement = document.createElement('canvas');
    const lCtx = canvas.getContext('2d')!;

    canvas.width = crop.width;
    canvas.height = crop.height;

    const imageData = ctx.getImageData(
        Math.round(crop.x + horizontalMargin),
        Math.round(crop.y + topMargin),
        crop.width,
        crop.height,
    );

    lCtx.putImageData(imageData, 0, 0);

    const base64 = canvas.toDataURL('image/png');

    return base64;
}

export default cropImage;
<file_sep>/src/manager/crop-move.ts
import {Crop, DragItemType, Pos2d} from "../CropManager";

function cropMove(
    type: DragItemType,
    cursor: Pos2d,
    crop: Crop,
    area: HTMLDivElement,
    aspectRatio: number | false,
    minSize: { width: number, height: number },
): Crop {
    const minHeight = minSize.height;
    const minWidth = typeof aspectRatio !== "number" ? minSize.width : minHeight * aspectRatio;

    const rect = area.getBoundingClientRect();

    const rawX = cursor.x - rect.left;
    const rawY = cursor.y - rect.top;

    let x = crop.x;
    let y = crop.y;
    let width = crop.width;
    let height = crop.height;

    if (type === 'lt') {
        if (typeof aspectRatio !== 'number') {
            x = rawX < 0 ? 0 : rawX;
            y = rawY < 0 ? 0 : rawY;

            height = crop.height - (y - crop.y);
            width = crop.width - (x - crop.x);
        } else {
            y = rawY < 0 ? 0 : rawY;

            height = crop.height - (y - crop.y);
            width = height * aspectRatio;

            x = crop.x + (crop.width - width)
        }

        if (width < minWidth) {
            x = crop.x;
            width = crop.width;
        }
        if (height < minHeight) {
            y = crop.y;
            height = crop.height;
        }
    } else if (type === 'rt') {
        if (typeof aspectRatio !== 'number') {
            y = rawY < 0 ? 0 : rawY;

            width = rawX - crop.x;
            height = crop.height - (y - crop.y);
        } else {
            y = rawY < 0 ? 0 : rawY;

            height = crop.height - (y - crop.y);
            width = height * aspectRatio;
        }

        if (width < minWidth) {
            width = crop.width;
        }
        if (crop.x + width > rect.width) {
            width = crop.width;
        }
        if (height < minHeight) {
            y = crop.y;
            height = crop.height;
        }
    } else if (type === 'lb') {
        if (typeof aspectRatio !== 'number') {
            x = rawX < 0 ? 0 : rawX;

            width = crop.width - (x - crop.x);
            height = rawY - crop.y;
        } else {
            height = rawY - crop.y;

            // TODO duplicate lines
            if (height < minHeight) {
                height = crop.height;
            }
            if (crop.y + height > rect.height) {
                height = crop.height;
            }

            width = height * aspectRatio;

            x = crop.x + (crop.width - width);
        }

        if (width < minWidth) {
            x = crop.x;
            width = crop.width;
        }
        if (height < minHeight) {
            height = crop.height;
        }
        if (crop.y + height > rect.height) {
            height = crop.height;
        }
    } else if (type === 'rb') {
        if (typeof aspectRatio !== 'number') {
            width = rawX - crop.x;
            height = rawY - crop.y;
        } else {
            height = rawY - crop.y;

            // TODO duplicate lines
            if (height < minHeight) {
                height = crop.height;
            }
            if (crop.y + height > rect.height) {
                height = crop.height;
            }

            width = height * aspectRatio;
        }

        if (width < minWidth) {
            width = crop.width;
        }
        if (crop.x + width > rect.width) {
            width = crop.width;
        }
        if (height < minHeight) {
            height = crop.height;
        }
        if (crop.y + height > rect.height) {
            height = crop.height;
        }
    }

    return {
        width,
        height,
        x,
        y,
    };
}

export default cropMove;<file_sep>/src/manager/image-zoom.ts
import {Crop} from "../CropManager";

function cropperZoomTo(img: HTMLImageElement, imageCrop: Crop, crop: Crop, prevZoom: number, zoom: number) {
    const prevWidth = img.width * prevZoom;
    const prevHeight = img.height * prevZoom;
    const width = img.width * zoom;
    const height = img.height * zoom;

    const diffW = width - prevWidth;
    const diffH = height - prevHeight;

    const cropCenterX = (crop.x + crop.width / 2) - imageCrop.x;
    const xPercent = cropCenterX / prevWidth;
    const cropCenterY = (crop.y + crop.height / 2) - imageCrop.y;
    const yPercent = cropCenterY / prevHeight;

    const x = Math.round(imageCrop.x - (diffW * xPercent));
    const y = Math.round(imageCrop.y - (diffH * yPercent));

    return {
        x, y,
    };
}

export default cropperZoomTo;
<file_sep>/src/CropManager.ts
import {Store} from "@muzikanto/observable";
import cropMove from "./manager/crop-move";
import cropperZoomTo from "./manager/image-zoom";
import imageMove from "./manager/image-move";
import cropperImageScale from "./manager/image-scale";
import imageMoveToAngle from "./manager/image-move-angle";
import cropImage from "./manager/crop-image";

export interface Pos2d {
    x: number;
    y: number;
}

export interface Crop extends Pos2d {
    width: number;
    height: number;
}

export interface CropManagerState {
    crop: Crop;
    imageCrop: Crop;
    zoom: number;
    angle: number;
    flipX: boolean;
    flipY: boolean;
    aspectRatio: number | false;
    minSize: { width: number; height: number; };

    initialZoom: number;
    initialChanged: boolean;
    changed: boolean;
    lastChanged: Date | null;
    loadedImage: boolean;

    filterBrightness: number;
    filterSaturate: number;
    filterBlur: number;
    filterContrast: number;
    filterGrayScale: number;
}

export type DragItemType = 'lt' | 'rt' | 'lb' | 'rb' | 'image';

export type DraggedData = {
    type: DragItemType;
    start: { x: number, y: number; };
}

class CropManager {
    public canvas: HTMLCanvasElement;
    public ctx: CanvasRenderingContext2D;
    public image: HTMLImageElement | null = null;
    public area: HTMLDivElement;

    public dragged: DraggedData | null = null;
    public store: Store<CropManagerState>;
    public defaultState: CropManagerState;
    protected fillStyle: string;

    constructor(store: Store<CropManagerState>, canvas: HTMLCanvasElement, area: HTMLDivElement, options: { backgroundFillStyle?: string; }) {
        this.canvas = canvas;
        this.area = area;
        this.store = store;
        this.defaultState = store.get();
        this.fillStyle = options.backgroundFillStyle || 'rgba(0,0,0,0.6)';

        this.ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
    }

    protected changeState = (nextState: Partial<CropManagerState>) => {
        const state = this.store.get();

        if (!state.initialChanged) {
            this.store.set({...state, ...nextState, initialChanged: true, lastChanged: new Date()});
        } else {
            this.store.set({...state, ...nextState, changed: true, lastChanged: new Date()});
        }

        this.drawImage();
    };
    public refreshState = () => {
        this.store.set(this.defaultState);
        this.drawImage();
    };

    public loadImage = (src: string) => {
        this.store.set({...this.store.get(), loadedImage: false});

        const img = new Image();

        img.setAttribute('crossOrigin', 'Anonymous');

        img.src = src;
        img.onload = () => {
            const defaultConfig = {
                ...this.store.get(),
                ...this.getDefaultConfig(this.store.get().aspectRatio),
                loadedImage: true,
            };

            this.changeState(defaultConfig);

            this.defaultState = defaultConfig;
        };

        this.image = img;
    };

    protected getDefaultConfig = (aspectRatio: number | false) => {
        const rect = this.area.getBoundingClientRect();
        const img = this.image!;

        let zoom = typeof aspectRatio === 'number' ?
            aspectRatio > 1 ?
                (rect.height * aspectRatio) / img.width
                : rect.height / img.height
            : rect.height / img.height;
        let cropWidth = typeof aspectRatio === 'number' ? rect.height * aspectRatio : img.width * zoom;
        let cropHeight = typeof aspectRatio === 'number' ? rect.height : img.height * zoom;

        const cropX = (rect.width / 2) - cropWidth / 2;
        const cropY = 0;

        const imageWidth = img.width;
        const imageHeight = img.height;
        const imageX = cropX - (imageWidth * zoom - cropWidth) / 2;
        const imageY = cropY - (imageHeight * zoom - cropHeight) / 2;

        return {
            crop: {
                x: cropX,
                y: cropY,
                width: cropWidth,
                height: cropHeight,
            },
            imageCrop: {
                x: imageX,
                y: imageY,
                width: imageWidth,
                height: imageHeight,
            },
            zoom,
            initialZoom: zoom,
            aspectRatio,
        };
    };

    protected drawImage() {
        const state = this.store.get();
        const image = this.image!;
        const crop = state.crop;
        // const imageCrop = state.imageCrop;
        const ctx = this.ctx;

        const horizontalMargin = 24;
        const topMargin = 152;

        const zoom = state.zoom;
        const angle = state.angle;
        const rad = angle * Math.PI / 180;
        const flipX = state.flipX ? -1 : 1;
        const flipY = state.flipY ? -1 : 1;

        const x = state.imageCrop.x + 24;
        const y = state.imageCrop.y + 152;
        const w = Math.round(image.width * zoom);
        const h = Math.round(image.height * zoom);

        const cropLeft = Math.ceil(crop.x + horizontalMargin);
        const cropRight = Math.ceil(cropLeft + crop.width);
        const cropTop = crop.y + topMargin;
        const cropBottom = cropTop + crop.height;

        // const translateImageX = (imageCrop.x + horizontalMargin + (imageCrop.width * zoom) / 2);
        // const translateImageY = (imageCrop.y + topMargin + (imageCrop.height * zoom) / 2);
        const translateX = Math.round(crop.x + horizontalMargin + (crop.width) / 2);
        const translateY = Math.round(crop.y + topMargin + (crop.height) / 2);

        // clear canvas
        ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        ctx.save();

       ctx.filter = [
           `brightness(${state.filterBrightness}%)`,
           `saturate(${state.filterSaturate}%)`,
           `blur(${state.filterBlur}px)`,
           `contrast(${state.filterContrast})`,
           `grayscale(${state.filterGrayScale})`,
       ].join(' ');

        // set center of canvas
        ctx.translate(translateX, translateY);

        ctx.rotate(rad);
        ctx.scale(flipX, flipY);

        // draw image
        ctx.drawImage(image,
            (x - translateX) * flipX,
            (y - translateY) * flipY,
            w * flipX,
            h * flipY,
        );

        ctx.restore();

        // darken background
        ctx.fillStyle = this.fillStyle;

        ctx.fillRect(
            0,
            0,
            cropLeft,
            this.canvas.height,
        );
        ctx.fillRect(
            cropRight,
            0,
            this.canvas.width,
            this.canvas.height,
        );
        ctx.fillRect(
            cropLeft,
            0,
            Math.ceil(crop.width),
            cropTop,
        );
        ctx.fillRect(
            cropLeft,
            cropBottom,
            Math.ceil(crop.width),
            this.canvas.height,
        );
    }

    public move = (cursor: { x: number, y: number }) => {
        if (this.dragged) {
            if (['lt', 'rt', 'lb', 'rb'].indexOf(this.dragged.type) > -1) {
                this.dragCropperMove(cursor)
            } else {
                this.dragImageMove(cursor);
            }
        }

        return null;
    };

    public dragCropperStart = (type: Exclude<DragItemType, 'image'>, start: { x: number, y: number }) => {
        this.dragged = {type, start};
    };
    public dragCropperMove = (target: { x: number, y: number }) => {
        if (this.dragged) {
            const state = this.store.get();

            const {x, y, width, height} = this.moveCropToPos(
                this.dragged.type,
                target,
                state.crop,
                this.area,
                state.aspectRatio,
                state.minSize,
            );

            //     const {
            //         zoom: nextZoom,
            //         cropImage: nextCropImage,
            //     } = this.scaleZoomImage(crop, nextCrop, imageCrop, state.zoom, state.minZoom);

            this.moveCrop(x, y, {width, height});
        }
    };
    public moveCrop = (x: number, y: number, size?: { width: number, height: number; }) => {
        this.changeState({
            crop: {...this.store.get().crop, x, y, ...size},
        });
    };
    protected moveCropToPos = cropMove;

    public dragImageStart = (start: { x: number; y: number; }) => {
        this.dragged = {type: 'image', start};
    };
    public dragImageMove = (target: { x: number; y: number; }) => {
        const state = this.store.get();

        if (this.dragged) {
            const {x, y} = this.moveImageToPos(
                target, this.dragged.start,
                state.crop, state.imageCrop, state.zoom, state.angle,
            );

            this.dragged!.start = target;

            this.moveImage(x, y);
        }
    };
    public moveImage = (x: number, y: number) => {
        this.changeState({
            imageCrop: {...this.store.get().imageCrop, x, y},
        });
    };
    protected moveImageToPos = imageMove;
    protected scaleZoomImage = cropperImageScale;
    protected imageMoveToAngle = imageMoveToAngle;

    public setDragged = (type: DragItemType, start: { x: number, y: number; }) => {
        this.dragged = {type, start};
    };
    public clearDragged = () => {
        this.dragged = null;
    };

    public zoom = (deltaY: number) => {
        const state = this.store.get();

        const imageCrop = state.imageCrop;
        const crop = state.crop;
        const img = this.image!;

        const prevZoom = state.zoom;
        let zoom = prevZoom - (deltaY / 5000);

        if (zoom < 0.1) {
            return;
        }

        const nextImage = {...state.imageCrop};

        if (deltaY < 0) {
            // инкремент
            const nextImageCrop = this.zoomTo(img, imageCrop, crop, state.zoom, zoom);

            nextImage.x = nextImageCrop.x;
            nextImage.y = nextImageCrop.y;
        } else {
            // декремент

            // let nextImageWidth = imageCrop.width * zoom;
            // let nextImageHeight = imageCrop.height * zoom;

            // если картинка меньше зума, ставим максимально возможный
            // if (nextImageWidth < crop.width) {
            //     zoom = crop.width / imageCrop.width;
            //     nextImageWidth = imageCrop.width * zoom;
            //     nextImageHeight = imageCrop.height * zoom;
            // }
            // if (nextImageHeight < crop.height) {
            //     zoom = crop.height / imageCrop.height;
            //     nextImageWidth = imageCrop.width * zoom;
            //     nextImageHeight = imageCrop.height * zoom;
            // }

            const nextImageCrop = this.zoomTo(img, imageCrop, crop, state.zoom, zoom);

            nextImage.x = nextImageCrop.x;
            nextImage.y = nextImageCrop.y;

            // const nextImageCropToAngle = this.imageMoveToAngle(
            //     crop,
            //     imageCrop,
            //     nextImage,
            //     state.zoom,
            //     nextImageWidth,
            //     nextImageHeight,
            // );
            // nextImage.x = nextImageCropToAngle.x;
            // nextImage.y = nextImageCropToAngle.y;

            // if (nextImageWidth < crop.width) {
            //     return;
            // }
            // if (nextImageHeight < crop.height) {
            //     return;
            // }
        }

        this.changeState({
            imageCrop: nextImage,
            zoom,
        });
    };
    protected zoomTo = cropperZoomTo;

    public rotate = (angle: number) => {
        this.changeState({angle: angle % 360});
    };
    public rotateLeft = () => {
        const state = this.store.get();

        const nextAngle = (state.angle + 90);

        this.rotate(nextAngle);
    };
    public rotateRight = () => {
        const state = this.store.get();

        const nextAngle = (state.angle - 90);

        this.rotate(nextAngle);
    };

    public flipX = (flip?: boolean) => {
        const flipped = typeof flip === 'undefined' ? !this.store.get().flipX : flip;

        this.changeState({flipX: flipped});
    };
    public flipY = (flip?: boolean) => {
        const flipped = typeof flip === 'undefined' ? !this.store.get().flipY : flip;

        this.changeState({flipY: flipped});
    };

    public aspectRatio = (aspectRatio: number | false) => {
        const newConfig = this.getDefaultConfig(aspectRatio);

        this.changeState(newConfig);
    };

    public toBase64 = () => {
        const {crop} = this.store.get();

        return cropImage(this.ctx!, crop);
    }

    public filterGrayScale = (filterGrayScale: number) => {
        this.changeState({filterGrayScale});
    }
    public filterBlur = (filterBlur: number) => {
        this.changeState({filterBlur});
    }
    public filterBrightness = (filterBrightness: number) => {
        this.changeState({filterBrightness});
    }
    public filterSaturate = (filterSaturate: number) => {
        this.changeState({filterSaturate});
    }
    public filterContrast = (filterContrast: number) => {
        this.changeState({filterContrast});
    }
}

export default CropManager;
<file_sep>/src/manager/image-scale.ts
import {Crop} from "../CropManager";
import cropperRound from "../helpers";

function cropperImageScale(prevCrop: Crop, nextCrop: Crop, imageCrop: Crop, zoom: number, minZoom: number) {
    const nextCropImage = {...imageCrop};
    let nextZoom = zoom;

    const minWidth = cropperRound(imageCrop.width * minZoom);
    const minHeight = cropperRound(imageCrop.height * minZoom);

    const imageWidth = cropperRound(imageCrop.width * zoom);
    const imageHeight = cropperRound(imageCrop.height * zoom);

    const diffX = prevCrop.x - nextCrop.x || nextCrop.width - prevCrop.width;
    const diffY = prevCrop.y - nextCrop.y || nextCrop.height - prevCrop.height;

    if (diffX > 0) {
        if (imageHeight >= minHeight) {
            const lWZoom = nextCrop.width / imageCrop.width;
            const lHZoom = nextCrop.height / imageCrop.height;
            const newZoom = lWZoom > lHZoom ? lWZoom : lHZoom;

            const nextImageHeight = imageCrop.height * newZoom;

            if (nextCrop.width < imageWidth) {
                if (nextCrop.x < nextCropImage.x) {
                    nextCropImage.x = nextCropImage.x + (nextCrop.x - prevCrop.x);
                } else if (nextCrop.x + nextCrop.width > nextCropImage.x + imageWidth) {
                    nextCropImage.x = nextCropImage.x + (nextCrop.width - prevCrop.width);
                }
            } else {
                if (nextImageHeight > minHeight) {
                    nextZoom = newZoom;
                    nextCropImage.x = nextCrop.x;
                    nextCropImage.y = nextCropImage.y - (nextImageHeight - imageHeight) / 2;
                }
            }
        }
    } else if (diffX < 0) {
        if (imageHeight >= minHeight) {
            const lWZoom = nextCrop.width / imageCrop.width;
            const lHZoom = nextCrop.height / imageCrop.height;
            const newZoom = lWZoom > lHZoom ? lWZoom : lHZoom;

            const nextImageWidth = imageCrop.width * newZoom;
            const nextImageHeight = imageCrop.height * newZoom;

            if (nextCrop.width > imageWidth) {
                if (nextCrop.x + nextCrop.width > nextCropImage.x + imageWidth) {
                    nextCropImage.x = nextCropImage.x + (nextCrop.width - prevCrop.width);
                }
            } else {
                if (nextImageHeight > minHeight) {
                    nextZoom = newZoom;
                    nextCropImage.x = nextCropImage.x - (nextImageWidth - imageWidth) / 2;
                    nextCropImage.y = nextCropImage.y - (nextImageHeight - imageHeight) / 2;
                }
            }
        }
    }

    if (diffY > 0) {
        if (imageWidth >= minWidth) {
            const lWZoom = nextCrop.width / imageCrop.width;
            const lHZoom = nextCrop.height / imageCrop.height;
            const newZoom = lWZoom > lHZoom ? lWZoom : lHZoom;

            const nextImageWidth = imageCrop.width * newZoom;

            if (nextCrop.height < imageHeight) {
                if (nextCrop.y < nextCropImage.y) {
                    nextCropImage.y = nextCrop.y;
                } else if (nextCrop.y + nextCrop.height > nextCropImage.y + imageHeight) {
                    nextCropImage.y = nextCropImage.y - (prevCrop.height - nextCrop.height);
                }
            } else {
                if (nextImageWidth > minWidth) {
                    nextZoom = newZoom;
                    nextCropImage.x = nextCropImage.x - (nextImageWidth - imageWidth) / 2;
                    nextCropImage.y = nextCrop.y;
                }
            }
        }
    } else if (diffY < 0) {
        if (imageWidth >= minWidth) {
            const lWZoom = nextCrop.width / imageCrop.width;
            const lHZoom = nextCrop.height / imageCrop.height;
            const newZoom = lWZoom > lHZoom ? lWZoom : lHZoom;

            const nextImageWidth = imageCrop.width * newZoom;

            if (nextCrop.height < imageHeight) {
                if (nextCrop.y + nextCrop.height > nextCropImage.y + imageHeight) {
                    nextCropImage.y = nextCropImage.y - (nextCrop.height - prevCrop.height);
                }
            } else {
                if (nextImageWidth > minWidth) {
                    nextZoom = newZoom;
                    nextCropImage.x = nextCropImage.x - (nextImageWidth - imageWidth) / 2;
                    nextCropImage.y = nextCrop.y;
                }
            }
        }
    }

    return {
        zoom: nextZoom,
        cropImage: nextCropImage,
    }
}

export default cropperImageScale;
<file_sep>/src/manager/image-move.ts
import {Crop, Pos2d} from "../CropManager";

function rotateVector(vector: { x: number; y: number }, angle: number) {
    return {
        x: vector.x * Math.cos(angle) - vector.y * Math.sin(angle),
        y: vector.x * Math.sin(angle) + vector.y * Math.cos(angle),
    }
}

function imageMove(start: Pos2d, target: Pos2d, crop: Crop, imageCrop: Crop, zoom: number, angle: number): Crop {
    const nextImageCrop = {...imageCrop};

    const rad = angle * Math.PI / 180;

    const diffX = start.x - target.x;
    const diffY = start.y - target.y;

    const r = rotateVector({x: diffX, y: diffY}, -rad);

    const nextX = nextImageCrop.x + r.x;
    const nextY = nextImageCrop.y + r.y;

    // if (nextX <= crop.x && nextX + imageCrop.width * zoom > (crop.x + crop.width)) {
    //     nextImageCrop.x = nextX;
    // }
    // if (nextY <= crop.y && nextY + imageCrop.height * zoom > (crop.y + crop.height)) {
    //     nextImageCrop.y = nextY;
    // }

    nextImageCrop.x = Math.round(nextX);
    nextImageCrop.y = Math.round(nextY);

    return nextImageCrop;
}

export default imageMove;
<file_sep>/README.md
## Cropper

[![npm version](https://badge.fury.io/js/%40muzikanto%2Fcropper.svg)](https://badge.fury.io/js/%40muzikanto%2Fcropper)
[![downloads](https://img.shields.io/npm/dm/@muzikanto/cropper.svg)](https://www.npmjs.com/package/@muzikanto/cropper)
[![dependencies Status](https://david-dm.org/muzikanto/cropper/status.svg)](https://david-dm.org/muzikanto/cropper)
[![size](https://img.shields.io/bundlephobia/minzip/@muzikanto/cropper)](https://bundlephobia.com/result?p=@muzikanto/cropper)

<!-- TOC -->

-  [Introduction](#introduction)
-  [Installation](#installation)
-  [Examples](#examples)
-  [Use](#use)
-  [License](#license)

<!-- /TOC -->

## Introduction

Peer dependencies: 
    `react`, `react-dom`,
    `material-ui/core`, `material-ui/styles`, `material-ui/icons`

## Installation

```sh
npm i @muzikanto/cropper
# or
yarn add @muzikanto/cropper
```

### Introduction

if you are interested in the package, please create an issue in github with your wishes, and then I will make changes

### new features

- add cross browser
- add easy customizing styles
- add light theme
- add transition effects
- add filters tab
- add resize original tab
- add limited selection
- add easy use CropManager

## Examples

[Example in storybook](https://muzikanto.github.io/cropper)

### Use

```typescript jsx
function Component() {
    const [base64, setBase64] = React.useState('');
    
  return (
      <>
          <BaseCropper
              src="https://github.com/Muzikanto/cropper/blob/master/src/test.jpg"
              onChange={(v) => setBase64(v)}
    
              flippedX={true} // show flip x button
              flippedY={true} // show flip y button
              rotatedLeft={true} // show rotate left button
              rotatedRight={true} // show rotate right button
              rotateToAngle={true} // show bottom rotate line
              sizePreview={true} // show size preview
              aspectRatio={[
                  'free', 'square', 'landscape', 'portrait',
                  {icon: <CustomIcon/>, value: 13 / 10, label: 'custom'},
              ]} // list of aspects
              // aspectRatio={ 16 / 10 } one value => hide select aspectRatio
          />
          <img src={base64}/>
      </>
  );
}
```

## License

[MIT](LICENSE)
<file_sep>/src/helpers/index.ts
function cropperRound(v: number) {
    return Math.round(v * 1000) / 1000;
}

export default cropperRound;
<file_sep>/src/manager/image-move-angle.ts
import {Crop} from "../CropManager";

function imageMoveToAngle(crop: Crop, prevImageCrop: Crop, nextImageCrop: Crop, zoom: number, nextWidth: number, nextHeight: number) {
    const nextImageWidth = nextWidth;
    const nextImageHeight = nextHeight;

    const imageWidth = prevImageCrop.width * zoom;
    const imageHeight = prevImageCrop.height * zoom;

    const nextImage = {...nextImageCrop};

    const topDiff = crop.y - prevImageCrop.y;
    const leftDiff = crop.x - prevImageCrop.x;

    const rightDiff = (crop.x + crop.width) - (nextImage.x + imageWidth);
    const bottomDiff = (crop.y + crop.height) - (nextImage.y + imageHeight);

    const top = Math.abs(topDiff) < Math.abs(bottomDiff);
    const left = Math.abs(leftDiff) < Math.abs(rightDiff);

    const lt = left && top;
    const rt = !left && top;
    const lb = left && !top;
    const rb = !left && !top;

    if (lt) {
        if (crop.x < nextImage.x) {
            nextImage.x = crop.x;
        }
        if (crop.y < nextImage.y) {
            nextImage.y = crop.y;
        }
    }
    if (rt) {
        const imageRightX = nextImage.x + nextImageWidth;
        const cropRightX = crop.x + crop.width;

        if (cropRightX > imageRightX) {
            nextImage.x = nextImage.x + (cropRightX - imageRightX);
        }
        if (crop.y < nextImage.y) {
            nextImage.y = crop.y;
        }
    }
    if (lb) {
        const imageBottomY = nextImage.y + nextImageHeight;
        const croBottomY = crop.y + crop.height;

        if (crop.x < nextImage.x) {
            nextImage.x = crop.x;
        }
        if (croBottomY > imageBottomY) {
            nextImage.y = nextImage.y + (croBottomY - imageBottomY);
        }
    }
    if (rb) {
        const imageRightX = nextImage.x + nextImageWidth;
        const cropRightX = crop.x + crop.width;
        const imageBottomY = nextImage.y + nextImageHeight;
        const croBottomY = crop.y + crop.height;

        if (cropRightX > imageRightX) {
            nextImage.x = nextImage.x + (cropRightX - imageRightX);
        }
        if (croBottomY > imageBottomY) {
            nextImage.y = nextImage.y + (croBottomY - imageBottomY);
        }
    }

    return nextImage;
}

export default imageMoveToAngle;