DirectX 因素

随心使用字符边框几何图形

Charles Petzold

下载代码示例

在个人计算机上数字排版中最重要的进展至轮廓字体位图字体发生超过 20 年前与开关。在 Windows 3.1 之前的 Windows 版本,屏幕上的文本是从组成的小位图的特定点大小的字体文件生成的。这些位图可扩大为介于两者之间的大小,但不是能没有保真损失。

Adobe 系统公司开创了一种方法来显示计算机字体与 PostScript,定义字体字符组成的直线和贝塞尔曲线的图形轮廓。您可以缩放到任何维度和算法"提示",帮助保持保真度的小点大小在这些轮廓。作为 PostScript 字体在个人计算机屏幕上,苹果公司的替代开发的 TrueType 字体规范,后来通过 Microsoft。最终演变成今天的共同 OpenType 标准。

这些天,我们理所当然屏幕上的字体,以及能力旋转或倾斜使用图形转换文本连续的可的伸缩性。然而它也是可能获得的实际几何定义的这些字体字符轮廓和使用他们的不寻常的用途,例如大纲显示文本字符,或剪切,或执行非线性变换。

从对剪切几何形状字体

如果你想要获得字符轮廓几何形状在 Windows 存储应用程序中,不会帮助 Windows 运行时 API。你得使用 DirectX。IDWriteFontFace 的 GetGlyphRunOutline 方法将字符轮廓写入到 IDWriteGeometrySink (这是相同,ID2D1SimplifiedGeometrySink) 的定义 (或有助于) 一个 ID2D1PathGeometry 对象。

图 1 命名我该模板创建的 DirectX App (XAML) 的 ClipToText Windows 8.1 应用程序中显示呈现类的构造函数。该项目包括可发布的 Miramonte 加粗的字体文件,和的代码演示如何将转换标志符号运行路径几何图形。像往常一样,我已删除了四处游荡的 HRESULT 值为清楚起见的检查。

图 1 转换标志符号运行到路径几何图形

 

ClipToTextRenderer::ClipToTextRenderer(
  const std::shared_ptr<DeviceResources>& deviceResources) :
  m_deviceResources(deviceResources)
{
  // Get font file
  ComPtr<IDWriteFactory> factory = m_deviceResources->GetDWriteFactory();
  String^ filePath = Package::Current->InstalledLocation->Path +
     "\\Fonts\\Miramob.ttf";
  ComPtr<IDWriteFontFile> fontFile;
  factory->CreateFontFileReference(filePath->Data(), 
    nullptr, &fontFile);
  // Get font face
  ComPtr<IDWriteFontFace> fontFace;
  factory->CreateFontFace(DWRITE_FONT_FACE_TYPE_TRUETYPE,
                          1,
                          fontFile.GetAddressOf(),
                          0,
                          DWRITE_FONT_SIMULATIONS_NONE,
                          &fontFace);
  // Create path geometry and open it
  m_deviceResources->GetD2DFactory()->CreatePathGeometry(&m_clipGeometry);
  ComPtr<ID2D1GeometrySink> geometrySink;
  m_clipGeometry->Open(&geometrySink);
  // Get glyph run outline ("CLIP")
  uint16 glyphIndices [] = { 0x0026, 0x002F, 0x002C, 0x0033 };
  float emSize = 96.0f;
      // 72 points, arbitrary in this program
  fontFace->GetGlyphRunOutline(emSize,
                               glyphIndices,
                               nullptr,
                               nullptr,
                               ARRAYSIZE(glyphIndices),
                               false,
                               false,
                               geometrySink.Get());
  // Don't forget to close the geometry sink!
  geometrySink->Close();
  CreateDeviceDependentResources();
}

虽然中的代码图 1 获取 IDWriteFontFace 对象从私人加载字体,应用程序还可以获取字体的脸对象从字体集合,包括系统字体集合中的字体。 中的代码图 1 指定标志符号索引明确对应的文本"剪辑,"但您也可以从使用 GetGlyphIndices 方法为 Unicode 字符的字符串派生标志符号索引。

一旦您已经创建一个 ID2D1PathGeometry 对象,您可以使用它为灌装 (结果看起来就像这种情况下呈现的文本),绘图 (其中呈现只是轮廓),或修剪。 图 2 显示的缩放和平移要定义一个裁剪区域,涵盖整个显示区域的路径几何图形的渲染方法。 请记住路径几何图形有消极和积极的坐标。 (0,0) 的路径几何图形起源对应于在开始运行的标志符号的基线。

剪切路径几何图形与图 2

bool ClipToTextRenderer::Render()
{
  if (!m_needsRedraw)
    return false;
  ID2D1DeviceContext* context = m_deviceResources->GetD2DDeviceContext();
  Windows::Foundation::Size outputBounds = m_deviceResources->GetOutputBounds();
  context->SaveDrawingState(m_stateBlock.Get());
  context->BeginDraw();
  context->Clear(ColorF(ColorF::DarkBlue));
  // Get the clip geometry bounds
  D2D_RECT_F geometryBounds;
  m_clipGeometry->GetBounds(D2D1::IdentityMatrix(), &geometryBounds);
  // Define transforms to center and scale clipping geometry
  Matrix3x2F orientationTransform =
     m_deviceResources->GetOrientationTransform2D();
  Matrix3x2F translateTransform =
     Matrix3x2F::Translation(SizeF(-geometryBounds.left, -geometryBounds.top));
  float scaleHorz = outputBounds.Width / 
                    (geometryBounds.right - geometryBounds.left);
  float scaleVert = outputBounds.Height / 
                    (geometryBounds.bottom - geometryBounds.top);
  Matrix3x2F scaleTransform = Matrix3x2F::Scale(SizeF(scaleHorz, scaleVert));
  // Set the geometry for clipping
  ComPtr<ID2D1Layer> layer;
  context->CreateLayer(&layer);
  context->PushLayer(
    LayerParameters(InfiniteRect(),
                    m_clipGeometry.Get(),
                    D2D1_ANTIALIAS_MODE_PER_PRIMITIVE,
                    translateTransform * scaleTransform
                         * orientationTransform), layer.Get());
  // Draw lines radiating from center
  translateTransform = Matrix3x2F::Translation(outputBounds.Width / 2,
                         outputBounds.Height / 2);
  for (float angle = 0; angle < 360; angle += 1)
  {
    Matrix3x2F rotationTransform = Matrix3x2F::Rotation(angle);
    context->SetTransform(rotationTransform * translateTransform * 
                          orientationTransform);
    context->DrawLine(Point2F(0, 0),
                      Point2F(outputBounds.Width / 2, outputBounds.Height / 2),
                      m_whiteBrush.Get(), 2);
  }
  context->PopLayer();
  HRESULT hr = context->EndDraw();
  if (hr != D2DERR_RECREATE_TARGET)
  {
    DX::ThrowIfFailed(hr);
  }
  context->RestoreDrawingState(m_stateBlock.Get());
  m_needsRedraw = false;
  return true;
}

Render 方法然后绘制一系列的线条,从屏幕上,创建的图像所示的中心辐射图 3

The ClipToText Display
图 3 ClipToText 显示

几何定义的更深处

一般来说,路径几何图形是的集合的数字,其中每个是已连接的网段的集合。 这些段采取直线、 二次、 三次贝塞尔曲线和弧线 (这是在一个椭圆的圆周上的曲线) 的形式。 图可以关闭,或者关闭,在这种情况下,终结点是连接到开始点,或打开。

GetGlyphRunOutline 方法将字形轮廓写到 IDWriteGeometrySink,这是 ID2D1SimplifiedGeometrySink 相同。 这反过来又对定期 ID2D1GeometrySink 的父类别。 使用 ID2D1SimplifiedGeometry­而不是 ID2D1GeometrySink 接收器意味着最终的路径几何图形包含完全由直线和立方贝塞尔曲线组成的数字 — — 没有二次贝塞尔曲线和无弧。

为字体字符轮廓,这些细分市场始终关闭 — — 就是图的终结点连接到起始点。 在"剪贴"由五个数字组成的字符的 ClipToText 程序中创建的路径几何图形 — — 每一项的第三个字母和两个最后一封信要考虑内部的上半部分的体育一图

也许你会喜欢对实际线路和贝塞尔曲线组成的路径几何图形,所以你可以操纵它们奇怪和不寻常的方式访问。 首先,这不可能。 一旦已使用数据初始化一个 ID2D1PathGeometry 对象,该对象是不可变的和该接口提供没有办法可以获取内容。

不过有一个解决方案:您可以编写您自己的类,实现 ID2D1SimplifiedGeometrySink 接口,并将该类的实例传递给 GetGlyphRunOutline 方法。 您的 ID2D1SimplifiedGeometrySink 的自定义实现必须包含名为 BeginFigure、 AddLines、 AddBeziers 和 EndFigure (及其他几个) 的方法。 在这些方法中,您可以保存整个路径几何在一棵树的结构,可以定义。

