您的位置:首页 > 其它

实现任意角度渐变填充(三) -- 多色渐变填充文字

2009-11-18 13:56 676 查看
本文介绍怎样实现任意角度的文字多色渐变填充。

因为文字填充区是由若干个不规则的图形组成的,因此渐变填充文字比渐变填充矩形(见《实现任意角度渐变填充(一) -- 双色渐变填充矩形》和《实现任意角度渐变填充(二) -- 多色渐变填充矩形》)要复杂一些。需要先建立一个临时位图,以黑底白字形式将文字画在临时位图上,然后以临时位图数据为掩码图,对文字填充区域进行填充,如果掩码图某坐标的象素值为白色,那么对文字填充区的相应坐标的象素进行填充,否则不填充,下面是多色渐变文字填充的实现代码:

type
  PARGBQuad = ^TARGBQuad;
  TARGBQuad = packed record
    case Integer of
      0: (Color: LongWord);
      1: (Blue, Green, Red, Alpha: Byte);
      2: (Argb: array[0..3] of Byte);
  end;

  TColorBuffer = array of TARGBQuad;

  PFillData = ^TFillData;
  TFillData = packed record
    Width: Integer;           // 宽度
    Height: Integer;          // 高度
    Scan0: Pointer;           // 扫描线首地址
    PScan0: PARGBQuad;        // 扫描线指针(填充起始像素地址)
    ScanDelta: Integer;       // 扫描线指针增量
    ScanOffset: Integer;      // 扫描线指针偏移
    xDelta: Integer;          // 列增量
    yDelta: Integer;          // 行增量
    AllocScan0: Boolean;      // 是否分配扫描线
    AlphaBlend: Boolean;      // 填充颜色是否含Alpha信息
    InvertColors: Boolean;    // 是否颠倒填充颜色
  end;

function GetFillData(Width, Height, Stride: Integer; Scan0: Pointer;
  Angle: Single): TFillData;
var
  n: Integer;
begin
  n := Trunc(Angle / 90);
  Angle := (Angle - 90 * n) * PI / 180;
  Result.xDelta := Round(Cos(Angle) * 256);
  Result.yDelta := Round(Sin(Angle) * 256);
  if Stride <= 0 then
    Stride := Width shl 2;
  if Scan0 = nil then
  begin
    GetMem(Result.Scan0, Height * Stride);
    Result.AllocScan0 := True;
  end
  else
  begin
    Result.Scan0 := Scan0;
    Result.AllocScan0 := False;
  end;
  Result.PScan0 := Result.Scan0;
  if (n and 1) = 0 then
  begin
    Result.Width := Width;
    Result.Height := Height;
    Result.ScanDelta := 4;
    Result.ScanOffset := Stride;
  end
  else
  begin
    Result.Width := Height;
    Result.Height := Width;
    Result.ScanDelta := Stride;
    Result.ScanOffset := -4;
    Inc(Result.PScan0, Width - 1);
  end;
  Result.InvertColors := (n mod 4) > 1;
end;

procedure FreeFillData(var Data: TFillData);
begin
  if Data.AllocScan0 then
  begin
    FreeMem(Data.Scan0);
    Data.AllocScan0 := False;
  end;
end;

// 计算Colors在颜色缓冲区各元素的颜色值, 返回颜色缓冲区
function GetGradientColors(var Data: TFillData; const Colors: array of LongWord;
  const Positions: array of Single): TColorBuffer;

  procedure SetColors(Buffer: PARGBQuad; Size: Integer; Color1, Color2: TARGBQuad);
  var
    I: Integer;
    Delta: Integer;
    Cumulate: Integer;
    p: PARGBQuad;
  begin
    Delta := Round((1 shl 24) / Size);
    Cumulate := Delta * Size;
    p := Buffer;
    I := 0;
    while I < Cumulate do
    begin
      p.Alpha := ((I * (Color2.Alpha - Color1.Alpha)) shr 24) + Color1.Alpha;
      p.Red := ((I * (Color2.Red - Color1.Red)) shr 24) + Color1.Red;
      p.Green := ((I * (Color2.Green - Color1.Green)) shr 24) + Color1.Green;
      p.Blue := ((I * (Color2.Blue - Color1.Blue)) shr 24) + Color1.Blue;
      // 转换PARGB格式为ARGB格式
      if (p.Alpha > 0) and (p.Alpha < 255) then
      begin
        p.Red := p.Red * 255 div p.Alpha;
        p.Green := p.Green * 255 div p.Alpha;
        p.Blue := p.Blue * 255 div p.Alpha;
      end;
      Inc(I, Delta);
      Inc(p);
    end;
  end;

var
  I, Len, Size: Integer;
  Poss: array of Integer;
  p: PARGBQuad;
begin
  Size := (Data.Width * Data.xDelta + Data.Height * Data.yDelta) shr 8;
  SetLength(Result, Size);
  SetLength(Poss, Length(Positions));
  Data.AlphaBlend := False;
  p := PARGBQuad(@Colors[0]);
  for I := Low(Poss) to High(Poss) do
  begin
    Poss[I] := Round(Positions[I] * Size);
    // ARGB格式转换为PARGB格式
    if p.Alpha <> 255 then
    begin
      p.Red := p.Alpha * p.Red div 255;
      p.Green := p.Alpha * p.Green div 255;
      p.Blue := p.Alpha * p.Blue div 255;
      Data.AlphaBlend := True;
    end;
    Inc(p);
  end;
  p := @Result[0];
  Len := High(Colors) - 1;
  if Data.InvertColors then
  begin
    for I := Len downto 0 do
    begin
      Size := Poss[I + 1] - Poss[I];
      SetColors(p, Size, TARGBQuad(Colors[I + 1]), TARGBQuad(Colors[I]));
      Inc(p, Size);
    end;
  end
  else
  begin
    for I := 0 to Len do
    begin
      Size := Poss[I + 1] - Poss[I];
      SetColors(p, Size, TARGBQuad(Colors[I]), TARGBQuad(Colors[I + 1]));
      Inc(p, Size);
    end;
  end;
end;

function GetTextMaskData(Str: string; Font: TFont; Angle: Single;
  var Width, Height: Integer): TFillData;
var
  saveBitmap, Bitmap: HBITMAP;
  DC, memDC: HDC;
  Canvas: TCanvas;
  bmi: TBitmapInfo;
begin
  Canvas := TCanvas.Create;
  DC := GetDC(0);
  try
    Canvas.Handle := DC;
    Canvas.Font.Assign(Font);
    Canvas.Font.Color := clWhite;    // 文字为白色
    Canvas.Brush.Color := clBlack;   // 背景为黑色
    Width := Canvas.TextWidth(Str);  // 获取文字串宽度
    Height := Canvas.TextHeight(Str);// 获取文字串高度度
    Bitmap := CreateCompatibleBitmap(DC, Width, Height);
    memDC := CreateCompatibleDC(DC);
    try
      saveBitmap := SelectObject(memDC, Bitmap);
      Canvas.Handle := memDC;
      Canvas.TextOut(0, 0, Str);     // 在内存位图上写文字串
      SelectObject(memDC, saveBitmap);
      Result := GetFillData(Width, Height, 0, nil, Angle);
      bmi.bmiHeader := GetBitmapInfoHeader(Width, Height);
      // 将内存位图拷贝到填充掩码数据中
      GetDIBits(DC, Bitmap, 0, Height, Result.Scan0, bmi, DIB_RGB_COLORS);
    finally
      DeleteDC(memDC);
      DeleteObject(Bitmap);
    end;
  finally
    ReleaseDC(0, DC);
    Canvas.Free;
  end;
end;

procedure LinearMaskFill(const Data, MaskData: TFillData; const ColorBuf: TColorBuffer);
var
  x, y, x0, y0: Integer;
  p, p0, m, m0: PARGBQuad;
  c: TARGBQuad;
begin
  p0 := Data.PScan0;
  m0 := MaskData.PScan0;
  y0 := 0;
  if Data.AlphaBlend then
  begin
    for y := 1 to Data.Height do
    begin
      x0 := y0;
      p := p0;
      m := m0;
      for x := 1 to Data.Width do
      begin
        if TColor(m.Color) = clWhite then
        begin
          c := ColorBuf[x0 shr 8];
          p.Alpha := ((c.Alpha * (c.Alpha - p.Alpha)) shr 8) + p.Alpha;
          p.Red := ((c.Alpha * (c.Red - p.Red)) shr 8) + p.Red;
          p.Green := ((c.Alpha * (c.Green - p.Green)) shr 8) + p.Green;
          p.Blue := ((c.Alpha * (c.Blue - p.Blue)) shr 8) + p.Blue;
        end;
        Inc(Integer(m), MaskData.ScanDelta);
        Inc(Integer(p), Data.ScanDelta);
        Inc(x0, Data.xDelta);
      end;
      Inc(Integer(p0), Data.ScanOffset);
      Inc(Integer(m0), MaskData.ScanOffset);
      Inc(y0, Data.yDelta);
    end;
  end
  else
  begin
    for y := 1 to Data.Height do
    begin
      x0 := y0;
      p := p0;
      m := m0;
      for x := 1 to Data.Width do
      begin
        if TColor(m.Color) = clWhite then
          p.Color := ColorBuf[x0 shr 8].Color;
        Inc(Integer(m), MaskData.ScanDelta);
        Inc(Integer(p), Data.ScanDelta);
        Inc(x0, Data.xDelta);
      end;
      Inc(Integer(p0), Data.ScanOffset);
      Inc(Integer(m0), MaskData.ScanOffset);
      Inc(y0, Data.yDelta);
    end;
  end;
end;

function GetBitmapInfoHeader(Width, Height: Integer): TBitmapInfoHeader;
begin
  Result.biSize := Sizeof(TBitmapInfoHeader);
  Result.biWidth := Width;
  Result.biHeight := Height;
  Result.biPlanes := 1;
  Result.biBitCount := 32;
  Result.biCompression := BI_RGB;
end;

function GetFillDataFromCanvas(Canvas: TCanvas; Rect: TRect; Angle: Single;
  var bmi: TBitmapInfo): TFillData;
var
  saveBitmap, Bitmap: HBITMAP;
  memDC: HDC;
  Width, Height: Integer;
begin
  Width := Rect.Right - Rect.Left;
  Height := Rect.Bottom - Rect.Top;
  Result := GetFillData(Width, Height, 0, nil, 360 - Angle);
  Bitmap := CreateCompatibleBitmap(Canvas.Handle, Width, Height);
  memDC := CreateCompatibleDC(Canvas.Handle);
  try
    // 获取画布的32位位图数据到Scan0
    saveBitmap := SelectObject(memDC, Bitmap);
    BitBlt(memDC, 0, 0, Width, Height, Canvas.Handle, Rect.Left, Rect.Top, SRCCOPY);
    SelectObject(memDC, saveBitmap);
    bmi.bmiHeader := GetBitmapInfoHeader(Width, Height);
    GetDIBits(Canvas.Handle, Bitmap, 0, Height, Result.Scan0, bmi, DIB_RGB_COLORS);
  finally
    DeleteDC(memDC);
    DeleteObject(Bitmap);
  end;
end;

procedure CopyFillDataToCanvas(Canvas: TCanvas; Rect: TRect; Scan0: Pointer;
  bmi: TBitmapInfo);
var
  saveBitmap, Bitmap: HBITMAP;
  memDC: HDC;
begin
  Bitmap := CreateDIBItmap(Canvas.Handle, bmi.bmiHeader, CBM_INIT, Scan0, bmi, DIB_RGB_COLORS);
  memDC := CreateCompatibleDC(Canvas.Handle);
  saveBitmap := SelectObject(memDC, Bitmap);
  try
    BitBlt(Canvas.Handle, Rect.Left, Rect.Top,
      bmi.bmiHeader.biWidth, bmi.bmiHeader.biHeight, memDC, 0, 0, SRCCOPY);
  finally
    SelectObject(memDC, saveBitmap);
    DeleteDC(memDC);
    DeleteObject(Bitmap);
  end;
end;

// 多色渐变填充文字。参数:画布,文字,字体,显示坐标,
// 填充颜色数组,渐变位置数组,角度
// 注:Positions各元素值是介于0-1之间的、递增的数值,头尾元素值必须为0和1
procedure LinearGradientFillText(Canvas: TCanvas; Str: string; Font: TFont;
  x, y: Integer; const Colors: array of LongWord;
  const Positions: array of Single; Angle: Single); overload;
