Advertisement
alien_fx_fiend

Infinite Doodle Canvas Direct2D Needs Colors fix + Speed Optimization

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