Add radial mode, UI controls, front camera, update to API 35
- Add radial/elliptical blur mode with aspect ratio control - Add UI sliders for blur intensity, falloff, and shape - Add front camera support with flip button - Update minimum SDK to API 35 (Android 15) - Enable landscape orientation (fullSensor) - Rename app to "Naiv Tilt Shift Camera" - Set APK output name to naiv-tilt-shift - Add project specification document Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e8a5fa4811
commit
d3ca23b71c
11 changed files with 679 additions and 94 deletions
|
|
@ -22,20 +22,23 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.FlipCameraAndroid
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
|
|
@ -54,6 +57,7 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
import kotlinx.coroutines.launch
|
||||
import no.naiv.tiltshift.camera.CameraManager
|
||||
import no.naiv.tiltshift.camera.ImageCaptureHandler
|
||||
import no.naiv.tiltshift.effect.BlurMode
|
||||
import no.naiv.tiltshift.effect.BlurParameters
|
||||
import no.naiv.tiltshift.effect.TiltShiftRenderer
|
||||
import no.naiv.tiltshift.storage.PhotoSaver
|
||||
|
|
@ -90,6 +94,7 @@ fun CameraScreen(
|
|||
var isCapturing by remember { mutableStateOf(false) }
|
||||
var showSaveSuccess by remember { mutableStateOf(false) }
|
||||
var showSaveError by remember { mutableStateOf<String?>(null) }
|
||||
var showControls by remember { mutableStateOf(false) }
|
||||
|
||||
var currentRotation by remember { mutableStateOf(Surface.ROTATION_0) }
|
||||
var currentLocation by remember { mutableStateOf<Location?>(null) }
|
||||
|
|
@ -97,6 +102,7 @@ fun CameraScreen(
|
|||
val zoomRatio by cameraManager.zoomRatio.collectAsState()
|
||||
val minZoom by cameraManager.minZoomRatio.collectAsState()
|
||||
val maxZoom by cameraManager.maxZoomRatio.collectAsState()
|
||||
val isFrontCamera by cameraManager.isFrontCamera.collectAsState()
|
||||
|
||||
// Collect orientation updates
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
@ -172,30 +178,86 @@ fun CameraScreen(
|
|||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
// Top bar
|
||||
Row(
|
||||
// Top bar with controls
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// Zoom indicator
|
||||
ZoomIndicator(currentZoom = zoomRatio)
|
||||
|
||||
// Settings button (placeholder)
|
||||
IconButton(
|
||||
onClick = { /* TODO: Settings */ }
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = "Settings",
|
||||
tint = Color.White
|
||||
// Zoom indicator
|
||||
ZoomIndicator(currentZoom = zoomRatio)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// Camera flip button
|
||||
IconButton(
|
||||
onClick = {
|
||||
cameraManager.switchCamera()
|
||||
haptics.click()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FlipCameraAndroid,
|
||||
contentDescription = "Switch Camera",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
// Toggle controls button
|
||||
IconButton(
|
||||
onClick = {
|
||||
showControls = !showControls
|
||||
haptics.tick()
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = if (showControls) "Hide" else "Ctrl",
|
||||
color = Color.White,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mode toggle
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
ModeToggle(
|
||||
currentMode = blurParams.mode,
|
||||
onModeChange = { mode ->
|
||||
blurParams = blurParams.copy(mode = mode)
|
||||
haptics.click()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Control panel (sliders)
|
||||
AnimatedVisibility(
|
||||
visible = showControls,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = 16.dp)
|
||||
) {
|
||||
ControlPanel(
|
||||
params = blurParams,
|
||||
onParamsChange = { newParams ->
|
||||
blurParams = newParams
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Bottom controls
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -204,18 +266,20 @@ fun CameraScreen(
|
|||
.padding(bottom = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Zoom presets
|
||||
ZoomControl(
|
||||
currentZoom = zoomRatio,
|
||||
minZoom = minZoom,
|
||||
maxZoom = maxZoom,
|
||||
onZoomSelected = { zoom ->
|
||||
cameraManager.setZoom(zoom)
|
||||
haptics.click()
|
||||
}
|
||||
)
|
||||
// Zoom presets (only show for back camera)
|
||||
if (!isFrontCamera) {
|
||||
ZoomControl(
|
||||
currentZoom = zoomRatio,
|
||||
minZoom = minZoom,
|
||||
maxZoom = maxZoom,
|
||||
onZoomSelected = { zoom ->
|
||||
cameraManager.setZoom(zoom)
|
||||
haptics.click()
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
// Capture button
|
||||
CaptureButton(
|
||||
|
|
@ -234,7 +298,7 @@ fun CameraScreen(
|
|||
blurParams = blurParams,
|
||||
deviceRotation = currentRotation,
|
||||
location = currentLocation,
|
||||
isFrontCamera = false
|
||||
isFrontCamera = isFrontCamera
|
||||
)
|
||||
|
||||
when (result) {
|
||||
|
|
@ -294,7 +358,7 @@ fun CameraScreen(
|
|||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.clip(androidx.compose.foundation.shape.RoundedCornerShape(16.dp))
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Color(0xFFF44336))
|
||||
.padding(24.dp)
|
||||
) {
|
||||
|
|
@ -315,6 +379,140 @@ fun CameraScreen(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode toggle for Linear / Radial blur modes.
|
||||
*/
|
||||
@Composable
|
||||
private fun ModeToggle(
|
||||
currentMode: BlurMode,
|
||||
onModeChange: (BlurMode) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(Color(0x80000000))
|
||||
.padding(4.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
ModeButton(
|
||||
text = "Linear",
|
||||
isSelected = currentMode == BlurMode.LINEAR,
|
||||
onClick = { onModeChange(BlurMode.LINEAR) }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
ModeButton(
|
||||
text = "Radial",
|
||||
isSelected = currentMode == BlurMode.RADIAL,
|
||||
onClick = { onModeChange(BlurMode.RADIAL) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModeButton(
|
||||
text: String,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(if (isSelected) Color(0xFFFFB300) else Color.Transparent)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = if (isSelected) Color.Black else Color.White,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Control panel with sliders for blur parameters.
|
||||
*/
|
||||
@Composable
|
||||
private fun ControlPanel(
|
||||
params: BlurParameters,
|
||||
onParamsChange: (BlurParameters) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(200.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Color(0xCC000000))
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Blur intensity slider
|
||||
SliderControl(
|
||||
label = "Blur",
|
||||
value = params.blurAmount,
|
||||
valueRange = BlurParameters.MIN_BLUR..BlurParameters.MAX_BLUR,
|
||||
onValueChange = { onParamsChange(params.copy(blurAmount = it)) }
|
||||
)
|
||||
|
||||
// Falloff slider
|
||||
SliderControl(
|
||||
label = "Falloff",
|
||||
value = params.falloff,
|
||||
valueRange = BlurParameters.MIN_FALLOFF..BlurParameters.MAX_FALLOFF,
|
||||
onValueChange = { onParamsChange(params.copy(falloff = it)) }
|
||||
)
|
||||
|
||||
// Aspect ratio slider (radial mode only)
|
||||
if (params.mode == BlurMode.RADIAL) {
|
||||
SliderControl(
|
||||
label = "Shape",
|
||||
value = params.aspectRatio,
|
||||
valueRange = BlurParameters.MIN_ASPECT..BlurParameters.MAX_ASPECT,
|
||||
onValueChange = { onParamsChange(params.copy(aspectRatio = it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SliderControl(
|
||||
label: String,
|
||||
value: Float,
|
||||
valueRange: ClosedFloatingPointRange<Float>,
|
||||
onValueChange: (Float) -> Unit
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
color = Color.White,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Text(
|
||||
text = "${(value * 100).toInt()}%",
|
||||
color = Color.White.copy(alpha = 0.7f),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
Slider(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
valueRange = valueRange,
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = Color(0xFFFFB300),
|
||||
activeTrackColor = Color(0xFFFFB300),
|
||||
inactiveTrackColor = Color.White.copy(alpha = 0.3f)
|
||||
),
|
||||
modifier = Modifier.height(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture button with animation for capturing state.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue