How to paint on a Canvas with Transparency and Opacity?

user1175743 picture user1175743 · Apr 27, 2012 · Viewed 28.7k times · Source

Overview

From the GR32 library, I am using TImgView32 to render a grid which will be my transparent background like so:

enter image description here

Placed inside the TImgView32 I have a regular TImage, where I will be drawing onto the canvas, something like this:

enter image description here

Task

What I would like to achieve is the ability to set the opacity of the brush, allowing further possibilities for image editing in my program. Rather than have one flat color been drawn, setting the opacity of the brush allows different levels of color depth etc.

I found this question earlier whilst searching around: Draw opacity ellipse in Delphi 2010 - Andreas Rejbrand has provided a few examples in his answer for that question.

I have looked at what Andreas did, and came up with my own simplified attempt but I am stuck with a problem. Take a look at these next two images, the first is with the transparent background and the second with a black background to show the problem more clearer:

enter image description here enter image description here

As you can see, around the brush (circle) is a really annoying square I cannot get rid of. All that should be visible is the brush. This is my code used to produce those results:

procedure DrawOpacityBrush(ACanvasBitmap: TBitmap; X, Y: Integer;
  AColor: TColor; ASize: Integer; Opacity: Integer);
var
  Bmp: TBitmap;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.SetSize(ASize, ASize);
    Bmp.Transparent := False;

    with Bmp.Canvas do
    begin
      Pen.Color := AColor;
      Pen.Style := psSolid;
      Pen.Width := ASize;
      MoveTo(ASize div 2, ASize div 2);
      LineTo(ASize div 2, ASize div 2);
    end;

    ACanvasBitmap.Canvas.Draw(X, Y, Bmp, Opacity);
  finally
    Bmp.Free;
  end;
end;

procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  DrawOpacityBrush(Image1.Picture.Bitmap, X, Y, clRed, 50, 85);
end;

which produces this on a regular bitmap:

enter image description here

The idea I had (based on Andreas way of creating an ellipse with opacity) was to render a typical brush on the canvas, assign it to a offscreen bitmap then redraw it onto the main bitmap with the opacity. Which works, except that annoying square around the edge.

How can I render a brush with opacity as illustrated in the screenshots, but without that square around the brush circle?

If I set Bmp.Transparent := True there is still a white box, but no opacity anymore. Just a solid white square and solid filled red circle.

Answer

Remy Lebeau picture Remy Lebeau · Apr 27, 2012

The Opacity feature of TCanvas.Draw() does not support what you are trying to do, at least not the way you are trying to use it. To accomplish the affect you want, you need to create a 32-bit TBitmap so you have a per-pixel alpha channel, then fill in the alpha for each pixel, setting the alpha to 0 for pixels you don't want, and setting the alpha to the desired opacity for the pixels you do want. Then, when calling TCanvas.Draw(), set its opacity to 255 to tell it to use just the per-pixel opacities.

procedure DrawOpacityBrush(ACanvas: TCanvas; X, Y: Integer; AColor: TColor; ASize: Integer; Opacity: Byte);
var
  Bmp: TBitmap;
  I, J: Integer;
  Pixels: PRGBQuad;
  ColorRgb: Integer;
  ColorR, ColorG, ColorB: Byte;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.PixelFormat := pf32Bit; // needed for an alpha channel
    Bmp.SetSize(ASize, ASize);

    with Bmp.Canvas do
    begin
      Brush.Color := clFuchsia; // background color to mask out
      ColorRgb := ColorToRGB(Brush.Color);
      FillRect(Rect(0, 0, ASize, ASize));
      Pen.Color := AColor;
      Pen.Style := psSolid;
      Pen.Width := ASize;
      MoveTo(ASize div 2, ASize div 2);
      LineTo(ASize div 2, ASize div 2);
    end;

    ColorR := GetRValue(ColorRgb);
    ColorG := GetGValue(ColorRgb);
    ColorB := GetBValue(ColorRgb);

    for I := 0 to Bmp.Height-1 do
    begin
      Pixels := PRGBQuad(Bmp.ScanLine[I]);
      for J := 0 to Bmp.Width-1 do
      begin
        with Pixels^ do
        begin
          if (rgbRed = ColorR) and (rgbGreen = ColorG) and (rgbBlue = ColorB) then
            rgbReserved := 0
          else
            rgbReserved := Opacity; 
          // must pre-multiply the pixel with its alpha channel before drawing
          rgbRed := (rgbRed * rgbReserved) div $FF;
          rgbGreen := (rgbGreen * rgbReserved) div $FF;
          rgbBlue := (rgbBlue * rgbReserved) div $FF;
        end;
        Inc(Pixels);
      end;
    end;

    ACanvas.Draw(X, Y, Bmp, 255);
  finally
    Bmp.Free;
  end;
end;

procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton;  
  Shift: TShiftState; X, Y: Integer);  
begin  
  DrawOpacityBrush(Image1.Picture.Bitmap.Canvas, X, Y, clRed, 50, 85);  
end;