Gráficos vectoriales con Visual Basic .NET

Marzo 2003

Cipriano Valdezate Sayalero y Manuel Valdezate Sayalero

 


 

Matrices

 

Introducción

 

Transformar figuras consiste, como hemos visto, en rotar, trasladar y/o escalar su eje de coordenadas. Intuitivamente, sin embargo, no operamos con ejes de coordenadas, sino con los dibujos mismos. Podríamos, por tanto, definir transformación como un mapeado de puntos, tal que a cada punto de la figura original le correspondiera un punto en la figura transformada o final. El punto de la figura final se obtendría aplicando una operación al de la figura inicial, de tal modo que, si realizáramos una transformación determinada a una figura, todos los puntos de la figura final se obtendrían aplicando la misma operación a sus correspondientes puntos de la figura inicial. Esta operación es una multiplicación y/o suma de matrices, lo cual es lógico, pues podemos considerar un punto como una matriz [x,y] compuesta de una fila y dos columnas.

 

No nos vamos a adentrar en teoría de matrices ni pretendemos ser rigurosos en las definiciones, ni mucho menos nos preocuparemos aquí de demostrar nada algebráicamente, sino que nos limitaremos a exponer la mínima teoría imprescindible para entender el funcionamiento del objeto Matrix, que nos será de gran utilizad para personalizar nuestras transformaciones.

 

Una matriz es un conjunto de valores dispuestos en filas y columnas. En esto, una matriz es lo mismo que un array. Igual que los arrays, las matrices tienen dimensones. Las transformaciones de figuras bidimensionales utilizan matrices de dos dimensiones, es decir, compuestas de filas y columnas. He aquí un ejemplo de una matriz de dos filas y dos columnas:

 

 

Esta es, precisamente, una de las matrices que se utilizan para obtener los puntos de la figura transformada. Cada elemento de la matriz se define por su posición, así, por ejemplo, m(1,2) es el elemento situado en la primera fila y segunda columna. Ésta es una matriz de orden (2,2), que significa que tiene dos filas y dos columnas. Las matrices cuyo número de filas es igual al número de columnas se llaman matrices cuadradas de orden n, por consiguiente nos podemos referir a esta matriz como una matriz cuadrada de orden 2.

 

La diferencia entre los arrays y las matrices radica en que sobre la matriz se definen determinadas propiedades y operaciones que la convierten en un array especial. Expondremos a continuación las operaciones que se aplican en el mapeado de puntos, que, como ya hemos dicho, son la suma y el producto de matrices.

 

Operaciones con matrices

 

Suma de matrices

Sólo se pueden sumar matrices del mismo orden. La matriz suma es otra matriz del mismo orden que se obtiene sumando los elementos de las dos matrices situados en la misma posición. Por ejemplo:

 

 

Aunque, como veremos más adelante, el objeto Matrix realiza todas las operaciones que necesitemos por nosotros, vamos a escribir, a título meramente ilustrativo, una función que halle la suma de dos matrices. Le entregaremos un par de arrays de tipo Decimal y de la misma dimensión que harán de matrices, y devolverá otro array cuyos elementos serán la matriz suma:

 

Public Function Suma(ByVal Matriz1(,) As Decimal, ByVal Matriz2(,) As Decimal) As Decimal(,)

            'Comprobamos que las dos matrices son del mismo orden,

            'es decir, que las dos tienen igual número de filas

            'e igual número de columnas

If Not Matriz1.GetUpperBound(0) = Matriz2.GetUpperBound(0) AndAlso _

Matriz1.GetUpperBound(1) = Matriz2.GetUpperBound(1) Then Exit Function

 

            'Creamos la matriz suma

Dim MatrizSuma(Matriz1.GetUpperBound(0), Matriz1.GetUpperBound(1)) As Decimal

 

            Dim i, j As Short

            'Y sumamos las matrices

            For i = 0 To Matriz1.GetUpperBound(0)

                For j = 0 To Matriz1.GetUpperBound(1)

                    MatrizSuma(i, j) = Matriz1(i, j) + Matriz2(i, j)

                Next

            Next

 

            Return MatrizSuma

End Function

 

Podemos utilizar este procedimiento para porbar la función:

 

Private Sub Prueba_de_Suma()

        Dim A As Array = New Decimal(,) {{2, 3}, {4, 1}}

        Dim B As Array = New Decimal(,) {{1, 2}, {3, 4}}

        Dim R As Decimal(,) = Suma(A, B)

 

MessageBox.Show(String.Format("{0} {1}" _

& ControlChars.CrLf & "{2} {3}", _

R(0, 0), R(0, 1), R(1, 0), R(1, 1)))

 

End Sub

 

El resultado es la misma matriz que obtuvimos arriba.

 

 

Producto de matrices

 

Sólo se pueden multiplicar dos matrices si el número de columnas de la primera coincide con el número de filas de la segunda. El resultado es una matriz de tantas filas como tiene la primera y tantas columnas la segunda. Además, esta operación no es conmutativa, es decir, el orden de los factores sí altera el producto. Expresado algebraicamente:

 

A(m,n) x B(n,p) = R(m,p)

 

De ello se deduce que para que dos matrices se puedan multiplicar en los dos sentidos, es decir, para que podamos efectuar A x B y B x A, las matrices deben ser cuadradas. Ahora bien, la matriz resultante en cada caso sería diferente, porque no es lo mismo A x B que B x A. Esto, como veremos más adelante, tiene una importancia crucial en GDI+, y veremos que variando el orden en que multipliquemos las matrices obtendremos transformaciones diferentes.

 

Los elementos de la matriz producto R no se calculan multiplicando los correspondientes de las matrices A y B, como es el caso de la suma. Requiere alguna cuenta más. Hemos preparado un pequeño truco fácil de seguir que, además de calcular el producto de matrices, nos servirá de guía para escribir, al igual que hemos hecho en el apartado anterior, una función en Visual Basic que, dadas dos matrices cualesquiera que cumplan los requisitos arriba expuestos, nos calculará su matriz producto.

 

Vamos a multiplicar las siguientes matrices  A x B y obtendremos la matriz R, cuyo número de filas es igual al número de filas de A (2) y cuyo número de columnas es igual al número de columnas de B (2). Asímismo, el número de filas de A coincide con el de columnas de B (2). Puesto que vamos a basarnos en este truco para escribir una función en Visual Basic, numeraremos los índices de cada elemento en base 0.

 

 

El truco siguiente halla uno a uno los elementos de la matriz producto R.

Hallaremos, para ejemplificarlo, el elemento R(1,0).

 

Primero escribirmos:

 

A( , ) * B( , )

 

Y lo repetimos tantas veces como el número de columnas de la matriz A = número de filas de la matriz B = 3

 

A( , ) * B( , )

A( , ) * B( , )

A( , ) * B( , )

 

El primer índice del elemento que estamos hallando R(1,0) será el primer índice de todos los elementos de A, y el segundo índice R(1,0) será el segundo índice de todos los elementos B:

 

A(1, ) * B( ,0)

A(1, ) * B( ,0)

A(1, ) * B( ,0)

 

Añadimos los índices que faltan empezando por cero (o por 1, si hubiésemos definido los índices en base 1) e incrementándolos en una unidad de arriba a abajo:

 

A(1,0) * B(0 ,0)

A(1,1) * B(1 ,0)

A(1,2) * B(2 ,0)

 

Localizamos el valor de cada elemento en las matrices A y B, efectuamos las multiplicaciones y sumamos los resultados:

 

A(1,0) * B(0 ,0) = 3 * 1 = 3

A(1,1) * B(1 ,0) = 2 * 3 = 6

A(1,2) * B(2 ,0) = 1 * 2 = 2

 

R(1,0) = 3 + 6 + 2 = 11

 

Aplicando este sencillo truco a cada uno de los elementos de R obtenemos la matriz producto R = A x B:

 

 

He aquí la función:

 

Public Function Producto(ByVal A(,) As Decimal, ByVal B(,) As Decimal) As Decimal(,)

 

            'Comprobamos que las matrices cumplen los requisitos

            If A.GetUpperBound(1) <> B.GetUpperBound(0) Then

                 Exit Function

            End If

 

            Dim i, j, k As Short

            'Creamos la matriz producto

            Dim R(A.GetUpperBound(0), B.GetUpperBound(1)) As Decimal

 

            'Este array de dos columnas

            'es meramente operativo

            'equivale a las dos columnas

            'que hemos creado en el ejemplo:

            'A(1,0) * B(0 ,0)

            'A(1,1) * B(1 ,0)

            'A(1,2) * B(2 ,0)

            Dim T(A.GetUpperBound(1), 1) As Decimal

 

            'Los dos primeros bucles For

            'sirven para ir visitando

            'todas las posiciones de la matriz R

            For i = 0 To R.GetUpperBound(0)

                For j = 0 To R.GetUpperBound(1)

 

                    'i contiene el primer índice de R

                    'que colocamos en todas las posiciones

                    'A(i, ) de los elementos de la primera matriz

                    'A la vez, en la segunda posción A( ,k)

                    'vamos aumentando el índice desde cero

                    'hasta el número de columnas de A,

                    'que coincide con el de filas de B

                    'igual que hemos hecho en el ejemplo

                    For k = 0 To A.GetUpperBound(1)

                        T(k, 0) = A(i, k)

                    Next

 

                    'j contiene el segundo índice de R

                    'que colocamos en todas las posiciones

                    'B(,j) de los elementos de la segunda matriz

                    'A la vez, en la primera posción B(k, )

                    'vamos aumentando el índice desde cero

                    'hasta el número de filas de B,

                    'que coincide con el de columnas de A

                    'igual que hemos hecho en el ejemplo

                    For k = 0 To B.GetUpperBound(0)

                        T(k, 1) = B(k, j)

                    Next

 

                    'Ya tenemos definidos todos los elementos.

                    'Multiplicamos cada pareja del array T

                    'y vamos acumulando los resultados.

                    'El resultado final lo colocamos en su posición

                    'en la matriz producto R

                    'exactamente igual que en el ejemplo

                    For k = 0 To T.GetUpperBound(0)

                        R(i, j) += T(k, 0) * T(k, 1)

                    Next

 

                Next

            Next

 

            Return R

 

End Function

 

Pruebe el lector a multiplicar las matrices del ejemplo:

 

Private Sub Prueba_de_producto()

        Dim A(,) As Decimal = {{1, 2, 3}, {3, 2, 1}}

        Dim B(,) As Decimal = {{1, 2}, {3, 1}, {2, 1}}

 

        Dim R(,) As Decimal = Producto(A, B)

 

MessageBox.Show(String.Format("{0} {1}" _

& ControlChars.CrLf & "{2} {3}", _

R(0, 0), R(0, 1), R(1, 0), R(1, 1)))

 

End Sub

 

Tipos de matrices

 

Ahora que sabemos operar con matrices, vamos a tipificar las matrices que generan las tres transformaciones básicas (traslación, rotación y escalado) y otras para las que VSNET no ofrece implementación ad hoc.

 

El método que genera transformaciones multiplicando todos los puntos de la figura origen por la matriz que recibe como parámetro se llama Transform(Matrix). El constructor de la clase Matrix espera 6 parámetros: cuatro de ellos representan la matriz cuadrada que multiplicada por todos los puntos de la figura origen genera diversos tipos de transformaciones, incluida la rotación que acabamos de ver, y los otros dos representan la matriz de una fila y dos columnas que sumada a todos los puntos de la figura origen genera, como también hemos visto más arriba, la figura trasladada. Incluimos en su lugar dentro de las matrices los nombres de los parámetros para que el lector los identifique

 

Matriz generadora de transformaciones multiplicando por ella los puntos de la figura origen:

 

Matriz que sumada a cada punto de la figura origen genera su traslación: [dx, dy]

 

La sintáxis del constructor del objeto Matrix es la siguiente:

 

New Matrix(m11, m12, m21, m22, dx, dy)

 

 

Traslación

Se obtiene sumando a cada punto una matriz, obviamente compuesta de una fila y

dos columnas:

 

Punto: [x, y]

Matriz de traslación: [dx, dy]

 