var
  bmi: TBitmapInfo;
  Data, MaskData: TFillData;
  ColorBuf: TColorBuffer;
  Width, Height: Integer;
  r: TRect;
begin
  // 如果画布无效,退出
  if IsRectEmpty(Canvas.ClipRect) or (Str = '') then Exit;
  // 获取文字填充掩码数据
  MaskData := GetTextMaskData(Str, Font, 360 - Angle, Width, Height);
  r := Rect(x, y, Width + x, Height + y);
  Data := GetFillDataFromCanvas(Canvas, r, Angle, bmi);
  try
    // 获取渐变颜色数据
    ColorBuf := GetGradientColors(Data, Colors, Positions);
    // 渐变填充位图数据
    LinearMaskFill(Data, MaskData, ColorBuf);
    CopyFillDataToCanvas(Canvas, r, Data.Scan0, bmi);
  finally
    FreeFillData(Data);
    FreeFillData(MaskData);
  end;
end;

// 多色渐变填充文字。参数:窗口句柄,文字,字体,显示坐标,
// 填充颜色数组,渐变位置数组,角度
// 注:Positions各元素值是介于0-1之间的、递增的数值,头尾元素值必须为0和1
procedure LinearGradientFillText(Handle: THandle; Str: string; Font: TFont;
  x, y: Integer; const Colors: array of LongWord;
  const Positions: array of Single; Angle: Single); overload;
var
  Canvas: TCanvas;
  DC: HDC;
begin
  Canvas := TCanvas.Create;
  DC := GetDC(Handle);
  try
    Canvas.Handle := DC;
    LinearGradientFillText(Canvas, Str, Font, x, y, Colors, Positions, Angle);
  finally
    Canvas.Free;
    ReleaseDC(Handle, DC);
  end;
end;


上面的代码中,除了建立文字掩码图的过程和文字填充过程外,大部分过程和《实现任意角度渐变填充(二) -- 多色渐变填充矩形》是相同的,文字填充过程也是在矩形填充过程基础上修改的。

下面是个在窗口显示文字的例子:

var
  Font: TFont;
  s: string;
begin
  Font := TFont.Create;
  Font.Name := '华文行楷';
  Font.Size := 80;
  Font.Style := [fsBold];
  s := '世界你好!';
  // 填充窗口背景
  LinearGradientFillRect(Handle, ClientRect, $FF0000FF, $FFF0F8FF, 90);
  // 90度三色填充文字
  LinearGradientFillText(Canvas, s, Font, 50, 50,
    [$FFFFFF00, $FFFF0000, $FFFFFF00], [0, 0.7, 1], 90);
  Font.Name := '华文彩云';
  // 30度五色填充文字
  LinearGradientFillText(Canvas, s, Font, 50, 180,
    [$FF008000, $FFFFFF00, $FFFF0000, $FF0000FF, $FFFFA500], [0, 0.25, 0.5, 0.75, 1], 30);
  Font.Free;
end;


例子中的LinearGradientFillRect见《实现任意角度渐变填充(一) -- 双色渐变填充矩形》或者《实现任意角度渐变填充(二) -- 多色渐变填充矩形》。

例子运行界面截图:



从界面截图看,多色文字填充过程是成功的。但是遗憾的是文字边缘有很多锯齿,为此,我对掩码图的***过程作了改进,对文字掩码图进行了反走样处理,同时,文字填充过程也要做相应的改变,下面是改进后的过程代码:

procedure AntiAliasMask(Dest, Source: PARGBQuad; Width, Height: Integer);
var
  x, y, i, j: Integer;
  pd, ps, p: PARGBQuad;
  Offset: Integer;
  Alpha: LongWord;
begin
  pd := Dest;
  ps := Source;
  Offset := Width + 2 - 3;
  for y := 1 to Height do
  begin
    for x := 1 to Width do
    begin
      p := ps;
      Alpha := 0;
      for i := 1 to 3 do
      begin
        for j := 1 to 3 do
        begin
          Inc(Alpha, p.Blue);
          Inc(p);
        end;
        Inc(p, Offset);
      end;
      pd.Color := (Alpha + 9) div 9;
      Inc(pd);
      Inc(ps);
    end;
    Inc(ps, 2);
  end;
