Compartir vía


Enumeración e información de trazado

Obtener información sobre trazados y enumeración del contenido

La clase SKPath define varias propiedades y métodos que permiten obtener información sobre la ruta de acceso. Las propiedades Bounds y TightBounds (y los métodos relacionados) obtienen las dimensiones métricas de una ruta de acceso. El método Contains permite determinar si un punto determinado está dentro de una ruta de acceso.

A veces resulta útil determinar la longitud total de todas las líneas y curvas que componen un trazado. Calcular esta longitud no es una tarea algorítmicamente simple, por lo que toda una clase denominada PathMeasure se dedica a ella.

A veces también resulta útil obtener todas las operaciones de dibujo y los puntos que componen una ruta de acceso. Al principio, esta instalación podría parecer innecesaria: si el programa ha creado la ruta de acceso, el programa ya conoce el contenido. Sin embargo, ha visto que las rutas de acceso también se pueden crear mediante efectos de ruta de acceso y convirtiendo cadenas de texto en rutas de acceso. También puede obtener todas las operaciones y puntos de dibujo que componen estas rutas de acceso. Una posibilidad es aplicar una transformación algorítmica a todos los puntos, por ejemplo, para encapsular texto alrededor de un hemisferio:

Texto ajustado en un hemisferio

Obtención de la longitud de la ruta de acceso

En el artículo Rutas de acceso y texto ha visto cómo usar el método DrawTextOnPath para dibujar una cadena de texto cuya línea base sigue el curso de una ruta de acceso. Pero ¿qué ocurre si desea ajustar el tamaño del texto para que se ajuste a la ruta de acceso con precisión? Dibujar texto alrededor de un círculo es fácil porque la circunferencia de un círculo es fácil de calcular. Pero la circunferencia de una elipse o la longitud de una curva Bézier no es tan simple.

La clase SKPathMeasure puede ayudar. El constructor acepta un argumento SKPath y la propiedad Length revela su longitud.

Esta clase se muestra en el ejemplo Longitud de la ruta de acceso, que se basa en la página Bezier Curve. El archivo PathLengthPage.xaml deriva de InteractivePage e incluye una interfaz táctil:

<local:InteractivePage xmlns="http://xamarin.com/schemas/2014/forms"
                       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                       xmlns:local="clr-namespace:SkiaSharpFormsDemos"
                       xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
                       xmlns:tt="clr-namespace:TouchTracking"
                       x:Class="SkiaSharpFormsDemos.Curves.PathLengthPage"
                       Title="Path Length">
    <Grid BackgroundColor="White">
        <skia:SKCanvasView x:Name="canvasView"
                           PaintSurface="OnCanvasViewPaintSurface" />
        <Grid.Effects>
            <tt:TouchEffect Capture="True"
                            TouchAction="OnTouchEffectAction" />
        </Grid.Effects>
    </Grid>
</local:InteractivePage>

El archivo de código subyacente PathLengthPage.xaml.cs permite mover cuatro puntos táctiles para definir los puntos de conexión y los puntos de control de una curva Bézier cúbica. Tres campos definen una cadena de texto, un objeto SKPaint y un ancho calculado del texto:

public partial class PathLengthPage : InteractivePage
{
    const string text = "Compute length of path";

    static SKPaint textPaint = new SKPaint
    {
        Style = SKPaintStyle.Fill,
        Color = SKColors.Black,
        TextSize = 10,
    };

    static readonly float baseTextWidth = textPaint.MeasureText(text);
    ...
}

El campo baseTextWidth es el ancho del texto basado en una configuración TextSize de 10.

El controlador PaintSurface dibuja la curva Bézier y, a continuación, ajusta el texto para ajustarse a lo largo de su longitud completa:

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

    canvas.Clear();

    // Draw path with cubic Bezier curve
    using (SKPath path = new SKPath())
    {
        path.MoveTo(touchPoints[0].Center);
        path.CubicTo(touchPoints[1].Center,
                     touchPoints[2].Center,
                     touchPoints[3].Center);

        canvas.DrawPath(path, strokePaint);

        // Get path length
        SKPathMeasure pathMeasure = new SKPathMeasure(path, false, 1);

        // Find new text size
        textPaint.TextSize = pathMeasure.Length / baseTextWidth * 10;

        // Draw text on path
        canvas.DrawTextOnPath(text, path, 0, 0, textPaint);
    }
    ...
}

La propiedad Length del objeto recién creado SKPathMeasure obtiene la longitud de la ruta de acceso. La longitud de la ruta de acceso se divide por el valor baseTextWidth (que es el ancho del texto basado en un tamaño de texto de 10) y, a continuación, multiplicado por el tamaño de texto base de 10. El resultado es un nuevo tamaño de texto para mostrar el texto a lo largo de esa ruta de acceso:

Captura de pantalla triple de la página Longitud de la ruta de acceso

A medida que la curva Bézier se vuelve más larga o más corta, puede ver que el tamaño del texto cambia.

Recorrer la ruta de acceso

SKPathMeasure puede hacer algo más que medir la longitud de la ruta de acceso. Para cualquier valor entre cero y la longitud de la ruta de acceso, un objeto SKPathMeasure puede obtener la posición en la ruta de acceso y la tangente a la curva de trazado en ese punto. La tangente está disponible como vector en forma de objeto SKPoint o como rotación encapsulada en un objeto SKMatrix. Estos son los métodos de SKPathMeasure que obtienen esta información de maneras variadas y flexibles:

Boolean GetPosition (Single distance, out SKPoint position)

Boolean GetTangent (Single distance, out SKPoint tangent)

Boolean GetPositionAndTangent (Single distance, out SKPoint position, out SKPoint tangent)

Boolean GetMatrix (Single distance, out SKMatrix matrix, SKPathMeasureMatrixFlags flag)

Los miembros de la enumeración SKPathMeasureMatrixFlags son:

  • GetPosition
  • GetTangent
  • GetPositionAndTangent

La página Unicycle Half-Pipe anima una figura de palo en un unicycle que parece montar hacia atrás y hacia adelante a lo largo de una curva Bézier cúbica:

Captura de pantalla triple de la página Unicycle Half-Pipe

El objeto SKPaint que se usa para realizar la pulsación tanto en la canalización media como en el unicycle se define como un campo de la clase UnicycleHalfPipePage. También se define el objeto SKPath para el unicycle:

public class UnicycleHalfPipePage : ContentPage
{
    ...
    SKPaint strokePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 3,
        Color = SKColors.Black
    };

    SKPath unicyclePath = SKPath.ParseSvgPathData(
        "M 0 0" +
        "A 25 25 0 0 0 0 -50" +
        "A 25 25 0 0 0 0 0 Z" +
        "M 0 -25 L 0 -100" +
        "A 15 15 0 0 0 0 -130" +
        "A 15 15 0 0 0 0 -100 Z" +
        "M -25 -85 L 25 -85");
    ...
}

La clase contiene las invalidaciones estándar de los métodos OnAppearing y OnDisappearing para la animación. El controlador PaintSurface crea la ruta de acceso para la canalización media y, a continuación, la dibuja. A continuación, se crea un objeto SKPathMeasure en función de esta ruta de acceso:

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

        canvas.Clear();

        using (SKPath pipePath = new SKPath())
        {
            pipePath.MoveTo(50, 50);
            pipePath.CubicTo(0, 1.25f * info.Height,
                             info.Width - 0, 1.25f * info.Height,
                             info.Width - 50, 50);

            canvas.DrawPath(pipePath, strokePaint);

            using (SKPathMeasure pathMeasure = new SKPathMeasure(pipePath))
            {
                float length = pathMeasure.Length;

                // Animate t from 0 to 1 every three seconds
                TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
                float t = (float)(timeSpan.TotalSeconds % 5 / 5);

                // t from 0 to 1 to 0 but slower at beginning and end
                t = (float)((1 - Math.Cos(t * 2 * Math.PI)) / 2);

                SKMatrix matrix;
                pathMeasure.GetMatrix(t * length, out matrix,
                                      SKPathMeasureMatrixFlags.GetPositionAndTangent);

                canvas.SetMatrix(matrix);
                canvas.DrawPath(unicyclePath, strokePaint);
            }
        }
    }
}

El controlador PaintSurface calcula un valor de t que va de 0 a 1 cada cinco segundos. A continuación, usa la función Math.Cos para convertirla en un valor de t que va de 0 a 1 y de vuelta a 0, donde 0 corresponde al unicycle al principio de la parte superior izquierda, mientras que 1 corresponde al unicycle situado en la parte superior derecha. La función coseno hace que la velocidad sea más lenta en la parte superior de la tubería y más rápido en la parte inferior.

