Enumeración e información de trazado
Obtener información sobre trazados y enumeración del contenido
La clase SKPath
define varias propiedades y métodos que permiten obtener información sobre la ruta de acceso. Las propiedades Bounds
y TightBounds
(y los métodos relacionados) obtienen las dimensiones métricas de una ruta de acceso. El método Contains
permite determinar si un punto determinado está dentro de una ruta de acceso.
A veces resulta útil determinar la longitud total de todas las líneas y curvas que componen un trazado. Calcular esta longitud no es una tarea algorítmicamente simple, por lo que toda una clase denominada PathMeasure
se dedica a ella.
A veces también resulta útil obtener todas las operaciones de dibujo y los puntos que componen una ruta de acceso. Al principio, esta instalación podría parecer innecesaria: si el programa ha creado la ruta de acceso, el programa ya conoce el contenido. Sin embargo, ha visto que las rutas de acceso también se pueden crear mediante efectos de ruta de acceso y convirtiendo cadenas de texto en rutas de acceso. También puede obtener todas las operaciones y puntos de dibujo que componen estas rutas de acceso. Una posibilidad es aplicar una transformación algorítmica a todos los puntos, por ejemplo, para encapsular texto alrededor de un hemisferio:
Obtención de la longitud de la ruta de acceso
En el artículo Rutas de acceso y texto ha visto cómo usar el método DrawTextOnPath
para dibujar una cadena de texto cuya línea base sigue el curso de una ruta de acceso. Pero ¿qué ocurre si desea ajustar el tamaño del texto para que se ajuste a la ruta de acceso con precisión? Dibujar texto alrededor de un círculo es fácil porque la circunferencia de un círculo es fácil de calcular. Pero la circunferencia de una elipse o la longitud de una curva Bézier no es tan simple.
La clase SKPathMeasure
puede ayudar. El constructor acepta un argumento SKPath
y la propiedad Length
revela su longitud.
Esta clase se muestra en el ejemplo Longitud de la ruta de acceso, que se basa en la página Bezier Curve. El archivo PathLengthPage.xaml deriva de InteractivePage
e incluye una interfaz táctil:
<local:InteractivePage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SkiaSharpFormsDemos"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
x:Class="SkiaSharpFormsDemos.Curves.PathLengthPage"
Title="Path Length">
<Grid BackgroundColor="White">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</local:InteractivePage>
El archivo de código subyacente PathLengthPage.xaml.cs permite mover cuatro puntos táctiles para definir los puntos de conexión y los puntos de control de una curva Bézier cúbica. Tres campos definen una cadena de texto, un objeto SKPaint
y un ancho calculado del texto:
public partial class PathLengthPage : InteractivePage
{
const string text = "Compute length of path";
static SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Black,
TextSize = 10,
};
static readonly float baseTextWidth = textPaint.MeasureText(text);
...
}
El campo baseTextWidth
es el ancho del texto basado en una configuración TextSize
de 10.
El controlador PaintSurface
dibuja la curva Bézier y, a continuación, ajusta el texto para ajustarse a lo largo de su longitud completa:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Draw path with cubic Bezier curve
using (SKPath path = new SKPath())
{
path.MoveTo(touchPoints[0].Center);
path.CubicTo(touchPoints[1].Center,
touchPoints[2].Center,
touchPoints[3].Center);
canvas.DrawPath(path, strokePaint);
// Get path length
SKPathMeasure pathMeasure = new SKPathMeasure(path, false, 1);
// Find new text size
textPaint.TextSize = pathMeasure.Length / baseTextWidth * 10;
// Draw text on path
canvas.DrawTextOnPath(text, path, 0, 0, textPaint);
}
...
}
La propiedad Length
del objeto recién creado SKPathMeasure
obtiene la longitud de la ruta de acceso. La longitud de la ruta de acceso se divide por el valor baseTextWidth
(que es el ancho del texto basado en un tamaño de texto de 10) y, a continuación, multiplicado por el tamaño de texto base de 10. El resultado es un nuevo tamaño de texto para mostrar el texto a lo largo de esa ruta de acceso:
A medida que la curva Bézier se vuelve más larga o más corta, puede ver que el tamaño del texto cambia.
Recorrer la ruta de acceso
SKPathMeasure
puede hacer algo más que medir la longitud de la ruta de acceso. Para cualquier valor entre cero y la longitud de la ruta de acceso, un objeto SKPathMeasure
puede obtener la posición en la ruta de acceso y la tangente a la curva de trazado en ese punto. La tangente está disponible como vector en forma de objeto SKPoint
o como rotación encapsulada en un objeto SKMatrix
. Estos son los métodos de SKPathMeasure
que obtienen esta información de maneras variadas y flexibles:
Boolean GetPosition (Single distance, out SKPoint position)
Boolean GetTangent (Single distance, out SKPoint tangent)
Boolean GetPositionAndTangent (Single distance, out SKPoint position, out SKPoint tangent)
Boolean GetMatrix (Single distance, out SKMatrix matrix, SKPathMeasureMatrixFlags flag)
Los miembros de la enumeración SKPathMeasureMatrixFlags
son:
GetPosition
GetTangent
GetPositionAndTangent
La página Unicycle Half-Pipe anima una figura de palo en un unicycle que parece montar hacia atrás y hacia adelante a lo largo de una curva Bézier cúbica:
El objeto SKPaint
que se usa para realizar la pulsación tanto en la canalización media como en el unicycle se define como un campo de la clase UnicycleHalfPipePage
. También se define el objeto SKPath
para el unicycle:
public class UnicycleHalfPipePage : ContentPage
{
...
SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = 3,
Color = SKColors.Black
};
SKPath unicyclePath = SKPath.ParseSvgPathData(
"M 0 0" +
"A 25 25 0 0 0 0 -50" +
"A 25 25 0 0 0 0 0 Z" +
"M 0 -25 L 0 -100" +
"A 15 15 0 0 0 0 -130" +
"A 15 15 0 0 0 0 -100 Z" +
"M -25 -85 L 25 -85");
...
}
La clase contiene las invalidaciones estándar de los métodos OnAppearing
y OnDisappearing
para la animación. El controlador PaintSurface
crea la ruta de acceso para la canalización media y, a continuación, la dibuja. A continuación, se crea un objeto SKPathMeasure
en función de esta ruta de acceso:
public class UnicycleHalfPipePage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPath pipePath = new SKPath())
{
pipePath.MoveTo(50, 50);
pipePath.CubicTo(0, 1.25f * info.Height,
info.Width - 0, 1.25f * info.Height,
info.Width - 50, 50);
canvas.DrawPath(pipePath, strokePaint);
using (SKPathMeasure pathMeasure = new SKPathMeasure(pipePath))
{
float length = pathMeasure.Length;
// Animate t from 0 to 1 every three seconds
TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
float t = (float)(timeSpan.TotalSeconds % 5 / 5);
// t from 0 to 1 to 0 but slower at beginning and end
t = (float)((1 - Math.Cos(t * 2 * Math.PI)) / 2);
SKMatrix matrix;
pathMeasure.GetMatrix(t * length, out matrix,
SKPathMeasureMatrixFlags.GetPositionAndTangent);
canvas.SetMatrix(matrix);
canvas.DrawPath(unicyclePath, strokePaint);
}
}
}
}
El controlador PaintSurface
calcula un valor de t
que va de 0 a 1 cada cinco segundos. A continuación, usa la función Math.Cos
para convertirla en un valor de t
que va de 0 a 1 y de vuelta a 0, donde 0 corresponde al unicycle al principio de la parte superior izquierda, mientras que 1 corresponde al unicycle situado en la parte superior derecha. La función coseno hace que la velocidad sea más lenta en la parte superior de la tubería y más rápido en la parte inferior.
Observe que este valor de t
debe multiplicarse por la longitud de la ruta de acceso del primer argumento a GetMatrix
. A continuación, la matriz se aplica al objeto SKCanvas
para dibujar la ruta de acceso de un solo ciclo.
Enumerar la ruta de acceso
Dos clases incrustadas de SKPath
permiten enumerar el contenido de la ruta de acceso. Estas clases son SKPath.Iterator
y SKPath.RawIterator
. Las dos clases son muy similares, pero SKPath.Iterator
pueden eliminar elementos de la ruta de acceso con una longitud cero o cerca de una longitud cero. El RawIterator
se usa en el ejemplo siguiente.
Puede obtener un objeto de tipo SKPath.RawIterator
llamando al método CreateRawIterator
de SKPath
. La enumeración a través de la ruta de acceso se realiza llamando repetidamente al método Next
. Páselo a una matriz de cuatro valores SKPoint
:
SKPoint[] points = new SKPoint[4];
...
SKPathVerb pathVerb = rawIterator.Next(points);
El método Next
devuelve un miembro del tipo de enumeración SKPathVerb
. Estos valores indican el comando de dibujo determinado en la ruta de acceso. El número de puntos válidos insertados en la matriz depende de este verbo:
Move
con un único puntoLine
con dos puntosCubic
con cuatro puntosQuad
con tres puntosConic
con tres puntos (y también llama al métodoConicWeight
para el peso)Close
con un puntoDone
El verbo Done
indica que la enumeración de ruta de acceso está completa.
Observe que no hay verbos Arc
. Esto indica que todos los arcos se convierten en curvas Bézier cuando se agregan al trazado.
Parte de la información de la matriz SKPoint
es redundante. Por ejemplo, si un verbo Move
va seguido de un verbo Line
, el primero de los dos puntos que acompañan Line
es el mismo que el punto Move
. En la práctica, esta redundancia es muy útil. Cuando se obtiene un verbo Cubic
, va acompañado de los cuatro puntos que definen la curva Bézier cúbica. No es necesario conservar la posición actual establecida por el verbo anterior.
Sin embargo, el verbo problemático es Close
. Este comando dibuja una línea recta desde la posición actual hasta el principio del contorno establecido anteriormente por el comando Move
. Idealmente, el verbo Close
debe proporcionar estos dos puntos en lugar de solo un punto. Lo peor es que el punto que acompaña al verbo Close
siempre es (0, 0). Al enumerar a través de una ruta de acceso, probablemente tendrá que conservar el punto Move
y la posición actual.
Enumeración, aplanamiento y malformado
A veces es deseable aplicar una transformación algorítmica a una ruta de acceso a la forma incorrecta de alguna manera:
La mayoría de estas letras constan de líneas rectas, pero estas líneas rectas aparentemente se han retorcido en curvas. ¿Cómo es posible?
La clave es que las líneas rectas originales se dividen en una serie de líneas rectas más pequeñas. Estas líneas rectas más pequeñas individuales se pueden manipular de maneras diferentes para formar una curva.
Para ayudar con este proceso, el ejemplo contiene una clase de estáticaPathExtensions
con un método Interpolate
que divide una línea recta en numerosas líneas cortas que son solo una unidad de longitud. Además, la clase contiene varios métodos que convierten los tres tipos de curvas Bézier en una serie de líneas rectas diminutas que aproximan la curva. (Las fórmulas paramétricas se presentaron en el artículo Tres tipos de curvas Bézier). Este proceso se denomina aplanar la curva:
static class PathExtensions
{
...
static SKPoint[] Interpolate(SKPoint pt0, SKPoint pt1)
{
int count = (int)Math.Max(1, Length(pt0, pt1));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * pt0.X + t * pt1.X;
float y = (1 - t) * pt0.Y + t * pt1.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenCubic(SKPoint pt0, SKPoint pt1, SKPoint pt2, SKPoint pt3)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2) + Length(pt2, pt3));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * (1 - t) * (1 - t) * pt0.X +
3 * t * (1 - t) * (1 - t) * pt1.X +
3 * t * t * (1 - t) * pt2.X +
t * t * t * pt3.X;
float y = (1 - t) * (1 - t) * (1 - t) * pt0.Y +
3 * t * (1 - t) * (1 - t) * pt1.Y +
3 * t * t * (1 - t) * pt2.Y +
t * t * t * pt3.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenQuadratic(SKPoint pt0, SKPoint pt1, SKPoint pt2)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * (1 - t) * pt0.X + 2 * t * (1 - t) * pt1.X + t * t * pt2.X;
float y = (1 - t) * (1 - t) * pt0.Y + 2 * t * (1 - t) * pt1.Y + t * t * pt2.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenConic(SKPoint pt0, SKPoint pt1, SKPoint pt2, float weight)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float denominator = (1 - t) * (1 - t) + 2 * weight * t * (1 - t) + t * t;
float x = (1 - t) * (1 - t) * pt0.X + 2 * weight * t * (1 - t) * pt1.X + t * t * pt2.X;
float y = (1 - t) * (1 - t) * pt0.Y + 2 * weight * t * (1 - t) * pt1.Y + t * t * pt2.Y;
x /= denominator;
y /= denominator;
points[i] = new SKPoint(x, y);
}
return points;
}
static double Length(SKPoint pt0, SKPoint pt1)
{
return Math.Sqrt(Math.Pow(pt1.X - pt0.X, 2) + Math.Pow(pt1.Y - pt0.Y, 2));
}
}
A todos estos métodos se hace referencia desde el método de extensión CloneWithTransform
también se incluyen en esta clase y se muestran a continuación. Este método clona una ruta de acceso mediante la enumeración de los comandos de ruta de acceso y la construcción de una nueva ruta de acceso basada en los datos. Sin embargo, la nueva ruta de acceso consta solo de llamadas MoveTo
y LineTo
. Todas las curvas y líneas rectas se reducen a una serie de líneas diminutas.
Al llamar CloneWithTransform
, se pasa al método un Func<SKPoint, SKPoint>
, que es una función con un parámetro SKPaint
que devuelve un valor SKPoint
. Se llama a esta función para cada punto para aplicar una transformación algorítmica personalizada:
static class PathExtensions
{
public static SKPath CloneWithTransform(this SKPath pathIn, Func<SKPoint, SKPoint> transform)
{
SKPath pathOut = new SKPath();
using (SKPath.RawIterator iterator = pathIn.CreateRawIterator())
{
SKPoint[] points = new SKPoint[4];
SKPathVerb pathVerb = SKPathVerb.Move;
SKPoint firstPoint = new SKPoint();
SKPoint lastPoint = new SKPoint();
while ((pathVerb = iterator.Next(points)) != SKPathVerb.Done)
{
switch (pathVerb)
{
case SKPathVerb.Move:
pathOut.MoveTo(transform(points[0]));
firstPoint = lastPoint = points[0];
break;
case SKPathVerb.Line:
SKPoint[] linePoints = Interpolate(points[0], points[1]);
foreach (SKPoint pt in linePoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[1];
break;
case SKPathVerb.Cubic:
SKPoint[] cubicPoints = FlattenCubic(points[0], points[1], points[2], points[3]);
foreach (SKPoint pt in cubicPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[3];
break;
case SKPathVerb.Quad:
SKPoint[] quadPoints = FlattenQuadratic(points[0], points[1], points[2]);
foreach (SKPoint pt in quadPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[2];
break;
case SKPathVerb.Conic:
SKPoint[] conicPoints = FlattenConic(points[0], points[1], points[2], iterator.ConicWeight());
foreach (SKPoint pt in conicPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[2];
break;
case SKPathVerb.Close:
SKPoint[] closePoints = Interpolate(lastPoint, firstPoint);
foreach (SKPoint pt in closePoints)
{
pathOut.LineTo(transform(pt));
}
firstPoint = lastPoint = new SKPoint(0, 0);
pathOut.Close();
break;
}
}
}
return pathOut;
}
...
}
Dado que la ruta clonada se reduce a pequeñas líneas rectas, la función de transformación tiene la capacidad de convertir líneas rectas en curvas.
Observe que el método conserva el primer punto de cada contorno de la variable denominada firstPoint
y la posición actual después de cada comando de dibujo de la variable lastPoint
. Estas variables son necesarias para construir la línea de cierre final cuando se encuentra un verbo Close
.
El ejemplo GlobularText usa este método de extensión para encapsular aparentemente texto alrededor de un hemisferio en un efecto 3D:
El constructor de clases GlobularTextPage
realiza esta transformación. Crea un objeto SKPaint
para el texto y, a continuación, obtiene un objeto SKPath
del método GetTextPath
. Esta es la ruta de acceso que se pasa al método de extensión CloneWithTransform
junto con una función de transformación:
public class GlobularTextPage : ContentPage
{
SKPath globePath;
public GlobularTextPage()
{
Title = "Globular Text";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
using (SKPaint textPaint = new SKPaint())
{
textPaint.Typeface = SKTypeface.FromFamilyName("Times New Roman");
textPaint.TextSize = 100;
using (SKPath textPath = textPaint.GetTextPath("HELLO", 0, 0))
{
SKRect textPathBounds;
textPath.GetBounds(out textPathBounds);
globePath = textPath.CloneWithTransform((SKPoint pt) =>
{
double longitude = (Math.PI / textPathBounds.Width) *
(pt.X - textPathBounds.Left) - Math.PI / 2;
double latitude = (Math.PI / textPathBounds.Height) *
(pt.Y - textPathBounds.Top) - Math.PI / 2;
longitude *= 0.75;
latitude *= 0.75;
float x = (float)(Math.Cos(latitude) * Math.Sin(longitude));
float y = (float)Math.Sin(latitude);
return new SKPoint(x, y);
});
}
}
}
...
}
La función de transformación calcula primero dos valores denominados longitude
y latitude
ese intervalo va desde –π/2 en la parte superior e izquierda del texto, para π/2 en la parte derecha e inferior del texto. El intervalo de estos valores no es visualmente satisfactorio, por lo que se reducen multiplicando por 0,75. (Pruebe el código sin esos ajustes. El texto se vuelve demasiado oscuro en los polos norte y sur, y demasiado delgado en los lados). Estas coordenadas esféricas tridimensionales se convierten en coordenadas bidimensionales x
y y
por fórmulas estándar.
La nueva ruta de acceso se almacena como un campo. El controlador PaintSurface
simplemente necesita centrar y escalar la ruta de acceso para mostrarla en la pantalla:
public class GlobularTextPage : ContentPage
{
SKPath globePath;
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPaint pathPaint = new SKPaint())
{
pathPaint.Style = SKPaintStyle.Fill;
pathPaint.Color = SKColors.Blue;
pathPaint.StrokeWidth = 3;
pathPaint.IsAntialias = true;
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(0.45f * Math.Min(info.Width, info.Height)); // radius
canvas.DrawPath(globePath, pathPaint);
}
}
}
Esta es una técnica muy versátil. Si la matriz de efectos de ruta de acceso descritos en el artículo Efectos de la ruta de acceso no abarca bastante algo que sentía que debe incluirse, esta es una manera de rellenar los huecos.