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 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-05 12:07:50 +01:00
commit 72156f4e5d

View file

@ -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()),