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;
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.
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;
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;
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));
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;
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.