feat: BREAKING CHANGE: Express zoom
factor always in actual factor value (1, 2, 128, ...) instead of 0.0-1.0 scale (#306)
* Make `zoom` go on "factor" scale * Clean up `zoom` code * fix float conversion * fix zoom interpretation * Update docs for new zoom scale * fix float conversion
This commit is contained in:
parent
6e2dc4e73b
commit
445af943c3
@ -82,7 +82,7 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
|
|||||||
// other props
|
// other props
|
||||||
var isActive = false
|
var isActive = false
|
||||||
var torch = "off"
|
var torch = "off"
|
||||||
var zoom = 0.0 // in percent
|
var zoom: Float = 1f // in "factor"
|
||||||
var enableZoomGesture = false
|
var enableZoomGesture = false
|
||||||
var frameProcessorFps = 1.0
|
var frameProcessorFps = 1.0
|
||||||
|
|
||||||
@ -162,7 +162,7 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
|
|||||||
|
|
||||||
scaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
scaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||||
zoom = min(max(((zoom + 1) * detector.scaleFactor) - 1, 0.0), 1.0)
|
zoom = max(min((zoom * detector.scaleFactor), maxZoom), minZoom)
|
||||||
update(arrayListOfZoom)
|
update(arrayListOfZoom)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -275,8 +275,8 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
|
|||||||
configureSession()
|
configureSession()
|
||||||
}
|
}
|
||||||
if (shouldReconfigureZoom) {
|
if (shouldReconfigureZoom) {
|
||||||
val scaled = (zoom.toFloat() * (maxZoom - minZoom)) + minZoom
|
val zoomClamped = max(min(zoom.toFloat(), maxZoom), minZoom)
|
||||||
camera!!.cameraControl.setZoomRatio(scaled)
|
camera!!.cameraControl.setZoomRatio(zoomClamped)
|
||||||
}
|
}
|
||||||
if (shouldReconfigureTorch) {
|
if (shouldReconfigureTorch) {
|
||||||
camera!!.cameraControl.enableTorch(torch == "on")
|
camera!!.cameraControl.enableTorch(torch == "on")
|
||||||
|
@ -131,9 +131,10 @@ class CameraViewManager : SimpleViewManager<CameraView>() {
|
|||||||
|
|
||||||
@ReactProp(name = "zoom")
|
@ReactProp(name = "zoom")
|
||||||
fun setZoom(view: CameraView, zoom: Double) {
|
fun setZoom(view: CameraView, zoom: Double) {
|
||||||
if (view.zoom != zoom)
|
val zoomFloat = zoom.toFloat()
|
||||||
|
if (view.zoom != zoomFloat)
|
||||||
addChangedPropToTransaction(view, "zoom")
|
addChangedPropToTransaction(view, "zoom")
|
||||||
view.zoom = zoom
|
view.zoom = zoomFloat
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = "enableZoomGesture")
|
@ReactProp(name = "enableZoomGesture")
|
||||||
|
@ -90,9 +90,11 @@ A Camera device has different minimum, maximum and neutral zoom values. Those va
|
|||||||
<img src="https://developer.android.com/images/training/camera/multi-camera-4.gif" width="45%" />
|
<img src="https://developer.android.com/images/training/camera/multi-camera-4.gif" width="45%" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
The Camera's `zoom` property expects values to be in the same "factor" scale as the `minZoom`, `neutralZoom` and `maxZoom` values - so if you pass `zoom={device.minZoom}` it is at the minimum available zoom, where as if you pass `zoom={device.maxZoom}` the maximum zoom value possible is zoomed in. It is recommended that you start at `device.neutralZoom` and let the user manually zoom out to the fish-eye camera on demand (if available).
|
||||||
|
|
||||||
### Logarithmic scale
|
### Logarithmic scale
|
||||||
|
|
||||||
A Camera's `zoom` property is represented in a **logarithmic scale**. That means, increasing from `0` to `0.1` will appear to be a much larger offset than increasing from `0.9` to `1`. If you want to implement a zoom gesture (`<PinchGestureHandler>`, `<PanGestureHandler>`), try to flatten the `zoom` property to a **linear scale** by raising it **exponentially**. (`zoom.value ** 2`)
|
A Camera's `zoom` property is represented in a **logarithmic scale**. That means, increasing from `1` to `2` will appear to be a much larger offset than increasing from `127` to `128`. If you want to implement a zoom gesture (`<PinchGestureHandler>`, `<PanGestureHandler>`), try to flatten the `zoom` property to a **linear scale** by raising it **exponentially**. (`zoom.value ** 2`)
|
||||||
|
|
||||||
### Pinch-to-zoom
|
### Pinch-to-zoom
|
||||||
|
|
||||||
|
@ -204,7 +204,7 @@ dependencies {
|
|||||||
implementation jscFlavor
|
implementation jscFlavor
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation project(':camera')
|
implementation project(':react-native-vision-camera')
|
||||||
implementation "androidx.camera:camera-core:1.1.0-alpha05"
|
implementation "androidx.camera:camera-core:1.1.0-alpha05"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,5 +2,5 @@ rootProject.name = 'VisionCameraExample'
|
|||||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||||
include ':app'
|
include ':app'
|
||||||
|
|
||||||
include ':camera'
|
include ':react-native-vision-camera'
|
||||||
project(':camera').projectDir = new File(rootProject.projectDir, '../../android')
|
project(':react-native-vision-camera').projectDir = new File(rootProject.projectDir, '../../android')
|
||||||
|
@ -106,21 +106,15 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
//#region Animated Zoom
|
//#region Animated Zoom
|
||||||
// This just maps the zoom factor to a percentage value.
|
// This just maps the zoom factor to a percentage value.
|
||||||
// so e.g. for [min, neutr., max] values [1, 2, 128] this would result in [0, 0.0081, 1]
|
// so e.g. for [min, neutr., max] values [1, 2, 128] this would result in [0, 0.0081, 1]
|
||||||
const minZoomFactor = device?.minZoom ?? 1;
|
const minZoom = device?.minZoom ?? 1;
|
||||||
const neutralZoomFactor = device?.neutralZoom ?? 1;
|
const maxZoom = Math.min(device?.maxZoom ?? 1, MAX_ZOOM_FACTOR);
|
||||||
const maxZoomFactor = device?.maxZoom ?? 1;
|
|
||||||
const maxZoomFactorClamped = Math.min(maxZoomFactor, MAX_ZOOM_FACTOR);
|
|
||||||
|
|
||||||
const neutralZoomOut = (neutralZoomFactor - minZoomFactor) / (maxZoomFactor - minZoomFactor);
|
|
||||||
const neutralZoomIn = (neutralZoomOut / maxZoomFactorClamped) * maxZoomFactor;
|
|
||||||
const maxZoomOut = maxZoomFactorClamped / maxZoomFactor;
|
|
||||||
|
|
||||||
const cameraAnimatedProps = useAnimatedProps(() => {
|
const cameraAnimatedProps = useAnimatedProps(() => {
|
||||||
const z = interpolate(zoom.value, [0, neutralZoomIn, 1], [0, neutralZoomOut, maxZoomOut], Extrapolate.CLAMP);
|
const z = Math.max(Math.min(zoom.value, maxZoom), minZoom);
|
||||||
return {
|
return {
|
||||||
zoom: isNaN(z) ? 0 : z,
|
zoom: z,
|
||||||
};
|
};
|
||||||
}, [maxZoomOut, neutralZoomOut, neutralZoomIn, zoom]);
|
}, [maxZoom, minZoom, zoom]);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Callbacks
|
//#region Callbacks
|
||||||
@ -165,10 +159,11 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Effects
|
//#region Effects
|
||||||
|
const neutralZoom = device?.neutralZoom ?? 1;
|
||||||
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 = neutralZoomIn;
|
zoom.value = neutralZoom;
|
||||||
}, [neutralZoomIn, zoom]);
|
}, [neutralZoom, zoom]);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Pinch to Zoom Gesture
|
//#region Pinch to Zoom Gesture
|
||||||
@ -182,7 +177,7 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
// we're trying to map the scale gesture to a linear zoom here
|
// we're trying to map the scale gesture to a linear zoom here
|
||||||
const startZoom = context.startZoom ?? 0;
|
const startZoom = context.startZoom ?? 0;
|
||||||
const scale = interpolate(event.scale, [1 - 1 / SCALE_FULL_ZOOM, 1, SCALE_FULL_ZOOM], [-1, 0, 1], Extrapolate.CLAMP);
|
const scale = interpolate(event.scale, [1 - 1 / SCALE_FULL_ZOOM, 1, SCALE_FULL_ZOOM], [-1, 0, 1], Extrapolate.CLAMP);
|
||||||
zoom.value = interpolate(scale, [-1, 0, 1], [0, startZoom, 1], Extrapolate.CLAMP);
|
zoom.value = interpolate(scale, [-1, 0, 1], [minZoom, startZoom, maxZoom], Extrapolate.CLAMP);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -237,6 +232,8 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
camera={camera}
|
camera={camera}
|
||||||
onMediaCaptured={onMediaCaptured}
|
onMediaCaptured={onMediaCaptured}
|
||||||
cameraZoom={zoom}
|
cameraZoom={zoom}
|
||||||
|
minZoom={minZoom}
|
||||||
|
maxZoom={maxZoom}
|
||||||
flash={supportsFlash ? flash : 'off'}
|
flash={supportsFlash ? flash : 'off'}
|
||||||
enabled={isCameraInitialized && isActive}
|
enabled={isCameraInitialized && isActive}
|
||||||
setIsPressingButton={setIsPressingButton}
|
setIsPressingButton={setIsPressingButton}
|
||||||
|
@ -32,6 +32,8 @@ interface Props extends ViewProps {
|
|||||||
camera: React.RefObject<Camera>;
|
camera: React.RefObject<Camera>;
|
||||||
onMediaCaptured: (media: PhotoFile | VideoFile, type: 'photo' | 'video') => void;
|
onMediaCaptured: (media: PhotoFile | VideoFile, type: 'photo' | 'video') => void;
|
||||||
|
|
||||||
|
minZoom: number;
|
||||||
|
maxZoom: number;
|
||||||
cameraZoom: Reanimated.SharedValue<number>;
|
cameraZoom: Reanimated.SharedValue<number>;
|
||||||
|
|
||||||
flash: 'off' | 'on';
|
flash: 'off' | 'on';
|
||||||
@ -44,6 +46,8 @@ interface Props extends ViewProps {
|
|||||||
const _CaptureButton: React.FC<Props> = ({
|
const _CaptureButton: React.FC<Props> = ({
|
||||||
camera,
|
camera,
|
||||||
onMediaCaptured,
|
onMediaCaptured,
|
||||||
|
minZoom,
|
||||||
|
maxZoom,
|
||||||
cameraZoom,
|
cameraZoom,
|
||||||
flash,
|
flash,
|
||||||
enabled,
|
enabled,
|
||||||
@ -191,15 +195,14 @@ const _CaptureButton: React.FC<Props> = ({
|
|||||||
const offsetYForFullZoom = context.startY - yForFullZoom;
|
const offsetYForFullZoom = context.startY - yForFullZoom;
|
||||||
|
|
||||||
// extrapolate [0 ... 1] zoom -> [0 ... Y_FOR_FULL_ZOOM] finger position
|
// extrapolate [0 ... 1] zoom -> [0 ... Y_FOR_FULL_ZOOM] finger position
|
||||||
context.offsetY = interpolate(Math.sqrt(cameraZoom.value), [0, 1], [0, offsetYForFullZoom], Extrapolate.CLAMP);
|
context.offsetY = interpolate(cameraZoom.value, [minZoom, maxZoom], [0, offsetYForFullZoom], Extrapolate.CLAMP);
|
||||||
},
|
},
|
||||||
onActive: (event, context) => {
|
onActive: (event, context) => {
|
||||||
const offset = context.offsetY ?? 0;
|
const offset = context.offsetY ?? 0;
|
||||||
const startY = context.startY ?? SCREEN_HEIGHT;
|
const startY = context.startY ?? SCREEN_HEIGHT;
|
||||||
const yForFullZoom = startY * 0.7;
|
const yForFullZoom = startY * 0.7;
|
||||||
|
|
||||||
const zoom = interpolate(event.absoluteY - offset, [yForFullZoom, startY], [1, 0], Extrapolate.CLAMP);
|
cameraZoom.value = interpolate(event.absoluteY - offset, [yForFullZoom, startY], [maxZoom, minZoom], Extrapolate.CLAMP);
|
||||||
cameraZoom.value = zoom ** 2;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -59,7 +59,7 @@ public final class CameraView: UIView {
|
|||||||
// other props
|
// other props
|
||||||
@objc var isActive = false
|
@objc var isActive = false
|
||||||
@objc var torch = "off"
|
@objc var torch = "off"
|
||||||
@objc var zoom: NSNumber = 0.0 // in percent
|
@objc var zoom: NSNumber = 1.0 // in "factor"
|
||||||
@objc var videoStabilizationMode: NSString?
|
@objc var videoStabilizationMode: NSString?
|
||||||
// events
|
// events
|
||||||
@objc var onInitialized: RCTDirectEventBlock?
|
@objc var onInitialized: RCTDirectEventBlock?
|
||||||
@ -212,10 +212,9 @@ public final class CameraView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if shouldUpdateZoom {
|
if shouldUpdateZoom {
|
||||||
let zoomPercent = CGFloat(max(min(self.zoom.doubleValue, 1.0), 0.0))
|
let zoomClamped = max(min(CGFloat(self.zoom.doubleValue), self.maxAvailableZoom), self.minAvailableZoom)
|
||||||
let zoomScaled = (zoomPercent * (self.maxAvailableZoom - self.minAvailableZoom)) + self.minAvailableZoom
|
self.zoom(factor: zoomClamped, animated: false)
|
||||||
self.zoom(factor: zoomScaled, animated: false)
|
self.pinchScaleOffset = zoomClamped
|
||||||
self.pinchScaleOffset = zoomScaled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldCheckActive && self.captureSession.isRunning != self.isActive {
|
if shouldCheckActive && self.captureSession.isRunning != self.isActive {
|
||||||
|
@ -143,7 +143,7 @@ export interface CameraDeviceFormat {
|
|||||||
*/
|
*/
|
||||||
fieldOfView: number;
|
fieldOfView: number;
|
||||||
/**
|
/**
|
||||||
* The maximum zoom factor
|
* The maximum zoom factor (e.g. `128`)
|
||||||
*/
|
*/
|
||||||
maxZoom: number;
|
maxZoom: number;
|
||||||
/**
|
/**
|
||||||
@ -216,27 +216,26 @@ export interface CameraDevice {
|
|||||||
*/
|
*/
|
||||||
isMultiCam: boolean;
|
isMultiCam: boolean;
|
||||||
/**
|
/**
|
||||||
* Minimum available zoom factor
|
* Minimum available zoom factor (e.g. `1`)
|
||||||
*/
|
*/
|
||||||
minZoom: number;
|
minZoom: number;
|
||||||
/**
|
/**
|
||||||
* Maximum available zoom factor
|
* Maximum available zoom factor (e.g. `128`)
|
||||||
*/
|
*/
|
||||||
maxZoom: number;
|
maxZoom: number;
|
||||||
/**
|
/**
|
||||||
* The zoom factor where the camera is "neutral".
|
* The zoom factor where the camera is "neutral".
|
||||||
*
|
*
|
||||||
* * For single-physical cameras this property is always `1.0`.
|
* * For single-physical cameras this property is always `1.0`.
|
||||||
* * 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.
|
* * 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_ ("fish-eye") 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
|
* @example
|
||||||
* const device = ...
|
* const device = ...
|
||||||
*
|
*
|
||||||
* const neutralZoomPercent = (device.neutralZoom - device.minZoom) / (device.maxZoom - device.minZoom)
|
* const zoom = useSharedValue(device.neutralZoom) // <-- initial value so it doesn't start at ultra-wide
|
||||||
* const zoomFactor = useSharedValue(neutralZoomPercent) // <-- initial value so it doesn't start at ultra-wide
|
|
||||||
* const cameraProps = useAnimatedProps(() => ({
|
* const cameraProps = useAnimatedProps(() => ({
|
||||||
* zoom: zoomFactor.value
|
* zoom: zoom.value
|
||||||
* }))
|
* }))
|
||||||
*/
|
*/
|
||||||
neutralZoom: number;
|
neutralZoom: number;
|
||||||
|
@ -61,11 +61,15 @@ export interface CameraProps extends ViewProps {
|
|||||||
*/
|
*/
|
||||||
torch?: 'off' | 'on';
|
torch?: 'off' | 'on';
|
||||||
/**
|
/**
|
||||||
* Specifies the zoom factor of the current camera, in percent. (`0.0` - `1.0`)
|
* Specifies the zoom factor of the current camera, in "factor"/scale.
|
||||||
|
*
|
||||||
|
* This value ranges from `minZoom` (e.g. `1`) to `maxZoom` (e.g. `128`). It is recommended to set this value
|
||||||
|
* to the CameraDevice's `neutralZoom` per default and let the user zoom out to the fish-eye (ultra-wide) camera
|
||||||
|
* on demand (if available)
|
||||||
*
|
*
|
||||||
* **Note:** Linearly increasing this value always appears logarithmic to the user.
|
* **Note:** Linearly increasing this value always appears logarithmic to the user.
|
||||||
*
|
*
|
||||||
* @default 0.0
|
* @default 1.0
|
||||||
*/
|
*/
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user