Saturday, January 07, 2012

My First Smart Program

Last week I wrote about my first impressions about the OP4JS/Smart Mobile Studio project. This week I’ll show you my first program.

I was always fascinated by fractals. I’ve read Fractals Everywhere, I’ve written my own Mandelbrot set-generating program and I’ve played a lot with different programs that generated all kinds of fractals. For my first Smart program (i.e. a program written with the Smart Mobile Studio – but that’s a handful to type) I’ve also selected to paint a Mandelbrot set on the screen.

First I generated a new project of the Visual type. That also generated my main form, which is also the only form in the program.

The Application

In Smart, the application object is a first class citizen where you create stuff. In my example, application object (which is stored in its own unit) is really simple.

unit Mandelbrot;

interface

uses w3system, w3ctrls, w3forms, w3application,
Main;

type
TApplication = class(TW3CustomApplication)
private
public
procedure ApplicationStarting;override;
end;

implementation

procedure TApplication.ApplicationStarting;
var
mForm: TW3CustomForm;
begin
mForm:=TFormMain.Create(display.view);
mForm.name:='Mandelbrot demo';
RegisterFormInstance(mForm,true);
inherited;
GotoForm('Mandelbrot demo');
end;

end.

As you can see, the code overrides the ApplicationStarting event. Here, a form is created and registered with the runtime system. Everything except the ‘GotoForm’ call (which is usually not needed at all – I’ll explain later why I needed it in my program) is generated automatically, you only have to set the form name (if the default name, Form1, is not good enough).

The Form

A form is, just like in the VCL, a TObject descendant. Smart automatically generated the empty form unit for me and I only had to write few lines of code. I have highlighted those lines.

unit Main;

interface

uses w3system, w3ctrls, w3forms, w3application,
MandelbrotCanvas;

type
TFormMain=class(TW3form)
private
FCanvas: TW3MandelbrotCanvas;
protected
procedure FinalizeObject; override;
procedure FormActivated; override;
procedure InitializeObject; override;
procedure Resize; override;
procedure StyleTagObject; override;
end;

implementation

procedure TFormMain.FormActivated;
begin
inherited;
FCanvas.Visible := true;
FCanvas.Invalidate;
end;

procedure TFormMain.FinalizeObject;
begin
FCanvas.Free;
inherited;
end;

procedure TFormMain.InitializeObject;
begin
inherited;
FCanvas := TW3MandelbrotCanvas.Create(self);
FCanvas.Visible := false;
end;

procedure TFormMain.Resize;
begin
inherited;
FCanvas.SetBounds(5, 5, Width - 10, Height - 10);
end;


procedure TFormMain.StyleTagObject;
begin
inherited;
StyleClass:='TW3CustomForm';
end;

end.

Components should be created in InitializeObject and destroyed in FinalizeObject. [Those two methods are useful in all kinds of scenarios, because they replace AfterConstruction and BeforeDestruction.] As there’s currently no graphical designer yet in Alpha 2 release, I had to create my canvas component by hand.

Components should be always positioned in the Resize method because in the InitializeObject they are not yet placed in the HTML DOM and are therefore not yet a full-fledged HTML citizen. My code simply sets canvas position so that there’s some border around it.

StyleTagObject method was generated automatically and connects my form with it’s CSS style. I could set a different style class here or add another class in addition to the default one.

I ran into some problems because I wanted my program to automatically start painting Mandelbrot set when it is started. I needed a good starting point, a place in code where I knew that everything was initialized and positioned. Resize was my first idea but it turned out that it is called few times before the form is fully initialized so it was ruled out. With some digging in the RTL code (which is fully included as a source code!) I found out that I can call GotoForm in my application object and that this will trigger FormActivated method. In this method I can then make canvas visible and invalidate it to force repaint. The important part of the trick was to call GotoForm after calling inherited because inherited ApplicationStarting really triggered all those resizes and set everything up for me.

The Canvas

The canvas is actually my own custom control, derived from the built-in TW3GraphicControl, which is base class for all controls and provides access to the HTML5 Canvas.

unit MandelbrotCanvas;

interface

uses w3system, w3components, MandelbrotEngine;

type
TW3MandelbrotCanvas = class(TW3GraphicControl)
private
FEngine: TMandelbrotEngine;
protected
procedure FinalizeObject; override;
procedure InitializeObject; override;
function MapIterationToColor(iteration: integer): TW3RGBA;
procedure MapIterationsToImageLine(const iterations: TIterationArray;
imageLine: TW3ImageData);
procedure Paint; override;
procedure Resize; override;
end;

implementation

const CMaxIterations = 120;
const CInitialLeft = -0.8;
const CInitialTop = -1.2;
const CInitialRight = 2.2;

function MakeColor(R, G, B: integer): TW3RGBA;
begin
Result.R := R;
Result.G := G;
Result.B := B;
Result.A := 255;
end;

{ TW3MandelbrotCanvas }

procedure TW3MandelbrotCanvas.FinalizeObject;
begin
FEngine.Free;
inherited;
end;

procedure TW3MandelbrotCanvas.InitializeObject;
begin
inherited;
FEngine := TMandelbrotEngine.Create;
FEngine.MaxIterations := CMaxIterations;
end;

procedure TW3MandelbrotCanvas.MapIterationsToImageLine(
const iterations: TIterationArray; imageLine: TW3ImageData);
var
x: integer;
begin
Assert(iterations.Length = imageLine.Width);
for x := 0 to iterations.Length - 1 do
imageLine.setPixel(x, 0, MapIterationToColor(iterations[x]));
end;

function TW3MandelbrotCanvas.MapIterationToColor(iteration: integer): TW3RGBA;
var
R, G, B: integer;
begin
if iteration = CMaxIterations then
Result := MakeColor(0, 0, 0)
else begin
iteration := iteration * 128;
B := iteration mod 256;
iteration := (iteration div 256) * 128;
G := iteration mod 256;
iteration := (iteration div 256) * 128;
R := iteration mod 256;
Result := MakeColor(R, G, B);
end;
end;

procedure TW3MandelbrotCanvas.Paint;
var
imageLine : TW3ImageData;
iterations: TIterationArray;
y : integer;
begin
if not Visible then
Exit;
imageLine := Canvas.CreateImageData(FEngine.View.Right - FEngine.View.Left + 1, 1);
for y := FEngine.View.Top to FEngine.View.Bottom do begin
iterations := FEngine.GetLine(y);
MapIterationsToImageLine(iterations, imageLine);
Canvas.PutImageData(imageLine, FEngine.View.Left, y);
end;
end;

procedure TW3MandelbrotCanvas.Resize;
var
offset, scale: float;
begin
inherited;
FEngine.View := IntRect(0, 0, Width - 1, Height - 1);
FEngine.World := FloatRect(CInitialLeft, CInitialTop, CInitialRight);
// center vertically
FEngine.World := FloatRect(FEngine.World.Left, - FEngine.World.Height / 2,
FEngine.World.Right);
// scale if screen height is insufficient
scale := 2 * Abs(CInitialTop) / FEngine.World.Height;
if scale > 1 then
FEngine.World := FloatRect(scale * FEngine.World.Left,
- scale * FEngine.World.Height / 2, scale * FEngine.World.Right);
// move right if there is place
if FEngine.World.Right > CInitialRight then begin
offset := - (FEngine.World.Right - CInitialRight) / 2;
FEngine.World := FloatRect(offset + FEngine.World.Left,
- FEngine.World.Height / 2, offset + FEngine.World.Right);
end;
end;

Again, InitializeObject and FinalizeObject are used to create Mandelbrot engine (which is the part of the code doing useful calculations). Resize is scary and complicated, but it just makes sure that the displayed area of the Mandebrot set (FEngine.World) is nicely centered and in the same aspect ratio as the on-screen view (FEngine.View).

The main magic happens in the Paint method. If the canvas is not yet visible, it simply exits. [Yes, Paint is actually called even if the component is not visible. Three times.] Otherwise, it creates one pixel high off-screen bitmap containing one graphical line. For each line in the view, it asks the engine to return an array with some data, converts this to colors (MapIterationsToImageLine) and writes the off-screen bitmap (now containing colors representing one line of the Mandelbrot set) to the canvas.

The Engine

The engine contains two important methods and some declarations and methods that were later moved into helper units and which I won’t show here. [Especially because this part was later modified a lot. I’ll rather show you a cleaner design when I return to this topic.]

The two important methods from the MandelbrotEngine unit are:

function  TMandelbrotEngine.GetIterationFor(x, y: integer): integer;
var
rX, rY, rU, rV, rZ: float;
begin
rX := World.Left + World.Width * (x - View.Left) / View.Width;
rY := World.Top + World.Height * (y - View.Top) / View.Height;
Result := 0;
rU := 0;
rV := 0;
repeat
rZ := Sqr(rU) - Sqr(rV) - rX;
rV := 2 * rU * rV - rY;
rU := rZ;
Inc(Result);
until (Sqr(rU) + Sqr(rV) > 4) or (Result = FMaxIterations);
end;

function TMandelbrotEngine.GetLine(y: integer): TIterationArray;
var
x: integer;
begin
Result := new Integer[View.Right - View.Left + 1];
for x := View.Left to View.Right do
Result[x-View.Left] := GetIterationFor(x, y)
end;

GetLine goes over one display line, calls GetIterationFor for each pixel and stores the result into the array which will be returned to the caller. [Returning this data via the var parameter would be – in my opinion – a cleaner solution but Smart has some problems with the var parameters which were not yet fully resolved.]

The Result

I could run my program in an emulator …

image

… or in a browser to get a bigger picture (click on it).

image

I could not, however, run the program successfully on my iPad. It painted about 20% of the picture and stopped. I found out that it was taking too long to execute and iOS simply stopped my script. There’s a way around this problem and I’ll return to it in a later installment of this series.

2 comments:

  1. Interesting.. is the compiled JavaScript version online somewhere?

    ReplyDelete
  2. It is now: http://www.gabrijelcic.org/Mandelbrot/

    ReplyDelete