Este artigo foi traduzido por máquina.

Fator DirectX

Geometrias de contorno de caracteres alucinantes

Charles Petzold

Baixar o código de exemplo

Charles Petzold
O avanço mais significativo em tipografia digital em computadores pessoais ocorreu há mais de 20 anos atrás com o interruptor de fontes bitmap fontes de contorno. Em versões do Windows anteriores ao Windows 3.1, texto apresentado no ecrã foi gerado a partir de arquivos fonte, consistindo de bitmaps pequenos de tamanhos de ponto específico. Esses bitmaps pode ser dimensionados para tamanhos nas entrelinhas, mas não sem uma perda de fidelidade.

Adobe Systems Inc. pioneiro em uma abordagem alternativa para exibir fontes de computador com PostScript, que definiam caracteres de fonte com contornos gráficos constituído por linhas retas e curvas de Bézier. Você pode escalar esses contornos para qualquer dimensão e algorítmicas "dicas" ajudou a preservar fidelidade com tamanhos de pontos pequenos. Como uma alternativa para PostScript para fontes na tela do computador pessoal, a Apple Inc. desenvolveu a especificação de fonte TrueType, que mais tarde adoptada a Microsoft. Que eventualmente evoluiu para o padrão OpenType comum de hoje.

Hoje em dia, nós tomam para concedido a escalabilidade contínua de fontes na tela, bem como a capacidade de girar ou inclinar o texto usando transformações gráficas. Ainda, também é possível obter as geometrias reais que definem os contornos desses caracteres de fonte e usá-los para efeitos incomuns, como caracteres de texto de estrutura de tópicos, ou recorte ou executar transformações não-lineares.

Da fonte a geometria de recorte

Se você deseja obter geometrias de contorno de caractere em um aplicativo Windows Store, a API de tempo de execução do Windows não vai ajudar. Você terá que usar o DirectX. O método de GetGlyphRunOutline de IDWriteFontFace grava os contornos de caracteres em um IDWriteGeometrySink (que é o mesmo que um ID2D1SimplifiedGeometrySink) que define (ou contribui para) um objeto ID2D1PathGeometry.

Figura 1 mostra o construtor de uma classe de renderização em um aplicativo Windows 8.1 chamado ClipToText que criei com o modelo de aplicativo DirectX (XAML). O projeto inclui o arquivo de fonte Miramonte bold (realce) pode ser distribuído, e o código mostra como converter um glifo para uma geometria de caminho. Como de costume, eu removi as verificações de errantes valores HRESULT para fins de clareza.

Figura 1-conversão de um glifo correr para uma geometria de caminho

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();
}

Embora o código em Figura 1 Obtém um objeto de IDWriteFontFace a partir de uma fonte em particular carregada, aplicativos também podem obter objetos de rosto fonte de fontes em coleções de fonte, incluindo a coleção de fontes do sistema. O código em Figura 1 especifica a índices de glifo índices de glifo correspondente explicitamente o texto "CLIP", mas você também pode derivam uma seqüência de caracteres Unicode, usando o método GetGlyphIndices.

Uma vez que você criou um objeto ID2D1PathGeometry, você pode usá-lo para enchimento (em cujo caso o resultado parece como processado texto), desenho (que processa apenas os contornos), ou recorte. Figura 2 mostra um método de renderização que dimensiona e traduz a geometria de caminho para definir uma região de recorte que abrange a área de tela inteira. Tenha em mente a geometria de caminho tem coordenadas negativas e positivas. A (0, 0) origem da geometria de caminho corresponde à linha de base no início do glifo executar.

Figura 2 recorte com uma geometria de caminho

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;
}

O método Render, em seguida, desenha uma série de linhas que irradiam do centro da tela, criando a imagem mostrada em Figura 3.

The ClipToText Display
Figura 3 o Display ClipToText

Aprofundar as definições de geometria

De um modo geral, uma geometria de caminho é uma coleção de figuras, cada uma das quais é uma coleção de segmentos conectados. Estes segmentos assumem a forma de linhas retas, quadráticas e cúbicas curvas de Bézier e arcos (que são curvas na circunferência de uma elipse). Uma figura pode ser que também fechado, caso em que o ponto de extremidade é conectado ao ponto de partida, ou aberto.

O método GetGlyphRunOutline grava os contornos de glifo em um IDWriteGeometrySink, que é o mesmo que um ID2D1SimplifiedGeometrySink. Este, por sua vez, é a classe pai para uma ID2D1GeometrySink regular. Usando o ID2D1SimplifiedGeometry­pia em vez de ID2D1GeometrySink implica que a geometria de caminho resultante contém figuras constituídos apenas por linhas retas e curvas de Bézier cúbicas — sem curvas Bézier quadráticas e sem arcos.

Para contornos de caracteres de fonte, esses segmentos são sempre fechados — ou seja, o ponto de extremidade da figura se conecta ao ponto de início. A geometria do percurso criada no programa ClipToText para os personagens de "CLIP" consiste de cinco figuras — uma figura para cada uma das primeiras três letras e dois para a última carta a conta para o interior da parte superior do t.

Talvez você gostaria de acesso ao reais linhas e curvas de Bézier que formam a geometria do caminho, então você pode manipulá-los de maneira estranha e incomum. Em primeiro lugar, isso não parece possível. Uma vez que um objeto de ID2D1PathGeometry tiver sido inicializado com dados, o objeto é imutável, e a interface não fornece nenhuma maneira de obter o conteúdo.

Há uma solução, no entanto: Você pode escrever sua própria classe que implementa a interface ID2D1SimplifiedGeometrySink e passar uma instância dessa classe para o método de GetGlyphRunOutline. Sua implementação personalizada de ID2D1SimplifiedGeometrySink deve conter métodos chamados BeginFigure, AddLines, AddBeziers e EndFigure (entre outros). Em um desses métodos, você pode salvar a geometria de todo o percurso em uma árvore de estruturas, que você pode definir.

Isto é o que eu fiz. As estruturas que defini para salvar o conteúdo de uma geometria de caminho são mostradas em Figura 4. Essas estruturas mostram como uma geometria de caminho é uma coleção de objetos de figura de caminho, e cada figura do caminho é uma coleção de segmentos conectados que consiste de linhas retas e curvas Bézier cúbicas.

Figura 4 estruturas para salvar o conteúdo de uma geometria de caminho

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);
};

Minha implementação do ID2D1SimplifiedGeometrySink chama-se InterrogableGeometrySink, assim chamado porque ele contém um método que retorna a geometria resultante como um objeto PathGeometryData. As partes mais interessantes do InterrogableGeometrySink são mostradas em Figura 5.

Figura 5 mais da classe 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;
}

Simplesmente passe uma instância de InterrogableGeometrySink para GetGlyphRunOutline para obter o objeto PathGeometryData que descreve os contornos de caracteres. PathGeometryData também contém um método chamado GeneratePathGeometry que usa a árvore de figuras e segmentos para criar um objeto ID2D1PathGeometry, que você pode usar para desenhar, de enchimento ou de recorte. A diferença é que, antes de chamar GeneratePathGeometry, seu programa pode modificar os pontos que compõem a linha e segmentos Bézier. Você ainda pode adicionar ou remover segmentos ou figuras.

A classe InterrogableGeometrySink e as estruturas de suporte são parte de um projeto chamado RealTextEditor; "Na real" quer dizer que você pode editar os contornos de texto em vez do texto em si. Quando o programa chega, ele exibe os grandes personagens "DX". Toque ou clique na tela para alternar entre o modo de edição. No modo de edição, os personagens são descritos e pontos aparecer.

Pontos verdes marcam as extremidades dos segmentos de linha e segmentos Bézier e começos. Pontos vermelhos são os pontos de controle de Bézier. Pontos de controle estão ligados aos pontos de extremidade correspondentes com linhas vermelhas. Você pode pegar esses pontos com o mouse — eles são um pouco pequenos demais para os dedos — e arrastá-los, distorcendo os caracteres de texto de maneiras estranhas, como Figura 6 demonstra.

Modified Character Outlines in RealTextEditor
Figura 6 modificado contornos de caracteres em RealTextEditor

RealTextEditor não tem nenhuma facilidade para salvar suas geometrias de caráter personalizado, mas você certamente poderia adicionar um. A intenção deste programa não é para editar caracteres de fonte, mas que ilustram bem como caracteres de fonte são definidos por uma série de linhas retas e curvas de Bézier conectados em figuras fechadas — neste caso três figuras, duas para dentro e fora da D e outra para o X.

Manipulações algorítmicas

Uma vez que você tem uma definição de geometria de caminho em forma de estruturas tais como PathGeometryData, PathFigureData e PathSegmentData, você também pode manipular os pontos individuais através de algoritmos, torcendo e virando personagens no que maneira você por favor, talvez criando uma imagem como mostrado na Figura 7.

The OscillatingText Program
Figura 7 o programa de OscillatingText

Bem, não exatamente. A imagem mostrada em Figura 7 não é possível usando um objeto PathGeometryData, gerado a partir do Interrogable­GeometrySink classe só te mostrei. Em muitas fontes sans-serif simples, o capital H consiste de 12 pontos conectados por linhas retas. Se você está lidando exclusivamente com esses pontos, não há nenhuma maneira você pode modificá-los para que as linhas retas do H se tornar curvas.

No entanto, você pode resolver esse problema com uma versão melhorada do InterrogableGeometrySink chamado InterpolatableGeometrySink. Sempre que essa nova classe encontra uma linha reta no método AddLines, quebra essa linha em várias linhas menores. (Você pode controlar este recurso com um argumento do Construtor). O resultado é uma definição de geometria de caminho completamente maleável.

O programa de OscillatingText responsável pela imagem em Figura 7 realmente oscila o interior dos personagens e para trás, tanto como uma dança de hula. Este algoritmo é implementado no método Update na classe de renderização. Duas cópias de um PathGeometryData são retidas: A fonte (identificada como "src") descreve o contorno original do texto, e o destino ("dst") contém pontos modificados baseados no algoritmo. O método Update conclui chamando GeneratePathGeometry sobre a estrutura de destino, e isso é o que o programa exibe em seu método Render.

Às vezes, quando algoritmicamente alterando uma geometria de caminho, você pode preferir trabalhar exclusivamente com linhas ao invés de curvas de Bézier. Você pode fazer isso. Você pode definir um objeto de ID2D1PathGeometry de uma chamada para GetGlyphRunOutline e em seguida, chamar simplificar na que ID2D1PathGeometry usando o D2D1_GEOMETRY_SIMPLI­constante de FICATION_OPTION_LINES e um Interpolatable­GeometrySink instância.

Do DirectX para o Runtime do Windows

Se você está familiarizado com as estruturas de API de tempo de execução do Windows, o PathGeometryData, PathFigureData e PathSegmentData em Figura 4 provavelmente parece muito familiar. O namespace Windows::Xaml::UI::Media contém classes semelhantes, denominadas PathGeometry, PathFigure e PathSegment, da qual derivam a PolyLineSegment e PolyBezierSegment. Estas são as classes que você usa para definir uma geometria do caminho no Windows Runtime, que você normalmente processar usando o elemento de caminho.

Claro, a semelhança não deveria ser surpreendente. Afinal, o tempo de execução do Windows é construído sobre o DirectX. O que esta similaridade implica é que você pode escrever uma classe que implementa ID2D1Simpli­fiedGeometrySink para construir uma árvore de objetos PathGeometry, PathFigure e PolyLineSegment PolyBezierSegment. O objeto PathGeometry resultante é diretamente utilizável por um aplicativo de tempo de execução do Windows e pode ser referenciado em um arquivo XAML. (Você pode também escrever uma implementação de ID2D1SimplifiedGeometrySink que gera uma representação de XAML de um PathGeometry e que inserir um arquivo XAML em qualquer ambiente baseados em XAML, como o Silverlight).

A solução de TripleAnimatedOutline demonstra essa técnica. A solução contém um projeto de componente de tempo de execução do Windows chamado SimpleDWriteLib que contém uma classe ref público chamada TextGeometryGenerator, que fornece acesso para as fontes do sistema e gera geometrias de contorno baseadas essas fontes. Porque essa classe ref é parte de um componente de tempo de execução do Windows, a interface pública consiste unicamente de tipos de tempo de execução do Windows. Eu fiz essa interface pública consistem principalmente de propriedades de dependência, assim que poderia ser usado com ligações em um arquivo XAML. O projeto de SimpleDWriteLib também contém uma classe privada chamada InteroperableGeometrySink que implementa a interface ID2D1SimplifiedGeometrySink e constrói um objeto PathGeometry de tempo de execução do Windows.

Você pode usar este PathGeometry com um elemento Path. Mas cuidado: Quando o mecanismo de layout de tempo de execução Windows calcula o tamanho de um elemento Path para fins de layout, ele só usa coordenadas positivas. Para facilitar o PathGeometry usar em um arquivo XAML, TextGeometryGenerator define um DWRITE_GLYPH_OFFSET que modifica as coordenadas com base no campo capHeight da estrutura de métricas de fonte. Isto serve para ajustar as coordenadas da geometria para começar na parte superior dos caracteres de fonte ao invés de na origem e eliminar as coordenadas mais negativas.

Para demonstrar a interoperabilidade do componente SimpleDWriteLib, o projeto de aplicativo de TripleAnimatedOutline escrito em Visual Basic. Mas não se preocupe: Não tive de escrever qualquer código Visual Basic . Tudo o que eu adicionei a este projecto está o arquivo MainPage. xaml mostrado na Figura 8. O ListBox exibe todas as fontes no sistema do usuário, e uma geometria de estrutura de tópicos com base na fonte selecionada é animada de três maneiras:

  • Pontos viajam os personagens;
  • Um pincel de gradiente varre passado o texto;
  • Uma transformação de projeção-gira em torno do eixo vertical.

Figura 8 o arquivo XAML TripleAnimatedOutline

<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>

Um segundo programa também usa SimpleDWriteLib. Este é o RippleText, um programa c# que usa um evento CompositionTarget para executar uma animação no código. Semelhante ao OscillatingText, RippleText Obtém dois objetos PathGeometry idênticos. Ele usa um como uma fonte imutável e o outro como um destino cujos pontos são transformados através de algoritmos. O algoritmo envolve um gráfico de seno animado que é aplicado para as coordenadas verticais, resultando em distorções tais como as mostradas na Figura 9.

The RippleText Display
Figura 9 visor RippleText

Embora os exemplos que mostrei aqui são extremos em muitos aspectos, você certamente tem a opção para criar efeitos mais sutis. Eu suspeito que grande parte do WordArt recurso em Microsoft Word é construído em torno de técnicas que envolvam a manipulação de caracteres descreve, assim que pode fornecer alguma inspiração.

Você pode também integrar estas técnicas mais normal código de exibição de texto baseado no IDWriteTextLayout. Essa interface possui um método chamado Draw que aceita uma instância de uma classe que implementa a interface IDWriteTextRenderer. Isso é uma classe que você vai escrever-se para ter acesso ao objeto DWRITE_GLYPH_RUN que descreve o texto a ser processado. Você pode fazer alterações para o glifo de execução e em seguida, processar a versão modificada, ou você pode gerar as geometrias de contorno do personagem naquele momento e modificar os contornos antes para renderização.

Muito do poder do DirectX reside na sua flexibilidade e adaptabilidade a diferentes cenários.

Charles Petzold é um colaborador de longa data de MSDN Magazine e autor de "Programação Windows, 6ª edição" (Microsoft Press, 2012), um livro sobre como escrever aplicativos para Windows 8. Seu site é charlespetzold.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Jim Galasyn (Microsoft)