panpf / zoomimage

ZoomImage is an gesture zoom viewing of images library specially designed for Compose Multiplatform and Android View. Supported scale, pan, locate, rotation, and super-large image subsampling.
Apache License 2.0
303 stars 16 forks source link

Canvas绘制图形时位置出现偏离 #46

Open ChannelMU opened 2 weeks ago

ChannelMU commented 2 weeks ago

Describe the bug

Canvas绘制图形时位置出现偏离

Affected platforms

Select of the platforms below:

Affected components

Select of the components below:

Versions

Running Devices

Please accurately describe the device Model, OS version, and CPU Architecture

Sample code

data class CustomScalesCalculator(
    val multiple: Float = Multiple
) : ScalesCalculator {

    override fun calculate(
        containerSize: IntSizeCompat,
        contentSize: IntSizeCompat,
        contentOriginSize: IntSizeCompat,
        contentScale: ContentScaleCompat,
        minScale: Float,
        initialScale: Float,
    ): ScalesCalculator.Result {
        // initialScale is usually determined by the ReadMode, so initialScale takes precedence
        val mediumScale = if (contentScale == ContentScaleCompat.FillBounds) {
            minScale * 10
        } else if (initialScale > minScale) {
            initialScale
        } else {
            minScale * 2
        }
        val maxScale = mediumScale * 80
        return ScalesCalculator.Result(
            minScale = minScale,
            mediumScale = mediumScale,
            maxScale = maxScale
        )
    }
}

@Preview
@Composable
fun TestPage() {
    val zoomState = rememberZoomState()
    zoomState.zoomable.scalesCalculator= CustomScalesCalculator()
    zoomState.zoomable.limitOffsetWithinBaseVisibleRect=false

    val multipleX=(zoomState.zoomable.contentSize.width.toDouble()/zoomState.zoomable.contentOriginSize.width.toDouble())*zoomState.zoomable.transform.scale.scaleX
    val multipleY=(zoomState.zoomable.contentSize.height.toDouble()/zoomState.zoomable.contentOriginSize.height.toDouble())*zoomState.zoomable.transform.scale.scaleY

    // canvas points
    val points = listOf(
        Pair(100f, 100f),
        Pair(3000f, 2000f),
    )

    Box (modifier = Modifier.size(200.dp)){

        SketchZoomAsyncImage(
            imageUri = "file:///storage/emulated/0/Android/data/com.example.test/TEST.JPG",
            contentDescription = "view image",
            modifier = Modifier.fillMaxWidth(),
            state = zoomState,
        )

        Canvas(modifier = Modifier.fillMaxWidth()) {

            val offsetX = zoomState.zoomable.transform.offset.x
            val offsetY = zoomState.zoomable.transform.offset.y

            for (point in points) {
                drawCircle(
                    color = Color.Blue,
                    center = Offset(
                        (offsetX + point.first * multipleX).toFloat(),
                        (offsetY + point.second * multipleY).toFloat()
                    ),
                    radius = 8f
                )
            }
        }
    }
}

Reproduction steps 在一张6000 x 4000的图像上使用Canvas在(100,100),(3000,2000)处绘制点,放大后发现绘制的图形均有一定的偏离,且当图像的contentSize变小时这种偏离增大 测试图像(6000*4000,格网间隔为100): TEST

Expected behavior

A clear and concise description of what you expected to happen.

Screenshots 设置:Box.size(200.dp)时: 点位(100,100) 200dp_100_100 点位(3000,2000) 200dp_3000_2000 设置:Box.size(2000.dp)时: 点位(100,100) 2000dp_100_100 点位(3000,2000) 2000dp_3000_2000

Additional context

Add any other context about the problem here.

ChannelMU commented 2 weeks ago

在相同的缩放系数和transform.offset.x下,像点的位置不同导致偏离量有差别,这是否与由于子采样区域加载有关?我该如何修正?

panpf commented 1 week ago
  1. 首先你绘制点的坐标需要基于内容大小重新计算,内容大小可以通过 zoomState.zoomable.contentSize 属性获取
  2. 然后 Canvas 应该始终位于左上角位置,并对 Canvas 应用来自 zoomState 的缩放和位移,如下:
    Canvas(modifier = Modifier.zooming(zoomState.zoomable))
ChannelMU commented 1 week ago
  1. 首先你绘制点的坐标需要基于内容大小重新计算,内容大小可以通过 zoomState.zoomable.contentSize 属性获取
  2. 然后 Canvas 应该始终位于左上角位置,并对 Canvas 应用来自 zoomState 的缩放和位移,如下:
    Canvas(modifier = Modifier.zooming(zoomState.zoomable))

    抱歉,我试着修改了下,还是有同样的问题,以下是修改后的代码

    
    @Composable
    fun TestPage() {
    val zoomState = rememberZoomState()
    zoomState.zoomable.scalesCalculator= CustomScalesCalculator()
    zoomState.zoomable.limitOffsetWithinBaseVisibleRect=false
//图像大小为6000*4000
//标记点3000,2000

//计算原像点在原图的位置比例
val xin=100f/6000f
val yin=100f/4000f

//计算在当前内容大小下,像点应在的位置
val x=xin*zoomState.zoomable.contentSize.width
val y=yin*zoomState.zoomable.contentSize.height

Box (modifier = Modifier.size(2000.dp)){

    SketchZoomAsyncImage(
        imageUri = "file:///storage/emulated/0/Android/data/com.example.app_zt/files/Project1/TEST.JPG",
        contentDescription = "view image",
        modifier = Modifier.fillMaxSize(),
        state = zoomState,
    )

    Canvas(
        modifier = Modifier
            .fillMaxSize() // 确保Canvas填满整个父容器
            .zooming(zoomState.zoomable)
    ) {
        // 绘制蓝色背景
        drawRect(
            color =  Color.Blue.copy(alpha = 0.5f),
            size = size // 覆盖整个画布
        )
        // 绘制点(例如红色圆点)
            drawCircle(
                color = Color.Red,
                center = Offset(x, y),
                radius = 2/zoomState.zoomable.transform.scale.scaleX,
            )
    }
}

}


我在外部Box设置Size200.dp和2000.dp下进行了测试,标记点原始坐标(100,100):
2000dp下:
![Screenshot_20241015-191055](https://github.com/user-attachments/assets/5d3171bc-b912-4d24-94c8-9c654a7264e0)

200dp下:
![Screenshot_20241015-191028](https://github.com/user-attachments/assets/c81f7ef9-aede-40b2-af59-8cdb80e4e62b)

以下是中心点(3000,2000)的测试结果:
2000dp下:
![Screenshot_20241015-191851](https://github.com/user-attachments/assets/1f31f9ad-8e0d-4d41-adca-e87e61def6a7)

200dp下:
![Screenshot_20241015-191823](https://github.com/user-attachments/assets/044be68d-4dd1-45cd-80ae-23f3e5e33a73)

这也不是像素起点的问题,我偏移了0.5像素((100.5,100.5),(3000.5,2000.5))后同样无法精确定位
ChannelMU commented 1 week ago

在我自己的测试中,即使是在相同的Content大小下,标记点位置的不同,它们偏离的方向距离也不同,我不清楚是否还有哪些细节我没注意到,如果可以的话,希望能够提供一个完整的示例。 非常感谢您的帮助(:

panpf commented 1 week ago

你可以看一下 SketchZoomAsyncImage 函数的 210 行是怎么把子采样的碎片覆盖到图片上并保持同样的缩放和位移的

https://github.com/panpf/zoomimage/blob/8627b3429ce148cccbaee751cc6793a63cb48b69/zoomimage-compose-sketch-core/src/commonMain/kotlin/com/github/panpf/zoomimage/SketchZoomAsyncImage.kt#L210

panpf commented 1 week ago

整理思路还是缩略图是放置在左上角的,ZoomState 基于左上角计算缩放和位移,你就基于左上角绘制你的点,然后应用同样的缩放和位移

panpf commented 1 week ago
Snipaste_2024-10-16_22-14-32
Box(Modifier.fillMaxSize()) {
    val zoomState = rememberSketchZoomState()
    SketchZoomAsyncImage(
        uri = "/Users/panpf/Downloads/375937945-2b813cbd-b97d-4723-8210-b67f6117cef4.jpg",
        contentDescription = "",
        sketch = LocalPlatformContext.current.sketch,
        modifier = Modifier.fillMaxSize(),
        zoomState = zoomState,
    )

    val scaleFactor by remember {
        derivedStateOf {
            val contentSize = zoomState.zoomable.contentSize
            ScaleFactor(
                scaleX = contentSize.width / 6000f,
                scaleY = contentSize.height / 4000f
            )
        }
    }
    Canvas(modifier = Modifier.fillMaxSize().zooming(zoomState.zoomable)) {
        val point1 = Offset(1500f, 1000f)
        val scaledPoint1 = Offset(
            point1.x * scaleFactor.scaleX,
            point1.y * scaleFactor.scaleY
        )
        drawCircle(
            color = Color.Yellow.copy(alpha = 0.75f),
            radius = 20f,
            center = scaledPoint1
        )

        val point2 = Offset(3000f, 2000f)
        val scaledPoint2 = Offset(
            point2.x * scaleFactor.scaleX,
            point2.y * scaleFactor.scaleY
        )
        drawCircle(
            color = Color.Yellow.copy(alpha = 0.75f),
            radius = 20f,
            center = scaledPoint2
        )

        val point3 = Offset(4500f, 3000f)
        val scaledPoint3 = Offset(
            point3.x * scaleFactor.scaleX,
            point3.y * scaleFactor.scaleY
        )
        drawCircle(
            color = Color.Yellow.copy(alpha = 0.75f),
            radius = 20f,
            center = scaledPoint3
        )
    }
}

我花了 5 分钟帮你写了个示例,你仔细看一下吧

ChannelMU commented 1 week ago

抱歉现在才回复,我测试了您的代码,采用compose-sketch:1.1.0-beta01,仅修改了图像文件路径并设置radius=2f,很遗憾在放大后可以很明显看到绘制的位置并不是在正中心: Screenshot_20241018-154144 上图为(3000f,2000f)设置半径为2f 放大后的显示