Sdílet prostřednictvím


Data cesty SVG v SkiaSharpu

Definování cest pomocí textových řetězců ve formátu Scalable Vector Graphics

Třída SKPath podporuje definici celých objektů cesty z textových řetězců ve formátu vytvořeném specifikací SVG (Scalable Vector Graphics). Později v tomto článku se dozvíte, jak můžete reprezentovat celou cestu, například tuto cestu v textovém řetězci:

Ukázková cesta definovaná s daty cesty SVG

SVG je grafický programovací jazyk založený na jazyce XML pro webové stránky. Vzhledem k tomu, že SVG musí umožňovat, aby cesty byly definovány v revizích namísto řady volání funkcí, obsahuje standard SVG extrémně stručný způsob zadávání celé grafické cesty jako textového řetězce.

V rámci skiaSharpu se tento formát označuje jako "SVG path-data". Formát je podporován také v programovacích prostředích založených na jazyce Windows XAML, včetně Windows Presentation Foundation a Univerzální platforma Windows, kde se označuje jako syntaxe značek cesty nebo syntaxe příkazů Přesunout a kreslit. Může také sloužit jako formát výměny pro obrázky vektorové grafiky, zejména v textových souborech, jako je XML.

Třída SKPath definuje dvě metody se slovy SvgPathData v jejich názvech:

public static SKPath ParseSvgPathData(string svgPath)

public string ToSvgPathData()

Statická ParseSvgPathData metoda převede řetězec na SKPath objekt, zatímco ToSvgPathData převede SKPath objekt na řetězec.

Tady je řetězec SVG pro pěticípou hvězdu zacentrovanou na bod (0, 0) s poloměrem 100:

"M 0 -100 L 58.8 90.9, -95.1 -30.9, 95.1 -30.9, -58.8 80.9 Z"

Písmena jsou příkazy, které vytvářejí SKPath objekt: M označuje MoveTo volání, L je LineToa Z je Close uzavřít obrys. Každá dvojice čísel poskytuje souřadnici X a Y bodu. Všimněte si, že za L příkazem následuje několik bodů oddělených čárkami. V řadě souřadnic a bodů se čárkami a prázdnými znaky zachází stejně. Někteří programátoři raději dávají čárky mezi souřadnice X a Y místo mezi body, ale čárky nebo mezery jsou nutné pouze k tomu, aby nedocházelo k nejednoznačnosti. Toto je naprosto právní:

"M0-100L58.8 90.9-95.1-30.9 95.1-30.9-58.8 80.9Z"

Syntaxe dat cesty SVG je formálně zdokumentovaná v části 8.3 specifikace SVG. Tady je souhrn:

Přesunout na

M x y

Tím se v cestě zahájí nový obrys nastavením aktuální pozice. Data cesty by měla vždy začínat příkazem M .

LineTo

L x y ...

Tento příkaz přidá do cesty přímku (nebo čáry) a nastaví novou aktuální pozici na konec posledního řádku. Můžete postupovat L podle příkazu s několika dvojicemi souřadnic x a y .

Vodorovná čára

H x ...

Tento příkaz přidá do cesty vodorovnou čáru a nastaví novou aktuální pozici na konec řádku. Můžete postupovat H podle příkazu s několika souřadnicemi x , ale nemá moc smysl.

Svislá čára

V y ...

Tento příkaz přidá svislou čáru do cesty a nastaví novou aktuální pozici na konec řádku.

Zavřít

Z

Příkaz C zavře obrys přidáním rovné čáry z aktuální pozice na začátek obrysu.

ArcTo

Příkaz pro přidání eliptického oblouku do obrysu je zdaleka nejsložitější příkaz v celé specifikaci cesty SVG. Jedná se o jediný příkaz, ve kterém mohou čísla představovat něco jiného než hodnoty souřadnic:

A rx ry rotation-angle large-arc-flag sweep-flag x y ...

Parametry rx a ry jsou vodorovné a svislé paprsky tří teček. Úhel otáčení je ve stupních ve směru hodinových ručiček.

Nastavte u velkého oblouku příznak na 1 pro velký oblouk nebo na 0 pro malý oblouk.

Nastavte příznak úklidu na 1 pro směru hodinových ručiček a na 0 pro proti směru hodinových ručiček.

Oblouk se nakreslí k bodu (x, y), který se stane novou aktuální pozicí.

CubicTo

C x1 y1 x2 y2 x3 y3 ...

Tento příkaz přidá krychlovou Bézierovou křivku z aktuální pozice do (x3, y3), která se stane novou aktuální pozicí. Body (x1, y1) a (x2, y2) jsou kontrolní body.

Více bézierových křivek lze zadat jedním C příkazem. Počet bodů musí být násobkem 3.

K dispozici je také příkaz "smooth" Bézierovy křivky:

S x2 y2 x3 y3 ...

Tento příkaz by měl následovat za normálním příkazem Bézier (i když to není nezbytně nutné). Hladký příkaz Bézier vypočítá první kontrolní bod tak, aby byl odrazem druhého kontrolního bodu předchozího Béziera kolem jejich vzájemného bodu. Tyto tři body jsou tedy kolineární a spojení mezi dvěma Bézierovými křivkami je hladké.

QuadTo

Q x1 y1 x2 y2 ...

U kvadratických bézierových křivek musí být počet bodů násobkem 2. Řídicí bod je (x1, y1) a koncový bod (a nová aktuální pozice) je (x2, y2)

K dispozici je také hladký kvadratický příkaz:

T x2 y2 ...

Řídicí bod se vypočítá na základě řídicího bodu předchozí kvadratické křivky.

Všechny tyto příkazy jsou k dispozici také v "relativních" verzích, kde jsou souřadnicové body relativní k aktuální pozici. Tyto relativní příkazy začínají malá písmena, například c místo C relativní verze krychlových příkazů Bézier.

Toto je rozsah definice cesty SVG k datům. Neexistuje žádné zařízení pro opakující se skupiny příkazů ani pro provedení jakéhokoli typu výpočtu. Příkazy pro ConicTo specifikace oblouku nebo jiné typy nejsou k dispozici.

Statická SKPath.ParseSvgPathData metoda očekává platný řetězec příkazů SVG. Pokud je zjištěna jakákoli syntaktická chyba, vrátí nullmetoda . To je jediná indikace chyby.

Tato ToSvgPathData metoda je praktická pro získání dat cesty SVG z existujícího SKPath objektu k přenosu do jiného programu nebo k uložení v textovém formátu souboru, jako je XML. (Metoda ToSvgPathData není v ukázkovém kódu v tomto článku ukázaná.) Neočekávejte ToSvgPathData, že vrátí řetězec odpovídající přesně volání metody, která vytvořila cestu. Konkrétně zjistíte, že oblouky se převedou na více QuadTo příkazů a takto se zobrazují v datech cesty vrácených z ToSvgPathData.

Stránka Hello data cesty uvádí slovo "HELLO" pomocí dat cesty SVG. Objekty SKPath i SKPaint objekty jsou definovány jako pole ve PathDataHelloPage třídě:

public class PathDataHelloPage : ContentPage
{
    SKPath helloPath = SKPath.ParseSvgPathData(
        "M 0 0 L 0 100 M 0 50 L 50 50 M 50 0 L 50 100" +                // H
        "M 125 0 C 60 -10, 60 60, 125 50, 60 40, 60 110, 125 100" +     // E
        "M 150 0 L 150 100, 200 100" +                                  // L
        "M 225 0 L 225 100, 275 100" +                                  // L
        "M 300 50 A 25 50 0 1 0 300 49.9 Z");                           // O

    SKPaint paint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Blue,
        StrokeWidth = 10,
        StrokeCap = SKStrokeCap.Round,
        StrokeJoin = SKStrokeJoin.Round
    };
    ...
}

Cesta definující textový řetězec začíná v levém horním rohu v bodě(0, 0). Každé písmeno je 50 jednotek široké a 100 jednotek vysoké a písmena jsou oddělena dalšími 25 jednotkami, což znamená, že celá cesta je 350 jednotek široká.

"H" "Hello" se skládá ze tří obrysových čar, zatímco "E" je dvě propojené krychlové Bézierovy křivky. Všimněte si, že za C příkazem následuje šest bodů a dva z kontrolních bodů mají souřadnice Y –10 a 110, které je umístí mimo rozsah souřadnic Y ostatních písmen. L je dvě propojené čáry, zatímco "O" je tři tečky, které se vykreslují příkazem A .

Všimněte si, že M příkaz, který začíná posledním obrysem, nastaví pozici na bod (350, 50), což je svislý střed levé strany "O". Jak je uvedeno prvními čísly za A příkazem, má tři tečky vodorovný poloměr 25 a svislý poloměr 50. Koncový bod je označen poslední dvojicí čísel v A příkazu, která představuje bod (300, 49,9). To se záměrně mírně liší od počátečního bodu. Pokud je koncový bod nastavený jako počáteční bod, oblouk se nevykreslí. Pokud chcete nakreslit úplné tři tečky, musíte nastavit koncový bod blízko počátečního bodu (ale ne rovno) nebo musíte použít dva nebo více A příkazů, z nichž každý je součástí celého elipsy.

Do konstruktoru stránky můžete přidat následující příkaz a pak nastavit zarážku pro prozkoumání výsledného řetězce:

string str = helloPath.ToSvgPathData();

Zjistíte, že oblouk byl nahrazen dlouhou Q řadou příkazů pro kusmeální aproximaci oblouku pomocí kvadratických Bézierových křivek.

Obslužná PaintSurface rutina získá těsné hranice cesty, která nezahrnuje kontrolní body pro křivky "E" a "O". Tyto tři transformace přesunou střed cesty k bodu (0, 0), škálují cestu na velikost plátna (ale také berou v úvahu šířku tahů) a pak přesuňte střed cesty na střed plátna:

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

        canvas.Clear();

        SKRect bounds;
        helloPath.GetTightBounds(out bounds);

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

        canvas.Scale(info.Width / (bounds.Width + paint.StrokeWidth),
                     info.Height / (bounds.Height + paint.StrokeWidth));

        canvas.Translate(-bounds.MidX, -bounds.MidY);

        canvas.DrawPath(helloPath, paint);
    }
}

Cesta vyplní plátno, které vypadá přiměřenou při prohlížení v režimu na šířku:

Trojitý snímek obrazovky se stránkou Hello data cesty

Stránka Path Data Cat je podobná. Cesta i malování objektů jsou definovány jako pole ve PathDataCatPage třídě:

public class PathDataCatPage : ContentPage
{
    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 paint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Orange,
        StrokeWidth = 5
    };
    ...
}

Hlava kočky je kruh, a zde je vykreslen se dvěma A příkazy, z nichž každá nakreslí středník. Oba A příkazy pro hlavu definují vodorovné a svislé paprsky 100. První oblouk začíná na (240, 100) a končí na (240, 300), což se stane počátečním bodem druhého oblouku, který končí zpět (240, 100).

Oba oči se také vykreslují dvěma A příkazy a stejně jako u hlavy kočky končí druhý A příkaz ve stejném bodě jako začátek prvního A příkazu. Tyto dvojice příkazů ale nedefinují tři tečky A . S každým obloukem je 40 jednotek a poloměr je také 40 jednotek, což znamená, že tyto oblouky nejsou plné půlkruhy.

Obslužná PaintSurface rutina provádí podobné transformace jako předchozí vzorek, ale nastaví jeden Scale faktor pro zachování poměru stran a poskytnutí malého okraje, aby se kocour nedotýkal stran obrazovky:

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

        canvas.Clear(SKColors.Black);

        SKRect bounds;
        catPath.GetBounds(out bounds);

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

        canvas.Scale(0.9f * Math.Min(info.Width / bounds.Width,
                                     info.Height / bounds.Height));

        canvas.Translate(-bounds.MidX, -bounds.MidY);

        canvas.DrawPath(catPath, paint);
    }
}

Tady je spuštěný program:

Trojitý snímek obrazovky se stránkou Path Data Cat

Za normálních okolností, pokud SKPath je objekt definován jako pole, musí být obrysy cesty definovány v konstruktoru nebo jiné metodě. Při použití dat cesty SVG jste však viděli, že cestu lze zadat zcela v definici pole.

Předchozí ukázka Ugly Analog Clock v článku Transformace otočení zobrazila ruce hodin jako jednoduché čáry. Níže uvedený program Pretty Analog Clock nahrazuje tyto čáry SKPath objekty definovanými jako pole ve PrettyAnalogClockPage třídě spolu s SKPaint objekty:

public class PrettyAnalogClockPage : ContentPage
{
    ...
    // Clock hands pointing straight up
    SKPath hourHandPath = SKPath.ParseSvgPathData(
        "M 0 -60 C   0 -30 20 -30  5 -20 L  5   0" +
                "C   5 7.5 -5 7.5 -5   0 L -5 -20" +
                "C -20 -30  0 -30  0 -60 Z");

    SKPath minuteHandPath = SKPath.ParseSvgPathData(
        "M 0 -80 C   0 -75  0 -70  2.5 -60 L  2.5   0" +
                "C   2.5 5 -2.5 5 -2.5   0 L -2.5 -60" +
                "C 0 -70  0 -75  0 -80 Z");

    SKPath secondHandPath = SKPath.ParseSvgPathData(
        "M 0 10 L 0 -80");

    // SKPaint objects
    SKPaint handStrokePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Black,
        StrokeWidth = 2,
        StrokeCap = SKStrokeCap.Round
    };

    SKPaint handFillPaint = new SKPaint
    {
        Style = SKPaintStyle.Fill,
        Color = SKColors.Gray
    };
    ...
}

Hodiny a minuty ruce jsou nyní uzavřeny oblasti. Aby se tyto ruce navzájem odlišily, jsou kresleny černou obrysovou i šedou výplní pomocí handStrokePaint objektů a handFillPaint objektů.

