diff --git a/.dogcats/config.toml b/.dogcats/config.toml new file mode 100644 index 0000000..7d9577a --- /dev/null +++ b/.dogcats/config.toml @@ -0,0 +1 @@ +namespace = "tilt-shift-camera" diff --git a/.dogcats/issues.jsonl b/.dogcats/issues.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8598de8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Dogcat JSONL merge driver +.dogcats/*.jsonl merge=dcat-jsonl diff --git a/.gitignore b/.gitignore index b084b0c..ac015b8 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,5 @@ lint/tmp/ ehthumbs.db Thumbs.db .signing/ +.dogcats/config.local.toml +.dogcats/.issues.lock diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d180f1e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,30 @@ +# Agent Instructions + +## Issue tracking + +This project uses **dcat** for issue tracking. You MUST run `dcat prime --opinionated` for instructions. +Then run `dcat list --agent-only` to see the list of issues. Generally we work on bugs first, and always on high priority issues first. + +When running multiple `dcat` commands, make separate parallel Bash tool calls instead of chaining them with `&&` and `echo` separators. + +Mark each issue `in_progress` right when you start working on it — not before. Set `in_review` when work on that issue is done before moving on. The status should reflect what you are *actually* working on right now. + +It is okay to work on multiple related issues at the same time, but do NOT batch-mark an entire backlog as `in_progress` upfront. If there is a priority conflict, ask the user which to focus on first. + +If the user brings up a new bug, feature or anything else that warrants changes to the code, ALWAYS ask if we should create an issue for it before you start working on the code. When creating issues, set appropriate labels using `--labels` based on the issue content (e.g. `cli`, `tui`, `api`, `docs`, `testing`, `refactor`, `ux`, `performance`, etc.). + +When research or discussion produces findings relevant to an existing issue, ask these as **separate questions in order**: + +1. First ask: "Should I update issue [id] with these findings?" +2. Only after that, separately ask: "Should I start working on the implementation?" +Do NOT combine these into one question. The user may want to update the issue without starting work. + +### Closing Issues - IMPORTANT + +NEVER close issues without explicit user approval. When work is complete: + +1. Set status to `in_review`: `dcat update --status in_review $issueId` +2. Ask the user to test +3. Ask if we can close it: "Can I close issue [id] '[title]'?" +4. Only run `dcat close` after user confirms +5. Ask: "Should I add this to CHANGELOG.md?" — update if yes diff --git a/CLAUDE.md b/CLAUDE.md index 09d9b5d..249fe6a 100644 --- a/CLAUDE.md +++ b/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 | @@ -101,3 +116,6 @@ Bitmaps emitted to `StateFlow`s are **never eagerly recycled** immediately after - `minSdk = 35` (Android 15) — intentional for personal use. Lower to 26-29 if distributing. - Dependencies updated to March 2026 versions (AGP 9.1, Kotlin 2.3, Compose BOM 2026.03). - Fragment shader uses `int` uniform branching in GLSL ES 1.00 — works but could be cleaner with ES 3.00. + +## Issue tracking & task management +- Do NOT use the `TodoWrite` tool in this project. Issue tracking and progress is managed with **dcat** as specified in `AGENTS.md` — see that file for the full workflow (`dcat prime --opinionated`, `dcat list --agent-only`, status transitions, closing rules, etc.). diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c56c4e9..9fed438 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,7 +31,7 @@ diff --git a/app/src/main/java/no/naiv/tiltshift/camera/CameraManager.kt b/app/src/main/java/no/naiv/tiltshift/camera/CameraManager.kt index b6f659d..51d42ed 100644 --- a/app/src/main/java/no/naiv/tiltshift/camera/CameraManager.kt +++ b/app/src/main/java/no/naiv/tiltshift/camera/CameraManager.kt @@ -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? = 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,19 +173,6 @@ class CameraManager(private val context: Context) { } } - /** - * Updates the target rotation for Preview and ImageCapture use cases. - * Call when the device rotates: this rotates the SurfaceTexture transform matrix - * (so the GL preview stays upright) and tags captures with the right orientation. - * Safe to call on the main thread; CameraX permits live target-rotation updates. - */ - fun setTargetRotation(rotation: Int) { - if (targetRotation == rotation) return - targetRotation = rotation - preview?.targetRotation = rotation - imageCapture?.targetRotation = rotation - } - /** * 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). diff --git a/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt b/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt index b0401e7..b93db9a 100644 --- a/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt +++ b/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt @@ -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) { 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 98c640f..d170d58 100644 --- a/app/src/main/java/no/naiv/tiltshift/effect/TiltShiftRenderer.kt +++ b/app/src/main/java/no/naiv/tiltshift/effect/TiltShiftRenderer.kt @@ -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) { - isFrontCamera = front + 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) { diff --git a/app/src/main/java/no/naiv/tiltshift/effect/TiltShiftShader.kt b/app/src/main/java/no/naiv/tiltshift/effect/TiltShiftShader.kt index ebbfb18..cdafd8f 100644 --- a/app/src/main/java/no/naiv/tiltshift/effect/TiltShiftShader.kt +++ b/app/src/main/java/no/naiv/tiltshift/effect/TiltShiftShader.kt @@ -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) 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 167b376..bbb706d 100644 --- a/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt +++ b/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt @@ -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 { diff --git a/app/src/main/java/no/naiv/tiltshift/ui/ControlPanel.kt b/app/src/main/java/no/naiv/tiltshift/ui/ControlPanel.kt index dbc5112..ad94ad0 100644 --- a/app/src/main/java/no/naiv/tiltshift/ui/ControlPanel.kt +++ b/app/src/main/java/no/naiv/tiltshift/ui/ControlPanel.kt @@ -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) ) { diff --git a/app/src/main/res/raw/tiltshift_vertex.glsl b/app/src/main/res/raw/tiltshift_vertex.glsl index ca79319..e009424 100644 --- a/app/src/main/res/raw/tiltshift_vertex.glsl +++ b/app/src/main/res/raw/tiltshift_vertex.glsl @@ -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; } diff --git a/version.properties b/version.properties index 7f96eb7..214906f 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ versionMajor=1 versionMinor=1 -versionPatch=6 -versionCode=8 +versionPatch=15 +versionCode=17