Efectos de ruta de acceso en SkiaSharp
Descubra los distintos efectos de trazado que permiten usar los trazados para el trazado y el relleno
Un efecto de ruta de acceso es una instancia de la clase SKPathEffect
que se crea con uno de los ocho métodos de creación estáticos definidos por la clase. A continuación, el objeto SKPathEffect
se establece en la propiedad PathEffect
de un objeto SKPaint
para una variedad de efectos interesantes, por ejemplo, pulsando una línea con una ruta de acceso replicada pequeña:
Los efectos de la ruta de acceso permiten:
- Trazo de una línea con puntos y guiones
- Trazo de una línea con cualquier ruta de acceso rellenada
- Rellenar un área con líneas de sombreado
- Rellenar un área con una ruta de acceso en mosaico
- Hacer esquinas afiladas redondeadas
- Agregar "vibración" aleatoria a líneas y curvas
Además, puede combinar dos o más efectos de ruta de acceso.
En este artículo también se muestra cómo usar el método GetFillPath
de SKPaint
para convertir una ruta de acceso en otra mediante la aplicación de propiedades de SKPaint
, incluidos StrokeWidth
y PathEffect
. Esto da como resultado algunas técnicas interesantes, como obtener una ruta de acceso que es un contorno de otra ruta de acceso. GetFillPath
también resulta útil en relación con los efectos de la ruta de acceso.
Puntos y guiones
El uso del método PathEffect.CreateDash
se describió en el artículo Puntos y guiones. El primer argumento del método es una matriz que contiene un número par de dos o más valores, alternando entre longitudes de guiones y longitudes de huecos entre los guiones:
public static SKPathEffect CreateDash (Single[] intervals, Single phase)
Estos valores no son relativos al ancho del trazo. Por ejemplo, si el ancho del trazo es 10 y desea una línea compuesta de guiones cuadrados y espacios cuadrados, establezca la matriz de intervals
en { 10, 10 }. El argumento phase
indica dónde comienza la línea dentro del patrón de guiones. En este ejemplo, si desea que la línea comience con el espacio cuadrado, establezca phase
en 10.
Los extremos de los guiones se ven afectados por la propiedad StrokeCap
de SKPaint
. Para anchos de trazos anchos, es muy común establecer esta propiedad en SKStrokeCap.Round
para redondear los extremos de los guiones. En este caso, los valores de la matriz intervals
no incluyan la longitud adicional resultante del redondeo. Este hecho significa que un punto circular requiere especificar un ancho de cero. Para un ancho de trazo de 10, para crear una línea con puntos circulares y huecos entre los puntos del mismo diámetro, use una matriz intervals
de { 0, 20 }.
La página Texto con puntos animados es similar a la página Texto descrito en el artículo Integración de texto y gráficos en que muestra caracteres de texto descritos estableciendo la propiedad Style
del objeto SKPaint
en SKPaintStyle.Stroke
. Además, el Texto animado con puntos usa SKPathEffect.CreateDash
para darle a este contorno una apariencia de puntos, y el programa también anima el argumento phase
del método SKPathEffect.CreateDash
para hacer que los puntos parezcan viajar alrededor de los caracteres del texto. Esta es la página en modo horizontal:
La clase AnimatedDottedTextPage
comienza definiendo algunas constantes y también invalida los métodos OnAppearing
y OnDisappearing
para la animación:
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;
}
...
}
El controlador PaintSurface
comienza creando un objeto SKPaint
para mostrar el texto. La propiedad TextSize
se ajusta en función del ancho de la pantalla:
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);
}
}
}
}
Al final del método, se llama al método SKPathEffect.CreateDash
mediante dashArray
que se define como un campo y el valor de phase
animado. La instancia de SKPathEffect
se establece en la propiedad PathEffect
del objeto SKPaint
para mostrar el texto.
Como alternativa, puede establecer el objeto SKPathEffect
en el objeto SKPaint
antes de medir el texto y centrarlo en la página. Sin embargo, en ese caso, los puntos animados y guiones provocan cierta variación en el tamaño del texto representado, y el texto tiende a vibrar un poco. (¡Pruébelo!)
También observará que, como los puntos animados rodean los caracteres de texto, hay un punto determinado en cada curva cerrada donde los puntos parecen aparecer y salir de la existencia. Aquí es donde comienza y termina la ruta de acceso que define el contorno de caracteres. Si la longitud de la ruta de acceso no es un múltiplo entero de la longitud del patrón de guion (en este caso, 20 píxeles), solo puede caber parte de ese patrón al final de la ruta de acceso.
Es posible ajustar la longitud del patrón de guion para ajustarse a la longitud de la ruta de acceso, pero eso requiere determinar la longitud de la ruta de acceso, una técnica que se trata en el artículo Información de ruta de acceso y enumeración.
El programa Transformación del punto/guión anima el propio patrón de guiones para que los guiones parezcan dividirse en puntos, que se combinan para formar guiones de nuevo:
La clase DotDashMorphPage
invalida los métodos OnAppearing
y OnDisappearing
igual que lo hizo el programa anterior, pero la clase define el objeto SKPaint
como campo:
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);
}
}
}
}
El controlador PaintSurface
crea una ruta de acceso elíptica basada en el tamaño de la página y ejecuta una larga sección de código que establece las variables dashArray
y phase
. A medida que la variable animada t
oscila entre 0 y 1, los bloques de if
dividen ese tiempo en cuatro cuartos y, en cada uno de esos cuartos, tsub
también oscila entre 0 y 1. Al final, el programa crea SKPathEffect
y lo establece en el objeto SKPaint
para dibujar.
De ruta de acceso a ruta de acceso
El método GetFillPath
de SKPaint
convierte una ruta de acceso en otra en función de la configuración del objeto SKPaint
. Para ver cómo funciona esto, reemplace la llamada canvas.DrawPath
en el programa anterior por el código siguiente:
SKPath newPath = new SKPath();
bool fill = ellipsePaint.GetFillPath(ellipsePath, newPath);
SKPaint newPaint = new SKPaint
{
Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);
En este nuevo código, la llamada GetFillPath
convierte ellipsePath
(que es simplemente un óvalo) en newPath
, que a continuación se muestra con newPaint
. El objeto newPaint
se crea con todos los valores de propiedad predeterminados, excepto que la propiedad Style
se establece en función del valor devuelto booleano de GetFillPath
.
Los objetos visuales son idénticos, excepto el color, que se establece en ellipsePaint
pero no newPaint
. En lugar de la elipse simple definida en ellipsePath
, newPath
contiene numerosos contornos de trazado que definen la serie de puntos y guiones. Este es el resultado de aplicar varias propiedades de ellipsePaint
(en concreto, StrokeWidth
, StrokeCap
y PathEffect
) a ellipsePath
y colocar la ruta de acceso resultante en newPath
. El GetFillPath
método devuelve un valor booleano que indica si se va a rellenar la ruta de acceso de destino; en este ejemplo, el valor devuelto es true
para rellenar la ruta de acceso.
Pruebe a cambiar la configuración de Style
en newPaint
a SKPaintStyle.Stroke
y verá los contornos de ruta individuales descritos con una línea de ancho de un píxel.
Trazar con una ruta de acceso
El método SKPathEffect.Create1DPath
es conceptualmente similar a SKPathEffect.CreateDash
, salvo que se especifica una ruta de acceso en lugar de un patrón de guiones y huecos. Esta ruta de acceso se replica varias veces para trazar la línea o curva.
La sintaxis es:
public static SKPathEffect Create1DPath (SKPath path, Single advance,
Single phase, SKPath1DPathEffectStyle style)
En general, la ruta de acceso que pase a Create1DPath
será pequeña y centrada alrededor del punto (0, 0). El parámetro advance
indica la distancia entre los centros de la ruta de acceso a medida que la ruta de acceso se replica en la línea. Normalmente, este argumento se establece en el ancho aproximado de la ruta de acceso. El argumento phase
desempeña el mismo papel que en el método CreateDash
.
SKPath1DPathEffectStyle
tiene tres miembros:
Translate
Rotate
Morph
El miembro Translate
hace que la ruta de acceso permanezca en la misma orientación que se replica a lo largo de una línea o curva. Para Rotate
, la ruta de acceso se gira en función de una tangente a la curva. La ruta de acceso tiene su orientación normal para las líneas horizontales. Morph
es similar a Rotate
, salvo que la propia ruta de acceso también está curvada para que coincida con la curvatura de la línea que se va a trazar.
En la página Efecto de ruta de acceso 1D se muestran estas tres opciones. El archivo OneDimensionalPathEffectPage.xaml define un selector que contiene tres elementos correspondientes a los tres miembros de la enumeración:
<?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>
El archivo de código subyacente OneDimensionalPathEffectPage.xaml.cs define tres objetos SKPathEffect
como campos. Todos se crean mediante SKPathEffect.Create1DPath
con SKPath
objetos creados a través de SKPath.ParseSvgPathData
. La primera es una caja simple, la segunda es una forma de diamante y la tercera es un rectángulo. Se usan para demostrar los tres estilos de efecto:
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);
}
}
}
El controlador PaintSurface
crea una curva Bézier que recorre en bucle por sí mismo y accede al selector para determinar qué PathEffect
se debe usar para trazarlo. Las tres opciones ( Translate
, Rotate
y Morph
) se muestran de izquierda a derecha:
La ruta de acceso especificada en el método SKPathEffect.Create1DPath
siempre se rellena. La ruta de acceso especificada en el método DrawPath
siempre se traza si el objeto SKPaint
tiene su propiedad PathEffect
establecida en un efecto de ruta de acceso 1D. Tenga en cuenta que el objeto pathPaint
no tiene configuración Style
, que normalmente tiene como valor predeterminado Fill
, pero la ruta de acceso se traza independientemente.
El cuadro usado en el ejemplo de Translate
es de 20 píxeles cuadrados y el argumento advance
se establece en 24. Esta diferencia provoca un hueco entre los cuadros cuando la línea es aproximadamente horizontal o vertical, pero los cuadros se superponen un poco cuando la línea es diagonal porque la diagonal del cuadro es de 28,3 píxeles.
La forma de diamante ejemplo Rotate
también tiene un ancho de 20 píxeles. advance
se establece en 20 para que los puntos continúen tocándose a medida que el diamante se gira junto con la curvatura de la línea.
La forma del rectángulo del ejemplo de Morph
es de 50 píxeles de ancho con un valor de advance
de 55 para hacer un pequeño hueco entre los rectángulos a medida que se doblan alrededor de la curva Bézier.
Si el argumento advance
es menor que el tamaño de la ruta de acceso, las rutas de acceso replicadas se pueden superponer. Esto puede dar lugar a algunos efectos interesantes. La página Cadena vinculada muestra una serie de círculos superpuestos que parecen parecerse a una cadena vinculada, que se bloquea en la forma distintiva de un catenario:
Mire muy cerca y verá que esos no son realmente círculos. Cada vínculo de la cadena es de dos arcos, de tamaño y posición, por lo que parecen conectarse con vínculos adyacentes.
Una cadena o cable de distribución uniforme de peso se bloquea en forma de catenario. Un arco construido en forma de un catenario invertido se beneficia de una distribución igual de presión del peso de un arco. La catenaria tiene una descripción matemática aparentemente simple:
y = a · cosh(x / a)
El cosh es la función de coseno hiperbólico. Para x igual a 0, cosh es cero e y es igual a a. Ese es el centro de la catenaria. Al igual que la función de coseno, se dice que cosh es incluso, lo que significa que cosh(–x) es igual a cosh(x) y los valores aumentan para aumentar los argumentos positivos o negativos. Estos valores describen las curvas que forman los lados del catenario.
Buscar el valor adecuado de a para ajustar la catenaria a las dimensiones de la página del teléfono no es un cálculo directo. Si w y h son el ancho y alto de un rectángulo, el valor óptimo de a satisface la siguiente ecuación:
cosh(w / 2 / a) = 1 + h / a
El siguiente método de la clase LinkedChainPage
incorpora esa igualdad haciendo referencia a las dos expresiones de la izquierda y derecha del signo igual como left
y right
. Para valores pequeños de un, left
es mayor que right
; para valores grandes de a, left
es menor que right
. El bucle while
se estrecha en un valor óptimo de a:
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;
}
El objeto SKPath
para los vínculos se crea en el constructor de la clase y, a continuación, el objeto de SKPathEffect
resultante se establece en la propiedad PathEffect
del objeto SKPaint
que se almacena como campo:
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);
}
}
...
}
El trabajo principal del controlador de PaintSurface
es crear una ruta de acceso para el propio catenario. Después de determinar el óptimo a y almacenarlo en la variable optA
, también debe calcular un desplazamiento desde la parte superior de la ventana. A continuación, puede acumular una colección de SKPoint
valores para el catenario, convertirlo en una ruta de acceso y dibujar la ruta con el objeto SKPaint
creado anteriormente:
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);
}
}
...
}
Este programa define la ruta de acceso utilizada en Create1DPath
para tener su punto (0, 0) en el centro. Esto parece razonable porque el punto (0, 0) del trazado está alineado con la línea o curva que adorna. Sin embargo, puede usar un punto no centrado (0, 0) para algunos efectos especiales.
La página Cinta transportadora crea un camino que se asemeja a una cinta transportadora oblonga con una parte superior e inferior curvadas cuyo tamaño se ajusta a las dimensiones de la ventana. Esa ruta de acceso se traza con un objeto simple SKPaint
de 20 píxeles de ancho y gris coloreado y, a continuación, se vuelve a trazar con otro objeto SKPaint
con un objeto SKPathEffect
que hace referencia a una ruta de acceso similar a un cubo pequeño:
El punto (0, 0) de la ruta de acceso del cubo es el mango, por lo que cuando se anima el argumento phase
, los cubos parecen girar alrededor de la cinta transportadora, quizás descodificación del agua en la parte inferior y volcarlo en la parte superior.
La clase ConveyorBeltPage
implementa animaciones con invalidaciones de los métodos OnAppearing
y OnDisappearing
. La ruta de acceso del cubo se define en el constructor de la página:
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));
}
...
El código de creación del cubo se completa con dos transformaciones que hacen que el cubo sea un poco más grande y gire hacia el lado. Aplicar estas transformaciones era más fácil que ajustar todas las coordenadas del código anterior.
El controlador PaintSurface
comienza definiendo una ruta de acceso para la cinta transportadora. Esto es simplemente un par de líneas y un par de semicírculos que se dibujan con una línea de gris oscuro de 20 píxeles:
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);
}
}
}
}
La lógica para dibujar la cinta transportadora no funciona en modo horizontal.
Los cubos se deben espaciar alrededor de 200 píxeles en la cinta transportadora. Sin embargo, la cinta transportadora probablemente no es un múltiplo de 200 píxeles de largo, lo que significa que, como phase
el argumento de SKPathEffect.Create1DPath
es animado, los cubos aparecerán y no existirán.
Por este motivo, el programa calcula primero un valor denominado length
que es la longitud de la cinta transportadora. Dado que la cinta transportadora consta de líneas rectas y semicírculos, se trata de un cálculo sencillo. A continuación, el número de cubos se calcula dividiendo length
entre 200. Esto se redondea al entero más cercano y ese número se divide en length
. El resultado es un espaciado para un número entero de cubos. El argumento phase
es simplemente una fracción de eso.
De ruta de acceso a ruta de acceso de nuevo
En la parte inferior del controlador de DrawSurface
en cinta transportadora, comente la llamada canvas.DrawPath
y reemplácela por el código siguiente:
SKPath newPath = new SKPath();
bool fill = bucketsPaint.GetFillPath(conveyerPath, newPath);
SKPaint newPaint = new SKPaint
{
Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);
Al igual que con el ejemplo anterior de GetFillPath
, verá que los resultados son los mismos excepto para el color. Después de ejecutar GetFillPath
, el objeto newPath
contiene varias copias de la ruta de acceso del cubo, cada una situada en el mismo lugar en el que la animación las colocó en el momento de la llamada.
Sombreado de un área
El método SKPathEffect.Create2DLines
rellena un área con líneas paralelas, a menudo denominadas líneas de sombreado. El método tiene la siguiente sintaxis:
public static SKPathEffect Create2DLine (Single width, SKMatrix matrix)
El argumento width
especifica el ancho del trazo de las líneas de sombreado. El parámetro matrix
es una combinación de escalado y rotación opcional. El factor de escala indica el incremento de píxel que Skia usa para espaciar las líneas de sombreado. La separación entre las líneas es el factor de escalado menos el argumento width
. Si el factor de escalado es menor o igual que el valor de width
, no habrá espacio entre las líneas de sombreado y el área aparecerá rellenada. Especifique el mismo valor para el escalado horizontal y vertical.
De forma predeterminada, las líneas de sombreado son horizontales. Si el parámetro matrix
contiene rotación, las líneas de sombreado se giran en el sentido de las agujas del reloj.
La página Relleno de sombreado muestra este efecto de ruta de acceso. La clase HatchFillPage
define tres efectos de ruta de acceso como campos, el primero para líneas de sombreado horizontales con un ancho de 3 píxeles con un factor de escalado que indica que están separados por 6 píxeles. Por lo tanto, la separación entre las líneas es de tres píxeles. El segundo efecto de ruta de acceso es para líneas de sombreado verticales con un ancho de seis píxeles espaciados de 24 píxeles (por lo que la separación es de 18 píxeles) y la tercera es para líneas de sombreado diagonales de 12 píxeles de ancho espaciado de 36 píxeles.
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;
}
}
Observe el método de matriz Multiply
. Dado que los factores de escalado horizontal y vertical son los mismos, el orden en que se multiplican las matrices de escalado y rotación no importa.
El controlador PaintSurface
usa estos tres efectos de ruta de acceso con tres colores diferentes en combinación con fillPaint
para rellenar un rectángulo redondeado de tamaño para ajustarse a la página. Se omite la propiedad Style
establecida en fillPaint
; cuando el objeto SKPaint
incluye un efecto de ruta de acceso creado a partir de SKPathEffect.Create2DLine
, el área se rellena independientemente de lo siguiente:
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);
}
}
...
}
Si observa detenidamente los resultados, verá que las líneas de sombreado rojo y azul no se limitan precisamente al rectángulo redondeado. (Esto es aparentemente una característica del código Skia subyacente). Si esto no es satisfactorio, se muestra un enfoque alternativo para las líneas de sombreado diagonales en verde: el rectángulo redondeado se usa como una ruta de recorte y las líneas de sombreado se dibujan en toda la página.
El controlador PaintSurface
concluye con una llamada para simplemente trazar el rectángulo redondeado, por lo que puede ver la discrepancia con las líneas de sombreado rojo y azul:
La pantalla de Android no parece realmente así: el escalado de la captura de pantalla ha provocado que las líneas rojas delgadas y los espacios finos se consoliden en líneas rojas aparentemente más anchas y espacios más anchos.
Rellenar con un trazado
SKPathEffect.Create2DPath
le permite rellenar un área con una ruta de acceso que se replica horizontal y verticalmente, para poner en mosaico el área:
public static SKPathEffect Create2DPath (SKMatrix matrix, SKPath path)
Los factores de escala SKMatrix
indican el espaciado horizontal y vertical de la ruta de acceso replicada. Pero no se puede girar la ruta de acceso con este argumento matrix
; Si desea girar la ruta de acceso, gire la ruta de acceso mediante el método Transform
definido por SKPath
.
La ruta de acceso replicada normalmente se alinea con los bordes izquierdo y superior de la pantalla en lugar del área que se va a rellenar. Puede invalidar este comportamiento proporcionando factores de traducción entre 0 y los factores de escalado para especificar desplazamientos horizontales y verticales desde los lados izquierdo y superior.
La página Relleno de mosaico de ruta de acceso muestra este efecto de ruta de acceso. La ruta de acceso utilizada para el mosaico del área se define como un campo en la clase PathTileFillPage
. Las coordenadas horizontales y verticales oscilan entre –40 y 40, lo que significa que esta ruta de acceso es de 80 píxeles cuadrados:
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);
}
}
}
}
En el controlador PaintSurface
, SKPathEffect.Create2DPath
llama a establece el espaciado horizontal y vertical en 64 para hacer que los mosaicos cuadrados de 80 píxeles se superpongan. Afortunadamente, el camino parece una pieza de rompecabezas y combina muy bien con los mosaicos contiguos:
El escalado de la captura de pantalla original provoca cierta distorsión, especialmente en la pantalla Android.
Tenga en cuenta que estos mosaicos siempre aparecen enteros y nunca se truncan. En las dos primeras capturas de pantalla, ni siquiera es evidente que el área que se va a rellenar es un rectángulo redondeado. Si desea truncar estos mosaicos en un área determinada, use una ruta de recorte.
Intente establecer la propiedad Style
del objeto SKPaint
en Stroke
y verá los mosaicos individuales que se describen en lugar de rellenarse.
También es posible rellenar un área con un mapa de bits en mosaico, como se muestra en el artículo Mosaico de mapa de bits SkiaSharp.
Redondeo de esquinas afiladas
El programa Heptágono redondeado presentado en Tres formas de dibujar un arco usó un arco tangente para curvar los puntos de una figura de siete lados. La página Otro heptágono redondeado muestra un enfoque mucho más sencillo que usa un efecto de ruta de acceso creado a partir del método SKPathEffect.CreateCorner
:
public static SKPathEffect CreateCorner (Single radius)
Aunque el único argumento se denomina radius
, debe establecerlo en la mitad del radio de esquina deseado. (Esta es una característica del código Skia subyacente).
Este es el controlador PaintSurface
de la clase AnotherRoundedHeptagonPage
:
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);
}
}
}
Puede usar este efecto con la pulsación o el relleno en función de la propiedad Style
del objeto SKPaint
. Aquí se está ejecutando:
Verá que este heptágono redondeado es idéntico al programa anterior. Si necesita más convincente que el radio de la esquina es realmente 100 en lugar de los 50 especificados en la llamada SKPathEffect.CreateCorner
, puede quitar la marca de comentario de la instrucción final en el programa y ver un círculo de 100 radios superpuesto en la esquina.
Vibración aleatoria
A veces, las líneas rectas impecables de los gráficos de ordenador no son bastante lo que desea y se desea una pequeña aleatoriedad. En ese caso, querrá probar el método SKPathEffect.CreateDiscrete
:
public static SKPathEffect CreateDiscrete (Single segLength, Single deviation, UInt32 seedAssist)
Puede usar este efecto de ruta de acceso para presionar o rellenar. Las líneas se separan en segmentos conectados (la longitud aproximada de la que se especifica) segLength
y se extienden en diferentes direcciones. La extensión de la desviación de la línea original se especifica mediante deviation
.
El argumento final es una inicialización que se usa para generar la secuencia pseudoaleatoriedad utilizada para el efecto. El efecto de vibración se verá un poco diferente para diferentes semillas. El argumento tiene un valor predeterminado de cero, lo que significa que el efecto es el mismo siempre que se ejecuta el programa. Si desea una vibración diferente cada vez que se vuelva a dibujar la pantalla, puede establecer la inicialización en la propiedad Millisecond
de un valor de DataTime.Now
(por ejemplo).
La página Experimento de vibración permite experimentar con valores diferentes en la pulsación de un rectángulo:
El programa es sencillo. El archivo JitterExperimentPage.xaml crea una instancia de dos elementos Slider
y un 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>
Se llama al controlador PaintSurface
del archivo de código subyacente JitterExperimentPage.xaml.cs cada vez que cambia un valor Slider
. Llama a SKPathEffect.CreateDiscrete
usando los dos valores de Slider
y lo usa para trazar un rectángulo:
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);
}
}
}
También puede usar este efecto para rellenar, en cuyo caso el contorno del área rellena está sujeto a estas desviaciones aleatorias. La página Texto de vibración muestra el uso de este efecto de ruta de acceso para mostrar texto. La mayoría del código del controlador de PaintSurface
de la clase JitterTextPage
se dedica a cambiar el tamaño y centrar el texto:
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);
}
}
Aquí se ejecuta en modo horizontal:
Esquematización de ruta de acceso
Ya ha visto dos ejemplos pequeños del método GetFillPath
de SKPaint
, que existe dos versiones:
public Boolean GetFillPath (SKPath src, SKPath dst, Single resScale = 1)
public Boolean GetFillPath (SKPath src, SKPath dst, SKRect cullRect, Single resScale = 1)
Solo se requieren los dos primeros argumentos. El método accede a la ruta de acceso a la que hace referencia el argumento src
, modifica los datos de ruta de acceso en función de las propiedades de trazo del objeto SKPaint
(incluida la propiedad PathEffect
) y, a continuación, escribe los resultados en la ruta de acceso dst
. El parámetro resScale
permite reducir la precisión para crear una ruta de acceso de destino más pequeña y el argumento cullRect
puede eliminar contornos fuera de un rectángulo.
Un uso básico de este método no implica efectos de ruta de acceso en absoluto: si el objeto SKPaint
tiene su propiedad Style
establecida en SKPaintStyle.Stroke
, y no tiene su PathEffect
establecido, GetFillPath
crea una ruta de acceso que representa un contorno de la ruta de origen como si hubiera sido acariciada por las propiedades de la pintura.
Por ejemplo, si la ruta de acceso src
es un círculo simple de radio 500, y el objeto SKPaint
especifica un ancho de trazo de 100, la ruta de acceso de dst
se convierte en dos círculos concéntricos, uno con un radio de 450 y el otro con un radio de 550. Se llama al método GetFillPath
porque rellenar esta ruta de acceso dst
es la misma que la ruta de acceso de src
. Pero también puede trazar la ruta de acceso dst
para ver los contornos de la ruta de acceso.
Pulsar para describir la ruta de acceso muestra esto. Se crean instancias de SKCanvasView
y TapGestureRecognizer
en el archivo TapToOutlineThePathPage.xaml. El archivo de código subyacente de TapToOutlineThePathPage.xaml.cs define tres objetos SKPaint
como campos, dos para la pulsación con anchos de trazo de 100 y 20, y el tercero para rellenar:
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();
}
...
}
Si no se ha pulsado la pantalla, el controlador de PaintSurface
usa el blueFill
y redThickStroke
objetos de pintura para representar una ruta circular:
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);
}
}
}
}
}
El círculo se rellena y se trazos como cabría esperar:
Al pulsar la pantalla, outlineThePath
se establece en true
y el controlador de PaintSurface
crea un objeto SKPath
nuevo y lo usa como ruta de acceso de destino en una llamada a GetFillPath
en el objeto de pintura de redThickStroke
. Esa ruta de acceso de destino se rellena y se traza con redThinStroke
, lo que da como resultado lo siguiente:
Los dos círculos rojos indican claramente que el trazado circular original se ha convertido en dos contornos circulares.
Este método puede ser muy útil en el desarrollo de rutas de acceso que se usarán para el método SKPathEffect.Create1DPath
. Las rutas de acceso que especifique en estos métodos siempre se rellenan cuando se replican las rutas de acceso. Si no desea que se rellene toda la ruta de acceso, debe definir cuidadosamente los contornos.
Por ejemplo, en la Cadena vinculada de ejemplo, los vínculos se definieron con una serie de cuatro arcos, cada par de los cuales se basaban en dos radios para describir el área del trazado que se va a rellenar. Es posible reemplazar el código de la clase LinkedChainPage
para hacerlo de forma un poco diferente.
En primer lugar, querrá volver a definir la constante linkRadius
:
const float linkRadius = 27.5f;
const float linkThickness = 5;
linkPath
ahora son solo dos arcos basados en ese radio único, con los ángulos de inicio deseados y ángulos de barrido:
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);
}
}
}
A continuación, el objeto outlinePath
es el destinatario del esquema de linkPath
cuando se traza con las propiedades especificadas en strokePaint
.
Otro ejemplo que usa esta técnica viene a continuación para la ruta de acceso que se usa en un método.
Combinación de efectos de ruta de acceso
Los dos métodos finales de creación estática de SKPathEffect
son SKPathEffect.CreateSum
y SKPathEffect.CreateCompose
:
public static SKPathEffect CreateSum (SKPathEffect first, SKPathEffect second)
public static SKPathEffect CreateCompose (SKPathEffect outer, SKPathEffect inner)
Ambos métodos combinan dos efectos de ruta de acceso para crear un efecto de ruta de acceso compuesto. El método CreateSum
crea un efecto de ruta de acceso similar a los dos efectos de ruta de acceso aplicados por separado, mientras que CreateCompose
aplica un efecto de ruta de acceso (inner
) y, a continuación, aplica outer
a ese.
Ya ha visto cómo el método GetFillPath
de SKPaint
puede convertir una ruta de acceso a otra en función de las propiedades de SKPaint
(incluido PathEffect
), por lo que no debe ser demasiado misterioso cómo un objeto SKPaint
puede realizar esa operación dos veces con los dos efectos de ruta especificados en los métodos CreateSum
o CreateCompose
.
Un uso obvio de CreateSum
es definir un objeto SKPaint
que rellena una ruta de acceso con un efecto de ruta de acceso y traza la ruta con otro efecto de ruta de acceso. Esto se demuestra en el ejemplo Gatos en marco, que muestra una serie de gatos dentro de un marco con bordes festoneados:
La clase CatsInFramePage
comienza definiendo varios campos. Es posible que reconozca el primer campo de la clase PathDataCatPage
del artículo Datos de ruta SVG. La segunda ruta de acceso se basa en una línea y arco para el patrón escalar del marco:
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
};
...
}
catPath
podría usarse en el método SKPathEffect.Create2DPath
si la propiedad Style
objeto SKPaint
está establecida en Stroke
. Sin embargo, si catPath
se usa directamente en este programa, se rellenará toda la cabeza del gato y los bigotes ni siquiera serán visibles. (¡Pruébelo!) Es necesario obtener el esquema de esa ruta de acceso y usar ese esquema en el SKPathEffect.Create2DPath
método.
El constructor realiza este trabajo. En primer lugar, aplica dos transformaciones a catPath
para mover el punto (0, 0) al centro y reducir verticalmente el tamaño. GetFillPath
obtiene todos los contornos de los contornos de outlinedCatPath
y ese objeto se usa en la llamada SKPathEffect.Create2DPath
. Los factores de escala del SKMatrix
valor son ligeramente mayores que el tamaño horizontal y vertical del gato para proporcionar un poco de búfer entre los mosaicos, mientras que los factores de traducción se derivaron de forma empírica para que un gato completo esté visible en la esquina superior izquierda del marco:
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);
}
...
}
A continuación, el constructor llama a SKPathEffect.Create1DPath
para el marco escalar. Observe que el ancho de la ruta de acceso es de 100 píxeles, pero el avance es de 75 píxeles para que la ruta de acceso replicada se superponga alrededor del marco. La instrucción final del constructor llama a SKPathEffect.CreateSum
para combinar los dos efectos de ruta de acceso y establecer el resultado en el objeto SKPaint
.
Todo este trabajo permite que el controlador de PaintSurface
sea bastante sencillo. Solo necesita definir un rectángulo y dibujarlo mediante 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);
}
}
Los algoritmos detrás de los efectos de ruta de acceso siempre provocan que se muestre toda la ruta de acceso utilizada para mostrar o rellenar, lo que puede hacer que algunos objetos visuales aparezcan fuera del rectángulo. La llamada ClipRect
antes de la llamada a DrawRect
permite que los objetos visuales sean considerablemente más limpios. (¡Pruébelo sin recorte!)
Es habitual usar SKPathEffect.CreateCompose
para agregar algún vibración a otro efecto de ruta de acceso. Sin duda, puede experimentar por su cuenta, pero este es un ejemplo algo diferente:
Las líneas de sombreado discontinuas rellenan una elipse con líneas de sombreado discontinuas. La mayoría del trabajo de la clase DashedHatchLinesPage
se realiza directamente en las definiciones de campo. Estos campos definen un efecto de guión y un efecto de sombreado. Se definen como static
porque, a continuación, se hace referencia a ellos en una llamada SKPathEffect.CreateCompose
en la definición de SKPaint
:
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;
}
}
El controlador de PaintSurface
debe contener solo la sobrecarga estándar más una llamada a 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);
}
...
}
Como ya ha descubierto, las líneas de sombreado no están restringidas precisamente al interior del área y, en este ejemplo, siempre comienzan a la izquierda con un guión completo:
Ahora que ha visto efectos de ruta que van desde puntos simples y guiones a combinaciones extrañas, use su imaginación y vea lo que puede crear.