react-native-rectangle-doc-scanner 3.27.0 → 3.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CropEditor.js +11 -6
- package/dist/DocScanner.d.ts +1 -0
- package/dist/DocScanner.js +10 -5
- package/dist/FullDocScanner.js +36 -7
- package/dist/types.d.ts +1 -0
- package/dist/utils/overlay.js +25 -105
- package/package.json +1 -1
- package/src/CropEditor.tsx +15 -8
- package/src/DocScanner.tsx +14 -6
- package/src/FullDocScanner.tsx +62 -10
- package/src/types.ts +1 -0
- package/src/utils/overlay.tsx +39 -158
package/dist/CropEditor.js
CHANGED
|
@@ -83,17 +83,22 @@ const CropEditor = ({ document, overlayColor = 'rgba(0,0,0,0.5)', overlayStrokeC
|
|
|
83
83
|
}, [document]);
|
|
84
84
|
// Get initial rectangle from detected quad or use default
|
|
85
85
|
const getInitialRectangle = (0, react_1.useCallback)(() => {
|
|
86
|
-
if (!
|
|
86
|
+
if (!imageSize) {
|
|
87
87
|
return undefined;
|
|
88
88
|
}
|
|
89
|
-
const
|
|
90
|
-
|
|
89
|
+
const baseWidth = document.width > 0 ? document.width : imageSize.width;
|
|
90
|
+
const baseHeight = document.height > 0 ? document.height : imageSize.height;
|
|
91
|
+
const sourceRectangle = document.rectangle
|
|
92
|
+
? document.rectangle
|
|
93
|
+
: document.quad && document.quad.length === 4
|
|
94
|
+
? (0, coordinate_1.quadToRectangle)(document.quad)
|
|
95
|
+
: null;
|
|
96
|
+
if (!sourceRectangle) {
|
|
91
97
|
return undefined;
|
|
92
98
|
}
|
|
93
|
-
|
|
94
|
-
const scaled = (0, coordinate_1.scaleRectangle)(rect, document.width, document.height, imageSize.width, imageSize.height);
|
|
99
|
+
const scaled = (0, coordinate_1.scaleRectangle)(sourceRectangle, baseWidth, baseHeight, imageSize.width, imageSize.height);
|
|
95
100
|
return scaled;
|
|
96
|
-
}, [document.quad, document.width, document.height, imageSize]);
|
|
101
|
+
}, [document.rectangle, document.quad, document.width, document.height, imageSize]);
|
|
97
102
|
const handleImageLoad = (0, react_1.useCallback)((event) => {
|
|
98
103
|
// This is just for debugging - actual size is loaded via Image.getSize in useEffect
|
|
99
104
|
console.log('[CropEditor] Image onLoad event triggered');
|
package/dist/DocScanner.d.ts
CHANGED
package/dist/DocScanner.js
CHANGED
|
@@ -73,6 +73,7 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
73
73
|
const captureResolvers = (0, react_1.useRef)(null);
|
|
74
74
|
const [isAutoCapturing, setIsAutoCapturing] = (0, react_1.useState)(false);
|
|
75
75
|
const [detectedRectangle, setDetectedRectangle] = (0, react_1.useState)(null);
|
|
76
|
+
const lastRectangleRef = (0, react_1.useRef)(null);
|
|
76
77
|
(0, react_1.useEffect)(() => {
|
|
77
78
|
if (!autoCapture) {
|
|
78
79
|
setIsAutoCapturing(false);
|
|
@@ -87,17 +88,18 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
87
88
|
}, [quality]);
|
|
88
89
|
const handlePictureTaken = (0, react_1.useCallback)((event) => {
|
|
89
90
|
setIsAutoCapturing(false);
|
|
90
|
-
const normalizedRectangle = normalizeRectangle(event.rectangleCoordinates ?? null);
|
|
91
|
+
const normalizedRectangle = normalizeRectangle(event.rectangleCoordinates ?? null) ?? lastRectangleRef.current;
|
|
91
92
|
const quad = normalizedRectangle ? (0, coordinate_1.rectangleToQuad)(normalizedRectangle) : null;
|
|
92
93
|
const initialPath = event.initialImage ?? null;
|
|
93
94
|
const croppedPath = event.croppedImage ?? null;
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
95
|
+
const editablePath = initialPath ?? croppedPath;
|
|
96
|
+
if (editablePath) {
|
|
96
97
|
onCapture?.({
|
|
97
|
-
path:
|
|
98
|
+
path: editablePath,
|
|
98
99
|
initialPath,
|
|
99
100
|
croppedPath,
|
|
100
101
|
quad,
|
|
102
|
+
rectangle: normalizedRectangle,
|
|
101
103
|
width: event.width ?? 0,
|
|
102
104
|
height: event.height ?? 0,
|
|
103
105
|
});
|
|
@@ -157,6 +159,9 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
157
159
|
setIsAutoCapturing(false);
|
|
158
160
|
}
|
|
159
161
|
}
|
|
162
|
+
if (payload.rectangleCoordinates) {
|
|
163
|
+
lastRectangleRef.current = payload.rectangleCoordinates;
|
|
164
|
+
}
|
|
160
165
|
const isGoodRectangle = payload.lastDetectionType === 0;
|
|
161
166
|
setDetectedRectangle(isGoodRectangle && rectangleOnScreen ? payload : null);
|
|
162
167
|
onRectangleDetect?.(payload);
|
|
@@ -170,7 +175,7 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
170
175
|
}
|
|
171
176
|
},
|
|
172
177
|
}), [capture]);
|
|
173
|
-
const overlayPolygon = detectedRectangle?.rectangleOnScreen ?? null;
|
|
178
|
+
const overlayPolygon = detectedRectangle?.rectangleOnScreen ?? detectedRectangle?.rectangleCoordinates ?? null;
|
|
174
179
|
const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
|
|
175
180
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
176
181
|
react_1.default.createElement(react_native_document_scanner_1.default, { ref: scannerRef, style: styles.scanner, detectionCountBeforeCapture: minStableFrames, overlayColor: overlayColor, enableTorch: enableTorch, quality: normalizedQuality, useBase64: useBase64, manualOnly: !autoCapture, detectionConfig: detectionConfig, onPictureTaken: handlePictureTaken, onError: handleError, onRectangleDetect: handleRectangleDetect }),
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -63,6 +63,26 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
63
63
|
}
|
|
64
64
|
react_native_1.Image.getSize(ensureFileUri(capturedDoc.path), (width, height) => setImageSize({ width, height }), () => setImageSize({ width: capturedDoc.width, height: capturedDoc.height }));
|
|
65
65
|
}, [capturedDoc]);
|
|
66
|
+
(0, react_1.useEffect)(() => {
|
|
67
|
+
if (!capturedDoc || !imageSize || cropRectangle) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : imageSize.width;
|
|
71
|
+
const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : imageSize.height;
|
|
72
|
+
let initialRectangle = null;
|
|
73
|
+
if (capturedDoc.rectangle) {
|
|
74
|
+
initialRectangle = (0, coordinate_1.scaleRectangle)(capturedDoc.rectangle, baseWidth, baseHeight, imageSize.width, imageSize.height);
|
|
75
|
+
}
|
|
76
|
+
else if (capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
77
|
+
const quadRectangle = (0, coordinate_1.quadToRectangle)(capturedDoc.quad);
|
|
78
|
+
if (quadRectangle) {
|
|
79
|
+
initialRectangle = (0, coordinate_1.scaleRectangle)(quadRectangle, baseWidth, baseHeight, imageSize.width, imageSize.height);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (initialRectangle) {
|
|
83
|
+
setCropRectangle(initialRectangle);
|
|
84
|
+
}
|
|
85
|
+
}, [capturedDoc, imageSize, cropRectangle]);
|
|
66
86
|
const resetState = (0, react_1.useCallback)(() => {
|
|
67
87
|
setScreen('scanner');
|
|
68
88
|
setCapturedDoc(null);
|
|
@@ -73,6 +93,8 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
73
93
|
const handleCapture = (0, react_1.useCallback)((document) => {
|
|
74
94
|
const normalizedPath = stripFileUri(document.path);
|
|
75
95
|
const nextQuad = document.quad && document.quad.length === 4 ? document.quad : null;
|
|
96
|
+
const quadRectangle = nextQuad ? (0, coordinate_1.quadToRectangle)(nextQuad) : null;
|
|
97
|
+
const nextRectangle = document.rectangle ?? quadRectangle ?? null;
|
|
76
98
|
const normalizedInitial = document.initialPath != null ? stripFileUri(document.initialPath) : normalizedPath;
|
|
77
99
|
const normalizedCropped = document.croppedPath != null ? stripFileUri(document.croppedPath) : null;
|
|
78
100
|
setCapturedDoc({
|
|
@@ -81,8 +103,9 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
81
103
|
initialPath: normalizedInitial,
|
|
82
104
|
croppedPath: normalizedCropped,
|
|
83
105
|
quad: nextQuad,
|
|
106
|
+
rectangle: nextRectangle,
|
|
84
107
|
});
|
|
85
|
-
setCropRectangle(
|
|
108
|
+
setCropRectangle(null);
|
|
86
109
|
setScreen('crop');
|
|
87
110
|
}, []);
|
|
88
111
|
const handleCropChange = (0, react_1.useCallback)((rectangle) => {
|
|
@@ -104,13 +127,19 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
104
127
|
if (!cropManager?.crop) {
|
|
105
128
|
throw new Error('CustomCropManager.crop is not available');
|
|
106
129
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
130
|
+
const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : size.width;
|
|
131
|
+
const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : size.height;
|
|
132
|
+
const rectangleFromDetection = capturedDoc.rectangle
|
|
133
|
+
? (0, coordinate_1.scaleRectangle)(capturedDoc.rectangle, baseWidth, baseHeight, size.width, size.height)
|
|
109
134
|
: null;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
135
|
+
let fallbackRectangle = rectangleFromDetection;
|
|
136
|
+
if (!fallbackRectangle && capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
137
|
+
const quadRectangle = (0, coordinate_1.quadToRectangle)(capturedDoc.quad);
|
|
138
|
+
if (quadRectangle) {
|
|
139
|
+
fallbackRectangle = (0, coordinate_1.scaleRectangle)(quadRectangle, baseWidth, baseHeight, size.width, size.height);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const rectangle = cropRectangle ?? fallbackRectangle;
|
|
114
143
|
const base64 = await new Promise((resolve, reject) => {
|
|
115
144
|
cropManager.crop({
|
|
116
145
|
topLeft: rectangle?.topLeft ?? { x: 0, y: 0 },
|
package/dist/types.d.ts
CHANGED
package/dist/utils/overlay.js
CHANGED
|
@@ -44,22 +44,7 @@ try {
|
|
|
44
44
|
catch (error) {
|
|
45
45
|
SvgModule = null;
|
|
46
46
|
}
|
|
47
|
-
const SCAN_DURATION_MS = 2200;
|
|
48
47
|
const GRID_STEPS = [1 / 3, 2 / 3];
|
|
49
|
-
const calculateMetrics = (polygon) => {
|
|
50
|
-
const minX = Math.min(polygon.topLeft.x, polygon.bottomLeft.x, polygon.topRight.x, polygon.bottomRight.x);
|
|
51
|
-
const maxX = Math.max(polygon.topLeft.x, polygon.bottomLeft.x, polygon.topRight.x, polygon.bottomRight.x);
|
|
52
|
-
const minY = Math.min(polygon.topLeft.y, polygon.topRight.y, polygon.bottomLeft.y, polygon.bottomRight.y);
|
|
53
|
-
const maxY = Math.max(polygon.topLeft.y, polygon.topRight.y, polygon.bottomLeft.y, polygon.bottomRight.y);
|
|
54
|
-
return {
|
|
55
|
-
minX,
|
|
56
|
-
maxX,
|
|
57
|
-
minY,
|
|
58
|
-
maxY,
|
|
59
|
-
width: maxX - minX,
|
|
60
|
-
height: maxY - minY,
|
|
61
|
-
};
|
|
62
|
-
};
|
|
63
48
|
const createPointsString = (polygon) => [
|
|
64
49
|
`${polygon.topLeft.x},${polygon.topLeft.y}`,
|
|
65
50
|
`${polygon.topRight.x},${polygon.topRight.y}`,
|
|
@@ -80,116 +65,51 @@ const createGridLines = (polygon) => GRID_STEPS.flatMap((step) => {
|
|
|
80
65
|
{ x1: verticalStart.x, y1: verticalStart.y, x2: verticalEnd.x, y2: verticalEnd.y },
|
|
81
66
|
];
|
|
82
67
|
});
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
(0, react_1.
|
|
100
|
-
|
|
101
|
-
scanProgress.setValue(0);
|
|
102
|
-
setScanY(null);
|
|
103
|
-
if (!active || !metrics || travelDistance <= 0) {
|
|
104
|
-
return undefined;
|
|
105
|
-
}
|
|
106
|
-
const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
|
|
107
|
-
react_native_1.Animated.timing(scanProgress, {
|
|
108
|
-
toValue: 1,
|
|
109
|
-
duration: SCAN_DURATION_MS,
|
|
110
|
-
easing: react_native_1.Easing.inOut(react_native_1.Easing.quad),
|
|
111
|
-
useNativeDriver: false,
|
|
112
|
-
}),
|
|
113
|
-
react_native_1.Animated.timing(scanProgress, {
|
|
114
|
-
toValue: 0,
|
|
115
|
-
duration: SCAN_DURATION_MS,
|
|
116
|
-
easing: react_native_1.Easing.inOut(react_native_1.Easing.quad),
|
|
117
|
-
useNativeDriver: false,
|
|
118
|
-
}),
|
|
119
|
-
]));
|
|
120
|
-
loop.start();
|
|
121
|
-
return () => {
|
|
122
|
-
loop.stop();
|
|
123
|
-
scanProgress.stopAnimation();
|
|
124
|
-
};
|
|
125
|
-
}, [active, metrics, scanProgress, travelDistance]);
|
|
126
|
-
(0, react_1.useEffect)(() => {
|
|
127
|
-
if (!metrics || travelDistance <= 0) {
|
|
128
|
-
setScanY(null);
|
|
129
|
-
return undefined;
|
|
130
|
-
}
|
|
131
|
-
const listenerId = scanProgress.addListener(({ value }) => {
|
|
132
|
-
const nextValue = metrics.minY + travelDistance * value;
|
|
133
|
-
if (Number.isFinite(nextValue)) {
|
|
134
|
-
setScanY(nextValue);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
return () => {
|
|
138
|
-
scanProgress.removeListener(listenerId);
|
|
139
|
-
};
|
|
140
|
-
}, [metrics, scanProgress, travelDistance]);
|
|
141
|
-
if (!polygon || !metrics || metrics.width <= 0 || metrics.height <= 0) {
|
|
68
|
+
const getBounds = (polygon) => {
|
|
69
|
+
const minX = Math.min(polygon.topLeft.x, polygon.bottomLeft.x, polygon.topRight.x, polygon.bottomRight.x);
|
|
70
|
+
const maxX = Math.max(polygon.topLeft.x, polygon.bottomLeft.x, polygon.topRight.x, polygon.bottomRight.x);
|
|
71
|
+
const minY = Math.min(polygon.topLeft.y, polygon.topRight.y, polygon.bottomLeft.y, polygon.bottomRight.y);
|
|
72
|
+
const maxY = Math.max(polygon.topLeft.y, polygon.topRight.y, polygon.bottomLeft.y, polygon.bottomRight.y);
|
|
73
|
+
return {
|
|
74
|
+
minX,
|
|
75
|
+
minY,
|
|
76
|
+
width: maxX - minX,
|
|
77
|
+
height: maxY - minY,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const ScannerOverlay = ({ active: _active, // kept for compatibility; no animation currently
|
|
81
|
+
color = '#0b7ef4', lineWidth = react_native_1.StyleSheet.hairlineWidth, polygon, }) => {
|
|
82
|
+
const points = (0, react_1.useMemo)(() => (polygon ? createPointsString(polygon) : null), [polygon]);
|
|
83
|
+
const gridLines = (0, react_1.useMemo)(() => (polygon ? createGridLines(polygon) : []), [polygon]);
|
|
84
|
+
const bounds = (0, react_1.useMemo)(() => (polygon ? getBounds(polygon) : null), [polygon]);
|
|
85
|
+
if (!polygon || !points || !bounds) {
|
|
142
86
|
return null;
|
|
143
87
|
}
|
|
144
88
|
if (SvgModule) {
|
|
145
|
-
const { default: Svg, Polygon, Line
|
|
146
|
-
const gridLines = createGridLines(polygon);
|
|
147
|
-
const points = createPointsString(polygon);
|
|
148
|
-
const scanRectY = scanY ?? metrics.minY;
|
|
89
|
+
const { default: Svg, Polygon, Line } = SvgModule;
|
|
149
90
|
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: react_native_1.StyleSheet.absoluteFill },
|
|
150
91
|
react_1.default.createElement(Svg, { style: react_native_1.StyleSheet.absoluteFill },
|
|
151
92
|
react_1.default.createElement(Polygon, { points: points, fill: color, opacity: 0.15 }),
|
|
152
93
|
gridLines.map((line, index) => (react_1.default.createElement(Line, { key: `grid-${index}`, x1: line.x1, y1: line.y1, x2: line.x2, y2: line.y2, stroke: color, strokeWidth: lineWidth, opacity: 0.5 }))),
|
|
153
|
-
react_1.default.createElement(Polygon, { points: points, stroke: color, strokeWidth: lineWidth, fill: "none" })
|
|
154
|
-
react_1.default.createElement(Defs, null,
|
|
155
|
-
react_1.default.createElement(LinearGradient, { id: "scanGradient", x1: "0", y1: "0", x2: "0", y2: "1" },
|
|
156
|
-
react_1.default.createElement(Stop, { offset: "0%", stopColor: "rgba(255,255,255,0)" }),
|
|
157
|
-
react_1.default.createElement(Stop, { offset: "50%", stopColor: color, stopOpacity: 0.8 }),
|
|
158
|
-
react_1.default.createElement(Stop, { offset: "100%", stopColor: "rgba(255,255,255,0)" }))),
|
|
159
|
-
active && travelDistance > 0 && Number.isFinite(scanRectY) && (react_1.default.createElement(Rect, { x: metrics.minX, width: metrics.width, height: scanBarHeight, fill: "url(#scanGradient)", y: scanRectY })))));
|
|
94
|
+
react_1.default.createElement(Polygon, { points: points, stroke: color, strokeWidth: lineWidth, fill: "none" }))));
|
|
160
95
|
}
|
|
161
|
-
const relativeTranslate = metrics && travelDistance > 0
|
|
162
|
-
? react_native_1.Animated.multiply(scanProgress, travelDistance)
|
|
163
|
-
: fallbackBase;
|
|
164
96
|
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: react_native_1.StyleSheet.absoluteFill },
|
|
165
97
|
react_1.default.createElement(react_native_1.View, { style: [
|
|
166
98
|
styles.fallbackBox,
|
|
167
99
|
{
|
|
168
|
-
left:
|
|
169
|
-
top:
|
|
170
|
-
width:
|
|
171
|
-
height:
|
|
100
|
+
left: bounds.minX,
|
|
101
|
+
top: bounds.minY,
|
|
102
|
+
width: bounds.width,
|
|
103
|
+
height: bounds.height,
|
|
172
104
|
borderColor: color,
|
|
173
105
|
borderWidth: lineWidth,
|
|
174
106
|
},
|
|
175
|
-
] }
|
|
176
|
-
styles.fallbackScanBar,
|
|
177
|
-
{
|
|
178
|
-
backgroundColor: color,
|
|
179
|
-
height: scanBarHeight,
|
|
180
|
-
transform: [{ translateY: relativeTranslate }],
|
|
181
|
-
},
|
|
182
|
-
] })))));
|
|
107
|
+
] })));
|
|
183
108
|
};
|
|
184
109
|
exports.ScannerOverlay = ScannerOverlay;
|
|
185
110
|
const styles = react_native_1.StyleSheet.create({
|
|
186
111
|
fallbackBox: {
|
|
187
112
|
position: 'absolute',
|
|
188
113
|
backgroundColor: 'rgba(11, 126, 244, 0.1)',
|
|
189
|
-
overflow: 'hidden',
|
|
190
|
-
},
|
|
191
|
-
fallbackScanBar: {
|
|
192
|
-
width: '100%',
|
|
193
|
-
opacity: 0.4,
|
|
194
114
|
},
|
|
195
115
|
});
|
package/package.json
CHANGED
package/src/CropEditor.tsx
CHANGED
|
@@ -70,26 +70,33 @@ export const CropEditor: React.FC<CropEditorProps> = ({
|
|
|
70
70
|
|
|
71
71
|
// Get initial rectangle from detected quad or use default
|
|
72
72
|
const getInitialRectangle = useCallback((): CropperRectangle | undefined => {
|
|
73
|
-
if (!
|
|
73
|
+
if (!imageSize) {
|
|
74
74
|
return undefined;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const
|
|
78
|
-
|
|
77
|
+
const baseWidth = document.width > 0 ? document.width : imageSize.width;
|
|
78
|
+
const baseHeight = document.height > 0 ? document.height : imageSize.height;
|
|
79
|
+
|
|
80
|
+
const sourceRectangle = document.rectangle
|
|
81
|
+
? document.rectangle
|
|
82
|
+
: document.quad && document.quad.length === 4
|
|
83
|
+
? quadToRectangle(document.quad)
|
|
84
|
+
: null;
|
|
85
|
+
|
|
86
|
+
if (!sourceRectangle) {
|
|
79
87
|
return undefined;
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
// Scale from original detection coordinates to image coordinates
|
|
83
90
|
const scaled = scaleRectangle(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
sourceRectangle,
|
|
92
|
+
baseWidth,
|
|
93
|
+
baseHeight,
|
|
87
94
|
imageSize.width,
|
|
88
95
|
imageSize.height
|
|
89
96
|
);
|
|
90
97
|
|
|
91
98
|
return scaled as CropperRectangle;
|
|
92
|
-
}, [document.quad, document.width, document.height, imageSize]);
|
|
99
|
+
}, [document.rectangle, document.quad, document.width, document.height, imageSize]);
|
|
93
100
|
|
|
94
101
|
const handleImageLoad = useCallback((event: any) => {
|
|
95
102
|
// This is just for debugging - actual size is loaded via Image.getSize in useEffect
|
package/src/DocScanner.tsx
CHANGED
|
@@ -33,6 +33,7 @@ export type DocScannerCapture = {
|
|
|
33
33
|
initialPath: string | null;
|
|
34
34
|
croppedPath: string | null;
|
|
35
35
|
quad: Point[] | null;
|
|
36
|
+
rectangle: Rectangle | null;
|
|
36
37
|
width: number;
|
|
37
38
|
height: number;
|
|
38
39
|
};
|
|
@@ -127,6 +128,7 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
127
128
|
} | null>(null);
|
|
128
129
|
const [isAutoCapturing, setIsAutoCapturing] = useState(false);
|
|
129
130
|
const [detectedRectangle, setDetectedRectangle] = useState<RectangleDetectEvent | null>(null);
|
|
131
|
+
const lastRectangleRef = useRef<Rectangle | null>(null);
|
|
130
132
|
|
|
131
133
|
useEffect(() => {
|
|
132
134
|
if (!autoCapture) {
|
|
@@ -146,19 +148,21 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
146
148
|
(event: PictureEvent) => {
|
|
147
149
|
setIsAutoCapturing(false);
|
|
148
150
|
|
|
149
|
-
const normalizedRectangle =
|
|
151
|
+
const normalizedRectangle =
|
|
152
|
+
normalizeRectangle(event.rectangleCoordinates ?? null) ?? lastRectangleRef.current;
|
|
150
153
|
const quad = normalizedRectangle ? rectangleToQuad(normalizedRectangle) : null;
|
|
151
154
|
|
|
152
155
|
const initialPath = event.initialImage ?? null;
|
|
153
156
|
const croppedPath = event.croppedImage ?? null;
|
|
154
|
-
const
|
|
157
|
+
const editablePath = initialPath ?? croppedPath;
|
|
155
158
|
|
|
156
|
-
if (
|
|
159
|
+
if (editablePath) {
|
|
157
160
|
onCapture?.({
|
|
158
|
-
path:
|
|
161
|
+
path: editablePath,
|
|
159
162
|
initialPath,
|
|
160
163
|
croppedPath,
|
|
161
164
|
quad,
|
|
165
|
+
rectangle: normalizedRectangle,
|
|
162
166
|
width: event.width ?? 0,
|
|
163
167
|
height: event.height ?? 0,
|
|
164
168
|
});
|
|
@@ -224,13 +228,17 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
224
228
|
};
|
|
225
229
|
|
|
226
230
|
if (autoCapture) {
|
|
227
|
-
|
|
231
|
+
if (payload.stableCounter >= Math.max(minStableFrames - 1, 0)) {
|
|
228
232
|
setIsAutoCapturing(true);
|
|
229
233
|
} else if (payload.stableCounter === 0) {
|
|
230
234
|
setIsAutoCapturing(false);
|
|
231
235
|
}
|
|
232
236
|
}
|
|
233
237
|
|
|
238
|
+
if (payload.rectangleCoordinates) {
|
|
239
|
+
lastRectangleRef.current = payload.rectangleCoordinates;
|
|
240
|
+
}
|
|
241
|
+
|
|
234
242
|
const isGoodRectangle = payload.lastDetectionType === 0;
|
|
235
243
|
setDetectedRectangle(isGoodRectangle && rectangleOnScreen ? payload : null);
|
|
236
244
|
onRectangleDetect?.(payload);
|
|
@@ -252,7 +260,7 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
252
260
|
[capture],
|
|
253
261
|
);
|
|
254
262
|
|
|
255
|
-
const overlayPolygon = detectedRectangle?.rectangleOnScreen ?? null;
|
|
263
|
+
const overlayPolygon = detectedRectangle?.rectangleOnScreen ?? detectedRectangle?.rectangleCoordinates ?? null;
|
|
256
264
|
const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
|
|
257
265
|
|
|
258
266
|
return (
|
package/src/FullDocScanner.tsx
CHANGED
|
@@ -116,6 +116,42 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
116
116
|
);
|
|
117
117
|
}, [capturedDoc]);
|
|
118
118
|
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!capturedDoc || !imageSize || cropRectangle) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : imageSize.width;
|
|
125
|
+
const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : imageSize.height;
|
|
126
|
+
|
|
127
|
+
let initialRectangle: Rectangle | null = null;
|
|
128
|
+
|
|
129
|
+
if (capturedDoc.rectangle) {
|
|
130
|
+
initialRectangle = scaleRectangle(
|
|
131
|
+
capturedDoc.rectangle,
|
|
132
|
+
baseWidth,
|
|
133
|
+
baseHeight,
|
|
134
|
+
imageSize.width,
|
|
135
|
+
imageSize.height,
|
|
136
|
+
);
|
|
137
|
+
} else if (capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
138
|
+
const quadRectangle = quadToRectangle(capturedDoc.quad as Quad);
|
|
139
|
+
if (quadRectangle) {
|
|
140
|
+
initialRectangle = scaleRectangle(
|
|
141
|
+
quadRectangle,
|
|
142
|
+
baseWidth,
|
|
143
|
+
baseHeight,
|
|
144
|
+
imageSize.width,
|
|
145
|
+
imageSize.height,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (initialRectangle) {
|
|
151
|
+
setCropRectangle(initialRectangle);
|
|
152
|
+
}
|
|
153
|
+
}, [capturedDoc, imageSize, cropRectangle]);
|
|
154
|
+
|
|
119
155
|
const resetState = useCallback(() => {
|
|
120
156
|
setScreen('scanner');
|
|
121
157
|
setCapturedDoc(null);
|
|
@@ -128,6 +164,8 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
128
164
|
(document: CapturedDocument) => {
|
|
129
165
|
const normalizedPath = stripFileUri(document.path);
|
|
130
166
|
const nextQuad = document.quad && document.quad.length === 4 ? (document.quad as Quad) : null;
|
|
167
|
+
const quadRectangle = nextQuad ? quadToRectangle(nextQuad) : null;
|
|
168
|
+
const nextRectangle = document.rectangle ?? quadRectangle ?? null;
|
|
131
169
|
const normalizedInitial =
|
|
132
170
|
document.initialPath != null ? stripFileUri(document.initialPath) : normalizedPath;
|
|
133
171
|
const normalizedCropped =
|
|
@@ -139,8 +177,9 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
139
177
|
initialPath: normalizedInitial,
|
|
140
178
|
croppedPath: normalizedCropped,
|
|
141
179
|
quad: nextQuad,
|
|
180
|
+
rectangle: nextRectangle,
|
|
142
181
|
});
|
|
143
|
-
setCropRectangle(
|
|
182
|
+
setCropRectangle(null);
|
|
144
183
|
setScreen('crop');
|
|
145
184
|
},
|
|
146
185
|
[],
|
|
@@ -173,22 +212,35 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
173
212
|
throw new Error('CustomCropManager.crop is not available');
|
|
174
213
|
}
|
|
175
214
|
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
? quadToRectangle(capturedDoc.quad as Quad)
|
|
179
|
-
: null;
|
|
215
|
+
const baseWidth = capturedDoc.width > 0 ? capturedDoc.width : size.width;
|
|
216
|
+
const baseHeight = capturedDoc.height > 0 ? capturedDoc.height : size.height;
|
|
180
217
|
|
|
181
|
-
const
|
|
218
|
+
const rectangleFromDetection = capturedDoc.rectangle
|
|
182
219
|
? scaleRectangle(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
220
|
+
capturedDoc.rectangle,
|
|
221
|
+
baseWidth,
|
|
222
|
+
baseHeight,
|
|
186
223
|
size.width,
|
|
187
224
|
size.height,
|
|
188
225
|
)
|
|
189
226
|
: null;
|
|
190
227
|
|
|
191
|
-
|
|
228
|
+
let fallbackRectangle: Rectangle | null = rectangleFromDetection;
|
|
229
|
+
|
|
230
|
+
if (!fallbackRectangle && capturedDoc.quad && capturedDoc.quad.length === 4) {
|
|
231
|
+
const quadRectangle = quadToRectangle(capturedDoc.quad as Quad);
|
|
232
|
+
if (quadRectangle) {
|
|
233
|
+
fallbackRectangle = scaleRectangle(
|
|
234
|
+
quadRectangle,
|
|
235
|
+
baseWidth,
|
|
236
|
+
baseHeight,
|
|
237
|
+
size.width,
|
|
238
|
+
size.height,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const rectangle = cropRectangle ?? fallbackRectangle;
|
|
192
244
|
|
|
193
245
|
const base64 = await new Promise<string>((resolve, reject) => {
|
|
194
246
|
cropManager.crop(
|
package/src/types.ts
CHANGED
package/src/utils/overlay.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
3
|
import type { Rectangle } from '../types';
|
|
4
4
|
|
|
5
5
|
let SvgModule: typeof import('react-native-svg') | null = null;
|
|
@@ -11,19 +11,35 @@ try {
|
|
|
11
11
|
SvgModule = null;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const SCAN_DURATION_MS = 2200;
|
|
15
14
|
const GRID_STEPS = [1 / 3, 2 / 3];
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
const createPointsString = (polygon: Rectangle): string =>
|
|
17
|
+
[
|
|
18
|
+
`${polygon.topLeft.x},${polygon.topLeft.y}`,
|
|
19
|
+
`${polygon.topRight.x},${polygon.topRight.y}`,
|
|
20
|
+
`${polygon.bottomRight.x},${polygon.bottomRight.y}`,
|
|
21
|
+
`${polygon.bottomLeft.x},${polygon.bottomLeft.y}`,
|
|
22
|
+
].join(' ');
|
|
23
|
+
|
|
24
|
+
const interpolatePoint = (a: { x: number; y: number }, b: { x: number; y: number }, t: number) => ({
|
|
25
|
+
x: a.x + (b.x - a.x) * t,
|
|
26
|
+
y: a.y + (b.y - a.y) * t,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const createGridLines = (polygon: Rectangle) =>
|
|
30
|
+
GRID_STEPS.flatMap((step) => {
|
|
31
|
+
const horizontalStart = interpolatePoint(polygon.topLeft, polygon.bottomLeft, step);
|
|
32
|
+
const horizontalEnd = interpolatePoint(polygon.topRight, polygon.bottomRight, step);
|
|
33
|
+
const verticalStart = interpolatePoint(polygon.topLeft, polygon.topRight, step);
|
|
34
|
+
const verticalEnd = interpolatePoint(polygon.bottomLeft, polygon.bottomRight, step);
|
|
35
|
+
|
|
36
|
+
return [
|
|
37
|
+
{ x1: horizontalStart.x, y1: horizontalStart.y, x2: horizontalEnd.x, y2: horizontalEnd.y },
|
|
38
|
+
{ x1: verticalStart.x, y1: verticalStart.y, x2: verticalEnd.x, y2: verticalEnd.y },
|
|
39
|
+
];
|
|
40
|
+
});
|
|
25
41
|
|
|
26
|
-
const
|
|
42
|
+
const getBounds = (polygon: Rectangle) => {
|
|
27
43
|
const minX = Math.min(
|
|
28
44
|
polygon.topLeft.x,
|
|
29
45
|
polygon.bottomLeft.x,
|
|
@@ -51,40 +67,12 @@ const calculateMetrics = (polygon: Rectangle): PolygonMetrics => {
|
|
|
51
67
|
|
|
52
68
|
return {
|
|
53
69
|
minX,
|
|
54
|
-
maxX,
|
|
55
70
|
minY,
|
|
56
|
-
maxY,
|
|
57
71
|
width: maxX - minX,
|
|
58
72
|
height: maxY - minY,
|
|
59
73
|
};
|
|
60
74
|
};
|
|
61
75
|
|
|
62
|
-
const createPointsString = (polygon: Rectangle): string =>
|
|
63
|
-
[
|
|
64
|
-
`${polygon.topLeft.x},${polygon.topLeft.y}`,
|
|
65
|
-
`${polygon.topRight.x},${polygon.topRight.y}`,
|
|
66
|
-
`${polygon.bottomRight.x},${polygon.bottomRight.y}`,
|
|
67
|
-
`${polygon.bottomLeft.x},${polygon.bottomLeft.y}`,
|
|
68
|
-
].join(' ');
|
|
69
|
-
|
|
70
|
-
const interpolatePoint = (a: { x: number; y: number }, b: { x: number; y: number }, t: number) => ({
|
|
71
|
-
x: a.x + (b.x - a.x) * t,
|
|
72
|
-
y: a.y + (b.y - a.y) * t,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const createGridLines = (polygon: Rectangle) =>
|
|
76
|
-
GRID_STEPS.flatMap((step) => {
|
|
77
|
-
const horizontalStart = interpolatePoint(polygon.topLeft, polygon.bottomLeft, step);
|
|
78
|
-
const horizontalEnd = interpolatePoint(polygon.topRight, polygon.bottomRight, step);
|
|
79
|
-
const verticalStart = interpolatePoint(polygon.topLeft, polygon.topRight, step);
|
|
80
|
-
const verticalEnd = interpolatePoint(polygon.bottomLeft, polygon.bottomRight, step);
|
|
81
|
-
|
|
82
|
-
return [
|
|
83
|
-
{ x1: horizontalStart.x, y1: horizontalStart.y, x2: horizontalEnd.x, y2: horizontalEnd.y },
|
|
84
|
-
{ x1: verticalStart.x, y1: verticalStart.y, x2: verticalEnd.x, y2: verticalEnd.y },
|
|
85
|
-
];
|
|
86
|
-
});
|
|
87
|
-
|
|
88
76
|
export interface ScannerOverlayProps {
|
|
89
77
|
active: boolean;
|
|
90
78
|
color?: string;
|
|
@@ -93,89 +81,21 @@ export interface ScannerOverlayProps {
|
|
|
93
81
|
}
|
|
94
82
|
|
|
95
83
|
export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
|
|
96
|
-
active,
|
|
84
|
+
active: _active, // kept for compatibility; no animation currently
|
|
97
85
|
color = '#0b7ef4',
|
|
98
86
|
lineWidth = StyleSheet.hairlineWidth,
|
|
99
87
|
polygon,
|
|
100
88
|
}) => {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
const metrics = useMemo(() => (polygon ? calculateMetrics(polygon) : null), [polygon]);
|
|
106
|
-
|
|
107
|
-
const scanBarHeight = useMemo(() => {
|
|
108
|
-
if (!metrics) return 0;
|
|
109
|
-
return Math.max(metrics.height * 0.2, 16);
|
|
110
|
-
}, [metrics]);
|
|
111
|
-
|
|
112
|
-
const travelDistance = useMemo(() => {
|
|
113
|
-
if (!metrics) {
|
|
114
|
-
return 0;
|
|
115
|
-
}
|
|
116
|
-
return Math.max(metrics.height - scanBarHeight, 0);
|
|
117
|
-
}, [metrics, scanBarHeight]);
|
|
118
|
-
|
|
119
|
-
useEffect(() => {
|
|
120
|
-
scanProgress.stopAnimation();
|
|
121
|
-
scanProgress.setValue(0);
|
|
122
|
-
setScanY(null);
|
|
123
|
-
|
|
124
|
-
if (!active || !metrics || travelDistance <= 0) {
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const loop = Animated.loop(
|
|
129
|
-
Animated.sequence([
|
|
130
|
-
Animated.timing(scanProgress, {
|
|
131
|
-
toValue: 1,
|
|
132
|
-
duration: SCAN_DURATION_MS,
|
|
133
|
-
easing: Easing.inOut(Easing.quad),
|
|
134
|
-
useNativeDriver: false,
|
|
135
|
-
}),
|
|
136
|
-
Animated.timing(scanProgress, {
|
|
137
|
-
toValue: 0,
|
|
138
|
-
duration: SCAN_DURATION_MS,
|
|
139
|
-
easing: Easing.inOut(Easing.quad),
|
|
140
|
-
useNativeDriver: false,
|
|
141
|
-
}),
|
|
142
|
-
]),
|
|
143
|
-
);
|
|
89
|
+
const points = useMemo(() => (polygon ? createPointsString(polygon) : null), [polygon]);
|
|
90
|
+
const gridLines = useMemo(() => (polygon ? createGridLines(polygon) : []), [polygon]);
|
|
91
|
+
const bounds = useMemo(() => (polygon ? getBounds(polygon) : null), [polygon]);
|
|
144
92
|
|
|
145
|
-
|
|
146
|
-
return () => {
|
|
147
|
-
loop.stop();
|
|
148
|
-
scanProgress.stopAnimation();
|
|
149
|
-
};
|
|
150
|
-
}, [active, metrics, scanProgress, travelDistance]);
|
|
151
|
-
|
|
152
|
-
useEffect(() => {
|
|
153
|
-
if (!metrics || travelDistance <= 0) {
|
|
154
|
-
setScanY(null);
|
|
155
|
-
return undefined;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const listenerId = scanProgress.addListener(({ value }) => {
|
|
159
|
-
const nextValue = metrics.minY + travelDistance * value;
|
|
160
|
-
if (Number.isFinite(nextValue)) {
|
|
161
|
-
setScanY(nextValue);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
return () => {
|
|
166
|
-
scanProgress.removeListener(listenerId);
|
|
167
|
-
};
|
|
168
|
-
}, [metrics, scanProgress, travelDistance]);
|
|
169
|
-
|
|
170
|
-
if (!polygon || !metrics || metrics.width <= 0 || metrics.height <= 0) {
|
|
93
|
+
if (!polygon || !points || !bounds) {
|
|
171
94
|
return null;
|
|
172
95
|
}
|
|
173
96
|
|
|
174
97
|
if (SvgModule) {
|
|
175
|
-
const { default: Svg, Polygon, Line
|
|
176
|
-
const gridLines = createGridLines(polygon);
|
|
177
|
-
const points = createPointsString(polygon);
|
|
178
|
-
const scanRectY = scanY ?? metrics.minY;
|
|
98
|
+
const { default: Svg, Polygon, Line } = SvgModule;
|
|
179
99
|
|
|
180
100
|
return (
|
|
181
101
|
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
|
|
@@ -194,60 +114,26 @@ export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
|
|
|
194
114
|
/>
|
|
195
115
|
))}
|
|
196
116
|
<Polygon points={points} stroke={color} strokeWidth={lineWidth} fill="none" />
|
|
197
|
-
<Defs>
|
|
198
|
-
<LinearGradient id="scanGradient" x1="0" y1="0" x2="0" y2="1">
|
|
199
|
-
<Stop offset="0%" stopColor="rgba(255,255,255,0)" />
|
|
200
|
-
<Stop offset="50%" stopColor={color} stopOpacity={0.8} />
|
|
201
|
-
<Stop offset="100%" stopColor="rgba(255,255,255,0)" />
|
|
202
|
-
</LinearGradient>
|
|
203
|
-
</Defs>
|
|
204
|
-
{active && travelDistance > 0 && Number.isFinite(scanRectY) && (
|
|
205
|
-
<Rect
|
|
206
|
-
x={metrics.minX}
|
|
207
|
-
width={metrics.width}
|
|
208
|
-
height={scanBarHeight}
|
|
209
|
-
fill="url(#scanGradient)"
|
|
210
|
-
y={scanRectY}
|
|
211
|
-
/>
|
|
212
|
-
)}
|
|
213
117
|
</Svg>
|
|
214
118
|
</View>
|
|
215
119
|
);
|
|
216
120
|
}
|
|
217
121
|
|
|
218
|
-
const relativeTranslate =
|
|
219
|
-
metrics && travelDistance > 0
|
|
220
|
-
? Animated.multiply(scanProgress, travelDistance)
|
|
221
|
-
: fallbackBase;
|
|
222
|
-
|
|
223
122
|
return (
|
|
224
123
|
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
|
|
225
124
|
<View
|
|
226
125
|
style={[
|
|
227
126
|
styles.fallbackBox,
|
|
228
127
|
{
|
|
229
|
-
left:
|
|
230
|
-
top:
|
|
231
|
-
width:
|
|
232
|
-
height:
|
|
128
|
+
left: bounds.minX,
|
|
129
|
+
top: bounds.minY,
|
|
130
|
+
width: bounds.width,
|
|
131
|
+
height: bounds.height,
|
|
233
132
|
borderColor: color,
|
|
234
133
|
borderWidth: lineWidth,
|
|
235
134
|
},
|
|
236
135
|
]}
|
|
237
|
-
|
|
238
|
-
{active && travelDistance > 0 && (
|
|
239
|
-
<Animated.View
|
|
240
|
-
style={[
|
|
241
|
-
styles.fallbackScanBar,
|
|
242
|
-
{
|
|
243
|
-
backgroundColor: color,
|
|
244
|
-
height: scanBarHeight,
|
|
245
|
-
transform: [{ translateY: relativeTranslate }],
|
|
246
|
-
},
|
|
247
|
-
]}
|
|
248
|
-
/>
|
|
249
|
-
)}
|
|
250
|
-
</View>
|
|
136
|
+
/>
|
|
251
137
|
</View>
|
|
252
138
|
);
|
|
253
139
|
};
|
|
@@ -256,10 +142,5 @@ const styles = StyleSheet.create({
|
|
|
256
142
|
fallbackBox: {
|
|
257
143
|
position: 'absolute',
|
|
258
144
|
backgroundColor: 'rgba(11, 126, 244, 0.1)',
|
|
259
|
-
overflow: 'hidden',
|
|
260
|
-
},
|
|
261
|
-
fallbackScanBar: {
|
|
262
|
-
width: '100%',
|
|
263
|
-
opacity: 0.4,
|
|
264
145
|
},
|
|
265
146
|
});
|