Initial implementation of Tilt-Shift Camera Android app

A dedicated camera app for tilt-shift photography with:
- Real-time OpenGL ES 2.0 shader-based blur preview
- Touch gesture controls (drag, rotate, pinch) for adjusting effect
- CameraX integration for camera preview and high-res capture
- EXIF metadata with GPS location support
- MediaStore integration for saving to gallery
- Jetpack Compose UI with haptic feedback

Tech stack: Kotlin, CameraX, OpenGL ES 2.0, Jetpack Compose
Min SDK: 26 (Android 8.0), Target SDK: 35 (Android 15)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-01-28 15:26:41 +01:00
commit 07e10ac9c3
38 changed files with 3489 additions and 0 deletions

View file

@ -0,0 +1,159 @@
package no.naiv.tiltshift.effect
import android.content.Context
import android.graphics.SurfaceTexture
import android.opengl.GLES11Ext
import android.opengl.GLES20
import android.opengl.GLSurfaceView
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
/**
* OpenGL renderer for applying tilt-shift effect to camera preview.
*
* This renderer receives camera frames via SurfaceTexture and applies
* the tilt-shift blur effect using GLSL shaders.
*/
class TiltShiftRenderer(
private val context: Context,
private val onSurfaceTextureAvailable: (SurfaceTexture) -> Unit
) : GLSurfaceView.Renderer {
private lateinit var shader: TiltShiftShader
private var surfaceTexture: SurfaceTexture? = null
private var cameraTextureId: Int = 0
private lateinit var vertexBuffer: FloatBuffer
private lateinit var texCoordBuffer: FloatBuffer
private var surfaceWidth: Int = 0
private var surfaceHeight: Int = 0
// Current effect parameters (updated from UI thread)
@Volatile
var blurParameters: BlurParameters = BlurParameters.DEFAULT
// Quad vertices (full screen)
private val vertices = floatArrayOf(
-1f, -1f, // Bottom left
1f, -1f, // Bottom right
-1f, 1f, // Top left
1f, 1f // Top right
)
// Texture coordinates (flip Y for camera)
private val texCoords = floatArrayOf(
0f, 1f, // Bottom left
1f, 1f, // Bottom right
0f, 0f, // Top left
1f, 0f // Top right
)
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
GLES20.glClearColor(0f, 0f, 0f, 1f)
// Initialize shader
shader = TiltShiftShader(context)
shader.initialize()
// Create vertex buffer
vertexBuffer = ByteBuffer.allocateDirect(vertices.size * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertices)
vertexBuffer.position(0)
// Create texture coordinate buffer
texCoordBuffer = ByteBuffer.allocateDirect(texCoords.size * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(texCoords)
texCoordBuffer.position(0)
// Create camera texture
val textures = IntArray(1)
GLES20.glGenTextures(1, textures, 0)
cameraTextureId = textures[0]
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTextureId)
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
// Create SurfaceTexture for camera frames
surfaceTexture = SurfaceTexture(cameraTextureId).also {
onSurfaceTextureAvailable(it)
}
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
surfaceWidth = width
surfaceHeight = height
}
override fun onDrawFrame(gl: GL10?) {
// Update texture with latest camera frame
surfaceTexture?.updateTexImage()
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
// Use shader and set parameters
shader.use(cameraTextureId, blurParameters, surfaceWidth, surfaceHeight)
// Set vertex positions
GLES20.glEnableVertexAttribArray(shader.aPositionLocation)
GLES20.glVertexAttribPointer(
shader.aPositionLocation,
2,
GLES20.GL_FLOAT,
false,
0,
vertexBuffer
)
// Set texture coordinates
GLES20.glEnableVertexAttribArray(shader.aTexCoordLocation)
GLES20.glVertexAttribPointer(
shader.aTexCoordLocation,
2,
GLES20.GL_FLOAT,
false,
0,
texCoordBuffer
)
// Draw quad
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
// Cleanup
GLES20.glDisableVertexAttribArray(shader.aPositionLocation)
GLES20.glDisableVertexAttribArray(shader.aTexCoordLocation)
}
/**
* Updates blur parameters. Thread-safe.
*/
fun updateParameters(params: BlurParameters) {
blurParameters = params
}
/**
* Releases OpenGL resources.
* Must be called from GL thread.
*/
fun release() {
shader.release()
surfaceTexture?.release()
surfaceTexture = null
if (cameraTextureId != 0) {
GLES20.glDeleteTextures(1, intArrayOf(cameraTextureId), 0)
cameraTextureId = 0
}
}
}