msdnmagazine.com February 2014 77
positive Y axis, your thumb points to positive Z. With a left -hand
coordinate system, it’s the same except using the left hand.
My goal here is to obtain a 2D path geometry of a short text string,
and then twist it around the origin into a 3D ring so the beginning
meets the end, similar to the illustration shown in Figure 1. Because
I’ll be converting 2D coordinates to 3D coordinates and then back
to 2D, I’ve chosen to use a 3D coordinate system with Y coordinates
increasing going down, just like in 2D. Th e positive Z axis comes out
of the screen, but it’s really a left -hand coordinate system.
To make this whole job as easy as possible, I’ve used a font fi le
stored as a program resource, and created an IDWriteFontFile
object for obtaining the IDWriteFontFace object. Alternatively,
you could obtain an IDWriteFontFace through a more roundabout
method from the system font collection.
The ID2D1PathGeometry object generated from the Get-
GlyphRunOutline method is then passed through the Simplify method
using the D2D1_GEOMETRY_SIMPLIFICATION_OPTION_LINES
argument to fl atten all Bézier curves into sequences of short lines. Th at
simplifi ed geometry is passed into a custom ID2D1GeometrySink
implementation named FlattenedGeometrySink to further decom-
pose all the straight lines into much shorter straight lines. Th e result
is a completely malleable geometry consisting only of lines.
To ease the manipulation of these coordinates, FlattenedGeometry-
Sink generates a collection of Polygon objects. Figure 2 shows the
defi nition of the Polygon structure. It’s basically just a collection of
connected 2D points. Each Polygon object corresponds to a closed
fi gure in the path geometry. Not all fi gures in path geometries are
closed, but those in text glyphs are always closed, so this structure
is fi ne for that purpose. Some characters (such as C, E and X) are
described by just one Polygon; some (A, D and O) consist of two
Polygon objects for the inside and outside; some (B, for example)
consist of three; and some symbol characters may have many more.
Among the downloadable code for this column is a Windows
Store program named CircularText that creates a collection of Poly-
gon objects based on the text “Text in an Infi nite Circle of,” where
the end is intended to connect back to the beginning in a circle. Th e
text string is actually specifi ed in the program as “ext in an Infi nite
Circle of T” to avoid a space at the beginning or end that would
disappear when a path geometry is generated from the glyphs.
Th e CircularTextRenderer class in the CircularText project con-
tains two std::vector objects of type Polygon called m_srcPolygons
(the original Polygon objects generated from the path geometry)
and m_dstPolygons (the Polygon objects used to generate
the rendered path geometry). Figure 3 shows the method
struct Polygon
{
// Constructors
Polygon()
{
}
Polygon(size_t pointCount)
{
Points = std::vector<D2D1_POINT_2F>(pointCount);
}
// Move constructor
Polygon(Polygon && other) : Points(std::move(other.Points))
{
}
std::vector<D2D1_POINT_2F> Points;
static HRESULT CreateGeometry(ID2D1Factory* factory,
const std::vector<Polygon>& polygons,
ID2D1PathGeometry** pathGeometry);
};
HRESULT Polygon::CreateGeometry(ID2D1Factory* factory,
const std::vector<Polygon>& polygons,
ID2D1PathGeometry** pathGeometry)
{
HRESULT hr;
if (FAILED(hr = factory->CreatePathGeometry(pathGeometry)))
return hr;
Microsoft::WRL::ComPtr<ID2D1GeometrySink> geometrySink;
if (FAILED(hr = (*pathGeometry)->Open(&geometrySink)))
return hr;
for (const Polygon& polygon : polygons)
{
if (polygon.Points.size() > 0)
{
geometrySink->BeginFigure(polygon.Points[0],
D2D1_FIGURE_BEGIN_FILLED);
if (polygon.Points.size() > 1)
{
geometrySink->AddLines(polygon.Points.data() + 1,
polygon.Points.size() - 1);
}
geometrySink->EndFigure(D2D1_FIGURE_END_CLOSED);
}
}
return geometrySink->Close();
}
Figure 2 The Polygon Class for Storing Closed Path Figures
void CircularTextRenderer::CreateWindowSizeDependentResources()
{
// Get window size and geometry size
Windows::Foundation::Size logicalSize = m_deviceResources->GetLogicalSize();
float geometryWidth = m_geometryBounds.right - m_geometryBounds.left;
float geometryHeight = m_geometryBounds.bottom - m_geometryBounds.top;
// Calculate a few factors for converting 2D to 3D
float radius = logicalSize.Width / 2 - 50;
float circumference = 2 * 3.14159f * radius;
float scale = circumference / geometryWidth;
float height = scale * geometryHeight;
for (size_t polygonIndex = 0; polygonIndex < m_srcPolygons.size(); polygonIndex++)
{
const Polygon& srcPolygon = m_srcPolygons.at(polygonIndex);
Polygon& dstPolygon = m_dstPolygons.at(polygonIndex);
for (size_t pointIndex = 0; pointIndex < srcPolygon.Points.size(); pointIndex++)
{
const D2D1_POINT_2F pt = srcPolygon.Points.at(pointIndex);
float radians = 2 * 3.14159f * (pt.x - m_geometryBounds.left) / geometryWidth;
float x = radius * sin(radians);
float z = radius * cos(radians);
float y = height * ((pt.y - m_geometryBounds.top) / geometryHeight - 0.5f);
dstPolygon.Points.at(pointIndex) = Point2F(x, y);
}
}
// Create path geometry from Polygon collection
DX::ThrowIfFailed(
Polygon::CreateGeometry(m_deviceResources->GetD2DFactory(),
m_dstPolygons,
&m_pathGeometry)
);
}
Figure 3 From 2D to 3D to 2D in the CircularText Program