react-native-rectangle-doc-scanner 3.23.0 → 3.27.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 +133 -106
- package/package.json +3 -3
- package/src/external.d.ts +62 -52
- package/src/utils/overlay.tsx +222 -155
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);
|
|
@@ -100,69 +58,138 @@ const getPolygonMetrics = (polygon) => {
|
|
|
100
58
|
maxY,
|
|
101
59
|
width: maxX - minX,
|
|
102
60
|
height: maxY - minY,
|
|
103
|
-
centerX: minX + (maxX - minX) / 2,
|
|
104
61
|
};
|
|
105
62
|
};
|
|
106
|
-
const
|
|
63
|
+
const createPointsString = (polygon) => [
|
|
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
|
+
const interpolatePoint = (a, b, t) => ({
|
|
70
|
+
x: a.x + (b.x - a.x) * t,
|
|
71
|
+
y: a.y + (b.y - a.y) * t,
|
|
72
|
+
});
|
|
73
|
+
const createGridLines = (polygon) => GRID_STEPS.flatMap((step) => {
|
|
74
|
+
const horizontalStart = interpolatePoint(polygon.topLeft, polygon.bottomLeft, step);
|
|
75
|
+
const horizontalEnd = interpolatePoint(polygon.topRight, polygon.bottomRight, step);
|
|
76
|
+
const verticalStart = interpolatePoint(polygon.topLeft, polygon.topRight, step);
|
|
77
|
+
const verticalEnd = interpolatePoint(polygon.bottomLeft, polygon.bottomRight, step);
|
|
78
|
+
return [
|
|
79
|
+
{ x1: horizontalStart.x, y1: horizontalStart.y, x2: horizontalEnd.x, y2: horizontalEnd.y },
|
|
80
|
+
{ x1: verticalStart.x, y1: verticalStart.y, x2: verticalEnd.x, y2: verticalEnd.y },
|
|
81
|
+
];
|
|
82
|
+
});
|
|
107
83
|
const ScannerOverlay = ({ active, color = '#0b7ef4', lineWidth = react_native_1.StyleSheet.hairlineWidth, polygon, }) => {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const gradientPositions = (0, react_native_skia_1.useValue)([0, 0.5, 1]);
|
|
119
|
-
(0, react_1.useEffect)(() => {
|
|
84
|
+
const scanProgress = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
85
|
+
const fallbackBase = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
86
|
+
const [scanY, setScanY] = (0, react_1.useState)(null);
|
|
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 travelDistance = (0, react_1.useMemo)(() => {
|
|
120
94
|
if (!metrics) {
|
|
121
|
-
return;
|
|
95
|
+
return 0;
|
|
122
96
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const start = metrics.minY - bandSize + travel * progress;
|
|
132
|
-
const end = start + bandSize;
|
|
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 Math.max(metrics.height - scanBarHeight, 0);
|
|
98
|
+
}, [metrics, scanBarHeight]);
|
|
99
|
+
(0, react_1.useEffect)(() => {
|
|
100
|
+
scanProgress.stopAnimation();
|
|
101
|
+
scanProgress.setValue(0);
|
|
102
|
+
setScanY(null);
|
|
103
|
+
if (!active || !metrics || travelDistance <= 0) {
|
|
104
|
+
return undefined;
|
|
147
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();
|
|
148
121
|
return () => {
|
|
149
|
-
|
|
150
|
-
|
|
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);
|
|
151
135
|
}
|
|
136
|
+
});
|
|
137
|
+
return () => {
|
|
138
|
+
scanProgress.removeListener(listenerId);
|
|
152
139
|
};
|
|
153
|
-
}, [
|
|
154
|
-
if (!polygon || !
|
|
140
|
+
}, [metrics, scanProgress, travelDistance]);
|
|
141
|
+
if (!polygon || !metrics || metrics.width <= 0 || metrics.height <= 0) {
|
|
155
142
|
return null;
|
|
156
143
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
react_1.default.createElement(
|
|
144
|
+
if (SvgModule) {
|
|
145
|
+
const { default: Svg, Polygon, Line, Defs, LinearGradient, Stop, Rect } = SvgModule;
|
|
146
|
+
const gridLines = createGridLines(polygon);
|
|
147
|
+
const points = createPointsString(polygon);
|
|
148
|
+
const scanRectY = scanY ?? metrics.minY;
|
|
149
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: react_native_1.StyleSheet.absoluteFill },
|
|
150
|
+
react_1.default.createElement(Svg, { style: react_native_1.StyleSheet.absoluteFill },
|
|
151
|
+
react_1.default.createElement(Polygon, { points: points, fill: color, opacity: 0.15 }),
|
|
152
|
+
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 })))));
|
|
160
|
+
}
|
|
161
|
+
const relativeTranslate = metrics && travelDistance > 0
|
|
162
|
+
? react_native_1.Animated.multiply(scanProgress, travelDistance)
|
|
163
|
+
: fallbackBase;
|
|
164
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: react_native_1.StyleSheet.absoluteFill },
|
|
165
|
+
react_1.default.createElement(react_native_1.View, { style: [
|
|
166
|
+
styles.fallbackBox,
|
|
167
|
+
{
|
|
168
|
+
left: metrics.minX,
|
|
169
|
+
top: metrics.minY,
|
|
170
|
+
width: metrics.width,
|
|
171
|
+
height: metrics.height,
|
|
172
|
+
borderColor: color,
|
|
173
|
+
borderWidth: lineWidth,
|
|
174
|
+
},
|
|
175
|
+
] }, active && travelDistance > 0 && (react_1.default.createElement(react_native_1.Animated.View, { style: [
|
|
176
|
+
styles.fallbackScanBar,
|
|
177
|
+
{
|
|
178
|
+
backgroundColor: color,
|
|
179
|
+
height: scanBarHeight,
|
|
180
|
+
transform: [{ translateY: relativeTranslate }],
|
|
181
|
+
},
|
|
182
|
+
] })))));
|
|
167
183
|
};
|
|
168
184
|
exports.ScannerOverlay = ScannerOverlay;
|
|
185
|
+
const styles = react_native_1.StyleSheet.create({
|
|
186
|
+
fallbackBox: {
|
|
187
|
+
position: 'absolute',
|
|
188
|
+
backgroundColor: 'rgba(11, 126, 244, 0.1)',
|
|
189
|
+
overflow: 'hidden',
|
|
190
|
+
},
|
|
191
|
+
fallbackScanBar: {
|
|
192
|
+
width: '100%',
|
|
193
|
+
opacity: 0.4,
|
|
194
|
+
},
|
|
195
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-rectangle-doc-scanner",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.27.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,53 @@
|
|
|
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, useState } 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));
|
|
5
|
+
let SvgModule: typeof import('react-native-svg') | null = null;
|
|
23
6
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
});
|
|
58
|
-
|
|
59
|
-
const createLinePath = (start: Point, end: Point): SkPath => {
|
|
60
|
-
const path = Skia.Path.Make();
|
|
61
|
-
path.moveTo(start.x, start.y);
|
|
62
|
-
path.lineTo(end.x, end.y);
|
|
63
|
-
return path;
|
|
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;
|
|
85
24
|
};
|
|
86
25
|
|
|
87
|
-
const
|
|
88
|
-
const minX = Math.min(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
26
|
+
const calculateMetrics = (polygon: Rectangle): PolygonMetrics => {
|
|
27
|
+
const minX = Math.min(
|
|
28
|
+
polygon.topLeft.x,
|
|
29
|
+
polygon.bottomLeft.x,
|
|
30
|
+
polygon.topRight.x,
|
|
31
|
+
polygon.bottomRight.x,
|
|
32
|
+
);
|
|
33
|
+
const maxX = Math.max(
|
|
34
|
+
polygon.topLeft.x,
|
|
35
|
+
polygon.bottomLeft.x,
|
|
36
|
+
polygon.topRight.x,
|
|
37
|
+
polygon.bottomRight.x,
|
|
38
|
+
);
|
|
39
|
+
const minY = Math.min(
|
|
40
|
+
polygon.topLeft.y,
|
|
41
|
+
polygon.topRight.y,
|
|
42
|
+
polygon.bottomLeft.y,
|
|
43
|
+
polygon.bottomRight.y,
|
|
44
|
+
);
|
|
45
|
+
const maxY = Math.max(
|
|
46
|
+
polygon.topLeft.y,
|
|
47
|
+
polygon.topRight.y,
|
|
48
|
+
polygon.bottomLeft.y,
|
|
49
|
+
polygon.bottomRight.y,
|
|
50
|
+
);
|
|
92
51
|
|
|
93
52
|
return {
|
|
94
53
|
minX,
|
|
@@ -97,11 +56,41 @@ const getPolygonMetrics = (polygon: Rectangle) => {
|
|
|
97
56
|
maxY,
|
|
98
57
|
width: maxX - minX,
|
|
99
58
|
height: maxY - minY,
|
|
100
|
-
centerX: minX + (maxX - minX) / 2,
|
|
101
59
|
};
|
|
102
60
|
};
|
|
103
61
|
|
|
104
|
-
const
|
|
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
|
+
export interface ScannerOverlayProps {
|
|
89
|
+
active: boolean;
|
|
90
|
+
color?: string;
|
|
91
|
+
lineWidth?: number;
|
|
92
|
+
polygon?: Rectangle | null;
|
|
93
|
+
}
|
|
105
94
|
|
|
106
95
|
export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
|
|
107
96
|
active,
|
|
@@ -109,90 +98,168 @@ export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
|
|
|
109
98
|
lineWidth = StyleSheet.hairlineWidth,
|
|
110
99
|
polygon,
|
|
111
100
|
}) => {
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
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]);
|
|
101
|
+
const scanProgress = useRef(new Animated.Value(0)).current;
|
|
102
|
+
const fallbackBase = useRef(new Animated.Value(0)).current;
|
|
103
|
+
const [scanY, setScanY] = useState<number | null>(null);
|
|
124
104
|
|
|
125
|
-
|
|
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(() => {
|
|
126
113
|
if (!metrics) {
|
|
127
|
-
return;
|
|
114
|
+
return 0;
|
|
128
115
|
}
|
|
116
|
+
return Math.max(metrics.height - scanBarHeight, 0);
|
|
117
|
+
}, [metrics, scanBarHeight]);
|
|
129
118
|
|
|
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
|
-
};
|
|
154
|
-
|
|
155
|
-
gradientStart.current = vec(metrics.centerX, metrics.minY);
|
|
156
|
-
gradientEnd.current = vec(metrics.centerX, metrics.maxY);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
scanProgress.stopAnimation();
|
|
121
|
+
scanProgress.setValue(0);
|
|
122
|
+
setScanY(null);
|
|
157
123
|
|
|
158
|
-
if (active) {
|
|
159
|
-
|
|
160
|
-
} else {
|
|
161
|
-
gradientColors.current = [transparentColor, transparentColor, transparentColor];
|
|
124
|
+
if (!active || !metrics || travelDistance <= 0) {
|
|
125
|
+
return undefined;
|
|
162
126
|
}
|
|
163
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
|
+
);
|
|
144
|
+
|
|
145
|
+
loop.start();
|
|
164
146
|
return () => {
|
|
165
|
-
|
|
166
|
-
|
|
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);
|
|
167
162
|
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return () => {
|
|
166
|
+
scanProgress.removeListener(listenerId);
|
|
168
167
|
};
|
|
169
|
-
}, [
|
|
168
|
+
}, [metrics, scanProgress, travelDistance]);
|
|
170
169
|
|
|
171
|
-
if (!polygon || !
|
|
170
|
+
if (!polygon || !metrics || metrics.width <= 0 || metrics.height <= 0) {
|
|
172
171
|
return null;
|
|
173
172
|
}
|
|
174
173
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
if (SvgModule) {
|
|
175
|
+
const { default: Svg, Polygon, Line, Defs, LinearGradient, Stop, Rect } = SvgModule;
|
|
176
|
+
const gridLines = createGridLines(polygon);
|
|
177
|
+
const points = createPointsString(polygon);
|
|
178
|
+
const scanRectY = scanY ?? metrics.minY;
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
|
|
182
|
+
<Svg style={StyleSheet.absoluteFill}>
|
|
183
|
+
<Polygon points={points} fill={color} opacity={0.15} />
|
|
184
|
+
{gridLines.map((line, index) => (
|
|
185
|
+
<Line
|
|
186
|
+
key={`grid-${index}`}
|
|
187
|
+
x1={line.x1}
|
|
188
|
+
y1={line.y1}
|
|
189
|
+
x2={line.x2}
|
|
190
|
+
y2={line.y2}
|
|
191
|
+
stroke={color}
|
|
192
|
+
strokeWidth={lineWidth}
|
|
193
|
+
opacity={0.5}
|
|
194
|
+
/>
|
|
195
|
+
))}
|
|
196
|
+
<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
|
+
</Svg>
|
|
214
|
+
</View>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const relativeTranslate =
|
|
219
|
+
metrics && travelDistance > 0
|
|
220
|
+
? Animated.multiply(scanProgress, travelDistance)
|
|
221
|
+
: fallbackBase;
|
|
178
222
|
|
|
179
223
|
return (
|
|
180
|
-
<View pointerEvents="none" style={StyleSheet.
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
224
|
+
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
|
|
225
|
+
<View
|
|
226
|
+
style={[
|
|
227
|
+
styles.fallbackBox,
|
|
228
|
+
{
|
|
229
|
+
left: metrics.minX,
|
|
230
|
+
top: metrics.minY,
|
|
231
|
+
width: metrics.width,
|
|
232
|
+
height: metrics.height,
|
|
233
|
+
borderColor: color,
|
|
234
|
+
borderWidth: lineWidth,
|
|
235
|
+
},
|
|
236
|
+
]}
|
|
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
|
+
]}
|
|
193
248
|
/>
|
|
194
|
-
|
|
195
|
-
</
|
|
249
|
+
)}
|
|
250
|
+
</View>
|
|
196
251
|
</View>
|
|
197
252
|
);
|
|
198
253
|
};
|
|
254
|
+
|
|
255
|
+
const styles = StyleSheet.create({
|
|
256
|
+
fallbackBox: {
|
|
257
|
+
position: 'absolute',
|
|
258
|
+
backgroundColor: 'rgba(11, 126, 244, 0.1)',
|
|
259
|
+
overflow: 'hidden',
|
|
260
|
+
},
|
|
261
|
+
fallbackScanBar: {
|
|
262
|
+
width: '100%',
|
|
263
|
+
opacity: 0.4,
|
|
264
|
+
},
|
|
265
|
+
});
|