Trasformazione di rotazione

Esplora gli effetti e le animazioni possibili con la trasformazione Ruota skiaSharp

Con la trasformazione di rotazione, gli oggetti grafici SkiaSharp interrompono il vincolo di allineamento con gli assi orizzontali e verticali:

Testo ruotato intorno a un centro

Per ruotare un oggetto grafico intorno al punto (0, 0), SkiaSharp supporta sia un RotateDegrees metodo che un RotateRadians metodo:

public void RotateDegrees (Single degrees)

public Void RotateRadians (Single radians)

Un cerchio di 360 gradi è uguale a 2π radianti, quindi è facile convertire tra le due unità. Usare qualsiasi opzione sia utile. Tutte le funzioni trigonometriche nella classe .NET Math usano unità di radianti.

La rotazione è in senso orario per gli angoli crescenti. Anche se la rotazione sul sistema di coordinate cartesiano è antiorario per convenzione, la rotazione in senso orario è coerente con le coordinate Y che aumentano in modo crescente come in SkiaSharp. Sono consentiti angoli negativi e angoli maggiori di 360 gradi.

Le formule di trasformazione per la rotazione sono più complesse di quelle per la conversione e la scalabilità. Per un angolo di α, le formule di trasformazione sono:

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

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

La pagina Rotazione di base illustra il RotateDegrees metodo . Il file BasicRotate.xaml.cs visualizza del testo con la linea di base allineata al centro della pagina e la ruota in base a un Slider oggetto con un intervallo compreso tra -360 e 360. Ecco la parte pertinente del PaintSurface gestore:

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

Poiché la rotazione è allineata al centro dell'angolo superiore sinistro dell'area di disegno, per la maggior parte degli angoli impostati in questo programma, il testo viene ruotato sullo schermo:

Screenshot triplo della pagina Rotazione di base

Molto spesso è consigliabile ruotare un elemento centrato intorno a un punto pivot specificato usando queste versioni dei RotateDegrees metodi e RotateRadians :

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

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

La pagina Ruota al centro è simile alla rotazione di base, ad eccezione del fatto che la versione espansa di RotateDegrees viene usata per impostare il centro di rotazione sullo stesso punto usato per posizionare il testo:

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

Ora il testo ruota intorno al punto usato per posizionare il testo, ovvero il centro orizzontale della linea di base del testo:

Screenshot triplo della pagina Ruota al centro

Come per la versione centrata del Scale metodo, la versione centrata della RotateDegrees chiamata è un collegamento. Ecco il metodo :

RotateDegrees (degrees, px, py);

Tale chiamata equivale a quanto segue:

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

Si scoprirà che a volte è possibile combinare Translate le chiamate con Rotate le chiamate. Ad esempio, ecco le RotateDegrees chiamate e DrawText nella pagina Ruota al centro;

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

La RotateDegrees chiamata equivale a due Translate chiamate e a un non centrato RotateDegrees:

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 DrawText chiamata per visualizzare il testo in una determinata posizione equivale a una Translate chiamata per tale posizione seguita dal DrawText 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);

Le due chiamate consecutive Translate si annullano l'una dall'altra:

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

Concettualmente, le due trasformazioni vengono applicate nell'ordine opposto a come appaiono nel codice. La DrawText chiamata visualizza il testo nell'angolo superiore sinistro dell'area di disegno. La RotateDegrees chiamata ruota il testo rispetto all'angolo superiore sinistro. La chiamata sposta quindi Translate il testo al centro dell'area di disegno.

Esistono in genere diversi modi per combinare rotazione e traslazione. La pagina Testo ruotato crea la visualizzazione seguente:

Screenshot triplo della pagina Testo ruotato

Ecco il PaintSurface gestore della RotatedTextPage classe :

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

I xCenter valori e yCenter indicano il centro dell'area di disegno. Il yText valore è un piccolo offset rispetto a quello. Questo valore è la coordinata Y necessaria per posizionare il testo in modo che sia realmente centrato verticalmente nella pagina. Il for ciclo imposta quindi una rotazione in base al centro dell'area di disegno. La rotazione è in incrementi di 30 gradi. Il testo viene disegnato utilizzando il yText valore . Il numero di spazi vuoti prima della parola "ROTATE" nel text valore è stato determinato empiricamente per stabilire la connessione tra queste 12 stringhe di testo sembra essere un dodecagon.

Un modo per semplificare questo codice consiste nell'incrementare l'angolo di rotazione di 30 gradi ogni volta che si passa attraverso il ciclo dopo la DrawText chiamata. In questo modo si elimina la necessità di chiamate a Save e Restore. Si noti che la degrees variabile non viene più usata all'interno del corpo del for blocco:

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

È anche possibile usare la forma semplice di RotateDegrees anteponendo il ciclo con una chiamata a per Translate spostare tutto al centro dell'area di disegno:

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

Il calcolo modificato yText non incorpora yCenterpiù . Ora il call center il DrawText testo verticalmente nella parte superiore dell'area di disegno.

Poiché le trasformazioni vengono applicate concettualmente opposte a come vengono visualizzate nel codice, spesso è possibile iniziare con più trasformazioni globali, seguite da più trasformazioni locali. Questo è spesso il modo più semplice per combinare rotazione e traslazione.

Si supponga, ad esempio, di voler disegnare un oggetto grafico che ruota intorno al suo centro in modo molto simile a un pianeta che ruota sull'asse. Ma vuoi anche che questo oggetto ruota intorno al centro dello schermo molto simile a un pianeta che ruota intorno al sole.

È possibile eseguire questa operazione posizionando l'oggetto nell'angolo superiore sinistro dell'area di disegno e quindi usando un'animazione per ruotarla attorno a tale angolo. Successivamente, traslare l'oggetto orizzontalmente come un raggio orbitale. Applicare ora una seconda rotazione animata, anche intorno all'origine. In questo modo l'oggetto ruota attorno all'angolo. Convertire ora al centro dell'area di disegno.

Ecco il PaintSurface gestore che contiene queste chiamate di trasformazione in ordine 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);
    }
}

I revolveDegrees campi e rotateDegrees vengono animati. Questo programma usa una tecnica di animazione diversa basata sulla Xamarin.FormsAnimation classe . Questa classe è descritta nel capitolo 22 di Download pdf gratuito di Creazione di app per dispositivi mobili con Xamarin.Forms) L'override OnAppearing crea due Animation oggetti con metodi di callback e quindi li chiama Commit per una durata di animazione:

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

Il primo Animation oggetto si anima revolveDegrees da 0 gradi a 360 gradi oltre 10 secondi. Il secondo anima rotateDegrees da 0 a 360 gradi ogni 1 secondo e invalida anche la superficie per generare un'altra chiamata al PaintSurface gestore. L'override OnDisappearing annulla queste due animazioni:

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

Il programma Clock analogico brutto (così chiamato perché un orologio analogico più attraente verrà descritto in un articolo successivo) usa la rotazione per disegnare i segni minuti e ore dell'orologio e per ruotare le mani. Il programma disegna l'orologio utilizzando un sistema di coordinate arbitrario basato su un cerchio centrato al punto (0, 0) con un raggio di 100. Usa la traduzione e il ridimensionamento per espandere e allineare il cerchio nella pagina.

Le Translate chiamate e Scale si applicano a livello globale all'orologio, quindi queste sono le prime da chiamare dopo l'inizializzazione degli SKPaint oggetti:

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

Ci sono 60 segni di due diverse dimensioni che devono essere disegnate in un cerchio intorno all'orologio. La DrawCircle chiamata disegna tale cerchio nel punto (0, -90), che rispetto al centro dell'orologio corrisponde alle 12:00. La RotateDegrees chiamata incrementa l'angolo di rotazione di 6 gradi dopo ogni segno di graduazione. La angle variabile viene usata esclusivamente per determinare se viene disegnato un cerchio grande o un piccolo cerchio:

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

Infine, il PaintSurface gestore ottiene l'ora corrente e calcola i gradi di rotazione per l'ora, il minuto e le mani seconde. Ogni mano viene disegnata nella posizione 12:00 in modo che l'angolo di rotazione sia relativo a quanto segue:

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

L'orologio è certamente funzionale anche se le mani sono piuttosto grezze:

Triplo screenshot della pagina Testo orologio analogico brutto

Per un orologio più interessante, vedere l'articolo SVG Path Data in SkiaSharp.