Pfadeffekte in SkiaSharp
Entdecken Sie die verschiedenen Pfadeffekte, mit denen Pfade zum Streichen und Füllen verwendet werden können.
Ein Pfadeffekt ist ein instance der Klasse, die SKPathEffect
mit einer von acht statischen Erstellungsmethoden erstellt wird, die von der -Klasse definiert werden. Das SKPathEffect
-Objekt wird dann auf die PathEffect
-Eigenschaft eines SKPaint
Objekts für eine Vielzahl von interessanten Effekten festgelegt, z. B. beim Streichen einer Zeile mit einem kleinen replizierten Pfad:
Pfadeffekte ermöglichen Folgendes:
- Streichen einer Linie mit Punkten und Bindestrichen
- Strich einer Zeile mit einem beliebigen ausgefüllten Pfad
- Füllen eines Bereichs mit Schraffurlinien
- Füllen eines Bereichs mit einem kachelnden Pfad
- Abgerundete Ecken
- Hinzufügen von zufälligen "Jitter" zu Linien und Kurven
Darüber hinaus können Sie zwei oder mehr Pfadeffekte kombinieren.
In diesem Artikel wird auch veranschaulicht, wie Sie die GetFillPath
-Methode von SKPaint
verwenden, um einen Pfad in einen anderen Pfad zu konvertieren, indem Sie die Eigenschaften von SKPaint
anwenden, einschließlich StrokeWidth
und PathEffect
. Dies führt zu einigen interessanten Techniken, z. B. zum Abrufen eines Pfads, der eine Gliederung eines anderen Pfads ist. GetFillPath
ist auch im Zusammenhang mit Pfadeffekten hilfreich.
Punkte und Striche
Die Verwendung der PathEffect.CreateDash
-Methode wurde im Artikel Punkte und Bindestriche beschrieben. Das erste Argument der -Methode ist ein Array, das eine gerade Anzahl von zwei oder mehr Werten enthält, die zwischen Bindestrichlängen und Lückenlängen zwischen den Bindestrichen wechseln:
public static SKPathEffect CreateDash (Single[] intervals, Single phase)
Diese Werte sind nicht relativ zur Strichbreite. Wenn die Strichbreite beispielsweise 10 beträgt und Sie eine Linie aus quadratischen Bindestrichen und quadratischen Lücken benötigen, legen Sie das intervals
Array auf { 10, 10 } fest. Das phase
-Argument gibt an, wo innerhalb des Bindestrichmusters die Linie beginnt. Wenn die Linie in diesem Beispiel mit der quadratischen Lücke beginnen soll, legen Sie auf 10 fest phase
.
Die Enden der Bindestriche werden von der StrokeCap
-Eigenschaft von SKPaint
beeinflusst. Bei breiten Strichbreiten ist es sehr üblich, diese Eigenschaft auf festzulegen, um SKStrokeCap.Round
die Enden der Bindestriche zu runden. In diesem Fall enthalten die Werte im intervals
Array nicht die zusätzliche Länge, die sich aus der Rundung ergibt. Dies bedeutet, dass ein kreisförmiger Punkt die Angabe einer Breite von 0 (null) erfordert. Verwenden Sie für eine Strichbreite von 10 ein Array von { 0, 20 }, um eine intervals
Linie mit kreisförmigen Punkten und Lücken zwischen den Punkten desselben Durchmessers zu erstellen.
Die Seite Animierter gepunkteter Text ähnelt der seite "Umrissener Text ", die im Artikel Integrieren von Text und Grafiken beschrieben wird, da sie umrissene Textzeichen anzeigt, indem die Style
-Eigenschaft des SKPaint
-Objekts auf SKPaintStyle.Stroke
festgelegt wird. Darüber hinaus verwendet SKPathEffect.CreateDash
Animierter gepunkteter Text, um dieser Gliederung ein gepunktetes Aussehen zu verleihen, und das Programm animiert auch das phase
Argument der SKPathEffect.CreateDash
-Methode, damit die Punkte scheinbar um die Textzeichen herumzubewegen scheinen. Dies ist die Seite im Querformat:
Die AnimatedDottedTextPage
-Klasse beginnt mit der Definition einiger Konstanten und überschreibt auch die OnAppearing
Methoden und OnDisappearing
für die Animation:
public class AnimatedDottedTextPage : ContentPage
{
const string text = "DOTTED";
const float strokeWidth = 10;
static readonly float[] dashArray = { 0, 2 * strokeWidth };
SKCanvasView canvasView;
bool pageIsActive;
public AnimatedDottedTextPage()
{
Title = "Animated Dotted Text";
canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
}
protected override void OnAppearing()
{
base.OnAppearing();
pageIsActive = true;
Device.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
{
canvasView.InvalidateSurface();
return pageIsActive;
});
}
protected override void OnDisappearing()
{
base.OnDisappearing();
pageIsActive = false;
}
...
}
Der PaintSurface
Handler erstellt zunächst ein SKPaint
-Objekt zum Anzeigen des Texts. Die TextSize
-Eigenschaft wird basierend auf der Bildschirmbreite angepasst:
public class AnimatedDottedTextPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Create an SKPaint object to display the text
using (SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = strokeWidth,
StrokeCap = SKStrokeCap.Round,
Color = SKColors.Blue,
})
{
// Adjust TextSize property so text is 95% of screen width
float textWidth = textPaint.MeasureText(text);
textPaint.TextSize *= 0.95f * info.Width / textWidth;
// Find the text bounds
SKRect textBounds = new SKRect();
textPaint.MeasureText(text, ref textBounds);
// Calculate offsets to center the text on the screen
float xText = info.Width / 2 - textBounds.MidX;
float yText = info.Height / 2 - textBounds.MidY;
// Animate the phase; t is 0 to 1 every second
TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
float t = (float)(timeSpan.TotalSeconds % 1 / 1);
float phase = -t * 2 * strokeWidth;
// Create dotted line effect based on dash array and phase
using (SKPathEffect dashEffect = SKPathEffect.CreateDash(dashArray, phase))
{
// Set it to the paint object
textPaint.PathEffect = dashEffect;
// And draw the text
canvas.DrawText(text, xText, yText, textPaint);
}
}
}
}
Am Ende der -Methode wird die SKPathEffect.CreateDash
-Methode mit dem aufgerufen, das dashArray
als Feld definiert ist, und dem animierten phase
Wert. Die SKPathEffect
instance wird auf die PathEffect
-Eigenschaft des SKPaint
-Objekts festgelegt, um den Text anzuzeigen.
Alternativ können Sie das SKPathEffect
Objekt auf das SKPaint
-Objekt festlegen, bevor Sie den Text messen und auf der Seite zentrieren. In diesem Fall verursachen die animierten Punkte und Bindestriche jedoch eine gewisse Abweichung in der Größe des gerenderten Texts, und der Text neigt dazu, ein wenig zu vibrieren. (Probieren Sie es aus!)
Sie werden auch feststellen, dass, wenn die animierten Punkte um die Textzeichen kreisen, in jeder geschlossenen Kurve einen bestimmten Punkt gibt, an dem die Punkte ein- und aus dem Existieren scheinen. Hier beginnt und endet der Pfad, der die Zeichengliederung definiert. Wenn die Pfadlänge kein integrales Vielfaches der Länge des Bindestrichmusters ist (in diesem Fall 20 Pixel), kann nur ein Teil dieses Musters am Ende des Pfads passen.
Es ist möglich, die Länge des Bindestrichmusters an die Länge des Pfads anzupassen, aber dafür muss die Länge des Pfads bestimmt werden, eine Technik, die im Artikel Pfadinformationen und Enumeration behandelt wird.
Das Dot/Dash Morph-Programm animiert das Bindestrichmuster selbst so, dass Bindestriche in Punkte zu teilen scheinen, die sich zu Bindestrichen zusammenführen:
Die DotDashMorphPage
-Klasse überschreibt die OnAppearing
Methoden und OnDisappearing
wie das vorherige Programm, aber die -Klasse definiert das SKPaint
-Objekt als Feld:
public class DotDashMorphPage : ContentPage
{
const float strokeWidth = 30;
static readonly float[] dashArray = new float[4];
SKCanvasView canvasView;
bool pageIsActive = false;
SKPaint ellipsePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = strokeWidth,
StrokeCap = SKStrokeCap.Round,
Color = SKColors.Blue
};
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Create elliptical path
using (SKPath ellipsePath = new SKPath())
{
ellipsePath.AddOval(new SKRect(50, 50, info.Width - 50, info.Height - 50));
// Create animated path effect
TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
float t = (float)(timeSpan.TotalSeconds % 3 / 3);
float phase = 0;
if (t < 0.25f) // 1, 0, 1, 2 --> 0, 2, 0, 2
{
float tsub = 4 * t;
dashArray[0] = strokeWidth * (1 - tsub);
dashArray[1] = strokeWidth * 2 * tsub;
dashArray[2] = strokeWidth * (1 - tsub);
dashArray[3] = strokeWidth * 2;
}
else if (t < 0.5f) // 0, 2, 0, 2 --> 1, 2, 1, 0
{
float tsub = 4 * (t - 0.25f);
dashArray[0] = strokeWidth * tsub;
dashArray[1] = strokeWidth * 2;
dashArray[2] = strokeWidth * tsub;
dashArray[3] = strokeWidth * 2 * (1 - tsub);
phase = strokeWidth * tsub;
}
else if (t < 0.75f) // 1, 2, 1, 0 --> 0, 2, 0, 2
{
float tsub = 4 * (t - 0.5f);
dashArray[0] = strokeWidth * (1 - tsub);
dashArray[1] = strokeWidth * 2;
dashArray[2] = strokeWidth * (1 - tsub);
dashArray[3] = strokeWidth * 2 * tsub;
phase = strokeWidth * (1 - tsub);
}
else // 0, 2, 0, 2 --> 1, 0, 1, 2
{
float tsub = 4 * (t - 0.75f);
dashArray[0] = strokeWidth * tsub;
dashArray[1] = strokeWidth * 2 * (1 - tsub);
dashArray[2] = strokeWidth * tsub;
dashArray[3] = strokeWidth * 2;
}
using (SKPathEffect pathEffect = SKPathEffect.CreateDash(dashArray, phase))
{
ellipsePaint.PathEffect = pathEffect;
canvas.DrawPath(ellipsePath, ellipsePaint);
}
}
}
}
Der PaintSurface
Handler erstellt einen elliptischen Pfad basierend auf der Größe der Seite und führt einen langen Codeabschnitt aus, der die dashArray
Variablen und phase
festlegt. Da die animierte Variable t
von 0 bis 1 reicht, teilen sich die if
Blöcke diese Zeit in vier Quartale auf und liegen in jedem dieser Quartale tsub
ebenfalls zwischen 0 und 1. Ganz am Ende erstellt das Programm die SKPathEffect
und legt es auf das SKPaint
Objekt zum Zeichnen fest.
Von Pfad zu Pfad
Die GetFillPath
-Methode von SKPaint
wandelt einen Pfad basierend auf den Einstellungen im -Objekt in SKPaint
einen anderen um. Um zu sehen, wie dies funktioniert, ersetzen Sie den canvas.DrawPath
Aufruf im vorherigen Programm durch den folgenden Code:
SKPath newPath = new SKPath();
bool fill = ellipsePaint.GetFillPath(ellipsePath, newPath);
SKPaint newPaint = new SKPaint
{
Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);
In diesem neuen Code konvertiert der GetFillPath
-Aufruf (bei dem ellipsePath
es sich nur um ein Oval handelt) in newPath
, das dann mit newPaint
angezeigt wird. Das newPaint
Objekt wird mit allen Standardeigenschafteneinstellungen erstellt, mit der Ausnahme, dass die Style
Eigenschaft basierend auf dem booleschen Rückgabewert von GetFillPath
festgelegt wird.
Die visuellen Elemente sind identisch, mit Ausnahme der Farbe, die in ellipsePaint
festgelegt ist, aber nicht newPaint
. Anstelle der in definierten ellipsePath
newPath
einfachen Ellipse enthält zahlreiche Pfadkonturen, die die Reihe von Punkten und Bindestrichen definieren. Dies ist das Ergebnis, wenn verschiedene Eigenschaften von ellipsePaint
(insbesondere StrokeWidth
, StrokeCap
und PathEffect
) auf ellipsePath
angewendet und der resultierende Pfad in newPath
festgelegt wird. Die GetFillPath
-Methode gibt einen booleschen Wert zurück, der angibt, ob der Zielpfad ausgefüllt werden soll. In diesem Beispiel ist true
der Rückgabewert für das Füllen des Pfads.
Versuchen Sie, die Style
Einstellung in newPaint
zu SKPaintStyle.Stroke
ändern, und Sie sehen die einzelnen Pfadkonturen, die mit einer Linie mit einer Breite von einem Pixel umrissen sind.
Streicheln mit einem Pfad
Die SKPathEffect.Create1DPath
-Methode ähnelt vom Konzept her mit der SKPathEffect.CreateDash
Ausnahme, dass Sie einen Pfad anstelle eines Musters von Bindestrichen und Lücken angeben. Dieser Pfad wird mehrmals repliziert, um die Linie oder Kurve zu streichen.
Die Syntax lautet:
public static SKPathEffect Create1DPath (SKPath path, Single advance,
Single phase, SKPath1DPathEffectStyle style)
Im Allgemeinen ist der Pfad, zu Create1DPath
dem Sie gehen, klein und um den Punkt (0, 0) zentriert. Der advance
Parameter gibt den Abstand zwischen den Mittelpunkten des Pfads an, während der Pfad in der Zeile repliziert wird. In der Regel legen Sie dieses Argument auf die ungefähre Breite des Pfads fest. Das phase
Argument spielt hier die gleiche Rolle wie in der CreateDash
-Methode.
Die SKPath1DPathEffectStyle
hat drei Member:
Translate
Rotate
Morph
Das Translate
Element bewirkt, dass der Pfad in derselben Ausrichtung verbleibt, wie er entlang einer Linie oder Kurve repliziert wird. Für Rotate
wird der Pfad basierend auf einem Tangens zur Kurve gedreht. Der Pfad hat seine normale Ausrichtung für horizontale Linien. Morph
ist ähnlich, Rotate
außer dass der Pfad selbst ebenfalls gekrümmt ist, um der Krümmung der zu streichelnden Linie zu entsprechen.
Auf der Seite 1D-Pfadeffekt werden diese drei Optionen veranschaulicht. Die Datei OneDimensionalPathEffectPage.xaml definiert eine Auswahl, die drei Elemente enthält, die den drei Membern der Enumeration entsprechen:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharpFormsDemos.Curves.OneDimensionalPathEffectPage"
Title="1D Path Effect">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Picker x:Name="effectStylePicker"
Title="Effect Style"
Grid.Row="0"
SelectedIndexChanged="OnPickerSelectedIndexChanged">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>Translate</x:String>
<x:String>Rotate</x:String>
<x:String>Morph</x:String>
</x:Array>
</Picker.ItemsSource>
<Picker.SelectedIndex>
0
</Picker.SelectedIndex>
</Picker>
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface"
Grid.Row="1" />
</Grid>
</ContentPage>
Die CodeBehind-Datei OneDimensionalPathEffectPage.xaml.cs definiert drei SKPathEffect
Objekte als Felder. Diese werden alle mit SKPathEffect.Create1DPath
SKPath
mit -Objekten erstellt, die mit SKPath.ParseSvgPathData
erstellt wurden. Die erste ist eine einfache Box, die zweite ist eine Rautenform, und das dritte ist ein Rechteck. Diese werden verwendet, um die drei Effektstile zu veranschaulichen:
public partial class OneDimensionalPathEffectPage : ContentPage
{
SKPathEffect translatePathEffect =
SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 -10 L 10 -10, 10 10, -10 10 Z"),
24, 0, SKPath1DPathEffectStyle.Translate);
SKPathEffect rotatePathEffect =
SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 0 L 0 -10, 10 0, 0 10 Z"),
20, 0, SKPath1DPathEffectStyle.Rotate);
SKPathEffect morphPathEffect =
SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -25 -10 L 25 -10, 25 10, -25 10 Z"),
55, 0, SKPath1DPathEffectStyle.Morph);
SKPaint pathPaint = new SKPaint
{
Color = SKColors.Blue
};
public OneDimensionalPathEffectPage()
{
InitializeComponent();
}
void OnPickerSelectedIndexChanged(object sender, EventArgs args)
{
if (canvasView != null)
{
canvasView.InvalidateSurface();
}
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPath path = new SKPath())
{
path.MoveTo(new SKPoint(0, 0));
path.CubicTo(new SKPoint(2 * info.Width, info.Height),
new SKPoint(-info.Width, info.Height),
new SKPoint(info.Width, 0));
switch ((string)effectStylePicker.SelectedItem))
{
case "Translate":
pathPaint.PathEffect = translatePathEffect;
break;
case "Rotate":
pathPaint.PathEffect = rotatePathEffect;
break;
case "Morph":
pathPaint.PathEffect = morphPathEffect;
break;
}
canvas.DrawPath(path, pathPaint);
}
}
}
Der PaintSurface
Handler erstellt eine Bézierkurve, die sich um sich selbst dreht, und greift auf die Auswahl zu, um zu bestimmen, welche PathEffect
zum Streichen verwendet werden soll. Die drei Optionen – Translate
, Rotate
und Morph
– werden von links nach rechts angezeigt:
Der in der SKPathEffect.Create1DPath
-Methode angegebene Pfad wird immer ausgefüllt. Der in der DrawPath
-Methode angegebene Pfad wird immer gestrichelt, wenn die -Eigenschaft des SKPaint
Objekts auf einen 1D-Pfadeffekt festgelegt ist PathEffect
. Beachten Sie, dass das pathPaint
-Objekt keine Style
Einstellung aufweist, die normalerweise standardmäßig auf Fill
festgelegt ist, aber der Pfad wird unabhängig davon strichen.
Das im Translate
Beispiel verwendete Feld ist 20 Pixel quadratisch, und das advance
Argument ist auf 24 festgelegt. Dieser Unterschied verursacht eine Lücke zwischen den Feldern, wenn die Linie ungefähr horizontal oder vertikal ist, die Felder sich jedoch etwas überlappen, wenn die Linie diagonal ist, da die Diagonale des Felds 28,3 Pixel beträgt.
Die Rautenform im Rotate
Beispiel ist ebenfalls 20 Pixel breit. Ist advance
auf 20 festgelegt, sodass die Punkte weiterhin berühren, während der Diamant zusammen mit der Krümmung der Linie gedreht wird.
Die Rechteckform im Morph
Beispiel ist 50 Pixel breit mit einer advance
Einstellung von 55, um eine kleine Lücke zwischen den Rechtecken zu bilden, während sie um die Bézierkurve gebogen sind.
Wenn das advance
Argument kleiner als die Größe des Pfads ist, können sich die replizierten Pfade überlappen. Dies kann zu einigen interessanten Effekten führen. Auf der Seite Verknüpfte Kette wird eine Reihe überlappender Kreise angezeigt, die einer verknüpften Kette ähneln, die in der charakteristischen Form einer Oberleitung hängt:
Schauen Sie genau hin, und Sie werden sehen, dass es sich nicht um Kreise handelt. Jedes Glied in der Kette besteht aus zwei Bögen, die so groß und positioniert sind, dass sie mit angrenzenden Gliedern verbunden zu sein scheinen.
Eine Kette oder ein Kabel mit einheitlicher Gewichtsverteilung hängt in Form einer Oberleitung. Ein Bogen, der in Form eines invertierten Oberleitungsbaus gebaut wurde, profitiert von einer gleichmäßigen Verteilung des Drucks vom Gewicht eines Bogens. Die Oberleitung hat eine scheinbar einfache mathematische Beschreibung:
y = a · cosh(x / a)
Der Cosh ist die hyperbolische Kosinusfunktion. Für x gleich 0 ist cosh null und y gleich a. Das ist die Mitte der Oberleitung. Wie die Kosinusfunktion soll coshgleichmäßig sein, was bedeutet, dass cosh(–x) gleich cosh(x) ist und die Werte für zunehmende positive oder negative Argumente steigen. Diese Werte beschreiben die Kurven, die die Seiten der Oberleitung bilden.
Die Suche nach dem richtigen Wert von einer , um die Oberleitung an die Abmessungen der Seite des Telefons anzupassen, ist keine direkte Berechnung. Wenn w und h die Breite und Höhe eines Rechtecks sind, erfüllt der optimale Wert eines die folgende Gleichung:
cosh(w / 2 / a) = 1 + h / a
Die folgende Methode in der LinkedChainPage
-Klasse schließt diese Gleichheit ein, indem sie auf die beiden Ausdrücke links und rechts des Gleichheitszeichens als left
und right
verweist. Für kleine Werte von ist größerleft
als right
; für große Werte ist einleft
kleiner als right
. Die while
Schleife verengt sich auf einen optimalen Wert von:
float FindOptimumA(float width, float height)
{
Func<float, float> left = (float a) => (float)Math.Cosh(width / 2 / a);
Func<float, float> right = (float a) => 1 + height / a;
float gtA = 1; // starting value for left > right
float ltA = 10000; // starting value for left < right
while (Math.Abs(gtA - ltA) > 0.1f)
{
float avgA = (gtA + ltA) / 2;
if (left(avgA) < right(avgA))
{
ltA = avgA;
}
else
{
gtA = avgA;
}
}
return (gtA + ltA) / 2;
}
Das SKPath
Objekt für die Links wird im Konstruktor der Klasse erstellt, und das resultierende SKPathEffect
Objekt wird dann auf die PathEffect
Eigenschaft des Objekts festgelegt, das SKPaint
als Feld gespeichert ist:
public class LinkedChainPage : ContentPage
{
const float linkRadius = 30;
const float linkThickness = 5;
Func<float, float, float> catenary = (float a, float x) => (float)(a * Math.Cosh(x / a));
SKPaint linksPaint = new SKPaint
{
Color = SKColors.Silver
};
public LinkedChainPage()
{
Title = "Linked Chain";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
// Create the path for the individual links
SKRect outer = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
SKRect inner = outer;
inner.Inflate(-linkThickness, -linkThickness);
using (SKPath linkPath = new SKPath())
{
linkPath.AddArc(outer, 55, 160);
linkPath.ArcTo(inner, 215, -160, false);
linkPath.Close();
linkPath.AddArc(outer, 235, 160);
linkPath.ArcTo(inner, 395, -160, false);
linkPath.Close();
// Set that path as the 1D path effect for linksPaint
linksPaint.PathEffect =
SKPathEffect.Create1DPath(linkPath, 1.3f * linkRadius, 0,
SKPath1DPathEffectStyle.Rotate);
}
}
...
}
Der Standard Auftrag des PaintSurface
Handlers besteht darin, einen Pfad für die Oberleitung selbst zu erstellen. Nachdem Sie das optimale a ermittelt und in der optA
Variablen gespeichert haben, muss auch ein Offset von oben im Fenster berechnet werden. Anschließend kann eine Auflistung von SKPoint
Werten für die Oberleitung gesammelt, in einen Pfad umgewandelt und der Pfad mit dem zuvor erstellten Objekt gezeichnet werden SKPaint
:
public class LinkedChainPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.Black);
// Width and height of catenary
int width = info.Width;
float height = info.Height - linkRadius;
// Find the optimum 'a' for this width and height
float optA = FindOptimumA(width, height);
// Calculate the vertical offset for that value of 'a'
float yOffset = catenary(optA, -width / 2);
// Create a path for the catenary
SKPoint[] points = new SKPoint[width];
for (int x = 0; x < width; x++)
{
points[x] = new SKPoint(x, yOffset - catenary(optA, x - width / 2));
}
using (SKPath path = new SKPath())
{
path.AddPoly(points, false);
// And render that path with the linksPaint object
canvas.DrawPath(path, linksPaint);
}
}
...
}
Dieses Programm definiert den Pfad, der in Create1DPath
verwendet wird, um seinen Punkt (0, 0) in der Mitte zu haben. Dies erscheint sinnvoll, da der Punkt (0, 0) des Pfads an der Linie oder Kurve ausgerichtet ist, die er schmückt. Sie können jedoch einen nicht zentrierten Punkt (0, 0) für einige Spezialeffekte verwenden.
Die Seite "Förderband " erstellt einen Pfad, der einem länglichen Förderband mit einer gekrümmten Ober- und Unterseite ähnelt, die den Abmessungen des Fensters entspricht. Dieser Pfad wird mit einem einfachen SKPaint
Objekt mit einer Breite von 20 Pixeln und grauer Farbe strichen und dann erneut mit einem anderen SKPaint
Objekt mit einem SKPathEffect
Objekt gestrichelt, das auf einen Pfad verweist, der einem kleinen Bucket ähnelt:
Der Punkt (0, 0) des Löffelpfads ist der Griff. Wenn das phase
Argument also animiert ist, scheinen sich die Eimer um das Förderband zu drehen, vielleicht schöpfen sie Wasser unten auf und lassen es oben aus.
Die ConveyorBeltPage
-Klasse implementiert eine Animation mit Außerkraftsetzungen der OnAppearing
Methoden und OnDisappearing
. Der Pfad für den Bucket wird im Konstruktor der Seite definiert:
public class ConveyorBeltPage : ContentPage
{
SKCanvasView canvasView;
bool pageIsActive = false;
SKPaint conveyerPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = 20,
Color = SKColors.DarkGray
};
SKPath bucketPath = new SKPath();
SKPaint bucketsPaint = new SKPaint
{
Color = SKColors.BurlyWood,
};
public ConveyorBeltPage()
{
Title = "Conveyor Belt";
canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
// Create the path for the bucket starting with the handle
bucketPath.AddRect(new SKRect(-5, -3, 25, 3));
// Sides
bucketPath.AddRoundedRect(new SKRect(25, -19, 27, 18), 10, 10,
SKPathDirection.CounterClockwise);
bucketPath.AddRoundedRect(new SKRect(63, -19, 65, 18), 10, 10,
SKPathDirection.CounterClockwise);
// Five slats
for (int i = 0; i < 5; i++)
{
bucketPath.MoveTo(25, -19 + 8 * i);
bucketPath.LineTo(25, -13 + 8 * i);
bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
SKPathDirection.CounterClockwise, 65, -13 + 8 * i);
bucketPath.LineTo(65, -19 + 8 * i);
bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
SKPathDirection.Clockwise, 25, -19 + 8 * i);
bucketPath.Close();
}
// Arc to suggest the hidden side
bucketPath.MoveTo(25, -17);
bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
SKPathDirection.Clockwise, 65, -17);
bucketPath.LineTo(65, -19);
bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
SKPathDirection.CounterClockwise, 25, -19);
bucketPath.Close();
// Make it a little bigger and correct the orientation
bucketPath.Transform(SKMatrix.MakeScale(-2, 2));
bucketPath.Transform(SKMatrix.MakeRotationDegrees(90));
}
...
Der Bucketerstellungscode wird mit zwei Transformationen abgeschlossen, die den Bucket etwas größer machen und ihn seitlich drehen. Das Anwenden dieser Transformationen war einfacher als das Anpassen aller Koordinaten im vorherigen Code.
Der PaintSurface
Handler definiert zunächst einen Pfad für das Förderband selbst. Dies ist einfach ein Linienpaar und ein Paar von Halbkreisen, die mit einer 20 Pixel breiten dunkelgrauen Linie gezeichnet werden:
public class ConveyorBeltPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float width = info.Width / 3;
float verticalMargin = width / 2 + 150;
using (SKPath conveyerPath = new SKPath())
{
// Straight verticals capped by semicircles on top and bottom
conveyerPath.MoveTo(width, verticalMargin);
conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
SKPathDirection.Clockwise, 2 * width, verticalMargin);
conveyerPath.LineTo(2 * width, info.Height - verticalMargin);
conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
SKPathDirection.Clockwise, width, info.Height - verticalMargin);
conveyerPath.Close();
// Draw the conveyor belt itself
canvas.DrawPath(conveyerPath, conveyerPaint);
// Calculate spacing based on length of conveyer path
float length = 2 * (info.Height - 2 * verticalMargin) +
2 * ((float)Math.PI * width / 2);
// Value will be somewhere around 200
float spacing = length / (float)Math.Round(length / 200);
// Now animate the phase; t is 0 to 1 every 2 seconds
TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
float t = (float)(timeSpan.TotalSeconds % 2 / 2);
float phase = -t * spacing;
// Create the buckets PathEffect
using (SKPathEffect bucketsPathEffect =
SKPathEffect.Create1DPath(bucketPath, spacing, phase,
SKPath1DPathEffectStyle.Rotate))
{
// Set it to the Paint object and draw the path again
bucketsPaint.PathEffect = bucketsPathEffect;
canvas.DrawPath(conveyerPath, bucketsPaint);
}
}
}
}
Die Logik zum Zeichnen des Förderbands funktioniert nicht im Querformatmodus.
Die Löffel sollten etwa 200 Pixel voneinander entfernt auf dem Förderband angeordnet sein. Das Förderband ist jedoch wahrscheinlich kein Vielfaches von 200 Pixel lang, was bedeutet, dass, wenn das phase
Argument von SKPathEffect.Create1DPath
animiert ist, Buckets in die Existenz und aus der Existenz kommen.
Aus diesem Grund berechnet das Programm zunächst einen Wert namens length
der Länge des Förderbands. Da das Förderband aus geraden Linien und Halbkreisen besteht, ist dies eine einfache Berechnung. Als Nächstes wird die Anzahl der Buckets berechnet, indem durch 200 dividiert length
wird. Dies wird auf die nächste ganze Zahl gerundet, und diese Zahl wird dann in length
unterteilt. Das Ergebnis ist ein Abstand für eine integrale Anzahl von Buckets. Das phase
Argument ist nur ein Bruchteil davon.
Von Pfad zu Pfad erneut
Kommentieren Sie am unteren Rand des DrawSurface
Handlers in Conveyor Belt den canvas.DrawPath
Aufruf aus, und ersetzen Sie ihn durch den folgenden Code:
SKPath newPath = new SKPath();
bool fill = bucketsPaint.GetFillPath(conveyerPath, newPath);
SKPaint newPaint = new SKPaint
{
Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);
Wie im vorherigen Beispiel von GetFillPath
sehen Sie, dass die Ergebnisse mit Ausnahme der Farbe identisch sind. Nach dem Ausführen GetFillPath
enthält das newPath
Objekt mehrere Kopien des Bucketpfads, die jeweils an der gleichen Stelle positioniert sind, an der die Animation sie zum Zeitpunkt des Aufrufs positioniert hat.
Schraffur eines Bereichs
Die SKPathEffect.Create2DLines
-Methode füllt einen Bereich mit parallelen Linien, die häufig als Schraffurlinien bezeichnet werden. Die Methode weist die folgende Syntax auf:
public static SKPathEffect Create2DLine (Single width, SKMatrix matrix)
Das width
Argument gibt die Strichbreite der Schraffurlinien an. Der matrix
Parameter ist eine Kombination aus Skalierung und optionaler Drehung. Der Skalierungsfaktor gibt den Pixelinkrement an, den Skia verwendet, um die Schraffurlinien zwischenzuräumen. Die Trennung zwischen den Zeilen ist der Skalierungsfaktor minus das width
Argument. Wenn der Skalierungsfaktor kleiner oder gleich dem width
Wert ist, gibt es keinen Leerraum zwischen den Schraffurlinien, und der Bereich scheint gefüllt zu sein. Geben Sie denselben Wert für die horizontale und vertikale Skalierung an.
Schraffurlinien sind standardmäßig horizontal. Wenn der matrix
Parameter Drehung enthält, werden die Schraffurlinien im Uhrzeigersinn gedreht.
Auf der Seite Hatch Fill wird dieser Pfadeffekt veranschaulicht. Die HatchFillPage
Klasse definiert drei Pfadeffekte als Felder, den ersten für horizontale Schraffurlinien mit einer Breite von 3 Pixeln mit einem Skalierungsfaktor, der angibt, dass sie 6 Pixel voneinander getrennt sind. Die Trennung zwischen den Linien beträgt daher drei Pixel. Der zweite Pfadeffekt ist für vertikale Schraffurlinien mit einer Breite von sechs Pixeln im Abstand von 24 Pixeln (sodass die Trennung 18 Pixel beträgt), und der dritte für diagonale Schraffurlinien mit einer Breite von 12 Pixeln mit einem Abstand von 36 Pixeln.
public class HatchFillPage : ContentPage
{
SKPaint fillPaint = new SKPaint();
SKPathEffect horzLinesPath = SKPathEffect.Create2DLine(3, SKMatrix.MakeScale(6, 6));
SKPathEffect vertLinesPath = SKPathEffect.Create2DLine(6,
Multiply(SKMatrix.MakeRotationDegrees(90), SKMatrix.MakeScale(24, 24)));
SKPathEffect diagLinesPath = SKPathEffect.Create2DLine(12,
Multiply(SKMatrix.MakeScale(36, 36), SKMatrix.MakeRotationDegrees(45)));
SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = 3,
Color = SKColors.Black
};
...
static SKMatrix Multiply(SKMatrix first, SKMatrix second)
{
SKMatrix target = SKMatrix.MakeIdentity();
SKMatrix.Concat(ref target, first, second);
return target;
}
}
Beachten Sie die Matrixmethode Multiply
. Da die horizontalen und vertikalen Skalierungsfaktoren identisch sind, spielt die Reihenfolge, in der die Skalierungs- und Rotationsmatrizen multipliziert werden, keine Rolle.
Der PaintSurface
Handler verwendet diese drei Pfadeffekte mit drei verschiedenen Farben in Kombination mit fillPaint
, um ein abgerundetes Rechteck zu füllen, um die Seite anzupassen. Die Style
auf festgelegte fillPaint
Eigenschaft wird ignoriert. Wenn das SKPaint
Objekt einen Pfadeffekt enthält, der aus erstellt wurde, wird der Bereich unabhängig davon SKPathEffect.Create2DLine
gefüllt:
public class HatchFillPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPath roundRectPath = new SKPath())
{
// Create a path
roundRectPath.AddRoundedRect(
new SKRect(50, 50, info.Width - 50, info.Height - 50), 100, 100);
// Horizontal hatch marks
fillPaint.PathEffect = horzLinesPath;
fillPaint.Color = SKColors.Red;
canvas.DrawPath(roundRectPath, fillPaint);
// Vertical hatch marks
fillPaint.PathEffect = vertLinesPath;
fillPaint.Color = SKColors.Blue;
canvas.DrawPath(roundRectPath, fillPaint);
// Diagonal hatch marks -- use clipping
fillPaint.PathEffect = diagLinesPath;
fillPaint.Color = SKColors.Green;
canvas.Save();
canvas.ClipPath(roundRectPath);
canvas.DrawRect(new SKRect(0, 0, info.Width, info.Height), fillPaint);
canvas.Restore();
// Outline the path
canvas.DrawPath(roundRectPath, strokePaint);
}
}
...
}
Wenn Sie sich die Ergebnisse genau ansehen, werden Sie feststellen, dass die roten und blauen Schraffurlinien nicht genau auf das abgerundete Rechteck beschränkt sind. (Dies ist anscheinend ein Merkmal des zugrunde liegenden Skia-Codes.) Wenn dies nicht zufriedenstellend ist, wird ein alternativer Ansatz für die diagonalen Schraffurlinien in Grün angezeigt: Das abgerundete Rechteck wird als Abschneidepfad verwendet, und die Schraffurlinien werden auf der gesamten Seite gezeichnet.
Der PaintSurface
Handler schließt mit einem Aufruf ab, einfach das abgerundete Rechteck zu streichen, sodass Sie die Diskrepanz mit den roten und blauen Schraffurlinien sehen können:
Der Android-Bildschirm sieht nicht wirklich so aus: Die Skalierung des Screenshots hat dazu geführt, dass sich die dünnen roten Linien und dünnen Leerzeichen in scheinbar breitere rote Linien und breitere Leerzeichen konsolidieren.
Füllen mit einem Pfad
Mit SKPathEffect.Create2DPath
können Sie einen Bereich mit einem Pfad füllen, der horizontal und vertikal repliziert wird, im Effekt, dass der Bereich kachelt:
public static SKPathEffect Create2DPath (SKMatrix matrix, SKPath path)
Die SKMatrix
Skalierungsfaktoren geben den horizontalen und vertikalen Abstand des replizierten Pfads an. Sie können den Pfad jedoch nicht mit diesem matrix
Argument rotieren. Wenn Der Pfad gedreht werden soll, drehen Sie den Pfad selbst mithilfe der Transform
von definierten SKPath
Methode.
Der replizierte Pfad wird normalerweise am linken und oberen Rand des Bildschirms ausgerichtet und nicht am gefüllten Bereich. Sie können dieses Verhalten überschreiben, indem Sie Übersetzungsfaktoren zwischen 0 und den Skalierungsfaktoren bereitstellen, um horizontale und vertikale Offsets von der linken und oberen Seite anzugeben.
Auf der Seite "Pfadkachelfüllung" wird dieser Pfadeffekt veranschaulicht. Der Pfad, der zum Kacheln des Bereichs verwendet wird, wird als Feld in der PathTileFillPage
-Klasse definiert. Die horizontalen und vertikalen Koordinaten reichen von –40 bis 40, was bedeutet, dass dieser Pfad 80 Pixel quadratisch ist:
public class PathTileFillPage : ContentPage
{
SKPath tilePath = SKPath.ParseSvgPathData(
"M -20 -20 L 2 -20, 2 -40, 18 -40, 18 -20, 40 -20, " +
"40 -12, 20 -12, 20 12, 40 12, 40 40, 22 40, 22 20, " +
"-2 20, -2 40, -20 40, -20 8, -40 8, -40 -8, -20 -8 Z");
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPaint paint = new SKPaint())
{
paint.Color = SKColors.Red;
using (SKPathEffect pathEffect =
SKPathEffect.Create2DPath(SKMatrix.MakeScale(64, 64), tilePath))
{
paint.PathEffect = pathEffect;
canvas.DrawRoundRect(
new SKRect(50, 50, info.Width - 50, info.Height - 50),
100, 100, paint);
}
}
}
}
PaintSurface
Im Handler legen die SKPathEffect.Create2DPath
Aufrufe den horizontalen und vertikalen Abstand auf 64 fest, damit sich die quadratischen Kacheln mit 80 Pixeln überlappen. Glücklicherweise ähnelt der Weg einem Puzzleteil, das sich gut mit angrenzenden Kacheln einflechtet:
Die Skalierung aus dem ursprünglichen Screenshot verursacht einige Verzerrungen, insbesondere auf dem Android-Bildschirm.
Beachten Sie, dass diese Kacheln immer ganz aussehen und nie abgeschnitten werden. Auf den ersten beiden Screenshots ist nicht einmal ersichtlich, dass es sich bei dem gefüllten Bereich um ein abgerundetes Rechteck handelt. Wenn Sie diese Kacheln auf einen bestimmten Bereich abschneiden möchten, verwenden Sie einen Beschneidungspfad.
Versuchen Sie, die Style
-Eigenschaft des SKPaint
-Objekts auf festzulegen Stroke
, und Sie sehen, dass die einzelnen Kacheln umrissen statt gefüllt sind.
Es ist auch möglich, einen Bereich mit einer gekachelten Bitmap zu füllen, wie im Artikel SkiaSharp Bitmap tiling gezeigt.
Scharfe Ecken runden
Das im Artikel Three Ways to Draw an Arcvorgestellte Programm "Gerundete Heptagon" verwendete einen Tangentenbogen, um die Punkte einer siebenseitigen Figur zu kurven. Die Seite Another Rounded Heptagon zeigt einen viel einfacheren Ansatz, der einen Pfadeffekt verwendet, der aus der SKPathEffect.CreateCorner
-Methode erstellt wurde:
public static SKPathEffect CreateCorner (Single radius)
Obwohl das einzelne Argument den Namen hat radius
, müssen Sie es auf die Hälfte des gewünschten Eckradius festlegen. (Dies ist ein Merkmal des zugrunde liegenden Skia-Codes.)
Hier sehen Sie den PaintSurface
Handler in der AnotherRoundedHeptagonPage
-Klasse:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
int numVertices = 7;
float radius = 0.45f * Math.Min(info.Width, info.Height);
SKPoint[] vertices = new SKPoint[numVertices];
double vertexAngle = -0.5f * Math.PI; // straight up
// Coordinates of the vertices of the polygon
for (int vertex = 0; vertex < numVertices; vertex++)
{
vertices[vertex] = new SKPoint(radius * (float)Math.Cos(vertexAngle),
radius * (float)Math.Sin(vertexAngle));
vertexAngle += 2 * Math.PI / numVertices;
}
float cornerRadius = 100;
// Create the path
using (SKPath path = new SKPath())
{
path.AddPoly(vertices, true);
// Render the path in the center of the screen
using (SKPaint paint = new SKPaint())
{
paint.Style = SKPaintStyle.Stroke;
paint.Color = SKColors.Blue;
paint.StrokeWidth = 10;
// Set argument to half the desired corner radius!
paint.PathEffect = SKPathEffect.CreateCorner(cornerRadius / 2);
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.DrawPath(path, paint);
// Uncomment DrawCircle call to verify corner radius
float offset = cornerRadius / (float)Math.Sin(Math.PI * (numVertices - 2) / numVertices / 2);
paint.Color = SKColors.Green;
// canvas.DrawCircle(vertices[0].X, vertices[0].Y + offset, cornerRadius, paint);
}
}
}
Sie können diesen Effekt entweder beim Streichen oder Füllen basierend auf der Style
-Eigenschaft des SKPaint
Objekts verwenden. Hier wird es ausgeführt:
Sie sehen, dass dieser abgerundete Heptagon mit dem vorherigen Programm identisch ist. Wenn Sie überzeugender benötigen, dass der Eckradius wirklich 100 ist und nicht die SKPathEffect.CreateCorner
im Aufruf angegebenen 50, können Sie die Auskommentierung der letzten Anweisung im Programm aufheben und einen 100-Radius-Kreis über der Ecke sehen.
Zufälliger Jitter
Manchmal sind die makellosen geraden Linien der Computergrafik nicht ganz das, was Sie wollen, und ein wenig Zufälligkeit ist erwünscht. In diesem Fall sollten Sie die SKPathEffect.CreateDiscrete
Methode ausprobieren:
public static SKPathEffect CreateDiscrete (Single segLength, Single deviation, UInt32 seedAssist)
Sie können diesen Pfadeffekt entweder zum Streichen oder Füllen verwenden. Linien werden in verbundene Segmente unterteilt – deren ungefähre Länge durch segLength
angegeben wird – und erstrecken sich in verschiedene Richtungen. Der Umfang der Abweichung von der ursprünglichen Zeile wird durch deviation
angegeben.
Das letzte Argument ist ein Seed, mit dem die pseudo-zufällige Sequenz generiert wird, die für den Effekt verwendet wird. Der Jitter-Effekt sieht bei verschiedenen Samen etwas anders aus. Das Argument hat den Standardwert 0. Dies bedeutet, dass der Effekt immer gleich ist, wenn Sie das Programm ausführen. Wenn Sie einen anderen Jitter wünschen, wenn der Bildschirm neu gestrichen wird, können Sie den Seed auf die Millisecond
Eigenschaft eines DataTime.Now
Werts festlegen (z. B. ).
Auf der Seite Jitter-Experiment können Sie mit verschiedenen Werten beim Streichen eines Rechtecks experimentieren:
Das Programm ist einfach. Die Datei JitterExperimentPage.xaml instanziiert zwei Slider
Elemente und ein SKCanvasView
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharpFormsDemos.Curves.JitterExperimentPage"
Title="Jitter Experiment">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.Resources>
<ResourceDictionary>
<Style TargetType="Label">
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="Slider">
<Setter Property="Margin" Value="20, 0" />
<Setter Property="Minimum" Value="0" />
<Setter Property="Maximum" Value="100" />
</Style>
</ResourceDictionary>
</Grid.Resources>
<Slider x:Name="segLengthSlider"
Grid.Row="0"
ValueChanged="sliderValueChanged" />
<Label Text="{Binding Source={x:Reference segLengthSlider},
Path=Value,
StringFormat='Segment Length = {0:F0}'}"
Grid.Row="1" />
<Slider x:Name="deviationSlider"
Grid.Row="2"
ValueChanged="sliderValueChanged" />
<Label Text="{Binding Source={x:Reference deviationSlider},
Path=Value,
StringFormat='Deviation = {0:F0}'}"
Grid.Row="3" />
<skia:SKCanvasView x:Name="canvasView"
Grid.Row="4"
PaintSurface="OnCanvasViewPaintSurface" />
</Grid>
</ContentPage>
Der PaintSurface
Handler in der CodeBehind-Datei JitterExperimentPage.xaml.cs wird aufgerufen, wenn sich ein Slider
Wert ändert. Es ruft SKPathEffect.CreateDiscrete
mit den beiden Slider
Werten auf und verwendet diese, um ein Rechteck zu streichen:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float segLength = (float)segLengthSlider.Value;
float deviation = (float)deviationSlider.Value;
using (SKPaint paint = new SKPaint())
{
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 5;
paint.Color = SKColors.Blue;
using (SKPathEffect pathEffect = SKPathEffect.CreateDiscrete(segLength, deviation))
{
paint.PathEffect = pathEffect;
SKRect rect = new SKRect(100, 100, info.Width - 100, info.Height - 100);
canvas.DrawRect(rect, paint);
}
}
}
Sie können diesen Effekt auch zum Füllen verwenden. In diesem Fall unterliegt der Umriss des gefüllten Bereichs diesen zufälligen Abweichungen. Die Seite Jitter Text veranschaulicht die Verwendung dieses Pfadeffekts zum Anzeigen von Text. Der größte Teil des Codes im PaintSurface
Handler der JitterTextPage
-Klasse ist der Größen- und Zentrierung des Texts gewidmet:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
string text = "FUZZY";
using (SKPaint textPaint = new SKPaint())
{
textPaint.Color = SKColors.Purple;
textPaint.PathEffect = SKPathEffect.CreateDiscrete(3f, 10f);
// Adjust TextSize property so text is 95% of screen width
float textWidth = textPaint.MeasureText(text);
textPaint.TextSize *= 0.95f * info.Width / textWidth;
// Find the text bounds
SKRect textBounds = new SKRect();
textPaint.MeasureText(text, ref textBounds);
// Calculate offsets to center the text on the screen
float xText = info.Width / 2 - textBounds.MidX;
float yText = info.Height / 2 - textBounds.MidY;
canvas.DrawText(text, xText, yText, textPaint);
}
}
Hier wird es im Querformatmodus ausgeführt:
Pfadgliederung
Sie haben bereits zwei kleine Beispiele für die GetFillPath
Methode von SKPaint
gesehen, die zwei Versionen enthält:
public Boolean GetFillPath (SKPath src, SKPath dst, Single resScale = 1)
public Boolean GetFillPath (SKPath src, SKPath dst, SKRect cullRect, Single resScale = 1)
Es sind nur die ersten beiden Argumente erforderlich. Die -Methode greift auf den Pfad zu, auf den das src
Argument verweist, ändert die Pfaddaten basierend auf den Stricheigenschaften im SKPaint
-Objekt (einschließlich der PathEffect
-Eigenschaft) und schreibt die Ergebnisse dann in den dst
Pfad. Der resScale
Parameter ermöglicht es, die Genauigkeit zu reduzieren, um einen kleineren Zielpfad zu erstellen, und das cullRect
Argument kann Konturen außerhalb eines Rechtecks beseitigen.
Eine grundlegende Verwendung dieser Methode beinhaltet überhaupt keine Pfadeffekte: Wenn die -Eigenschaft des SKPaint
Objekts auf SKPaintStyle.Stroke
festgelegt ist Style
und nicht festgelegt istPathEffect
, wird ein Pfad erstellt, der GetFillPath
eine Gliederung des Quellpfads darstellt, als ob es von den Paint-Eigenschaften gezeichnet worden wäre.
Wenn der src
Pfad beispielsweise ein einfacher Kreis mit Radius 500 ist und das SKPaint
Objekt eine Strichbreite von 100 angibt, wird der dst
Pfad zu zwei konzentrischen Kreisen, einer mit einem Radius von 450 und der andere mit einem Radius von 550. Die -Methode wird aufgerufen GetFillPath
, da das Füllen dieses Pfads dst
mit dem Streichen des Pfads src
identisch ist. Sie können aber auch den dst
Pfad streichen, um die Pfadgliederungen anzuzeigen.
Dies wird durch Tippen auf den Pfad veranschaulicht. Die SKCanvasView
und TapGestureRecognizer
werden in der Datei TapToOutlineThePathPage.xaml instanziiert. Die CodeBehind-Datei TapToOutlineThePathPage.xaml.cs definiert drei SKPaint
Objekte als Felder, zwei zum Streichen mit Strichbreiten von 100 und 20 und das dritte zum Füllen:
public partial class TapToOutlineThePathPage : ContentPage
{
bool outlineThePath = false;
SKPaint redThickStroke = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Red,
StrokeWidth = 100
};
SKPaint redThinStroke = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Red,
StrokeWidth = 20
};
SKPaint blueFill = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Blue
};
public TapToOutlineThePathPage()
{
InitializeComponent();
}
void OnCanvasViewTapped(object sender, EventArgs args)
{
outlineThePath ^= true;
(sender as SKCanvasView).InvalidateSurface();
}
...
}
Wenn der Bildschirm nicht getippt wurde, verwendet der Handler die PaintSurface
blueFill
Objekte und redThickStroke
paint, um einen Kreispfad zu rendern:
public partial class TapToOutlineThePathPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPath circlePath = new SKPath())
{
circlePath.AddCircle(info.Width / 2, info.Height / 2,
Math.Min(info.Width / 2, info.Height / 2) -
redThickStroke.StrokeWidth);
if (!outlineThePath)
{
canvas.DrawPath(circlePath, blueFill);
canvas.DrawPath(circlePath, redThickStroke);
}
else
{
using (SKPath outlinePath = new SKPath())
{
redThickStroke.GetFillPath(circlePath, outlinePath);
canvas.DrawPath(outlinePath, blueFill);
canvas.DrawPath(outlinePath, redThinStroke);
}
}
}
}
}
Der Kreis wird wie erwartet gefüllt und gestrichelt:
Wenn Sie auf den Bildschirm tippen, wird auf true
festgelegt, outlineThePath
und der PaintSurface
Handler erstellt ein neues SKPath
Objekt und verwendet dieses als Zielpfad in einem Aufruf GetFillPath
von für das redThickStroke
Paint-Objekt. Dieser Zielpfad wird dann gefüllt und mit redThinStroke
Strichen strichen, was Folgendes ergibt:
Die beiden roten Kreise weisen deutlich darauf hin, dass der ursprüngliche Kreisweg in zwei kreisförmige Konturen umgewandelt wurde.
Diese Methode kann sehr nützlich sein, um Pfade zu entwickeln, die für die SKPathEffect.Create1DPath
-Methode verwendet werden. Die Pfade, die Sie in diesen Methoden angeben, werden immer gefüllt, wenn die Pfade repliziert werden. Wenn Sie nicht möchten, dass der gesamte Pfad ausgefüllt wird, müssen Sie die Gliederungen sorgfältig definieren.
Im Beispiel für verknüpfte Kette wurden die Glieder beispielsweise mit einer Reihe von vier Bögen definiert, von denen jedes Paar auf zwei Radien basierte, um den Bereich des zu füllenden Pfads zu skizzieren. Es ist möglich, den Code in der LinkedChainPage
-Klasse zu ersetzen, um dies etwas anders zu tun.
Zunächst sollten Sie die linkRadius
Konstante neu definieren:
const float linkRadius = 27.5f;
const float linkThickness = 5;
Der linkPath
ist jetzt nur noch zwei Bogen, die auf diesem einzelnen Radius basieren, mit den gewünschten Startwinkeln und Kehrwinkeln:
using (SKPath linkPath = new SKPath())
{
SKRect rect = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
linkPath.AddArc(rect, 55, 160);
linkPath.AddArc(rect, 235, 160);
using (SKPaint strokePaint = new SKPaint())
{
strokePaint.Style = SKPaintStyle.Stroke;
strokePaint.StrokeWidth = linkThickness;
using (SKPath outlinePath = new SKPath())
{
strokePaint.GetFillPath(linkPath, outlinePath);
// Set that path as the 1D path effect for linksPaint
linksPaint.PathEffect =
SKPathEffect.Create1DPath(outlinePath, 1.3f * linkRadius, 0,
SKPath1DPathEffectStyle.Rotate);
}
}
}
Das outlinePath
-Objekt ist dann der Empfänger der Gliederung von linkPath
, wenn es mit den in strokePaint
angegebenen Eigenschaften gezeichnet wird.
Ein weiteres Beispiel, das diese Technik verwendet, wird als Nächstes für den pfad angezeigt, der in einer -Methode verwendet wird.
Kombinieren von Pfadeffekten
Die beiden letzten statischen Erstellungsmethoden von SKPathEffect
sind SKPathEffect.CreateSum
und SKPathEffect.CreateCompose
:
public static SKPathEffect CreateSum (SKPathEffect first, SKPathEffect second)
public static SKPathEffect CreateCompose (SKPathEffect outer, SKPathEffect inner)
Beide Methoden kombinieren zwei Pfadeffekte, um einen zusammengesetzten Pfadeffekt zu erstellen. Die CreateSum
-Methode erstellt einen Pfadeffekt, der den beiden separat angewendeten Pfadeffekten ähnelt, während CreateCompose
ein Pfadeffekt (der inner
) angewendet wird, und wendet dann den outer
auf diesen an.
Sie haben bereits gesehen, wie die GetFillPath
Methode von SKPaint
basierend auf SKPaint
Eigenschaften (einschließlich PathEffect
) einen Pfad in einen anderen Pfad konvertieren kann, sodass es nicht allzu geheimnisvoll sein sollte, wie ein SKPaint
Objekt diesen Vorgang zweimal mit den beiden Pfadeffekten ausführen kann, die in den CreateSum
Methoden oder CreateCompose
angegeben sind.
Eine offensichtliche Verwendung von CreateSum
besteht darin, ein SKPaint
Objekt zu definieren, das einen Pfad mit einem Pfadeffekt füllt und den Pfad mit einem anderen Pfadeffekt stricht. Dies wird im Beispiel "Cats in Frame " veranschaulicht, in dem ein Array von Katzen in einem Frame mit scalloierten Kanten angezeigt wird:
Die CatsInFramePage
-Klasse definiert zunächst mehrere Felder. Möglicherweise erkennen Sie das erste Feld aus der PathDataCatPage
-Klasse aus dem Artikel SVG-Pfaddaten . Der zweite Pfad basiert auf einer Linie und einem Bogen für das Jakobsmuschelmuster des Rahmens:
public class CatsInFramePage : ContentPage
{
// From PathDataCatPage.cs
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 catStroke = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = 5
};
SKPath scallopPath =
SKPath.ParseSvgPathData("M 0 0 L 50 0 A 60 60 0 0 1 -50 0 Z");
SKPaint framePaint = new SKPaint
{
Color = SKColors.Black
};
...
}
Kann catPath
in der SKPathEffect.Create2DPath
-Methode verwendet werden, wenn die SKPaint
Objekteigenschaft Style
auf Stroke
festgelegt ist. Wenn die catPath
jedoch direkt in diesem Programm verwendet wird, wird der gesamte Kopf der Katze gefüllt, und die Schnurrköpfe sind nicht einmal sichtbar. (Probieren Sie es aus!) Es ist erforderlich, die Gliederung dieses Pfads abzurufen und diese Gliederung in der SKPathEffect.Create2DPath
-Methode zu verwenden.
Dieser Auftrag wird vom Konstruktor ausgeführt. Zuerst werden zwei Transformationen auf angewendet, um catPath
den Punkt (0, 0) in die Mitte zu verschieben und ihn in der Größe herunterzuskalieren. GetFillPath
ruft alle Konturen der Konturen in outlinedCatPath
ab, und dieses Objekt wird im SKPathEffect.Create2DPath
Aufruf verwendet. Die Skalierungsfaktoren im SKMatrix
Wert sind etwas größer als die horizontale und vertikale Größe der Katze, um einen kleinen Puffer zwischen den Kacheln bereitzustellen, während die Übersetzungsfaktoren etwas empirisch abgeleitet wurden, sodass eine vollständige Katze in der oberen linken Ecke des Rahmens sichtbar ist:
public class CatsInFramePage : ContentPage
{
...
public CatsInFramePage()
{
Title = "Cats in Frame";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
// Move (0, 0) point to center of cat path
catPath.Transform(SKMatrix.MakeTranslation(-240, -175));
// Now catPath is 400 by 250
// Scale it down to 160 by 100
catPath.Transform(SKMatrix.MakeScale(0.40f, 0.40f));
// Get the outlines of the contours of the cat path
SKPath outlinedCatPath = new SKPath();
catStroke.GetFillPath(catPath, outlinedCatPath);
// Create a 2D path effect from those outlines
SKPathEffect fillEffect = SKPathEffect.Create2DPath(
new SKMatrix { ScaleX = 170, ScaleY = 110,
TransX = 75, TransY = 80,
Persp2 = 1 },
outlinedCatPath);
// Create a 1D path effect from the scallop path
SKPathEffect strokeEffect =
SKPathEffect.Create1DPath(scallopPath, 75, 0, SKPath1DPathEffectStyle.Rotate);
// Set the sum the effects to frame paint
framePaint.PathEffect = SKPathEffect.CreateSum(fillEffect, strokeEffect);
}
...
}
Der Konstruktor ruft SKPathEffect.Create1DPath
dann den scalloped Frame auf. Beachten Sie, dass die Breite des Pfads 100 Pixel beträgt, der Vorlauf jedoch 75 Pixel beträgt, sodass der replizierte Pfad um den Frame herum überlappen wird. Die letzte Anweisung des Konstruktors ruft auf SKPathEffect.CreateSum
, um die beiden Pfadeffekte zu kombinieren und das Ergebnis auf das SKPaint
-Objekt festzulegen.
All diese Arbeit ermöglicht es dem PaintSurface
Handler, recht einfach zu sein. Es muss nur ein Rechteck definiert und mit gezeichnet werden framePaint
:
public class CatsInFramePage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKRect rect = new SKRect(50, 50, info.Width - 50, info.Height - 50);
canvas.ClipRect(rect);
canvas.DrawRect(rect, framePaint);
}
}
Die Algorithmen hinter den Pfadeffekten führen immer dazu, dass der gesamte Pfad angezeigt wird, der zum Streichen oder Füllen verwendet wird, was dazu führen kann, dass einige Visuals außerhalb des Rechtecks angezeigt werden. Durch ClipRect
den Aufruf vor dem DrawRect
Anruf können die Visuals erheblich sauberer sein. (Probieren Sie es aus, ohne auszuschneiden!)
Es ist üblich, einen SKPathEffect.CreateCompose
Jitter zu einem anderen Pfadeffekt hinzuzufügen. Sie können sicherlich selbst experimentieren, aber hier ist ein etwas anderes Beispiel:
Die Gestrichelten Schraffurlinien füllen eine Ellipse mit Geschlüpften Linien, die gestrichelt sind. Die meiste Arbeit in der DashedHatchLinesPage
Klasse wird direkt in den Felddefinitionen ausgeführt. Diese Felder definieren einen Bindestricheffekt und einen Schraffureffekt. Sie werden als static
definiert, da dann in einem SKPathEffect.CreateCompose
Aufruf in der SKPaint
Definition auf sie verwiesen wird:
public class DashedHatchLinesPage : ContentPage
{
static SKPathEffect dashEffect =
SKPathEffect.CreateDash(new float[] { 30, 30 }, 0);
static SKPathEffect hatchEffect = SKPathEffect.Create2DLine(20,
Multiply(SKMatrix.MakeScale(60, 60),
SKMatrix.MakeRotationDegrees(45)));
SKPaint paint = new SKPaint()
{
PathEffect = SKPathEffect.CreateCompose(dashEffect, hatchEffect),
StrokeCap = SKStrokeCap.Round,
Color = SKColors.Blue
};
...
static SKMatrix Multiply(SKMatrix first, SKMatrix second)
{
SKMatrix target = SKMatrix.MakeIdentity();
SKMatrix.Concat(ref target, first, second);
return target;
}
}
Der PaintSurface
Handler muss nur den Standardaufwand plus einen Aufruf von enthalten DrawOval
:
public class DashedHatchLinesPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
canvas.DrawOval(info.Width / 2, info.Height / 2,
0.45f * info.Width, 0.45f * info.Height,
paint);
}
...
}
Wie Sie bereits herausgefunden haben, sind die Schraffurlinien nicht genau auf das Innere des Bereichs beschränkt, und in diesem Beispiel beginnen sie immer links mit einem ganzen Bindestrich:
Nachdem Sie nun Pfadeffekte gesehen haben, die von einfachen Punkten und Bindestrichen bis hin zu seltsamen Kombinationen reichen, nutzen Sie Ihre Fantasie und sehen Sie, was Sie erstellen können.