Advertisement
alien_fx_fiend

Infinite Canvas Doodle Using D2D Hardw Accel (Bug fix StatBar Not Updating In RealTime)

Mar 23rd, 2025 (edited)
1,015
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 32.02 KB | Source Code | 0 0
  1. ==++ Here's the full source of (file 1/1) "Doodle-D2D-HAcc.cpp"::: ++==
  2. ```Doodle-D2D-HAcc.cpp
  3. #define NOMINMAX
  4. #define WIN32_LEAN_AND_MEAN
  5.  
  6. #include <windows.h>
  7. #include <windowsx.h>
  8. #include <commctrl.h>
  9. #include <commdlg.h>
  10. #include <d2d1.h>
  11. #include <dwrite.h>
  12. #include <string>  // add if not already included
  13. #include <cmath>
  14. #include <vector>
  15. #include <mutex>
  16. #include <fstream>
  17. #include <thread>
  18. #include <algorithm>
  19. #include "resource.h"
  20.  
  21. #pragma comment(lib, "d2d1.lib")
  22. #pragma comment(lib, "comctl32.lib")
  23.  
  24. //----------------------------------------------------------------
  25. // Data Structures and Globals
  26. //----------------------------------------------------------------
  27.  
  28. struct DrawPoint {
  29.    int x, y;
  30.    DWORD timestamp;
  31.    DrawPoint() : x(0), y(0), timestamp(0) {}
  32.    DrawPoint(int px, int py) : x(px), y(py), timestamp(GetTickCount()) {}
  33. };
  34.  
  35. struct SerializedStroke {
  36.    std::vector<DrawPoint> points;
  37.    COLORREF color;
  38.    int brushSize;
  39.    bool isEraser;
  40. };
  41.  
  42. std::mutex strokeMutex;
  43. std::vector<SerializedStroke> strokeHistory;
  44. std::vector<DrawPoint> strokeBuffer;
  45. const double MIN_DISTANCE = 2.0;
  46.  
  47. COLORREF currentBrushColor = RGB(24, 123, 205);
  48. int brushSize = 10;
  49. bool isDrawing = false;
  50. bool isEraserMode = false;
  51. bool isPaintbrushSelected = true;
  52. bool isSpacePressed = false;
  53. POINT lastMousePos = { 0, 0 };
  54.  
  55. int scrollX = 0;
  56. int scrollY = 0;
  57. float gridZoomFactor = 1.0f;
  58. bool showGrid = true;
  59. bool useAlphaGrid = false;
  60. int gridOpacity = 255;
  61. const int GRID_SIZE = 100;
  62.  
  63. HINSTANCE hInst;
  64. HWND hWnd;
  65. //HWND hStatusBar = NULL;
  66. // Global DirectWrite objects:
  67. IDWriteFactory* pDWriteFactory = nullptr;
  68. IDWriteTextFormat* pTextFormat = nullptr;
  69. std::wstring g_statusText = L"";
  70. // Add DirectWrite globals and a global status string:
  71. //IDWriteFactory* pDWriteFactory = nullptr;
  72. //IDWriteTextFormat* pTextFormat = nullptr;
  73. DWORD lastStatusUpdateTime = 0;
  74. const DWORD STATUS_UPDATE_INTERVAL = 50;
  75. HDC hStatusBufferDC = NULL;
  76. HBITMAP hStatusBufferBitmap = NULL;
  77.  
  78. // Serialization globals
  79. const wchar_t* STATE_FILE = L"canvas_state2.bin";
  80. bool isLoading = false;
  81. bool sessionDirty = false;
  82.  
  83. // For Direct2D
  84. ID2D1Factory* pFactory = nullptr;
  85. ID2D1HwndRenderTarget* pRenderTarget = nullptr;
  86. ID2D1BitmapRenderTarget* pOffscreenRT = nullptr;
  87. bool offscreenDirty = true;
  88. int lastOffscreenScrollX = 0;
  89. int lastOffscreenScrollY = 0;
  90.  
  91. //----------------------------------------------------------------
  92. // Function Declarations
  93. //----------------------------------------------------------------
  94.  
  95. void SaveCanvasState();
  96. void LoadCanvasStateAsync(HWND hwnd);
  97. void UpdateStatus(HWND hwnd);
  98. void InitializeStatusBuffer(HWND hStatus);
  99. void UpdateOffscreenBuffer(HWND hwnd);
  100. HRESULT CreateDeviceResources(HWND hwnd);
  101. void DiscardDeviceResources();
  102. void DrawSmoothStroke(ID2D1RenderTarget* pRT, const std::vector<DrawPoint>& points, bool isEraser, COLORREF strokeColor, int strokeSize, int offsetX, int offsetY);
  103. void DrawGrid(ID2D1RenderTarget* pRT, const D2D1_RECT_F& rect);
  104.  
  105. //----------------------------------------------------------------
  106. // Serialization Functions
  107. //----------------------------------------------------------------
  108.  
  109. void SaveCanvasState() {
  110.    std::ofstream file(STATE_FILE, std::ios::binary | std::ios::out);
  111.    if (!file)
  112.        return;
  113.    file.write(reinterpret_cast<const char*>(&gridZoomFactor), sizeof(float));
  114.    file.write(reinterpret_cast<const char*>(&showGrid), sizeof(bool));
  115.    file.write(reinterpret_cast<const char*>(&useAlphaGrid), sizeof(bool));
  116.    file.write(reinterpret_cast<const char*>(&gridOpacity), sizeof(int));
  117.    file.write(reinterpret_cast<const char*>(&currentBrushColor), sizeof(COLORREF));
  118.    file.write(reinterpret_cast<const char*>(&brushSize), sizeof(int));
  119.    {
  120.        std::lock_guard<std::mutex> lock(strokeMutex);
  121.        size_t strokeCount = strokeHistory.size();
  122.        file.write(reinterpret_cast<const char*>(&strokeCount), sizeof(size_t));
  123.        for (const auto& stroke : strokeHistory) {
  124.            std::vector<DrawPoint> optimizedPoints;
  125.            if (!stroke.points.empty()) {
  126.                optimizedPoints.push_back(stroke.points[0]);
  127.                for (size_t i = 1; i < stroke.points.size(); ++i) {
  128.                    const DrawPoint& prev = optimizedPoints.back();
  129.                    const DrawPoint& curr = stroke.points[i];
  130.                    double dx = curr.x - prev.x;
  131.                    double dy = curr.y - prev.y;
  132.                    double distance = sqrt(dx * dx + dy * dy);
  133.                    if (distance >= MIN_DISTANCE)
  134.                        optimizedPoints.push_back(curr);
  135.                }
  136.            }
  137.            size_t pointCount = optimizedPoints.size();
  138.            file.write(reinterpret_cast<const char*>(&pointCount), sizeof(size_t));
  139.            if (pointCount > 0)
  140.                file.write(reinterpret_cast<const char*>(optimizedPoints.data()), pointCount * sizeof(DrawPoint));
  141.            file.write(reinterpret_cast<const char*>(&stroke.color), sizeof(COLORREF));
  142.            file.write(reinterpret_cast<const char*>(&stroke.brushSize), sizeof(int));
  143.            file.write(reinterpret_cast<const char*>(&stroke.isEraser), sizeof(bool));
  144.        }
  145.    }
  146.    file.close();
  147. }
  148.  
  149. void LoadCanvasStateAsync(HWND hwnd) {
  150.    isLoading = true;
  151.    std::thread([hwnd]() {
  152.        std::ifstream file(STATE_FILE, std::ios::binary | std::ios::in);
  153.        if (!file) {
  154.            isLoading = false;
  155.            return;
  156.        }
  157.        try {
  158.            file.read(reinterpret_cast<char*>(&gridZoomFactor), sizeof(float));
  159.            file.read(reinterpret_cast<char*>(&showGrid), sizeof(bool));
  160.            file.read(reinterpret_cast<char*>(&useAlphaGrid), sizeof(bool));
  161.            file.read(reinterpret_cast<char*>(&gridOpacity), sizeof(int));
  162.            file.read(reinterpret_cast<char*>(&currentBrushColor), sizeof(COLORREF));
  163.            file.read(reinterpret_cast<char*>(&brushSize), sizeof(int));
  164.            size_t strokeCount = 0;
  165.            file.read(reinterpret_cast<char*>(&strokeCount), sizeof(size_t));
  166.            std::vector<SerializedStroke> loadedStrokes;
  167.            for (size_t i = 0; i < strokeCount && file.good(); ++i) {
  168.                SerializedStroke stroke;
  169.                size_t pointCount = 0;
  170.                file.read(reinterpret_cast<char*>(&pointCount), sizeof(size_t));
  171.                if (pointCount > 0 && pointCount < 1000000) {
  172.                    for (size_t j = 0; j < pointCount; ++j) {
  173.                        DrawPoint point;
  174.                        file.read(reinterpret_cast<char*>(&point.x), sizeof(int));
  175.                        file.read(reinterpret_cast<char*>(&point.y), sizeof(int));
  176.                        file.read(reinterpret_cast<char*>(&point.timestamp), sizeof(DWORD));
  177.                        stroke.points.push_back(point);
  178.                    }
  179.                    file.read(reinterpret_cast<char*>(&stroke.color), sizeof(COLORREF));
  180.                    file.read(reinterpret_cast<char*>(&stroke.brushSize), sizeof(int));
  181.                    file.read(reinterpret_cast<char*>(&stroke.isEraser), sizeof(bool));
  182.                    loadedStrokes.push_back(stroke);
  183.                }
  184.            }
  185.            {
  186.                std::lock_guard<std::mutex> lock(strokeMutex);
  187.                strokeHistory = std::move(loadedStrokes);
  188.            }
  189.        }
  190.        catch (...) {
  191.            isLoading = false;
  192.            return;
  193.        }
  194.        file.close();
  195.        isLoading = false;
  196.        // Post a message to update offscreen buffer after loading
  197.        PostMessage(hwnd, WM_USER + 1, 0, 0);
  198.        }).detach();
  199. }
  200.  
  201. //----------------------------------------------------------------
  202. // Direct2D Initialization and Resource Management
  203. //----------------------------------------------------------------
  204.  
  205. HRESULT CreateDeviceResources(HWND hwnd) {
  206.    if (pRenderTarget)
  207.        return S_OK;
  208.    RECT rc;
  209.    GetClientRect(hwnd, &rc);
  210.    D2D1_SIZE_U size = D2D1::SizeU(rc.right, rc.bottom);
  211.  
  212.    HRESULT hr = pFactory->CreateHwndRenderTarget(
  213.        D2D1::RenderTargetProperties(),
  214.        D2D1::HwndRenderTargetProperties(hwnd, size),
  215.        &pRenderTarget
  216.    );
  217.    if (SUCCEEDED(hr)) {
  218.        // Create an offscreen compatible render target for persistent drawing.
  219.        hr = pRenderTarget->CreateCompatibleRenderTarget(
  220.            D2D1::SizeF((FLOAT)rc.right, (FLOAT)rc.bottom),
  221.            &pOffscreenRT
  222.        );
  223.        if (SUCCEEDED(hr)) {
  224.            // Mark offscreen as dirty so it is initially updated.
  225.            offscreenDirty = true;
  226.            lastOffscreenScrollX = scrollX;
  227.            lastOffscreenScrollY = scrollY;
  228.        }
  229.    }
  230.    return hr;
  231. }
  232.  
  233. void DiscardDeviceResources() {
  234.    if (pOffscreenRT) {
  235.        pOffscreenRT->Release();
  236.        pOffscreenRT = nullptr;
  237.    }
  238.    if (pRenderTarget) {
  239.        pRenderTarget->Release();
  240.        pRenderTarget = nullptr;
  241.    }
  242. }
  243.  
  244. //----------------------------------------------------------------
  245. // Drawing Functions (Direct2D versions)
  246. //----------------------------------------------------------------
  247.  
  248. void DrawSmoothStroke(ID2D1RenderTarget* pRT, const std::vector<DrawPoint>& points, bool isEraser, COLORREF strokeColor, int strokeSize, int offsetX, int offsetY) {
  249.    if (points.empty())
  250.        return;
  251.  
  252.    // Determine color; for eraser use white.
  253.    D2D1_COLOR_F color = isEraser ? D2D1::ColorF(D2D1::ColorF::White) :
  254.        D2D1::ColorF(
  255.            GetRValue(strokeColor) / 255.0f,
  256.            GetGValue(strokeColor) / 255.0f,
  257.            GetBValue(strokeColor) / 255.0f
  258.        );
  259.  
  260.    ID2D1SolidColorBrush* pBrush = nullptr;
  261.    if (FAILED(pRT->CreateSolidColorBrush(color, &pBrush)))
  262.        return;
  263.  
  264.    if (points.size() == 1) {
  265.        const DrawPoint& pt = points[0];
  266.        D2D1_ELLIPSE ellipse = D2D1::Ellipse(
  267.            D2D1::Point2F((FLOAT)(pt.x - offsetX), (FLOAT)(pt.y - offsetY)),
  268.            (FLOAT)brushSize, (FLOAT)brushSize);
  269.        pRT->FillEllipse(ellipse, pBrush);
  270.    }
  271.    else {
  272.        for (size_t i = 1; i < points.size(); ++i) {
  273.            const DrawPoint& prev = points[i - 1];
  274.            const DrawPoint& curr = points[i];
  275.            double dx = curr.x - prev.x;
  276.            double dy = curr.y - prev.y;
  277.            double distance = sqrt(dx * dx + dy * dy);
  278.            if (distance > 0) {
  279.                int steps = std::max(1, (int)(distance / 2));
  280.                for (int step = 0; step <= steps; ++step) {
  281.                    double t = step / (double)steps;
  282.                    int x = (int)(prev.x + dx * t);
  283.                    int y = (int)(prev.y + dy * t);
  284.                    D2D1_ELLIPSE ellipse = D2D1::Ellipse(
  285.                        D2D1::Point2F((FLOAT)(x - offsetX), (FLOAT)(y - offsetY)),
  286.                        (FLOAT)brushSize, (FLOAT)brushSize);
  287.                    pRT->FillEllipse(ellipse, pBrush);
  288.                }
  289.            }
  290.        }
  291.    }
  292.    pBrush->Release();
  293. }
  294.  
  295. void DrawGrid(ID2D1RenderTarget* pRT, const D2D1_RECT_F& rect) {
  296.    // Use a solid orange color for grid lines.
  297.    ID2D1SolidColorBrush* pGridBrush = nullptr;
  298.    pRT->CreateSolidColorBrush(D2D1::ColorF(1.0f, 0.55f, 0.0f), &pGridBrush);
  299.    int scaledGridSize = (int)(GRID_SIZE * gridZoomFactor);
  300.    // Compute starting positions based on scroll offsets.
  301.    int modX = scrollX % scaledGridSize;
  302.    if (modX < 0)
  303.        modX += scaledGridSize;
  304.    float startX = -modX;
  305.    for (float x = startX; x < rect.right; x += scaledGridSize) {
  306.        pRT->DrawLine(D2D1::Point2F(x, rect.top), D2D1::Point2F(x, rect.bottom), pGridBrush, 1.0f);
  307.    }
  308.    int modY = scrollY % scaledGridSize;
  309.    if (modY < 0)
  310.        modY += scaledGridSize;
  311.    float startY = -modY;
  312.    for (float y = startY; y < rect.bottom; y += scaledGridSize) {
  313.        pRT->DrawLine(D2D1::Point2F(rect.left, y), D2D1::Point2F(rect.right, y), pGridBrush, 1.0f);
  314.    }
  315.    pGridBrush->Release();
  316. }
  317.  
  318. //----------------------------------------------------------------
  319. // Offscreen Buffer Update (using pOffscreenRT)
  320. //----------------------------------------------------------------
  321.  
  322. void UpdateOffscreenBuffer(HWND hwnd) {
  323.    if (!pOffscreenRT)
  324.        return;
  325.    pOffscreenRT->BeginDraw();
  326.    // Clear offscreen render target to white.
  327.    pOffscreenRT->Clear(D2D1::ColorF(D2D1::ColorF::White));
  328.    // Redraw all strokes.
  329.    {
  330.        std::lock_guard<std::mutex> lock(strokeMutex);
  331.        for (const auto& stroke : strokeHistory) {
  332.            DrawSmoothStroke(pOffscreenRT, stroke.points, stroke.isEraser, stroke.color, stroke.brushSize, scrollX, scrollY);
  333.        }
  334.    }
  335.    HRESULT hr = pOffscreenRT->EndDraw();
  336.    // Mark offscreen as clean.
  337.    offscreenDirty = false;
  338.    lastOffscreenScrollX = scrollX;
  339.    lastOffscreenScrollY = scrollY;
  340. }
  341.  
  342. //----------------------------------------------------------------
  343. // Status Bar Functions (GDI remains unchanged)
  344. //----------------------------------------------------------------
  345.  
  346. void InitializeStatusBuffer(HWND hStatus) {
  347.    if (hStatusBufferDC) {
  348.        DeleteDC(hStatusBufferDC);
  349.        DeleteObject(hStatusBufferBitmap);
  350.    }
  351.    HDC hdc = GetDC(hStatus);
  352.    RECT rect;
  353.    GetClientRect(hStatus, &rect);
  354.    hStatusBufferDC = CreateCompatibleDC(hdc);
  355.    hStatusBufferBitmap = CreateCompatibleBitmap(hdc, rect.right, rect.bottom);
  356.    SelectObject(hStatusBufferDC, hStatusBufferBitmap);
  357.    ReleaseDC(hStatus, hdc);
  358. }
  359.  
  360. void UpdateStatus(HWND hwnd) {
  361.    DWORD currentTime = GetTickCount();
  362.    if (currentTime - lastStatusUpdateTime < STATUS_UPDATE_INTERVAL)
  363.        return;
  364.    lastStatusUpdateTime = currentTime;
  365.    wchar_t status[512];
  366.    BYTE r = GetRValue(currentBrushColor);
  367.    BYTE g = GetGValue(currentBrushColor);
  368.    BYTE b = GetBValue(currentBrushColor);
  369.    swprintf_s(status, 512,
  370.        L"Mode: %s | Brush: %d | Color: RGB(%d,%d,%d) | Grid: %s%s | Zoom: %.1fx | Opacity: %d%% | Canvas Pos: (%d,%d)",
  371.        isEraserMode ? L"Eraser" : L"Draw",
  372.        brushSize,
  373.        r, g, b,
  374.        showGrid ? L"On" : L"Off",
  375.        useAlphaGrid ? L"(Alpha)" : L"",
  376.        gridZoomFactor,
  377.        (gridOpacity * 100) / 255,
  378.        scrollX, scrollY
  379.    );
  380.    g_statusText = status;
  381. }
  382.  
  383. /* void UpdateStatus(HWND hwnd) {
  384.    DWORD currentTime = GetTickCount();
  385.    if (currentTime - lastStatusUpdateTime < STATUS_UPDATE_INTERVAL)
  386.        return;
  387.    lastStatusUpdateTime = currentTime;
  388.    if (!hStatusBar)
  389.        return;
  390.    if (!hStatusBufferDC) {
  391.        InitializeStatusBuffer(hStatusBar);
  392.    }
  393.    RECT statusRect;
  394.    GetClientRect(hStatusBar, &statusRect);
  395.    wchar_t status[512];
  396.    BYTE r = GetRValue(currentBrushColor);
  397.    BYTE g = GetGValue(currentBrushColor);
  398.    BYTE b = GetBValue(currentBrushColor);
  399.    swprintf_s(status, 512,
  400.        L"Mode: %s | Brush: %d | Color: RGB(%d,%d,%d) | Grid: %s%s | Zoom: %.1fx | Opacity: %d%% | Canvas Pos: (%d,%d)",
  401.        isEraserMode ? L"Eraser" : L"Draw",
  402.        brushSize,
  403.        r, g, b,
  404.        showGrid ? L"On" : L"Off",
  405.        useAlphaGrid ? L"(Alpha)" : L"",
  406.        gridZoomFactor,
  407.        (gridOpacity * 100) / 255,
  408.        scrollX, scrollY
  409.    );
  410.    SendMessage(hStatusBar, SB_SETTEXT, 0, (LPARAM)status);
  411. } */
  412.  
  413. //----------------------------------------------------------------
  414. // Window Procedure
  415. //----------------------------------------------------------------
  416.  
  417. LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
  418.    HRESULT hr;
  419.    switch (uMsg) {
  420.    case WM_CREATE:
  421.    {
  422.        // Initialize Direct2D Factory
  423.        HRESULT hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory);
  424.        if (FAILED(hr))
  425.            return -1;
  426.  
  427.        // Initialize DirectWrite Factory and Text Format for the status text.
  428.        HRESULT hrDWrite = DWriteCreateFactory(
  429.            DWRITE_FACTORY_TYPE_SHARED,
  430.            __uuidof(IDWriteFactory),
  431.            reinterpret_cast<IUnknown**>(&pDWriteFactory)
  432.        );
  433.        if (SUCCEEDED(hrDWrite))
  434.        {
  435.            hrDWrite = pDWriteFactory->CreateTextFormat(
  436.                L"Segoe UI",                // Font family name.
  437.                NULL,                       // Use system font collection.
  438.                DWRITE_FONT_WEIGHT_NORMAL,
  439.                DWRITE_FONT_STYLE_NORMAL,
  440.                DWRITE_FONT_STRETCH_NORMAL,
  441.                14.0f,                      // Font size.
  442.                L"",                        // Locale.
  443.                &pTextFormat
  444.            );
  445.            if (SUCCEEDED(hrDWrite))
  446.            {
  447.                pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING);
  448.                pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
  449.            }
  450.        }
  451.  
  452.        // (Remove GDI status bar creation; status will be rendered via Direct2D.)
  453.  
  454.        // Device resources (pRenderTarget and pOffscreenRT) will be created in WM_SIZE.
  455.        LoadCanvasStateAsync(hwnd);
  456.        return 0;
  457.    }
  458.    case WM_SIZE:
  459.    {
  460.        RECT rcClient;
  461.        GetClientRect(hwnd, &rcClient);
  462.  
  463.        // Resize (or create) the main render target.
  464.        if (pRenderTarget)
  465.        {
  466.            pRenderTarget->Resize(D2D1::SizeU(rcClient.right, rcClient.bottom));
  467.        }
  468.        else
  469.        {
  470.            HRESULT hr = CreateDeviceResources(hwnd);
  471.            if (FAILED(hr))
  472.                return -1;
  473.        }
  474.  
  475.        // Recreate the offscreen render target.
  476.        if (pOffscreenRT)
  477.        {
  478.            pOffscreenRT->Release();
  479.            pOffscreenRT = nullptr;
  480.        }
  481.        HRESULT hr = pRenderTarget->CreateCompatibleRenderTarget(
  482.            D2D1::SizeF((FLOAT)rcClient.right, (FLOAT)rcClient.bottom),
  483.            &pOffscreenRT
  484.        );
  485.        if (SUCCEEDED(hr))
  486.        {
  487.            offscreenDirty = true;               // Force update of the offscreen buffer.
  488.            lastOffscreenScrollX = scrollX;
  489.            lastOffscreenScrollY = scrollY;
  490.            UpdateOffscreenBuffer(hwnd);         // Rebuild the offscreen content.
  491.        }
  492.  
  493.        // Update status (which now contains the grid state) and force a full redraw.
  494.        UpdateStatus(hwnd);
  495.        InvalidateRect(hwnd, NULL, TRUE);
  496.        return 0;
  497.    }
  498.        case WM_KEYDOWN:
  499.    {
  500.        if (GetKeyState(VK_MENU) & 0x8000)
  501.            return DefWindowProc(hwnd, uMsg, wParam, lParam);
  502.        // Panning using Space + Drag is handled in WM_MOUSEMOVE.
  503.        // Additionally, allow arrow keys for panning.
  504.        if (wParam == VK_LEFT) {
  505.            scrollX -= 20;
  506.            offscreenDirty = true;
  507.            InvalidateRect(hwnd, NULL, FALSE);
  508.        }
  509.        else if (wParam == VK_RIGHT) {
  510.            scrollX += 20;
  511.            offscreenDirty = true;
  512.            InvalidateRect(hwnd, NULL, FALSE);
  513.        }
  514.        else if (wParam == VK_UP) {
  515.            scrollY -= 20;
  516.            offscreenDirty = true;
  517.            InvalidateRect(hwnd, NULL, FALSE);
  518.        }
  519.        else if (wParam == VK_DOWN) {
  520.            scrollY += 20;
  521.            offscreenDirty = true;
  522.            InvalidateRect(hwnd, NULL, FALSE);
  523.        }
  524.        else if (wParam == VK_SPACE && !isSpacePressed) {
  525.            isSpacePressed = true;
  526.            GetCursorPos(&lastMousePos);
  527.            ScreenToClient(hwnd, &lastMousePos);
  528.            SetCursor(LoadCursor(NULL, IDC_SIZEALL));
  529.            SetCapture(hwnd);
  530.        }
  531.        else if (wParam == 0x50) {
  532.            isPaintbrushSelected = true;
  533.            isEraserMode = false;
  534.            UpdateStatus(hwnd);
  535.            InvalidateRect(hwnd, NULL, TRUE);
  536.        }
  537.        else if (wParam == 0x45) {
  538.            isPaintbrushSelected = false;
  539.            isEraserMode = true;
  540.            UpdateStatus(hwnd);
  541.            InvalidateRect(hwnd, NULL, TRUE);
  542.        }
  543.        else if (wParam == 'Q') {
  544.            CHOOSECOLOR cc = { sizeof(CHOOSECOLOR) };
  545.            static COLORREF customColors[16] = { 0 };
  546.            cc.hwndOwner = hwnd;
  547.            cc.rgbResult = currentBrushColor;
  548.            cc.lpCustColors = customColors;
  549.            cc.Flags = CC_FULLOPEN | CC_RGBINIT;
  550.            if (ChooseColor(&cc))
  551.                currentBrushColor = cc.rgbResult;
  552.            UpdateStatus(hwnd);
  553.            offscreenDirty = true;
  554.            InvalidateRect(hwnd, NULL, TRUE);
  555.        }
  556.        else if (wParam == VK_ADD || wParam == VK_OEM_PLUS) {
  557.            brushSize = std::min(50, brushSize + 5);
  558.            offscreenDirty = true;  // Ensure new brush size is applied in drawing.
  559.            UpdateStatus(hwnd);
  560.            InvalidateRect(hwnd, NULL, TRUE);
  561.        }
  562.        else if (wParam == VK_SUBTRACT || wParam == VK_OEM_MINUS) {
  563.            brushSize = std::max(5, brushSize - 5);
  564.            offscreenDirty = true;
  565.            UpdateStatus(hwnd);
  566.            InvalidateRect(hwnd, NULL, TRUE);
  567.        }
  568.        else if (wParam == 0x43) {
  569.            std::lock_guard<std::mutex> lock(strokeMutex);
  570.            strokeHistory.clear();
  571.            sessionDirty = true;  // Mark session as changed.
  572.            offscreenDirty = true;
  573.            InvalidateRect(hwnd, NULL, TRUE);
  574.        }
  575.        else if (wParam == VK_HOME) {
  576.            scrollX = 0;
  577.            scrollY = 0;
  578.            lastOffscreenScrollX = 0;
  579.            lastOffscreenScrollY = 0;
  580.            offscreenDirty = true;
  581.            UpdateOffscreenBuffer(hwnd);
  582.            UpdateStatus(hwnd);
  583.            InvalidateRect(hwnd, NULL, TRUE);
  584.        }
  585.        else if (wParam == 'G') {
  586.            showGrid = !showGrid;
  587.            offscreenDirty = true;  // Mark offscreen dirty so grid redraws.
  588.            UpdateStatus(hwnd);
  589.            InvalidateRect(hwnd, NULL, TRUE);
  590.        }
  591.        else if (wParam == 'A') {
  592.            useAlphaGrid = !useAlphaGrid;
  593.            UpdateStatus(hwnd);
  594.            InvalidateRect(hwnd, NULL, FALSE);
  595.        }
  596.        else if (wParam == VK_PRIOR) {
  597.            gridZoomFactor *= 1.1f;
  598.            offscreenDirty = true;
  599.            UpdateStatus(hwnd);
  600.            InvalidateRect(hwnd, NULL, FALSE);
  601.        }
  602.        else if (wParam == VK_NEXT) {
  603.            gridZoomFactor *= 0.9f;
  604.            if (gridZoomFactor < 0.1f)
  605.                gridZoomFactor = 0.1f;
  606.            offscreenDirty = true;
  607.            UpdateStatus(hwnd);
  608.            InvalidateRect(hwnd, NULL, FALSE);
  609.        }
  610.        else if (wParam == VK_OEM_6 && useAlphaGrid) {
  611.            gridOpacity = std::min(255, gridOpacity + 15);
  612.            offscreenDirty = true;
  613.            UpdateStatus(hwnd);
  614.            InvalidateRect(hwnd, NULL, FALSE);
  615.        }
  616.        else if (wParam == VK_OEM_4 && useAlphaGrid) {
  617.            gridOpacity = std::max(0, gridOpacity - 15);
  618.            offscreenDirty = true;
  619.            UpdateStatus(hwnd);
  620.            InvalidateRect(hwnd, NULL, FALSE);
  621.        }
  622.        else if (wParam == VK_ESCAPE) {
  623.            if (isSpacePressed) {
  624.                isSpacePressed = false;
  625.                ReleaseCapture();
  626.            }
  627.            if (sessionDirty) {
  628.                SaveCanvasState();
  629.                sessionDirty = false;
  630.            }
  631.            PostQuitMessage(0);
  632.            return 0;
  633.        }
  634.        else if (wParam == VK_F1) {
  635.            MessageBox(hwnd,
  636.                L"Infinite Canvas Doodle App (Direct2D Accelerated)\n"
  637.                L"P=Brush, E=Eraser, C=Clear, +/-=BrushSize, Space+Drag/Arrow Keys=Panning, Home=Reset, Q=Color, G=Grid, A=Alpha, PgUp=ZoomIn, PgDn=ZoomOut, F1=About",
  638.                L"Information", MB_OK | MB_ICONINFORMATION);
  639.            return 0;
  640.        }
  641.        return 0;
  642.    }
  643.    case WM_KEYUP:
  644.    {
  645.        if (wParam == VK_SPACE) {
  646.            isSpacePressed = false;
  647.            SetCursor(LoadCursor(NULL, IDC_ARROW));
  648.            ReleaseCapture();
  649.            return 0;
  650.        }
  651.        return 0;
  652.    }
  653.    case WM_LBUTTONDOWN:
  654.    {
  655.        isDrawing = true;
  656.        int worldX = GET_X_LPARAM(lParam) + scrollX;
  657.        int worldY = GET_Y_LPARAM(lParam) + scrollY;
  658.        strokeBuffer.clear();
  659.        strokeBuffer.push_back(DrawPoint(worldX, worldY));
  660.        SetCapture(hwnd);
  661.        InvalidateRect(hwnd, NULL, FALSE);
  662.        return 0;
  663.    }
  664.    case WM_LBUTTONUP:
  665.    {
  666.        if (isDrawing) {
  667.            isDrawing = false;
  668.            SerializedStroke stroke;
  669.            stroke.points = strokeBuffer;
  670.            stroke.color = currentBrushColor;
  671.            stroke.brushSize = brushSize;
  672.            stroke.isEraser = isEraserMode;
  673.            {
  674.                std::lock_guard<std::mutex> lock(strokeMutex);
  675.                strokeHistory.push_back(stroke);
  676.            }
  677.            strokeBuffer.clear();
  678.            ReleaseCapture();
  679.            InvalidateRect(hwnd, NULL, FALSE);
  680.            sessionDirty = true;
  681.            if (sessionDirty) {
  682.                SaveCanvasState();
  683.                sessionDirty = false;
  684.            }
  685.            offscreenDirty = true;
  686.            UpdateOffscreenBuffer(hwnd);
  687.            UpdateStatus(hwnd);
  688.        }
  689.        return 0;
  690.    }
  691.    case WM_MOUSEMOVE:
  692.    {
  693.        int x = GET_X_LPARAM(lParam);
  694.        int y = GET_Y_LPARAM(lParam);
  695.        if (isSpacePressed) {
  696.            RECT clientRect;
  697.            GetClientRect(hwnd, &clientRect);
  698.            int deltaX = x - lastMousePos.x;
  699.            int deltaY = y - lastMousePos.y;
  700.            scrollX -= deltaX;
  701.            scrollY -= deltaY;
  702.            lastMousePos.x = x;
  703.            lastMousePos.y = y;
  704.            if (scrollX != lastOffscreenScrollX || scrollY != lastOffscreenScrollY)
  705.                offscreenDirty = true;
  706.            UpdateStatus(hwnd);
  707.            InvalidateRect(hwnd, NULL, FALSE);
  708.        }
  709.        else if (isDrawing && (wParam & MK_LBUTTON)) {
  710.            int worldX = x + scrollX;
  711.            int worldY = y + scrollY;
  712.            if (strokeBuffer.empty())
  713.                strokeBuffer.push_back(DrawPoint(worldX, worldY));
  714.            else {
  715.                const DrawPoint& lastPt = strokeBuffer.back();
  716.                double dx = worldX - lastPt.x;
  717.                double dy = worldY - lastPt.y;
  718.                double distance = sqrt(dx * dx + dy * dy);
  719.                if (distance >= MIN_DISTANCE)
  720.                    strokeBuffer.push_back(DrawPoint(worldX, worldY));
  721.            }
  722.            // Compute dirty rectangle for the new segment (optional, for partial redraw)
  723.            RECT dirty;
  724.            int clientPrevX = strokeBuffer.back().x - scrollX;
  725.            int clientPrevY = strokeBuffer.back().y - scrollY;
  726.            int clientNewX = x;
  727.            int clientNewY = y;
  728.            dirty.left = std::min(clientPrevX, clientNewX) - brushSize;
  729.            dirty.top = std::min(clientPrevY, clientNewY) - brushSize;
  730.            dirty.right = std::max(clientPrevX, clientNewX) + brushSize;
  731.            dirty.bottom = std::max(clientPrevY, clientNewY) + brushSize;
  732.            InvalidateRect(hwnd, &dirty, FALSE);
  733.        }
  734.        return 0;
  735.    }
  736.    case WM_USER + 1:
  737.    {
  738.        // Custom message after state loading.
  739.        offscreenDirty = true;
  740.        UpdateOffscreenBuffer(hwnd);
  741.        InvalidateRect(hwnd, NULL, TRUE);
  742.        break;
  743.    }
  744.    case WM_ERASEBKGND:
  745.        return 1;
  746.    case WM_PAINT:
  747.    {
  748.        PAINTSTRUCT ps;
  749.        BeginPaint(hwnd, &ps);
  750.  
  751.        pRenderTarget->BeginDraw();
  752.  
  753.        // Update offscreen buffer if dirty.
  754.        if (offscreenDirty)
  755.        {
  756.            UpdateOffscreenBuffer(hwnd);
  757.        }
  758.  
  759.        // Draw the persistent offscreen render target.
  760.        ID2D1Bitmap* pOffscreenBitmap = nullptr;
  761.        pOffscreenRT->GetBitmap(&pOffscreenBitmap);
  762.        pRenderTarget->DrawBitmap(pOffscreenBitmap);
  763.        pOffscreenBitmap->Release();
  764.  
  765.        // Overlay in-progress stroke.
  766.        if (isDrawing && !strokeBuffer.empty())
  767.        {
  768.            DrawSmoothStroke(pRenderTarget, strokeBuffer, isEraserMode, currentBrushColor, brushSize, scrollX, scrollY);
  769.        }
  770.  
  771.        // Get the full client area for grid drawing.
  772.        RECT rcClient;
  773.        GetClientRect(hwnd, &rcClient);
  774.        D2D1_RECT_F d2dRect = D2D1::RectF(0, 0, (FLOAT)rcClient.right, (FLOAT)rcClient.bottom);
  775.        if (showGrid)
  776.        {
  777.            DrawGrid(pRenderTarget, d2dRect);
  778.        }
  779.  
  780.        // Render the status bar using Direct2D/DirectWrite.
  781.        {
  782.            float statusBarHeight = 30.0f;
  783.            D2D1_RECT_F statusRect = D2D1::RectF(
  784.                0,
  785.                (FLOAT)rcClient.bottom - statusBarHeight,
  786.                (FLOAT)rcClient.right,
  787.                (FLOAT)rcClient.bottom
  788.            );
  789.            // Fill status bar background.
  790.            ID2D1SolidColorBrush* pStatusBgBrush = nullptr;
  791.            pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(0.2f, 0.2f, 0.2f), &pStatusBgBrush);
  792.            pRenderTarget->FillRectangle(statusRect, pStatusBgBrush);
  793.            pStatusBgBrush->Release();
  794.            // Draw status text.
  795.            ID2D1SolidColorBrush* pTextBrush = nullptr;
  796.            pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pTextBrush);
  797.            pRenderTarget->DrawTextW(
  798.                g_statusText.c_str(),
  799.                static_cast<UINT32>(g_statusText.length()),
  800.                pTextFormat,
  801.                &statusRect,
  802.                pTextBrush,
  803.                D2D1_DRAW_TEXT_OPTIONS_NONE,
  804.                DWRITE_MEASURING_MODE_NATURAL
  805.            );
  806.            pTextBrush->Release();
  807.        }
  808.  
  809.        HRESULT hr = pRenderTarget->EndDraw();
  810.        EndPaint(hwnd, &ps);
  811.        return 0;
  812.    }
  813.    case WM_SETCURSOR:
  814.    {
  815.        if (LOWORD(lParam) == HTCLIENT) {
  816.            if (isSpacePressed) {
  817.                SetCursor(LoadCursor(NULL, IDC_SIZEALL));
  818.                return TRUE;
  819.            }
  820.            else if (isPaintbrushSelected || isEraserMode) {
  821.                SetCursor(LoadCursor(NULL, IDC_CROSS));
  822.                return TRUE;
  823.            }
  824.        }
  825.        return DefWindowProc(hwnd, uMsg, wParam, lParam);
  826.    }
  827.    case WM_DESTROY:
  828.    {
  829.        if (sessionDirty)
  830.        {
  831.            SaveCanvasState();
  832.            sessionDirty = false;
  833.        }
  834.        DiscardDeviceResources();
  835.        if (pFactory)
  836.        {
  837.            pFactory->Release();
  838.            pFactory = nullptr;
  839.        }
  840.        if (pTextFormat)
  841.        {
  842.            pTextFormat->Release();
  843.            pTextFormat = nullptr;
  844.        }
  845.        if (pDWriteFactory)
  846.        {
  847.            pDWriteFactory->Release();
  848.            pDWriteFactory = nullptr;
  849.        }
  850.        PostQuitMessage(0);
  851.        return 0;
  852.    }
  853.    default:
  854.        return DefWindowProc(hwnd, uMsg, wParam, lParam);
  855.    }
  856.    return 0;
  857. }
  858.  
  859. //----------------------------------------------------------------
  860. // WinMain
  861. //----------------------------------------------------------------
  862.  
  863. int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow) {
  864.    INITCOMMONCONTROLSEX icex = { sizeof(INITCOMMONCONTROLSEX), ICC_BAR_CLASSES };
  865.    InitCommonControlsEx(&icex);
  866.    const wchar_t CLASS_NAME[] = L"InfiniteCanvasClass";
  867.    WNDCLASS wc = {};
  868.    wc.lpfnWndProc = WindowProc;
  869.    wc.hInstance = hInstance;
  870.    wc.lpszClassName = CLASS_NAME;
  871.    wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1));
  872.    RegisterClass(&wc);
  873.    hInst = hInstance;
  874.    hWnd = CreateWindowEx(0, CLASS_NAME,
  875.        L"Infinite Canvas Doodle App (Direct2D Accelerated, P=Brush, E=Eraser, C=Clear, +/-=BrushSize, Space+Drag/Arrow=Panning, Home=Reset, Q=Color, G=Grid, A=Alpha, PgUp=ZoomIn, PgDn=ZoomOut, F1=About)",
  876.        WS_OVERLAPPEDWINDOW | WS_MAXIMIZE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
  877.        NULL, NULL, hInstance, NULL);
  878.    if (hWnd == NULL)
  879.        return 0;
  880.    // Enable double buffering via WS_EX_COMPOSITED.
  881.    SetWindowLongPtr(hWnd, GWL_EXSTYLE, GetWindowLongPtr(hWnd, GWL_EXSTYLE) | WS_EX_COMPOSITED);
  882.    ShowWindow(hWnd, SW_SHOWMAXIMIZED);
  883.    MSG msg = {};
  884.    while (GetMessage(&msg, NULL, 0, 0)) {
  885.        TranslateMessage(&msg);
  886.        DispatchMessage(&msg);
  887.    }
  888.    return 0;
  889. }
  890. ```
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement