react-native-rectangle-doc-scanner 3.15.0 → 3.18.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.
@@ -5,6 +5,10 @@ type PictureEvent = {
5
5
  width?: number;
6
6
  height?: number;
7
7
  };
8
+ export type RectangleDetectEvent = {
9
+ stableCounter: number;
10
+ lastDetectionType: number;
11
+ };
8
12
  export interface DetectionConfig {
9
13
  processingWidth?: number;
10
14
  cannyLowThreshold?: number;
@@ -31,6 +35,7 @@ interface Props {
31
35
  gridColor?: string;
32
36
  gridLineWidth?: number;
33
37
  detectionConfig?: DetectionConfig;
38
+ onRectangleDetect?: (event: RectangleDetectEvent) => void;
34
39
  }
35
40
  type DocScannerHandle = {
36
41
  capture: () => Promise<PictureEvent>;
@@ -40,10 +40,17 @@ exports.DocScanner = void 0;
40
40
  const react_1 = __importStar(require("react"));
41
41
  const react_native_1 = require("react-native");
42
42
  const react_native_document_scanner_1 = __importDefault(require("react-native-document-scanner"));
43
+ const overlay_1 = require("./utils/overlay");
43
44
  const DEFAULT_OVERLAY_COLOR = '#0b7ef4';
44
- exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAULT_OVERLAY_COLOR, autoCapture = true, minStableFrames = 8, enableTorch = false, quality = 90, useBase64 = false, children, showGrid = true, }, ref) => {
45
+ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAULT_OVERLAY_COLOR, autoCapture = true, minStableFrames = 8, enableTorch = false, quality = 90, useBase64 = false, children, showGrid = true, gridColor, gridLineWidth, detectionConfig, onRectangleDetect, }, ref) => {
45
46
  const scannerRef = (0, react_1.useRef)(null);
46
47
  const captureResolvers = (0, react_1.useRef)(null);
48
+ const [isAutoCapturing, setIsAutoCapturing] = (0, react_1.useState)(false);
49
+ (0, react_1.useEffect)(() => {
50
+ if (!autoCapture) {
51
+ setIsAutoCapturing(false);
52
+ }
53
+ }, [autoCapture]);
47
54
  const normalizedQuality = (0, react_1.useMemo)(() => {
48
55
  if (react_native_1.Platform.OS === 'ios') {
49
56
  // iOS expects 0-1
@@ -52,6 +59,7 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
52
59
  return Math.min(100, Math.max(0, quality));
53
60
  }, [quality]);
54
61
  const handlePictureTaken = (0, react_1.useCallback)((event) => {
62
+ setIsAutoCapturing(false);
55
63
  const path = event.croppedImage ?? event.initialImage;
56
64
  if (path) {
57
65
  onCapture?.({
@@ -99,6 +107,17 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
99
107
  console.warn('[DocScanner] manual capture failed', error);
100
108
  });
101
109
  }, [autoCapture, capture]);
110
+ const handleRectangleDetect = (0, react_1.useCallback)((event) => {
111
+ if (autoCapture) {
112
+ if (event.stableCounter >= Math.max(minStableFrames - 1, 0)) {
113
+ setIsAutoCapturing(true);
114
+ }
115
+ else if (event.stableCounter === 0) {
116
+ setIsAutoCapturing(false);
117
+ }
118
+ }
119
+ onRectangleDetect?.(event);
120
+ }, [autoCapture, minStableFrames, onRectangleDetect]);
102
121
  (0, react_1.useImperativeHandle)(ref, () => ({
103
122
  capture,
104
123
  reset: () => {
@@ -109,7 +128,8 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
109
128
  },
110
129
  }), [capture]);
111
130
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
112
- react_1.default.createElement(react_native_document_scanner_1.default, { ref: scannerRef, style: styles.scanner, detectionCountBeforeCapture: minStableFrames, overlayColor: overlayColor, enableTorch: enableTorch, quality: normalizedQuality, useBase64: useBase64, manualOnly: !autoCapture, onPictureTaken: handlePictureTaken, onError: handleError }),
131
+ react_1.default.createElement(react_native_document_scanner_1.default, { ref: scannerRef, style: styles.scanner, detectionCountBeforeCapture: minStableFrames, overlayColor: overlayColor, enableTorch: enableTorch, quality: normalizedQuality, useBase64: useBase64, manualOnly: !autoCapture, detectionConfig: detectionConfig, onPictureTaken: handlePictureTaken, onError: handleError, onRectangleDetect: handleRectangleDetect }),
132
+ showGrid && (react_1.default.createElement(overlay_1.ScannerOverlay, { active: autoCapture && isAutoCapturing, color: gridColor ?? overlayColor, lineWidth: gridLineWidth })),
113
133
  !autoCapture && react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture }),
114
134
  children));
