Getting started with NeuroTask Scripting, 2nd Edition
Getting started with NeuroTask Scripting, 2nd Edition
Jaap Murre
Buy on Leanpub

Table of Contents

Preface

Audience

This book is written for anyone who wants to make an Internet-based experiment or questionnaire, but who knows nothing about programming. It is a step-by-step guide that shows how to create online experiments using NeuroTask Scripting. With a few lines of easy-to-learn JavaScript, you can create complex experiments that instantly run online, easily share them, and sit back as your data flows in.

Typographic Convention

Throughout this book, normal text is typeset in the font you are currently reading. Alternatively, when the book gives examples of lines of code, exactly as you would write them in NeuroTask, those pieces of JavaScript appear in a different font for clarity, e.g., “This is a JavaScript text string, shown in a special font to make clear it is not ordinary text”.

Acknowledgements

I would like to thank Julia Broderick-Hale for her revision of the first three chapters of the second edition and for her extensive editing work.

1 Getting Started with NeuroTask Scripting

1.1 What Are Scripts and Why Do We Need Them?

Suppose, you want to make a word recall experiment where you present eight words on the screen, one by one. Each word is to be shown for 2 s, followed by a 1 s pause. After all the words have been presented, you want to ask the subject to type all the words remembered into a large text box, in any order. In other words, you have an idea for an experiment that specifies step-by-step what you want to happen. If you wrote down these steps in a plain English list, it might look something like this:

  • Show word 1 for 2 s
  • Pause 1 s
  • Show word 2 for 2 s
  • Pause 1 s
  • Show word 8 for 2 s
  • Pause 1 s
  • Have subject write down the words remembered

The ellipses (…) above replace showing words 3 through 7, which are skipped over to avoid repetition.

This step-by-step plan is, in fact, already very close to a script that you’d write in NeuroTask. The only difference is that scripts use a specific, standardized format for writing the steps that tell the computer what you want it to do. A NeuroTask script that would achieve the steps above would look something like this:

text("word 1");
await(2000);
clear();
await(1000);

text("word 2");
await(2000);
clear();
await(1000);

...

text("word 8");
await(2000);
clear();
await(1000);

largeinput("Write down the words you remember");

As you can see, the first line of the script is an instruction to the computer to show a word. This instruction to show a word is called the text function. Functions appear on new lines, and can be thought of as commands for the computer - the action you want it to perform next. Next, there is an instruction to wait until 2000 ms have passed (time is measured in milliseconds in NeuroTask). The waiting instruction is called the await function. Then comes the clear function, which gives the instruction to clear the screen of any words. Finally, after all the words have been shown and cleared, there is the largeinput function, which instructs the computer to show a large text box where the subject can type the words that they remember.

You will also notice that there are parens (i.e., round brackets), quotation marks, and semi-colons. Using punctuation (part of what is known as syntax) is an important part of writing scripts in this specific, standardized format that computers can decipher. Parens follow functions (such as await and clear, seen above) and contain details the computer needs to complete the function (such as how long to wait, or what words to show). Quotation marks tell the computer that you’re entering what’s called string - a string of letters that the computer doesn’t need to decipher, just a piece of content for it to use (such as some text you’d like it to display to the subject). Semi-colons act as the period at the end of each step in your instructions. They allow the computer to break down your instructions into step-by-step pieces. Once you know how to translate your plan into a script, you can instantly run it online, and have others (your subjects) run it too.

1.2 Scripting Psychological Experiments

So, we’ve learned that scripts tell the subject’s computer what stimuli to present and which data to collect from the subjects. The team at NeuroTask Scripting has worked hard to make frequently-used experimental tasks easy to script, even for a beginner.

NeuroTask scripts are written in JavaScript, the programming language that is built into all internet browsers such as Internet Explorer, Safari, Chrome, and even the browsers on your smart phone. This means that the internet-based experiments created with NeuroTask Scripting will run on virtually any computer with an internet connection. Creating web-based experiments from scratch, i.e., without NeuroTask Scripting is quite difficult and involved. With NeuroTask, it is a breeze. It is quite feasible to write a full-blown experiment in one hour. But don’t take our word for it. See for yourself.

For those of you who are already experienced JavaScript programmers, it is important to know that we are not limiting your use of JavaScript. You also have all of the Dojo and jQuery libraries at your disposal, included by default, and it is possible to pull in other libraries as well. We are, in fact, using a superset of JavaScript, called StratifiedJS, which adds even more functionality and additional (optional) libraries.

Writing Scripts

There are two steps to creating an online experiment script, and step 2 is optional:

  1. Write your experiment script
  2. (Optional) Make and upload your stimuli

You’re done! Your script is automatically ‘live’ on the internet and it has its own unique web address (also known as its URL). You can email this web address to friends and family, put the link on Facebook, etc. If participants do your experiment and you have a paid NeuroTask package, their data will automatically be saved in your NeuroTask Scripting account, where you can easily download it in Excel or other formats. This streamlined method of easy experiment sharing and automatic data collection is the same whether you are running a simple pilot or informal experiment, or whether you are running a large experiment or using subjects from crowdsourcing websites such as Amazon’s Mechanical Turk.

Then, wait as your data comes in and start analyzing. While you are waiting, it is possible to see who has already completed your experiment using the session monitor (found at this icon: ).

1.3 Scripts

The Structure of an Experiment Script

Most experimental tasks in psychology involve about the same steps. These steps must be expressed somehow in a script. In an online experiment, you will typically:

  • Welcome the subject
  • Ask for informed consent
  • Give instructions
  • Present stimuli (such as words)
  • Record responses (such as words remembered)
  • Debrief, where you may thank the subject for participating

Let’s look at a short example script that does most of this.

Your first script: A small experiment

Guess what the following script is doing. Well, the title sort of gives it away, at least if you have followed an introductory course on psychology. Note that light bluish green text in italics that follows two forward slashes is commentary that the computer will overlook. Using this commentary feature when scripting is very handy for writing “notes to self”, organizing your script, reminding yourself the purpose of each section, and adding explanation for others who may read your script. This script would run perfectly in NeuroTask. All black and red text is necessary to its function, whereas all green text that follows two forward slashes could be removed with no affect.

Script 1.1. Brown-Peterson task
 1 text("Try to remember the following three letters");
 2 await(5000); // 5000 ms or 5 s
 3 
 4 text("YHZ");
 5 await(3000);
 6 
 7 text("Now, count back in threes starting with 307: 307, 304, 301,...");
 8 await(25000);
 9 
10 input("Write down the letters remembered","brown_peterson");
11 
12 text("Thank you for participating!");
13 await(3000);

If you actually did this experiment online, you would see a white screen (i.e., a blank web page) with the words, “Try to remember the following three letters” (the quotation marks would not be shown).

After five seconds, the text would change into “YHZ”.

Then, the count-back instruction would appear for a longish 25 s period, during which the subject would supposedly count back in threes.

After this 25 s interval, a text box would appear with the label, “Write down the letters remembered” above it. There is an “OK” button below the text box that is automatically generated by the input function so that subjects can move forward when they’ve finished typing.

After pressing “OK”, the words “Thank you for participating!” appear for 3 s and the subject is finished. The computer has reached the end of the script, and the screen reads a simple “Powered by NeuroTask Online Experiments” (during which the script is saving any remaining unsaved data points).

Though this script is short, it contains most of the steps of a standard experiment, and it can easily be changed to contain different stimuli, as we will illustrate below. The presentation time of the text may be varied from very long to ultra-short, instructions may be made more elaborate, and so on. So, we see that this brief script is a miniature model for a whole range of memory experiments.

The Brown-Peterson Task is a classic experiment in memory psychology in which John Brown, in 1958, and later, Lloyd and Margaret Peterson, in 1959, showed that even three letters are forgotten in as little as 15 seconds, but only if the subjects are not allowed to consciously rehearse them. Rehearsal is typically prevented by having subjects count backward in threes. The effect is not obtained, however, in the first trial, i.e., after having studied just one letter triplet; the task should be repeated several times with different letter triplets, while making sure that subjects are not secretly rehearsing the letters in the 25 s interval. Experiments like this were early evidence that conscious rehearsal is important for the maintenance of short-term memory and that without it, short-term memory fades very quickly.

Walking through Script 1.1

Let’s walk through the script in small steps, starting with line 1:

1 text("Try to remember the following words");

Here, we see the text() function with the message, “Try to remember the following words”. The quotation marks tell the system it is text. The formal word for text in many computer languages is string, as in: a string of characters. You can also use single quotation marks instead of double quotation marks, which would read: text('Welcome'). We call text() a function and “Try to remember the following words” the argument of the function text(). A function is a general action you’d like the computer to carry out, and the argument provides the details of how you’d like that action done (in this case, what specifically the text should say). Arguments of a function must always be enclosed in round brackets: ( and ).

The semi-colon ; at the end of a line in a script helps the system to distinguish between subsequent script statements. It is like the full-stop (period) at the end of a sentence, which signals to us, the human reader, that one sentence ends and the next one starts. The semi-colon may be left out, especially when each statement is on its own line as is the case here, but it is highly recommended to always add it1.

Now, let’s look at line 2:

2 await(5000); // 5000 ms or 5 s

This line uses the await() function with the argument 5000. When await() is called like this, the computer will pause its execution of the the script for 5000 ms, and then continue. Note that the number of ms is denoted as 5000 and not “5000” (or ‘5000’). That is because it is an integer number which the computer system can read automatically, unlike strings which require quotation marks.

There is also an addition in line 2 that reads: // 5000 ms or 5 s. This is a comment, which is completely ignored by the system. It is handy to include comments as notes to oneself or to others. Here, we make clear that 5000 means: 5000 ms. A // comment only runs to the end of the line, meaning that when the computer encounters //, it will stop reading that line and skip immediately to the start of the next line. With multiple lines of comments, you must repeat the // at the beginning of each line, e.g.,

// Starting a comment.
// Continuing it on the next line.
// As many times as we'd like.

There is also a type of comment that spans many lines. It starts with /* and ends with */, both on their own line (with no text before or after them on their line), e.g.,

/*
   Starting a comment
   that spans many lines.
   It will just continue
   Until we decide to stop it with an asterisk and forward slash.
*/

Make sure you start with /* and end with */ exactly in that order. People familiar with SPSS may forget the forward slash, because in SPSS comments are indicated with * alone, but in JavaScript (and therefore in NeuroTask), leaving out the forward slashes will cause an error.

So, there are two type of comments:

  • Single line, beginning with //
  • Line-spanning, beginning with /* and ending with */

On lines 4 through 8, the following two statements in the script follow exactly the same show this text for this many milliseconds pattern, except with different arguments:

4 text("YHZ");
5 await(3000);
6 
7 text("Now, count back in threes starting with 307: 307, 304, 301,...");
8 await(25000);

On line 10, the user’s response is recorded with the line:

10 input("Write down the letters remembered","brown_peterson");

The input() function requires a label as its argument, in this case: “Write down the letters remembered”. The input() function displays this label with a text box (i.e., a text input field) below it, in which the user can type the letters that they remember. Below that is an “OK” button. The second argument, “brown_peterson”, is optional (though highly recommended) and is the name given to the variable that is created to store whatever the subject types into the text area.

Now you may wonder, “What is happening with the subject’s answers?”. All responses (that come from buttons, text inputs, drop-down select lists, checked boxes, etc. — formally called form controls or controls for short) will be saved automatically into your account’s data area. viewable both at the Data Dashboard page (found at this icon: ) and, in a different layout, at the Logged Data page (found at this icon: ). You can inspect these responses and download the data in Excel and other formats. It is important to give meaningful names to your variables, so that later you know what the values in your data tables represent.

If you don’t provide a name for the variable, the subject’s input will be saved under an automatically-generated name, such as “input_1”. In a small experiment this may not present any problems, but when your experiment collects a lot of data (i.e., multiple variables), mistakes are easily made. It is therefore highly recommended to create meaningful names for all recorded data.

The last lines of the script show a thank you message for 3 s. After this, the NeuroTask branding screen appears signalling that the experiment is over.

13 text("Thank you for participating!");
14 await(3000);

Script 1.2: A free recall experiment

Let’s take Script 1.1 and turn it into a free recall test with eight words. Each word is shown for 2 s, then it is removed from the screen, followed by 1 s pause, after which the subsequent word is shown.

Script 1.2. Free recall task.
 1 text("Try to remember the following words");
 2 await(5000); // 5000 ms or 5 s
 3 
 4 text("glass");
 5 await(2000);
 6 clear();
 7 await(1000);
 8 
 9 text("chair");
10 await(2000);
11 clear();
12 await(1000);
13 
14 text("train");
15 await(2000);
16 clear();
17 await(1000);
18 
19 text("balloon");
20 await(2000);
21 clear();
22 await(1000);
23 
24 text("horse");
25 await(2000);
26 clear();
27 await(1000);
28 
29 text("curtain");
30 await(2000);
31 clear();
32 await(1000);
33 
34 text("pencil");
35 await(2000);
36 clear();
37 await(1000);
38 
39 text("baker");
40 await(2000);
41 clear();
42 await(1000);
43 
44 text("Now, count back in threes starting with 307: 307, 304, 301,...");
45 await(20000);
46 
47 largeinput("Write down the words remembered","words_remembered");
48 
49 text("Thank you for participating!");

We introduce a new function here, clear(), which removes the current text from the screen, i.e., clearing it. The rest of the script is highly similar to Script 1.1. A small difference between Scripts 1.1 and 1.2 is that in 1.2 we use largeinput() instead of input(). The difference is that largeinput() gives a large text box, of about five rows, in which the subject can type their responses, whereas input() gives just a single line text box, which may feel cramped when typing up to eight words:

Words entered into a largeinput() text box.

If your subjects did this task online, you would see their answers, exactly as they typed them in, under the label “words_remembered” in the data area (Data Dashboard or Logged Data pages) of this experiment script:

Inspecting data entered by a subject

Perhaps, we should here point out why we used an underscore in "words_remembered" and not a space. The reason is that variable names cannot contain spaces in JavaScript. An underscore is often used instead of a space.

Script 1.3: A shorter script with a for loop

Our third script creates an experiment identical to 1.2, but achieves it with a much simpler script thanks to two new concepts: array and for() loops. There is nothing wrong with Script 1.2, but some might find it a bit long and if you wanted to make an adjustment, such as presenting the words for 3200 instead of 2000 ms, you would have to adjust each trial individually. This is both tedious and prone to error, especially in larger scripts.

Whenever there is a highly repetitive pattern in a script, it is a good idea to rewrite it using a so called loop. Take a look at the rewritten script:

Script 1.3. Free recall task like Script 1.2, but now with a for loop.
 1 var i;
 2 var words = ["glass", "chair", "train", "balloon",
 3                  "horse", "curtain", "pencil", "baker"];
 4 
 5 text("Try to remember the following words");
 6 await(5000); // 5000 ms or 5 s
 7 
 8 for (i = 0; i < words.length; i = i + 1)
 9 {
10     text(words[i]);
11     await(2000);
12     clear();
13     await(1000);
14 }
15 
16 text("Now, count back in threes starting with 307: 307, 304, 301,...");
17 await(20000);
18 
19 largeinput("Write down the words remembered","words_remembered");
20 
21 text("Thank you for participating!");
22 await(3000);

White Space

You may insert arbitrary white space (spaces, tabs, and newlines) almost anywhere in a script, which can be useful if you want to make the layout of your scripts more legible. For example, in the demonstration scripts thus far, we have used empty lines to create groupings to help see the structure of the script. These empty lines are completely ignored by the computer system, as is most other white space. There are very few places where you cannot add arbitrary white space, but they are rather intuitive, such as within a string (e.g., if we wrote "gla ss" the computer would not ignore the white space and would dutifully display “gla ss” on the screen) and within numbers (e.g., exponents such as 2e10 will not be correctly interpreted as exponents if written 2 e 10).

Variables

You may notice the use of the word var, which stands for variable. Unlike the lines of script that we’ve seen so far, which have been more action-based (e.g., functions that we ask the computer to perform), variables allow us to simply introduce some content (such as the word list that is our experiment’s stimuli) to which we can refer later in the script. Introducing variables is a bit like listing the ingredients in a recipe. For example, a recipe might list “Apples: 4, peeled, and cut into 1cm slices” at the beginning of the recipe, then later, in the action-based part of the recipe, simply refer to doing something with “the apples”.

Variables are used to store (and later, to access) information in scripts. You can think of variables like storage cubbies. First, you create a variable simply by writing var followed by the variable a name (e.g., writing “var i” or “var words”). In JavaScript, variables names can contain numbers and underscores, but may not start with a number. They are case-sensitive, meaning that variables word, Word, WORD would be considered three completely different variables. This tells the system that we will want to store some data (values) and require memory space to do so. It creates an empty storage cubby, labelled with the name you gave it. This is called declaring a variable. You may declare a new variable at any point in a script.

Assigning Values to Variables

After declaring a variable, you can put something (or even some things) in that storage cubby (variable) for safe keeping. This is called assigning value to a variable. What makes variables so useful is that you can add to or swap out the contents of your storage cubby at any time, and also refer to the storage cubby name to see what’s inside it (to access its current value). The rest of this section, 1.3, will show why this is useful.

The first variable in the script has a very short name. It is called i. We will capitalize on the fact that variable values can be changed, and use i to count (or keep track of) how many words we’ve shown on the screen from our word list. To use i as a counter, we set its initial value to 0. The equal sign is used to assign a value to a variable. So, i = 0 means, “Give i the value 0”. This equals sign and the value to its right are called the assignment. Throughout the script, we will use a function to tell the computer to increase the current value of i by 1 each time that a word is displayed, thus keeping count of how many have been shown.

The second variable is declared in line 2. We have called this variable words. The variable words is immediately assigned a value in the form of an array (i.e., list) of strings (i.e., words), which correspond to the stimulus words from Script 1.2. Here, we see that variables (storage cubbies) can hold not just one item (value), but also many items (an array of values). Again, the value of words is set using the equal sign =, followed by the array of words.

You can also assign the result of some calculation to a variable. For example, when you do:

i = 60*24;

the system would first calculate the result of the expression 60*24 (meaning 60 times 24), which equals 1440, and then assign that value to i. So, the above statement is equivalent to:

