Transformación de rotación

Exploración de los efectos y las animaciones posibles con la transformación de rotación de SkiaSharp

Con la transformación de rotación, los objetos gráficos de SkiaSharp se liberan de la restricción de la alineación con los ejes horizontales y verticales:

Texto girado alrededor de un centro

Para girar un objeto gráfico alrededor del punto (0, 0), SkiaSharp admite los métodos RotateDegrees y RotateRadians:

public void RotateDegrees (Single degrees)

public Void RotateRadians (Single radians)

Un círculo de 360 grados es lo mismo que 2π radianes, por lo que es fácil pasar de una unidad a otra. Utilice la que le sea más cómoda. Todas las funciones trigonométricas de la clase Math de .NET usan unidades de radianes.

La rotación es en el sentido de las agujas del reloj para aumentar los ángulos. (Aunque la rotación en el sistema de coordenadas cartesianas es en sentido contrario a las agujas del reloj por convención, la rotación en sentido horario es congruente con las coordenadas Y que aumentan hacia abajo como en SkiaSharp). Se permiten ángulos negativos y ángulos mayores que 360 grados.

Las fórmulas de transformación en la rotación son más complejas que las de traslación y escala. Para un ángulo de α, las fórmulas de transformación son:

x' = x•cos(α) – y•sin(α)

y' = x•sin(α) + y•cos(α)

En la página Basic Rotate se demuestra el método RotateDegrees. El archivo BasicRotate.xaml.cs muestra texto con su línea base centrada en la página y lo gira en función de un objeto Slider con un intervalo de –360 a 360. Esta es la parte importante del controlador PaintSurface:

using (SKPaint textPaint = new SKPaint
{
    Style = SKPaintStyle.Fill,
    Color = SKColors.Blue,
    TextAlign = SKTextAlign.Center,
    TextSize = 100
})
{
    canvas.RotateDegrees((float)rotateSlider.Value);
    canvas.DrawText(Title, info.Width / 2, info.Height / 2, textPaint);
}

Dado que la rotación se centra alrededor de la esquina superior izquierda del lienzo, para la mayoría de los ángulos establecidos en este programa, el texto se gira fuera de la pantalla:

Captura de pantalla triple de la página Rotación básica

Con mucha frecuencia, necesitará girar algo centrado alrededor de un punto dinámico especificado mediante estas versiones de los métodos RotateDegrees y RotateRadians:

public void RotateDegrees (Single degrees, Single px, Single py)

public void RotateRadians (Single radians, Single px, Single py)

La página Centered Rotate es igual que Basic Rotate, excepto que la versión expandida de RotateDegrees se usa para establecer el centro de rotación en el mismo punto usado para colocar el texto:

using (SKPaint textPaint = new SKPaint
{
    Style = SKPaintStyle.Fill,
    Color = SKColors.Blue,
    TextAlign = SKTextAlign.Center,
    TextSize = 100
})
{
    canvas.RotateDegrees((float)rotateSlider.Value, info.Width / 2, info.Height / 2);
    canvas.DrawText(Title, info.Width / 2, info.Height / 2, textPaint);
}

Ahora el texto gira alrededor del punto utilizado para colocar el texto, que es el centro horizontal de la línea de base del texto:

Captura de pantalla triple de la página Rotación centrada

Al igual que con la versión centrada del método Scale, la versión centrada de la llamada a RotateDegrees es un acceso directo. Este es el método:

RotateDegrees (degrees, px, py);

Esa llamada es equivalente a lo siguiente:

canvas.Translate(px, py);
canvas.RotateDegrees(degrees);
canvas.Translate(-px, -py);

Descubrirá que a veces puede combinar llamadas a Translate con llamadas a Rotate. Por ejemplo, estas son las llamadas a RotateDegrees y DrawText en la página Centered Rotate.

canvas.RotateDegrees((float)rotateSlider.Value, info.Width / 2, info.Height / 2);
canvas.DrawText(Title, info.Width / 2, info.Height / 2, textPaint);

La llamada a RotateDegrees es equivalente a dos llamadas a Translate y un objeto RotateDegrees no centrado:

canvas.Translate(info.Width / 2, info.Height / 2);
canvas.RotateDegrees((float)rotateSlider.Value);
canvas.Translate(-info.Width / 2, -info.Height / 2);
canvas.DrawText(Title, info.Width / 2, info.Height / 2, textPaint);

La llamada a DrawText para mostrar texto en una ubicación determinada es equivalente a una llamada a Translate para esa ubicación seguida de DrawText en el punto (0, 0):

canvas.Translate(info.Width / 2, info.Height / 2);
canvas.RotateDegrees((float)rotateSlider.Value);
canvas.Translate(-info.Width / 2, -info.Height / 2);
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.DrawText(Title, 0, 0, textPaint);

Las dos llamadas consecutivas Translate se cancelan entre sí:

canvas.Translate(info.Width / 2, info.Height / 2);
canvas.RotateDegrees((float)rotateSlider.Value);
canvas.DrawText(Title, 0, 0, textPaint);

Conceptualmente, las dos transformaciones se aplican en el orden opuesto a cómo aparecen en el código. La llamada a DrawText muestra el texto en la esquina superior izquierda del lienzo. La llamada a RotateDegrees gira ese texto en relación con la esquina superior izquierda. A continuación, la llamada a Translate mueve el texto al centro del lienzo.

Normalmente hay varias maneras de combinar la rotación y la traslación. La página Rotated Text crea la siguiente visualización:

Captura de pantalla triple de la página Texto rotado

Este es el controlador PaintSurface de la clase RotatedTextPage:

static readonly string text = "    ROTATE";
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    using (SKPaint textPaint = new SKPaint
    {
        Color = SKColors.Black,
        TextSize = 72
    })
    {
        float xCenter = info.Width / 2;
        float yCenter = info.Height / 2;

        SKRect textBounds = new SKRect();
        textPaint.MeasureText(text, ref textBounds);
        float yText = yCenter - textBounds.Height / 2 - textBounds.Top;

        for (int degrees = 0; degrees < 360; degrees += 30)
        {
            canvas.Save();
            canvas.RotateDegrees(degrees, xCenter, yCenter);
            canvas.DrawText(text, xCenter, yText, textPaint);
            canvas.Restore();
        }
    }
}

Los valores xCenter y yCenter indican el centro del lienzo. El valor yText se desplaza algo de eso. Este valor es la coordenada Y necesaria para colocar el texto de forma que esté realmente centrado verticalmente en la página. El bucle for establece entonces una rotación según el centro del lienzo. La rotación va en incrementos de 30 grados. El texto se dibuja con el valor yText. El número de espacios en blanco antes de la palabra "ROTATE" en el valor text se determinó empíricamente para hacer que la conexión entre estas 12 cadenas de texto parezca un dodecágono.

Una manera de simplificar este código es incrementar el ángulo de rotación en incrementos de 30 grados mediante el bucle después de la llamada a DrawText. De esta forma se elimina la necesidad de llamar a Save y a Restore. Observe que la variable degrees ya no se usa dentro del cuerpo del bloque for:

for (int degrees = 0; degrees < 360; degrees += 30)
{
    canvas.DrawText(text, xCenter, yText, textPaint);
    canvas.RotateDegrees(30, xCenter, yCenter);
}

También es posible usar la forma simple de RotateDegrees si el bucle va precedido de una llamada a Translate para mover todo al centro del lienzo:

float yText = -textBounds.Height / 2 - textBounds.Top;

canvas.Translate(xCenter, yCenter);

for (int degrees = 0; degrees < 360; degrees += 30)
{
    canvas.DrawText(text, 0, yText, textPaint);
    canvas.RotateDegrees(30);
}

El cálculo de yText modificado ya no incorpora yCenter. Ahora, la llamada a DrawText centra el texto verticalmente en la parte superior del lienzo.

Dado que las transformaciones se aplican conceptualmente al contrario de como aparecen en el código, a menudo es posible comenzar con transformaciones más globales, seguidas de transformaciones más locales. Esta es con frecuencia la manera más fácil de combinar rotación y traslación.

Por ejemplo, supongamos que desea dibujar un objeto gráfico que gira alrededor de su centro de forma muy similar a como un planeta gira sobre su eje. Pero también quiere que este objeto gire alrededor del centro de la pantalla de forma muy similar a como un planeta gira alrededor del sol.

Para ello, coloca el objeto en la esquina superior izquierda del lienzo y, a continuación, usa una animación para girarlo alrededor de esa esquina. A continuación, traslada el objeto horizontalmente como un radio orbital. Ahora aplica una segunda rotación animada, también alrededor del origen. Esto hace que el objeto gire alrededor de la esquina. Ahora, lo traslada al centro del lienzo.

Este es el controlador PaintSurface que contiene estas llamadas de transformación en orden inverso:

float revolveDegrees, rotateDegrees;
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    using (SKPaint fillPaint = new SKPaint
    {
        Style = SKPaintStyle.Fill,
        Color = SKColors.Red
    })
    {
        // Translate to center of canvas
        canvas.Translate(info.Width / 2, info.Height / 2);

        // Rotate around center of canvas
        canvas.RotateDegrees(revolveDegrees);

        // Translate horizontally
        float radius = Math.Min(info.Width, info.Height) / 3;
        canvas.Translate(radius, 0);

        // Rotate around center of object
        canvas.RotateDegrees(rotateDegrees);

        // Draw a square
        canvas.DrawRect(new SKRect(-50, -50, 50, 50), fillPaint);
    }
}

Los campos revolveDegrees y rotateDegrees están animados. Este programa usa una técnica de animación diferente basada en la clase Xamarin.FormsAnimation. (Esta clase se describe en el capítulo 22 de ladescarga en PDF gratuita de Creación de aplicaciones móviles con Xamarin.Forms). La invalidación OnAppearing crea dos objetos Animation con métodos de devolución de llamada y, luego, llama a Commit en ellos durante el tiempo de animación:

protected override void OnAppearing()
{
    base.OnAppearing();

    new Animation((value) => revolveDegrees = 360 * (float)value).
        Commit(this, "revolveAnimation", length: 10000, repeat: () => true);

    new Animation((value) =>
    {
        rotateDegrees = 360 * (float)value;
        canvasView.InvalidateSurface();
    }).Commit(this, "rotateAnimation", length: 1000, repeat: () => true);
}

El primer objeto Animation anima revolveDegrees de 0 a 360 grados durante 10 segundos. El segundo anima rotateDegrees de 0 a 360 grados cada 1 segundo y también invalida la superficie para generar otra llamada al controlador PaintSurface. La invalidación OnDisappearing cancela estas dos animaciones:

protected override void OnDisappearing()
{
    base.OnDisappearing();
    this.AbortAnimation("revolveAnimation");
    this.AbortAnimation("rotateAnimation");
}

El programa Ugly Analog Clock (llamado así porque se describirá un reloj analógico más atractivo en un artículo posterior) utiliza la rotación para dibujar las marcas de hora y minuto del reloj y para girar las manecillas. El programa dibuja el reloj utilizando un sistema de coordenadas arbitrario basado en un círculo centrado en el punto (0, 0) con un radio de 100. Usa la traslación y el escalado para expandir y centrar ese círculo en la página.

Las llamadas a Translate y Scale se aplican globalmente al reloj, por lo que son las primeras a las que se llamará siguiendo la inicialización de los objetos SKPaint:

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

    canvas.Clear();

    using (SKPaint strokePaint = new SKPaint())
    using (SKPaint fillPaint = new SKPaint())
    {
        strokePaint.Style = SKPaintStyle.Stroke;
        strokePaint.Color = SKColors.Black;
        strokePaint.StrokeCap = SKStrokeCap.Round;

        fillPaint.Style = SKPaintStyle.Fill;
        fillPaint.Color = SKColors.Gray;

        // Transform for 100-radius circle centered at origin
        canvas.Translate(info.Width / 2f, info.Height / 2f);
        canvas.Scale(Math.Min(info.Width / 200f, info.Height / 200f));
        ...
    }
}

Hay 60 marcas de dos tamaños diferentes que deben dibujarse en un círculo alrededor del reloj. La llamada a DrawCircle dibuja ese círculo en el punto (0, –90), que, en relación con el centro del reloj, corresponde a las 12:00. La llamada RotateDegrees incrementa el ángulo de rotación en 6 grados después de cada marca de minutero. La variable angle se usa únicamente para determinar si se dibuja un círculo grande o un círculo pequeño:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
        // Hour and minute marks
        for (int angle = 0; angle < 360; angle += 6)
        {
            canvas.DrawCircle(0, -90, angle % 30 == 0 ? 4 : 2, fillPaint);
            canvas.RotateDegrees(6);
        }
    ...
    }
}

Por último, el controlador PaintSurface obtiene la hora actual y calcula los grados de rotación de las manecillas de hora, minuto y segundo. Cada manecilla se dibuja en la posición 12:00 para que el ángulo de rotación sea relativo a eso:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
        DateTime dateTime = DateTime.Now;

        // Hour hand
        strokePaint.StrokeWidth = 20;
        canvas.Save();
        canvas.RotateDegrees(30 * dateTime.Hour + dateTime.Minute / 2f);
        canvas.DrawLine(0, 0, 0, -50, strokePaint);
        canvas.Restore();

        // Minute hand
        strokePaint.StrokeWidth = 10;
        canvas.Save();
        canvas.RotateDegrees(6 * dateTime.Minute + dateTime.Second / 10f);
        canvas.DrawLine(0, 0, 0, -70, strokePaint);
        canvas.Restore();

        // Second hand
        strokePaint.StrokeWidth = 2;
        canvas.Save();
        canvas.RotateDegrees(6 * dateTime.Second);
        canvas.DrawLine(0, 10, 0, -80, strokePaint);
        canvas.Restore();
    }
}

El reloj es ciertamente funcional aunque las manecillas son bastante burdas:

Captura de pantalla triple de la página Ugly Analog Clock Text

Para que el reloj sea más atractivo, consulte el artículo Datos de trazado de SVG en SkiaSharp.