Add 2D focus positioning, fix rotation tracking, sync preview with effect
2D positioning: - Add positionX parameter to BlurParameters (was only Y before) - Update shader with uPositionX and uPositionY uniforms - Single-finger drag now moves focus center anywhere on screen - Update gradient mask generation for capture Rotation tracking: - Remove dampening from rotation gesture (1:1 tracking) - Rotate gesture now directly tracks finger movement - Preview effect rotates in sync with overlay Overlay and shader sync: - Both now use same positionX, positionY, angle parameters - Preview blur effect matches overlay visualization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e3e05af0b8
commit
e53155e8ee
5 changed files with 53 additions and 41 deletions
|
|
@ -204,7 +204,8 @@ class ImageCaptureHandler(
|
|||
val mask = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val pixels = IntArray(width * height)
|
||||
|
||||
val centerY = height * params.position
|
||||
val centerX = width * params.positionX
|
||||
val centerY = height * params.positionY
|
||||
val focusHalfHeight = height * params.size * 0.5f
|
||||
val transitionHeight = focusHalfHeight * 0.5f
|
||||
|
||||
|
|
@ -213,8 +214,8 @@ class ImageCaptureHandler(
|
|||
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
// Rotate point around center
|
||||
val dx = x - width / 2f
|
||||
// Rotate point around focus center
|
||||
val dx = x - centerX
|
||||
val dy = y - centerY
|
||||
val rotatedY = -dx * sinAngle + dy * cosAngle
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@ package no.naiv.tiltshift.effect
|
|||
* Parameters controlling the tilt-shift blur effect.
|
||||
*
|
||||
* @param angle The rotation angle of the blur gradient in radians (0 = horizontal blur bands)
|
||||
* @param position The center position of the in-focus region (0.0 to 1.0, relative to screen)
|
||||
* @param positionX The horizontal center of the in-focus region (0.0 to 1.0)
|
||||
* @param positionY The vertical center of the in-focus region (0.0 to 1.0)
|
||||
* @param size The size of the in-focus region (0.0 to 1.0, as fraction of screen height)
|
||||
* @param blurAmount The intensity of the blur effect (0.0 to 1.0)
|
||||
*/
|
||||
data class BlurParameters(
|
||||
val angle: Float = 0f,
|
||||
val position: Float = 0.5f,
|
||||
val positionX: Float = 0.5f,
|
||||
val positionY: Float = 0.5f,
|
||||
val size: Float = 0.3f,
|
||||
val blurAmount: Float = 0.8f
|
||||
) {
|
||||
|
|
@ -25,17 +27,20 @@ data class BlurParameters(
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns a copy with the angle adjusted by the given delta.
|
||||
* Returns a copy with the angle set to the given value.
|
||||
*/
|
||||
fun withAngleDelta(delta: Float): BlurParameters {
|
||||
return copy(angle = angle + delta)
|
||||
fun withAngle(newAngle: Float): BlurParameters {
|
||||
return copy(angle = newAngle)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy with the position clamped to valid range.
|
||||
*/
|
||||
fun withPosition(newPosition: Float): BlurParameters {
|
||||
return copy(position = newPosition.coerceIn(0f, 1f))
|
||||
fun withPosition(newX: Float, newY: Float): BlurParameters {
|
||||
return copy(
|
||||
positionX = newX.coerceIn(0f, 1f),
|
||||
positionY = newY.coerceIn(0f, 1f)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ class TiltShiftShader(private val context: Context) {
|
|||
// Uniform locations
|
||||
private var uTextureLocation: Int = 0
|
||||
private var uAngleLocation: Int = 0
|
||||
private var uPositionLocation: Int = 0
|
||||
private var uPositionXLocation: Int = 0
|
||||
private var uPositionYLocation: Int = 0
|
||||
private var uSizeLocation: Int = 0
|
||||
private var uBlurAmountLocation: Int = 0
|
||||
private var uResolutionLocation: Int = 0
|
||||
|
|
@ -61,7 +62,8 @@ class TiltShiftShader(private val context: Context) {
|
|||
// Get uniform locations
|
||||
uTextureLocation = GLES20.glGetUniformLocation(programId, "uTexture")
|
||||
uAngleLocation = GLES20.glGetUniformLocation(programId, "uAngle")
|
||||
uPositionLocation = GLES20.glGetUniformLocation(programId, "uPosition")
|
||||
uPositionXLocation = GLES20.glGetUniformLocation(programId, "uPositionX")
|
||||
uPositionYLocation = GLES20.glGetUniformLocation(programId, "uPositionY")
|
||||
uSizeLocation = GLES20.glGetUniformLocation(programId, "uSize")
|
||||
uBlurAmountLocation = GLES20.glGetUniformLocation(programId, "uBlurAmount")
|
||||
uResolutionLocation = GLES20.glGetUniformLocation(programId, "uResolution")
|
||||
|
|
@ -84,7 +86,8 @@ class TiltShiftShader(private val context: Context) {
|
|||
|
||||
// Set effect parameters
|
||||
GLES20.glUniform1f(uAngleLocation, params.angle)
|
||||
GLES20.glUniform1f(uPositionLocation, params.position)
|
||||
GLES20.glUniform1f(uPositionXLocation, params.positionX)
|
||||
GLES20.glUniform1f(uPositionYLocation, params.positionY)
|
||||
GLES20.glUniform1f(uSizeLocation, params.size)
|
||||
GLES20.glUniform1f(uBlurAmountLocation, params.blurAmount)
|
||||
GLES20.glUniform2f(uResolutionLocation, width.toFloat(), height.toFloat())
|
||||
|
|
|
|||
|
|
@ -33,17 +33,17 @@ import kotlin.math.sin
|
|||
*/
|
||||
private enum class GestureType {
|
||||
NONE,
|
||||
DRAG_POSITION, // Single finger drag to move focus position
|
||||
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
|
||||
}
|
||||
|
||||
// Sensitivity factors for gesture controls (lower = less sensitive)
|
||||
private const val POSITION_SENSITIVITY = 0.15f // Drag to move focus line
|
||||
private const val ROTATION_SENSITIVITY = 0.2f // Two-finger rotation
|
||||
private const val SIZE_SENSITIVITY = 0.25f // Pinch to resize blur zone
|
||||
private const val ZOOM_SENSITIVITY = 0.4f // Pinch to zoom camera
|
||||
// Rotation uses 1:1 tracking (no dampening) for natural feel
|
||||
private const val POSITION_SENSITIVITY = 0.5f // Drag to move focus center
|
||||
private const val SIZE_SENSITIVITY = 0.3f // Pinch to resize blur zone
|
||||
private const val ZOOM_SENSITIVITY = 0.5f // Pinch to zoom camera
|
||||
|
||||
/**
|
||||
* Overlay that shows tilt-shift effect controls and handles gestures.
|
||||
|
|
@ -56,10 +56,10 @@ fun TiltShiftOverlay(
|
|||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var currentGesture by remember { mutableStateOf(GestureType.NONE) }
|
||||
var initialZoom by remember { mutableFloatStateOf(1f) }
|
||||
var initialAngle by remember { mutableFloatStateOf(0f) }
|
||||
var initialSize by remember { mutableFloatStateOf(0.3f) }
|
||||
var initialPosition by remember { mutableFloatStateOf(0.5f) }
|
||||
var initialPositionX by remember { mutableFloatStateOf(0.5f) }
|
||||
var initialPositionY by remember { mutableFloatStateOf(0.5f) }
|
||||
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
|
|
@ -70,15 +70,15 @@ fun TiltShiftOverlay(
|
|||
currentGesture = GestureType.NONE
|
||||
|
||||
var previousCentroid = firstDown.position
|
||||
var previousPointerCount = 1
|
||||
var accumulatedRotation = 0f
|
||||
var accumulatedZoom = 1f
|
||||
var accumulatedDragX = 0f
|
||||
var accumulatedDragY = 0f
|
||||
|
||||
initialAngle = params.angle
|
||||
initialSize = params.size
|
||||
initialPosition = params.position
|
||||
initialZoom = 1f
|
||||
initialPositionX = params.positionX
|
||||
initialPositionY = params.positionY
|
||||
|
||||
do {
|
||||
val event = awaitPointerEvent()
|
||||
|
|
@ -110,8 +110,8 @@ fun TiltShiftOverlay(
|
|||
|
||||
when (currentGesture) {
|
||||
GestureType.ROTATE -> {
|
||||
// Apply dampening to rotation
|
||||
accumulatedRotation += rotation * ROTATION_SENSITIVITY
|
||||
// 1:1 rotation tracking - no dampening
|
||||
accumulatedRotation += rotation
|
||||
val newAngle = initialAngle + accumulatedRotation
|
||||
onParamsChange(params.copy(angle = newAngle))
|
||||
}
|
||||
|
|
@ -132,24 +132,25 @@ fun TiltShiftOverlay(
|
|||
}
|
||||
}
|
||||
|
||||
// Single finger
|
||||
// Single finger - drag to move focus center (2D)
|
||||
pointers.size == 1 -> {
|
||||
if (currentGesture == GestureType.NONE) {
|
||||
currentGesture = GestureType.DRAG_POSITION
|
||||
}
|
||||
|
||||
if (currentGesture == GestureType.DRAG_POSITION) {
|
||||
// Apply dampening to position drag
|
||||
val deltaX = (centroid.x - previousCentroid.x) / size.width
|
||||
val deltaY = (centroid.y - previousCentroid.y) / size.height
|
||||
accumulatedDragX += deltaX * POSITION_SENSITIVITY
|
||||
accumulatedDragY += deltaY * POSITION_SENSITIVITY
|
||||
val newPosition = (initialPosition + accumulatedDragY).coerceIn(0f, 1f)
|
||||
onParamsChange(params.copy(position = newPosition))
|
||||
val newX = (initialPositionX + accumulatedDragX).coerceIn(0f, 1f)
|
||||
val newY = (initialPositionY + accumulatedDragY).coerceIn(0f, 1f)
|
||||
onParamsChange(params.copy(positionX = newX, positionY = newY))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previousCentroid = centroid
|
||||
previousPointerCount = pointers.size
|
||||
|
||||
// Consume all pointer changes
|
||||
pointers.forEach { it.consume() }
|
||||
|
|
@ -172,12 +173,13 @@ private fun determineGestureType(
|
|||
height: Float,
|
||||
params: BlurParameters
|
||||
): GestureType {
|
||||
// Calculate distance from focus center line
|
||||
val focusCenterY = height * params.position
|
||||
// Calculate distance from focus center
|
||||
val focusCenterX = width * params.positionX
|
||||
val focusCenterY = height * params.positionY
|
||||
val focusHalfHeight = height * params.size * 0.5f
|
||||
|
||||
// Rotate centroid to align with focus line
|
||||
val dx = centroid.x - width / 2f
|
||||
val dx = centroid.x - focusCenterX
|
||||
val dy = centroid.y - focusCenterY
|
||||
val rotatedY = -dx * sin(params.angle) + dy * cos(params.angle)
|
||||
|
||||
|
|
@ -207,8 +209,8 @@ private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) {
|
|||
val width = size.width
|
||||
val height = size.height
|
||||
|
||||
val centerX = width / 2f
|
||||
val centerY = height * params.position
|
||||
val centerX = width * params.positionX
|
||||
val centerY = height * params.positionY
|
||||
val focusHalfHeight = height * params.size * 0.5f
|
||||
val angleDegrees = params.angle * (180f / PI.toFloat())
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ uniform samplerExternalOES uTexture;
|
|||
|
||||
// Effect parameters
|
||||
uniform float uAngle; // Rotation angle in radians
|
||||
uniform float uPosition; // Center position of focus (0-1)
|
||||
uniform float uPositionX; // Horizontal center of focus (0-1)
|
||||
uniform float uPositionY; // Vertical center of focus (0-1)
|
||||
uniform float uSize; // Size of in-focus region (0-1)
|
||||
uniform float uBlurAmount; // Maximum blur intensity (0-1)
|
||||
uniform vec2 uResolution; // Texture resolution for proper sampling
|
||||
|
|
@ -19,15 +20,15 @@ varying vec2 vTexCoord;
|
|||
|
||||
// Calculate signed distance from the focus line
|
||||
float focusDistance(vec2 uv) {
|
||||
// Rotate coordinate system around center
|
||||
vec2 center = vec2(0.5, uPosition);
|
||||
vec2 rotated = uv - center;
|
||||
// Center point of the focus region
|
||||
vec2 center = vec2(uPositionX, uPositionY);
|
||||
vec2 offset = uv - center;
|
||||
|
||||
float cosA = cos(uAngle);
|
||||
float sinA = sin(uAngle);
|
||||
|
||||
// After rotation, measure vertical distance from center line
|
||||
float rotatedY = -rotated.x * sinA + rotated.y * cosA;
|
||||
// After rotation, measure perpendicular distance from center line
|
||||
float rotatedY = -offset.x * sinA + offset.y * cosA;
|
||||
|
||||
return abs(rotatedY);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue