i wrote a fake loading screen

Over the weekend I watched a video by Alexei Makarenkov with the title “Loading bar is not what it seems…”, where he talks about how game developers cheat with the loading bar.

In short: the loading bar in games is fake, it could move as you like, but it moves in jerks, human perception considers this loading scenario to be the most plausible, and players do not believe in smooth loading. It’s better to see once than hear a hundred times, here’s the video: The loading bar is not what it seems… (Caution, there is an advertisement for a red bank).

But if you look too lazy, then Alexei says further that this was already predictable – an open secret, but as a rule, no one talks about this. When people find out the truth, they are “slightly” surprised. Moreover, in the articles and lectures of developers, even in those devoted to the design of loading screens, they do not write about fakes.

And here I can try to fill in the gap, and talk about how I created a fake loading screen. No, I’m not a game developer, but loading screens are not limited to games. Personally, I wrote such a dummy for an application in Silverlight. How long ago, it was, only the water of the muddy river remembers: all the statute of limitations has already passed, everyone has already forgotten about this application, and about Silverlight, so you can remove the secrecy stamp, blow off the dust from the old code and remember how it was.


Olds here? Instead of a disclaimer

There will be a necrocode in the publication, taking into account the fact that Silverlight is no longer supported, I will proceed from the assumption that no one wants to understand this, I will try to give explanations sufficient to form an idea and understanding. Still, the article is not about Silverlight, but about “how developers cheat with loading screens”.

Problem. Instead of an introduction

Initially, we didn’t really need a loading screen, and even more so there was no goal to deceive anyone. The project has a default loading indicator, it coped with its duty, you don’t even need to write anything, typical code, in my opinion, is generated when the project is created:

aspx page
<%@ Page Language="c#" AutoEventWireup="true" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
    <head id="Head1" runat="server">
        <title>Silverlight Client</title>

        <style type="text/css">
        html, body {
	        height: 100%;
	        overflow: auto;
        }
        body {
	        padding: 0;
	        margin: 0;
        }
        #silverlightControlHost {
	        height: 100%;
	        text-align:center;
        }
        </style>

        <script type="text/javascript" src="https://habr.com/ru/articles/747224/Silverlight.js"></script>

        <script type="text/javascript">
            function redirect(url) {
                window.location.href = url;
            }

            function onSilverlightError(sender, args) {
                var appSource = "";
                if (sender != null && sender != 0) {
                    appSource = sender.getHost().Source;
                }

                var errorType = args.ErrorType;
                var iErrorCode = args.ErrorCode;

                if (errorType == "ImageError" || errorType == "MediaError") {
                    return;
                }

                var errMsg = "Unhandled Error in Silverlight Application " + appSource + "\n";

                errMsg += "Code: " + iErrorCode + "    \n";
                errMsg += "Category: " + errorType + "       \n";
                errMsg += "Message: " + args.ErrorMessage + "     \n";

                if (errorType == "ParserError") {
                    errMsg += "File: " + args.xamlFile + "     \n";
                    errMsg += "Line: " + args.lineNumber + "     \n";
                    errMsg += "Position: " + args.charPosition + "     \n";
                }
                else if (errorType == "RuntimeError") {
                    if (args.lineNumber != 0) {
                        errMsg += "Line: " + args.lineNumber + "     \n";
                        errMsg += "Position: " + args.charPosition + "     \n";
                    }
                    errMsg += "MethodName: " + args.methodName + "     \n";
                }

                throw new Error(errMsg);
            }
        </script>
    </head>

    <body>
        <form id="form1" runat="server" style="height:100%">
            <div id="silverlightControlHost">
                <object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
		          <param name="source" value="ClientBin/MainApplication.xap"/>
                  <param name="windowless" value="true"/>
		          <param name="onError" value="onSilverlightError" />
		          <param name="background" value="#FFDFF0F8" />
		          <param name="minRuntimeVersion" value="4.0.50826.0" />
		          <param name="autoUpgrade" value="true" />
		          <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=4.0.50826.0" style="text-decoration:none">
 			          <img src="https://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight" style="border-style:none"/>
		          </a>
	            </object>
                <iframe id="_sl_historyFrame" style="visibility:hidden;height:0px;width:0px;border:0px">
                </iframe>
            </div>
        </form>
    </body>

</html>

The xap file is indicated, and while it is loading, there is a download indicator: a spinning wheel and a number showing the download percentage depending on the size of the xap file being downloaded: 2 Mb out of 8 downloaded, will show 25%. This is the default behavior – you don’t need to write anything extra.

Everything was fine until one fine day, on which no one touched anything, it itself, the size of the downloaded xap file did not begin to be estimated at 0 bytes. Of course, the file did not become weightless, just, for some reason, when downloading, someone, or something, killed the header with the file size.

On the loading screen, the wheel with the inscription 0% was proudly spinning, these 0% hung for a relatively long time, usually loading took a couple of minutes, and then abruptly 100% …

It took a day to find a solution – it was not possible to solve it with a swoop. On the other hand, it’s a pity to spend time on this: there seems to be no error, so what if the loading bar hangs at zero for a long time – this does not affect the operation of the application in any way, but it’s unpleasant, so they didn’t reset the problem, but decided that it had priority low and it will be solved in free time from other tasks.

A week passed, periodically returned to this problem, but no solution was found.

Some more time passed. And then users began to resent, they say the loading indicator hangs, the cache was cleared, the cookies were cleared, the computer was rebooted, the browser was changed, and it was at zero and did not load anything, and everyone has such a thing. What happened to the application? They explained that so they say and so – no need for fuss, wait and everything will be. Users gained patience, made sure that everything worked, but the sediment remained, and in order not to spread panic, it was necessary to repair the loading indicator.

We returned to the task, a couple more days passed, and we did not find the reason why the xap-file size estimate is zero, and even no considerations on this score remained.

At that moment, we embarked on a slippery slope. Well, the most obvious thing is that users do not complain about the fact that the file size is not determined, but that the download bar is frozen, they don’t care about the file from the high bell tower.

Evil temptation. Instead of an excuse

Yes, the key to solving the problem lay in the plane “return the correct title” and everything will be as it was, but here we have not achieved anything. It is a pity for the time spent – we were not ready to spend it on this “not a mistake” from the very beginning, and when the searches do not lead to a result, but lead to even greater waste – so doubly sorry for the time. As a result, we decided to look for a solution in another plane.

We knew approximately how long the download took (we measured it), it is clear that this value is not constant, it depends on the network, but in a typical scenario, the download fluctuated around two minutes. Accordingly, we needed to write a loading screen that would entertain users during this time. In fact, a little more – just in case with a margin.

Implementation of deception. Instead of hunting for a bug

Fortunately, in Silverlight, the task of customizing the loading screen is typical, it is aimed not at fake screens, but at any embellishment, but one way or another it is easy to google, and there who is pursuing what goals – who is embellishment, who is a fake progress bar. Two parameters need to be added splashscreensource And onsourcedownloadprogresschanged:

<div id="silverlightControlHost">
    <object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
		<param name="source" value="ClientBin/MainApplication.xap"/>
        <param name="splashscreensource" value="LoadScene.xaml" />
        <param name="onsourcedownloadprogresschanged" value="onSourceDownloadProgressChanged" />
		<param name="windowless" value="true" />
		<param name="onError" value="onSilverlightError" />
		<param name="background" value="white" />
		<param name="minRuntimeVersion" value="4.0.50826.0" />
		<param name="autoUpgrade" value="true" />
		<a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=4.0.50826.0" style="text-decoration:none">
 			<img src="https://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight" style="border-style:none"/>
		</a>
	</object>
    <iframe id="_sl_historyFrame" style="visibility:hidden;height:0px;width:0px;border:0px"></iframe>
</div>

The first one is the visual representation, xaml file (LoadScene.xaml):

Loading bar layout

Loading bar layout

The second is a script to handle the download.

Initially, it was just a strip. I don’t know how it happened, but over time, the bar, designed to fill evenly over two minutes, turned into two: one to show the overall progress, the second to show the loading of the “current” module:

Loading screen layout

Loading screen layout

How do we know which module is being loaded and how long it will take? Yes, not from where – this is also a fake. An ordinary array with a list of strings that are supposedly the names of modules. Names are displayed instead of the word “load”.

As a result, the user sees that the bottom bar is the progress of the module, it loads quite quickly, probably this was the expected effect: the system loads individual modules very quickly, and it takes a long time to load because there are a lot of modules.

Below is the XAML markup for the second option:

LoadScene.xaml
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
            
        <Image Grid.Row="0" 
               Source="../images/header/header-left.png" 
               VerticalAlignment="Top"
               Stretch="None" />
        
        <Image Grid.Row="1" Source="../images/back.png" Stretch="UniformToFill" />

        <Grid Grid.Row="1" Grid.ColumnSpan="3">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition MaxWidth="310"/>
                <ColumnDefinition MaxWidth="50"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <Grid Grid.Column="1" HorizontalAlignment="Center" 
                  Width="300" Margin="5">
                <Rectangle Name="progressBarBackground"
                           Fill="White" Stroke="Black"
                           StrokeThickness="1" Height="20" Width="300" />
                <Rectangle Name="progressBar" HorizontalAlignment="Left"
                           Fill="#FF084c7c" Height="18" Width="0" MaxWidth="298" 
                           Margin="1,0,0,0" />
            </Grid>

            <Grid HorizontalAlignment="Center" Width="300" 
                  Grid.Row="1" Grid.Column="1" Margin="5">
                <Rectangle Name="progressBarBackground2"
                           Fill="White" Stroke="Black"
                           StrokeThickness="1" Height="20" Width="300" />
                <Rectangle Name="progressBar2" HorizontalAlignment="Left"
                           Fill="#FF084c7c" Height="18" Width="0" MaxWidth="298"
                           Margin="1,0,0,0" />
            </Grid>

            <TextBlock Grid.Column="2" x:Name="LoadingText" Margin="5"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center"
                       MinWidth="40"
                       Foreground="Black" FontWeight="Normal" 
                       FontFamily="Arial" FontSize="16" Text="0%"/>
            <TextBlock Grid.Row="1" Grid.Column="2" x:Name="LoadingText2" Margin="5"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center"
                       MinWidth="40"
                       Foreground="Black" FontWeight="Normal" 
                       FontFamily="Arial" FontSize="16" Text="0%"/>
            <TextBlock Grid.Row="2" x:Name="MessageText" Margin="5"
                        Grid.ColumnSpan="4"
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        Foreground="Black" FontWeight="Normal" 
                        FontFamily="Arial" FontSize="12" Text="Загрузка"/>
        </Grid>
    </Grid>
</Grid>

Here we have four squares: two for the top progress bar (progressBarBackground, progressBar), two for the bottom.

one square each progressBarBackground And progressBarBackground2 – represent an empty unfilled progress bar, and one more progressBar And progressBar2 change their width as they “load” and thus illustrate the movement of the progress bar.

There are also several text blocks for displaying the number in percentage and the name of the executable module.

Actually, to implement the progress animation, you need to change the width of progressBar And progressBar2well, change the inscriptions periodically.

For all this, it is necessary to implement onSourceDownloadProgressChangedback to the aspx file:

<script type="text/javascript">

	var id = 0;

	var diff = ["Загрузка модуля справочников", "Загрузка модуля отображения информации", "Загрузка атрибутивных данных", "Загрузка модуля редактирования", "Формирование списка документов", "", ""];
	var i = 0;

	function onSourceDownloadProgressChanged(sender, eventArgs)
	{
		var val = sender.findName("progressBar").Width / sender.findName("progressBarBackground").Width;
		if (eventArgs.progress > val)
		{
			sender.findName("LoadingText").Text = Math.round((eventArgs.progress * 100)) + "%";
			sender.findName("progressBar").Width = eventArgs.progress * sender.findName("progressBarBackground").Width;

			if (eventArgs.progress >= 1 / 4 * (i + 1) || eventArgs.progress >= 0.98) {
				sender.findName("LoadingText2").Text = "100%";
				sender.findName("progressBar2").Width = sender.findName("progressBarBackground2").Width;
			}
		}

		if (id === 0)
		{
			sender.findName("MessageText").Text = diff[i];

			id = setInterval(function() {
				var rel = sender.findName("progressBar").Width / sender.findName("progressBarBackground").Width;
				rel += (Math.random() * 2 + 2) / 100;
				if (rel <= 0.96) {
					sender.findName("LoadingText").Text = Math.round((rel * 100)) + "%";
					sender.findName("progressBar").Width = rel * sender.findName("progressBarBackground").Width;
				}
			}, 3500);

			setInterval(function ()
			{
				var rel1 = sender.findName("progressBar").Width / sender.findName("progressBarBackground").Width;
				var rel2 = sender.findName("progressBar2").Width / sender.findName("progressBarBackground2").Width;
				rel2 += (Math.random() * 2 + 2) / 100;

				if (rel1 >= 0.96) {
					sender.findName("progressBar2").Width = sender.findName("progressBarBackground2").Width;
					sender.findName("LoadingText2").Text = "100%";
				}
				else if (rel2 >= 1) {
					sender.findName("progressBar2").Width = 0;
					sender.findName("LoadingText2").Text = "0%";
					i++;
				} else {
					sender.findName("LoadingText2").Text = Math.round((rel2 * 100)) + "%";
					sender.findName("progressBar2").Width = rel2 * sender.findName("progressBarBackground2").Width;
				}
				sender.findName("MessageText").Text = diff[i];

			}, 500);
		}
	}

</script>

What can you pay attention to here, firstly: diff is a fake list of loadable modules, and i is the index of the currently loaded module.

And secondly: on the function onSourceDownloadProgressChanged, in a normal scenario – if the file size comes in correctly, it is called with some periodicity and its parameters contain what proportion of the file has already been downloaded, so we can use this for honest visualization. However, in our case, the function is called only twice: at the very beginning, when 0 is loaded, and at the very end, when 100% is loaded.

This code:

var val = sender.findName("progressBar").Width / sender.findName("progressBarBackground").Width;
if (eventArgs.progress > val)
{
	sender.findName("LoadingText").Text = Math.round((eventArgs.progress * 100)) + "%";
	sender.findName("progressBar").Width = eventArgs.progress * sender.findName("progressBarBackground").Width;

	if (eventArgs.progress >= 1 / 4 * (i + 1) || eventArgs.progress >= 0.98) {
		sender.findName("LoadingText2").Text = "100%";
		sender.findName("progressBar2").Width = sender.findName("progressBarBackground2").Width;
	}
}

Written just in case, so that there are no overlaps if the error with determining the file size disappears as suddenly as it arose.

In this case, the code should try to plausibly coordinate fake and real progress. At least so that it is not very conspicuous and our deception is not revealed.

This did not happen, but I expect that under such a combination of circumstances, the bar will fill up smoothly, as in a fake algorithm, and then jerkily, when the real download progress starts to overtake the fake one.

The module loading bar will also start to move in jerks due to the condition in lines 7 – 10. Its essence is that if we have loaded 25% of the total size, then we should not show that the first module is loading, but write about the second – with the first end. If the overall progress exceeded 50%, then the second module should also be stopped loading, showing that it is 100% loaded and moving on, etc. at the rate of 25% per module, we will show four modules and that’s enough.

Well, if the overall progress approaches 100%, then the module currently being loaded should also pretend to be fully loaded.

One listing up, line 22 has a condition

if (id === 0)

Made for the same purposes – in case the function starts to be called correctly. If the condition is not checked, then a lot of cycles will start in setInterval and the loading bar will move very fast, reach 100% and freeze for a couple of minutes.

I think this is what sets our fake loading bar apart from most other fakes: we have provided an adjustment for real progress.

Now about the intervals themselves. There are two of them.

First:

id = setInterval(function() {
	var rel = sender.findName("progressBar").Width / sender.findName("progressBarBackground").Width;
	rel += (Math.random() * 2 + 2) / 100;
	if (rel <= 0.96) {
		sender.findName("LoadingText").Text = Math.round((rel * 100)) + "%";
		sender.findName("progressBar").Width = rel * sender.findName("progressBarBackground").Width;
	}
}, 3500);

Every 3.5 seconds changes the overall progress bar by a random amount from 2 to 4 percent. It freezes at 96% and pretends that there is just a little bit left, but it froze at some difficult operation, after which it is immediately 100% and the application is launched. Usually the download was completed before it reached 96%.

Second:

setInterval(function ()
{
	var rel1 = sender.findName("progressBar").Width / sender.findName("progressBarBackground").Width;
	var rel2 = sender.findName("progressBar2").Width / sender.findName("progressBarBackground2").Width;
	rel2 += (Math.random() * 2 + 2) / 100;

	if (rel1 >= 0.96) {
		sender.findName("progressBar2").Width = sender.findName("progressBarBackground2").Width;
		sender.findName("LoadingText2").Text = "100%";
	}
	else if (rel2 >= 1) {
		sender.findName("progressBar2").Width = 0;
		sender.findName("LoadingText2").Text = "0%";
		i++;
	} else {
		sender.findName("LoadingText2").Text = Math.round((rel2 * 100)) + "%";
		sender.findName("progressBar2").Width = rel2 * sender.findName("progressBarBackground2").Width;
	}
	sender.findName("MessageText").Text = diff[i];

}, 500);

The second interval controls the loading bandwidth of the module. If the main loading bar is stuck at 96%, then we pretend that the current module is 100% loaded, but we don’t go to the next module, even if there is something else in the list. And so it remains.

In other situations, we smoothly reach 100%, increase i by one – getting the “next module” from the array, reset the module loading progress bar to 0, and start all over again.

The loading of the “module” is 7 times faster than the “general” loading, therefore, just in case, it is necessary to have 7 elements in the array, it will not go beyond the array boundary. when the overall progress reaches 96% – we stop incrementing the variable i. Although this does not seem reliable to me now, it would be better to do an additional check on the value ianyway.

That’s the whole implementation.

Conclusion. Instead of repentance

Thus, we fool the user for his own money. And it is not difficult to deceive him! He is happy to be deceived! And this is not a figure of speech, I don’t remember verbatim, but the desire of the collective user was formulated something like this: “Do at least something so that we see that the application does not freeze, and roughly imagine how much more is left to wait.”

From this point of view, we achieved what the user wanted, the application even loaded faster than the progress bar promised, as a rule, the download was already 70-80% complete – a nice bonus for your expectation. Well, no one else reloaded the page believing that it was frozen. Even if it hung at 96%, it is unlikely that someone would have pressed F5, because there was one last jerk left and the download could end at any moment.

If you are reading this as a user, don’t be surprised that sometimes the loading bar is really not what it seems. But I believe that in the depths of your soul you yourself understood this a long time ago, and are even ready to put up with it, and moreover, are ready to forgive us – those who fake the loading screen, because it is almost always a lie for good.

If you’re reading this as a developer, spoofing a loading screen is okay, and sometimes necessary.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *