Compare commits
11 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a82629461 | |||
| b057613bac | |||
| e4892c4b12 | |||
| 5b553c7196 | |||
| a2dfa7db3d | |||
| b0691adfa3 | |||
| dd4471c7d2 | |||
| 1cd2b0a57c | |||
| c0bab85d63 | |||
| 4f8661f648 | |||
| 6d7be66341 |
10 changed files with 82 additions and 133 deletions
15
CLAUDE.md
15
CLAUDE.md
|
|
@ -86,6 +86,21 @@ Bitmaps emitted to `StateFlow`s are **never eagerly recycled** immediately after
|
|||
- Error/success dismiss indicators use cancellable `Job` tracking to prevent race conditions
|
||||
- `writeExifToUri()` returns boolean and logs at ERROR level on failure
|
||||
|
||||
### Orientation policy (do not change without asking)
|
||||
|
||||
- Activity is `screenOrientation="portrait"` in the manifest. The GL surface and Compose layout therefore never rotate.
|
||||
- The camera passthrough uses a fixed 90° texcoord rotation (`texCoordsBack`/`texCoordsFront`). There is no `setTargetRotation`/`setDisplayRotation`/`getTransformMatrix`-based rotation tracking — past attempts (v1.1.6 through v1.1.13) all introduced subtle bugs and were reverted.
|
||||
- The camera image lives in the device's portrait frame and visually follows the phone as it tilts. Per-orientation correctness comes from the EXIF orientation tag written at capture (`OrientationDetector.degreesToExifOrientation(rotationToDegrees(deviceRotation), isFrontCamera)`), not from rotating the bitmap.
|
||||
- `OrientationEventListener` (`viewModel.currentRotation`) is for EXIF only. It is **not** the same as `Display.rotation`: it fires at the 45° tilt threshold while the activity rotates later, so it must not drive anything that has to stay in sync with the GL surface.
|
||||
- `SurfaceTexture.getTransformMatrix()` with a custom `SurfaceProvider` does not change on `Preview.targetRotation` updates; rebinding doesn't reliably help either. Don't go down that road again.
|
||||
|
||||
### Local Android testing
|
||||
|
||||
- AVD: `tilfluktsrom` (Pixel 6, API 35, Google APIs) at `~/.android/avd/`. Boot with `emulator -avd tilfluktsrom -no-snapshot-save -gpu swiftshader_indirect -no-audio`.
|
||||
- `adb -s emulator-5554 emu rotate` cycles `ROTATION_0 → 3 → 2 → 1 → 0` (decrements by 90°), not the other way.
|
||||
- The default virtual scene is too rotationally symmetric to verify which way is "up". For orientation testing, pass `-virtualscene-poster wall=/path/to/marker.png` and ensure the scene camera faces the wall, or just don't trust emulator screenshots as proof of correct orientation.
|
||||
- Camera bitmaps captured at startup tend to look black for a few seconds while CameraX rebinds. Wait ≥10 s after `am start` before screenshotting.
|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Purpose | Notes |
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="fullSensor"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:windowSoftInputMode="adjustNothing"
|
||||
android:theme="@style/Theme.TiltShiftCamera">
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@ package no.naiv.tiltshift.camera
|
|||
|
||||
import android.content.Context
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.util.Log
|
||||
import android.util.Size
|
||||
import android.view.Display
|
||||
import android.view.Surface
|
||||
import androidx.camera.core.Camera
|
||||
import androidx.camera.core.CameraSelector
|
||||
|
|
@ -72,14 +70,6 @@ class CameraManager(private val context: Context) {
|
|||
/** Weak reference to avoid preventing Activity GC across config changes. */
|
||||
private var lifecycleOwnerRef: WeakReference<LifecycleOwner>? = null
|
||||
|
||||
/**
|
||||
* Target rotation passed to CameraX use cases. Drives the SurfaceTexture
|
||||
* transform matrix and the rotation metadata on captured images.
|
||||
* Initialized to the display rotation when the camera binds; updated by
|
||||
* [setTargetRotation] when the device orientation changes.
|
||||
*/
|
||||
private var targetRotation: Int = Surface.ROTATION_0
|
||||
|
||||
/**
|
||||
* Starts the camera with the given lifecycle owner.
|
||||
* The surfaceTextureProvider should return the SurfaceTexture from the GL renderer.
|
||||
|
|
@ -90,13 +80,6 @@ class CameraManager(private val context: Context) {
|
|||
) {
|
||||
this.surfaceTextureProvider = surfaceTextureProvider
|
||||
this.lifecycleOwnerRef = WeakReference(lifecycleOwner)
|
||||
// Capture initial display rotation so the very first frame is oriented correctly,
|
||||
// before the OrientationEventListener has had a chance to fire.
|
||||
// Note: Context.getDisplay() throws on Application contexts; DisplayManager works
|
||||
// for any context type and returns the default display.
|
||||
targetRotation = (context.getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager)
|
||||
?.getDisplay(Display.DEFAULT_DISPLAY)?.rotation
|
||||
?: Surface.ROTATION_0
|
||||
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||
cameraProviderFuture.addListener({
|
||||
|
|
@ -127,13 +110,11 @@ class CameraManager(private val context: Context) {
|
|||
|
||||
preview = Preview.Builder()
|
||||
.setResolutionSelector(resolutionSelector)
|
||||
.setTargetRotation(targetRotation)
|
||||
.build()
|
||||
|
||||
// Image capture use case
|
||||
val captureBuilder = ImageCapture.Builder()
|
||||
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
|
||||
.setTargetRotation(targetRotation)
|
||||
|
||||
imageCapture = captureBuilder.build()
|
||||
|
||||
|
|
@ -192,23 +173,6 @@ class CameraManager(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the target rotation for Preview and ImageCapture use cases.
|
||||
*
|
||||
* Rebinds the use cases so CameraX issues a fresh SurfaceRequest with a
|
||||
* resolution matching the new rotation and a corresponding texture transform
|
||||
* matrix. Calling `preview.targetRotation = rotation` alone is insufficient
|
||||
* for a custom SurfaceProvider — the new rotation only takes effect on
|
||||
* subsequently bound streams, leaving the live SurfaceTexture matrix and
|
||||
* buffer size stale (which made the preview appear locked to the original
|
||||
* portrait orientation when the device was rotated to landscape).
|
||||
*/
|
||||
fun setTargetRotation(rotation: Int) {
|
||||
if (targetRotation == rotation) return
|
||||
targetRotation = rotation
|
||||
lifecycleOwnerRef?.get()?.let { bindCameraUseCases(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import kotlin.coroutines.resume
|
|||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
import no.naiv.tiltshift.util.OrientationDetector
|
||||
|
||||
/**
|
||||
* Handles capturing photos with the tilt-shift effect applied.
|
||||
|
|
@ -121,10 +122,19 @@ class ImageCaptureHandler(
|
|||
var thumbnail: Bitmap? = null
|
||||
try {
|
||||
thumbnail = createThumbnail(captureResult.processed)
|
||||
// Camera bitmap is in the device's portrait frame (CameraX
|
||||
// rotated the sensor 90° because the activity is locked to
|
||||
// portrait). Tag the EXIF with the user's physical tilt
|
||||
// when they pressed the shutter so viewers display the
|
||||
// photo right-side-up regardless of how the phone was held.
|
||||
val exifOrientation = OrientationDetector.degreesToExifOrientation(
|
||||
OrientationDetector.rotationToDegrees(deviceRotation),
|
||||
isFrontCamera
|
||||
)
|
||||
val result = photoSaver.saveBitmapPair(
|
||||
original = captureResult.original,
|
||||
processed = captureResult.processed,
|
||||
orientation = ExifInterface.ORIENTATION_NORMAL,
|
||||
orientation = exifOrientation,
|
||||
location = location
|
||||
)
|
||||
if (result is SaveResult.Success) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import android.opengl.GLES11Ext
|
|||
import android.opengl.GLES20
|
||||
import android.opengl.GLSurfaceView
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.nio.FloatBuffer
|
||||
|
|
@ -39,8 +38,9 @@ class TiltShiftRenderer(
|
|||
private var surfaceTexture: SurfaceTexture? = null
|
||||
private var cameraTextureId: Int = 0
|
||||
|
||||
// Camera quad: crop-to-fill vertices, standard texcoords (rotation comes from texMatrix)
|
||||
// Camera quad: crop-to-fill vertices + rotated texcoords (pass 1 only)
|
||||
private lateinit var cameraVertexBuffer: FloatBuffer
|
||||
private lateinit var cameraTexCoordBuffer: FloatBuffer
|
||||
|
||||
// Fullscreen quad for blur passes (no crop, standard texcoords)
|
||||
private lateinit var fullscreenVertexBuffer: FloatBuffer
|
||||
|
|
@ -54,9 +54,6 @@ class TiltShiftRenderer(
|
|||
private var fboTexA: Int = 0
|
||||
private var fboTexB: Int = 0
|
||||
|
||||
// SurfaceTexture transform matrix, refreshed each frame on the GL thread.
|
||||
private val texMatrix = FloatArray(16)
|
||||
|
||||
// Current effect parameters (updated from UI thread)
|
||||
@Volatile
|
||||
var blurParameters: BlurParameters = BlurParameters.DEFAULT
|
||||
|
|
@ -69,14 +66,33 @@ class TiltShiftRenderer(
|
|||
private var cameraWidth: Int = 0
|
||||
@Volatile
|
||||
private var cameraHeight: Int = 0
|
||||
|
||||
/** Display rotation as a Surface.ROTATION_* constant; affects effective aspect. */
|
||||
@Volatile
|
||||
private var displayRotation: Int = Surface.ROTATION_0
|
||||
|
||||
@Volatile
|
||||
private var vertexBufferDirty: Boolean = false
|
||||
|
||||
// Texture coordinates rotated 90° for portrait mode (back camera)
|
||||
// (Camera sensors are landscape-oriented, we rotate to portrait)
|
||||
private val texCoordsBack = floatArrayOf(
|
||||
1f, 1f, // Bottom left of screen -> bottom right of texture
|
||||
1f, 0f, // Bottom right of screen -> top right of texture
|
||||
0f, 1f, // Top left of screen -> bottom left of texture
|
||||
0f, 0f // Top right of screen -> top left of texture
|
||||
)
|
||||
|
||||
// Texture coordinates for front camera (mirrored + rotated)
|
||||
// Front camera needs horizontal mirror for natural selfie view
|
||||
private val texCoordsFront = floatArrayOf(
|
||||
0f, 1f, // Bottom left of screen
|
||||
0f, 0f, // Bottom right of screen
|
||||
1f, 1f, // Top left of screen
|
||||
1f, 0f // Top right of screen
|
||||
)
|
||||
|
||||
@Volatile
|
||||
private var currentTexCoords = texCoordsBack
|
||||
|
||||
@Volatile
|
||||
private var updateTexCoordBuffer = false
|
||||
|
||||
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
|
||||
GLES20.glClearColor(0f, 0f, 0f, 1f)
|
||||
|
||||
|
|
@ -88,8 +104,12 @@ class TiltShiftRenderer(
|
|||
cameraVertexBuffer.put(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f))
|
||||
cameraVertexBuffer.position(0)
|
||||
|
||||
// Fullscreen quad for blur passes (standard coords). The same buffer is reused
|
||||
// for the camera passthrough texcoords — rotation is applied via uTexMatrix.
|
||||
// Camera texcoord buffer (rotated for portrait)
|
||||
cameraTexCoordBuffer = allocateFloatBuffer(8)
|
||||
cameraTexCoordBuffer.put(currentTexCoords)
|
||||
cameraTexCoordBuffer.position(0)
|
||||
|
||||
// Fullscreen quad for blur passes (standard coords)
|
||||
fullscreenVertexBuffer = allocateFloatBuffer(8)
|
||||
fullscreenVertexBuffer.put(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f))
|
||||
fullscreenVertexBuffer.position(0)
|
||||
|
|
@ -125,17 +145,20 @@ class TiltShiftRenderer(
|
|||
}
|
||||
|
||||
override fun onDrawFrame(gl: GL10?) {
|
||||
val st = surfaceTexture
|
||||
st?.updateTexImage()
|
||||
// Pull the latest sensor-to-display transform; updated by SurfaceTexture each frame
|
||||
// and reflects whatever rotation CameraX requested via Preview.targetRotation.
|
||||
st?.getTransformMatrix(texMatrix)
|
||||
surfaceTexture?.updateTexImage()
|
||||
|
||||
if (vertexBufferDirty) {
|
||||
recomputeVertices()
|
||||
vertexBufferDirty = false
|
||||
}
|
||||
|
||||
if (updateTexCoordBuffer) {
|
||||
cameraTexCoordBuffer.clear()
|
||||
cameraTexCoordBuffer.put(currentTexCoords)
|
||||
cameraTexCoordBuffer.position(0)
|
||||
updateTexCoordBuffer = false
|
||||
}
|
||||
|
||||
val params = blurParameters
|
||||
|
||||
// --- Pass 1: Camera → FBO-A (passthrough with crop-to-fill) ---
|
||||
|
|
@ -146,10 +169,10 @@ class TiltShiftRenderer(
|
|||
)
|
||||
GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight)
|
||||
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
|
||||
shader.usePassthrough(cameraTextureId, texMatrix, isFrontCamera)
|
||||
shader.usePassthrough(cameraTextureId)
|
||||
drawQuad(
|
||||
shader.passthroughPositionLoc, shader.passthroughTexCoordLoc,
|
||||
cameraVertexBuffer, fullscreenTexCoordBuffer
|
||||
cameraVertexBuffer, cameraTexCoordBuffer
|
||||
)
|
||||
|
||||
// --- Pass 2: FBO-A → FBO-B (horizontal blur) ---
|
||||
|
|
@ -180,7 +203,11 @@ class TiltShiftRenderer(
|
|||
}
|
||||
|
||||
fun setFrontCamera(front: Boolean) {
|
||||
if (isFrontCamera != front) {
|
||||
isFrontCamera = front
|
||||
currentTexCoords = if (front) texCoordsFront else texCoordsBack
|
||||
updateTexCoordBuffer = true
|
||||
}
|
||||
}
|
||||
|
||||
fun setCameraResolution(width: Int, height: Int) {
|
||||
|
|
@ -191,14 +218,6 @@ class TiltShiftRenderer(
|
|||
}
|
||||
}
|
||||
|
||||
/** Updates the display rotation so crop-to-fill picks the right effective aspect. */
|
||||
fun setDisplayRotation(rotation: Int) {
|
||||
if (displayRotation != rotation) {
|
||||
displayRotation = rotation
|
||||
vertexBufferDirty = true
|
||||
}
|
||||
}
|
||||
|
||||
fun release() {
|
||||
shader.release()
|
||||
surfaceTexture?.release()
|
||||
|
|
@ -235,24 +254,16 @@ class TiltShiftRenderer(
|
|||
/**
|
||||
* Recomputes camera vertex positions to achieve crop-to-fill.
|
||||
*
|
||||
* The camera buffer is the sensor's native landscape resolution. The texMatrix
|
||||
* rotates it to match the display, so the *effective* displayed dimensions
|
||||
* depend on the display rotation: in portrait the buffer is rotated 90°
|
||||
* (effective width = cameraHeight), in landscape it is unrotated.
|
||||
* We scale the vertex quad so the frame fills the surface without stretching —
|
||||
* the GPU clips the overflow.
|
||||
* The camera sensor is landscape; after the 90° rotation applied via texture coordinates,
|
||||
* the effective portrait dimensions are (cameraHeight × cameraWidth). We scale the vertex
|
||||
* quad so the camera frame fills the surface without stretching — the GPU clips the overflow.
|
||||
*/
|
||||
private fun recomputeVertices() {
|
||||
var scaleX = 1f
|
||||
var scaleY = 1f
|
||||
|
||||
if (cameraWidth > 0 && cameraHeight > 0 && surfaceWidth > 0 && surfaceHeight > 0) {
|
||||
val isPortrait = displayRotation == Surface.ROTATION_0 ||
|
||||
displayRotation == Surface.ROTATION_180
|
||||
val effectiveW = if (isPortrait) cameraHeight else cameraWidth
|
||||
val effectiveH = if (isPortrait) cameraWidth else cameraHeight
|
||||
|
||||
val cameraRatio = effectiveW.toFloat() / effectiveH
|
||||
val cameraRatio = cameraHeight.toFloat() / cameraWidth
|
||||
val screenRatio = surfaceWidth.toFloat() / surfaceHeight
|
||||
|
||||
if (cameraRatio > screenRatio) {
|
||||
|
|
|
|||
|
|
@ -20,15 +20,6 @@ import kotlin.math.sin
|
|||
*/
|
||||
class TiltShiftShader(private val context: Context) {
|
||||
|
||||
private companion object {
|
||||
val IDENTITY_MATRIX = floatArrayOf(
|
||||
1f, 0f, 0f, 0f,
|
||||
0f, 1f, 0f, 0f,
|
||||
0f, 0f, 1f, 0f,
|
||||
0f, 0f, 0f, 1f
|
||||
)
|
||||
}
|
||||
|
||||
// --- Passthrough program (camera → FBO) ---
|
||||
|
||||
private var passthroughProgramId: Int = 0
|
||||
|
|
@ -38,8 +29,6 @@ class TiltShiftShader(private val context: Context) {
|
|||
var passthroughTexCoordLoc: Int = 0
|
||||
private set
|
||||
private var passthroughTextureLoc: Int = 0
|
||||
private var passthroughTexMatrixLoc: Int = 0
|
||||
private var passthroughMirrorLoc: Int = 0
|
||||
|
||||
// --- Blur program (FBO → FBO/screen) ---
|
||||
|
||||
|
|
@ -50,8 +39,6 @@ class TiltShiftShader(private val context: Context) {
|
|||
var blurTexCoordLoc: Int = 0
|
||||
private set
|
||||
private var blurTextureLoc: Int = 0
|
||||
private var blurTexMatrixLoc: Int = 0
|
||||
private var blurMirrorLoc: Int = 0
|
||||
private var blurModeLoc: Int = 0
|
||||
private var blurPositionXLoc: Int = 0
|
||||
private var blurPositionYLoc: Int = 0
|
||||
|
|
@ -81,8 +68,6 @@ class TiltShiftShader(private val context: Context) {
|
|||
passthroughPositionLoc = GLES20.glGetAttribLocation(passthroughProgramId, "aPosition")
|
||||
passthroughTexCoordLoc = GLES20.glGetAttribLocation(passthroughProgramId, "aTexCoord")
|
||||
passthroughTextureLoc = GLES20.glGetUniformLocation(passthroughProgramId, "uTexture")
|
||||
passthroughTexMatrixLoc = GLES20.glGetUniformLocation(passthroughProgramId, "uTexMatrix")
|
||||
passthroughMirrorLoc = GLES20.glGetUniformLocation(passthroughProgramId, "uMirrorX")
|
||||
|
||||
// Blur program
|
||||
val blurFragSource = loadShaderSource(R.raw.tiltshift_fragment)
|
||||
|
|
@ -93,8 +78,6 @@ class TiltShiftShader(private val context: Context) {
|
|||
blurPositionLoc = GLES20.glGetAttribLocation(blurProgramId, "aPosition")
|
||||
blurTexCoordLoc = GLES20.glGetAttribLocation(blurProgramId, "aTexCoord")
|
||||
blurTextureLoc = GLES20.glGetUniformLocation(blurProgramId, "uTexture")
|
||||
blurTexMatrixLoc = GLES20.glGetUniformLocation(blurProgramId, "uTexMatrix")
|
||||
blurMirrorLoc = GLES20.glGetUniformLocation(blurProgramId, "uMirrorX")
|
||||
blurModeLoc = GLES20.glGetUniformLocation(blurProgramId, "uMode")
|
||||
blurPositionXLoc = GLES20.glGetUniformLocation(blurProgramId, "uPositionX")
|
||||
blurPositionYLoc = GLES20.glGetUniformLocation(blurProgramId, "uPositionY")
|
||||
|
|
@ -113,19 +96,12 @@ class TiltShiftShader(private val context: Context) {
|
|||
|
||||
/**
|
||||
* Activates the passthrough program and binds the camera texture.
|
||||
*
|
||||
* @param cameraTextureId The OES texture receiving camera frames.
|
||||
* @param texMatrix 4x4 transform from SurfaceTexture.getTransformMatrix() —
|
||||
* encodes sensor-to-display rotation and Y-flip.
|
||||
* @param mirrorX true to horizontally mirror (front camera selfie view).
|
||||
*/
|
||||
fun usePassthrough(cameraTextureId: Int, texMatrix: FloatArray, mirrorX: Boolean) {
|
||||
fun usePassthrough(cameraTextureId: Int) {
|
||||
GLES20.glUseProgram(passthroughProgramId)
|
||||
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
|
||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTextureId)
|
||||
GLES20.glUniform1i(passthroughTextureLoc, 0)
|
||||
GLES20.glUniformMatrix4fv(passthroughTexMatrixLoc, 1, false, texMatrix, 0)
|
||||
GLES20.glUniform1f(passthroughMirrorLoc, if (mirrorX) 1f else 0f)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -152,10 +128,6 @@ class TiltShiftShader(private val context: Context) {
|
|||
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fboTextureId)
|
||||
GLES20.glUniform1i(blurTextureLoc, 0)
|
||||
|
||||
// FBO content is already in display orientation — pass identity matrix and no mirror.
|
||||
GLES20.glUniformMatrix4fv(blurTexMatrixLoc, 1, false, IDENTITY_MATRIX, 0)
|
||||
GLES20.glUniform1f(blurMirrorLoc, 0f)
|
||||
|
||||
GLES20.glUniform1i(blurModeLoc, if (params.mode == BlurMode.RADIAL) 1 else 0)
|
||||
GLES20.glUniform1f(blurPositionXLoc, params.positionX)
|
||||
GLES20.glUniform1f(blurPositionYLoc, params.positionY)
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ fun CameraScreen(
|
|||
val isFrontCamera by viewModel.cameraManager.isFrontCamera.collectAsState()
|
||||
val previewResolution by viewModel.cameraManager.previewResolution.collectAsState()
|
||||
val cameraError by viewModel.cameraManager.error.collectAsState()
|
||||
val currentRotation by viewModel.currentRotation.collectAsState()
|
||||
|
||||
// Gallery picker
|
||||
val galleryLauncher = rememberLauncherForActivityResult(
|
||||
|
|
@ -165,13 +164,6 @@ fun CameraScreen(
|
|||
}
|
||||
}
|
||||
|
||||
// Forward device rotation to renderer (aspect math) and CameraX (target rotation)
|
||||
LaunchedEffect(currentRotation, renderer) {
|
||||
renderer?.setDisplayRotation(currentRotation)
|
||||
viewModel.cameraManager.setTargetRotation(currentRotation)
|
||||
glSurfaceView?.requestRender()
|
||||
}
|
||||
|
||||
// Start camera when surface texture is available
|
||||
LaunchedEffect(surfaceTexture) {
|
||||
surfaceTexture?.let {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,7 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.RestartAlt
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -113,10 +110,8 @@ fun ControlPanel(
|
|||
Column(
|
||||
modifier = modifier
|
||||
.width(200.dp)
|
||||
.wrapContentHeight()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(AppColors.OverlayDarker)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,22 +1,12 @@
|
|||
// Vertex shader for tilt-shift effect.
|
||||
//
|
||||
// uTexMatrix: applied to texcoords. For the passthrough pass it carries the
|
||||
// SurfaceTexture transform (sensor → display rotation, plus Y-flip). For the
|
||||
// blur passes it is identity.
|
||||
// uMirrorX: 1.0 to horizontally mirror texcoords (front-camera selfie view),
|
||||
// 0.0 otherwise. Applied AFTER uTexMatrix.
|
||||
// Vertex shader for tilt-shift effect
|
||||
// Passes through position and calculates texture coordinates
|
||||
|
||||
attribute vec4 aPosition;
|
||||
attribute vec2 aTexCoord;
|
||||
|
||||
uniform mat4 uTexMatrix;
|
||||
uniform float uMirrorX;
|
||||
|
||||
varying vec2 vTexCoord;
|
||||
|
||||
void main() {
|
||||
gl_Position = aPosition;
|
||||
vec2 tc = (uTexMatrix * vec4(aTexCoord, 0.0, 1.0)).xy;
|
||||
if (uMirrorX > 0.5) tc.x = 1.0 - tc.x;
|
||||
vTexCoord = tc;
|
||||
vTexCoord = aTexCoord;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
versionMajor=1
|
||||
versionMinor=1
|
||||
versionPatch=8
|
||||
versionCode=10
|
||||
versionPatch=15
|
||||
versionCode=17
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue