경로 정보 및 열거형

Download Sample 샘플 다운로드

경로에 대한 정보를 얻고 내용을 열거합니다.

이 클래스는 SKPath 경로에 대한 정보를 가져올 수 있는 여러 속성과 메서드를 정의합니다. 및 TightBounds 속성(및 관련 메서드)은 Bounds 경로의 메트릭 차원을 가져옵니다. 이 Contains 메서드를 사용하면 특정 지점이 경로 내에 있는지 확인할 수 있습니다.

경로를 구성하는 모든 선과 곡선의 총 길이를 결정하는 것이 유용한 경우도 있습니다. 이 길이를 계산하는 것은 알고리즘적으로 간단한 작업이 아니므로 명명된 PathMeasure 전체 클래스가 이 작업에 전념합니다.

경로를 구성하는 모든 그리기 작업 및 점을 가져오는 것도 유용할 수 있습니다. 처음에는 이 기능이 불필요해 보일 수 있습니다. 프로그램이 경로를 만든 경우 프로그램에서 이미 내용을 알고 있습니다. 그러나 경로 효과 및 텍스트 문자열을 경로로 변환하여 경로를 만들 수도 있습니다. 이러한 경로를 구성하는 모든 그리기 작업 및 점을 가져올 수도 있습니다. 한 가지 가능성은 알고리즘 변환을 모든 점에 적용하여(예: 반구 주위에 텍스트를 래핑하는) 것입니다.

Text wrapped on a hemisphere

경로 길이 가져오기

경로 및 텍스트 문서에서는 메서드를 사용하여 DrawTextOnPath 경로의 과정을 따르는 기준선의 텍스트 문자열을 그리는 방법을 알아보았습니다. 그러나 경로에 정확하게 맞도록 텍스트의 크기를 조정하려면 어떻게 해야 할까요? 원 주위에 텍스트를 그리는 것은 원의 둘레를 계산하는 것이 간단하기 때문에 쉽습니다. 그러나 타원의 둘레 또는 베지어 곡선의 길이는 그렇게 간단하지 않습니다.

클래스가 SKPathMeasure 도움이 될 수 있습니다. 생성자는 인수를 SKPath 허용하고 속성은 Length 해당 길이를 표시합니다.

이 클래스는 베지어 곡선 페이지를 기반으로 하는 경로 길이 샘플에서 설명합니다. PathLengthPage.xaml 파일은 터치 인터페이스에서 InteractivePage 파생되며 다음을 포함합니다.

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

PathLengthPage.xaml.cs 코드 숨김 파일을 사용하면 4개의 터치 포인트를 이동하여 입방형 베지어 곡선의 끝점과 제어점을 정의할 수 있습니다. 세 개의 필드는 텍스트 문자열, SKPaint 개체 및 텍스트의 계산된 너비를 정의합니다.

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

baseTextWidth 필드는 10의 설정에 TextSize 따라 텍스트의 너비입니다.

PaintSurface 처리기는 Bézier 곡선을 그린 다음 전체 길이에 맞게 텍스트 크기를 조정합니다.

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

새로 만든 SKPathMeasure 개체의 속성은 Length 경로의 길이를 가져옵니다. 경로 길이는 값(텍스트 크기 10을 기준으로 텍스트 너비)으로 나눈 baseTextWidth 다음 기본 텍스트 크기 10을 곱합니다. 결과는 해당 경로를 따라 텍스트를 표시하기 위한 새 텍스트 크기입니다.

Triple screenshot of the Path Length page

Bézier 곡선이 더 길어지거나 짧아지면 텍스트 크기가 변경되는 것을 볼 수 있습니다.

경로 트래버스

SKPathMeasure 는 경로의 길이를 측정하는 것 이상을 수행할 수 있습니다. 0과 경로 길이 SKPathMeasure 사이의 값에 대해 개체는 경로의 위치와 해당 지점에서 경로 곡선에 대한 탄젠트를 가져올 수 있습니다. 탄젠트를 개체의 SKPoint 형태로 벡터로 사용하거나 개체에 SKMatrix 캡슐화된 회전으로 사용할 수 있습니다. 이 정보를 다양하고 유연한 방법으로 가져오는 방법은 SKPathMeasure 다음과 같습니다.

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)

열거형의 SKPathMeasureMatrixFlags 멤버는 다음과 같습니다.

  • GetPosition
  • GetTangent
  • GetPositionAndTangent

유니사이클 하프 파이프 페이지는 입방형 베지어 곡선을 따라 앞뒤로 주행하는 것처럼 보이는 유니사이클에 스틱 그림에 애니메이션 효과를 낸다.

Triple screenshot of the Unicycle Half-Pipe page

SKPaint 하프 파이프와 유니사이클을 모두 쓰다듬는 데 사용되는 개체는 클래스의 UnicycleHalfPipePage 필드로 정의됩니다. 또한 유니사이클에 SKPath 대한 개체도 정의됩니다.

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

이 클래스에는 애니메이션에 대한 메서드 및 OnDisappearing 메서드의 표준 재정의가 OnAppearing 포함됩니다. PaintSurface 처리기는 반 파이프에 대한 경로를 만든 다음 그립니다. SKPathMeasure 그런 다음 이 경로에 따라 개체가 만들어집니다.

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

PaintSurface 처리기는 5초마다 0에서 1까지의 t 값을 계산합니다. 그런 다음 함수를 사용하여 Math.Cos 0에서 1까지의 값으로 변환하고, 0은 왼쪽 상단의 t 시작 부분에 있는 유니사이클에 해당하고, 1은 오른쪽 위에 있는 유니사이클에 해당합니다. 코사인 함수는 속도가 파이프 맨 위에서 가장 느리고 아래쪽에서 가장 빠릅니다.

이 값은 첫 번째 인수GetMatrixt 경로 길이를 곱해야 합니다. 그러면 행렬이 유니사이클 경로를 그리기 SKCanvas 위해 개체에 적용됩니다.

경로 열거

포함된 두 클래스를 SKPath 사용하면 경로의 내용을 열거할 수 있습니다. 이러한 클래스는 다음과 같습니다 SKPath.IteratorSKPath.RawIterator. 두 클래스는 매우 유사하지만 SKPath.Iterator 길이가 0이거나 길이가 0에 가까운 경로의 요소를 제거할 수 있습니다. 아래 RawIterator 예제에서 사용됩니다.

의 메서드SKPath를 호출하여 형식 SKPath.RawIterator 의 개체를 CreateRawIterator 가져올 수 있습니다. 경로를 통해 열거하는 작업은 메서드를 반복적으로 호출하여 수행됩니다 Next . 4개의 SKPoint 값 배열을 전달합니다.

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

메서드는 Next 열거형 형식의 멤버를 SKPathVerb 반환합니다. 이러한 값은 경로의 특정 그리기 명령을 나타냅니다. 배열에 삽입된 유효한 점의 수는 다음 동사에 따라 달라집니다.

  • Move 단일 지점이 있는 경우
  • Line 2점
  • Cubic 4점
  • Quad 3점
  • Conic 3포인트가 있는 경우(가중치에 ConicWeight 대한 메서드도 호출)
  • Close 1포인트가 있는 경우
  • Done

Done 동사는 경로 열거가 완료되었음을 나타냅니다.

동사가 없습니다 Arc . 이는 경로에 추가할 때 모든 호가 베지어 곡선으로 변환됨을 나타냅니다.

배열의 일부 정보는 SKPoint 중복됩니다. 예를 들어 동사 뒤에 Line 동사가 있는 경우 Move 동사와 함께 제공되는 Line 두 점 중 첫 번째는 점과 Move 같습니다. 실제로 이러한 중복성은 매우 유용합니다. 동사를 얻을 Cubic 때, 그것은 입방 베지어 곡선을 정의하는 네 점 모두와 함께 제공됩니다. 이전 동사에서 설정한 현재 위치를 유지할 필요가 없습니다.

그러나 문제가 있는 동사는 .입니다 Close. 이 명령은 현재 위치에서 이전에 명령으로 설정된 윤곽선의 시작 부분으로 직선을 Move 그립니다. 이상적으로 동 Close 사는 1점이 아닌 이 두 점을 제공해야 합니다. 더 나쁜 것은 동사와 함께 제공되는 점이 Close 항상 (0, 0)이라는 것입니다. 경로를 열거할 때 점과 현재 위치를 유지해야 Move 할 수 있습니다.

열거형, 평면화 및 잘못된 형식

알고리즘 변환을 경로에 적용하여 어떤 식으로든 잘못된 형식을 지정하는 것이 바람직한 경우도 있습니다.

Text wrapped on a hemisphere

이러한 문자의 대부분은 직선으로 구성되어 있지만,이 직선은 분명히 곡선으로 왜곡되었습니다. 어떻게 가능한가요?

핵심은 원래 직선이 일련의 작은 직선으로 나뉘어 있다는 것입니다. 이러한 개별 작은 직선은 곡선을 형성하기 위해 다른 방법으로 조작 할 수 있습니다.

이 프로세스를 돕기 위해 SkiaSharpFormsDemos 샘플에는 직선을 길이가 한 단위에 불과한 수많은 짧은 줄로 나누는 메서드가 포함된 정적 PathExtensions 클래스 Interpolate 가 포함되어 있습니다. 또한 클래스에는 세 가지 유형의 Bézier 곡선을 곡선을 근사하는 일련의 작은 직선으로 변환하는 여러 메서드가 포함되어 있습니다. (매개 변수 수식은 문서에 제공되었습니다.세 가지 유형의 베지어 곡선.) 이 프로세스를 곡선 평면화라고 합니다.

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

이러한 모든 메서드는 이 클래스에 포함된 확장 메서드 CloneWithTransform 에서 참조되며 아래와 같습니다. 이 메서드는 경로 명령을 열거하고 데이터를 기반으로 새 경로를 생성하여 경로를 복제합니다. 그러나 새 경로는 호출로만 MoveTo 구성됩니다 LineTo . 모든 곡선과 직선은 일련의 작은 선으로 줄어듭니다.

호출CloneWithTransform할 때 값을 반환하는 매개 변수가 있는 함수 SKPaint 인 a 메서드Func<SKPoint, SKPoint>에 전달합니다SKPoint. 이 함수는 사용자 지정 알고리즘 변환을 적용하기 위해 모든 지점에 대해 호출됩니다.

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

복제된 경로는 작은 직선으로 축소되므로 변환 함수는 직선을 곡선으로 변환하는 기능을 갖습니다.

메서드는 호출 firstPoint 된 변수에서 각 윤곽선의 첫 번째 점과 변수의 각 그리기 명령 lastPoint뒤의 현재 위치를 유지합니다. 이러한 변수는 동사가 발견될 때 Close 최종 닫는 선을 생성하는 데 필요합니다.

GlobularText 샘플은 이 확장 메서드를 사용하여 3D 효과로 반구 주위에 텍스트를 래핑하는 것처럼 보입니다.

Triple screenshot of the Globular Text page

GlobularTextPage 클래스 생성자는 이 변환을 수행합니다. 텍스트에 SKPaint 대한 개체를 만든 다음 메서드에서 개체를 SKPathGetTextPath 가져옵니다. 변환 함수와 함께 확장 메서드에 전달되는 CloneWithTransform 경로입니다.

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

변환 함수는 먼저 텍스트의 위쪽과 latitude 왼쪽에 있는 longitude –π/2부터 텍스트의 오른쪽과 아래쪽에 있는 π/2까지의 두 값을 계산합니다. 이러한 값의 범위는 시각적으로 만족스럽지 않으므로 0.75를 곱하여 줄입니다. (이러한 조정 없이 코드를 사용해 보세요. 텍스트는 북극과 남극에서 너무 모호해지고 측면에는 너무 얇아집니다.) 이러한 3차원 구형 좌표는 표준 수식에 의해 2차원 xy 좌표로 변환됩니다.

새 경로는 필드로 저장됩니다. PaintSurface 그런 다음 처리기는 단순히 경로를 가운데에 두고 크기를 조정하여 화면에 표시하기만 하면 됩니다.

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

이것은 매우 다재다능한 기술입니다. 경로 효과 문서에 설명된 경로 효과 배열이 포함되어야 한다고 생각되는 항목을 포함하지 않는 경우 간격을 채우는 방법입니다.