react-native-image-stitcher 0.15.1 → 0.16.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/CHANGELOG.md +147 -1
- package/README.md +116 -5
- package/android/src/main/cpp/image_stitcher_jni.cpp +107 -11
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +87 -30
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
- package/cpp/crop_quad.cpp +162 -0
- package/cpp/crop_quad.hpp +163 -0
- package/cpp/stitcher.cpp +651 -55
- package/cpp/stitcher.hpp +10 -0
- package/cpp/warp_guard.hpp +212 -0
- package/dist/camera/Camera.d.ts +196 -12
- package/dist/camera/Camera.js +629 -35
- package/dist/camera/CameraView.js +62 -5
- package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
- package/dist/camera/CaptureCountdownOverlay.js +239 -0
- package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
- package/dist/camera/CaptureFrameCounterOverlay.js +142 -0
- package/dist/camera/CaptureMemoryPill.d.ts +9 -1
- package/dist/camera/CaptureMemoryPill.js +3 -3
- package/dist/camera/CapturePreview.js +2 -1
- package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
- package/dist/camera/CaptureStatusOverlay.js +22 -5
- package/dist/camera/CaptureThumbnailStrip.js +2 -1
- package/dist/camera/LateralMotionModal.d.ts +85 -0
- package/dist/camera/LateralMotionModal.js +134 -0
- package/dist/camera/PanHowToOverlay.d.ts +76 -0
- package/dist/camera/PanHowToOverlay.js +222 -0
- package/dist/camera/PanoramaSettings.d.ts +8 -6
- package/dist/camera/PanoramaSettings.js +26 -5
- package/dist/camera/PanoramaSettingsModal.js +4 -4
- package/dist/camera/RectCropPreview.d.ts +161 -0
- package/dist/camera/RectCropPreview.js +480 -0
- package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
- package/dist/camera/RotateToLandscapePrompt.js +138 -0
- package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
- package/dist/camera/buildPanoramaInitialSettings.js +9 -0
- package/dist/camera/cameraErrorMessages.d.ts +30 -1
- package/dist/camera/cameraErrorMessages.js +26 -10
- package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
- package/dist/camera/cameraGuidanceCopy.js +80 -0
- package/dist/camera/captureCountdown.d.ts +52 -0
- package/dist/camera/captureCountdown.js +76 -0
- package/dist/camera/captureWarnings.d.ts +90 -0
- package/dist/camera/captureWarnings.js +108 -0
- package/dist/camera/classifyStitchError.d.ts +30 -0
- package/dist/camera/classifyStitchError.js +42 -0
- package/dist/camera/cropGeometry.d.ts +136 -0
- package/dist/camera/cropGeometry.js +223 -0
- package/dist/camera/displayDecodeImageProps.d.ts +25 -0
- package/dist/camera/displayDecodeImageProps.js +29 -0
- package/dist/camera/guidanceGraphics.d.ts +58 -0
- package/dist/camera/guidanceGraphics.js +280 -0
- package/dist/camera/guidanceTokens.d.ts +54 -0
- package/dist/camera/guidanceTokens.js +58 -0
- package/dist/camera/panModeGate.d.ts +54 -0
- package/dist/camera/panModeGate.js +62 -0
- package/dist/camera/pickCaptureFormat.d.ts +71 -0
- package/dist/camera/pickCaptureFormat.js +85 -0
- package/dist/camera/stitchDebugInfo.d.ts +27 -0
- package/dist/camera/stitchDebugInfo.js +55 -0
- package/dist/camera/usePanMotion.d.ts +250 -0
- package/dist/camera/usePanMotion.js +451 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +33 -2
- package/dist/stitching/computeInscribedRect.d.ts +40 -0
- package/dist/stitching/computeInscribedRect.js +55 -0
- package/dist/stitching/cropQuad.d.ts +78 -0
- package/dist/stitching/cropQuad.js +116 -0
- package/dist/stitching/incremental.d.ts +45 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +56 -8
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +191 -7
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
- package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
- package/package.json +5 -1
- package/src/camera/Camera.tsx +994 -47
- package/src/camera/CameraView.tsx +75 -5
- package/src/camera/CaptureCountdownOverlay.tsx +272 -0
- package/src/camera/CaptureFrameCounterOverlay.tsx +183 -0
- package/src/camera/CaptureMemoryPill.tsx +17 -3
- package/src/camera/CapturePreview.tsx +5 -0
- package/src/camera/CaptureStatusOverlay.tsx +35 -7
- package/src/camera/CaptureThumbnailStrip.tsx +4 -0
- package/src/camera/LateralMotionModal.tsx +199 -0
- package/src/camera/PanHowToOverlay.tsx +246 -0
- package/src/camera/PanoramaSettings.ts +34 -11
- package/src/camera/PanoramaSettingsModal.tsx +4 -4
- package/src/camera/RectCropPreview.tsx +820 -0
- package/src/camera/RotateToLandscapePrompt.tsx +188 -0
- package/src/camera/buildPanoramaInitialSettings.ts +30 -1
- package/src/camera/cameraErrorMessages.ts +39 -2
- package/src/camera/cameraGuidanceCopy.ts +145 -0
- package/src/camera/captureCountdown.ts +83 -0
- package/src/camera/captureWarnings.ts +190 -0
- package/src/camera/classifyStitchError.ts +68 -0
- package/src/camera/cropGeometry.ts +268 -0
- package/src/camera/displayDecodeImageProps.ts +25 -0
- package/src/camera/guidanceGraphics.tsx +347 -0
- package/src/camera/guidanceTokens.ts +57 -0
- package/src/camera/panModeGate.ts +81 -0
- package/src/camera/pickCaptureFormat.ts +130 -0
- package/src/camera/stitchDebugInfo.ts +71 -0
- package/src/camera/usePanMotion.ts +667 -0
- package/src/index.ts +66 -3
- package/src/stitching/computeInscribedRect.ts +81 -0
- package/src/stitching/cropQuad.ts +167 -0
- package/src/stitching/incremental.ts +45 -0
- package/cpp/tests/CMakeLists.txt +0 -104
- package/cpp/tests/README.md +0 -86
- package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
- package/cpp/tests/pose_test.cpp +0 -74
- package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
- package/cpp/tests/stubs/jsi/jsi.h +0 -33
- package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
- package/cpp/tests/warp_guard_test.cpp +0 -48
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
- package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
- package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
- package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
- package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
- package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
- package/src/camera/__tests__/useContentRotation.test.ts +0 -89
- package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
- package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
- package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
|
@@ -57,6 +57,15 @@ exports.CameraView = void 0;
|
|
|
57
57
|
const react_1 = __importStar(require("react"));
|
|
58
58
|
const react_native_1 = require("react-native");
|
|
59
59
|
const react_native_vision_camera_1 = require("react-native-vision-camera");
|
|
60
|
+
const pickCaptureFormat_1 = require("./pickCaptureFormat");
|
|
61
|
+
/**
|
|
62
|
+
* Cap on the chosen capture format's PHOTO long edge (px). 4032 ≈ 12 MP at
|
|
63
|
+
* 4:3 ("4K"-ish), matching the 1× lens, so the ultra-wide stops producing a
|
|
64
|
+
* 48 MP / ~6000 px still. Set to 2016 for "2K" (~3 MP). `0` reverts to pure
|
|
65
|
+
* max-video. TODO(v0.16): expose as a `<Camera photoMaxLongEdge>` prop once
|
|
66
|
+
* the 0.5× panorama 8-bit check passes on-device.
|
|
67
|
+
*/
|
|
68
|
+
const PHOTO_LONG_EDGE_CAP = 4032;
|
|
60
69
|
/**
|
|
61
70
|
* A forwardRef'd wrapper that exposes the underlying Camera ref
|
|
62
71
|
* to callers (so ``cameraRef.current.takePhoto()`` keeps working),
|
|
@@ -102,10 +111,58 @@ exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash
|
|
|
102
111
|
// aspect on essentially every phone camera (incl. ultra-wide), so a
|
|
103
112
|
// matching format is virtually always available; `useCameraFormat`
|
|
104
113
|
// returns the closest match and never throws.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
//
|
|
115
|
+
// Resolution preference matters too: filtering on aspect ALONE lets
|
|
116
|
+
// vision-camera settle on whatever 4:3 format sorts first — observed as
|
|
117
|
+
// a 192×144 VIDEO stream on the iPhone 16 Pro (the photo still uses the
|
|
118
|
+
// format's full-res photo dims, so you'd get a sharp capture behind a
|
|
119
|
+
// mush preview). So we also request the highest video resolution.
|
|
120
|
+
//
|
|
121
|
+
// Why `'max'` and not a bounded target like 1920×1440? We tried the
|
|
122
|
+
// bounded target and it FAILED on the iPhone 16 Pro: the nearest
|
|
123
|
+
// 1920×1440 format is a 10-bit format (pixel formats x420 / x422 only —
|
|
124
|
+
// and it is NOT flagged HDR, so the `videoHdr` filter can't dodge it).
|
|
125
|
+
// The frame processor + the stitcher's CV pipeline need 8-bit
|
|
126
|
+
// `420v`/`420f`, so vision-camera raises
|
|
127
|
+
// `device/pixel-format-not-supported` and silently falls back to a
|
|
128
|
+
// default pixel format — breaking non-AR stitching. vision-camera does
|
|
129
|
+
// NOT expose a format's supported pixel formats to JS (no
|
|
130
|
+
// `pixelFormats` field; `FormatFilter` has no pixel-format key), so we
|
|
131
|
+
// can't select an 8-bit format by inspection. Empirically the device's
|
|
132
|
+
// MAX 4:3 video format is 8-bit (420v/420f) on the iPhone 16 Pro, and
|
|
133
|
+
// Android formats are near-universally 8-bit YUV_420_888, so `'max'` is
|
|
134
|
+
// the robust choice: a sharp preview on a frame-processor-compatible
|
|
135
|
+
// pipeline. Trade-off: the max format tends to run at 30 fps (fine for
|
|
136
|
+
// hold-to-pan) and feeds full-res frames to the non-AR gate — if that
|
|
137
|
+
// ever shows up as dropped frames we can downscale for the gate
|
|
138
|
+
// natively while keeping full-res keyframes. Aspect stays the
|
|
139
|
+
// top-priority filter, so 4:3 WYSIWYG parity holds on every device.
|
|
140
|
+
//
|
|
141
|
+
// Still resolution: a plain `videoResolution:'max'` filter (what we used
|
|
142
|
+
// before) maximises VIDEO and lets the PHOTO ride along — on the iPhone 16
|
|
143
|
+
// Pro ULTRA-WIDE that pairs a 48 MP still (8064×6048) with the max-video
|
|
144
|
+
// format, so a tap photo came out ~6000 px. `pickCaptureFormat` instead
|
|
145
|
+
// picks the SHARPEST-video 4:3 format whose photo is within
|
|
146
|
+
// PHOTO_LONG_EDGE_CAP (verified on-device: the ultra-wide then chooses
|
|
147
|
+
// 3264×2448 video + 12 MP photo — still a crisp preview, no 48 MP still).
|
|
148
|
+
// The cap is on the PHOTO; video stays as high as the cap allows, so the
|
|
149
|
+
// 8-bit/sharp-preview rationale above still holds.
|
|
150
|
+
//
|
|
151
|
+
// preferHighFps: a panorama preview must stay SMOOTH while panning. Video-
|
|
152
|
+
// resolution-first would pick the 3264×2448 **@30 fps** format over the
|
|
153
|
+
// 1920×1440 **@60 fps** one — visibly jittery. Keyframes are clamped to
|
|
154
|
+
// 640/1280 px before stitching, so the extra video resolution buys nothing
|
|
155
|
+
// here; a 60 fps stream just looks right. We opt the panorama camera in.
|
|
156
|
+
const format = (0, react_1.useMemo)(() => (0, pickCaptureFormat_1.pickCaptureFormat)(device?.formats ?? [], {
|
|
157
|
+
maxPhotoLongEdge: PHOTO_LONG_EDGE_CAP,
|
|
158
|
+
aspect: 4 / 3,
|
|
159
|
+
preferHighFps: true,
|
|
160
|
+
}), [device]);
|
|
161
|
+
// Pin the session frame rate to the format's max, capped at 60. Picking a
|
|
162
|
+
// 60 fps-capable format is necessary but NOT sufficient — without an explicit
|
|
163
|
+
// `fps`, vision-camera can leave the session at a lower default, which is the
|
|
164
|
+
// jitter the user saw. min(maxFps, 60) is always within the format's range.
|
|
165
|
+
const fps = (0, react_1.useMemo)(() => (format ? Math.min(format.maxFps ?? 30, 60) : undefined), [format]);
|
|
109
166
|
// Measured size of our container, so we can size the <Camera> view to
|
|
110
167
|
// the largest box of the capture's aspect ratio that fits inside it
|
|
111
168
|
// (the rest becomes the black letterbox). We deliberately size the
|
|
@@ -158,7 +215,7 @@ exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash
|
|
|
158
215
|
// surrounding bars black. See the cameraStyle computation above.
|
|
159
216
|
style: cameraStyle, device: device, isActive: isActive, photo: true, video: video,
|
|
160
217
|
// Pin preview + photo to the same 4:3 format (WYSIWYG capture).
|
|
161
|
-
format: format, ...(zoom != null ? { zoom } : {}),
|
|
218
|
+
format: format, ...(fps != null ? { fps } : {}), ...(zoom != null ? { zoom } : {}),
|
|
162
219
|
// Bake the device orientation into the captured pixels.
|
|
163
220
|
// Without this, vision-camera writes the file in the camera
|
|
164
221
|
// sensor's native landscape and relies on EXIF metadata to
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CaptureCountdownOverlay — the blinking auto-stop countdown shown at the
|
|
3
|
+
* user-perceived TOP-LEFT during an in-progress panorama capture (item 5
|
|
4
|
+
* of the first-time-user GUIDANCE flow).
|
|
5
|
+
*
|
|
6
|
+
* ┌──────────────────────────────────────────────────┐
|
|
7
|
+
* │ ● 9 │ ← top-left, blinks
|
|
8
|
+
* │ │
|
|
9
|
+
* │ (camera preview / pan in progress) │
|
|
10
|
+
* │ │
|
|
11
|
+
* └──────────────────────────────────────────────────┘
|
|
12
|
+
*
|
|
13
|
+
* Why this exists
|
|
14
|
+
* The capture auto-finalizes after a fixed window (the parent owns the
|
|
15
|
+
* real timer — see Camera's `countdownSecondsFrom`). Without a visible
|
|
16
|
+
* counter the auto-stop feels abrupt ("why did it stop?"). A calm
|
|
17
|
+
* blinking "● N" tells the user how many seconds of pan they have left.
|
|
18
|
+
*
|
|
19
|
+
* What it does
|
|
20
|
+
* - Renders an amber glow dot + a white tabular-nums integer
|
|
21
|
+
* (`secondsRemaining`, computed by the parent) using the shared
|
|
22
|
+
* {@link GUIDANCE_COUNTDOWN} design tokens.
|
|
23
|
+
* - Pins itself to the user-perceived top-left corner across all four
|
|
24
|
+
* device orientations. The app layout is portrait-locked, so — like
|
|
25
|
+
* {@link CaptureStatusOverlay} / {@link PanoramaGuidance} — we anchor
|
|
26
|
+
* to the matching layout corner and apply a rotation transform so the
|
|
27
|
+
* number reads upright in the user's hold.
|
|
28
|
+
* - Blinks the WHOLE timer (dot + number) between
|
|
29
|
+
* `GUIDANCE_COUNTDOWN.blinkMinOpacity` and `blinkMaxOpacity` over
|
|
30
|
+
* `blinkPeriodMs` with an ease-in-out loop on the native driver.
|
|
31
|
+
*
|
|
32
|
+
* The displayed number is COSMETIC only — the parent owns the authoritative
|
|
33
|
+
* auto-stop timer and computes `secondsRemaining`. This component never
|
|
34
|
+
* fires a stop; it purely visualises the remaining time, so a dropped frame
|
|
35
|
+
* or a re-render hiccup can never desync from (or pre-empt) the real timer.
|
|
36
|
+
*
|
|
37
|
+
* Pure-presentational and `pointerEvents="none"`: it never steals taps from
|
|
38
|
+
* the camera / shutter beneath it, and renders nothing when `!visible` so
|
|
39
|
+
* the host can mount it unconditionally without layout shifts.
|
|
40
|
+
*/
|
|
41
|
+
import React from 'react';
|
|
42
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
43
|
+
import { type DeviceOrientation } from './useDeviceOrientation';
|
|
44
|
+
export interface CaptureCountdownOverlayProps {
|
|
45
|
+
/**
|
|
46
|
+
* Show / hide. Driven by the host while a capture is in progress
|
|
47
|
+
* (typically `statusPhase === 'recording'`). Renders nothing when
|
|
48
|
+
* false so the host can mount it unconditionally.
|
|
49
|
+
*/
|
|
50
|
+
visible: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Whole seconds of capture remaining, as computed by the parent's
|
|
53
|
+
* authoritative timer (Camera's `countdownSecondsFrom`). Displayed
|
|
54
|
+
* verbatim via `Math.max(0, Math.round(...))` so a fractional or
|
|
55
|
+
* transiently-negative value never renders as "-0" or "3.0".
|
|
56
|
+
*
|
|
57
|
+
* COSMETIC ONLY — this component does not own the auto-stop.
|
|
58
|
+
*/
|
|
59
|
+
secondsRemaining: number;
|
|
60
|
+
/**
|
|
61
|
+
* Physical device orientation (typically from `useDeviceOrientation`).
|
|
62
|
+
* Drives the corner anchoring + rotation so the number sits at the
|
|
63
|
+
* user-perceived top-left and reads upright.
|
|
64
|
+
*/
|
|
65
|
+
orientation: DeviceOrientation;
|
|
66
|
+
/** Outer style passthrough. */
|
|
67
|
+
style?: StyleProp<ViewStyle>;
|
|
68
|
+
}
|
|
69
|
+
export declare function CaptureCountdownOverlay({ visible, secondsRemaining, orientation, style, }: CaptureCountdownOverlayProps): React.JSX.Element | null;
|
|
70
|
+
//# sourceMappingURL=CaptureCountdownOverlay.d.ts.map
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* CaptureCountdownOverlay — the blinking auto-stop countdown shown at the
|
|
5
|
+
* user-perceived TOP-LEFT during an in-progress panorama capture (item 5
|
|
6
|
+
* of the first-time-user GUIDANCE flow).
|
|
7
|
+
*
|
|
8
|
+
* ┌──────────────────────────────────────────────────┐
|
|
9
|
+
* │ ● 9 │ ← top-left, blinks
|
|
10
|
+
* │ │
|
|
11
|
+
* │ (camera preview / pan in progress) │
|
|
12
|
+
* │ │
|
|
13
|
+
* └──────────────────────────────────────────────────┘
|
|
14
|
+
*
|
|
15
|
+
* Why this exists
|
|
16
|
+
* The capture auto-finalizes after a fixed window (the parent owns the
|
|
17
|
+
* real timer — see Camera's `countdownSecondsFrom`). Without a visible
|
|
18
|
+
* counter the auto-stop feels abrupt ("why did it stop?"). A calm
|
|
19
|
+
* blinking "● N" tells the user how many seconds of pan they have left.
|
|
20
|
+
*
|
|
21
|
+
* What it does
|
|
22
|
+
* - Renders an amber glow dot + a white tabular-nums integer
|
|
23
|
+
* (`secondsRemaining`, computed by the parent) using the shared
|
|
24
|
+
* {@link GUIDANCE_COUNTDOWN} design tokens.
|
|
25
|
+
* - Pins itself to the user-perceived top-left corner across all four
|
|
26
|
+
* device orientations. The app layout is portrait-locked, so — like
|
|
27
|
+
* {@link CaptureStatusOverlay} / {@link PanoramaGuidance} — we anchor
|
|
28
|
+
* to the matching layout corner and apply a rotation transform so the
|
|
29
|
+
* number reads upright in the user's hold.
|
|
30
|
+
* - Blinks the WHOLE timer (dot + number) between
|
|
31
|
+
* `GUIDANCE_COUNTDOWN.blinkMinOpacity` and `blinkMaxOpacity` over
|
|
32
|
+
* `blinkPeriodMs` with an ease-in-out loop on the native driver.
|
|
33
|
+
*
|
|
34
|
+
* The displayed number is COSMETIC only — the parent owns the authoritative
|
|
35
|
+
* auto-stop timer and computes `secondsRemaining`. This component never
|
|
36
|
+
* fires a stop; it purely visualises the remaining time, so a dropped frame
|
|
37
|
+
* or a re-render hiccup can never desync from (or pre-empt) the real timer.
|
|
38
|
+
*
|
|
39
|
+
* Pure-presentational and `pointerEvents="none"`: it never steals taps from
|
|
40
|
+
* the camera / shutter beneath it, and renders nothing when `!visible` so
|
|
41
|
+
* the host can mount it unconditionally without layout shifts.
|
|
42
|
+
*/
|
|
43
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
44
|
+
if (k2 === undefined) k2 = k;
|
|
45
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
46
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
47
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
48
|
+
}
|
|
49
|
+
Object.defineProperty(o, k2, desc);
|
|
50
|
+
}) : (function(o, m, k, k2) {
|
|
51
|
+
if (k2 === undefined) k2 = k;
|
|
52
|
+
o[k2] = m[k];
|
|
53
|
+
}));
|
|
54
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
55
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
56
|
+
}) : function(o, v) {
|
|
57
|
+
o["default"] = v;
|
|
58
|
+
});
|
|
59
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
60
|
+
var ownKeys = function(o) {
|
|
61
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
62
|
+
var ar = [];
|
|
63
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
64
|
+
return ar;
|
|
65
|
+
};
|
|
66
|
+
return ownKeys(o);
|
|
67
|
+
};
|
|
68
|
+
return function (mod) {
|
|
69
|
+
if (mod && mod.__esModule) return mod;
|
|
70
|
+
var result = {};
|
|
71
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
72
|
+
__setModuleDefault(result, mod);
|
|
73
|
+
return result;
|
|
74
|
+
};
|
|
75
|
+
})();
|
|
76
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
77
|
+
exports.CaptureCountdownOverlay = CaptureCountdownOverlay;
|
|
78
|
+
const react_1 = __importStar(require("react"));
|
|
79
|
+
const react_native_1 = require("react-native");
|
|
80
|
+
const guidanceTokens_1 = require("./guidanceTokens");
|
|
81
|
+
function CaptureCountdownOverlay({ visible, secondsRemaining, orientation, style, }) {
|
|
82
|
+
// Single Animated.Value looping min→max→min drives the whole-timer
|
|
83
|
+
// blink. Cheap (no JS listeners, native driver) and only spins up
|
|
84
|
+
// while visible; torn down on hide so the loop isn't running the
|
|
85
|
+
// rest of the time the screen is up.
|
|
86
|
+
const blink = (0, react_1.useRef)(new react_native_1.Animated.Value(guidanceTokens_1.GUIDANCE_COUNTDOWN.blinkMaxOpacity)).current;
|
|
87
|
+
(0, react_1.useEffect)(() => {
|
|
88
|
+
if (!visible) {
|
|
89
|
+
// Reset to fully-opaque so the next show starts bright rather
|
|
90
|
+
// than mid-fade.
|
|
91
|
+
blink.setValue(guidanceTokens_1.GUIDANCE_COUNTDOWN.blinkMaxOpacity);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Symmetric fade down + back up, so a full min→max→min cycle takes
|
|
95
|
+
// `blinkPeriodMs` (each half-leg is half the period).
|
|
96
|
+
const halfPeriod = guidanceTokens_1.GUIDANCE_COUNTDOWN.blinkPeriodMs / 2;
|
|
97
|
+
const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
|
|
98
|
+
react_native_1.Animated.timing(blink, {
|
|
99
|
+
toValue: guidanceTokens_1.GUIDANCE_COUNTDOWN.blinkMinOpacity,
|
|
100
|
+
duration: halfPeriod,
|
|
101
|
+
easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
|
|
102
|
+
useNativeDriver: true,
|
|
103
|
+
}),
|
|
104
|
+
react_native_1.Animated.timing(blink, {
|
|
105
|
+
toValue: guidanceTokens_1.GUIDANCE_COUNTDOWN.blinkMaxOpacity,
|
|
106
|
+
duration: halfPeriod,
|
|
107
|
+
easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
|
|
108
|
+
useNativeDriver: true,
|
|
109
|
+
}),
|
|
110
|
+
]));
|
|
111
|
+
loop.start();
|
|
112
|
+
return () => loop.stop();
|
|
113
|
+
}, [visible, blink]);
|
|
114
|
+
if (!visible)
|
|
115
|
+
return null;
|
|
116
|
+
// Clamp to a non-negative integer. The parent's timer may briefly
|
|
117
|
+
// report a fractional or sub-zero value at the auto-stop boundary;
|
|
118
|
+
// we never want to render "-1" or "2.4".
|
|
119
|
+
const displaySeconds = Math.max(0, Math.round(secondsRemaining));
|
|
120
|
+
const cornerStyle = countdownStyleForOrientation(orientation);
|
|
121
|
+
return (react_1.default.createElement(react_native_1.Animated.View, { pointerEvents: "none", style: [styles.root, cornerStyle, { opacity: blink }, style], accessibilityRole: "timer", accessibilityLiveRegion: "polite", accessibilityLabel: `${displaySeconds} seconds remaining` },
|
|
122
|
+
react_1.default.createElement(react_native_1.View, { style: styles.dot }),
|
|
123
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.number, numberOfLines: 1, allowFontScaling: false }, displaySeconds)));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Style placing the countdown at the user-perceived TOP-LEFT corner with
|
|
127
|
+
* the number reading upright in the user's hold, inset by
|
|
128
|
+
* `GUIDANCE_COUNTDOWN.inset` from that corner.
|
|
129
|
+
*
|
|
130
|
+
* Mirrors the corner-anchor + percentage-translate self-centering of
|
|
131
|
+
* {@link CaptureStatusOverlay}'s `bannerStyleForOrientation`, but anchored
|
|
132
|
+
* to the user's top-LEFT instead of top-center. For each orientation we
|
|
133
|
+
* anchor the row to the layout corner that maps to the user's top-left,
|
|
134
|
+
* then rotate the row about its center so it reads upright:
|
|
135
|
+
*
|
|
136
|
+
* portrait → layout top-left, 0°
|
|
137
|
+
* landscape-left → layout bottom-left, +90°
|
|
138
|
+
* landscape-right → layout top-right, -90°
|
|
139
|
+
* portrait-upside-down → layout bottom-right, 180°
|
|
140
|
+
*
|
|
141
|
+
* The `translate('±50%')` pair pins the row's CENTER a fixed `inset`
|
|
142
|
+
* from the chosen corner so the post-rotation top-left edge lands at
|
|
143
|
+
* `inset` regardless of the row's own width/height — the same trick the
|
|
144
|
+
* banner uses to stay corner-aligned without measuring its content.
|
|
145
|
+
*/
|
|
146
|
+
function countdownStyleForOrientation(orientation) {
|
|
147
|
+
const { inset } = guidanceTokens_1.GUIDANCE_COUNTDOWN;
|
|
148
|
+
switch (orientation) {
|
|
149
|
+
case 'landscape-left':
|
|
150
|
+
// Device held so user-top runs along the layout LEFT edge; the
|
|
151
|
+
// user's top-left maps to the layout BOTTOM-left. +90° makes the
|
|
152
|
+
// row read upright.
|
|
153
|
+
return {
|
|
154
|
+
position: 'absolute',
|
|
155
|
+
bottom: inset,
|
|
156
|
+
left: inset,
|
|
157
|
+
transform: [
|
|
158
|
+
{ translateX: '50%' },
|
|
159
|
+
{ translateY: '-50%' },
|
|
160
|
+
{ rotate: '90deg' },
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
case 'landscape-right':
|
|
164
|
+
// User-top runs along the layout RIGHT edge; user's top-left maps
|
|
165
|
+
// to the layout TOP-right. -90° makes the row read upright.
|
|
166
|
+
return {
|
|
167
|
+
position: 'absolute',
|
|
168
|
+
top: inset,
|
|
169
|
+
right: inset,
|
|
170
|
+
transform: [
|
|
171
|
+
{ translateX: '-50%' },
|
|
172
|
+
{ translateY: '50%' },
|
|
173
|
+
{ rotate: '-90deg' },
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
case 'portrait-upside-down':
|
|
177
|
+
// User-top-left maps to the layout BOTTOM-right; 180° flips the row.
|
|
178
|
+
return {
|
|
179
|
+
position: 'absolute',
|
|
180
|
+
bottom: inset,
|
|
181
|
+
right: inset,
|
|
182
|
+
transform: [
|
|
183
|
+
{ translateX: '-50%' },
|
|
184
|
+
{ translateY: '-50%' },
|
|
185
|
+
{ rotate: '180deg' },
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
case 'portrait':
|
|
189
|
+
default:
|
|
190
|
+
return {
|
|
191
|
+
position: 'absolute',
|
|
192
|
+
top: inset,
|
|
193
|
+
left: inset,
|
|
194
|
+
transform: [
|
|
195
|
+
{ translateX: '50%' },
|
|
196
|
+
{ translateY: '50%' },
|
|
197
|
+
],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const styles = react_native_1.StyleSheet.create({
|
|
202
|
+
root: {
|
|
203
|
+
// position: 'absolute' is re-applied by countdownStyleForOrientation
|
|
204
|
+
// alongside the corner offsets + transform; kept here too so the row
|
|
205
|
+
// lays out as a self-sized box even before the orientation style
|
|
206
|
+
// merges in.
|
|
207
|
+
position: 'absolute',
|
|
208
|
+
flexDirection: 'row',
|
|
209
|
+
alignItems: 'center',
|
|
210
|
+
},
|
|
211
|
+
dot: {
|
|
212
|
+
width: guidanceTokens_1.GUIDANCE_COUNTDOWN.dotSize,
|
|
213
|
+
height: guidanceTokens_1.GUIDANCE_COUNTDOWN.dotSize,
|
|
214
|
+
borderRadius: guidanceTokens_1.GUIDANCE_COUNTDOWN.dotSize / 2,
|
|
215
|
+
backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
|
|
216
|
+
marginRight: guidanceTokens_1.GUIDANCE_COUNTDOWN.dotGap,
|
|
217
|
+
// Amber glow around the dot. iOS honours all four shadow props;
|
|
218
|
+
// Android renders the glow via `elevation` (set below) since RN
|
|
219
|
+
// ignores view shadowColor there.
|
|
220
|
+
shadowColor: guidanceTokens_1.GUIDANCE_COUNTDOWN.dotGlow,
|
|
221
|
+
shadowOpacity: 1,
|
|
222
|
+
shadowRadius: guidanceTokens_1.GUIDANCE_COUNTDOWN.dotSize,
|
|
223
|
+
shadowOffset: { width: 0, height: 0 },
|
|
224
|
+
elevation: 6,
|
|
225
|
+
},
|
|
226
|
+
number: {
|
|
227
|
+
color: guidanceTokens_1.GUIDANCE_TOKENS.white,
|
|
228
|
+
fontSize: guidanceTokens_1.GUIDANCE_COUNTDOWN.fontSize,
|
|
229
|
+
fontWeight: guidanceTokens_1.GUIDANCE_COUNTDOWN.fontWeight,
|
|
230
|
+
// Tabular figures keep the glyph box a fixed width so the number
|
|
231
|
+
// doesn't jitter horizontally as it ticks 9→8→…→0.
|
|
232
|
+
fontVariant: ['tabular-nums'],
|
|
233
|
+
// Drop shadow for legibility over a bright/busy preview.
|
|
234
|
+
textShadowColor: guidanceTokens_1.GUIDANCE_TOKENS.scrim,
|
|
235
|
+
textShadowOffset: { width: 0, height: 1 },
|
|
236
|
+
textShadowRadius: 3,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
//# sourceMappingURL=CaptureCountdownOverlay.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CaptureFrameCounterOverlay — a live "k / n" keyframe counter shown at the
|
|
3
|
+
* user-perceived TOP-CENTRE during a panorama capture.
|
|
4
|
+
*
|
|
5
|
+
* ┌──────────────────────────────────────────────────┐
|
|
6
|
+
* │ ● 3 / 6 │ ← top-centre
|
|
7
|
+
* │ │
|
|
8
|
+
* │ (camera preview / pan in progress) │
|
|
9
|
+
* └──────────────────────────────────────────────────┘
|
|
10
|
+
*
|
|
11
|
+
* Replaces the time countdown (item 5) as the primary capture HUD: instead
|
|
12
|
+
* of "seconds left" it shows how many keyframes have been captured out of
|
|
13
|
+
* the configured maximum, so the user can see the capture filling up and
|
|
14
|
+
* understand WHY it auto-finalizes at the cap (the parent stops + stitches
|
|
15
|
+
* when `framesCaptured` reaches `framesMax`).
|
|
16
|
+
*
|
|
17
|
+
* Pure-presentational + `pointerEvents="none"` (never steals taps); renders
|
|
18
|
+
* nothing when `!visible` so the host can mount it unconditionally. Pins
|
|
19
|
+
* itself to the user-perceived TOP-CENTRE across all four orientations: the
|
|
20
|
+
* app is typically portrait-locked, so we anchor the pill to the layout edge
|
|
21
|
+
* that maps to the user's top and counter-rotate it to read upright (same
|
|
22
|
+
* idea as CaptureCountdownOverlay, but centre-anchored instead of corner).
|
|
23
|
+
*/
|
|
24
|
+
import React from 'react';
|
|
25
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
26
|
+
import { type DeviceOrientation } from './useDeviceOrientation';
|
|
27
|
+
export interface CaptureFrameCounterOverlayProps {
|
|
28
|
+
/** Show / hide. Driven by the host while a capture is recording. */
|
|
29
|
+
visible: boolean;
|
|
30
|
+
/** Keyframes accepted so far this capture (the engine's live count). */
|
|
31
|
+
framesCaptured: number;
|
|
32
|
+
/** Configured keyframe cap — the capture auto-finalizes when reached. */
|
|
33
|
+
framesMax: number;
|
|
34
|
+
/** Physical device orientation (from `useDeviceOrientation`). */
|
|
35
|
+
orientation: DeviceOrientation;
|
|
36
|
+
/** Outer style passthrough. */
|
|
37
|
+
style?: StyleProp<ViewStyle>;
|
|
38
|
+
}
|
|
39
|
+
export declare function CaptureFrameCounterOverlay({ visible, framesCaptured, framesMax, orientation, style, }: CaptureFrameCounterOverlayProps): React.JSX.Element | null;
|
|
40
|
+
/**
|
|
41
|
+
* Flex alignment that pins content to the user-perceived TOP-CENTRE for a
|
|
42
|
+
* given device hold, plus the rotation that makes it read upright:
|
|
43
|
+
*
|
|
44
|
+
* portrait → layout top edge, centred, 0°
|
|
45
|
+
* landscape-left → layout left edge, centred, +90°
|
|
46
|
+
* landscape-right → layout right edge, centred, -90°
|
|
47
|
+
* portrait-upside-down → layout bottom edge, centred, 180°
|
|
48
|
+
*
|
|
49
|
+
* `inset` is the distance from the user's top edge (larger values push the
|
|
50
|
+
* content further down the screen) — exported so other top-anchored overlays
|
|
51
|
+
* (e.g. the too-fast pill) can stack BELOW the counter by passing a bigger
|
|
52
|
+
* inset, and stay correctly placed + upright in every orientation.
|
|
53
|
+
*/
|
|
54
|
+
export declare function topCenterForOrientation(orientation: DeviceOrientation, inset: number): {
|
|
55
|
+
container: ViewStyle;
|
|
56
|
+
rotate: string;
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=CaptureFrameCounterOverlay.d.ts.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* CaptureFrameCounterOverlay — a live "k / n" keyframe counter shown at the
|
|
5
|
+
* user-perceived TOP-CENTRE during a panorama capture.
|
|
6
|
+
*
|
|
7
|
+
* ┌──────────────────────────────────────────────────┐
|
|
8
|
+
* │ ● 3 / 6 │ ← top-centre
|
|
9
|
+
* │ │
|
|
10
|
+
* │ (camera preview / pan in progress) │
|
|
11
|
+
* └──────────────────────────────────────────────────┘
|
|
12
|
+
*
|
|
13
|
+
* Replaces the time countdown (item 5) as the primary capture HUD: instead
|
|
14
|
+
* of "seconds left" it shows how many keyframes have been captured out of
|
|
15
|
+
* the configured maximum, so the user can see the capture filling up and
|
|
16
|
+
* understand WHY it auto-finalizes at the cap (the parent stops + stitches
|
|
17
|
+
* when `framesCaptured` reaches `framesMax`).
|
|
18
|
+
*
|
|
19
|
+
* Pure-presentational + `pointerEvents="none"` (never steals taps); renders
|
|
20
|
+
* nothing when `!visible` so the host can mount it unconditionally. Pins
|
|
21
|
+
* itself to the user-perceived TOP-CENTRE across all four orientations: the
|
|
22
|
+
* app is typically portrait-locked, so we anchor the pill to the layout edge
|
|
23
|
+
* that maps to the user's top and counter-rotate it to read upright (same
|
|
24
|
+
* idea as CaptureCountdownOverlay, but centre-anchored instead of corner).
|
|
25
|
+
*/
|
|
26
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
27
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
28
|
+
};
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.CaptureFrameCounterOverlay = CaptureFrameCounterOverlay;
|
|
31
|
+
exports.topCenterForOrientation = topCenterForOrientation;
|
|
32
|
+
const react_1 = __importDefault(require("react"));
|
|
33
|
+
const react_native_1 = require("react-native");
|
|
34
|
+
const guidanceTokens_1 = require("./guidanceTokens");
|
|
35
|
+
function CaptureFrameCounterOverlay({ visible, framesCaptured, framesMax, orientation, style, }) {
|
|
36
|
+
if (!visible || framesMax <= 0)
|
|
37
|
+
return null;
|
|
38
|
+
// Clamp the displayed numerator into [0, framesMax] — the engine can
|
|
39
|
+
// briefly report the cap-th accept before the parent finalizes.
|
|
40
|
+
const k = Math.max(0, Math.min(framesCaptured, framesMax));
|
|
41
|
+
const { container, rotate } = topCenterForOrientation(orientation, guidanceTokens_1.GUIDANCE_COUNTDOWN.inset);
|
|
42
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [styles.layer, container, style] },
|
|
43
|
+
react_1.default.createElement(react_native_1.View, { style: [styles.pill, { transform: [{ rotate }] }] },
|
|
44
|
+
react_1.default.createElement(react_native_1.View, { style: styles.dot }),
|
|
45
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.text, allowFontScaling: false, numberOfLines: 1 },
|
|
46
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.count }, k),
|
|
47
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.slash },
|
|
48
|
+
" / ",
|
|
49
|
+
framesMax)))));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Flex alignment that pins content to the user-perceived TOP-CENTRE for a
|
|
53
|
+
* given device hold, plus the rotation that makes it read upright:
|
|
54
|
+
*
|
|
55
|
+
* portrait → layout top edge, centred, 0°
|
|
56
|
+
* landscape-left → layout left edge, centred, +90°
|
|
57
|
+
* landscape-right → layout right edge, centred, -90°
|
|
58
|
+
* portrait-upside-down → layout bottom edge, centred, 180°
|
|
59
|
+
*
|
|
60
|
+
* `inset` is the distance from the user's top edge (larger values push the
|
|
61
|
+
* content further down the screen) — exported so other top-anchored overlays
|
|
62
|
+
* (e.g. the too-fast pill) can stack BELOW the counter by passing a bigger
|
|
63
|
+
* inset, and stay correctly placed + upright in every orientation.
|
|
64
|
+
*/
|
|
65
|
+
function topCenterForOrientation(orientation, inset) {
|
|
66
|
+
switch (orientation) {
|
|
67
|
+
case 'landscape-left':
|
|
68
|
+
return {
|
|
69
|
+
container: {
|
|
70
|
+
justifyContent: 'center',
|
|
71
|
+
alignItems: 'flex-start',
|
|
72
|
+
paddingLeft: inset,
|
|
73
|
+
},
|
|
74
|
+
rotate: '90deg',
|
|
75
|
+
};
|
|
76
|
+
case 'landscape-right':
|
|
77
|
+
return {
|
|
78
|
+
container: {
|
|
79
|
+
justifyContent: 'center',
|
|
80
|
+
alignItems: 'flex-end',
|
|
81
|
+
paddingRight: inset,
|
|
82
|
+
},
|
|
83
|
+
rotate: '-90deg',
|
|
84
|
+
};
|
|
85
|
+
case 'portrait-upside-down':
|
|
86
|
+
return {
|
|
87
|
+
container: {
|
|
88
|
+
justifyContent: 'flex-end',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
paddingBottom: inset,
|
|
91
|
+
},
|
|
92
|
+
rotate: '180deg',
|
|
93
|
+
};
|
|
94
|
+
case 'portrait':
|
|
95
|
+
default:
|
|
96
|
+
return {
|
|
97
|
+
container: {
|
|
98
|
+
justifyContent: 'flex-start',
|
|
99
|
+
alignItems: 'center',
|
|
100
|
+
paddingTop: inset,
|
|
101
|
+
},
|
|
102
|
+
rotate: '0deg',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const styles = react_native_1.StyleSheet.create({
|
|
107
|
+
// Full-screen, non-interactive layer; the per-orientation flex alignment
|
|
108
|
+
// places the pill on the correct edge, centred along it.
|
|
109
|
+
layer: { ...react_native_1.StyleSheet.absoluteFillObject },
|
|
110
|
+
pill: {
|
|
111
|
+
flexDirection: 'row',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
paddingVertical: guidanceTokens_1.GUIDANCE_PILL.paddingVertical,
|
|
114
|
+
paddingHorizontal: guidanceTokens_1.GUIDANCE_PILL.paddingHorizontal,
|
|
115
|
+
borderRadius: guidanceTokens_1.GUIDANCE_PILL.borderRadius,
|
|
116
|
+
backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.scrim,
|
|
117
|
+
borderWidth: react_native_1.StyleSheet.hairlineWidth,
|
|
118
|
+
borderColor: guidanceTokens_1.GUIDANCE_TOKENS.hairline,
|
|
119
|
+
},
|
|
120
|
+
dot: {
|
|
121
|
+
width: guidanceTokens_1.GUIDANCE_PILL.dotSize,
|
|
122
|
+
height: guidanceTokens_1.GUIDANCE_PILL.dotSize,
|
|
123
|
+
borderRadius: guidanceTokens_1.GUIDANCE_PILL.dotSize / 2,
|
|
124
|
+
backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
|
|
125
|
+
marginRight: guidanceTokens_1.GUIDANCE_PILL.dotGap,
|
|
126
|
+
},
|
|
127
|
+
text: {
|
|
128
|
+
// Tabular figures keep the counter from jittering as k ticks up.
|
|
129
|
+
fontVariant: ['tabular-nums'],
|
|
130
|
+
},
|
|
131
|
+
count: {
|
|
132
|
+
color: guidanceTokens_1.GUIDANCE_TOKENS.white,
|
|
133
|
+
fontSize: 17,
|
|
134
|
+
fontWeight: '700',
|
|
135
|
+
},
|
|
136
|
+
slash: {
|
|
137
|
+
color: guidanceTokens_1.GUIDANCE_TOKENS.amber,
|
|
138
|
+
fontSize: 15,
|
|
139
|
+
fontWeight: '600',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
//# sourceMappingURL=CaptureFrameCounterOverlay.js.map
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* polls native every 500 ms and is unwanted in production builds.
|
|
17
17
|
*/
|
|
18
18
|
import React from 'react';
|
|
19
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
19
20
|
export interface CaptureMemoryPillProps {
|
|
20
21
|
/** Top inset (status bar / notch). Pill pinned `topInset + 56`. */
|
|
21
22
|
topInset?: number;
|
|
@@ -23,6 +24,13 @@ export interface CaptureMemoryPillProps {
|
|
|
23
24
|
* for no visible benefit; higher loses correlation with capture
|
|
24
25
|
* activity. */
|
|
25
26
|
pollIntervalMs?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Optional position override. When supplied it REPLACES the default
|
|
29
|
+
* top-right anchor (`top: topInset + 56, right: 12`), so the pill can be
|
|
30
|
+
* reused on other screens (e.g. the crop/preview surface) without colliding
|
|
31
|
+
* with their own corner UI. Pass the full absolute position you want.
|
|
32
|
+
*/
|
|
33
|
+
style?: StyleProp<ViewStyle>;
|
|
26
34
|
}
|
|
27
|
-
export declare function CaptureMemoryPill({ topInset, pollIntervalMs, }: CaptureMemoryPillProps): React.JSX.Element | null;
|
|
35
|
+
export declare function CaptureMemoryPill({ topInset, pollIntervalMs, style, }: CaptureMemoryPillProps): React.JSX.Element | null;
|
|
28
36
|
//# sourceMappingURL=CaptureMemoryPill.d.ts.map
|
|
@@ -55,7 +55,7 @@ exports.CaptureMemoryPill = CaptureMemoryPill;
|
|
|
55
55
|
const react_1 = __importStar(require("react"));
|
|
56
56
|
const react_native_1 = require("react-native");
|
|
57
57
|
const incremental_1 = require("../stitching/incremental");
|
|
58
|
-
function CaptureMemoryPill({ topInset = 0, pollIntervalMs = 500, }) {
|
|
58
|
+
function CaptureMemoryPill({ topInset = 0, pollIntervalMs = 500, style, }) {
|
|
59
59
|
const [memMB, setMemMB] = (0, react_1.useState)(null);
|
|
60
60
|
(0, react_1.useEffect)(() => {
|
|
61
61
|
const native = (0, incremental_1.getIncrementalNativeModule)();
|
|
@@ -86,14 +86,14 @@ function CaptureMemoryPill({ topInset = 0, pollIntervalMs = 500, }) {
|
|
|
86
86
|
: 'rgba(34, 197, 94, 0.92)'; // green
|
|
87
87
|
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [
|
|
88
88
|
styles.container,
|
|
89
|
-
{
|
|
89
|
+
{ backgroundColor: bg },
|
|
90
|
+
style ?? { top: topInset + 56, right: 12 },
|
|
90
91
|
], accessibilityRole: "alert" },
|
|
91
92
|
react_1.default.createElement(react_native_1.Text, { style: styles.text }, `${Math.round(memMB)} MB`)));
|
|
92
93
|
}
|
|
93
94
|
const styles = react_native_1.StyleSheet.create({
|
|
94
95
|
container: {
|
|
95
96
|
position: 'absolute',
|
|
96
|
-
right: 12,
|
|
97
97
|
paddingHorizontal: 10,
|
|
98
98
|
paddingVertical: 5,
|
|
99
99
|
borderRadius: 999,
|