February 2012
    Get Version

January 2007
» Bezier Text

December 2005
    Rotated Ellipses

December 2004
    PDF Page Count

January 2003
    Boolean Blues

March 2002
    Networked Drives

January 2002
    Treeview Troubles
    Appending to Exe's



Freeware Components
Drawing Text along a Path January 2007

Firstly, this can be implemented in one of two ways:
  • spreading characters evenly along the path, or
  • keeping the font's character spacing and centering the text on the path.

However, before we start, there are a couple of tips to note:

When drawing characters along a path, although the algorithm is a little more complicated, it looks best when the characters are drawn using the angle of the curve at each character's midpoint (though the differences are only subtle in the following example):
   
Using angle at left edge of characters     Using angle at middle of characters


Also, to avoid excessive character bunching inside curves and excessive character spreading outside curves it's best to have the text positioned directly on top of the path (not above or below it).


Anyhow, while there's nothing complicated about this process, there are a number of steps so it requires a little patience.


Steps to spread characters evenly spaced along a path:

  1. Define the path (eg a bezier curve)
  2. 'Flatten' the path to create an array of points pathPts[] which represent the path
  3. Calculate the length of each path segment (distance between points) and save the cumulative totals of these lengths in another array - distances[] (ie an array of distances from the start of the path to each point in path).
  4. Calculate the width of each character in the Text for the given font and canvas (device context).
  5. Calculate the necessary space (width) between each character
    interChrSpc = (lengthOfPath-totalAllCharWidths)/NumberOfSpaces
  6. Now, for each character:
    1. Calculate distance d from the start of the text to it's midpoint (based on charWidths and interCharSpace)
    2. Using distances[] - find the corresponding point in pathPts[] - pathPts[i] - that's just greater than distance d.
    3. Calculate the angle a of the segment pathPts[i-1] & pathPts[i].
    4. Again using distances[], find exact point p between pathPts[i-1] & pathPts[i] that corresponds with distance d in step 6i.
    5. Using angle a, find the point at which character drawing starts (ie top left corner of char) from point p.
    6. Finally draw the character at angle a.


Code snippet ...
unit beziertext;

interface

uses
  Windows, Graphics, Math;

procedure TextAlongBezier(canvas: TCanvas;
  bezierPts: array of TPoint; const s: string);

implementation

//--------------------------------------------------------------------------
//Helper functions
//--------------------------------------------------------------------------

function DistanceBetween2Pts(pt1,pt2: TPoint): single;
begin
  result := sqrt((pt1.X - pt2.X)*(pt1.X - pt2.X) +
    (pt1.Y - pt2.Y)*(pt1.Y - pt2.Y));
end;
//--------------------------------------------------------------------------

function GetPtAtDistAndAngleFromPt(pt: TPoint;
  dist: integer; angle: single): TPoint;
begin
  result.X := round(dist * cos(angle));
  result.Y := -round(dist * sin(angle)); //nb: Y axis is +ve down
  inc(result.X , pt.X);
  inc(result.Y , pt.Y);
end;
//--------------------------------------------------------------------------

function PtBetween2Pts(pt1, pt2: TPoint;
  relativeDistFromPt1: single): TPoint;
begin
  //nb: 0 <= relativeDistFromPt1 <= 1
  if pt2.X = pt1.X then
    result.X := pt2.X else
    result.X := pt1.X + round((pt2.X - pt1.X)*relativeDistFromPt1);
  if pt2.Y = pt1.Y then
    result.Y := pt2.Y else
    result.Y := pt1.Y + round((pt2.Y - pt1.Y)*relativeDistFromPt1);
end;
//--------------------------------------------------------------------------

function GetAnglePt2FromPt1(pt1, pt2: TPoint): single;
begin
  //nb: result is in radians
  dec(pt2.X,pt1.X);
  dec(pt2.Y,pt1.Y);
  with pt2 do
    if X = 0 then
    begin
      result := pi/2;
      if Y > 0 then result := 3*result; //nb: Y axis is +ve down
    end else
    begin
      result := arctan2(-Y,X);
      if result < 0 then result := result + pi * 2;
    end;
end;
//--------------------------------------------------------------------------

procedure AngledCharOut(Canvas: TCanvas; pt: TPoint;
  c: char; radians: single; offsetX, offsetY: integer);
var
  lf: TLogFont;
  OldFontHdl,NewFontHdl: HFont;
  angle: integer;
begin
  angle := round(radians * 180/pi);
  if angle > 180 then angle := angle - 360;

  //workaround because textout() without any rotation is malaligned
  //relative to other rotated text ...
  if angle = 0 then angle := 1;

  with Canvas do
  begin
    //create an angled font based on the current canvas's font ...
    if GetObject(Font.Handle, SizeOf(lf), @lf) = 0 then exit;
    lf.lfEscapement := Angle * 10;
    lf.lfOrientation := Angle * 10;
    lf.lfOutPrecision := OUT_TT_ONLY_PRECIS;
    NewFontHdl := CreateFontIndirect(lf);
    OldFontHdl := selectObject(handle,NewFontHdl);
    //offset the character by the (rotated) X & Y amounts ...
    if offsetX < 0 then
      pt := GetPtAtDistAndAngleFromPt(pt, -offsetX, radians + Pi)
    else if offsetX > 0 then
      pt := GetPtAtDistAndAngleFromPt(pt, offsetX, radians);
    if offsetY < 0 then
      pt := GetPtAtDistAndAngleFromPt(pt, -offsetY, radians + pi/2)
    else if offsetY > 0 then
      pt := GetPtAtDistAndAngleFromPt(pt, offsetY, radians - pi/2);
    //draw the rotated character ...
    TextOut(pt.x, pt.y, c);
    //finally restore the unrotated canvas font ...
    selectObject(handle,OldFontHdl);
    DeleteObject(NewFontHdl);
  end;
