In this little tutorial we are going to take a trip back in time, back to the golden age of personal computing. A time when the ultimate computer was the Commodore Amiga and the latest kid on the block was wire-frame 3d graphics and polygon art. I am of course talking about the 1990’s and the glowing polygon effect.
Continue reading
Category Archives: Documentation
Tutorial, iOS, Android, Graphics, Games, CSS, Animations, HTML5, Javascript, General programming
Working with javascript structures
When interacting with ordinary javascript libraries or a remote webservice you natually have to go native with your datastructures. Under Smart this can be handled in a variety of ways but the two most common approaches are to either use a record or a variant.
Using records
Records in Smart work just like they do in other object pascal environments. And since Smart compiles to javascript rather than machine opcodes, a record will always compile to an ordinary javascript structure. I should note that javascript really doesnt distinguish between what we would call a record and objects. In javascript everything is an object.
So the following object pascal code:
[sourcecode language=”delphi”]
type TMyInfo = Record
miName: String;
miAge: Integer;
miGender: integer;
End;
[/sourcecode]
Results in the following predictable javascript code:
[sourcecode language=”javascript”]
{
miName: "",
miAge: 0,
miGender: 0
}
[/sourcecode]
This means that when you want to interact with native javascript libraries that expects a structure of values, you can use records to match it directly.
Variants
Using variants to achieve the same thing is perhaps easier, especially since you dont have to predefine the structure by hand. As of writing however it does require you to initialize the variant manually. We will probably add some RTL functions to simplify this very soon, but for now you can use this “one liner” ASM snippet:
[sourcecode language=”delphi”]
Procedure testvariant;
var
mData: variant;
Begin
asm @mData = {}; end; // Set variant to "empty JS object"
mData.miName:=’john doe’;
mData.miAge:=39;
mData.miGender:=1;
end;
[/sourcecode]
The resulting javascript structure will be exactly the same as that produced by the record. One difference to keep in mind though, is that the compiler will output the names “as is”. There is no way for Smart to map the content of a native object so make sure you get the names right.
How do i move graphics from the canvas to an image?
In alpha release 3 several missing bits and pieces from the HTML5 standard was added to Smart Mobile Studio. One of the key methods that were missing from our canvas implementation was toDataUrl – a function which has it’s opposite in TW3ControlBackground and TW3Image fromURL and LoadFromUrl methods.
So in effect, it is now very simple to transport off-screen graphics (graphics you have created on a canvas) and use it as either a background or as the content of a normal image.
Here is an quick example of how to create a picture dynamically and display it in a TW3Image control. Also notice that you can actually assign the image to the background of any TW3CustomControl descendant.
To run the example create a new visual project and replace the code in Form1 with the following:
[sourcecode language=”delphi”]
unit Form1;
interface
uses w3system, w3ctrls, w3forms, w3graphics,w3application;
type
TForm1=class(TW3form)
private
{ Private methods }
FImage: TW3Image;
FGraph: TW3GraphicContext;
FButton: TW3Button;
procedure HandleButtonClicked(Sender:TObject);
protected
{ Protected methods }
Procedure InitializeObject;override;
Procedure FinalizeObject;override;
Procedure StyleTagObject;override;
procedure resize;Override;
end;
Implementation
//############################################################################
// TForm1
//############################################################################
Procedure TForm1.InitializeObject;
Begin
inherited;
FImage:=TW3Image.Create(self);
FImage.Background.fromColor(clWhite);
FButton:=TW3Button.Create(self);
FButton.OnClick:=HandleButtonClicked;
FButton.Caption:=’Click to generate’;
FGraph:=TW3GraphicContext.Create(NIL);
End;
Procedure TForm1.FinalizeObject;
Begin
FImage.free;
FButton.free;
FGraph.free;
inherited;
End;
procedure TForm1.HandleButtonClicked(Sender:TObject);
var
FCanvas: TW3Canvas;
aFrom: String;
aTo: String;
Begin
FGraph.Allocate(FImage.Width,FImage.Height);
FCanvas:=TW3Canvas.Create(FGraph);
try
// Clear background
FCanvas.fillstyle:=’rgb(0,0,99)’;
FCanvas.fillrectF(0,0,FGraph.Width,FGraph.Height);
// Draw some text on the canvas
FCanvas.font:=’10pt verdana’;
FCanvas.FillStyle:=’rgb(255,255,255)’;
FCanvas.FillTextF(‘This was generated on a canvas!’,10,20,MAXINT);
//You can load it in as a picture
FImage.LoadFromUrl(FCanvas.toDataUrl(”));
//.. or load it as a background to a picture (or control)
//FImage.background.fromUrl(FCanvas.toDataUrl(”));
finally
FCanvas.free
end;
FGraph.release;
end;
procedure TForm1.resize;
var
hd: Integer;
dy: Integer;
Begin
FImage.setBounds(5,5,width-10,(height div 2) + (height div 3) );
hd:=height – (FImage.top + FImage.Height + 5);
dy:=(hd div 2) – (FButton.height div 2);
FButton.setBounds(5,dy + FImage.Top + FImage.height,width-10,FButton.height);
end;
Procedure TForm1.StyleTagObject;
Begin
inherited;
StyleClass:=’TW3CustomForm’;
End;
end.
[/sourcecode]
How do i plot pixels on a offscreen bitmap?
Just like ordinary Delphi, Javascript has it’s own canvas object. As you can imagine the canvas of the browser is quite different from the good old Delphi TCanvas, but not so alien that it doesn’t have it’s similarities. But you will need to brush up on the documentation from Apple in order to use it.
But HTML5 also have something else up it’s sleeve, namely that you can create off-screen bitmaps (wrapped in the TW3ImageData class from w3graphics.pas). The difference between a Delphi bitmap and a HTML5 bitmap is 3 fold:
- The bitmap is obtained from the canvas, not the other way around (Delphi’s TBimap has a canvas property)
- Off-screen bitmaps do not have a canvas, they are in reality DIBS (device independent bitmaps)
- Off-screen bitmaps are 32bit exclusively (RGBA, 8888 format).
This is actually good news because it means we can create games and multimedia dealing with pixel data rather than the (excuse me) somewhat cumbersome canvas object. Most javascript developers actually avoid this part because they are used to “high level” coding only – but coders coming from the Delphi community have been knocking out low level graphics libraries for ages. So if you have ever played around with DIBS (device independent bitmaps) in Delphi – you should feel right at home.
Here is a quick example of how to plot some pixels off-screen, and copy the results onto the screen (note: this is a game project):
[sourcecode language=”delphi”]
unit Project8;
interface
uses w3system, w3components, w3application,
w3game, w3gameapp, w3graphics, w3components;
type
TApplication=class(TW3CustomGameApplication)
private
{ Private methods }
FBuffer: TW3ImageData;
protected
{ protected methods }
procedure ApplicationStarting;override;
procedure ApplicationClosing;override;
Procedure PaintView(Canvas:TW3Canvas);override;
end;
Implementation
//############################################################################
// TApplication
//############################################################################
procedure TApplication.ApplicationStarting;
Begin
inherited;
//Initialize refresh interval, set this to 1 for optimal speed
GameView.Delay:=20;
(* Create a our "offscreen bitmap" *)
FBuffer:=GameView.Canvas.CreateImageData(32,32);
//Start the redraw-cycle with framecounter active
//Note: the framecounter impacts rendering speed. Disable
//the framerate for maximum speed (false)
GameView.StartSession(true);
End;
procedure TApplication.ApplicationClosing;
Begin
GameView.EndSession;
inherited;
End;
// Note: In a real live game you would try to cache as much
// info as you can. Typical tricks are:
// 1: Only get the width/height when resized
// 2: Pre-calculate strings, especially RGB/RGBA values
// 3: Only redraw what has changed, avoid a full repaint
// The code below is just to get you started
procedure TApplication.PaintView(Canvas:TW3Canvas);
Begin
// Clear background
canvas.fillstyle:=’rgb(0,0,99)’;
canvas.fillrectF(0,0,gameview.width,gameview.height);
// Draw our framerate on the screen
canvas.font:=’10pt verdana’;
canvas.FillStyle:=’rgb(255,255,255)’;
canvas.FillTextF(‘FPS:’ + IntToStr(GameView.FrameRate),10,20,MAXINT);
// plot some pixels
if FBuffer<>NIL then
Begin
FBuffer.setPixelC(0,2,clGreen);
FBuffer.setPixelC(1,2,clGreen);
FBuffer.setPixelC(2,2,clGreen);
FBuffer.setPixelC(0,4,clRed);
FBuffer.setPixelC(1,4,clRed);
FBuffer.setPixelC(2,4,clRed);
// paste graphics to display
Canvas.putImageData(FBuffer,20,20);
end;
End;
end.
[/sourcecode]
The result may not look so impressive right now, but it’s the foundation for what will become a very impressive browser based graphics system:
In the future we are going to add a lot more functionality to the TW3ImageData class. Things like line, ellipse, fillrect, circle and flood-fill is already prototyped. But if you feel like playing around with pixels – this is a good start.
Open the file w3graphics.pas and have a look at the methods and properties of TW3ImageData for more information. Also check out this excellent canvas tutorial by Mark Pilgrim.
How do I rotate a control by X degrees?
All descendants of TW3Customcontrol, which is the primary object most visual controls derive from, has a property called Angle. This is a floating point property which will rotate your control in whatever angle you define. Please bear in mind that this property will override any webkit rotation in the current style. If you want the stylesheet to retain it’s position, mark the CSS tag with !important;
For instance:
[sourcecode language=”delphi”]
unit Form1;
interface
uses w3system, w3ctrls, w3forms, w3application;
type
TForm1=class(TW3form)
private
{ Private methods }
FButton: TW3Button;
Procedure HandleClick(Sender:TObject);
protected
{ Protected methods }
Procedure InitializeObject;override;
Procedure FinalizeObject;override;
Procedure StyleTagObject;override;
end;
Implementation
//############################################################################
// TForm1
//############################################################################
Procedure TForm1.InitializeObject;
Begin
inherited;
FButton:=TW3Button.Create(self);
FButton.caption:=’Click me’;
FButton.setBounds(100,50,100,28);
FButton.onClick:=HandleClick;
End;
Procedure TForm1.HandleClick(Sender:TObject);
Begin
if FButton.Angle=60 then
FButton.Angle:=0 else
FButton.Angle:=60;
end;
Procedure TForm1.FinalizeObject;
Begin
inherited;
End;
Procedure TForm1.StyleTagObject;
Begin
inherited;
StyleClass:=’TW3CustomForm’;
End;
end.
[/sourcecode]
When you click the button it will toggle between normal (0 deg) and tilted (60 deg), like this:
How do i create and use a timer?
Under Delphi and free-pascal there is a component called TTimer. Under Smart Mobile Studio however all types of timer objects have a slightly different architecture – and also different names (we will probably provide a TTimer alias before release). The class you are looking for is called “TW3EventRepeater”.
To use a timer start by adding the unit “w3time” to your unit’s uses section. Unlike Delphi, TW3EventRepeater derives from TObject rather than TComponent. There is no point in creating a component (which adds quite a bit of overhead) when a normal object will do.
Using the event repeater from a form is a snap:
[sourcecode language=”delphi”]
unit Form1;
interface
uses w3system, w3ctrls, w3forms, w3application, w3time;
type
TForm1=class(TW3form)
private
{ Private methods }
FTimer: TW3EventRepeater;
FCount: Integer;
function HandleTimer(sender:TObject):Boolean;
protected
{ Protected methods }
Procedure FormActivated;override;
end;
Implementation
//############################################################################
// TForm1
//############################################################################
Procedure TForm1.FormActivated;
Begin
FTimer:=TW3EventRepeater.Create(HandleTimer,1000);
end;
function TForm1.HandleTimer(sender:TObject):Boolean;
Begin
result:=False;
inc(FCount);
if FCount>9 then
Begin
TW3EventRepeater(sender).free;
result:=true;
w3_showmessage(‘time is up!’);
end;
end;
end.
[/sourcecode]
As expected the timer kicks in after 10 seconds informing us that the time has expired
You may have noticed that the callback was a function and not a procedure? If you return “true” the timer will stop of it’s own accord. Since this is javascript and not winapi you can actually dispose of the timer while the timer is called. Because the timer object does not go out of scope until the function returns.
Add a new form to my application
Adding a new form to a visual application is a piece of cake. Start by opening your project (or create a new visual project), then click the “add form” button in the IDE or add form from the project menu.
This will present you with a brand new unit, ready to be used by your application. Now switch over to your project unit (in this example, the project was automatically called “project7”, and thus the project unit is called “project7″). The project units looks something like this:
[sourcecode language=”delphi”]
unit Project7;
interface
uses w3system, w3ctrls, w3forms, w3application,
form1;
type
TApplication=class(TW3CustomApplication)
private
{ Private methods }
public
{ protected methods }
procedure ApplicationStarting;override;
end;
Implementation
//############################################################################
// TApplication
//############################################################################
procedure TApplication.ApplicationStarting;
var
mForm: TW3CustomForm;
Begin
//Add code above this line
mForm:=TForm1.Create(display.view);
mForm.name:=’Form1′;
RegisterFormInstance(mForm,true);
inherited;
End;
end.
[/sourcecode]
Before you can actually display and work with your form, the application must know about the form’s existance. So we need to register an instance of the form with the form manager inside TApplication (remember to add “form2″ to your uses section). Simply create an instance of your form by copying the code already present. We alter the name ofcourse so we dont get any conflicts, and we send False rather than True as the last parameter:
[sourcecode language=”delphi”]
procedure TApplication.ApplicationStarting;
var
mForm: TW3CustomForm;
Begin
//Add code above this line
mForm:=TForm1.Create(display.view);
mForm.name:=’Form1′;
RegisterFormInstance(mForm,true);
mForm:=TForm2.Create(display.view);
mForm.name:=’Form2′;
RegisterFormInstance(mForm,false);
inherited;
End;
[/sourcecode]
The reason we pass in “false” as the second parameter is because this value defines the “main” form of the application. All visual applications must have a main form, which is also the form the application displays when you start it.
We can now use the methods GotoForm or GotoFormByRef to navigate between forms. Both these commands (which are members of TCustomApplication) takes a second parameter which controls the special effect. As of writing you can only slide the new form in from either the left or right, but more interesting effects are planned.
How do i create a class
You can create a class under Smart Mobile Studio the exact same way as you would under Embarcadero Delphi or free-pascal. Just like ordinary object pascal, classes, custom types and functions are stored in a unit file. Below is an example of a very simple class which has one public member:
[sourcecode language=”delphi”]
unit myobject;
interface
uses w3system;
type
TMyObject = Class(TObject)
public
procedure dosomething;
End;
implementation
procedure TMyObject.dosomething;
Begin
w3_showmessage(‘you called me’);
end;
end.
[/sourcecode]
To add a new unit to your project, click the “add unit” button in the IDE, or select add unit from the project menu. You can then proceed to type in the code you want. By default the IDE provides some skeleton code for your unit, so you don’t have to type everything.
To use the unit you have just created – simply add the name of the unit to the uses section in an already existing unit. All unit have a uses section which makes the code in other units visible to that particular unit.
More information
Working with controls, the boxing model
Smart mobile studio introduces a few minor differences to an otherwise “VCL like” RTL. Just like Delphi you can create visual and non-visual controls, you have the same methods to work with (like resize) and you can move and size your child controls with left, top, width and height properties.
One important issue however, is that reading and writing non pascal properties (left, top, width and height being prime examples) relies on your control’s HTML to be visible to the DOM. In short, reading properties in the constructor can cause the browser to report NaN (not a number) as a result. This has nothing to do with our RTL but rather how the browser realizes cascading styles.
Modern HTML5 actually have 3 sets of styles:
- A style “class” applied to an element from a style sheet (see “styleClass” property in TW3CustomControl)
- Inline styles, which overrides whatever is set in the style sheet
- Calculated styles, which is the combined style the browser has compiled from the above. This is why it’s call “cascading” style-sheets, because they flow into each other resulting in a final style.
Since we want our components to reflect “real” values, our component model reads values from the calculated style. If we did not do this – things like margins defined in a css style would be invisible to your controls (and thus size calculations would be wrong).
[sourcecode language=”delphi”]
Procedure TForm1.InitializeObject;
Begin
inherited;
FLabel:=TW3Label.Create(self);
FLabel.left:=10;
FLabel.top:=10;
FLabel.width:=Self.Width – 20;
//Self.Width will return NaN, it should be moved to Resize
End;
[/sourcecode]
So to sum up:
- Positions (etc) are written as in-line styles but read from calculated styles
- Calculated styles are only available “post” creation of a control, which means reading position values in the constructor is not the correct way of doing things
Use resize
The proper way of handling a good layout under Smart is to deal with positions under the resize method. This doesn’t require any more code than usual – you just have to put your layout code under resize rather than in the constructor. This is how most languages deal with it (the C# object model is particularly strict about these things).
[sourcecode language=”delphi”]
Procedure TForm1.InitializeObject;
Begin
inherited;
FLabel:=TW3Label.Create(self);
End;
Procedure TForm1.Resize;
Begin
inherited;
FLabel.left:=10;
FLabel.top:=10;
FLabel.width:=Self.Width – 20;
End;
[/sourcecode]
Your first OP4JS custom control
Since the basic framework is nearing completion at high speed (although some modifications might appear) I thought it could be interesting to demonstrate how to create your very own custom control under OP4JS.
In the Delphi world there are two archetypal programmers: Those that stick to the high level interface of Delphi, meaning that they only use pre-made controls and rarely go beneath the bonnet so to speak – and the second group which create components, packages and love to fiddle around with low-level coding. This type of stereotyping is something more or less unique to RAD languages (which would include Delphi, Lazarus, Visual Basic, Real Basic and languages that are “design” based). In more mature languages like C++, Delphi (yes delphi is just as low level as the C++) or C# low-level work is almost unavoidable. But with Object Pascal you at least have a choice (!)
With OP4JS we have tried to retain much of the HLA (high level abstraction) of Delphi, but the system demands that if you want something unique – you have to create your own controls in order to stand out. If not, you might as well go with the many template-based “app machines” out there that produce programs that all look and feel the same.
The basics of a control
There are two types of controls you can create under OP4JS: Graphical controls and ordinary parent/child controls (like you are used to from Delphi). A graphical control is an object that contains a HTML5 graphics context and canvas, which means you have to draw it yourself. This is good for things like games, charts or anything that cant be represented using “normal” html.
The second type, which is the one 90% of all the standard controls that ship with OP4JS derive from, is TCustomControl. So let’s have a look at how setting up a fancy iPhone header looks like:
type TW3HeaderControl = Class(TCustomControl) private FLabel: TLabel; FBackBtn: TW3ToolButton; FNextBtn: TW3ToolButton; Protected Procedure Resize;Override; protected Procedure InitializeObject;override; Procedure FinalizeObject;override; Procedure StyleTagObject;override; End;
Under Delphi you would probably expect to see a constructor and destructor in the above code, but under OP4JS we try to synchronize the number of steps taken to establish a control (i.e number of resize and position operations). So to solve this we have introduced 3 new methods: InitializeObject, FinalizeObject and StyleTagObject. As the names imply you create any sub-controls and initialize your variables in InitializeObject (just think of it as your constructor), you destroy those instances in FinalizeObject, and you apply any extra styling or property alteration in the StyleTagObject method.
Clever styling
When a control is in the state of creation, it automatically set it’s own style class (think html here) to the name of it’s class. So in order to give our little control a nice look – we edit the default style sheet (this is automatically created when you create a new application) and make it look like a true iPhone header. The rule of thumb is: The name of the CSS class always matches the name of the OP4JS control:
.TW3HeaderControl { background-image:-webkit-gradient(linear, left top, left bottom, from(#b2bbca), color-stop(0.25, #a7b0c3), color-stop(0.5, #909cb3), color-stop(0.5, #8593ac), color-stop(0.75, #7c8ba5), to(#73839f)); border-top: 1px solid #cdd5df; border-bottom: 1px solid #2d3642; -webkit-box-shadow: rgba(33, 33, 33, 0.2) 0px 1px 1px; }
Adding behavior
If you don’t know anything about CSS or HTML, don’t worry, you can always copy a style you like from the default skin, and there are plenty of examples around the Internet to learn from (or just get a good book, that would save you a lot of time). OK now we need to add some code to our unit to make it come alive:
Procedure TW3HeaderControl.InitializeObject; Begin inherited; FLabel:=TLabel.Create(self); FLabel.BorderStyle:=bsDotted; end; Procedure TW3HeaderControl.FinalizeObject; Begin FLabel.free; if assigned(FBackBtn) then FBackBtn.free; if assigned(FNextBtn) then FNextBtn.free; inherited; end; Procedure TW3HeaderControl.StyleTagObject; Begin inherited StyleTagObject; end; Procedure TW3HeaderControl.Resize; var wd,hd: Integer; mTemp: Integer; dx: Integer; Begin wd:=Width; hd:=Height; mTemp:=wd; dx:=4; dec(mTemp,4); //left margin dec(mTemp,4); //Right margin if assigned(FBackBtn) and (FBackBtn.Visible) then Begin //space between "back" & label dec(mTemp,FBackBtn.width); dec(mTemp,2); //offset the left edge of the label inc(dx,FBackBtn.Width); inc(dx,2); // position the back-button FBackBtn.Left:=4; FBackBtn.Top:=4; end; if assigned(FNextBtn) and (FNextBtn.Visible) then Begin //space between label & "next" button dec(mTemp,FNextBtn.width); dec(mTemp,2); FNextBtn.Left:=mTemp; FNextBtn.Top:=4; end; FLabel.left:=dx; FLabel.top:=4; FLabel.Width:=mTemp; FLabel.Height:=Height-8; end;
Why all that code? First of all because it’s just a quick example and secondly, because a typical iPhone header have 3 parts: A back button, a next button and some text in the middle. The above code checks to see if you actually have created a back or next button and shrinks the text area accordingly. Let’s see how it looks so far:
I have set the label to have a dotted frame just to make it easier to work with, we will of-course remove the frame all together later. But now, let’s add some more methods to the class interface. We want to expose the label object to give the user access to it’s full capabilities (caption, font, background, orientation and all the rest), and we also want both back and next buttons.
But, we want to keep resources to a bare minimum, so we will use the old “need” trick and only create the buttons when asked for. So the buttons will only be created when you access them:
TW3HeaderControl = Class(TCustomControl) private FLabel: TLabel; FBackBtn: TW3ToolButton; FNextBtn: TW3ToolButton; procedure ReleaseButtons; Procedure NeedBackButton; Procedure NeedNextButton; function getBackButton:TW3ToolButton; function getNextButton:TW3ToolButton; protected Procedure Resize;Override; protected Procedure InitializeObject;override; Procedure FinalizeObject;override; Procedure StyleTagObject;override; public Property BackButton:TW3ToolButton read getBackButton; Property NextButton:TW3ToolButton read getNextButton; property Title:TLabel read FLabel; Property Options:TW3HeaderButtonOptions read FOptions write setOptions; End;
And the implementation for our new methods are (this is just a quick example):
Procedure TW3HeaderControl.NeedBackButton; Begin // Create object on the fly if required if not assigned(FBackBtn) then Begin BeginUpdate; FBackBtn:=TW3ToolButton.Create(Self); SetReSized; EndUpdate; end; end; Procedure TW3HeaderControl.NeedNextButton; Begin // Create object on the fly if required if not assigned(FNextbtn) then Begin BeginUpdate; FNextbtn:=TW3ToolButton.Create(Self); SetReSized; EndUpdate; end; end; function TW3HeaderControl.getBackButton:TW3ToolButton; Begin NeedBackButton; result:=FBackBtn; end; function TW3HeaderControl.getNextButton:TW3ToolButton; Begin NeedNextButton; result:=FNextBtn; end; procedure TW3HeaderControl.ReleaseButtons; Begin if assigned(FBackBtn) then Begin FBackBtn.free; FBackBtn:=NIL; end; if assigned(FBackBtn) then Begin FNextBtn.free; FNextBtn:=NIL; end; end;
The final product
To use our new control, all we have to write (in our Form’s InitializeObject for instance) is something like:
FMyCoolHeader:=TW3HeaderControl.Create(self); FMyCoolHeader.BackButton.Caption:='Back'; FMyCoolHeader.BackButton.onClick:=HandleBackButtonClicked; FMyCoolHeader.Title.Caption:='My cool header!';
And voila – we have a nifty iPhone header that looks and feels like the real thing 🙂
Notes and addendum
This example is very, very simple. It was put together in 5 minutes just to demonstrate how easy it is to get going and is in no-way an example of code quality or approach. I can think of 1000 ways of making a better header control (the one that ships with OP4JS has a lot more code and does not create buttons this way, and yes — exceptions and error handling is a must).
Focus, callstack and recursive processing
The world of mobile computing is getting better and better, but there are some limitations when dealing with smart gadgets running iOS or Android. The first thing is obviously the speed, which doesn’t even come close to what an average home computer can muster. Even my 5 year old test machine can outrun any mobile device without making so much as a dent in the cpu-o-meter.
The second limitation people are going to discover, depending of-course on the line of work you do, is that mobile devices is the proverbial banker when it comes to memory – implying that your call stack is but a microbial-fraction of what we are used to. What does this mean? Well every time you call a method under Javascript, you eat up one call-slot. If this procedure in turns calls another, and another (and so on) it quickly gobbles up the meager call stack limit. The size of the call stack is varied and depends on the vendor, as of writing iOS is sticking to it’s near obsolete range of 100 recursive calls while Android is said (I couldn’t find any official information on this) to have a call stack of about 800.
To kiss or not to kiss, that is the question
Kiss (keep it simple stupid) is one way to go when it comes to mobile development. But there are times when you must resort to classical speed mechanisms to reach the full potential of a system. As such I am in the process of optimizing the base-classes for speed and simplicity (some form of balance preferably). The method is really simple and it’s known as loop expansion (or just “unrolling”). Let’s for sake of argument say you have a control with a lot of child objects, like a grid or a list. Instead of doing a simple for/next loop, like this:
[sourcecode language=”delphi”]
Procedure TMyObject.DoSomething;
var
x:Integer;
Begin
for x:=0 to FChildren.Count-1 do
Begin
process(FChildren[x]);
end;
end;
[/sourcecode]
The above example is perfectly valid, but it’s quite slow depending on the data-load you expect. A list control could be expected to handle hundreds if not thousands of elements – so we want to speed things up and minimize loop time:
[sourcecode language=”delphi”]
Procedure TMyObject.DoSomething;
var
mLongs:Integer;
mShort:Integer;
mCount:Integer;
mIndex:Integer;
begin
mCount:=FChildren.Count;
mLongs:=mCount shr 3;
mShort:=mCount mod 8;
while mLongs>0 do
Begin
process(FChildren[mIndex]);inc(mIndex);
process(FChildren[mIndex]);inc(mIndex);
process(FChildren[mIndex]);inc(mIndex);
process(FChildren[mIndex]);inc(mIndex);
process(FChildren[mIndex]);inc(mIndex);
process(FChildren[mIndex]);inc(mIndex);
process(FChildren[mIndex]);inc(mIndex);
process(FChildren[mIndex]);inc(mIndex);
dec(mLongs);
end;
while mShort>0 do
Begin
process(FChildren[mIndex]);inc(mIndex);
dec(mShort);
end;
end;
[/sourcecode]
Speed-wise, at least when it comes to crunching fixed data like pixels or typed JS arrays, is nearly incomparable to the first approach. It obviously produce more code – but the benefits are just to good to ignore (roughly 4 times as fast).
As long as your final build does not exceed 10 megabytes, which is Apple’s limit on single javascript files, it’s not a problem (and that would be a huge, huge project if you ask me).
Men first, women and children last
When doing recursive calls that affect an entire object model you have to deal with the men first, meaning of-course those elements that does not have children attach to them (oh this is gonna get me into so much trouble if my wife reads it). Why? Because we want to minimize the number of recursive calls (or steps taken) a control has to make. Let’s use the simple formula to demonstrate (this would also be loop expanded in the final release):
[sourcecode language=”delphi”]
Procedure TMyObject.DoSomething;
var
x:Integer;
mCount:Integer;
mStack: TObjectList;
mObj: TObject;
Begin
//avoid multiple calls to getCount in FChildren array
mCount:=FChildren.Count-1;
for x:=0 to mCount do
Begin
//minimize call to getItem in FChildren array
mObj:=FChildren[x];
if mObj.count=0 then
// No kids? Do singles first
process(mObj) else
Begin
//kids? Ok, we’ll handle that later
if mStack=NIL then
mStack:=TobjectList.Create;
mStack.add(mObj);
end;
end;
if mStack<>NIL then
Begin
mCount:=mStack.Count;
while mCount>0 do
Begin
mObj:=mStack[mCount-1];
process(mObj);
dec(mCount);
end;
mStack.free;
end;
end;
[/sourcecode]
And last but not least, the “process” method should be inlined if possible, thats 50% of the point here. If you can avoid a secondary call – then avoid it, that’s how you cut down on call-stack overload and as a nice benefit you get a speed boost.
Notes
#1 [29.09.11, 18:31]: Eric Grange pointed out (as did two readers on this page) that the same effect will happen if we apply FChildren.Count-1 and start from zero. That would be slightly faster to since we dont execute a continous subtraction of the index. A clear error on my part.
#2 [29.09.11, 18:31]: Interestingly, the -1 trick was originally used for string processing (hence starting at 1) and has nothing to do with speed per-see, except that you save an “if FChildren.Count>0 then” in some languages. But this tiny advantage is indeed lost by the use of [-1] in the loop itself. Some basic compilers will execute the code even if FChildren.count =0. This is an old habit from the days of Amiga BlitzBasic programming.
#3 [29.09.11, 18:35]: Also the “process()” call should only be concidered pseudo code. It’s the loop expansion that’s being demonstrated. Preferably you would work with the data directly at this point rather than call another proc, since the whole point is to limit the number of calls while speeding up a process. It’s not meant as a literal example, but as the bare-bones of a technique.