react-native-rectangle-doc-scanner 0.66.0 → 0.70.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/README.md +97 -168
- package/android/build.gradle +55 -0
- package/android/consumer-rules.pro +1 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +11 -0
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerModule.kt +37 -0
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerPackage.kt +16 -0
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerView.kt +536 -0
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerViewManager.kt +50 -0
- package/dist/DocScanner.d.ts +12 -7
- package/dist/DocScanner.js +97 -42
- package/dist/FullDocScanner.d.ts +3 -0
- package/dist/FullDocScanner.js +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/utils/overlay.js +77 -48
- package/docs/native-module-architecture.md +178 -0
- package/ios/RNRDocScannerModule.swift +49 -0
- package/ios/RNRDocScannerView.swift +477 -0
- package/ios/RNRDocScannerViewManager.m +21 -0
- package/ios/RNRDocScannerViewManager.swift +47 -0
- package/package.json +6 -5
- package/react-native-rectangle-doc-scanner.podspec +22 -0
- package/src/DocScanner.tsx +153 -76
- package/src/FullDocScanner.tsx +10 -0
- package/src/external.d.ts +12 -45
- package/src/index.ts +1 -1
- package/src/utils/overlay.tsx +83 -54
package/src/DocScanner.tsx
CHANGED
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
import React, {
|
|
2
|
-
ComponentType,
|
|
3
2
|
ReactNode,
|
|
3
|
+
forwardRef,
|
|
4
4
|
useCallback,
|
|
5
|
+
useImperativeHandle,
|
|
5
6
|
useMemo,
|
|
6
7
|
useRef,
|
|
7
8
|
useState,
|
|
8
9
|
} from 'react';
|
|
9
10
|
import {
|
|
10
|
-
|
|
11
|
+
findNodeHandle,
|
|
12
|
+
NativeModules,
|
|
13
|
+
requireNativeComponent,
|
|
11
14
|
StyleSheet,
|
|
12
15
|
TouchableOpacity,
|
|
13
16
|
View,
|
|
14
17
|
} from 'react-native';
|
|
15
|
-
import
|
|
18
|
+
import type { NativeSyntheticEvent } from 'react-native';
|
|
16
19
|
import { Overlay } from './utils/overlay';
|
|
17
20
|
import type { Point } from './types';
|
|
18
21
|
|
|
22
|
+
const MODULE_NAME = 'RNRDocScannerModule';
|
|
23
|
+
const VIEW_NAME = 'RNRDocScannerView';
|
|
24
|
+
|
|
25
|
+
const NativeDocScannerModule = NativeModules[MODULE_NAME];
|
|
26
|
+
|
|
27
|
+
if (!NativeDocScannerModule) {
|
|
28
|
+
const fallbackMessage =
|
|
29
|
+
`The native module '${MODULE_NAME}' is not linked. Make sure you have run pod install, ` +
|
|
30
|
+
`synced Gradle, and rebuilt the app after installing 'react-native-rectangle-doc-scanner'.`;
|
|
31
|
+
throw new Error(fallbackMessage);
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
type NativeRectangle = {
|
|
20
35
|
topLeft: Point;
|
|
21
36
|
topRight: Point;
|
|
@@ -23,43 +38,39 @@ type NativeRectangle = {
|
|
|
23
38
|
bottomLeft: Point;
|
|
24
39
|
};
|
|
25
40
|
|
|
26
|
-
type
|
|
27
|
-
rectangleCoordinates
|
|
28
|
-
stableCounter
|
|
41
|
+
type RectangleEvent = {
|
|
42
|
+
rectangleCoordinates: NativeRectangle | null;
|
|
43
|
+
stableCounter: number;
|
|
44
|
+
frameWidth: number;
|
|
45
|
+
frameHeight: number;
|
|
29
46
|
};
|
|
30
47
|
|
|
31
|
-
type
|
|
32
|
-
croppedImage?: string;
|
|
48
|
+
type PictureEvent = {
|
|
49
|
+
croppedImage?: string | null;
|
|
33
50
|
initialImage?: string;
|
|
34
51
|
width?: number;
|
|
35
52
|
height?: number;
|
|
36
53
|
};
|
|
37
54
|
|
|
38
|
-
type
|
|
39
|
-
capture: () => Promise<NativeCaptureResult>;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
type NativeDocumentScannerProps = {
|
|
55
|
+
type NativeDocScannerProps = {
|
|
43
56
|
style?: object;
|
|
44
|
-
overlayColor?: string;
|
|
45
57
|
detectionCountBeforeCapture?: number;
|
|
58
|
+
autoCapture?: boolean;
|
|
46
59
|
enableTorch?: boolean;
|
|
47
|
-
hideControls?: boolean;
|
|
48
|
-
useBase64?: boolean;
|
|
49
60
|
quality?: number;
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
useBase64?: boolean;
|
|
62
|
+
onRectangleDetect?: (event: NativeSyntheticEvent<RectangleEvent>) => void;
|
|
63
|
+
onPictureTaken?: (event: NativeSyntheticEvent<PictureEvent>) => void;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type DocScannerHandle = {
|
|
67
|
+
capture: () => Promise<PictureEvent>;
|
|
68
|
+
reset: () => void;
|
|
52
69
|
};
|
|
53
70
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
>;
|
|
71
|
+
const NativeDocScanner = requireNativeComponent<NativeDocScannerProps>(VIEW_NAME);
|
|
72
|
+
type NativeDocScannerInstance = React.ElementRef<typeof NativeDocScanner>;
|
|
57
73
|
|
|
58
|
-
/**
|
|
59
|
-
* Detection configuration is no longer used now that the native
|
|
60
|
-
* implementation handles edge detection. Keeping it for backwards
|
|
61
|
-
* compatibility with existing consumer code.
|
|
62
|
-
*/
|
|
63
74
|
export interface DetectionConfig {
|
|
64
75
|
processingWidth?: number;
|
|
65
76
|
cannyLowThreshold?: number;
|
|
@@ -87,22 +98,23 @@ interface Props {
|
|
|
87
98
|
const DEFAULT_OVERLAY_COLOR = '#e7a649';
|
|
88
99
|
const GRID_COLOR_FALLBACK = 'rgba(231, 166, 73, 0.35)';
|
|
89
100
|
|
|
90
|
-
export const DocScanner
|
|
101
|
+
export const DocScanner = forwardRef<DocScannerHandle, Props>(({
|
|
91
102
|
onCapture,
|
|
92
103
|
overlayColor = DEFAULT_OVERLAY_COLOR,
|
|
93
104
|
autoCapture = true,
|
|
94
105
|
minStableFrames = 8,
|
|
95
106
|
enableTorch = false,
|
|
96
|
-
quality,
|
|
107
|
+
quality = 90,
|
|
97
108
|
useBase64 = false,
|
|
98
109
|
children,
|
|
99
110
|
showGrid = true,
|
|
100
111
|
gridColor,
|
|
101
112
|
gridLineWidth = 2,
|
|
102
|
-
}) => {
|
|
103
|
-
const
|
|
113
|
+
}, ref) => {
|
|
114
|
+
const viewRef = useRef<NativeDocScannerInstance | null>(null);
|
|
104
115
|
const capturingRef = useRef(false);
|
|
105
116
|
const [quad, setQuad] = useState<Point[] | null>(null);
|
|
117
|
+
const [stable, setStable] = useState(0);
|
|
106
118
|
const [frameSize, setFrameSize] = useState<{ width: number; height: number } | null>(null);
|
|
107
119
|
|
|
108
120
|
const effectiveGridColor = useMemo(
|
|
@@ -110,80 +122,142 @@ export const DocScanner: React.FC<Props> = ({
|
|
|
110
122
|
[gridColor],
|
|
111
123
|
);
|
|
112
124
|
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
|
|
125
|
+
const ensureViewHandle = useCallback(() => {
|
|
126
|
+
const nodeHandle = findNodeHandle(viewRef.current);
|
|
127
|
+
if (!nodeHandle) {
|
|
128
|
+
throw new Error('Unable to obtain native view handle for DocScanner.');
|
|
117
129
|
}
|
|
130
|
+
return nodeHandle;
|
|
118
131
|
}, []);
|
|
119
132
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
const resetNativeStability = useCallback(() => {
|
|
134
|
+
try {
|
|
135
|
+
const handle = ensureViewHandle();
|
|
136
|
+
NativeDocScannerModule.reset(handle);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.warn('[DocScanner] unable to reset native stability', error);
|
|
126
139
|
}
|
|
140
|
+
}, [ensureViewHandle]);
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
coordinates.topRight,
|
|
131
|
-
coordinates.bottomRight,
|
|
132
|
-
coordinates.bottomLeft,
|
|
133
|
-
];
|
|
134
|
-
|
|
135
|
-
setQuad(nextQuad);
|
|
136
|
-
}, []);
|
|
137
|
-
|
|
138
|
-
const handlePictureTaken = useCallback(
|
|
139
|
-
(event: NativeCaptureResult) => {
|
|
142
|
+
const emitCaptureResult = useCallback(
|
|
143
|
+
(payload: PictureEvent) => {
|
|
140
144
|
capturingRef.current = false;
|
|
141
145
|
|
|
142
|
-
const path =
|
|
146
|
+
const path = payload.croppedImage ?? payload.initialImage;
|
|
143
147
|
if (!path) {
|
|
144
148
|
return;
|
|
145
149
|
}
|
|
146
150
|
|
|
147
|
-
const width =
|
|
148
|
-
const height =
|
|
149
|
-
|
|
151
|
+
const width = payload.width ?? frameSize?.width ?? 0;
|
|
152
|
+
const height = payload.height ?? frameSize?.height ?? 0;
|
|
150
153
|
onCapture?.({
|
|
151
154
|
path,
|
|
152
155
|
quad,
|
|
153
156
|
width,
|
|
154
157
|
height,
|
|
155
158
|
});
|
|
159
|
+
setStable(0);
|
|
160
|
+
resetNativeStability();
|
|
161
|
+
},
|
|
162
|
+
[frameSize, onCapture, quad, resetNativeStability],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const handleRectangleDetect = useCallback(
|
|
166
|
+
(event: NativeSyntheticEvent<RectangleEvent>) => {
|
|
167
|
+
const { rectangleCoordinates, stableCounter, frameWidth, frameHeight } = event.nativeEvent;
|
|
168
|
+
setStable(stableCounter);
|
|
169
|
+
setFrameSize({ width: frameWidth, height: frameHeight });
|
|
170
|
+
|
|
171
|
+
if (!rectangleCoordinates) {
|
|
172
|
+
setQuad(null);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setQuad([
|
|
177
|
+
rectangleCoordinates.topLeft,
|
|
178
|
+
rectangleCoordinates.topRight,
|
|
179
|
+
rectangleCoordinates.bottomRight,
|
|
180
|
+
rectangleCoordinates.bottomLeft,
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
if (autoCapture && stableCounter >= minStableFrames) {
|
|
184
|
+
triggerCapture();
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
[autoCapture, minStableFrames],
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const handlePictureTaken = useCallback(
|
|
191
|
+
(event: NativeSyntheticEvent<PictureEvent>) => {
|
|
192
|
+
emitCaptureResult(event.nativeEvent);
|
|
156
193
|
},
|
|
157
|
-
[
|
|
194
|
+
[emitCaptureResult],
|
|
158
195
|
);
|
|
159
196
|
|
|
197
|
+
const captureNative = useCallback((): Promise<PictureEvent> => {
|
|
198
|
+
if (capturingRef.current) {
|
|
199
|
+
return Promise.reject(new Error('capture_in_progress'));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const handle = ensureViewHandle();
|
|
204
|
+
capturingRef.current = true;
|
|
205
|
+
return NativeDocScannerModule.capture(handle)
|
|
206
|
+
.then((result: PictureEvent) => {
|
|
207
|
+
emitCaptureResult(result);
|
|
208
|
+
return result;
|
|
209
|
+
})
|
|
210
|
+
.catch((error: Error) => {
|
|
211
|
+
capturingRef.current = false;
|
|
212
|
+
throw error;
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
capturingRef.current = false;
|
|
216
|
+
return Promise.reject(error);
|
|
217
|
+
}
|
|
218
|
+
}, [emitCaptureResult, ensureViewHandle]);
|
|
219
|
+
|
|
220
|
+
const triggerCapture = useCallback(() => {
|
|
221
|
+
if (capturingRef.current) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
captureNative().catch((error: Error) => {
|
|
226
|
+
console.warn('[DocScanner] capture failed', error);
|
|
227
|
+
});
|
|
228
|
+
}, [captureNative]);
|
|
229
|
+
|
|
160
230
|
const handleManualCapture = useCallback(() => {
|
|
161
|
-
if (autoCapture
|
|
231
|
+
if (autoCapture) {
|
|
162
232
|
return;
|
|
163
233
|
}
|
|
234
|
+
captureNative().catch((error: Error) => {
|
|
235
|
+
console.warn('[DocScanner] manual capture failed', error);
|
|
236
|
+
});
|
|
237
|
+
}, [autoCapture, captureNative]);
|
|
164
238
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
239
|
+
useImperativeHandle(
|
|
240
|
+
ref,
|
|
241
|
+
() => ({
|
|
242
|
+
capture: captureNative,
|
|
243
|
+
reset: () => {
|
|
244
|
+
setStable(0);
|
|
245
|
+
resetNativeStability();
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
248
|
+
[captureNative, resetNativeStability],
|
|
249
|
+
);
|
|
173
250
|
|
|
174
251
|
return (
|
|
175
|
-
<View style={styles.container}
|
|
176
|
-
<
|
|
177
|
-
ref={
|
|
178
|
-
scannerRef.current = instance as DocumentScannerHandle | null;
|
|
179
|
-
}}
|
|
252
|
+
<View style={styles.container}>
|
|
253
|
+
<NativeDocScanner
|
|
254
|
+
ref={viewRef}
|
|
180
255
|
style={StyleSheet.absoluteFill}
|
|
181
|
-
|
|
182
|
-
|
|
256
|
+
detectionCountBeforeCapture={minStableFrames}
|
|
257
|
+
autoCapture={autoCapture}
|
|
183
258
|
enableTorch={enableTorch}
|
|
184
|
-
hideControls
|
|
185
|
-
useBase64={useBase64}
|
|
186
259
|
quality={quality}
|
|
260
|
+
useBase64={useBase64}
|
|
187
261
|
onRectangleDetect={handleRectangleDetect}
|
|
188
262
|
onPictureTaken={handlePictureTaken}
|
|
189
263
|
/>
|
|
@@ -201,11 +275,12 @@ export const DocScanner: React.FC<Props> = ({
|
|
|
201
275
|
{children}
|
|
202
276
|
</View>
|
|
203
277
|
);
|
|
204
|
-
};
|
|
278
|
+
});
|
|
205
279
|
|
|
206
280
|
const styles = StyleSheet.create({
|
|
207
281
|
container: {
|
|
208
282
|
flex: 1,
|
|
283
|
+
backgroundColor: '#000',
|
|
209
284
|
},
|
|
210
285
|
button: {
|
|
211
286
|
position: 'absolute',
|
|
@@ -217,3 +292,5 @@ const styles = StyleSheet.create({
|
|
|
217
292
|
backgroundColor: '#fff',
|
|
218
293
|
},
|
|
219
294
|
});
|
|
295
|
+
|
|
296
|
+
export type { DocScannerHandle };
|
package/src/FullDocScanner.tsx
CHANGED
|
@@ -56,6 +56,9 @@ export interface FullDocScannerProps {
|
|
|
56
56
|
onClose?: () => void;
|
|
57
57
|
detectionConfig?: DetectionConfig;
|
|
58
58
|
overlayColor?: string;
|
|
59
|
+
gridColor?: string;
|
|
60
|
+
gridLineWidth?: number;
|
|
61
|
+
showGrid?: boolean;
|
|
59
62
|
overlayStrokeColor?: string;
|
|
60
63
|
handlerColor?: string;
|
|
61
64
|
strings?: FullDocScannerStrings;
|
|
@@ -71,6 +74,9 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
71
74
|
onClose,
|
|
72
75
|
detectionConfig,
|
|
73
76
|
overlayColor = '#3170f3',
|
|
77
|
+
gridColor,
|
|
78
|
+
gridLineWidth,
|
|
79
|
+
showGrid,
|
|
74
80
|
overlayStrokeColor = '#3170f3',
|
|
75
81
|
handlerColor = '#3170f3',
|
|
76
82
|
strings,
|
|
@@ -83,6 +89,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
83
89
|
const [cropRectangle, setCropRectangle] = useState<Rectangle | null>(null);
|
|
84
90
|
const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null);
|
|
85
91
|
const [processing, setProcessing] = useState(false);
|
|
92
|
+
const resolvedGridColor = gridColor ?? overlayColor;
|
|
86
93
|
|
|
87
94
|
const mergedStrings = useMemo<Required<FullDocScannerStrings>>(
|
|
88
95
|
() => ({
|
|
@@ -239,6 +246,9 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
239
246
|
<DocScanner
|
|
240
247
|
autoCapture={!manualCapture}
|
|
241
248
|
overlayColor={overlayColor}
|
|
249
|
+
showGrid={showGrid}
|
|
250
|
+
gridColor={resolvedGridColor}
|
|
251
|
+
gridLineWidth={gridLineWidth}
|
|
242
252
|
minStableFrames={minStableFrames ?? 6}
|
|
243
253
|
detectionConfig={detectionConfig}
|
|
244
254
|
onCapture={handleCapture}
|
package/src/external.d.ts
CHANGED
|
@@ -1,48 +1,3 @@
|
|
|
1
|
-
declare module 'react-native-document-scanner-plugin' {
|
|
2
|
-
import type { ComponentType } from 'react';
|
|
3
|
-
import type { ViewStyle } from 'react-native';
|
|
4
|
-
|
|
5
|
-
export type RectangleCoordinates = {
|
|
6
|
-
topLeft: { x: number; y: number };
|
|
7
|
-
topRight: { x: number; y: number };
|
|
8
|
-
bottomRight: { x: number; y: number };
|
|
9
|
-
bottomLeft: { x: number; y: number };
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type RectangleEvent = {
|
|
13
|
-
rectangleCoordinates?: RectangleCoordinates;
|
|
14
|
-
stableCounter?: number;
|
|
15
|
-
lastDetectionType?: 'initial' | 'updated' | 'lost';
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type CaptureResult = {
|
|
19
|
-
croppedImage?: string;
|
|
20
|
-
initialImage?: string;
|
|
21
|
-
width?: number;
|
|
22
|
-
height?: number;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type DocumentScannerProps = {
|
|
26
|
-
ref?: (value: DocumentScannerHandle | null) => void;
|
|
27
|
-
style?: ViewStyle;
|
|
28
|
-
overlayColor?: string;
|
|
29
|
-
detectionCountBeforeCapture?: number;
|
|
30
|
-
enableTorch?: boolean;
|
|
31
|
-
hideControls?: boolean;
|
|
32
|
-
useBase64?: boolean;
|
|
33
|
-
quality?: number;
|
|
34
|
-
onRectangleDetect?: (event: RectangleEvent) => void;
|
|
35
|
-
onPictureTaken?: (result: CaptureResult) => void;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type DocumentScannerHandle = {
|
|
39
|
-
capture: () => Promise<CaptureResult>;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const DocumentScanner: ComponentType<DocumentScannerProps>;
|
|
43
|
-
export default DocumentScanner;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
1
|
declare module '@shopify/react-native-skia' {
|
|
47
2
|
import type { ComponentType, ReactNode } from 'react';
|
|
48
3
|
import type { ViewStyle } from 'react-native';
|
|
@@ -76,6 +31,18 @@ declare module '@shopify/react-native-skia' {
|
|
|
76
31
|
export const Path: ComponentType<PathProps>;
|
|
77
32
|
}
|
|
78
33
|
|
|
34
|
+
declare module 'react-native-rectangle-doc-scanner/RNRDocScannerModule' {
|
|
35
|
+
export type NativeCaptureResult = {
|
|
36
|
+
croppedImage?: string | null;
|
|
37
|
+
initialImage?: string;
|
|
38
|
+
width?: number;
|
|
39
|
+
height?: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function capture(viewTag: number): Promise<NativeCaptureResult>;
|
|
43
|
+
export function reset(viewTag: number): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
79
46
|
declare module 'react-native-perspective-image-cropper' {
|
|
80
47
|
import type { ComponentType } from 'react';
|
|
81
48
|
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type {
|
|
|
11
11
|
|
|
12
12
|
// Types
|
|
13
13
|
export type { Point, Quad, Rectangle, CapturedDocument } from './types';
|
|
14
|
-
export type { DetectionConfig } from './DocScanner';
|
|
14
|
+
export type { DetectionConfig, DocScannerHandle } from './DocScanner';
|
|
15
15
|
|
|
16
16
|
// Utilities
|
|
17
17
|
export {
|
package/src/utils/overlay.tsx
CHANGED
|
@@ -8,6 +8,23 @@ const lerp = (start: Point, end: Point, t: number): Point => ({
|
|
|
8
8
|
y: start.y + (end.y - start.y) * t,
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
+
const withAlpha = (value: string, alpha: number): string => {
|
|
12
|
+
const hexMatch = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value.trim());
|
|
13
|
+
if (!hexMatch) {
|
|
14
|
+
return `rgba(231, 166, 73, ${alpha})`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const hex = hexMatch[1];
|
|
18
|
+
const normalize = hex.length === 3
|
|
19
|
+
? hex.split('').map((ch) => ch + ch).join('')
|
|
20
|
+
: hex;
|
|
21
|
+
|
|
22
|
+
const r = parseInt(normalize.slice(0, 2), 16);
|
|
23
|
+
const g = parseInt(normalize.slice(2, 4), 16);
|
|
24
|
+
const b = parseInt(normalize.slice(4, 6), 16);
|
|
25
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
26
|
+
};
|
|
27
|
+
|
|
11
28
|
type OverlayProps = {
|
|
12
29
|
quad: Point[] | null;
|
|
13
30
|
color?: string;
|
|
@@ -22,6 +39,14 @@ type OverlayGeometry = {
|
|
|
22
39
|
gridPaths: ReturnType<typeof Skia.Path.Make>[];
|
|
23
40
|
};
|
|
24
41
|
|
|
42
|
+
const buildPath = (points: Point[]) => {
|
|
43
|
+
const path = Skia.Path.Make();
|
|
44
|
+
path.moveTo(points[0].x, points[0].y);
|
|
45
|
+
points.slice(1).forEach((p) => path.lineTo(p.x, p.y));
|
|
46
|
+
path.close();
|
|
47
|
+
return path;
|
|
48
|
+
};
|
|
49
|
+
|
|
25
50
|
export const Overlay: React.FC<OverlayProps> = ({
|
|
26
51
|
quad,
|
|
27
52
|
color = '#e7a649',
|
|
@@ -31,71 +56,75 @@ export const Overlay: React.FC<OverlayProps> = ({
|
|
|
31
56
|
gridLineWidth = 2,
|
|
32
57
|
}) => {
|
|
33
58
|
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
59
|
+
const fillColor = useMemo(() => withAlpha(color, 0.2), [color]);
|
|
34
60
|
|
|
35
61
|
const { outlinePath, gridPaths }: OverlayGeometry = useMemo(() => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
return { outlinePath: null, gridPaths: [] };
|
|
41
|
-
}
|
|
62
|
+
let transformedQuad: Point[] | null = null;
|
|
63
|
+
let sourceQuad: Point[] | null = null;
|
|
64
|
+
let sourceFrameSize = frameSize;
|
|
42
65
|
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
if (quad && frameSize) {
|
|
67
|
+
sourceQuad = quad;
|
|
68
|
+
} else {
|
|
69
|
+
const marginRatio = 0.12;
|
|
70
|
+
const marginX = screenWidth * marginRatio;
|
|
71
|
+
const marginY = screenHeight * marginRatio;
|
|
72
|
+
const maxWidth = screenWidth - marginX * 2;
|
|
73
|
+
const maxHeight = screenHeight - marginY * 2;
|
|
74
|
+
const a4Ratio = Math.SQRT2; // ~1.414 height / width
|
|
75
|
+
let width = maxWidth;
|
|
76
|
+
let height = width * a4Ratio;
|
|
77
|
+
if (height > maxHeight) {
|
|
78
|
+
height = maxHeight;
|
|
79
|
+
width = height / a4Ratio;
|
|
80
|
+
}
|
|
81
|
+
const left = (screenWidth - width) / 2;
|
|
82
|
+
const top = (screenHeight - height) / 2;
|
|
83
|
+
transformedQuad = [
|
|
84
|
+
{ x: left, y: top },
|
|
85
|
+
{ x: left + width, y: top },
|
|
86
|
+
{ x: left + width, y: top + height },
|
|
87
|
+
{ x: left, y: top + height },
|
|
88
|
+
];
|
|
89
|
+
sourceFrameSize = null;
|
|
57
90
|
}
|
|
58
91
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (needsRotation) {
|
|
62
|
-
// Camera is landscape, screen is portrait - need to rotate 90 degrees
|
|
63
|
-
// Transform: rotate 90° clockwise and scale
|
|
64
|
-
// New coordinates: x' = y * (screenWidth / frameHeight), y' = (frameWidth - x) * (screenHeight / frameWidth)
|
|
65
|
-
const scaleX = screenWidth / frameSize.height;
|
|
66
|
-
const scaleY = screenHeight / frameSize.width;
|
|
67
|
-
|
|
92
|
+
if (sourceQuad && sourceFrameSize) {
|
|
68
93
|
if (__DEV__) {
|
|
69
|
-
console.log('[Overlay]
|
|
94
|
+
console.log('[Overlay] drawing quad:', sourceQuad);
|
|
95
|
+
console.log('[Overlay] color:', color);
|
|
96
|
+
console.log('[Overlay] screen dimensions:', screenWidth, 'x', screenHeight);
|
|
97
|
+
console.log('[Overlay] frame dimensions:', sourceFrameSize.width, 'x', sourceFrameSize.height);
|
|
70
98
|
}
|
|
71
99
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
const isFrameLandscape = sourceFrameSize.width > sourceFrameSize.height;
|
|
101
|
+
const isScreenPortrait = screenHeight > screenWidth;
|
|
102
|
+
const needsRotation = isFrameLandscape && isScreenPortrait;
|
|
103
|
+
|
|
104
|
+
if (needsRotation) {
|
|
105
|
+
const scaleX = screenWidth / sourceFrameSize.height;
|
|
106
|
+
const scaleY = screenHeight / sourceFrameSize.width;
|
|
107
|
+
|
|
108
|
+
transformedQuad = sourceQuad.map((p) => ({
|
|
109
|
+
x: p.y * scaleX,
|
|
110
|
+
y: (sourceFrameSize.width - p.x) * scaleY,
|
|
111
|
+
}));
|
|
112
|
+
} else {
|
|
113
|
+
const scaleX = screenWidth / sourceFrameSize.width;
|
|
114
|
+
const scaleY = screenHeight / sourceFrameSize.height;
|
|
115
|
+
|
|
116
|
+
transformedQuad = sourceQuad.map((p) => ({
|
|
117
|
+
x: p.x * scaleX,
|
|
118
|
+
y: p.y * scaleY,
|
|
119
|
+
}));
|
|
83
120
|
}
|
|
84
|
-
|
|
85
|
-
transformedQuad = quad.map((p) => ({
|
|
86
|
-
x: p.x * scaleX,
|
|
87
|
-
y: p.y * scaleY,
|
|
88
|
-
}));
|
|
89
121
|
}
|
|
90
122
|
|
|
91
|
-
if (
|
|
92
|
-
|
|
123
|
+
if (!transformedQuad) {
|
|
124
|
+
return { outlinePath: null, gridPaths: [] };
|
|
93
125
|
}
|
|
94
126
|
|
|
95
|
-
const skPath =
|
|
96
|
-
skPath.moveTo(transformedQuad[0].x, transformedQuad[0].y);
|
|
97
|
-
transformedQuad.slice(1).forEach((p) => skPath.lineTo(p.x, p.y));
|
|
98
|
-
skPath.close();
|
|
127
|
+
const skPath = buildPath(transformedQuad);
|
|
99
128
|
const grid: ReturnType<typeof Skia.Path.Make>[] = [];
|
|
100
129
|
|
|
101
130
|
if (showGrid) {
|
|
@@ -122,7 +151,7 @@ export const Overlay: React.FC<OverlayProps> = ({
|
|
|
122
151
|
}
|
|
123
152
|
|
|
124
153
|
return { outlinePath: skPath, gridPaths: grid };
|
|
125
|
-
}, [quad, screenWidth, screenHeight, frameSize, showGrid]);
|
|
154
|
+
}, [quad, screenWidth, screenHeight, frameSize, showGrid, color]);
|
|
126
155
|
|
|
127
156
|
if (__DEV__) {
|
|
128
157
|
console.log('[Overlay] rendering Canvas with dimensions:', screenWidth, 'x', screenHeight);
|
|
@@ -134,7 +163,7 @@ export const Overlay: React.FC<OverlayProps> = ({
|
|
|
134
163
|
{outlinePath && (
|
|
135
164
|
<>
|
|
136
165
|
<Path path={outlinePath} color={color} style="stroke" strokeWidth={8} />
|
|
137
|
-
<Path path={outlinePath} color=
|
|
166
|
+
<Path path={outlinePath} color={fillColor} style="fill" />
|
|
138
167
|
{gridPaths.map((gridPath, index) => (
|
|
139
168
|
<Path
|
|
140
169
|
// eslint-disable-next-line react/no-array-index-key
|