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:
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 LineTo
a 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í null
metoda . 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:
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:
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: