Advertisement
alien_fx_fiend

Infinite Doodle Canvas Direct2D So-Called Performance Boosts But Still Laggy AF!

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