0

I am trying to create a test that explores the boundaries of our subconscious. I want to briefly display a number and see if the user can use their intuition to guess the value - is their subconscious able to read the number faster than their conscious self. So I am trying to flash a number onto the screen for a few milliseconds. Chrome does not seem to behave as well as Edge in with this code. How can I make it work more consistently across browsers?

I have tried various ways of hiding and revealing the number. Finally ending up with this version.

<script>

function onLoad() {
    numberOfPoints = Math.floor(Math.random() * (99 - 9 + 1)) + 9;
    document.f.points.value = numberOfPoints;
    setTimeout(hideRun, 3000);
}

function hideRun() {
    hide();
    document.getElementById("hiddenNumber").innerHTML = numberOfPoints;
    document.getElementById("hiddenNumber").style.display = 'block';
    setTimeout(hide, 5);
}

function hide() {
    document.getElementById("hiddenNumber").style.display = 'none';
}

</script>

<body onload="onLoad()">
    <div id=hiddenNumber style="display: block;">GET READY</div>
</body>

In this case I am hoping to display the Get Ready text for 3 seconds, then show a random number for 5 milliseconds. Although I have no way to actually measure it, the 5 milliseconds on a chrome browser is a lot longer than with the Edge browser.

You can try it yourself here: Test Timer

Aᴍɪʀ
  • 7,623
  • 3
  • 38
  • 52
Melchester
  • 421
  • 3
  • 18
  • 1
    `setTimeout` is not reliable under 12ms or so, IIRC. I don't know if there's a more elegant way, but one possibility would be to *block* for a few ms, after the change has rendered, and then `hide()`. – CertainPerformance Jan 07 '19 at 05:05
  • A reference document for the aforementioned `setTimeout` limits... http://www.adequatelygood.com/Minimum-Timer-Intervals-in-JavaScript.html – tresf Jan 07 '19 at 05:35

2 Answers2

2

Thinking in terms of time is not reliable here, because you don't know when the browser will paint to screen, nor when the screen will do its v-sync.
So you'd better think of it in term of frames.

Luckily, we can hook callbacks to the before-paint event, using requestAnimationFrame method.

let id = 0;
btn.onclick = e => {
  cancelAnimationFrame(id); // stop potential previous long running
  let i = 0,
    max = inp.value;
  id = requestAnimationFrame(loop);
  
  function loop(t) {
    // only at first frame
    if(!i) out.textContent = 'some dummy text';
    // until we reached the required number of frames
    if(++i <= max) {
      id= requestAnimationFrame(loop);
    }
    else {
      out.textContent = '';
    }
  }
};
Number of frames: <input type="number" min="1" max="30" id="inp" value="1"><button id="btn">flash</button>
<div id="out"></div>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • With Google Chrome using the above code snippet with a value of `1`, I still only see about 75% success. About 1/4 of the time, it doesn't blink any text. – tresf Jan 07 '19 at 05:30
  • 1
    @tresf interesting, I also experience it on Chrome (wrote it on FF), but only once in twenty. Setting it to two frames always work though... But there is something fishy in Chrome's behavior I agree, however, this is *per specs*, the most reliable way we have. I tried using a canvas in case it was the CSS painting that got delayed somehow (e.g by Houdini's incoming) but it does the same... They do fire rAF callback without triggering a repaint, or too late. – Kaiido Jan 07 '19 at 05:34
  • Your `1:20` may be behavioral (as may be my `3:4`). If you click at a consistent speed, the shutter speed of clicking versus refreshing could be accidentally syncing. If you haven't already, try clicking at random. Also, my hardware is Apple, running on a battery. Not sure if these things can influence the browser's natural refresh . – tresf Jan 07 '19 at 05:38
  • I am also on an Apple computer here, but anyway, [per specs](https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model) the requestAnimationFrame callbacks should fire in the same event loop than the one responsible for painting. If there is no painting in my code, that is a browser bug. I might open an issue when I get time. – Kaiido Jan 07 '19 at 05:41
  • How do you know that the `textContent = ''` always occurs on a subsequent frame (and not the previous one?) – tresf Jan 07 '19 at 05:52
  • @tresf `if(++i <= max) {}else{//here we already did at least one complete loop}` – Kaiido Jan 07 '19 at 05:54
  • Yes, I understand the loop, but where in the specification does it guarantee that calling it twice in a row very quickly can't produce the same frame? – tresf Jan 07 '19 at 05:56
  • 1
    I do cancel any running loop at the beginning of the function (`cancelAnimationFrame(id);`). And fyi, callbacks are stacked in rAF, so if you do `requestAnimationFrame(fn); requestAnimationFrame(fn);` `fn` will get called twice in the same frame. – Kaiido Jan 07 '19 at 05:58
  • So I'll ask again, how do you know `textContent = ''` occurs on a subsequent frame (and not the previous one)? Does calling `requestAnimationFrame` again from within a callback guarantee some type of next frame? If not, the behavior we're observing could very well match the "twice in the same frame" scenario. – tresf Jan 07 '19 at 06:11
  • 1
    @tresf yes, we are adding the requestAnimationFrame from within the requestAnimationFrame's callback executor, so calling it there indeed guarantess that you will wait a full frame before callback gets executed. [Here](https://stackoverflow.com/questions/54047609/how-do-i-obtain-pixel-data-from-a-canvas-i-dont-own/54051000#54051000) I recently gave a more in depth explanation of how rAF works if you wish to read it. And [here is a fiddle](http://jsfiddle.net/v8tb19p2/) I just prepared for Chrome's issue, a bit simpler than the current snippet. – Kaiido Jan 07 '19 at 06:15
  • Thank you for your help. I am (clearly) a little out of my depth here. If I use "frames" as you suggest does that mean the absolute minimum amount of time that I can show the message is one sixtieth of a second? I need to be able to flash the message faster than it can be read - which is a lot faster than 1/60 th. I thought that I was achieving that, because at low durations i could not see anyting, but perhaps it was simply not being shown. – Melchester Jan 07 '19 at 08:24
  • @Melchester yes, 1/60th of a second is the fastest you will ever get on a 60Hz display (which is a standard). Some high-end screens are able to render at higher frequencies, but whether the browser will is not sure at all... – Kaiido Jan 07 '19 at 08:28
  • Actually, after testing on a dual-display setup with one low-end screen (50hz), Chrome does slow down rAF to 50FPS when the window is on the low-end display ;-) So they might very well speed up when on an higher-end display (that I don't have at hand though...) – Kaiido Jan 07 '19 at 08:36
0

Can you try a 2D canvas and see if that helps?

<html>
<head>
<script>
var numberOfPoints;

var canvas;
var context;

function onLoad() {
  canvas = document.getElementById("myCanvas");
  context = canvas.getContext("2d");
  context.font = "30px Arial";
  // context.fillText("...", 10, 50);
  numberOfPoints = Math.floor(Math.random() * (99 - 9 + 1) ) + 9;
  setTimeout(hideRun, 3000);
}
function hideRun() {
  context.fillText(numberOfPoints, 10, 50);
  setTimeout(hide, 5);
}
function hide() {
  context.clearRect(0, 0, canvas.width, canvas.height);
}
</script>
</head>
<body onload="onLoad()">
<canvas id="myCanvas"></canvas>
</body>
</html>

In my tests, it seems to show the number more consistently versus the CSS property, but to be absolutely sure, I would recommend a 60fps screen reader to record and validate the cross-browser accuracy.

tresf
  • 7,103
  • 6
  • 40
  • 101
  • 1
    Thanks for your suggestion. My first attempt, which I should have mentioned, was using canvas, but that seemed to be slower. So I tried to keep it as simple as possible. – Melchester Jan 07 '19 at 08:17