Add CLAUDE.md with architecture, patterns, and recent fixes
Documents the app's purpose, architecture, rendering pipeline, build instructions, and key design decisions (bitmap lifecycle, thread safety, error handling) established during the recent audit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
11a79076bc
commit
c3e4dc0e79
1 changed files with 102 additions and 0 deletions
102
CLAUDE.md
Normal file
102
CLAUDE.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Tilt-Shift Camera
|
||||
|
||||
Android camera app that applies a real-time tilt-shift (miniature/diorama) blur effect to the camera preview and to imported gallery images. Built with Kotlin, Jetpack Compose, CameraX, and OpenGL ES 2.0.
|
||||
|
||||
## What it does
|
||||
|
||||
- Live camera preview with GPU-accelerated tilt-shift blur via GLSL fragment shader
|
||||
- Gallery import with CPU-based preview that updates in real time as you adjust parameters
|
||||
- Supports both **linear** (band) and **radial** (elliptical) blur modes
|
||||
- Gesture controls: drag to position, pinch to resize, two-finger rotate
|
||||
- Slider panel for precise control of blur, falloff, size, angle, aspect ratio
|
||||
- Multi-lens support on devices with multiple back cameras
|
||||
- EXIF GPS tagging from device location
|
||||
- Saves processed images to MediaStore (scoped storage)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
no.naiv.tiltshift/
|
||||
MainActivity.kt # Entry point, permissions, edge-to-edge setup
|
||||
ui/
|
||||
CameraScreen.kt # Main Compose UI, GL surface, controls
|
||||
CameraViewModel.kt # State management, gallery preview loop, bitmap lifecycle
|
||||
TiltShiftOverlay.kt # Gesture handling and visual guides
|
||||
ZoomControl.kt # Zoom presets and indicator
|
||||
LensSwitcher.kt # Multi-lens picker
|
||||
theme/AppColors.kt # Color constants
|
||||
camera/
|
||||
CameraManager.kt # CameraX lifecycle, zoom, lens binding
|
||||
ImageCaptureHandler.kt # Capture pipeline, CPU blur/mask, gallery processing
|
||||
LensController.kt # Enumerates physical camera lenses
|
||||
effect/
|
||||
TiltShiftRenderer.kt # GLSurfaceView.Renderer for live camera preview
|
||||
TiltShiftShader.kt # Compiles GLSL, sets uniforms (incl. precomputed trig)
|
||||
BlurParameters.kt # Data class for all effect parameters
|
||||
storage/
|
||||
PhotoSaver.kt # MediaStore writes, EXIF metadata, IS_PENDING pattern
|
||||
SaveResult.kt # Sealed class for save outcomes
|
||||
util/
|
||||
LocationProvider.kt # FusedLocationProvider flow (accepts coarse or fine)
|
||||
OrientationDetector.kt # Device rotation for EXIF
|
||||
HapticFeedback.kt # Null-safe vibration wrapper
|
||||
```
|
||||
|
||||
### Rendering pipeline
|
||||
|
||||
- **Camera preview**: OpenGL ES 2.0 via `GLSurfaceView` + `TiltShiftRenderer`. Camera frames arrive as `GL_TEXTURE_EXTERNAL_OES` from a `SurfaceTexture`. The fragment shader (`tiltshift_fragment.glsl`) applies blur per-fragment using precomputed `uCosAngle`/`uSinAngle` uniforms and an unrolled 9-tap Gaussian kernel.
|
||||
- **Gallery preview**: CPU-based. A 1024px-max downscaled source is kept in `galleryPreviewSource`. `CameraViewModel.startPreviewLoop()` uses `collectLatest` on blur params (with 80ms debounce) to reactively recompute the preview via `ImageCaptureHandler.applyTiltShiftPreview()`.
|
||||
- **Final save**: Full-resolution CPU pipeline — stack blur at 1/4 scale, gradient mask at 1/4 scale with bilinear upscale, per-pixel compositing. Camera captures save both original + processed; gallery imports save only the processed version (original already on device).
|
||||
|
||||
## Build & run
|
||||
|
||||
```bash
|
||||
./gradlew assembleRelease # Build release APK
|
||||
./gradlew compileDebugKotlin # Quick compile check
|
||||
adb install -r app/build/outputs/apk/release/naiv-tilt-shift-release.apk
|
||||
```
|
||||
|
||||
Signing config is loaded from `local.properties` (not committed).
|
||||
|
||||
## Key design decisions and patterns
|
||||
|
||||
### Bitmap lifecycle (important!)
|
||||
|
||||
Bitmaps emitted to `StateFlow`s are **never eagerly recycled** immediately after replacement. Compose may still be drawing the old bitmap in the current frame. Instead:
|
||||
|
||||
- A `pendingRecyclePreview` / `pendingRecycleThumbnail` field holds the bitmap from the *previous* update
|
||||
- On the *next* update, the pending bitmap is recycled (Compose has had a full frame to finish)
|
||||
- Final cleanup happens in `cancelGalleryPreview()` (which `join()`s the preview job first) and `onCleared()`
|
||||
|
||||
### Thread safety
|
||||
|
||||
- `galleryPreviewSource` is `@Volatile` (accessed from Main thread, IO dispatcher, and cancel path)
|
||||
- `TiltShiftRenderer.currentTexCoords` is `@Volatile` (written by UI thread, read by GL thread)
|
||||
- `cancelGalleryPreview()` cancels + `join()`s the preview job before recycling the source bitmap, because `applyTiltShiftEffect` is a long CPU loop with no suspension points
|
||||
- GL resources are released via `glSurfaceView.queueEvent {}` (must run on GL thread)
|
||||
- `CameraManager.captureExecutor` is shut down in `release()` to prevent thread leaks
|
||||
|
||||
### Error handling
|
||||
|
||||
- `bitmap.compress()` return value is checked; failure reported to user
|
||||
- `loadBitmapFromUri()` logs all null-return paths (stream open, dimensions, decode)
|
||||
- Error/success dismiss indicators use cancellable `Job` tracking to prevent race conditions
|
||||
- `writeExifToUri()` returns boolean and logs at ERROR level on failure
|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Purpose | Notes |
|
||||
|-----------|---------|-------|
|
||||
| `CAMERA` | Camera preview and capture | Required |
|
||||
| `ACCESS_FINE_LOCATION` | GPS EXIF tagging | Optional; coarse-only grant also works |
|
||||
| `ACCESS_COARSE_LOCATION` | GPS EXIF tagging | Fallback if fine denied |
|
||||
| `ACCESS_MEDIA_LOCATION` | Read GPS from gallery images | Required on Android 10+ |
|
||||
| `VIBRATE` | Haptic feedback | Always granted |
|
||||
|
||||
## Known limitations / future work
|
||||
|
||||
- `minSdk = 35` (Android 15) — intentional for personal use. Lower to 26-29 if distributing.
|
||||
- Accompanist Permissions (`0.36.0`) is deprecated; should migrate to first-party `activity-compose` API.
|
||||
- No user-facing toggle to disable GPS tagging — location is embedded whenever permission is granted.
|
||||
- Dependencies are pinned to late-2024 versions; periodic bumps recommended.
|
||||
- Fragment shader uses `int` uniform branching in GLSL ES 1.00 — works but could be cleaner with ES 3.00.
|
||||
Loading…
Add table
Add a link
Reference in a new issue