115
135
  });
@@ -1,15 +1,8 @@
1
1
  import React from 'react';
2
- import type { Point } from '../types';
3
- type OverlayProps = {
4
- quad: Point[] | null;
2
+ export interface ScannerOverlayProps {
3
+ /** 활성화 스캔 바가 움직이며 자동 촬영 중임을 표시합니다. */
4
+ active: boolean;
5
5
  color?: string;
6
- frameSize: {
7
- width: number;
8
- height: number;
9
- } | null;
10
- showGrid?: boolean;
11
- gridColor?: string;
12
- gridLineWidth?: number;
13
- };
14
- export declare const Overlay: React.FC<OverlayProps>;
15
- export {};
6
+ lineWidth?: number;
7
+ }
8
+ export declare const ScannerOverlay: React.FC<ScannerOverlayProps>;
@@ -33,150 +33,106 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Overlay = void 0;
36
+ exports.ScannerOverlay = void 0;
37
37
  const react_1 = __importStar(require("react"));
38
38
  const react_native_1 = require("react-native");
39
- const react_native_skia_1 = require("@shopify/react-native-skia");
40
- const lerp = (start, end, t) => ({
41
- x: start.x + (end.x - start.x) * t,
42
- y: start.y + (end.y - start.y) * t,
43
- });
44
- const withAlpha = (value, alpha) => {
45
- const hexMatch = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value.trim());
46
- if (!hexMatch) {
47
- return `rgba(231, 166, 73, ${alpha})`;
48
- }
49
- const hex = hexMatch[1];
50
- const normalize = hex.length === 3
51
- ? hex.split('').map((ch) => ch + ch).join('')
52
- : hex;
53
- const r = parseInt(normalize.slice(0, 2), 16);
54
- const g = parseInt(normalize.slice(2, 4), 16);
55
- const b = parseInt(normalize.slice(4, 6), 16);
56
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
57
- };
58
- const buildPath = (points) => {
59
- const path = react_native_skia_1.Skia.Path.Make();
60
- path.moveTo(points[0].x, points[0].y);
61
- points.slice(1).forEach((p) => path.lineTo(p.x, p.y));
62
- path.close();
63
- return path;
64
- };
65
- const orderQuad = (points) => {
66
- if (points.length !== 4) {
67
- return points;
68
- }
69
- const center = points.reduce((acc, point) => ({ x: acc.x + point.x / 4, y: acc.y + point.y / 4 }), { x: 0, y: 0 });
70
- const sorted = [...points].sort((a, b) => {
71
- const angleA = Math.atan2(a.y - center.y, a.x - center.x);
72
- const angleB = Math.atan2(b.y - center.y, b.x - center.x);
73
- return angleA - angleB;
74
- });
75
- // Ensure the first point is the top-left (smallest y, then smallest x)
76
- let startIndex = 0;
77
- for (let i = 1; i < sorted.length; i += 1) {
78
- const current = sorted[i];
79
- const candidate = sorted[startIndex];
80
- if (current.y < candidate.y || (current.y === candidate.y && current.x < candidate.x)) {
81
- startIndex = i;
82
- }
83
- }
84
- return [
85
- sorted[startIndex % 4],
86
- sorted[(startIndex + 1) % 4],
87
- sorted[(startIndex + 2) % 4],
88
- sorted[(startIndex + 3) % 4],
89
- ];
90
- };
91
- const Overlay = ({ quad, color = '#e7a649', frameSize, showGrid = true, gridColor = 'rgba(231, 166, 73, 0.35)', gridLineWidth = 2, }) => {
92
- const { width: screenWidth, height: screenHeight } = (0, react_native_1.useWindowDimensions)();
93
- const fillColor = (0, react_1.useMemo)(() => withAlpha(color, 0.2), [color]);
94
- const { outlinePath, gridPaths } = (0, react_1.useMemo)(() => {
95
- let transformedQuad = null;
96
- let sourceQuad = null;
97
- let sourceFrameSize = frameSize;
98
- if (quad && frameSize) {
99
- sourceQuad = quad;
100
- }
101
- else {
102
- // No detection yet – skip drawing
103
- return { outlinePath: null, gridPaths: [] };
104
- }
105
- if (sourceQuad && sourceFrameSize) {
106
- if (__DEV__) {
107
- console.log('[Overlay] drawing quad:', sourceQuad);
108
- console.log('[Overlay] color:', color);
109
- console.log('[Overlay] screen dimensions:', screenWidth, 'x', screenHeight);
110
- console.log('[Overlay] frame dimensions:', sourceFrameSize.width, 'x', sourceFrameSize.height);
111
- }
112
- const isFrameLandscape = sourceFrameSize.width > sourceFrameSize.height;
113
- const isScreenPortrait = screenHeight > screenWidth;
114
- const needsRotation = isFrameLandscape && isScreenPortrait;
115
- if (needsRotation) {
116
- const scaleX = screenWidth / sourceFrameSize.height;
117
- const scaleY = screenHeight / sourceFrameSize.width;
118
- transformedQuad = sourceQuad.map((p) => ({
119
- x: p.y * scaleX,
120
- y: (sourceFrameSize.width - p.x) * scaleY,
121
- }));
122
- }
123
- else {
124
- const scaleX = screenWidth / sourceFrameSize.width;
125
- const scaleY = screenHeight / sourceFrameSize.height;
126
- transformedQuad = sourceQuad.map((p) => ({
127
- x: p.x * scaleX,
128
- y: p.y * scaleY,
129
- }));
130
- }
131
- }
132
- if (!transformedQuad) {
133
- return { outlinePath: null, gridPaths: [] };
134
- }
135
- const normalizedQuad = orderQuad(transformedQuad);
136
- const skPath = buildPath(normalizedQuad);
137
- const grid = [];
138
- if (showGrid) {
139
- const [topLeft, topRight, bottomRight, bottomLeft] = normalizedQuad;
140
- const steps = [1 / 3, 2 / 3];
141
- steps.forEach((t) => {
142
- const start = lerp(topLeft, topRight, t);
143
- const end = lerp(bottomLeft, bottomRight, t);
144
- const verticalPath = react_native_skia_1.Skia.Path.Make();
145
- verticalPath.moveTo(start.x, start.y);
146
- verticalPath.lineTo(end.x, end.y);
147
- grid.push(verticalPath);
148
- });
149
- steps.forEach((t) => {
150
- const start = lerp(topLeft, bottomLeft, t);
151
- const end = lerp(topRight, bottomRight, t);
152
- const horizontalPath = react_native_skia_1.Skia.Path.Make();
153
- horizontalPath.moveTo(start.x, start.y);
154
- horizontalPath.lineTo(end.x, end.y);
155
- grid.push(horizontalPath);
156
- });
39
+ const BAR_THICKNESS = 4;
40
+ const ScannerOverlay = ({ active, color = 'rgba(255,255,255,0.8)', lineWidth = react_native_1.StyleSheet.hairlineWidth, }) => {
41
+ const animatedValue = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
42
+ const loopRef = (0, react_1.useRef)(null);
43
+ const [frameHeight, setFrameHeight] = (0, react_1.useState)(0);
44
+ const borderStyle = (0, react_1.useMemo)(() => ({
45
+ borderColor: color,
46
+ borderWidth: lineWidth,
47
+ }), [color, lineWidth]);
48
+ (0, react_1.useEffect)(() => {
49
+ loopRef.current?.stop();
50
+ if (!active || frameHeight <= 0) {
51
+ animatedValue.stopAnimation();
52
+ animatedValue.setValue(0);
53
+ return;
157
54
  }
158
- return { outlinePath: skPath, gridPaths: grid };
159
- }, [quad, screenWidth, screenHeight, frameSize, showGrid, color]);
160
- if (__DEV__) {
161
- console.log('[Overlay] rendering Canvas with dimensions:', screenWidth, 'x', screenHeight);
162
- }
163
- return (react_1.default.createElement(react_native_1.View, { style: styles.container, pointerEvents: "none" },
164
- react_1.default.createElement(react_native_skia_1.Canvas, { style: { width: screenWidth, height: screenHeight } }, outlinePath && (react_1.default.createElement(react_1.default.Fragment, null,
165
- react_1.default.createElement(react_native_skia_1.Path, { path: outlinePath, color: color, style: "stroke", strokeWidth: 8 }),
166
- react_1.default.createElement(react_native_skia_1.Path, { path: outlinePath, color: fillColor, style: "fill" }),
167
- gridPaths.map((gridPath, index) => (react_1.default.createElement(react_native_skia_1.Path
168
- // eslint-disable-next-line react/no-array-index-key
169
- , {
170
- // eslint-disable-next-line react/no-array-index-key
171
- key: `grid-${index}`, path: gridPath, color: gridColor, style: "stroke", strokeWidth: gridLineWidth }))))))));
55
+ const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
56
+ react_native_1.Animated.timing(animatedValue, {
57
+ toValue: 1,
58
+ duration: 1600,
59
+ easing: react_native_1.Easing.inOut(react_native_1.Easing.quad),
60
+ useNativeDriver: true,
61
+ }),
62
+ react_native_1.Animated.timing(animatedValue, {
63
+ toValue: 0,
64
+ duration: 1600,
65
+ easing: react_native_1.Easing.inOut(react_native_1.Easing.quad),
66
+ useNativeDriver: true,
67
+ }),
68
+ ]));
69
+ loopRef.current = loop;
70
+ loop.start();
71
+ return () => {
72
+ loop.stop();
73
+ };
74
+ }, [active, animatedValue, frameHeight]);
75
+ const handleLayout = (event) => {
76
+ const { height } = event.nativeEvent.layout;
77
+ setFrameHeight(height);
78
+ };
79
+ const translateY = frameHeight <= BAR_THICKNESS
80
+ ? 0
81
+ : animatedValue.interpolate({
82
+ inputRange: [0, 1],
83
+ outputRange: [0, frameHeight - BAR_THICKNESS],
84
+ });
85
+ return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: react_native_1.StyleSheet.absoluteFill },
86
+ react_1.default.createElement(react_native_1.View, { onLayout: handleLayout, style: [styles.frame, borderStyle] },
87
+ react_1.default.createElement(react_native_1.View, { style: [
88
+ styles.horizontalLine,
89
+ { top: '33%', borderBottomColor: color, borderBottomWidth: lineWidth },
90
+ ] }),
91
+ react_1.default.createElement(react_native_1.View, { style: [
92
+ styles.horizontalLine,
93
+ { top: '66%', borderBottomColor: color, borderBottomWidth: lineWidth },
94
+ ] }),
95
+ react_1.default.createElement(react_native_1.View, { style: [
96
+ styles.verticalLine,
97
+ { left: '33%', borderRightColor: color, borderRightWidth: lineWidth },
98
+ ] }),
99
+ react_1.default.createElement(react_native_1.View, { style: [
100
+ styles.verticalLine,
101
+ { left: '66%', borderRightColor: color, borderRightWidth: lineWidth },
102
+ ] }),
103
+ react_1.default.createElement(react_native_1.Animated.View, { style: [
104
+ styles.scanBar,
105
+ {
106
+ opacity: active ? 1 : 0,
107
+ backgroundColor: color,
108
+ transform: [{ translateY }],
109
+ },
110
+ ] }))));
172
111
  };
173
- exports.Overlay = Overlay;
112
+ exports.ScannerOverlay = ScannerOverlay;
174
113
  const styles = react_native_1.StyleSheet.create({
175
- container: {
114
+ frame: {
115
+ flex: 1,
116
+ borderRadius: 12,
117
+ overflow: 'hidden',
118
+ backgroundColor: 'transparent',
119
+ },
120
+ horizontalLine: {
121
+ position: 'absolute',
122
+ right: 0,
123
+ left: 0,
124
+ height: 0,
125
+ },
126
+ verticalLine: {
176
127
  position: 'absolute',
177
128
  top: 0,
129
+ bottom: 0,
130
+ width: 0,
131
+ },
132
+ scanBar: {
133
+ position: 'absolute',
178
134
  left: 0,
179
135
  right: 0,
180
- bottom: 0,
136
+ height: BAR_THICKNESS,
181
137
  },
182
138
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.15.0",
3
+ "version": "3.18.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -2,12 +2,15 @@ import React, {
2
2
  ReactNode,
3
3
  forwardRef,
4
4
  useCallback,
5
+ useEffect,
5
6
  useImperativeHandle,
6
7
  useMemo,
7
8
  useRef,
9
+ useState,
8
10
  } from 'react';
9
11
  import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
10
12
  import DocumentScanner from 'react-native-document-scanner';
13
+ import { ScannerOverlay } from './utils/overlay';
11
14
 
12
15
  type PictureEvent = {
13
16
  croppedImage?: string | null;
@@ -16,6 +19,11 @@ type PictureEvent = {
16
19
  height?: number;
17
20
  };
18
21
 
22
+ export type RectangleDetectEvent = {
23
+ stableCounter: number;
24
+ lastDetectionType: number;
25
+ };
26
+
19
27
  export interface DetectionConfig {
20
28
  processingWidth?: number;
21
29
  cannyLowThreshold?: number;
@@ -38,6 +46,7 @@ interface Props {
38
46
  gridColor?: string;
39
47
  gridLineWidth?: number;
40
48
  detectionConfig?: DetectionConfig;
49
+ onRectangleDetect?: (event: RectangleDetectEvent) => void;
41
50
  }
42
51
 
43
52
  type DocScannerHandle = {
@@ -59,6 +68,10 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
59
68
  useBase64 = false,
60
69
  children,
61
70
  showGrid = true,
71
+ gridColor,
72
+ gridLineWidth,
73
+ detectionConfig,
74
+ onRectangleDetect,
62
75
  },
63
76
  ref,
64
77
  ) => {
@@ -67,6 +80,13 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
67
80
  resolve: (value: PictureEvent) => void;
68
81
  reject: (reason?: unknown) => void;
69
82
  } | null>(null);
83
+ const [isAutoCapturing, setIsAutoCapturing] = useState(false);
84
+
85
+ useEffect(() => {
86
+ if (!autoCapture) {
87
+ setIsAutoCapturing(false);
88
+ }
89
+ }, [autoCapture]);
70
90
 
71
91
  const normalizedQuality = useMemo(() => {
72
92
  if (Platform.OS === 'ios') {
@@ -78,6 +98,8 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
78
98
 
79
99
  const handlePictureTaken = useCallback(
80
100
  (event: PictureEvent) => {
101
+ setIsAutoCapturing(false);
102
+
81
103
  const path = event.croppedImage ?? event.initialImage;
82
104
  if (path) {
83
105
  onCapture?.({
@@ -134,6 +156,21 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
134
156
  });
135
157
  }, [autoCapture, capture]);
136
158
 
159
+ const handleRectangleDetect = useCallback(
160
+ (event: RectangleDetectEvent) => {
161
+ if (autoCapture) {
162
+ if (event.stableCounter >= Math.max(minStableFrames - 1, 0)) {
163
+ setIsAutoCapturing(true);
164
+ } else if (event.stableCounter === 0) {
165
+ setIsAutoCapturing(false);
166
+ }
167
+ }
168
+
169
+ onRectangleDetect?.(event);
170
+ },
171
+ [autoCapture, minStableFrames, onRectangleDetect],
172
+ );
173
+
137
174
  useImperativeHandle(
138
175
  ref,
139
176
  () => ({
@@ -159,9 +196,18 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
159
196
  quality={normalizedQuality}
160
197
  useBase64={useBase64}
161
198
  manualOnly={!autoCapture}
199
+ detectionConfig={detectionConfig}
162
200
  onPictureTaken={handlePictureTaken}
163
201
  onError={handleError}
202
+ onRectangleDetect={handleRectangleDetect}
164
203
  />
204
+ {showGrid && (
205
+ <ScannerOverlay
206
+ active={autoCapture && isAutoCapturing}
207
+ color={gridColor ?? overlayColor}
208
+ lineWidth={gridLineWidth}
209
+ />
210
+ )}
165
211
  {!autoCapture && <TouchableOpacity style={styles.button} onPress={handleManualCapture} />}
166
212
  {children}
167
213
  </View>
package/src/external.d.ts CHANGED
@@ -77,8 +77,17 @@ declare module 'react-native-document-scanner' {
77
77
  useBase64?: boolean;
78
78
  quality?: number;
79
79
  manualOnly?: boolean;
80
+ detectionConfig?: {
81
+ processingWidth?: number;
82
+ cannyLowThreshold?: number;
83
+ cannyHighThreshold?: number;
84
+ snapDistance?: number;
85
+ maxAnchorMisses?: number;
86
+ maxCenterDelta?: number;
87
+ };
80
88
  onPictureTaken?: (event: DocumentScannerResult) => void;
81
89
  onError?: (error: Error) => void;
90
+ onRectangleDetect?: (event: { stableCounter: number; lastDetectionType: number }) => void;
82
91
  }
83
92
 
84
93
  export default class DocumentScanner extends Component<DocumentScannerProps> {
@@ -0,0 +1,143 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { Animated, Easing, LayoutChangeEvent, StyleSheet, View } from 'react-native';
3
+
4
+ export interface ScannerOverlayProps {
5
+ /** 활성화 시 스캔 바가 움직이며 자동 촬영 중임을 표시합니다. */
6
+ active: boolean;
7
+ color?: string;
8
+ lineWidth?: number;
9
+ }
10
+
11
+ const BAR_THICKNESS = 4;
12
+
13
+ export const ScannerOverlay: React.FC<ScannerOverlayProps> = ({
14
+ active,
15
+ color = 'rgba(255,255,255,0.8)',
16
+ lineWidth = StyleSheet.hairlineWidth,
17
+ }) => {
18
+ const animatedValue = useRef(new Animated.Value(0)).current;
19
+ const loopRef = useRef<Animated.CompositeAnimation | null>(null);
20
+ const [frameHeight, setFrameHeight] = useState(0);
21
+
22
+ const borderStyle = useMemo(
23
+ () => ({
24
+ borderColor: color,
25
+ borderWidth: lineWidth,
26
+ }),
27
+ [color, lineWidth],
28
+ );
29
+
30
+ useEffect(() => {
31
+ loopRef.current?.stop();
32
+ if (!active || frameHeight <= 0) {
33
+ animatedValue.stopAnimation();
34
+ animatedValue.setValue(0);
35
+ return;
36
+ }
37
+
38
+ const loop = Animated.loop(
39
+ Animated.sequence([
40
+ Animated.timing(animatedValue, {
41
+ toValue: 1,
42
+ duration: 1600,
43
+ easing: Easing.inOut(Easing.quad),
44
+ useNativeDriver: true,
45
+ }),
46
+ Animated.timing(animatedValue, {
47
+ toValue: 0,
48
+ duration: 1600,
49
+ easing: Easing.inOut(Easing.quad),
50
+ useNativeDriver: true,
51
+ }),
52
+ ]),
53
+ );
54
+
55
+ loopRef.current = loop;
56
+ loop.start();
57
+
58
+ return () => {
59
+ loop.stop();
60
+ };
61
+ }, [active, animatedValue, frameHeight]);
62
+
63
+ const handleLayout = (event: LayoutChangeEvent) => {
64
+ const { height } = event.nativeEvent.layout;
65
+ setFrameHeight(height);
66
+ };
67
+
68
+ const translateY =
69
+ frameHeight <= BAR_THICKNESS
70
+ ? 0
71
+ : animatedValue.interpolate({
72
+ inputRange: [0, 1],
73
+ outputRange: [0, frameHeight - BAR_THICKNESS],
74
+ });
75
+
76
+ return (
77
+ <View pointerEvents="none" style={StyleSheet.absoluteFill}>
78
+ <View onLayout={handleLayout} style={[styles.frame, borderStyle]}>
79
+ <View
80
+ style={[
81
+ styles.horizontalLine,
82
+ { top: '33%', borderBottomColor: color, borderBottomWidth: lineWidth },
83
+ ]}
84
+ />
85
+ <View
86
+ style={[
87
+ styles.horizontalLine,
88
+ { top: '66%', borderBottomColor: color, borderBottomWidth: lineWidth },
89
+ ]}
90
+ />
91
+ <View
92
+ style={[
93
+ styles.verticalLine,
94
+ { left: '33%', borderRightColor: color, borderRightWidth: lineWidth },
95
+ ]}
96
+ />
97
+ <View
98
+ style={[
99
+ styles.verticalLine,
100
+ { left: '66%', borderRightColor: color, borderRightWidth: lineWidth },
101
+ ]}
102
+ />
103
+ <Animated.View
104
+ style={[
105
+ styles.scanBar,
106
+ {
107
+ opacity: active ? 1 : 0,
108
+ backgroundColor: color,
109
+ transform: [{ translateY }],
110
+ },
111
+ ]}
112
+ />
113
+ </View>
114
+ </View>
115
+ );
116
+ };
117
+
118
+ const styles = StyleSheet.create({
119
+ frame: {
120
+ flex: 1,
121
+ borderRadius: 12,
122
+ overflow: 'hidden',
123
+ backgroundColor: 'transparent',
124
+ },
125
+ horizontalLine: {
126
+ position: 'absolute',
127
+ right: 0,
128
+ left: 0,
129
+ height: 0,
130
+ },
131
+ verticalLine: {
132
+ position: 'absolute',
133
+ top: 0,
134
+ bottom: 0,
135
+ width: 0,
136
+ },
137
+ scanBar: {
138
+ position: 'absolute',
139
+ left: 0,
140
+ right: 0,
141
+ height: BAR_THICKNESS,
142
+ },
143
+ });
@@ -220,15 +220,15 @@
220
220
  }
221
221
  }
222
222
 
223
- if (self.context && _coreImageContext)
223
+ if (self.context && _coreImageContext && _glkView)
224
224
  {
225
225
  // Calculate the rect to draw the image with aspect fill
226
- CGRect drawRect = self.bounds;
226
+ CGRect viewBounds = _glkView.bounds;
227
227
  CGRect imageExtent = image.extent;
228
228
 
229
229
  // Calculate aspect ratios
230
230
  CGFloat imageAspect = imageExtent.size.width / imageExtent.size.height;
231
- CGFloat viewAspect = drawRect.size.width / drawRect.size.height;
231
+ CGFloat viewAspect = viewBounds.size.width / viewBounds.size.height;
232
232
 
233
233
  CGRect fromRect = imageExtent;
234
234
 
@@ -245,6 +245,12 @@
245
245
  fromRect = CGRectMake(0, yOffset, imageExtent.size.width, newHeight);
246
246
  }
247
247
 
248
+ // GLKView renderbuffer expects pixel dimensions, not point-based bounds
249
+ CGRect drawRect = CGRectMake(0,
250
+ 0,
251
+ (CGFloat)_glkView.drawableWidth,
252
+ (CGFloat)_glkView.drawableHeight);
253
+
248
254
  [_coreImageContext drawImage:image inRect:drawRect fromRect:fromRect];
249
255
  [self.context presentRenderbuffer:GL_RENDERBUFFER];
250
256