Smart Mobile Studio
  • News
  • Forums
  • Download
  • Store
  • Showcases
    • Featured demos
    • The Smart Contest 2013, Round 1 – Graphics
  • Documentation
    • Get the book
    • System requirements
    • Prerequisites
    • Getting started
      • Introduction
      • Application architecture
      • The application object
      • Forms and navigation
      • Message dialogs
      • Themes and styles
    • Project types
      • Visual project
      • Game project
      • Console project
    • Layout manager
    • Networking
      • TW3HttpRequest
      • TW3JSONP
      • Loading files
  • About

Writing small splash screen / pre-loader code

Posted on 27.09.2015 by Smart Mobile Studio Team Posted in Developers log, News

For large applications it is often needed to have a splash screen that is shown while pre-loading the required resources in the background. It is especially needed when the application should run from the web with a low connection speed.

Typically the splash screen should show a static image or simple animation. At the same time it should pre-load and cache all resources which are required to run the application. This well-defined task does not need a lot code to run and thus should be written as efficient as possible to avoid long load times for the splash screen itself.

This article is about writing small splash screen / pre-loader code, which can be used upfront any of your application.

Basic Preloader

The basic approach to this is to have a dedicated form showing as the very first form. It contains code to load the images, which are later be used by the other forms. This simple preloading mechanism is shown in the ZenSky example.

In particular the resource names are first collected like:

var Names: array of String;
Names.Add('res/body.jpg')
Names.add('res/cloud_1.png');
Names.add('res/cloud_2.png');
Names.add('res/sun.png');

next the (yet empty) images are created with

var Images: array of TW3Image;
for var Index := Low(Names) to High(Names) do
  Images.add(TW3Image.Create(nil));

At this time the images are still empty. In addition the dimensions are also not specified at this time.

In order to load the images we need to supply the previously specified resource names (which are actually locations) to the images. This can be done with the following code:

for var Index := Low(Names) to High(Names) do
  Images[Index].LoadFromURL(Names[Index]);

If we want to track loading we can also add an event handler before loading. So instead, the above code becomes:

for var Index := Low(Names) to High(Names) do
begin
  Images[Index].OnLoad := ImageLoaded;
  Images[Index].LoadFromURL(Names[Index]);
end;

Where ImageLoad is a simple function that handles showing a progress bar. It can also be replaced with a lambda function like:

var Counter: Integer = 0;
for var Index := Low(Names) to High(Names) do
begin
  Images[Index].OnLoad := lambda
    Inc(Counter);
    WriteLn('Progress: ' + IntToStr(Counter) + ' of ' + IntToStr(Names.Length));
  end;
  Images[Index].LoadFromURL(Names[Index]);
end;

Unfortunately, at this point we can not say, which image is loaded. If we take the local variable ‘index’ into account, it will be most likely equal to High(Names) as the OnLoad event is typically fired far afeter the for-loop has been finished. However, in order to only pre-load the images it is not really required to know what image loaded as long as all images do load at all.

If you look at the mentioned ZenSky example you can see how this basically works, at least in a small scale. This said, it has some disadvantages especially for large applications. For example you can typically only pre-load additional resources and not (parts of) the application itself. This means that you have to load the applications code in the first place, which – depending on the complexity of your application – can itself be easily more than a megabyte.

Just to give you a figure, an empty application (with default settings) is already about 430 kB in size. It drops to 198 kB if code packing and obfuscation is activated, but loading this over GSM is still tedious.

If your application performs heavy tasks upon starting (like pre-calculation or caching of arbitrary values), the loading of resources might not be the bottleneck when starting the application.

Separate Splash-Screen / Preloader

An alternative to this can be to write a dedicated splash screen / pre-loader separately. It can be separated since resources must only be loaded once in order to be cached. They do not need to be linked to a form immediately.

When a long pre-load time is expected it can be desired to show a nice animation instead of a simple static image with a progress bar. In this case a simple canvas application comes in mind. Unfortunately the canvas application itself isn’t that simple and thus an empty canvas application already is about 142 kB big in size even with drawing all registers supported by the compiler.

Since we don’t need much for the preloader, an even simpler application can be written instead. It uses only the officially W3C API and thus can be as small as natively written JavaScript applications. With code packing and obfuscation it can even be smaller while still remaining the high level programming language advantage such as strong typing and object oriented programming.

It’s also a good way to dive into HTML5 and JavaScript programming. A topic that is well documented with tons of examples that can be converted to Pascal easily.