V předchozím ukázku Ugly Analog Clock byly malé kruhy, které označily hodiny a minuty, nakresleny ve smyčce. V tomto vzorku Pretty Analog Clock se používá zcela jiný přístup: hodiny a minuty značky jsou tečkované čáry nakreslené s minuteMarkPaint objekty a hourMarkPaint objekty:

public class PrettyAnalogClockPage : ContentPage
{
    ...
    SKPaint minuteMarkPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Black,
        StrokeWidth = 3,
        StrokeCap = SKStrokeCap.Round,
        PathEffect = SKPathEffect.CreateDash(new float[] { 0, 3 * 3.14159f }, 0)
    };

    SKPaint hourMarkPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Black,
        StrokeWidth = 6,
        StrokeCap = SKStrokeCap.Round,
        PathEffect = SKPathEffect.CreateDash(new float[] { 0, 15 * 3.14159f }, 0)
    };
    ...
}

Článek Tečky a Pomlčky popisuje, jak můžete použít metodu SKPathEffect.CreateDash k vytvoření přerušované čáry. První argument je float pole, které má obecně dva prvky: první prvek je délka pomlček a druhý prvek je mezera mezi pomlčkami. StrokeCap Pokud je vlastnost nastavena na SKStrokeCap.Round, pak zaokrouhlené konce pomlčky efektivně prodloužit délku pomlčky o šířku tahu na obou stranách pomlčky. Nastavení prvního prvku pole na hodnotu 0 tedy vytvoří tečkovanou čáru.

Vzdálenost mezi těmito tečkami se řídí druhým prvkem pole. Jak uvidíte krátce, tyto dva SKPaint objekty se používají k kreslení kruhů s poloměrem 90 jednotek. Obvod tohoto kruhu je tedy 180π, což znamená, že 60minutové značky musí být zobrazeny každých 3π jednotek, což je druhá hodnota v matici float v minuteMarkPaint. 12hodinové značky musí být zobrazeny každých 15π jednotek, což je hodnota v druhém float poli.

Třída PrettyAnalogClockPage nastaví časovač k zneplatnění povrchu každých 16 milisekund a obslužná rutina PaintSurface se volá podle této rychlosti. Předchozí definice SKPath objektů a SKPaint objektů umožňují velmi čistý kód výkresu:

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

        canvas.Clear();

        // Transform for 100-radius circle in center
        canvas.Translate(info.Width / 2, info.Height / 2);
        canvas.Scale(Math.Min(info.Width / 200, info.Height / 200));

        // Draw circles for hour and minute marks
        SKRect rect = new SKRect(-90, -90, 90, 90);
        canvas.DrawOval(rect, minuteMarkPaint);
        canvas.DrawOval(rect, hourMarkPaint);

        // Get time
        DateTime dateTime = DateTime.Now;

        // Draw hour hand
        canvas.Save();
        canvas.RotateDegrees(30 * dateTime.Hour + dateTime.Minute / 2f);
        canvas.DrawPath(hourHandPath, handStrokePaint);
        canvas.DrawPath(hourHandPath, handFillPaint);
        canvas.Restore();

        // Draw minute hand
        canvas.Save();
        canvas.RotateDegrees(6 * dateTime.Minute + dateTime.Second / 10f);
        canvas.DrawPath(minuteHandPath, handStrokePaint);
        canvas.DrawPath(minuteHandPath, handFillPaint);
        canvas.Restore();

        // Draw second hand
        double t = dateTime.Millisecond / 1000.0;

        if (t < 0.5)
        {
            t = 0.5 * Easing.SpringIn.Ease(t / 0.5);
        }
        else
        {
            t = 0.5 * (1 + Easing.SpringOut.Ease((t - 0.5) / 0.5));
        }

        canvas.Save();
        canvas.RotateDegrees(6 * (dateTime.Second + (float)t));
        canvas.DrawPath(secondHandPath, handStrokePaint);
        canvas.Restore();
    }
}

S druhou rukou se ale dělá něco zvláštního. Vzhledem k tomu, že se hodiny aktualizují každých 16 milisekund, Millisecond může se vlastnost DateTime hodnoty použít k animaci druhé ruky úklidu místo jedné, která se pohybuje v diskrétních přeskakování z sekundy na sekundu. Tento kód ale neumožňuje plynulý pohyb. Místo toho používá Xamarin.FormsSpringIn funkce usnadnění animace SpringOut pro jiný druh pohybu. Tyto funkce pro usnadnění způsobují, že se druhá ruka přesune tak, že se posune trochu zpátky, než se přesune, a pak mírně přestřílí cíl, což je efekt, který bohužel nejde reprodukovat na těchto statických snímcích obrazovky:

Trojitý snímek obrazovky se stránkou Pretty Analog Clock