// 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(ceil(640.0f * dpiX / 96.0f)), static_cast(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(&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(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((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(ceil(maskSize.width * dpiX / 96.0f)), static_cast(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(pDemoApp) ); result = 1; } else { DemoApp *pDemoApp = reinterpret_cast( ::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(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; }