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
|
|
@ -8,13 +8,13 @@ import androidx.compose.foundation.gestures.calculateZoom
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
|
@ -23,11 +23,13 @@ import androidx.compose.ui.graphics.drawscope.rotate
|
|||
import androidx.compose.ui.input.pointer.PointerEventType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.naiv.tiltshift.effect.BlurMode
|
||||
import no.naiv.tiltshift.effect.BlurParameters
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* Type of gesture being performed.
|
||||
|
|
@ -182,8 +184,8 @@ fun TiltShiftOverlay(
|
|||
* Determines the type of two-finger gesture based on touch position.
|
||||
*
|
||||
* Zones (from center outward):
|
||||
* - Very center (< 30% of focus height): Rotation
|
||||
* - Near focus line (30% - 200% of focus height): Size adjustment
|
||||
* - Very center (< 30% of focus size): Rotation
|
||||
* - Near focus region (30% - 200% of focus size): Size adjustment
|
||||
* - Far outside (> 200%): Camera zoom
|
||||
*/
|
||||
private fun determineGestureType(
|
||||
|
|
@ -192,39 +194,50 @@ private fun determineGestureType(
|
|||
height: Float,
|
||||
params: BlurParameters
|
||||
): GestureType {
|
||||
// Calculate distance from focus center
|
||||
val focusCenterX = width * params.positionX
|
||||
val focusCenterY = height * params.positionY
|
||||
val focusHalfHeight = height * params.size * 0.5f
|
||||
val focusSize = height * params.size * 0.5f
|
||||
|
||||
// Rotate centroid to align with focus line
|
||||
val dx = centroid.x - focusCenterX
|
||||
val dy = centroid.y - focusCenterY
|
||||
val rotatedY = -dx * sin(params.angle) + dy * cos(params.angle)
|
||||
|
||||
val distFromCenter = kotlin.math.abs(rotatedY)
|
||||
val distFromCenter = when (params.mode) {
|
||||
BlurMode.LINEAR -> {
|
||||
// For linear mode, use perpendicular distance to focus line
|
||||
val rotatedY = -dx * sin(params.angle) + dy * cos(params.angle)
|
||||
kotlin.math.abs(rotatedY)
|
||||
}
|
||||
BlurMode.RADIAL -> {
|
||||
// For radial mode, use distance from center
|
||||
sqrt(dx * dx + dy * dy)
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
// Very center of focus zone -> rotation (small area)
|
||||
distFromCenter < focusHalfHeight * 0.3f -> {
|
||||
GestureType.ROTATE
|
||||
}
|
||||
// Anywhere near the blur effect -> size adjustment (large area)
|
||||
distFromCenter < focusHalfHeight * 2.0f -> {
|
||||
GestureType.PINCH_SIZE
|
||||
}
|
||||
distFromCenter < focusSize * 0.3f -> GestureType.ROTATE
|
||||
// Near the blur effect -> size adjustment (large area)
|
||||
distFromCenter < focusSize * 2.0f -> GestureType.PINCH_SIZE
|
||||
// Far outside -> camera zoom
|
||||
else -> {
|
||||
GestureType.PINCH_ZOOM
|
||||
}
|
||||
else -> GestureType.PINCH_ZOOM
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the tilt-shift visualization overlay.
|
||||
* Uses extended geometry so rotated elements don't clip at screen edges.
|
||||
* Supports both linear and radial modes.
|
||||
*/
|
||||
private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) {
|
||||
when (params.mode) {
|
||||
BlurMode.LINEAR -> drawLinearOverlay(params)
|
||||
BlurMode.RADIAL -> drawRadialOverlay(params)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the linear mode overlay (horizontal band with rotation).
|
||||
*/
|
||||
private fun DrawScope.drawLinearOverlay(params: BlurParameters) {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
|
||||
|
|
@ -239,23 +252,23 @@ private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) {
|
|||
val dashEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 10f), 0f)
|
||||
|
||||
// Calculate diagonal for extended drawing (ensures coverage when rotated)
|
||||
val diagonal = kotlin.math.sqrt(width * width + height * height)
|
||||
val extendedHalf = diagonal // Extend lines/rects well beyond screen
|
||||
val diagonal = sqrt(width * width + height * height)
|
||||
val extendedHalf = diagonal
|
||||
|
||||
rotate(angleDegrees, pivot = Offset(centerX, centerY)) {
|
||||
// Draw blur zone indicators (top and bottom) - extended horizontally
|
||||
// Draw blur zone indicators (top and bottom)
|
||||
drawRect(
|
||||
color = blurZoneColor,
|
||||
topLeft = Offset(centerX - extendedHalf, centerY - focusHalfHeight - extendedHalf),
|
||||
size = androidx.compose.ui.geometry.Size(extendedHalf * 2, extendedHalf)
|
||||
size = Size(extendedHalf * 2, extendedHalf)
|
||||
)
|
||||
drawRect(
|
||||
color = blurZoneColor,
|
||||
topLeft = Offset(centerX - extendedHalf, centerY + focusHalfHeight),
|
||||
size = androidx.compose.ui.geometry.Size(extendedHalf * 2, extendedHalf)
|
||||
size = Size(extendedHalf * 2, extendedHalf)
|
||||
)
|
||||
|
||||
// Draw focus zone boundary lines - extended horizontally
|
||||
// Draw focus zone boundary lines
|
||||
drawLine(
|
||||
color = focusLineColor,
|
||||
start = Offset(centerX - extendedHalf, centerY - focusHalfHeight),
|
||||
|
|
@ -271,7 +284,7 @@ private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) {
|
|||
pathEffect = dashEffect
|
||||
)
|
||||
|
||||
// Draw center focus line - extended horizontally
|
||||
// Draw center focus line
|
||||
drawLine(
|
||||
color = focusLineColor,
|
||||
start = Offset(centerX - extendedHalf, centerY),
|
||||
|
|
@ -298,3 +311,70 @@ private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the radial mode overlay (ellipse/circle).
|
||||
*/
|
||||
private fun DrawScope.drawRadialOverlay(params: BlurParameters) {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
|
||||
val centerX = width * params.positionX
|
||||
val centerY = height * params.positionY
|
||||
val focusRadius = height * params.size * 0.5f
|
||||
val angleDegrees = params.angle * (180f / PI.toFloat())
|
||||
|
||||
// Colors for overlay
|
||||
val focusLineColor = Color(0xFFFFB300) // Amber
|
||||
val blurZoneColor = Color(0x30FFFFFF) // Semi-transparent white
|
||||
val dashEffect = PathEffect.dashPathEffect(floatArrayOf(15f, 8f), 0f)
|
||||
|
||||
// Calculate ellipse dimensions based on aspect ratio
|
||||
val ellipseWidth = focusRadius * 2 * params.aspectRatio
|
||||
val ellipseHeight = focusRadius * 2
|
||||
|
||||
rotate(angleDegrees, pivot = Offset(centerX, centerY)) {
|
||||
// Draw focus ellipse outline (inner boundary)
|
||||
drawOval(
|
||||
color = focusLineColor,
|
||||
topLeft = Offset(centerX - ellipseWidth / 2, centerY - ellipseHeight / 2),
|
||||
size = Size(ellipseWidth, ellipseHeight),
|
||||
style = Stroke(width = 3.dp.toPx())
|
||||
)
|
||||
|
||||
// Draw outer blur boundary (with falloff)
|
||||
val outerScale = 1f + params.falloff
|
||||
drawOval(
|
||||
color = focusLineColor.copy(alpha = 0.5f),
|
||||
topLeft = Offset(
|
||||
centerX - (ellipseWidth * outerScale) / 2,
|
||||
centerY - (ellipseHeight * outerScale) / 2
|
||||
),
|
||||
size = Size(ellipseWidth * outerScale, ellipseHeight * outerScale),
|
||||
style = Stroke(width = 2.dp.toPx(), pathEffect = dashEffect)
|
||||
)
|
||||
|
||||
// Draw center crosshair
|
||||
val crosshairSize = 20.dp.toPx()
|
||||
drawLine(
|
||||
color = focusLineColor,
|
||||
start = Offset(centerX - crosshairSize, centerY),
|
||||
end = Offset(centerX + crosshairSize, centerY),
|
||||
strokeWidth = 2.dp.toPx()
|
||||
)
|
||||
drawLine(
|
||||
color = focusLineColor,
|
||||
start = Offset(centerX, centerY - crosshairSize),
|
||||
end = Offset(centerX, centerY + crosshairSize),
|
||||
strokeWidth = 2.dp.toPx()
|
||||
)
|
||||
|
||||
// Draw rotation indicator (small line at top of ellipse)
|
||||
drawLine(
|
||||
color = focusLineColor,
|
||||
start = Offset(centerX, centerY - ellipseHeight / 2 - 5.dp.toPx()),
|
||||
end = Offset(centerX, centerY - ellipseHeight / 2 - 20.dp.toPx()),
|
||||
strokeWidth = 3.dp.toPx()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue