Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Outline Text - Part 2

0.00/5 (No votes)
13 Aug 2018 2  
Outline Text Part 2

Table of Contents

Introduction

The new Canvas class in version 2 can accomplish whatever the OutlineText family can do, plus much more. The reason for the Canvas class is that I cannot possibly put every outline text effect inside OutlineText. With Canvas, you can mix and match the effect, limited only by your imagination. The library contains C++ GDI+, C# GDI+ and C# WPF code and their examples. Only the C++ version will be discussed here because the C# GDI+ version code is mostly similar. As for C# WPF version, I believe most would directly use the WPF outline text support (which this library is based on); there is nothing in the library that the WPF text outline cannot achieve.

Version 2

The new version 2 includes a three helper class with static functions. They are Canvas, DrawGradient and MaskColor. Bulk work is done in Canvas. DrawGradient has one function to draw linear gradients as shown below (colors is a vector of colors), MaskColor defines Red, Green and Blue for mask colors). The Bitmap, Color, FontFamily, StringFormat and Point you see below are GDI+ classes.

static bool Draw(Bitmap& bmp, const std::vector<Color>& colors, bool bHorizontal);

A TextContext structure is to pass information about the rendered text.

struct TextContext
{
    //! fontFamily is the font family
    FontFamily* pFontFamily;
    //! fontStyle is the font style, eg, bold, italic or bold
    FontStyle fontStyle;
    //! nfontSize is font size
    int nfontSize;
    //! pszText is the text to be displayed
    const wchar_t* pszText;
    //! ptDraw is the point to draw
    Point ptDraw;
    //! strFormat is the string format
    StringFormat strFormat;
};

The user can use these factory methods to create outline strategies.

static ITextStrategy* TextGlow(Color clrText, Color clrOutline, int nThickness);

static ITextStrategy* TextGlow(Brush* pbrushText, Color clrOutline, int nThickness);

static ITextStrategy* TextOutline(Color clrText, Color clrOutline, int nThickness);

static ITextStrategy* TextOutline(Brush* pbrushText, Color clrOutline, int nThickness);

static ITextStrategy* TextGradOutline(Color clrText, Color clrOutline1, Color clrOutline2, 
	int nThickness);

static ITextStrategy* TextGradOutline(Brush* pbrushText, Color clrOutline1, Color clrOutline2, 
	int nThickness);

static ITextStrategy* TextNoOutline(Color clrText);

static ITextStrategy* TextNoOutline(Brush* pbrushText);

static ITextStrategy* TextOnlyOutline(Color clrOutline, int nThickness, bool bRoundedEdge);

A bunch of helper GenImage functions to generate image based on color or gradient:

static Bitmap* GenImage(int width, int height); // transparent image

static Bitmap* GenImage(int width, int height, std::vector<Color>& vec, bool bHorizontal);

static Bitmap* GenImage(int width, int height, Color clr);

static Bitmap* GenImage(int width, int height, Color clr, BYTE alpha=0xff);

Functions to generate a mask based on the strategy:

static Bitmap* GenMask(
    ITextStrategy* pStrategy, 
    int width, 
    int height, 
    Point offset,
    TextContext* pTextContext);
	
static Bitmap* GenMask(
    ITextStrategy* pStrategy, 
    int width, 
    int height, 
    Point offset,
    TextContext* pTextContext,
	Matrix& transformMatrix);

After generating a mask, we usually need to measure its top-left starting point and bottom-right ending point.

static bool MeasureMaskLength( Bitmap* pMask, Color maskColor,
    UINT& top, UINT& left, UINT& bottom, UINT& right);

After that, we will apply an image (or color) to destination (pCanvas) where the mask pixel matches the maskColor.

static bool ApplyImageToMask(Bitmap* pImage, Bitmap* pMask, 
    Bitmap* pCanvas, Color maskColor);

static bool ApplyColorToMask(Color clr, Bitmap* pMask, 
    Bitmap* pCanvas, Color maskColor);

static bool ApplyColorToMask(Color clr, Bitmap* pMask, 
    Bitmap* pCanvas, Color maskColor, Point offset);
	
static bool ApplyShadowToMask(Color clrShadow, Bitmap* pMask, 
    Bitmap* pCanvas, Color maskColor);

static bool ApplyShadowToMask(Color clrShadow, Bitmap* pMask, 
    Bitmap* pCanvas, Color maskColor, Point offset);

If one has no use for mask, he/she can opt to draw the strategy directly to the destination. Use mask if you have an image to use for the text body or outline, instead of a plain color.

static bool DrawTextImage(ITextStrategy* pStrategy, 
    Bitmap* pImage, Point offset, TextContext* pTextContext);
	
static bool DrawTextImage(ITextStrategy* pStrategy, 
    Bitmap* pImage, Point offset, TextContext* pTextContext,
	Matrix& transformMatrix);

First Effect

To keep the article short, the source code will only be shown step by step for the 1st effect. For the rest of the effects that followed, they are using the same few functions. I am more concerned about teaching the general theory than how to do it specifically with the library. With the general theory in hand, we can do it with any library.

Let us begin. We generate a blue mask text outline and measure its top-left starting point and bottom-right ending point.

auto strategyOutline2 = Canvas::TextOutline(MaskColor::Blue(), MaskColor::Blue(), 8);

Bitmap* maskOutline2 = Canvas::GenMask(strategyOutline2, rect.Width(), rect.Height(), Point(0,0), &context);

UINT top = 0;
UINT bottom = 0;
UINT left = 0;
UINT right = 0;
Canvas::MeasureMaskLength(maskOutline2, MaskColor::Blue(), top, left, bottom, right);
right += 2;
bottom += 2;

Next, using the DrawGradient helper class, generate the horizontal rainbow gradient according to the measured starting and ending point. It is not usual RGB rainbow but in RBG sequence.

DrawGradient grad;
Bitmap* bmpGrad = new Bitmap(right - left, bottom - top, PixelFormat32bppARGB);

using namespace std;
vector<Color> vec;
vec.push_back(Color(255,0,0)); // Red
vec.push_back(Color(0,0,255)); // Blue
vec.push_back(Color(0,255,0)); // Green
grad.Draw(*bmpGrad, vec, true);

Because the Canvas class can only combine images of the same dimensions, we must blit the gradient image to bitmap with the same dimension as the destination. Notice the gradient shifted a little right and down to the starting position of the text.

Bitmap* bmpGrad2 = new Bitmap(rect.Width(), rect.Height(), PixelFormat32bppARGB);
Graphics graphGrad(bmpGrad2);
graphGrad.SetSmoothingMode(SmoothingModeAntiAlias);
graphGrad.SetInterpolationMode(InterpolationModeHighQualityBicubic);
graphGrad.DrawImage(bmpGrad, (int)left, (int)top, (int)(right - left), (int)(bottom - top));

Then blit the gradient only when the mask pixel is blue. We get the result below.

Canvas::ApplyImageToMask(bmpGrad2, maskOutline2, canvas, MaskColor::Blue());

Finally, we draw a white text with black outline.

auto strategyOutline1 = Canvas::TextOutline(Color(255,255,255), Color(0,0,0), 4);
Canvas::DrawTextImage(strategyOutline1, canvas, Point(0,0), &context);

// Finally blit the rendered canvas onto the window
graphics.DrawImage(canvas, 0, 0, rect.Width(), rect.Height());

This is the full source code.

// Create rainbow Text Effect in Aquarion EVOL anime
using namespace Gdiplus;
using namespace TextDesigner;
CPaintDC dc(this);
Graphics graphics(dc.GetSafeHdc());
graphics.SetSmoothingMode(SmoothingModeAntiAlias);
graphics.SetInterpolationMode(InterpolationModeHighQualityBicubic);

// Create the outline strategy which is used later on for measuring 
// the size of text in order to generate a correct sized gradient image
auto strategyOutline2 = Canvas::TextOutline(MaskColor::Blue(), MaskColor::Blue(), 8);

CRect rect;
GetClientRect(&rect);
Bitmap* canvas = Canvas::GenImage(rect.Width(), rect.Height(), Color::White, 0);

// Text context to store string and font info to be sent as parameter to Canvas methods
TextContext context;

// Load a font from its file into private collection, 
// instead of from system font collection
//=============================================================
Gdiplus::PrivateFontCollection fontcollection;

CString szFontFile = L"..\\CommonFonts\\Ruzicka TypeK.ttf";

Gdiplus::Status nResults = fontcollection.AddFontFile(szFontFile);
FontFamily fontFamily;
int nNumFound=0;
fontcollection.GetFamilies(1,&fontFamily,&nNumFound);

