2 Capturing Keys and Reaction Times
2.1 Achieving Precise Timing with await()
By now, we know how to put text on the screen for a specific length of time (as in the example below). Presenting stimuli for a specific length of time is a crucial aspect of many psychological experiments. Section 2.1 will explore two potential sources of timing error.
text("QWT");
await(2000);
clear();
Here, we have used the await() function above to present the string “QWT” on the screen for 2 s. Unfortunately, computer screens have a limited refresh rate, typically 60 times per seconds.1 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. It also means that stimulus presentation length may be different than 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.
To work around the limitations presented by screen refresh rates for brief presentation times, it is best to choose a time that is just below the closest multiple of the refresh period, e.g., 16 ms, 33 ms, 50 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.2
In addition to screen refresh intervals, these is a second potential source of timing error; it is possible that many processes may be running on a computer, which (especially on older computers) may affect your experiment.
To avoid timing error from other processes a computer may be running, we suggest using the built-in properties of the await() function to monitor the actual duration of a presentation time. The await() function automatically returns a so-called event variable, which contains information about the presentation.
Consider the following example: you want to present a very brief message on the screen. Then, after only 33ms, quickly mask it with a ###### pattern. You might do this as follows:
var e;
text("Eat popcorn!"); // Show stimulus
e = await(33); // Wait two screen refreshes
text("############"); // Show mask
By assigning await(33) to variable e, we assign the value of the event returned by the await() function to e. Since this value is clearly 33, this seems pointless, at first glance. However, the returned value has more information about the event than just its intended value (i.e., the intended presentation time of 33 ms). By adding a line in the script, we can then check any of the following properties:
-
e.intendedTime: the intended waiting time, which is 33 (ms) here -
e.duration: the actual duration fo waiting time, which may have been, for example, 34 or 39 (ms) -
e.delta: this is simplye.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, for example, higher than 3 ms or lower than -3 ms, you may decide the timing was too far off, and discard the trial. In our experience, when attempting to present stimuli for brief periods (like 33 ms) on old browsers and computers, many trials 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. Modern browsers are now so ubiquitous that this is rarely an issue, however it remains good practice to check using the event properties described above.
2.2 Handling Various Types of Events with Await()
The await() function can do more than simply waiting a specific 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, discussed later in this chapter, 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, on Macs, the only mouse button). On a touchscreen (tablet, phone, etc.) a ‘click’ event can be caused by tapping the screen. A ‘click’ event can 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). Await()-ing a ‘click’ event is implemented as follows:
text("Click this text");
await("click");
text("You clicked!");
If you ran this, you’d see the text, “Click this text” (without quotation marks). Then, as soon as you clicked the text (or tapped, if you were working on a touchscreen), the text would change to “You clicked!”.
You can await a double-click with the left mouse button, by writing "dblclick" in place of "click" as the awaited event.
You can also await a right-click event, by writing "contextmenu" (not "rightclick"). This name comes from the menu that a right click is typically used to access. This menu can also be accessed by pressing the Context-Menu-Key on a Windows keyboard (you may never have noticed it; it is typically located to the left of the right Ctrl-Key). Both a right mouse click and a Context-Menu-Key press will successfully trigger a "contextmenu" event, hence why it is called "contextmenu" and not "rightclick". There is no right button on most Apple computers, though NeuroTask will correctly interpret Apple’s alternative (control-click) as a right click.
2.3 Await-ing Keyboard Events
A Simplified Script for a Lexical Decision Task
In this chapter, we will create a script for a lexical decision task. The task will involve presenting words and non-words in random order. The subject will have to indicate whether the stimulus is a word or not, as quickly and as accurately as possible. The subject will press the L-Key to indicate a word and the S-Key to indicate a non-word. In the first version of this example script, 2.1, we will use a short list of only five words and five non-words to make it easier to follow. The first version does not yet contain all the components that a real experiment would (e.g., instructions), but it does provide a good starting point from which we can develop a more complete script. A more complete version of this script is included at the end of this chapter.
1 var words = ["apple","table","grass","bike","sand"],
2 nonwords = ["aplap","lbate","rasag","kibe","snad"],
3 stimuli = [],
4 i,
5 e;
6
7 stimuli = stimuli.concat(words,nonwords); // Combine the two arrays into one new
8 // 10-element array
9 stimuli = shuffle(stimuli); // Randomize order of the words/non-words
10
11 // Instructions would come here
12
13 for (i = 0; i < stimuli.length; ++i)
14 {
15 text(stimuli[i],300); // show a word/non-word (the element in
16 // position "index i" of the shuffled
17 // list), at 300% font size
18 e = awaitkey('s,l'); // Wait until either the S- or L-key is
19 // pressed. If so:
20 clear(); // Remove word from screen
21 await(1000); // Pause for 1 s
22 }
23
24 // Feedback/debriefing would come here
The first thing that is new to us in this script is that we have more than one array: words, nonwords, and a third empty array, called stimuli. The following statement copies the contents of words and of nonwords into stimuli:
7 stimuli = stimuli.concat(words,nonwords);
Now, stimuli contains a copy of all 10 of the words and non-words. The term concat stems from “to concatenate”, which means to join or link together. As described in the chapter 1 subsection on Arrays, the dot, ., indicates that we are describing a property of the array that precedes the dot, stimuli. In this case, the property is that this array combines the contents of words and nonwords. In plain English, line 7 would read “The (previously-empty) array stimuli has taken copies of the arrays words and nonwords, and joined them into one long list for its own contents.” The variable stimuli now contains all ten words and non-words, in this order:
("apple","table","grass","bike","sand","aplap","lbate","rasag","kibe","snad")
Next, we shuffle the list of words and non-words into a randomized order using the shuffle() function, which is exactly like shuffling a deck of cards:
9 stimuli = shuffle(stimuli);
The only other thing that is new in Script 2.1 is on line 15: awaitkey('s,l'). This statement instructs the system to wait for the subject to press either the S-Key or the L-Key before continuing the script. In this section, you will find out how this works.
Key presses
In a typical experimental task, a subject must respond with one key for a “yes” response and another for a “no” response. Key press responses can also represent options besides “yes” and “no”. For example, in the lexical decision task from Script 2.1, the subject would be asked to press the S-Key if they detect a word, and the L-Key if they detect a non-word. In NeuroTask, this could be achieved 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 to other things happening (e.g., mouse clicks).
I) Printable Keys
Printable keys generally refer to keys that would show up when typing a word document (e.g., letter keys, number keys at the top of a Mac keyboard, punctuation keys, the space bar, etc.). They contrast non-printable keys, which are listed just below. Aside from checking the list below, a good tip for differentiating them is that keyboard shortcuts do not contain two printable keys; they use at least one non-printable key.
Whether the subject presses the letter in lower- or in upper-case (e.g., using the Shift Key), the system will correctly recognize the response and move on with the script. For details on how to tell whether a capital was pressed, see section 2.6, Shift-Alt-Ctrl.
II) Any Key
If you want the subject to press any (printable) key to continue, but don’t care which one, use the await("keypress") statement.
III) Non-printable Keys
In some experiments you may want to use non-printable keys, like the arrow keys, to have subjects manipulate something on the screen. These keys are handled in exactly the same way shown above.
For this to work, you must used the accepted names of the keys, which are as follows. These names can also be found in the Quick Reference side panel when scripting (look for Scripting: Keys).
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
Event Properties
The many types of events (such as those generated by a mouse, keyboard, sound player, or video player) each have certain properties associated with them. Consider the following script. As soon as the subject presses an accepted key, say the S-Key, a value is returned to (i.e., recorded in) the event variable e, thanks to the fact that we wrote e = at the beginning of line 4. This value has many useful properties.
1 var e;
2
3 text("Press s or l");
4 e = awaitkey('s,l');
5 text("You pressed " + e.key);
The first is the key property, which tells the name of the key that was pressed. In the example above, the key property is used to provide confirmation. As soon as the subject presses the S-Key or the L-Key, their screen would read either “You pressed s” or “You pressed l”. If any other keys were pressed, awaitkey('s,l') would do nothing at all except keep waiting for s or l. More usages for the key property are discussed in the More Examples with Keyboard Events subsection towards the end of this chapter.
A second event property is the type property. This can be used to what kind of key press action triggered the end of the await() or awaitkey() function. When a printable key is pressed, the type property returned is “keypress”. When a non-printable key is pressed, the type property returned is “keydown”. Note, there is also a “keyup” event, for when you have specified that the release of a key is to trigger the end of the await(). This property is discussed further in Reaction Times with Timeouts, at the end of section 2.4. and in section 2.5 if...then statements.
Other types of event properties include the RT property and the event property, discussed later in this chapter in the Reaction Time: In Practice (Short-hand) and Reaction Times with Timeouts subsections, and in the Shift-Alt-Ctrl subsection, respectively.
2.4 Measuring Reaction Time (with now(), await(), and awaitkey())
Measuring elapsed time, such as a reaction time, can be accomplished in a variety of ways. In the upcoming subsections we will describe a short-hand that is very useful in typical reaction time scenarios, but first we will cover a more standard approach that, although slightly longer in its script, is good to have an understanding of, because it is extremely modifiable and widely applicable.
Reaction Time in Theory (Long-hand)
The standard method involves using the now() statement, in conjunction with either the await() or the awaitkey() function. When assigned to a variable, the now() statement returns a high-resolution timer value to that variable in milliseconds, with microsecond (i.e., one thousandth of a millisecond) precision. Alone, this value is rather meaningless, because the timer automatically starts at an arbitrary moment (e.g., when the subject opened the webpage, or started their computer) .4 Used together, however, these timer values can be very useful, such as for calculating reaction time:
now().1 var t1, t2, RT;
2
3 text("Press s or l"); // Normally you would show a stimulus to be judged here
4 t1 = now(); // The now() statement starts a timer or 'stopwatch'
5 awaitkey('s,l');
6 t2 = now();
7 RT = t2 - t1;
8 text("Your reaction time was: " + RT + " ms");
Modern Internet browsers strive to attain microsecond precision in the high-resolution timers, but whether they succeed varies from case to case. Older browsers do not support high-resolution timing. In such cases, the now() function will use whatever timing precision is available. 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.
Reaction Time in Practice (Short-hand)
As shown in Script 2.2, one approach to record a reaction time is to use the now() statement. This approach works, but it is unnecessarily complex in this case. As described in the previous subsection, Key Presses, when you use awaitkey(), the returned event variable will automatically contain some properties, including an RT property with the subject’s reaction time. This is possible because, by default, the awaitkey() function keeps track of when it starts to wait, and of when it finally receives its awaited key. Since the awaitkey() function was called immediately after the text() function displayed our instructions, it started at the same moment the text was displayed, meaning that the time until ‘s’ or ‘l’ is pressed is equal to the reaction time we’re seeking.
NeuroTask includes this property automatically for the simple reason that measuring reaction time is so common in experiments that it made sense to simplify it. Both await() and awaitkey() will always calculate the reaction time and make it available in the RT property (in capitals). This means 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 + " ms"); // concatenates RT and strings
If you run this, you will notice that, due to the microsecond precision, the reaction time value has many digits behind the decimal point, for example 831.471948. If you don’t want this, you can access rounded values with JavaScript’s built-in function, toFixed(), which takes as its argument the number of decimal places you’d like shown and returns a string with the digit formatted with that number of decimals. To display a reaction time rounded to the nearest hundredth, for example 831.47, the last line would then read:
6 text("Your reaction time was: " + e.RT.toFixed(2) + " ms");
Reaction Times with Timeouts
The awaitkey() function has an optional, second argument which can be included after the first (obligatory) argument, which specifies which keys are to be accepted. This optional second argument is the “timeout” parameter (i.e., limit). By adding a number for this parameter, you can specify a maximum number of ms to wait for one of the specified keys, before moving on regardless. It looks like this:
1 var e;
2
3 text("Press s or l within 3 seconds"); // Normally you would show a stimulus to be
4 // judged here
5 e = awaitkey('s,l',3000); // ",3000" has been added here, specifying
6 // the maximum ms to wait
7 text("Your reaction time was: " + e.RT.toFixed(2) + " ms");
The effect of this addition 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 using the type property, written e.type. In the same way that a timer measures RT for the awaitkey() and await() functions by default for convenience, the event that triggers moving past these functions is also available by default, and can be accessed through the type property. If the subject pressed a key after 3 s, e.type would be “timeout”. Otherwise, if an accepted key was pressed in time, e.type would be "keypress".
In the case of a timeout, the e.RT property would be close to, but not necessarily precisely, 3000, since RT measures the time until the await() or awaitkey() function is moved on from (regardless of reason). Thus, RT may read something like 2998.341 when indeed the subject timed out and could have had a reaction time that greatly exceeded 3000 ms. For this reason, RT alone is not a reliable indicator of a timeout, and the type property should first be examined.
2.5 if...then statements
We often want to give different “next steps” in our instructions based on what has just occurred. For example: we may want to show the subject feedback or reminders based on their performance; we may want to save answers into categories based on whether they were correct or not; we may want to have subjects complete our tasks in one order or another (counterbalancing) based on whether their subject ID number is odd or even. To have the script proceed in a specific way based on whether certain conditions have occurred (such as a subject failing to respond quickly enough), 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 (in under 3 seconds)");
9 }
10 else
11 {
12 text("Your RT was: " + e.RT.toFixed(2) + " ms");
13 }
The system will check whether the current subject’s e.type property equals “timeout”. If it does, it will run (only) the part between the first set of curly brackets. If it equals any type other than “timeout” (which in this case could only be “keypress”, from s or l being pressed), it will run the second part, within the curly brackets that follow else.
The else part can be left out if you don’t need it. For example, you may only want to give feedback if the subject was too slow. In this case, if the subject timed out, they would see the “please respond faster” message and then whatever comes next in the script, whereas if they responded in time, the system would proceed with the rest of the script immediately.
Operators
In Script 2.5 directly above, we use the ===operator to test whether a variable equals some value. Please note: there are no fewer than three equal signs here^equal] Earlier in this chapter, we saw the +operator used to join strings. Other 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 operators in many different ways. Suppose we want to keep track of how many of the S-Key presses exceeded 3000 ms. We could write:
1 var e,
2 slow_s_count = 0;
3
4 text("Press s or l within 3 s");
5 e = awaitkey('s,l');
6
7
8 if (e.key === "s" && e.time === "timeout") // "&&" (the "and" operator) creates
9 // a specific condition
10 {
11 ++slow_s_count;
12 }
This instruction would increment (i.e., increase by 1) the variable slow_s_count only if the S-Key was pressed and the response timed out.
Order of Interpretation of Operators
You can use as many round brackets, (), as you want to make the conditional expression easier to read, just be sure to close all brackets. For example:
if ((e.key === "s") && (e.RT > 3000))
{
++slow_s_count;
}
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, writing 2 + 40/100 would give 2.4, which is not what you intended. The is, of course, because in standard math as in JavaScript, division has a higher precedence than addition. Division is evaluated before addition here, giving 2 plus 0.4. By adding brackets, (2 + 40)/100, you can achieve the intended result, 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 More Examples with 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(), shown below. The e.key property would then hold the value of the key that was pressed, e.g., “g” or “a” (always in lower-case).
var e = await("keypress");
For non-printable keys, we can use the same approach, now using the “keydown” event. For example,
var e = await("keydown");
if (e.key === keys.UP_ARROW)
{
// move an image up or do something else that is useful
}
In addition to “keypress” and “keydown”, it is also possible to await() the “keyup” event. For example, when instructing subjects to press down the space bar and release it when to 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 accepted keys, making this a more suitable task for awaitkey(). However, awaitkey() can also be called with a keyboard event as the first argument. This will wait until the Right-Arrow-Key has been released:
awaitkey("keyup","RIGHT_ARROW");
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 be named in the returned event’s 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 and 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 already 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.7 A More Complete Script for a Lexical Decision Task
Given what we have learned about events, we can now add some more important details to the script for the lexical decision task.
1 var words = ['apple','table','grass','bike','sand'],
2 nonwords = ['aplap','lbate','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. Logging is most easily done with the log() function, which takes a value and a name, which will appear in your data sheets, e.g., “RT”. If your event variable is called e, you could log the reaction time with log(e.RT,"RT"). Each time this is called, a new row would be added to your data sheet with in the name column ‘RT’. If you have two conditions, that is a problem, because you don’t know which of the two conditions it was. This is often solved by gluing the condition to the RT. So in condition 1, you could log the reaction time and key pressed as:
var e = awaitkey("s,l",3000);
log(e.RT,"RT_condition1");
log(e.key,"key_condition1");
We will return to these aspects in Chapter 7 about data logging and handling. You can skip ahead and take a peek at section 7.1 if you want to start logging data now. Unless you have a very simple design, it is worth it to spend some time planning how to name your variables and conditions, and how to log these, so that the final data set can easily be imported into SPSS or Jasp.
Why was there no need to think about logging before? That is because the standard form controls in NeuroTask Scripting, like input() and largeinput(), will automatically log the data for you (using the log() function). So, you only have to worry about it when you are collecting responses without them.