Efectos de ruta de acceso en SkiaSharp

Download SampleDescargar el ejemplo

Descubra los distintos efectos de trazado que permiten usar los trazados para el trazado y el relleno

Un efecto de ruta de acceso es una instancia de la clase SKPathEffect que se crea con uno de los ocho métodos de creación estáticos definidos por la clase. A continuación, el objeto SKPathEffect se establece en la propiedad PathEffect de un objeto SKPaint para una variedad de efectos interesantes, por ejemplo, pulsando una línea con una ruta de acceso replicada pequeña:

The Linked Chain sample

Los efectos de la ruta de acceso permiten:

  • Trazo de una línea con puntos y guiones
  • Trazo de una línea con cualquier ruta de acceso rellenada
  • Rellenar un área con líneas de sombreado
  • Rellenar un área con una ruta de acceso en mosaico
  • Hacer esquinas afiladas redondeadas
  • Agregar "vibración" aleatoria a líneas y curvas

Además, puede combinar dos o más efectos de ruta de acceso.

En este artículo también se muestra cómo usar el método GetFillPath de SKPaint para convertir una ruta de acceso en otra mediante la aplicación de propiedades de SKPaint, incluidos StrokeWidth y PathEffect. Esto da como resultado algunas técnicas interesantes, como obtener una ruta de acceso que es un contorno de otra ruta de acceso. GetFillPath también resulta útil en relación con los efectos de la ruta de acceso.

Puntos y guiones

El uso del método PathEffect.CreateDash se describió en el artículo Puntos y guiones. El primer argumento del método es una matriz que contiene un número par de dos o más valores, alternando entre longitudes de guiones y longitudes de huecos entre los guiones:

public static SKPathEffect CreateDash (Single[] intervals, Single phase)

Estos valores no son relativos al ancho del trazo. Por ejemplo, si el ancho del trazo es 10 y desea una línea compuesta de guiones cuadrados y espacios cuadrados, establezca la matriz de intervals en { 10, 10 }. El argumento phase indica dónde comienza la línea dentro del patrón de guiones. En este ejemplo, si desea que la línea comience con el espacio cuadrado, establezca phase en 10.

Los extremos de los guiones se ven afectados por la propiedad StrokeCap de SKPaint. Para anchos de trazos anchos, es muy común establecer esta propiedad en SKStrokeCap.Round para redondear los extremos de los guiones. En este caso, los valores de la matriz intervalsno incluyan la longitud adicional resultante del redondeo. Este hecho significa que un punto circular requiere especificar un ancho de cero. Para un ancho de trazo de 10, para crear una línea con puntos circulares y huecos entre los puntos del mismo diámetro, use una matriz intervals de { 0, 20 }.

La página Texto con puntos animados es similar a la página Texto descrito en el artículo Integración de texto y gráficos en que muestra caracteres de texto descritos estableciendo la propiedad Style del objeto SKPaint en SKPaintStyle.Stroke. Además, el Texto animado con puntos usa SKPathEffect.CreateDash para darle a este contorno una apariencia de puntos, y el programa también anima el argumento phase del método SKPathEffect.CreateDash para hacer que los puntos parezcan viajar alrededor de los caracteres del texto. Esta es la página en modo horizontal:

Triple screenshot of the Animated Dotted Text page

La clase AnimatedDottedTextPage comienza definiendo algunas constantes y también invalida los métodos OnAppearing y OnDisappearing para la animación:

public class AnimatedDottedTextPage : ContentPage
{
    const string text = "DOTTED";
    const float strokeWidth = 10;
    static readonly float[] dashArray = { 0, 2 * strokeWidth };

    SKCanvasView canvasView;
    bool pageIsActive;

    public AnimatedDottedTextPage()
    {
        Title = "Animated Dotted Text";

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();
        pageIsActive = true;

        Device.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
        {
            canvasView.InvalidateSurface();
            return pageIsActive;
        });
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        pageIsActive = false;
    }
    ...
}

El controlador PaintSurface comienza creando un objeto SKPaint para mostrar el texto. La propiedad TextSize se ajusta en función del ancho de la pantalla:

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

        canvas.Clear();

        // Create an SKPaint object to display the text
        using (SKPaint textPaint = new SKPaint
            {
                Style = SKPaintStyle.Stroke,
                StrokeWidth = strokeWidth,
                StrokeCap = SKStrokeCap.Round,
                Color = SKColors.Blue,
            })
        {
            // Adjust TextSize property so text is 95% of screen width
            float textWidth = textPaint.MeasureText(text);
            textPaint.TextSize *= 0.95f * info.Width / textWidth;

            // Find the text bounds
            SKRect textBounds = new SKRect();
            textPaint.MeasureText(text, ref textBounds);

            // Calculate offsets to center the text on the screen
            float xText = info.Width / 2 - textBounds.MidX;
            float yText = info.Height / 2 - textBounds.MidY;

            // Animate the phase; t is 0 to 1 every second
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 1 / 1);
            float phase = -t * 2 * strokeWidth;

            // Create dotted line effect based on dash array and phase
            using (SKPathEffect dashEffect = SKPathEffect.CreateDash(dashArray, phase))
            {
                // Set it to the paint object
                textPaint.PathEffect = dashEffect;

                // And draw the text
                canvas.DrawText(text, xText, yText, textPaint);
            }
        }
    }
}

Al final del método, se llama al método SKPathEffect.CreateDash mediante dashArray que se define como un campo y el valor de phase animado. La instancia de SKPathEffect se establece en la propiedad PathEffect del objeto SKPaint para mostrar el texto.

Como alternativa, puede establecer el objeto SKPathEffect en el objeto SKPaint antes de medir el texto y centrarlo en la página. Sin embargo, en ese caso, los puntos animados y guiones provocan cierta variación en el tamaño del texto representado, y el texto tiende a vibrar un poco. (¡Pruébelo!)

También observará que, como los puntos animados rodean los caracteres de texto, hay un punto determinado en cada curva cerrada donde los puntos parecen aparecer y salir de la existencia. Aquí es donde comienza y termina la ruta de acceso que define el contorno de caracteres. Si la longitud de la ruta de acceso no es un múltiplo entero de la longitud del patrón de guion (en este caso, 20 píxeles), solo puede caber parte de ese patrón al final de la ruta de acceso.

Es posible ajustar la longitud del patrón de guion para ajustarse a la longitud de la ruta de acceso, pero eso requiere determinar la longitud de la ruta de acceso, una técnica que se trata en el artículo Información de ruta de acceso y enumeración.

El programa Transformación del punto/guión anima el propio patrón de guiones para que los guiones parezcan dividirse en puntos, que se combinan para formar guiones de nuevo:

Triple screenshot of the Dot Dash Morph page

La clase DotDashMorphPage invalida los métodos OnAppearing y OnDisappearing igual que lo hizo el programa anterior, pero la clase define el objeto SKPaint como campo:

public class DotDashMorphPage : ContentPage
{
    const float strokeWidth = 30;
    static readonly float[] dashArray = new float[4];

    SKCanvasView canvasView;
    bool pageIsActive = false;

    SKPaint ellipsePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = strokeWidth,
        StrokeCap = SKStrokeCap.Round,
        Color = SKColors.Blue
    };
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Create elliptical path
        using (SKPath ellipsePath = new SKPath())
        {
            ellipsePath.AddOval(new SKRect(50, 50, info.Width - 50, info.Height - 50));

            // Create animated path effect
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 3 / 3);
            float phase = 0;

            if (t < 0.25f)  // 1, 0, 1, 2 --> 0, 2, 0, 2
            {
                float tsub = 4 * t;
                dashArray[0] = strokeWidth * (1 - tsub);
                dashArray[1] = strokeWidth * 2 * tsub;
                dashArray[2] = strokeWidth * (1 - tsub);
                dashArray[3] = strokeWidth * 2;
            }
            else if (t < 0.5f)  // 0, 2, 0, 2 --> 1, 2, 1, 0
            {
                float tsub = 4 * (t - 0.25f);
                dashArray[0] = strokeWidth * tsub;
                dashArray[1] = strokeWidth * 2;
                dashArray[2] = strokeWidth * tsub;
                dashArray[3] = strokeWidth * 2 * (1 - tsub);
                phase = strokeWidth * tsub;
            }
            else if (t < 0.75f) // 1, 2, 1, 0 --> 0, 2, 0, 2
            {
                float tsub = 4 * (t - 0.5f);
                dashArray[0] = strokeWidth * (1 - tsub);
                dashArray[1] = strokeWidth * 2;
                dashArray[2] = strokeWidth * (1 - tsub);
                dashArray[3] = strokeWidth * 2 * tsub;
                phase = strokeWidth * (1 - tsub);
            }
            else               // 0, 2, 0, 2 --> 1, 0, 1, 2
            {
                float tsub = 4 * (t - 0.75f);
                dashArray[0] = strokeWidth * tsub;
                dashArray[1] = strokeWidth * 2 * (1 - tsub);
                dashArray[2] = strokeWidth * tsub;
                dashArray[3] = strokeWidth * 2;
            }

            using (SKPathEffect pathEffect = SKPathEffect.CreateDash(dashArray, phase))
            {
                ellipsePaint.PathEffect = pathEffect;
                canvas.DrawPath(ellipsePath, ellipsePaint);
            }
        }
    }
}

El controlador PaintSurface crea una ruta de acceso elíptica basada en el tamaño de la página y ejecuta una larga sección de código que establece las variables dashArray y phase. A medida que la variable animada t oscila entre 0 y 1, los bloques de if dividen ese tiempo en cuatro cuartos y, en cada uno de esos cuartos, tsub también oscila entre 0 y 1. Al final, el programa crea SKPathEffect y lo establece en el objeto SKPaint para dibujar.

De ruta de acceso a ruta de acceso

El método GetFillPath de SKPaint convierte una ruta de acceso en otra en función de la configuración del objeto SKPaint. Para ver cómo funciona esto, reemplace la llamada canvas.DrawPath en el programa anterior por el código siguiente:

SKPath newPath = new SKPath();
bool fill = ellipsePaint.GetFillPath(ellipsePath, newPath);
SKPaint newPaint = new SKPaint
{
    Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);

En este nuevo código, la llamada GetFillPath convierte ellipsePath (que es simplemente un óvalo) en newPath, que a continuación se muestra con newPaint. El objeto newPaint se crea con todos los valores de propiedad predeterminados, excepto que la propiedad Style se establece en función del valor devuelto booleano de GetFillPath.

Los objetos visuales son idénticos, excepto el color, que se establece en ellipsePaint pero no newPaint. En lugar de la elipse simple definida en ellipsePath, newPath contiene numerosos contornos de trazado que definen la serie de puntos y guiones. Este es el resultado de aplicar varias propiedades de ellipsePaint (en concreto, StrokeWidth, StrokeCapy PathEffect) a ellipsePath y colocar la ruta de acceso resultante en newPath. El GetFillPath método devuelve un valor booleano que indica si se va a rellenar la ruta de acceso de destino; en este ejemplo, el valor devuelto es true para rellenar la ruta de acceso.

Pruebe a cambiar la configuración de Style en newPaint a SKPaintStyle.Stroke y verá los contornos de ruta individuales descritos con una línea de ancho de un píxel.

Trazar con una ruta de acceso

El método SKPathEffect.Create1DPath es conceptualmente similar a SKPathEffect.CreateDash, salvo que se especifica una ruta de acceso en lugar de un patrón de guiones y huecos. Esta ruta de acceso se replica varias veces para trazar la línea o curva.

La sintaxis es:

public static SKPathEffect Create1DPath (SKPath path, Single advance,
                                         Single phase, SKPath1DPathEffectStyle style)

En general, la ruta de acceso que pase a Create1DPath será pequeña y centrada alrededor del punto (0, 0). El parámetro advance indica la distancia entre los centros de la ruta de acceso a medida que la ruta de acceso se replica en la línea. Normalmente, este argumento se establece en el ancho aproximado de la ruta de acceso. El argumento phase desempeña el mismo papel que en el método CreateDash.

SKPath1DPathEffectStyle tiene tres miembros:

  • Translate
  • Rotate
  • Morph

El miembro Translate hace que la ruta de acceso permanezca en la misma orientación que se replica a lo largo de una línea o curva. Para Rotate, la ruta de acceso se gira en función de una tangente a la curva. La ruta de acceso tiene su orientación normal para las líneas horizontales. Morph es similar a Rotate, salvo que la propia ruta de acceso también está curvada para que coincida con la curvatura de la línea que se va a trazar.

En la página Efecto de ruta de acceso 1D se muestran estas tres opciones. El archivo OneDimensionalPathEffectPage.xaml define un selector que contiene tres elementos correspondientes a los tres miembros de la enumeración:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Curves.OneDimensionalPathEffectPage"
             Title="1D Path Effect">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Picker x:Name="effectStylePicker"
                Title="Effect Style"
                Grid.Row="0"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type x:String}">
                    <x:String>Translate</x:String>
                    <x:String>Rotate</x:String>
                    <x:String>Morph</x:String>
                </x:Array>
            </Picker.ItemsSource>
            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>

        <skia:SKCanvasView x:Name="canvasView"
                           PaintSurface="OnCanvasViewPaintSurface"
                           Grid.Row="1" />
    </Grid>
</ContentPage>

El archivo de código subyacente OneDimensionalPathEffectPage.xaml.cs define tres objetos SKPathEffect como campos. Todos se crean mediante SKPathEffect.Create1DPath con SKPath objetos creados a través de SKPath.ParseSvgPathData. La primera es una caja simple, la segunda es una forma de diamante y la tercera es un rectángulo. Se usan para demostrar los tres estilos de efecto:

public partial class OneDimensionalPathEffectPage : ContentPage
{
    SKPathEffect translatePathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 -10 L 10 -10, 10 10, -10 10 Z"),
                                  24, 0, SKPath1DPathEffectStyle.Translate);

    SKPathEffect rotatePathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 0 L 0 -10, 10 0, 0 10 Z"),
                                  20, 0, SKPath1DPathEffectStyle.Rotate);

    SKPathEffect morphPathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -25 -10 L 25 -10, 25 10, -25 10 Z"),
                                  55, 0, SKPath1DPathEffectStyle.Morph);

    SKPaint pathPaint = new SKPaint
    {
        Color = SKColors.Blue
    };

    public OneDimensionalPathEffectPage()
    {
        InitializeComponent();
    }

    void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        if (canvasView != null)
        {
            canvasView.InvalidateSurface();
        }
    }

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

        canvas.Clear();

        using (SKPath path = new SKPath())
        {
            path.MoveTo(new SKPoint(0, 0));
            path.CubicTo(new SKPoint(2 * info.Width, info.Height),
                         new SKPoint(-info.Width, info.Height),
                         new SKPoint(info.Width, 0));

            switch ((string)effectStylePicker.SelectedItem))
            {
                case "Translate":
                    pathPaint.PathEffect = translatePathEffect;
                    break;

                case "Rotate":
                    pathPaint.PathEffect = rotatePathEffect;
                    break;

                case "Morph":
                    pathPaint.PathEffect = morphPathEffect;
                    break;
            }

            canvas.DrawPath(path, pathPaint);
        }
    }
}

El controlador PaintSurface crea una curva Bézier que recorre en bucle por sí mismo y accede al selector para determinar qué PathEffect se debe usar para trazarlo. Las tres opciones ( Translate, Rotatey Morph ) se muestran de izquierda a derecha:

Triple screenshot of the 1D Path Effect page

La ruta de acceso especificada en el método SKPathEffect.Create1DPath siempre se rellena. La ruta de acceso especificada en el método DrawPath siempre se traza si el objeto SKPaint tiene su propiedad PathEffect establecida en un efecto de ruta de acceso 1D. Tenga en cuenta que el objeto pathPaint no tiene configuración Style, que normalmente tiene como valor predeterminado Fill, pero la ruta de acceso se traza independientemente.

El cuadro usado en el ejemplo de Translate es de 20 píxeles cuadrados y el argumento advance se establece en 24. Esta diferencia provoca un hueco entre los cuadros cuando la línea es aproximadamente horizontal o vertical, pero los cuadros se superponen un poco cuando la línea es diagonal porque la diagonal del cuadro es de 28,3 píxeles.

La forma de diamante ejemplo Rotate también tiene un ancho de 20 píxeles. advance se establece en 20 para que los puntos continúen tocándose a medida que el diamante se gira junto con la curvatura de la línea.

La forma del rectángulo del ejemplo de Morph es de 50 píxeles de ancho con un valor de advance de 55 para hacer un pequeño hueco entre los rectángulos a medida que se doblan alrededor de la curva Bézier.

Si el argumento advance es menor que el tamaño de la ruta de acceso, las rutas de acceso replicadas se pueden superponer. Esto puede dar lugar a algunos efectos interesantes. La página Cadena vinculada muestra una serie de círculos superpuestos que parecen parecerse a una cadena vinculada, que se bloquea en la forma distintiva de un catenario:

Triple screenshot of the Linked Chain page

Mire muy cerca y verá que esos no son realmente círculos. Cada vínculo de la cadena es de dos arcos, de tamaño y posición, por lo que parecen conectarse con vínculos adyacentes.

Una cadena o cable de distribución uniforme de peso se bloquea en forma de catenario. Un arco construido en forma de un catenario invertido se beneficia de una distribución igual de presión del peso de un arco. La catenaria tiene una descripción matemática aparentemente simple:

y = a · cosh(x / a)

El cosh es la función de coseno hiperbólico. Para x igual a 0, cosh es cero e y es igual a a. Ese es el centro de la catenaria. Al igual que la función de coseno, se dice que cosh es incluso, lo que significa que cosh(–x) es igual a cosh(x) y los valores aumentan para aumentar los argumentos positivos o negativos. Estos valores describen las curvas que forman los lados del catenario.

Buscar el valor adecuado de a para ajustar la catenaria a las dimensiones de la página del teléfono no es un cálculo directo. Si w y h son el ancho y alto de un rectángulo, el valor óptimo de a satisface la siguiente ecuación:

cosh(w / 2 / a) = 1 + h / a

El siguiente método de la clase LinkedChainPage incorpora esa igualdad haciendo referencia a las dos expresiones de la izquierda y derecha del signo igual como left y right. Para valores pequeños de un, left es mayor que right; para valores grandes de a, left es menor que right. El bucle while se estrecha en un valor óptimo de a:

float FindOptimumA(float width, float height)
{
    Func<float, float> left = (float a) => (float)Math.Cosh(width / 2 / a);
    Func<float, float> right = (float a) => 1 + height / a;

    float gtA = 1;         // starting value for left > right
    float ltA = 10000;     // starting value for left < right

    while (Math.Abs(gtA - ltA) > 0.1f)
    {
        float avgA = (gtA + ltA) / 2;

        if (left(avgA) < right(avgA))
        {
            ltA = avgA;
        }
        else
        {
            gtA = avgA;
        }
    }

    return (gtA + ltA) / 2;
}

El objeto SKPath para los vínculos se crea en el constructor de la clase y, a continuación, el objeto de SKPathEffect resultante se establece en la propiedad PathEffect del objeto SKPaint que se almacena como campo:

public class LinkedChainPage : ContentPage
{
    const float linkRadius = 30;
    const float linkThickness = 5;

    Func<float, float, float> catenary = (float a, float x) => (float)(a * Math.Cosh(x / a));

    SKPaint linksPaint = new SKPaint
    {
        Color = SKColors.Silver
    };

    public LinkedChainPage()
    {
        Title = "Linked Chain";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        // Create the path for the individual links
        SKRect outer = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
        SKRect inner = outer;
        inner.Inflate(-linkThickness, -linkThickness);

        using (SKPath linkPath = new SKPath())
        {
            linkPath.AddArc(outer, 55, 160);
            linkPath.ArcTo(inner, 215, -160, false);
            linkPath.Close();

            linkPath.AddArc(outer, 235, 160);
            linkPath.ArcTo(inner, 395, -160, false);
            linkPath.Close();

            // Set that path as the 1D path effect for linksPaint
            linksPaint.PathEffect =
                SKPathEffect.Create1DPath(linkPath, 1.3f * linkRadius, 0,
                                          SKPath1DPathEffectStyle.Rotate);
        }
    }
    ...
}

El trabajo principal del controlador de PaintSurface es crear una ruta de acceso para el propio catenario. Después de determinar el óptimo a y almacenarlo en la variable optA, también debe calcular un desplazamiento desde la parte superior de la ventana. A continuación, puede acumular una colección de SKPoint valores para el catenario, convertirlo en una ruta de acceso y dibujar la ruta con el objeto SKPaint creado anteriormente:

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

        canvas.Clear(SKColors.Black);

        // Width and height of catenary
        int width = info.Width;
        float height = info.Height - linkRadius;

        // Find the optimum 'a' for this width and height
        float optA = FindOptimumA(width, height);

        // Calculate the vertical offset for that value of 'a'
        float yOffset = catenary(optA, -width / 2);

        // Create a path for the catenary
        SKPoint[] points = new SKPoint[width];

        for (int x = 0; x < width; x++)
        {
            points[x] = new SKPoint(x, yOffset - catenary(optA, x - width / 2));
        }

        using (SKPath path = new SKPath())
        {
            path.AddPoly(points, false);

            // And render that path with the linksPaint object
            canvas.DrawPath(path, linksPaint);
        }
    }
    ...
}

Este programa define la ruta de acceso utilizada en Create1DPath para tener su punto (0, 0) en el centro. Esto parece razonable porque el punto (0, 0) del trazado está alineado con la línea o curva que adorna. Sin embargo, puede usar un punto no centrado (0, 0) para algunos efectos especiales.

La página Cinta transportadora crea un camino que se asemeja a una cinta transportadora oblonga con una parte superior e inferior curvadas cuyo tamaño se ajusta a las dimensiones de la ventana. Esa ruta de acceso se traza con un objeto simple SKPaint de 20 píxeles de ancho y gris coloreado y, a continuación, se vuelve a trazar con otro objeto SKPaint con un objeto SKPathEffect que hace referencia a una ruta de acceso similar a un cubo pequeño:

Triple screenshot of the Conveyor Belt page

El punto (0, 0) de la ruta de acceso del cubo es el mango, por lo que cuando se anima el argumento phase, los cubos parecen girar alrededor de la cinta transportadora, quizás descodificación del agua en la parte inferior y volcarlo en la parte superior.

La clase ConveyorBeltPage implementa animaciones con invalidaciones de los métodos OnAppearing y OnDisappearing. La ruta de acceso del cubo se define en el constructor de la página:

public class ConveyorBeltPage : ContentPage
{
    SKCanvasView canvasView;
    bool pageIsActive = false;

    SKPaint conveyerPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 20,
        Color = SKColors.DarkGray
    };

    SKPath bucketPath = new SKPath();

    SKPaint bucketsPaint = new SKPaint
    {
        Color = SKColors.BurlyWood,
    };

    public ConveyorBeltPage()
    {
        Title = "Conveyor Belt";

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        // Create the path for the bucket starting with the handle
        bucketPath.AddRect(new SKRect(-5, -3, 25, 3));

        // Sides
        bucketPath.AddRoundedRect(new SKRect(25, -19, 27, 18), 10, 10,
                                  SKPathDirection.CounterClockwise);
        bucketPath.AddRoundedRect(new SKRect(63, -19, 65, 18), 10, 10,
                                  SKPathDirection.CounterClockwise);

        // Five slats
        for (int i = 0; i < 5; i++)
        {
            bucketPath.MoveTo(25, -19 + 8 * i);
            bucketPath.LineTo(25, -13 + 8 * i);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.CounterClockwise, 65, -13 + 8 * i);
            bucketPath.LineTo(65, -19 + 8 * i);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.Clockwise, 25, -19 + 8 * i);
            bucketPath.Close();
        }

        // Arc to suggest the hidden side
        bucketPath.MoveTo(25, -17);
        bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                         SKPathDirection.Clockwise, 65, -17);
        bucketPath.LineTo(65, -19);
        bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                         SKPathDirection.CounterClockwise, 25, -19);
        bucketPath.Close();

        // Make it a little bigger and correct the orientation
        bucketPath.Transform(SKMatrix.MakeScale(-2, 2));
        bucketPath.Transform(SKMatrix.MakeRotationDegrees(90));
    }
    ...

El código de creación del cubo se completa con dos transformaciones que hacen que el cubo sea un poco más grande y gire hacia el lado. Aplicar estas transformaciones era más fácil que ajustar todas las coordenadas del código anterior.

El controlador PaintSurface comienza definiendo una ruta de acceso para la cinta transportadora. Esto es simplemente un par de líneas y un par de semicírculos que se dibujan con una línea de gris oscuro de 20 píxeles:

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

        canvas.Clear();

        float width = info.Width / 3;
        float verticalMargin = width / 2 + 150;

        using (SKPath conveyerPath = new SKPath())
        {
            // Straight verticals capped by semicircles on top and bottom
            conveyerPath.MoveTo(width, verticalMargin);
            conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                               SKPathDirection.Clockwise, 2 * width, verticalMargin);
            conveyerPath.LineTo(2 * width, info.Height - verticalMargin);
            conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                               SKPathDirection.Clockwise, width, info.Height - verticalMargin);
            conveyerPath.Close();

            // Draw the conveyor belt itself
            canvas.DrawPath(conveyerPath, conveyerPaint);

            // Calculate spacing based on length of conveyer path
            float length = 2 * (info.Height - 2 * verticalMargin) +
                           2 * ((float)Math.PI * width / 2);

            // Value will be somewhere around 200
            float spacing = length / (float)Math.Round(length / 200);

            // Now animate the phase; t is 0 to 1 every 2 seconds
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 2 / 2);
            float phase = -t * spacing;

            // Create the buckets PathEffect
            using (SKPathEffect bucketsPathEffect =
                        SKPathEffect.Create1DPath(bucketPath, spacing, phase,
                                                  SKPath1DPathEffectStyle.Rotate))
            {
                // Set it to the Paint object and draw the path again
                bucketsPaint.PathEffect = bucketsPathEffect;
                canvas.DrawPath(conveyerPath, bucketsPaint);
            }
        }
    }
}

La lógica para dibujar la cinta transportadora no funciona en modo horizontal.

Los cubos se deben espaciar alrededor de 200 píxeles en la cinta transportadora. Sin embargo, la cinta transportadora probablemente no es un múltiplo de 200 píxeles de largo, lo que significa que, como phase el argumento de SKPathEffect.Create1DPath es animado, los cubos aparecerán y no existirán.

Por este motivo, el programa calcula primero un valor denominado length que es la longitud de la cinta transportadora. Dado que la cinta transportadora consta de líneas rectas y semicírculos, se trata de un cálculo sencillo. A continuación, el número de cubos se calcula dividiendo length entre 200. Esto se redondea al entero más cercano y ese número se divide en length. El resultado es un espaciado para un número entero de cubos. El argumento phase es simplemente una fracción de eso.

De ruta de acceso a ruta de acceso de nuevo

En la parte inferior del controlador de DrawSurface en cinta transportadora, comente la llamada canvas.DrawPath y reemplácela por el código siguiente:

SKPath newPath = new SKPath();
bool fill = bucketsPaint.GetFillPath(conveyerPath, newPath);
SKPaint newPaint = new SKPaint
{
    Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);

Al igual que con el ejemplo anterior de GetFillPath, verá que los resultados son los mismos excepto para el color. Después de ejecutar GetFillPath, el objeto newPath contiene varias copias de la ruta de acceso del cubo, cada una situada en el mismo lugar en el que la animación las colocó en el momento de la llamada.

Sombreado de un área

El método SKPathEffect.Create2DLines rellena un área con líneas paralelas, a menudo denominadas líneas de sombreado. El método tiene la siguiente sintaxis:

public static SKPathEffect Create2DLine (Single width, SKMatrix matrix)

El argumento width especifica el ancho del trazo de las líneas de sombreado. El parámetro matrix es una combinación de escalado y rotación opcional. El factor de escala indica el incremento de píxel que Skia usa para espaciar las líneas de sombreado. La separación entre las líneas es el factor de escalado menos el argumento width. Si el factor de escalado es menor o igual que el valor de width, no habrá espacio entre las líneas de sombreado y el área aparecerá rellenada. Especifique el mismo valor para el escalado horizontal y vertical.

