From 72156f4e5d53c3b6457e310a29b6a2dcef060e6a Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Thu, 5 Mar 2026 12:07:50 +0100 Subject: [PATCH] Add dark outlines to overlay guides and improve gesture zones - Draw all guide lines with a dark outline behind the accent color to ensure visibility over bright/amber camera scenes - Clamp gesture zone focus size to MIN_FOCUS_SIZE_PX (150px) so rotation zone remains usable at small focus sizes - Add semantics contentDescription to Canvas for TalkBack - Use AppColors for centralized color references Co-Authored-By: Claude Opus 4.6 --- .../no/naiv/tiltshift/ui/TiltShiftOverlay.kt | 121 +++++++++++++++--- 1 file changed, 105 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/no/naiv/tiltshift/ui/TiltShiftOverlay.kt b/app/src/main/java/no/naiv/tiltshift/ui/TiltShiftOverlay.kt index 8c84e76..d7e6e4a 100644 --- a/app/src/main/java/no/naiv/tiltshift/ui/TiltShiftOverlay.kt +++ b/app/src/main/java/no/naiv/tiltshift/ui/TiltShiftOverlay.kt @@ -22,9 +22,12 @@ import androidx.compose.ui.graphics.drawscope.Stroke 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.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import no.naiv.tiltshift.effect.BlurMode import no.naiv.tiltshift.effect.BlurParameters +import no.naiv.tiltshift.ui.theme.AppColors import kotlin.math.PI import kotlin.math.atan2 import kotlin.math.cos @@ -39,13 +42,16 @@ private enum class GestureType { DRAG_POSITION, // Single finger drag to move focus center ROTATE, // Two-finger rotation PINCH_SIZE, // Pinch near blur edges to resize - PINCH_ZOOM // Pinch in center to zoom camera + PINCH_ZOOM // Pinch far outside to zoom camera } // Sensitivity factor for size pinch (lower = less sensitive) private const val SIZE_SENSITIVITY = 0.3f private const val ZOOM_SENSITIVITY = 0.5f +/** Minimum focus size (in px) for gesture zones to ensure usable touch targets. */ +private const val MIN_FOCUS_SIZE_PX = 150f + /** * Calculates the angle between two touch points. */ @@ -70,9 +76,16 @@ fun TiltShiftOverlay( var currentGesture by remember { mutableStateOf(GestureType.NONE) } + val modeLabel = if (params.mode == BlurMode.LINEAR) "linear" else "radial" + Canvas( modifier = modifier .fillMaxSize() + .semantics { + contentDescription = "Tilt-shift overlay: $modeLabel mode. " + + "Drag to move focus. Pinch near edges to resize. " + + "Pinch near center to rotate. Use sliders in controls panel for alternative adjustment." + } .pointerInput(Unit) { awaitEachGesture { val firstDown = awaitFirstDown(requireUnconsumed = false) @@ -187,6 +200,8 @@ fun TiltShiftOverlay( * - Very center (< 30% of focus size): Rotation * - Near focus region (30% - 200% of focus size): Size adjustment * - Far outside (> 200%): Camera zoom + * + * Focus size is clamped to a minimum pixel value to keep zones usable at small sizes. */ private fun determineGestureType( centroid: Offset, @@ -196,7 +211,8 @@ private fun determineGestureType( ): GestureType { val focusCenterX = width * params.positionX val focusCenterY = height * params.positionY - val focusSize = height * params.size * 0.5f + // Clamp focus size to minimum to keep rotation zone reachable + val focusSize = maxOf(height * params.size * 0.5f, MIN_FOCUS_SIZE_PX) val dx = centroid.x - focusCenterX val dy = centroid.y - focusCenterY @@ -236,6 +252,7 @@ private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) { /** * Draws the linear mode overlay (horizontal band with rotation). + * All guide lines are drawn with a dark outline first for visibility over bright scenes. */ private fun DrawScope.drawLinearOverlay(params: BlurParameters) { val width = size.width @@ -246,15 +263,20 @@ private fun DrawScope.drawLinearOverlay(params: BlurParameters) { val focusHalfHeight = height * params.size * 0.5f val angleDegrees = params.angle * (180f / PI.toFloat()) - // Colors for overlay - val focusLineColor = Color(0xFFFFB300) // Amber - val blurZoneColor = Color(0x40FFFFFF) // Semi-transparent white + val focusLineColor = AppColors.Accent + val outlineColor = AppColors.OverlayOutline + val blurZoneColor = AppColors.OverlayLinearBlur val dashEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 10f), 0f) // Calculate diagonal for extended drawing (ensures coverage when rotated) val diagonal = sqrt(width * width + height * height) val extendedHalf = diagonal + val outlineWidth = 4.dp.toPx() + val lineWidth = 2.dp.toPx() + val centerLineWidth = 3.dp.toPx() + val centerOutlineWidth = 5.dp.toPx() + rotate(angleDegrees, pivot = Offset(centerX, centerY)) { // Draw blur zone indicators (top and bottom) drawRect( @@ -268,32 +290,60 @@ private fun DrawScope.drawLinearOverlay(params: BlurParameters) { size = Size(extendedHalf * 2, extendedHalf) ) - // Draw focus zone boundary lines + // Draw focus zone boundary lines (outline first, then color) + // Top boundary + drawLine( + color = outlineColor, + start = Offset(centerX - extendedHalf, centerY - focusHalfHeight), + end = Offset(centerX + extendedHalf, centerY - focusHalfHeight), + strokeWidth = outlineWidth, + pathEffect = dashEffect + ) drawLine( color = focusLineColor, start = Offset(centerX - extendedHalf, centerY - focusHalfHeight), end = Offset(centerX + extendedHalf, centerY - focusHalfHeight), - strokeWidth = 2.dp.toPx(), + strokeWidth = lineWidth, + pathEffect = dashEffect + ) + // Bottom boundary + drawLine( + color = outlineColor, + start = Offset(centerX - extendedHalf, centerY + focusHalfHeight), + end = Offset(centerX + extendedHalf, centerY + focusHalfHeight), + strokeWidth = outlineWidth, pathEffect = dashEffect ) drawLine( color = focusLineColor, start = Offset(centerX - extendedHalf, centerY + focusHalfHeight), end = Offset(centerX + extendedHalf, centerY + focusHalfHeight), - strokeWidth = 2.dp.toPx(), + strokeWidth = lineWidth, pathEffect = dashEffect ) - // Draw center focus line + // Draw center focus line (outline + color) + drawLine( + color = outlineColor, + start = Offset(centerX - extendedHalf, centerY), + end = Offset(centerX + extendedHalf, centerY), + strokeWidth = centerOutlineWidth + ) drawLine( color = focusLineColor, start = Offset(centerX - extendedHalf, centerY), end = Offset(centerX + extendedHalf, centerY), - strokeWidth = 3.dp.toPx() + strokeWidth = centerLineWidth ) // Draw rotation indicator at center val indicatorRadius = 30.dp.toPx() + drawCircle( + color = outlineColor, + radius = indicatorRadius, + center = Offset(centerX, centerY), + style = Stroke(width = 4.dp.toPx()) + ) drawCircle( color = focusLineColor.copy(alpha = 0.5f), radius = indicatorRadius, @@ -303,6 +353,12 @@ private fun DrawScope.drawLinearOverlay(params: BlurParameters) { // Draw angle tick mark val tickLength = 15.dp.toPx() + drawLine( + color = outlineColor, + start = Offset(centerX, centerY - indicatorRadius + tickLength), + end = Offset(centerX, centerY - indicatorRadius - 5.dp.toPx()), + strokeWidth = 5.dp.toPx() + ) drawLine( color = focusLineColor, start = Offset(centerX, centerY - indicatorRadius + tickLength), @@ -314,6 +370,7 @@ private fun DrawScope.drawLinearOverlay(params: BlurParameters) { /** * Draws the radial mode overlay (ellipse/circle). + * All guide lines are drawn with a dark outline first for visibility over bright scenes. */ private fun DrawScope.drawRadialOverlay(params: BlurParameters) { val width = size.width @@ -324,9 +381,8 @@ private fun DrawScope.drawRadialOverlay(params: BlurParameters) { 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 focusLineColor = AppColors.Accent + val outlineColor = AppColors.OverlayOutline val dashEffect = PathEffect.dashPathEffect(floatArrayOf(15f, 8f), 0f) // Calculate ellipse dimensions based on aspect ratio @@ -334,7 +390,13 @@ private fun DrawScope.drawRadialOverlay(params: BlurParameters) { val ellipseHeight = focusRadius * 2 rotate(angleDegrees, pivot = Offset(centerX, centerY)) { - // Draw focus ellipse outline (inner boundary) + // Draw focus ellipse outline (outline + color) + drawOval( + color = outlineColor, + topLeft = Offset(centerX - ellipseWidth / 2, centerY - ellipseHeight / 2), + size = Size(ellipseWidth, ellipseHeight), + style = Stroke(width = 5.dp.toPx()) + ) drawOval( color = focusLineColor, topLeft = Offset(centerX - ellipseWidth / 2, centerY - ellipseHeight / 2), @@ -344,6 +406,15 @@ private fun DrawScope.drawRadialOverlay(params: BlurParameters) { // Draw outer blur boundary (with falloff) val outerScale = 1f + params.falloff + drawOval( + color = outlineColor, + topLeft = Offset( + centerX - (ellipseWidth * outerScale) / 2, + centerY - (ellipseHeight * outerScale) / 2 + ), + size = Size(ellipseWidth * outerScale, ellipseHeight * outerScale), + style = Stroke(width = 4.dp.toPx(), pathEffect = dashEffect) + ) drawOval( color = focusLineColor.copy(alpha = 0.5f), topLeft = Offset( @@ -354,14 +425,26 @@ private fun DrawScope.drawRadialOverlay(params: BlurParameters) { style = Stroke(width = 2.dp.toPx(), pathEffect = dashEffect) ) - // Draw center crosshair + // Draw center crosshair (outline + color) val crosshairSize = 20.dp.toPx() + drawLine( + color = outlineColor, + start = Offset(centerX - crosshairSize, centerY), + end = Offset(centerX + crosshairSize, centerY), + strokeWidth = 4.dp.toPx() + ) drawLine( color = focusLineColor, start = Offset(centerX - crosshairSize, centerY), end = Offset(centerX + crosshairSize, centerY), strokeWidth = 2.dp.toPx() ) + drawLine( + color = outlineColor, + start = Offset(centerX, centerY - crosshairSize), + end = Offset(centerX, centerY + crosshairSize), + strokeWidth = 4.dp.toPx() + ) drawLine( color = focusLineColor, start = Offset(centerX, centerY - crosshairSize), @@ -369,7 +452,13 @@ private fun DrawScope.drawRadialOverlay(params: BlurParameters) { strokeWidth = 2.dp.toPx() ) - // Draw rotation indicator (small line at top of ellipse) + // Draw rotation indicator (small line at top of ellipse, outline + color) + drawLine( + color = outlineColor, + start = Offset(centerX, centerY - ellipseHeight / 2 - 5.dp.toPx()), + end = Offset(centerX, centerY - ellipseHeight / 2 - 20.dp.toPx()), + strokeWidth = 5.dp.toPx() + ) drawLine( color = focusLineColor, start = Offset(centerX, centerY - ellipseHeight / 2 - 5.dp.toPx()),