├── .babelrc
├── .buckconfig
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .gitignore
├── .prettierrc.js
├── .watchmanconfig
├── LICENSE
├── README.md
├── index.js
├── package-lock.json
├── package.json
├── src
├── Cropper
│ ├── Cropper.page.tsx
│ ├── Cropper.style.ts
│ └── Cropper.tsx
├── Main.tsx
├── common
│ ├── DefaultFooter.tsx
│ └── index.ts
├── constants
│ └── index.ts
└── utils
│ ├── cropper.ts
│ └── index.ts
├── tsconfig.json
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["module:metro-react-native-babel-preset"]
3 | }
4 |
--------------------------------------------------------------------------------
/.buckconfig:
--------------------------------------------------------------------------------
1 |
2 | [android]
3 | target = Google Inc.:Google APIs:23
4 |
5 | [maven_repositories]
6 | central = https://repo1.maven.org/maven2
7 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Ignore the 'dist' folder
2 | dist/*
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: '@react-native-community',
4 | parser: '@typescript-eslint/parser',
5 | plugins: ['@typescript-eslint'],
6 | rules: {
7 | "prefer-template": 0,
8 | "max-len": 0,
9 | "quote-props": 0,
10 | "no-return-assign": 0,
11 | "global-require": 0,
12 | "no-underscore-dangle": 0,
13 | "react/sort-comp": 0,
14 | "no-useless-concat": 0,
15 | "arrow-body-style": 0,
16 | "object-shorthand": 0,
17 | "no-mixed-operators": 0,
18 | "no-undef-init": 0,
19 | "camelcase": 0,
20 | "jsx-quotes": 0,
21 | "react-native/no-inline-styles": 0
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | project.xcworkspace
24 |
25 | # Android/IntelliJ
26 | #
27 | build/
28 | .idea
29 | .gradle
30 | local.properties
31 | *.iml
32 |
33 | # node.js
34 | #
35 | node_modules/
36 | npm-debug.log
37 | yarn-error.log
38 |
39 | # BUCK
40 | buck-out/
41 | \.buckd/
42 | *.keystore
43 |
44 | # fastlane
45 | #
46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
47 | # screenshots whenever they are needed.
48 | # For more information about the recommended setup visit:
49 | # https://docs.fastlane.tools/best-practices/source-control/
50 |
51 | */fastlane/report.xml
52 | */fastlane/Preview.html
53 | */fastlane/screenshots
54 |
55 | # Bundle artifact
56 | *.jsbundle
57 |
58 | # Distribution folder
59 | dist/
60 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | bracketSpacing: true,
3 | jsxBracketSameLine: false,
4 | singleQuote: true,
5 | trailingComma: 'all',
6 | jsxSingleQuote: true,
7 | printWidth: 150,
8 | };
9 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 ggunti
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-amazing-cropper
2 |
3 | Image cropper for react native made with Animated API (with rotation possibility) - **for iOS & android**
4 |
5 |
It is written in typescript!
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | This component depends on `@bam.tech/react-native-image-resizer` and `@react-native-community/image-editor` libraries. They need to be installed and linked to your project before.
19 |
20 | **STEPS TO INSTALL:** 1.\* `npm install --save @bam.tech/react-native-image-resizer @react-native-community/image-editor` 2. `react-native link @bam.tech/react-native-image-resizer @react-native-community/image-editor` 3.\* `npm install --save react-native-amazing-cropper`
21 |
22 | Step 2 is not needed for react-native >= 0.60 because of autolinking. Instead just run `pod install` inside `ios` directory.
23 |
24 | #### Properties
25 |
26 | ---
27 |
28 | | Prop | Type | Description |
29 | | :------------------------ | :---------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30 | | onDone | `function` | A function which accepts 2 arguments: `croppedImageUri` and `garbageUris`. `garbageUris` is an array of string (it has 1 or 2 uri) and should be ignored. It is returned only to give you the possibility to clear the garbage uris from cache (ex. using `unlink` method from [react-native-fs](https://github.com/itinance/react-native-fs); see [this issue](https://github.com/ggunti/react-native-amazing-cropper/issues/36)). Called when user press the 'DONE' button |
31 | | onError | `function` | A function which accepts 1 argument `err` of type `Error`. Called when rotation or cropping fails |
32 | | onCancel | `function` | A function without arguments. Called when user press the 'CANCEL' button |
33 | | imageUri | `string` | The uri of the image you want to crop or rotate |
34 | | imageWidth | `number` | The width (in pixels) of the image you passed in `imageUri` |
35 | | imageHeight | `number` | The height (in pixels) of the image you passed in `imageUri` |
36 | | initialRotation | `number` | Number which set the default rotation of the image when cropper is initialized. Default is `0` |
37 | | footerComponent | `component` | Custom component for footer. Default is ` ` |
38 | | NOT_SELECTED_AREA_OPACITY | `number` | The opacity of the area which is not selected by the cropper. Should be a value between `0` and `1`. Default is `0.5` |
39 | | BORDER_WIDTH | `number` | The border width [(see image)](https://i.imgur.com/CMSS953.png). Default is `50` |
40 | | COMPONENT_WIDTH | `number` | The width of the entire component. Default is `Dimensions.get('window').width`, not recommended to change. |
41 | | COMPONENT_HEIGHT | `number` | The height of the entire component. Default is `Dimensions.get('window').height`, you should change it to fix [hidden footer issue](https://github.com/ggunti/react-native-amazing-cropper/issues/30). |
42 |
43 | #### Usage example 1 (using the default footer)
44 |
45 | ---
46 |
47 | ```javascript
48 | import React, { Component } from 'react';
49 | import AmazingCropper from 'react-native-amazing-cropper';
50 |
51 | class AmazingCropperPage extends Component {
52 | onDone = (croppedImageUri, garbageUris) => {
53 | console.log('croppedImageUri = ', croppedImageUri);
54 | // clear the garbage uris from cache
55 | // send image to server for example
56 | };
57 |
58 | onError = (err) => {
59 | console.log(err);
60 | };
61 |
62 | onCancel = () => {
63 | console.log('Cancel button was pressed');
64 | // navigate back
65 | };
66 |
67 | render() {
68 | return (
69 |
79 | );
80 | }
81 | }
82 | ```
83 |
84 | #### Usage example 2 (using the default footer with custom text)
85 |
86 | ---
87 |
88 | ```javascript
89 | import React, { Component } from 'react';
90 | import AmazingCropper, { DefaultFooter } from 'react-native-amazing-cropper';
91 |
92 | class AmazingCropperPage extends Component {
93 | onDone = (croppedImageUri, garbageUris) => {
94 | console.log('croppedImageUri = ', croppedImageUri);
95 | // clear the garbage uris from cache
96 | // send image to server for example
97 | };
98 |
99 | onError = (err) => {
100 | console.log(err);
101 | };
102 |
103 | onCancel = () => {
104 | console.log('Cancel button was pressed');
105 | // navigate back
106 | };
107 |
108 | render() {
109 | return (
110 | }
113 | onDone={this.onDone}
114 | onError={this.onError}
115 | onCancel={this.onCancel}
116 | imageUri='https://www.lifeofpix.com/wp-content/uploads/2018/09/manhattan_-11-1600x2396.jpg'
117 | imageWidth={1600}
118 | imageHeight={2396}
119 | />
120 | );
121 | }
122 | }
123 | ```
124 |
125 | #### Usage example 3 (using own fully customized footer)
126 |
127 | ---
128 |
129 | Write your custom footer component.
130 | Don't forget to call the **props.onDone**, **props.onRotate** and **props.onCancel** methods inside it (the Cropper will pass them automatically to your footer component). Example of custom footer component:
131 |
132 | ```javascript
133 | import React from 'react';
134 | import { View, TouchableOpacity, Text, Platform, StyleSheet } from 'react-native';
135 | import PropTypes from 'prop-types';
136 | import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons';
137 |
138 | const CustomCropperFooter = (props) => (
139 |
140 |
141 | CANCEL
142 |
143 |
144 |
145 |
146 |
147 | DONE
148 |
149 |
150 | );
151 |
152 | export default CustomCropperFooter;
153 |
154 | CustomCropperFooter.propTypes = {
155 | onDone: PropTypes.func,
156 | onRotate: PropTypes.func,
157 | onCancel: PropTypes.func,
158 | };
159 |
160 | const styles = StyleSheet.create({
161 | buttonsContainer: {
162 | flexDirection: 'row',
163 | alignItems: 'center', // 'flex-start'
164 | justifyContent: 'space-between',
165 | height: '100%',
166 | },
167 | text: {
168 | color: 'white',
169 | fontSize: 16,
170 | },
171 | touchable: {
172 | padding: 10,
173 | },
174 | rotateIcon: {
175 | color: 'white',
176 | fontSize: 26,
177 | ...Platform.select({
178 | android: {
179 | textShadowOffset: { width: 1, height: 1 },
180 | textShadowColor: '#000000',
181 | textShadowRadius: 3,
182 | shadowOpacity: 0.9,
183 | elevation: 1,
184 | },
185 | ios: {
186 | shadowOffset: { width: 1, height: 1 },
187 | shadowColor: '#000000',
188 | shadowRadius: 3,
189 | shadowOpacity: 0.9,
190 | },
191 | }),
192 | },
193 | });
194 | ```
195 |
196 | Now just pass your footer component to the Cropper like here:
197 |
198 | ```javascript
199 | import React, { Component } from 'react';
200 | import AmazingCropper from 'react-native-amazing-cropper';
201 | import CustomCropperFooter from './src/components/CustomCropperFooter.component';
202 |
203 | class AmazingCropperPage extends Component {
204 | onDone = (croppedImageUri, garbageUris) => {
205 | console.log('croppedImageUri = ', croppedImageUri);
206 | // clear the garbage uris from cache
207 | // send image to server for example
208 | };
209 |
210 | onError = (err) => {
211 | console.log(err);
212 | };
213 |
214 | onCancel = () => {
215 | console.log('Cancel button was pressed');
216 | // navigate back
217 | };
218 |
219 | render() {
220 | return (
221 | }
225 | onDone={this.onDone}
226 | onError={this.onError}
227 | onCancel={this.onCancel}
228 | imageUri='https://www.lifeofpix.com/wp-content/uploads/2018/09/manhattan_-11-1600x2396.jpg'
229 | imageWidth={1600}
230 | imageHeight={2396}
231 | />
232 | );
233 | }
234 | }
235 | ```
236 |
237 | ### Do you need a mobile app for android & iOS? [Hire me](https://order-software.com)
238 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import Main from './src/Main';
2 | import { DefaultFooter } from './src/common';
3 |
4 | export default Main;
5 | export { DefaultFooter };
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-amazing-cropper",
3 | "version": "0.2.8",
4 | "description": "Custom react native cropper with rotation",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
10 | "build": "tsc"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/ggunti/react-native-amazing-cropper.git"
15 | },
16 | "author": {
17 | "name": "Gotha Guntter",
18 | "email": "ggunty96@gmail.com"
19 | },
20 | "license": "MIT",
21 | "peerDependencies": {
22 | "@bam.tech/react-native-image-resizer": "*",
23 | "@react-native-community/image-editor": "*",
24 | "react": "*",
25 | "react-native": "*"
26 | },
27 | "devDependencies": {
28 | "@react-native-community/eslint-config": "^0.0.5",
29 | "@types/jest": "^25.1.2",
30 | "@types/react": "^16.9.19",
31 | "@types/react-native": "^0.61.15",
32 | "@types/react-test-renderer": "^16.9.2",
33 | "@typescript-eslint/eslint-plugin": "^2.12.0",
34 | "@typescript-eslint/parser": "^2.12.0",
35 | "babel-jest": "^24.9.0",
36 | "eslint": "^6.5.1",
37 | "jest": "^24.9.0",
38 | "metro-react-native-babel-preset": "^0.56.0",
39 | "react-test-renderer": "16.9.0",
40 | "typescript": "^3.9.10"
41 | },
42 | "jest": {
43 | "preset": "react-native",
44 | "moduleFileExtensions": [
45 | "ts",
46 | "tsx",
47 | "js",
48 | "jsx",
49 | "json",
50 | "node"
51 | ]
52 | },
53 | "keywords": [
54 | "react-native",
55 | "react",
56 | "crop",
57 | "rotate",
58 | "image",
59 | "cropper"
60 | ],
61 | "bugs": {
62 | "url": "https://github.com/ggunti/react-native-amazing-cropper/issues"
63 | },
64 | "homepage": "https://github.com/ggunti/react-native-amazing-cropper"
65 | }
66 |
--------------------------------------------------------------------------------
/src/Cropper/Cropper.page.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Animated, PanResponder, Platform, PanResponderInstance, PanResponderGestureState } from 'react-native';
3 | import ImageResizer from '@bam.tech/react-native-image-resizer';
4 | import ImageEditor from '@react-native-community/image-editor';
5 | import { Q } from '../constants';
6 | import Cropper from './Cropper';
7 | import { getCropperLimits } from '../utils';
8 |
9 | type CropperPageProps = {
10 | footerComponent: JSX.Element;
11 | onDone: (croppedImageUri: string, garbageUris: string[]) => void;
12 | onError: (err: Error) => void;
13 | onCancel: () => void;
14 | imageUri: string;
15 | imageWidth: number;
16 | imageHeight: number;
17 | TOP_VALUE: number;
18 | LEFT_VALUE: number;
19 | BOTTOM_VALUE: number;
20 | RIGHT_VALUE: number;
21 | initialRotation: number;
22 | NOT_SELECTED_AREA_OPACITY: number;
23 | BORDER_WIDTH: number;
24 | COMPONENT_WIDTH: number;
25 | COMPONENT_HEIGHT: number;
26 | };
27 |
28 | interface ExtendedAnimatedValue extends Animated.Value {
29 | _value: number;
30 | _offset: number;
31 | }
32 |
33 | interface ExtendedAnimatedValueXY extends Animated.AnimatedValueXY {
34 | x: ExtendedAnimatedValue;
35 | y: ExtendedAnimatedValue;
36 | }
37 |
38 | type Position = 'topPosition' | 'leftPosition' | 'bottomPosition' | 'rightPosition';
39 |
40 | interface State {
41 | topOuterPosition: ExtendedAnimatedValueXY;
42 | topOuterPanResponder: PanResponderInstance;
43 | leftOuterPosition: ExtendedAnimatedValueXY;
44 | leftOuterPanResponder: PanResponderInstance;
45 | bottomOuterPosition: ExtendedAnimatedValueXY;
46 | bottomOuterPanResponder: PanResponderInstance;
47 | rightOuterPosition: ExtendedAnimatedValueXY;
48 | rightOuterPanResponder: PanResponderInstance;
49 |
50 | topPosition: ExtendedAnimatedValueXY;
51 | topPanResponder: PanResponderInstance;
52 | leftPosition: ExtendedAnimatedValueXY;
53 | leftPanResponder: PanResponderInstance;
54 | bottomPosition: ExtendedAnimatedValueXY;
55 | bottomPanResponder: PanResponderInstance;
56 | rightPosition: ExtendedAnimatedValueXY;
57 | rightPanResponder: PanResponderInstance;
58 |
59 | topLeftPosition: ExtendedAnimatedValueXY;
60 | topLeftPanResponder: PanResponderInstance;
61 | bottomLeftPosition: ExtendedAnimatedValueXY;
62 | bottomLeftPanResponder: PanResponderInstance;
63 | bottomRightPosition: ExtendedAnimatedValueXY;
64 | bottomRightPanResponder: PanResponderInstance;
65 | topRightPosition: ExtendedAnimatedValueXY;
66 | topRightPanResponder: PanResponderInstance;
67 |
68 | rectanglePosition: ExtendedAnimatedValueXY;
69 | rectanglePanResponder: PanResponderInstance;
70 |
71 | TOP_LIMIT: number;
72 | LEFT_LIMIT: number;
73 | BOTTOM_LIMIT: number;
74 | RIGHT_LIMIT: number;
75 |
76 | TOP_VALUE: number;
77 | LEFT_VALUE: number;
78 | BOTTOM_VALUE: number;
79 | RIGHT_VALUE: number;
80 | rotation: number;
81 | }
82 |
83 | class CropperPage extends Component {
84 | constructor(props: CropperPageProps) {
85 | super(props);
86 | const { imageWidth, imageHeight, BORDER_WIDTH, COMPONENT_WIDTH, COMPONENT_HEIGHT } = props;
87 | const W_INT = this.W - 2 * BORDER_WIDTH;
88 | const H_INT = this.H - 2 * BORDER_WIDTH;
89 | const { TOP_LIMIT, LEFT_LIMIT, BOTTOM_LIMIT, RIGHT_LIMIT, DIFF } = getCropperLimits(
90 | imageWidth,
91 | imageHeight,
92 | props.initialRotation,
93 | W_INT,
94 | H_INT,
95 | this.W,
96 | this.H,
97 | BORDER_WIDTH,
98 | Q,
99 | );
100 |
101 | const TOP_VALUE = props.TOP_VALUE !== 0 ? props.TOP_VALUE : TOP_LIMIT;
102 | const LEFT_VALUE = props.LEFT_VALUE !== 0 ? props.LEFT_VALUE : LEFT_LIMIT;
103 | const BOTTOM_VALUE = props.BOTTOM_VALUE !== 0 ? props.BOTTOM_VALUE : BOTTOM_LIMIT;
104 | const RIGHT_VALUE = props.RIGHT_VALUE !== 0 ? props.RIGHT_VALUE : RIGHT_LIMIT;
105 |
106 | const topOuterPosition = new Animated.ValueXY({ x: LEFT_VALUE - BORDER_WIDTH, y: TOP_VALUE - BORDER_WIDTH }) as ExtendedAnimatedValueXY;
107 | const topOuterPanResponder = PanResponder.create({ onStartShouldSetPanResponder: () => false });
108 | const leftOuterPosition = new Animated.ValueXY({ x: LEFT_VALUE - BORDER_WIDTH, y: TOP_VALUE - BORDER_WIDTH }) as ExtendedAnimatedValueXY;
109 | const leftOuterPanResponder = PanResponder.create({ onStartShouldSetPanResponder: () => false });
110 | const bottomOuterPosition = new Animated.ValueXY({ x: LEFT_VALUE - BORDER_WIDTH, y: COMPONENT_HEIGHT - BOTTOM_VALUE }) as ExtendedAnimatedValueXY;
111 | const bottomOuterPanResponder = PanResponder.create({ onStartShouldSetPanResponder: () => false });
112 | const rightOuterPosition = new Animated.ValueXY({ x: COMPONENT_WIDTH - RIGHT_VALUE, y: TOP_VALUE - BORDER_WIDTH }) as ExtendedAnimatedValueXY;
113 | const rightOuterPanResponder = PanResponder.create({ onStartShouldSetPanResponder: () => false });
114 |
115 | const topPosition = new Animated.ValueXY({ x: LEFT_VALUE - BORDER_WIDTH, y: TOP_VALUE - BORDER_WIDTH }) as ExtendedAnimatedValueXY;
116 | const topPanResponder = this.initSidePanResponder('topPosition');
117 | const leftPosition = new Animated.ValueXY({ x: LEFT_VALUE - BORDER_WIDTH, y: TOP_VALUE - BORDER_WIDTH }) as ExtendedAnimatedValueXY;
118 | const leftPanResponder = this.initSidePanResponder('leftPosition');
119 | const bottomPosition = new Animated.ValueXY({ x: LEFT_VALUE - BORDER_WIDTH, y: COMPONENT_HEIGHT - BOTTOM_VALUE }) as ExtendedAnimatedValueXY;
120 | const bottomPanResponder = this.initSidePanResponder('bottomPosition');
121 | const rightPosition = new Animated.ValueXY({
122 | x: COMPONENT_WIDTH - RIGHT_VALUE,
123 | y: TOP_VALUE - BORDER_WIDTH - DIFF / 2,
124 | }) as ExtendedAnimatedValueXY;
125 | const rightPanResponder = this.initSidePanResponder('rightPosition');
126 |
127 | const topLeftPosition = new Animated.ValueXY({ x: LEFT_VALUE - BORDER_WIDTH, y: TOP_VALUE - BORDER_WIDTH }) as ExtendedAnimatedValueXY;
128 | const topLeftPanResponder = this.initCornerPanResponder('topPosition', 'leftPosition');
129 | const bottomLeftPosition = new Animated.ValueXY({ x: LEFT_VALUE - BORDER_WIDTH, y: COMPONENT_HEIGHT - BOTTOM_VALUE }) as ExtendedAnimatedValueXY;
130 | const bottomLeftPanResponder = this.initCornerPanResponder('bottomPosition', 'leftPosition');
131 | const bottomRightPosition = new Animated.ValueXY({
132 | x: COMPONENT_WIDTH - RIGHT_VALUE,
133 | y: COMPONENT_HEIGHT - BOTTOM_VALUE,
134 | }) as ExtendedAnimatedValueXY;
135 | const bottomRightPanResponder = this.initCornerPanResponder('bottomPosition', 'rightPosition');
136 | const topRightPosition = new Animated.ValueXY({ x: COMPONENT_WIDTH - RIGHT_VALUE, y: TOP_VALUE - BORDER_WIDTH }) as ExtendedAnimatedValueXY;
137 | const topRightPanResponder = this.initCornerPanResponder('topPosition', 'rightPosition');
138 |
139 | const rectanglePosition = new Animated.ValueXY({ x: LEFT_VALUE, y: TOP_VALUE }) as ExtendedAnimatedValueXY;
140 | const rectanglePanResponder = this.initRectanglePanResponder();
141 |
142 | this.state = {
143 | topOuterPosition,
144 | topOuterPanResponder,
145 | leftOuterPosition,
146 | leftOuterPanResponder,
147 | bottomOuterPosition,
148 | bottomOuterPanResponder,
149 | rightOuterPosition,
150 | rightOuterPanResponder,
151 |
152 | topPosition,
153 | topPanResponder,
154 | leftPosition,
155 | leftPanResponder,
156 | bottomPosition,
157 | bottomPanResponder,
158 | rightPosition,
159 | rightPanResponder,
160 |
161 | topLeftPosition,
162 | topLeftPanResponder,
163 | bottomLeftPosition,
164 | bottomLeftPanResponder,
165 | bottomRightPosition,
166 | bottomRightPanResponder,
167 | topRightPosition,
168 | topRightPanResponder,
169 |
170 | rectanglePosition,
171 | rectanglePanResponder,
172 |
173 | TOP_LIMIT,
174 | LEFT_LIMIT,
175 | BOTTOM_LIMIT,
176 | RIGHT_LIMIT,
177 |
178 | TOP_VALUE,
179 | LEFT_VALUE,
180 | BOTTOM_VALUE,
181 | RIGHT_VALUE,
182 | rotation: props.initialRotation,
183 | };
184 | }
185 |
186 | isRectangleMoving = false;
187 | topOuter = undefined;
188 | leftOuter = undefined;
189 | bottomOuter = undefined;
190 | rightOuter = undefined;
191 | W = this.props.COMPONENT_WIDTH;
192 | H = this.props.COMPONENT_HEIGHT - Q;
193 |
194 | onCancel = () => {
195 | this.props.onCancel();
196 | };
197 |
198 | getTopOuterStyle = () => {
199 | return {
200 | ...this.state.topOuterPosition.getLayout(),
201 | top: this.state.TOP_LIMIT,
202 | left: this.state.LEFT_LIMIT,
203 | height: Animated.add(this.props.BORDER_WIDTH - this.state.TOP_LIMIT, this.state.topPosition.y),
204 | width: this.W,
205 | backgroundColor: `rgba(0, 0, 0, ${this.props.NOT_SELECTED_AREA_OPACITY})`,
206 | };
207 | };
208 |
209 | getLeftOuterStyle = () => {
210 | return {
211 | ...this.state.leftOuterPosition.getLayout(),
212 | top: Animated.add(this.props.BORDER_WIDTH, this.state.topPosition.y),
213 | left: this.state.LEFT_LIMIT,
214 | height: Animated.add(-this.props.BORDER_WIDTH, Animated.add(this.state.bottomPosition.y, Animated.multiply(-1, this.state.topPosition.y))),
215 | width: Animated.add(this.props.BORDER_WIDTH - this.state.LEFT_LIMIT, this.state.leftPosition.x),
216 | backgroundColor: `rgba(0, 0, 0, ${this.props.NOT_SELECTED_AREA_OPACITY})`,
217 | };
218 | };
219 |
220 | getBottomOuterStyle = () => {
221 | return {
222 | ...this.state.bottomOuterPosition.getLayout(),
223 | top: this.state.bottomPosition.y,
224 | left: this.state.LEFT_LIMIT,
225 | height: Animated.add(this.props.COMPONENT_HEIGHT - this.state.BOTTOM_LIMIT, Animated.multiply(-1, this.state.bottomPosition.y)),
226 | width: this.W,
227 | backgroundColor: `rgba(0, 0, 0, ${this.props.NOT_SELECTED_AREA_OPACITY})`,
228 | };
229 | };
230 |
231 | getRightOuterStyle = () => {
232 | return {
233 | ...this.state.rightOuterPosition.getLayout(),
234 | top: Animated.add(this.props.BORDER_WIDTH, this.state.topPosition.y),
235 | left: this.state.rightPosition.x,
236 | height: Animated.add(-this.props.BORDER_WIDTH, Animated.add(this.state.bottomPosition.y, Animated.multiply(-1, this.state.topPosition.y))),
237 | right: this.state.RIGHT_LIMIT,
238 | backgroundColor: `rgba(0, 0, 0, ${this.props.NOT_SELECTED_AREA_OPACITY})`,
239 | };
240 | };
241 |
242 | getTopLeftStyle = () => {
243 | return {
244 | ...this.state.topLeftPosition.getLayout(),
245 | top: this.state.topPosition.y,
246 | left: this.state.leftPosition.x,
247 | width: this.props.BORDER_WIDTH,
248 | paddingBottom: this.props.BORDER_WIDTH,
249 | };
250 | };
251 |
252 | getBottomLeftStyle = () => {
253 | return {
254 | ...this.state.bottomLeftPosition.getLayout(),
255 | top: this.state.bottomPosition.y,
256 | left: this.state.leftPosition.x,
257 | width: this.props.BORDER_WIDTH,
258 | paddingTop: this.props.BORDER_WIDTH,
259 | };
260 | };
261 |
262 | getBottomRightStyle = () => {
263 | return {
264 | ...this.state.bottomRightPosition.getLayout(),
265 | top: this.state.bottomPosition.y,
266 | left: this.state.rightPosition.x,
267 | height: this.props.BORDER_WIDTH,
268 | paddingLeft: this.props.BORDER_WIDTH,
269 | };
270 | };
271 |
272 | getTopRightStyle = () => {
273 | return {
274 | ...this.state.topRightPosition.getLayout(),
275 | top: this.state.topPosition.y,
276 | left: this.state.rightPosition.x,
277 | height: this.props.BORDER_WIDTH,
278 | paddingLeft: this.props.BORDER_WIDTH,
279 | };
280 | };
281 |
282 | getTopSideStyle = () => {
283 | return {
284 | ...this.state.topPosition.getLayout(),
285 | left: Animated.add(this.props.BORDER_WIDTH, this.state.leftPosition.x),
286 | width: Animated.add(-this.props.BORDER_WIDTH, Animated.add(this.state.rightPosition.x, Animated.multiply(-1, this.state.leftPosition.x))),
287 | paddingBottom: this.props.BORDER_WIDTH,
288 | };
289 | };
290 |
291 | getLeftSideStyle = () => {
292 | return {
293 | ...this.state.leftPosition.getLayout(),
294 | top: Animated.add(this.props.BORDER_WIDTH, this.state.topPosition.y),
295 | height: Animated.add(-this.props.BORDER_WIDTH, Animated.add(this.state.bottomPosition.y, Animated.multiply(-1, this.state.topPosition.y))),
296 | paddingLeft: this.props.BORDER_WIDTH,
297 | };
298 | };
299 |
300 | getBottomSideStyle = () => {
301 | return {
302 | ...this.state.bottomPosition.getLayout(),
303 | left: Animated.add(this.props.BORDER_WIDTH, this.state.leftPosition.x),
304 | width: Animated.add(-this.props.BORDER_WIDTH, Animated.add(this.state.rightPosition.x, Animated.multiply(-1, this.state.leftPosition.x))),
305 | paddingTop: this.props.BORDER_WIDTH,
306 | };
307 | };
308 |
309 | getRightSideStyle = () => {
310 | return {
311 | ...this.state.rightPosition.getLayout(),
312 | top: Animated.add(this.props.BORDER_WIDTH, this.state.topPosition.y),
313 | height: Animated.add(-this.props.BORDER_WIDTH, Animated.add(this.state.bottomPosition.y, Animated.multiply(-1, this.state.topPosition.y))),
314 | paddingLeft: this.props.BORDER_WIDTH,
315 | };
316 | };
317 |
318 | getRectangleStyle = () => {
319 | return {
320 | ...this.state.rectanglePosition.getLayout(),
321 | top: Animated.add(this.props.BORDER_WIDTH, this.state.topPosition.y),
322 | left: Animated.add(this.props.BORDER_WIDTH, this.state.leftPosition.x),
323 | width: Animated.add(-this.props.BORDER_WIDTH, Animated.add(this.state.rightPosition.x, Animated.multiply(-1, this.state.leftPosition.x))),
324 | height: Animated.add(-this.props.BORDER_WIDTH, Animated.add(this.state.bottomPosition.y, Animated.multiply(-1, this.state.topPosition.y))),
325 | zIndex: 3,
326 | };
327 | };
328 |
329 | getImageStyle = () => {
330 | const DIFF = this.state.topPosition.y._value - this.state.rightPosition.y._value;
331 | return {
332 | position: 'absolute',
333 | top: this.state.TOP_LIMIT - DIFF,
334 | left: this.state.LEFT_LIMIT + DIFF,
335 | bottom: this.state.BOTTOM_LIMIT - DIFF,
336 | right: this.state.RIGHT_LIMIT + DIFF,
337 | resizeMode: 'stretch',
338 | transform: [{ rotate: `${this.state.rotation.toString()}deg` }],
339 | };
340 | };
341 |
342 | isAllowedToMoveTopSide = (gesture: PanResponderGestureState) => {
343 | return (
344 | this.state.topPosition.y._offset + gesture.dy >= this.state.TOP_LIMIT - this.props.BORDER_WIDTH &&
345 | this.state.topPosition.y._offset + gesture.dy + this.props.BORDER_WIDTH + 1 < this.state.bottomPosition.y._offset
346 | );
347 | };
348 | isAllowedToMoveLeftSide = (gesture: PanResponderGestureState) => {
349 | return (
350 | this.state.leftPosition.x._offset + gesture.dx >= this.state.LEFT_LIMIT - this.props.BORDER_WIDTH &&
351 | this.state.leftPosition.x._offset + gesture.dx + this.props.BORDER_WIDTH + 1 < this.state.rightPosition.x._offset
352 | );
353 | };
354 | isAllowedToMoveBottomSide = (gesture: PanResponderGestureState) => {
355 | return (
356 | this.state.bottomPosition.y._offset + gesture.dy <= this.props.COMPONENT_HEIGHT - this.state.BOTTOM_LIMIT &&
357 | this.state.topPosition.y._offset + this.props.BORDER_WIDTH + 1 < this.state.bottomPosition.y._offset + gesture.dy
358 | );
359 | };
360 | isAllowedToMoveRightSide = (gesture: PanResponderGestureState) => {
361 | return (
362 | this.state.rightPosition.x._offset + gesture.dx <= this.props.COMPONENT_WIDTH - this.state.RIGHT_LIMIT &&
363 | this.state.leftPosition.x._offset + this.props.BORDER_WIDTH + 1 < this.state.rightPosition.x._offset + gesture.dx
364 | );
365 | };
366 |
367 | isAllowedToMove = (position: Position, gesture: PanResponderGestureState) => {
368 | if (position === 'topPosition') {
369 | return this.isAllowedToMoveTopSide(gesture);
370 | }
371 | if (position === 'leftPosition') {
372 | return this.isAllowedToMoveLeftSide(gesture);
373 | }
374 | if (position === 'bottomPosition') {
375 | return this.isAllowedToMoveBottomSide(gesture);
376 | }
377 | if (position === 'rightPosition') {
378 | return this.isAllowedToMoveRightSide(gesture);
379 | }
380 | };
381 |
382 | initSidePanResponder = (position: Position) => {
383 | return PanResponder.create({
384 | onStartShouldSetPanResponder: () => !this.isRectangleMoving,
385 | onPanResponderMove: (_event, gesture) => {
386 | if (this.isAllowedToMove(position, gesture)) {
387 | this.state[position].setValue({ x: gesture.dx, y: gesture.dy });
388 | }
389 | },
390 | onPanResponderRelease: () => {
391 | // make to not reset position
392 | this.state.topPosition.flattenOffset();
393 | this.state.leftPosition.flattenOffset();
394 | this.state.bottomPosition.flattenOffset();
395 | this.state.rightPosition.flattenOffset();
396 | },
397 | onPanResponderGrant: () => {
398 | this.state.topPosition.setOffset({
399 | x: this.state.topPosition.x._value,
400 | y: this.state.topPosition.y._value,
401 | });
402 | this.state.leftPosition.setOffset({
403 | x: this.state.leftPosition.x._value,
404 | y: this.state.leftPosition.y._value,
405 | });
406 | this.state.bottomPosition.setOffset({
407 | x: this.state.bottomPosition.x._value,
408 | y: this.state.bottomPosition.y._value,
409 | });
410 | this.state.rightPosition.setOffset({
411 | x: this.state.rightPosition.x._value,
412 | y: this.state.rightPosition.y._value,
413 | });
414 |
415 | this.state.topPosition.setValue({ x: 0, y: 0 });
416 | this.state.leftPosition.setValue({ x: 0, y: 0 });
417 | this.state.bottomPosition.setValue({ x: 0, y: 0 });
418 | this.state.rightPosition.setValue({ x: 0, y: 0 });
419 | },
420 | });
421 | };
422 |
423 | initRectanglePanResponder = () => {
424 | return PanResponder.create({
425 | onStartShouldSetPanResponder: () => !this.isRectangleMoving,
426 | onPanResponderMove: (_event, gesture) => {
427 | this.state.topPosition.setValue({ x: gesture.dx, y: gesture.dy });
428 | this.state.leftPosition.setValue({ x: gesture.dx, y: gesture.dy });
429 | this.state.bottomPosition.setValue({ x: gesture.dx, y: gesture.dy });
430 | this.state.rightPosition.setValue({ x: gesture.dx, y: gesture.dy });
431 | },
432 | onPanResponderRelease: () => {
433 | this.isRectangleMoving = true;
434 | // make to not reset position
435 | this.state.topPosition.flattenOffset();
436 | this.state.leftPosition.flattenOffset();
437 | this.state.bottomPosition.flattenOffset();
438 | this.state.rightPosition.flattenOffset();
439 |
440 | const width = this.state.rightPosition.x._value - this.state.leftPosition.x._value - this.props.BORDER_WIDTH;
441 | const height = this.state.bottomPosition.y._value - this.state.topPosition.y._value - this.props.BORDER_WIDTH;
442 | let isOutside = false;
443 |
444 | if (this.state.leftPosition.x._value < this.state.LEFT_LIMIT - this.props.BORDER_WIDTH) {
445 | isOutside = true;
446 | Animated.parallel([
447 | Animated.spring(this.state.leftPosition.x, { toValue: this.state.LEFT_LIMIT - this.props.BORDER_WIDTH, useNativeDriver: false }),
448 | Animated.spring(this.state.rightPosition.x, { toValue: this.state.LEFT_LIMIT + width, useNativeDriver: false }),
449 | ]).start(() => {
450 | this.isRectangleMoving = false;
451 | });
452 | }
453 | if (this.state.topPosition.y._value < this.state.TOP_LIMIT - this.props.BORDER_WIDTH) {
454 | isOutside = true;
455 | Animated.parallel([
456 | Animated.spring(this.state.topPosition.y, { toValue: this.state.TOP_LIMIT - this.props.BORDER_WIDTH, useNativeDriver: false }),
457 | Animated.spring(this.state.bottomPosition.y, { toValue: this.state.TOP_LIMIT + height, useNativeDriver: false }),
458 | ]).start(() => {
459 | this.isRectangleMoving = false;
460 | });
461 | }
462 | if (width + this.state.leftPosition.x._value + this.props.BORDER_WIDTH > this.props.COMPONENT_WIDTH - this.state.RIGHT_LIMIT) {
463 | isOutside = true;
464 | Animated.parallel([
465 | Animated.spring(this.state.leftPosition.x, {
466 | toValue: this.props.COMPONENT_WIDTH - this.state.RIGHT_LIMIT - width - this.props.BORDER_WIDTH,
467 | useNativeDriver: false,
468 | }),
469 | Animated.spring(this.state.rightPosition.x, { toValue: this.props.COMPONENT_WIDTH - this.state.RIGHT_LIMIT, useNativeDriver: false }),
470 | ]).start(() => {
471 | this.isRectangleMoving = false;
472 | });
473 | }
474 | if (height + this.state.topPosition.y._value + this.props.BORDER_WIDTH > this.props.COMPONENT_HEIGHT - this.state.BOTTOM_LIMIT) {
475 | isOutside = true;
476 | Animated.parallel([
477 | Animated.spring(this.state.topPosition.y, {
478 | toValue: this.props.COMPONENT_HEIGHT - this.state.BOTTOM_LIMIT - height - this.props.BORDER_WIDTH,
479 | useNativeDriver: false,
480 | }),
481 | Animated.spring(this.state.bottomPosition.y, { toValue: this.props.COMPONENT_HEIGHT - this.state.BOTTOM_LIMIT, useNativeDriver: false }),
482 | ]).start(() => {
483 | this.isRectangleMoving = false;
484 | });
485 | }
486 | if (!isOutside) {
487 | this.isRectangleMoving = false;
488 | }
489 | },
490 | onPanResponderGrant: () => {
491 | this.state.topPosition.setOffset({
492 | x: this.state.topPosition.x._value,
493 | y: this.state.topPosition.y._value,
494 | });
495 | this.state.leftPosition.setOffset({
496 | x: this.state.leftPosition.x._value,
497 | y: this.state.leftPosition.y._value,
498 | });
499 | this.state.bottomPosition.setOffset({
500 | x: this.state.bottomPosition.x._value,
501 | y: this.state.bottomPosition.y._value,
502 | });
503 | this.state.rightPosition.setOffset({
504 | x: this.state.rightPosition.x._value,
505 | y: this.state.rightPosition.y._value,
506 | });
507 | this.state.topPosition.setValue({ x: 0, y: 0 });
508 | this.state.leftPosition.setValue({ x: 0, y: 0 });
509 | this.state.bottomPosition.setValue({ x: 0, y: 0 });
510 | this.state.rightPosition.setValue({ x: 0, y: 0 });
511 | },
512 | });
513 | };
514 |
515 | initCornerPanResponder = (pos1: Position, pos2: Position) => {
516 | return PanResponder.create({
517 | onStartShouldSetPanResponder: () => !this.isRectangleMoving,
518 | onPanResponderMove: (_event, gesture) => {
519 | if (this.isAllowedToMove(pos1, gesture)) {
520 | this.state[pos1].setValue({ x: gesture.dx, y: gesture.dy });
521 | }
522 | if (this.isAllowedToMove(pos2, gesture)) {
523 | this.state[pos2].setValue({ x: gesture.dx, y: gesture.dy });
524 | }
525 | },
526 | onPanResponderRelease: () => {
527 | this.state.topPosition.flattenOffset();
528 | this.state.leftPosition.flattenOffset();
529 | this.state.bottomPosition.flattenOffset();
530 | this.state.rightPosition.flattenOffset();
531 | },
532 | onPanResponderGrant: () => {
533 | this.state.topPosition.setOffset({ x: this.state.topPosition.x._value, y: this.state.topPosition.y._value });
534 | this.state.leftPosition.setOffset({ x: this.state.leftPosition.x._value, y: this.state.leftPosition.y._value });
535 | this.state.bottomPosition.setOffset({ x: this.state.bottomPosition.x._value, y: this.state.bottomPosition.y._value });
536 | this.state.rightPosition.setOffset({ x: this.state.rightPosition.x._value, y: this.state.rightPosition.y._value });
537 |
538 | this.state.topPosition.setValue({ x: 0, y: 0 });
539 | this.state.leftPosition.setValue({ x: 0, y: 0 });
540 | this.state.bottomPosition.setValue({ x: 0, y: 0 });
541 | this.state.rightPosition.setValue({ x: 0, y: 0 });
542 | },
543 | });
544 | };
545 |
546 | setCropBoxLimits = ({
547 | TOP_LIMIT,
548 | LEFT_LIMIT,
549 | BOTTOM_LIMIT,
550 | RIGHT_LIMIT,
551 | }: {
552 | TOP_LIMIT: number;
553 | LEFT_LIMIT: number;
554 | BOTTOM_LIMIT: number;
555 | RIGHT_LIMIT: number;
556 | }) => {
557 | this.setState({
558 | TOP_LIMIT,
559 | LEFT_LIMIT,
560 | BOTTOM_LIMIT,
561 | RIGHT_LIMIT,
562 | });
563 | };
564 |
565 | setCropBoxValues = ({
566 | TOP_VALUE,
567 | LEFT_VALUE,
568 | BOTTOM_VALUE,
569 | RIGHT_VALUE,
570 | }: {
571 | TOP_VALUE: number;
572 | LEFT_VALUE: number;
573 | BOTTOM_VALUE: number;
574 | RIGHT_VALUE: number;
575 | }) => {
576 | this.setState({
577 | TOP_VALUE,
578 | LEFT_VALUE,
579 | BOTTOM_VALUE,
580 | RIGHT_VALUE,
581 | });
582 | };
583 |
584 | setCropBoxRotation = (rotation: number) => {
585 | this.setState({ rotation });
586 | };
587 |
588 | rotate90 = () => {
589 | this.setCropBoxRotation((360 + (this.state.rotation - 90)) % 360);
590 | };
591 |
592 | onRotate = () => {
593 | const W_INT = this.W - 2 * this.props.BORDER_WIDTH;
594 | const H_INT = this.H - 2 * this.props.BORDER_WIDTH;
595 | let imageWidth = 0;
596 | let imageHeight = 0;
597 | let rotation = 0;
598 | if (this.state.rotation % 180 === 90) {
599 | imageWidth = this.props.imageWidth > 0 ? this.props.imageWidth : 1280; // 340
600 | imageHeight = this.props.imageHeight > 0 ? this.props.imageHeight : 747; // 500
601 | rotation = 0;
602 | } else {
603 | imageWidth = this.props.COMPONENT_WIDTH - this.state.LEFT_LIMIT - this.state.RIGHT_LIMIT;
604 | imageHeight = this.props.COMPONENT_HEIGHT - this.state.TOP_LIMIT - this.state.BOTTOM_LIMIT;
605 | rotation = 90;
606 | }
607 | const { TOP_LIMIT, LEFT_LIMIT, BOTTOM_LIMIT, RIGHT_LIMIT, DIFF } = getCropperLimits(
608 | imageWidth,
609 | imageHeight,
610 | rotation,
611 | W_INT,
612 | H_INT,
613 | this.W,
614 | this.H,
615 | this.props.BORDER_WIDTH,
616 | Q,
617 | );
618 | this.rotate90();
619 | this.setCropBoxLimits({ TOP_LIMIT, LEFT_LIMIT, BOTTOM_LIMIT, RIGHT_LIMIT });
620 | const startPositionBeforeRotationX = this.state.leftPosition.x._value - this.state.LEFT_LIMIT + this.props.BORDER_WIDTH;
621 | const startPositionBeforeRotationY = this.state.topPosition.y._value - this.state.TOP_LIMIT + this.props.BORDER_WIDTH;
622 | const imageWidthBeforeRotation = this.props.COMPONENT_WIDTH - this.state.RIGHT_LIMIT - this.state.LEFT_LIMIT;
623 | const imageHeightBeforeRotation = this.props.COMPONENT_HEIGHT - this.state.BOTTOM_LIMIT - this.state.TOP_LIMIT;
624 | const rectangleWidthBeforeRotation = this.state.rightPosition.x._value - this.state.leftPosition.x._value - this.props.BORDER_WIDTH;
625 | const rectangleHeightBeforeRotation = this.state.bottomPosition.y._value - this.state.topPosition.y._value - this.props.BORDER_WIDTH;
626 | const imageWidthAfterRotation = this.props.COMPONENT_WIDTH - RIGHT_LIMIT - LEFT_LIMIT;
627 | const imageHeightAfterRotation = this.props.COMPONENT_HEIGHT - BOTTOM_LIMIT - TOP_LIMIT;
628 | const rectangleWidthAfterRotation = (imageWidthAfterRotation * rectangleHeightBeforeRotation) / imageHeightBeforeRotation;
629 | const rectangleHeightAfterRotation = (imageHeightAfterRotation * rectangleWidthBeforeRotation) / imageWidthBeforeRotation;
630 | const startPositionAfterRotationX = (startPositionBeforeRotationY * imageWidthAfterRotation) / imageHeightBeforeRotation;
631 | const startPositionAfterRotationY =
632 | ((imageWidthBeforeRotation - startPositionBeforeRotationX - rectangleWidthBeforeRotation) * imageHeightAfterRotation) /
633 | imageWidthBeforeRotation;
634 |
635 | this.state.topPosition.setValue({
636 | x: LEFT_LIMIT + startPositionAfterRotationX - this.props.BORDER_WIDTH,
637 | y: TOP_LIMIT + startPositionAfterRotationY - this.props.BORDER_WIDTH,
638 | });
639 | this.state.leftPosition.setValue({
640 | x: LEFT_LIMIT + startPositionAfterRotationX - this.props.BORDER_WIDTH,
641 | y: TOP_LIMIT + startPositionAfterRotationY - this.props.BORDER_WIDTH,
642 | });
643 | this.state.bottomPosition.setValue({
644 | x: LEFT_LIMIT + startPositionAfterRotationX - this.props.BORDER_WIDTH,
645 | y: TOP_LIMIT + startPositionAfterRotationY + rectangleHeightAfterRotation,
646 | });
647 | this.state.rightPosition.setValue({
648 | x: LEFT_LIMIT + startPositionAfterRotationX + rectangleWidthAfterRotation,
649 | y: TOP_LIMIT + startPositionAfterRotationY - this.props.BORDER_WIDTH - DIFF / 2,
650 | });
651 | // @ts-ignore
652 | this.topOuter.setNativeProps({ style: { top: TOP_LIMIT, height: 0 } });
653 | // @ts-ignore
654 | this.leftOuter.setNativeProps({ style: { left: LEFT_LIMIT, width: 0 } });
655 | // @ts-ignore
656 | this.bottomOuter.setNativeProps({ style: { top: BOTTOM_LIMIT, height: 0 } });
657 | // @ts-ignore
658 | this.rightOuter.setNativeProps({ style: { top: TOP_LIMIT, height: 0 } });
659 | };
660 |
661 | onDone = () => {
662 | if (this.isRectangleMoving) {
663 | return null;
664 | }
665 |
666 | const IMAGE_W = this.props.COMPONENT_WIDTH - this.state.RIGHT_LIMIT - this.state.LEFT_LIMIT;
667 | const IMAGE_H = this.props.COMPONENT_HEIGHT - this.state.BOTTOM_LIMIT - this.state.TOP_LIMIT;
668 | let x = this.state.leftPosition.x._value - this.state.LEFT_LIMIT + this.props.BORDER_WIDTH;
669 | let y = this.state.topPosition.y._value - this.state.TOP_LIMIT + this.props.BORDER_WIDTH;
670 | let width = this.state.rightPosition.x._value - this.state.leftPosition.x._value - this.props.BORDER_WIDTH;
671 | let height = this.state.bottomPosition.y._value - this.state.topPosition.y._value - this.props.BORDER_WIDTH;
672 | let imageWidth = this.props.imageWidth > 0 ? this.props.imageWidth : 1280; // 340
673 | let imageHeight = this.props.imageHeight > 0 ? this.props.imageHeight : 747; // 500
674 | if (this.state.rotation % 180 === 90) {
675 | const pivot = imageWidth;
676 | imageWidth = imageHeight;
677 | imageHeight = pivot;
678 | }
679 | width = (width * imageWidth) / IMAGE_W;
680 | height = (height * imageHeight) / IMAGE_H;
681 | x = (x * imageWidth) / IMAGE_W;
682 | y = (y * imageHeight) / IMAGE_H;
683 | const cropData = {
684 | offset: { x, y },
685 | size: { width, height },
686 | resizeMode: 'stretch' as 'stretch',
687 | };
688 | const garbageUris: string[] = [];
689 | ImageResizer.createResizedImage(
690 | this.props.imageUri,
691 | this.state.rotation % 180 === 0 ? imageWidth : imageHeight,
692 | this.state.rotation % 180 === 0 ? imageHeight : imageWidth,
693 | 'JPEG',
694 | 100,
695 | this.state.rotation,
696 | undefined,
697 | false,
698 | {
699 | mode: 'cover',
700 | onlyScaleDown: true,
701 | },
702 | )
703 | .then((res) => {
704 | garbageUris.push(res.uri);
705 | return ImageEditor.cropImage(res.uri, cropData);
706 | })
707 | .then((cropResult) => {
708 | this.props.onDone(cropResult.uri, garbageUris);
709 | })
710 | .catch((err: Error) => {
711 | this.props.onError(err);
712 | });
713 | };
714 |
715 | render() {
716 | return (
717 | (this.topOuter = ref)}
751 | leftOuterRef={(ref) => (this.leftOuter = ref)}
752 | bottomOuterRef={(ref) => (this.bottomOuter = ref)}
753 | rightOuterRef={(ref) => (this.rightOuter = ref)}
754 | COMPONENT_WIDTH={this.props.COMPONENT_WIDTH}
755 | COMPONENT_HEIGHT={this.props.COMPONENT_HEIGHT}
756 | W={this.W}
757 | H={this.H}
758 | />
759 | );
760 | }
761 | }
762 |
763 | export default CropperPage;
764 |
--------------------------------------------------------------------------------
/src/Cropper/Cropper.style.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { Q } from '../constants';
3 |
4 | export default function getStyles(COMPONENT_WIDTH: number, COMPONENT_HEIGHT: number, W: number) {
5 | return StyleSheet.create({
6 | container: {
7 | flex: 1,
8 | flexDirection: 'column',
9 | alignItems: 'center',
10 | justifyContent: 'center',
11 | backgroundColor: 'black',
12 | },
13 | secondContainer: {
14 | position: 'absolute',
15 | top: 0,
16 | left: 0,
17 | width: COMPONENT_WIDTH,
18 | height: COMPONENT_HEIGHT,
19 | },
20 | footerContainer: {
21 | position: 'absolute',
22 | top: COMPONENT_HEIGHT - Q,
23 | height: Q,
24 | width: W,
25 | },
26 | gridRow: {
27 | flex: 1,
28 | flexDirection: 'row',
29 | },
30 | gridColumn: {
31 | flex: 1,
32 | borderWidth: 1,
33 | borderColor: 'rgba(255, 255, 255, 0.5)',
34 | },
35 | animation: {
36 | position: 'absolute',
37 | backgroundColor: 'transparent',
38 | },
39 | topSideAnimation: {
40 | borderBottomWidth: 20,
41 | borderColor: 'transparent',
42 | zIndex: 4,
43 | },
44 | leftSideAnimation: {
45 | borderRightWidth: 20,
46 | borderColor: 'transparent',
47 | zIndex: 4,
48 | },
49 | bottomSideAnimation: {
50 | borderTopWidth: 20,
51 | borderColor: 'transparent',
52 | zIndex: 4,
53 | transform: [{ translateY: -20 }],
54 | },
55 | rightSideAnimation: {
56 | borderLeftWidth: 20,
57 | borderColor: 'transparent',
58 | zIndex: 4,
59 | transform: [{ translateX: -20 }],
60 | },
61 | topLeftAnimation: {
62 | borderLeftWidth: 56,
63 | borderRightWidth: 25,
64 | borderTopWidth: 31,
65 | borderColor: 'transparent',
66 | zIndex: 5,
67 | },
68 | bottomLeftAnimation: {
69 | borderLeftWidth: 56,
70 | borderRightWidth: 25,
71 | borderBottomWidth: 31,
72 | borderColor: 'transparent',
73 | zIndex: 5,
74 | transform: [{ translateY: -31 }],
75 | },
76 | bottomRightAnimation: {
77 | borderTopWidth: 25,
78 | borderRightWidth: 31,
79 | borderBottomWidth: 56,
80 | borderColor: 'transparent',
81 | zIndex: 5,
82 | transform: [{ translateX: -31 }, { translateY: -31 }],
83 | },
84 | topRightAnimation: {
85 | borderColor: 'transparent',
86 | borderTopWidth: 56,
87 | borderRightWidth: 31,
88 | borderBottomWidth: 25,
89 | zIndex: 5,
90 | transform: [{ translateX: -31 }],
91 | },
92 | borderDesign: {
93 | width: 30,
94 | height: 30,
95 | borderColor: 'white',
96 | },
97 | icon: {
98 | paddingRight: 10,
99 | flexDirection: 'row',
100 | },
101 | zoomNavBar: {
102 | width: '100%',
103 | height: 50,
104 | backgroundColor: '#5a2480',
105 | alignItems: 'center',
106 | position: 'absolute',
107 | flexDirection: 'row',
108 | justifyContent: 'space-between',
109 | bottom: 20,
110 | borderTopLeftRadius: 20,
111 | borderTopRightRadius: 20,
112 | paddingHorizontal: 20,
113 | },
114 | rightNav: {
115 | flexDirection: 'row',
116 | },
117 | });
118 | }
119 |
--------------------------------------------------------------------------------
/src/Cropper/Cropper.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Animated, Image, PanResponderInstance } from 'react-native';
3 | import getStyles from './Cropper.style';
4 |
5 | interface CropperProps {
6 | imageUri: string;
7 | footerComponent: JSX.Element;
8 | getTopOuterStyle: () => object;
9 | getLeftOuterStyle: () => object;
10 | getBottomOuterStyle: () => object;
11 | getRightOuterStyle: () => object;
12 | getTopLeftStyle: () => object;
13 | getBottomLeftStyle: () => object;
14 | getBottomRightStyle: () => object;
15 | getTopRightStyle: () => object;
16 | getTopSideStyle: () => object;
17 | getLeftSideStyle: () => object;
18 | getBottomSideStyle: () => object;
19 | getRightSideStyle: () => object;
20 | getRectangleStyle: () => object;
21 | getImageStyle: () => object;
22 | onDone: () => void;
23 | onRotate: () => void;
24 | onCancel: () => void;
25 | topOuterPanResponder: PanResponderInstance;
26 | leftOuterPanResponder: PanResponderInstance;
27 | bottomOuterPanResponder: PanResponderInstance;
28 | rightOuterPanResponder: PanResponderInstance;
29 | topPanResponder: PanResponderInstance;
30 | leftPanResponder: PanResponderInstance;
31 | bottomPanResponder: PanResponderInstance;
32 | rightPanResponder: PanResponderInstance;
33 | topLeftPanResponder: PanResponderInstance;
34 | bottomLeftPanResponder: PanResponderInstance;
35 | bottomRightPanResponder: PanResponderInstance;
36 | topRightPanResponder: PanResponderInstance;
37 | rectanglePanResponder: PanResponderInstance;
38 | topOuterRef: (ref: any) => any;
39 | leftOuterRef: (ref: any) => any;
40 | bottomOuterRef: (ref: any) => any;
41 | rightOuterRef: (ref: any) => any;
42 | COMPONENT_WIDTH: number;
43 | COMPONENT_HEIGHT: number;
44 | W: number;
45 | H: number;
46 | }
47 |
48 | const Cropper: React.FC = props => {
49 | const styles = getStyles(props.COMPONENT_WIDTH, props.COMPONENT_HEIGHT, props.W);
50 | return (
51 |
52 |
53 |
54 |
55 |
56 |
57 | {React.cloneElement(props.footerComponent, {
58 | onDone: props.onDone,
59 | onRotate: props.onRotate,
60 | onCancel: props.onCancel,
61 | })}
62 |
63 | {/*
64 | // @ts-ignore */}
65 |
66 | {/*
67 | // @ts-ignore */}
68 |
69 | {/*
70 | // @ts-ignore */ /* eslint-disable-line */ /* eslint-disable-next-line prettier/prettier */}
71 |
72 | {/*
73 | // @ts-ignore */}
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | {/* eslint-disable-next-line prettier/prettier */}
83 |
84 | {/* eslint-disable-next-line prettier/prettier */}
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
129 |
130 |
131 |
132 |
133 | );
134 | };
135 |
136 | export default Cropper;
137 |
--------------------------------------------------------------------------------
/src/Main.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import CropperPage from './Cropper/Cropper.page';
3 | import { DefaultFooter } from './common';
4 | import { SCREEN_WIDTH, SCREEN_HEIGHT } from './constants';
5 |
6 | export type AmazingCropperProps = {
7 | footerComponent?: JSX.Element;
8 | onDone: (croppedImageUri: string, garbageUris: string[]) => void;
9 | onError: (err: Error) => void;
10 | onCancel: () => void;
11 | imageUri: string;
12 | imageWidth: number;
13 | imageHeight: number;
14 | TOP_VALUE?: number;
15 | LEFT_VALUE?: number;
16 | BOTTOM_VALUE?: number;
17 | RIGHT_VALUE?: number;
18 | initialRotation?: number;
19 | NOT_SELECTED_AREA_OPACITY?: number;
20 | BORDER_WIDTH?: number;
21 | COMPONENT_WIDTH?: number;
22 | COMPONENT_HEIGHT?: number;
23 | } & typeof defaultProps;
24 |
25 | const defaultProps = {
26 | footerComponent: ,
27 | onDone: (_croppedImageUri: string, _garbageUris: string[]) => {},
28 | onError: (_err: Error) => {},
29 | onCancel: () => {},
30 | imageUri: '',
31 | imageWidth: 1280,
32 | imageHeight: 747,
33 | TOP_VALUE: 0,
34 | LEFT_VALUE: 0,
35 | BOTTOM_VALUE: 0,
36 | RIGHT_VALUE: 0,
37 | initialRotation: 0,
38 | NOT_SELECTED_AREA_OPACITY: 0.5,
39 | BORDER_WIDTH: 50,
40 | COMPONENT_WIDTH: SCREEN_WIDTH,
41 | COMPONENT_HEIGHT: SCREEN_HEIGHT,
42 | };
43 |
44 | class Main extends Component {
45 | static defaultProps = defaultProps;
46 |
47 | render() {
48 | return (
49 |
67 | );
68 | }
69 | }
70 |
71 | export default Main;
72 |
--------------------------------------------------------------------------------
/src/common/DefaultFooter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
3 |
4 | export type DefaultFooterProps = {
5 | onDone?: () => any;
6 | onRotate?: () => any;
7 | onCancel?: () => any;
8 | doneText: string;
9 | rotateText: string;
10 | cancelText: string;
11 | };
12 |
13 | const DefaultFooter: React.FC = props => (
14 |
15 |
16 | {props.cancelText}
17 |
18 |
19 | {props.rotateText}
20 |
21 |
22 | {props.doneText}
23 |
24 |
25 | );
26 |
27 | export { DefaultFooter };
28 |
29 | const styles = StyleSheet.create({
30 | buttonsContainer: {
31 | flexDirection: 'row',
32 | alignItems: 'center',
33 | justifyContent: 'space-between',
34 | height: '100%',
35 | },
36 | text: {
37 | color: 'white',
38 | fontSize: 16,
39 | },
40 | touchable: {
41 | padding: 10,
42 | },
43 | });
44 |
--------------------------------------------------------------------------------
/src/common/index.ts:
--------------------------------------------------------------------------------
1 | export * from './DefaultFooter';
2 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | import { Dimensions } from 'react-native';
2 |
3 | export const SCREEN_WIDTH = Dimensions.get('window').width;
4 | export const SCREEN_HEIGHT = Dimensions.get('window').height;
5 | export const Q = 100; // buttons container height
6 |
--------------------------------------------------------------------------------
/src/utils/cropper.ts:
--------------------------------------------------------------------------------
1 | function getCropperLimitsIfHorizontally(
2 | imageWidth: number,
3 | imageHeight: number,
4 | W_INT: number,
5 | H_INT: number,
6 | _W: number,
7 | _H: number,
8 | BW: number,
9 | Q: number,
10 | ) {
11 | let TOP_LIMIT = 0;
12 | let LEFT_LIMIT = 0;
13 | let BOTTOM_LIMIT = 0;
14 | let RIGHT_LIMIT = 0;
15 | const DIFF = 0;
16 |
17 | let w = 0;
18 | let h = 0;
19 | let DIST = 0;
20 | let TOTAL_DIST = 0;
21 |
22 | const w1 = W_INT;
23 | const h1 = (W_INT * imageHeight) / imageWidth;
24 | const w2 = (H_INT * imageWidth) / imageHeight;
25 | const h2 = H_INT;
26 | if (h1 <= H_INT) {
27 | h = h1;
28 | w = w1;
29 | DIST = (H_INT - h) / 2;
30 | TOTAL_DIST = DIST + BW;
31 | TOP_LIMIT = TOTAL_DIST;
32 | BOTTOM_LIMIT = TOTAL_DIST + Q;
33 | LEFT_LIMIT = BW;
34 | RIGHT_LIMIT = BW;
35 | } else {
36 | h = h2;
37 | w = w2;
38 | DIST = (W_INT - w) / 2;
39 | TOTAL_DIST = DIST + BW;
40 | TOP_LIMIT = BW;
41 | BOTTOM_LIMIT = BW + Q;
42 | LEFT_LIMIT = TOTAL_DIST;
43 | RIGHT_LIMIT = TOTAL_DIST;
44 | }
45 |
46 | return { TOP_LIMIT, LEFT_LIMIT, BOTTOM_LIMIT, RIGHT_LIMIT, DIFF };
47 | }
48 |
49 | function getCropperLimitsIfVertically(
50 | imageWidth: number,
51 | imageHeight: number,
52 | W_INT: number,
53 | H_INT: number,
54 | W: number,
55 | H: number,
56 | _BW: number,
57 | Q: number,
58 | ) {
59 | let TOP_LIMIT = 0;
60 | let LEFT_LIMIT = 0;
61 | let BOTTOM_LIMIT = 0;
62 | let RIGHT_LIMIT = 0;
63 | let DIFF = 0;
64 |
65 | let IMAGE_W = 0;
66 | let IMAGE_H = 0;
67 | const IMAGE_W_1 = W_INT;
68 | const IMAGE_H_1 = (W_INT * imageHeight) / imageWidth;
69 | const IMAGE_W_2 = (H_INT * imageWidth) / imageHeight;
70 | const IMAGE_H_2 = H_INT;
71 | if (IMAGE_H_1 <= H_INT) {
72 | IMAGE_H = IMAGE_H_1;
73 | IMAGE_W = IMAGE_W_1;
74 | } else {
75 | IMAGE_H = IMAGE_H_2;
76 | IMAGE_W = IMAGE_W_2;
77 | }
78 |
79 | let w = 0;
80 | let h = 0;
81 | const h1 = W_INT;
82 | const w1 = (IMAGE_W * h1) / IMAGE_H;
83 | const w2 = H_INT;
84 | const h2 = (IMAGE_H * w2) / IMAGE_W;
85 | if (w1 <= H_INT) {
86 | w = w1;
87 | h = h1;
88 | } else {
89 | w = w2;
90 | h = h2;
91 | }
92 | const Tnew = (H - h) / 2;
93 | const Bnew = Tnew + Q;
94 | const Lnew = (W - w) / 2;
95 | const Rnew = Lnew;
96 | DIFF = h - w;
97 | TOP_LIMIT = Tnew + DIFF / 2;
98 | LEFT_LIMIT = Lnew - DIFF / 2;
99 | BOTTOM_LIMIT = Bnew + DIFF / 2;
100 | RIGHT_LIMIT = Rnew - DIFF / 2;
101 |
102 | return { TOP_LIMIT, LEFT_LIMIT, BOTTOM_LIMIT, RIGHT_LIMIT, DIFF };
103 | }
104 |
105 | function getCropperLimits(
106 | imageWidth: number,
107 | imageHeight: number,
108 | rotation: number,
109 | W_INT: number,
110 | H_INT: number,
111 | W: number,
112 | H: number,
113 | BW: number,
114 | Q: number,
115 | ) {
116 | if (rotation % 180 === 0) {
117 | return getCropperLimitsIfHorizontally(imageWidth, imageHeight, W_INT, H_INT, W, H, BW, Q);
118 | }
119 | return getCropperLimitsIfVertically(imageWidth, imageHeight, W_INT, H_INT, W, H, BW, Q);
120 | }
121 |
122 | export { getCropperLimits };
123 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './cropper';
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "compilerOptions": {
4 | /* Basic Options */
5 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
6 | "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
7 | "lib": ["es6"], /* Specify library files to be included in the compilation. */
8 | "allowJs": true, /* Allow javascript files to be compiled. */
9 | // "checkJs": true, /* Report errors in .js files. */
10 | "jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11 | "declaration": true, /* Generates corresponding '.d.ts' file. */
12 | // "sourceMap": true, /* Generates corresponding '.map' file. */
13 | // "outFile": "./", /* Concatenate and emit output to single file. */
14 | "outDir": "dist", /* Redirect output structure to the directory. */
15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
16 | // "removeComments": true, /* Do not emit comments to output. */
17 | // "noEmit": true, /* Do not emit outputs. */
18 | "incremental": true, /* Enable incremental compilation */
19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
21 | "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
22 |
23 | /* Strict Type-Checking Options */
24 | "strict": true, /* Enable all strict type-checking options. */
25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
26 | // "strictNullChecks": true, /* Enable strict null checks. */
27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
28 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
29 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
30 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
31 |
32 | /* Additional Checks */
33 | // "noUnusedLocals": true, /* Report errors on unused locals. */
34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
35 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
37 |
38 | /* Module Resolution Options */
39 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
40 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
42 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
43 | // "typeRoots": [], /* List of folders to include type definitions from. */
44 | // "types": [], /* Type declaration files to be included in compilation. */
45 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
46 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
47 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
48 |
49 | /* Source Map Options */
50 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
51 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
52 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
53 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
54 |
55 | /* Experimental Options */
56 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
57 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
58 | },
59 | "exclude": [
60 | "node_modules", "dist", "babel.config.js", "metro.config.js", "jest.config.js"
61 | ]
62 | }
63 |
--------------------------------------------------------------------------------