diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt index 3d1880884b4a3..cff8c0a4eda4e 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt @@ -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 @@ -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) diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt index 8b65e5543ba69..4aec6c294cd73 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 { @@ -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, @@ -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 @@ -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) } } @@ -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) { diff --git a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/platform/SkiaLayerTest.kt b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/platform/SkiaLayerTest.kt index 66b58cb40a861..f5e45309c8dd1 100644 --- a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/platform/SkiaLayerTest.kt +++ b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/platform/SkiaLayerTest.kt @@ -19,6 +19,7 @@ package androidx.compose.ui.platform import androidx.compose.foundation.shape.CircleShape import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.DefaultCameraDistance import androidx.compose.ui.graphics.DefaultShadowColor import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RenderEffect @@ -139,7 +140,8 @@ class SkiaLayerTest { layer.resize(IntSize(100, 10)) layer.updateProperties( rotationX = 45f, - transformOrigin = TransformOrigin(0f, 0f) + transformOrigin = TransformOrigin(0f, 0f), + cameraDistance = Float.MAX_VALUE ) val matrix = layer.matrix @@ -153,7 +155,8 @@ class SkiaLayerTest { layer.resize(IntSize(100, 10)) layer.updateProperties( rotationX = 45f, - transformOrigin = TransformOrigin(1f, 1f) + transformOrigin = TransformOrigin(1f, 1f), + cameraDistance = Float.MAX_VALUE ) val matrix = layer.matrix @@ -167,7 +170,8 @@ class SkiaLayerTest { layer.resize(IntSize(100, 10)) layer.updateProperties( rotationY = 45f, - transformOrigin = TransformOrigin(0f, 0f) + transformOrigin = TransformOrigin(0f, 0f), + cameraDistance = Float.MAX_VALUE ) val matrix = layer.matrix @@ -181,7 +185,8 @@ class SkiaLayerTest { layer.resize(IntSize(100, 10)) layer.updateProperties( rotationY = 45f, - transformOrigin = TransformOrigin(1f, 1f) + transformOrigin = TransformOrigin(1f, 1f), + cameraDistance = Float.MAX_VALUE ) val matrix = layer.matrix @@ -254,29 +259,31 @@ class SkiaLayerTest { translationX = 60f, translationY = 7f, rotationX = 45f, - transformOrigin = TransformOrigin(0f, 0f) + transformOrigin = TransformOrigin(0f, 0f), + cameraDistance = Float.MAX_VALUE ) val matrix = layer.matrix val y = (10 * cos45).roundToInt() - val translationY = (7 * cos45).roundToInt() + val translationY = 7 assertEquals(IntOffset(0 + 60, 0 + translationY), matrix.map(Offset(0f, 0f)).round()) assertEquals(IntOffset(100 + 60, y + translationY), matrix.map(Offset(100f, 10f)).round()) } @Test - fun translation_rotationY_left_top_origi() { + fun translation_rotationY_left_top_origin() { layer.resize(IntSize(100, 10)) layer.updateProperties( translationX = 60f, translationY = 7f, rotationY = 45f, - transformOrigin = TransformOrigin(0f, 0f) + transformOrigin = TransformOrigin(0f, 0f), + cameraDistance = Float.MAX_VALUE ) val matrix = layer.matrix val x = (100 * cos45).roundToInt() - val translationX = (60 * cos45).roundToInt() + val translationX = 60 assertEquals(IntOffset(0 + translationX, 0 + 7), matrix.map(Offset(0f, 0f)).round()) assertEquals(IntOffset(x + translationX, 10 + 7), matrix.map(Offset(100f, 10f)).round()) } @@ -383,7 +390,7 @@ class SkiaLayerTest { rotationX: Float = 0f, rotationY: Float = 0f, rotationZ: Float = 0f, - cameraDistance: Float = 0f, + cameraDistance: Float = DefaultCameraDistance, transformOrigin: TransformOrigin = TransformOrigin.Center, shape: Shape = RectangleShape, clip: Boolean = false, diff --git a/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationX.png b/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationX.png index 9e380f536035b..e4564fde50262 100644 Binary files a/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationX.png and b/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationX.png differ diff --git a/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationXYZ.png b/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationXYZ.png new file mode 100644 index 0000000000000..ca3ba78ecb3e7 Binary files /dev/null and b/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationXYZ.png differ diff --git a/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationY.png b/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationY.png index e764df2ed60b2..ceb68915ab73e 100644 Binary files a/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationY.png and b/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationY.png differ diff --git a/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationZ.png b/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationZ.png index 63bf7d2536881..b021e92460d58 100644 Binary files a/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationZ.png and b/golden/compose/ui/ui-desktop/platform/androidx_compose_ui_platform_GraphicsLayerTest_rotationZ.png differ