fix: Represent neutralZoom in factor instead of percentage (#179)
* Use factor instead of percent for `neutralZoom` * fix zoom calculation * Update CameraPage.tsx
This commit is contained in:
parent
8aec647acd
commit
555474be7d
@ -173,7 +173,7 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
|
|||||||
map.putDouble("minZoom", 1.0)
|
map.putDouble("minZoom", 1.0)
|
||||||
map.putDouble("maxZoom", maxScalerZoom.toDouble())
|
map.putDouble("maxZoom", maxScalerZoom.toDouble())
|
||||||
}
|
}
|
||||||
map.putDouble("neutralZoom", characteristics.neutralZoomPercent.toDouble())
|
map.putDouble("neutralZoom", 1.0)
|
||||||
|
|
||||||
// TODO: Optimize?
|
// TODO: Optimize?
|
||||||
val maxImageOutputSize = cameraConfig.outputFormats
|
val maxImageOutputSize = cameraConfig.outputFormats
|
||||||
|
@ -56,25 +56,3 @@ fun CameraCharacteristics.getFieldOfView(): Double {
|
|||||||
|
|
||||||
return 2 * atan(sensorSize.bigger / (focalLengths[0] * 2)) * (180 / PI)
|
return 2 * atan(sensorSize.bigger / (focalLengths[0] * 2)) * (180 / PI)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CameraCharacteristics.supportsFps(fps: Int): Boolean {
|
|
||||||
return this.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)!!
|
|
||||||
.any { it.upper >= fps && it.lower <= fps }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the value at which the Zoom is at neutral state (wide-angle camera zoom 0) (in percent, between 0.0-1.0)
|
|
||||||
*
|
|
||||||
* * On single-camera physical devices this value will always be 0
|
|
||||||
* * On devices with multiple cameras, e.g. triple-camera, this value will be a value between 0.0 and 1.0, where the field-of-view and zoom looks "neutral"
|
|
||||||
*/
|
|
||||||
val CameraCharacteristics.neutralZoomPercent: Float
|
|
||||||
get() {
|
|
||||||
val zoomRange = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R)
|
|
||||||
this.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
|
|
||||||
else null
|
|
||||||
return if (zoomRange != null)
|
|
||||||
((1.0f - zoomRange.lower) / (zoomRange.upper - zoomRange.lower))
|
|
||||||
else
|
|
||||||
0.0f
|
|
||||||
}
|
|
||||||
|
@ -95,18 +95,23 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
}, [formats, fps, enableHdr]);
|
}, [formats, fps, enableHdr]);
|
||||||
|
|
||||||
//#region Animated Zoom
|
//#region Animated Zoom
|
||||||
const formatMaxZoom = format?.maxZoom ?? 1;
|
// This just maps the zoom factor to a percentage value.
|
||||||
const maxZoomFactor = Math.min(formatMaxZoom, MAX_ZOOM_FACTOR);
|
// so e.g. for [min, neutr., max] values [1, 2, 128] this would result in [0, 0.0081, 1]
|
||||||
const neutralZoom = device?.neutralZoom ?? 0;
|
const minZoomFactor = device?.minZoom ?? 1;
|
||||||
const neutralZoomScaled = (neutralZoom / maxZoomFactor) * formatMaxZoom;
|
const neutralZoomFactor = device?.neutralZoom ?? 1;
|
||||||
const maxZoomScaled = (1 / formatMaxZoom) * maxZoomFactor;
|
const maxZoomFactor = device?.maxZoom ?? 1;
|
||||||
|
const maxZoomFactorClamped = Math.min(maxZoomFactor, MAX_ZOOM_FACTOR);
|
||||||
|
|
||||||
const cameraAnimatedProps = useAnimatedProps(
|
const neutralZoomOut = (neutralZoomFactor - minZoomFactor) / (maxZoomFactor - minZoomFactor);
|
||||||
() => ({
|
const neutralZoomIn = (neutralZoomOut / maxZoomFactorClamped) * maxZoomFactor;
|
||||||
zoom: interpolate(zoom.value, [0, neutralZoomScaled, 1], [0, neutralZoom, maxZoomScaled], Extrapolate.CLAMP),
|
const maxZoomOut = maxZoomFactorClamped / maxZoomFactor;
|
||||||
}),
|
|
||||||
[maxZoomScaled, neutralZoom, neutralZoomScaled, zoom],
|
const cameraAnimatedProps = useAnimatedProps(() => {
|
||||||
);
|
const z = interpolate(zoom.value, [0, neutralZoomIn, 1], [0, neutralZoomOut, maxZoomOut], Extrapolate.CLAMP);
|
||||||
|
return {
|
||||||
|
zoom: isNaN(z) ? 0 : z,
|
||||||
|
};
|
||||||
|
}, [maxZoomOut, neutralZoomOut, neutralZoomIn, zoom]);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Callbacks
|
//#region Callbacks
|
||||||
@ -155,8 +160,8 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
//#region Effects
|
//#region Effects
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Run everytime the neutralZoomScaled value changes. (reset zoom when device changes)
|
// Run everytime the neutralZoomScaled value changes. (reset zoom when device changes)
|
||||||
zoom.value = neutralZoomScaled;
|
zoom.value = neutralZoomIn;
|
||||||
}, [neutralZoomScaled, zoom]);
|
}, [neutralZoomIn, zoom]);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Pinch to Zoom Gesture
|
//#region Pinch to Zoom Gesture
|
||||||
|
@ -68,7 +68,6 @@ extension CameraView {
|
|||||||
guard let videoDevice = AVCaptureDevice(uniqueID: cameraId) else {
|
guard let videoDevice = AVCaptureDevice(uniqueID: cameraId) else {
|
||||||
return invokeOnError(.device(.invalid))
|
return invokeOnError(.device(.invalid))
|
||||||
}
|
}
|
||||||
zoom = NSNumber(value: Double(videoDevice.neutralZoomPercent))
|
|
||||||
videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
|
videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
|
||||||
guard captureSession.canAddInput(videoDeviceInput!) else {
|
guard captureSession.canAddInput(videoDeviceInput!) else {
|
||||||
return invokeOnError(.parameter(.unsupportedInput(inputDescriptor: "video-input")))
|
return invokeOnError(.parameter(.unsupportedInput(inputDescriptor: "video-input")))
|
||||||
|
@ -96,8 +96,8 @@ final class CameraViewManager: RCTViewManager {
|
|||||||
"hasFlash": $0.hasFlash,
|
"hasFlash": $0.hasFlash,
|
||||||
"hasTorch": $0.hasTorch,
|
"hasTorch": $0.hasTorch,
|
||||||
"minZoom": $0.minAvailableVideoZoomFactor,
|
"minZoom": $0.minAvailableVideoZoomFactor,
|
||||||
|
"neutralZoom": $0.neutralZoomFactor,
|
||||||
"maxZoom": $0.maxAvailableVideoZoomFactor,
|
"maxZoom": $0.maxAvailableVideoZoomFactor,
|
||||||
"neutralZoom": $0.neutralZoomPercent,
|
|
||||||
"isMultiCam": $0.isMultiCam,
|
"isMultiCam": $0.isMultiCam,
|
||||||
"supportsDepthCapture": false, // TODO: supportsDepthCapture
|
"supportsDepthCapture": false, // TODO: supportsDepthCapture
|
||||||
"supportsRawCapture": false, // TODO: supportsRawCapture
|
"supportsRawCapture": false, // TODO: supportsRawCapture
|
||||||
|
@ -9,6 +9,12 @@
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
extension AVCaptureDevice {
|
extension AVCaptureDevice {
|
||||||
|
/**
|
||||||
|
Get the value at which the Zoom factor is neutral.
|
||||||
|
|
||||||
|
For normal wide-angle devices, this is always going to be 1.0, since this is the default scale.
|
||||||
|
For devices with an ultra-wide-angle camera, this value is going to be the value where the wide-angle device will switch over.
|
||||||
|
*/
|
||||||
var neutralZoomFactor: CGFloat {
|
var neutralZoomFactor: CGFloat {
|
||||||
if #available(iOS 13.0, *) {
|
if #available(iOS 13.0, *) {
|
||||||
if let indexOfWideAngle = self.constituentDevices.firstIndex(where: { $0.deviceType == .builtInWideAngleCamera }) {
|
if let indexOfWideAngle = self.constituentDevices.firstIndex(where: { $0.deviceType == .builtInWideAngleCamera }) {
|
||||||
@ -19,14 +25,4 @@ extension AVCaptureDevice {
|
|||||||
}
|
}
|
||||||
return 1.0
|
return 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
Get the value at which the Zoom value is neutral, in percent (0.0-1.0)
|
|
||||||
|
|
||||||
* On single-camera physical devices, this value will always be 0.0
|
|
||||||
* On devices with multiple cameras, e.g. triple-camera, this value will be a value between 0.0 and 1.0, where the field-of-view and zoom looks "neutral"
|
|
||||||
*/
|
|
||||||
var neutralZoomPercent: CGFloat {
|
|
||||||
return (neutralZoomFactor - minAvailableVideoZoomFactor) / (maxAvailableVideoZoomFactor - minAvailableVideoZoomFactor)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -228,12 +228,20 @@ export interface CameraDevice {
|
|||||||
*/
|
*/
|
||||||
maxZoom: number;
|
maxZoom: number;
|
||||||
/**
|
/**
|
||||||
* The zoom percentage (`0.0`-`1.0`) where the camera is "neutral".
|
* The zoom factor where the camera is "neutral".
|
||||||
*
|
*
|
||||||
* * For single-physical cameras this property is always `0.0`.
|
* * For single-physical cameras this property is always `1.0`.
|
||||||
* * For multi cameras this property is a value between `0.0` and `1.0`, where the camera is in wide-angle mode and hasn't switched to the ultra-wide (`0.5`x zoom) or telephoto camera yet.
|
* * For multi cameras this property is a value between `minZoom` and `maxZoom`, where the camera is in _wide-angle_ mode and hasn't switched to the _ultra-wide-angle_ (`0.5`x zoom) or telephoto camera yet.
|
||||||
*
|
*
|
||||||
* Use this value as an initial value for the zoom property if you implement custom zoom. (e.g. reanimated shared value should be initially set to this value)
|
* Use this value as an initial value for the zoom property if you implement custom zoom. (e.g. reanimated shared value should be initially set to this value)
|
||||||
|
* @example
|
||||||
|
* const device = ...
|
||||||
|
*
|
||||||
|
* const neutralZoomPercent = (device.neutralZoom - device.minZoom) / (device.maxZoom - device.minZoom)
|
||||||
|
* const zoomFactor = useSharedValue(neutralZoomPercent) // <-- initial value so it doesn't start at ultra-wide
|
||||||
|
* const cameraProps = useAnimatedProps(() => ({
|
||||||
|
* zoom: zoomFactor.value
|
||||||
|
* }))
|
||||||
*/
|
*/
|
||||||
neutralZoom: number;
|
neutralZoom: number;
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user