end;

//--------------------------------------------------------------------------
// TextAlongBezier()
//--------------------------------------------------------------------------

procedure TextAlongBezier(canvas: TCanvas;
  bezierPts: array of TPoint; const s: string);
var
  i, j, ptCnt, textLenPxls, textLenChars, vertOffset: integer;
  currentInsertionDist, charWidthDiv2: integer;
  pt: TPoint;
  flatPts: array of TPoint;
  types: array of byte;
  distances: array of single;
  dummyPtr: pointer;
  angle, spcPxls, bezierLen, relativeDistFRomPt1: single;
  charWidths: array[#32..#255] of integer;
begin
  textLenChars := length(s);
  //make sure there's text and a valid number of bezier points ...
  if (textLenChars = 0) or (high(bezierPts) mod 3 <> 0) then exit;

  with canvas do
  begin
    //Create the path ...
    BeginPath(handle);
    PolyBezier(bezierPts);
    EndPath(handle);
    //'Flatten' the path ...
    FlattenPath(handle);

    //Get Character widths for every printable character of the given font
    if not GetCharWidth32(handle,32,255, charWidths[#32]) then exit;

    //First get the number of points needed to define the 'flattened' path
    dummyPtr := nil; //nb: dummyPtr will be ignored in the GetPath() call
    ptCnt := GetPath(handle, dummyPtr, dummyPtr, 0);
    if ptCnt < 1 then exit;

    setLength(flatPts, ptCnt);
    setLength(types, ptCnt);
    setLength(distances, ptCnt);

    //Now we know the number of points needed, call GetPath() again
    //this time assigning the array of points (flatPts) ...
    GetPath(handle, flatPts[0], types[0], ptCnt);

    //calculate and fill the distances array ...
    distances[0] := 0;
    bezierLen := 0;
    for i := 1 to ptCnt -1 do
    begin
      bezierLen := bezierLen +
        DistanceBetween2Pts(flatPts[i], flatPts[i-1]);
      distances[i] := bezierLen;
    end;

    //calc length of text in pixels ...
    textLenPxls := 0;
    for i := 1 to textLenChars do inc(textLenPxls, charWidths[s[i]]);

    //calc space between chars to spread string along entire curve ...
    if textLenChars = 1 then
      spcPxls := 0 else
      spcPxls := (bezierLen - textLenPxls)/(textLenChars -1);

    SetBkMode (handle, TRANSPARENT);

    //Position the text over the top of the curve.
    //Empirically, moving characters up 2/3 of TextHeight seems OK ...
    vertOffset := -trunc(2/3* TextHeight('Yy'));

    j := 1;
    currentInsertionDist := 0;
    for i := 1 to textLenChars do
    begin
      charWidthDiv2 := charWidths[s[i]] div 2;
      //increment currentInsertionDist half the width of char to get
      //the slope of the curve at the midpoint of that character ...
      inc(currentInsertionDist, charWidthDiv2);

      //find the point on the flattened path corresponding to the
      //midpoint of the current character ...
      while (j < ptCnt -1) and (distances[j] < currentInsertionDist) do
        inc(j);
      if distances[j] = currentInsertionDist then
        pt := flatPts[j]
      else
      begin
        relativeDistFRomPt1 := (currentInsertionDist - distances[j-1]) /
          (distances[j] - distances[j-1]);
        pt := PtBetween2Pts(flatPts[j-1],flatPts[j],relativeDistFRomPt1);
      end;
      //get the angle of the path at this point ...
      angle := GetAnglePt2FromPt1(flatPts[j-1], flatPts[j]);

      //finally, draw the character at the given angle  ...
      AngledCharOut(canvas,pt,s[i], angle, -charWidthDiv2, vertOffset);

      //increment currentInsertionDist to the start of next character ...
      inc(currentInsertionDist,
        charWidthDiv2 + trunc(spcPxls) + round(frac(spcPxls*i)));
    end;

    //debug only - draw the path from the points ...
    //with flatPts[0] do canvas.moveto(X,Y);
    //for i := 1 to ptCnt -1 do with flatPts[i] do canvas.lineto(X,Y);
  end;
end;
//--------------------------------------------------------------------------

end.


And an example of calling TextAlongBezier() ...
procedure TForm1.FormPaint(Sender: TObject);
begin
  //Font.Name := "Tahoma"; Font.Size := 48; Font.Style := [fsBold];
  TextAlongBezier(
      canvas,
      [Point(300,100), Point(500,100), Point(500,400),
      Point(300,400), Point(100,400), Point(100,100), Point(300,100)],
      ' Try this quick quiz ');
end;
//--------------------------------------------------------------------------





Copyright © 2002-2007 Angus Johnson