Fix state persistence, drag tracking, and shader rotation sync

State persistence:
- Use rememberUpdatedState to always get latest params inside pointerInput
- Capture gestureStartParams at beginning of each gesture
- All adjustments now use initial values + accumulated change

Drag tracking:
- Track initialDragCentroid at drag start
- Calculate total drag offset from initial point (not frame-by-frame)
- Drag now properly moves focus center 1:1

Shader rotation sync:
- Adjust angle by -90° in shader to compensate for portrait texture rotation
- Preview blur effect now rotates in sync with overlay UI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-01-28 16:10:18 +01:00
commit e8a5fa4811
2 changed files with 36 additions and 35 deletions

View file

@ -11,6 +11,7 @@ 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
@ -60,12 +61,12 @@ fun TiltShiftOverlay(
onZoomChange: (Float) -> Unit,
modifier: Modifier = Modifier
) {
// Use rememberUpdatedState to always get latest values inside pointerInput
val currentParams by rememberUpdatedState(params)
val currentOnParamsChange by rememberUpdatedState(onParamsChange)
val currentOnZoomChange by rememberUpdatedState(onZoomChange)
var currentGesture by remember { mutableStateOf(GestureType.NONE) }
var initialAngle by remember { mutableFloatStateOf(0f) }
var initialTouchAngle by remember { mutableFloatStateOf(0f) }
var initialSize by remember { mutableFloatStateOf(0.3f) }
var initialPositionX by remember { mutableFloatStateOf(0.5f) }
var initialPositionY by remember { mutableFloatStateOf(0.5f) }
Canvas(
modifier = modifier
@ -75,14 +76,15 @@ fun TiltShiftOverlay(
val firstDown = awaitFirstDown(requireUnconsumed = false)
currentGesture = GestureType.NONE
var previousCentroid = firstDown.position
// Capture initial state at gesture start
val gestureStartParams = currentParams
var initialTouchAngle = 0f
var initialDragCentroid = firstDown.position
var accumulatedZoom = 1f
var rotationInitialized = false
var dragInitialized = false
initialAngle = params.angle
initialSize = params.size
initialPositionX = params.positionX
initialPositionY = params.positionY
var previousCentroid = firstDown.position
do {
val event = awaitPointerEvent()
@ -110,35 +112,32 @@ fun TiltShiftOverlay(
centroid,
size.width.toFloat(),
size.height.toFloat(),
params
currentParams
)
rotationInitialized = false
}
when (currentGesture) {
GestureType.ROTATE -> {
// Initialize rotation reference on first frame
if (!rotationInitialized) {
initialTouchAngle = currentTouchAngle
initialAngle = params.angle
rotationInitialized = true
} else {
// Direct angle mapping: effect angle = initial + (current touch angle - initial touch angle)
val angleDelta = currentTouchAngle - initialTouchAngle
val newAngle = initialAngle + angleDelta
onParamsChange(params.copy(angle = newAngle))
val newAngle = gestureStartParams.angle + angleDelta
currentOnParamsChange(currentParams.copy(angle = newAngle))
}
}
GestureType.PINCH_SIZE -> {
val dampenedZoom = 1f + (zoom - 1f) * SIZE_SENSITIVITY
accumulatedZoom *= dampenedZoom
val newSize = (initialSize * accumulatedZoom)
val newSize = (gestureStartParams.size * accumulatedZoom)
.coerceIn(BlurParameters.MIN_SIZE, BlurParameters.MAX_SIZE)
onParamsChange(params.copy(size = newSize))
currentOnParamsChange(currentParams.copy(size = newSize))
}
GestureType.PINCH_ZOOM -> {
val dampenedZoom = 1f + (zoom - 1f) * ZOOM_SENSITIVITY
onZoomChange(dampenedZoom)
currentOnZoomChange(dampenedZoom)
}
else -> {}
}
@ -148,26 +147,26 @@ fun TiltShiftOverlay(
pointers.size == 1 -> {
if (currentGesture == GestureType.NONE) {
currentGesture = GestureType.DRAG_POSITION
// Reset initial position at drag start
initialPositionX = params.positionX
initialPositionY = params.positionY
previousCentroid = centroid
dragInitialized = false
}
if (currentGesture == GestureType.DRAG_POSITION) {
// 1:1 drag - finger movement directly maps to position change
val deltaX = (centroid.x - previousCentroid.x) / size.width
val deltaY = (centroid.y - previousCentroid.y) / size.height
val newX = (params.positionX + deltaX).coerceIn(0f, 1f)
val newY = (params.positionY + deltaY).coerceIn(0f, 1f)
onParamsChange(params.copy(positionX = newX, positionY = newY))
if (!dragInitialized) {
initialDragCentroid = centroid
dragInitialized = true
} else {
// Calculate total drag from initial touch point
val totalDragX = (centroid.x - initialDragCentroid.x) / size.width
val totalDragY = (centroid.y - initialDragCentroid.y) / size.height
val newX = (gestureStartParams.positionX + totalDragX).coerceIn(0f, 1f)
val newY = (gestureStartParams.positionY + totalDragY).coerceIn(0f, 1f)
currentOnParamsChange(currentParams.copy(positionX = newX, positionY = newY))
}
}
}
}
previousCentroid = centroid
// Consume all pointer changes
pointers.forEach { it.consume() }
} while (event.type != PointerEventType.Release)

View file

@ -24,8 +24,10 @@ float focusDistance(vec2 uv) {
vec2 center = vec2(uPositionX, uPositionY);
vec2 offset = uv - center;
float cosA = cos(uAngle);
float sinA = sin(uAngle);
// Adjust angle by -90 degrees to compensate for portrait texture rotation
float adjustedAngle = uAngle - 1.5707963;
float cosA = cos(adjustedAngle);
float sinA = sin(adjustedAngle);
// After rotation, measure perpendicular distance from center line
float rotatedY = -offset.x * sinA + offset.y * cosA;
@ -70,8 +72,8 @@ vec4 sampleBlurred(vec2 uv, float blur) {
vec4 color = vec4(0.0);
vec2 texelSize = 1.0 / uResolution;
// Blur direction perpendicular to focus line
float blurAngle = uAngle + 1.5707963; // +90 degrees
// Blur direction perpendicular to focus line (adjusted for portrait texture rotation)
float blurAngle = uAngle; // Already perpendicular after -90 adjustment in focusDistance
vec2 blurDir = vec2(cos(blurAngle), sin(blurAngle));
// Scale blur radius by blur amount