本文章是由機器翻譯。

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


圖 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 演示。


圖 6 修改 RealTextEditor 中的字元輪廓

RealTextEditor 有沒有設施要保存您的自訂字元幾何圖形,但你肯定可以添加一個。此程式的意圖並不真的編輯字體字元,而是要清楚地說明了如何由一系列直線和貝茲曲線定義字體字元連接成封閉的數位 — — 在這種情況下三個數字,兩個內部和外部 D,另一個用於 X。

演算法的操作

一旦你有一個路徑幾何定義結構,如 PathGeometryData、 PathFigureData 和 PathSegmentData 的形式,你也可以操縱個別點通過演算法,扭曲和車削中字元任何方式你請或許創建圖像,例如如中所示的圖 7


圖 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


圖 9 RippleText 顯示

雖然給出的示例我已經在這裡是極端在很多方面,你當然有的選項來創建更加微妙的影響。我懷疑很多Microsoft Word中的功能圍繞著技術涉及操縱的字元,藝術字的輪廓,因此,可能會提供一些靈感。

你還可以將這些技術集成到基於 IDWriteTextLayout 的更正常文本顯示代碼。此介面具有一個名為接受實現 IDWriteTextRenderer 介面的類的實例的繪製方法。那是你自己要訪問的 DWRITE_GLYPH_RUN 物件的描述要呈現的文本就會寫一個類。您可以更改字形運行,然後呈現在修改後的版本,或者你可以在此時生成字元輪廓幾何形狀和修改呈現之前的輪廓。

很多 DirectX 的力量在於其靈活性和適應性到不同的方案。

CharlesPetzold 是 MSDN 雜誌和作者的"程式設計視窗,第 6 版"長期貢獻 (微軟出版社,2012年),一本關於編寫應用程式的 Windows 8 書。 他的網站是 charlespetzold.com

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