end;

function GetTextMaskData(Str: string; Font: TFont; Angle: Single;
  var Width, Height: Integer): TFillData;
var
  saveBitmap, Bitmap: HBITMAP;
  DC, memDC: HDC;
  Canvas: TCanvas;
  bmi: TBitmapInfo;
  Scan0: PARGBQuad;
  W, H: Integer;
begin
  Canvas := TCanvas.Create;
  DC := GetDC(0);
  try
    Canvas.Handle := DC;
    Canvas.Font.Assign(Font);
    Canvas.Font.Color := clWhite;    // 文字为白色
    Canvas.Brush.Color := clBlack;   // 背景为黑色
    Width := Canvas.TextWidth(Str);  // 获取文字串宽度
    Height := Canvas.TextHeight(Str);// 获取文字串高度度
    W := Width + 2;
    H := Height + 2;
    Bitmap := CreateCompatibleBitmap(DC, W, H);
    memDC := CreateCompatibleDC(DC);
    GetMem(Scan0, H * (W shl 2));
    try
      saveBitmap := SelectObject(memDC, Bitmap);
      Canvas.Handle := memDC;
      Canvas.TextOut(1, 1, Str);     // 在内存位图上写文字串
      SelectObject(memDC, saveBitmap);
      bmi.bmiHeader := GetBitmapInfoHeader(W, H);
      // 将内存位图拷贝到填充掩码数据中
      GetDIBits(DC, Bitmap, 0, H, Scan0, bmi, DIB_RGB_COLORS);
      Result := GetFillData(Width, Height, 0, nil, Angle);
      AntiAliasMask(Result.Scan0, Scan0, Width, Height);
    finally
      FreeMem(Scan0);
      DeleteDC(memDC);
      DeleteObject(Bitmap);
    end;
  finally
    ReleaseDC(0, DC);
    Canvas.Free;
  end;
end;

procedure LinearMaskFill(const Data, MaskData: TFillData; const ColorBuf: TColorBuffer);
var
  x, y, x0, y0: Integer;
  p, p0, m, m0: PARGBQuad;
  c: TARGBQuad;
  Alpha: Integer;
begin
  p0 := Data.PScan0;
  m0 := MaskData.PScan0;
  y0 := 0;
  for y := 1 to Data.Height do
  begin
    x0 := y0;
    p := p0;
    m := m0;
    for x := 1 to Data.Width do
    begin
      c := ColorBuf[x0 shr 8];
      Alpha := m.Color * c.Alpha;
      p.Alpha := ((Alpha * (c.Alpha - p.Alpha)) shr 16) + p.Alpha;
      p.Red := ((Alpha * (c.Red - p.Red)) shr 16) + p.Red;
      p.Green := ((Alpha * (c.Green - p.Green)) shr 16) + p.Green;
      p.Blue := ((Alpha * (c.Blue - p.Blue)) shr 16) + p.Blue;
      Inc(Integer(m), MaskData.ScanDelta);
      Inc(Integer(p), Data.ScanDelta);
      Inc(x0, Data.xDelta);
    end;
    Inc(Integer(p0), Data.ScanOffset);
    Inc(Integer(m0), MaskData.ScanOffset);
    Inc(y0, Data.yDelta);
  end;
end;


为了加快反走样过程和填充过程速度,下面是用BASM代码优化后的AntiAliasMask和LinearMaskFill过程:

procedure AntiAliasMask(Dest, Source: PARGBQuad; Width, Height: Integer);
asm
    push    esi
    push    edi
    push    ebx
    mov     esi, edx
    mov     edi, eax
    mov     ebx, ecx
    add     ebx, 2
    shl     ebx, 2
    cld
  @yLoop:
    push    ecx
  @xLoop:
    push    esi
    push    ecx
    xor     eax, eax
    mov     ecx, 3
  @AddLoop:
    movzx   edx, [esi]
    add     eax, edx
    movzx   edx, [esi + 4]
    add     eax, edx
    movzx   edx, [esi + 8]
    add     eax, edx
    add     esi, ebx
    loop    @AddLoop
    add     eax, 9
    mov     esi, 9
    cdq
    div     esi
    stosd
    pop     ecx
    pop     esi
    add     esi, 4
    loop    @xLoop
    pop     ecx
    add     esi, 8
    dec     Height
    jnz     @yLoop
    pop     ebx
    pop     edi
    pop     esi
end;

procedure LinearMaskFill(const Data, MaskData: TFillData; const ColorBuf: TColorBuffer);
var
  xDelta, yDelta, ScanDelta, ScanOffset: Integer;
  Width, Height: Integer;
asm
    push      esi
    push      edi
    push      ebx
    mov       edi, [eax].TFillData.PScan0
    mov       ebx, [edx].TFillData.PScan0
    mov       esi, ecx
    mov       ecx, [eax].TFillData.Width
    mov       Width, ecx
    mov       edx, [eax].TFillData.Height
    mov       Height, edx
    mov       edx, [eax].TFillData.xDelta
    mov       xDelta, edx
    mov       edx, [eax].TFillData.yDelta
    mov       yDelta, edx
    mov       edx, [eax].TFillData.ScanDelta
    mov       ScanDelta, edx
    mov       edx, [eax].TFillData.ScanOffset
    mov       ScanOffset, edx
    xor       ecx, ecx
    pxor      mm7, mm7
  @yLoop:
    push      Width
    push      edi
    push      ebx
    mov       edx, ecx
  @xLoop:
    mov       eax, edx
    shr       eax, 8
    movd      mm0, [esi + eax * 4]
    movzx     eax, [esi + eax * 4 + 3]
    imul      eax, [ebx]
    shr       eax, 8
    movd      mm2, eax
    movd      mm1, [edi]      // mm1 = 00 00 00 00 Ad Rd Gd Bd
    punpcklbw mm0, mm7        // mm0 = 00 As 00 Rs 00 Gs 00 Bs
    punpcklbw mm1, mm7        // mm1 = 00 Ad 00 Rd 00 Gd 00 Bd
    punpcklwd mm2, mm2
    punpcklwd mm2, mm2        // mm2 = Alpha Alpha Alpha Alpha
    psubw     mm0, mm1        // mm0 = As-Ad Rs-Rd Gs-Gd Bs-Bd
    pmullw    mm0, mm2        // mm0 = As*Alpha Rs*Alpha Gs*Alpha Bs*Alpha
    psllw     mm1, 8          // mm1 = Ad*256 Rd*256 Gd*256 Bd*256
    paddw     mm0, mm1        // mm0 = 00 An 00 Rn 00 Gn 00 Bn
    psrlw     mm0, 8          // mm0 = An/256 Rn/256 Gn/256 Bn/256
    packuswb  mm0, mm7        // mm0 = 00 00 00 00 An Rn Gn Bn
    movd      [edi], mm0
    add       edi, ScanDelta
    add       ebx, ScanDelta
    add       edx, xDelta
    dec       Width
    jnz       @xLoop
    pop       ebx
    pop       edi
    pop       Width
    add       edi, ScanOffset
    add       ebx, ScanOffset
    add       ecx, yDelta
    dec       Height
    jnz       @yLoop
    emms
    pop       ebx
    pop       edi
    pop       esi
end;


用改进后的多色填充文字过程再次运行上面的例子代码,其运行界面截图如下:



这个多色文字填充界面比前面的效果好多了。

指导和建议请来信:mao.maozefa@gmail.commaozefa@hotmail.com

后记:本文的填充过程支持ARGB颜色填充,GetGradientColors过程就是采用常规ARGB合成方法计算的颜色缓冲区各元素的颜色值,后经CSDN网友winnuke指出:如果填充颜色的Alpha小于255时,按常规ARGB合成方法计算的颜色会导致人眼视觉偏差,必须先将颜色的RGB按Alpha进行预乘,即转换为PARGB格式后进行合成,合成完毕后再转换回ARGB格式(见《http://topic.csdn.net/u/20091118/20/4f96e8c5-dcea-41ae-ac07-492526462b9d.html?64533》)。为此,本文对GetGradientColors进行了修改,但《实现任意角度渐变填充(一) -- 双色渐变填充矩形》一文中的SetGradientColors没作修改,因为那篇文章的内容只是作为本文的导入篇,就让它保持原貌吧。在这里再次对网友winnuke表示感谢!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: