Share via


Matrix Transforms in SkiaSharp

Dive deeper into SkiaSharp transforms with the versatile transform matrix

All the transforms applied to the SKCanvas object are consolidated in a single instance of the SKMatrix structure. This is a standard 3-by-3 transform matrix similar to those in all modern 2D graphics systems.

As you've seen, you can use transforms in SkiaSharp without knowing about the transform matrix, but the transform matrix is important from a theoretical perspective, and it is crucial when using transforms to modify paths or for handling complex touch input, both of which are demonstrated in this article and the next.

A bitmap subjected to an affine transform

The current transform matrix applied to the SKCanvas is available at any time by accessing the read-only TotalMatrix property. You can set a new transform matrix using the SetMatrix method, and you can restore that transform matrix to default values by calling ResetMatrix.

The only other SKCanvas member that directly works with the canvas's matrix transform is Concat which concatenates two matrices by multiplying them together.

The default transform matrix is the identity matrix and consists of 1's in the diagonal cells and 0's everywhere else:

| 1  0  0 |
| 0  1  0 |
| 0  0  1 |

You can create an identity matrix using the static SKMatrix.MakeIdentity method:

SKMatrix matrix = SKMatrix.MakeIdentity();

The SKMatrix default constructor does not return an identity matrix. It returns a matrix with all of the cells set to zero. Do not use the SKMatrix constructor unless you plan to set those cells manually.

When SkiaSharp renders a graphical object, each point (x, y) is effectively converted to a 1-by-3 matrix with a 1 in the third column:

| x  y  1 |

This 1-by-3 matrix represents a three-dimensional point with the Z coordinate set to 1. There are mathematical reasons (discussed later) why a two-dimensional matrix transform requires working in three dimensions. You can think of this 1-by-3 matrix as representing a point in a 3D coordinate system, but always on the 2D plane where Z equals 1.

This 1-by-3 matrix is then multiplied by the transform matrix, and the result is the point rendered on the canvas:

              | 1  0  0 |
| x  y  1 | × | 0  1  0 | = | x'  y'  z' |
              | 0  0  1 |

Using standard matrix multiplication, the converted points are as follows:

x' = x

y' = y

z' = 1

That's the default transform.

When the Translate method is called on the SKCanvas object, the tx and ty arguments to the Translate method become the first two cells in the third row of the transform matrix:

|  1   0   0 |
|  0   1   0 |
| tx  ty   1 |

The multiplication is now as follows:

              |  1   0   0 |
| x  y  1 | × |  0   1   0 | = | x'  y'  z' |
              | tx  ty   1 |

Here are the transform formulas:

x' = x + tx

y' = y + ty

Scaling factors have a default value of 1. When you call the Scale method on a new SKCanvas object, the resultant transform matrix contains the sx and sy arguments in the diagonal cells:

              | sx   0   0 |
| x  y  1 | × |  0  sy   0 | = | x'  y'  z' |
              |  0   0   1 |

The transform formulas are as follows:

x' = sx · x

y' = sy · y

The transform matrix after calling Skew contains the two arguments in the matrix cells adjacent to the scaling factors:

              │   1   ySkew   0 │
| x  y  1 | × │ xSkew   1     0 │ = | x'  y'  z' |
              │   0     0     1 │

The transform formulas are:

x' = x + xSkew · y

y' = ySkew · x + y

For a call to RotateDegrees or RotateRadians for an angle of α, the transform matrix is as follows:

              │  cos(α)  sin(α)  0 │
| x  y  1 | × │ –sin(α)  cos(α)  0 │ = | x'  y'  z' |
              │    0       0     1 │

Here are the transform formulas:

x' = cos(α) · x - sin(α) · y

y' = sin(α) · x - cos(α) · y

When α is 0 degrees, it's the identity matrix. When α is 180 degrees, the transform matrix is as follows:

| –1   0   0 |
|  0  –1   0 |
|  0   0   1 |

A 180-degree rotation is equivalent to flipping an object horizontally and vertically, which is also accomplished by setting scale factors of –1.

All these types of transforms are classified as affine transforms. Affine transforms never involve the third column of the matrix, which remains at the default values of 0, 0, and 1. The article Non-Affine Transforms discusses non-affine transforms.

Matrix Multiplication

One significant advantage with using the transform matrix is that composite transforms can be obtained by matrix multiplication, which is often referred to in the SkiaSharp documentation as concatenation. Many of the transform-related methods in SKCanvas refer to "pre-concatenation" or "pre-concat." This refers to the order of multiplication, which is important because matrix multiplication is not commutative.

For example, the documentation for the Translate method says that it "Pre-concats the current matrix with the specified translation," while the documentation for the Scale method says that it "Pre-concats the current matrix with the specified scale."

This means that the transform specified by the method call is the multiplier (the left-hand operand) and the current transform matrix is the multiplicand (the right-hand operand).

Suppose that Translate is called followed by Scale:

canvas.Translate(tx, ty);
canvas.Scale(sx, sy);

The Scale transform is multiplied by the Translate transform for the composite transform matrix:

| sx   0   0 |   |  1   0   0 |   | sx   0   0 |
|  0  sy   0 | × |  0   1   0 | = |  0  sy   0 |
|  0   0   1 |   | tx  ty   1 |   | tx  ty   1 |

Scale could be called before Translate like this:

canvas.Scale(sx, sy);
canvas.Translate(tx, ty);

In that case, the order of the multiplication is reversed, and the scaling factors are effectively applied to the translation factors:

|  1   0   0 |   | sx   0   0 |   |  sx      0    0 |
|  0   1   0 | × |  0  sy   0 | = |   0     sy    0 |
| tx  ty   1 |   |  0   0   1 |   | tx·sx  ty·sy  1 |

Here is the Scale method with a pivot point:

canvas.Scale(sx, sy, px, py);

This is equivalent to the following translate and scale calls:

canvas.Translate(px, py);
canvas.Scale(sx, sy);
canvas.Translate(–px, –py);

The three transform matrices are multiplied in reverse order from how the methods appear in code:

|  1    0   0 |   | sx   0   0 |   |  1   0  0 |   |    sx         0     0 |
|  0    1   0 | × |  0  sy   0 | × |  0   1  0 | = |     0        sy     0 |
| –px  –py  1 |   |  0   0   1 |   | px  py  1 |   | px–px·sx  py–py·sy  1 |

The SKMatrix Structure

The SKMatrix structure defines nine read/write properties of type float corresponding to the nine cells of the transform matrix:

│ ScaleX  SkewY   Persp0 │
│ SkewX   ScaleY  Persp1 │
│ TransX  TransY  Persp2 │

SKMatrix also defines a property named Values of type float[]. This property can be used to set or obtain the nine values in one shot in the order ScaleX, SkewX, TransX, SkewY, ScaleY, TransY, Persp0, Persp1, and Persp2.

The Persp0, Persp1, and Persp2 cells are discussed in the article Non-Affine Transforms. If these cells have their default values of 0, 0, and 1, then the transform is multiplied by a coordinate point like this:

              │ ScaleX  SkewY   0 │
| x  y  1 | × │ SkewX   ScaleY  0 │ = | x'  y'  z' |
              │ TransX  TransY  1 │

x' = ScaleX · x + SkewX · y + TransX

y' = SkewX · x + ScaleY · y + TransY

z' = 1

This is the complete two-dimensional affine transform. The affine transform preserves parallel lines, which means that a rectangle is never transformed into anything other than a parallelogram.

The SKMatrix structure defines several static methods to create SKMatrix values. These all return SKMatrix values:

SKMatrix also defines several static methods that concatenate two matrices, which means to multiply them. These methods are named Concat, PostConcat, and PreConcat, and there are two versions of each. These methods have no return values; instead, they reference existing SKMatrix values through ref arguments. In the following example, A, B, and R (for "result") are all SKMatrix values.

The two Concat methods are called like this:

SKMatrix.Concat(ref R, A, B);

SKMatrix.Concat(ref R, ref A, ref B);

These perform the following multiplication:

R = B × A

The other methods have only two parameters. The first parameter is modified, and on return from the method call, contains the product of the two matrices. The two PostConcat methods are called like this:

SKMatrix.PostConcat(ref A, B);

SKMatrix.PostConcat(ref A, ref B);

These calls perform the following operation:

A = A × B

The two PreConcat methods are similar:

SKMatrix.PreConcat(ref A, B);

SKMatrix.PreConcat(ref A, ref B);

These calls perform the following operation:

A = B × A

The versions of these methods with all ref arguments are slightly more efficient in calling the underlying implementations, but it might be confusing to someone reading your code and assuming that anything with a ref argument is modified by the method. Moreover, it's often convenient to pass an argument that is a result of one of the Make methods, for example:

SKMatrix result;
SKMatrix.Concat(result, SKMatrix.MakeTranslation(100, 100),
                        SKMatrix.MakeScale(3, 3));

This creates the following matrix:

│   3    0  0 │
│   0    3  0 │
│ 100  100  1 │

