What I want is the following:
- Preload images (up to 40-50), and do callback (e.g. alert) when this is done (this part is already fine I think)
- Main point: display images one after the other in the same
div
, recording display time as precisely as possible (also, any image may be displayed multiple time) - Display the same images in another
div
(this should be fairly easy if I have point 2)
For more context: this is a response time experiment where online participants see the pictures and make key responses to them (and their response time is measured). (Also, there is at least 200-500 ms between displays, so it is no problem if the preparation takes a little time before each display.)
I already have a full working code for this, which I put together from various previous answers: could someone verify that my code makes sense, and that there is nothing else I could do to make it more efficient?
One particular thing that confuses me is what "appendChild" exactly does in the browser. (In other words: I get that the element will be added to the page, but I don't know what that means for the browser.) Currently I'm appending the image just a little (100 ms) before I actually display it, which I then achieve by setting opacity from 0 to 1 (which is presumed to be the most optimal method for precise timing). Then I remove the image (empty the div
) before displaying the next image. But I wonder if there is any point to doing it like that. For example, what if I appended all images already in the "preloading phase" and just set their opacity to 0 whenever they are not needed? How does that affect performance? Or otherwise, what if (though I'd rather not do this) I append them right before the opacity change? Does "appending" require any time, or can it in any way affect the timing of the upcoming opacity change? (EDIT: Now I have the answer to the main question, but it would still be nice to get an explanation about this point.)
The obvious problem is that I cannot really measure the display time precisely (without external hardware), so I have to rely on what "seems to make sense".
In any case, below is my code.
var preload = ['https://www.gstatic.com/webp/gallery/1.jpg', 'https://www.gstatic.com/webp/gallery3/1.png', 'https://www.gstatic.com/webp/gallery/4.jpg', 'https://www.gstatic.com/webp/gallery3/5.png'];
var promises = [];
var images = {};
// the function below preloads images; its completion is detected by the function at the end of this script
for (var i = 0; i < preload.length; i++) {
(function(url, promise) {
var filename = url.split('/').pop().split('#')[0].split('?')[0];
images[filename] = new Image();
images[filename].id = filename; // i need an id to later change its opacity
images[filename].style.opacity = 0;
images[filename].style.willChange = 'opacity';
images[filename].style['max-height'] = '15%';
images[filename].style['max-width'] = '15%';
images[filename].onload = function() {
promise.resolve();
};
images[filename].src = url;
})(preload[i], promises[i] = $.Deferred());
}
// the function below does the actual display
function image_display(img_name, current_div) {
window.warmup_needed = true;
document.getElementById(current_div).innerHTML = ''; // remove previous images
document.getElementById(current_div).appendChild(images[img_name]); // append new image (invisible; opacity == 0)
chromeWorkaroundLoop(); // part of the timing mechanism
setTimeout(function() {
document.getElementById(img_name).style.opacity = 1; // HERE i make the image visible
requestPostAnimationFrame(function() {
window.stim_start = now(); // HERE i catch the time of display (image painted on screen)
warmup_needed = false;
});
}, 100); // time needed for raF timing "warmup"
}
// below are functions for precise timing; see https://stackoverflow.com/questions/50895206/
function monkeyPatchRequestPostAnimationFrame() {
const channel = new MessageChannel();
const callbacks = [];
let timestamp = 0;
let called = false;
channel.port2.onmessage = e => {
called = false;
const toCall = callbacks.slice();
callbacks.length = 0;
toCall.forEach(fn => {
try {
fn(timestamp);
} catch (e) {}
});
};
window.requestPostAnimationFrame = function(callback) {
if (typeof callback !== 'function') {
throw new TypeError('Argument 1 is not callable');
}
callbacks.push(callback);
if (!called) {
requestAnimationFrame((time) => {
timestamp = time;
channel.port1.postMessage('');
});
called = true;
}
};
}
if (typeof requestPostAnimationFrame !== 'function') {
monkeyPatchRequestPostAnimationFrame();
}
function chromeWorkaroundLoop() {
if (warmup_needed) {
requestAnimationFrame(chromeWorkaroundLoop);
}
}
// below i execute some example displays after preloading is complete
$.when.apply($, promises).done(function() {
console.log("All images ready!");
// now i can display images
// e.g.:
image_display('1.jpg', 'my_div');
// then, e.g. 1 sec later another one
setTimeout(function() {
image_display('5.png', 'my_div');
}, 1000);
});
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
<div id='my_div'></div>