Skip to content

Commit

Permalink
Fix SkiaLayer transform (#422)
Browse files Browse the repository at this point in the history
* Fix SkiaLayer transform

* Fixed cameraDistance min and conversation to pt

* Fix unit tests

* Revert hardcoded dpi like on android

* Update golden images for rotation tests

* Update compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt

Co-authored-by: dima.avdeev <99798741+dima-avdeev-jb@users.noreply.github.com>

* Update rotation tests

* Use screenshots generated by CI

---------

Co-authored-by: dima.avdeev <99798741+dima-avdeev-jb@users.noreply.github.com>
  • Loading branch information
MatkovIvan and dima-avdeev-jb authored Mar 10, 2023
1 parent 22b4b01 commit 319dbb1
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.test.InternalTestApi
import androidx.compose.ui.isLinux
import androidx.compose.ui.renderComposeScene
import androidx.compose.ui.test.InternalTestApi
import androidx.compose.ui.test.junit4.DesktopScreenshotTestRule
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -66,70 +71,122 @@ class GraphicsLayerTest {
screenshotRule.write(snapshot)
}

@Test
fun rotationZ() {
val snapshot = renderComposeScene(width = 40, height = 40) {
Box(
Modifier
.graphicsLayer(
translationX = 10f,
rotationZ = 90f,
scaleX = 2f,
scaleY = 0.5f,
transformOrigin = TransformOrigin(0f, 0f)
)
.requiredSize(10f.dp, 10f.dp).background(Color.Red)
@Composable
fun testRotationBoxes(
rotationX: Float = 0f,
rotationY: Float = 0f,
rotationZ: Float = 0f
) {
val size = DpSize(10.dp, 10.dp)
val backgroundBrush =
Brush.verticalGradient(
colors = listOf(Color.Red, Color.Blue)
)
Box(
Modifier
.graphicsLayer(
translationX = 10f,
translationY = 20f,
rotationZ = 45f
Box(
Modifier
.graphicsLayer(
translationX = 0f,
translationY = 0f,
rotationX = rotationX,
rotationY = rotationY,
rotationZ = rotationZ,
)
.requiredSize(10f.dp, 10f.dp).background(Color.Blue)
.requiredSize(size)
.background(brush = backgroundBrush)
)
Box(
Modifier
.graphicsLayer(
translationX = 20f,
translationY = 0f,
rotationX = rotationX,
rotationY = rotationY,
rotationZ = rotationZ,
transformOrigin = TransformOrigin(0f, 0f),
)
.requiredSize(size)
.background(brush = backgroundBrush)
)
Box(
Modifier
.graphicsLayer(
translationX = 0f,
translationY = 20f,
rotationX = rotationX,
rotationY = rotationY,
rotationZ = rotationZ,
cameraDistance = 0.1f
)
.requiredSize(size)
.background(brush = backgroundBrush)
)
Box(
Modifier
.graphicsLayer(
translationX = 20f,
translationY = 20f,
rotationX = -rotationX,
rotationY = -rotationY,
rotationZ = -rotationZ,
cameraDistance = 0.1f
)
.requiredSize(size)
.background(brush = backgroundBrush)
)

}

@Test
fun rotationX() {

// TODO Remove once approximate comparison will be available. The problem: there is a difference
// in antialiasing between platforms. The golden screenshot currently matches CI behaviour.
assumeTrue(isLinux)

val snapshot = renderComposeScene(width = 40, height = 40) {
testRotationBoxes(
rotationX = 45f,
)
}
screenshotRule.write(snapshot)
}

@Test
fun rotationX() {
fun rotationY() {

// TODO Remove once approximate comparison will be available. The problem: there is a difference
// in antialiasing between platforms. The golden screenshot currently matches CI behaviour.
assumeTrue(isLinux)

val snapshot = renderComposeScene(width = 40, height = 40) {
Box(
Modifier
.graphicsLayer(rotationX = 45f)
.requiredSize(10f.dp, 10f.dp).background(Color.Blue)
testRotationBoxes(
rotationY = 45f,
)
Box(
Modifier
.graphicsLayer(
translationX = 20f,
transformOrigin = TransformOrigin(0f, 0f),
rotationX = 45f
)
.requiredSize(10f.dp, 10f.dp).background(Color.Blue)
}
screenshotRule.write(snapshot)
}
@Test
fun rotationZ() {
val snapshot = renderComposeScene(width = 40, height = 40) {
testRotationBoxes(
rotationZ = 45f,
)
}
screenshotRule.write(snapshot)
}

@Test
fun rotationY() {
fun rotationXYZ() {

// TODO Remove once approximate comparison will be available. The problem: there is a difference
// in antialiasing between platforms. The golden screenshot currently matches CI behaviour.
assumeTrue(isLinux)

val snapshot = renderComposeScene(width = 40, height = 40) {
Box(
Modifier
.graphicsLayer(rotationY = 45f)
.requiredSize(10f.dp, 10f.dp).background(Color.Blue)
)
Box(
Modifier
.graphicsLayer(
translationX = 20f,
transformOrigin = TransformOrigin(0f, 0f),
rotationY = 45f
)
.requiredSize(10f.dp, 10f.dp).background(Color.Blue)
testRotationBoxes(
rotationX = 45f,
rotationY = 45f,
rotationZ = 45f,
)
}
screenshotRule.write(snapshot)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.ClipOp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.DefaultCameraDistance
import androidx.compose.ui.graphics.DefaultShadowColor
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Outline
Expand All @@ -40,13 +41,13 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.toSkiaRRect
import androidx.compose.ui.graphics.toSkiaRect
import androidx.compose.ui.node.OwnedLayer
import androidx.compose.ui.node.InvokeOnCanvas
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import kotlin.math.max
import org.jetbrains.skia.ClipMode
import org.jetbrains.skia.Picture
import org.jetbrains.skia.PictureRecorder
Expand All @@ -65,6 +66,12 @@ internal class SkiaLayer(
OutlineCache(density, size, RectangleShape, LayoutDirection.Ltr)
// Internal for testing
internal val matrix = Matrix()
private val inverseMatrix: Matrix
get() = Matrix().apply {
setFrom(matrix)
invert()
}

private val pictureRecorder = PictureRecorder()
private var picture: Picture? = null
private var isDestroyed = false
Expand All @@ -75,6 +82,7 @@ internal class SkiaLayer(
private var rotationX: Float = 0f
private var rotationY: Float = 0f
private var rotationZ: Float = 0f
private var cameraDistance: Float = DefaultCameraDistance
private var scaleX: Float = 1f
private var scaleY: Float = 1f
private var alpha: Float = 1f
Expand Down Expand Up @@ -112,11 +120,19 @@ internal class SkiaLayer(
}

override fun mapOffset(point: Offset, inverse: Boolean): Offset {
return getMatrix(inverse).map(point)
return if (inverse) {
inverseMatrix
} else {
matrix
}.map(point)
}

override fun mapBounds(rect: MutableRect, inverse: Boolean) {
getMatrix(inverse).map(rect)
if (inverse) {
inverseMatrix
} else {
matrix
}.map(rect)
}

override fun isInLayer(position: Offset): Boolean {
Expand All @@ -133,17 +149,6 @@ internal class SkiaLayer(
return isInOutline(outlineCache.outline, x, y)
}

private fun getMatrix(inverse: Boolean): Matrix {
return if (inverse) {
Matrix().apply {
setFrom(matrix)
invert()
}
} else {
matrix
}
}

override fun updateLayerProperties(
scaleX: Float,
scaleY: Float,
Expand All @@ -170,6 +175,7 @@ internal class SkiaLayer(
this.rotationX = rotationX
this.rotationY = rotationY
this.rotationZ = rotationZ
this.cameraDistance = max(cameraDistance, 0.001f)
this.scaleX = scaleX
this.scaleY = scaleY
this.alpha = alpha
Expand All @@ -186,27 +192,26 @@ internal class SkiaLayer(
invalidate()
}

// TODO(demin): support perspective projection for rotationX/rotationY (as in Android)
// TODO(njawad) Add camera distance leveraging Sk3DView along with rotationX/rotationY
// see https://cs.android.com/search?q=RenderProperties.cpp&sq= updateMatrix method
// for how 3d transformations along with camera distance are applied. b/173402019
private fun updateMatrix() {
val pivotX = transformOrigin.pivotFractionX * size.width
val pivotY = transformOrigin.pivotFractionY * size.height

matrix.reset()
matrix.translate(x = -pivotX, y = -pivotY)
matrix *= Matrix().apply {
translate(x = -pivotX, y = -pivotY)
}
matrix *= Matrix().apply {
translate(translationX, translationY)
rotateX(rotationX)
rotateY(rotationY)
rotateZ(rotationZ)
rotateY(rotationY)
rotateX(rotationX)
scale(scaleX, scaleY)
}
matrix *= Matrix().apply {
translate(x = pivotX, y = pivotY)
// the camera location is passed in inches, set in pt
val depth = cameraDistance * 72f
set(row = 2, column = 3, v = -1f / depth)
set(row = 2, column = 2, v = 0f)
}
matrix *= Matrix().apply {
translate(x = pivotX + translationX, y = pivotY + translationY)
}
}

Expand Down Expand Up @@ -234,11 +239,11 @@ internal class SkiaLayer(
}

override fun transform(matrix: Matrix) {
matrix.timesAssign(getMatrix(inverse = false))
matrix.timesAssign(this.matrix)
}

override fun inverseTransform(matrix: Matrix) {
matrix.timesAssign(getMatrix(inverse = true))
matrix.timesAssign(inverseMatrix)
}

private fun performDrawLayer(canvas: Canvas, bounds: Rect) {
Expand Down
Loading

0 comments on commit 319dbb1

Please sign in to comment.