Matriz final: [x, y] + [dx, dy] = [x+dx, y+dy]

 

El valor dx representa el movimiento por el eje de abcisas X y dy el movimiento por

el eje de ordenadas Y.

 

En el apartado anterior hemos visto que el objeto Matrix incluye la matriz de traslación [dx, dy]. Si queremos, por tanto, aplicar una sólo traslación mediante matrices, los elementos mxx han de representar la matriz identidad, que explicamos a continuación. Por ejemplo, si queremos mover una figura 15 píxeles por el eje X y 10 por el eje Y, la línea quedaría así: ("Trayecto" representa un Graphicspath)

 

Trayecto.Transform(New Matrix(1, 0, 0, 1, 15, 10))

 

Identidad

Transformación identidad es aquella que genera una figura exactamente igual, en la

misma posición, tamaño y orientación que la original, y se obtiene entregando

como parámetro al método Transform la matriz identidad:

 

Rotación

Consiste en el giro de los ejes de coordenadas. Se genera entregando al método

Transform la matriz siguiente:

 

donde a representa los grados de giro medidos en radianes. Los radianes miden la

longitud del arco cortado por los dos radios de longitud = 1 que distan entre sí los

grados especificados. Como la circunferencia mide 2pr y r=1, entonces 360º = 2p

radiantes, o lo que es lo mismo, 180º son p radiantes, y por tanto 1 grado =

p/180 radianes, de modo que si queremos expresar la matriz en grados, para así

utilizar la misma unidad de medida que entregábamos al método RotateTransform,

la matriz de rotación nos queda así:

 

 

Aplicando esta matriz obtenemos una figura girada g grados en el sentido de las

agujas del reloj.

 

A continuación presentamos un ejemplo:

 

Private Sub Rotación_con_y_sin_matriz()

Dim i As Short

Dim Lienzo As Graphics = Me.CreateGraphics

 

'Movemos el eje de coordenadas a un lugar cercano al centro

del formulario

Lienzo.TranslateTransform(Me.ClientSize.Width / 2, _

Me.ClientSize.Height / 2 - 100)

 

'Medidas del rectángulo

Dim Esquina As New Point(100, 0)

Dim Tamaño As New Size(150, 50)

 

'Dibujamos una línea desde el origen de coordenadas

'hasta el vértice superior izquierdo del rectángulo

Dim Trayecto1 As New GraphicsPath()

Trayecto1.AddLine(New Point(0, 0), Esquina)

Dim Lápiz1 As New Pen(Color.LightGreen, 8)

Lápiz1.EndCap = LineCap.ArrowAnchor

Lienzo.DrawPath(Lápiz1, Trayecto1)

 

'Dibujamos el rectángulo

Dim Trayecto2 As New GraphicsPath()

Trayecto2.AddRectangle(New Rectangle(Esquina, Tamaño))

Lienzo.FillPath(Brushes.Blue, Trayecto2)

 

'Construimos la matriz. Queremos rotar el rectángulo 90 grados

'y no queremos trasladar la figura, por eso

'los valores dx y dy son cero.

Dim D As Single = 90.0R * PI / 180

Dim Matriz As Matrix = New Matrix(Cos(D), Sin(D), -Sin(D),

Cos(D), 0.0F, 0.0F)

'Rotar el objeto Graphics mediante el método RotateTransform

...

Lienzo.RotateTransform(90) 'Inhabilita esta línea si quieres ver

'los métodos Trayecto1.Transform(M) y Trayecto2.Transform(M)

en acción

'... es lo mismo que entregar al método Tranform de los

GraphicPath

'la matriz M arriba definida

Trayecto1.Transform(Matriz) 'Inhabilita estas dos líneas si

quieres

Trayecto2.Transform(Matriz) 'ver el método

Lienzo.RotateTransform(90) en acción

 

'Dibujamos las figuras encapsuladas en los dos GraphicsPath.

'No hay que perder de vista que toda transformación se ejecuta

sobre

'el eje de coordenadas, no sobre las figuras mismas.

'Por tanto, tras transformar los ejes de coordenadas,

'debemos dibujar "manualmente" las figuras.

