Compartir a través de


Transformaciones de matriz en SkiaSharp

Profundice en las transformaciones de SkiaSharp con la versátil matriz de transformación

Todas las transformaciones aplicadas al objeto SKCanvas se consolidan en una sola instancia de la estructura SKMatrix. Se trata de una matriz de transformación estándar de 3 a 3 similar a la de todos los sistemas gráficos 2D modernos.

Como ha visto, puede usar transformaciones en SkiaSharp sin conocer la matriz de transformación, pero la matriz de transformación es importante desde una perspectiva teórica, y es fundamental al usar transformaciones para modificar rutas de acceso o para controlar entradas táctiles complejas, que se muestran en este artículo y en la siguiente.

Un mapa de bits sujeto a una transformación affine

La matriz de transformación actual aplicada a la SKCanvas está disponible en cualquier momento accediendo a la propiedad TotalMatrix de solo lectura. Puede establecer una nueva matriz de transformación mediante el método SetMatrix y puede restaurar esa matriz en valores predeterminados llamando a ResetMatrix.

El único miembro de SKCanvas que funciona directamente con la transformación de matriz del lienzo es Concat que concatena dos matrices multiplicándolas juntas.

La matriz de transformación predeterminada es la matriz de identidad y consta de varios 1 en las celdas diagonales y 0 en cualquier otra parte:

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

Puede crear una matriz de identidades mediante el método estático SKMatrix.MakeIdentity:

SKMatrix matrix = SKMatrix.MakeIdentity();

El constructor predeterminado SKMatrixno devuelve una matriz de identidades. Devuelve una matriz con todas las celdas establecidas en cero. No use el constructor SKMatrix a menos que planee establecer esas celdas manualmente.

Cuando SkiaSharp representa un objeto gráfico, cada punto (x, y) se convierte eficazmente en una matriz de 1 a 3 con un 1 en la tercera columna:

| x  y  1 |

Esta matriz de 1 a 3 representa un punto tridimensional con la coordenada Z establecida en 1. Hay razones matemáticas (descritas más adelante) por las que una transformación de matriz bidimensional requiere trabajar en tres dimensiones. Puede pensar en esta matriz de 1 a 3 como que representa un punto en un sistema de coordenadas 3D, pero siempre en el plano 2D donde Z es igual a 1.

Esta matriz de 1 a 3 se multiplica por la matriz de transformación y el resultado es el punto representado en el lienzo:

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

Con la multiplicación de matriz estándar, los puntos convertidos son los siguientes:

x' = x

y' = y

z' = 1

Esa es la transformación predeterminada.

Cuando se llama al método Translate en el objeto SKCanvas, los argumentos tx y ty en el método Translate se convierten en las dos primeras celdas de la tercera fila de la matriz de transformación:

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

La multiplicación es ahora la siguiente:

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

Estas son las fórmulas de transformación:

x' = x + tx

y' = y + ty

Los factores de escalado tienen un valor predeterminado de 1. Al llamar al método Scale en un nuevo objeto SKCanvas, la matriz de transformación resultante contiene los argumentos sx y sy en las celdas diagonales:

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

Las fórmulas de transformación son las siguientes:

x' = sx · x

y' = sy · y

La matriz de transformación después de llamar a Skew contiene los dos argumentos de las celdas de matriz adyacentes a los factores de escalado:

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

Las fórmulas de transformación son:

x' = x + xSkew · y

y' = ySkew · x + y

Para una llamada a RotateDegrees o RotateRadians para un ángulo de α, la matriz de transformación es la siguiente:

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

Estas son las fórmulas de transformación:

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

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

Cuando α es de 0 grados, es la matriz de identidad. Cuando α es de 180 grados, la matriz de transformación es la siguiente:

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

Un giro de 180 grados equivale a voltear un objeto horizontal y verticalmente, lo que también se logra estableciendo factores de escala de –1.

Todos estos tipos de transformaciones se clasifican como transformaciones afín. Las transformaciones afín nunca implican la tercera columna de la matriz, que permanece en los valores predeterminados de 0, 0 y 1. En el artículo Transformaciones no afines se describen las transformaciones no afines.

Multiplicación de matrices

Una ventaja importante con el uso de la matriz de transformación es que las transformaciones compuestas se pueden obtener mediante la multiplicación de matrices, que a menudo se conoce en la documentación de SkiaSharp como concatenación. Muchos de los métodos relacionados con la transformación de SKCanvas hacen referencia a la "preconcatenación" o "preconcat". Esto hace referencia al orden de multiplicación, que es importante porque la multiplicación de matriz no es conmutativa.

Por ejemplo, la documentación del método Translate dice que "Preconcatena la matriz actual con la traducción especificada", mientras que la documentación del método Scale dice que "Preconcatena la matriz actual con la escala especificada".

Esto significa que la transformación especificada por la llamada al método es el multiplicador (el operando izquierdo) y la matriz de transformación actual es la multiplicada (el operando derecho).

Supongamos que se llama a Translate seguido de Scale:

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

La transformación Scale se multiplica por la transformación Translate para la matriz de transformación compuesta:

| 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 podría llamarse antes de Translate de la siguiente manera:

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

En ese caso, el orden de la multiplicación se invierte y los factores de escalado se aplican eficazmente a los factores de traducción:

|  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 |

Este es el método Scale con un punto dinámico:

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

Esto equivale a las siguientes llamadas de traducción y escala:

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

Las tres matrices de transformación se multiplican en orden inverso a partir de cómo aparecen los métodos en el código:

|  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 |

La estructura SKMatrix

La estructura SKMatrix define nueve propiedades de lectura y escritura de tipo float correspondientes a las nueve celdas de la matriz de transformación:

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

SKMatrix también define una propiedad denominada Values de tipo float[]. Esta propiedad se puede usar para establecer u obtener los nueve valores de una toma en el orden ScaleX, SkewX, TransX, SkewY, ScaleY, TransY, Persp0, Persp1 y Persp2.

Las celdas Persp0, Persp1 y Persp2 se describen en el artículo Transformaciones no afines. Si estas celdas tienen sus valores predeterminados de 0, 0 y 1, la transformación se multiplica por un punto de coordenada como este:

              │ 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

Esta es la transformación afín bidimensional completa. La transformación afín conserva las líneas paralelas, lo que significa que un rectángulo nunca se transforma en nada distinto de un paralelogramo.

La estructura SKMatrix define varios métodos estáticos para crear valores SKMatrix. Todos estos devuelven valores SKMatrix:

SKMatrix también define varios métodos estáticos que concatenan dos matrices, lo que significa multiplicarlas. Estos métodos se denominan Concat, PostConcat y PreConcat, y hay dos versiones de cada uno. Estos métodos no tienen valores devueltos; en su lugar, hacen referencia a los valores de SKMatrix existentes a través de argumentos ref. En el ejemplo siguiente, A, B y R (para "resultado") son todos los valores de SKMatrix.

Los dos métodos Concat se llaman así:

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

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

Estos realizan la multiplicación siguiente:

R = B × A

Los otros métodos solo tienen dos parámetros. El primer parámetro se modifica y, a partir de la llamada al método, contiene el producto de las dos matrices. Los dos métodos PostConcat se llaman así:

SKMatrix.PostConcat(ref A, B);

SKMatrix.PostConcat(ref A, ref B);

Estas llamadas realizan la siguiente operación:

A = A × B

Los dos métodos PreConcat son similares:

SKMatrix.PreConcat(ref A, B);

SKMatrix.PreConcat(ref A, ref B);

Estas llamadas realizan la siguiente operación:

A = B × A

Las versiones de estos métodos con todos los argumentos de ref son ligeramente más eficaces al llamar a las implementaciones subyacentes, pero puede resultar confuso para alguien que lea el código y suponiendo que cualquier cosa con un argumento ref se modifique mediante el método. Además, a menudo es conveniente pasar un argumento que es el resultado de uno de los métodos de Make, por ejemplo:

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

Esto crea la siguiente matriz:

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

Esta es la transformación de escala multiplicada por la transformación de traducción. En este caso concreto, la estructura SKMatrix proporciona un acceso directo con un método denominado SetScaleTranslate:

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

Esta es una de las pocas veces en que es seguro usar el constructor SKMatrix. El método SetScaleTranslate establece las nueve celdas de la matriz. También es seguro usar el constructor SKMatrix con los métodos estáticos Rotate y RotateDegrees:

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);

Estos métodos no concatenan una transformación de rotación a una transformación existente. Los métodos establecen todas las celdas de la matriz. Son funcionalmente idénticos a los métodos MakeRotation y MakeRotationDegrees, excepto que no crean instancias del valor de SKMatrix.

Supongamos que tiene un objeto SKPath que desea mostrar, pero prefiere que tenga una orientación algo diferente o un punto central diferente. Puede modificar todas las coordenadas de esa ruta llamando al método Transform de SKPath con un argumento SKMatrix. La página Transformación de ruta muestra cómo hacerlo. La clase PathTransform hace referencia al objeto HendecagramPath en un campo, pero usa su constructor para aplicar una transformación a esa ruta de acceso:

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);
    }
    ...
}

El objeto HendecagramPath tiene un centro en (0, 0) y los 11 puntos de la estrella se extienden hacia fuera desde ese centro en 100 unidades en todas las direcciones. Esto significa que la ruta tiene coordenadas positivas y negativas. La página Transformación de ruta prefiere trabajar con una estrella tres veces más grande y con todas las coordenadas positivas. Además, no quiere que un punto de la estrella apunte directamente hacia arriba. En su lugar, quiere que un punto de la estrella apunte directamente hacia abajo. (Dado que la estrella tiene 11 puntos, no puede tener ambos). Esto requiere girar la estrella en 360 grados dividido entre 22.

El constructor crea un objeto SKMatrix a partir de tres transformaciones independientes mediante el método PostConcat con el siguiente patrón, donde A, B y C son instancias de SKMatrix:

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

Se trata de una serie de multiplicaciones sucesivas, por lo que el resultado es el siguiente:

A × B × C

Las multiplicaciones consecutivas ayudan a comprender lo que hace cada transformación. La transformación de escala aumenta el tamaño de las coordenadas de ruta por un factor de 3, por lo que las coordenadas van de –300 a 300. La transformación de giro gira la estrella alrededor de su origen. Después, la transformación de traducción la desplaza por 300 píxeles hacia la derecha y hacia abajo, por lo que todas las coordenadas se convierten en positivas.

Hay otras secuencias que producen la misma matriz. Esta es otra:

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

Esto gira primero la ruta de acceso alrededor de su centro y, a continuación, lo traduce a 100 píxeles a la derecha y hacia abajo, por lo que todas las coordenadas son positivas. A continuación, la estrella aumenta de tamaño en relación con su nueva esquina superior izquierda, que es el punto (0, 0).

El controlador PaintSurface simplemente puede representar esta ruta:

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);
        }
    }
}

Aparece en la esquina superior izquierda del lienzo:

Captura de pantalla triple de la página Transformación de ruta de acceso

El constructor de este programa aplica la matriz a la ruta con la siguiente llamada:

transformedPath.Transform(matrix);

La ruta no conservar esta matriz como una propiedad. En su lugar, aplica la transformación a todas las coordenadas de la ruta. Si se vuelve a llamar a Transform, la transformación se vuelve a aplicar y la única manera en que puede volver es aplicando otra matriz que deshace la transformación. Afortunadamente, la estructura SKMatrix define un método TryInvert que obtiene la matriz que invierte una matriz determinada:

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

El método se denomina TryInverse porque no todas las matrices son invertibles, pero es probable que no se use una matriz invertible para una transformación de gráficos.

También puede aplicar una transformación de matriz a un valor de SKPoint, una matriz de puntos, un SKRect o incluso un solo número dentro del programa. La estructura SKMatrix admite estas operaciones con una colección de métodos que comienzan con la palabra Map, como estas:

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);

Si usa ese último método, tenga en cuenta que la estructura SKRect no es capaz de representar un rectángulo girado. El método solo tiene sentido para un valor de SKMatrix que representa la traducción y el escalado.

Experimentación interactiva

Una manera de entender la transformación afín es moviendo interactivamente tres esquinas de un mapa de bits alrededor de la pantalla y viendo los resultados de transformación. Esta es la idea detrás de la página de Mostrar matriz afín. Esta página requiere otras dos clases que también se usan en otras demostraciones:

La clase TouchPoint muestra un círculo translúcido que se puede arrastrar alrededor de la pantalla. TouchPoint requiere que un SKCanvasView o un elemento primario de un SKCanvasView tengan asociado el TouchEffect. Establezca la propiedad Capture en true. En el controlador de eventos TouchAction, el programa debe llamar al método ProcessTouchEvent en TouchPoint para cada instancia de TouchPoint. El método devuelve true si el evento táctil dio lugar al movimiento del punto táctil. Además, el controlador de PaintSurface debe llamar al método Paint en cada instancia de TouchPoint y pasarlo al objeto SKCanvas.

TouchPoint muestra una manera común de que un objeto visual SkiaSharp se pueda encapsular en una clase independiente. La clase puede definir propiedades para especificar características del objeto visual y un método denominado Paint con un argumento SKCanvas puede representarlo.

La propiedad Center de TouchPoint indica la ubicación del objeto. Esta propiedad se puede establecer para inicializar la ubicación; la propiedad cambia cuando el usuario arrastra el círculo alrededor del lienzo.

La página Mostrar matriz afín también requiere la clase MatrixDisplay. Esta clase muestra las celdas de un objeto SKMatrix. Tiene dos métodos públicos: Measure para obtener las dimensiones de la matriz representada y Paint para mostrarla. La clase contiene una propiedad MatrixPaint de tipo SKPaint que se puede reemplazar por un color o tamaño de fuente diferente.

El archivo ShowAffineMatrixPage.xaml crea una instancia del SKCanvasView y adjunta un TouchEffect. El archivo de código subyacente ShowAffineMatrixPage.xaml.cs crea tres objetos TouchPoint y, a continuación, los establece en posiciones correspondientes a tres esquinas de un mapa de bits que carga desde un recurso incrustado:

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);
    }
    ...
}

Una matriz afín se define de forma única por tres puntos. Los tres objetos TouchPoint corresponden a las esquinas superior izquierda, superior derecha e inferior izquierda del mapa de bits. Dado que una matriz afín solo es capaz de transformar un rectángulo en un paralegrama, el cuarto punto está implícito en los otros tres. El constructor concluye con una llamada a ComputeMatrix, que calcula las celdas de un objeto SKMatrix a partir de estos tres puntos.

El controlador de TouchAction llama al método ProcessTouchEvent de cada TouchPoint. El valor de scale convierte de coordenadas de Xamarin.Forms a píxeles:

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();
        }
    }
    ...
}

Si TouchPoint se ha movido, el método llama a ComputeMatrix de nuevo e invalida la superficie.

El método ComputeMatrix determina la matriz implícita por esos tres puntos. La matriz denominada A transforma un rectángulo cuadrado de un píxel en un paralelismo basado en los tres puntos, mientras que la transformación de escala denominada S escala el mapa de bits a un rectángulo cuadrado de un píxel. La matriz compuesta es 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;
    }
    ...
}

Por último, el método PaintSurface representa el mapa de bits basado en esa matriz, muestra la matriz en la parte inferior de la pantalla y representa los puntos táctiles en las tres esquinas del mapa de bits:

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);
        }
    }
  }

La pantalla de iOS siguiente muestra el mapa de bits cuando la página se carga por primera vez, mientras que las otras dos pantallas lo muestran después de alguna manipulación:

Captura de pantalla triple de la página Mostrar matriz de Affine

Aunque parece que los puntos táctiles arrastran las esquinas del mapa de bits, es solo una ilusión. La matriz calculada a partir de los puntos táctiles transforma el mapa de bits para que las esquinas coincidan con los puntos táctiles.

Es más natural que los usuarios muevan, cambien el tamaño y giren mapas de bits no arrastrando las esquinas, sino usando uno o dos dedos directamente en el objeto para arrastrar, reducir y girar. Esto se trata en el siguiente artículo Manipulación táctil.

La razón de la matriz 3 a 3

Es posible que se espere que un sistema de gráficos bidimensionales requiera solo una matriz de transformación de 2 a 2:

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

Esto funciona para escalar, rotar e incluso escalonar, pero no es capaz de las transformaciones más básicas, que es la traducción.

El problema es que la matriz de 2 a 2 representa una transformación lineal en dos dimensiones. Una transformación lineal conserva algunas operaciones aritméticas básicas, pero una de las implicaciones es que una transformación lineal nunca modifica el punto (0, 0). Una transformación lineal hace que la traducción sea imposible.

En tres dimensiones, una matriz de transformación lineal tiene este aspecto:

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

La celda etiquetada SkewXY significa que el valor sesga la coordenada X en función de los valores de Y; la celda SkewXZ significa que el valor sesga la coordenada X en función de los valores de Z; y los valores sesgarán de forma similar para las demás celdas de Skew.

Es posible restringir esta matriz de transformación 3D a un plano bidimensional estableciendo SkewZX y SkewZY en 0 y ScaleZ a 1:

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

Si los gráficos bidimensionales se dibujan completamente en el plano en el espacio 3D donde Z es igual a 1, la multiplicación de transformación tiene este aspecto:

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

Todo permanece en el plano bidimensional donde Z es igual a 1, pero las celdas SkewXZ y SkewYZ se convierten efectivamente en factores de traducción bidimensional.

Así es como una transformación lineal tridimensional actúa como una transformación no lineal bidimensional. (Por analogía, las transformaciones en gráficos 3D se basan en una matriz de 4 a 4).

La estructura SKMatrix de SkiaSharp define las propiedades de esa tercera fila:

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

Los valores no cero de Persp0 y Persp1 dan lugar a transformaciones que mueven objetos fuera del plano bidimensional donde Z es igual a 1. Lo que sucede cuando esos objetos se mueven de nuevo a ese plano se tratan en el artículo sobre Transformaciones no afines.