我想要的是以下内容:
div
中一个接一个地显示图像,并尽可能精确地记录显示时间(而且,任何图像都可以显示多次)div
中显示相同的图像(如果我有第2点的话,这应该很容易)更多背景信息:这是一个响应时间实验,在线参与者可以看到图片并对其做出关键响应(并测量他们的响应时间)。 (此外,显示器之间至少要有200-500毫秒的间隔,因此在每次显示之前准备工作都需要一点时间也是没有问题的。)
我已经有完整的工作代码,可以将它与以前的各种答案结合在一起:有人可以验证我的代码是否有意义,还有什么我可以做的才能使它更有效?
令我困惑的一件事是“ appendChild”在浏览器中的确切作用。 (换句话说:我知道该元素将被添加到页面中,但是我不知道这对浏览器意味着什么。)目前,我在实际显示图像之前将图像附加了一点(100毫秒) ,然后将不透明度从0设置为1(which is presumed to be the most optimal method for precise timing)。然后,在显示下一张图像之前,删除该图像(空div
)。但是我想知道这样做是否有任何意义。例如,如果我添加了所有已经在“预加载阶段”中的图像,并且只要不需要它们就将其不透明度设置为0,该怎么办?这如何影响性能?否则,如果(不希望这样做)在不透明性更改之前添加它们怎么办? “追加”是否需要任何时间,或者是否可以以任何方式影响即将发生的不透明性更改的时间? (编辑:现在我有了主要问题的答案,但是得到关于这一点的解释仍然很好。)
一个明显的问题是,我无法真正精确地测量显示时间(没有外部硬件),因此我不得不依靠“似乎有意义”的东西。
无论如何,下面是我的代码。
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>
答案 0 :(得分:2)
一个问题是,onload
官方仅告诉我们有关网络状态(+元数据解码)的信息。
在将浏览器呈现到屏幕上之前,浏览器可能还需要执行一些其他操作,例如将其完全解码为可以移动到GPU的位图,从而最终呈现到屏幕上。 / p>
通过使用同步drawImage
方法在画布上绘制图像,至少可以在Firefox中看到 ;对于大图像,此drawImage()
调用即使在代码运行后也可能需要几毫秒,这表明它们尚未完全解码:
var preload = ['https://upload.wikimedia.org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg'];
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].onerror = promise.reject;
images[filename].src = url;
})(preload[i] + '?r='+Math.random(), 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;
const c = document.createElement('canvas');
c.width = document.getElementById(img_name).width
c.height = document.getElementById(img_name).height;
console.time('paint1'); c.getContext("2d").drawImage(document.getElementById(img_name),0,0);
console.timeEnd('paint1');
console.time('paint2'); c.getContext("2d").drawImage(document.getElementById(img_name),0,0);
console.timeEnd('paint2');
// 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('Black_hole_-_Messier_87.jpg', 'my_div');
}).catch(console.error);
<h4>run this snippet in Firefox</h4>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
<div id='my_div'></div>
在我的FF上我得到
所有图像都准备好了!
paint1:665.000ms
paint2:0.000ms
这意味着,有了这个巨大的图像,在您开始测量我的响应时间后,屏幕将在650ms内保持空白。
在HTMLImageElement接口中添加了一个.decode()
方法,该方法应该使我们更接近绘制时间,但是如果您正确阅读了此答案的介绍,那么我们已经找到了更好的解决方案:
CanvasRenderingContext2D.drawImage()
是同步的。
与其呈现不同的<img>
,而是冒着完整的页面重排,触发CSS转换或其他可能延迟图像显示,停留在较低级别,使用单个可见DOM元素并使用同步的风险的风险绘画方法。换句话说,使用HTMLCanvasElement 。
var canvas = document.getElementById('mycanvas');
var ctx = canvas.getContext('2d');
// modified version of image_display function
function image_display(img_name, current_div) {
window.warmup_needed = true;
chromeWorkaroundLoop(); // part of the timing mechanism
setTimeout(function() {
// clear previous
ctx.clearRect( 0, 0, canvas.width, canvas.height );
// request the painting to canvas
// synchronous decoding + conversion to bitmap
var img = images[img_name];
// probably better to use a fixed sized canvas
// but if you wish you could also change its width and height attrs
var ratio = img.naturalHeight / img.naturalWidth;
ctx.drawImage( img, 0, 0, canvas.width, canvas.width * ratio );
// at this point it's been painted to the canvas,
// remains only passing it to the screen
requestPostAnimationFrame(function() {
window.stim_start = performance.now();
warmup_needed = false;
});
}, 100); // time needed for raF timing "warmup"
}
// preloading is kept the same
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());
}
// 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);
});
canvas {
max-width: 15%;
max-height: 15%
}
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
<canvas id="mycanvas" width="500" height="500"></canvas>
现在提防,Web浏览器远非完美的工具,可以帮助您完成所要执行的操作。与响应时间准确性相比,他们更喜欢UI响应性和低内存使用率。