context.pFontFamily = &fontFamily;
context.fontStyle = FontStyleRegular;
context.nfontSize = 36;

context.pszText = L"I cross over the deep blue void";
context.ptDraw = Point(0, 0);

// Generate the mask image for measuring the size of the text image required
//============================================================================
Bitmap* maskOutline2 = Canvas::GenMask
(strategyOutline2, rect.Width(), rect.Height(), Point(0,0), &context);

UINT top = 0;
UINT bottom = 0;
UINT left = 0;
UINT right = 0;
Canvas::MeasureMaskLength(maskOutline2, MaskColor::Blue(), top, left, bottom, right);
right += 2;
bottom += 2;

// Generate the gradient image
//=============================
DrawGradient grad;
Bitmap* bmpGrad = new Bitmap(right - left, bottom - top, PixelFormat32bppARGB);
using namespace std;
vector<Color> vec;
vec.push_back(Color(255,0,0)); // Red
vec.push_back(Color(0,0,255)); // Blue
vec.push_back(Color(0,255,0)); // Green
grad.Draw(*bmpGrad, vec, true);

// Because Canvas::ApplyImageToMask requires the all images to have equal dimension,
// we need to blit our new gradient image onto a larger image to be same size as canvas image
//==============================================================================================
Bitmap* bmpGrad2 = new Bitmap(rect.Width(), rect.Height(), PixelFormat32bppARGB);
Graphics graphGrad(bmpGrad2);
graphGrad.SetSmoothingMode(SmoothingModeAntiAlias);
graphGrad.SetInterpolationMode(InterpolationModeHighQualityBicubic);
graphGrad.DrawImage(bmpGrad, (int)left, (int)top, (int)(right - left), (int)(bottom - top));

// Apply the rainbow text against the blue mask onto the canvas
Canvas::ApplyImageToMask(bmpGrad2, maskOutline2, canvas, MaskColor::Blue());

// Draw the (white body and black outline) text onto the canvas
//==============================================================
auto strategyOutline1 = Canvas::TextOutline(Color(255,255,255), Color(0,0,0), 4);
Canvas::DrawTextImage(strategyOutline1, canvas, Point(0,0), &context);

// Finally blit the rendered canvas onto the window
graphics.DrawImage(canvas, 0, 0, rect.Width(), rect.Height());

// Release all the resources
//============================
delete bmpGrad;
delete bmpGrad2;
delete canvas;

delete maskOutline2;
delete strategyOutline2;
delete strategyOutline1;

The sample code for this example can be found in the AquarionXxx projects. It is called Aquarion because it is copied from the mecha anime of the same name.

Fake Bezel

For the next example we go with an easy effect. It is easy to create a fake bezel effect. First, we will draw a big outline of 8 pixel width.

Then we draw one bright and one dark outline of width, 4 pixel which are slightly misplaced by (-4, -4) and (4, 4) respectively.

To finish up, we draw a text body with no outline.

The sample code is available in the C++ and C# FakeBezel projects.

Fake 3D

We will fake an Orthogonal 3D effect in this section. Firstly, we do a blue masked outline. The text body and outline has the same color.

Secondly, we blitted the blue mask diagonally.

Thirdly, we will measure the blue mask and generate a gradient of this size.

Because the Canvas class can only combine images of the same dimension, we will blit this gradient to the final canvas image.

For the gradient text body, we need to measure its extent as well, we draw a blue text with no outline and measure it. Then we use the GDI+ to generate the gradient brush according to the measurements.

We will create the same text but this time with a gradient brush (This is provided by the TextNoOutline factory method) and draw onto the canvas.

Sample code is in the Fake3D projects.

Fake 3D Part 2

This is the AirBus Special font we will use.

The problem with the previous Orthogonal 3D is that they looked unnatural. For this example, we tilt italics font by rotating 10 degree counter-clockwise to achieve a slightly more natural look. Rotation is done using affine transform.

Remember the rotated italics text from the first article? To achieve the intended effect, we have to repeat this effect character by character, instead of applying to the whole string because the whole string will slant upwards if we do that.

To start, we will draw a blue text body for mask.

Then proceed to blit this mask diagonally and measure it.

After applying the gradient to the mask and drawing the gradient text body, the character would look below before the transforms.

After the transforms, the final text look leaner because we scale it to be smaller in the x-axis.

These are the affine transforms for each character.

CString text = L"PENTHOUSE";
int x_offset = 0;
float y_offset = 0.0f;

for(int i=0; i<text.GetLength(); ++i)
{
    CString str = L"";
    str += text.GetAt(i);
    context.pszText = str.GetBuffer(1);

    // Scale to be leaner
    graphics.ScaleTransform(0.75, 1.0);
    // Rotate 10 degrees counter-clockwise.
    graphics.RotateTransform(-10.0);
    DrawChar(x_offset, rect, context, graphics);
    graphics.ResetTransform();
    // shift down for the 2nd char onwards
    y_offset += 7.0f;
    graphics.TranslateTransform(0.0, y_offset);

    str.ReleaseBuffer();
}

Quality Issues With Applying Affine Transform to Image Blit

Applying affine transform to image blit:

Applying affine transform to text generation. Vertical strokes looks better now.

To resolve the vertical stroke quality issues of affine transformation to image blit, overloaded versions of GenMask and DrawTextImage with transformMatrix parameter are provided to apply affine transform to the text generation instead. Unfortunately, this cannot work in WPF library for unknown reasons that transforms are applied twice, so the WPF demo is unchanged.

static Bitmap* GenMask(
    ITextStrategy* pStrategy, 
    int width, 
    int height, 
    Point offset,
    TextContext* pTextContext);
	
static Bitmap* GenMask(
    ITextStrategy* pStrategy, 
    int width, 
    int height, 
    Point offset,
    TextContext* pTextContext,
	Matrix& transformMatrix);
static bool DrawTextImage(ITextStrategy* pStrategy, 
    Bitmap* pImage, Point offset, TextContext* pTextContext);
	
static bool DrawTextImage(ITextStrategy* pStrategy, 
    Bitmap* pImage, Point offset, TextContext* pTextContext,
	Matrix& transformMatrix);

Be Happy

The last example does not fall under any category. I just call it "Be Happy," a personal wish for everyone to be happy everyday. This effect is copied from the ALBA font which we will duplicate with another similar font. This is the font we will use for the effect.

Firstly, we draw the inner green text with outline width of 16 pixels.

Then we 'dragged' the green outline by 5 pixels downwards though it is not obvious.

The inner white outline text of 8 pixel width, is next. Similarly, we 'dragged' it downwards.

Finally, we draw the green text(with 1 pixel outline).

The code can be found in the BeHappyXxx project for anyone wishes to peruse the code.

Inner Gradient

Text Designer 2.1.0 beta now supports inner gradient on the text body with the TextGradOutlineLastStrategy class which render the gradient outline last, as opposed to TextGradOutlineStrategy rendering the outline first and text body last. These 2 class support sinusoid gradient generation in addition to linear gradient. Ellipse gradient is not supported as the effect does not look nice. Right now, sinusoid gradient generation does not have gamma correction which I hope to fix in near future.

The 1st step to render the above effect is render the blue linear gradient outline with TextGradOutlineStrategy on our canvas bitmap. To choose between linear and sinusoid gradient is a matter of experimentation with the colors and outline width.

Next, we generate the blue mask for the text body.

This is the text body with pinkish-yellow sinusoid gradient outline with TextGradOutlineLastStrategy

Lastly we apply the pinkish-yellow outline with the blue mask to the canvas bitmap. The final result is below.

The demo code can be found in the InnerOutlineXxx project. WPF is not supported for this demo as it always render the text body last.

Conclusion

We have seen how to do five different effects with the new Canvas class. There are two effects in the source code not shown in the article. I encourage you to read and explore the code. If there are any steps you cannot understand, you can always save the intermediate bitmap generated by that step and see for yourself in MS Paint. The source code is hosted at Codeplex. As for the future, I am more geared towards a portable library based on Freetype. But before that, DirectWrite version will be released first. Expect more examples and tutorials.

Repository is moved from Codeplex to GitHub because Codeplex is closed down.

History

  • 2015-01-14: Initial release
  • 2015-11-12: Version 2 Preview 7 added overloaded GenMask and DrawTextImage with transformMatrix parameter, and ApplyShadowToMask to generate shadow image. Fake 3D Part 2 section is updated.
  • 2017-10-29: Version 2.1.0 Beta with inner gradient outline effect.
  • 2018-08-14: UWP Win2D 0.5.0 Beta support only DirectXPixelFormat.B8G8R8A8UIntNormalized. GDI+ Version 2.1.1 Beta with minor optimization of moving part of array index computation out from inner loop.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here