@ -1,32 +1,56 @@
package example.imageviewer.model
package example.imageviewer.model
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.geometry.isSpecified
import kotlin.math.max
/ * *
/ * *
* Encapsulate all transformations about showing some target ( an image , relative to its center )
* Encapsulate all transformations about showing some target ( an image , relative to its center )
* scaled and shifted in some area ( a window , relative to its center )
* scaled and shifted in some area ( a window , relative to its center )
* /
* /
class ScalableState {
class ScalableState {
val scaleLimits = 0.2f .. 10f
var zoomLimits = 1.0f .. 5f
private var offset by mutableStateOf ( Offset . Zero )
/ * *
/ * *
* Offset of the camera before scaling ( an offset in pixels in the area coordinate system )
* Zoom of the target relative to the area size . 1.0 - the target completely fits the area .
* /
* /
var offset by mutableStateOf ( Offset . Zero )
var zoom by mutableStateOf ( 1f )
private set
var scale by mutableStateOf ( 1f )
private set
private set
private var areaSize : Size = Size . Unspecified
private var areaSize : Size by mutableStateOf ( Size . Unspecified )
private var targetSize : Size = Size . Zero
private var targetSize : Size by mutableStateOf ( Size . Zero )
/ * *
* A transformation that should be applied to render the target in the area .
* offset - in pixels in the area coordinate system , should be applied before scaling
* scale - scale of the target in the area
* /
val transformation : Transformation by derivedStateOf {
Transformation (
offset = offset ,
scale = zoomToScale ( zoom )
)
}
/ * *
* The calculated base scale for 100 % zoom . Calculated so that the target fits the area .
* /
private val scaleFor100PercentZoom by derivedStateOf {
if ( targetSize . isSpecified && areaSize . isSpecified ) {
max ( areaSize . width / targetSize . width , areaSize . height / targetSize . height )
} else {
1.0f
}
}
private var offsetXLimits = Float . NEGATIVE _INFINITY .. Float . POSITIVE _INFINITY
private fun zoomToScale ( zoom : Float ) = zoom * scaleFor100PercentZoom
private var offsetYLimits = Float . NEGATIVE _INFINITY .. Float . POSITIVE _INFINITY
/ * *
/ * *
* Limit the target center position , so :
* Limit the target center position , so :
@ -46,22 +70,20 @@ class ScalableState {
private fun applyLimits ( ) {
private fun applyLimits ( ) {
if ( targetSize . isSpecified && areaSize . isSpecified ) {
if ( targetSize . isSpecified && areaSize . isSpecified ) {
offsetXLimits = centerLimits ( targetSize . width * scale , areaSize . width )
val offsetXLimits = centerLimits ( targetSize . width * transformation . scale , areaSize . width )
offsetYLimits = centerLimits ( targetSize . height * scale , areaSize . height )
val offsetYLimits = centerLimits ( targetSize . height * transformation . scale , areaSize . height )
offset = Offset (
offset = Offset (
offset . x . coerceIn ( offsetXLimits ) ,
offset . x . coerceIn ( offsetXLimits ) ,
offset . y . coerceIn ( offsetYLimits ) ,
offset . y . coerceIn ( offsetYLimits ) ,
)
)
} else {
offsetXLimits = Float . NEGATIVE _INFINITY .. Float . POSITIVE _INFINITY
offsetYLimits = Float . NEGATIVE _INFINITY .. Float . POSITIVE _INFINITY
}
}
}
}
private fun centerLimits ( image Size: Float , areaSize : Float ) : ClosedFloatingPointRange < Float > {
private fun centerLimits ( target Size: Float , areaSize : Float ) : ClosedFloatingPointRange < Float > {
val areaCenter = areaSize / 2
val areaCenter = areaSize / 2
val imageCenter = image Size / 2
val targetCenter = target Size / 2
val extra = ( image Center - areaCenter ) . coerceAtLeast ( 0f )
val extra = ( target Center - areaCenter ) . coerceAtLeast ( 0f )
return - extra / 2. . extra / 2
return - extra / 2. . extra / 2
}
}
@ -75,17 +97,37 @@ class ScalableState {
* After we apply the new scale , the camera should be focused on the same point in
* After we apply the new scale , the camera should be focused on the same point in
* the target coordinate system .
* the target coordinate system .
* /
* /
fun addScale ( scale Multiplier : Float , focus : Offset = Offset . Zero ) {
fun addZoom ( zoom Multiplier : Float , focus : Offset = Offset . Zero ) {
setScale ( scale * scale Multiplier , focus )
setZoom ( zoom * zoom Multiplier , focus )
}
}
fun setScale ( scale : Float , focus : Offset = Offset . Zero ) {
/ * *
val newScale = scale . coerceIn ( scaleLimits )
* @param focus on which point the camera is focused in the area coordinate system .
val focusInTargetSystem = ( focus - offset ) / this . scale
* After we apply the new scale , the camera should be focused on the same point in
// calculate newOffset from this equation:
* the target coordinate system .
// focusInTargetSystem = (focus - newOffset) / newScale
* /
offset = focus - focusInTargetSystem * newScale
fun setZoom ( zoom : Float , focus : Offset = Offset . Zero ) {
this . scale = newScale
val newZoom = zoom . coerceIn ( zoomLimits )
val newOffset = Transformation . offsetOf (
point = transformation . pointOf ( focus ) ,
transformedPoint = focus ,
scale = zoomToScale ( newZoom )
)
this . offset = newOffset
this . zoom = newZoom
applyLimits ( )
applyLimits ( )
}
}
data class Transformation (
val offset : Offset ,
val scale : Float ,
) {
fun pointOf ( transformedPoint : Offset ) = ( transformedPoint - offset ) / scale
companion object {
// is derived from the equation `point = (transformedPoint - offset) / scale`
fun offsetOf ( point : Offset , transformedPoint : Offset , scale : Float ) =
transformedPoint - point * scale
}
}
}
}