这是自己做了什么。 我定义的内容保存的路径几何图形的结构所示图 4。 这些结构显示路径几何图形是对象的集合的路径图,如何和每个路径图是一个已连接的网段组成的直线和立方贝塞尔曲线的集合。

图 4 结构的内容保存的路径几何图形

struct PathSegmentData
{
  bool IsBezier;
  std::vector<D2D1_POINT_2F> Points;
          // for IsBezier == false
  std::vector<D2D1_BEZIER_SEGMENT> Beziers;
   // for IsBezier == true
};
struct PathFigureData
{
  D2D1_POINT_2F StartPoint;
  D2D1_FIGURE_BEGIN FigureBegin;
  D2D1_FIGURE_END FigureEnd;
  std::vector<PathSegmentData> Segments;
};
struct PathGeometryData
{
  D2D1_FILL_MODE FillMode;
  std::vector<PathFigureData> Figures;
  Microsoft::WRL::ComPtr<ID2D1PathGeometry>
  GeneratePathGeometry(ID2D1Factory * factory);
};

我实现的 ID2D1SimplifiedGeometrySink 被称为 InterrogableGeometrySink,因为它包含一个方法,返回的结果作为 PathGeometryData 对象几何而得名。 最有趣的部分,InterrogableGeometrySink 所示图 5

图 5 大部分的 InterrogableGeometrySink 类

void InterrogableGeometrySink::BeginFigure(D2D1_POINT_2F startPoint,
                    D2D1_FIGURE_BEGIN figureBegin)
{
  m_pathFigureData.StartPoint = startPoint;
  m_pathFigureData.FigureBegin = figureBegin;
  m_pathFigureData.Segments.clear();
}
void InterrogableGeometrySink::AddLines(const D2D1_POINT_2F *points,
                UINT pointsCount)
{
  PathSegmentData polyLineSegment;
  polyLineSegment.IsBezier = false;
  polyLineSegment.Points.assign(points, points + pointsCount);
  m_pathFigureData.Segments.push_back(polyLineSegment);
}
void InterrogableGeometrySink::AddBeziers(const D2D1_BEZIER_SEGMENT *beziers,
                   UINT beziersCount)
{
  PathSegmentData polyBezierSegment;
  polyBezierSegment.IsBezier = true;
  polyBezierSegment.Beziers.assign(beziers, beziers + beziersCount);
  m_pathFigureData.Segments.push_back(polyBezierSegment);
}
void InterrogableGeometrySink::EndFigure(D2D1_FIGURE_END figureEnd)
{
  m_pathFigureData.FigureEnd = figureEnd;
  m_pathGeometryData.Figures.push_back(m_pathFigureData);
}
HRESULT InterrogableGeometrySink::Close()
{
  // Assume that the class accessing the geometry sink knows what it's doing
  return S_OK;
}
// Method for this implementation
PathGeometryData InterrogableGeometrySink::GetPathGeometryData()
{
  return m_pathGeometryData;
}

只需将 InterrogableGeometrySink 的实例传递到 GetGlyphRunOutline 来获取所描述字符轮廓的 PathGeometryData 对象。 PathGeometryData 还包含一个名为 GeneratePathGeometry,它使用的图形和线段树来创建一个 ID2D1PathGeometry 对象,然后可以使用绘图、 填充或修剪方法。 不同的是在调用 GeneratePathGeometry 之前, 您的程序可以修改的线和贝塞尔曲线段上的点。 您甚至可以添加或删除部分或数字。

InterrogableGeometrySink 类和支撑结构是一个名为 RealTextEditor ; 项目的一部分 "真实"是指您可以编辑文本轮廓而不是文本本身。 当该程序后时,它将显示大字符"DX"。点击或单击屏幕切换编辑模式。 在编辑模式中,列出了字符和显示圆点。

绿色圆点标志的起点和线段和贝塞尔线段的端点。 红色的点是贝塞尔控制点。 管制站都连接到相应的终结点与红线。 你可以拿这些用鼠标点 — — 他们的手指有点太小 — — 并拖动它们,扭曲的文本字符以奇怪的方式,作为图 6 演示。

Modified Character Outlines in RealTextEditor
图 6 修改 RealTextEditor 中的字符轮廓

RealTextEditor 有没有设施要保存您的自定义字符几何图形,但你肯定可以添加一个。 此程序的意图并不真的编辑字体字符,而是要清楚地说明了如何由一系列直线和贝塞尔曲线定义字体字符连接成封闭的数字 — — 在这种情况下三个数字,两个内部和外部 D,另一个用于 X。

算法的操作

一旦你有一个路径几何定义结构,如 PathGeometryData、 PathFigureData 和 PathSegmentData 的形式,你也可以操纵个别点通过算法,扭曲和车削中字符任何方式你请或许创建图像,例如如中所示的图 7

The OscillatingText Program
图 7 OscillatingText 程序

好吧,不完全正确。 中所示的图像图 7 是不可能使用 PathGeometryData 对象生成的 Interrogable­GeometrySink 类只是给你。 在许多简单无衬线字体中,资本 H 包括 12 点由直线连接。 如果你只处理那些点,没有办法可以修改他们所以 H 之间的直线变成曲线。

但是,您可以解决这个问题的增强版本,称为 InterpolatableGeometrySink 的 InterrogableGeometrySink。 每当此新类时遇到的 AddLines 方法中的一条直线,它分成多个较小的行的那条线。 (您可以控制此功能与构造函数参数)。其结果是完全玛钢路径的几何定义。

负责中的图像的 OscillatingText 程序图 7 其实摇摆的字符来来回回,内部多像草裙舞。 这种算法是在呈现类的更新方法中实现的。 保留的 PathGeometryData 两个副本:(作为"src"标识) 的源描述原始文本轮廓,和目的地 ("dst") 包含基于算法的修改的点。 Update 方法通过调用 GeneratePathGeometry 的目标结构,得出结论认为,这是该程序在其 Render 方法中的显示。

有时当通过算法改变路径几何图形,您可能宁愿使用纯粹的行,而不是贝塞尔曲线的工作。 你能做到。 您可以定义从调用 GetGlyphRunOutline,一个 ID2D1PathGeometry 对象,然后调用简化的 ID2D1PathGeometry 上使用 D2D1_GEOMETRY_SIMPLI­FICATION_OPTION_LINES 常量和 Interpolatable­GeometrySink 实例。

从 DirectX 到 Windows 运行时

如果您熟悉 Windows 运行时 API、 PathGeometryData、 PathFigureData 和 PathSegmentData 结构的图 4 可能看起来很熟悉。 Windows::Xaml::UI::Media 命名空间包含类似命名为 PathGeometry、 PathFigure、 PathSegment、 PolyLineSegment 和 PolyBezierSegment 从中派生的类。 这些都是使用在 Windows 运行时,您通常呈现使用 Path 元素中定义路径几何图形的类。

当然,相似性也不足为奇。 毕竟,Windows 运行库是建基于 DirectX。 这种相似性的暗示是您可以编写一个类,实现 ID2D1Simpli­fiedGeometrySink 以生成的 PathGeometry、 PathFigure、 PolyLineSegment 和 PolyBezierSegment 的对象树。 由此产生的 PathGeometry 对象是直接可用的 Windows 运行时应用程序,并可以在 XAML 文件中引用。 (您可能还编写生成 XAML 表示形式的 PathGeometry 的 ID2D1SimplifiedGeometrySink 实现和任何基于 XAML 的环境,如 Silverlight 中的 XAML 文件中的插入。

TripleAnimatedOutline 解决方案演示此技术。 此解决方案包含一个名为 SimpleDWriteLib,其中包含一个名为 TextGeometryGenerator,它提供对系统字体的访问,并生成基于这些字体的轮廓几何形状的公共 ref 类的 Windows 运行时组件项目。 因为此 ref 类是 Windows 运行时组件的一部分,纯粹的 Windows 运行时类型由包含的公共接口。 我做了组成主要是依赖项属性的所以它可以用 XAML 文件中的绑定该公共接口。 SimpleDWriteLib 项目还包含一个名为 InteroperableGeometrySink,实现 ID2D1SimplifiedGeometrySink 接口和构造 Windows 运行库 PathGeometry 对象的私有类。

然后可以将此 PathGeometry 使用路径元素。 但是请注意:当 Windows 运行库布局引擎计算为布局目的路径元素的大小时,它只使用积极的坐标。 为了便于在 XAML 文件中使用 PathGeometry,TextGeometryGenerator 定义修改坐标基于字体度量标准结构的 capHeight 字段 DWRITE_GLYPH_OFFSET。 这有助于调整的几何坐标,开始在顶部的字体字符,而不是在原点,并消除最消极的坐标。

为了演示的互操作性的 SimpleDWriteLib 组件,在 Visual Basic 中写入 TripleAnimatedOutline 应用程序项目。 不过别担心:我没有写任何 Visual Basic 代码。 我添加到此项目的一切都是在 MainPage.xaml 文件中所示图 8。 列表框中显示的所有字体在用户的系统上,并基于所选的字体轮廓几何图形动画三种方式:

  • 点周游字符 ;
  • 渐变画笔扫过去的案文 ;
  • 投影变换旋转它绕垂直轴。

图 8 TripleAnimatedOutline XAML 文件

<Page
  x:Class="TripleAnimatedOutline.MainPage"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:TripleAnimatedOutline"
  xmlns:dwritelib="using:SimpleDWriteLib">
  <Page.Resources>
    <dwritelib:TextGeometryGenerator x:Key="geometryGenerator"
                                     Text="Outline"
                                     FontFamily="Times New Roman"
                                     FontSize="192"
                                     FontStyle="Italic" />
  </Page.Resources>
  <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <ListBox Grid.Column="0"
             ItemsSource="{Binding Source={StaticResource geometryGenerator},
                                   Path=FontFamilies}"
             SelectedItem="{Binding Source={StaticResource geometryGenerator},
                                    Path=FontFamily,
                                    Mode=TwoWay}" />
    <Path Name="path"
          Grid.Column="1"
          Data="{Binding Source={StaticResource geometryGenerator}, Path=Geometry}"
          Fill="LightGray"
          StrokeThickness="6"
          StrokeDashArray="0 2"
          StrokeDashCap="Round"
          HorizontalAlignment="Center"
          VerticalAlignment="Center">
      <Path.Stroke>
        <LinearGradientBrush StartPoint="0 0" EndPoint="1 0"
                                     SpreadMethod="Reflect">
          <GradientStop Offset="0" Color="Red" />
          <GradientStop Offset="1" Color="Blue" />
          <LinearGradientBrush.RelativeTransform>
            <TranslateTransform x:Name="brushTransform" />
          </LinearGradientBrush.RelativeTransform>
        </LinearGradientBrush>
      </Path.Stroke>
      <Path.Projection>
        <PlaneProjection x:Name="projectionTransform" />
      </Path.Projection>
    </Path>
  </Grid>
  <Page.Triggers>
    <EventTrigger>
      <BeginStoryboard>
        <Storyboard>
          <DoubleAnimation Storyboard.TargetName="path"
                           Storyboard.TargetProperty="StrokeDashOffset"
                           EnableDependentAnimation="True"
                           From="0" To="2" Duration="0:0:1"
                           RepeatBehavior="Forever" />
          <DoubleAnimation Storyboard.TargetName="brushTransform"
                           Storyboard.TargetProperty="X"
                           EnableDependentAnimation="True"
                           From="0" To="2" Duration="0:0:3.1"
                           RepeatBehavior="Forever" />
          <DoubleAnimation Storyboard.TargetName="projectionTransform"
                           Storyboard.TargetProperty="RotationY"
                           EnableDependentAnimation="True"
                           From="0" To="360" Duration="0:0:6.7"
                           RepeatBehavior="Forever" />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Page.Triggers>
</Page>

第二个程序也使用 SimpleDWriteLib。 这是 RippleText,一个 C# 程序,使用 CompositionTarget.Rendering 事件将在代码中执行一个动画。 类似于 OscillatingText,RippleText 获取两个完全相同的 PathGeometry 对象。 它使用作为永恒不变的源,另一个作为其点通过算法改变的目的地。 该算法涉及动画的正弦曲线应用于垂直坐标,从而导致扭曲所示图 9

The RippleText Display
图 9 RippleText 显示

虽然给出的示例我已经在这里是极端在很多方面,你当然有的选项来创建更加微妙的影响。 我怀疑大部分在 Microsoft Word 中的艺术字功能围绕技术涉及操纵的字符轮廓,因此,可能会提供一些灵感。

你还可以将这些技术集成到基于 IDWriteTextLayout 的更正常文本显示代码。 此接口具有一个名为接受实现 IDWriteTextRenderer 接口的类的实例的绘制方法。 那是你自己要访问的 DWRITE_GLYPH_RUN 对象的描述要呈现的文本就会写一个类。 您可以更改字形运行,然后呈现在修改后的版本,或者你可以在此时生成字符轮廓几何形状和修改呈现之前的轮廓。

很多 DirectX 的力量在于其灵活性和适应性到不同的方案。

查尔斯 · Petzold 是 MSDN 杂志和作者的"编程窗口,第 6 版"长期贡献 (微软出版社,2012年),一本关于编写应用程序的 Windows 8 书。 他的网站是 charlespetzold.com

衷心感谢以下技术专家对本文的审阅: Jim Galasyn (Microsoft)