De forma predeterminada, las líneas de sombreado son horizontales. Si el parámetro matrix contiene rotación, las líneas de sombreado se giran en el sentido de las agujas del reloj.

La página Relleno de sombreado muestra este efecto de ruta de acceso. La clase HatchFillPage define tres efectos de ruta de acceso como campos, el primero para líneas de sombreado horizontales con un ancho de 3 píxeles con un factor de escalado que indica que están separados por 6 píxeles. Por lo tanto, la separación entre las líneas es de tres píxeles. El segundo efecto de ruta de acceso es para líneas de sombreado verticales con un ancho de seis píxeles espaciados de 24 píxeles (por lo que la separación es de 18 píxeles) y la tercera es para líneas de sombreado diagonales de 12 píxeles de ancho espaciado de 36 píxeles.

public class HatchFillPage : ContentPage
{
    SKPaint fillPaint = new SKPaint();

    SKPathEffect horzLinesPath = SKPathEffect.Create2DLine(3, SKMatrix.MakeScale(6, 6));

    SKPathEffect vertLinesPath = SKPathEffect.Create2DLine(6,
        Multiply(SKMatrix.MakeRotationDegrees(90), SKMatrix.MakeScale(24, 24)));

    SKPathEffect diagLinesPath = SKPathEffect.Create2DLine(12,
        Multiply(SKMatrix.MakeScale(36, 36), SKMatrix.MakeRotationDegrees(45)));

    SKPaint strokePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 3,
        Color = SKColors.Black
    };
    ...
    static SKMatrix Multiply(SKMatrix first, SKMatrix second)
    {
        SKMatrix target = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref target, first, second);
        return target;
    }
}

Observe el método de matriz Multiply. Dado que los factores de escalado horizontal y vertical son los mismos, el orden en que se multiplican las matrices de escalado y rotación no importa.

El controlador PaintSurface usa estos tres efectos de ruta de acceso con tres colores diferentes en combinación con fillPaint para rellenar un rectángulo redondeado de tamaño para ajustarse a la página. Se omite la propiedad Style establecida en fillPaint; cuando el objeto SKPaint incluye un efecto de ruta de acceso creado a partir de SKPathEffect.Create2DLine, el área se rellena independientemente de lo siguiente:

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

        canvas.Clear();

        using (SKPath roundRectPath = new SKPath())
        {
            // Create a path
            roundRectPath.AddRoundedRect(
                new SKRect(50, 50, info.Width - 50, info.Height - 50), 100, 100);

            // Horizontal hatch marks
            fillPaint.PathEffect = horzLinesPath;
            fillPaint.Color = SKColors.Red;
            canvas.DrawPath(roundRectPath, fillPaint);

            // Vertical hatch marks
            fillPaint.PathEffect = vertLinesPath;
            fillPaint.Color = SKColors.Blue;
            canvas.DrawPath(roundRectPath, fillPaint);

            // Diagonal hatch marks -- use clipping
            fillPaint.PathEffect = diagLinesPath;
            fillPaint.Color = SKColors.Green;

            canvas.Save();
            canvas.ClipPath(roundRectPath);
            canvas.DrawRect(new SKRect(0, 0, info.Width, info.Height), fillPaint);
            canvas.Restore();

            // Outline the path
            canvas.DrawPath(roundRectPath, strokePaint);
        }
    }
    ...
}

Si observa detenidamente los resultados, verá que las líneas de sombreado rojo y azul no se limitan precisamente al rectángulo redondeado. (Esto es aparentemente una característica del código Skia subyacente). Si esto no es satisfactorio, se muestra un enfoque alternativo para las líneas de sombreado diagonales en verde: el rectángulo redondeado se usa como una ruta de recorte y las líneas de sombreado se dibujan en toda la página.

El controlador PaintSurface concluye con una llamada para simplemente trazar el rectángulo redondeado, por lo que puede ver la discrepancia con las líneas de sombreado rojo y azul:

Triple screenshot of the Hatch Fill page

La pantalla de Android no parece realmente así: el escalado de la captura de pantalla ha provocado que las líneas rojas delgadas y los espacios finos se consoliden en líneas rojas aparentemente más anchas y espacios más anchos.

Rellenar con un trazado

SKPathEffect.Create2DPath le permite rellenar un área con una ruta de acceso que se replica horizontal y verticalmente, para poner en mosaico el área:

public static SKPathEffect Create2DPath (SKMatrix matrix, SKPath path)

Los factores de escala SKMatrix indican el espaciado horizontal y vertical de la ruta de acceso replicada. Pero no se puede girar la ruta de acceso con este argumento matrix; Si desea girar la ruta de acceso, gire la ruta de acceso mediante el método Transform definido por SKPath.

La ruta de acceso replicada normalmente se alinea con los bordes izquierdo y superior de la pantalla en lugar del área que se va a rellenar. Puede invalidar este comportamiento proporcionando factores de traducción entre 0 y los factores de escalado para especificar desplazamientos horizontales y verticales desde los lados izquierdo y superior.

La página Relleno de mosaico de ruta de acceso muestra este efecto de ruta de acceso. La ruta de acceso utilizada para el mosaico del área se define como un campo en la clase PathTileFillPage. Las coordenadas horizontales y verticales oscilan entre –40 y 40, lo que significa que esta ruta de acceso es de 80 píxeles cuadrados:

public class PathTileFillPage : ContentPage
{
    SKPath tilePath = SKPath.ParseSvgPathData(
        "M -20 -20 L 2 -20, 2 -40, 18 -40, 18 -20, 40 -20, " +
        "40 -12, 20 -12, 20 12, 40 12, 40 40, 22 40, 22 20, " +
        "-2 20, -2 40, -20 40, -20 8, -40 8, -40 -8, -20 -8 Z");
    ...
    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.Color = SKColors.Red;

            using (SKPathEffect pathEffect =
                   SKPathEffect.Create2DPath(SKMatrix.MakeScale(64, 64), tilePath))
            {
                paint.PathEffect = pathEffect;

                canvas.DrawRoundRect(
                    new SKRect(50, 50, info.Width - 50, info.Height - 50),
                    100, 100, paint);
            }
        }
    }
}

En el controlador PaintSurface, SKPathEffect.Create2DPath llama a establece el espaciado horizontal y vertical en 64 para hacer que los mosaicos cuadrados de 80 píxeles se superpongan. Afortunadamente, el camino parece una pieza de rompecabezas y combina muy bien con los mosaicos contiguos:

Triple screenshot of the Path Tile Fill page

El escalado de la captura de pantalla original provoca cierta distorsión, especialmente en la pantalla Android.

Tenga en cuenta que estos mosaicos siempre aparecen enteros y nunca se truncan. En las dos primeras capturas de pantalla, ni siquiera es evidente que el área que se va a rellenar es un rectángulo redondeado. Si desea truncar estos mosaicos en un área determinada, use una ruta de recorte.

Intente establecer la propiedad Style del objeto SKPaint en Stroke y verá los mosaicos individuales que se describen en lugar de rellenarse.

También es posible rellenar un área con un mapa de bits en mosaico, como se muestra en el artículo Mosaico de mapa de bits SkiaSharp.

