Tips

 
  home
     

Projects

Products

Coding Corner
  Components
  Tips

Contact Us

Design Background

Curriculum Vitae

Forum

Links

 

 

Tip #2: How to draw anti-aliased circles and disks
Added: 30Aug2003
Author: Nils Haeck
Category: Graphics

Question:
How can I draw anti-aliased circles and disks on a bitmap without using the Windows GDI?

Applicable:
Use this code to render circles with floating point precision:

  • The circle can be placed with sub-pixel accuracy
  • The circle can have configurable anti-aliased edges (the width, aka "feather" can be chosen)

Theory:
We will use the basic equation for a circle. Using two coordinates, X and Y, and the center of the circle located at Xc and Yc, and a radius R:

sqr(X-Xc) + sqr(Y-Yc) = sqr(R)

Notice that the value sqr(R) is always the same, so we can pre-calculate it. On each row of pixels (called a scanline), the value of sqr(Y-Yc) is also constant, because the Y value is constant. And just so, for each column of pixels the value of sqr(X - Xc) is constant.

Using this, we can precalculate these values, and then the only thing left to do is to compare the correct values in each pixel with simple addition. This results in very fast code.

How to do anti-aliasing
Anti-aliasing means that we must find "inbetween" values whenever we are "on" the edge of the circle. Suppose we want to draw a disk, with radius R:

if sqr(X-Xc) + sqr(Y-Yc) < sqr(R) we are on the disk, if not, we are outside of the disk.

An easy way of implementing anti-aliasing here is to assume that there are two circles. We have an inner circle R1 and an outer circle R2:

if sqr(X-Xc) + sqr(Y-Yc) < sqr(R1) we are on the disk,

if sqr(X-Xc) + sqr(Y-Yc) > sqr(R2) we are outside,

and in the other cases we have fallen inbetween both circles. This is the area where the color must change from background color to disk color.

First we find our local radius: R = sqrt(sqr(X-Xc) + sqr(Y-Yc)). This requires a square root calculation, but fortunately only in the relatively small area of the anti-aliasing zone.

Next, we find the factor ranging from 0 (outside) to 1 (inside) and use this one to scale our color:

Factor = (R2 - R) / (R2 - R1), which is 1 if R=R1 and 0 if R=R2

How does this look?
Lets have a look at a zoomed-in version of above screenshot and you can see how the zone inbetween gives pixels with intermediate colors (zoomed in 8x).

Download
You can download complete source including demo project here. It also includes a demo executable for the Windows platform.

Source code
The actual Pascal source procedure for the disk drawing is printed below. Feather is the width of the anti-aliasing area.

procedure DrawDisk(Bitmap: TBitmap; CenterX, CenterY, Radius,
  Feather: single);
// Draw a disk on Bitmap. Bitmap must be a 256 color (pf8bit)
// palette bitmap, and parts outside the disk will get palette
// index 0, parts inside will get palette index 255, and in the
// antialiased area (feather), the pixels will get values
// inbetween.
// ***Parameters***
// Bitmap:
//   The bitmap to draw on
// CenterX, CenterY:
//   The center of the disk (float precision). Note that [0, 0]
//   would be the center of the first pixel. To draw in the
//   exact middle of a 100x100 bitmap, use CenterX = 49.5 and
//   CenterY = 49.5
// Radius:
//   The radius of the drawn disk in pixels (float precision)
// Feather:
//   The feather area. Use 1 pixel for a 1-pixel antialiased
//   area. Pixel centers outside 'Radius + Feather / 2' become
//   0, pixel centers inside 'Radius - Feather/2' become 255.
//   Using a value of 0 will yield a bilevel image.
// Copyright (c) 2003 Nils Haeck M.Sc. www.simdesign.nl
var
  x, y: integer;
  LX, RX, LY, RY: integer;
  Fact: integer;
  RPF2, RMF2: single;
  P: PByteArray;
  SqY, SqDist: single;
  sqX: array of single;
begin
  // Determine some helpful values (singles)
  RPF2 := sqr(Radius + Feather/2);
  RMF2 := sqr(Radius - Feather/2);

  // Determine bounds:
  LX := Max(floor(CenterX - RPF2), 0);
  RX := Min(ceil (CenterX + RPF2), Bitmap.Width - 1);
  LY := Max(floor(CenterY - RPF2), 0);
  RY := Min(ceil (CenterY + RPF2), Bitmap.Height - 1);

  // Optimization run: find squares of X first
  SetLength(SqX, RX - LX + 1);
  for x := LX to RX do
    SqX[x - LX] := sqr(x - CenterX);

  // Loop through Y values
  for y := LY to RY do begin
    P := Bitmap.Scanline[y];
    SqY := Sqr(y - CenterY);
    // Loop through X values
    for x := LX to RX do begin

      // determine squared distance from center for this pixel
      SqDist := SqY + SqX[x - LX];

      // inside inner circle? Most often..
      if sqdist < RMF2 then begin
        // inside the inner circle.. just give the scanline the
        // new color
        P[x] := 255
      end else begin
        // inside outer circle?
        if sqdist < RPF2 then begin
          // We are inbetween the inner and outer bound, now
          // mix the color
          Fact := round(((Radius - sqrt(sqdist)) * 2 / Feather)
            * 127.5 + 127.5);
          // just in case limit to [0, 255]
          P[x] := Max(0, Min(Fact, 255));
        end else begin
          P[x] := 0;
        end;
      end;
    end;
  end;
end;
   Page last changed: 05Nov2006 © Copyright 2002-2005 SimDesign