Skip to content

Zoom to specific pixel on image? #135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
clintsinger opened this issue Mar 25, 2025 · 6 comments
Open

Zoom to specific pixel on image? #135

clintsinger opened this issue Mar 25, 2025 · 6 comments

Comments

@clintsinger
Copy link

I have a use case were I need to be able to programmatically zoom to a specific pixel on an image. I was hoping the zoomTo function would help me with this, but the centroid offset doesn't make sense to me.

I was trying to zoom to the desired pixel using the following code:

LaunchedEffect(key1 = center, key2 = zoomFactor) {
        delay(1000)
        val c = center?.toImage(scope.imageSize, scope.bounds)

        val centroid = if (c != null) {
            Offset(c.x, c.y)
        } else {
            Offset.Unspecified
        }

        zoomableState.zoomTo(
            zoomFactor = zoomFactor ?: 1f,
            centroid = centroid
        )

        Vlog.i("map", "(launch) zf[${zoomFactor}], c[${centroid}]")
    }

When I do that, it just zooms to some location that doesn't match my expectations.

When I query the zoomableState.contentTransformation.centroid it seems to keep its values fairly contained to some small numbers. Perhaps to the size of the viewport?

zf[1.0] c[Offset(362.2, 713.0)] csz[Size(3604.0, 3444.0)]

Is there some way to achieve my desired goal of centering the pixel that I want at the desired zoom level? I understand that if the pixel can't be centered, such as the image is at its bounds, the closest appropriate pixel would be centered.

@saket
Copy link
Owner

saket commented Mar 29, 2025

Perhaps to the size of the viewport?

Hmmm you're right, this is confusing because the coordinate space of centroid isn't clear. The constant conversion between the content and the viewport's coordinate spaces in the codebase makes things like these difficult.

centroid = centroid.takeOrElse { gestureStateInputs.viewportSize.center },

Can you try converting your point on the image to the viewport's space? Here's something that might be useful:

private object CoordinateSpaceConverter {
/** Converts a point in viewport coordinates to content coordinates. */
fun viewportToContent(
viewportPoint: Offset,
unscaledContentBounds: Rect,
transformation: ZoomableContentTransformation,
): Offset {
val scale = transformation.scale
// The transformation applied to content to get to viewport coordinates is:
// 1. Shift by -contentBounds.topLeft (to handle content not at 0,0)
// 2. Scale by scale factor
// 3. Shift by transformed content bounds
//
// So to go to content coordinates, this walks backwards:
// 1. Shift back by -transformedBounds.topLeft
// 2. Divide by scale
// 3. Shift back by +contentBounds.topLeft
val transformedContentBounds = unscaledContentBounds.scaledAndOffsetBy(scale, transformation.offset)
return ((viewportPoint - transformedContentBounds.topLeft) / scale) + unscaledContentBounds.topLeft
}
}

@saket
Copy link
Owner

saket commented Apr 4, 2025

I've made some progress on this by introducing a coordinate system to differentiate between the two coordinate spaces (viewport vs zoomable content). Could you try this out in 0.16.0-SNAPSHOT and share your feedback on the API? 🙂

val imageCenter = SpatialOffset(
  offset = unscaledImageSize.center,
  space = CoordinateSpace.ZoomableContent,
)
zoomableState.zoomTo(
  zoomFactor = zoomFactor,
  centroid = imageCenter,
)

FWIW this zooms while retaining the centroid's position fixed on screen -- just like what would happen if you double clicked at that point. I understand that this isn't exactly what you're looking for, and I'll be following up on that soon.

You can find more details on the new APIs here:

/**
* `Modifier.zoomable()`'s coordinate system for representing spatial offsets in
* [CoordinateSpace.Viewport][CoordinateSpace.Companion.Viewport] and
* [CoordinateSpace.ZoomableContent][CoordinateSpace.Companion.ZoomableContent].
*
* Usage example:
*
* ```kotlin
* val state = rememberZoomableState()
*
* val viewportCenter = SpatialOffset(
* offset = Offset(100f, 100f),
* space = CoordinateSpace.Viewport,
* )
*
* // If a 200 x 200 viewport is showing a zoomed-out 500 x 500 image,
* // this will return (250f, 250f) in the image's coordinate space.
* val imageCenter: Offset = with(state.coordinateSystem) {
* viewportCenter.offsetIn(CoordinateSpace.ZoomableContent)
* }
* ```
*/
@ExperimentalTelephotoApi
val coordinateSystem: CoordinateSystem
get() = ZoomableCoordinateSystem(this)

/**
* A 2D offset bound to a specific [CoordinateSpace] inside a [CoordinateSystem].
*
* `SpatialOffset` ensures that positional data is always contextualized, preventing miscalculations
* across incompatible coordinate spaces (e.g., viewport vs. image space).
*
* For reading the offset in a space, use [SpatialOffset.offsetIn][CoordinateSystem.offsetIn] with
* a [CoordinateSystem] receiver:
*
* ```kotlin
* val offsetInViewport = SpatialOffset(
* offset = Offset(100f, 200f),
* space = CoordinateSpace.Viewport,
* )
*
* val offsetInImage: Offset = with(zoomableState.coordinateSystem) {
* offsetInViewport.offsetIn(CoordinateSpace.ZoomableContent)
* }
* ```
*/
@Poko
@Immutable
@ExperimentalTelephotoApi
class SpatialOffset(

@clintsinger
Copy link
Author

Perhaps I'm being a bit dim here, but is 0.16.0-SNAPSHOT something I should be able to access from within Android studio directly?

i.e. implementation("me.saket.telephoto:zoomable-image-coil3:0.16.0-SNAPSHOT")

Because that version doesn't appear to be available and I can't find any reference to it anywhere.

@brinsche
Copy link

I think you need

maven {
    url "https://oss.sonatype.org/content/repositories/snapshots"
}

in the repositories in your settings.gradle

@clintsinger
Copy link
Author

I have added the repository but it's not importing...

settings.gradle.kts

pluginManagement {
    repositories {
        ...
        mavenCentral()
        maven {
            setUrl("https://oss.sonatype.org/content/repositories/snapshots")
        }
    }
}

libs.versions.toml

zoomableImageCoil3 = "0.16.0-snapshot"
...
zoomable-image-coil3 = { module = "me.saket.telephoto:zoomable-image-coil3", version.ref = "zoomableImageCoil3" }`

@brinsche
Copy link

I think it needs to be in the dependencyResolutionManagement block not pluginManagement

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants