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.
@@ -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
- LayoutChangeEvent,
11
+ findNodeHandle,
12
+ NativeModules,
13
+ requireNativeComponent,
11
14
  StyleSheet,
12
15
  TouchableOpacity,
13
16
  View,
14
17
  } from 'react-native';
15
- import DocumentScanner from 'react-native-document-scanner-plugin';
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 NativeRectangleEvent = {
27
- rectangleCoordinates?: NativeRectangle | null;
28
- stableCounter?: number;
41
+ type RectangleEvent = {
42
+ rectangleCoordinates: NativeRectangle | null;
43
+ stableCounter: number;
44
+ frameWidth: number;
45
+ frameHeight: number;
29
46
  };
30
47
 
31
- type NativeCaptureResult = {
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 DocumentScannerHandle = {
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
- onRectangleDetect?: (event: NativeRectangleEvent) => void;
51
- onPictureTaken?: (event: NativeCaptureResult) => void;
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 NativeDocumentScanner = DocumentScanner as unknown as ComponentType<
55
- NativeDocumentScannerProps & { ref?: React.Ref<DocumentScannerHandle> }
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: React.FC<Props> = ({
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 scannerRef = useRef<DocumentScannerHandle | null>(null);
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 handleLayout = useCallback((event: LayoutChangeEvent) => {
114
- const { width, height } = event.nativeEvent.layout;
115
- if (width > 0 && height > 0) {
116
- setFrameSize({ width, height });
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 handleRectangleDetect = useCallback((event: NativeRectangleEvent) => {
121
- const coordinates = event?.rectangleCoordinates;
122
-
123
- if (!coordinates) {
124
- setQuad(null);
125
- return;
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
- const nextQuad: Point[] = [
129
- coordinates.topLeft,
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 = event?.croppedImage ?? event?.initialImage;
146
+ const path = payload.croppedImage ?? payload.initialImage;
143
147
  if (!path) {
144
148
  return;
145
149
  }
146
150
 
147
- const width = event?.width ?? frameSize?.width ?? 0;
148
- const height = event?.height ?? frameSize?.height ?? 0;
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
- [frameSize, onCapture, quad],
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 || capturingRef.current || !scannerRef.current) {
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
- capturingRef.current = true;
166
- scannerRef.current
167
- .capture()
168
- .catch((error) => {
169
- console.warn('[DocScanner] manual capture failed', error);
170
- capturingRef.current = false;
171
- });
172
- }, [autoCapture]);
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} onLayout={handleLayout}>
176
- <NativeDocumentScanner
177
- ref={(instance) => {
178
- scannerRef.current = instance as DocumentScannerHandle | null;
179
- }}
252
+ <View style={styles.container}>
253
+ <NativeDocScanner
254
+ ref={viewRef}
180
255
  style={StyleSheet.absoluteFill}
181
- overlayColor="transparent"
182
- detectionCountBeforeCapture={autoCapture ? minStableFrames : 10000}
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 };
@@ -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 {
@@ -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
- if (!quad || !frameSize) {
37
- if (__DEV__) {
38
- console.log('[Overlay] no quad or frameSize', { quad, frameSize });
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 (__DEV__) {
44
- console.log('[Overlay] drawing quad:', quad);
45
- console.log('[Overlay] color:', color);
46
- console.log('[Overlay] screen dimensions:', screenWidth, 'x', screenHeight);
47
- console.log('[Overlay] frame dimensions:', frameSize.width, 'x', frameSize.height);
48
- }
49
-
50
- // Check if camera is in landscape mode (width > height) but screen is portrait (height > width)
51
- const isFrameLandscape = frameSize.width > frameSize.height;
52
- const isScreenPortrait = screenHeight > screenWidth;
53
- const needsRotation = isFrameLandscape && isScreenPortrait;
54
-
55
- if (__DEV__) {
56
- console.log('[Overlay] needs rotation:', needsRotation);
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
- let transformedQuad: Point[];
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] rotation scale factors:', scaleX, 'x', scaleY);
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
- transformedQuad = quad.map((p) => ({
73
- x: p.y * scaleX,
74
- y: (frameSize.width - p.x) * scaleY,
75
- }));
76
- } else {
77
- // Same orientation - just scale
78
- const scaleX = screenWidth / frameSize.width;
79
- const scaleY = screenHeight / frameSize.height;
80
-
81
- if (__DEV__) {
82
- console.log('[Overlay] scale factors:', scaleX, 'x', scaleY);
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 (__DEV__) {
92
- console.log('[Overlay] transformed quad:', transformedQuad);
123
+ if (!transformedQuad) {
124
+ return { outlinePath: null, gridPaths: [] };
93
125
  }
94
126
 
95
- const skPath = Skia.Path.Make();
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="rgba(231, 166, 73, 0.2)" style="fill" />
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