轮廓文本 - 第二部分






4.92/5 (40投票s)
轮廓文本 第二部分
目录
引言
版本 2 中的新 Canvas
类可以完成 OutlineText
系列可以完成的所有工作,并且做得更多。之所以引入 Canvas
类,是因为我不可能在 OutlineText
中包含所有轮廓文本效果。使用 Canvas
,您可以混合搭配效果,仅受限于您的想象力。该库包含 C++ GDI+、C# GDI+ 和 C# WPF 代码及其示例。这里只讨论 C++ 版本,因为 C# GDI+ 版本代码大多相似。至于 C# WPF 版本,我相信大多数人会直接使用 WPF 的轮廓文本支持(本库基于此);库中没有任何内容是 WPF 文本轮廓无法实现的。
版本 2
新版本 2 包含三个带有 static
函数的辅助类。它们是 Canvas
、DrawGradient
和 MaskColor
。批量工作在 Canvas
中完成。DrawGradient
有一个函数用于绘制线性渐变,如下所示(colors
是一个颜色向量),MaskColor
定义了蒙版颜色的 Red
、Green
和 Blue
)。您在下面看到的 Bitmap
、Color
、FontFamily
、StringFormat
和 Point
是 GDI+ 类。
static bool Draw(Bitmap& bmp, const std::vector<Color>& colors, bool bHorizontal);
TextContext
结构用于传递有关渲染文本的信息。
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;
};
用户可以使用这些工厂方法来创建轮廓策略。
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);
一系列辅助 GenImage
函数,用于根据颜色或渐变生成图像
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);
生成基于策略的蒙版的功能
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 MeasureMaskLength( Bitmap* pMask, Color maskColor,
UINT& top, UINT& left, UINT& bottom, UINT& right);
之后,我们将图像(或颜色)应用于目标(pCanvas
),其中蒙版像素匹配 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);
如果不需要蒙版,用户可以选择直接将策略绘制到目标。当您使用图像作为文本主体或轮廓,而不是纯颜色时,请使用蒙版。
static bool DrawTextImage(ITextStrategy* pStrategy,
Bitmap* pImage, Point offset, TextContext* pTextContext);
static bool DrawTextImage(ITextStrategy* pStrategy,
Bitmap* pImage, Point offset, TextContext* pTextContext,
Matrix& transformMatrix);
第一个效果
为使文章简短,源代码将仅分步展示第一个效果。对于后面所有的效果,它们都使用了相同的几个函数。我更关心的是讲解通用理论,而不是如何用这个库具体实现。掌握了通用理论,您就可以用任何库来实现。
我们开始吧。我们生成一个蓝色的蒙版文本轮廓,并测量其左上角起始点和右下角结束点。
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;
接下来,使用 DrawGradient
辅助类,根据测量的起始点和结束点生成水平彩虹渐变。这不是普通的 RGB 彩虹,而是 RBG 顺序。
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);
由于 Canvas
类只能组合相同尺寸的图像,因此我们必须将渐变图像位图绘制到与目标相同尺寸的位图上。请注意,渐变稍微向右和向下移动到文本的起始位置。
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);
// Finally blit the rendered canvas onto the window
graphics.DrawImage(canvas, 0, 0, rect.Width(), rect.Height());
这是完整的源代码。
// 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;
本例子的示例代码可以在 AquarionXxx
项目中找到。之所以称为 Aquarion,是因为它是从同名机甲动漫中复制的。
伪边框
对于下一个例子,我们来做一个简单的效果。制作伪边框效果很容易。首先,我们将绘制一个 8 像素宽的大轮廓。
然后我们绘制一个亮色和一个暗色的 4 像素宽轮廓,它们分别轻微错位 (-4, -4) 和 (4, 4)。
最后,我们绘制一个没有轮廓的文本主体。
示例代码可在 C++ 和 C# FakeBezel 项目中找到。
伪 3D
我们将在此部分模拟一个正交 3D 效果。首先,我们做一个蓝色的蒙版轮廓。文本主体和轮廓颜色相同。
其次,我们对蓝色蒙版进行对角线位图绘制。
第三,我们将测量蓝色蒙版并生成此尺寸的渐变。
由于 Canvas
类只能组合相同尺寸的图像,因此我们将此渐变位图绘制到最终画布图像上。
对于渐变文本主体,我们也需要测量其范围,我们绘制一个没有轮廓的蓝色文本并进行测量。然后我们使用 GDI+ 根据测量结果生成渐变画笔。
我们将创建相同的文本,但这次使用渐变画笔(由 TextNoOutline
工厂方法提供),并将其绘制到画布上。
示例代码在 Fake3D
项目中。
伪 3D 第二部分
我们将使用这个AirBus Special字体。
上一个正交 3D 的问题是它们看起来不自然。在这个例子中,我们将斜体字体倾斜 10 度逆时针旋转,以获得更自然的外观。旋转是使用仿射变换完成的。
还记得第一篇文章中的旋转斜体文本吗?为了达到预期的效果,我们必须逐个字符重复此效果,而不是应用于整个 string
,因为那样的话整个 string
会向上倾斜。
首先,我们将绘制一个蓝色的文本主体作为蒙版。
然后继续对该蒙版进行对角线位图绘制并进行测量。
在将渐变应用于蒙版并绘制了渐变文本主体之后,在进行变换之前,字符的外观如下。
经过变换后,最终文本看起来更细长,因为我们在 x 轴上将其缩放得更小。
这些是每个字符的仿射变换。
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();
}
将仿射变换应用于图像位图绘制时的质量问题
将仿射变换应用于图像位图绘制
将仿射变换应用于文本生成。垂直笔画现在看起来更好了。
为了解决将仿射变换应用于图像位图绘制时出现的垂直笔画质量问题,提供了带有 transformMatrix
参数的 GenMask
和 DrawTextImage
的重载版本,以将仿射变换应用于文本生成。不幸的是,这在 WPF 库中无效,原因不明,因为变换应用了两次,所以 WPF 演示保持不变。
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);
快乐就好
最后一个例子不属于任何类别。我称之为“快乐就好”,是我个人希望每个人每天都快乐。这个效果是从 ALBA 字体复制的,我们将用另一种类似的字体来复制它。这是我们将用于此效果的字体。
首先,我们绘制内部绿色文本,轮廓宽度为 16 像素。
然后我们将绿色轮廓向下“拖动”了 5 像素,虽然不是很明显。
接下来是内部 8 像素宽的白色轮廓文本。同样,我们将其向下“拖动”了。
最后,我们绘制绿色文本(带 1 像素轮廓)。
代码可在 BeHappyXxx
项目中找到,供任何希望仔细研究代码的人参考。
内部渐变
Text Designer 2.1.0 beta 现在支持文本主体内的渐变,使用了 TextGradOutlineLastStrategy
类,该类最后渲染渐变轮廓,而 TextGradOutlineStrategy
则先渲染轮廓,文本主体最后渲染。这 2 个类除了线性渐变外,还支持正弦渐变生成。不支持椭圆渐变,因为效果不佳。目前,正弦渐变生成没有伽马校正,我希望在不久的将来能修复它。
渲染上述效果的第一步是使用 TextGradOutlineStrategy
在我们的画布位图上渲染蓝色线性渐变轮廓。选择线性还是正弦渐变取决于颜色和轮廓宽度的实验。
接下来,我们生成文本主体的蓝色蒙版。
这是带有粉黄色正弦渐变轮廓的文本主体,使用了 TextGradOutlineLastStrategy
。
最后,我们将粉黄色轮廓与蓝色蒙版一起应用到画布位图上。最终结果如下。
演示代码可在 InnerOutlineXxx
项目中找到。此演示不支持 WPF,因为它总是最后渲染文本主体。
结论
我们已经了解了如何使用新的 Canvas
类实现五种不同的效果。源代码中有两种效果未在文章中展示。我鼓励您阅读和探索代码。如果您有任何不理解的步骤,您可以随时保存该步骤生成的中间位图,并在 MS Paint 中自行查看。源代码托管在 Codeplex。未来,我更倾向于开发基于 Freetype 的可移植库。但在那之前,将先发布 DirectWrite 版本。敬请期待更多示例和教程。
由于 Codeplex 已关闭,存储库已从 Codeplex 迁移到 GitHub。
历史
- 2015-01-14:初次发布
- 2015-11-12:版本 2 预览 7 添加了带有
transformMatrix
参数的重载GenMask
和DrawTextImage
,以及ApplyShadowToMask
来生成阴影图像。 伪 3D 第二部分 已更新。 - 2017-10-29:版本 2.1.0 Beta,新增内部渐变轮廓效果。
- 2018-08-14:UWP Win2D 0.5.0 Beta 仅支持
DirectXPixelFormat.B8G8R8A8UIntNormalized
。GDI+ 版本 2.1.1 Beta,对将部分数组索引计算移出内循环进行了少量优化。