The new Paint is significantly more complicated than the old one.
firstY : integer;
imageLine : TW3ImageData;
start : float;
y : integer;
if not Visible then
if not FInitialized then
imageLine := Canvas.CreateImageData(FCoord.View.Width + 1, 1);
start := Now;
firstY := FResumePaint;
if firstY < 0 then begin
firstY := FCoord.View.Top;
FResumePaint := -1;
for y := firstY to FCoord.View.Bottom do begin
iterations := FEngine.GetLine(FCoord.World.Left, FCoord.World.Right,
FCoord.MapToWorldY(y), FCoord.View.Width + 1);
Canvas.PutImageData(imageLine, FCoord.View.Left, UpDown(y));
if HasElapsedMs(start, CPaintTimeout_ms) and (y < FCoord.View.Bottom) then
FResumePaint := y + 1;
if not assigned(FTimer) then
FTimer := TW3EventRepeater.Create(PaintTimer, 1);
FTimer := nil;
After few initial checks and optional initialization, the code allocates one high off-screen image buffer (Canvas.CreateImageData). If this is a fresh paint (and not a continuation of a previous work), view is cleared (ClearView) and the paint loop starts from the top of the view (FCoord.View.Top). Otherwise, paint loop starts from the first unpainted line (FResumePaint).
The code then loops from the starting line to the end of the view (for loop). In each iteration it gets one line of data from the Mandelbrot engine (FEngine.GetLine), converts the data to pixels (MapIterationsToImageLine) and writes the data to the screen (Canvas.PutImageData). Then it checks the time. If more than CPaintTimeout_ms (one second in the current code) has elapsed, the code stores next line to be painted in the FResumePaint field and sets the timer which will call the Paint method as soon as possible.
function TW3MandelbrotCanvas.PaintTimer(sender: TObject): boolean;
Then the story continues until the last line is painted and timer is destroyed. After that, new paint will be requested by the main form when the user action calls for it.
Last time I mentioned painting lines and rectangles on the canvas to indicate zoom and pan operations.
Again, there are two ways to achieve nice smooth effect that is pleasing for the eye. As the user moves the line around, the rectangle/line must also move. The problem is not painting the new rectangle/line but restoring the Mandelbrot set image where the rectangle/line was painted previously.
One option is to keep Mandelbrot set image in the big off-screen bitmap and then always paint Mandelbrot set first (directly from this off-screen bitmap) and paint rectangle or line over it.
Another possibility is to store the bitmap under the rectangle/line into an off-screen buffer before it is painted and then restore it when the image must be restored. This is the way my program handles the job.
I’ll just show few examples of the code here. GrabArrow stores everything under the line (and that may be a large area as we can’t really store diagonal part of the image (or rather, it could be done but would be quite complicated) but must store a rectangle that contains the line.
procedure TEmbelishCanvas.GrabArrow(pFrom, pTo: TPoint);
FArrow.Active := true;
FArrow.PFrom := pFrom;
FArrow.PTo := pTo;
rect := FixOrientation(w3_Rect(pFrom.X, pFrom.Y, pTo.X, pTo.Y));
FArrow.Image := FCanvas_ref.GetImageData(rect.Left - 1, rect.Top - 1,
rect.Right - rect.Left + 3, rect.Bottom - rect.Top + 3);
[FixOrientation is a helper function that “orients” the rectangle (swaps corners if necessary) so that Left is not larger then Right and Top is not larger than Bottom.]
GrabMarquee stores the area under the rectangle and does it in an optimized manner – it only grabs four small rectangles lying directly below the rectangle sides.
procedure TEmbelishCanvas.GrabMarquee(rect: TRect);
width, height: integer;
width := rect.Right - rect.Left;
height := rect.Bottom - rect.Top;
FMarquee.Active := true;
FMarquee.Rect := rect;
FMarquee.Top := FCanvas_ref.GetImageData(
rect.Left - 1, rect.Top - 1, width + 2, 2);
FMarquee.Bottom := FCanvas_ref.GetImageData(
rect.Left - 1, rect.Bottom - 1, width + 2, 2);
FMarquee.Left := FCanvas_ref.GetImageData(
rect.Left - 1, rect.Top - 1, 2, height + 2);
FMarquee.Right := FCanvas_ref.GetImageData(
rect.Right - 1, rect.Top - 1, 2, height);
Hide does the reverse operation – restores the original bitmap.
if FArrow.Active then begin
rect := FixOrientation(w3_Rect(FArrow.PFrom.X, FArrow.PFrom.Y,
FCanvas_ref.PutImageData(FArrow.Image, rect.Left - 1, rect.Top - 1);
FArrow.Active := false;
if FMarquee.Active then begin
FMarquee.Top, FMarquee.Rect.Left - 1, FMarquee.Rect.Top - 1);
FMarquee.Bottom, FMarquee.Rect.Left - 1, FMarquee.Rect.Bottom - 1);
FMarquee.Left, FMarquee.Rect.Left - 1, FMarquee.Rect.Top - 1);
FMarquee.Right, FMarquee.Rect.Right - 1, FMarquee.Rect.Top - 1);
FMarquee.Active := false;
The remaining code contains two paint methods (one draws a line, another a rectangle) and two public members ShowArrow and ShowMarquee which are called from the mouse handling events.
procedure TEmbelishCanvas.PaintArrow(pFrom, pTo: TPoint);
FCanvas_ref.StrokeStyle := '#FFFFFF';
FCanvas_ref.LineWidth := 1;
FCanvas_ref.LineF(pFrom.X, pFrom.Y, pTo.X, pTo.Y);
procedure TEmbelishCanvas.PaintMarquee(rect: TRect);
FCanvas_ref.StrokeStyle := '#FFFFFF';
FCanvas_ref.LineWidth := 1;
FCanvas_ref.StrokeRectF(rect.Left - 0.5, rect.Top - 0.5, rect.Right - rect.Left + 1, rect.Bottom - rect.Top + 1);
procedure TEmbelishCanvas.ShowArrow(pFrom, pTo: TPoint);
procedure TEmbelishCanvas.ShowMarquee(rect: TRect);
if (rect.Right = rect.Left) or (rect.Top = rect.Bottom) then
If you check the program in the desktop browser (Chrome and Safari work best) you’ll see that it also displays the current position and zoom factor in the top left corner (just two TW3Label components) and when you zoom into the image you’ll see the blue arrow buttons lighting up.
Those two buttons enable you to quickly browse through the last 10 images. Each image is stored in a off-screen buffer when it is calculated and clicking the arrow button just puts an image from this cache on the screen so this is blindingly fast. The mechanics of the implementation are exactly the same as for the embellishment drawing – GetImageData is used to grab pixels and PutImageData puts them back.
You’ll also see two disabled buttons (“L” and “T”) which are just a placeholders for future code and a Help button (“?”) which will display a short help.
I’ll return to the Help button implementation (it actually displays a new form) and to speed buttons later. Next time you can expect something way more interesting – I’ll teach the program to recognize touch.