i = 1440;`

You can also put names of variable on the right-hand side of an assignment, like this:

minutes = 60;
hours = 24;
i = minutes * hours;

Again, i would be 1440 after these lines had been processed. While the example above is not very practical (it’s just a long-winded way of writing a calculation that we could do faster), we see how putting variables on the right-hand side of an assignment can be useful in Script 1.3. To use i as a counter, we use the existing value of i on the right-hand side, add 1 to that, and assign this new result to i, overwriting the existing value, like so:

i = i + 1;

So, if i was 0 at before this assignment, it is 0 + 1 (i.e., 1) after the assignment. This is one way to increase the value of i by 1. In script 1.3, we tell the computer to do this each time it displays a word. You could also decrease i by 1, like so:

i = i - 1;

or multiply i by 2:

i = 2*i;

Arrays

An array in JavaScript is a type of list that can be compared to a wide storage unit containing many items, all lined up in a row. The items in an array are called elements. The elements in an array are numbered starting at 0 (and not at 1). This is automatic and cannot be changed. All elements of an array must be separated by commas, or else they will be seen as one element. To indicate to the system that words is an array (not just one value), the list of values must be put inside square brackets [ and ]. The word glass is in position 0, the word chair is in position 1, etc. When we want to access an element of our array (e.g., to display it on the screen) we do so by writing the name of the array followed by the position in square brackets. To get element 0, we would write words[0], which would produce the word “glass”.

Each array has a length (i.e., number of elements) and this value can be found by writing words.length. The dot . indicates that we are accessing a property of the array. Here, words.length is 8, because there are 8 words in the array. So although the length of words is 8, its elements are numbered 0 to 7.

for loops

Another new part of script 1.3 is the for loop, which tells the system to loop back over a portion of the instructions (the part inside the curly brackets that follow) for a specific number of times, before continuing onto the rest of the script:

 8 for (i = 0; i < words.length; i = i + 1)
 9 {
10     text(words[i]);
11     await(2000);
12     clear();
13     await(1000);
14 }

In plain English, line 8 would say: “Start with i equalling 0. If the current value of i is less than the length of words, do the steps in the curly brackets below, then add 1 to the value of i.”

The index of a variable refers either to the current or to a specific value of the variable, depending on what kind of variable it is. Because i is a counter variable with only one value at any given time (that keeps track of how many times the loop has run), index i refers to its current value. The words variable, on the other hand, contains an array, so “words index 0” (written “words[0]”) would refer to the specific value in position 0 of the array, i.e., the word “glass”.

Parts of a for loop: Head and Body

The for loop always has the same shape: it has a head and body. The head always looks like this, with three arguments separated by semi-colons:

for (initialization; condition; step)

The body is the part in curly brackets:2

{
    ...
}

In the head, the initialization sets variable i to its initial value (in this case, i = 0).

The condition indicates when to stop. In this case, the condition says i < words.length. Since the variable words contains an array of 8 elements (words), words.length is 8. Therefore, as long as the count of words that have been presented is 7 or below, the loop will run again (recall that word counting started at 0). When the loop reaches the point that i is 8, the condition will be false, because 8 < 8 is not true. Since 8 < 8 is not true, the statements (i.e., lines of script) in the body will not be executed anymore, and the system will move on to processing the next line of script after the closing bracket of the body.

The step part of the head of the for loop determines how the index (i.e., current value of)i will be changed after each repetition of the loop. In most cases, it is increased by 1 (called an increment). In other situations, it can be more useful to decrease it by 1 (called a decrement) or to change it in some other way.

It is important to know that even if, for some reason, you do not want to specify part of the head, you still need to write both semi-colons. This way, the system knows whether to interpret the statements as the initialization, the condition, or the step. For example, if you chose to initialize the index of i before the loop, which is perfectly fine, you would still need to put in both semi-colons, as follows:

var i = 0;
var m = words.length;

for (; i < m; i = i + 1)
{
    // body
}

Increment

In JavaScript, the expression i = i + 1 has a short-hand form which can be used instead: ++i. This step of “plus 1” is known as increment3 i. Similarly, --i is short-hand for i = i - 1 and is known by decrement i. These short-hands do not achieve anything different; they are simply more concise forms.

Declaring Multiple Values at Once

Another short-hand exists for declaring several variables in a row. When several var expressions follow each other, you can combine them using a comma and only write var once, as follows:

var i, m = words.length;

Given that white space (such as spaces and newlines) is irrelevant, you may find the following layout, which has the same meaning, easier to read:

var i,
    m = words.length;

Using the shorthands of increment and declaring multiple values at once, the whole loop would then look like this. This is the format of the for loop that you will see most often:

var i,
    m = words.length;

for (i = 0; i < m; ++i)
{
    // body
}

Script 1.4: Even shorter scripts with getwords()

Script 1.3 is already a rather mature script that can easily be maintained. Many people, however, prefer to go one step further and separate their scripts from their stimuli. In the case of words (or sentences, or sentence fragments), NeuroTask offers the function getwords() that allows you to do this easily. The procedure is to first make a text file (i.e., with the extension “.txt”) that contains the stimulus words without quotation marks, but separated by commas. Spaces after each comma are optional. For example, the file called words.txt might contain:

glass,chair,train,balloon,horse,curtain,pencil,baker

This file must be uploaded with NeuroTask’s Upload Files feature (found at this icon: ).

Uploading or linking of stimulus file words.txt.

You can verify whether the upload was successful, because if it was, the filename will appear in the Quick Reference side panel next to your script (look under Files and Stimuli: Text Files). If you see it, it means it has been successfully uploaded and is ready to be used within the script.

Uploading or linking of stimulus file words.txt.

The script then becomes:

Script 1.4. Free recall task like Script 3, but now using getwords().
 1 var i,
 2     words = getwords("words.txt");
 3 
 4 text("Try to remember the following words");
 5 await(5000); // 5000 ms or 5 s
 6 
 7 for (i = 0; i < words.length; ++i)
 8 {
 9     text(words[i]);
10     await(2000);
11     clear();
12     await(1000);
13 }
14 
15 text("Now, count back in threes starting with 307: 307, 304, 301,...");
16 await(20000);
17 largeinput("Write down the words remembered","words_remembered");
18 text("Thank you for participating!");
19 await(3000);

The benefit of using this method is that you can upload a file with different words at any point. Also, the words do not need to be wrapped in quotation marks to make them strings, which can save time when a word list is quite long. Additionally, files may be shared among scripts, which is relevant for researchers running multiple studies or conditions that use the same stimuli.

1.4 You have started with online experiments!

Even though the scripts in this chapter have been short and simple, they were not trivial. The Brown-Peterson script could serve as a demonstration for students, especially if it were repeated a few times with different letter stimuli. The free recall script could be used as an excellent basis for, well, any type of free recall experiment. Both types of scripts record the behavior of the subjects using the input field. In the next chapter, we will look at how you can record other types of behavior and measure reaction times.

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 simply e.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.

Script 2.1. Lexical decision experiment.
 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:

Script 2.2. Measuring a reaction time with 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:

Script 2.3. Measuring a reaction time with the 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:

Script 2.4 Measuring a reaction time with a 3 s timeout.
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:

Script 2.5. Checking for timeout with an 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:

Script 2.6. Counting slow key presses.
 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.

Script 2.7. Complete lexical decision experiment.
 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.

3 Screen layout with “Box” and “Block

3.1 Maintaining Experimental Control in an Online Setting: Layout

When a psychological experiment is conducted in the lab, the experimenter has full control over all hardware used: type of keyboard, computer, mouse, screen, etc. On the web, things are a little different. In this chapter, we discuss variations in screen and window size, which will likely vary considerably from subject to subject, unless you test them in the lab.

Nowadays, subjects may use screens that range in size from high-resolution ultra-wide monitors, to tiny mobile phone screens and even round smartwatch screens. New formats come out continually. How your experiment is viewed by each subject will vary not only due to the size of their screen, but also due to the size of the window on that screen. Even on a large screen, your experiment may still be run in a tiny window.

This presents a problem for psychological experiments, which require stimuli to be presented in specific positions with precision and accuracy. How can you be certain your stimulus layout will stay exactly the same? What happens when users scroll down or zoom in on your page? What if they are on a tablet and turn it from vertical to horizontal, which gives a quite different layout? NeuroTask has taken measures to help solve these issues.

3.2 Two Standard Layout Choices in NeuroTask: Fill and Square

To deal with the huge range of screen formats (and with variable window sizes), NeuroTask experiments use one of two types of layouts: square or fill. These layouts greatly simplify positioning stimuli on the subject’s screen. The square layout, which we will use by default throughout this book, limits the area in which your experiment is shown to the largest square that can be drawn across the window in which your experiment is being run. This offers the benefit of a constant layout, at the expense of not using all the space available. The fill layout does fill the whole window. Neither layout allows scrolling1. We will first discuss the fill and then the square layout.

The fill layout

The fill layout makes most efficient use of the display (screen or window) real-estate. An important drawback to consider is that the displays your subjects use can vary greatly, from extremely short and wide to extremely tall and narrow. It is possible to design webpages that adjust themselves to such extremes, for example, by using a framework like Twitter Bootstrap. This is the styling framework we use for the NeuroTask website and we highly recommend it. This fluidity is fantastic for webpages, but for experiments you almost always want to control where screen elements go. Therefore, for optimal control over your experiment’s display, we strongly recommend the square format. In the end, it is up to you if and when you want to use the fill format. For example, you may want to use it when showing instructions or when presenting survey questions.

The square layout

This layout automatically determines the largest square that can be drawn in a window. On a wide screen (or, rather, wide window), the square will leave areas to the left and right unused, called the background. If the screen (or window) changes, the square will automatically adapt its size. Anything drawn or shown in the square layout will always keep the same relative positions and proportions, even when resizing a window or when a tablet is turned sideways; within the square, things stay the same, relatively speaking.

3.3 Cross-Browser Layout Issues

Font size

When a subject increases the screen or window size, such that the square layout increases in size, the font should (and does) increase in size as well, to remain suitable to the display size. With a square layout, the font increases proportionally to the size of the square. With a fill layout, it is less straightforward. What makes most sense when a subject stretches a small square window to a wide band of triple the width but still the original height? We have chosen to make font size proportional to the diagonal of the window, a compromise which prevents the font from becoming uncomfortably large when a window is only large along one dimension, but which still allows a suitable font size increase when a window is made larger along both dimensions.

Zooming and Text-Sizing

Another potential problem with subjects using their own choice of browser is that, ordinarily, users can zoom in or out on webpages. On many browsers, pressing Ctrl+ makes the whole page larger. On some browsers, you can set just the text font size larger or smaller, while images and other elements on the page remain the same size. NeuroTask overrides and disables zooming and text sizing options. This guarantees, insofar as it is possible, that the screen layout of your stimuli remains as intended.

Font Type

Beyond font size, font type can be problematic for online display as well. Different browsers may show the same text in slightly different implementations of a specific font. NeuroTask’s text() function displays text in Arial font by default. We chose Arial as the default because, compared to other fonts, it is most consistently supported across different browsers and shows the least variation.

The NeuroTask framework strives to show text and other stimuli as similarly as possible on different browsers and types of hardware. This is not as easy as it may seem. For example, one browser may always show a certain font 15% larger than another browser. We strive to correct for such differences, such that only upon close inspection will you see any variation between different browsers’ renderings of a given text.

If you do not want to use Arial, it is possible to use other fonts. In fact, you can do all sorts of arbitrary styling of text (including font type, weight, color, and other aspects). This is explained in greater detail in Chapter 5 called “Style”. If you do decide to play around with text styling, bear in mind that some stylings may show considerable cross-browser variation, and that unless participants are completing your experiment in your lab, use of different browsers may be out of your control.

Centering Text and Images

In many experiments, stimuli are shown in the center of the screen (often in a large font). As it turns out, it is surprisingly cumbersome to center text (or images) both vertically and horizontally on a webpage in a way that works on all browsers. It is not rocket science, but it is also not something with which you should have to concern yourself. As such, NeuroTask has built the text() function to contain the code necessary to achieve effective cross-browser centering by default.

3.4 The Backbone of Layout in NeuroTask: Box and Block

After this fairly long introduction to the potential challenges of screen layout, you may wonder how you can make optimal use of the solutions we have developed (beyond using the defaults: square layout, Arial font, and the auto-centered text() function). The answer lies in the use of two features, Box and Block, which allow for precise relative screen layouts.

With a box, you define the layout and color scheme for your entire experiment. Most experiments only have one box and in fact use the default, predefined box called “main”, which is what we recommend. Many blocks can fit inside a box and can be used to show one stimulus in one area of the display, while simultaneously showing other things (e.g. an overarching instruction, buttons, etc.) in other areas of the display. Experiments often have many blocks. We will now discuss each of these in turn.

3.5 Box

The Pre-set Box Called “main”

A box is a large area in which you display the various components your experiment (e.g., instructions, questions, stimuli, images, etc.). As discussed in section 3.2, NeuroTask automatically presents your experiment in the largest square that can be drawn across the subject’s display. It does this by automatically creating a maximally-large, white (or rather, transparent) box with no border. In other words, by default, there is always one box already present in every NeuroTask script, called “main”. When you create a short script with no mention of boxes, your experiment is actually being displayed within “main”, the maximally-large, default box.

For most experiments, you’ll only need this single box. It is possible to create additional boxes, but we don’t recommend doing so unless you are an expert programmer. One reason for this is that many popular functions (such as input() and largeinput()) don’t layer, so subjects would not be able to click or type within the text input field if there was a transparent box on top blocking it. The only occasion to make a second box is if you insist upon making a box that fills the entire screen (but to do so, it is important to have a very strong grasp on style).

main” is the Default for Many Box Functions

Given that a box (main) exists by default, several box methods can be called without actually specifying a box. In other words, if you don’t specify a box when doing any of the many actions that can be done to boxes, the action will be applied to main by default. For example, to add a block (detailed in the next section) within main, you could simplify the standard main.addblock(...) script to just addblock(...).

Customizing Boxes

One benefit of the fact that your scripts automatically run within a box (specifically, within main) is that it’s possible to make stylistic changes to boxes. You may specify the color of the box (i.e., the main stimulus area) and of the surrounding unused area (i.e., the background). You may also specify a margin around the stimulus area, as well as a border of a certain color and thickness. You can set the display manner of a box as square or fill, discussed in section 3.2. In order to make changes to a box that has not yet been defined, you must first declare it as a variable, like so:

var main = new Box();

But don’t do this in your script! This has already been done for you. Only declare a new Box if you really need and know what you are doing. As it stands, the box (main) would remain invisible (i.e., has no discernible border or fill color), but still functions as a specific, delineated area in which to display your experiment.

Lightgrey/#d3d3d3 Box

Box Color

Often, you will want a box with a specific fill color, such as light grey. Colored boxes can be achieved in two main ways: by using the names of colors, or by using HEX color codes. A HEX color code is a six-character combination of letters and numbers that can specify a precise color by describing it as a combination of different amounts of red, green, and blue. If none of NeuroTask’s predefined colors are quite right for you, this is a great way to achieve your exact vision. In the chapter on Style, we discuss the various ways in which you can specify shades of colors further.

To make main light grey (rather than its default transparent color) using the color name method, you would write:

var main = new Box("lightgrey");

To make main light grey (rather than its default transparent color) using the HEX code method, you would write:

var main = new Box("#d3d3d3");

Box Borders, Backgrounds, and Fonts

Sky Blue Box with Orange Border

You can make your box more polished by adding color to the unused background area, adding a border, adding a margin, and setting a default font style that will be used throughout your experiment unless otherwise specified. Take this example:

var main = new Box("skyblue","grey","red",
                   100,40,"orange",2);

Let’s go through the elements of this box in order. This script fragment creates a sky blue box with a grey background (i.e., unused areas outside the square box). Throughout the experiment, text will automatically appear in red font that is 100% as big as the standard font size (i.e., is the standard size). There is a 40-pixel-wide orange border. Finally, the box has a 2% margin inside which everything is set, to prevent things from “touching” the edge of the box. Having both a margin and a border can provide a nice aesthetic (as demonstrated in the grey box shown next), but if you only wish to have one, you may fill in “0” for the other.

White Box with #f3f3f3 Border

A more elegant, grey-scale box could be created with the following script fragment. Here, we see that it is possible to mix and match predetermined Javascript color names with HEX color codes to achieve a desired look. This creates a white box with a very light grey (#fafafa) background. Font color is black and font size is the standard 100%. There is a 5-pixel-wide grey (#f3f3f3) border, set inside a 5% margin.

var main = new Box("white","#fafafa","black",
                   100,5,"#f3f3f3",5);

It is common in experiments to want a subject’s entire display to appear black, except for your stimuli. In this case, we suggest making both the box and the background (unused area) black. In doing so, you can achieve total screen coverage while still using the square rather than the fill layout (and thus still benefitting from the automatic resizing that the square layout offers, described in section 3.2). This script fragment creates a black box on a black background with white font. We will come back to this layout in section 3.7 on the makebox() function. Note that in this example, we are creating a new box called “black_box” rather than altering main. This is only recommended for expert programmers. Most users would do better to make the same specifications to var main = instead, but we include this example for variety.

var black_box = new Box("black","black","white");

So, you now know about Boxes, keeping in mind that you should rarely have to declare one yourself. It is much better to alter the existing main Box with the functions given in the “Style” Chapter 5, for example, to change the background to black with a white font.

3.6 Block

Blocks can be used to organize the space within a box. For example, in many experiments (such as the Simon or Posner Task), the subject must fixate at a cross in the middle of the screen while letters are shown to the left and right of the cross. In other experiments, stimuli may be presented in any of the four quadrants of the screen or in other configurations, such as circular arrangements. To manage such locations, we have created the block: a rectangular portion of a box. You can put as many blocks in a box as you want, adding or taking them away at any time throughout your experiment.

Adding Blocks

Adding a block to your box is done with the addblock() function, which is written like this:

var b = main.addblock(10,25,80,50,
                      "yellow","Display text");
Yellow Block with text

To create this block (which we have named “b”), we first indicate which box to create it within (main). Because main is NeuroTask’s default box, this script would also work if simply written “var b = addblock(...”, however we have included the complete version so that this script may serve as customizable example. 10 indicates that the block starts 10% in from the box’s left side. 25 indicates that the block starts 25% of the way down from the box’s top edge. 80 indicates that the block then spans 80% of the box’s width (i.e., leaving another 10% to the box’s right edge). 50 indicates that the block spans 50% of the height of the box (i.e., also leaving 25% of the box’s height below it). The last two arguments are optional, and they indicate that the block should be yellow and that it should read “Display text”.

This script fragment has created a block (b) that is centered within the box main. There is an easier way to achieve a centered block that doesn’t involve doing the arithmetic to work out that an 80%-wide block should have 10% on both sides in order to be centered, or that a 50% tall block should have 25% above and below in order to be centered. The addblock() function accepts “center” instead of a number as its first two arguments. The following script fragment achieves the exact same thing as the fragment above:

var b = main.addblock("center","center",80,50,"yellow","Display text");

You may also simply specify that you want your block to directly touch any of the four box edges. NeuroTask accepts “left” and “right” for the first (default left) argument, and “top” or “bottom” for the second (default top) argument. For example, the following script fragment would create a small square block that is 10% wide and 10% high, and that is positioned in the very bottom-right corner of the box main.

x

1 var b = main.addblock("right","bottom",10,10);

Removing and Destroying Blocks

In some experiments, you may want to create a bunch of blocks (i.e., block objects), delete them, create some others, and so on. Since blocks always exist within a box, they may be created and gotten rid of using the addblock() and removeblock() functions. These functions are usually connected to the name of the box using the ‘dot’ construction, though this part can be omitted if the box in question is NeuroTask’s default box, main. In the following example, we first create a block “b” in box main, and then remove it.

var b = main.addblock("center","center",80,50,"yellow","Display text");
    // [Here you'd do something with the block, e.g., show text/an image within it]
main.removeblock(b); // Then you'd remove the block by writing this

Removing vs. Destroying

Destroying a block permanently and completely deletes it. That is, any contents (such as text, images, graphics, or video players) are deleted and their resources (notably, memory) are freed so they can be reused by your script. This is a healthy practice if you are writing a long script that involves lots of memory-heavy media.

Removing a block also permanently destroys it, unless you add an extra parameter, “true”. By adding the parameter “true”, the block is only temporarily taken off the display. This is useful if you want to reuse it later or reassign it to another box. For example, you might want it to temporarily disappear from the display in between sets of experimental trials, while you give new instructions, then have it reappear for the next set of trials. It is, of course, simpler to call back existing blocks than to recreate them (and a shorter script is, on principle, better than a longer script, both in terms of being more readable and less error-prone). To reassign a block to another box, use the pushblock() function. In the following script fragment, we are removing block b from box main and adding it to a box Q:

main.removeblock(b,true); // removes block b from main, but doesn't destroy it
Q.pushblock(b); // re-establishes block b inside box Q

A block can always be destroyed by calling the destroy() function. For example:

main.removeblock(b,true);	// b is taken out of main/off the screen, but still
				// exists off-screen and can be brought back.
// [You'd do other things here, with Block b handy if needed]
b.destroy();    		// b removed from screen AND permanently destroyed.

Example Block Layouts

In this section, we provide script fragments for two commonly-used block layouts. Feel free to use these in your scripts. Both layouts only occupy 90% of the box’s height and 90% of its width. A 5% buffer inside the box’s perimeter makes text look nicer by preventing it from touching right up against the box’s sides. These block layouts are created within the box main which, by design, covers the largest possible square that can be drawn on a subject’s display (i.e., by definition touches either the top and bottom or left and right sides of the subject’s display). As will become apparent, it is quite easy to construct a block layout that suits your needs.

The first example layout is a single, centered, square block. It is handy for instructions, survey questions shown one at a time, and for showing images or words in the center of the screen. The script for this layout is:

var centerblock = main.addblock("center","center",90,90);
Centered Example Block Layout (Colorized for Demonstration)

The second example layout covers the exact same space as the centered, square block, but is made up of three smaller blocks: a header, center, and footer block. This layout is handy for using the top of the screen for messages and feedback, the middle for stimuli, and the bottom for, say, a button. Note that using these numbers for the last and third-to-last parameter of each block makes our first block start 5% down from the box’s top, then divides the 90% of the box’s height that we want to utilize as follows: a quarter for the header (22.5), a half for the center (45), and a quarter for the footer (22.5).

var headerblock = main.addblock("center",5,90,22.5),
    centerblock = main.addblock("center",27.5,90,45),
    footerblock = main.addblock("center",72.5,90,22.5);
Header-Center-Footer Example Block Layout (Colorized for Demonstration)

These proportions can be changed easily to fit your needs. For example, if you wanted more space in the middle with a narrower header and footer, you need only remember that the third-to-last argument indicates how far down from the top the block starts, and the last indicates how long it extends. So for a narrower header and footer, you could write “...,5,--,15”, “...,20,--,60”, and “...,80,--,15”.

Showing Text in Blocks

Once you have a block in place, you can show text within it. This is most easily done using the text() function and the dot “.” construction that we have encountered before, most recently earlier in 3.6. The following script displays text in the blocks created using the header-center-footer example layout. Notice that we do not have to create main as it is present by default:

1 var headerblock = main.addblock("center",5,90,22.5),
2     centerblock = main.addblock("center",27.5,90,45),
3     footerblock = main.addblock("center",72.5,90,22.5);
4 
5     headerblock.text("This concludes the experiment.");
6     centerblock.text("Thank you for participating!",200);
7     footerblock.text("Powered by NeuroTask",40);

Pictured beneath, this script above displays some text in each block in standard, double, and smaller (40%-of-standard) font size, respectively. By default, everything is centered in its block, but this can be changed by altering the display style, as will be explained in the chapter on Style. The colorized image below demonstrates how the blocks within main dictate the text placement. The uncolored version is how it would appear to a subject (unless color was added to the blocks and box background).

Text shown in the same block layout: with color added (left) and left transparent (right)

For the web experts among you, it is important to know that you are not limited to plain text. NeuroTask accepts any type of HTML2 as text.

Showing Images in Blocks

You may also display images within blocks using the setimage() function below. Two things are important to note: 1) The preload() function, described further in section 4.4 (Preloading Images), is crucial as it prevents slow mid-experiment image loading that may potentially occur if a subject uses a slow computer, and 2) Images must be uploaded to the Files and Stimuli section of your experiment’s Quick Reference Panel, as described in section 4.2 (Image Linking and Uploading), for the setimage() function to work. Provided you had uploaded an image called “cow.png” to your script, the script fragment below would show the photo in the center of the screen (within the block “centerblock”):

var centerblock = addblock("center","center",90,90);
centerblock.setimage("cow.png");

If you have several blocks, each can contain its own image, which can be changed arbitrarily. It is not possible to combine text and images in one block, at least not with the text() and setimage() function; use separate blocks if you need to do this (e.g., an image in one block, with a word in another block below it).

3.7 Combining Box and Block: the makebox() convenience function

If you don’t want to make your own blocks or use either of our example layouts (above), NeuroTask also has a convenience function called “makebox()”. The makebox() function’s first argument specifies a block layout. It supports two options: “centered” and “threerows”. “Centered” creates one block 90% of the box’s height and width, exactly as in the example layout in the previous section. If no argument is entered, “centered” is the default. “Threerows” creates a similar layout to the example in the previous section, except that it’s blocks span 100% (not 90%) of the box’s height and width. So, instead of writing:

main.headerblock = main.addblock("center","top",100,25);
main.centerblock = main.addblock("center","center",100,50);
main.footerblock = main.addblock("center","bottom",100,25);

You could just write:

main = makebox("threerows");

To show text within the blocks created by the makebox() function, use the “dot” construction that we’ve encountered before to specify which box and block the text should be shown in. We recommend using the makebox() function to add blocks within the preset box main (as we’ve done above), rather than creating a new box (detailed in 3.5). The “centered” option’s block is called “centerblock”, and the “threerows” option’s blocks are called “headerblock”, “centerblock”, and “footerblock”. To write an instruction in the header, you would write:

main.headerblock.text("Please stay focused and respond as quickly as possible.");

This message would remain on the screen inside until such time as you cleared it. In other words, you could proceed to display an entire reaction time experiment within the centerblock, all while your reminder to stay focused and respond quickly is displayed at the top of the screen. Then, when no longer need it, you would clear your reminder by writing main.headerblock.clear();.

The makebox() function’s second argument specifies a color scheme. There are six supported options for the second argument. If no second argument is entered, “white” is the default. “White” makes the entire background white (blocks, box, and background outside the square box) and the font black. Experimenters often want to display white text on an all-black screen (blocks, box, and background), which can be achieved with the second option, “black”. The third supported option is “light” which creates the grey-on-grey layout shown in the last image in section 3.5 (captioned “White Box with #f3f3f3 Border”) with black font. We recommend using one of these first three options, which maintain the recommended square layout (explained in section 3.2). Those who want to use the fill layout can do so using the other three supported options: “wide-white”, “wide-black”, and “wide-light”. So, to show images or (white) text at the center of a completely-black screen, instead of writing:

main = new Box ('black','black','white');
var centerblock = main.addblock("center","center",90,90);

You could just write:

main = makebox('centered','black');

In the Appendix, we provide the code that supports the makebox() function, should it interest you. We may support additional layout and color schemes in the future, should there be interest.

3.8 Deleting All Contents of a Box

When you no longer want the contents of a box object (i.e., the blocks you’ve laid out within it), you can call the following script fragment. This removes and destroys all blocks and other contents, if any. The box can now be re-used and filled with new blocks using addblock().

box.clearall();

3.9 Using Blocks as Stimuli

While block layouts are traditionally used as locations to display stimuli such as text and images, they can also be used as the stimuli themselves. We will show some examples of this usage of block layouts, starting with the Corsi Block Tapping Task.

Corsi Block Tapping Task

The Corsi Block Tapping Task is a classic neuropsychological test. When done manually, the neuropsychologists lay out blocks over a table in a more or less random arrangement. They then tap select blocks in a certain order, and the subject (or patient) must replicate it, tapping the same blocks in the same order. The neuropsychologist starts with a short sequence, increasing it until the subject starts to make errors. The longest sequence before that is taken as the subject’s ‘span’, which, in most people, is five to seven blocks.

A computerized version shows blocks on the screen, which ‘light up’ in a certain sequence. In the Appendix, we present a complete script for such a task. Here, we will only study how to create such an arrangement of blocks on the screen, as follows:

Script 3.1. Corsi Blocks layout.
1 var coords = [[10,10],[20,40],[20,80],[40,30],[45,60],
2              [60,10],[60,75],[70,50],[80,20],[85,45]],
3     i;
4 
5 for (i = 0; i < coords.length; i++)
6 {
7     main.addblock(coords[i][0],coords[i][1],10,10,"lightgrey");
8 }
Corsi Blocks display with ten blocks produced by the script fragment

The variable “coords” lists pairs of numbers that will come to be the coordinate of the top, left corner of each block.

The for-loop creates the blocks. Each time the loop repeats (i), it focuses on a different pair of coordinates until it has created all ten blocks.

We use an array of arrays here: coords = [[10,10],[20,40],...]. In JavaScript you can put anything in an array, including other arrays. To get to a value in an array of arrays requires two steps: First, we specify which element we want in the overarching array, then (since that element is itself an array), we specify which element we want within it. Since Javascript numbers the elements in an array starting at 0, not 1, “coords[0][0]” equals 10, because the first zero points to the first set of coordinates, and the second zero points to the first number within that pair. Similarly, “coords[1][0]” equals 20 and “coords[1][1]” equals 40.

Therefore, when i is 0, the for-loop begins a block 10% in from the left edge of main, and 10% down from the top edge of main. The block then extends 10% over and 10% down. The last three arguments (10,10,"lightgrey") indicate that all blocks will be the same size and light grey.

Random Dot Stimuli

The same approach can also be used to generate random dot displays. These have been used by many experimenters to study memory, including by Posner and Keele (1968). In this experiment, a prototype dot configuration P was defined and various distortions P1, P2, …, Pn, were made by randomly shifting dots around. Subjects were presented with half the distortions and encouraged to remember them. They were not told the patterns were in fact all distortions of a single prototype pattern. After some time, they then viewed all distortions with the prototype intermingled; their task was to say whether they had seen a specific distortion before. The crux of this experiment was that subjects were very likely to falsely recognize the unseen prototype P as among the patterns seen earlier.

A small change in the script fragment of the Corsi task, produces a random dot display:

var coords = [[10,10],[20,40],[20,80],[40,30],[45,60],
                  [60,10],[60,75],[70,50],[80,20],[85,45]],
    block, i;

for (i = 0; i < coords.length; i++)
{
    block = main.addblock(coords[i][0],coords[i][1],10,10);
    block.text("&bull;",200);
}
Random dot pattern produced by the script fragment

Here we see the code (&bull;) used to display a round bullet point, •, which does not have a key on most keyboards. The code for this and for other symbols can be found online on one of many websites on HTML. You can use these so-called HTML ‘entities’ (symbols) directly in your scripts by copying either the entity name or the entity number. (You can also do a web search for HTML entities to find overview lists). We could also have used an ordinary dot in this script, “.”, but this appears as a tiny square in the Arial font, which we found less appealing than a nice round bullet.

Additional code (to distort the patterns, add a presentation phase, and add a recognition phase) would be necessary to make the whole experiment work. Here, we only cover how to create the blocks for such a task. In the Appendix, we provide the additional code, combining subject matter from the entire book into complete experiment scripts.

4 Images

Many psychological experiments use images as stimuli. NeuroTask Scripting allows you to upload your own image stimuli and present these in experiments. In this chapter, we will show you in some detail how to do this. Showing an image on the Internet is simple, but showing them for exact time periods, like 300 ms, requires some extra precautions. Fortunately, the NeuroTask Scripting framework gives you a solid base from which to work taking care of many details that could compromise your presentation times if not handled properly.

Suppose you have an image called ‘cow.png’ (png is a popular image format on the Internet; others are jpg and gif). To show it for 1 s and then hide it, you can use the following script fragment:

setimage("cow.png");
await(1000);
hideimage();

This will show the image for 1000 ms in a square that fills the entire center middle of the screen. If you want to show the image at a smaller size, say 50% smaller, write setimage("cow.png",50).

It is also possible to show images by providing an web address (URL) of an image somewhere else on the web, for example,

setimage("https://placekitten.com/200/200");
await(1000);
hideimage();

This will show a random image of a cat, which is provided (for free) by placekitten.com. If you would run this code for the first time, you might see a visible delay during which the image is loading and not yet visible. Such a delay can be considerable, such that the image is not shown for the intended 1000 ms but for far less, for example, only 326 ms. This all depends on the speed of the Internet connection and how busy the server is of the remote website from which you require the image. Strangely enough, it may also depend on when the browser deems it time to show the image; it may hold an image for hundreds of milliseconds before showing it, for reasons unknown.

To make sure all images are ready to be presented without unnecessary delays, we will introduce preloading of images. This ensures that an image is shown (almost) immediately, and not after a varying delay period. But first we will go over the basics of showing images, which we will discuss while developing a new experiment script.

4.1 Visual recognition task

As a case-study, we describe a complete visual memory task, where we first show a number of images, called targets, for a certain duration (here 1 s). Then, the targets are mixed with a number of new, unseen images, called foils. The combined images are then shown to the subject in random order and the task is for each image to determine whether it was seen before or not.

In the example script below, there are only three targets and three foils. In a real experiment, you would want more of both of course. As responses, we collect the total number of target images correctly identified as ‘old’ (or ‘seen’); these are called hits. We also collect the total number of foils that were incorrectly identified as ‘old’; these are called false alarms. You might normally also collect reaction times and other data, but we want to keep this script as short as possible. In the Appendix, we will present a similar script that has been extended with some extra features. But even this simple script can be used to collect useful data, provided you have suitable images.

The hits and false alarms measures are important in psychology, because with only these, it is possible to derive an unbiased estimate of the subject’s recognition memory. This estimate is called d’ (say ‘d-prime’) and the theory behind it is called Signal Detection Theory. A useful alternative to d’ is the measure simply called A.

A minimal visual recognition memory script is as follows:

Script 3.1. Visual recognition memory script.
 1 var targets = ['car1.png','car2.png','car3.png'],
 2     foils = ['car4.png','car5.png','car6.png'],
 3     all = [],
 4     hits = 0, false_alarms = 0,
 5     i, e;
 6 
 7 // Here we should put preloading code, see below in this chapter
 8 
 9 instruction("Study the following images");
10 
11 for (i = 0; i < targets.length; i++)
12 {
13     setimage(targets[i]);
14     await(1000);
15     hideimage();
16     await(1000);
17 }
18 
19 instruction("For each of the following images, <br />"
20     + "press 's' for 'seen' or 'old'<br />"
21     + "or 'l' for 'unseen' or 'new'");
22 
23 all = all.concat(targets,foils);    // Add all image names to array 'all'
24 shuffle(all);                       // Randomize image order
25 
26 for (i = 0; i < all.length; i++)
27 {
28     setimage(all[i]);
29     e = awaitkey("s,l");         // 's' is 'old', 'l' is 'new'
30 
31     if (contains(targets,all[i]) && e.key === 's')
32     {
33          ++hits;
34     }
35 
36     if (contains(foils,all[i]) && e.key === 's')
37     {
38         ++false_alarms;
39         }
40 
41     await(1000);
42 }
43 log(hits,"hits");                      // Log data into your account
44 log(false_alarms,"false_alarms");
45 
46 text("Thank you for participating!");  // 'debriefing'
47 await(3000);

For this particular script to work, you would first have to upload six images of cars, called ‘car1.png’ to ‘car6.png’. Uploading is explained in the next section.

There are a few new functions used in this script, concat(), contains(), and log(), which we will explain in turn.

concat(array1,array2)

We already encountered this function in the previous chapter. Each array (even an empty one) has this function, which takes as arguments one or more arrays, of which the elements are added (copied) in order. So

var a = [0], b = [1,2,3], c = [4,5,6];

a = a.concat(b,c);

would change a from [0] into [0,1,2,3,4,5,6].

In the script above, we use concat() to merge the targets and foils into a single array, which is then randomized with shuffle(). It is important to know that a.concat() does not change array a but only returns the changed array. Here, we reassign the return value to a, replacing its original content, which was [0].

contains(array,element)

This is a handy function specific to NeuroTask (borrowed from StratifiedJS). The first argument is an array and the second an item that may or may not be in the array. For example,

contains([1,2,3],1);    // Returns true
contains([1,2,3],7);    // Returns false

In the script, we use contains() to check whether the current stimulus is a target or a foil.

log(variable,label)

NeuroTask-specific function that stores data to your NeuroTask account (and which can then be viewed and downloaded in the Data section of your account). The first argument is the variable in the script which value you want to log (the name of the variable itself is not logged). The second argument must be a string: the name or label you want to give the variable in the Data section of your account.

log(false_alarms,"FA");

will log the value of the variable false_alarms into your account with the label ‘FA’.

Logged data is marked with a (randomly generated) ID that identifies the experimental session; all logged data in that session will have that same ID. Each data item is also given a timestamp, so you know exactly (data/time, with seconds precision) when each piece of data was stored. For more information, see the chapter on data storage.

If you log values with the same label repeatedly, as in log(34,"FA") and then later log(17,"FA"), both values will be retained in your Data section, with the same label “FA”. It is up to you to make sure you understand what this means.

It is assumed in this script that you will do the further analyses (e.g., of d’) later, though it would have been possible to add this step to the script as well. In most cases, it is preferable to have all the raw data, and we would for instance want to store the misses and correct rejections as well. It is generally better to store too much data than too little.

4.2 Image linking and uploading

As illustrated at the start of this chapter, in NeuroTask you can either show your own images, uploaded to your NeuroTask account, or display images from elsewhere on the web, using web addresses (known as URLs). Bear in mind that displaying images on other web sites may not be legal and NeuroTask assumes no responsibility for it whatsoever. There are, however, many websites that provide free images that you may use in this manner.

Uploading images

Uploading images to your NeuroTask account is done in the script management part of your account that is called Upload and Manage Files. Just check the script menu item that has a cloud icon with a little up arrow like this: . On the Upload webpage, you can click the ‘Upload Files’ button and then select image files stored on your computer.

Section where you can upload and manage your files

On most modern browsers you can also simply drag the selected files on the area below and around the Upload button. Or you can select files with the Open dialogue:

If you hold the Ctrl-Key, you can select multiple-files
List of files to upload

When all files you want to upload are listed correctly, you are not done yet: You must click the large orange Upload button to actually transfer the files! If you forget this, the uploaded files are simply ignored and nothing happens.

Click the Upload button to start the transfer

After you click ‘Upload’, there may be short wait during which you will see a message in green:

Message seen while upload is in progress

When transfer is completed, the uploaded images are stored in your account and tied to your script. You can verify this by going to the script edit page of your script and clicking on the Image Files header in the Quick Reference - Files and Stimuli section. This should reveal all images in your script, showing both the images and the file names you gave them (these can currently not be edited in NeuroTask, so pick good names before you upload your images).

Image files are now uploaded and available to your script

Linking images

Suppose, you have a second script and want to re-use images already uploaded to the first script. When you click on the Image Files header of your second script, you will not be able to see any images. Existing, already uploaded image files (and other files) first need to be linked to a new script. This linking is also done in the Upload and Manage Files section. This time click on Stored Files, which should reveal a list of all files uploaded and stored (not just image files are shown).

Stored Files button to link to already uploaded files

When you hover over an image (or other file) in this list, a green +-button appears. If you click it, the file will be linked to your script. When finished, you must click the large orange Upload button to complete linking of the files! Now, you should be able to see the linked files underneath the Image Files header in the side panel.

Uploading versus linking images

You can mix uploading and linking files, as the difference is not essential for how your experiments appear to your subjects. If, however, you delete a script, all uploaded files that have been uploaded to the script will be deleted as well. That means that other scripts that use such a deleted image (i.e., link to it), can no longer access it; it is removed from your account. If an image file is merely linked to a certain script, it will not be removed when you delete that script.

In NeuroTask, deleting scripts is discouraged and we recommend merely archiving a script. This means it is removed from the main script listing and moved to the script archive. From the archive it can easily be unarchived if desired (just set its Status back top Open in the Script Settings panel, the one that has the ‘cog’ symbol). Archiving a script in this manner does not cause deletion of files uploaded to it. In fact, archiving instead of deleting scripts reduces the risk of malfunctioning scripts due to inadvertently deleted stimuli.

Once a script has been archived, if you really want to, it is possible to remove it permanently by clicking on the red delete cross. This will lead to loss of all files that were uploaded to that script, but not the ones that were merely linked to it (and hence had been uploaded to a different script).

4.3 Where your images are stored

Your images are stored in the cloud. We are currently using Amazon’s S3 storage service for this. This means that when subjects participate in your experiment, the images they see on the screen must first be retrieved from Amazon. Amazon’s storage is a very large and very fast. But even so, it may take some time for your image to be available at a subject’s computer, as it would from any other other location on the web. In the next section, we’ll discuss how to deal with this.

NeuroTask does not allow other websites to link directly to your images (so called ‘deep linking’). To prevent this, each image file to be retrieved from Amazon is given a temporary, very long, more or less random file name, which is valid for only a short period of time (about one hour). This means it is useless to refer to your image with this file name on some other web page (or FaceBook!), because the link will soon go ‘stale’ and stop working.

So, NeuroTask Scripting takes some serious steps towards protecting your images from deep linking, while you do not have to worry about this. In scripts, you can simply use your own image names and forget that these are converted automatically to long Amazon names. In fact, if we had never told you about this, you would not have noticed it!

4.4 Preloading images

When a subject sees an image on the screen in one of your experiments, the image file has first been retrieved from the computers (called servers) at Amazon. This retrieval process takes time, more so if you use many, large images, if the Internet connection is slow, and if the subject’s computer is not very fast. In order, to guarantee that images are ready to be displayed when needed, they must all have been retrieved to the subject’s computer before they are used. This is called preloading of images.

The preload() function

NeuroTask experimenters are shielded from all the complexities of image retrieval sketched above through the preload() function, which retrieves image files ‘behind the scenes’ and makes sure they are ready-to-go with a very small latency. This greatly increases the reliability of your experiments.

Unless you really don’t care about image presentation time, you should therefore always use preloading. So how does this work? For the script introduced above we would add preload() statements, as follows:

preload("cars1.png");
preload("cars2.png");
preload("cars3.png");
preload("cars4.png");
preload("cars5.png");
preload("cars6.png");

image.await("preloading_completed");

This should be placed where it says in the script: “Here we should put preloading code”.

The image.await("preloading_completed") statement will wait until all six car images have been loaded and are ready-to-go. Note that if any of the images cannot be loaded, for example because you made a mistake in spelling their name, the script will not run past the image.await("preloading_completed") statement.

You may wonder why you have to say image.await() here, rather than just await(). The reason is that there is also a sound.await() function, which waits for completion of preloaded sound files. Video files cannot be preloaded.

It is often useful in scripts to preload a whole range of numbered image stimuli, as we have done here for car images 1 to 6. Because this is so common, NeuroTask includes a special function for preloading image ranges. The script fragment above would be shortened to:

preload_range("cars{0}.png",1,7);
image.await("preloading_completed");

Where it says {0} in the file name the numbers 1 to (but not including) 7 are substituted. It may be clearer to have the range of numbers extended to include 7 here, but we have chosen not to do this, so that this function has the same usage as the range() function, which is already in common use outside NeuroTask. For example, range(1,7) produces an array [1,2,3,4,5,6]. This mimics the behavior of similar functions found in other computer languages, notably Python, which popularized a similar range() function.

Use of preload_range() is entirely optional but can make your scripts shorter and easier to maintain.

Preloading is Block-specific

If you preload an image, it is not just retrieved so that it is available to show in the subject’s browser. In fact, it is actually already shown invisibly. This may sound strange but there is a reason for this. By telling the browsers “Show image ‘cow.png’” it would normally start finding space to hold the image, record its position etc. This takes some time, occasionally a lot of time if the browser decides to do this later. All of this may lead to long, unwanted latencies, even if the image file itself is already present.

By showing an image during the preloading stage in browser but with an invisible ‘style’ ensures that the browser has taken all measures to display the image. You can then make it visible with showimage('cow.png'), at which point the browser only has to change the ‘style’ from invisible to visible. Many invisible images can be present on top of each other; they will not interfere.

Because of all this, preloading is specific to a block: you will have to preload an image for each block where you want to show it.

The scripts above do not show the block, but this is only because the NeuroTask Scripting system adds main.centerblock, like it does with the text() and setimage() command (also see previous chapter):

preload_range("cars{0}.png",1,7);

is identical to

main.centerblock.preload_range("cars{0}.png",1,7);

This means that if you create your own blocks with addblock(), you need to preload images within those blocks. E.g., if you have a block b, you must write:

b.preload("cow.png");
b.preload_range("cars{0}.png",1,7);

If you want to show a cow simultaneously in two blocks b and c, you can write:

b.preload("cow.png");
c.preload("cow.png");
image.await("preloading_completed");

await(2500); // Wait a little bit

b.showimage("cow.png");
c.showimage("cow.png");

The image.await("preloading_completed") is not block-specific; it will wait until all images have been preloaded, no matter which blocks are involved. The last lines show the cow image first in block b and then in block c. In practice, they will be shown nearly simultaneously because the scripts themselves are executed very fast and because of the preloading the browser will show them rapidly as well. In tests we usually find latencies in the order of less then 0.3 ms.

4.5 Resizing images

Once an image has been added to a block with setimage(), it can be resized by calling the block’s resize() function. This will resize the block and as a result all images within the block will be resized as well. If you want to control sizes of a set of individual images, it is easiest to put them into different blocks. Or you can resize a block to a desired size before showing the intended image.

An example of how to implement resizing can be seen the following simple animation script, which will show a fairly smooth animation of a cow getting larger:

Script 3.2. Image resizing example.
 1 var b = addblock("center","center",25,25),
 2     m = addblock("center","bottom",100,25).text("Press Esc to finish");
 3 
 4 b.preload("cow.png",100);
 5 image.await("preloading_completed");
 6 
 7 b.showimage("cow.png");
 8 for (var i = 0; i < 50; i++)
 9 {
10     await(16);
11     b.resize(25+i,25+i);
12 }
13 
14 awaitkey("ESCAPE");

A difference with earlier scripts is that we are now using showimage() instead of setimage(). You may (only) use showimage() if you are absolutely certain the image has been preloaded with the preload() function. An advantage is that showimage() has less overhead than setimage(), causing a shorter latency, which may be important with very brief image presentation times, say 16 or 32 ms. This is why we recommend using showimage() as much as possible.

5 Style

For many experiments, styling is not very important, for example, if you just want to show some words or images on the screen and measure reaction times. In other cases, however, having exactly the right color of a text may be crucial. Or you may really need borders around blocks, or show a list of items in a specific format, or use text that is outlined to the right rather than centered, and so on. In this chapter, we will tell how to accomplish this. Our approach is based on the styling framework called Cascading Style Sheets or CSS, which is the standard on the Internet. The advanced NeuroTask Scripting manual gives a little more background on this. For most experiments, you only need to know the names of the styles you wish to change, such as ‘color’ and ‘font-size’, and what values are allowed. These will be discussed in this chapter. Nearly all styling can be done with a single function, aptly called style().

5.1 Style with style()

Suppose, we would like to have a block in the right-bottom corner with a small trial counter in it that says for example ‘7/15’, meaning this is trial 7 of a total of 15 trials. In order to achieve this, we would do something like:

var b = addblock("right","bottom",10,10);

b.text("7/15");

Now, suppose furthermore you want it to be less distracting by making the font a little smaller and the font color a bit lighter. How would you do it? This is easily accomplished with the style() function, which allows you to change fonts, colors, borders, and many other style properties (often simply called ‘styles’). In this particular case you might do this:

var b = main.addblock("right","bottom",10,10);

b.text("7/15");
b.style("color","grey");
b.style("font-size","80%");

The style() function takes a style property name as the first argument and its value as the second argument.

Chaining of block function calls

The example above may also be written in a short-hand form that many people find easier, namely:

addblock("right","bottom",10,10)
    .text("7/15")
    .style("color","grey")
    .style("font-size","80%");

This is allowed because addblock() returns a Block object, and text() and style() also return this same object. Thus when we have b.text("Hello"), where b is a block, this function call will itself return b, so we could do this:

b = b.text("Hello");
b.style("color","red");

This can also be written as

b.text("Hello").style("color","red");

or, with a different spacing, as

b.text("Hello")
 .style("color","red");

Remember that white space is irrelevant in JavaScript, except within strings. Especially with style() function calls, chaining allows more compact code. Many Block functions return a Block object, with some exceptions, like setimage(), setvideo(), and a few others.

5.2 Setting font size of an entire Box

Styles also work on a Box, such as ‘main’. However, if you try to set the font-size style of a Box, it works but upon a window resize by the subject your font-size will be lost. This is because the system is constantly recalculating the font size. To solve this, you can do the following:

main.setfontsize(75);

This sets the font size of the (default) Box ‘main’ to 75% percent, a setting that will be retained on window resizing.

5.3 Queries with tag name, class and id

While you do not really need to use HTML markup in your experiments, it may come in handy from time to time. If you are going to use it, it is good to know that the style() function is powerful enough to act as a style rule for all the HTML code in a block or box.

1 var s = "<div><h2>Instruction</h2><p>Welcome to this experiment.</p></div>",
2 b = main.addblock().text(s);

This assigns a string to s with HTML code that contains a <h2> header and a paragraph, which is wrapped in <p>...</p> tags. This code in turn is put inside the newly formed block using text(). The header will be shown as large and bold in the browser. It is browser-dependent exactly how this is done (i.e., how large and how bold).

Now suppose that you want to turn the header blue rather than have it remain black. How can this be achieved? This can be done by adding the tag name, in this case ‘h2’ (without the <..>) as a third argument:

1 var s = "<div><h2>Instruction</h2><p>Welcome to this experiment.</p></div>",
2     b = main.addblock().text(s);
3 
4 b.style("color","blue","h2");

This will color all <h2> headers in block b blue.

It is also possible to call the style() function on a Box object, like main:

main.style("color","blue","h2");

In this example, you would not notice a difference, but if you had several blocks within main with headers, all of these would be color blue. Blocks in other Box objects, however, would not be styled.

It is also possible to use classes in queries. For example, in case you want to create a Stroop task where color words are shown in different colors (not congruent with the color they mean), we could write:

var s = "<div class='r'>blue</div>"
      + "<div class='g'>black</div>"
      + "<div class='b'>green</div>",
b = main.addblock().text(s);

b.style("color","red",".r")
 .style("color","green",".g")
 .style("color","blue",".b");

Here, “.r” refers to the class “r”, which was used in the HTML code fragment. You can assign arbitrary classes in HTML and then use the style() function to fine-tune their appearance. The advanced manual has more information on this.

5.4 Color

So far, we have specified color either with color names, like ‘lightgrey’ and ‘red’. In this section, we will explain more about colors and how to specify them with NeuroTask Scripting.

Named colors

There is a surprisingly long list of color names that are officially recognized for usage on the web. They do not just include ‘red’, ‘green’, and ‘blue’, but also ‘crimson’, ‘lime,’ and ‘cadetblue’. And what about ‘slateblue’, ‘lightgoldenrodyellow’ and ‘papayawhip’? You can use all these color names in NeuroTask Scripting styles (just check the Quick Reference side panel when you are editing a script; all legal color names are listed there with a color sample). Despite the inspiring list of named colors, there are many circumstances where you need yet more shades of color. It takes a bit of getting used to, but specifying colors on the Internet is not too difficult. The two main notations for shades of color are RGB and Hex (or hexadecimal).

RGB and Hex

Each color you see on the screen is composed of red, green, and blue. On another list of color names on the web, we can see the relative portions of each of these for all named colors in two often-used ways.

The first method is called RGB, for Red, Green, Blue. Each color ‘channel’ is specified with a number from 0 to 255. This is the number of levels that fit into one byte, so a color value can be represented by three bytes. Suppose, we want to make the level-2 headings slateblue. There is a handy online color converter where you can type in various color names (and codes) to convert them. For slateblue we get rgb(106, 90, 205). We use this as follows:

b.style("color","rgb(106,90,205)");

(Spaces within rgb() specifications are not necessary, though they are allowed.) In this example, and all others below in this chapter, b refers to a block, e.g., one that displays some text.

Many people (read: programmers) find this way of specifying too cumbersome and they have come up with a shorter format, called hexadecimal or Hex for short. We will briefly explain it here, because even if you don’t want to work with it, you may still encounter these color codes frequently. Slateblue in Hex is #6a5acd, which can be entered like this:

b.style("color","#6a5acd");

The # symbol signals that this is a color, which consists of values red = 6a, green = 5a, and blue is cd. Rather than the decimal system, the hexadecimal system is used which does not run from 0 to 9 but from 0 to 15 (and thus has 16 levels). Instead of 10, 11, …, 15, hexadecimal notation uses a, b, …, f. In hexadecimal, we don’t count to 9 but to f: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f. So what does 6a mean? We must multiply 6 not with 10 but with 16 and than add a (a = 10). This gives 6×16+10=106, which is the value we entered above. Similarly, 5a is 5×16 plus a (= 10), which gives 90. And cd is c (= 12) times 16 (= 192) plus d (=13), which gives 205.

Perhaps, the easiest way to specify colors is using RGB with percentages. The example may then be specified as:

b.style("color","rgb(42%,35%,80%)");

You may also use decimal values like 42.3%.

There are several other ways of specifying colors based on hue, saturation and lightness or value, which will not discuss here even though you may use them in the style() function. There is a good Wikipedia entry about them.

Opacity and transparency

Opacity is the opposite of transparency, so that high opacity means low transparency, in other words, a quite opaque object is not very see-through. In CSS, there is an ‘opacity’ style property that can be specified like this:

b.style("opacity",0.2);

Here, some block b is made “not very opaque” (i.e., 20% opaque = 80% transparent). If b is green and overlaps an underlying block c that is red, you would be able to see c below (or ‘through’) b. Legal values for ‘opacity’ are decimal values 0.0 to 1.0 (1.0 = completely non-transparent).

5.5 Fonts and text styles

Modern CSS styles allow many aspects of text appearance to be specified: font-type or font-family, size, color, weight, decoration, underline, strike-through, kerning, etc. This is great, except that many font types and other properties do not look very similar on different browsers. From the perspective of web design, this is often not a great problem, but for the psychophysics of reading and word recognition, this is unfortunately highly relevant. We, therefore, recommend to not ‘over-style’ your stimuli.

Font family

The text() function is optimized for the Arial font type, meaning that other font types may show up a different sizes on different browsers, while with Arial we actively try to compensate for this. We may include such support for other fonts in the future.

Because not all browsers support all fonts, CSS allows specification of the font you really want and then a few fall-backs in case the font is not available on the computer. For example:

b.style("font-family","Arial, Helvetica, sans-serif");

This says: “I prefer Arial but if not available use Helvetica, and if even that is missing use the default sans-serif font on the computer”. Arial is an improved version of Helvetica, so the two fonts will be highly similar. For a more classic look you may prefer a serif font:

b.style("font-family","'Times New Roman', Georgia, serif");

The quotes around ‘Times New Roman’ are necessary here. As argued above, unless your stimuli absolutely require this, only use non-Arial fonts in instructions and other parts of your experiment where psychophysical aspects are less important.

Font size and other text style properties

Font weight: bold and bolder

To make text in a block bold, you can use the font-weight style property. For example, to make all text in a block b bold we would use this:

b.style("font-weight","bold");

Other legal values instead of bold are: normal (useful to remove bold), lighter, bolder, and the values 100, 200, 300, …, 900, where 400 is normal and 700 is bold. This makes 900 extra bold and 100 quite light (little ‘ink’ will be used).

If you only want part of a sentence to be bold, you may surround it with <b>...</b> tags, like so:

b.text("Your participation is <b>much appreciated</b>!");

Font style: italic

Setting the font style to italic means you are setting it to, well, italic:

b.style("font-style","italic");

Other legal values are normal (useful for removing italic) and oblique, which is rarely used.

The corresponding tag is <i>...</i>.

Text decoration: underline and line-through

Underlining may occasionally come in handy and can be achieved either with <u>...</u> tag or with the text-decoration property:

b.style("text-decoration","underline");

Other useful values are overline (a line above the words) and line-through (a line striking through the words).

Font size and line-height

The font-size property sets the size of the letters in a text. We highly recommend only specifying font size in percentages, because NeuroTask normalizes the font sizes such that they are near-equal for 100% sized Arial normal text. This implies that text at other percentages scales proportionally, so that the relative size of text, images and other display elements stays the same at all screen sizes.

Increasing the size of the text is done as follows:

b.style("font-size","150%");

Specifying size in percentages is also built into the text() function, as in text("Hello",150), which displays text at 150% text size.

There are several other unit types available in CSS to express font sizes, such as em, pt, px, but apart from em they do not scale well and may show great differences between browsers. You may use px (pixels), if you are absolutely sure on what particular screen size your subjects will do their experiment and if it is important to know the font size in pixels.

If you find that the lines are too close together you may change the line-height CSS property. This is one of the few CSS properties where it is better not to use units. You could use 1 for normal line-height and something like 1.5 for wider spacing (more white between lines), e.g.,

b.style("line-height","1.5");

Text align: left, right, or justify

For technical reasons that are explained in some detail in the advanced manual the style() function is not very suitable to align text. Because non-centered text alignment is quite frequent in experiments (in instructions, debriefing, etc.), we have defined a convenience function called align(), which should be used instead of the style function (which will not work as expected):

b.align("left");

Other legal values are centered (default setting), right, and justified. The latter fills out the text evenly, aiming to create straight margins left and right by adding extra space between words.

Top, left, width, height, and getshape()

To get or set the exact size of a block is trickier than you might think because there are several coordinate systems active on a web page and of course there are the usual cross-browser issues. But because in NeuroTask Scripting sizes and locations are expressed strictly in percentages we avoid most of these problems. To find out the size and location of a block, you can use the getshape() function.

In most cases, however, the shape properties left, top, width, and height (there are no other shape properties) can be obtained reliably from properties of a block, for example:

b.text("Width: " + b.width + "%, Height: " + b.height + "%");

This displays the width and height of a block, e.g., “Width: 90%, Height: 90%”.

You can also get all shape properties with a call to getshape():

var shape = b.getshape();

b.text("Width: " + shape.width + "%, Height: " + shape.height + "%");

This gives the same result as above.

The main reason for using getshape() over direct access of the shape properties on the block (i.e., with b.width), is the fact that getshape() optionally recalculates these properties. The shape properties do not normally change, even if the users resizes the window or screen orientation: the relative coordinates (in percentages) stay the same. If for some reason you know that a block’s shape properties are not synchronized anymore, call getshape(true), where true indicates that it must first recalculate and refresh (i.e., synchronize) the shape properties. After that, direct access of say width or height will give reliable values again.

Blocks also have a setshape() function, which takes the same arguments as the first four of the addblock() function. Calls to setshape() will also keep the shape properties on the Block object in sync.

5.6 Borders

Boxes, blocks and many HTML elements can be given a border using style. CSS offers quite a number of ways of specifying these. A border around an entire block b can be set like this:

b.style("border","solid thin black");

The order of solid, thin and black is unimportant. Other values for the border-style instead of solid are, for example, dotted and dashed, and there are several others.

In addition to thin, the border-width property can take values like thick, medium or a number with a unit, such as “0.5em”. The latter specification is more reliable if you want a fairly thick border, as there is no standard definition for what ‘thick’ is. Using pixels (e.g., “5px”) is a possibility but then the perceived width will depend on screen resolution: on an high-resolution display (e.g., Retina or 4K screen), there are more pixels per inch and the line will look thinner than on a low-resolution display.

For example to get a fairly thick line of 0.5em, which is red and dashed, you might do:

b.style("border","red dashed 0.5em");

If you want a really thin line, use thin or “1px”. Using something like “0.1em” may cause the calculated size to be less than one pixel (e.g., with a small font size we might have 0.1em = 0.8px) and some browsers (i.e., Chrome) then rounds this down to zero and no border will be shown. This is one of the few exceptions where it is better to use the pixel (px) unit rather than percentages or em units.

For border color you can use any of the methods defined above, in the section on color.

It is also possible change border-style, border-width, and border-color separately, or even to give each side of a block its own style. Such styling details are beyond the scope of this book, though the style() function will happily apply them. You can find more information on borders at cssportal.com, html.net, and w3schools.com.

5.7 Padding

If you find that the text in a block comes too close to the borders, you may add some padding with the padding property:

b.style("padding","2em");

This will put a space of twice the letter m between the text and the border of the block. You can set the left, top, right, and bottom padding sizes separately with padding-left, padding-top, padding-right, and padding-bottom. Other units than em, such as percentages, are allowed as well, as explained at cssportal.com. We strongly advise to only use either em or percentages in order ensure correct scaling.

It is also possible to specify a margin, which is space on the outside of the border of box, block or HTML element (like a div). Because boxes and blocks are positioned absolutely, the margin property is less useful and changing it may in fact interfere with certain centering settings. If you want to apply the margin property to text inside a block, consult an online resource like the cssportal.com. You can still use the style() function for this, as it will accept any legal CSS style property.

5.8 Preset functions versus block functions

So far, we have encountered several functions that are not tied to a block, like text() and setimage(). These functions are included to make it easier for beginners to start programming, and for expert users to save some code writing. Though they do not appear to be tied to a particular block, this is in fact not the case. At the start of a script, a default preset Box called main is created, which is square and white. In that box is a single white (transparent) block that is also square with a width and height of 90%. This block can be accessed as main.centerblock. Calls to the stand-alone text() function in fact are translated into using main.centerblock.text() with a little bit of additional styling of the font size. The code for the text() function is something like:

function text(s,size)
{
    if (size === undefined)
    {
        size = 100;
    }
    main.tofront();
    return main.centerblock.style("fontSize",""+ size/100 + "em").text(s);
}

Here s is the text string and size is text size in percentages. It is allowed to call this function without a second argument, like text("Thank you!"). In that case the size argument will have the value undefined. To make sure the default value of 100 is assigned to size, we check for this case and if necessary assign the default value 100.

The line main.tofront(); moves the main box all the way on top under the assumption that if you are going to display text, you will also want to see it. If there are other boxes on top of some box, its text may not be visible. But the front box is always completely visible. There is also a toback() and a tooriginal() function, which returns it to its original order.

The same effects can also be obtained by manipulating a box’s z-index CSS property, e.g.,

main.style("z-index","999");

The z-index is an imaginary axis sticking out of the web page. The higher it is, the more ‘in front’ a box will appear. This property can be used for boxes and blocks but does not work for blocks that are in different boxes: If Box A is in front of Box B, no block in B can be in front of a block in A.

6 Survey questions with form controls

In many experiments you will want to know a few details of the participants, such as age, gender, and level of education. To be able to easily obtain such information, a number of so called form controls have been included in the NeuroTask Scripting framework. With these participants can enter information in text fields, select their age or gender from a list, etc. NeuroTask form controls are built with the normal controls available in HTML forms, adding services for automatic layout and data storage. This means that for simple types of questions, it is easy to include some survey-type questions in your scripts, which are shown in a screen-by-screen manner. It is not currently possible to have a single, scrollable survey form, so the size of the questions is limited to the available screen size. We may add scrollable survey forms in the future.

If your experiment consists completely out of survey questions, however, you could also use a (free) online survey tool, for example Google Forms if your forms are relatively simple.

Having said all that, it is possible to build a very sophisticated survey system on the basis of NeuroTask Scripting, with the added advantage that there are virtually no limitations to the type of questions you may want to ask and the ways you want to handle feedback and question order. In the rest of the chapter, we will assume that you are studying the effects of self-reported sleep on health and cognition. Part of your experiment consists of a brief sleep survey.

6.1 Instruction

We already encountered the instruction() function in earlier chapters. It takes an instruction message, an optional button label (default is “OK”), and a header (default is “Instruction”), which is rendered as an <h2> header (see previous chapter for an explanation of this). The instruction() function is is mainly a convenience function that places left-outlined text in the main.centerblock with a button underneath. Let’s start our survey with a brief instruction:1

instruction("Please, answer the following questions about your usual sleep habits.");

This will look something like:

Instruction for the sleep survey

As soon as the participant clicks the OK button, the screen is cleared.

If you target non-English speakers you can change the button and header:

instruction("Beantwoord alstublief de volgende vragen over uw gebruikelijke
            slaappatroon","OK","Instructies");

It is not very difficult to create your own instruction screens with various layouts, as we will see below.

6.2 Button

The button is probably the most basic type of control. The only thing you can do with it is to click it (or touch tap or press Enter when it is in focus). The button will then emit a so called ‘click’ event that can be caught by await('click').

All NeuroTask form controls discussed here generate data that is automatically stored to your account (if the script is currently ‘activated’) and can be inspected in the data panel. If you had tested the instruction() script fragment above and then inspected your data panel, this would have looked something like:

Data panel after a single button click

As you can see, the data panel for this experiment script is pretty empty, except for a single entry that has an event name ‘instruction_button_1’ and as value ‘click’. This is the result of clicking the OK button of the instruction once, during testing. If you had several screens with instructions these would be numbered automatically. The ‘click’ value is not very useful here but the date and time might be, for example, to check when they completed reading the instructions. More useful values can be obtained with the input form controls.

In many cases, you will want to use a single block per form control, so that you can use the blocks to layout each screen. The button() function adds a button to a block:

b.button();

This would put a button in the middle of block b. By default the label will say “OK”. This can be changed with the first argument:

b.button("Continue");

The arguments, all of which are strings, are as follows. Many of these types of arguments are also present in other controls.

message
Button label (default is “OK”).
response_name
The event name as found in the data panel (default is button_1, button_2, etc., auto-numbered).
response_value
The event value as found in the data panel (default is “click”). If you change the event value to something more useful, like “instruction_button”, note that the event that is emitted to the await() function is still called “click”, as “click” is a standard JavaScript event name.
id
An “id” given to the <button> DOM node so that it can be accessed with CSS rules and queries (default is to set the id equal to the response_name, e.g., “button_1”). An id is necessary if you want to style say the background color of the button.
     b.button("Back","navigation","backward","back_button")
      .style("background-color","red","#back_button");
If you left out the “#back_button” id, the entire block would turn red, instead of just the button.
querystring
This is used to place the button inside a DOM node(s) that fit the query. For example, the instruction() function uses this:
    var b = text("<div class='instruction'><h2>" + h + "</h2>"
                 + s + "</div><div id='instruction_button'></div>");
    b.button(sb,"instruction_button_" + (++instruction_buttons),
             undefined,undefined,"#instruction_button");
Here h is the header text, s is the instruction text, and sb is button label. The button is placed in the <div> with id “instruction_button”. This example also illustrate how you can layout forms by first writing HTML and then placing controls at strategic locations using id values. In general, however, we advise using the one-control-per-block rule.

6.3 Input

The input field is a form control that allows any text to be typed in by the user. It is suitable for brief answers. As part of a sleep inventory, we may have a question somewhere like this:

input("Enter any medication you are currently using","medication");
Example of the stand-alone input() function

This is the stand-alone input() function which uses the preset main.centerblock to display its text. It shows the question above the field and the answer is stored with event name “medication” in the data panel. By default the width of the input field is 100 (a percentage, relative to the width of the block: 100% is the entire block). If you don’t like such a wide input field, you can add a third argument to make the input field less wide, e.g., 50.

The data panel for this experiment now looks as shown below. As you can see, the answer is stored with the Event Name “medication”.

Data panel after adding an input field

All NeuroTask Scripting form controls are accompanied by an OK button, unless the startform() function is used as will be explained below. The stand-alone input() function furthermore adds the service of awaiting the “click” event and then clearing the screen. The (simplified) code for this function is:

function input(s,v,width)
{
    // NOT SHOWN: apply default values
    main.centerblock.style("fontSize","1.25em").input(s,v,undefined,width);
    main.centerblock.await('button:click');
    clear();
    // NOT SHOWN: return value
}

First, we increase the font size with 25% and then call the input() function on the main.centerblock block. Then, we wait until the OK button has been clicked. The reason it says await('button:click') rather then just await('click') is that we are only waiting for a button click. If the user would click in the input field, for instance to start editing or select text, we would not want to move on yet.

The full set of arguments than can be given to the block input() function are:

input(message,response_name,width,id,querystring)

These arguments are identical to those of button(), with two differences: (i) response_value is missing because the value now is the the string entered by the participant, and (ii) you can set the width of the string in percentages (a decimal number).

6.4 Using response values in scripts

So far, we have seen that any values entered in form controls by participants are automatically stored online, in your NeuroTask Scripting account (provided your script is currently ‘activated’, meaning it is allowed to store data), where they can be inspected in the data panel of your experiment script. The details of how this happens and exactly what is stored are explained in a later chapter. Online storage is great, but what if you want to use the data in the script itself? There are many reasons why you might want to do this: calcation of scores, error checking, feedback to participants, conditional questions (e.g., Question 2 is only asked if Question 1 says ‘Yes’), and so on.

Return values

The most straightforward way is to simply obtain the return value:

var meds = input("Enter any medication you are currently using"
                + "(Leave empty if you are not using any)","medication");

If the participant entered ‘benedryl’, this will be assigned to the variable meds, as a string value. You could use it like this not very useful but still illustrative example:

text("You are using: " + meds);

There is another way that may be easier in some cases: the response object.

The response object

All values entered by participants in NeuroTask form controls are automatically stored in the response object. So with this script fragment

input("Enter any medication you are currently using","medication");

we could access in the script the value entered by the participant by writing either response.medication or response["medication"] (these notations are equivalent).

We could now use this value for a conditional question, following the question above:

1 if (response.medication !== "") // If the answer was not empty
2 {
3     input("Which ones of these make you feel agitated?"
4          + "(Leave empty if none apply)","agitated");
5 }

Because you can do arbitrary processing on the response with JavaScript, it is possible to make arbitrarily complex questionnaires.

Feedback with

Suppose, we want to ask more about the medication provided. Then it would be nice to remind the participant of the value entered earlier. NeuroTask has a built-in system where earlier values can easily be introduced in the text presented with the text() function or any of the NeuroTask Scripting controls discussed in this chapter, as follows:

if (response.medication !== "")
{
    input("Enter any side-effects you are experiencing from {medication}",
          "side_effects");
}

We are using the answer label medication used in the earlier input() question. By putting curly braces {...} around it (no spaces allowed), we can immediately re-use the value entered by the participant. For example, if the participant had entered ‘benedryl’, the question would automatically turn into:

input("Enter any side-effects you are experiencing from benedryl",
      "side_effects");

Having the response object and the format() function are merely provided as a convenience. You could achieve the same using the returned values from controls and adding strings together as in First and last name: " + name.first + " " + name.last. It is up to you to decide what to use.

6.5 Largeinput

In the example above, asking for side-effects of prescription drugs, there might be a long list for some patients and a single input line would be awkward to use. When we expect a lot of text as an answer, we have the largeinput() function.

largeinput("Enter any side-effects you are experiencing from {medication}",
           "side_effects");

This looks very similar to the singe-line input field, except that by default there are about five lines visible.

Question with largeinput() field with default settings.

If more lines are needed, these will scroll into view automatically. It is possible to show more lines or fewer lines of text with a third argument, which is an integer specifying how many lines to display (5 is default). The other arguments are like input() (including the width argument):

largeinput(message,response_name,rows,width,id,querystring)

6.6 Select, radio, and scale

The select(), radio(), and scale() functions are treated in one section because all three allow the participant to select one response out of several choices. select() shows a drop-down menu, radio() a vertically layed-out set of choices and scale() a Likert scale with optional left and right labels.

select()

The select() function is used as follows

select("How many hours do you sleep per night on average?",
       ['less than 5','6','7','8','9','10','more than 10'],"hours_sleep");

and the result will be something like this:

Select drop-down form control

The response value is the string that was selected by the participant.

In some cases, you may want to recode the response options and only store the recoded value, e.g., ‘male’ = 0 and ‘female’ = 1. This may save some time working with Excel or SPSS adding the recode formulas there. (Then again, male is more descriptive than 0 if you come back to your data after many years.) To achieve recoding, replace each option like ‘male’ with [‘male’,0], where 0 is the recoded value (may also be a string: whatever you need here). For example:

select("What is your gender?",[['male',0],['female',1]],'gender');

In your data, you will now see 0 and 1 instead of ‘male’ and ‘female’. The same approach also works with the radio() control, described next.

radio()

The radio() control takes the same arguments as the select() control, but gives a different appearance.

radio("How many hours do you sleep per night on average?",
     ['less than 5','6','7','8','9','10','more than 10'],"hours_sleep");

which looks like this:

Select drop-down form control

scale()

The scale() control gives a Likert scale:

    scale("How well do you sleep at night?",
           "Very badly","Very well","sleep_quality");
Select drop-down form control

You can set the number of points with a fifth argument, e.g. 7 to get a seven-point scale (5 is default). It is currently not possible add labels above the points, e.g., from 1 to 5 or -2 to 2, but there are shared scripts that can do this, including some fairly complex table-like scales with several rows.

6.7 Check

The check() control gives one or more checkboxes, each with its own label.

1 check("Do you experience any of the following?",
2      [
3 	["Trouble getting to sleep at night","sleep1"],
4 	["Restless legs","sleep2"],
5 	["Frequently waking up","sleep3"],
6 	["Feeling very hot","sleep4"]
7      ],
8      "sleep_problems");

Because the sentences are too long to usefully serve as event labels, alternative labels have been provided: “sleep1” to “sleep4”. These could have also been named “night”, “restless”, “waking”, and “hot” to make them more descriptive; there is no need to use numbers. Information entered by participants is saved in two ways: (1) as an array of choices, e.g., [‘sleep1’,’sleep4’], and (2) as a series of variables “sleep_problems_sleep1” to “sleep_problems_sleep4”, which are the combination of the control’s variable “sleep_problems” and that of the individual options (joined with a “_” character). This is done to facilitate analysis once you have downloaded the data.

6.8 Sleep questionnaire example

If we put all questions above together we have a mini-sleep questionnaire:

Script 6.1. Brief sleep survey.
 1 instruction("Please, answer the following questions about your usual sleep habits.");
 2 input("Enter any medication you are currently using","medication");
 3 if (response.medication)
 4 {
 5     largeinput("Enter any side-effects you are experiencing from {medication}",
 6                "side_effects");
 7 }
 8 radio("How many hours do you sleep per night on average?",
 9       ['less than 5','6','7','8','9','10','more than 10'],"hours_sleep");
10 scale("How well do you sleep at night?","Very badly","Very well","sleep_quality");
11 check("Do you experience any of the following?",[
12 	["Trouble getting to sleep at night","sleep1"],
13 	["Restless legs","sleep2"],
14 	["Frequently waking up","sleep3"],
15 	["Feeling very hot","sleep4"]
16       ],"sleep_problems");
17 text("Thank you for participating!",200);
18 await(8000);

When a participant has completed the survey questions, the response object will contain something like the following information:

 1 response =
 2 {
 3     hours_sleep: "7",
 4     instruction_button_1: "click",
 5     medication: "benedryl",
 6     side_effects: "snoring",
 7     sleep_problems: ["sleep1","sleep4"],
 8     sleep_problems_sleep1: "true",
 9     sleep_problems_sleep2: "false",
10     sleep_problems_sleep3: "false",
11     sleep_problems_sleep4: "true",
12     sleep_quality: "4"
13 }

All of these variables can be used in the text() function and in controls like the input() or largeinput() functions, using curly brackets notation like:

text("Earlier you stated that you sleep {hours_sleep} hours per night");

The same information as in the response object will also be stored in the data section of your account, provided the script is ‘activated’.

Now suppose that you have another question that you only want to pose if the participant has trouble getting to sleep at night. Then you can do this:

if (response["sleep_problems_sleep1"] === "true")
{
    largeinput("Can you describe your difficulties getting to sleep at night?",
    "night");
}

As a result of this, some subjects will have the “night” data with additional information (possibly and empty string) and others will not.

6.9 Combining controls with startform() and endform()

So far, all form controls have been used as stand-alone controls: one input field or one Likert scale per page. These all had one OK button automatically positioned underneath. What if you want to combine several form controls on one page (each control would still be in its own block)? This can be accomplished with the startform() and endform() functions.

Using these functions ensures two things:

  1. Only one OK button is shown.
  2. Only when the OK button has been pressed, will the data be read-out (and stored) from the form controls, so that it is possible for the participant to change any responses until OK is pressed.

We will assume that you put each individual control in its own block. An example:

Script 6.2. Example form asking for birth date.
 1 var months = ["January", "February", "March", "April", "May", "June",
 2               "July", "August", "September", "October", "November", "December"];
 3 
 4 startform();
 5     main.addblock('center',25,100,20)
 6         .text("What is your date of birth?",120);
 7     main.addblock(15,'center',20,20)
 8             .select("Year",range(1910,2000),"year");
 9     main.addblock(35,'center',30,20)
10             .select("Month",months,"month");
11     main.addblock(65,'center',15,20)
12             .select("Day",range(1,31),"day");
13 
14     main.addblock('center','bottom',30,20)
15             .button("OK","inputbutton","click","inputbutton")
16             .await('click');
17 endform();
18 
19 main.addblock('center','top',100,20)
20     .text("<b>You were born on {month} {day}, {year}</b>");

This is a useful form control that combines three select controls into a birthday selector. Normally, when you use select(), an OK button appears automatically. The startform() function prevents this. Instead, you have to provide your own button. As soon as endform() is reached, the data are ‘harvested’ from the form controls and stored in the response object and sent to your NeuroTask Scripting account. After endform(), things are back to normal: if you would use select() or other controls, they would appear with their own OK buttons again.

6.10 Validation

At the moment it is not possible to add validation checks directly to the controls, although we plan to add these in the future. With this we mean checks to see whether a participant filled something in a ‘required’ input field, or whether the (inadvertently?) stated their age as 180 years old. It is however possible to inspect the value provided by a participant and to give feedback, possible showing the same control again with a message. This means that you can built in validation with some effort. An example:

Script 6.3.
 1 var message = "Please enter your age (required)", age;
 2 while (1)
 3 {
 4     input(message,"age");
 5     age = parseFloat(response.age);
 6     if (!age)
 7     {
 8         message = "Please enter your age.<br>This is a <i>required</i> field."
 9     }
10     else if (age < 5 || age > 120)
11     {
12         message = "" + age + " is not a valid value for age";
13     }
14     else
15     {
16         break;
17     }
18 }
19 text("Your age is {age}");
20 await(10000);

Admittedly, this is cumbersome but it works until validation has been added. It does have advantage that your validation may be arbitrarily complex. A disadvantage may be that all invalid values are also sent to your account’s data. On the other hand, this may give you some insights into your participants’ erroneous data entry.

Note that when you use startform() and endform() you can only validate the entire form, as values are only available past the endform() statement.

7 Data logging and handling

We already mentioned several times how to log your data in NeuroTask Scripting. The basic function for this is log(), which adds a row of new data to the data table in your account. For most experiments this type of storage is sufficient In other words, data logging is easy and the form controls such as input(), check(), and scale() log the subject’s responses automatically. This chapter describes the rest of the data management system, including a few cases where you need more options to store and perhaps retrieve data.

As an example of why you would want to store and retrieve some data, consider the case that you invite a certain group of patients to take a test battery that consists of three fairly long tests (say 12 min each). Some patients may get interrupted and then later return to the test battery. In such a case, it would be handy if you could somehow retrieve which parts of the test battery that had already taken, so that you could skip these when they return. For these and several other use cases, NeuroTask Scripting has functions that let you not only log but also update and retrieve data in a running script.

In this chapter, we will first discuss the log() function. Then, we will tell a few things about where your data ends up, some tricks to get a better view on your data, and how to download it. Finally, we will discuss how you could store and retrieve data for more complicated situations.

7.1 Data logging with log()

The log() function is the basis of all data logging. NeuroTask Scripting distinguishes between data logging and storing: Data logging never overwrites existing data and always adds a new row to your data table. Data storing, however, will overwrite the data row in your table if it has the same label. We will discuss data storing towards the end of this chapter.

To log some data, you can simply write:

log(42,"var3");

This will add a row of data to the data table in your account with name ‘var3’. It will also include a timestamp with the exact date and time when the data was logged. And it will have information about the subject and session.

In most cases, you will first collect a response from a participant in a variable and then log its value:

Script 7.1. Collecting and logging a reaction time.
 1 var e, b;
 2 
 3 b = addblock().text("Get ready to click with the (left) mouse button...");
 4 await(4000);
 5 b.clear();
 6 
 7 await(randint(1000,3000);
 8 b.text("Now");
 9 e = b.await("click");
10 
11 log(e.RT,"Reaction Time");
12 await(5000);

This script is a simple reaction time task. First, a text appears on the screen with the text ‘Get ready to click with the (left) mouse button…’. After 4 s the text disappears and after a random interval of 1 to 3 s the text ‘Now’ appears. We use the randint(min,max) function which generates a random integer in the range min to (but not including) max. The click event is caught by the await() function its return value capture in variable e. As explained in the chapter on events, the return value already contains a property RT that holds the reaction time, which we log as shown.

For reaction times, we would normally like to run several trials and then average these. This can be done with a for loop. Let’s run ten trials and adjust the script accordingly:

Script 7.2. Collecting and logging ten reaction times.
 1 var e, b, i;
 2 
 3 b = addblock();
 4 
 5 for (i = 0; i < 10; ++i)
 6 {
 7     b.text("Get ready to click with the (left) mouse button...");
 8     await(2000);
 9     b.clear();
10 
11     await(randint(1000,3000);
12     b.text("Now");
13     e = b.await("click");
14 
15     log(e.RT,"Reaction Time");
16     await(1500);
17 }
18 
19 await(5000);

This script has a for loop that runs ten times. We have moved creating of the block outside the loop, because we don’t want to create it ten times; we just want to show the instruction text ten times. We have also add a 1.5 s wait at the end of the loop so that after clicking the participant gets a brief pause. We shortened the time the instruction is shown to 2 s, because 4 s felt quite long if you run several trials.

Script 7.2 will log ‘Reaction Time’ ten times, each time with different values, which will show up as rows in your data table. Once you have downloaded the table (discussed below) you could analyze the reaction times, for example, in Excel. To facilitate this and to give the participant some feedback, let’s keep track of the total time in a variable named total, calculate the average reaction time and assign to variable average, log this variable, and use it to give some feedback to the participant.

Script 7.3. Reaction time experiment with calculation of the average.
 1 var e, b, i, total, average;
 2 
 3 total = 0;
 4 
 5 b = addblock();
 6 
 7 for (i = 0; i < 10; ++i)
 8 {
 9     b.text("Get ready to click with the (left) mouse button...");
10     await(2000);
11     b.clear();
12 
13     await(randint(1000,3000);
14     b.text("Now");
15     e = b.await("click");
16 
17     log(e.RT,"Reaction Time");
18     total += e.RT
19     await(1500);
20 }
21 
22 average = total/10;
23 
24 log(average,"Average RT");
25 b.text("You average reaction time was: " + average + " ms");
26 await(5000);

This script is starting to become a real reaction time experiment. Now, let’s look at how the data appears in the data table of your account. But first we need to go over one more thing: Even without your logging, a NeuroTask script always logs certain data automatically.

7.2 Data that is always logged in ‘activated’ scripts

When a participant starts a new session by clicking the ‘Start’ button (which you may have given another label), the NeuroTask Scripting system records the beginning of a session and also collects certain data about the browser, whether high-precision timing is available, and the size of the screen. At the end of the session the time is recorded as well. A full list of automatically recorded data is as follows:

Name Value
nt_session_state started
nt_browser_with_version Chrome 41
nt_browser_type Chrome
nt_operating_system Windows
nt_screen_height 1080
nt_screen_width 1920
nt_window_height 640
nt_window_width 1280
nt_precision high
nt_RAF general
nt_now general
nt_session_state finished

Note that in the past, we used to also log the subject’s IP address, but in compliance with EU data laws we are no longer recording these in the database. All automatically logged variable names have the ‘nt_’ prefix. As we will explain in the next section, this makes it easy to single them out in the data tables of your account, or to suppress them.

The data can give some insight into the quality of the data collected. Knowing the browser and operating system may be important to monitor whether participants were working on an outdated browser or an atypical operating system. In some case, this may lead to exclusion from the experiment. The variable ‘nt_now’ indicates whether the high resolution timer now() is available and nt_RAF says something about whether precise onset timing of visual stimuli is possible.1 Both of these variables are summarized in the ‘nt_precision’ variable. If it says ‘high’, timing is likely to be in the millisecond precision range. If it says ‘low’, timing may be up to 16 ms or more off, which may or may not be a problem.

Screen and window size are important indicators of the visual resolution, but they say nothing about the physical size of the participant’s screen. It is impossible to find out the physical dimensions, short of asking participants to somehow measure their own screen. They do, however, help to recognize whether they were working on very low resolution screens or wet her they did the experiment in a small window of a large screen, and therefore may have been distracted.

The ‘nt_session_state’ here says ‘started’ and then ‘finished’, which may not seem very informative, but bear in mind that there is also a column with date/time, not shown here, at which each variable was recorded. With this you can easily estimate the total length of a session. A very short or very long session may indicate strange behavior, such as giving brief nonsense answers (or always choosing the first option) or use of external sources and notes.

The ‘nt_session_state’ variable can take on two other values, namely ‘blurred’ and ‘focused’. A ‘blur’ occurs when the participant moves away from your experiment by clicking on another window. When the participant returns, a ‘focus’ event occurs. Frequent ‘blurs’ may cause problems. It may, for example, indicate that a participant is checking Facebook or email, while in the middle of a reaction time experiment.

Together, the automatically collected ‘nt_’ variables give valuable information about the reliability of the collected data and the diligence of your participants.

7.3 Data tables in your account

All data collected ends up in the NeuroTask Scripting database, which resides with the TransIP corporation’s servers in the Netherlands. The TransIP servers are iso 27001 certified. They are protected with very high security standards and are subject to the Dutch and European laws on privacy and government access. Your own logged data is accessed in the data tables. An convenient way to inspect your data is to look up your script in the Scripts listing table and click on the data table icon. In the figure below you can see the data for two subjects who took the sleep survey.

Data table for the sleep survey discussed in the previous chapter

If you are reading this on a fairly large screen or print-out, you may be able to read the name and value of variables that were logged. The ‘Created’ column tells you to the second when the variable was logged. Other columns, from left to right are, ‘Index’, ‘Type’, ‘ID’, ‘Script’, and ‘Subject’. ‘Index’ is a number that uniquely identifies the row of data in the entire NeuroTask database. ‘Type’ and ‘ID’ will be discussed below. ‘Script’ is the id number of the script that generate the data. ‘Subject’ is an index number assigned to each anonymous subject. (The ‘Invite’ column is shown here in older screen shot, but is suppressed at the moment; it may be active again in the future after we have redeveloped the invite and tracking system.)

In this particular data table, you can see that the survey was taken by two subjects with numbers 727 and 734. Both are anonymous, which you can find out by clicking on them: their labels will read ‘anon727’ and ‘anon734’, which is the default labeling system for anonymous subjects.

What a ‘session’ is

We did not yet discuss the ‘Type’ and ‘ID’ columns. ‘Type’ refers to the level at which you are logging data. By default, you are logging data at the ‘session’ level: one ‘subject’ W was doing an experiment with ‘script’ Y written by ‘author’ (i.e., user) Z. The same subject, may use his or her invite link again to take the experiment for the second time. The ‘subject’, ‘script’, and ‘author’ will be the same but the ‘session’ will now be different: Once an experiment has been completed the session is closed.

What if a session has not been closed? If a subject stops doing an experiment midway, the experiment ‘session’ is not closed. If the subjects continues after a while, data is added to the same session. This is the meaning of ‘session’ you see in the ‘Type’ column. One subject can participate in several consecutive sessions: after completing the experiment, he may sometimes decide to do it again (e.g., to get a better score). After starting the experiment for the second time, a completely new session will be opened with a new session ID in the data tables.

A session is recognized by the NeuroTask Scripting system only if the subject continues on the same computer, using the same internet browser. The reason is that the NeuroTask Scripting system leaves so called session cookies behind and these are located in the browser’s database on a specific computer.

When does the system decide the session has to be closed, even though the subject has not finished it? We have set this period at 48 hours, meaning that a subject has two days to get back to the experiment to finish it. After that, an entirely new session is started. So, if a subject starts an experiment on Monday, is interrupted and only gets back to it on Friday (by clicking the experiment URL again), the old session will be closed and the experiment will be treated as a new experiment session.

7.4 Data storage and retrieval

Suppose, your experiment consists of two parts, say a memory task followed by a sleep survey. A subject does the memory task but then needs to do something else and turns off her computer. Later that day she remembers she still need to finish the survey part, turns her computer back on and surfs back to your experiment. To her great annoyance she has to do the memory task again and she decides not to bother with the experiment anymore. How can you prevent this situation?

Above, when discussing what a session is, we said that a session remains open for 48 hours, giving a subject to return to it within two days (even an anonymous subject). But how do you know whether a subject has already completed a section of your experiment? For this, we use store() and retrieve().

The store() function works very similar to the log() function with one great difference: It always overwrites earlier values stored with the same name. The log() function always adds a new row, the store() function adds new rows only if it is storing a value with a certain ID for the first time. Thereafter, it will keep that row and will update the value in that row. The retrieve() function will retrieve the current value for a certain ID, returning undefined if no row exists yet in the database. Also, the retrieve() will halt the script until the value has been retrieved and then continues. This may sometimes cause a slight delay (less than one second usually).

The store() and retrieve() function allow us to use bookmarks, as follows:

Script 7.4. Skipping a part the subject has already done.
 1 var bookmark;
 2 
 3 bookmark = retrieve("Where");
 4 
 5 if (bookmark !== "Part 1 Completed")
 6 {
 7     // Here comes the memory task
 8 
 9     store("Part 1 Completed","Where");
10 }
11 
12 // Here comes the sleep survey

The retrieve() function will return undefined if the variable does not (yet) exist, which will be the case when a subject first starts this experiment. After completing the memory task, the variable ‘Where’ will have the value ‘Part 1 Completed’. This means that the first part of the experiment will now be skipped. You can also use this technique to prevent subjects from doing an experiment twice, though in our experience it is better to allow this and simply not analyze this additional data.

Let’s extend the technique above to more than two parts. We will use a number as a bookmark and increase it as the subject does more parts:

Script 7.5. Skipping a part the subject has already done.
 1 var bookmark = retrieve("Where") || 0;
 2 
 3 if (bookmark < 1)
 4 {
 5     text("Press '1'"); // dummy task
 6     awaitkey('1');
 7     store(1,"Where");
 8 }
 9 
10 if (bookmark < 2)
11 {
12     text("Press '2'"); // dummy task
13     awaitkey('2');
14     store(2,"Where");
15 }
16 
17 if (bookmark < 3)
18 {
19     text("Press '3'"); // dummy task
20     awaitkey('3');
21     store(3,"Where");
22 }
23 text("Experiment completed!");

Here we use a well-known JavaScript idiom to assign a default value to a variable that may have value undefined (or null):

var bookmark = retrieve("Where") || 0;

The ||-operator evaluates the expression from left to right. As soon as it encounters something that evaluates to true it returns that value, or else the last value evaluated. If retrieve() returns undefined2, as it will when the variable does not yet exist, the ||-operator will skip the first part and try the second part, which it then returns.

When a subject does this script for the first time, bookmark is 0 and the first part will be presented. If the subject does task 1 (i.e., press 1) and then closes the webpage, the value of bookmark is stored as 1. On opening the experiment again, the usual Landing Page is shown, but now when subject presses the Start button, he or she skips the first part and is now asked to press 2.

If the subject completes the entire experiment and then returns to it later, he or she will start over from the beginning, as a second experimental session will be created with a new subject. The reason is that the system has no way of finding out whether the same person is doing the experiment again or a friend, relative or other person who has access to the same computer.

The retrieve() function also has a third argument, namely a timeout, which is set to 5000 ms by default. If the retrieve() function does not succeed in retrieving a value from the server within the timeout period, it will return null. You can disable timeout by setting the third argument to 0. But beware, in case the internet connection fails the script will stay stuck and not move forward.

You can check for null and undefined like this:

var x = retrieve("Where");

if (x === undefined) // Note the triple equal sign ===
{
    // Variable is not yet stored
}

if (x === null)
{
    // There was a timeout
}

Storing ‘behind the scenes’ or storing now

There is no timeout period for store() or log(), because these function do their job ‘behind the scenes’. While the rest of your script continues to run, the store() and log() functions are collecting data until they have enough or until no data has come in for a while. Then, they communicate with the server and send the data for processing and storage there. This does not interfere with your script. Just be sure that you do not suddenly have the subject move away from the script (e.g., by redirecting to a survey site) without first calling closesession(), which halts the script until all data has been written to the database. With retrieve(), a value must be retrieved before the script can usefully continue. Therefore, we let the script wait until the value has been secured or until timeout.

If you ever have a situation where you need to be certain for the good operation of your script that a value has with certainty been stored on the server, you can use the store_now() function, which works like store() but blocks further processing until the server has confirmed the correct storage of the variable. (This function stores and does not log the data.)

Storing at the ‘script’ and ‘author’ level

By default store(), store_now() and retrieve() operate at the ‘session’ level. The bookmark above was associated with the current session in the database. It may also be necessary to store variables at the level of the script. For example, you may want to count how many times subjects have done your experiment and use this number in your script to assign a new subject to one of two conditions (e.g., on even count to condition 1). This is achieved by storing variables at the ‘script’ level (always written in lower case).

You can also use this functionality to create multi-player turn-based games. Examples are given at https://scripting.neurotask.com/howto/multiplayer_game and another at https://scripting.neurotask.com/howto/tic_tac_toe_multiplayer_version. Here, the ‘script’ level is used to first have a new player A matched to waiting player B. Both players are given a long random ID string. Now player A can store a game move to that ID, e.g., in a chess game ‘bishop to e5’ could be sent as store_now('Be5','ssefse24948594','script'). After having, thus, ‘sent’ a move by storing it in this manner, the script will keep polling the id to see whether a counter move has been posted. (Or a more elaborate JSON structure may be ‘sent’ where also the sender is identified and the time.) If so, the player is notified and can make a move again. Etc. Each script can have many ‘channels’ in this manner, each linking two sessions.

In some cases, it is useful to store variables at the level of the author, for example, to keep track of information in several different scripts. You can use the ‘author’ level for this. Both ‘script’ and ‘author’ variables can be found via the Scripts menu at Scripts -> Script Data -> Stored Script Variables and Scripts -> Script Data -> Stored Author Variables, resp.

increase() and decrease()

As note above, there may be cases where you want to keep count of something, for example, how often a certain experiment has been done. Or you may want to assign consecutive numbers to your subjects and use these for your own bookkeeping. For this purpose, we have included the increase() function, which like store_now() will wait for a confirmation and then return the increased value. You can use it like this:

var subject_id = increase("Subject ID","script");

If the variable ‘Subject ID’ does not yet exist, it will be inserted into the data table with starting value 0 plus your initial increase value, which is 1 by default. If it exists, the value of the variable will be increased. This only works with (signed) integers. You can increase with a greater step size, by specifying this as a third argument, e.g., increase("Total_failed","script",failures) to update the total number of failures recorded (e.g., subjects failing a certain test).

So, if this would be the very first time the script is run by any subject, subject_id would receive the value 1. On each subsequent run, the return value would be 1 higher. Also, the ‘Subject ID’ variable in your data table would reflect how many subjects have done your experiment.

The decrease() function works completely similar to increase(), except that the specified amount is decreased from the current value. Note that both increase() and decrease() have default timeout periods of 5000 ms, which can be adjusted by adding a fourth argument (e.g., 10000 for 10 s or 0 if you want no timeout at all, see comments above), e.g., increase("Total_failed","script",failures,2000).

7.5 Working with the data tables

In this section, we will go over a few tricks for working with data tables. Note that these are not just used for session data, but also to present overviews of subjects, invites, and scripts. All of these can be downloaded to Excel and other formats and managed as described below. We will here focus on how to manage data collected in an experiment, but the principles are similar if you want to say manage the overview table in which all subjects are listed.

If you go to a script and then click on the table icon, you will see all data collected by that script during experimental sessions; each time log() was called another row was added to the table. If you have many subjects taking your tests and if you collect much data per script, this may result in thousands of rows. Fortunately, the data tables offer several ways to make inspection of your data more manageable, notably sorting and filtering.

Clicking on one of the headers (Type, ID, etc.) will sort the table according to that column. By default only 20 rows are shown, but in the Rows menu at the bottom, you can set this as high as 320. Next to the Rows menu, at the bottom of the table, is the Columns menu. There, you can uncheck the columns you are not interested in at the moment. These will be hidden from view and, if you wish, may be excluded from a download.

To alert you to any hidden rows and columns (lest you forget you had hidden these), at the very top of the table, the number of hidden rows and columns (if any) is given in the blue field above the column. If there are not hidden rows or columns, these fields turn grey.

Filtering data

Each column in the tables has a so called ‘filter’ option, which allow you to hide certain rows. Below the column header is a field that says ‘Type filter’ in pale grey letters, where you can type a ‘search’ text. You could, for example, type 'nt_' in the filter field of the ‘Name’ column. Any row of which the variable name contains 'nt_' somewhere (not necessarily at the beginning) will be shown and all other rows will be hidden. The number of rows hidden is shown in the blue field at the top-left of the table. The 'nt_' variables are the ones that are collected automatically. If you want to hide those rows, e.g., to focus on your own data, you must put an exclamation mark at the beginning, which in many computer languages, including JavaScript, means opposite or negation. So, you would enter '!nt_' in the filter field. By planning ahead a little with the names you give to your variables, you could make good use of these filter options. For example, you could filter on 'cond1' or 'cond2', or have summary variables like 'summary_test1', 'summary_test2', where you could filter on 'summary_' to get a quick overview.

For advanced users, there is even the possibility to use regular expressions as filters, if you start a filter text with a question mark it is interpreted as a regular expression. Regular expressions are quite powerful but hard to learn and their usage here is beyond the scope of this introductory manual. As an example: ‘?e$’ would show all names ending in ‘e’. And '?nt_|rt_' would show all values that contain either 'nt_' or 'rt_' (or both).

In the a date column, such as the Created column, you can filter among others by ‘days ago’. Today’s data can be shown with '0..1' (two dots). Last week’s data, including today, with '-7..1' and so on. Decimals are allowed to if you want say the last few hours. You can also click on the > < symbols, which will present you with calenders where you can select the ‘from’ and ‘to’ dates you wish to inspect.

In numeric fields, such as the Script (ID) column, you can use ranges as well, such as 1000..1200. Or !1000..1200 to exclude this range. Just typing say 1023 will show only the data of the script with ID 1023.

Finally there is the checkbox filter, which has values ‘indeterminate’ (which is the default, meaning it is not active), ‘checked’ where only the checked rows are shown, and ‘unchecked’ where only the unchecked rows are shown.

All of these filters not only change your view of the table by hiding certain rows, the view achieved by them can also be exported, if you want that.

7.6 Exporting data

If you scroll all the way down below the table, you see a title bar that says ‘Excel or CSV Export’. Clicking on brings up an image like this:

Data export panel

In the File Format menu on the left you can select the type of file you want to download:3

  • Excel 2007
  • Excel 5
  • CSV (plain text with comma-separated values)
  • HTML (web page)
  • PDF

With the Selection menu you specify which ‘view’ of the table you wish to download:

  • All
  • Filtered
  • Checked

‘All’ gives you all rows. Filtered gives you only the visible rows and columns. This is handy if you want to download a specific portion of your data. Checked will export only the columns that you checked by clicking the checkboxes on the very left-hand side. This is useful for very specific selections of rows.

The Separators menu is relevant if you want to export your data for use in a translated (non USA English) version of Excel, such as Dutch, which uses the comma for a decimal sign and the point to separate thousands.

In the menus at the bottom of the data table, you can also find three Quick Export options. These do not run via the NeuroTask server, but work locally, in the browser. They only produce Excel and the formatting is somewhat atypical. You can use it as a fallback in case there is a problem with the regular exporting facility, such as a sudden internet connectivity problem.

Because the data table is quite sophisticated in its filtering capacities, it becomes very slow and memory hungry for large data sets. If you have collected more than 100,000 data points (i.e., rows), this page will automatically redirect to data_alt which has only limited export possibilities. For this reason it is better to use the datadashboard, which also is much better suited to export data in a format suitable for subsequent import into SPSS and Jasp. It also allows filtering, if you first go to the ‘filter page’ via the Scripts menu: Scripts -> Script Data -> Prefilter Data for the Data Dashboard. There you can retrieve only data for one subject, or collected since so many days ago (e.g., to analyze the latest data), or of only a single named variable. The data dashboard will be discussed in more details below.

Pivot tables, or how to make your tables ‘square’ again

Some users may wonder about our choice to store all data in a big table with many rows and few columns. Wouldn’t it be handier to have a square table with say the subjects as rows and their data as columns. With thought very hard about this and our conclusion was: No, this is not handier except in certain specific case. The format we use is the least limiting format, because of one extremely handy tool: pivot tables. These are present in all Excel programs and we strongly encourage you to spend one or two hours familiarizing yourself with them.

A pivot table turns a long table like you download from NeuroTask Scripting into an arbitrary square table with summary statistics such as ‘count’, ‘mean’, and ‘standard deviation’. You can format an Excel pivot table in minutes, using dragging and dropping of fields. For example, suppose you have this:

x y
1 2
1 4
2 3
2 6

x could be subjects IDs and y a percentage correct on certain trials or reaction times.

With a pivot table we can take the mean value over all repeated measurements in conditions 1 and 2, which would give us this:

x mean
1 3
2 4.5

What an Excel pivot cannot do is to turn the long table into a square with textual and other values; you are forced to use numeric values and summary statistics as values of the pivot table (but not of the row and column headers).

7.7 Logging, storing, and the ‘response’ object

Data which is collected with the log() or store() functions is always also available in the response object. So, if in your script you write:

log(391,"average_rt");

The value 391 can be accesses in the response object, e.g.,

var save_for_later = response['average_rt'];

More useful is the fact that each string in NeuroTask Scripting has a format() function, which can take the response object as an argument, such that its key-value pairs can be used in a string, as follows:

var s = "Your reaction time was {average_rt} ms".format(response);

Now string s will be equal to ‘Your reaction time was 391 ms”. As is explained in the chapter on form controls, each of these controls already does this by default, so that earlier responses can be used in the labels and explanatory text of these controls.

7.8 Deleting data

In the Scripts -> Script Data menu, there is also an item marked with a red cross that says Page for Deleting Logged Data. At that page, you can delete logged data either for a specific subject, a specific logged event (for all subjects), or you can delete all logged data for the script. Deleting data may be desirable for security reasons and because of ethical considerations. Needless to say: be very careful with this. Data deleted in this manner can no longer be retrieved, not even by the NeuroTask Scripting staff!

7.9 Data Dashboard

The data dashboard, shown in the button bar with the data dashboard icon, is now the default way to view and export your data. It is much faster than the other data exports and can handle much larger files: we have tested Excel exports to files with up to 900,000 rows of data (using Firefox on Windows). To take full advantage of its capacities, it is necessary to log your data with the appropriate conditions. This works as follows.
Suppose, you are doing a lexical decision reaction time experiment with one dependent variable, called ‘RT’ (reaction time in ms). There are two conditions. ‘Condition 1’ is showing long or short words to which the subject must respond with ‘Word’ (press ‘s’ key) or ‘Non-Word’ (press ‘l’ key) as fast as possible. ‘Condition 2’ is whether it is in face a word or not. We can now log the ‘RT’ variable with the conditions as follows. (We have here entered some random RT values a particular subject might have.)

logtrial(120,"RT","short","word");
logtrial(150,"RT","short","nonword");
logtrial(130,"RT","long","word");
logtrial(200,"RT","long","nonword");

logtrial(160,"RT","short","word");
logtrial(165,"RT","short","nonword");
logtrial(170,"RT","long","word");
logtrial(180,"RT","long","nonword");

Here, we have simply entered some values for the reaction times; normally these would be obtained from actual subject responses. From left to write we have: value, variable name, conditions. If we run this experiment so that the data are stored and move to the data dashboard, we see something like:

Data table in the data dashboard logged with logtrial().

The conditions are indicated as ‘Con1’, ‘Con2’, … You can have as many conditions as you want but you cannot at present name them yourself. The dependent variable, however, will keep its own name. Now, this format can be imported easily into SPSS, JASP or some other analysis program.
You can still store one-off variables (e.g., age, education, gender, …). E.g.:

logtrial(22,"age");

This usage and its effect are identical to the log() function:

log(22,"age");

It is possible to store many different dependent variables with the same or different conditions:

logtrial(120,"RT","short","word");
logtrial(1,"Accuracy","short","nonword"); // 1 correct answer

Both dependent variables will now be available, each in their own column, though you are also able to select just one variable via a dropdown select.
The logtrail() function is merely a ‘convenience’ function that translates the above into:

log(120,"RT/short/word");
log(1,"Score/short/nonword"); // 1 correct answer

The /-notation used with log() gives identical results to the logtrial() function and you can use either; there is no real reason to prefer one over the other apart from convenience.

The data dashboard has a preview table to give you an idea of your data and options to show/not show data index, subject ID, etc. You can also hide the trial data (i.e., that stored with logtrial()), export only nt data (with technical properties of subjects’ browsers), only data from one condition, etc.

There are many export formats available, some quite arcane. The page may have some initial delay while it is setting up the download. For long downloads the progress can be followed in the progress bar. Once the data have been downloaded to the page, exports are very fast and do not require additional server access; they are generated in the browser.

In case you are mainly collecting survey data, the data dashboard format is less convenient, perhaps, and you may prefer the other data page.

8 Animation and drag-and-drop

8.1 Animation

It is easy to add simple animations to your experiments. They are often useful to give feedback, making interactions with the experiment more natural. For example, after a subject clicks on a block, it might change color briefly to indicate the click was received. Or if a subject clicks on a wrong block or gives a incorrect answer, the block might briefly turn red to indicate an error. Of course, you could do a lot more with animation, such as adding game-like elements to your experiments.

With ‘blinking’ we mean a brief change in color of a block. Because this is such a common task, NeuroTask Scripting includes the blink() function, which gradually turns the color from whatever it currently is to black and then back again. The total animation time is half a second but both the color and the animation time can be adjusted. An illustrative script is as follows:

Script 8.1. Animation with blink().
1 var b = addblock("center","center","30","20","lightblue","Click me!");
2 
3 b.await("click");
4 b.blink("");
5 
6 await(5000);

This shows a light blue block that says ‘Click me!’ in black letters. When you click it, it gradually turns black in 250 ms and then light blue again in the next 250 ms. If you don’t like the black you can specifiy another color. With red, we obtain:

Script 8.2. Animation with blink(), turning to red instead of black.
1 var b = addblock("center","center","30","20","lightblue","Click me!");
2 
3 b.await("click");
4 b.blink("red");
5 
6 await(5000);

If you want a brief flash, you can shorten the animation time, e.g.,

Script 8.3. Brief ‘flash’ animation with blink().
1 var b = addblock("center","center","30","20","lightblue","Click me!");
2 
3 b.await("click");
4 b.blink("red",200);
5 
6 await(5000);

Blinking other properties with toggle()

Now, suppose you don’t want to blink the background color but the font color instead, how would you do this? To blink properties other than the background color, we have included the toggle() function, which takes a style property name, a begin value, an end value, an optional duration, and an optional units argument (see next section on the animate() function). Blinking the font color to red would be accomplished as follows:

Script 8.4. Blinking font color with toggle().
1 var b = addblock("center","center","30","20","lightgrey","<b>Click me!</b>");
2 
3 b.await("click");
4 b.toggle("color","black","red",1000);
5 
6 await(5000);

On clicking block b, the font color changes from black to red in 500 ms and back again to black in another 500 ms (1000 ms total). To make the change more noticeable, we have added <b> tags, making the ‘Click me!’ text appear in bold-face.

Using the animate() function

The toggle() function always goes back and forth. If you do not want this, you can use the animate() function. Suppose we want to gradually increase the size of the font with a factor three with an animation that lasts one second. Using the animate() function, we would do something like:

Script 8.5. Animating a font-size increase with animate().
1 var b = addblock("center","center","60","20","lightgrey","<b>Click me!</b>");
2 
3 b.await("click");
4 b.animate("font-size",100,300,1000,"%");
5 
6 await(5000);

When you click on the block, the text increases gradually to three times its size, while staying centered. In the script, line 4 does most of the work; it says “Taking one second, animate the ‘font-size’ property from 100% (its current value) to 300%”.

The last argument to the animate() function in line 4 is very important. If you leave it out, the default unit type for font-size will be used, which is ‘point’ (1/72 inch, abbreviated to ‘pt’). Hundred points is very large: 10 to 12 point is normal-sized text and 300-point text is positively huge. Using 100% would give the normal size of a font. We strongly recommend working with percentages as much as possible. For colors, it is not necessary to specify the type of unit (see our toggle() example), because the default value suffices here.

A general approach to animation with RAF()

The functions discussed above are implemented with the Dojo library’s fx package. This package offers several other animation options but these are more suitable for an advanced book. Instead, we will here discuss a general approach to animations that is fairly easy to build with NeuroTask Scripting compared with ‘plain’ JavaScript. For this, we need to use the RAF() function, which is an abbreviation of requestAnimationFrame(). The latter is now a standard function in all modern browsers. Only very old browsers do not support it, such as Internet Explorer 9 and older (noting that even IE11 is no longer officially supported by Microsoft), see https://caniuse.com/?search=requestanimationframe for an updated overview. The RAF() function halts processing until the browser is about to refresh (i.e., update) the screen. Normally, this happens exactly 60 times per second, though this so called ‘refresh rate’ may take other values on certain computers, notably on smartphones.

The way RAF() can be used is by preparing the new screen and then waiting until the screen refreshes, at which point the changes are shown on the screen. E.g., you could repeatedly move a block 1% to the right and 1% down with b.move(1,1) to create an animation of a moving block. If you would not use the RAF() function, however, the animation would appear jerky. Also, the speed of the animation would depend on the processing speed of your system: on a faster system the animation would appear (very) fast as well. With RAF() you can pace this more precisely.

Using icons with the icon() function

As an example, we will consider a script that shows a moving ‘bug’ on the screen. The ‘bug’ is not shown with an image file but with a so called icon from a symbolic font (i.e., a font that includes drawings or icons instead of letters and numbers) using the function call c.icon('bug'). This particular icon comes from the well-known Font Awesome 3.2.1 Collection, which is available with NeuroTask Scripting by default. All icons in version 3.2.1 of the collection can be used, where you must leave out the ‘icon-‘ part. Font Awesome also includes many useful arrow shapes. We realize that new versions of Font Awesome are constantly coming out but for backward compatibility, we keep supporting version 3.2.1. We may add newer versions in the future.

You do not need to preload icons with the icon() function and you can optionally give a size (in %) as a second argument. The function will return the block, so you can style the icon as with text, like this

c.icon('bug',350).style('color','crimson');

to get a bug icon that is 350% in size and has the color ‘crimson’ (a type of red). Note that we use the style property ‘color’ here because the icon shapes are like the letters and other characters in a font and we must adjust the font color. A great advantage of icons over images is that they scale well: even at large magnifications the curved lines remain smooth.

An animation loop

The code for the animation is as follows:

Script 8.6. Animation with RAF() and a for loop.
1 var c = addblock(1,1,15,15).icon('bug',350),
2     i;
3 
4 for (i = 0; i < 100; i++)
5 {
6     RAF();
7 
8     c.move(1,1);
9 }

We see a for loop that goes through 100 iterations. Each of these iterations will wait until the next animation frame with RAF(). Each wait will last about 16.7 ms (at 60 Hz). After the screen has refreshed, the block with the bug icon is moved with the move() function, moving block c one percent of the width and height of the main box. The bug starts in the left top and moves towards the right and bottom.

Using the approach above it is easy to change other properties while waiting for the next animation frame, for example, we might increase the size of the bug as follows:

Script 8.7. Animating multiple properties in an animation loop.
 1 var c = addblock(1,1,15,15).icon('bug',350),
 2     i;
 3 
 4 for (i = 0; i < 100; i++)
 5 {
 6     RAF();
 7 
 8     c.move(1,1);
 9     c.style('font-size',100+i+'%');
10 }

The bug now crawls down and right while increasing in size from 100% to 199%.

We could extend the crawling time and add some logic so the bug won’t crawl outside the box. We could also add some randomness to make its movements more interesting. This might be accomplished as follows:

Script 8.8. Randomly moving bug in an animation loop.
 1 var c = addblock(1,1,15,15).icon('bug',350),
 2     i,
 3     speed = 1,
 4     hor = speed, ver = speed;
 5 
 6 for (i = 0; i < 2000; i++)
 7 {
 8     RAF();
 9 
10     c.move(hor,ver);
11 
12     if (random() < .025)
13     {
14         hor = -hor;
15     }
16 
17     if (random() < .025)
18     {
19         ver = -ver;
20     }
21 
22     if (c.left < 0 || c.left > 85)
23     {
24         hor = -hor;
25     }
26 
27     if (c.top < 0 || c.top > 85)
28     {
29         ver = -ver;
30     }
31 }

We randomly make it change direction using the random() function, which returns a pseudo-random number between 0 and 1. We also invert its direction when it threatens to leave the box. We will return to this script in the final section of this chapter, turning it into a simple game. But for this, we first need to discuss dragging and dropping of blocks.

8.2 Drag-and-drop

Drag-and-drop basics

In many experiments it is necessary that the subject indicates some type of choice or selection by dragging screen elements to certain locations. Making a block moveable is pretty easy, but finding out where it ends up and whether it has, for example, been dropped on some other block requires a little bit of coding effort. The basic code for making a block moveable is:

Script 8.9. Making a block moveable with moveable().
1 var b = main.addblock("center",70,20,20,"green","Move me!").moveable();
2 
3 await(10000);

This puts a green block of 20% by 20% on the screen with the text ‘Move me!’. Simply adding moveable() is all that is required to make the block moveable. Just click on it with the left mouse button and move the cursors while you keep pressing the button down, the block will move with the cursor. When you release the button, the block will remain where you dragged it.

Dragging a block inside (on top of) another

A common task in experiments is to move items to locations, for example, moving a card to one of two piles or moving symbols to grid locations where they were observed during an earlier presentation. We now know how make the to-be-moved items moveable, but how can we verify whether the items have been put into a correct or incorrect destination? One way to do this is by periodically checking whether an item (i.e., a block) is inside one of the target blocks. Checking has to be fairly frequent, for example, every 50 ms, as follows:

Script 8.10. Detecting that a block has been dragged inside (on top of) another.
 1 var a = addblock("center",10,25,25,"lightcoral"),
 2     b = addblock("center",70,20,20,"lawngreen").moveable(),
 3     i;
 4 
 5 for (i = 0; i < 200; i++)
 6 {
 7     await(50);
 8 
 9     if (b.inside(a))
10     {
11        b.text("Completely inside");
12     }
13     else
14     {
15         b.text("Not completely inside");
16    }
17 }

You can move the green block around. It will almost immediately show the text ‘Not completely inside’. If you move the green block completely inside the red (i.e., light coral colored) block, the text will change to ‘Completely inside’. Try moving the green block inside and outside the red block a few times; the text keeps changing. After 10 s the demo will stop automatically. This approach works well because dragging of a block may continue while the await() function is waiting for the 50 ms to pass.

To check whether the green block b is inside the red block a, we use the built-in inside() function, calling b.inside(a), which returns true only if block b is completely inside (on top of) block a.

Drag-and-drop with multiple drop targets

Suppose that in some task a card has to be placed on one of five piles. How can you recognize on which pile it was dragged? We can use a variant on the technique above with some extra logic, as follows:

Script 8.11. Detecting on which pile a block has been dragged.
 1 var a = [addblock(10,10,12,12,"lightcoral"),
 2          addblock(25,10,12,12,"lightcoral"),
 3          addblock(40,10,12,12,"lightcoral"),
 4          addblock(55,10,12,12,"lightcoral"),
 5          addblock(70,10,12,12,"lightcoral")],
 6     b = addblock("center",70,8,8,"lawngreen","?").moveable(),
 7     i, j, is_inside;
 8 
 9 for (i = 0; i < 200; i++)
10 {
11     await(50);
12 
13     is_inside = false;
14 
15     for (j = 0; j < a.length; j++)
16     {
17         if (b.inside(a[j]))
18         {
19             b.text(j+1)
20             .style('background-color','maroon');
21             is_inside = true;
22         }
23     }
24 
25     if (!is_inside)
26     {
27         b.text("?")
28          .style('background-color','lawngreen');
29     }
30 }

We now have five target blocks a[0] to a[4]. There is also a new variable is_inside, which is true only if the moveable block is inside any of the five target blocks. In that case, the moveable block shows the number of block (counting from 1 rather than from 0 so the subject does not get confused) and the background is turned maroon. If after the for loop in lines 15 to 23 the is_inside variable is still false, the text is set to ‘?’ and the background color to lawn green in lines 19 and 20. This approach can be used to create a card sorting task.

8.3 Putting everything together: A simple game

This combines both the animation and drag-and-drop discussed in this chapter. A red bug moves around. The block c contains a bug icon and block b a target (or bullseye) icon. The game loop runs for 2000 cycles and waits until the next animation frame (about 16.7 ms usually). Then, it moves the block, checks whether the bug block is inside the target block and, if so, writes ‘Caught!’ in message block a and breaks out of the loop. If the bug is not caught there is a 2.5% chance that horizontal or vertical directions are reversed. Finally there is a check whether the bug is moving out of the Box, in which case the relevant direction is reversed as well.

Script 8.11. Game where you must catch a randomly moving bug by dragging a target symbol on top if it.
 1 var a = addblock("center","bottom",80,20).text("Press Space bar to start!"),
 2     c = addblock(1,1,15,15).icon('bug',350)
 3                            .style('color','crimson'),
 4     b = addblock(40,40,20,20).icon('bullseye',450)
 5                              .style('color','navy').moveable(),
 6     speed = 1,
 7     hor = speed, ver = speed;
 8 
 9 awaitkey(" ");
10 a.clear();
11 
12 for (var i = 0; i < 2000; i++)
13 {
14     RAF();
15 
16     c.move(hor,ver);
17 
18     if (c.inside(b))
19     {
20         a.text("Caught!!!");
21         break;
22     }
23 
24     if (random() < .025)
25     {
26         hor = -hor;
27     }
28 
29     if (random() < .025)
30     {
31         ver = -ver;
32     }
33 
34     if (c.left < 0 || c.left > 85)
35     {
36         hor = -hor;
37     }
38 
39    if (c.top < 0 || c.top > 85)
40     {
41         ver = -ver;
42     }
43 }
44 
45 c.animate('font-size','350%','0%',1000); // fade out the bug
46 await(5000);

9 Sound

Sound support in NeuroTask Scripting is based on the battle-tested JavaScript library SoundManager2, which provides a great cross-browser sound player. It supports older (i.e., really, really old) browsers using a Flash plugin and for recent browsers it uses native HTML5 sound play back whenever available. Like with video in the next chapter, NeuroTask Scripting aims to hide details that may daunt beginners. For advanced users, however, most of the more complex stuff can still be reached through a range of options.

Sound is not attached to a Block as that does not seem to make much sense. A sound continues until the end or until interrupted.

Playing a sound file is done as follows:

play('cow.mp3');
await('soundended');

This will play the entire sound file, halting execution until the end of the sound has been reached. Then this script will finish. If you do not add the await() statement, the script will continue while still playing the sound. In this case, it would show the end screen.

9.1 Preloading sounds

Consider the following script:

 1 var main = new Box('lightgrey'),
 2     a = main.addblock("center","top",50,25),
 3     b = main.addblock("center","center",50,50,"lightgrey","Loading...");
 4 
 5 image.preload('cow.png');
 6 image.await('preloading_completed');
 7 
 8 sound.preload('cow.mp3','mooh');
 9 sound.await('preloading_completed');
10 
11 a.text("Click the cow!");
12 b.clear();
13 b.setimage("cow.png",25);
14 b.await('click');
15 b.clear();
16 await(2000);
17 b.setimage("cow.png",50);
18 
19 sound.play('mooh');
20 await('soundended');
21 
22 a.clear();
23 b.text("Mooh said the cow!");

This will show the text ‘Loading…’ in the center for a while, which is cleared, after which it says ‘Click the cow!’ near the top. As soon as you click the cow image in the center (with the left mouse button), a mooh sound is heard. When it has ended, the text ‘Mooh said the cow!’ appears.

In the script, you see preload() statements. What these do is exactly that: they are preloading both the cow image and the mooh sound. The await() statements ensure that after line 9, both the image and the sound are available. Execution of the script continues only after both the image and sound file are completely loaded. If preloading was not done, it is likely that when encountering

13 b.setimage("cow.png",25);

the script would start loading the image, which in some cases might lead to noticeable delays. The same applies to the sound; a large sound file might take a second or so to download. Preloading ensures that, once it has been completed and all files are available, the script will run smoothly. This is essential in many experiments.

The sound.preload() statement allows giving the sound file, ‘cow.mp3’, a name or ID. Here we use ‘mooh’. This is sometimes handy for long file names, but it remains fully optional; there is no obligation to use the ID.

9.2 Advanced options

Options are taken straight from SoundManager2 and are given as a second argument to play(). They are:

 1 autoLoad: false,        // enable automatic loading (otherwise .load() will call
 2                         // with .play())
 3 autoPlay: false,        // enable playing of file ASAP (much faster if "stream" is
 4                         // true)
 5 from: null,             // position to start playback within a sound (msec)
 6 loops: 1,               // number of times to play the sound
 7 multiShot: true,        // let sounds "restart" or "chorus" when played multiple
 8                         // times.
 9 multiShotEvents: false, // allow events (onfinish()) to fire for each shot, if
10                         // supported.
11 onid3: null,            // callback function for "ID3 data is added/available"
12 onload: null,           // callback function for "load finished"
13 onstop: null,           // callback for "user stop"
14 onfinish: null,         // callback function for "sound finished playing"
15 onpause: null,          // callback for "pause"
16 onplay: null,           // callback for "play" start
17 onresume: null,         // callback for "resume" (pause toggle)
18 position: null,         // offset (milliseconds) to seek to within downloaded sound.
19 pan: 0,                 // "pan" settings, left-to-right, -100 to 100
20 stream: true,           // allows playing before entire file has loaded (recommended)
21 to: null,               // position to end playback within a sound (msec)
22 type: null,             // MIME-like hint for canPlay() tests, eg. 'audio/mp3'
23 usePolicyFile: false,   // enable crossdomain.xml request for remote domains
24                         // (ID3/waveform access)
25 volume: 100,            // self-explanatory. 0-100, the latter being the max.
26 whileloading: null,     // callback function for updating progress (X of Y bytes
27                         // received)
28 whileplaying: null,     // callback during play (position update)

So, you could do sound.play("mooh",{volume:25}) to play at 25% volume level. It is possible to play only a fragment with to and from in the options shown above. You could for example have an mp3 file with many sounds, each lasting say exactly 1000 ms, and then play just a segment with from and to.

10 Working with video

Video playback in NeuroTask.js based on the JavaScript library MediaElement.js, which provides an excellent cross-browser video player that supports both older browsers and newer ones. NeuroTask Scripting hides most of the complexities, though these are still available if you need to do things that are not supported out of the box.

Adding video to a script is straightforward, requiring just two statements:

setvideo('big_buck_bunny');
await("videoended");

This will show a video player with the movie. You should specify the name of the movie without the exentsion (e.g., write ‘big_buck_bunny’ and not ‘big_buck_bunny.mp4’).

The final statement, await("videoended") makes sure the script does not close immediately, but waits for the end of the movie. You can also specify other events for which to wait, such as a certain time or a certain key to be pressed.

To actually show a movie, you must first upload it to your account. After that, it will appear in the list of video files in the Quick Reference side panel on the ‘editscript’ page. The movie file should be in .mp4. It is highly recommended to also upload the same file in .webm format, so that it all browsers and platforms are supported. If you just want to run it on Chrome, you only need to provide the .mp4 format. Formats other than .mp4 and .webm are not supported (no .avi, .mkv, etc.), but you can convert any movie into the desired formats with free video conversion tools, such as Any Video Converter.

10.1 Subtitles and chapters

You can add subtitles, chapters (and a little more control) as follows:

var s = {
    subtitles: "bunny_subtitles.srt",
    chapters: "bunny_chapters.vtt"
}

main.addblock("center","center",100,60).setvideo('big_buck_bunny',100,100,s);

await("videoended");

First you specify all the options in an object (here called s). The two files specify the subtitles and chapters. A subtitle file is in the .srt format, which is a standard format for subtitles. A good website to create these and get more explanation about them is Subtitle Horse. Chapters are shown at the top of the screen and allow jumping through a movie, which may come in handy with instruction videos.

It is necessary to construct the block in which you show the video explicitly so there is enough room to show the subtitles. This also gives you more control over the size and location. If you specify the relative width and height of a video as 100 (percent), as in the statement above, the video will fill exactly the block it is in. You must make sure that the block has a similar shape (aspect ratio) as the video.

10.2 Advanced options

The following script illustrates some advanced options. This video starts by itself and cannot to be paused or skipped. The participant in the experiment is thus forced to see the entire video. However, as an illustration, we had added the option to skip it by clicking with the left mouse button on block b3, a so called ‘click’ event.

The waitfor ... or ... construct allows two or more events to be specified, where the first one to occur is handled and the rest is ignored. So, we we click on b3, the script is no longer waiting for the end of the movie to occur.

 1 var b1, b2, b3, options, advanced_options;
 2 
 3 main = new Box();
 4 
 5 b1 = main.addblock("center","top",100,25)
 6          .text("Testing advanced (MediaElement.js) options<br>"
 7              + "This 1 min movie cannot be skipped or fast forwarded<br>"
 8              + "It has to be watched entirely");
 9 b2 = main.addblock("center","center",60,60);
10 b3 = main.addblock("center","bottom",100,15).button("Skip Anyway!");
11 
12 var video_options = {
13         autoplay: true,
14         subtitles: "bunny_subtitles.srt"},
15     advanced_options = {
16         clickToPlayPause: false,
17         features: ['volume','fullscreen'],
18         enableKeyboard: false};
19 
20 waitfor
21 {
22     b2.setvideo('big_buck_bunny',100,80,video_options,"bunny",advanced_options)
23       .await('videoended');
24 }
25 or
26 {
27     b3.await('click');
28 }
29 
30 await(1000);
31 
32 b2.clear();
33 b3.clear();
34 b1.text("This could have been an instruction video or a scene for eye-witness
35         questions, etc.");

Other options and advanced options are listed in the following sections.

10.3 Supported video options

The following options are supported:

 1 == arguments ==
 2 
 3 video_url   url without an extension, preceded by a path within the videos
 4             directory, e.g., animals/grazing_cows
 5 width       percentage of block size (100% is default)
 6 height      percentage of block size (100% is default)
 7 options     object with objects, see below for details
 8 id          id for the video/media element (default is 'videoplayer1' for first
 9             player on page, and then 2, 3, ...)
10 
11 == options ==
12 
13 options object (all members below are optional; all paths are within the videos
14             directory)
15 
16 hasposter   false by default (this is the image shown at the beginning, before
17             playing)
18 poster      path/name.jpg or .png (default is url.jpg), e.g., animals/grazing_cows_
19             evening.jpg
20 
21 autoplay    false On true, start playing immediately
22 
23 style       'ted', 'wmp', or 'mejs' (= default). N.B. Must be lower-case.
24 formats     ['mp4','webm'] is default. Also allowed: 'ogv'. N.B. Even with one
25             format, you still need an array, e.g., ['mp4'].
 1 subtitles   path/name.srt, e.g., animals/cows.srt (e.g.,
 2             see http://subtitle-horse.com/ for online editor)
 3 chapters    path/name.vtt, e.g., animals/cows_and_horses.vtt
 4 
 5 from        position where to start in ms (0 is default)
 6 to          position where to end in ms (-1, signals end of movie, is
 7             default) (the to option is not implemented yet)
 8 
 9 volume      80, initial volume (mejs uses 0.8 here; we remain consistent with
10             SoundManager2)

In addition, the following advanced options are supported:

 1 // if the video width is not specified, this is the default
 2 defaultVideoWidth: 480,
 3 // if the video height is not specified, this is the default
 4 defaultVideoHeight: 270,
 5 // if set, overrides video width
 6 videoWidth: -1,
 7 // if set, overrides video height
 8 videoHeight: -1,
 9 // width of audio player
10 audioWidth: 400,
11 // height of audio player
12 audioHeight: 30,
13 // initial volume when the player starts
14 startVolume: 0.8,
15 // useful for audio player loops; the mediaelement player can also play (just) audio
16 loop: false,
17 // enables Flash and Silverlight to resize to content size
18 enableAutosize: true,
19 // the order of controls you want on the control bar (and other plugins below)
20  features: ['playpause','progress','current','duration','tracks','volume',
21             'fullscreen'],
22 // Hide controls when playing and mouse is not over the video
23 alwaysShowControls: false,
24 // force iPad's native controls
25 iPadUseNativeControls: false,
26 // force iPhone's native controls
27 iPhoneUseNativeControls: false,
28 // force Android's native controls
29 AndroidUseNativeControls: false,
30 // forces the hour marker (##:00:00)
31 alwaysShowHours: false,
32 // show framecount in timecode (##:00:00:00)
33 showTimecodeFrameCount: false,
34 // used when showTimecodeFrameCount is set to true
35 framesPerSecond: 25,
36 // turns keyboard support on and off for this instance
37 enableKeyboard: true,
38 // when this player starts, it will pause other players
39 pauseOtherPlayers: true,
40 // array of keyboard commands
41 keyActions: []

10.4 Getting the video player

Once created with b.setvideo(), using the correct arguments, the video player in block b can be accessed with b.video. This allows a few additional functions to be used, such as b.video.play() and b.video.pause(). There is no stop() function because the HTML5 standard does not include this. Other properties can be found at http://mediaelementjs.com/#api. We will incorportate and document additional options later.

10.5 Showing the same video simultaneously in two blocks

For some purposes you may want to show the same video simultaneously in several blocks, e.g., left and right of a fixation point to test the distracting effect of certain moving stimuli. It is not possible to achieve absolutely simultaneous playback but you can get pretty close. The following script shows two movies, left and right, which start automatically and run to the end. There are no options for the subject to change anything about these movies: no skipping, full-screen, etc.

 1 var b1, b2, b3, cover, options, advanced_options;
 2 
 3 b1 = addblock("center","top",100,25)
 4      .text("Two snakes");
 5 
 6 b2 = addblock("left","center",30,20);
 7 b3 = addblock("right","center",30,20);
 8 
 9 // Cover player until it is playing to hide initial startup stuff
10 cover2 = addblock("left","center",32,32,"white");
11 cover3 = addblock("right","center",32,32,"white");
12 
13 var video_options = {
14         autoplay: true},            // Start right away
15     advanced_options = {
16         clickToPlayPause: false,    // Disable click
17         features: [],               // No controls visible (Play etc.)
18         pauseOtherPlayers: false,   // Allow two or more players
19         enableKeyboard: false};     // No keyboard shortcuts
20 
21 b2.setvideo('animated_snake_moving_1',100,100,video_options,"snake1",
22             advanced_options);
23 // no black background
24 query(".mejs__container",b2.node).style("background-color","white");
25 query(".mejs__controls",b2.node).style("display","none"); // Hide controls
26 
27 // Make sure the second video player has a different ID, here 'snake2'
28 b3.setvideo('animated_snake_moving_1',100,100,video_options,"snake2",
29             advanced_options);
30 query(".mejs__container",b3.node).style("background-color","white");
31 query(".mejs__controls",b3.node).style("display","none");
32 
33 b3.await('videoplaying'); // Starting a video may take some time, so we wait for this
34 
35 cover2.style("background-color","transparent"); // Then, we remove the cover on
36              //playing
37 cover3.style("background-color","transparent");
38 
39 await("videoended"); // We finish up as soon as the first player has ended
40 
41 b2.deletevideo(); // Delete the players; we will (re)create new ones all the time
42 b3.deletevideo();
43 b2.clear(); // We clear the block
44 b3.clear();
45 
46 await(500);
47 
48 b1.text("That's all folks!");
49 
50 await(2000);

Blocks b2 and b3 show the same video, which is assumed to be available in ‘mp3’ and ‘webm’ format. Both blocks are initially covered by two white, non-transparent ‘cover’ blocks. The reason we do this here is that while the video players are loading their movie, they will show a wait cursor and may occasionally flash. This is now hidden behind the cover. Both videos will start playing nearly immediately at which point they emit the ‘videoplaying’ event. The covers are then made transparent. As soon as the videos have ended we delete the video players and clear the blocks.

The MediaElement player will show black bars if the video native size does not coincide with the specified size. We restyle these black bars to white bars, making them invisible. This done by finding the background DOM node with query(".mejs-container",b2.node) and then using the style() function on the returned DOM node. We similarly make the block with volume controls etc. invisible. Though we specified with the features option, features: [], that no controls should be visible, some browsers still show an empty grey area for this.

11 Graphics

With graphics, here, we mean vector graphics. That is, you do not show premade jpeg or png images but actually draw something on the screen, like arrows, circles, or squares. Modern browsers and most old ones support this form of graphics in some form or other. A difficulty is that older browsers used different conventions and methods to draw the graphics, which is why it is necessary to use a JavaScript vector graphics library. If not, your drawings will not show up on certain (older) browsers. A good graphics library will convert the drawing commands into a format the browser of the subject’s machine will understand.

With NeuroTask Scripting, vector graphics are supported by the Dojo graphics library, which has great cross-browser support: Old browsers are supported using their native graphics systems: Firefox 1.5-3.0, Safari(Webkit) 3+, Opera 9+, Chrome 1.0+, iPhone Safari 2.1+, and Internet Explorer 6-7-8. For more modern browsers Canvas is used, the modern-day vector graphics rendering standard: Firefox 3.0+, Safari 3.0+ including iOS Safari 1.0+), Opera 9.0+, Chrome, and Internet Explorer 9+. Note that even the ancient Internet Explorer 6 is supported by the Dojo graphics library (Dojo GFX)!

A simple script that draws a red circle on the screen with Dojo GFX looks like this:

x.1 Using Dojo GFX to draw a red circle.
 1 var b = main.addblock(40,40,20,20,"lightgrey"), // Create a light grey block
 2     e;
 3 
 4 e = b.getsurface().createEllipse({
 5     cx: "50%",
 6     cy: "50%",
 7     rx: "40%",
 8     ry: "40%"
 9 });
10 
11 e.setFill("red");
12 
13 awaitkey(' ');

A (Dojo GFX) graphics surface is obained from Block b with getsurface(), which can then be used to draw shapes on. If a surface does not yet exist (which is the case in newly created blocks), getsurface() will create one. Else, it will simply return the existing surface. The circle is drawn with createEllipse() using relative (percent) coordinates for the center of the circle, (cx,cy), horizontal radius rx, and vertical radius ry. It is necessary to call setFill() explicitly with a color as the default is to show a fully transparent shape (i.e., you will not see anything). You can use all the familiar ways to indicate color, including an array of three integers in the range 0 to 255 to indicate red, green, and blue color values.

A Block’s surface can be cleared with b.clearsurface() (for a Block b), which will remove all shapes from the surface but leaves the surface itself in tact, ready for new drawing operations.

There are quite a few functions you can use, which are not yet discussed in this release of the book, including showing SVG files, mouse event handling, color gradients, text in different fonts and at any angle etc. These are covered, however, by the documentation about the Dojo GFX libraries. All of this is relevant and can be used immediately in NeuroTask scripts.

Notice that Dojo GFX uses inter-caps, as in createEllipse(), but NeuroTask Scripting does not (e.g., we use addblock() instead of addBlock()).

12 Pivot Tables

NeuroTask.js offers a exec.pivottable() command to summarize data, which is useful to give feedback to participants or to keep an eye on the progress of an experiment. It is also possible to use it as a component in more complex user interfaces and as preprocessing for charting (see below).

1 function exec.pivottable(/*zipped or unzipped data*/data,/*string*/rows,
2                          /*string*/columns,/*(optional) object with key:function
3                          pairs*/func)

A pivottable takes data from data and puts this in a table with rows and columns. The values are calculated with func, which is an object with key:function pairs. The keys are used to name the variable in which the result is stored, e.g., ‘mean’ or ‘variance’. Because ‘mean’ is the default value it does not have to be provided.

1 exec.pivottable([{x:1,y:2},{x:1,y:4},{x:2,y:3},{x:2,y:6}],"x","y")

returns

1 [{x:1,mean:3},{x:2,mean:4.5}]

The mean is taken over the y-values, which are collapsed by the pivot table. E.g., x could be trials in condition 1 and2, and y could be measurements, such as number of words recognized correctly, then the data would be stored as with the log() command as:

x y
1 2
1 4
2 3
2 6

With a pivot table we can take the mean value over all repeated measurements in conditions 1 and 2, which
would give us this:

x mean
1 3
2 4.5

Other operations can be used as well. Any function that takes an array as input and returns a single value can be used, including user-defined functions. Several predefined functions are offered in stat.js:

  • stat.mean

Average

  • stat.median

Median

  • stat.stddev

Standard deviation

  • stat.stderr

Standard error

  • stat.variance

Variance

  • stat.sumsqerr

Sum of squared errors (i.e., sume of squared deviations from mean)

  • stat.min

Lowest value

  • stat.max

Highest value

  • stat.range

Difference between highest and lowest value

One or more of these functions can be called as follows:

exec.pivottable([,,,],
“x”,”y”,)
exec.setpivottable(/*string*/response_variable_name,
                   /*string*/rows,/*string*/columns,
                   /*optional object with key: function pairs*/func,/*string*/name)

This is the run-time variant of pivottable() that will add this statement to the execution flow of the experiment and puts the result in the response object. Data is taken from logged data array with getdata(name),
where name is 'default' by default. The table is stored in the response object as response_variable_name.

13 Synchronous and asynchronous programming

13.1 Asynchronous programming (is still possible in NeuroTask Scripting!)

By nature, JavaScript is asynchronous. In practice, this means that the main program (called thread) always continues. It is impossible to have a script wait somewhere for something. So, how can you still do timed operations? Or how do you wait for events like a click? This can be done in ‘vanilla’ JavaScript but usually a library like jQuery is used (preloaded in NeuroTask Scripting), as in the following example:

var c = main.addblock("center","center",20,5,"Click Me!","yellow");

c.node.id = "jantje";

$("#jantje").on("click",function(){
    console.log("ONE");
});

console.log("TWO");

awaitkey(" ");

What do we expect to see on the console output? ONE, TWO? No, we will see TWO and then each time you click the yellow block, ONE is added. The script ends when the spacebar is pressed and the yellow block disappears. The construction:

$("#jantje").on("click",function(){
    console.log("ONE");
});

is an example of asynchronous programming. This is typically based on having a certain element (e.g., a block) ‘listen’ for an event (here a ‘click’). when the event occurs, a function is called. Such a function is often called a handler. Here, the handler prints “ONE”. Most JavaScript applications are constructed from these asynchronous constructs. They are particularly handy when many events can occur and when the handling is brief and when the order over evens is not very important. This fits many web applications. It does not, however, fit how most experiments work. These are much more akin to recipes as we saw in the beginning of this book. Do this, then do that, then wait a while, etc.

The main weak point of asynchronous-only systems is that they cannot halt the script execution until a certain time has lapsed or until a certain event has occurred. Only synchronous system can do this. So, in ‘vanilla’ JavaScript, if you want to do something after 3000 ms, you must do something like:

setTimeout(function(){
	console.log("house");
}, 3000);

This works and waits 3000 ms to write “house” to the console. Now, suppose I wanted to write “dog” after another 3000 ms and then “cat” after another 3000 ms? Then, the code would become:

setTimeout(function(){
	console.log("house");
	setTimeout(function(){
		console.log("dog");
		setTimeout(function(){
			console.log("cat");
		}, 3000);
	}, 3000);
}, 3000);

You can see where this is going. Of course, there are ways to make this more readable and maintanable, expecially with modern additions to JavaScript, but these can also be rather complicated compared to simply writing:

await(3000);
console.log("house");
await(3000);
console.log("dog");
await(3000);
console.log("cat");

It is not necessarily the case that this approach to programming is better, but in my opinion it is certainly much better for programming almost all experiments one might encounter in practice. Having said that, as you can see from the first example here, there is nothing keeping you from adding asynchronous constructs with jQuery, like the standard JavaScript setTimeout() function or the jQuery on() function. Especially, if you are making a game in NeuroTask Scripting or have a display with many elements on which you might click, for example, an asynchronous solution might work better.

13.2 The waitfor .. or construction

In the chapter on showing video, we encountered the waitfor statement. This is an extension of JavaScript by StratifiedJS we are using with NeuroTask Scripting. If there are many events than can happen and you do not want to use asynchronous programming, this is a handy construction. Many of the elements of the NeuroTask Scripting libraries have been implemented with it, for example, the timeout aspects.

Suppose, we have an experiment where we present random integers from 1 to 10 to the subject and he has to click on a “yes” or “no” block to answer the question “Is this larger than 5?”. This goes on forever until the subject clicks the “Quit” button or hits the Esc key. Answers have to given within 3000 ms, else a text pops up telling the subject to respond faster. This is a good scenaria to use the waitfor..or statement. Let’s look at the example:

var e, n, response,
    left = addblock(10,80,15,10).button("Yes"),
    right = addblock(75,80,15,10).button("No");

var esc = addblock(80,5,15,10).button("Quit","navigation","quit","quit_button")
              .style("background-color","bisque","#quit_button");

while (true) // i.e., do this forever until a break is encountered
{
    n = randint(1,11);
    text(n + " is larger than 5");

    waitfor
    {
        e = left.await('click');
        response = "yes";
    } or {
        e = right.await('click');
        response = "no";
    } or {
        awaitkey('ESCAPE');
        break;
    } or {
        esc.await('click');
        break;
    } or {
        await(3000);
        text("Please, answer within 3 seconds");
        await(1500);
    }

    text("You clicked " + response);
    await(1000);
    clear();
}

text("Thanks!");
await(1000);

This demo can also be found at: https://scripting.neurotask.com/howto/waitfor_or_demo.

So, an infinite loop shows a text like “6 is larger than 5” and then one of five things can happen:

- left (yes) block is clicked (there is no scoring in this simplified version)
- right (no) block is clicked
- Esc key is pressed (calls break, which breaks out of the loop, ending the experiment)
- Quit block is clicked (likewise)
- Timeout time of 3000 ms is reached (shows a text “Please, answer within 3 seconds” for 1500 ms)

If any of those events happens (click, keypress, timeout), only that part of the statement is evaluated (within the curly brackets) and all others will remain ignored. This is why they are strung together with or: anything of the events suffice to direct execution there. See https://conductance.io/reference/#sjs:%23language/syntax::waitfor-or for more details.

Appendix

Background Code

Below is the code behind the makebox() function described in section 3.7. This function can be called without adding this to your script. We include it for those interested.

var makebox = function(layout,scheme)
{
    var box;

    layout = layout || "centered";
    scheme = scheme || "white";

    switch (scheme)
    {
        case 'white': box = new Box();
        break;
        case 'black': box = new Box('black','black','white');
        break;
        case 'light': box = new Box('white','#fafafa','black',100,5,'#f3f3f3',5,"squ\
are");
        break;
        case 'wide-white': box = new Box(0,0,0,0,0,0,0,"wide");
        break;
        case 'wide-black': box = new Box('black','black','white',0,0,0,0,"wide");
        break;
        case 'wide-light': box = new Box('white','#fafafa','black',100,5,'#f3f3f3',5\
,"wide");
        break;
        default: exec.error("scriptlib","Unknown box scheme: " + scheme);
    }

    switch (layout)
    {
        case 'empty': break;
        case 'centered':
            box.centerblock = box.addblock("center","center",90,90);
        break;
        case 'threerows':
            box.headerblock = box.addblock("center","top",100,25);
            box.centerblock = box.addblock("center","center",100,50);
            box.footerblock = box.addblock("center","bottom",100,25);
        break;
        default: exec.error("scriptlib","Unknown blocks layout: " + scheme);
    }

    return box;
};

Example Scripts

In this section, we will demonstrate how to write scripts for classic experiments and neuropsychological tasks.

Corsi Block Tapping Task

The Corsi Block Tapping Task is often used in neuropsychological assessment to estimate visuo-spatial memory span. It is described more fully in section 3.9.

 1 var coords = [[10,10],[20,40],[20,80],[40,30],[45,60],     // block coordinates
 2               [60,10],[60,75],[70,50],[80,20],[85,45]],    // [[left,top], ...]
 3     blocks = [], i, b, series, e, block,
 4     block_color = "lightgrey",
 5     span = 2, errors, attempts = 0, correct = 0, kessels_score;
 6 
 7 instruction("Watch the blocks 'light up' and then click them in the correct order. "
 8            + "The test starts with a series of 2 blocks.");
 9 clear();
10 await(1500);
11 
12 for (i = 0; i < coords.length; i++) // Create block layout, use `i` as id
13 {
14     blocks.push(main.addblock(coords[i][0],coords[i][1],10,10,block_color,"",i));
15 }
16 
17 while (true)   // Do 'forever', but really only until `break` is called
18 {
19     await(2000);
20     series = shuffle(range(10));            // Randomize series
21     for (i = 0; i < span; i++)              // 'Play' series
22     {
23         b = series[i];
24         blocks[b].style("backgroundColor","black");
25         await(500);
26         blocks[b].style("backgroundColor",block_color);
27         await(500);
28     }
29 
30     errors = 0;
31     for (i = 0; i < span; i++)              // Have subject click series
32     {
33         e = await("click");                 // Event `e` also contains target node
34         b = parseInt(e.event.target.id);    // parseInt turns a string into a number
35         blocks[b].blink("black",250);       // 250 ms blink to black
36         if (series[i] !== b)                // Count errors
37         {
38             ++errors;
39         }
40     }
41 
42     ++attempts;
43     if (errors === 0)
44     {
45         ++correct;
46         if (attempts === 2)             // If twice correct, increase span
47         {
48             ++span;
49             attempts = 0;
50         }
51     }
52     else
53     {
54         if (attempts === 2)             // If twice wrong, stop test
55         {
56             --span;
57             kessels_score =  span*correct; // Kessels et al. (2000)
58             break;                      // Break out of while loop
59         }
60     }
61 }
62 
63 await(1000);
64 main.addblock("center","center",90,90,"white","Your Corsi blocks span is " + span);

Random Dot Pattern Recognition

Loosely based after Posner and Keele (1968), Kolodny, Squire and Knowlton, etc. This experiment is described more fully in section 3.9.

  • Simplest version: show as images, store keystrokes, don’t analyze, no feedback
 1 var pattern = new Box(),
 2     coords = [[20,20],[30,40],[30,80],[40,30],[45,60],
 3              [60,10],[60,75],[70,50],[80,20],[75,45]],
 4     no_of_targets = 5, study_time = 2000, series,
 5     hits = 0, false_alarms = 0, prototype_alarm = 0;
 6 
 7 function showPattern(coords)
 8 {
 9     var block, blocks = [], i;
10     for (i = 0; i < coords.length; i++)
11     {
12         block = pattern.addblock(coords[i][0],coords[i][1],10,10);
13         block.text("•",200);
14         blocks.push(block);
15     }
16     return blocks;
17 }
18 
19 function distortCoordinates(coordinates,distortion,seed)
20 {
21     var i, c = coordinates;
22     distortion = distortion || 10;
23     Math.seedrandom(seed);      // Make distortion repeatable with `seed`
24     for (i = 0; i < c.length; i++) // Distort all coordinates
25     {
26         c[i][0] += randomint(-distortion,distortion);
27         c[i][1] += randomint(-distortion,distortion);
28     }
29     return c;
30 }
31 
32 function presentStimuli(indices,distortion)
33 {
34     var c, i, p;
35     for (i = 0; i < indices.length; i++)
36     {
37         p = indices[i];
38         c = distortCoordinates(coords,distortion,p); // Use p as seed
39         showPattern(c);
40         await(study_time);
41         pattern.clearall();              // Remove pattern from screen
42         await(1000);
43     }
44 }
45 
46 function getResponses(indices,distortion)
47 {
48     var c, i, p, e;
49     for (i = 0; i < indices.length; i++)
50     {
51         p = indices[i];
52         if (p === -1)
53         {
54             c = coords; // -1 means: show the prototype
55          }
56         else
57         {
58             c = distortCoordinates(coords,distortion,p); // Use p as seed
59         }
60         showPattern(c);
61         e = awaitkey("s,l");
62 
63         if (e.key ===  "s") // Pattern believed to be old
64         {
65             if (-1 < p && p < no_of_targets)
66             {
67                 ++hits; // Targets have indices 0,1,2, ..., no_of_targets-1
68             }
69             else
70             {
71                 ++false_alarms;
72             }
73 
74             if (p === -1)           // Prototype has index -1
75             {
76                 ++prototype_alarm; // False recognition of prototype
77             }
78         }
79         pattern.clearall();                // Remove pattern from screen
80         await(1000);
81     }
82 }
83 
84 instruction("Study the following random dot patterns for later recognition. "
85             + "Patterns appear automatically.");
86 presentStimuli(range(no_of_targets),5);
87 instruction("Try to recognize the following patterns. "
88             + "Type the 's' or 'S' for an old or 'seen' pattern "
89             + "and 'l' or 'L' for a new pattern");
90 series = shuffle(range(-1,2*no_of_targets));
91 getResponses(series);
92 text("You had " + hits + " hits and you recognized the prototype as "
93     + (prototype_alarm ? "old." : "new."));

Notes

Getting Started with NeuroTask Scripting

1In some case, ambiguities may arise if semi-colons are left out. They also often make it easier to find an error.

2It is not necessary to indent the statements in the body, but it makes for more readable code and often helps to prevent errors; it is completely legal to leave out all spaces in this example. This is an example of adding arbitrary white space to make a script more legible to the human eye. If you are used to the programming language Python, you will certainly recognize how valuable indenting can be to keep scripts readable and easy to maintain.

3This is borrowed from the programming language C, as is much of JavaScript, such as the for loop and the way values are assigned with the expression i = 0.

Capturing Keys and Reaction Times

1Some screens now use a refresh rate of 70, 90, or 120 times per second. Especially phone screens may have higher refresh rates to reflect dragging and other actions better.

2This works well on modern Internet browsers but is not reliable on older browsers. The details on this browser technology, based on requestAnimationFrame, are discussed in section 7.2 and chapter 8, as well as in the advanced NeuroTask manual.

3Later, we will see a way around this using the waitfor statement, with which you can combine several await() statements.

4This means that while the now() timer is extremely precise, it cannot tell you anything about what time of day it was when the subject completed your experiment. It cannot place when the timer was started and stopped within any greater context. Fortunately, date and time are automatically logged by the browser whenever an event occurs and reported in the timestamp property of all NeuroTask events.

Screen layout with “Box” and “Block

1We may add a format that allows scrolling in the future, but at the moment we see no good application for it in experiments.

2HTML is the language in which the structure and markup of webpages is specified. At the HTML Dog website (http://htmldog.com/guides/html/beginner/), you can find a good beginner’s guide. For most experiments, you do not need to know HTML, though it may be handy to know a few basics, like how to show a word in bold face or italics.

Style

1If you look up some style properties, you will notice that (especially somewhat older) browsers do not support them all in the same way. Therefore, to the extent that it is possible, NeuroTask Scripting relies on the extensive and well-tested Dojo Toolkit to correct cross-browser problems.

Survey questions with form controls

1Normally an experiment would include a much longer instruction section, including an information brochure and an informed consent form.

Data logging and handling

1RAF 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 though depends on the computer or phone. See the advanced manual for a more in-depth discussion of this.

2Many values in JavaScript are considered false, e.g., undefined, null, 0, and the empty string "". We advise not to rely on this except with well-known usage cases, like this.

3SPSS output is not supported at the moment, but will be in the future, as will be other formats if there is a demand for it.