Implement two-pass separable Gaussian blur for camera preview

Replace the single-pass 9-tap directional blur with a three-pass
pipeline: passthrough (camera→FBO), horizontal blur (FBO-A→FBO-B),
vertical blur (FBO-B→screen). This produces a true 2D Gaussian with
a 13-tap kernel per pass, eliminating the visible banding/streaking
of the old approach.

Key changes:
- TiltShiftRenderer: FBO ping-pong with two color textures, separate
  fullscreen quad for blur passes (no crop-to-fill), drawQuad helper
- TiltShiftShader: manages two programs (passthrough + blur), blur
  program uses raw screen-space angle (no camera rotation adjustment)
- tiltshift_fragment.glsl: rewritten for sampler2D in screen space,
  aspect correction on X axis (height-normalized), uBlurDirection
  uniform for H/V selection, wider falloff (3x multiplier)
- New tiltshift_passthrough_fragment.glsl for camera→FBO copy
- TiltShiftOverlay: shrink PINCH_SIZE zone (1.3x, was 2.0x) so
  pinch-to-zoom is reachable over more of the screen
- CameraManager: optimistic zoom update fixes pinch-to-zoom stalling
  (stale zoomRatio base prevented delta accumulation)

Bump version to 1.1.3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-18 17:38:50 +01:00
commit f3baa723be
7 changed files with 397 additions and 298 deletions

View file

@ -174,24 +174,14 @@ class CameraManager(private val context: Context) {
}
/**
* Sets the zoom ratio. Updates UI state only after the camera confirms the change.
* Sets the zoom ratio. Updates UI state immediately so that rapid pinch-to-zoom
* gestures accumulate correctly (each frame uses the latest ratio as its base).
* If the camera rejects the value, the next successful set corrects the state.
*/
fun setZoom(ratio: Float) {
val clamped = ratio.coerceIn(_minZoomRatio.value, _maxZoomRatio.value)
val future = camera?.cameraControl?.setZoomRatio(clamped)
if (future != null) {
future.addListener({
try {
future.get()
_zoomRatio.value = clamped
} catch (e: Exception) {
Log.w(TAG, "Zoom operation failed", e)
}
}, ContextCompat.getMainExecutor(context))
} else {
// Optimistic update when camera not available (e.g. during init)
_zoomRatio.value = clamped
}
_zoomRatio.value = clamped
camera?.cameraControl?.setZoomRatio(clamped)
}
/**