Lock activity to portrait; drop all camera-image rotation tracking

Stop trying to rotate the camera image based on device orientation.
The activity is now locked to portrait (screenOrientation="portrait"),
so the GL surface stays portrait-sized regardless of how the device
is held, and the camera passthrough goes back to the simple
texCoordsBack 90° rotation that was working before any of the
v1.1.6–1.1.13 attempts at landscape support.

Net effect: the camera image stays in the device's portrait frame
and visually follows the phone as it tilts (since there is no
inverse rotation cancelling it). The UI is also locked to the
portrait layout for now — a follow-up will add Modifier.graphicsLayer
rotations to the icon overlays so they stay readable when the phone
is held sideways. screenOrientation switched from fullSensor to
portrait; the rest of the file changes are reverts of the orientation
plumbing introduced in v1.1.6 and its follow-ups.

Bump to 1.1.14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-05-11 16:44:23 +02:00
commit 356bf41423
4 changed files with 24 additions and 78 deletions

View file

@ -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">

View file

@ -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
@ -70,32 +69,26 @@ class TiltShiftRenderer(
@Volatile
private var vertexBufferDirty: Boolean = false
// 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 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
)
// 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
// 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 displayRotation: Int = Surface.ROTATION_0
@Volatile
private var currentTexCoords = texCoordsBackByRotation[Surface.ROTATION_0]
private var currentTexCoords = texCoordsBack
@Volatile
private var updateTexCoordBuffer = false
@ -212,7 +205,8 @@ class TiltShiftRenderer(
fun setFrontCamera(front: Boolean) {
if (isFrontCamera != front) {
isFrontCamera = front
refreshTexCoords()
currentTexCoords = if (front) texCoordsFront else texCoordsBack
updateTexCoordBuffer = true
}
}
@ -224,27 +218,6 @@ 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()
@ -281,22 +254,16 @@ class TiltShiftRenderer(
/**
* Recomputes camera vertex positions to achieve crop-to-fill.
*
* 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.
* 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) {

View file

@ -1,13 +1,9 @@
package no.naiv.tiltshift.ui
import android.content.Context
import android.content.Intent
import android.graphics.SurfaceTexture
import android.hardware.display.DisplayManager
import android.opengl.GLSurfaceView
import android.util.Log
import android.view.Display
import android.view.Surface
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@ -168,23 +164,6 @@ fun CameraScreen(
}
}
// Forward the activity's actual rotation (Display.rotation) to the
// renderer so the camera image stays world-aligned as the activity rotates
// with the device. Don't drive this from OrientationEventListener — its
// 45° threshold fires *before* the activity has rotated, briefly leaving
// the texcoord set out of sync with the GL surface orientation.
// LocalConfiguration triggers a recomposition on configuration change,
// which is when Display.rotation can have changed.
val configuration = androidx.compose.ui.platform.LocalConfiguration.current
val displayRotation = remember(configuration) {
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.getDisplay(Display.DEFAULT_DISPLAY)?.rotation ?: Surface.ROTATION_0
}
LaunchedEffect(displayRotation, renderer) {
renderer?.setDisplayRotation(displayRotation)
glSurfaceView?.requestRender()
}
// Start camera when surface texture is available
LaunchedEffect(surfaceTexture) {
surfaceTexture?.let {

View file

@ -1,4 +1,4 @@
versionMajor=1
versionMinor=1
versionPatch=13
versionCode=15
versionPatch=14
versionCode=16