Let’s start by creating a really empty project.

You can start with nearly any application type (canvas or visual application) and removing all units / code. The resulting, empty application should compile and run fine (with an empty, white screen).

You can also copy the Empty.spg template to the ‘Repository’ directory of Smart Mobile Studio (along the other .spg files for a ‘Canvas Project’ and ‘Visual Components Project’).

Once you have your empty application you can now try to add some content. For example the following code:

// create paragraph node
var ParagraphElement := JHTMLParagraphElement(Document.createElement('p'));

var TextNode := Document.createTextNode('Hello World!');

// append text node to paragraph element
ParagraphElement.appendChild(TextNode);

// append paragraph element to document body
Document.body.appendChild(ParagraphElement);

will get you a page with a single paragraph showing the text ‘Hello World’. The code is a simplified Pascal translation of the example given at w3schools.com.

Similar to this you can also head over to the another w3schools.com example, which targets to bring an HTML5 canvas onto the screen.

Here’s the code for adding a canvas element to the HTML5 DOM:

uses
  W3C.HTML5, W3C.DOM, W3C.Canvas2DContext, W3C.CSS, W3C.CSSOM, W3C.Console;

// create canvas element
var CanvasElement := JHTMLCanvasElement(Document.createElement('canvas'));

// specify ID of canvas element (not necessary)
CanvasElement.id := 'myCanvas';

// specify width/height
CanvasElement.width := 400;
CanvasElement.height := 200;

// add a 1 pixel solid border to the element
var CanvasStyle := JElementCSSInlineStyle(CanvasElement).style;
CanvasStyle.setProperty('border', '1px solid #000000');

// append canvas element to document body
Document.body.appendChild(CanvasElement);

Once you have the canvas element you can start drawing:

// get 2D canvas context
var Context := JCanvasRenderingContext2D(CanvasElement.getContext('2d'));

// draw a simple rectangle
Context.fillStyle := '#00FF00';
Context.fillRect(0, 0, 350, 175);

// create a radial gradient
var Gradient := Context.createRadialGradient(75, 50, 5, 90, 60, 100);
Gradient.addColorStop(0, "red");
Gradient.addColorStop(1, "white");

// fill a circle (full arc) with the gradient
Context.arc(50, 50, 50, 0, 2 * Pi);
Context.fillStyle := Gradient;
Context.fill;

// write 'Hello World' text
Context.font := '30px Arial';
Context.fillStyle := '#0000FF';
Context.fillText('Hello World', 10, 50);

If you don’t want to draw by code you can also export your favorite vector graphic (SVG) with Inkscape to HTML5 Canvas code. Using a vector graphic has the advantage to always retain a sharp image, regardless of the screen resolution. This also avoids the need to load a big splash screen image, which itself can eat up page-loading performance.

Given the fact that gradients and text rendering is possible you can easily create astonishing graphics or bring your companies logo to the screen.

With the above code, the canvas element is not yet shown full screen. This can be achieved with the following code:

uses
  W3C.HTML5, W3C.DOM, W3C.Canvas2DContext, W3C.CSS, W3C.CSSOM, W3C.Console;

// style body
var BodyStyle := JHTMLBodyElement(Document.body).style;
BodyStyle.setProperty('border', '0');
BodyStyle.setProperty('margin', '0');
BodyStyle.setProperty('padding', '0');
BodyStyle.setProperty('overflow', 'hidden');

// create canvas element
var CanvasElement := JHTMLCanvasElement(Document.createElement('canvas'));

// fullscreen canvas
CanvasElement.style.setProperty('display', 'block');

// append canvas element to document body
Document.body.appendChild(CanvasElement);

Now the canvas element has no border, margin or padding. It displays as a block with a hiddon overflow, which after all just means that it is shown fullscreen.

Splash-Screen Animation

To encapsulate the drawing code we can put it into a dedicated unit. This unit can furthermore encapsulate the drawing code in a simple class.

Let’s start with the simple unit Animation.pas

type
  TAnimation = class
  private
    FCanvas: JHTMLCanvasElement;
    FContext: JCanvasRenderingContext2D;
  public
    constructor Create(ACanvas: JHTMLCanvasElement);

    property Canvas: JHTMLCanvasElement read FCanvas;
    property Context: JCanvasRenderingContext2D read FContext;
  end;

implementation

constructor TAnimation.Create(ACanvas: JHTMLCanvasElement);
begin
  // cache canvas and context
  FCanvas := ACanvas;
  FContext := JCanvasRenderingContext2D(ACanvas.getContext('2d'));
end;

It just encapsulates the HTML5 canvas. Yet there’s no animation going, but we can easily add this via ‘RequestAnimFrame’. The code can be altered to:

type
  TAnimation = class
  private
    FCanvas: JHTMLCanvasElement;
    FContext: JCanvasRenderingContext2D;
    FAnimHandle: Variant;
  public
    constructor Create(ACanvas: JHTMLCanvasElement); virtual;
    destructor Destroy; override;

    procedure PaintCanvas;
    procedure ResizeCanvas;

    property Canvas: JHTMLCanvasElement read FCanvas;
    property Context: JCanvasRenderingContext2D read FContext;
end;

var RequestAnimFrame: function(const Meth: procedure): Variant;
var CancelAnimFrame: procedure(Handle: Variant);

implementation

procedure InitAnimationFrameShim;
begin
  asm
    @RequestAnimFrame = (function(){
      return  window.requestAnimationFrame       ||
              window.webkitRequestAnimationFrame ||
              window.mozRequestAnimationFrame    ||
              window.msRequestAnimationFrame     ||
              function( callback ){
                return window.setTimeout(callback, 1000 / 60);
              };
    })();
    @CancelAnimFrame = (function(){
      return  window.cancelAnimationFrame       ||
              window.webkitCancelAnimationFrame ||
              window.mozCancelAnimationFrame    ||
              window.msCancelAnimationFrame     ||
              function( handle ){
                window.clearTimeout(handle);
              };
    })();
  end;
end;

function RequestAnimationFrame(const AMethod: procedure): Variant;
begin
  if not Assigned(RequestAnimFrame) then
    InitAnimationFrameShim;
  Result := RequestAnimFrame(AMethod);
end;

procedure CancelAnimationFrame(Handle: Variant);
begin
  if not Assigned(CancelAnimFrame) then
    InitAnimationFrameShim;
  CancelAnimFrame(Handle);
end;

{ TAnimation }

constructor TAnimation.Create(ACanvas: JHTMLCanvasElement);
begin
  // cache canvas and context
  FCanvas := ACanvas;
  FContext := JCanvasRenderingContext2D(ACanvas.getContext('2d'));

  Window.addEventListener('resize', lambda
      ResizeCanvas;
    end);

  ResizeCanvas;

  // start animation
  FAnimHandle := RequestAnimationFrame(PaintCanvas);
end;

procedure TAnimation.ResizeCanvas;
begin
  Canvas.width := Round(Window.innerWidth);
  Canvas.height := Round(Window.innerHeight);
end;

procedure TAnimation.PaintCanvas;
begin
  // continue animation
  FAnimHandle := RequestAnimationFrame(PaintCanvas);
end;

This very simple canvas application is compiled only 3 kB in size, making it highly suitable for an animated splash screen.

Drawing Vector Graphics is easy with HTML5

Now to avoid duplicating code, I’d like to only focus on the TAnimation.PaintCanvas code. In this procedure we already have access to the 2D context and thus we can write stuff like this to draw a rounded blue icon shape:

  Context.fillStyle := #43A0CF;
  Context.beginPath;
  Context.moveTo(0, 100);
  Context.lineTo(0, 412);
  Context.quadraticCurveTo(0, 512, 100, 512);
  Context.lineTo(412, 512);
  Context.quadraticCurveTo(512, 512, 512, 412);
  Context.lineTo(512, 100);
  Context.quadraticCurveTo(512, 0, 412, 0);
  Context.lineTo(100, 0);
  Context.quadraticCurveTo(0, 0, 0, 100);
  Context.fill;

Splash1

As can be seen, the borders do not appear to be anti-aliased. It turns out that this is only an artifact of not clearing the previous drawing as with the HTML5 canvas the content persists until its dimensions change or until it is cleared explicitly.

And this is what we need to do in order to get a clear rendering. So we have to add:

  Context.ClearRect(0, 0, Canvas.Width, Canvas.Height);

in front of the code.

Splash2

Up to now the icon shape is a bit boring as it is only drawn in blue. We can add a nice gradient by changing the fixed color fill style to a gradient:

  var Gradient := Context.createLinearGradient(0, 0, 512, 512);
  Gradient.addColorStop(0, '#43A0CF');
  Gradient.addColorStop(1, '#2E88B5');
  Context.fillStyle := Gradient;

Splash3

After this we can also add our logo shape to the icon background. It’s something like this:

  // specify basic stroke settings
  Context.lineJoin := 'miter';
  Context.lineCap := 'butt';
  Context.miterLimit := 4;
  Context.strokeStyle := '#000000';

  // draw 3 lines of width 20 pixel
  Context.lineWidth := 20.000;
  Context.beginPath;
  Context.moveTo(102.730, 374.105);
  Context.bezierCurveTo(142.452, 406.851, 208.461, 430.028, 259.962, 429.915);
  Context.bezierCurveTo(362.868, 429.689, 381.442, 380.023, 383.963, 367.203);
  Context.moveTo(266.878, 384.509);
  Context.bezierCurveTo(314.150, 384.559, 329.524, 366.546, 329.861, 343.333);
  Context.bezierCurveTo(330.244, 304.216, 271.020, 300.559, 218.077, 285.230);
  Context.bezierCurveTo(123.398, 257.307, 108.723, 220.349, 108.276, 177.412);
  Context.bezierCurveTo(108.571, 161.080, 111.193, 143.477, 124.977, 119.236);
  Context.bezierCurveTo(154.897, 66.909, 214.936, 63.227, 251.687, 62.113);
  Context.moveTo(198.926, 155.535);
  Context.bezierCurveTo(193.175, 168.867, 193.320, 188.571, 216.054, 199.784);
  Context.bezierCurveTo(234.157, 208.713, 236.830, 209.909, 310.220, 226.407);
  Context.bezierCurveTo(337.624, 232.567, 362.655, 246.463, 376.020, 255.677);
  Context.bezierCurveTo(386.210, 262.701, 394.448, 270.401, 400.511, 279.365);
  Context.bezierCurveTo(406.574, 288.328, 410.735, 298.139, 413.591, 308.141);
  Context.bezierCurveTo(419.614, 329.237, 417.804, 349.397, 414.301, 366.639);
  Context.stroke;
  
  // draw bottom line of width 30 pixel
  Context.lineWidth := 30.000;
  Context.beginPath;
  Context.moveTo(92.542, 386.423);
  Context.bezierCurveTo(149.302, 431.678, 211.049, 447.248, 259.962, 447.135);
  Context.stroke;

  // draw top line of width 60 pixel
  Context.lineWidth := 60.000;
  Context.beginPath;
  Context.moveTo(394.891, 128.539);
  Context.bezierCurveTo(278.286, 53.356, 177.462, 80.713, 157.636, 130.226);
  Context.stroke;

Splash4

At this point we have the basic logo on the screen. However, the size is yet fixed at 512 x 512 pixel and the position is top left. Thus we need to scale and translate the logo.

First we need to determine the new size of the logo. It should cover 90% of the smallest dimension. A ratio for this can be determined by using:

  Ratio := 0.9 * Min(Canvas.Width, Canvas.Height) / 512;

Now we can directly scale the context by this ratio.

  Context.Scale(Ratio, Ratio);

Using this we will note that the icon either zooms in or out infinitely. This is because we haven’t reset the scaling. So after the drawing we must add the contrary:

  Context.Scale(1 / Ratio, 1 / Ratio);

In order to obtain the basic scaling.

If we know the absolute scaling, we can also specify the absolute transformation upfront by using:

  Context.setTransform(Ratio, 0, 0, Ratio, 0, 0);

This sets the scaling to the determined ratio regardless of any previous scaling.

Next we need to determine the translation. This is a bit more tricky, but we end up having something like this (after the scale):

var Counter: Integer = 0;
  Context.Translate(
    0.5 * (Canvas.Width / Ratio - 512),
    0.5 * (Canvas.Height / Ratio - 512));

Eventually we would need the inverse operation, but we can also rely on the fact that we reset the transformation matrix with setTransform all the time. Both can be shortended to:

  Context.setTransform(Ratio, 0, 0, Ratio,
    0.5 * (Canvas.Width / Ratio - 512),
    0.5 * (Canvas.Height / Ratio - 512));

Splash5

At this point we only draw a static logo to the screen. Eventually we can animate this a bit by adding:

  Context.setTransform(Ratio, 0, 0, Ratio, 0, 0);

  Context.Translate(0.5 * (Canvas.Width / Ratio), 0.5 * (Canvas.Height / Ratio));
  Context.Rotate(4 * Pi * FAnimValue);
  FAnimValue *= 0.98;
  Context.Scale(1 - FAnimValue, 1 - FAnimValue);
  Context.Translate(-0.5 * (Canvas.Width / Ratio), -0.5 * (Canvas.Height / Ratio));

  Context.Translate(
    0.5 * (Canvas.Width / Ratio - 512),
    0.5 * (Canvas.Height / Ratio - 512));

it uses an animation value stored in the class scope. It should be initialized with the value 1, but other values might work as well;

Splash6

With this our logo rotates and zooms in.

Simple HTML5 Preloader

In addition to this nice splash screen, we can easily add the code to pre-load any resources. However, in order to keep the dependencies low, we should now do this only with W3C APIs. Luckily this isn’t much different to what it looked like with the SmartCL.

It boils down to something like this:

uses
  W3C.DOM, W3C.HTML5, W3C.Console;

var Names: array of String;
var Image: JHTMLImageElement;
var Counter: Integer 

Names.Add('res/body.jpg')
Names.add('res/cloud_1.png');
Names.add('res/cloud_2.png');
Names.add('res/sun.png');

for var Index := Low(Names) to High(Names) do
begin
  Image := JHTMLImageElement(Document.createElement('img'));
  JGlobalEventHandlers(Image).onLoad := lambda
    Inc(Counter);
    Console.Log('Progress: ' + IntToStr(Counter) + ' of ' + IntToStr(Names.Length));
  end;
  Image.src := Names[Index];
end;  

Of course we can now also use the progress to draw a nice progress bar onto the HTML5 canvas. Or we can replace the animation value with the progress to base the zooming/roation on the actual load progress.

The dedicated preloader has also the big advantage to pre-load the main script in advance. This can be done quite similar to loading images:

// load main script asynchronous
var MainScript := JHTMLScriptElement(Document.createElement('script'));
MainScript.&type := 'text/javascript';
MainScript.async := true;
JGlobalEventHandlers(MainScript).onLoad := lambda
  Console.Log('Main script loaded');
end;
MainScript.src := 'Main.js';
Document.body.appendChild(MainScript);

The only missing piece at this point is to signal the loading to the main application. This can be done by exporting a function in the main program like:

procedure StartApp(Canvas: JHTMLCanvasElement); export;
begin
  Console.Log('The application can be started');
end;

In this function the canvas element is also passed. It can either be used by the main application or needs to be removed.

The presented splash screen is around 8 kB in size and should not only load fast, but also draw a nice animation without the need of further external elements like CSS or images.

company logo DOM Efficient HTML5 Pre-loader small Splash screen
« Structures, dealing with name pairs
Smart Mobile Studio 2.2 (beta-4) »

Pages

  • About
  • Feature Matrix
  • Forums
  • News
  • Release History
  • Download
  • Showcases
    • The Smart Contest 2013, Round 1 – Graphics
  • Store
  • Documentation
    • Creating your own controls
    • Debugging, exceptions and error handling
    • Differences between Delphi and Smart
    • Get the book
    • Getting started
      • Introduction
      • Local storage, session storage and global storage
      • Application architecture
      • The application object
      • Forms and navigation
      • Message dialogs
      • pmSmart Box Model
      • Themes and styles
    • Layout manager
    • Networking
      • Loading files
      • TW3HttpRequest
      • TW3JSONP
    • Prerequisites
    • Real data, talking to sqLite
    • System requirements
    • Project types
      • Visual project
      • Game project
      • Console project

Archives

  • December 2019
  • December 2018
  • November 2018
  • July 2018
  • June 2018
  • February 2018
  • September 2017
  • April 2017
  • November 2016
  • October 2016
  • September 2016
  • April 2016
  • March 2016
  • January 2016
  • October 2015
  • September 2015
  • July 2015
  • April 2015
  • January 2015
  • December 2014
  • October 2014
  • September 2014
  • August 2014
  • July 2014
  • June 2014
  • March 2014
  • February 2014
  • January 2014
  • December 2013
  • November 2013
  • October 2013
  • August 2013
  • July 2013
  • June 2013
  • May 2013
  • April 2013
  • March 2013
  • February 2013
  • January 2013
  • December 2012
  • November 2012
  • August 2012
  • July 2012
  • June 2012
  • May 2012
  • April 2012
  • March 2012
  • February 2012
  • January 2012
  • November 2011
  • October 2011
  • September 2011

Categories

  • Announcements (25)
  • Developers log (119)
  • Documentation (26)
  • News (104)
  • News and articles (16)

WordPress

  • Register
  • Log in
  • WordPress

Subscribe

  • Entries (RSS)
  • Comments (RSS)
© Optimale Systemer AS