Table of Contents
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.
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* pFontFamily;
FontStyle fontStyle;
int nfontSize;
const wchar_t* pszText;
Point ptDraw;
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);
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);
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));
vec.push_back(Color(0,0,255));
vec.push_back(Color(0,255,0));
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);
graphics.DrawImage(canvas, 0, 0, rect.Width(), rect.Height());
This is the full source code.
using namespace Gdiplus;
using namespace TextDesigner;
CPaintDC dc(this);
Graphics graphics(dc.GetSafeHdc());
graphics.SetSmoothingMode(SmoothingModeAntiAlias);
graphics.SetInterpolationMode(InterpolationModeHighQualityBicubic);
auto strategyOutline2 = Canvas::TextOutline(MaskColor::Blue(), MaskColor::Blue(), 8);
CRect rect;
GetClientRect(&rect);
Bitmap* canvas = Canvas::GenImage(rect.Width(), rect.Height(), Color::White, 0);
TextContext context;
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);
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;
DrawGradient grad;
Bitmap* bmpGrad = new Bitmap(right - left, bottom - top, PixelFormat32bppARGB);
using namespace std;
vector<Color> vec;
vec.push_back(Color(255,0,0));
vec.push_back(Color(0,0,255));
vec.push_back(Color(0,255,0));
grad.Draw(*bmpGrad, vec, true);
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));
Canvas::ApplyImageToMask(bmpGrad2, maskOutline2, canvas, MaskColor::Blue());
auto strategyOutline1 = Canvas::TextOutline(Color(255,255,255), Color(0,0,0), 4);
Canvas::DrawTextImage(strategyOutline1, canvas, Point(0,0), &context);
graphics.DrawImage(canvas, 0, 0, rect.Width(), rect.Height());
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.
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.
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.
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);
graphics.ScaleTransform(0.75, 1.0);
graphics.RotateTransform(-10.0);
DrawChar(x_offset, rect, context, graphics);
graphics.ResetTransform();
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);
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.
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.
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.
- 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.