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:
		| @@ -82,7 +82,7 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner { | ||||
|   // other props | ||||
|   var isActive = false | ||||
|   var torch = "off" | ||||
|   var zoom = 0.0 // in percent | ||||
|   var zoom: Float = 1f // in "factor" | ||||
|   var enableZoomGesture = false | ||||
|   var frameProcessorFps = 1.0 | ||||
|  | ||||
| @@ -162,7 +162,7 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner { | ||||
|  | ||||
|     scaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() { | ||||
|       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) | ||||
|         return true | ||||
|       } | ||||
| @@ -275,8 +275,8 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner { | ||||
|           configureSession() | ||||
|         } | ||||
|         if (shouldReconfigureZoom) { | ||||
|           val scaled = (zoom.toFloat() * (maxZoom - minZoom)) + minZoom | ||||
|           camera!!.cameraControl.setZoomRatio(scaled) | ||||
|           val zoomClamped = max(min(zoom.toFloat(), maxZoom), minZoom) | ||||
|           camera!!.cameraControl.setZoomRatio(zoomClamped) | ||||
|         } | ||||
|         if (shouldReconfigureTorch) { | ||||
|           camera!!.cameraControl.enableTorch(torch == "on") | ||||
|   | ||||
| @@ -131,9 +131,10 @@ class CameraViewManager : SimpleViewManager<CameraView>() { | ||||
|  | ||||
|   @ReactProp(name = "zoom") | ||||
|   fun setZoom(view: CameraView, zoom: Double) { | ||||
|     if (view.zoom != zoom) | ||||
|     val zoomFloat = zoom.toFloat() | ||||
|     if (view.zoom != zoomFloat) | ||||
|       addChangedPropToTransaction(view, "zoom") | ||||
|     view.zoom = zoom | ||||
|     view.zoom = zoomFloat | ||||
|   } | ||||
|  | ||||
|   @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%" /> | ||||
| </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 | ||||
|  | ||||
| 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 | ||||
|  | ||||
|   | ||||
| @@ -204,7 +204,7 @@ dependencies { | ||||
|         implementation jscFlavor | ||||
|     } | ||||
|  | ||||
|     implementation project(':camera') | ||||
|     implementation project(':react-native-vision-camera') | ||||
|     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) | ||||
| include ':app' | ||||
|  | ||||
| include ':camera' | ||||
| project(':camera').projectDir = new File(rootProject.projectDir, '../../android') | ||||
| include ':react-native-vision-camera' | ||||
| project(':react-native-vision-camera').projectDir = new File(rootProject.projectDir, '../../android') | ||||
|   | ||||
| @@ -106,21 +106,15 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => { | ||||
|   //#region Animated Zoom | ||||
|   // 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] | ||||
|   const minZoomFactor = device?.minZoom ?? 1; | ||||
|   const neutralZoomFactor = device?.neutralZoom ?? 1; | ||||
|   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 minZoom = device?.minZoom ?? 1; | ||||
|   const maxZoom = Math.min(device?.maxZoom ?? 1, MAX_ZOOM_FACTOR); | ||||
|  | ||||
|   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 { | ||||
|       zoom: isNaN(z) ? 0 : z, | ||||
|       zoom: z, | ||||
|     }; | ||||
|   }, [maxZoomOut, neutralZoomOut, neutralZoomIn, zoom]); | ||||
|   }, [maxZoom, minZoom, zoom]); | ||||
|   //#endregion | ||||
|  | ||||
|   //#region Callbacks | ||||
| @@ -165,10 +159,11 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => { | ||||
|   //#endregion | ||||
|  | ||||
|   //#region Effects | ||||
|   const neutralZoom = device?.neutralZoom ?? 1; | ||||
|   useEffect(() => { | ||||
|     // Run everytime the neutralZoomScaled value changes. (reset zoom when device changes) | ||||
|     zoom.value = neutralZoomIn; | ||||
|   }, [neutralZoomIn, zoom]); | ||||
|     zoom.value = neutralZoom; | ||||
|   }, [neutralZoom, zoom]); | ||||
|   //#endregion | ||||
|  | ||||
|   //#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 | ||||
|       const startZoom = context.startZoom ?? 0; | ||||
|       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 | ||||
| @@ -237,6 +232,8 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => { | ||||
|         camera={camera} | ||||
|         onMediaCaptured={onMediaCaptured} | ||||
|         cameraZoom={zoom} | ||||
|         minZoom={minZoom} | ||||
|         maxZoom={maxZoom} | ||||
|         flash={supportsFlash ? flash : 'off'} | ||||
|         enabled={isCameraInitialized && isActive} | ||||
|         setIsPressingButton={setIsPressingButton} | ||||
|   | ||||
| @@ -32,6 +32,8 @@ interface Props extends ViewProps { | ||||
|   camera: React.RefObject<Camera>; | ||||
|   onMediaCaptured: (media: PhotoFile | VideoFile, type: 'photo' | 'video') => void; | ||||
|  | ||||
|   minZoom: number; | ||||
|   maxZoom: number; | ||||
|   cameraZoom: Reanimated.SharedValue<number>; | ||||
|  | ||||
|   flash: 'off' | 'on'; | ||||
| @@ -44,6 +46,8 @@ interface Props extends ViewProps { | ||||
| const _CaptureButton: React.FC<Props> = ({ | ||||
|   camera, | ||||
|   onMediaCaptured, | ||||
|   minZoom, | ||||
|   maxZoom, | ||||
|   cameraZoom, | ||||
|   flash, | ||||
|   enabled, | ||||
| @@ -191,15 +195,14 @@ const _CaptureButton: React.FC<Props> = ({ | ||||
|       const offsetYForFullZoom = context.startY - yForFullZoom; | ||||
|  | ||||
|       // 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) => { | ||||
|       const offset = context.offsetY ?? 0; | ||||
|       const startY = context.startY ?? SCREEN_HEIGHT; | ||||
|       const yForFullZoom = startY * 0.7; | ||||
|  | ||||
|       const zoom = interpolate(event.absoluteY - offset, [yForFullZoom, startY], [1, 0], Extrapolate.CLAMP); | ||||
|       cameraZoom.value = zoom ** 2; | ||||
|       cameraZoom.value = interpolate(event.absoluteY - offset, [yForFullZoom, startY], [maxZoom, minZoom], Extrapolate.CLAMP); | ||||
|     }, | ||||
|   }); | ||||
|   //#endregion | ||||
|   | ||||
| @@ -59,7 +59,7 @@ public final class CameraView: UIView { | ||||
|   // other props | ||||
|   @objc var isActive = false | ||||
|   @objc var torch = "off" | ||||
|   @objc var zoom: NSNumber = 0.0 // in percent | ||||
|   @objc var zoom: NSNumber = 1.0 // in "factor" | ||||
|   @objc var videoStabilizationMode: NSString? | ||||
|   // events | ||||
|   @objc var onInitialized: RCTDirectEventBlock? | ||||
| @@ -212,10 +212,9 @@ public final class CameraView: UIView { | ||||
|         } | ||||
|  | ||||
|         if shouldUpdateZoom { | ||||
|           let zoomPercent = CGFloat(max(min(self.zoom.doubleValue, 1.0), 0.0)) | ||||
|           let zoomScaled = (zoomPercent * (self.maxAvailableZoom - self.minAvailableZoom)) + self.minAvailableZoom | ||||
|           self.zoom(factor: zoomScaled, animated: false) | ||||
|           self.pinchScaleOffset = zoomScaled | ||||
|           let zoomClamped = max(min(CGFloat(self.zoom.doubleValue), self.maxAvailableZoom), self.minAvailableZoom) | ||||
|           self.zoom(factor: zoomClamped, animated: false) | ||||
|           self.pinchScaleOffset = zoomClamped | ||||
|         } | ||||
|  | ||||
|         if shouldCheckActive && self.captureSession.isRunning != self.isActive { | ||||
|   | ||||
| @@ -143,7 +143,7 @@ export interface CameraDeviceFormat { | ||||
|    */ | ||||
|   fieldOfView: number; | ||||
|   /** | ||||
|    * The maximum zoom factor | ||||
|    * The maximum zoom factor (e.g. `128`) | ||||
|    */ | ||||
|   maxZoom: number; | ||||
|   /** | ||||
| @@ -216,27 +216,26 @@ export interface CameraDevice { | ||||
|    */ | ||||
|   isMultiCam: boolean; | ||||
|   /** | ||||
|    * Minimum available zoom factor | ||||
|    * Minimum available zoom factor (e.g. `1`) | ||||
|    */ | ||||
|   minZoom: number; | ||||
|   /** | ||||
|    * Maximum available zoom factor | ||||
|    * Maximum available zoom factor (e.g. `128`) | ||||
|    */ | ||||
|   maxZoom: number; | ||||
|   /** | ||||
|    * The zoom factor where the camera is "neutral". | ||||
|    * | ||||
|    * * 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) | ||||
|    * @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 zoom = useSharedValue(device.neutralZoom) // <-- initial value so it doesn't start at ultra-wide | ||||
|    * const cameraProps = useAnimatedProps(() => ({ | ||||
|    *   zoom: zoomFactor.value | ||||
|    *   zoom: zoom.value | ||||
|    * })) | ||||
|    */ | ||||
|   neutralZoom: number; | ||||
|   | ||||
| @@ -61,11 +61,15 @@ export interface CameraProps extends ViewProps { | ||||
|    */ | ||||
|   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. | ||||
|    * | ||||
|    * @default 0.0 | ||||
|    * @default 1.0 | ||||
|    */ | ||||
|   zoom?: number; | ||||
|   /** | ||||
|   | ||||
		Reference in New Issue
	
	Block a user