Efectos de ruta de acceso en SkiaSharp

Descargar ejemploDescargar el ejemplo

Descubra los distintos efectos de ruta de acceso que permiten usar las rutas de acceso para esstroking y relleno

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

Ejemplo de cadena vinculada

Los efectos de la ruta le 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 GetFillPath método de SKPaint para convertir una ruta de acceso en otra mediante la aplicación de propiedades de SKPaint, incluido 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 es útil en relación con los efectos de la ruta de acceso.

Puntos y guiones

El uso del PathEffect.CreateDash método se ha descrito 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 intervals matriz en { 10, 10 }. El phase argumento indica dónde comienza el patrón de guiones de la línea. En este ejemplo, si desea que la línea comience con el intervalo cuadrado, establezca en phase 10.

Los extremos de los guiones se ven afectados por la StrokeCap propiedad 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 intervals matriz no incluyen 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, utilice una intervals matriz 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 Style propiedad del SKPaint objeto SKPaintStyle.Strokeen . Además, El texto de puntos animados usa SKPathEffect.CreateDash para dar a este contorno una apariencia punteada, y el programa también anima el phase argumento del SKPathEffect.CreateDash método para hacer que los puntos parezcan viajar alrededor de los caracteres de texto. Esta es la página en modo horizontal:

Captura de pantalla triple de la página Texto con puntos animados

La AnimatedDottedTextPage clase comienza definiendo algunas constantes y también invalida los OnAppearing métodos 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 PaintSurface controlador comienza creando un SKPaint objeto para mostrar el texto. La TextSize propiedad 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 SKPathEffect.CreateDash método mediante el dashArray que se define como un campo y el valor animado phase . La SKPathEffect instancia se establece en la PathEffect propiedad del SKPaint objeto para mostrar el texto.

Como alternativa, puede establecer el SKPathEffect objeto en el SKPaint objeto antes de medir el texto y centrarlo en la página. Sin embargo, en ese caso, los puntos animados y guiones provocan alguna 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 giran alrededor de los caracteres de texto, hay un punto determinado en cada curva cerrada donde los puntos parecen salir y salir de la existencia. Aquí es donde comienza y finaliza la ruta de acceso que define el esquema de caracteres. Si la longitud de la ruta de acceso no es un múltiplo entero de la longitud del patrón de guiones (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 guiones 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 Dot/Dash Morph anima el patrón de guiones para que los guiones parezcan dividirse en puntos, que se combinan para formar guiones de nuevo:

Captura de pantalla triple de la página Dot Dash Morph

La DotDashMorphPage clase invalida los OnAppearing métodos y OnDisappearing como hizo el programa anterior, pero la clase define el SKPaint objeto como un 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 PaintSurface controlador 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 dashArray variables y phase . A medida que la variable t animada oscila entre 0 y 1, los if bloques dividen ese tiempo en cuatro trimestres y, en cada uno de esos trimestres, tsub también oscila entre 0 y 1. Al final, el programa crea y SKPathEffect lo establece en el objeto para dibujar SKPaint .

De ruta de acceso a ruta de acceso

El GetFillPath método de SKPaint convierte una ruta de acceso en otra en función de la configuración del SKPaint objeto . Para ver cómo funciona esto, reemplace la canvas.DrawPath llamada 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 GetFillPath llamada convierte ( ellipsePath que es simplemente un óvalo) en newPath, que luego se muestra con newPaint. El newPaint objeto se crea con todos los valores de propiedad predeterminados, excepto que la Style propiedad 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 o no la ruta de acceso de destino; en este ejemplo, el valor devuelto es true para rellenar la ruta de acceso.

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

Stroking with a Path

El SKPathEffect.Create1DPath método es conceptualmente similar a SKPathEffect.CreateDash excepto 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 a la que pase Create1DPath será pequeña y centrada alrededor del punto (0, 0). El advance parámetro 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 phase argumento desempeña aquí el mismo rol que en el CreateDash método .

SKPath1DPathEffectStyle tiene tres miembros:

  • Translate
  • Rotate
  • Morph

El Translate miembro 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 excepto 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 SKPathEffect objetos como campos. Todos ellos se crean con SKPathEffect.Create1DPath objetos SKPath creados mediante SKPath.ParseSvgPathData. La primera es una caja simple, la segunda es una forma de diamante y la tercera es un rectángulo. Estos se usan para mostrar 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 PaintSurface controlador crea una curva Bézier que recorre el propio bucle 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:

Captura de pantalla triple de la página Efecto de ruta de acceso 1D

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

El cuadro usado en el Translate ejemplo es de 20 píxeles cuadrados y el advance argumento 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 del Rotate ejemplo también tiene 20 píxeles de ancho. 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 Morph ejemplo es de 50 píxeles de ancho con un advance valor 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 advance argumento 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 ser similares a una cadena vinculada, que se bloquea en la forma distintiva de una catenaria:

Captura de pantalla triple de la página Cadena vinculada

Mira muy cerca y verás 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 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 sencilla:

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 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.

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

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

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

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 SKPath objeto de los vínculos se crea en el constructor de la clase y, a continuación, el objeto resultante SKPathEffect se establece en la PathEffect propiedad del SKPaint objeto almacenado 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 PaintSurface controlador es crear una ruta de acceso para el propio catenario. Después de determinar el valor óptimo de y almacenarlo en la optA variable, 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, convertirla en una ruta de acceso y dibujar la ruta de acceso con el objeto creado SKPaint 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 usada en Create1DPath para tener su punto (0, 0) en el centro. Esto parece razonable porque el punto (0, 0) de la ruta de acceso 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 trazado similar a una cinta transportadora de oblong con una parte superior e inferior curvada que se ajusta a las dimensiones de la ventana. Esa ruta de acceso se trazo con un objeto simple SKPaint de 20 píxeles de ancho y gris coloreado, y luego se vuelve a trazar con otro SKPaint objeto con un SKPathEffect objeto que hace referencia a una ruta de acceso similar a un cubo pequeño:

Captura de pantalla triple de la página Cinta transportadora

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

La ConveyorBeltPage clase implementa la animación con invalidaciones de los OnAppearing métodos 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 de cubos 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 PaintSurface controlador comienza definiendo una ruta de acceso para la propia cinta transportadora. Esto es simplemente un par de líneas y un par de semicírculos dibujados con una línea 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 argumento phase de SKPathEffect.Create1DPath es animado, los cubos aparecerán y descontará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 semicircules, se trata de un cálculo sencillo. A continuación, el número de cubos se calcula dividiendo length por 200. Se redondea al entero más cercano y ese número se divide lengthen . El resultado es un espaciado para un número entero de cubos. El phase argumento es simplemente una fracción de eso.

De ruta de acceso a ruta de acceso de nuevo

En la parte inferior del controlador en DrawSurfacecinta transportadora, convierta en comentario la canvas.DrawPath llamada 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 GetFillPathejecutar , el newPath objeto contiene varias copias de la ruta de acceso del cubo, cada una situada en el mismo lugar en el que la animación las coloca en el momento de la llamada.

Sombrear un área

El SKPathEffect.Create2DLines método 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 width argumento especifica el ancho del trazo de las líneas de sombreado. El matrix parámetro es una combinación de escalado y rotación opcional. El factor de escala indica el incremento de píxeles que Skia usa para espaciar las líneas de sombreado. La separación entre las líneas es el factor de escalado menos el width argumento . Si el factor de escalado es menor o igual que el width valor, 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 matrix parámetro contiene rotación, las líneas de sombreado giran en el sentido de las agujas del reloj.

La página Relleno de sombreado muestra este efecto de ruta de acceso. La HatchFillPage clase 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 escala que indica que están espaciados 6 píxeles separados. 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 separados.

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, no importa el orden en que se multiplican las matrices de escalado y rotación.

El PaintSurface controlador 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. La Style propiedad establecida en fillPaint se omite; cuando el SKPaint objeto 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 examina 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 diagonales de sombreado en verde: el rectángulo redondeado se usa como trazado de recorte y las líneas de sombreado se dibujan en toda la página.

El PaintSurface controlador 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:

Captura de pantalla triple de la página Relleno de sombreado

La pantalla De Android no es similar a la siguiente: el escalado de la captura de pantalla ha provocado que las líneas rojas finas y los espacios finos se consoliden en líneas rojas aparentemente más anchas y espacios más anchos.

Relleno con un trazado

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

public static SKPathEffect Create2DPath (SKMatrix matrix, SKPath path)

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

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

En la página Relleno del icono de ruta de acceso se muestra este efecto de ruta de acceso. La ruta de acceso usada para la colocación en mosaico del área se define como un campo de la PathTileFillPage clase . Las coordenadas horizontales y verticales van de –40 a 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 PaintSurface controlador, las SKPathEffect.Create2DPath llamadas establecen el espaciado horizontal y vertical en 64 para hacer que los iconos cuadrados de 80 píxeles se superpongan. Afortunadamente, el camino se parece a una pieza de rompecabezas, mallando bien con mosaicos adyacentes:

Captura de pantalla triple de la página Relleno del icono de ruta de acceso

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

Tenga en cuenta que estos iconos 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 iconos en un área determinada, use una ruta de recorte.

Intente establecer la Style propiedad del SKPaint objeto Strokeen y verá los iconos 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 de SkiaSharp.

Redondeo de esquinas afiladas

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

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 PaintSurface controlador de la AnotherRoundedHeptagonPage clase :

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 pulsación o relleno en función de la Style propiedad del SKPaint objeto . Aquí se está ejecutando:

Captura de pantalla triple de la página Otro heptagon redondeado

Verá que este heptagon redondeado es idéntico al programa anterior. Si necesita más convincente que el radio de esquina es realmente 100 en lugar de los 50 especificados en la SKPathEffect.CreateCorner llamada, 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 SKPathEffect.CreateDiscrete método :

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

Puede usar este efecto de ruta de acceso para stroking o relleno. Las líneas se separan en segmentos conectados ( la longitud aproximada de la cual se especifica mediante 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 un valor de 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 pintar la pantalla, puede establecer la inicialización en la Millisecond propiedad de un DataTime.Now valor (por ejemplo).

La página Experimento de vibración permite experimentar con valores diferentes al hacer clic en un rectángulo:

Captura de pantalla triple de la página JitterExperiment

El programa es sencillo. El archivo JitterExperimentPage.xaml crea una instancia de dos Slider elementos 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>

Cuando PaintSurface cambia un Slider valor, se llama al controlador del archivo de código subyacente JitterExperimentPage.xaml.cs. Llama a SKPathEffect.CreateDiscrete mediante los dos Slider valores 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 Jitter Text muestra el uso de este efecto de ruta de acceso para mostrar texto. La mayoría del código PaintSurface del controlador de la JitterTextPage clase 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:

Captura de pantalla triple de la página JitterText

Esquematización de la ruta de acceso

Ya ha visto dos pequeños ejemplos del GetFillPath método de SKPaint, que existen 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 src argumento , modifica los datos de ruta de acceso en función de las propiedades de trazo del SKPaint objeto (incluida la PathEffect propiedad ) y, a continuación, escribe los resultados en la ruta de dst acceso. El resScale parámetro permite reducir la precisión para crear una ruta de acceso de destino más pequeña y el cullRect argumento 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 SKPaint objeto tiene su Style propiedad establecida SKPaintStyle.Strokeen y no tiene su PathEffect conjunto, GetFillPath crea una ruta de acceso que representa un esquema de la ruta de acceso de origen como si hubiera sido trazos por las propiedades de pintura.

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

El tap to Outline the Path (Pulsar para describir la ruta de acceso) muestra esto. TapGestureRecognizer y SKCanvasView se crean instancias en el archivo TapToOutlineThePathPage.xaml. El archivo de código subyacente TapToOutlineThePathPage.xaml.cs define tres SKPaint objetos como campos, dos para tocar 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 PaintSurface controlador usa los blueFill objetos y redThickStroke paint para representar una ruta de acceso 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:

Captura de pantalla triple de la página Normal Tap To Outline The Path (Pulsar para describir la ruta de acceso)

Al pulsar la pantalla, outlineThePath se establece trueen y el PaintSurface controlador crea un objeto nuevo SKPath y lo usa como ruta de acceso de destino en una llamada a GetFillPath en el redThickStroke objeto paint. Después, esa ruta de acceso de destino se rellena y se trazos con redThinStroke, lo que da como resultado lo siguiente:

Captura de pantalla triple de la página Tap To Outline The Path (Pulsar para describir la ruta de acceso)

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 SKPathEffect.Create1DPath método . 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 el ejemplo Cadena vinculada, 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 LinkedChainPage clase para hacerlo de forma un poco diferente.

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

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

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

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 outlinePath objeto es el destinatario del esquema de linkPath cuando se traza con las propiedades especificadas en strokePaint.

Otro ejemplo que usa esta técnica es el siguiente para la ruta de acceso usada en un método .

Combinación de efectos de ruta de acceso

Los dos métodos de creación estáticos finales 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 CreateSum método crea un efecto de ruta de acceso similar a los dos efectos de ruta de acceso aplicados por separado, mientras CreateCompose que aplica un efecto de ruta de acceso (el inner) y, a continuación, aplica a outer eso.

Ya ha visto cómo el GetFillPath método de SKPaint puede convertir una ruta de acceso a otra en función SKPaint de las propiedades (incluida PathEffect) para que no sea demasiado misteriosa como un SKPaint objeto puede realizar esa operación dos veces con los dos efectos de ruta de acceso especificados en los CreateSum métodos o CreateCompose .

Un uso obvio de CreateSum es definir un SKPaint objeto que rellena una ruta de acceso con un efecto de ruta de acceso y trazo la ruta de acceso con otro efecto de ruta de acceso. Esto se muestra en el ejemplo Cats in Frame , que muestra una matriz de gatos dentro de un marco con bordes escalares:

Captura de pantalla triple de la página Cats In Frame

La CatsInFramePage clase comienza definiendo varios campos. Es posible que reconozca el primer campo de la PathDataCatPage clase del artículo Datos de ruta de acceso 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 Se podría usar en el SKPathEffect.Create2DPath método si la SKPaint propiedad del objeto Style está establecida Strokeen . Sin embargo, si se catPath utiliza 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 para catPath mover el punto (0, 0) al centro y reducirlo verticalmente en tamaño. GetFillPath obtiene todos los contornos de los contornos de outlinedCatPathy ese objeto se usa en la SKPathEffect.Create2DPath llamada. 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 SKPathEffect.Create1DPath al 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 SKPathEffect.CreateSum a para combinar los dos efectos de ruta de acceso y establecer el resultado en el SKPaint objeto .

Todo este trabajo permite que el PaintSurface controlador 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 la 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 ClipRect llamada anterior a la DrawRect llamada 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 DashedHatchLinesPage clase 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 luego se hace referencia a ellos en una SKPathEffect.CreateCompose llamada en la SKPaint definición:

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 PaintSurface controlador solo debe contener 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:

Captura de pantalla triple de la página Líneas de sombreado discontinuas

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.