2025-11-28 00:35:46 +09:00

947 lines
31 KiB
C++

// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved
#include "TextAnimationSample.h"
/******************************************************************
* *
* WinMain *
* *
* Application entrypoint *
* *
******************************************************************/
int WINAPI WinMain(
HINSTANCE /* hInstance */,
HINSTANCE /* hPrevInstance */,
LPSTR /* lpCmdLine */,
int /* nCmdShow */
)
{
// Ignoring the return value because we want to continue running even in the
// unlikely event that HeapSetInformation fails.
HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0);
if (SUCCEEDED(CoInitialize(NULL)))
{
{
DemoApp app;
if (SUCCEEDED(app.Initialize()))
{
app.RunMessageLoop();
}
}
CoUninitialize();
}
return 0;
}
/******************************************************************
* *
* DemoApp::DemoApp constructor *
* *
* Initialize member data *
* *
******************************************************************/
DemoApp::DemoApp() :
m_hwnd(NULL),
m_pD2DFactory(NULL),
m_pDWriteFactory(NULL),
m_pRT(NULL),
m_pTextFormat(NULL),
m_pTextLayout(NULL),
m_pBlackBrush(NULL),
m_pOpacityRT(NULL)
{
m_startTime = 0;
m_animationStyle = AnimationStyle::Translation;
m_renderingMethod = TextRenderingMethod::Default;
}
/******************************************************************
* *
* DemoApp::~DemoApp destructor *
* *
* Tear down resources *
* *
******************************************************************/
DemoApp::~DemoApp()
{
SafeRelease(&m_pD2DFactory);
SafeRelease(&m_pDWriteFactory);
SafeRelease(&m_pRT);
SafeRelease(&m_pTextFormat);
SafeRelease(&m_pTextLayout);
SafeRelease(&m_pBlackBrush);
SafeRelease(&m_pOpacityRT);
}
/******************************************************************
* *
* DemoApp::Initialize *
* *
* Create application window and device-independent resources *
* *
******************************************************************/
HRESULT DemoApp::Initialize()
{
HRESULT hr;
m_fRunning = FALSE;
hr = CreateDeviceIndependentResources();
if (SUCCEEDED(hr))
{
//register window class
WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = DemoApp::WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = sizeof(LONG_PTR);
wcex.hInstance = HINST_THISCOMPONENT;
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = NULL;
wcex.lpszMenuName = NULL;
wcex.lpszClassName = L"D2DDemoApp";
RegisterClassEx(&wcex);
// Create the application window.
//
// Because the CreateWindow function takes its size in pixels, we
// obtain the system DPI and use it to scale the window size.
FLOAT dpiX;
FLOAT dpiY;
m_pD2DFactory->GetDesktopDpi(&dpiX, &dpiY);
m_hwnd = CreateWindow(
L"D2DDemoApp",
L"D2D Demo App",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
static_cast<UINT>(ceil(640.0f * dpiX / 96.0f)),
static_cast<UINT>(ceil(480.0f * dpiY / 96.0f)),
NULL,
NULL,
HINST_THISCOMPONENT,
this
);
hr = m_hwnd ? S_OK : E_FAIL;
if (SUCCEEDED(hr))
{
m_fRunning = TRUE;
ShowWindow(m_hwnd, SW_SHOWNORMAL);
UpdateWindow(m_hwnd);
}
}
return hr;
}
/******************************************************************
* *
* DemoApp::CreateDeviceIndependentResources *
* *
* This method is used to create resources which are not bound *
* to any device. Their lifetime effectively extends for the *
* duration of the app. These resources include the D2D, *
* DWrite factories; and a DWrite Text Format object *
* (used for identifying particular font characteristics) and *
* a D2D geometry. *
* *
******************************************************************/
HRESULT DemoApp::CreateDeviceIndependentResources()
{
static const WCHAR msc_fontName[] = L"Gabriola";
static const FLOAT msc_fontSize = 50;
static const WCHAR sc_helloWorld[] = L"The quick brown fox jumped over the lazy dog!";
static const UINT stringLength = ARRAYSIZE(sc_helloWorld) - 1;
HRESULT hr;
//create D2D factory
hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pD2DFactory);
if (SUCCEEDED(hr))
{
//create DWrite factory
hr = DWriteCreateFactory(
DWRITE_FACTORY_TYPE_ISOLATED, //DWRITE_FACTORY_TYPE_SHARED
__uuidof(m_pDWriteFactory),
reinterpret_cast<IUnknown **>(&m_pDWriteFactory)
);
}
if (SUCCEEDED(hr))
{
//create DWrite text format object
hr = m_pDWriteFactory->CreateTextFormat(
msc_fontName,
NULL,
DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL,
msc_fontSize,
L"", //locale
&m_pTextFormat
);
}
if (SUCCEEDED(hr))
{
//center the text horizontally
m_pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
hr = m_pDWriteFactory->CreateTextLayout(
&sc_helloWorld[0],
stringLength,
m_pTextFormat,
300, // maxWidth
1000, // maxHeight
&m_pTextLayout
);
}
if (SUCCEEDED(hr))
{
//
// We use typographic features here to show how to account for the
// overhangs that these features will produce. See the code in
// ResetAnimation that calls GetOverhangMetrics(). Note that there are
// fonts that can produce overhangs even without the use of typographic
// features- this is just one example.
//
IDWriteTypography *pTypography = NULL;
hr = m_pDWriteFactory->CreateTypography(&pTypography);
if (SUCCEEDED(hr))
{
DWRITE_FONT_FEATURE fontFeature =
{
DWRITE_FONT_FEATURE_TAG_STYLISTIC_SET_7,
1
};
hr = pTypography->AddFontFeature(fontFeature);
if (SUCCEEDED(hr))
{
DWRITE_TEXT_RANGE textRange = {0, stringLength};
hr = m_pTextLayout->SetTypography(pTypography, textRange);
}
pTypography->Release();
}
}
return hr;
}
/******************************************************************
* *
* DemoApp::CreateDeviceResources *
* *
* This method creates resources which are bound to a particular *
* D3D device. It's all centralized here, in case the resources *
* need to be recreated in case of D3D device loss (eg. display *
* change, remoting, removal of video card, etc). *
* *
******************************************************************/
HRESULT DemoApp::CreateDeviceResources()
{
HRESULT hr = S_OK;
if (!m_pRT)
{
RECT rc;
GetClientRect(m_hwnd, &rc);
D2D1_SIZE_U size = D2D1::SizeU(
rc.right - rc.left,
rc.bottom - rc.top
);
//
// Create a D2D render target
//
// Note: we only use D2D1_PRESENT_OPTIONS_IMMEDIATELY so that we can
// easily measure the framerate. Most apps should not use this
// flag.
//
hr = m_pD2DFactory->CreateHwndRenderTarget(
D2D1::RenderTargetProperties(),
D2D1::HwndRenderTargetProperties(m_hwnd, size, D2D1_PRESENT_OPTIONS_IMMEDIATELY),
&m_pRT
);
if (SUCCEEDED(hr))
{
//
// Nothing in this sample requires antialiasing so we set the antialias
// mode to aliased up front.
//
m_pRT->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
//create a black brush
hr = m_pRT->CreateSolidColorBrush(
D2D1::ColorF(D2D1::ColorF::Black),
&m_pBlackBrush
);
}
if (SUCCEEDED(hr))
{
hr = ResetAnimation(
true // resetClock
);
}
}
return hr;
}
/******************************************************************
* *
* DemoApp::DiscardDeviceResources *
* *
* Discard device-specific resources which need to be recreated *
* when a D3D device is lost *
* *
******************************************************************/
void DemoApp::DiscardDeviceResources()
{
SafeRelease(&m_pRT);
SafeRelease(&m_pBlackBrush);
SafeRelease(&m_pOpacityRT);
}
/******************************************************************
* *
* DemoApp::RunMessageLoop *
* *
* Main window message loop *
* *
******************************************************************/
void DemoApp::RunMessageLoop()
{
while (this->IsRunning())
{
MSG msg;
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
/******************************************************************
* *
* DemoApp::OnChar *
* *
* Responds to input from the user. *
* *
******************************************************************/
HRESULT DemoApp::OnChar(SHORT key)
{
HRESULT hr = S_OK;
bool resetAnimation = true;
bool resetClock = true;
switch (key)
{
case 't':
if (m_animationStyle & AnimationStyle::Translation)
{
m_animationStyle &= ~AnimationStyle::Translation;
}
else
{
m_animationStyle |= AnimationStyle::Translation;
}
break;
case 'r':
if (m_animationStyle & AnimationStyle::Rotation)
{
m_animationStyle &= ~AnimationStyle::Rotation;
}
else
{
m_animationStyle |= AnimationStyle::Rotation;
}
break;
case 's':
if (m_animationStyle & AnimationStyle::Scaling)
{
m_animationStyle &= ~AnimationStyle::Scaling;
}
else
{
m_animationStyle |= AnimationStyle::Scaling;
}
break;
case '1':
case '2':
case '3':
m_renderingMethod = static_cast<TextRenderingMethod::Enum>(key - '1');
resetClock = false;
break;
default:
resetAnimation = false;
resetClock = false;
}
if (resetAnimation)
{
hr = ResetAnimation(resetClock);
}
return hr;
}
/******************************************************************
* *
* DemoApp::UpdateWindowText *
* *
* This method updates the window title bar with info about the *
* current animation style and rendering method. It also outputs *
* the framerate. *
* *
******************************************************************/
void DemoApp::UpdateWindowText()
{
static LONGLONG sc_lastTimeStatusShown = 0;
//
// Update the window status no more than 10 times a second. Without this
// check, the performance bottleneck could potentially be the time it takes
// for Windows to update the title.
//
if ( m_times.GetCount() > 0
&& m_times.GetLast() > sc_lastTimeStatusShown + 1000000
)
{
//
// Determine the frame rate by computing the difference in clock time
// between this frame and the frame we rendered 10 frames ago.
//
sc_lastTimeStatusShown = m_times.GetLast();
LARGE_INTEGER frequency;
QueryPerformanceFrequency(&frequency);
float fps = 0.0f;
if (m_times.GetCount() > 0)
{
fps = (m_times.GetCount()-1) * frequency.QuadPart /
static_cast<float>((m_times.GetLast() - m_times.GetFirst()));
}
//
// Add other useful information to the window title.
//
wchar_t *style = NULL;
switch (m_animationStyle)
{
case AnimationStyle::None:
style = L"None";
break;
case AnimationStyle::Translation:
style = L"Translation";
break;
case AnimationStyle::Rotation:
style = L"Rotation";
break;
case AnimationStyle::Scaling:
style = L"Scale";
break;
}
wchar_t *method = NULL;
switch (m_renderingMethod)
{
case TextRenderingMethod::Default:
method = L"Default";
break;
case TextRenderingMethod::Outline:
method = L"Outline";
break;
case TextRenderingMethod::UseA8Target:
method = L"UseA8Target";
break;
}
wchar_t title[255];
StringCchPrintf(
title,
ARRAYSIZE(title),
L"AnimationStyle: %s%s%s, Method: %s, %#.1f fps",
(m_animationStyle & AnimationStyle::Translation) ? L"+t" : L"-t",
(m_animationStyle & AnimationStyle::Rotation) ? L"+r" : L"-r",
(m_animationStyle & AnimationStyle::Scaling) ? L"+s" : L"-s",
method,
fps
);
SetWindowText(m_hwnd, title);
}
}
/******************************************************************
* *
* DemoApp::ResetAnimation *
* *
* This method does the necessary work to change the current *
* animation style. *
* *
******************************************************************/
HRESULT DemoApp::ResetAnimation(bool resetClock)
{
HRESULT hr = S_OK;
if (resetClock)
{
m_startTime = GetTickCount();
}
//
// Release the opacity mask. We will regenerate it if the current animation
// style demands it.
//
SafeRelease(&m_pOpacityRT);
if (m_renderingMethod == TextRenderingMethod::Outline)
{
//
// Set the rendering mode to OUTLINE mode. To do this we first create
// a default params object and then make a copy with the given modification.
//
IDWriteRenderingParams *pDefaultParams = NULL;
hr = m_pDWriteFactory->CreateRenderingParams(&pDefaultParams);
if (SUCCEEDED(hr))
{
IDWriteRenderingParams *pRenderingParams = NULL;
hr = m_pDWriteFactory->CreateCustomRenderingParams(
pDefaultParams->GetGamma(),
pDefaultParams->GetEnhancedContrast(),
pDefaultParams->GetClearTypeLevel(),
pDefaultParams->GetPixelGeometry(),
DWRITE_RENDERING_MODE_OUTLINE,
&pRenderingParams
);
if (SUCCEEDED(hr))
{
m_pRT->SetTextRenderingParams(pRenderingParams);
pRenderingParams->Release();
}
pDefaultParams->Release();
}
}
else
{
// Reset the rendering mode to default.
m_pRT->SetTextRenderingParams(NULL);
}
if (SUCCEEDED(hr) && m_renderingMethod == TextRenderingMethod::UseA8Target)
{
//
// Create a compatible A8 Target to store the text as an opacity mask.
//
// Note: To reduce sampling error in the scale animation, it might be
// preferable to create multiple masks for the text at different
// resolutions.
//
FLOAT dpiX;
FLOAT dpiY;
m_pRT->GetDpi(&dpiX, &dpiY);
//
// It is important to obtain the overhang metrics here in case the text
// extends beyond the layout max-width and max-height.
//
DWRITE_OVERHANG_METRICS overhangMetrics;
m_pTextLayout->GetOverhangMetrics(&overhangMetrics);
//
// Because the overhang metrics can be off slightly given that these
// metrics do not account for antialiasing, we add an extra pixel for
// padding.
//
D2D1_SIZE_F padding = D2D1::SizeF(96.0f / dpiX, 96.0f / dpiY);
m_overhangOffset = D2D1::Point2F(ceil(overhangMetrics.left + padding.width), ceil(overhangMetrics.top + padding.height));
//
// The true width of the text is the max width + the overhang
// metrics + padding in each direction.
//
D2D1_SIZE_F maskSize = D2D1::SizeF(
overhangMetrics.right + padding.width + m_overhangOffset.x + m_pTextLayout->GetMaxWidth(),
overhangMetrics.bottom + padding.height + m_overhangOffset.y + m_pTextLayout->GetMaxHeight()
);
// Round up to the nearest pixel
D2D1_SIZE_U maskPixelSize = D2D1::SizeU(
static_cast<UINT>(ceil(maskSize.width * dpiX / 96.0f)),
static_cast<UINT>(ceil(maskSize.height * dpiY / 96.0f))
);
//
// Create the compatible render target using desiredPixelSize to avoid
// blurriness issues caused by a fractional-pixel desiredSize.
//
D2D1_PIXEL_FORMAT alphaOnlyFormat = D2D1::PixelFormat(DXGI_FORMAT_A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED);
hr = m_pRT->CreateCompatibleRenderTarget(
NULL,
&maskPixelSize,
&alphaOnlyFormat,
D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE,
&m_pOpacityRT
);
if (SUCCEEDED(hr))
{
//
// Draw the text to the opacity mask. Note that we can use pixel
// snapping now given that subpixel translation can now happen during
// the FillOpacityMask method.
//
m_pOpacityRT->BeginDraw();
m_pOpacityRT->Clear(D2D1::ColorF(D2D1::ColorF::Black, 0.0f));
m_pOpacityRT->DrawTextLayout(
m_overhangOffset,
m_pTextLayout,
m_pBlackBrush,
D2D1_DRAW_TEXT_OPTIONS_NO_SNAP
);
hr = m_pOpacityRT->EndDraw();
}
}
return hr;
}
/******************************************************************
* *
* DemoApp::CalculateTransform *
* *
* Calculates the transform based on the current time *
* *
******************************************************************/
void DemoApp::CalculateTransform(D2D1_MATRIX_3X2_F *pTransform)
{
// calculate a 't' value that will linearly interpolate from 0 to 1 and back every 20 seconds
DWORD currentTime = GetTickCount();
if ( m_startTime == 0 )
{
m_startTime = currentTime;
}
float t = 2 * (( currentTime - m_startTime) % 20000) / 20000.0f;
if (t > 1.0f)
{
t = 2 - t;
}
// calculate animation parameters
float rotation = 0;
float translationOffset = 0;
float scaleMultiplier = 1.0f;
if (m_animationStyle & AnimationStyle::Translation)
{
// range from -100 to 100
translationOffset = (t - 0.5f) * 200;
}
if (m_animationStyle & AnimationStyle::Rotation)
{
// range from 0 to 360
rotation = t * 360.0f;
}
if (m_animationStyle & AnimationStyle::Scaling)
{
// range from 1/4 to 2x the normal size
scaleMultiplier = t * 1.75f + 0.25f;
}
D2D1_SIZE_F size = m_pRT->GetSize();
*pTransform =
D2D1::Matrix3x2F::Rotation(rotation)
* D2D1::Matrix3x2F::Scale(scaleMultiplier, scaleMultiplier)
* D2D1::Matrix3x2F::Translation(translationOffset + size.width / 2.0f, translationOffset + size.height / 2.0f);
}
/******************************************************************
* *
* DemoApp::OnRender *
* *
* Called whenever the application needs to display the client *
* window. *
* *
* Note that this function will not render anything if the window *
* is occluded (e.g. when the screen is locked). *
* Also, this function will automatically discard device-specific *
* resources if the D3D device disappears during function *
* invocation, and will recreate the resources the next time it's *
* invoked. *
* *
******************************************************************/
HRESULT DemoApp::OnRender()
{
HRESULT hr;
//
// We use a ring buffer to store the clock time for the last 10 frames.
// This lets us eliminate a lot of noise when computing framerate.
//
LARGE_INTEGER time;
QueryPerformanceCounter(&time);
m_times.Add(time.QuadPart);
hr = CreateDeviceResources();
if (SUCCEEDED(hr) && !(m_pRT->CheckWindowState() & D2D1_WINDOW_STATE_OCCLUDED))
{
D2D1_MATRIX_3X2_F transform;
CalculateTransform(&transform);
m_pRT->BeginDraw();
m_pRT->Clear(D2D1::ColorF(D2D1::ColorF::White));
m_pRT->SetTransform(transform);
DWRITE_TEXT_METRICS textMetrics;
m_pTextLayout->GetMetrics(&textMetrics);
if (m_renderingMethod == TextRenderingMethod::UseA8Target)
{
//
// Offset the destination rect such that the text will be centered
// on the render target. Given that we have offset the text in the
// A8 target by the overhang offset, we must factor that into the
// destination rect now.
//
D2D1_SIZE_F opacityRTSize = m_pOpacityRT->GetSize();
D2D1_POINT_2F offset = D2D1::Point2F(
-textMetrics.width / 2.0f - m_overhangOffset.x,
-textMetrics.height / 2.0f - m_overhangOffset.y
);
//
// Round the offset to the nearest pixel. Note that the rounding
// done here is unecessary, but it causes the text to be less
// blurry.
//
FLOAT dpiX;
FLOAT dpiY;
m_pRT->GetDpi(&dpiX, &dpiY);
D2D1_POINT_2F roundedOffset = D2D1::Point2F(
floor(offset.x * dpiX / 96.0f + 0.5f) * 96.0f / dpiX,
floor(offset.y * dpiY / 96.0f + 0.5f) * 96.0f / dpiY
);
D2D1_RECT_F destinationRect = D2D1::RectF(
roundedOffset.x,
roundedOffset.y,
roundedOffset.x + opacityRTSize.width,
roundedOffset.y + opacityRTSize.height
);
ID2D1Bitmap *pBitmap = NULL;
m_pOpacityRT->GetBitmap(&pBitmap);
pBitmap->GetDpi(&dpiX, &dpiY);
//
// The antialias mode must be set to D2D1_ANTIALIAS_MODE_ALIASED
// for this method to succeed. We've set this mode already though
// so no need to do it again.
//
m_pRT->FillOpacityMask(
pBitmap,
m_pBlackBrush,
D2D1_OPACITY_MASK_CONTENT_TEXT_NATURAL,
&destinationRect
);
pBitmap->Release();
}
else
{
// Disable pixel snapping to get a smoother animation.
m_pRT->DrawTextLayout(
D2D1::Point2F(-textMetrics.width / 2.0f, -textMetrics.height / 2.0f),
m_pTextLayout,
m_pBlackBrush,
D2D1_DRAW_TEXT_OPTIONS_NO_SNAP
);
}
hr = m_pRT->EndDraw();
if (hr == D2DERR_RECREATE_TARGET)
{
hr = S_OK;
DiscardDeviceResources();
}
// To animate as quickly as possible, we request another WM_PAINT
// immediately.
InvalidateRect(m_hwnd, NULL, FALSE);
}
UpdateWindowText();
return hr;
}
/******************************************************************
* *
* DemoApp::OnResize *
* *
* If the application receives a WM_SIZE message, this method *
* resizes the render target appropriately. *
* *
******************************************************************/
void DemoApp::OnResize(UINT width, UINT height)
{
if (m_pRT)
{
D2D1_SIZE_U size;
size.width = width;
size.height = height;
// Note: This method can fail, but it's okay to ignore the
// error here -- it will be repeated on the next call to
// EndDraw.
m_pRT->Resize(size);
}
}
/******************************************************************
* *
* DemoApp::OnDestroy *
* *
* If the application receives a WM_MOVE message, this method *
* takes the appropriate action. *
* *
******************************************************************/
void DemoApp::OnDestroy()
{
m_fRunning = FALSE;
}
/******************************************************************
* *
* DemoApp::WndProc *
* *
* Window message handler *
* *
******************************************************************/
LRESULT CALLBACK DemoApp::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
LRESULT result = 0;
if (message == WM_CREATE)
{
LPCREATESTRUCT pcs = (LPCREATESTRUCT)lParam;
DemoApp *pDemoApp = (DemoApp *)pcs->lpCreateParams;
::SetWindowLongPtrW(
hwnd,
GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(pDemoApp)
);
result = 1;
}
else
{
DemoApp *pDemoApp = reinterpret_cast<DemoApp *>(
::GetWindowLongPtrW(
hwnd,
GWLP_USERDATA
));
bool wasHandled = false;
if (pDemoApp)
{
switch(message)
{
case WM_SIZE:
{
UINT width = LOWORD(lParam);
UINT height = HIWORD(lParam);
pDemoApp->OnResize(width, height);
}
result = 0;
wasHandled = true;
break;
case WM_CHAR:
{
pDemoApp->OnChar(static_cast<SHORT>(wParam));
}
result = 0;
wasHandled = true;
break;
case WM_PAINT:
case WM_DISPLAYCHANGE:
{
PAINTSTRUCT ps;
BeginPaint(hwnd, &ps);
pDemoApp->OnRender();
EndPaint(hwnd, &ps);
}
result = 0;
wasHandled = true;
break;
case WM_DESTROY:
{
pDemoApp->OnDestroy();
PostQuitMessage(0);
}
result = 1;
wasHandled = true;
break;
}
}
if (!wasHandled)
{
result = DefWindowProc(hwnd, message, wParam, lParam);
}
}
return result;
}