From e8a5fa4811ad9a53bebc60fb5630b6fa856b6dc4 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Wed, 28 Jan 2026 16:10:18 +0100 Subject: [PATCH] Fix state persistence, drag tracking, and shader rotation sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../no/naiv/tiltshift/ui/TiltShiftOverlay.kt | 61 +++++++++---------- app/src/main/res/raw/tiltshift_fragment.glsl | 10 +-- 2 files changed, 36 insertions(+), 35 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 c68bab3..3a8e289 100644 --- a/app/src/main/java/no/naiv/tiltshift/ui/TiltShiftOverlay.kt +++ b/app/src/main/java/no/naiv/tiltshift/ui/TiltShiftOverlay.kt @@ -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) diff --git a/app/src/main/res/raw/tiltshift_fragment.glsl b/app/src/main/res/raw/tiltshift_fragment.glsl index e5957f5..d6b59c8 100644 --- a/app/src/main/res/raw/tiltshift_fragment.glsl +++ b/app/src/main/res/raw/tiltshift_fragment.glsl @@ -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