This is the scale transform multiplied by the translate transform. In this particular case, the SKMatrix structure provides a shortcut with a method named SetScaleTranslate:

SKMatrix R = new SKMatrix();
R.SetScaleTranslate(3, 3, 100, 100);

This is one of the few times when it's safe to use the SKMatrix constructor. The SetScaleTranslate method sets all nine cells of the matrix. It is also safe to use the SKMatrix constructor with the static Rotate and RotateDegrees methods:

SKMatrix R = new SKMatrix();

SKMatrix.Rotate(ref R, radians);

SKMatrix.Rotate(ref R, radians, px, py);

SKMatrix.RotateDegrees(ref R, degrees);

SKMatrix.RotateDegrees(ref R, degrees, px, py);

These methods do not concatenate a rotate transform to an existing transform. The methods set all the cells of the matrix. They are functionally identical to the MakeRotation and MakeRotationDegrees methods except that they don't instantiate the SKMatrix value.

Suppose you have an SKPath object that you want to display, but you would prefer that it have a somewhat different orientation, or a different center point. You can modify all the coordinates of that path by calling the Transform method of SKPath with an SKMatrix argument. The Path Transform page demonstrates how to do this. The PathTransform class references the HendecagramPath object in a field but uses its constructor to apply a transform to that path:

public class PathTransformPage : ContentPage
{
    SKPath transformedPath = HendecagramArrayPage.HendecagramPath;

    public PathTransformPage()
    {
        Title = "Path Transform";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        SKMatrix matrix = SKMatrix.MakeScale(3, 3);
        SKMatrix.PostConcat(ref matrix, SKMatrix.MakeRotationDegrees(360f / 22));
        SKMatrix.PostConcat(ref matrix, SKMatrix.MakeTranslation(300, 300));

        transformedPath.Transform(matrix);
    }
    ...
}

The HendecagramPath object has a center at (0, 0), and the 11 points of the star extend outward from that center by 100 units in all directions. This means that the path has both positive and negative coordinates. The Path Transform page prefers to work with a star three times as large, and with all positive coordinates. Moreover, it doesn't want one point of the star to point straight up. It wants instead for one point of the star to point straight down. (Because the star has 11 points, it can't have both.) This requires rotating the star by 360 degrees divided by 22.

The constructor builds an SKMatrix object from three separate transforms using the PostConcat method with the following pattern, where A, B, and C are instances of SKMatrix:

SKMatrix matrix = A;
SKMatrix.PostConcat(ref A, B);
SKMatrix.PostConcat(ref A, C);

This is a series of successive multiplications, so the result is as follows:

A × B × C

The consecutive multiplications aid in understanding what each transform does. The scale transform increases the size of the path coordinates by a factor of 3, so the coordinates range from –300 to 300. The rotate transform rotates the star around its origin. The translate transform then shifts it by 300 pixels right and down, so all the coordinates become positive.

There are other sequences that produce the same matrix. Here's another one:

SKMatrix matrix = SKMatrix.MakeRotationDegrees(360f / 22);
SKMatrix.PostConcat(ref matrix, SKMatrix.MakeTranslation(100, 100));
SKMatrix.PostConcat(ref matrix, SKMatrix.MakeScale(3, 3));

This rotates the path around its center first, and then translates it 100 pixels to the right and down so all the coordinates are positive. The star is then increased in size relative to its new upper-left corner, which is the point (0, 0).

The PaintSurface handler can simply render this path:

public class PathTransformPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Magenta;
            paint.StrokeWidth = 5;

            canvas.DrawPath(transformedPath, paint);
        }
    }
}

It appears in the upper-left corner of the canvas:

Triple screenshot of the Path Transform page

The constructor of this program applies the matrix to the path with the following call:

transformedPath.Transform(matrix);

The path does not retain this matrix as a property. Instead, it applies the transform to all of the coordinates of the path. If Transform is called again, the transform is applied again, and the only way you can go back is by applying another matrix that undoes the transform. Fortunately, the SKMatrix structure defines a TryInvert method that obtains the matrix that reverses a given matrix:

SKMatrix inverse;
bool success = matrix.TryInverse(out inverse);

The method is called TryInverse because not all matrices are invertible, but a non-invertible matrix is not likely to be used for a graphics transform.

You can also apply a matrix transform to an SKPoint value, an array of points, an SKRect, or even just a single number within your program. The SKMatrix structure supports these operations with a collection of methods that begin with the word Map, such as these:

SKPoint transformedPoint = matrix.MapPoint(point);

SKPoint transformedPoint = matrix.MapPoint(x, y);

