react-native-rectangle-doc-scanner 3.23.0 → 3.26.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/utils/overlay.d.ts +0 -1
- package/dist/utils/overlay.js +119 -107
- package/package.json +3 -3
- package/src/external.d.ts +62 -52
- package/src/utils/overlay.tsx +207 -156
package/dist/utils/overlay.d.ts
CHANGED
package/dist/utils/overlay.js
CHANGED
|
@@ -36,59 +36,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.ScannerOverlay = void 0;
|
|
37
37
|
const react_1 = __importStar(require("react"));
|
|
38
38
|
const react_native_1 = require("react-native");
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const clampedAlpha = clamp(alpha, 0, 1);
|
|
51
|
-
return `rgba(${r}, ${g}, ${b}, ${clampedAlpha})`;
|
|
52
|
-
};
|
|
53
|
-
const createPolygonPath = (polygon) => {
|
|
54
|
-
if (!polygon) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
const path = react_native_skia_1.Skia.Path.Make();
|
|
58
|
-
path.moveTo(polygon.topLeft.x, polygon.topLeft.y);
|
|
59
|
-
path.lineTo(polygon.topRight.x, polygon.topRight.y);
|
|
60
|
-
path.lineTo(polygon.bottomRight.x, polygon.bottomRight.y);
|
|
61
|
-
path.lineTo(polygon.bottomLeft.x, polygon.bottomLeft.y);
|
|
62
|
-
path.close();
|
|
63
|
-
return path;
|
|
64
|
-
};
|
|
65
|
-
const interpolate = (a, b, t) => ({
|
|
66
|
-
x: a.x + (b.x - a.x) * t,
|
|
67
|
-
y: a.y + (b.y - a.y) * t,
|
|
68
|
-
});
|
|
69
|
-
const createLinePath = (start, end) => {
|
|
70
|
-
const path = react_native_skia_1.Skia.Path.Make();
|
|
71
|
-
path.moveTo(start.x, start.y);
|
|
72
|
-
path.lineTo(end.x, end.y);
|
|
73
|
-
return path;
|
|
74
|
-
};
|
|
75
|
-
const createGridPaths = (polygon) => {
|
|
76
|
-
if (!polygon) {
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
const lines = [];
|
|
80
|
-
const steps = [1 / 3, 2 / 3];
|
|
81
|
-
steps.forEach((t) => {
|
|
82
|
-
const horizontalStart = interpolate(polygon.topLeft, polygon.bottomLeft, t);
|
|
83
|
-
const horizontalEnd = interpolate(polygon.topRight, polygon.bottomRight, t);
|
|
84
|
-
lines.push(createLinePath(horizontalStart, horizontalEnd));
|
|
85
|
-
const verticalStart = interpolate(polygon.topLeft, polygon.topRight, t);
|
|
86
|
-
const verticalEnd = interpolate(polygon.bottomLeft, polygon.bottomRight, t);
|
|
87
|
-
lines.push(createLinePath(verticalStart, verticalEnd));
|
|
88
|
-
});
|
|
89
|
-
return lines;
|
|
90
|
-
};
|
|
91
|
-
const getPolygonMetrics = (polygon) => {
|
|
39
|
+
let SvgModule = null;
|
|
40
|
+
try {
|
|
41
|
+
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
|
|
42
|
+
SvgModule = require('react-native-svg');
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
SvgModule = null;
|
|
46
|
+
}
|
|
47
|
+
const SCAN_DURATION_MS = 2200;
|
|
48
|
+
const GRID_STEPS = [1 / 3, 2 / 3];
|
|
49
|
+
const calculateMetrics = (polygon) => {
|
|
92
50
|
const minX = Math.min(polygon.topLeft.x, polygon.bottomLeft.x, polygon.topRight.x, polygon.bottomRight.x);
|
|
93
51
|
const maxX = Math.max(polygon.topLeft.x, polygon.bottomLeft.x, polygon.topRight.x, polygon.bottomRight.x);
|
|
94
52
|
const minY = Math.min(polygon.topLeft.y, polygon.topRight.y, polygon.bottomLeft.y, polygon.bottomRight.y);
|
|
@@ -103,66 +61,120 @@ const getPolygonMetrics = (polygon) => {
|
|
|
103
61
|
centerX: minX + (maxX - minX) / 2,
|
|
104
62
|
};
|
|
105
63
|
};
|
|
106
|
-
const
|
|
64
|
+
const createPointsString = (polygon) => [
|
|
65
|
+
`${polygon.topLeft.x},${polygon.topLeft.y}`,
|
|
66
|
+
`${polygon.topRight.x},${polygon.topRight.y}`,
|
|
67
|
+
`${polygon.bottomRight.x},${polygon.bottomRight.y}`,
|
|
68
|
+
`${polygon.bottomLeft.x},${polygon.bottomLeft.y}`,
|
|
69
|
+
].join(' ');
|
|
70
|
+
const interpolatePoint = (a, b, t) => ({
|
|
71
|
+
x: a.x + (b.x - a.x) * t,
|
|
72
|
+
y: a.y + (b.y - a.y) * t,
|
|
73
|
+
});
|
|
74
|
+
const createGridLines = (polygon) => GRID_STEPS.flatMap((step) => {
|
|
75
|
+
const horizontalStart = interpolatePoint(polygon.topLeft, polygon.bottomLeft, step);
|
|
76
|
+
const horizontalEnd = interpolatePoint(polygon.topRight, polygon.bottomRight, step);
|
|
77
|
+
const verticalStart = interpolatePoint(polygon.topLeft, polygon.topRight, step);
|
|
78
|
+
const verticalEnd = interpolatePoint(polygon.bottomLeft, polygon.bottomRight, step);
|
|
79
|
+
return [
|
|
80
|
+
{ x1: horizontalStart.x, y1: horizontalStart.y, x2: horizontalEnd.x, y2: horizontalEnd.y },
|
|
81
|
+
{ x1: verticalStart.x, y1: verticalStart.y, x2: verticalEnd.x, y2: verticalEnd.y },
|
|
82
|
+
];
|
|
83
|
+
});
|
|
107
84
|
const ScannerOverlay = ({ active, color = '#0b7ef4', lineWidth = react_native_1.StyleSheet.hairlineWidth, polygon, }) => {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const metrics = (0, react_1.useMemo)(() => (polygon ?
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
(0, react_1.useEffect)(() => {
|
|
120
|
-
if (!metrics) {
|
|
121
|
-
return;
|
|
85
|
+
const scanProgress = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
86
|
+
const fallbackBase = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
87
|
+
const metrics = (0, react_1.useMemo)(() => (polygon ? calculateMetrics(polygon) : null), [polygon]);
|
|
88
|
+
const scanBarHeight = (0, react_1.useMemo)(() => {
|
|
89
|
+
if (!metrics)
|
|
90
|
+
return 0;
|
|
91
|
+
return Math.max(metrics.height * 0.2, 16);
|
|
92
|
+
}, [metrics]);
|
|
93
|
+
const scanTranslate = (0, react_1.useMemo)(() => {
|
|
94
|
+
if (!metrics || scanBarHeight === 0) {
|
|
95
|
+
return null;
|
|
122
96
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const clampedStart = clamp(start, metrics.minY, metrics.maxY);
|
|
134
|
-
const clampedEnd = clamp(end, metrics.minY, metrics.maxY);
|
|
135
|
-
gradientStart.current = (0, react_native_skia_1.vec)(metrics.centerX, clampedStart);
|
|
136
|
-
gradientEnd.current = (0, react_native_skia_1.vec)(metrics.centerX, clampedEnd <= clampedStart ? clampedStart + 1 : clampedEnd);
|
|
137
|
-
gradientColors.current = [transparentColor, highlightColor, transparentColor];
|
|
138
|
-
frame = requestAnimationFrame(animate);
|
|
139
|
-
};
|
|
140
|
-
gradientStart.current = (0, react_native_skia_1.vec)(metrics.centerX, metrics.minY);
|
|
141
|
-
gradientEnd.current = (0, react_native_skia_1.vec)(metrics.centerX, metrics.maxY);
|
|
142
|
-
if (active) {
|
|
143
|
-
animate();
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
gradientColors.current = [transparentColor, transparentColor, transparentColor];
|
|
97
|
+
return scanProgress.interpolate({
|
|
98
|
+
inputRange: [0, 1],
|
|
99
|
+
outputRange: [metrics.minY, Math.max(metrics.minY, metrics.maxY - scanBarHeight)],
|
|
100
|
+
});
|
|
101
|
+
}, [metrics, scanBarHeight, scanProgress]);
|
|
102
|
+
(0, react_1.useEffect)(() => {
|
|
103
|
+
if (!active || !metrics || metrics.height <= 1) {
|
|
104
|
+
scanProgress.stopAnimation();
|
|
105
|
+
scanProgress.setValue(0);
|
|
106
|
+
return undefined;
|
|
147
107
|
}
|
|
108
|
+
const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
|
|
109
|
+
react_native_1.Animated.timing(scanProgress, {
|
|
110
|
+
toValue: 1,
|
|
111
|
+
duration: SCAN_DURATION_MS,
|
|
112
|
+
easing: react_native_1.Easing.inOut(react_native_1.Easing.quad),
|
|
113
|
+
useNativeDriver: false,
|
|
114
|
+
}),
|
|
115
|
+
react_native_1.Animated.timing(scanProgress, {
|
|
116
|
+
toValue: 0,
|
|
117
|
+
duration: SCAN_DURATION_MS,
|
|
118
|
+
easing: react_native_1.Easing.inOut(react_native_1.Easing.quad),
|
|
119
|
+
useNativeDriver: false,
|
|
120
|
+
}),
|
|
121
|
+
]));
|
|
122
|
+
loop.start();
|
|
148
123
|
return () => {
|
|
149
|
-
|
|
150
|
-
cancelAnimationFrame(frame);
|
|
151
|
-
}
|
|
124
|
+
loop.stop();
|
|
152
125
|
};
|
|
153
|
-
}, [active,
|
|
154
|
-
if (!polygon || !
|
|
126
|
+
}, [active, metrics, scanProgress]);
|
|
127
|
+
if (!polygon || !metrics || metrics.width <= 0 || metrics.height <= 0) {
|
|
155
128
|
return null;
|
|
156
129
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
react_1.default.createElement(
|
|
130
|
+
if (SvgModule) {
|
|
131
|
+
const { default: Svg, Polygon, Line, Defs, LinearGradient, Stop, Rect } = SvgModule;
|
|
132
|
+
const AnimatedRect = react_native_1.Animated.createAnimatedComponent(Rect);
|
|
133
|
+
const gridLines = createGridLines(polygon);
|
|
134
|
+
const points = createPointsString(polygon);
|
|
135
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: react_native_1.StyleSheet.absoluteFill },
|
|
136
|
+
react_1.default.createElement(Svg, { style: react_native_1.StyleSheet.absoluteFill },
|
|
137
|
+
react_1.default.createElement(Polygon, { points: points, fill: color, opacity: 0.15 }),
|
|
138
|
+
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 }))),
|
|
139
|
+
react_1.default.createElement(Polygon, { points: points, stroke: color, strokeWidth: lineWidth, fill: "none" }),
|
|
140
|
+
react_1.default.createElement(Defs, null,
|
|
141
|
+
react_1.default.createElement(LinearGradient, { id: "scanGradient", x1: "0", y1: "0", x2: "0", y2: "1" },
|
|
142
|
+
react_1.default.createElement(Stop, { offset: "0%", stopColor: "rgba(255,255,255,0)" }),
|
|
143
|
+
react_1.default.createElement(Stop, { offset: "50%", stopColor: color, stopOpacity: 0.8 }),
|
|
144
|
+
react_1.default.createElement(Stop, { offset: "100%", stopColor: "rgba(255,255,255,0)" }))),
|
|
145
|
+
active && scanTranslate && (react_1.default.createElement(AnimatedRect, { x: metrics.minX, width: metrics.width, height: scanBarHeight, fill: "url(#scanGradient)", y: scanTranslate })))));
|
|
146
|
+
}
|
|
147
|
+
// Fallback rendering without react-native-svg
|
|
148
|
+
const relativeTranslate = scanTranslate != null ? react_native_1.Animated.subtract(scanTranslate, metrics.minY) : fallbackBase;
|
|
149
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: react_native_1.StyleSheet.absoluteFill },
|
|
150
|
+
react_1.default.createElement(react_native_1.View, { style: [
|
|
151
|
+
styles.fallbackBox,
|
|
152
|
+
{
|
|
153
|
+
left: metrics.minX,
|
|
154
|
+
top: metrics.minY,
|
|
155
|
+
width: metrics.width,
|
|
156
|
+
height: metrics.height,
|
|
157
|
+
borderColor: color,
|
|
158
|
+
borderWidth: lineWidth,
|
|
159
|
+
},
|
|
160
|
+
] }, active && (react_1.default.createElement(react_native_1.Animated.View, { style: [
|
|
161
|
+
styles.fallbackScanBar,
|
|
162
|
+
{
|
|
163
|
+
backgroundColor: color,
|
|
164
|
+
height: scanBarHeight,
|
|
165
|
+
transform: [{ translateY: relativeTranslate }],
|
|
166
|
+
},
|
|
167
|
+
] })))));
|
|
167
168
|
};
|
|
168
169
|
exports.ScannerOverlay = ScannerOverlay;
|
|
170
|
+
const styles = react_native_1.StyleSheet.create({
|
|
171
|
+
fallbackBox: {
|
|
172
|
+
position: 'absolute',
|
|
173
|
+
backgroundColor: 'rgba(11, 126, 244, 0.1)',
|
|
174
|
+
overflow: 'hidden',
|
|
175
|
+
},
|
|
176
|
+
fallbackScanBar: {
|
|
177
|
+
width: '100%',
|
|
178
|
+
opacity: 0.4,
|
|
179
|
+
},
|
|
180
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-rectangle-doc-scanner",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.26.0",
|
|
4
4
|
"description": "Native-backed document scanner for React Native with customizable overlays.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"*.json"
|
|
31
31
|
],
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@shopify/react-native-skia": "*",
|
|
34
33
|
"react": "*",
|
|
35
34
|
"react-native": "*",
|
|
36
|
-
"react-native-perspective-image-cropper": "*"
|
|
35
|
+
"react-native-perspective-image-cropper": "*",
|
|
36
|
+
"react-native-svg": "*"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/react": "^18.2.41",
|
package/src/external.d.ts
CHANGED
|
@@ -1,55 +1,3 @@
|
|
|
1
|
-
declare module '@shopify/react-native-skia' {
|
|
2
|
-
import type { ComponentType, ReactNode } from 'react';
|
|
3
|
-
import type { ViewStyle } from 'react-native';
|
|
4
|
-
|
|
5
|
-
export type SkPath = {
|
|
6
|
-
moveTo: (x: number, y: number) => void;
|
|
7
|
-
lineTo: (x: number, y: number) => void;
|
|
8
|
-
close: () => void;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export const Skia: {
|
|
12
|
-
Path: {
|
|
13
|
-
Make: () => SkPath;
|
|
14
|
-
};
|
|
15
|
-
Color: (color: string | number) => number;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type CanvasProps = {
|
|
19
|
-
style?: ViewStyle;
|
|
20
|
-
children?: ReactNode;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const Canvas: ComponentType<CanvasProps>;
|
|
24
|
-
|
|
25
|
-
export type PathProps = {
|
|
26
|
-
path: SkPath;
|
|
27
|
-
style?: 'stroke' | 'fill';
|
|
28
|
-
strokeWidth?: number;
|
|
29
|
-
color?: string;
|
|
30
|
-
children?: ReactNode;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const Path: ComponentType<PathProps>;
|
|
34
|
-
|
|
35
|
-
export type SkiaValue<T> = {
|
|
36
|
-
current: T;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const useValue: <T>(initialValue: T) => SkiaValue<T>;
|
|
40
|
-
|
|
41
|
-
export const vec: (x: number, y: number) => { x: number; y: number };
|
|
42
|
-
|
|
43
|
-
export type LinearGradientProps = {
|
|
44
|
-
start: SkiaValue<{ x: number; y: number }> | { x: number; y: number };
|
|
45
|
-
end: SkiaValue<{ x: number; y: number }> | { x: number; y: number };
|
|
46
|
-
colors: SkiaValue<number[]> | number[];
|
|
47
|
-
positions?: SkiaValue<number[]> | number[];
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const LinearGradient: ComponentType<LinearGradientProps>;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
1
|
declare module 'react-native-perspective-image-cropper' {
|
|
54
2
|
import type { ComponentType } from 'react';
|
|
55
3
|
|
|
@@ -135,3 +83,65 @@ declare module 'react-native-document-scanner' {
|
|
|
135
83
|
capture(): Promise<DocumentScannerResult>;
|
|
136
84
|
}
|
|
137
85
|
}
|
|
86
|
+
|
|
87
|
+
declare module 'react-native-svg' {
|
|
88
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
89
|
+
|
|
90
|
+
export type SvgProps = {
|
|
91
|
+
children?: ReactNode;
|
|
92
|
+
style?: any;
|
|
93
|
+
width?: number | string;
|
|
94
|
+
height?: number | string;
|
|
95
|
+
viewBox?: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type PolygonProps = {
|
|
99
|
+
points: string;
|
|
100
|
+
fill?: string;
|
|
101
|
+
stroke?: string;
|
|
102
|
+
strokeWidth?: number;
|
|
103
|
+
opacity?: number;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type LineProps = {
|
|
107
|
+
x1: number;
|
|
108
|
+
y1: number;
|
|
109
|
+
x2: number;
|
|
110
|
+
y2: number;
|
|
111
|
+
stroke?: string;
|
|
112
|
+
strokeWidth?: number;
|
|
113
|
+
opacity?: number;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type RectProps = {
|
|
117
|
+
x: number;
|
|
118
|
+
y: number;
|
|
119
|
+
width: number;
|
|
120
|
+
height: number;
|
|
121
|
+
fill?: string;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type StopProps = {
|
|
125
|
+
offset: string;
|
|
126
|
+
stopColor: string;
|
|
127
|
+
stopOpacity?: number;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export type LinearGradientProps = {
|
|
131
|
+
id: string;
|
|
132
|
+
x1?: string;
|
|
133
|
+
y1?: string;
|
|
134
|
+
x2?: string;
|
|
135
|
+
y2?: string;
|
|
136
|
+
children?: ReactNode;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
declare const Svg: ComponentType<SvgProps>;
|
|
140
|
+
export default Svg;
|
|
141
|
+
export const Polygon: ComponentType<PolygonProps>;
|
|
142
|
+
export const Line: ComponentType<LineProps>;
|
|
143
|
+
export const Rect: ComponentType<RectProps>;
|
|
144
|
+
export const Defs: ComponentType<{ children?: ReactNode }>;
|
|
145
|
+
export const LinearGradient: ComponentType<LinearGradientProps>;
|
|
146
|
+
export const Stop: ComponentType<StopProps>;
|
|
147
|
+
}
|
package/src/utils/overlay.tsx
CHANGED
|
@@ -1,94 +1,54 @@
|
|
|
1
|
-
import React, { useEffect, useMemo } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
Canvas,
|
|
5
|
-
LinearGradient,
|
|
6
|
-
Path,
|
|
7
|
-
Skia,
|
|
8
|
-
SkPath,
|
|
9
|
-
useValue,
|
|
10
|
-
vec,
|
|
11
|
-
} from '@shopify/react-native-skia';
|
|
12
|
-
import type { Point, Rectangle } from '../types';
|
|
1
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { Animated, Easing, StyleSheet, View } from 'react-native';
|
|
3
|
+
import type { Rectangle } from '../types';
|
|
13
4
|
|
|
14
|
-
|
|
15
|
-
/** 자동 캡처 중임을 표시할 때 true로 설정합니다. */
|
|
16
|
-
active: boolean;
|
|
17
|
-
color?: string;
|
|
18
|
-
lineWidth?: number;
|
|
19
|
-
polygon?: Rectangle | null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
|
|
23
|
-
|
|
24
|
-
const withAlpha = (inputColor: string, alpha: number): string => {
|
|
25
|
-
const parsed = processColor(inputColor);
|
|
26
|
-
const normalized = typeof parsed === 'number' ? parsed >>> 0 : null;
|
|
27
|
-
|
|
28
|
-
if (normalized == null) {
|
|
29
|
-
return inputColor;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const r = (normalized >> 16) & 0xff;
|
|
33
|
-
const g = (normalized >> 8) & 0xff;
|
|
34
|
-
const b = normalized & 0xff;
|
|
35
|
-
const clampedAlpha = clamp(alpha, 0, 1);
|
|
36
|
-
|
|
37
|
-
return `rgba(${r}, ${g}, ${b}, ${clampedAlpha})`;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const createPolygonPath = (polygon: Rectangle | null): SkPath | null => {
|
|
41
|
-
if (!polygon) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const path = Skia.Path.Make();
|
|
46
|
-
path.moveTo(polygon.topLeft.x, polygon.topLeft.y);
|
|
47
|
-
path.lineTo(polygon.topRight.x, polygon.topRight.y);
|
|
48
|
-
path.lineTo(polygon.bottomRight.x, polygon.bottomRight.y);
|
|
49
|
-
path.lineTo(polygon.bottomLeft.x, polygon.bottomLeft.y);
|
|
50
|
-
path.close();
|
|
51
|
-
return path;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const interpolate = (a: Point, b: Point, t: number): Point => ({
|
|
55
|
-
x: a.x + (b.x - a.x) * t,
|
|
56
|
-
y: a.y + (b.y - a.y) * t,
|
|
57
|
-
});
|
|
5
|
+
let SvgModule: typeof import('react-native-svg') | null = null;
|
|
58
6
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const createGridPaths = (polygon: Rectangle | null): SkPath[] => {
|
|
67
|
-
if (!polygon) {
|
|
68
|
-
return [];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const lines: SkPath[] = [];
|
|
72
|
-
const steps = [1 / 3, 2 / 3];
|
|
73
|
-
|
|
74
|
-
steps.forEach((t) => {
|
|
75
|
-
const horizontalStart = interpolate(polygon.topLeft, polygon.bottomLeft, t);
|
|
76
|
-
const horizontalEnd = interpolate(polygon.topRight, polygon.bottomRight, t);
|
|
77
|
-
lines.push(createLinePath(horizontalStart, horizontalEnd));
|
|
78
|
-
|
|
79
|
-
const verticalStart = interpolate(polygon.topLeft, polygon.topRight, t);
|
|
80
|
-
const verticalEnd = interpolate(polygon.bottomLeft, polygon.bottomRight, t);
|
|
81
|
-
lines.push(createLinePath(verticalStart, verticalEnd));
|
|
82
|
-
});
|
|
7
|
+
try {
|
|
8
|
+
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
|
|
9
|
+
SvgModule = require('react-native-svg');
|
|
10
|
+
} catch (error) {
|
|
11
|
+
SvgModule = null;
|
|
12
|
+
}
|
|
83
13
|
|
|
84
|
-
|
|
14
|
+
const SCAN_DURATION_MS = 2200;
|
|
15
|
+
const GRID_STEPS = [1 / 3, 2 / 3];
|
|
16
|
+
|
|
17
|
+
type PolygonMetrics = {
|
|
18
|
+
minX: number;
|
|
19
|
+
maxX: number;
|
|
20
|
+
minY: number;
|
|
21
|
+
maxY: number;
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
centerX: number;
|
|
85
25
|
};
|
|
86
26
|
|
|
87
|
-
const
|
|
88
|
-
const minX = Math.min(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
27
|
+
const calculateMetrics = (polygon: Rectangle): PolygonMetrics => {
|
|
28
|
+
const minX = Math.min(
|
|
29
|
+
polygon.topLeft.x,
|
|
30
|
+
polygon.bottomLeft.x,
|
|
31
|
+
polygon.topRight.x,
|
|
32
|
+
polygon.bottomRight.x,
|
|
33
|
+
);
|
|
34
|
+
const maxX = Math.max(
|
|
35
|
+
polygon.topLeft.x,
|
|
36
|
+
polygon.bottomLeft.x,
|
|
37
|
+
polygon.topRight.x,
|
|
38
|
+
polygon.bottomRight.x,
|
|
39
|
+
);
|
|
40
|
+
const minY = Math.min(
|
|
41
|
+
polygon.topLeft.y,
|
|
42
|
+
polygon.topRight.y,
|
|
43
|
+
polygon.bottomLeft.y,
|
|
44
|
+
polygon.bottomRight.y,
|
|
45
|
+
);
|
|
46
|
+
const maxY = Math.max(
|
|
47
|
+
polygon.topLeft.y,
|
|
48
|
+
polygon.topRight.y,
|
|
49
|
+
polygon.bottomLeft.y,
|
|
50
|
+
polygon.bottomRight.y,
|
|
51
|
+
);
|
|
92
52
|
|
|
93
53
|
return {
|
|
94
54
|
minX,
|
|
@@ -101,7 +61,38 @@ const getPolygonMetrics = (polygon: Rectangle) => {
|
|
|
101
61
|
};
|
|
102
62
|
};
|
|
103
63
|
|
|
104
|
-
const
|
|
64
|
+
const createPointsString = (polygon: Rectangle): string =>
|
|
65
|
+
[
|
|
66
|
+
`${polygon.topLeft.x},${polygon.topLeft.y}`,
|
|
67
|
+
`${polygon.topRight.x},${polygon.topRight.y}`,
|
|
68
|
+
`${polygon.bottomRight.x},${polygon.bottomRight.y}`,
|
|
69
|
+
`${polygon.bottomLeft.x},${polygon.bottomLeft.y}`,
|
|
70
|
+
].join(' ');
|
|
71
|
+
|
|
72
|
+
const interpolatePoint = (a: { x: number; y: number }, b: { x: number; y: number }, t: number) => ({
|
|
73
|
+
x: a.x + (b.x - a.x) * t,
|
|
74
|
+
y: a.y + (b.y - a.y) * t,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const createGridLines = (polygon: Rectangle) =>
|
|
78
|
+
GRID_STEPS.flatMap((step) => {
|
|
79
|
+
const horizontalStart = interpolatePoint(polygon.topLeft, polygon.bottomLeft, step);
|
|
80
|
+
const horizontalEnd = interpolatePoint(polygon.topRight, polygon.bottomRight, step);
|
|
81
|
+
const verticalStart = interpolatePoint(polygon.topLeft, polygon.topRight, step);
|
|
82
|
+
const verticalEnd = interpolatePoint(polygon.bottomLeft, polygon.bottomRight, step);
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
{ x1: horizontalStart.x, y1: horizontalStart.y, x2: horizontalEnd.x, y2: horizontalEnd.y },
|
|
86
|
+
{ x1: verticalStart.x, y1: verticalStart.y, x2: verticalEnd.x, y2: verticalEnd.y },
|
|
87
|
+
];
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export interface ScannerOverlayProps {
|
|
91
|
+
active: boolean;
|
|
92
|
+
color?: string;
|
|
93
|
+
lineWidth?: number;
|
|
94
|
+
polygon?: Rectangle | null;
|
|
95
|
+
}
|
|
105
96
|
|
|
106
97
|
export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
|
|
107
98
|
active,
|
|
@@ -109,90 +100,150 @@ export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
|
|
|
109
100
|
lineWidth = StyleSheet.hairlineWidth,
|
|
110
101
|
polygon,
|
|
111
102
|
}) => {
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const metrics = useMemo(() => (polygon ? getPolygonMetrics(polygon) : null), [polygon]);
|
|
115
|
-
|
|
116
|
-
const gradientStart = useValue(vec(0, 0));
|
|
117
|
-
const gradientEnd = useValue(vec(0, 0));
|
|
118
|
-
const gradientColors = useValue<number[]>([
|
|
119
|
-
Skia.Color(withAlpha(color, 0)),
|
|
120
|
-
Skia.Color(withAlpha(color, 0.85)),
|
|
121
|
-
Skia.Color(withAlpha(color, 0)),
|
|
122
|
-
]);
|
|
123
|
-
const gradientPositions = useValue<number[]>([0, 0.5, 1]);
|
|
103
|
+
const scanProgress = useRef(new Animated.Value(0)).current;
|
|
104
|
+
const fallbackBase = useRef(new Animated.Value(0)).current;
|
|
124
105
|
|
|
125
|
-
|
|
126
|
-
if (!metrics) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
106
|
+
const metrics = useMemo(() => (polygon ? calculateMetrics(polygon) : null), [polygon]);
|
|
129
107
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const animate = () => {
|
|
136
|
-
const now = Date.now() % SCAN_DURATION_MS;
|
|
137
|
-
const progress = now / SCAN_DURATION_MS;
|
|
138
|
-
const travel = metrics.height + bandSize * 2;
|
|
139
|
-
const start = metrics.minY - bandSize + travel * progress;
|
|
140
|
-
const end = start + bandSize;
|
|
141
|
-
|
|
142
|
-
const clampedStart = clamp(start, metrics.minY, metrics.maxY);
|
|
143
|
-
const clampedEnd = clamp(end, metrics.minY, metrics.maxY);
|
|
144
|
-
|
|
145
|
-
gradientStart.current = vec(metrics.centerX, clampedStart);
|
|
146
|
-
gradientEnd.current = vec(
|
|
147
|
-
metrics.centerX,
|
|
148
|
-
clampedEnd <= clampedStart ? clampedStart + 1 : clampedEnd,
|
|
149
|
-
);
|
|
150
|
-
gradientColors.current = [transparentColor, highlightColor, transparentColor];
|
|
151
|
-
|
|
152
|
-
frame = requestAnimationFrame(animate);
|
|
153
|
-
};
|
|
108
|
+
const scanBarHeight = useMemo(() => {
|
|
109
|
+
if (!metrics) return 0;
|
|
110
|
+
return Math.max(metrics.height * 0.2, 16);
|
|
111
|
+
}, [metrics]);
|
|
154
112
|
|
|
155
|
-
|
|
156
|
-
|
|
113
|
+
const scanTranslate = useMemo(() => {
|
|
114
|
+
if (!metrics || scanBarHeight === 0) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return scanProgress.interpolate({
|
|
119
|
+
inputRange: [0, 1],
|
|
120
|
+
outputRange: [metrics.minY, Math.max(metrics.minY, metrics.maxY - scanBarHeight)],
|
|
121
|
+
});
|
|
122
|
+
}, [metrics, scanBarHeight, scanProgress]);
|
|
157
123
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!active || !metrics || metrics.height <= 1) {
|
|
126
|
+
scanProgress.stopAnimation();
|
|
127
|
+
scanProgress.setValue(0);
|
|
128
|
+
return undefined;
|
|
162
129
|
}
|
|
163
130
|
|
|
131
|
+
const loop = Animated.loop(
|
|
132
|
+
Animated.sequence([
|
|
133
|
+
Animated.timing(scanProgress, {
|
|
134
|
+
toValue: 1,
|
|
135
|
+
duration: SCAN_DURATION_MS,
|
|
136
|
+
easing: Easing.inOut(Easing.quad),
|
|
137
|
+
useNativeDriver: false,
|
|
138
|
+
}),
|
|
139
|
+
Animated.timing(scanProgress, {
|
|
140
|
+
toValue: 0,
|
|
141
|
+
duration: SCAN_DURATION_MS,
|
|
142
|
+
easing: Easing.inOut(Easing.quad),
|
|
143
|
+
useNativeDriver: false,
|
|
144
|
+
}),
|
|
145
|
+
]),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
loop.start();
|
|
164
149
|
return () => {
|
|
165
|
-
|
|
166
|
-
cancelAnimationFrame(frame);
|
|
167
|
-
}
|
|
150
|
+
loop.stop();
|
|
168
151
|
};
|
|
169
|
-
}, [active,
|
|
152
|
+
}, [active, metrics, scanProgress]);
|
|
170
153
|
|
|
171
|
-
if (!polygon || !
|
|
154
|
+
if (!polygon || !metrics || metrics.width <= 0 || metrics.height <= 0) {
|
|
172
155
|
return null;
|
|
173
156
|
}
|
|
174
157
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
158
|
+
if (SvgModule) {
|
|
159
|
+
const { default: Svg, Polygon, Line, Defs, LinearGradient, Stop, Rect } = SvgModule;
|
|
160
|
+
const AnimatedRect = Animated.createAnimatedComponent(Rect);
|
|
161
|
+
|
|
162
|
+
const gridLines = createGridLines(polygon);
|
|
163
|
+
const points = createPointsString(polygon);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
|
|
167
|
+
<Svg style={StyleSheet.absoluteFill}>
|
|
168
|
+
<Polygon points={points} fill={color} opacity={0.15} />
|
|
169
|
+
{gridLines.map((line, index) => (
|
|
170
|
+
<Line
|
|
171
|
+
key={`grid-${index}`}
|
|
172
|
+
x1={line.x1}
|
|
173
|
+
y1={line.y1}
|
|
174
|
+
x2={line.x2}
|
|
175
|
+
y2={line.y2}
|
|
176
|
+
stroke={color}
|
|
177
|
+
strokeWidth={lineWidth}
|
|
178
|
+
opacity={0.5}
|
|
179
|
+
/>
|
|
180
|
+
))}
|
|
181
|
+
<Polygon points={points} stroke={color} strokeWidth={lineWidth} fill="none" />
|
|
182
|
+
<Defs>
|
|
183
|
+
<LinearGradient id="scanGradient" x1="0" y1="0" x2="0" y2="1">
|
|
184
|
+
<Stop offset="0%" stopColor="rgba(255,255,255,0)" />
|
|
185
|
+
<Stop offset="50%" stopColor={color} stopOpacity={0.8} />
|
|
186
|
+
<Stop offset="100%" stopColor="rgba(255,255,255,0)" />
|
|
187
|
+
</LinearGradient>
|
|
188
|
+
</Defs>
|
|
189
|
+
{active && scanTranslate && (
|
|
190
|
+
<AnimatedRect
|
|
191
|
+
x={metrics.minX}
|
|
192
|
+
width={metrics.width}
|
|
193
|
+
height={scanBarHeight}
|
|
194
|
+
fill="url(#scanGradient)"
|
|
195
|
+
y={scanTranslate}
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
198
|
+
</Svg>
|
|
199
|
+
</View>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Fallback rendering without react-native-svg
|
|
204
|
+
const relativeTranslate =
|
|
205
|
+
scanTranslate != null ? Animated.subtract(scanTranslate, metrics.minY) : fallbackBase;
|
|
178
206
|
|
|
179
207
|
return (
|
|
180
|
-
<View pointerEvents="none" style={StyleSheet.
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
208
|
+
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
|
|
209
|
+
<View
|
|
210
|
+
style={[
|
|
211
|
+
styles.fallbackBox,
|
|
212
|
+
{
|
|
213
|
+
left: metrics.minX,
|
|
214
|
+
top: metrics.minY,
|
|
215
|
+
width: metrics.width,
|
|
216
|
+
height: metrics.height,
|
|
217
|
+
borderColor: color,
|
|
218
|
+
borderWidth: lineWidth,
|
|
219
|
+
},
|
|
220
|
+
]}
|
|
221
|
+
>
|
|
222
|
+
{active && (
|
|
223
|
+
<Animated.View
|
|
224
|
+
style={[
|
|
225
|
+
styles.fallbackScanBar,
|
|
226
|
+
{
|
|
227
|
+
backgroundColor: color,
|
|
228
|
+
height: scanBarHeight,
|
|
229
|
+
transform: [{ translateY: relativeTranslate }],
|
|
230
|
+
},
|
|
231
|
+
]}
|
|
193
232
|
/>
|
|
194
|
-
|
|
195
|
-
</
|
|
233
|
+
)}
|
|
234
|
+
</View>
|
|
196
235
|
</View>
|
|
197
236
|
);
|
|
198
237
|
};
|
|
238
|
+
|
|
239
|
+
const styles = StyleSheet.create({
|
|
240
|
+
fallbackBox: {
|
|
241
|
+
position: 'absolute',
|
|
242
|
+
backgroundColor: 'rgba(11, 126, 244, 0.1)',
|
|
243
|
+
overflow: 'hidden',
|
|
244
|
+
},
|
|
245
|
+
fallbackScanBar: {
|
|
246
|
+
width: '100%',
|
|
247
|
+
opacity: 0.4,
|
|
248
|
+
},
|
|
249
|
+
});
|