|
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;
|