[Snowman] A practical way of preloading images

My current project has a lot of images and it would be pretty ugly if an image takes a few seconds to load after the passage appears (the “delayed display problem”), which will happen if the story is played online.

My solution was loading all the images and hiding them with CSS in the initial passages. It helps, but the player may go to the next passage before they finish loading and some image will be missing, creating the delayed display problem in some later passage.

The ideal solution, I thought, would be using JS to detect when all the images in a passage have been downloaded, and not allowing the player to exit the passage until that. I found two JS scripts in Github (this and this) that do that, but both failed and my JS is way too limited to understand why.

Would you have any suggestion for this? Thanks!

I don’t know anything about Snowman or Twine, but here are some things you could try.

A lot of web games load everything before the game even starts. You could probably find some image loader library that loads everything and then fires some callback, and you could launch your game from that.

If you want to preload things on the fly, try only preloading the things that will be on whatever screens the user can be on next (maybe this is what you’re already doing). The browser will only load so many different things at once, so you could get stuck loading things the player won’t see until later, while they’re waiting for something that’s supposed to be on the screen already.

You could also save images as interlaced, so there won’t be ugly blank space when you first run into them. If that’s good enough you might not need preloading at all, unless your visitors are on satellite internet or dialup modems or something.

Also, make sure your images are as lightweight as possible. Sounds obvious, but it’s easy to forget. If they’re photographic images (JPEG), crank the quality down as much as you can to where it still looks good. If they’re illustrations (PNG), try running pngcrush on them, or pngquant/pngnq if they don’t have a lot of color variation. Don’t use JPEG for illustrations or PNG for photographs, and make sure they’re not being scaled down in browser.

The total disk cache for browsers is usually around 50 MB, and files larger than about 5 MB won’t get sent to disk cache. Those numbers are for Firefox on desktop, not sure about others. If you can crunch the images enough, you can take advantage of that for frequent visitors. Memory cache is much larger, obviously.

@cpb is absolutely correct. Start with preloading images and act after everything has been loaded.

For Snowman specifically, here is some code using the Preload-It code you linked to, @pseudavid.

I’ll include it all in Twee code and then discuss what is going on in each step.

:: StoryTitle
Snowman: Preloading Images

:: UserScript[script]

// Use jQuery's getScript() function to get and load a remote JS file
$.getScript( "https://unpkg.com/preload-it", function( data, textStatus) {
  
	// Prepare the loaded Preload library
	const preload = Preload();
	
	// Define what should happen when complete
	preload.oncomplete = function(items) {
		
		// Create an empty 'images' object
		window.images = {};
		
		// Add a property to window.images
		window.images = items;
		
		// Show() the correct starting passage
		window.story.show("True Beginning");
	};
  
	// Finally, fetch the files
	preload.fetch([
    'https://images.pexels.com/photos/248797/pexels-photo-248797.jpeg'
	]);
	
});

:: Start
Loading...

:: True Beginning
Images have loaded and now we can work!

<img src="<%= window.images[0].url %>" >

Using getScript()

Using jQuery’s getScript() function. The jQuery library is available in Snowman. It can be used anywhere and, because these images need to be loaded before anything else, putting this code in the Story JavaScript section makes the most sense.

The function can take multiple arguments, but we care about two of them: what to load and what should happen after it loads. In this example, the source “https://unpkg.com/preload-it” is loaded. jQuery retrieves this source and then executes it, loading and creating anything the script tells it to do.

In the “what should happen after loading argument,” there is a function. This helps us define that callback @cpb was talking about. The callback here waits for the script to be loaded and then runs its own code.

Using Preload-It

Based on the exact instructions from Preload-It, the code looks roughly like the following:

const preload = Preload();

preload.oncomplete = function(items) {
  ..
};

preload.fetch([
   ...
])

(For those not as knowledgeable about JavaScript ES6, Preload-It uses arrow functions for things. I’ve converted them back into “normal” ES5 functions.)

Using this library, we prepare it, define what should happen after it fetches, and then, finally, actually fetch a file.

Working with Snowman

You didn’t include which version of Snowman you are using, @pseudavid, so I wrote code that will work across 1.3, 1.4, and 2.0.2.

