Fast Graphic Deformation Using Scanlines

By: Tim DelChiaro

Abstract: Article from the October 2009 issue of Blaise Pascal Magazine

    Fast Graphic Deformation Using Scanlines

by Peter Bijlsma

From the October 2009 issue of Blaise Pascal Magazine

Sometimes it’s necessary to manipulate picture representations, e.g. to correct the perspective. Programs like Photoshop have that functionality: buildings who are leaning towards each other can be set straight in no time. How can we accomplish such a deliberate deformation? We know, either by own experience or by reading articles or books about programming, that routines manipulating images on pixel-level are very slow. Older Pascallers (like me) will remember the Putpixel procedure and our, sometimes desperate, attempts to learn enough machine language to make our routines faster by using Inline code. Nowadays we do not have only faster “machines”, but also the programming possibilities developed. Delphi gave us a powerful property to bitmaps: Scanlines. This article will describe the use of scanlines in connection with a program called “Deform!”.

    Scanlines

The Bitmap.Scanline property is a pointer to a horizontal row of pixels in an image. As this property is read-only, we must make a copy of the scanlinepointers in order to be able to control the pixels in these rows (or lines) ourselves. In this copying-process we can also enhance the “granularity” of the pointers, so that we can address not only the separate pixels in a line, but also the Red, Blue and Green components of these pixels. This is done by defining a new type in the header of our program:

type
  TPixCol = record //sequence is Blue, Green, Red in scanlines
    B : byte;
    G : byte;
    R : byte;
  end;
  TPixArray = array[0..5000] of TPixCol;  //5000 lines is enough for our use
  pPixArray = ^TPixArray;

Suppose we select an area of our image in Image1 (we’ll call this “main image”) by drawing a rectangle around the part we want to manipulate. We copy this area into a new bitmap, OBM[0], which then contains a copy of the original pixels of the selected part of our image. By doing the same for the adjacent areas we obtain 9 bitmaps, see figure 1:

Hide image
deform fig 1

Figure 1 Selected area and adjacent areas in main image copied to OBM bitmaps

The bitmap copies are stored using the CopyRect procedure:

for I := 0 to 8 do begin
  SrcRect[I] := Rect{corner coordinates of area};
  DestRect := Rect(0,0,Ls[I],Ns[I]);
  OBM[I].Height := Ns[I];
  OBM[I].Width := Ls[I];
  OBM[I].Canvas.CopyRect(DestRect, Image1.Canvas, SrcRect[I]);
end;

The SrcRect and DestRect rectangles (type TRect) have a height of Ns (number of scanlines) and a width of Ls (Length of scanlines). Next, the scanlinepointers of an area to be manipulated must be copied to a dynamic array of type pPixArray, called ScanlinSel:

SetLength(ScanlinSel, Ns);

for I := 0 to Ns-1 do ScanlinSel[I] := OBM.Scanline[I];

Now we can reach each pixel as for instance ScanlinSel[Y][X].R points to the Red byte of pixel number X in row number Y of the OBM involved. But as we will see, in most cases we don’t need access to the separate Red, Blue or Green components.

In the following sections I’ll describe procedures to deform the selected area (and consequently the adjacent areas). For this purpose we use another bitmap (VirtBM) in which the scanlinepointers of the manipulated areas are stored temporarily.

    Linear Distortion

With Linear Distortion I mean that the pixels in the original bitmap copy OBM are stretched or compressed only in the X and the Y directions. The selected area is leading and the adjacent areas are changed synchronously to warrant smooth transitions between the various areas. See figure 2 for the process. The first image (at left) is the original image, in the next image the mouse cursor is used to draw a rectangle: the selected area. In the third image the rectangle is deformed by grabbing the sides with the mouse cursor and moving them to other positions. The last image at right shows us the result.

Hide image
Click to see full-sized image

Figure 2 Linear Distortion Process

Each area (OBM[0] through OBM[8]) is in this process deformed in the X and the Y direction. This is done in the procedure LinearDistort, which puts the result in VirtBM. After copying VirtBM back to the original bitmap (this happens nine times), the process is completed. Now we see the need to store a copy of the original in OBM first: because the new (deformed) areas can overlap the original areas. The procedure LinearDistort needs the number of selected scanlines, the length of the selected scanlines, the number of new scanlines in VirtBM and the length of the new scanlines as parameters. The procedure is as follows:

procedure TForm1.LinearDistort(NmbSelSLs, LenSelSLs, NmbVirtSLs, LenVirtSLs: Integer);
var Ysel, Yvirt, J, CyW: Integer;
    CyF, Fry, Xfact, Yfact: Single;
    {sub-procedure FillXVirt is incorporated here:}
    procedure FillXvirt;      //stretch-shrink horizontal lines, see later
    begin
    ||
    end
begin
  VirtBM.Width := LenVirtSLs;
  VirtBM.Height := NmbVirtSLs;
  SetLength(ScanlinVirt, NmbVirtSLs);
  for J := 0 to NmbVirtSLs-1 do ScanlinVirt[J] := VirtBM.ScanLine[J];
  Yfact := NmbVirtSLs/NmbSelSLs;   //factor to stretch-shrink vertically
  Xfact := LenVirtSLs/LenSelSLs;   //factor to stretch-shrink horizontally
  Yvirt := 0; CyF := 0.5;
  CyW := Trunc(Yfact); Fry := Frac(Yfact);
  for Ysel := 0 to NmbSelSLs-1 do begin
    if CyW>0 then for J := 1 to CyW do begin
      FillXvirt;
      inc(Yvirt);
    end;
    CyF := CyF + Fry;
    if CyF >= 1 then begin
      FillXvirt;
      inc(Yvirt);
      CyF := CyF - 1;
    end;
  end;
end; 

In this procedure first the scanlinepointers to VirtBM are copied to the array ScanlinVirt. Like ScanlinSel, ScanlinVirt is a dynamic array of type pPixArray. To stretch or shrink the pixelarrays in OBM (copy of the original, array ScanlinSel points to these pixels) to the deformed ones in VirtBM (with pointers ScanlinVirt) the proportion-factors must be calculated first. If for instance the height of the bitmap in VirtBM is a quarter bigger than the original in OBM and the width is a three quarters of the original, factors Yfact and Xfact become 1.25 and 0.75 respectively. The truncated whole number (1 in this case for Y) is stored in CyW and the fraction (0.25) in Fry. For each Y value in the selected area the procedure determines whether and how often Ysel must be copied to the new pointer Yvirt. If counter CyW is greater than zero, CyW new lines are generated, while counter CyF generates a new line when the sum of the fractions during the loop add up to at least one. In our example this means that each time after four loops an additional line is generated. To obtain a better distribution I’ve preset counter CyF to 0.5.

The new lines are generated in sub-procedure FillXvirt. This procedure takes care that the length (X direction) of the new lines are calculated using the factor Xfact. The same algorithm is used as for the Y direction described above, but now a random number between 0 and 1 is used as preset for CxF to avoid loss of information in the event of very thin vertical lines:

procedure FillXvirt;      //stretch-shrink horizontal lines
var Xvirt, Xsel, I, CxW: Integer;
    CxF, Frx: Single;
begin
  Xvirt := 0; CxF := random;
  CxW := Trunc(Xfact); Frx := Frac(Xfact);
  for Xsel := 0 to LenSelSLs-1 do begin
    if CxW>0 then for I := 1 to CxW do begin
      ScanlinVirt[Yvirt][Xvirt] := ScanlinSel[Ysel][Xsel];
      inc(Xvirt);
    end;
    CxF := CxF + Frx;
    if CxF >= 1 then begin
      ScanlinVirt[Yvirt][Xvirt] := ScanlinSel[Ysel][Xsel];
      inc(Xvirt);
      CxF := CxF - 1;
    end;
  end;
end;

The pointers to ScanlinSel are now copied to the pointers of ScanlinVirt dependent upon the size of the new bitmap VirtBM. After looping through LinearDistort, VirtBM contains the deformed bitmap of one area, which can subsequently be transferred to the main image through the Canvas.Draw procedure. Summarized, the code to change the content of all nine areas (see figure 1) in a linear way is:

for J := 0 to 8 do begin
  SetLength(ScanlinSel, Ns[J]);
  if (Lsv[J]>0) and (Nsv[J]>0) then begin
    for I := 0 to Ns[J]-1 do ScanlinSel[I] := OBM[J].ScanLine[I];
    LinearDistort(Ns[J], Ls[J], Nsv[J], Lsv[J]);     //result in VirtBM
    Image1.Canvas.Draw(SPoint[J].X, SPoint[J].Y, VirtBM);
  end;
end;

Where Ns and Ls are the number and length of the original scanlines, Nsv and Lsv are the number and length of the new (deformed) scanlines and SPoint (of type TPoint) is on the main image the upper left-hand corner where the deformed bitmap should be placed.

    Dynamic Distortion

Where in linear distortion the sides of the selected area could only be moved in the X and/or the Y direction to reshape the area, for dynamic distortion the corners of the selected area can be moved in any direction. This means that one side of the area can be stretched while the other (opposite) side can be compressed. Although the resulting area can now have an odd shape, the adjacent areas must follow the selection to warrant smooth transitions, see figure 3:

Hide image
Click to see full-sized image

Figure 3 Dynamic Distortion Process

The first image (at left) is the original image, in the next image the mouse cursor is used to draw a rectangle: the selected area. In the third image the rectangle is deformed by grabbing the corners with the mouse cursor and moving them to other positions. The last image at right shows us the result.

The random shapes of the areas make the calculations of the deformed pixel positions rather complicated. I decided to tackle this problem in various steps:

  1. Copy the selected area and the eight adjacent areas in OBM[0..8], see figure 1, as in linear distortion.
  2. After reshaping the selected area to the desired form, call for each area the procedure LinearDistort, but stretched only in the Y direction to the height of the longest side. This is done to obtain the required number of scanlines for the actual dynamic distortion process.
  3. Call for each area the procedure DynamicDistort, using the corner points of the shape as parameters. Calculate the position of each pixel in a virtual scanline which starts from a point on the left side of the modified shape and ends at a point at the right side of the shape. The start and end points have different Y positions and the lines have different lengths. As the number of scanlines is fixed to the longest side, some values on the other (shorter) side can be the same.
  4. Map the calculated pixels of the virtual scanlines to the original bitmap of the main image.

The DynamicDistort procedure starts with calculating the desired begin and end points of the virtual scanlines. By using the formula X = (Y-B)/A an X position is calculated for each Y position on the left and the right side of the modified shape. These points are stored in arrays SPar (start points) and EPar (end points), forming two lines being the left and right side of the shape. Coefficients A and B can be calculated from the corner coordinates which are given to the procedure as parameters:

procedure TForm1.DynamicDistort(BegLo, BegHi, EndLo, EndHi: TPoint; NmbOrgPix: Integer); 

Special care must be taken when a line is exactly vertical:

if BegHi.X = BegLo.X then begin ABeg := 1e9; BBeg := 0; end
else begin
  ABeg := (BegHi.Y-BegLo.Y)/(BegHi.X-BegLo.X);
  BBeg := BegHi.Y - ABeg*BegHi.X;
end;
if EndHi.X = EndLo.X then begin AEnd := 1e9; BEnd := 0; end
else begin
  AEnd := (EndHi.Y-EndLo.Y)/(EndHi.X-EndLo.X);
  BEnd := EndHi.Y - AEnd*EndHi.X;
end;

In the next part of the procedure the Start point array (SPar) and End point array (EPar) are filled. The lengths of these arrays are both equal to the longest side. The algorithm starts always with the longest side and compresses the other side in a similar way as we did for Linear Distortion. This is done by storing in subsequent array values of the shortest side equal Y values where necessary. Because the shrink-factor is always equal to or smaller than one, no CyW counter is needed:

NmbVirtSLs := max(BegLo.Y-BegHi.Y, EndLo.Y-EndHi.Y) + 1;
SetLength(SPar,NmbVirtSLs);
SetLength(EPar,NmbVirtSLs);
if NmbVirtSLs = BegLo.Y-BegHi.Y+1 then begin  // begin of shape is longest
  Fry := Frac((EndLo.Y-EndHi.Y+1)/(BegLo.Y-BegHi.Y+1));
  CyF := 0.5; I := 0;
  if Fry = 0 then Fry := 1; // heights are equal
  Ye := EndHi.Y; Xe := EndHi.X;
  for Yb := BegHi.Y to BegLo.Y do begin
    SPar[I].Y := Yb;
    if ABeg=1e9 then SPar[I].X := BegHi.X
    else SPar[I].X := round((Yb-BBeg)/ABeg);
    EPar[I].Y := Ye;
    EPar[I].X := Xe;
    CyF := CyF + Fry;
    if CyF >= 1 then begin   
      inc(Ye);
      if AEnd=1e9 then Xe := EndHi.X
      else Xe := round((Ye-BEnd)/AEnd);
      CyF := CyF - 1
    end;
    inc(I);
  end;
end else begin  // end of shape is longest
  Fry := Frac((BegLo.Y-BegHi.Y+1)/(EndLo.Y-EndHi.Y+1));
  CyF := 0.5; I := 0;
  if Fry = 0 then Fry := 1;
  Yb := BegHi.Y; Xb := BegHi.X;
  for Ye := EndHi.Y to EndLo.Y do begin
    SPar[I].Y := Yb;
    SPar[I].X := Xb;
    EPar[I].Y := Ye;
    if AEnd=1e9 then EPar[I].X := EndHi.X
    else EPar[I].X := round((Ye-BEnd)/AEnd);
    CyF := CyF + Fry;
    if CyF >= 1 then begin   
      inc(Yb);
      if ABeg=1e9 then Xb := BegHi.X
      else Xb := round((Yb-BBeg)/ABeg);
      CyF := CyF - 1
    end;
    inc(I);
  end;
end;

The final part of DynamicDistort calculates the pixel positions of the virtual scanlines in “real” coordinates. Use is made of the formula Y = A * X + B, where the coefficients A and B must be calculated for each virtual scanline. Next, the calculated pixel positions are assigned to the real scanline pointers of the ScanlinImg array, which holds a copy of the BitmapImg scanlines of the main picture in Image1.

for I := 0 to NmbVirtSLs-1 do begin
  NmbNewPix := EPar[I].X - Spar[I].X + 1;  // different for each line
  if SPar[I].X = EPar[I].X then begin ALin := 1e9; BLin := 0; end
  else begin
    ALin := (SPar[I].Y - EPar[I].Y)/(SPar[I].X - EPar[I].X);
    BLin := SPar[I].Y - ALin * SPar[I].X;
  end;
  CxW := Trunc(NmbNewPix/NmbOrgPix);
  Frx := Frac(NmbNewPix/NmbOrgPix);
  CxF := 0.5;
  Xe := SPar[I].X;
  Ye := round(ALin * Xe + BLin);
  if Ye > Image1.Picture.Height - 1 then Ye := Image1.Picture.Height - 1;
  if Ye < 0 then Ye := 0;
  for Xb := 0 to NmbOrgPix-1 do begin
    if CxW > 0 then for J := 1 to CxW do PlotPixel;
    CxF := CxF + Frx;
    if CxF>=1 then begin
      PlotPixel;
      CxF := CxF - 1;
    end;
  end;
end;

The pixel positions are calculated in the sub-procedure PlotPixel:

procedure PlotPixel;
begin
  ScanlinImg[Ye][Xe] := ScanlinVirt[I][Xb];
  if Ye+1 < Image1.Picture.Height - 2 then
    ScanlinImg[Ye+1][Xe] := ScanlinVirt[I][Xb]; // avoid empty pixel positions
  if abs(ALin)>0.9 then begin
    if Ye+3 < Image1.Picture.Height - 2 then begin
      ScanlinImg[Ye+2][Xe] := ScanlinVirt[I][Xb];
      ScanlinImg[Ye+3][Xe] := ScanlinVirt[I][Xb];
    end;
  end;
  inc(Xe);
  if Xe > Image1.Picture.Width - 1 then Xe := Image1.Picture.Width - 1;
  Ye := round(ALin * Xe + BLin);
  if Ye > Image1.Picture.Height - 1 then Ye := Image1.Picture.Height - 1;
  if Ye < 0 then Ye := 0;
end;

To avoid too many complicated calculations, I did not give any thoughts about anti-aliasing or the like. However, plotting the virtual scanlines could result in “empty” places, as each X position has only one Y position. This is illustrated in figure 4:

Hide image
Click to see full-sized image

Figure 4 Empty pixel positions appear when plotting virtual scanlines

This problem is solved simply by plotting two (Ye and Ye + 1) pixels for each X position. After all, if the additional pixel is superfluous, the next line will overwrite this position automatically. For very steep lines even more Y values are plotted for the same X.

A short note about the huge gain in speed when scanlines are used as opposed to direct pixel addressing. Replace the code of the “for Xb”-loop into:

//Image1.Canvas.Pen.Mode := pmCopy;   must be set before
For Xb := 0 to NmbOrgPix-1 do begin
  PixCol := ScanlinVirt[I][Xb].R + ScanlinVirt[I][Xb].G shl 8 + ScanlinVirt[I][Xb].B shl 16;
  Image1.Canvas.Pen.Color := PixCol;  if CxW > 0 then for J := 1 to CxW do begin
    Image1.Canvas.Pixels[Xe,Ye] := PixCol;
    if I < NmbVirtSLs-1 then Image1.Canvas.Pixels[Xe,Ye+1] := PixCol;
    inc(Xe);
    Ye := round(ALin * Xe + BLin);
  end;
  CxF := CxF + Frx;
  if CxF>=1 then begin
    Image1.Canvas.Pixels[Xe,Ye] := PixCol;
    if I < NmbVirtSLs-1 then Image1.Canvas.Pixels[Xe,Ye+1] := PixCol;
    inc(Xe);
    Ye := round(ALin * Xe + BLin);
    CxF := CxF - 1;
  end;
end;

In this procedure the correct colours are directly assigned to the pixels of Image1. Although this direct approach seems more efficient, the routine is annoyingly slow. Try this example just to learn that one should use scanlines instead!

Summarized, the code to change the content of all nine areas (see figure 1) in a random way is:

SetLength(ScanlinImg, Image1.Picture.Height);
for I := 0 to Image1.Picture.Height - 1 do ScanlinImg[I] := BitmapImg.ScanLine[I];
for J := 0 to 8 do begin
  SetLength(ScanlinSel, Ns[J]);
  if Nsv[J]>0 then begin
    for I := 0 to Ns[J]-1 do ScanlinSel[I] := OBM[J].ScanLine[I];
    LinearDistort(Ns[J], Ls[J], Nsv[J], Ls[J]); // first stretch to correct height
    SetLength(ScanlinSel, Nsv[J]);
    DynamicDistort(CornerLftlo[J], CornerLfthi[J] ,CornerRgtlo[J], CornerRgthi[0], Ls[J]);
  end;
Image1.Picture.Bitmap := BitmapImg;
end;

For Ns, Ls and Nsv see the section Linear Distortion. The Corner-variables are of type TPoint and refer to the four corners of the distorted area. The actual code, however, is slightly different. The complete source code (belonging to the program “Deform!”) can, as usual, be downloaded from the website. This brings us to:

    The Program “Deform!”

The program “Deform!” utilizes the procedures as described in the previous paragraphs. The program can be used to deform photographic or other images. Sometimes for a good reason, such as the straightening of objects or correcting the perspective, but also just for fun. See figure 5.

Hide image
Click to see full-sized image

Figure 5 “Deform!” can change your images

All buttons are located at the left-hand side, which enables the bitmap image in Image1 (at right) to follow the size of the window when it is resized by the user. As I admire the lady on the example image very much, I use a black bar to make her unrecognizable. The user can load either a JPG or a BMP image by clicking “Load Image”. Be careful with the “Fit to Screen” button: this is not a zoom function, but it changes the actual number of pixels. See for instance what happens when you load a picture, drag the right side of the window as far as possible to the left, click “Fit to Screen”, maximize the window and click “Fit to Screen” again. The image will be barely recognizable. The “Fit to Screen” process makes use of the LinearDistort procedure to resize, so complete rows and columns of pixels are thrown away by reducing the size. However, as you will find out, this is an easy way to reduce your X-megapixel high-resolution photographs to an acceptable size for use on the internet. The procedure takes care that the image fits best on your working area:

procedure TForm1.BtScreenFitClick(Sender: TObject);
var I, PH, PW, CH, CW: Integer;
    Aspect: single;
begin
  UndoBM.Assign(BitmapImg); //save original
  PH := Image1.Picture.Height;
  PW := Image1.Picture.Width;
  CH := Image1.Height;
  CW := Image1.Width;
  if (PW<=0) or (CW<=0) then exit;
  Aspect := PH/PW;
  if Aspect > CH/CW then CW := Trunc(CH/Aspect) else  CH := Trunc(CW * Aspect);
  SetLength(ScanlinSel, PH);
  for I := 0 to PH-1 do ScanlinSel[I] := BitmapImg.ScanLine[I];
  LinearDistort(PH, PW, CH, CW);
  BitmapImg.Assign(VirtBM);
  Image1.Picture.Bitmap := BitmapImg;
end;

The Distortion buttons are grouped together. After clicking one of these, an area can be selected by holding down the left mouse button and dragging the cursor over the image. Upon “Button Up” the area is fixed. The program contains obviously CursorButtonDown, Move and Up procedures, but I will not describe them here to save space. I only tell that, as the mouse routines are shared by all distortion modes, Booleans are used to control the process.

After “Distort Linear” has been clicked, the area set can be deformed by grabbing (mouse cursor and left button) the four sides of the area and drag them to the desired positions. On clicking “Go” the LinearDistort procedure is called nine times (once for each sub-area, see figure 1) causing the image to reshape according to figure 2.

When “Distort Inner Area” has been clicked, a special use of the DynamicDistort procedure is made. The area is now divided into four equal pieces. Only the centre point can be moved and after clicking “Go” the four areas are deformed by calling LinearDistort followed by DynamicDistort four times, see figure 6:

Hide image
Click to see full-sized image

Figure 6 Dynamic Distortion Process for a single point

The first image (at left) is the original image, in the next image the mouse cursor is used to draw a rectangle: the selected area. In the third image the rectangle is deformed by grabbing the centre point with the mouse cursor (plus left button) and moving it to another position. This process is called “Warp” in other programs. The last image at right shows us the result after clicking “Go”. The part of the image outside the selected area is not affected, so in this mode not the whole picture is deformed.

When “Distort All” has been clicked, the area set can be deformed by grabbing (mouse cursor and left button) the four corner points of the area and drag them to the desired positions. After clicking “Go” the LinearDistort and DynamicDistort procedures are called nine times (once for each sub-area) and the image is reshaped according to figure 3.

When “MirrorR(ight)” or “MirrorL(eft)” has been clicked, the selection area is split by a vertical division line in the centre. See figure 7, second image (as this gentleman doesn’t sing as good as the lady of figure 5, I didn’t use the black bar this time):

Hide image

Figure 7 The Mirror process

MirrorR causes the right side of the selection to be mirrored to the left side (third image) and MirrrorL does the opposite (fourth image). So this mode can be used to make faces (or objects) symmetrical. The code is rather simple:

begin
  SetLength(ScanlinSel,Ns[0]);
  for I := 0 to Ns[0]-1 do  ScanlinSel[I] := OBM[0].ScanLine[I];
  for I := 0 to Ns[0]-1 do for J := 0 to (Ls[0]-2) shr 1 do
    if MRight then ScanlinSel[I][J] := ScanlinSel[I][ls[0]-J-1]
    else ScanlinSel[I][ls[0]-J-1] := ScanlinSel[I][J];
  Image1.Canvas.Draw(Sel[3].X, Sel[3].Y, OBM[0]); 
end;

OBM[0] contains, as usual, a copy of the original selected area and Sel[3] (TPoint) is the upper left-hand corner of the selected area.

The last mode is “Crop”. If after several distortions you want to save the created image, the edges may be deformed in an undesirable way. These edges can be cut off by selecting the “good” area after “Crop” has been selected. Clicking “Go” causes that the new main image will be just the selected area:

Image1.Picture.Bitmap := OBM[0];

    Conclusion

May be after clicking “Go” you didn’t get the result you’d expected. No problem, after clicking “Undo” the former image is put back on the screen. But be careful: there is only one undo level! Of course you can utilize several distortion modes after each other. This may result in a picture like in figure 8:

Hide image

Figure 8 A possible final result

When the final result is satisfactory, you can save the image (as JPG or BPM) by clicking “Save Image”. As a special feature I added a “Save as Icon” possibility. This button can be used to make your own icons without using an icon-editor. The icon attached to this program is made with this procedure. Before using, crop your image to obtain an approximately square area. However, don’t set your expectations too high: as an icon has an area of only 32 x 32 pixels, hi-res pictures or photographs can hardly be used for this process.

Making a program like this is like eating peanuts: once you’ve started it’s not easy to stop. You always think of another nice useful or funny feature to add. Be my guest, download the source code and add anything you like. In the “Distort Inner Area” for instance two points can be used instead of only one point, or even four points. Anyway, have fun playing, changing or just using this program.

    Blaise Pascal Magazine

See more articles like this in each issue of Blaise Pascal Magazine - a magazine all about Delphi, Delphi Prism, Pascal and related languages. Blaise Pascal Magazine is available in English, Dutch and Portuguese.

 

Server Response from: ETNASC03