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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DISPLAY_DECODE_IMAGE_PROPS = void 0;
|
|
4
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
/**
|
|
6
|
+
* DISPLAY_DECODE_IMAGE_PROPS — props every <Image> that displays a
|
|
7
|
+
* FULL-RES capture (a stitched panorama or a photo file) must spread.
|
|
8
|
+
*
|
|
9
|
+
* Why this exists (the accumulation half of the OOM crash):
|
|
10
|
+
* On Android 8+, decoded bitmap pixels live in the NATIVE heap, and the
|
|
11
|
+
* source here is the full-resolution capture file — a wide panorama can
|
|
12
|
+
* be tens of megapixels. Without `resizeMethod="resize"`, Android/Fresco
|
|
13
|
+
* decodes the source at FULL resolution into a native bitmap that the
|
|
14
|
+
* mounted <Image> pins (not LRU-evictable), and Fresco's URI-keyed cache
|
|
15
|
+
* keeps it even after the view unmounts. Each capture (especially wide
|
|
16
|
+
* panoramas) then accumulates tens of MB of native heap until lmkd
|
|
17
|
+
* OOM-kills the app. 'resize' decodes at the on-screen (~device-width)
|
|
18
|
+
* size instead, making per-image memory tiny and panorama-size-
|
|
19
|
+
* independent. No-op on iOS (harmless).
|
|
20
|
+
*
|
|
21
|
+
* Centralised (rather than a bare `resizeMethod="resize"` at each call
|
|
22
|
+
* site) so the decode strategy + its rationale have one home, and so the
|
|
23
|
+
* contract is unit-testable without mounting a component. Spread it:
|
|
24
|
+
* <Image source={...} resizeMode="cover" {...DISPLAY_DECODE_IMAGE_PROPS} />
|
|
25
|
+
*/
|
|
26
|
+
exports.DISPLAY_DECODE_IMAGE_PROPS = {
|
|
27
|
+
resizeMethod: 'resize',
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=displayDecodeImageProps.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guidanceGraphics — code-drawn replacements for the two authored guidance
|
|
3
|
+
* GIFs (rotate-to-landscape, pan-capture). Built from pure React-Native
|
|
4
|
+
* core `View` + `Animated` primitives — NO `react-native-svg`, NO bundled
|
|
5
|
+
* image assets — so the library keeps its "zero extra native deps for
|
|
6
|
+
* guidance" contract (see `RectCropPreview`) AND no longer needs the host
|
|
7
|
+
* to add Fresco's `animated-gif` module on Android just to make the
|
|
8
|
+
* coach-marks move.
|
|
9
|
+
*
|
|
10
|
+
* Why not GIFs: the authored GIFs were 280 px sources shown at 240 dp;
|
|
11
|
+
* on a ~2.6×-density phone that 240 dp is ~630 physical px, so the 280 px
|
|
12
|
+
* source was up-scaled ~2.25× → visibly pixelated. A 256-colour GIF also
|
|
13
|
+
* bands. These vector-ish primitives are resolution-independent (they're
|
|
14
|
+
* just borders + transforms the GPU rasterises at native density) and fully
|
|
15
|
+
* themeable via `GUIDANCE_TOKENS`.
|
|
16
|
+
*
|
|
17
|
+
* Both graphics:
|
|
18
|
+
* • run a single `Animated.loop` on the NATIVE driver (transform/opacity
|
|
19
|
+
* only) so the loop is off the JS thread;
|
|
20
|
+
* • take a `playing` flag — the host renders them only while `visible`,
|
|
21
|
+
* but we still gate the loop so a mounted-but-paused graphic costs
|
|
22
|
+
* nothing;
|
|
23
|
+
* • scale every dimension off a single `size` (defaults to the shared
|
|
24
|
+
* `GUIDANCE_TOKENS.graphicSize`) so callers can resize without restyle.
|
|
25
|
+
*/
|
|
26
|
+
import React from 'react';
|
|
27
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
28
|
+
/** Pan direction the pan-graphic should animate (mirrors PanHowToOverlay). */
|
|
29
|
+
export type PanGraphicDirection = 'down' | 'right';
|
|
30
|
+
export interface GuidanceGraphicProps {
|
|
31
|
+
/** Canvas square size in px. Defaults to `GUIDANCE_TOKENS.graphicSize`. */
|
|
32
|
+
size?: number;
|
|
33
|
+
/** Run the animation loop. `false` parks the value at rest. */
|
|
34
|
+
playing?: boolean;
|
|
35
|
+
/** Outer style passthrough. */
|
|
36
|
+
style?: StyleProp<ViewStyle>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* RotatePhoneGraphic — a portrait phone outline that rotates 0°→90°→0°
|
|
40
|
+
* (portrait → landscape → portrait) on a loop, riding a faint amber guide
|
|
41
|
+
* ring with a clockwise arrowhead, demonstrating the "rotate to landscape"
|
|
42
|
+
* gesture. Replaces `rotate-to-landscape.gif`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function RotatePhoneGraphic({ size, playing, style, target, }: GuidanceGraphicProps & {
|
|
45
|
+
/** Orientation to rotate TO: 'landscape' (default) or 'portrait'. */
|
|
46
|
+
target?: 'landscape' | 'portrait';
|
|
47
|
+
}): React.JSX.Element;
|
|
48
|
+
/**
|
|
49
|
+
* PanPhoneGraphic — a phone outline (landscape for Mode-A `down`, portrait
|
|
50
|
+
* for Mode-B `right`) with an amber sweep band that travels across the pan
|
|
51
|
+
* axis on a loop, demonstrating the camera sweep. The band fades in/out at
|
|
52
|
+
* the travel ends so the loop reset is invisible. Replaces
|
|
53
|
+
* `pan-capture.gif`. The bouncing direction arrow stays in PanHowToOverlay.
|
|
54
|
+
*/
|
|
55
|
+
export declare function PanPhoneGraphic({ direction, size, playing, style, }: GuidanceGraphicProps & {
|
|
56
|
+
direction: PanGraphicDirection;
|
|
57
|
+
}): React.JSX.Element;
|
|
58
|
+
//# sourceMappingURL=guidanceGraphics.d.ts.map
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* guidanceGraphics — code-drawn replacements for the two authored guidance
|
|
5
|
+
* GIFs (rotate-to-landscape, pan-capture). Built from pure React-Native
|
|
6
|
+
* core `View` + `Animated` primitives — NO `react-native-svg`, NO bundled
|
|
7
|
+
* image assets — so the library keeps its "zero extra native deps for
|
|
8
|
+
* guidance" contract (see `RectCropPreview`) AND no longer needs the host
|
|
9
|
+
* to add Fresco's `animated-gif` module on Android just to make the
|
|
10
|
+
* coach-marks move.
|
|
11
|
+
*
|
|
12
|
+
* Why not GIFs: the authored GIFs were 280 px sources shown at 240 dp;
|
|
13
|
+
* on a ~2.6×-density phone that 240 dp is ~630 physical px, so the 280 px
|
|
14
|
+
* source was up-scaled ~2.25× → visibly pixelated. A 256-colour GIF also
|
|
15
|
+
* bands. These vector-ish primitives are resolution-independent (they're
|
|
16
|
+
* just borders + transforms the GPU rasterises at native density) and fully
|
|
17
|
+
* themeable via `GUIDANCE_TOKENS`.
|
|
18
|
+
*
|
|
19
|
+
* Both graphics:
|
|
20
|
+
* • run a single `Animated.loop` on the NATIVE driver (transform/opacity
|
|
21
|
+
* only) so the loop is off the JS thread;
|
|
22
|
+
* • take a `playing` flag — the host renders them only while `visible`,
|
|
23
|
+
* but we still gate the loop so a mounted-but-paused graphic costs
|
|
24
|
+
* nothing;
|
|
25
|
+
* • scale every dimension off a single `size` (defaults to the shared
|
|
26
|
+
* `GUIDANCE_TOKENS.graphicSize`) so callers can resize without restyle.
|
|
27
|
+
*/
|
|
28
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
31
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
32
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
33
|
+
}
|
|
34
|
+
Object.defineProperty(o, k2, desc);
|
|
35
|
+
}) : (function(o, m, k, k2) {
|
|
36
|
+
if (k2 === undefined) k2 = k;
|
|
37
|
+
o[k2] = m[k];
|
|
38
|
+
}));
|
|
39
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
40
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
41
|
+
}) : function(o, v) {
|
|
42
|
+
o["default"] = v;
|
|
43
|
+
});
|
|
44
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
45
|
+
var ownKeys = function(o) {
|
|
46
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
47
|
+
var ar = [];
|
|
48
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
49
|
+
return ar;
|
|
50
|
+
};
|
|
51
|
+
return ownKeys(o);
|
|
52
|
+
};
|
|
53
|
+
return function (mod) {
|
|
54
|
+
if (mod && mod.__esModule) return mod;
|
|
55
|
+
var result = {};
|
|
56
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
57
|
+
__setModuleDefault(result, mod);
|
|
58
|
+
return result;
|
|
59
|
+
};
|
|
60
|
+
})();
|
|
61
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
62
|
+
exports.RotatePhoneGraphic = RotatePhoneGraphic;
|
|
63
|
+
exports.PanPhoneGraphic = PanPhoneGraphic;
|
|
64
|
+
const react_1 = __importStar(require("react"));
|
|
65
|
+
const react_native_1 = require("react-native");
|
|
66
|
+
const guidanceTokens_1 = require("./guidanceTokens");
|
|
67
|
+
const DEFAULT_SIZE = guidanceTokens_1.GUIDANCE_TOKENS.graphicSize;
|
|
68
|
+
/**
|
|
69
|
+
* A white rounded-rectangle "phone" outline with a small camera dot on its
|
|
70
|
+
* top short edge. The dot makes the device's up-axis legible, so when the
|
|
71
|
+
* rotate graphic turns the body the rotation reads unambiguously. Children
|
|
72
|
+
* (e.g. the pan sweep band) render over the screen area.
|
|
73
|
+
*/
|
|
74
|
+
function PhoneBody({ width, height, children, style, }) {
|
|
75
|
+
const radius = Math.min(width, height) * 0.16;
|
|
76
|
+
const short = Math.min(width, height);
|
|
77
|
+
const dotSize = Math.max(4, short * 0.09);
|
|
78
|
+
const inset = Math.max(4, short * 0.06);
|
|
79
|
+
// The front-facing camera always sits on a SHORT edge: top-centre for a
|
|
80
|
+
// tall (portrait) body, side-centre for a wide (landscape) body. (Was
|
|
81
|
+
// top-centre unconditionally, which put the dot mid-LONG-edge on a
|
|
82
|
+
// landscape body.)
|
|
83
|
+
const isWide = width > height;
|
|
84
|
+
const dotPos = isWide
|
|
85
|
+
? { left: inset, top: height / 2 - dotSize / 2 }
|
|
86
|
+
: { top: inset, left: width / 2 - dotSize / 2 };
|
|
87
|
+
return (react_1.default.createElement(react_native_1.View, { style: [
|
|
88
|
+
{
|
|
89
|
+
width,
|
|
90
|
+
height,
|
|
91
|
+
borderRadius: radius,
|
|
92
|
+
borderWidth: Math.max(2, width * 0.03),
|
|
93
|
+
borderColor: guidanceTokens_1.GUIDANCE_TOKENS.white,
|
|
94
|
+
},
|
|
95
|
+
styles.phoneBody,
|
|
96
|
+
style,
|
|
97
|
+
] },
|
|
98
|
+
react_1.default.createElement(react_native_1.View, { style: [
|
|
99
|
+
styles.cameraDot,
|
|
100
|
+
{ width: dotSize, height: dotSize, borderRadius: dotSize / 2 },
|
|
101
|
+
dotPos,
|
|
102
|
+
] }),
|
|
103
|
+
children));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* RotatePhoneGraphic — a portrait phone outline that rotates 0°→90°→0°
|
|
107
|
+
* (portrait → landscape → portrait) on a loop, riding a faint amber guide
|
|
108
|
+
* ring with a clockwise arrowhead, demonstrating the "rotate to landscape"
|
|
109
|
+
* gesture. Replaces `rotate-to-landscape.gif`.
|
|
110
|
+
*/
|
|
111
|
+
function RotatePhoneGraphic({ size = DEFAULT_SIZE, playing = true, style, target = 'landscape', }) {
|
|
112
|
+
// Single 0→1 loop value drives a ONE-WAY demonstration: hold the START
|
|
113
|
+
// orientation, rotate to the TARGET, hold, then fade out + reset (the
|
|
114
|
+
// reverse rotation happens while invisible). This avoids the symmetric
|
|
115
|
+
// oscillation, which dwelt at the target and read as "starts at target,
|
|
116
|
+
// rotates away" — i.e. backwards.
|
|
117
|
+
const t = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
118
|
+
(0, react_1.useEffect)(() => {
|
|
119
|
+
if (!playing) {
|
|
120
|
+
t.setValue(0);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const loop = react_native_1.Animated.loop(react_native_1.Animated.timing(t, {
|
|
124
|
+
toValue: 1,
|
|
125
|
+
duration: 2200,
|
|
126
|
+
easing: react_native_1.Easing.inOut(react_native_1.Easing.cubic),
|
|
127
|
+
useNativeDriver: true,
|
|
128
|
+
}));
|
|
129
|
+
loop.start();
|
|
130
|
+
return () => loop.stop();
|
|
131
|
+
}, [playing, t]);
|
|
132
|
+
// To-landscape: start portrait (tall), rotate anticlockwise to landscape.
|
|
133
|
+
// To-portrait: start landscape (wide), rotate clockwise to stand upright.
|
|
134
|
+
const toLandscape = target === 'landscape';
|
|
135
|
+
const targetDeg = toLandscape ? '-90deg' : '90deg';
|
|
136
|
+
// Hold START (0°) → rotate to TARGET → hold TARGET.
|
|
137
|
+
const rotate = t.interpolate({
|
|
138
|
+
inputRange: [0, 0.18, 0.62, 1],
|
|
139
|
+
outputRange: ['0deg', '0deg', targetDeg, targetDeg],
|
|
140
|
+
});
|
|
141
|
+
// Fade in at START, hold through the rotation, fade out at TARGET so the
|
|
142
|
+
// invisible reset (target→start on loop) is never seen.
|
|
143
|
+
const phoneOpacity = t.interpolate({
|
|
144
|
+
inputRange: [0, 0.12, 0.82, 1],
|
|
145
|
+
outputRange: [0, 1, 1, 0],
|
|
146
|
+
});
|
|
147
|
+
const ring = size * 0.78;
|
|
148
|
+
const ringInset = (size - ring) / 2;
|
|
149
|
+
const phoneW = toLandscape ? size * 0.3 : size * 0.56;
|
|
150
|
+
const phoneH = toLandscape ? size * 0.56 : size * 0.3;
|
|
151
|
+
return (react_1.default.createElement(react_native_1.View, { style: [{ width: size, height: size }, styles.center, style], pointerEvents: "none" },
|
|
152
|
+
react_1.default.createElement(react_native_1.View, { style: [
|
|
153
|
+
styles.ring,
|
|
154
|
+
{
|
|
155
|
+
width: ring,
|
|
156
|
+
height: ring,
|
|
157
|
+
borderRadius: ring / 2,
|
|
158
|
+
top: ringInset,
|
|
159
|
+
left: ringInset,
|
|
160
|
+
borderColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
|
|
161
|
+
},
|
|
162
|
+
] }),
|
|
163
|
+
react_1.default.createElement(react_native_1.View, { style: [
|
|
164
|
+
styles.arrowHead,
|
|
165
|
+
toLandscape ? styles.arrowHeadLeft : styles.arrowHeadRight,
|
|
166
|
+
{ top: ringInset - 5, left: size / 2 - 5 },
|
|
167
|
+
] }),
|
|
168
|
+
react_1.default.createElement(react_native_1.Animated.View, { style: { opacity: phoneOpacity, transform: [{ rotate }] } },
|
|
169
|
+
react_1.default.createElement(PhoneBody, { width: phoneW, height: phoneH }))));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* PanPhoneGraphic — a phone outline (landscape for Mode-A `down`, portrait
|
|
173
|
+
* for Mode-B `right`) with an amber sweep band that travels across the pan
|
|
174
|
+
* axis on a loop, demonstrating the camera sweep. The band fades in/out at
|
|
175
|
+
* the travel ends so the loop reset is invisible. Replaces
|
|
176
|
+
* `pan-capture.gif`. The bouncing direction arrow stays in PanHowToOverlay.
|
|
177
|
+
*/
|
|
178
|
+
function PanPhoneGraphic({ direction, size = DEFAULT_SIZE, playing = true, style, }) {
|
|
179
|
+
// One value loops 0→1; drives the phone's travel + perspective tilt
|
|
180
|
+
// together so the device reads as ROTATING as it sweeps along the arrow.
|
|
181
|
+
const t = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
182
|
+
(0, react_1.useEffect)(() => {
|
|
183
|
+
if (!playing) {
|
|
184
|
+
t.setValue(0);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const loop = react_native_1.Animated.loop(react_native_1.Animated.timing(t, {
|
|
188
|
+
toValue: 1,
|
|
189
|
+
duration: 1900,
|
|
190
|
+
easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
|
|
191
|
+
useNativeDriver: true,
|
|
192
|
+
}));
|
|
193
|
+
loop.start();
|
|
194
|
+
return () => loop.stop();
|
|
195
|
+
}, [playing, t, direction]);
|
|
196
|
+
const down = direction === 'down';
|
|
197
|
+
// Mode A (down) holds the phone LANDSCAPE; Mode B (right) PORTRAIT.
|
|
198
|
+
const phoneW = down ? size * 0.5 : size * 0.34;
|
|
199
|
+
const phoneH = down ? size * 0.34 : size * 0.5;
|
|
200
|
+
// Travel ± along the pan axis (down → +Y, right → +X), kept in-canvas.
|
|
201
|
+
const amp = size * 0.2;
|
|
202
|
+
const translate = t.interpolate({
|
|
203
|
+
inputRange: [0, 1],
|
|
204
|
+
outputRange: [-amp, amp],
|
|
205
|
+
});
|
|
206
|
+
// The device TILTS through the sweep — rotating about the cross-pan axis
|
|
207
|
+
// as it pans — which is the 3D "the phone is turning" read the flat
|
|
208
|
+
// band lacked. rotateX for a vertical (down) pan, rotateY for horizontal.
|
|
209
|
+
// The horizontal (right) tilt is INVERTED vs the vertical one so the edge
|
|
210
|
+
// on the side the phone is currently on reads LONGER (convex toward the
|
|
211
|
+
// viewer) — matched to on-device feedback for the portrait Mode-B pan.
|
|
212
|
+
const tilt = t.interpolate({
|
|
213
|
+
inputRange: [0, 1],
|
|
214
|
+
outputRange: down ? ['-24deg', '24deg'] : ['24deg', '-24deg'],
|
|
215
|
+
});
|
|
216
|
+
// Fade at the travel ends so the loop's restart is invisible.
|
|
217
|
+
const opacity = t.interpolate({
|
|
218
|
+
inputRange: [0, 0.15, 0.85, 1],
|
|
219
|
+
outputRange: [0, 1, 1, 0],
|
|
220
|
+
});
|
|
221
|
+
// `perspective` makes the rotateX/rotateY read as depth (a turning
|
|
222
|
+
// device), not a flat vertical squash.
|
|
223
|
+
const transform = down
|
|
224
|
+
? [{ perspective: 800 }, { translateY: translate }, { rotateX: tilt }]
|
|
225
|
+
: [{ perspective: 800 }, { translateX: translate }, { rotateY: tilt }];
|
|
226
|
+
return (react_1.default.createElement(react_native_1.View, { style: [{ width: size, height: size }, styles.center, style], pointerEvents: "none" },
|
|
227
|
+
react_1.default.createElement(react_native_1.Animated.View, { style: { opacity, transform } },
|
|
228
|
+
react_1.default.createElement(PhoneBody, { width: phoneW, height: phoneH },
|
|
229
|
+
react_1.default.createElement(react_native_1.View, { style: styles.screenGlow, pointerEvents: "none" })))));
|
|
230
|
+
}
|
|
231
|
+
const styles = react_native_1.StyleSheet.create({
|
|
232
|
+
center: { alignItems: 'center', justifyContent: 'center' },
|
|
233
|
+
phoneBody: {
|
|
234
|
+
alignItems: 'center',
|
|
235
|
+
justifyContent: 'center',
|
|
236
|
+
backgroundColor: 'transparent',
|
|
237
|
+
},
|
|
238
|
+
cameraDot: {
|
|
239
|
+
position: 'absolute',
|
|
240
|
+
backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.white,
|
|
241
|
+
},
|
|
242
|
+
ring: {
|
|
243
|
+
position: 'absolute',
|
|
244
|
+
borderWidth: 1.5,
|
|
245
|
+
opacity: 0.28,
|
|
246
|
+
backgroundColor: 'transparent',
|
|
247
|
+
},
|
|
248
|
+
// Amber CSS-triangle arrowhead at the top of the ring. The base props are
|
|
249
|
+
// shared; the direction-specific style colours the trailing border so the
|
|
250
|
+
// apex points along the rotation tangent.
|
|
251
|
+
arrowHead: {
|
|
252
|
+
position: 'absolute',
|
|
253
|
+
width: 0,
|
|
254
|
+
height: 0,
|
|
255
|
+
borderTopWidth: 6,
|
|
256
|
+
borderBottomWidth: 6,
|
|
257
|
+
borderTopColor: 'transparent',
|
|
258
|
+
borderBottomColor: 'transparent',
|
|
259
|
+
},
|
|
260
|
+
// Points LEFT (anticlockwise / to-landscape): RIGHT border amber.
|
|
261
|
+
arrowHeadLeft: {
|
|
262
|
+
borderRightWidth: 10,
|
|
263
|
+
borderRightColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
|
|
264
|
+
},
|
|
265
|
+
// Points RIGHT (clockwise / to-portrait): LEFT border amber.
|
|
266
|
+
arrowHeadRight: {
|
|
267
|
+
borderLeftWidth: 10,
|
|
268
|
+
borderLeftColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
|
|
269
|
+
},
|
|
270
|
+
// Faint amber fill inside the phone outline — a hint of the live
|
|
271
|
+
// preview so the turning device reads as a screen, not an empty frame.
|
|
272
|
+
screenGlow: {
|
|
273
|
+
width: '78%',
|
|
274
|
+
height: '70%',
|
|
275
|
+
borderRadius: 6,
|
|
276
|
+
backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
|
|
277
|
+
opacity: 0.14,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
//# sourceMappingURL=guidanceGraphics.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guidanceTokens — the single source of truth for the panorama capture
|
|
3
|
+
* GUIDANCE visual language (rotate prompt, pan how-to, countdown, too-fast
|
|
4
|
+
* pill, lateral popup). Values are taken verbatim from the design handoff
|
|
5
|
+
* ("Camera Capture Guides") so every guidance surface shares exact styling
|
|
6
|
+
* instead of re-declaring colors per component.
|
|
7
|
+
*
|
|
8
|
+
* The two looping device-motion graphics are drawn programmatically (see
|
|
9
|
+
* ./guidanceGraphics — pure RN View + Animated, no image assets); these
|
|
10
|
+
* tokens cover both those graphics and the code-built chrome around them.
|
|
11
|
+
*/
|
|
12
|
+
export declare const GUIDANCE_TOKENS: {
|
|
13
|
+
/** Device outline, caption text, countdown number. */
|
|
14
|
+
readonly white: "#FFFFFF";
|
|
15
|
+
/** Rotation ring/arrow, pan guide line, dots, glow — the one accent. */
|
|
16
|
+
readonly amber: "#FFC462";
|
|
17
|
+
/** Caption-pill / popup background scrim. */
|
|
18
|
+
readonly scrim: "rgba(0,0,0,0.42)";
|
|
19
|
+
/** Pill hairline border. */
|
|
20
|
+
readonly hairline: "rgba(255,255,255,0.16)";
|
|
21
|
+
/** On-screen size (px square) of the rotate / pan guidance graphics. */
|
|
22
|
+
readonly graphicSize: 240;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Caption-pill spec (item 2 "Rotate to landscape" + reused by the too-fast
|
|
26
|
+
* pill): full pill, scrim bg, hairline border, amber leading dot, white
|
|
27
|
+
* 13px/600 text.
|
|
28
|
+
*/
|
|
29
|
+
export declare const GUIDANCE_PILL: {
|
|
30
|
+
readonly paddingVertical: 8;
|
|
31
|
+
readonly paddingHorizontal: 15;
|
|
32
|
+
readonly borderRadius: 999;
|
|
33
|
+
readonly dotSize: 6;
|
|
34
|
+
readonly dotGap: 7;
|
|
35
|
+
readonly fontSize: 13;
|
|
36
|
+
readonly fontWeight: "600";
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Countdown spec (item 5): amber dot + glow, white 30px/700 tabular-nums
|
|
40
|
+
* number, whole timer blinks opacity 0.18↔1 over a 1s ease-in-out cycle.
|
|
41
|
+
*/
|
|
42
|
+
export declare const GUIDANCE_COUNTDOWN: {
|
|
43
|
+
readonly dotSize: 9;
|
|
44
|
+
readonly dotGap: 8;
|
|
45
|
+
readonly dotGlow: "rgba(255,196,98,0.85)";
|
|
46
|
+
readonly fontSize: 30;
|
|
47
|
+
readonly fontWeight: "700";
|
|
48
|
+
readonly blinkMinOpacity: 0.18;
|
|
49
|
+
readonly blinkMaxOpacity: 1;
|
|
50
|
+
readonly blinkPeriodMs: 1000;
|
|
51
|
+
/** top/left inset from the (user-perceived) corner. */
|
|
52
|
+
readonly inset: 16;
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=guidanceTokens.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* guidanceTokens — the single source of truth for the panorama capture
|
|
5
|
+
* GUIDANCE visual language (rotate prompt, pan how-to, countdown, too-fast
|
|
6
|
+
* pill, lateral popup). Values are taken verbatim from the design handoff
|
|
7
|
+
* ("Camera Capture Guides") so every guidance surface shares exact styling
|
|
8
|
+
* instead of re-declaring colors per component.
|
|
9
|
+
*
|
|
10
|
+
* The two looping device-motion graphics are drawn programmatically (see
|
|
11
|
+
* ./guidanceGraphics — pure RN View + Animated, no image assets); these
|
|
12
|
+
* tokens cover both those graphics and the code-built chrome around them.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.GUIDANCE_COUNTDOWN = exports.GUIDANCE_PILL = exports.GUIDANCE_TOKENS = void 0;
|
|
16
|
+
exports.GUIDANCE_TOKENS = {
|
|
17
|
+
/** Device outline, caption text, countdown number. */
|
|
18
|
+
white: '#FFFFFF',
|
|
19
|
+
/** Rotation ring/arrow, pan guide line, dots, glow — the one accent. */
|
|
20
|
+
amber: '#FFC462',
|
|
21
|
+
/** Caption-pill / popup background scrim. */
|
|
22
|
+
scrim: 'rgba(0,0,0,0.42)',
|
|
23
|
+
/** Pill hairline border. */
|
|
24
|
+
hairline: 'rgba(255,255,255,0.16)',
|
|
25
|
+
/** On-screen size (px square) of the rotate / pan guidance graphics. */
|
|
26
|
+
graphicSize: 240,
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Caption-pill spec (item 2 "Rotate to landscape" + reused by the too-fast
|
|
30
|
+
* pill): full pill, scrim bg, hairline border, amber leading dot, white
|
|
31
|
+
* 13px/600 text.
|
|
32
|
+
*/
|
|
33
|
+
exports.GUIDANCE_PILL = {
|
|
34
|
+
paddingVertical: 8,
|
|
35
|
+
paddingHorizontal: 15,
|
|
36
|
+
borderRadius: 999,
|
|
37
|
+
dotSize: 6,
|
|
38
|
+
dotGap: 7,
|
|
39
|
+
fontSize: 13,
|
|
40
|
+
fontWeight: '600',
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Countdown spec (item 5): amber dot + glow, white 30px/700 tabular-nums
|
|
44
|
+
* number, whole timer blinks opacity 0.18↔1 over a 1s ease-in-out cycle.
|
|
45
|
+
*/
|
|
46
|
+
exports.GUIDANCE_COUNTDOWN = {
|
|
47
|
+
dotSize: 9,
|
|
48
|
+
dotGap: 8,
|
|
49
|
+
dotGlow: 'rgba(255,196,98,0.85)',
|
|
50
|
+
fontSize: 30,
|
|
51
|
+
fontWeight: '700',
|
|
52
|
+
blinkMinOpacity: 0.18,
|
|
53
|
+
blinkMaxOpacity: 1,
|
|
54
|
+
blinkPeriodMs: 1000,
|
|
55
|
+
/** top/left inset from the (user-perceived) corner. */
|
|
56
|
+
inset: 16,
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=guidanceTokens.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* panModeGate — pure decision helper for the first-time-user "rotate the
|
|
3
|
+
* device" gate (guidance item 1).
|
|
4
|
+
*
|
|
5
|
+
* The non-AR panorama flow has two pan directions:
|
|
6
|
+
*
|
|
7
|
+
* - **vertical** — the user holds the phone LANDSCAPE and pans the camera
|
|
8
|
+
* TOP → BOTTOM down a tall fixture. Both `landscape-left` and
|
|
9
|
+
* `landscape-right` are valid holds.
|
|
10
|
+
* - **horizontal** — the user holds the phone PORTRAIT and pans LEFT →
|
|
11
|
+
* RIGHT across a wide scene. Both portrait holds are valid.
|
|
12
|
+
*
|
|
13
|
+
* A host restricts capture via the `panMode` flag:
|
|
14
|
+
* - `'vertical'` → landscape-only; a PORTRAIT hold is gated (rotate to
|
|
15
|
+
* landscape).
|
|
16
|
+
* - `'horizontal'` → portrait-only; a LANDSCAPE hold is gated (rotate to
|
|
17
|
+
* portrait).
|
|
18
|
+
* - `'both'` → either; the gate never fires.
|
|
19
|
+
*
|
|
20
|
+
* When the gate fires the host must NOT start the capture — it shows the
|
|
21
|
+
* rotate prompt (guidance item 2, pointing at the target orientation) and
|
|
22
|
+
* waits for the user to rotate.
|
|
23
|
+
*
|
|
24
|
+
* This module is the single pure predicate for that decision: no React, no
|
|
25
|
+
* sensors, no side effects, so the gate logic is unit-testable in the node
|
|
26
|
+
* jest env without booting a render or mocking the accelerometer.
|
|
27
|
+
*/
|
|
28
|
+
import type { DeviceOrientation } from './useDeviceOrientation';
|
|
29
|
+
/**
|
|
30
|
+
* Which device holds the panorama capture accepts.
|
|
31
|
+
*
|
|
32
|
+
* - `'vertical'` — LANDSCAPE only (top→bottom pan; the product default).
|
|
33
|
+
* Portrait holds are gated behind the rotate-to-landscape prompt.
|
|
34
|
+
* - `'horizontal'` — PORTRAIT only (left→right pan). Landscape holds are
|
|
35
|
+
* gated behind the rotate-to-portrait prompt.
|
|
36
|
+
* - `'both'` — LANDSCAPE or PORTRAIT; the gate never fires, the user
|
|
37
|
+
* captures in whichever hold they're already in.
|
|
38
|
+
*/
|
|
39
|
+
export type PanMode = 'vertical' | 'horizontal' | 'both';
|
|
40
|
+
/**
|
|
41
|
+
* True when the caller must BLOCK capture-start and show the rotate prompt
|
|
42
|
+
* for the current device hold:
|
|
43
|
+
* - `'vertical'` gates a PORTRAIT hold (needs landscape).
|
|
44
|
+
* - `'horizontal'` gates a LANDSCAPE hold (needs portrait).
|
|
45
|
+
* - `'both'` never gates.
|
|
46
|
+
*/
|
|
47
|
+
export declare function shouldGateForPanMode(panMode: PanMode, orientation: DeviceOrientation): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The orientation the user must rotate TO when a hold is gated, used to pick
|
|
50
|
+
* the rotate prompt's copy + graphic. `'vertical'` wants landscape,
|
|
51
|
+
* `'horizontal'` wants portrait; `'both'` never gates so returns `null`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function gateTargetOrientation(panMode: PanMode): 'landscape' | 'portrait' | null;
|
|
54
|
+
//# sourceMappingURL=panModeGate.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* panModeGate — pure decision helper for the first-time-user "rotate the
|
|
5
|
+
* device" gate (guidance item 1).
|
|
6
|
+
*
|
|
7
|
+
* The non-AR panorama flow has two pan directions:
|
|
8
|
+
*
|
|
9
|
+
* - **vertical** — the user holds the phone LANDSCAPE and pans the camera
|
|
10
|
+
* TOP → BOTTOM down a tall fixture. Both `landscape-left` and
|
|
11
|
+
* `landscape-right` are valid holds.
|
|
12
|
+
* - **horizontal** — the user holds the phone PORTRAIT and pans LEFT →
|
|
13
|
+
* RIGHT across a wide scene. Both portrait holds are valid.
|
|
14
|
+
*
|
|
15
|
+
* A host restricts capture via the `panMode` flag:
|
|
16
|
+
* - `'vertical'` → landscape-only; a PORTRAIT hold is gated (rotate to
|
|
17
|
+
* landscape).
|
|
18
|
+
* - `'horizontal'` → portrait-only; a LANDSCAPE hold is gated (rotate to
|
|
19
|
+
* portrait).
|
|
20
|
+
* - `'both'` → either; the gate never fires.
|
|
21
|
+
*
|
|
22
|
+
* When the gate fires the host must NOT start the capture — it shows the
|
|
23
|
+
* rotate prompt (guidance item 2, pointing at the target orientation) and
|
|
24
|
+
* waits for the user to rotate.
|
|
25
|
+
*
|
|
26
|
+
* This module is the single pure predicate for that decision: no React, no
|
|
27
|
+
* sensors, no side effects, so the gate logic is unit-testable in the node
|
|
28
|
+
* jest env without booting a render or mocking the accelerometer.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.shouldGateForPanMode = shouldGateForPanMode;
|
|
32
|
+
exports.gateTargetOrientation = gateTargetOrientation;
|
|
33
|
+
function isPortrait(orientation) {
|
|
34
|
+
return (orientation === 'portrait' || orientation === 'portrait-upside-down');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* True when the caller must BLOCK capture-start and show the rotate prompt
|
|
38
|
+
* for the current device hold:
|
|
39
|
+
* - `'vertical'` gates a PORTRAIT hold (needs landscape).
|
|
40
|
+
* - `'horizontal'` gates a LANDSCAPE hold (needs portrait).
|
|
41
|
+
* - `'both'` never gates.
|
|
42
|
+
*/
|
|
43
|
+
function shouldGateForPanMode(panMode, orientation) {
|
|
44
|
+
if (panMode === 'vertical')
|
|
45
|
+
return isPortrait(orientation);
|
|
46
|
+
if (panMode === 'horizontal')
|
|
47
|
+
return !isPortrait(orientation);
|
|
48
|
+
return false; // 'both'
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The orientation the user must rotate TO when a hold is gated, used to pick
|
|
52
|
+
* the rotate prompt's copy + graphic. `'vertical'` wants landscape,
|
|
53
|
+
* `'horizontal'` wants portrait; `'both'` never gates so returns `null`.
|
|
54
|
+
*/
|
|
55
|
+
function gateTargetOrientation(panMode) {
|
|
56
|
+
if (panMode === 'vertical')
|
|
57
|
+
return 'landscape';
|
|
58
|
+
if (panMode === 'horizontal')
|
|
59
|
+
return 'portrait';
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=panModeGate.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pickCaptureFormat — choose the vision-camera format for the capture stream.
|
|
3
|
+
*
|
|
4
|
+
* Replaces a plain `useCameraFormat([{ videoResolution: 'max' }, …])`, which
|
|
5
|
+
* picks the device's MAX-video format and lets the PHOTO resolution ride
|
|
6
|
+
* along — on the iPhone 16 Pro ultra-wide that pairs a **48 MP** still
|
|
7
|
+
* (8064×6048) with the 4032×3024 max-video format, so a tap photo came out
|
|
8
|
+
* ~6000 px. vision-camera 4.x exposes each format's photo/video resolution
|
|
9
|
+
* but NOT its pixel format / bit-depth, so we can't filter for 8-bit; the
|
|
10
|
+
* empirical rule is that the device's MAX 4:3 video format is 8-bit (the
|
|
11
|
+
* frame processor needs 8-bit for non-AR stitching), and lower video
|
|
12
|
+
* resolutions risk 10-bit.
|
|
13
|
+
*
|
|
14
|
+
* Strategy: among the ~4:3 formats whose photo long-edge is within
|
|
15
|
+
* `maxPhotoLongEdge`, pick the one with the HIGHEST video resolution (keeps
|
|
16
|
+
* the preview/stitch stream as sharp as possible while bounding the still),
|
|
17
|
+
* tie-breaking on higher fps, then the largest photo under the cap, then
|
|
18
|
+
* non-HDR (a hedge toward 8-bit). If NO format fits the cap, fall back to
|
|
19
|
+
* the overall max-video format (never returns nothing for a non-empty list).
|
|
20
|
+
*
|
|
21
|
+
* Verified against the real iPhone 16 Pro ultra-wide format list (see the
|
|
22
|
+
* unit test): cap 4032 → 4032×3024 photo (12 MP) + 3264×2448 video (was
|
|
23
|
+
* 8064×6048 photo); cap 2048 → 2016×1512 photo (3 MP) + 1920×1440 video.
|
|
24
|
+
*
|
|
25
|
+
* Pure + structurally-typed (no vision-camera import) so it unit-tests in the
|
|
26
|
+
* node jest env; `CameraDeviceFormat` is structurally assignable to
|
|
27
|
+
* `FormatLike`.
|
|
28
|
+
*/
|
|
29
|
+
/** The CameraDeviceFormat fields this picker reads. */
|
|
30
|
+
export interface FormatLike {
|
|
31
|
+
photoWidth: number;
|
|
32
|
+
photoHeight: number;
|
|
33
|
+
videoWidth: number;
|
|
34
|
+
videoHeight: number;
|
|
35
|
+
maxFps: number;
|
|
36
|
+
supportsVideoHdr: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface PickFormatOptions {
|
|
39
|
+
/**
|
|
40
|
+
* Cap on the chosen format's photo LONG edge, in px. The picker prefers
|
|
41
|
+
* the sharpest-video format whose photo fits this. `0` disables the cap
|
|
42
|
+
* (reverts to pure max-video). Default 4032 (≈12 MP at 4:3, "4K"-ish).
|
|
43
|
+
*/
|
|
44
|
+
maxPhotoLongEdge?: number;
|
|
45
|
+
/** Target capture aspect (W/H in landscape). Default 4/3. */
|
|
46
|
+
aspect?: number;
|
|
47
|
+
/** Aspect match tolerance. Default 0.05. */
|
|
48
|
+
aspectTolerance?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Prefer a SMOOTH (high-fps) preview over the sharpest video format. Off by
|
|
51
|
+
* default → max-video-resolution-first (back-compat). On (the panorama
|
|
52
|
+
* camera opts in) → rank by frame rate up to `fpsTarget` first, THEN video
|
|
53
|
+
* resolution. The default video-first sort picks e.g. a 3264×2448 **@30 fps**
|
|
54
|
+
* format over a 1920×1440 **@60 fps** one, halving the preview frame rate —
|
|
55
|
+
* visible as jitter while panning. The stitch clamps keyframes to 640/1280 px
|
|
56
|
+
* anyway, so the higher video resolution buys nothing for the panorama; a
|
|
57
|
+
* 60 fps stream just looks smooth.
|
|
58
|
+
*/
|
|
59
|
+
preferHighFps?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Ceiling for the fps preference when `preferHighFps` is on. Formats at or
|
|
62
|
+
* above this are treated as equally smooth (so resolution breaks the tie
|
|
63
|
+
* instead of chasing 120 fps at a lower resolution). Default 60.
|
|
64
|
+
*/
|
|
65
|
+
fpsTarget?: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Pick the best capture format, or `undefined` for an empty list.
|
|
69
|
+
*/
|
|
70
|
+
export declare function pickCaptureFormat<F extends FormatLike>(formats: readonly F[], opts?: PickFormatOptions): F | undefined;
|
|
71
|
+
//# sourceMappingURL=pickCaptureFormat.d.ts.map
|