SKPoint[] transformedPoints = matrix.MapPoints(pointArray);

float transformedValue = matrix.MapRadius(floatValue);

SKRect transformedRect = matrix.MapRect(rect);

If you use that last method, keep in mind that the SKRect structure is not capable of representing a rotated rectangle. The method only makes sense for an SKMatrix value representing translation and scaling.

Interactive Experimentation

One way to get a feel for the affine transform is by interactively moving three corners of a bitmap around the screen and seeing what transform results. This is the idea behind the Show Affine Matrix page. This page requires two other classes that are also used in other demonstrations:

The TouchPoint class displays a translucent circle that can be dragged around the screen. TouchPoint requires that an SKCanvasView or an element that is a parent of an SKCanvasView have the TouchEffect attached. Set the Capture property to true. In the TouchAction event handler, the program must call the ProcessTouchEvent method in TouchPoint for each TouchPoint instance. The method returns true if the touch event resulted in the touch point moving. Also, the PaintSurface handler must call the Paint method in each TouchPoint instance, passing to it the SKCanvas object.

TouchPoint demonstrates a common way that a SkiaSharp visual can be encapsulated in a separate class. The class can define properties for specifying characteristics of the visual, and a method named Paint with an SKCanvas argument can render it.

The Center property of TouchPoint indicates the location of the object. This property can be set to initialize the location; the property changes when the user drags the circle around the canvas.

The Show Affine Matrix Page also requires the MatrixDisplay class. This class displays the cells of an SKMatrix object. It has two public methods: Measure to obtain the dimensions of the rendered matrix, and Paint to display it. The class contains a MatrixPaint property of type SKPaint that can be replaced for a different font size or color.

The ShowAffineMatrixPage.xaml file instantiates the SKCanvasView and attaches a TouchEffect. The ShowAffineMatrixPage.xaml.cs code-behind file creates three TouchPoint objects and then sets them to positions corresponding to three corners of a bitmap that it loads from an embedded resource:

public partial class ShowAffineMatrixPage : ContentPage
{
    SKMatrix matrix;
    SKBitmap bitmap;
    SKSize bitmapSize;

    TouchPoint[] touchPoints = new TouchPoint[3];

    MatrixDisplay matrixDisplay = new MatrixDisplay();

    public ShowAffineMatrixPage()
    {
        InitializeComponent();

        string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            bitmap = SKBitmap.Decode(stream);
        }

        touchPoints[0] = new TouchPoint(100, 100);                  // upper-left corner
        touchPoints[1] = new TouchPoint(bitmap.Width + 100, 100);   // upper-right corner
        touchPoints[2] = new TouchPoint(100, bitmap.Height + 100);  // lower-left corner

        bitmapSize = new SKSize(bitmap.Width, bitmap.Height);
        matrix = ComputeMatrix(bitmapSize, touchPoints[0].Center,
                                           touchPoints[1].Center,
                                           touchPoints[2].Center);
    }
    ...
}

An affine matrix is uniquely defined by three points. The three TouchPoint objects correspond to the upper-left, upper-right, and lower-left corners of the bitmap. Because an affine matrix is only capable of transforming a rectangle into a parallelogram, the fourth point is implied by the other three. The constructor concludes with a call to ComputeMatrix, which calculates the cells of an SKMatrix object from these three points.

The TouchAction handler calls the ProcessTouchEvent method of each TouchPoint. The scale value converts from Xamarin.Forms coordinates to pixels:

public partial class ShowAffineMatrixPage : ContentPage
{
    ...
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        bool touchPointMoved = false;

        foreach (TouchPoint touchPoint in touchPoints)
        {
            float scale = canvasView.CanvasSize.Width / (float)canvasView.Width;
            SKPoint point = new SKPoint(scale * (float)args.Location.X,
                                        scale * (float)args.Location.Y);
            touchPointMoved |= touchPoint.ProcessTouchEvent(args.Id, args.Type, point);
        }

        if (touchPointMoved)
        {
            matrix = ComputeMatrix(bitmapSize, touchPoints[0].Center,
                                               touchPoints[1].Center,
                                               touchPoints[2].Center);
            canvasView.InvalidateSurface();
        }
    }
    ...
}

If any TouchPoint has moved, then the method calls ComputeMatrix again and invalidates the surface.

The ComputeMatrix method determines the matrix implied by those three points. The matrix called A transforms a one-pixel square rectangle into a parallelogram based on the three points, while the scale transform called S scales the bitmap to a one-pixel square rectangle. The composite matrix is S × A:

public partial class ShowAffineMatrixPage : ContentPage
{
    ...
    static SKMatrix ComputeMatrix(SKSize size, SKPoint ptUL, SKPoint ptUR, SKPoint ptLL)
    {
        // Scale transform
        SKMatrix S = SKMatrix.MakeScale(1 / size.Width, 1 / size.Height);

        // Affine transform
        SKMatrix A = new SKMatrix
        {
            ScaleX = ptUR.X - ptUL.X,
            SkewY = ptUR.Y - ptUL.Y,
            SkewX = ptLL.X - ptUL.X,
            ScaleY = ptLL.Y - ptUL.Y,
            TransX = ptUL.X,
            TransY = ptUL.Y,
            Persp2 = 1
        };

        SKMatrix result = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref result, A, S);
        return result;
    }
    ...
}

Finally, the PaintSurface method renders the bitmap based on that matrix, displays the matrix at the bottom of the screen, and renders the touch points at the three corners of the bitmap:

public partial class ShowAffineMatrixPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Display the bitmap using the matrix
        canvas.Save();
        canvas.SetMatrix(matrix);
        canvas.DrawBitmap(bitmap, 0, 0);
        canvas.Restore();

        // Display the matrix in the lower-right corner
        SKSize matrixSize = matrixDisplay.Measure(matrix);

        matrixDisplay.Paint(canvas, matrix,
            new SKPoint(info.Width - matrixSize.Width,
                        info.Height - matrixSize.Height));

        // Display the touchpoints
        foreach (TouchPoint touchPoint in touchPoints)
        {
            touchPoint.Paint(canvas);
        }
    }
  }

The iOS screen below shows the bitmap when the page is first loaded, while the two other screens show it after some manipulation:

Triple screenshot of the Show Affine Matrix page

Although it seems as if the touch points drag the corners of the bitmap, that's only an illusion. The matrix calculated from the touch points transforms the bitmap so that the corners coincide with the touch points.

It is more natural for users to move, resize, and rotate bitmaps not by dragging the corners, but by using one or two fingers directly on the object to drag, pinch, and rotate. This is covered in the next article Touch Manipulation.

The Reason for the 3-by-3 Matrix

It might be expected that a two-dimensional graphics system would require only a 2-by-2 transform matrix:

           │ ScaleX  SkewY  │
| x  y | × │                │ = | x'  y' |
           │ SkewX   ScaleY │

This works for scaling, rotation, and even skewing, but it is not capable of the most basic of transforms, which is translation.

The problem is that the 2-by-2 matrix represents a linear transform in two dimensions. A linear transform preserves some basic arithmetic operations, but one of the implications is that a linear transform never alters the point (0, 0). A linear transform makes translation impossible.

In three dimensions, a linear transform matrix looks like this:

              │ ScaleX  SkewYX  SkewZX │
| x  y  z | × │ SkewXY  ScaleY  SkewZY │ = | x'  y'  z' |
              │ SkewXZ  SkewYZ  ScaleZ │

The cell labeled SkewXY means that the value skews the X coordinate based on values of Y; the cell SkewXZ means that the value skews the X coordinate based on values of Z; and values skew similarly for the other Skew cells.

It's possible to restrict this 3D transform matrix to a two-dimensional plane by setting SkewZX and SkewZY to 0, and ScaleZ to 1:

              │ ScaleX  SkewYX   0 │
| x  y  z | × │ SkewXY  ScaleY   0 │ = | x'  y'  z' |
              │ SkewXZ  SkewYZ   1 │

If the two-dimensional graphics are drawn entirely on the plane in 3D space where Z equals 1, the transform multiplication looks like this:

              │ ScaleX  SkewYX   0 │
| x  y  1 | × │ SkewXY  ScaleY   0 │ = | x'  y'  1 |
              │ SkewXZ  SkewYZ   1 │

Everything stays on the two-dimensional plane where Z equals 1, but the SkewXZ and SkewYZ cells effectively become two-dimensional translation factors.

This is how a three-dimensional linear transform serves as a two-dimensional non-linear transform. (By analogy, transforms in 3D graphics are based on a 4-by-4 matrix.)

The SKMatrix structure in SkiaSharp defines properties for that third row:

              │ ScaleX  SkewY   Persp0 │
| x  y  1 | × │ SkewX   ScaleY  Persp1 │ = | x'  y'  z` |
              │ TransX  TransY  Persp2 │

Non-zero values of Persp0 and Persp1 result in transforms that move objects off the two-dimensional plane where Z equals 1. What happens when those objects are moved back to that plane is covered in the article on Non-Affine Transforms.