Lienzo.DrawPath(Lápiz1, Trayecto1)

Lienzo.FillPath(Brushes.Blue, Trayecto2)

 

Trayecto1.Dispose()

Trayecto2.Dispose()

Lienzo.Dispose()

End Sub

 

 

 

El ejemplo siguiente genera esta misma transformación “manualmente” utilizando

nuestra función de multiplicación de matrices. Multiplicaremos cada uno de los

cuatro vértices del rectángulo por la matriz, y el resultado serán sido los cuatro

vértices del rectángulo girado 90º que vemos en el dibujo. De hecho, todas las

tranformaciones expuestas en este capítulo pueden simularse manualmente

utilizando nuestras funciones Suma y Producto de matrices.

 

Private Sub Rotación_manual_Click()

Dim i As Short

Dim Lienzo As Graphics = Me.CreateGraphics

 

'Movemos el eje de coordenadas a un lugar cercano al centro

del formulario

Lienzo.TranslateTransform(Me.ClientSize.Width / 2, _

Me.ClientSize.Height / 2 - 100)

 

'Medidas del rectángulo

Dim Esquina As New Point(100, 0)

Dim Tamaño As New Size(150, 50)

 

'Dibujamos una línea desde el origen de coordenadas

'hasta el vértice superior izquierdo del rectángulo

Dim Lápiz1 As New Pen(Color.LightGreen, 8)

Lápiz1.EndCap = LineCap.ArrowAnchor

Lienzo.DrawLine(Lápiz1, New Point(0, 0), Esquina)

 

'Dibujamos el rectángulo original

Lienzo.FillRectangle(Brushes.Blue, New Rectangle(Esquina,

Tamaño))

 

'Hallamos los vértices del rectángulo

Dim N1 As Point = Esquina 'Noroeste

Dim N2 As Point = New Point(N1.X + Tamaño.Width, N1.Y)

'Noreste

Dim N3 As Point = New Point(N2.X, N2.Y + Tamaño.Height)

'Sureste

Dim N4 As Point = New Point(N1.X, N3.Y) 'Suroeste

 

'Construimos la matriz de rotación.

'Queremos rotar el rectángulo 90 grados

Dim D As Single = 90.0R * PI / 180

Dim Matriz As Array = New Decimal(,) {{Cos(D), Sin(D)}, {-

Sin(D), Cos(D)}}

 

'Hallamos los vértices del rectángulo transformado

'Primero hallamos las matrices producto

Dim K1(,) As Decimal = Producto(New Decimal(,) {{N1.X, N1.Y}},

Matriz)

Dim K2(,) As Decimal = Producto(New Decimal(,) {{N2.X, N2.Y}},

Matriz)

Dim K3(,) As Decimal = Producto(New Decimal(,) {{N3.X, N3.Y}},

Matriz)

Dim K4(,) As Decimal = Producto(New Decimal(,) {{N4.X, N4.Y}},

Matriz)

 

'y luego las convertimos en puntos

Dim T1 As Point = New Point(K1(0, 0), K1(0, 1))

Dim T2 As Point = New Point(K2(0, 0), K2(0, 1))

Dim T3 As Point = New Point(K3(0, 0), K3(0, 1))

Dim T4 As Point = New Point(K4(0, 0), K4(0, 1))

 

'y por fin dibujamos el rectángulo transformado "a mano"

Lienzo.FillPolygon(Brushes.Blue, New Point() {T1, T2, T3, T4})

'y la flecha

Lienzo.DrawLine(Lápiz1, New Point(0, 0), T1)

Lienzo.Dispose()

‘El resultado es exactamente el mismo obtenido con la función anterior

End Sub

 

Deformación

La deformación ocurre cuando giramos el eje X k grados y el eje Y l grados y k

<> l. Es decir, cada eje gira independientemente del otro. El ángulo que forma

cada brazo del eje con su contiguo deja de ser necesariamente de noventa grados,

y por tanto los ángulos de la figura se alteran. Esta es la matriz:

 

 

Donde k son los grados que gira el eje X y l los gradpos que gira el eje Y.

 

Veamos un ejemplo:

 

Private Sub Deformación()

Dim