Redondeo de esquinas afiladas

El programa Heptágono redondeado presentado en Tres formas de dibujar un arco usó un arco tangente para curvar los puntos de una figura de siete lados. La página Otro heptágono redondeado muestra un enfoque mucho más sencillo que usa un efecto de ruta de acceso creado a partir del método SKPathEffect.CreateCorner:

public static SKPathEffect CreateCorner (Single radius)

Aunque el único argumento se denomina radius, debe establecerlo en la mitad del radio de esquina deseado. (Esta es una característica del código Skia subyacente).

Este es el controlador PaintSurface de la clase AnotherRoundedHeptagonPage:

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

    canvas.Clear();

    int numVertices = 7;
    float radius = 0.45f * Math.Min(info.Width, info.Height);
    SKPoint[] vertices = new SKPoint[numVertices];
    double vertexAngle = -0.5f * Math.PI;       // straight up

    // Coordinates of the vertices of the polygon
    for (int vertex = 0; vertex < numVertices; vertex++)
    {
        vertices[vertex] = new SKPoint(radius * (float)Math.Cos(vertexAngle),
                                       radius * (float)Math.Sin(vertexAngle));
        vertexAngle += 2 * Math.PI / numVertices;
    }

    float cornerRadius = 100;

    // Create the path
    using (SKPath path = new SKPath())
    {
        path.AddPoly(vertices, true);

        // Render the path in the center of the screen
        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Blue;
            paint.StrokeWidth = 10;

            // Set argument to half the desired corner radius!
            paint.PathEffect = SKPathEffect.CreateCorner(cornerRadius / 2);

            canvas.Translate(info.Width / 2, info.Height / 2);
            canvas.DrawPath(path, paint);

            // Uncomment DrawCircle call to verify corner radius
            float offset = cornerRadius / (float)Math.Sin(Math.PI * (numVertices - 2) / numVertices / 2);
            paint.Color = SKColors.Green;
            // canvas.DrawCircle(vertices[0].X, vertices[0].Y + offset, cornerRadius, paint);
        }
    }
}

Puede usar este efecto con la pulsación o el relleno en función de la propiedad Style del objeto SKPaint. Aquí se está ejecutando:

Triple screenshot of the Another Rounded Heptagon page

Verá que este heptágono redondeado es idéntico al programa anterior. Si necesita más convincente que el radio de la esquina es realmente 100 en lugar de los 50 especificados en la llamada SKPathEffect.CreateCorner, puede quitar la marca de comentario de la instrucción final en el programa y ver un círculo de 100 radios superpuesto en la esquina.

Vibración aleatoria

A veces, las líneas rectas impecables de los gráficos de ordenador no son bastante lo que desea y se desea una pequeña aleatoriedad. En ese caso, querrá probar el método SKPathEffect.CreateDiscrete:

public static SKPathEffect CreateDiscrete (Single segLength, Single deviation, UInt32 seedAssist)

Puede usar este efecto de ruta de acceso para presionar o rellenar. Las líneas se separan en segmentos conectados (la longitud aproximada de la que se especifica) segLength y se extienden en diferentes direcciones. La extensión de la desviación de la línea original se especifica mediante deviation.

El argumento final es una inicialización que se usa para generar la secuencia pseudoaleatoriedad utilizada para el efecto. El efecto de vibración se verá un poco diferente para diferentes semillas. El argumento tiene un valor predeterminado de cero, lo que significa que el efecto es el mismo siempre que se ejecuta el programa. Si desea una vibración diferente cada vez que se vuelva a dibujar la pantalla, puede establecer la inicialización en la propiedad Millisecond de un valor de DataTime.Now (por ejemplo).

La página Experimento de vibración permite experimentar con valores diferentes en la pulsación de un rectángulo:

Triple screenshot of the JitterExperiment page

El programa es sencillo. El archivo JitterExperimentPage.xaml crea una instancia de dos elementos Slider y un SKCanvasView:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Curves.JitterExperimentPage"
             Title="Jitter Experiment">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid.Resources>
            <ResourceDictionary>
                <Style TargetType="Label">
                    <Setter Property="HorizontalTextAlignment" Value="Center" />
                </Style>

                <Style TargetType="Slider">
                    <Setter Property="Margin" Value="20, 0" />
                    <Setter Property="Minimum" Value="0" />
                    <Setter Property="Maximum" Value="100" />
                </Style>
            </ResourceDictionary>
        </Grid.Resources>

        <Slider x:Name="segLengthSlider"
                Grid.Row="0"
                ValueChanged="sliderValueChanged" />

        <Label Text="{Binding Source={x:Reference segLengthSlider},
                              Path=Value,
                              StringFormat='Segment Length = {0:F0}'}"
               Grid.Row="1" />

        <Slider x:Name="deviationSlider"
                Grid.Row="2"
                ValueChanged="sliderValueChanged" />

        <Label Text="{Binding Source={x:Reference deviationSlider},
                              Path=Value,
                              StringFormat='Deviation = {0:F0}'}"
               Grid.Row="3" />

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="4"
                           PaintSurface="OnCanvasViewPaintSurface" />
    </Grid>
</ContentPage>

Se llama al controlador PaintSurface del archivo de código subyacente JitterExperimentPage.xaml.cs cada vez que cambia un valor Slider. Llama a SKPathEffect.CreateDiscrete usando los dos valores de Slider y lo usa para trazar un rectángulo:

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

    canvas.Clear();

    float segLength = (float)segLengthSlider.Value;
    float deviation = (float)deviationSlider.Value;

    using (SKPaint paint = new SKPaint())
    {
        paint.Style = SKPaintStyle.Stroke;
        paint.StrokeWidth = 5;
        paint.Color = SKColors.Blue;

        using (SKPathEffect pathEffect = SKPathEffect.CreateDiscrete(segLength, deviation))
        {
            paint.PathEffect = pathEffect;

            SKRect rect = new SKRect(100, 100, info.Width - 100, info.Height - 100);
            canvas.DrawRect(rect, paint);
        }
    }
}

También puede usar este efecto para rellenar, en cuyo caso el contorno del área rellena está sujeto a estas desviaciones aleatorias. La página Texto de vibración muestra el uso de este efecto de ruta de acceso para mostrar texto. La mayoría del código del controlador de PaintSurface de la clase JitterTextPage se dedica a cambiar el tamaño y centrar el texto:

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

    canvas.Clear();

    string text = "FUZZY";

    using (SKPaint textPaint = new SKPaint())
    {
        textPaint.Color = SKColors.Purple;
        textPaint.PathEffect = SKPathEffect.CreateDiscrete(3f, 10f);

        // Adjust TextSize property so text is 95% of screen width
        float textWidth = textPaint.MeasureText(text);
        textPaint.TextSize *= 0.95f * info.Width / textWidth;

        // Find the text bounds
        SKRect textBounds = new SKRect();
        textPaint.MeasureText(text, ref textBounds);

        // Calculate offsets to center the text on the screen
        float xText = info.Width / 2 - textBounds.MidX;
        float yText = info.Height / 2 - textBounds.MidY;

        canvas.DrawText(text, xText, yText, textPaint);
    }
}

Aquí se ejecuta en modo horizontal:

Triple screenshot of the JitterText page

Esquematización de ruta de acceso

Ya ha visto dos ejemplos pequeños del método GetFillPath de SKPaint, que existe dos versiones:

public Boolean GetFillPath (SKPath src, SKPath dst, Single resScale = 1)

public Boolean GetFillPath (SKPath src, SKPath dst, SKRect cullRect, Single resScale = 1)

Solo se requieren los dos primeros argumentos. El método accede a la ruta de acceso a la que hace referencia el argumento src, modifica los datos de ruta de acceso en función de las propiedades de trazo del objeto SKPaint (incluida la propiedad PathEffect) y, a continuación, escribe los resultados en la ruta de acceso dst. El parámetro resScale permite reducir la precisión para crear una ruta de acceso de destino más pequeña y el argumento cullRect puede eliminar contornos fuera de un rectángulo.

Un uso básico de este método no implica efectos de ruta de acceso en absoluto: si el objeto SKPaint tiene su propiedad Style establecida en SKPaintStyle.Stroke, y no tiene su PathEffect establecido, GetFillPath crea una ruta de acceso que representa un contorno de la ruta de origen como si hubiera sido acariciada por las propiedades de la pintura.

Por ejemplo, si la ruta de acceso src es un círculo simple de radio 500, y el objeto SKPaint especifica un ancho de trazo de 100, la ruta de acceso de dst se convierte en dos círculos concéntricos, uno con un radio de 450 y el otro con un radio de 550. Se llama al método GetFillPath porque rellenar esta ruta de acceso dst es la misma que la ruta de acceso de src. Pero también puede trazar la ruta de acceso dst para ver los contornos de la ruta de acceso.

Pulsar para describir la ruta de acceso muestra esto. Se crean instancias de SKCanvasView y TapGestureRecognizer en el archivo TapToOutlineThePathPage.xaml. El archivo de código subyacente de TapToOutlineThePathPage.xaml.cs define tres objetos SKPaint como campos, dos para la pulsación con anchos de trazo de 100 y 20, y el tercero para rellenar:

public partial class TapToOutlineThePathPage : ContentPage
{
    bool outlineThePath = false;

    SKPaint redThickStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        StrokeWidth = 100
    };

    SKPaint redThinStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        StrokeWidth = 20
    };

    SKPaint blueFill = new SKPaint
    {
        Style = SKPaintStyle.Fill,
        Color = SKColors.Blue
    };

    public TapToOutlineThePathPage()
    {
        InitializeComponent();
    }

    void OnCanvasViewTapped(object sender, EventArgs args)
    {
        outlineThePath ^= true;
        (sender as SKCanvasView).InvalidateSurface();
    }
    ...
}

Si no se ha pulsado la pantalla, el controlador de PaintSurface usa el blueFill y redThickStroke objetos de pintura para representar una ruta circular:

public partial class TapToOutlineThePathPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPath circlePath = new SKPath())
        {
            circlePath.AddCircle(info.Width / 2, info.Height / 2,
                                 Math.Min(info.Width / 2, info.Height / 2) -
                                 redThickStroke.StrokeWidth);

            if (!outlineThePath)
            {
                canvas.DrawPath(circlePath, blueFill);
                canvas.DrawPath(circlePath, redThickStroke);
            }
            else
            {
                using (SKPath outlinePath = new SKPath())
                {
                    redThickStroke.GetFillPath(circlePath, outlinePath);

                    canvas.DrawPath(outlinePath, blueFill);
                    canvas.DrawPath(outlinePath, redThinStroke);
                }
            }
        }
    }
}

El círculo se rellena y se trazos como cabría esperar:

Triple screenshot of the normal Tap To Outline The Path page

Al pulsar la pantalla, outlineThePath se establece en true y el controlador de PaintSurface crea un objeto SKPath nuevo y lo usa como ruta de acceso de destino en una llamada a GetFillPath en el objeto de pintura de redThickStroke. Esa ruta de acceso de destino se rellena y se traza con redThinStroke, lo que da como resultado lo siguiente:

Triple screenshot of the outlined Tap To Outline The Path page

Los dos círculos rojos indican claramente que el trazado circular original se ha convertido en dos contornos circulares.

Este método puede ser muy útil en el desarrollo de rutas de acceso que se usarán para el método SKPathEffect.Create1DPath. Las rutas de acceso que especifique en estos métodos siempre se rellenan cuando se replican las rutas de acceso. Si no desea que se rellene toda la ruta de acceso, debe definir cuidadosamente los contornos.

Por ejemplo, en la Cadena vinculada de ejemplo, los vínculos se definieron con una serie de cuatro arcos, cada par de los cuales se basaban en dos radios para describir el área del trazado que se va a rellenar. Es posible reemplazar el código de la clase LinkedChainPage para hacerlo de forma un poco diferente.

En primer lugar, querrá volver a definir la constante linkRadius:

const float linkRadius = 27.5f;
const float linkThickness = 5;

linkPath ahora son solo dos arcos basados en ese radio único, con los ángulos de inicio deseados y ángulos de barrido:

using (SKPath linkPath = new SKPath())
{
    SKRect rect = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
    linkPath.AddArc(rect, 55, 160);
    linkPath.AddArc(rect, 235, 160);

    using (SKPaint strokePaint = new SKPaint())
    {
        strokePaint.Style = SKPaintStyle.Stroke;
        strokePaint.StrokeWidth = linkThickness;

        using (SKPath outlinePath = new SKPath())
        {
            strokePaint.GetFillPath(linkPath, outlinePath);

            // Set that path as the 1D path effect for linksPaint
            linksPaint.PathEffect =
                SKPathEffect.Create1DPath(outlinePath, 1.3f * linkRadius, 0,
                                          SKPath1DPathEffectStyle.Rotate);

        }

    }
}

A continuación, el objeto outlinePath es el destinatario del esquema de linkPath cuando se traza con las propiedades especificadas en strokePaint.

Otro ejemplo que usa esta técnica viene a continuación para la ruta de acceso que se usa en un método.

Combinación de efectos de ruta de acceso

Los dos métodos finales de creación estática de SKPathEffect son SKPathEffect.CreateSum y SKPathEffect.CreateCompose:

public static SKPathEffect CreateSum (SKPathEffect first, SKPathEffect second)

public static SKPathEffect CreateCompose (SKPathEffect outer, SKPathEffect inner)

Ambos métodos combinan dos efectos de ruta de acceso para crear un efecto de ruta de acceso compuesto. El método CreateSum crea un efecto de ruta de acceso similar a los dos efectos de ruta de acceso aplicados por separado, mientras que CreateCompose aplica un efecto de ruta de acceso (inner) y, a continuación, aplica outer a ese.

Ya ha visto cómo el método GetFillPath de SKPaint puede convertir una ruta de acceso a otra en función de las propiedades de SKPaint (incluido PathEffect), por lo que no debe ser demasiado misterioso cómo un objeto SKPaint puede realizar esa operación dos veces con los dos efectos de ruta especificados en los métodos CreateSum o CreateCompose.

