Click here to Skip to main content
14,357,676 members

Outline Text - Part 2

Rate this:
4.92 (38 votes)
Please Sign up or sign in to vote.
4.92 (38 votes)
13 Aug 2018CPOL
Outline Text Part 2

Image 1

Image 2

Image 3

Image 4

Image 5

Image 6

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

Image 7

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.

Image 8

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.

Image 9

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.

Image 10

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.

Image 11

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

Finally, we draw a white text with black outline.

Image 12

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

Image 13

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.

Image 14

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

Image 15

Image 16

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

Image 17

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

Fake 3D

Image 18

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.

Image 19

Secondly, we blitted the blue mask diagonally.

Image 20

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

Image 21

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

Image 22

Image 23

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.

Image 24

Image 25

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.

Image 26

Sample code is in the Fake3D projects.

Fake 3D Part 2

Image 27

This is the AirBus Special font we will use.

Image 28

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.

Image 29

Image 30

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.

Image 31

Then proceed to blit this mask diagonally and measure it.

Image 32

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

Image 33

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

Image 34

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:

Image 35

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

Image 36

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

Image 37

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.

Image 38

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

Image 39

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

Image 40

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

Image 41

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

Image 42

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

Inner Gradient

Image 43

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.

Image 44

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.

Image 45

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

Image 46

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

Image 47

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

Image 48

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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Shao Voon Wong
Software Developer (Senior)
Singapore Singapore
Shao Voon is from Singapore. CodeProject awarded him a MVP award in recognition of his article contributions in 2019. In his spare time, he prefers to writing application based on 3rd party library than writing his own library. His interest lies primarily in computer graphics, software optimization, security and Agile methodologies.

You can reach him by sending a message on CodeProject or at his Coding Tidbit Blog!

Comments and Discussions

 
QuestionRTF Text to transparent background PNG image or on GDI+ graphics Pin
soorya prakash19-Mar-18 14:50
membersoorya prakash19-Mar-18 14:50 
QuestionNew Project Hosting Site? Pin
robertjb2029-Oct-17 14:15
professionalrobertjb2029-Oct-17 14:15 
AnswerRe: New Project Hosting Site? Pin
Shao Voon Wong29-Oct-17 19:22
mvaShao Voon Wong29-Oct-17 19:22 
Praisevery nice Pin
BillW3323-Sep-16 6:29
professionalBillW3323-Sep-16 6:29 
Question+5 Great code/articles Pin
osstekz12-Apr-16 7:12
memberosstekz12-Apr-16 7:12 
GeneralMy vote of 5 Pin
VivianMC12-Nov-15 0:56
memberVivianMC12-Nov-15 0:56 
GeneralMy vote of 4 Pin
ToothRobber19-Jan-15 7:02
professionalToothRobber19-Jan-15 7:02 
QuestionCool Pin
yarp13-Jan-15 21:02
memberyarp13-Jan-15 21:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 12 Apr 2016

Stats

38.1K views
6.5K downloads
55 bookmarked