diff --git a/app/src/main/java/no/naiv/tiltshift/effect/TiltShiftRenderer.kt b/app/src/main/java/no/naiv/tiltshift/effect/TiltShiftRenderer.kt index d170d58..dc2b332 100644 --- a/app/src/main/java/no/naiv/tiltshift/effect/TiltShiftRenderer.kt +++ b/app/src/main/java/no/naiv/tiltshift/effect/TiltShiftRenderer.kt @@ -6,6 +6,7 @@ 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 @@ -69,26 +70,32 @@ class TiltShiftRenderer( @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 the back camera, indexed by Surface.ROTATION_*. + // The base orientation (index 0) applies the 90° CCW rotation that maps + // the landscape sensor frame to a portrait display. Indices 1/2/3 layer + // additional CCW rotations on top so the activity's rotation is + // compensated and world-up stays at clip-space-top. + private val texCoordsBackByRotation = arrayOf( + floatArrayOf(1f, 1f, 1f, 0f, 0f, 1f, 0f, 0f), // ROTATION_0 + floatArrayOf(1f, 0f, 0f, 0f, 1f, 1f, 0f, 1f), // ROTATION_90 + floatArrayOf(0f, 0f, 0f, 1f, 1f, 0f, 1f, 1f), // ROTATION_180 + floatArrayOf(0f, 1f, 1f, 1f, 0f, 0f, 1f, 0f) // ROTATION_270 ) - // 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 + // Front camera variants: same as back, but horizontally mirrored + // for the natural selfie view. + private val texCoordsFrontByRotation = arrayOf( + floatArrayOf(0f, 1f, 0f, 0f, 1f, 1f, 1f, 0f), // ROTATION_0 + floatArrayOf(0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f), // ROTATION_90 + floatArrayOf(1f, 0f, 1f, 1f, 0f, 0f, 0f, 1f), // ROTATION_180 + floatArrayOf(1f, 1f, 0f, 1f, 1f, 0f, 0f, 0f) // ROTATION_270 ) @Volatile - private var currentTexCoords = texCoordsBack + private var displayRotation: Int = Surface.ROTATION_0 + + @Volatile + private var currentTexCoords = texCoordsBackByRotation[Surface.ROTATION_0] @Volatile private var updateTexCoordBuffer = false @@ -205,8 +212,7 @@ class TiltShiftRenderer( fun setFrontCamera(front: Boolean) { if (isFrontCamera != front) { isFrontCamera = front - currentTexCoords = if (front) texCoordsFront else texCoordsBack - updateTexCoordBuffer = true + refreshTexCoords() } } @@ -218,6 +224,27 @@ class TiltShiftRenderer( } } + /** + * Updates the active display rotation. The texture-coordinate buffer is + * rebuilt so the camera image stays world-aligned as the activity rotates + * with the device under screenOrientation="fullSensor", and the + * crop-to-fill math picks the correct effective aspect ratio. + */ + fun setDisplayRotation(rotation: Int) { + if (displayRotation != rotation) { + displayRotation = rotation + refreshTexCoords() + vertexBufferDirty = true + } + } + + private fun refreshTexCoords() { + val table = if (isFrontCamera) texCoordsFrontByRotation else texCoordsBackByRotation + val idx = displayRotation.coerceIn(0, table.size - 1) + currentTexCoords = table[idx] + updateTexCoordBuffer = true + } + fun release() { shader.release() surfaceTexture?.release() @@ -254,16 +281,22 @@ class TiltShiftRenderer( /** * Recomputes camera vertex positions to achieve crop-to-fill. * - * 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. + * The camera sensor is landscape; after the orientation-dependent texcoord + * rotation, the effective dimensions seen on screen are either swapped + * (portrait orientations) or kept (landscape orientations). 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 cameraRatio = cameraHeight.toFloat() / cameraWidth + 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 screenRatio = surfaceWidth.toFloat() / surfaceHeight if (cameraRatio > screenRatio) { diff --git a/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt b/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt index bbb706d..d5999ed 100644 --- a/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt +++ b/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt @@ -164,6 +164,14 @@ fun CameraScreen( } } + // Forward device rotation to renderer so the camera image stays + // world-aligned as the activity rotates with the device. + val currentRotation by viewModel.currentRotation.collectAsState() + LaunchedEffect(currentRotation, renderer) { + renderer?.setDisplayRotation(currentRotation) + glSurfaceView?.requestRender() + } + // Start camera when surface texture is available LaunchedEffect(surfaceTexture) { surfaceTexture?.let { diff --git a/version.properties b/version.properties index ba10bb5..aaf41eb 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ versionMajor=1 versionMinor=1 -versionPatch=10 -versionCode=12 +versionPatch=12 +versionCode=14