Compare commits

..

No commits in common. "1212604cf78b4a36902b630c84e876e9d7260504" and "4d755dce315af7744479e3e8f61fa07631789c39" have entirely different histories.

6 changed files with 20 additions and 174 deletions

View file

@ -14,15 +14,6 @@ val keystoreProperties = Properties().apply {
} }
} }
// Load version from version.properties
val versionProperties = Properties().apply {
load(rootProject.file("version.properties").inputStream())
}
val vMajor = versionProperties["versionMajor"].toString().toInt()
val vMinor = versionProperties["versionMinor"].toString().toInt()
val vPatch = versionProperties["versionPatch"].toString().toInt()
val vCode = versionProperties["versionCode"].toString().toInt()
android { android {
namespace = "no.naiv.tiltshift" namespace = "no.naiv.tiltshift"
compileSdk = 35 compileSdk = 35
@ -43,8 +34,8 @@ android {
applicationId = "no.naiv.tiltshift" applicationId = "no.naiv.tiltshift"
minSdk = 35 minSdk = 35
targetSdk = 35 targetSdk = 35
versionCode = vCode versionCode = 1
versionName = "$vMajor.$vMinor.$vPatch" versionName = "1.0.0"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true

View file

@ -59,9 +59,6 @@ class CameraManager(private val context: Context) {
private val _isFrontCamera = MutableStateFlow(false) private val _isFrontCamera = MutableStateFlow(false)
val isFrontCamera: StateFlow<Boolean> = _isFrontCamera.asStateFlow() val isFrontCamera: StateFlow<Boolean> = _isFrontCamera.asStateFlow()
private val _previewResolution = MutableStateFlow(Size(0, 0))
val previewResolution: StateFlow<Size> = _previewResolution.asStateFlow()
/** Background executor for image capture callbacks to avoid blocking the main thread. */ /** Background executor for image capture callbacks to avoid blocking the main thread. */
private val captureExecutor: ExecutorService = Executors.newSingleThreadExecutor() private val captureExecutor: ExecutorService = Executors.newSingleThreadExecutor()
@ -164,7 +161,6 @@ class CameraManager(private val context: Context) {
} }
surfaceSize = request.resolution surfaceSize = request.resolution
_previewResolution.value = surfaceSize
surfaceTexture.setDefaultBufferSize(surfaceSize.width, surfaceSize.height) surfaceTexture.setDefaultBufferSize(surfaceSize.width, surfaceSize.height)
val surface = Surface(surfaceTexture) val surface = Surface(surfaceTexture)

View file

@ -19,8 +19,7 @@ import javax.microedition.khronos.opengles.GL10
*/ */
class TiltShiftRenderer( class TiltShiftRenderer(
private val context: Context, private val context: Context,
private val onSurfaceTextureAvailable: (SurfaceTexture) -> Unit, private val onSurfaceTextureAvailable: (SurfaceTexture) -> Unit
private val onFrameAvailable: () -> Unit
) : GLSurfaceView.Renderer { ) : GLSurfaceView.Renderer {
private lateinit var shader: TiltShiftShader private lateinit var shader: TiltShiftShader
@ -40,13 +39,13 @@ class TiltShiftRenderer(
@Volatile @Volatile
private var isFrontCamera: Boolean = false private var isFrontCamera: Boolean = false
// Camera resolution for aspect ratio correction (set from UI thread) // Quad vertices (full screen)
@Volatile private val vertices = floatArrayOf(
private var cameraWidth: Int = 0 -1f, -1f, // Bottom left
@Volatile 1f, -1f, // Bottom right
private var cameraHeight: Int = 0 -1f, 1f, // Top left
@Volatile 1f, 1f // Top right
private var vertexBufferDirty: Boolean = false )
// Texture coordinates rotated 90° for portrait mode (back camera) // Texture coordinates rotated 90° for portrait mode (back camera)
// (Camera sensors are landscape-oriented, we rotate to portrait) // (Camera sensors are landscape-oriented, we rotate to portrait)
@ -76,12 +75,11 @@ class TiltShiftRenderer(
shader = TiltShiftShader(context) shader = TiltShiftShader(context)
shader.initialize() shader.initialize()
// Allocate vertex buffer (8 floats = 4 vertices × 2 components) // Create vertex buffer
vertexBuffer = ByteBuffer.allocateDirect(8 * 4) vertexBuffer = ByteBuffer.allocateDirect(vertices.size * 4)
.order(ByteOrder.nativeOrder()) .order(ByteOrder.nativeOrder())
.asFloatBuffer() .asFloatBuffer()
// Fill with default full-screen quad; will be recomputed when camera resolution is known .put(vertices)
vertexBuffer.put(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f))
vertexBuffer.position(0) vertexBuffer.position(0)
// Create texture coordinate buffer // Create texture coordinate buffer
@ -104,7 +102,6 @@ class TiltShiftRenderer(
// Create SurfaceTexture for camera frames // Create SurfaceTexture for camera frames
surfaceTexture = SurfaceTexture(cameraTextureId).also { surfaceTexture = SurfaceTexture(cameraTextureId).also {
it.setOnFrameAvailableListener { onFrameAvailable() }
onSurfaceTextureAvailable(it) onSurfaceTextureAvailable(it)
} }
} }
@ -113,19 +110,12 @@ class TiltShiftRenderer(
GLES20.glViewport(0, 0, width, height) GLES20.glViewport(0, 0, width, height)
surfaceWidth = width surfaceWidth = width
surfaceHeight = height surfaceHeight = height
vertexBufferDirty = true
} }
override fun onDrawFrame(gl: GL10?) { override fun onDrawFrame(gl: GL10?) {
// Update texture with latest camera frame // Update texture with latest camera frame
surfaceTexture?.updateTexImage() surfaceTexture?.updateTexImage()
// Recompute vertex buffer for crop-to-fill when camera or surface dimensions change
if (vertexBufferDirty) {
recomputeVertices()
vertexBufferDirty = false
}
// Update texture coordinate buffer if camera changed // Update texture coordinate buffer if camera changed
if (updateTexCoordBuffer) { if (updateTexCoordBuffer) {
texCoordBuffer.clear() texCoordBuffer.clear()
@ -192,53 +182,6 @@ class TiltShiftRenderer(
@Volatile @Volatile
private var updateTexCoordBuffer = false private var updateTexCoordBuffer = false
/**
* Sets the camera preview resolution for crop-to-fill aspect ratio correction.
* Thread-safe vertex buffer is recomputed on the next frame.
*/
fun setCameraResolution(width: Int, height: Int) {
if (cameraWidth != width || cameraHeight != height) {
cameraWidth = width
cameraHeight = height
vertexBufferDirty = true
}
}
/**
* Recomputes 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 screen 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) {
// After 90° rotation: portrait width = cameraHeight, portrait height = cameraWidth
val cameraRatio = cameraHeight.toFloat() / cameraWidth
val screenRatio = surfaceWidth.toFloat() / surfaceHeight
if (cameraRatio > screenRatio) {
// Camera wider than screen → crop sides
scaleX = cameraRatio / screenRatio
} else {
// Camera taller than screen → crop top/bottom
scaleY = screenRatio / cameraRatio
}
}
vertexBuffer.clear()
vertexBuffer.put(floatArrayOf(
-scaleX, -scaleY,
scaleX, -scaleY,
-scaleX, scaleY,
scaleX, scaleY
))
vertexBuffer.position(0)
}
/** /**
* Releases OpenGL resources. * Releases OpenGL resources.
* Must be called from GL thread. * Must be called from GL thread.

View file

@ -74,8 +74,6 @@ import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -118,7 +116,6 @@ fun CameraScreen(
val minZoom by viewModel.cameraManager.minZoomRatio.collectAsState() val minZoom by viewModel.cameraManager.minZoomRatio.collectAsState()
val maxZoom by viewModel.cameraManager.maxZoomRatio.collectAsState() val maxZoom by viewModel.cameraManager.maxZoomRatio.collectAsState()
val isFrontCamera by viewModel.cameraManager.isFrontCamera.collectAsState() val isFrontCamera by viewModel.cameraManager.isFrontCamera.collectAsState()
val previewResolution by viewModel.cameraManager.previewResolution.collectAsState()
val cameraError by viewModel.cameraManager.error.collectAsState() val cameraError by viewModel.cameraManager.error.collectAsState()
// Gallery picker // Gallery picker
@ -164,14 +161,6 @@ fun CameraScreen(
glSurfaceView?.requestRender() glSurfaceView?.requestRender()
} }
// Update renderer with camera preview resolution for crop-to-fill
LaunchedEffect(previewResolution) {
if (previewResolution.width > 0) {
renderer?.setCameraResolution(previewResolution.width, previewResolution.height)
glSurfaceView?.requestRender()
}
}
// Start camera when surface texture is available // Start camera when surface texture is available
LaunchedEffect(surfaceTexture) { LaunchedEffect(surfaceTexture) {
surfaceTexture?.let { surfaceTexture?.let {
@ -183,28 +172,14 @@ fun CameraScreen(
LaunchedEffect(isGalleryPreview) { LaunchedEffect(isGalleryPreview) {
if (isGalleryPreview) { if (isGalleryPreview) {
glSurfaceView?.onPause() glSurfaceView?.onPause()
} else if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { } else {
glSurfaceView?.onResume() glSurfaceView?.onResume()
} }
} }
// Tie GLSurfaceView lifecycle to Activity lifecycle to prevent background rendering // Cleanup GL resources on GL thread (ViewModel handles its own cleanup in onCleared)
val currentIsGalleryPreview by rememberUpdatedState(isGalleryPreview) DisposableEffect(Unit) {
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
if (!currentIsGalleryPreview) {
glSurfaceView?.onResume()
}
}
Lifecycle.Event.ON_PAUSE -> glSurfaceView?.onPause()
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
glSurfaceView?.queueEvent { renderer?.release() } glSurfaceView?.queueEvent { renderer?.release() }
} }
} }
@ -233,16 +208,13 @@ fun CameraScreen(
GLSurfaceView(ctx).apply { GLSurfaceView(ctx).apply {
setEGLContextClientVersion(2) setEGLContextClientVersion(2)
val view = this val newRenderer = TiltShiftRenderer(ctx) { st ->
val newRenderer = TiltShiftRenderer( surfaceTexture = st
context = ctx, }
onSurfaceTextureAvailable = { st -> surfaceTexture = st },
onFrameAvailable = { view.requestRender() }
)
renderer = newRenderer renderer = newRenderer
setRenderer(newRenderer) setRenderer(newRenderer)
renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
glSurfaceView = this glSurfaceView = this
} }

View file

@ -1,52 +0,0 @@
#!/usr/bin/env bash
# Bumps the version in version.properties before a release build.
# Usage: ./bump-version.sh [major|minor|patch]
# Default: patch
set -euo pipefail
PROPS_FILE="$(dirname "$0")/version.properties"
if [[ ! -f "$PROPS_FILE" ]]; then
echo "Error: $PROPS_FILE not found" >&2
exit 1
fi
# Read current values
major=$(grep '^versionMajor=' "$PROPS_FILE" | cut -d= -f2)
minor=$(grep '^versionMinor=' "$PROPS_FILE" | cut -d= -f2)
patch=$(grep '^versionPatch=' "$PROPS_FILE" | cut -d= -f2)
code=$(grep '^versionCode=' "$PROPS_FILE" | cut -d= -f2)
bump_type="${1:-patch}"
case "$bump_type" in
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
patch)
patch=$((patch + 1))
;;
*)
echo "Usage: $0 [major|minor|patch]" >&2
exit 1
;;
esac
code=$((code + 1))
# Write updated values
cat > "$PROPS_FILE" << EOF
versionMajor=$major
versionMinor=$minor
versionPatch=$patch
versionCode=$code
EOF
echo "$major.$minor.$patch (versionCode=$code)"

View file

@ -1,4 +0,0 @@
versionMajor=1
versionMinor=1
versionPatch=1
versionCode=3