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:
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:
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:
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:
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:
Para que el reloj sea más atractivo, consulte el artículo Datos de trazado de SVG en SkiaSharp.