Search code examples
winapidirect2d

Text on Path for Direct2D


I recently encountered this SVG:

<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <!-- to hide the path, it is usually wrapped in a <defs> element -->
  <!-- <defs> -->
  <path
    id="MyPath"
    fill="none"
    stroke="red"
    d="M10,90 Q90,90 90,45 Q90,10 50,10 Q10,10 10,40 Q10,70 45,70 Q70,70 75,50" />
  <!-- </defs> -->

  <text>
    <textPath href="#MyPath">Quick brown fox jumps over the lazy dog.</textPath>
  </text>
</svg>

Which renders in browsers like that:

enter image description here

Now in Direct2D SVG this isn't supported. Even if it was, what 's the math algorithm behind this? I want to implement it myself in C++ but I can't find any resource that describes a logic for it. I am already using a custom Direct2D renderer but , given a path, I'm not sure how to proceed.

Essentialy, what I need is, when having a series of ID2D1Geometry's of Glyphs, to translate/rotate them properly to match the path in another ID2D1Geometry.

Edit: The best I 've got so far is this:

enter image description here

            float StartPoint = 0;
            // j.sPaths contains ID2D1Geometry for all letters, as rendered by GetGlyphRunOutline.
            for (size_t iip = 0; iip < j.sPaths.size(); iip++)
            {
                    if (g1) // The geometry in which to adjust the letters
                    {
                        nop();
                        D2D1::Matrix3x2F m1 = D2D1::Matrix3x2F::Identity();
                        D2F bnds = {};
                        j.sPaths[iip].pPath->GetBounds(m1, &bnds);
                        if (bnds.left > bnds.right)
                            continue;

                        // make sure this is at 0,0
                        float ExtraX = -bnds.left;
                        float ExtraY = -bnds.top;

                        auto wi2 = bnds.Width();
                        auto he2 = bnds.Height();
                        auto wi = wi2 / (float)vepXX.SizeCreated.cx; // vepXX.SizeCreated  = rendering area, Normalize everything to [0,1] because the geometry g1 is within [0,1].
                        auto he = he2 / (float)vepXX.SizeCreated.cy;


                        D2D1_POINT_2F StartPointOnThePath = {}, p0tv = {};
                        D2D1_POINT_2F EndpointOnThePath = {}, p1tv = {};
                        g1->ComputePointAtLength(StartPoint, m1, &StartPointOnThePath, &p0tv);
                        float Endpoint = StartPoint + wi;
                        g1->ComputePointAtLength(Endpoint, m1, &EndpointOnThePath,&p1tv);
                        float MidpointOnThePath = (EndpointOnThePath.x - StartPointOnThePath.x)/2.0f + StartPointOnThePath.x;

                        StartPoint = Endpoint;
                        if (oo)             StartPoint += oo->AddX; // Extra X option

                        float angle = 0;    
                        angle = (float)atan2(p1tv.y, p1tv.x);
                        float angled = angle * 180.0f / 3.1415f;
                        float xx = (MidpointOnThePath - wi/2)* vepXX.SizeCreated.cx; // convert to view
                        float yy = (EndpointOnThePath.y - he)* vepXX.SizeCreated.cy;    

                        D2D1_POINT_2F pr = {wi2/2.0f,he2/2.0f};
                        
                        auto xform = D2D1::IdentityMatrix();
                        // Translate the glyph to 0,0
                        xform = xform * D2D1::Matrix3x2F::Translation(ExtraX, ExtraY);

                        // Rotate to put
                        xform = xform * D2D1::Matrix3x2F::Rotation(angled,{wi2/2.0f,he2/2.0f});

                        // Translate to position
                        xform = xform * D2D1::Matrix3x2F::Translation(xx,yy);
                        
                        faa->CreateTransformedGeometry(j.sPaths[iip].pPath, &xform, &j.sPaths[iip].pPathX);
  // Draw the new geometry later.
                    }

Solution

  • Here is a fully working example, using ID2D1PathGeometry1::ComputePointAndSegmentAtLength that allows you to get points coordinates of a path from a length parameter. length represents the point on the path which is "at length distance" along this path from its start.

    The sample code uses a custom IDWriteTextRenderer implementation which is used to render each glyph along the path. This sample implementation mostly implements IDWriteTextRenderer::DrawGlyphRun for the whole string, which in turns re-uses ID2D1RenderTarget::DrawGlyphRun for each glyph after appropriate translation and rotation.

    #define _USE_MATH_DEFINES
    #include <windows.h>
    #include <d2d1_1.h>
    #include <d2d1helper.h>
    #include <dwrite.h>
    #include <winrt/base.h>
    
    using namespace winrt;
    
    com_ptr<ID2D1Factory1> _d2dFactory;
    com_ptr<ID2D1HwndRenderTarget> _renderTarget;
    com_ptr<ID2D1SolidColorBrush> _red;
    com_ptr<ID2D1SolidColorBrush> _black;
    com_ptr<ID2D1PathGeometry1> _geometry;
    com_ptr<IDWriteTextLayout> _layout;
    
    wchar_t _phrase[] = L"Quick brown fox jumps over the lazy dog";
    
    class Renderer : public IDWriteTextRenderer
    {
    public:
      STDMETHOD(DrawGlyphRun)(void* clientDrawingContext, // should be used to pass RenderTarget, Geometry, etc.
        FLOAT baselineOriginX, FLOAT baselineOriginY,
        DWRITE_MEASURING_MODE measuringMode,
        DWRITE_GLYPH_RUN const* glyphRun,
        DWRITE_GLYPH_RUN_DESCRIPTION const* glyphRunDescription, IUnknown* clientDrawingEffect
        )
      {
        auto advance = baselineOriginX;
        for (UINT32 i = 0; i < glyphRun->glyphCount; i++)
        {
          D2D1_POINT_DESCRIPTION desc;
          check_hresult(_geometry->ComputePointAndSegmentAtLength(advance, 0, nullptr, &desc));
          if (desc.endFigure) // path ends here, we could break
          {
            // continue with a small back out
            advance -= desc.lengthToEndSegment;
            check_hresult(_geometry->ComputePointAndSegmentAtLength(advance, 0, nullptr, &desc));
          }
    
          advance += glyphRun->glyphAdvances[i];
    
          // compute angle
          auto angle = M_PI_2 - atan2(desc.unitTangentVector.x, desc.unitTangentVector.y);
    
          // translate & rotate
          auto baseLineTx = D2D1::Matrix3x2F::Translation(0, glyphRun->fontEmSize - baselineOriginY);
          auto tx = D2D1::Matrix3x2F::Translation(desc.point.x, desc.point.y);
          auto rot = D2D1::Matrix3x2F::Rotation((FLOAT)(angle * 180 / M_PI));
          _renderTarget->SetTransform(baseLineTx * rot * tx);
    
          // draw 1 glyph
          DWRITE_GLYPH_RUN run = *glyphRun;
          run.glyphCount = 1;
          run.glyphAdvances = &glyphRun->glyphAdvances[i];
          run.glyphIndices = &glyphRun->glyphIndices[i];
          run.glyphOffsets = &glyphRun->glyphOffsets[i];
          _renderTarget->DrawGlyphRun(D2D1_POINT_2F(), &run, _black.get(), measuringMode);
        }
    
        _renderTarget->SetTransform(D2D1::IdentityMatrix()); // make sure we come back to no transform
        return S_OK;
      }
    
      STDMETHOD(IsPixelSnappingDisabled)(void* clientDrawingContext, BOOL* isDisabled)
      {
        *isDisabled = TRUE;
        return S_OK;
      }
    
      // nothing is implemented except IUnknown
      STDMETHOD(GetCurrentTransform)(void* clientDrawingContext, DWRITE_MATRIX* transform) { return E_NOTIMPL; }
      STDMETHOD(GetPixelsPerDip)(void* clientDrawingContext, FLOAT* pixelsPerDip) { return E_NOTIMPL; }
      STDMETHOD(DrawUnderline)(void* clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, DWRITE_UNDERLINE const* underline, IUnknown* clientDrawingEffect) { return E_NOTIMPL; }
      STDMETHOD(DrawStrikethrough)(void* clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, DWRITE_STRIKETHROUGH const* strikethrough, IUnknown* clientDrawingEffect) { return E_NOTIMPL; }
      STDMETHOD(DrawInlineObject)(void* clientDrawingContext, FLOAT originX, FLOAT originY, IDWriteInlineObject* inlineObject, BOOL isSideways, BOOL isRightToLeft, IUnknown* clientDrawingEffect) { return E_NOTIMPL; }
      STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject)
      {
        if (riid == __uuidof(IUnknown)) { *ppvObject = (IUnknown*)this; return S_OK; }
        if (riid == __uuidof(IDWritePixelSnapping)) { *ppvObject = (IDWritePixelSnapping*)this; return S_OK; }
        if (riid == __uuidof(IDWriteTextRenderer)) { *ppvObject = (IDWriteTextRenderer*)this; return S_OK; }
        *ppvObject = nullptr;
        return E_NOINTERFACE;
      }
    
      ULONG AddRef() { return 1; }
      ULONG Release() { return 1; }
    };
    
    Renderer _renderer{ };
    
    void Render()
    {
      _renderTarget->BeginDraw();
      _renderTarget->Clear(D2D1_COLOR_F(1, 1, 1));
    
      // draw geometry
      _renderTarget->DrawGeometry(_geometry.get(), _red.get(), 4);
    
      // draw phrase using our special render
      // with some start offset (250)
      // and some baseline offset (10)
      check_hresult(_layout->Draw(nullptr, &_renderer, 250, 10));
      check_hresult(_renderTarget->EndDraw());
    }
    
    void BuildGeometry(FLOAT width, FLOAT height)
    {
      auto margin = 60.0f;
      width -= 2 * margin;
      height -= 2 * margin;
      if (width <= 0 || height <= 0)
        return;
    
      _geometry = nullptr;
      check_hresult(_d2dFactory->CreatePathGeometry(_geometry.put()));
      com_ptr<ID2D1GeometrySink> sink;
      check_hresult(_geometry->Open(sink.put()));
      auto size = D2D_SIZE_F(width / 4, height / 2);
      sink->BeginFigure(D2D_POINT_2F(margin + width / 4, margin), D2D1_FIGURE_BEGIN_FILLED);
      sink->AddArc(D2D1_ARC_SEGMENT(D2D_POINT_2F(margin + width / 4, margin + height), size));
      sink->AddLine(D2D_POINT_2F(margin + width * 3 / 4, margin + height));
      sink->AddArc(D2D1_ARC_SEGMENT(D2D_POINT_2F(margin + width * 3 / 4, margin), size));
      sink->EndFigure(D2D1_FIGURE_END_CLOSED);
      check_hresult(sink->Close());
    }
    
    void Resize(UINT width, UINT height)
    {
      winrt::check_hresult(_renderTarget->Resize(D2D1_SIZE_U(width, height)));
      BuildGeometry((FLOAT)width, (FLOAT)height);
    }
    
    LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
      switch (uMsg)
      {
      case WM_PAINT:
      {
        PAINTSTRUCT ps;
        BeginPaint(hwnd, &ps);
        Render();
        EndPaint(hwnd, &ps);
        return 0;
      }
    
      case WM_SIZE:
        Resize(LOWORD(lParam), HIWORD(lParam));
        return 0;
    
      case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
      }
      return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
    
    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
    {
      WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
      wcex.style = CS_HREDRAW | CS_VREDRAW;
      wcex.lpfnWndProc = WndProc;
      wcex.hInstance = hInstance;
      wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
      wcex.lpszClassName = L"App";
      RegisterClassEx(&wcex);
    
      auto hwnd = CreateWindow(L"App", L"App", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 600, 400, nullptr, nullptr, hInstance, nullptr);
    
      // init some D2D & DWrite
      check_hresult(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, _d2dFactory.put()));
      check_hresult(_d2dFactory->CreateHwndRenderTarget(D2D1::RenderTargetProperties(), D2D1::HwndRenderTargetProperties(hwnd), _renderTarget.put()));
      check_hresult(_renderTarget->CreateSolidColorBrush(D2D1::ColorF(1, 0, 0), _red.put()));
      check_hresult(_renderTarget->CreateSolidColorBrush(D2D1::ColorF(0, 0, 0), _black.put()));
    
      com_ptr<IDWriteFactory> dwriteFactory;
      check_hresult(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), (IUnknown**)dwriteFactory.put()));
      com_ptr<IDWriteTextFormat> format;
      check_hresult(dwriteFactory->CreateTextFormat(L"Times New Roman", nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 50, L"", format.put()));
    
      // we want a straight line w/o limit
      check_hresult(dwriteFactory->CreateTextLayout(_phrase, lstrlen(_phrase), format.get(), FLT_MAX, 0, _layout.put()));
    
      ShowWindow(hwnd, SW_SHOWNORMAL);
      UpdateWindow(hwnd);
      MSG msg;
      while (GetMessage(&msg, nullptr, 0, 0) > 0)
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
      return 0;
    }
    

    And here is the result:

    enter image description here

    With 250px start offset and 10px baseline offset:

    enter image description here