It starts by defining a new global property, window.images. Defining this allows us to load all the images and then access them in future passages.

Next, it calls window.story.show(). (I’ll link to the 2.X branch documentation here.) By supplying the name of a passage, the function immediately “goes” to that passage, replacing the current passage’s contents.

In my example, the first, starting passage (called “Start”) has text that reads Loading.... It will show this until the call to the function window.story.show() when its contents are replaced with the contents of the passage “True Beginning”.

Splitting it up like this allows for adding in instructions, details, or just a loading message in one passage and then having its contents be replaces when the game finally loads everything.

Displaying Loaded Images

Finally, images are displayed using their url property of each entry in the now window.images array.

Using value interpolation, the Underscore template system can write a variable’s value into a passage. In this example, the code is the following:

<img src="<%= window.images[0].url %>" >

It write the url property of the first (0) entry in the window.images array to the passage and then, when the browser sees the HTML code, it would see it as the following:

<img src="https://images.pexels.com/photos/248797/pexels-photo-248797.jpeg">

Moving Beyond the Example

To extend this example, list all of the images to fetch in the preload.fetch() function call and then, to use them in passages, write their values through where they are in the array. That is, window.images[X].url depending on which number they are in the array sequence.

Depending on the number and size of the images, as cpb mentioned, this could be consuming at first. However, once loaded, displaying them will be very fast.

As I mentioned, and many, many games do, including something for the player to read or do as the loading is going on can “hide” the experience. You could just as easily add a link to the starting passage that allows them to continue only after all the loading is finished. That’s a very common game design solution.

For what it’s worth, my Universal Inventory System (UInv) for Twine/SugarCube includes an image caching system. You can take a look at the UInv cacheImages() function documentation if you want to get an idea of how to use it.

The UInv image cache loader defaults to loading only 5 images at a time from the queue, so you won’t flood the bandwidth trying to download too many files at the same time, and it also lets you set a maximum number of images cached. You can tweak those settings by modifying the setup.UInvImageCache object.

If you wanted to pull the image cache code out of UInv and put it into your code, feel free. (Any of the UInv “utility functions”, which all start with a lowercase letter, can be safely extracted from UInv.)

1 Like

Wow, this is amazing, thank you for all this! Based on your responses I finally got one of the scripts working.

I can’t stress how amazingly helpful the Twine community has been for years.

2 Likes

Hello! I tried this and it gave me an error, but I’m sure I’ve left something out.
I have this in my StoryInit passage:

<<script>>
$(document).one(':passageend', function (ev) {
	var path = "images/";
	var images = ["/* I have 52 images, but they're quite small */"];
	var CacheWait = setInterval(function () {
		if ($("#init-screen").css("display") == "none") {
			clearInterval(CacheWait);
			UInv.cacheImages(path, images, CacheEvent);
		}
	}, 300);
});
<</script>>

And it returns this error: “UInv is not defined.”

The error you are seeing indicates that the UInv JavaScript object/module doesn’t exist.

How are you loading/initialising the variation of the Universal Inventory System (for SugarCube) JavaScript library (the UInv object/module) you are referencing in your calling of the UInv.cacheImages() function?

Did you extract the image caching code from UInv (see the UniversalInventorySystem.js file) and put it in your Snowman code like I suggested above? If you did, then how you’ll need to call the cacheImages() function depends on how you extracted that code.

Or are you using this in SugarCube? In that case you could use the whole UInv object. You just need to copy the content of the UniversalInventorySystem.js file to the bottom of your JavaScript section, and then your code should work. (See the Getting Started section of the UInv help file for further details.)

Hope that helps! :slight_smile:

I thought there was something I needed to put in the JavaScript section, but haven’t found out what yet. That’s mainly why I asked here. :slightly_smiling_face:
I looked up the Universal Inventory System, downloaded a zip, looked in what I think were the instructions, but still didn’t find anything.

Thanks! I copied the whole contents of UniversalInventorySystem.js to the bottom of the JavaScript section (by the way, the Getting Started link is a 404 at least on my end). The error is different now: it says, “CacheEvent is not defined.” :slightly_smiling_face:

Whoops! Link is fixed now.

FYI - That’s just a link to the “Getting Started” section of the online version of the “UInv_Help_File.html”, which is included with UInv.

Ah, sorry, I didn’t notice that you used that parameter. If you don’t need a function which handles cache events, just remove that parameter, so the code is just: UInv.cacheImages(path, images);

If you do want to write a cache event handler, see the cacheImages() function documentation for instructions on ways to do that.

Enjoy! :slight_smile:

No errors now! However it doesn’t seem to change how long it takes an image to show up. The image I was testing with was only 14 kb, so it shouldn’t have taken any time to display at all even without preloading. There was even an image that was less than 1 kb that was almost taking a whole second to display. It takes less time to double click on it in my files and have it open in the image viewer.

I found that if I put the image in the passage, off where it can’t be seen, so that it loads with the passage, then it displays immediately enough in response to the click. The reason I was wanting to preload images was because I have some images displayed by <<replace>> along with a sound when you click on something, but if the image doesn’t display immediately it gets offset from the sound, which looks bad. It worked perfectly testing it from the Twine program, but when I uploaded the game to Itch.io it was offset completely.
But the method of just putting in the image out of site before where you need it to display seems to work even if its two passages previous.
I keep them out of sight by wrapping them in a div and adding a tower of &nbsp; above it. It may not be elegant, but it seems to work well.

Thank you anyway! I’m not sure what Itch’s problem was, so I don’t know why the image preloader wouldn’t work.

Some web-browsers are smart enough not to load images that aren’t being displayed within the visible area of the view-port, which means your trick won’t always work.

Do you think it would work if I put them behind another image, or would it detect that too?

Well, let’s check to see if it was actually able to load the images.

First, create a “StoryCaption” passage with this in it:

Waiting: <span id="waitCnt">0</span>
Loaded: <span id="loadCnt">0</span>
Failed: <span id="failCnt">0</span>

Then add this to your JavaScript section:

setup.CacheEvent = function (Image) {
	$("#waitCnt").empty().wiki(setup.UInvImageCache.waiting);
	$("#loadCnt").empty().wiki(setup.UInvImageCache.loaded);
	$("#failCnt").empty().wiki(setup.UInvImageCache.errors);
}

and also put setup.CacheEvent in as the third parameter to the UInv.cacheImages() call.

If the “failed” number is going up, but the “loaded” number doesn’t, then then you’re probably passing the filename or the path incorrectly.

Let us know what you find out.

I had to use PassageHeader because the game doesn’t use the ui bar; all the numbers stayed zero, I’m not sure what that means.

I found that it works fine if I include the images in StoryInit (all along it would work fine after you had played it once; all the images together are about 2 mb).

That’s my fault. I forgot you had to set the third parameter as a string, so it should have been like this:

UInv.cacheImages(path, images, "CacheEvent");

That should make it work if you want to check it.

Anyways, glad it sounds like you got the caching to work.

Oh, sorry this took so long! I thought I had clicked Reply when I hadn’t.

I went ahead and tested it: the Failed number went to 1 on the first passage, then went back to 0. Everything else remained 0.

Then that tells me that you’re doing something wrong and the images are failing to load. Probably due to a mistake in the line where you’re creating the array of images.

You might want to open up the console window to see what error messages are there.

If you can’t figure it out, then make sure you also mention whether the HTML file is online or not, whether the images are online or not, and if you’re playing it from Twine or a published HTML file or not.

Also, if you’re using JavaScript like that, there’s not much point in putting it inside a <<script>> macro inside StoryInit, when you could have the exact same thing in your JavaScript section, but without needing the macro part.

So, I found the problem (which you would have seen immediately no doubt if I had included the array of images when I first brought up the problem; the only reason I didn’t was because it was so large - lesson learned: include all parts or at least examples or something).
I was putting the whole string in quotes instead of the individual images ([“img.png, img.png”] instead of [“img.png”, “img.png”]). Fixed that, now it works of course! That also explains why the Failed number only went to one when I tested it.

So yes, the Loaded number went up this time, accounting for all the images, except for one, which failed - I couldn’t tell which one that was, but the console gave the same error as when my array was messed up:
Failed to load resource: the server responded with a status of 403 ()

Sorry for all the confusion I caused by not including the array in my first question! :sweat_smile: