本文介绍怎样实现任意角度的文字多色渐变填充。
因为文字填充区是由若干个不规则的图形组成的,因此渐变填充文字比渐变填充矩形(见《实现任意角度渐变填充(一) -- 双色渐变填充矩形》和《实现任意角度渐变填充(二) -- 多色渐变填充矩形》)要复杂一些。需要先建立一个临时位图,以黑底白字形式将文字画在临时位图上,然后以临时位图数据为掩码图,对文字填充区域进行填充,如果掩码图某坐标的象素值为白色,那么对文字填充区的相应坐标的象素进行填充,否则不填充,下面是多色渐变文字填充的实现代码:
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;
用改进后的多色填充文字过程再次运行上面的例子代码,其运行界面截图如下:
这个多色文字填充界面比前面的效果好多了。
指导和建议请来信:[email protected],[email protected]
后记:本文的填充过程支持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表示感谢!