Observe que este valor de t debe multiplicarse por la longitud de la ruta de acceso del primer argumento a GetMatrix. A continuación, la matriz se aplica al objeto SKCanvas para dibujar la ruta de acceso de un solo ciclo.

Enumerar la ruta de acceso

Dos clases incrustadas de SKPath permiten enumerar el contenido de la ruta de acceso. Estas clases son SKPath.Iterator y SKPath.RawIterator. Las dos clases son muy similares, pero SKPath.Iterator pueden eliminar elementos de la ruta de acceso con una longitud cero o cerca de una longitud cero. El RawIterator se usa en el ejemplo siguiente.

Puede obtener un objeto de tipo SKPath.RawIterator llamando al método CreateRawIterator de SKPath. La enumeración a través de la ruta de acceso se realiza llamando repetidamente al método Next. Páselo a una matriz de cuatro valores SKPoint:

SKPoint[] points = new SKPoint[4];
...
SKPathVerb pathVerb = rawIterator.Next(points);

El método Next devuelve un miembro del tipo de enumeración SKPathVerb. Estos valores indican el comando de dibujo determinado en la ruta de acceso. El número de puntos válidos insertados en la matriz depende de este verbo:

  • Move con un único punto
  • Line con dos puntos
  • Cubic con cuatro puntos
  • Quad con tres puntos
  • Conic con tres puntos (y también llama al método ConicWeight para el peso)
  • Close con un punto
  • Done

El verbo Done indica que la enumeración de ruta de acceso está completa.

Observe que no hay verbos Arc. Esto indica que todos los arcos se convierten en curvas Bézier cuando se agregan al trazado.

Parte de la información de la matriz SKPoint es redundante. Por ejemplo, si un verbo Move va seguido de un verbo Line, el primero de los dos puntos que acompañan Line es el mismo que el punto Move. En la práctica, esta redundancia es muy útil. Cuando se obtiene un verbo Cubic, va acompañado de los cuatro puntos que definen la curva Bézier cúbica. No es necesario conservar la posición actual establecida por el verbo anterior.

Sin embargo, el verbo problemático es Close. Este comando dibuja una línea recta desde la posición actual hasta el principio del contorno establecido anteriormente por el comando Move. Idealmente, el verbo Close debe proporcionar estos dos puntos en lugar de solo un punto. Lo peor es que el punto que acompaña al verbo Close siempre es (0, 0). Al enumerar a través de una ruta de acceso, probablemente tendrá que conservar el punto Move y la posición actual.

Enumeración, aplanamiento y malformado

A veces es deseable aplicar una transformación algorítmica a una ruta de acceso a la forma incorrecta de alguna manera:

Texto ajustado en un hemisferio

La mayoría de estas letras constan de líneas rectas, pero estas líneas rectas aparentemente se han retorcido en curvas. ¿Cómo es posible?

La clave es que las líneas rectas originales se dividen en una serie de líneas rectas más pequeñas. Estas líneas rectas más pequeñas individuales se pueden manipular de maneras diferentes para formar una curva.

Para ayudar con este proceso, el ejemplo contiene una clase de estáticaPathExtensions con un método Interpolate que divide una línea recta en numerosas líneas cortas que son solo una unidad de longitud. Además, la clase contiene varios métodos que convierten los tres tipos de curvas Bézier en una serie de líneas rectas diminutas que aproximan la curva. (Las fórmulas paramétricas se presentaron en el artículo Tres tipos de curvas Bézier). Este proceso se denomina aplanar la curva:

static class PathExtensions
{
    ...
    static SKPoint[] Interpolate(SKPoint pt0, SKPoint pt1)
    {
        int count = (int)Math.Max(1, Length(pt0, pt1));
        SKPoint[] points = new SKPoint[count];

        for (int i = 0; i < count; i++)
        {
            float t = (i + 1f) / count;
            float x = (1 - t) * pt0.X + t * pt1.X;
            float y = (1 - t) * pt0.Y + t * pt1.Y;
            points[i] = new SKPoint(x, y);
        }

        return points;
    }

    static SKPoint[] FlattenCubic(SKPoint pt0, SKPoint pt1, SKPoint pt2, SKPoint pt3)
    {
        int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2) + Length(pt2, pt3));
        SKPoint[] points = new SKPoint[count];

        for (int i = 0; i < count; i++)
        {
            float t = (i + 1f) / count;
            float x = (1 - t) * (1 - t) * (1 - t) * pt0.X +
                        3 * t * (1 - t) * (1 - t) * pt1.X +
                        3 * t * t * (1 - t) * pt2.X +
                        t * t * t * pt3.X;
            float y = (1 - t) * (1 - t) * (1 - t) * pt0.Y +
                        3 * t * (1 - t) * (1 - t) * pt1.Y +
                        3 * t * t * (1 - t) * pt2.Y +
                        t * t * t * pt3.Y;
            points[i] = new SKPoint(x, y);
        }

        return points;
    }

    static SKPoint[] FlattenQuadratic(SKPoint pt0, SKPoint pt1, SKPoint pt2)
    {
        int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
        SKPoint[] points = new SKPoint[count];

        for (int i = 0; i < count; i++)
        {
            float t = (i + 1f) / count;
            float x = (1 - t) * (1 - t) * pt0.X + 2 * t * (1 - t) * pt1.X + t * t * pt2.X;
            float y = (1 - t) * (1 - t) * pt0.Y + 2 * t * (1 - t) * pt1.Y + t * t * pt2.Y;
            points[i] = new SKPoint(x, y);
        }

        return points;
    }

    static SKPoint[] FlattenConic(SKPoint pt0, SKPoint pt1, SKPoint pt2, float weight)
    {
        int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
        SKPoint[] points = new SKPoint[count];

        for (int i = 0; i < count; i++)
        {
            float t = (i + 1f) / count;
            float denominator = (1 - t) * (1 - t) + 2 * weight * t * (1 - t) + t * t;
            float x = (1 - t) * (1 - t) * pt0.X + 2 * weight * t * (1 - t) * pt1.X + t * t * pt2.X;
            float y = (1 - t) * (1 - t) * pt0.Y + 2 * weight * t * (1 - t) * pt1.Y + t * t * pt2.Y;
            x /= denominator;
            y /= denominator;
            points[i] = new SKPoint(x, y);
        }

        return points;
    }

    static double Length(SKPoint pt0, SKPoint pt1)
    {
        return Math.Sqrt(Math.Pow(pt1.X - pt0.X, 2) + Math.Pow(pt1.Y - pt0.Y, 2));
    }
}

A todos estos métodos se hace referencia desde el método de extensión CloneWithTransform también se incluyen en esta clase y se muestran a continuación. Este método clona una ruta de acceso mediante la enumeración de los comandos de ruta de acceso y la construcción de una nueva ruta de acceso basada en los datos. Sin embargo, la nueva ruta de acceso consta solo de llamadas MoveTo y LineTo. Todas las curvas y líneas rectas se reducen a una serie de líneas diminutas.

Al llamar CloneWithTransform, se pasa al método un Func<SKPoint, SKPoint>, que es una función con un parámetro SKPaint que devuelve un valor SKPoint. Se llama a esta función para cada punto para aplicar una transformación algorítmica personalizada:

static class PathExtensions
{
    public static SKPath CloneWithTransform(this SKPath pathIn, Func<SKPoint, SKPoint> transform)
    {
        SKPath pathOut = new SKPath();

        using (SKPath.RawIterator iterator = pathIn.CreateRawIterator())
        {
            SKPoint[] points = new SKPoint[4];
            SKPathVerb pathVerb = SKPathVerb.Move;
            SKPoint firstPoint = new SKPoint();
            SKPoint lastPoint = new SKPoint();

            while ((pathVerb = iterator.Next(points)) != SKPathVerb.Done)
            {
                switch (pathVerb)
                {
                    case SKPathVerb.Move:
                        pathOut.MoveTo(transform(points[0]));
                        firstPoint = lastPoint = points[0];
                        break;

                    case SKPathVerb.Line:
                        SKPoint[] linePoints = Interpolate(points[0], points[1]);

                        foreach (SKPoint pt in linePoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        lastPoint = points[1];
                        break;

                    case SKPathVerb.Cubic:
                        SKPoint[] cubicPoints = FlattenCubic(points[0], points[1], points[2], points[3]);

                        foreach (SKPoint pt in cubicPoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        lastPoint = points[3];
                        break;

                    case SKPathVerb.Quad:
                        SKPoint[] quadPoints = FlattenQuadratic(points[0], points[1], points[2]);

                        foreach (SKPoint pt in quadPoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        lastPoint = points[2];
                        break;

                    case SKPathVerb.Conic:
                        SKPoint[] conicPoints = FlattenConic(points[0], points[1], points[2], iterator.ConicWeight());

                        foreach (SKPoint pt in conicPoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        lastPoint = points[2];
                        break;

                    case SKPathVerb.Close:
                        SKPoint[] closePoints = Interpolate(lastPoint, firstPoint);

                        foreach (SKPoint pt in closePoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        firstPoint = lastPoint = new SKPoint(0, 0);
                        pathOut.Close();
                        break;
                }
            }
        }
        return pathOut;
    }
    ...
}

Dado que la ruta clonada se reduce a pequeñas líneas rectas, la función de transformación tiene la capacidad de convertir líneas rectas en curvas.

Observe que el método conserva el primer punto de cada contorno de la variable denominada firstPoint y la posición actual después de cada comando de dibujo de la variable lastPoint. Estas variables son necesarias para construir la línea de cierre final cuando se encuentra un verbo Close.

El ejemplo GlobularText usa este método de extensión para encapsular aparentemente texto alrededor de un hemisferio en un efecto 3D:

Captura de pantalla triple de la página Texto Globular

El constructor de clases GlobularTextPage realiza esta transformación. Crea un objeto SKPaint para el texto y, a continuación, obtiene un objeto SKPath del método GetTextPath. Esta es la ruta de acceso que se pasa al método de extensión CloneWithTransform junto con una función de transformación:

public class GlobularTextPage : ContentPage
{
    SKPath globePath;

    public GlobularTextPage()
    {
        Title = "Globular Text";

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

        using (SKPaint textPaint = new SKPaint())
        {
            textPaint.Typeface = SKTypeface.FromFamilyName("Times New Roman");
            textPaint.TextSize = 100;

            using (SKPath textPath = textPaint.GetTextPath("HELLO", 0, 0))
            {
                SKRect textPathBounds;
                textPath.GetBounds(out textPathBounds);

                globePath = textPath.CloneWithTransform((SKPoint pt) =>
                {
                    double longitude = (Math.PI / textPathBounds.Width) *
                                            (pt.X - textPathBounds.Left) - Math.PI / 2;
                    double latitude = (Math.PI / textPathBounds.Height) *
                                            (pt.Y - textPathBounds.Top) - Math.PI / 2;

                    longitude *= 0.75;
                    latitude *= 0.75;

                    float x = (float)(Math.Cos(latitude) * Math.Sin(longitude));
                    float y = (float)Math.Sin(latitude);

                    return new SKPoint(x, y);
                });
            }
        }
    }
    ...
}

La función de transformación calcula primero dos valores denominados longitude y latitude ese intervalo va desde –π/2 en la parte superior e izquierda del texto, para π/2 en la parte derecha e inferior del texto. El intervalo de estos valores no es visualmente satisfactorio, por lo que se reducen multiplicando por 0,75. (Pruebe el código sin esos ajustes. El texto se vuelve demasiado oscuro en los polos norte y sur, y demasiado delgado en los lados). Estas coordenadas esféricas tridimensionales se convierten en coordenadas bidimensionales x y y por fórmulas estándar.

La nueva ruta de acceso se almacena como un campo. El controlador PaintSurface simplemente necesita centrar y escalar la ruta de acceso para mostrarla en la pantalla:

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

        canvas.Clear();

        using (SKPaint pathPaint = new SKPaint())
        {
            pathPaint.Style = SKPaintStyle.Fill;
            pathPaint.Color = SKColors.Blue;
            pathPaint.StrokeWidth = 3;
            pathPaint.IsAntialias = true;

            canvas.Translate(info.Width / 2, info.Height / 2);
            canvas.Scale(0.45f * Math.Min(info.Width, info.Height));     // radius
            canvas.DrawPath(globePath, pathPaint);
        }
    }
}

Esta es una técnica muy versátil. Si la matriz de efectos de ruta de acceso descritos en el artículo Efectos de la ruta de acceso no abarca bastante algo que sentía que debe incluirse, esta es una manera de rellenar los huecos.