Un uso obvio de CreateSum es definir un objeto SKPaint que rellena una ruta de acceso con un efecto de ruta de acceso y traza la ruta con otro efecto de ruta de acceso. Esto se demuestra en el ejemplo Gatos en marco, que muestra una serie de gatos dentro de un marco con bordes festoneados:

Triple screenshot of the Cats In Frame page

La clase CatsInFramePage comienza definiendo varios campos. Es posible que reconozca el primer campo de la clase PathDataCatPage del artículo Datos de ruta SVG. La segunda ruta de acceso se basa en una línea y arco para el patrón escalar del marco:

public class CatsInFramePage : ContentPage
{
    // From PathDataCatPage.cs
    SKPath catPath = SKPath.ParseSvgPathData(
        "M 160 140 L 150 50 220 103" +              // Left ear
        "M 320 140 L 330 50 260 103" +              // Right ear
        "M 215 230 L 40 200" +                      // Left whiskers
        "M 215 240 L 40 240" +
        "M 215 250 L 40 280" +
        "M 265 230 L 440 200" +                     // Right whiskers
        "M 265 240 L 440 240" +
        "M 265 250 L 440 280" +
        "M 240 100" +                               // Head
        "A 100 100 0 0 1 240 300" +
        "A 100 100 0 0 1 240 100 Z" +
        "M 180 170" +                               // Left eye
        "A 40 40 0 0 1 220 170" +
        "A 40 40 0 0 1 180 170 Z" +
        "M 300 170" +                               // Right eye
        "A 40 40 0 0 1 260 170" +
        "A 40 40 0 0 1 300 170 Z");

    SKPaint catStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 5
    };

    SKPath scallopPath =
        SKPath.ParseSvgPathData("M 0 0 L 50 0 A 60 60 0 0 1 -50 0 Z");

    SKPaint framePaint = new SKPaint
    {
        Color = SKColors.Black
    };
    ...
}

catPath podría usarse en el método SKPathEffect.Create2DPath si la propiedad Style objeto SKPaint está establecida en Stroke. Sin embargo, si catPath se usa directamente en este programa, se rellenará toda la cabeza del gato y los bigotes ni siquiera serán visibles. (¡Pruébelo!) Es necesario obtener el esquema de esa ruta de acceso y usar ese esquema en el SKPathEffect.Create2DPath método.

El constructor realiza este trabajo. En primer lugar, aplica dos transformaciones a catPath para mover el punto (0, 0) al centro y reducir verticalmente el tamaño. GetFillPath obtiene todos los contornos de los contornos de outlinedCatPath y ese objeto se usa en la llamada SKPathEffect.Create2DPath. Los factores de escala del SKMatrix valor son ligeramente mayores que el tamaño horizontal y vertical del gato para proporcionar un poco de búfer entre los mosaicos, mientras que los factores de traducción se derivaron de forma empírica para que un gato completo esté visible en la esquina superior izquierda del marco:

public class CatsInFramePage : ContentPage
{
    ...
    public CatsInFramePage()
    {
        Title = "Cats in Frame";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        // Move (0, 0) point to center of cat path
        catPath.Transform(SKMatrix.MakeTranslation(-240, -175));

        // Now catPath is 400 by 250
        // Scale it down to 160 by 100
        catPath.Transform(SKMatrix.MakeScale(0.40f, 0.40f));

        // Get the outlines of the contours of the cat path
        SKPath outlinedCatPath = new SKPath();
        catStroke.GetFillPath(catPath, outlinedCatPath);

        // Create a 2D path effect from those outlines
        SKPathEffect fillEffect = SKPathEffect.Create2DPath(
            new SKMatrix { ScaleX = 170, ScaleY = 110,
                           TransX = 75, TransY = 80,
                           Persp2 = 1 },
            outlinedCatPath);

        // Create a 1D path effect from the scallop path
        SKPathEffect strokeEffect =
            SKPathEffect.Create1DPath(scallopPath, 75, 0, SKPath1DPathEffectStyle.Rotate);

        // Set the sum the effects to frame paint
        framePaint.PathEffect = SKPathEffect.CreateSum(fillEffect, strokeEffect);
    }
    ...
}

A continuación, el constructor llama a SKPathEffect.Create1DPath para el marco escalar. Observe que el ancho de la ruta de acceso es de 100 píxeles, pero el avance es de 75 píxeles para que la ruta de acceso replicada se superponga alrededor del marco. La instrucción final del constructor llama a SKPathEffect.CreateSum para combinar los dos efectos de ruta de acceso y establecer el resultado en el objeto SKPaint.

Todo este trabajo permite que el controlador de PaintSurface sea bastante sencillo. Solo necesita definir un rectángulo y dibujarlo mediante framePaint:

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

        canvas.Clear();

        SKRect rect = new SKRect(50, 50, info.Width - 50, info.Height - 50);
        canvas.ClipRect(rect);
        canvas.DrawRect(rect, framePaint);
    }
}

Los algoritmos detrás de los efectos de ruta de acceso siempre provocan que se muestre toda la ruta de acceso utilizada para mostrar o rellenar, lo que puede hacer que algunos objetos visuales aparezcan fuera del rectángulo. La llamada ClipRect antes de la llamada a DrawRect permite que los objetos visuales sean considerablemente más limpios. (¡Pruébelo sin recorte!)

Es habitual usar SKPathEffect.CreateCompose para agregar algún vibración a otro efecto de ruta de acceso. Sin duda, puede experimentar por su cuenta, pero este es un ejemplo algo diferente:

Las líneas de sombreado discontinuas rellenan una elipse con líneas de sombreado discontinuas. La mayoría del trabajo de la clase DashedHatchLinesPage se realiza directamente en las definiciones de campo. Estos campos definen un efecto de guión y un efecto de sombreado. Se definen como static porque, a continuación, se hace referencia a ellos en una llamada SKPathEffect.CreateCompose en la definición de SKPaint:

public class DashedHatchLinesPage : ContentPage
{
    static SKPathEffect dashEffect =
        SKPathEffect.CreateDash(new float[] { 30, 30 }, 0);

    static SKPathEffect hatchEffect = SKPathEffect.Create2DLine(20,
        Multiply(SKMatrix.MakeScale(60, 60),
                 SKMatrix.MakeRotationDegrees(45)));

    SKPaint paint = new SKPaint()
    {
        PathEffect = SKPathEffect.CreateCompose(dashEffect, hatchEffect),
        StrokeCap = SKStrokeCap.Round,
        Color = SKColors.Blue
    };
    ...
    static SKMatrix Multiply(SKMatrix first, SKMatrix second)
    {
        SKMatrix target = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref target, first, second);
        return target;
    }
}

El controlador de PaintSurface debe contener solo la sobrecarga estándar más una llamada a DrawOval:

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

        canvas.Clear();

        canvas.DrawOval(info.Width / 2, info.Height / 2,
                        0.45f * info.Width, 0.45f * info.Height,
                        paint);
    }
    ...
}

Como ya ha descubierto, las líneas de sombreado no están restringidas precisamente al interior del área y, en este ejemplo, siempre comienzan a la izquierda con un guión completo:

Triple screenshot of the Dashed Hatch Lines page

Ahora que ha visto efectos de ruta que van desde puntos simples y guiones a combinaciones extrañas, use su imaginación y vea lo que puede crear.