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:
parent
6a1d66bd4b
commit
72156f4e5d
1 changed files with 105 additions and 16 deletions
|
|
@ -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()),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue