Skip to content
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

Fix SkiaLayer transform #422

Merged
merged 9 commits into from
Mar 10, 2023
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
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
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