Partilhar via


Enumeração e informações de caminho

Obter informações sobre caminhos e enumerar o conteúdo

A SKPath classe define várias propriedades e métodos que permitem obter informações sobre o caminho. As Bounds propriedades e TightBounds (e métodos relacionados) obtêm as dimensões métricas de um caminho. O Contains método permite determinar se um ponto específico está dentro de um caminho.

Às vezes, é útil determinar o comprimento total de todas as linhas e curvas que compõem um caminho. Calcular esse comprimento não é uma tarefa algoritmicamente simples, então uma classe inteira nomeada PathMeasure é dedicada a ele.

Às vezes, também é útil obter todas as operações de desenho e pontos que compõem um caminho. A princípio, essa facilidade pode parecer desnecessária: se o seu programa criou o caminho, o programa já conhece o conteúdo. No entanto, você viu que os caminhos também podem ser criados por efeitos de caminho e pela conversão de cadeias de caracteres de texto em caminhos. Você também pode obter todas as operações de desenho e pontos que compõem esses caminhos. Uma possibilidade é aplicar uma transformação algorítmica a todos os pontos, por exemplo, para quebrar o texto em torno de um hemisfério:

Texto quebrado em um hemisfério

Obtendo o comprimento do caminho

No artigo Caminhos e texto , você viu como usar o DrawTextOnPath método para desenhar uma cadeia de caracteres de texto cuja linha de base segue o curso de um caminho. Mas e se você quiser dimensionar o texto para que ele se encaixe no caminho com precisão? Desenhar texto em torno de um círculo é fácil porque a circunferência de um círculo é simples de calcular. Mas a circunferência de uma elipse ou o comprimento de uma curva de Bézier não é tão simples.

A SKPathMeasure turma pode ajudar. O construtor aceita um SKPath argumento e a Length propriedade revela seu comprimento.

Essa classe é demonstrada no exemplo Path Length , que se baseia na página Bezier Curve . O arquivo PathLengthPage.xaml deriva de e inclui uma interface de InteractivePage toque:

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

O arquivo code-behind PathLengthPage.xaml.cs permite mover quatro pontos de contato para definir os pontos finais e os pontos de controle de uma curva cúbica de Bézier. Três campos definem uma cadeia de caracteres de texto, um SKPaint objeto e uma largura calculada do 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);
    ...
}

O baseTextWidth campo é a largura do texto com base em uma TextSize configuração de 10.

O PaintSurface manipulador desenha a curva de Bézier e, em seguida, dimensiona o texto para caber ao longo de todo o seu comprimento:

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

A Length propriedade do objeto recém-criado SKPathMeasure obtém o comprimento do caminho. O comprimento do baseTextWidth caminho é dividido pelo valor (que é a largura do texto com base em um tamanho de texto de 10) e, em seguida, multiplicado pelo tamanho do texto base de 10. O resultado é um novo tamanho de texto para exibir o texto ao longo desse caminho:

Captura de tela tripla da página Comprimento do caminho

À medida que a curva de Bézier fica mais longa ou mais curta, você pode ver o tamanho do texto mudar.

Atravessando o Caminho

SKPathMeasure pode fazer mais do que apenas medir o comprimento do caminho. Para qualquer valor entre zero e o comprimento do caminho, um SKPathMeasure objeto pode obter a posição no caminho e a tangente à curva do caminho nesse ponto. A tangente está disponível como um vetor na forma de um SKPoint objeto, ou como uma rotação encapsulada em um SKMatrix objeto. Aqui estão os métodos de obtenção dessas SKPathMeasure informações de maneiras variadas e flexíveis:

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)

Os membros da SKPathMeasureMatrixFlags enumeração são:

  • GetPosition
  • GetTangent
  • GetPositionAndTangent

A página Unicycle Half-Pipe anima uma figura de vara em um monociclo que parece andar de um lado para o outro ao longo de uma curva cúbica de Bézier:

Captura de tela tripla da página Unicycle Half-Pipe

O SKPaint objeto usado para acariciar o half-pipe e o monociclo é definido como um campo na UnicycleHalfPipePage classe. Também está definido o SKPath objeto para o monociclo:

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

A classe contém as substituições padrão dos OnAppearing métodos e OnDisappearing para animação. O PaintSurface manipulador cria o caminho para o half-pipe e, em seguida, desenha-o. Um SKPathMeasure objeto é então criado com base neste caminho:

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

O PaintSurface manipulador calcula um valor t que vai de 0 a 1 a cada cinco segundos. Em seguida, ele usa a Math.Cos função para converter isso em um valor t que varia de 0 a 1 e volta para 0, onde 0 corresponde ao monociclo no início no canto superior esquerdo, enquanto 1 corresponde ao monociclo no canto superior direito. A função cosseno faz com que a velocidade seja mais lenta na parte superior do tubo e mais rápida na parte inferior.

Observe que esse valor de deve ser multiplicado pelo comprimento do t caminho para o primeiro argumento para GetMatrix. A matriz é então aplicada ao SKCanvas objeto para desenhar o caminho do monociclo.

Enumerando o caminho

Duas classes incorporadas de SKPath permitem que você enumere o conteúdo do caminho. Essas classes são SKPath.Iterator e SKPath.RawIterator. As duas classes são muito semelhantes, mas SKPath.Iterator podem eliminar elementos no caminho com um comprimento zero, ou perto de um comprimento zero. O RawIterator é usado no exemplo abaixo.

Você pode obter um objeto do tipo SKPath.RawIterator chamando o CreateRawIterator método de SKPath. A enumeração através do caminho é realizada chamando repetidamente o Next método. Passe para ele uma matriz de quatro SKPoint valores:

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

O Next método retorna um membro do SKPathVerb tipo de enumeração. Esses valores indicam o comando de desenho específico no caminho. O número de pontos válidos inseridos na matriz depende deste verbo:

  • Move com um único ponto
  • Line com dois pontos
  • Cubic com quatro pontos
  • Quad com três pontos
  • Conic com três pontos (e também chamar o ConicWeight método para o peso)
  • Close com um ponto
  • Done

O Done verbo indica que a enumeração de caminho está completa.

Observe que não Arc há verbos. Isso indica que todos os arcos são convertidos em curvas de Bézier quando adicionados ao caminho.

Algumas das informações na SKPoint matriz são redundantes. Por exemplo, se um Move verbo é seguido por um Line verbo, então o primeiro dos dois pontos que acompanham o Line é o mesmo que o Move ponto. Na prática, essa redundância é muito útil. Quando você recebe um Cubic verbo, ele é acompanhado por todos os quatro pontos que definem a curva cúbica de Bézier. Você não precisa manter a posição atual estabelecida pelo verbo anterior.

O verbo problemático, no entanto, é Close. Este comando traça uma linha reta da posição atual até o início do contorno estabelecido anteriormente pelo Move comando. Idealmente, o verbo Close deveria fornecer esses dois pontos em vez de apenas um ponto. O pior é que o ponto que acompanha o verbo Close é sempre (0, 0). Ao enumerar através de um caminho, você provavelmente precisará reter o Move ponto e a posição atual.

Enumeração, achatamento e malformação

Às vezes, é desejável aplicar uma transformação algorítmica a um caminho para malformá-lo de alguma forma:

Texto quebrado em um hemisfério

A maioria dessas letras consiste em linhas retas, mas essas linhas retas aparentemente foram torcidas em curvas. Como isso é possível?

A chave é que as linhas retas originais são quebradas em uma série de linhas retas menores. Essas linhas retas menores individuais podem então ser manipuladas de diferentes maneiras para formar uma curva.

Para ajudar nesse processo, o exemplo contém uma classe estática PathExtensions com um Interpolate método que divide uma linha reta em várias linhas curtas que têm apenas uma unidade de comprimento. Além disso, a classe contém vários métodos que convertem os três tipos de curvas de Bézier em uma série de minúsculas linhas retas que se aproximam da curva. (As fórmulas paramétricas foram apresentadas no artigo Três tipos de curvas de Bézier.) Esse processo é chamado de achatamento da 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));
    }
}

Todos esses métodos são referenciados a partir do método CloneWithTransform de extensão também incluído nesta classe e mostrado abaixo. Esse método clona um caminho enumerando os comandos path e construindo um novo caminho com base nos dados. No entanto, o novo caminho consiste apenas em MoveTo e LineTo chamadas. Todas as curvas e retas são reduzidas a uma série de linhas minúsculas.

Ao chamar CloneWithTransform, você passa para o método a Func<SKPoint, SKPoint>, que é uma função com um SKPaint parâmetro que retorna um SKPoint valor. Essa função é chamada para cada ponto para aplicar uma transformação 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;
    }
    ...
}

Como o caminho clonado é reduzido a linhas retas minúsculas, a função transform tem a capacidade de converter linhas retas em curvas.

Observe que o método retém o primeiro ponto de cada contorno na variável chamada firstPoint e a posição atual após cada comando de desenho na variável lastPoint. Essas variáveis são necessárias para construir a linha de fechamento final quando um Close verbo é encontrado.

O exemplo GlobularText usa esse método de extensão para aparentemente quebrar texto ao redor de um hemisfério em um efeito 3D:

Captura de tela tripla da página Texto Globular

O GlobularTextPage construtor de classe executa essa transformação. Ele cria um SKPaint objeto para o texto e, em seguida, obtém um SKPath objeto do GetTextPath método. Este é o caminho passado para o método de CloneWithTransform extensão junto com uma função de transformação:

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

A função transform primeiro calcula dois valores nomeados longitude e latitude que variam de –π/2 na parte superior e esquerda do texto, a π/2 à direita e na parte inferior do texto. O intervalo desses valores não é visualmente satisfatório, então eles são reduzidos multiplicando-se por 0,75. (Experimente o código sem esses ajustes. O texto torna-se demasiado obscuro nos polos norte e sul, e demasiado fino nas laterais.) Essas coordenadas esféricas tridimensionais são convertidas em coordenadas bidimensionais e y bidimensionais x por fórmulas padrão.

O novo caminho é armazenado como um campo. O PaintSurface manipulador, então, só precisa centralizar e dimensionar o caminho para exibi-lo na tela:

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 é uma técnica muito versátil. Se a matriz de efeitos de caminho descrita no artigo Efeitos de caminho não abranger algo que você achou que deveria ser incluído, essa é uma maneira de preencher as lacunas.