2. Capturing keys and reaction times
2.1 Simplified script for a lexical decision task
In this chapter, let’s create a script for a lexical decision task. We need a list of words and a list of non-words. Words and non-words are mixed and presented in random order and at each presentation the user must indicate whether it is a word or a non-word. This must be done accurately and as fast possible. Typically, if a word is detected, the user must press the S-Key, and if a non-word is detected the L-Key. In the first version of the example script, we will use only five words and five non-words, to make it easier to follow. The first version does not yet contain all the details, but provides a good starting point from which we can develop a more complete script:
1 var words = ['apple','table','grass','bike','sand'],
2 nonwords = ['aplap','blatle','rasag','kibe','snad'],
3 i,
4 stimuli = [],
5 e;
6
7 stimuli = stimuli.concat(words,nonwords); // Combine two arrays into one new one
8 stimuli = shuffle(stimuli); // Randomize the word/non-word order
9
10 // Instructions come here
11
12 for (i = 0; i < stimuli.length; ++i)
13 {
14 text(stimuli[i],300); // show a word or non-word at 300% size
15 e = awaitkey('s,l');
16 // Wait until either the S-key or the L-key has been pressed. If so...
17 clear(); // Remove word from screen
18 await(1000); // Pause for 1 s
19 }
20
21 // feedback/debriefing comes here
The first thing that is new here is that we have two arrays, words and nonwords, and a third empty array, called stimuli. The following statement adds (copies) the contents of words and nonwords to stimuli:
7 stimuli = stimuli.concat(words,nonwords);
Now stimuli contains a copy of the words and non-words. The word concat stems from to concatenate, meaning to join or chain together. To randomize the order of words and non-words, we use shuffle(stimuli), which is exactly like shuffling a deck of cards: the order of words and non-words is randomized.
The only other thing here that is really new, is awaitkey('s,l'). This statement is waiting for the subject to press either the S-Key or the L-Key. In this chapter, you will find out how this works.
2.2 Timing with await() and now()
By now, we know how to put text on the screen for a specific duration, like this:
text("QWT");
await(2000);
clear();
This will show the string "QWT" for 2 s and then remove it from the screen. But how can we measure reaction times?
Measuring some elapsed time, like a reaction time, is accomplished with the now() function, which returns a high resolution timer value in ms but with microsecond precision. To calculate a reaction time, RT, we might do:
now().1 var t1, t2, RT;
2
3 text("Press s or l");
4 // Normally you would show a stimulus to be judged here
5 t1 = now(); // Now is a timer or 'stopwatch'
6 awaitkey('s,l');
7 t2 = now();
8 RT = t2 - t1;
9 text("Your reaction time was: " + RT);
Modern Internet browsers strive to attain microsecond precision in the high-resolution timers, but whether they succeed will vary from case to case. Older browsers do not support high resolution timing. In such cases, the now() function will use whatever timing precision is available1. Fortunately, NeuroTask Scripting automatically logs for whether a subject’s browser had high-precision timing and a few other details relevant to assess the reliability of your data. We will discuss these below.
We have used the await() function above to present a word on the screen for say 2 s. Presenting stimuli for a certain time is a crucial aspect of many psychological experiments. Unfortunately, computer screens have a limited refresh rate, typically 60 times per seconds.2 This means that they will only write new information on the screen about every 16.7 ms. For movies, sixty images per second is fast enough. For very brief stimulus presentation, however, it may not suffice. Also, stimulus presentations may be different from what you intended. If the screen is only refreshed every 16.7 ms, this means it is impossible to show a stimulus for 28 ms. The closest duration would be 33 ms. You should be aware of this and choose a time that is just below the closest number of screen refreshes, e.g., 16 ms, 33 ms, etc. This maximizes your chances of achieving the intended timing of your stimuli, because the await() function is aware of when a new screen refresh is about to occur.3
So, even if you specify a certain number of ms to wait, say 33 ms, the system does not always manage to do this. In addition to the screen refresh intervals, another source of timing error is that many processes may be running on a computer and this may affect your experiment, especially on slower computers. To monitor the actual duration of the intended presentation time, await() returns a so called event variable, which contains this information.
Suppose, for example, you want to present a very brief message on the screen that is masked by a #### pattern after 33 ms. We might do this as follows:
var e;
text("Eat popcorn!"); // Show stimulus
e = await(33); // Wait two screen refreshes
text("############"); // Show mask
We save the value of the event returned by the await() function in variable e. We can now use this event variable e to verify whether we have achieved our intended presentation time of 33 ms. This is done by checking some of its properties:
-
e.intendedTime: the intended waiting time, which is 33 (ms) here -
e.duration: the realized duration which may for example be 34 or 39 (ms) -
e.delta: this ise.duration - e.intendedTime, included for convenience -
e.timestamp: low-resolution time in ms that can be converted into a date and time of day
If e.delta is more than say 3 ms or less than -3 ms, we may decide the timing was off and discard the trial. In our own experience, when attempting to present stimuli for brief periods, like 33 ms, on older browsers and computers, many trials would have to be thrown out as timing can be quite variable, but on modern browsers almost all trials may be kept as timing tends to be quite good.
2.3 Awaiting keyboard events
Key presses
IN a typical experimental task a subject must respond with one key for a Yes-response and with another for a No-response. For example, in some recognition task, you show a list of words, some of which have been seen before and the subject must indicate this. Pressing the S-Key may signal “Yes, I think this is an existing word” and likewise the L-Key may indicate a non-word. In NeuroTask this might be done as follows:
var e;
text("aplap"); // show a non-word
e = awaitkey("s,l");
That’s it. No further coding necessary: awaitkey("s,l") will halt processing of the rest of the script until either the S-Key or the L-Key has been pressed; it will not react to other keys or other things happening (e.g., mouse clicks).
There are two things to remark here. (1) The letters (more accurately: printable keyboard characters) are always converted to their lower-case form. (2) You can add as many letters as you want to the argument (no spaces allowed). For example, the following would also work:
1 event = awaitkey("q,w,e,a,s,d,i,o,p,k,l,;");
If you want to look for any key and don’t care which one, use the await("keypress") statement, as explained below.
As soon as the subject presses a key, say the S-Key, the event variable e will receive the return value from awaitkey(), which will contain a key property with the key’s name.
As an example, we extend the small script above to show the key on the screen:
var e;
text("Press s or l");
e = awaitkey('s,l');
text("You pressed " + e.key);
As soon as you press the S-Key or L-Key, this script snippet would say either “You pressed s” or “You pressed l”; if any other keys would be pressed, awaitkey('s,l') would keep waiting and do nothing at all.
As, shown in Script 2.2, one approach to record a reaction time is to use the now() statement. This approach will work, but you don’t need to do it: When you use awaitkey(), the returned event variable will aready contain an RT property with the reaction time. The reason that this can be calculated is than the awaitkey() function is called immediately after text() and stops immediately when ‘s’ or ‘l’ is pressed. By default, awaitkey() will keep track of when it starts to wait and when it finally receives its awaited key.
In short, because measuring reaction times is so common in experiments, ` await() and awaitkey() will always calculate the reaction time and make it available in the RT` property (in capitals). With that, Script 2.2 can be simplified to:
RT property.1 var e;
2
3 text("Press s or l");
4 // Normally you would show a stimulus to be judged here
5 e = awaitkey('s,l');
6 text("Your reaction time was: " + e.RT); // concatenate reaction time to string
If you run this, you will notice that due to the microsecond precision, the reaction time value has many digits behind the decimal point, like 831.471948. If you don’t want this, you can round the value with JavaScript’s built-in function toFixed(), which takes the number of decimals to be shown as an argument. The last line would then read:
6 text("Your reaction time was: " + e.RT.toFixed(2));
Now, a reaction time will look something like 831.47, in ms.
2.4 Reaction times with timeouts
The awaitkey() function also takes a second, optional ‘timeout’ parameter, where you can specify the number of ms to wait before moving on.
1 var e;
2
3 text("Press s or l within 3 seconds");
4 // Normally you would show a stimulus to be judged here
5 e = awaitkey('s,l',3000);
6 text("Your reaction time was: " + e.RT.toFixed(2));
The effect of this is that the program will wait for the S-Key or L-Key to be pressed, but if this does not happen within 3000 ms, it will simply move on.
How do you know whether the subject responded in time? The answer can be found in the type property of the event variable, here e.type. There are many types of events, for example, those generate with the mouse, keyboard, sound player, or video player. We will encounter some of these below. To help scripting, the event that triggered the awaitkey() or await() function is always available in the type property.
In this case, if the subject pressed a key after 3 s, e.type would be equal to “timeout”. Else, if a key was pressed in time, e.type would be "keypress". In case of a timeout, the e.RT property would be close to 3000. It is not a reliable indicator of a timeout, however, because even with a timeout the RT property may well be less than 3000 ms, say 2998.341 and is thus not guaranteed to exceed 3000 ms in case of a timeout.
2.5 if...then statements
We often want to give different feedback depending on the behavior of the subject. In this case we may want to give specific feedback when the user is too slow. To check whether certain conditions occur (like slow RTs), JavaScript has an if...then statement, which looks like this:
if...then statement. 1 var e;
2
3 text("Press s or l within 3 s");
4 e = awaitkey('s,l',3000);
5
6 if (e.type === "timeout")
7 {
8 text("Please, try to respond faster next time (within 3 seconds)");
9 }
10 else
11 {
12 text("Your RT was: " + e.RT.toFixed(2) + " ms");
13 }
The program will check whether the e.type property equals “timeout” and if so runs (only) the first part between curly braces. If it equals any different type, including “keypress”, it will run the second part, after else.
The else part may be left out, in case you don’t need it, for example if you only want to give feedback when the subject is too slow.
The condition must be between (round) brackets. The system will not even try to run your program if you forget say a closing bracket or curly brace but instead it will complain about a ‘syntax error’.
To test whether some variable equals some value, you use the ===-operator. Yes, there are no less than three equal signs here!4 Frequently used logical operators are:
| Operator | Description |
|---|---|
> |
greater than |
< |
less than |
>= |
greater than or equal |
<= |
less than or equal |
=== |
equal |
|| |
or |
&& |
and |
We can combine these in many different ways. Suppose we want to keep track of how many of the S-Key presses exceeded 3000 ms. We could then write:
1 var e,
2 slow_count = 0;
3
4 text("Press s or l within 3 s");
5 e = awaitkey('s,l');
6
7 if (e.key === "s" && e.time === "timeout")
8 {
9 ++slow_count;
10 }
If the S-Key was pressed and and the response timed out, we increment the variable slow_count
Order of interpretation of operators
You can use as many (round) brackets as you want to make the conditional expression perhaps easier to read, for example:
if ((e.key === "s") && (e.RT > 3000))
{
++slow_count;
}
Just be sure to close the brackets.
The brackets can also serve to force an order of evaluation that is non-standard. For example, if you want to divide the sum of 2 and 40 by 100 you could write 2 + 40/100, but this would give 2.4, which is not what you intended. The reason is that division has a higher precedence than addition. Division is evaluated before addition, here giving 2 plus 0.4. With brackets you can get the intended result: (2 + 40)/100, which would give 0.42. Whenever, you are uncertain about how an expression will be evaluated, you can always add brackets to ensure that the result is what you intend it to be.
2.6 Non-printable keys
In some experiments you may want to use non-printable keys like the arrow keys, say, to have subjects manipulate something on the screen. How do we handle these keys? Simple. We do exactly the same thing as above. Say, you are waiting for the subject to press the Up-Arrow-Key, you can simply write:
text("Press the Up-Arrow-Key");
awaitkey("UP_ARROW");
To make this work, you have to look up the names of the keys, which are as follows:
BACKSPACE
TAB
CLEAR
ENTER
SHIFT
CTRL
ALT
META // this is the 'Apple-Key' on macs
PAUSE
CAPS_LOCK
ESCAPE
SPACE
PAGE_UP
PAGE_DOWN
END
HOME
LEFT_ARROW
UP_ARROW
RIGHT_ARROW
DOWN_ARROW
INSERT
DELETE
HELP
LEFT_WINDOW
RIGHT_WINDOW
SELECT
NUMPAD_0
NUMPAD_1
NUMPAD_2
NUMPAD_3
NUMPAD_4
NUMPAD_5
NUMPAD_6
NUMPAD_7
NUMPAD_8
NUMPAD_9
NUMPAD_MULTIPLY
NUMPAD_PLUS
NUMPAD_ENTER
NUMPAD_MINUS
NUMPAD_PERIOD
NUMPAD_DIVIDE
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
F11
F12
F13
F14
F15
NUM_LOCK
SCROLL_LOCK
UP_DPAD
DOWN_DPAD
LEFT_DPAD
RIGHT_DPAD
copyKey // Mac/PC agnostic copy key, akin to Ctrl-C
These names can also be found in the Quick Reference panel (look for Scripting - Keys). Contrary to letter keys, the non-printable keys must be entered exactly as given above, that is, all upper-case. As with letter keys, you can also use a timeout period and add several non-printable keys together with commas (no spaces allowed):
text("Now press any Arrow Key within 5 s");
awaitkey("DOWN_ARROW,UP_ARROW,LEFT_ARROW,RIGHT_ARROW",5000);
It is not possible, however, to combine printable keys (like letters or digits) with non-printable keys, like arrows, so awaitkey("s,l,F1,F10") would not work 5. Oddly, enough in JavaScript the space is considered both printable and non-printable: it maybe entered as either awaitkey(" ") or awaitkey("SPACE"). The type property returned when a non-printable key is pressed is "keydown" and not "keypress", which is reserved for printable letters only. (Yes, there is also a "keyup" event.)
2.7 Handling other types of events with await()
The await() function kan do more than simply waiting for a number of milliseconds. It can be used to await any event that can occur, including mouse events, touch events, animation events, drag-and-drop events, events from controls such as input fields (discussed below), events from sound and video players, and many more. In fact, await() can also handle keyboard events, and the awaitkey() function is itself implemented with the await() function.
Click events
Let’s look at some examples. One of the most versatile events is the ‘click’ event, which is produced when a user clicks with the left mouse button (or with the only mouse button on Macs). On a tablet or phone, a ‘click’ event may be caused by tapping the screen. The click event may also be produced if there is a button (e.g., an OK button) on the screen and the user presses that (e.g., by pressing Enter when the button is in focus or with the left mouse button). Use of the event is as follows:
text("Click this text");
await("click");
text("You clicked!");
If you would run this, you’d see the text “Click this text”. As soon as you clicked the text (or tapped, if you were working on a touchscreen), the text would change into “You clicked!”.
There is also a ‘dblclick’ event, a double-click with the left mouse button.
There is no ‘rightclick’ event. Instead there is a ‘contextmenu’ event, which on Windows will be sent when the user clicks the right mouse button. There is no right button on most Apple computers. The ‘contextmenu’ event is also sent when the Context-Menu-Key is pressed on Windows keyboards (maybe you never noticed it is there; it is typically located to the left of the right Ctrl-Key).
2.8 More about keyboard events
As we mentioned above, await() can also listen for keyboard events. With await("keypress") we would be awaiting any printable key being pressed. To find out which one was pressed we would save the return value of await():
var e = await("keypress");
JavaScript allows declaring a variable and immediately initialize it, here use this short form to save the return value of await(). The e.key property will now hold the value of the key that was pressed, e.g., “g” or “a” (always in lower-case).
For non-printable keys, we can use the same approach, now using the “keydown” event. E.g.,
var e = await("keydown");
if (e.key === keys.UP_ARROW)
{
// move an image up or do something else that is useful
}
It is also possible to use the “keyup” event, e.g., when instructing subjects to press the space bar and release it when they must respond:
var e = awaitkey("keyup","SPACE");
text("You RT was : " + e.RT.toFixed(2));
This is good way to measure simple reaction times.
In most experiments, the subject has to press one of a set of specific keys, making this a more suitable task for awaitkey(). However, awaitkey() can also be called with a keyboard event as the first argument, for example,
awaitkey("keyup","RIGHT_ARROW");
This will wait until the Right-Arrow-Key has been released.
Shift-Alt-Ctrl
Suppose you have the following script:
var e = await("a");
In other words, you are waiting until the user presses the A-Key, which will returned in the event property e.key. This value will be lower-case, “a”. How do you know whether Shift was pressed as well? Or Alt, Ctrl, or Meta (i.e., the technical name for both the Windows-Key or Apple-Key, on the respective platforms)? This is sometimes important to know. Keys like Shift and Ctrl are known as key event modifiers. They can be accessed in the e.event property, which contains a great many additional details about an event. For now, we will focus on the following properties:
ctrlKey
altKey
shiftKey
metaKey
These indicate whether the Ctrl (Control), Alt, Shift, or Meta (Windows/Apple) key was held down.
For example, pressing Shift-A, which would give a capital A, can be measured as follows:
1 var e = await("a");
2
3 if (e.event.shiftKey) // Shift was pressed with 'a'
4 {
5 text("You pressed 'A'");
6 }
7 else
8 {
9 text("You pressed 'a'");
10 }
Note that, in line 3 we could also have written
if (e.event.shiftKey === true)
but this means exactly the same as
if (e.event.shiftKey)
because e.event.shiftKey has either the value true or false. If it has value true, the statement e.event.shiftKey areadly evaluates to true and turning this into true === true adds nothing.
There is a great deal more to be said about events, but we will discuss these when we get to the relevant parts. For example, we will explain sound events when we get to playing sounds and video events when we get to playing videos.
2.9 More complete script for a lexical decision task
Given the things we learned about events, we can now add important details to the script for the lexical decision task.
1 var words = ['apple','table','grass','bike','sand'],
2 nonwords = ['aplap','blatle','rasag','kibe','snad'],
3 i,
4 stimuli = [],
5 e;
6
7 stimuli = stimuli.concat(words,nonwords); // Combine two arrays into one new one
8 stimuli = shuffle(stimuli); // Randomize the word/non-word order
9
10 // Present some instructions, where <br> gives a line-break
11 text("You will see a word or non-word appear in the middle of the screen <br>"
12 + "Press the S-Key for a word or the L-key for a non-word <br>"
13 + "Try to be as accurate and fast as possible <br>"
14 + "Start the experiment by pressing the space bar.").align("left");
15 awaitkey(' '); // Wait until the space bar is pressed
16
17 for (i = 0; i < stimuli.length; ++i)
18 {
19 text(stimuli[i],300); // show a word or non-word at 300% size
20 e = awaitkey('s,l',2000); // Await s or l or 2 s time-out
21
22 // Wait until either the S-key or the L-key has been pressed. If so...
23 console.log(stimuli[i],e);
24
25 if (e.type === "timeout")
26 {
27 text("Please, try to respond faster (within 2 seconds)");
28 await(3000);
29 }
30
31 clear(); // Clear the screen
32 await(1000); // Give 1000 ms pause
33 }
34
35 text("<h2>Finished!</h2>Thank you for participating");
36 await(3000);
In the final version of this script, you would want at least to save the subject’s responses. Perhaps, you would also want to verify their correctness and log them into the data section of your NeuroTask account, together with their reaction times. We will return to these aspects in the chapter about storing data.
- The value returned by a high resolution timer cannot be converted into a time of day because these timers are started at an arbitrary moment, for example, when you open the web page or start your computer.↩
- Some screens use a refresh rate of 70 or 120. This is rare at the moment (March 2015) but may well change in the future. ↩
- RAF is an abbreviation of ‘requestAnimationFrame’, this is a signal that the next screen is about to be generated by the computer, which typically happens exactly 60 times per second. See the advanced manual for a more in-depth discussion of this.↩
- You may also encounter an equal operator with two
=signs in JavaScript,==, but we do not recommend using this unless you know exactly what you are doing. For example, the expression("0" == false)will unexpectedly evaluate totruebecause"0"is considered so called ‘falsy’ value. This may lead to subtle errors that can trip up even seasoned programmers.↩ - Later, we will see an way around this using the
waitforstatement, with which you can combine severalawait()statements.↩