Compartir vía


Trazados y texto en SkiaSharp

Exploración de la intersección de trazados y texto

En los sistemas de gráficos modernos, las fuentes de texto son colecciones de contornos de caracteres, normalmente definidas por curvas de Bézier cuadráticas. Por lo tanto, muchos sistemas de gráficos modernos incluyen una función para convertir caracteres de texto en un trazado de gráficos.

Ya ha visto que puede trazar los contornos de los caracteres de texto, así como rellenarlos. Esto le permite mostrar estos contornos de caracteres con un ancho de trazo determinado e incluso un efecto de trazado, tal como se describe en el artículo Efectos del trazado. Pero también es posible convertir una cadena de caracteres en un objeto SKPath. Esto significa que los contornos de texto se pueden usar para recortar con técnicas que se describen en el artículo Recorte con trazados y regiones.

Además de usar un efecto de trazado para trazar un contorno de caracteres, también puede crear efectos de trazado basados en un trazado derivado de una cadena de caracteres, e incluso puede combinar ambos efectos:

Efecto de ruta de acceso de texto

En el artículo anterior Efectos del trazado, vio cómo el método GetFillPath de SKPaint puede obtener un esquema de un trazado. También puede usar este método con trazados derivados de esquemas de caracteres.

Por último, en este artículo se muestra otra intersección de trazados y texto: el método DrawTextOnPath de SKCanvas permite mostrar una cadena de texto para que la línea base del texto siga un trazado curvo.

Conversión de texto a trazado

El método GetTextPath de SKPaint convierte una cadena de caracteres en un objeto SKPath:

public SKPath GetTextPath (String text, Single x, Single y)

Los argumentos x y y indican el punto inicial de la línea base del lado izquierdo del texto. Desempeñan el mismo papel que en el método DrawText de SKCanvas. Dentro del trazado, la línea base del lado izquierdo del texto tendrá las coordenadas (x, y).

El método GetTextPath es excesivo si solo desea rellenar o crear el trazado resultante. El método DrawText normal le permite hacerlo. El método GetTextPath es más útil para otras tareas que implican trazados.

Una de estas tareas es la de recortar. La página Clipping Text (Recorte de texto) crea un trazado de recorte basado en los contornos de caracteres de la palabra "CODE". Este trazado se extiende al tamaño de la página para recortar un mapa de bits que contenga una imagen del código fuente de Clipping Text:

Captura de pantalla triple de la página Recorte de texto

El constructor de clase ClippingTextPage carga el mapa de bits que se almacena como un recurso incrustado en la carpeta Media de la solución:

public class ClippingTextPage : ContentPage
{
    SKBitmap bitmap;

    public ClippingTextPage()
    {
        Title = "Clipping Text";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        string resourceID = "SkiaSharpFormsDemos.Media.PageOfCode.png";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            bitmap = SKBitmap.Decode(stream);
        }
    }
    ...
}

El controlador PaintSurface comienza creando un objeto SKPaint adecuado para el texto. Se establece la propiedad Typeface, así como TextSize, aunque para esta aplicación determinada la propiedad TextSize es puramente arbitraria. Observe también que no hay ninguna configuración Style.

La configuración de las propiedades TextSize y Style no es necesaria porque este objeto SKPaint se usa únicamente para la llamada GetTextPath mediante la cadena de texto "CODE". A continuación, el controlador mide el objeto SKPath resultante y aplica tres transformaciones para centrarlo y escalarlo al tamaño de la página. A continuación, el trazado se puede establecer como trazado de recorte:

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

        canvas.Clear(SKColors.Blue);

        using (SKPaint paint = new SKPaint())
        {
            paint.Typeface = SKTypeface.FromFamilyName(null, SKTypefaceStyle.Bold);
            paint.TextSize = 10;

            using (SKPath textPath = paint.GetTextPath("CODE", 0, 0))
            {
                // Set transform to center and enlarge clip path to window height
                SKRect bounds;
                textPath.GetTightBounds(out bounds);

                canvas.Translate(info.Width / 2, info.Height / 2);
                canvas.Scale(info.Width / bounds.Width, info.Height / bounds.Height);
                canvas.Translate(-bounds.MidX, -bounds.MidY);

                // Set the clip path
                canvas.ClipPath(textPath);
            }
        }

        // Reset transforms
        canvas.ResetMatrix();

        // Display bitmap to fill window but maintain aspect ratio
        SKRect rect = new SKRect(0, 0, info.Width, info.Height);
        canvas.DrawBitmap(bitmap,
            rect.AspectFill(new SKSize(bitmap.Width, bitmap.Height)));
    }
}

Una vez establecido el trazado de recorte, se puede mostrar el mapa de bits y se recortará en los contornos de caracteres. Observe el uso del método AspectFill de SKRect que calcula un rectángulo para rellenar la página mientras conserva la relación de aspecto.

La página Text Path Effect (Efecto del trazado de texto) convierte un solo carácter & en un trazado para crear un efecto de trazado 1D. A continuación, se usa un objeto paint con este efecto de trazado para trazar el contorno de una versión más grande de ese mismo carácter:

Captura de pantalla triple de la página Efecto de ruta de acceso de texto

Gran parte del trabajo de la clase TextPathEffectPath se produce en los campos y en el constructor. Los dos objetos SKPaint definidos como campos se usan con dos propósitos diferentes: el primero (denominado textPathPaint) sirve para convertir el carácter & con un TextSize de 50 a un trazado para el efecto de trazado 1D. El segundo (textPaint) sirve para mostrar la versión más grande del carácter & con ese efecto de trazado. Por ese motivo, el Style de este segundo objeto paint se establece en Stroke, pero la propiedad StrokeWidth no se establece porque no es necesaria cuando se usa un efecto de trazado 1D:

public class TextPathEffectPage : ContentPage
{
    const string character = "@";
    const float littleSize = 50;

    SKPathEffect pathEffect;

    SKPaint textPathPaint = new SKPaint
    {
        TextSize = littleSize
    };

    SKPaint textPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Black
    };

    public TextPathEffectPage()
    {
        Title = "Text Path Effect";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        // Get the bounds of textPathPaint
        SKRect textPathPaintBounds = new SKRect();
        textPathPaint.MeasureText(character, ref textPathPaintBounds);

        // Create textPath centered around (0, 0)
        SKPath textPath = textPathPaint.GetTextPath(character,
                                                    -textPathPaintBounds.MidX,
                                                    -textPathPaintBounds.MidY);
        // Create the path effect
        pathEffect = SKPathEffect.Create1DPath(textPath, littleSize, 0,
                                               SKPath1DPathEffectStyle.Translate);
    }
    ...
}

El constructor usa primero el objeto textPathPaint para medir el carácter & con un TextSize de 50. Los negativos de las coordenadas centrales de ese rectángulo se pasan al método GetTextPath para convertir el texto en un trazado. El trazado resultante tiene el punto (0, 0) en el centro del carácter, que es ideal para un efecto de trazado 1D.

Es posible que piense que el objeto SKPathEffect creado al final del constructor podría establecerse en la propiedad PathEffect de textPaint, en lugar de guardarse como un campo. Pero esto resultó no funcionar muy bien porque distorsionaba los resultados de la llamada MeasureText en el controlador PaintSurface:

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

        canvas.Clear();

        // Set textPaint TextSize based on screen size
        textPaint.TextSize = Math.Min(info.Width, info.Height);

        // Do not measure the text with PathEffect set!
        SKRect textBounds = new SKRect();
        textPaint.MeasureText(character, ref textBounds);

        // Coordinates to center text on screen
        float xText = info.Width / 2 - textBounds.MidX;
        float yText = info.Height / 2 - textBounds.MidY;

        // Set the PathEffect property and display text
        textPaint.PathEffect = pathEffect;
        canvas.DrawText(character, xText, yText, textPaint);
    }
}

Esa llamada MeasureText sirve para centrar el carácter en la página. Para evitar problemas, la propiedad PathEffect se establece en el objeto paint después de medir el texto, pero antes de mostrarse.

Esquemas de contornos de caracteres

Normalmente, el método GetFillPath de SKPaint convierte un trazado a otro aplicando propiedades paint, especialmente el ancho del trazo y el efecto de trazado. Cuando se usa sin efectos de trazado, GetFillPath crea eficazmente un trazado que describe otro trazado. Esto se mostró en la página Tap to Outline the Path (Pulsar para describir el trazado) del artículo Efectos del trazado.

También puede llamar a GetFillPath en el trazado devuelto desde GetTextPath, pero al principio es posible que no esté completamente seguro de cómo sería.

En la página Esquemas de contornos de caracteres se muestra la técnica. Todo el código pertinente está en el controlador PaintSurface de la clase CharacterOutlineOutlinesPage.

El constructor comienza creando un objeto SKPaint denominado textPaint con una propiedad TextSize basada en el tamaño de la página. Se convierte en un trazado mediante el método GetTextPath. Los argumentos de coordenadas en GetTextPath centran eficazmente el trazado en la pantalla:

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

    canvas.Clear();

    using (SKPaint textPaint = new SKPaint())
    {
        // Set Style for the character outlines
        textPaint.Style = SKPaintStyle.Stroke;

        // Set TextSize based on screen size
        textPaint.TextSize = Math.Min(info.Width, info.Height);

        // Measure the text
        SKRect textBounds = new SKRect();
        textPaint.MeasureText("@", ref textBounds);

        // Coordinates to center text on screen
        float xText = info.Width / 2 - textBounds.MidX;
        float yText = info.Height / 2 - textBounds.MidY;

        // Get the path for the character outlines
        using (SKPath textPath = textPaint.GetTextPath("@", xText, yText))
        {
            // Create a new path for the outlines of the path
            using (SKPath outlinePath = new SKPath())
            {
                // Convert the path to the outlines of the stroked path
                textPaint.StrokeWidth = 25;
                textPaint.GetFillPath(textPath, outlinePath);

                // Stroke that new path
                using (SKPaint outlinePaint = new SKPaint())
                {
                    outlinePaint.Style = SKPaintStyle.Stroke;
                    outlinePaint.StrokeWidth = 5;
                    outlinePaint.Color = SKColors.Red;

                    canvas.DrawPath(outlinePath, outlinePaint);
                }
            }
        }
    }
}

A continuación, el controlador PaintSurface crea otro trazado denominado outlinePath. Se convierte en el trazado de destino de la llamada a GetFillPath. La propiedad StrokeWidth de 25 hace que outlinePath describa el contorno de un trazado de 25 píxeles trazando los caracteres del texto. A continuación, este trazado se muestra en rojo con un ancho de trazo de 5:

Captura de pantalla triple de la página Esquemas de esquema de caracteres

Mire detenidamente y verá superposiciones en las que el contorno del trazado crea una esquina afilada. Estos son artefactos normales de este proceso.

Texto a lo largo de un trazado

Normalmente, el texto se muestra en una línea base horizontal. El texto se puede girar para ejecutarse vertical o diagonalmente, pero la línea base sigue siendo una línea recta.

Sin embargo, hay ocasiones en las que desea que el texto se ejecute a lo largo de una curva. Este es el propósito del método DrawTextOnPath de SKCanvas:

public Void DrawTextOnPath (String text, SKPath path, Single hOffset, Single vOffset, SKPaint paint)

El texto especificado en el primer argumento se realiza para ejecutarse a lo largo del trazado especificado como segundo argumento. Puede comenzar el texto en un desplazamiento desde el principio del trazado con el argumento hOffset. Normalmente, el trazado forma la línea base del texto: los ascendentes de texto están en un lado del trazado y los descendientes, en el otro. Pero puede desplazar la línea base del texto desde el trazado con el argumento vOffset.

Este método no tiene ninguna facilidad para proporcionar instrucciones sobre cómo establecer la propiedad TextSize de SKPaint para que el texto tenga el tamaño perfecto para ejecutarse desde el principio del trazado hasta el final. A veces, puede averiguar el tamaño del texto por su cuenta. En otras ocasiones, deberá usar las funciones de medición de trazado que se describen en el siguiente artículo, Enumeración e información de trazado.

El programa Circular Text (Texto circular) ajusta el texto alrededor de un círculo. Es fácil determinar la circunferencia de un círculo, por lo que es fácil ajustar el tamaño del texto exactamente. El controlador PaintSurface de la clase CircularTextPage calcula un radio de un círculo en función del tamaño de la página. Ese círculo se convierte en circularPath:

public class CircularTextPage : ContentPage
{
    const string text = "xt in a circle that shapes the te";
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPath circularPath = new SKPath())
        {
            float radius = 0.35f * Math.Min(info.Width, info.Height);
            circularPath.AddCircle(info.Width / 2, info.Height / 2, radius);

            using (SKPaint textPaint = new SKPaint())
            {
                textPaint.TextSize = 100;
                float textWidth = textPaint.MeasureText(text);
                textPaint.TextSize *= 2 * 3.14f * radius / textWidth;

                canvas.DrawTextOnPath(text, circularPath, 0, 0, textPaint);
            }
        }
    }
}

A continuación, se ajusta la propiedad TextSize de textPaint para que el ancho del texto coincida con la circunferencia del círculo:

Captura de pantalla triple de la página Texto circular

El texto también se ha elegido para que sea circular: la palabra "circle" (círculo) es a la vez sujeto de la frase y objeto de una frase preposicional.