There is a new edition of this book available: Getting started with NeuroTask Scripting, 2nd Edition
Getting Started with NeuroTask Scripting
Getting Started with NeuroTask Scripting
Jaap Murre
Buy on Leanpub

Table of Contents

Preface

Audience

This book is written for anyone who wants to make an Internet-based experiment 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 on the Internet.

Typographic convention

Throughout the book, normal text is typeset in the font you are currently reading, but pieces of JavaScript are written in a different font, e.g., "This is a JavaScript text string, shown in a special font to make clear it is not ordinary text".

1. Getting started with NeuroTask Scripting

1.1 What are scripts and why do we need them?

Suppose, you want to make an 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 rembered into a large text area in any order. In other words, you have a plan for the experiment that specifies step-by-step what you want to happen. If you wrote down these steps, it might look something like:

  • 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

This plan for an experiment is in fact already very close to a script. The only difference is that scripts use specific expressions and statements to tell the computer what you want to do. A NeuroTask Script that would do the 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");

The dots, …, above refer to words 3 to 7 which for reasons of space have been left out both in the plan and in the script.

As you can see in the script, there appears to be an instruction to the computer to show a word. This instruction is called text. There is also an instruction to wait until 2000 ms have passed, where the waiting instruction is called await. Then there is clear, which clears the screen from any words and finally largeinput, which shows a large input area where the subject can type words remembered.

You will also notice that there are parens (i.e., round brackets), quotes, and semi-colons. All of these are added to indicate to the computer what you want it to do. Once you know how to convert your plan into a script, you can instantly run it on the internet.

1.2 Scripting psychological experiments

So, we saw that scripts tell the computer what stimuli to present and which data to collect from the subjects. The team at NeuroTask Scripting has tried very hard to make often occurring experimental tasks easy to script, even for a beginner.

NeuroTask scripts are programmed 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 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. With 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. Also, all of the Dojo and jQuery libraries are 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 in creating an online experiment script, where step 2 is optional:

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

You’re done! Your script is ‘live’ on the internet and it has its own unique web address (also known as URL). You can now email this web address to friends and family, put a link on Facebook, etc. If participants do your experiment, their data will automatically be saved in your NeuroTask Scripting account, where you can easily download it in Excel or other formats. This is the way to go with pilots or informal experiments, or even with large experiments, for example, if you are using subjects from Amazon’s Mechanical Turk.

Inviting subjects

NeuroTask also provides facilities to

  1. Create subject records if you know the subject’s E-mail address
  2. Create personalized invitations to your experiment; NeuroTask will then E-mail subjects for you

Then, wait for the data to come in and start analyzing. While you are waiting it is possible to see who has already completed your experiment.

1.3 Scripts

The structure of an experiment script

Most experimental tasks in psychology involve about the same steps. The steps must be expressed somehow in a script. Typically, an online experiment will

  • Welcome the subject,
  • Ask for informed consent,
  • Give instructions,
  • Present stimuli, such as words, and
  • Record responses, e.g., words recognized. This is followed by a
  • Debriefing, 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. OK, the title sort of gives it away, at least if you have followed an introductory course on psychology.

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

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

After five seconds, the text changes into “YHZ”.

Then the count-back instruction appears with a longish 25 s wait, during which the subject is supposed to count back in threes.

After this interval, a text area appears with the label “Write down the letters remembered” above it. There is an “OK” button below the area.

After pressing “OK”, the thank-you text appears for 3 s and the subject is finished.

Though the script is short, it contains most of the steps of a standard experiment, and it can easily be changed to other 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 complete, 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 first 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. This is typically prevented by having the count backward in the threes. The effect is not obtained, however, in the first trial, i.e., after having studied and remembered just one letter triplet; the task should be repeated several times with different letters while making sure subjects are not secretedly rehearsing the letters in the 30 seconds interval. Experiments like these 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 text() with the message “Try to remember the following words”. The double-quotes 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 quotes instead of double quotes, like text('Welcome'). We call text() a function and “Try to remember the following words” the argument of the function text(). 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 at the end of a sentence, which helps us by signalling 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 //
3 
4 await(5000); // 5000 ms or 5 s

This line has the await() function with the argument 5000. When await() is called like this, the computer will stop the 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 treats differently than strings.

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 others. Here, we make clear that 5000 means: 5000 ms. A // comment only runs to the end of the line. With multiple lines of comments, you must repeat the //, e.g.,

// Starting a comment
// continuing it on the next line

There is also a type of comment that spans many lines. It starts with /* and ends with */. E.g.,

//

/*
    This is a comment
    that spans many lines.
    As many as I want really...
*/    

Make sure you start with /* and end with */ exactly in that order. In SPSS, comments are indicated with * characters without the forward slash /, but leaving out these forward slashes is not allowed in JavaScript and will cause an error.

So, there are two type of comments

  • Line-based, with //
  • Line-spanning, with /* */

The following two statements in the script follow exactly the same pattern, except with different arguments:

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

The user’s response is recorded with the line:

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

The input() function requires a label, here: “Write down the letters remembered”, and displays this with a text input field below it in which the user can type the letters. Below that is an “OK” button. The second argument, “brown_peterson”, is the name of the variable that is created to store and label whatever the subject types into the text area.

Now you may wonder: “What is happening with the subject’s answers?”. All responses, from buttons, text input controls, drop-down select lists, check boxes, etc. (formally called form controls or controls for short), will automatically be saved into your account’s data area. You can inspect these response and download the data in Excel and other formats. It is important to give meaningfull names to variables, because then you will more easily remember what the values in the data tables in your account mean.

If you don’t give a name, the subject’s input will be saved under a automatically generated name such as “input_1”. In a small experiment this presents no problems, but when your experiment collects a lot of data mistakes are easily made. It is therefore highly recommended to find meaningful names for all recorded data.

The last statements show a ‘Thank you’ text for 3 s. After this, the NeuroTask branding screen appears signaling that the experiment is over.

12 //
13 
14 text("Thank you for participating!");
15 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 following word is shown.

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

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

Words entered into a large input text area.
Words entered into a large input text area.

If your subjects would do this task online, you would see their answers, exactly as they typed it in, under the label “words_remembered” in the data area of this experiment script:

Inspecting data entered by a subject
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 the names of variables in JavaScript may not contain spaces and words_remembered is used like a variable in NeuroTask. An underscore is often used instead of a space.

Now, let’s simplify this script by introducting the concepts of array and for loop.

Script 1.3: A shorter script with a for loop

There is nothing wrong with Script 1.2, but some might find it a bit long and others may notice that it is tedious if you decided to increase the presentation time from 2000 to say 3200 ms: all times must be adjusted. Especially, in large scripts it is easy to skip one by mistake.

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);
Variables

First, you may notice the use of the word var , which stands for variable. The statement var i tells the system that we want to store some data and require memory space to do this. We also want to name the variable. This is called declaring a variable. It is much like hiring a storage unit to store some surplus furniture or other stuff and giving it a name so you can easily identify it. Now you can put something (a value) in the box (variable). When you then later need it, you will always have access to the contents (value) of the box (variable). Variable names in JavaScript may contain numbers and underscores (i.e., the _ character), but they may not start with a number (but can start with an underscore). They are case-sensitive, meaning that in JavaScript variables word, Word, WORD are considered three completely different variables and not for example spelling variations of the same variable.

White space

In most places in scripts, you may insert arbitrary white space (spaces, tabs, newlines) almost anywhere, which can be useful if you want to make the layout of your scripts more legible. In the scripts, so far we have used empty lines to great groupings to help see the structure of the script. These empty lines are completely ingored by the computer system, as is most other white space.

Assigning values to variables

The first variable in the script has a very short name, it is called i. We will use i to count the number of words we are going to present on the screen, starting with 0 rather than 1.

The next line is more complex and has a another variable, which we have called words. The variable words is immediately given a value in the form of an array of strings, which correspond to the stimulus words in Script 1.2. The value of the variable words is set using the equal sign =. So, i = 0 means: “Give i the value 0.”

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 is equal to 1440 and assign that value to i. So, the above statement is equivalent to

i = 1440;

You can also use the value of a variable on the right-hand side of an assignmnet, like this

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

Again, i would be 1440 after these lines had been processed. In Script 1.3 we use the value of i on the right-hand side, add 1 to its current value, and assign the result to i, overwriting its current value:

i = i + 1;

So, if i was 0 at before the assignment it is 0 + 1, or 1, after the assignment. This is one way to increase the value of i by 1. You could also decrease with 2,

i = i - 1;

or multiply it with two

i = 2*i;
Arrays

An array in JavaScript is a type of list that can be compared to not just one storage unit, but a whole row of them. These units (usually called elements) are number, starting at 0 (and not at 1). This is automatic and cannot be changed. All elements of an array must be separated by commas. To indicate to the system that words is an array, the list of values must start with a square bracked [ and end with one too ].

So, the variable words now holds all words in your experiment, but how do we access them? As said, the elements in this array are numbered 0 to 7 and to get at element 0 we write words[0], which would give us the word “glass”.

Each array also knows its own length (i.e., number of elements) and this value is 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.

for loops

Another new part of the script is the for loop2:

 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 this would say: “Repeat the statements between curly brackets as long as index i is less than the length of the words array, where i starts at 0 and increases with 1 after each repetition.” The length of an array can always be obtained with the length property. Here, words.length would equal 8 as the array contains 8 strings.

The for loop always has the same shape: it has a head and body. The head always looks like:

 for (initialization; condition; step)

The body is the part in curly brackets {...}.

In the head, initialization sets variable i to its initial value, here i = 0.

The condition determines when to stop. In this case, if i is 8, the condition is false because words.length is 8 and the condition says i < words.length. Since 8 < 8 is not true, the statements in the body will not be executed anymore. So, processing the statements the body will continue until i equals 8.

The step part of the head of the for loop determines how the index i is changed after each repetition. In most cases, it is increased with 1 (called an increment), but sometimes it is handier to decrease it (called decrement) or change it in some other way.

It is important to know that even if for some reason you would not use part of the head, you still need to write both semi-colons. So, if you would choose to initialize the index before the loop, which is perfectly fine, you still need to put in the semi-colon, 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 that can be used instead: ++i. This is known as increment3 i. Similarly, --i equals i = i - 1 and decrements i. It does not do anything different but is simply a more concise form.

Another short-hand is that, when several var expressions follow each other, you can combine them using a comma, as follows:

var i, m = words.length;

You only need to write var once. Again, white space such as spaces and newlines is irrelevant and you may also layout this more clearly as:

var i, 
    m = words.length;

The whole loop would than look like

var i, 
    m = words.length;

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

This is the format of the for loop that you will see most often.

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, sentence fragments), NeuroTask offers the function getwords() that allows you to easily achieve this. The procedure is to first make a text file that contains the stimulus words without quotes but separated by commas. 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 (look for the cloud symbol).

Uploading or linking of stimulus file words.txt.
Uploading or linking of stimulus file words.txt.

You can verify whether the upload was successful, because the filename will then appear in the menus on the right (look under Text Files). If you see it, it means you have uploaded it and that it is ready to be used with the script.

Uploading or linking of stimulus file words.txt.
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, words = getwords("words.txt");
 2 
 3 text("Try to remember the following words");
 4 await(5000); // 5000 ms or 5 s
 5 
 6 for (i = 0; i < words.length; ++i)
 7 {
 8     text(words[i]);
 9     await(2000);
10     clear();
11     await(1000);
12 }
13 
14 text("Now, count back in threes starting with 307: 307, 304, 301,...");
15 await(20000);
16 largeinput("Write down the words remembered","words_remembered");
17 text("Thank you for participating!");
18 await(3000);

The gain in using this method is that at any point you can upload a file with different words. Also, the words do not need to be wrapped in quotes to make them strings. Files may be shared among scripts, which is relevant if you have several scripts that use the same stimuli, for example, for different conditions.

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, uh, free recall experiments. Both types of scripts recorded 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.

  1. In some case, ambiguities may arise if semi-colons are left out. It is also easier often to find an error.
  2. It 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.
  3. This 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.

2. Capturing keys and reaction times

2.1 Simplified script for a lexical decision task

In this chapter, let’s create a script for a lexical decision task. We need a list of words and a list of non-words. Words and non-words are mixed and presented in random order and at each presentation the user must indicate whether it is a word or a non-word. This must be done accurately and as fast possible. Typically, if a word is detected, the user must press the S-Key, and if a non-word is detected the L-Key. In the first version of the example script, we will use only five words and five non-words, to make it easier to follow. The first version does not yet contain all the details, but provides a good starting point from which we can develop a more complete script:

Script 2.1. Lexical decision experiment.
 1 var words = ['apple','table','grass','bike','sand'],
 2     nonwords = ['aplap','blatle','rasag','kibe','snad'],
 3     i, 
 4     stimuli = [],
 5     e;
 6 
 7 stimuli = stimuli.concat(words,nonwords); // Combine two arrays into one new one
 8 stimuli = shuffle(stimuli);               // Randomize the word/non-word order
 9 
10 // Instructions come here
11 
12 for (i = 0; i < stimuli.length; ++i)
13 {
14     text(stimuli[i],300);         // show a word or non-word at 300% size
15     e = awaitkey('s,l');
16     // Wait until either the S-key or the L-key has been pressed. If so...
17     clear();                       // Remove word from screen
18     await(1000);                   // Pause for 1 s
19 }
20 
21 // feedback/debriefing comes here

The first thing that is new here is that we have two arrays, words and nonwords, and a third empty array, called stimuli. The following statement adds (copies) the contents of words and nonwords to stimuli:

7 stimuli = stimuli.concat(words,nonwords);

Now stimuli contains a copy of the words and non-words. The word concat stems from to concatenate, meaning to join or chain together. To randomize the order of words and non-words, we use shuffle(stimuli), which is exactly like shuffling a deck of cards: the order of words and non-words is randomized.

The only other thing here that is really new, is awaitkey('s,l'). This statement is waiting for the subject to press either the S-Key or the L-Key. In this chapter, you will find out how this works.

2.2 Timing with await() and now()

By now, we know how to put text on the screen for a specific duration, like this:

text("QWT");
await(2000);
clear();

This will show the string "QWT" for 2 s and then remove it from the screen. But how can we measure reaction times?

Measuring some elapsed time, like a reaction time, is accomplished with the now() function, which returns a high resolution timer value in ms but with microsecond precision. To calculate a reaction time, RT, we might do:

Script 2.2. Measuring a reaction time with now().
1 var t1, t2, RT;
2 
3 text("Press s or l");
4 // Normally you would show a stimulus to be judged here
5 t1 = now(); // Now is a timer or 'stopwatch'
6 awaitkey('s,l'); 
7 t2 = now();
8 RT = t2 - t1;
9 text("Your reaction time was: " + RT);

Modern Internet browsers strive to attain microsecond precision in the high-resolution timers, but whether they succeed will vary from case to case. Older browsers do not support high resolution timing. In such cases, the now() function will use whatever timing precision is available1. Fortunately, NeuroTask Scripting automatically logs for whether a subject’s browser had high-precision timing and a few other details relevant to assess the reliability of your data. We will discuss these below.

We have used the await() function above to present a word on the screen for say 2 s. Presenting stimuli for a certain time is a crucial aspect of many psychological experiments. Unfortunately, computer screens have a limited refresh rate, typically 60 times per seconds.2 This means that they will only write new information on the screen about every 16.7 ms. For movies, sixty images per second is fast enough. For very brief stimulus presentation, however, it may not suffice. Also, stimulus presentations may be different from what you intended. If the screen is only refreshed every 16.7 ms, this means it is impossible to show a stimulus for 28 ms. The closest duration would be 33 ms. You should be aware of this and choose a time that is just below the closest number of screen refreshes, e.g., 16 ms, 33 ms, etc. This maximizes your chances of achieving the intended timing of your stimuli, because the await() function is aware of when a new screen refresh is about to occur.3

So, even if you specify a certain number of ms to wait, say 33 ms, the system does not always manage to do this. In addition to the screen refresh intervals, another source of timing error is that many processes may be running on a computer and this may affect your experiment, especially on slower computers. To monitor the actual duration of the intended presentation time, await() returns a so called event variable, which contains this information.

Suppose, for example, you want to present a very brief message on the screen that is masked by a #### pattern after 33 ms. We might do this as follows:

var e;

text("Eat popcorn!");     // Show stimulus
e = await(33);            // Wait two screen refreshes 
text("############");     // Show mask

We save the value of the event returned by the await() function in variable e. We can now use this event variable e to verify whether we have achieved our intended presentation time of 33 ms. This is done by checking some of its properties:

  • e.intendedTime: the intended waiting time, which is 33 (ms) here
  • e.duration: the realized duration which may for example be 34 or 39 (ms)
  • e.delta: this is 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 more than say 3 ms or less than -3 ms, we may decide the timing was off and discard the trial. In our own experience, when attempting to present stimuli for brief periods, like 33 ms, on older browsers and computers, many trials would have to be thrown out as timing can be quite variable, but on modern browsers almost all trials may be kept as timing tends to be quite good.

2.3 Awaiting keyboard events

Key presses

IN a typical experimental task a subject must respond with one key for a Yes-response and with another for a No-response. For example, in some recognition task, you show a list of words, some of which have been seen before and the subject must indicate this. Pressing the S-Key may signal “Yes, I think this is an existing word” and likewise the L-Key may indicate a non-word. In NeuroTask this might be done as follows:

var e;

text("aplap");        // show a non-word
e = awaitkey("s,l"); 

That’s it. No further coding necessary: awaitkey("s,l") will halt processing of the rest of the script until either the S-Key or the L-Key has been pressed; it will not react to other keys or other things happening (e.g., mouse clicks).

There are two things to remark here. (1) The letters (more accurately: printable keyboard characters) are always converted to their lower-case form. (2) You can add as many letters as you want to the argument (no spaces allowed). For example, the following would also work:

1 event = awaitkey("q,w,e,a,s,d,i,o,p,k,l,;");

If you want to look for any key and don’t care which one, use the await("keypress") statement, as explained below.

As soon as the subject presses a key, say the S-Key, the event variable e will receive the return value from awaitkey(), which will contain a key property with the key’s name.

As an example, we extend the small script above to show the key on the screen:

var e;

text("Press s or l");
e = awaitkey('s,l'); 
text("You pressed " + e.key);

As soon as you press the S-Key or L-Key, this script snippet would say either “You pressed s” or “You pressed l”; if any other keys would be pressed, awaitkey('s,l') would keep waiting and do nothing at all.

As, shown in Script 2.2, one approach to record a reaction time is to use the now() statement. This approach will work, but you don’t need to do it: When you use awaitkey(), the returned event variable will aready contain an RT property with the reaction time. The reason that this can be calculated is than the awaitkey() function is called immediately after text() and stops immediately when ‘s’ or ‘l’ is pressed. By default, awaitkey() will keep track of when it starts to wait and when it finally receives its awaited key.

In short, because measuring reaction times is so common in experiments, ` await() and awaitkey() will always calculate the reaction time and make it available in the RT` property (in capitals). With that, Script 2.2 can be simplified to:

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); // concatenate reaction time to string

If you run this, you will notice that due to the microsecond precision, the reaction time value has many digits behind the decimal point, like 831.471948. If you don’t want this, you can round the value with JavaScript’s built-in function toFixed(), which takes the number of decimals to be shown as an argument. The last line would then read:

6 text("Your reaction time was: " + e.RT.toFixed(2));

Now, a reaction time will look something like 831.47, in ms.

2.4 Reaction times with timeouts

The awaitkey() function also takes a second, optional ‘timeout’ parameter, where you can specify the number of ms to wait before moving on.

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");
4 // Normally you would show a stimulus to be judged here
5 e = awaitkey('s,l',3000); 
6 text("Your reaction time was: " + e.RT.toFixed(2)); 

The effect of this is that the program will wait for the S-Key or L-Key to be pressed, but if this does not happen within 3000 ms, it will simply move on.

How do you know whether the subject responded in time? The answer can be found in the type property of the event variable, here e.type. There are many types of events, for example, those generate with the mouse, keyboard, sound player, or video player. We will encounter some of these below. To help scripting, the event that triggered the awaitkey() or await() function is always available in the type property.

In this case, if the subject pressed a key after 3 s, e.type would be equal to “timeout”. Else, if a key was pressed in time, e.type would be "keypress". In case of a timeout, the e.RT property would be close to 3000. It is not a reliable indicator of a timeout, however, because even with a timeout the RT property may well be less than 3000 ms, say 2998.341 and is thus not guaranteed to exceed 3000 ms in case of a timeout.

2.5 if...then statements

We often want to give different feedback depending on the behavior of the subject. In this case we may want to give specific feedback when the user is too slow. To check whether certain conditions occur (like slow RTs), JavaScript has an if...then statement, which looks like this:

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 (within 3 seconds)");
 9 }
10 else
11 {
12     text("Your RT was: " + e.RT.toFixed(2) + " ms");
13 }

The program will check whether the e.type property equals “timeout” and if so runs (only) the first part between curly braces. If it equals any different type, including “keypress”, it will run the second part, after else.

The else part may be left out, in case you don’t need it, for example if you only want to give feedback when the subject is too slow.

The condition must be between (round) brackets. The system will not even try to run your program if you forget say a closing bracket or curly brace but instead it will complain about a ‘syntax error’.

To test whether some variable equals some value, you use the ===-operator. Yes, there are no less than three equal signs here!4 Frequently used logical operators are:

Operator Description
> greater than
< less than
>= greater than or equal
<= less than or equal
=== equal
|| or
&& and

We can combine these in many different ways. Suppose we want to keep track of how many of the S-Key presses exceeded 3000 ms. We could then write:

Script 2.6. Counting slow key presses.
 1 var e, 
 2     slow_count = 0;
 3 
 4 text("Press s or l within 3 s");
 5 e = awaitkey('s,l'); 
 6 
 7 if (e.key === "s" && e.time === "timeout")
 8 {
 9     ++slow_count;
10 }

If the S-Key was pressed and and the response timed out, we increment the variable slow_count

Order of interpretation of operators

You can use as many (round) brackets as you want to make the conditional expression perhaps easier to read, for example:

if ((e.key === "s") && (e.RT > 3000))
{
    ++slow_count;
}

Just be sure to close the brackets.

The brackets can also serve to force an order of evaluation that is non-standard. For example, if you want to divide the sum of 2 and 40 by 100 you could write 2 + 40/100, but this would give 2.4, which is not what you intended. The reason is that division has a higher precedence than addition. Division is evaluated before addition, here giving 2 plus 0.4. With brackets you can get the intended result: (2 + 40)/100, which would give 0.42. Whenever, you are uncertain about how an expression will be evaluated, you can always add brackets to ensure that the result is what you intend it to be.

2.6 Non-printable keys

In some experiments you may want to use non-printable keys like the arrow keys, say, to have subjects manipulate something on the screen. How do we handle these keys? Simple. We do exactly the same thing as above. Say, you are waiting for the subject to press the Up-Arrow-Key, you can simply write:

text("Press the Up-Arrow-Key");
awaitkey("UP_ARROW");  

To make this work, you have to look up the names of the keys, which are as follows:

BACKSPACE
TAB
CLEAR
ENTER
SHIFT
CTRL
ALT
META              // this is the 'Apple-Key' on macs
PAUSE
CAPS_LOCK
ESCAPE
SPACE
PAGE_UP
PAGE_DOWN
END
HOME
LEFT_ARROW
UP_ARROW
RIGHT_ARROW
DOWN_ARROW
INSERT
DELETE
HELP
LEFT_WINDOW
RIGHT_WINDOW
SELECT
NUMPAD_0
NUMPAD_1
NUMPAD_2
NUMPAD_3
NUMPAD_4
NUMPAD_5
NUMPAD_6
NUMPAD_7
NUMPAD_8
NUMPAD_9
NUMPAD_MULTIPLY
NUMPAD_PLUS
NUMPAD_ENTER
NUMPAD_MINUS
NUMPAD_PERIOD
NUMPAD_DIVIDE
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
F11
F12
F13
F14
F15
NUM_LOCK
SCROLL_LOCK
UP_DPAD
DOWN_DPAD
LEFT_DPAD
RIGHT_DPAD
copyKey           // Mac/PC agnostic copy key, akin to Ctrl-C

These names can also be found in the Quick Reference panel (look for Scripting - Keys). Contrary to letter keys, the non-printable keys must be entered exactly as given above, that is, all upper-case. As with letter keys, you can also use a timeout period and add several non-printable keys together with commas (no spaces allowed):

text("Now press any Arrow Key within 5 s");
awaitkey("DOWN_ARROW,UP_ARROW,LEFT_ARROW,RIGHT_ARROW",5000);  

It is not possible, however, to combine printable keys (like letters or digits) with non-printable keys, like arrows, so awaitkey("s,l,F1,F10") would not work 5. Oddly, enough in JavaScript the space is considered both printable and non-printable: it maybe entered as either awaitkey(" ") or awaitkey("SPACE"). The type property returned when a non-printable key is pressed is "keydown" and not "keypress", which is reserved for printable letters only. (Yes, there is also a "keyup" event.)

2.7 Handling other types of events with await()

The await() function kan do more than simply waiting for a number of milliseconds. It can be used to await any event that can occur, including mouse events, touch events, animation events, drag-and-drop events, events from controls such as input fields (discussed below), events from sound and video players, and many more. In fact, await() can also handle keyboard events, and the awaitkey() function is itself implemented with the await() function.

Click events

Let’s look at some examples. One of the most versatile events is the ‘click’ event, which is produced when a user clicks with the left mouse button (or with the only mouse button on Macs). On a tablet or phone, a ‘click’ event may be caused by tapping the screen. The click event may also be produced if there is a button (e.g., an OK button) on the screen and the user presses that (e.g., by pressing Enter when the button is in focus or with the left mouse button). Use of the event is as follows:

text("Click this text");
await("click");
text("You clicked!");

If you would run this, you’d see the text “Click this text”. As soon as you clicked the text (or tapped, if you were working on a touchscreen), the text would change into “You clicked!”.

There is also a ‘dblclick’ event, a double-click with the left mouse button.

There is no ‘rightclick’ event. Instead there is a ‘contextmenu’ event, which on Windows will be sent when the user clicks the right mouse button. There is no right button on most Apple computers. The ‘contextmenu’ event is also sent when the Context-Menu-Key is pressed on Windows keyboards (maybe you never noticed it is there; it is typically located to the left of the right Ctrl-Key).

2.8 More about keyboard events

As we mentioned above, await() can also listen for keyboard events. With await("keypress") we would be awaiting any printable key being pressed. To find out which one was pressed we would save the return value of await():

var e = await("keypress");

JavaScript allows declaring a variable and immediately initialize it, here use this short form to save the return value of await(). The e.key property will now hold the value of the key that was pressed, e.g., “g” or “a” (always in lower-case).

For non-printable keys, we can use the same approach, now using the “keydown” event. E.g.,

var e = await("keydown");

if (e.key === keys.UP_ARROW)
{
    // move an image up or do something else that is useful
}

It is also possible to use the “keyup” event, e.g., when instructing subjects to press the space bar and release it when they must respond:

var e = awaitkey("keyup","SPACE");
text("You RT was : " + e.RT.toFixed(2));

This is good way to measure simple reaction times.

In most experiments, the subject has to press one of a set of specific keys, making this a more suitable task for awaitkey(). However, awaitkey() can also be called with a keyboard event as the first argument, for example,

awaitkey("keyup","RIGHT_ARROW");

This will wait until the Right-Arrow-Key has been released.

Shift-Alt-Ctrl

Suppose you have the following script:

var e = await("a");

In other words, you are waiting until the user presses the A-Key, which will returned in the event property e.key. This value will be lower-case, “a”. How do you know whether Shift was pressed as well? Or Alt, Ctrl, or Meta (i.e., the technical name for both the Windows-Key or Apple-Key, on the respective platforms)? This is sometimes important to know. Keys like Shift and Ctrl are known as key event modifiers. They can be accessed in the e.event property, which contains a great many additional details about an event. For now, we will focus on the following properties:

ctrlKey
altKey
shiftKey
metaKey

These indicate whether the Ctrl (Control), Alt, Shift, or Meta (Windows/Apple) key was held down.

For example, pressing Shift-A, which would give a capital A, can be measured as follows:

 1 var e = await("a");
 2 
 3 if (e.event.shiftKey)             // Shift was pressed with 'a'
 4 {
 5     text("You pressed 'A'");
 6 }
 7 else
 8 {
 9     text("You pressed 'a'");
10 }

Note that, in line 3 we could also have written

if (e.event.shiftKey === true)

but this means exactly the same as

if (e.event.shiftKey)

because e.event.shiftKey has either the value true or false. If it has value true, the statement e.event.shiftKey areadly evaluates to true and turning this into true === true adds nothing.

There is a great deal more to be said about events, but we will discuss these when we get to the relevant parts. For example, we will explain sound events when we get to playing sounds and video events when we get to playing videos.

2.9 More complete script for a lexical decision task

Given the things we learned about events, we can now add important details to the script for the lexical decision task.

Script 2.7. Complete lexical decision experiment.
 1 var words = ['apple','table','grass','bike','sand'],
 2     nonwords = ['aplap','blatle','rasag','kibe','snad'],
 3     i, 
 4     stimuli = [],
 5     e;
 6 
 7 stimuli = stimuli.concat(words,nonwords);    // Combine two arrays into one new one
 8 stimuli = shuffle(stimuli);                  // Randomize the word/non-word order
 9 
10 // Present some instructions, where <br> gives a line-break
11 text("You will see a word or non-word appear in the middle of the screen <br>"
12    + "Press the S-Key for a word or the L-key for a non-word <br>"
13    + "Try to be as accurate and fast as possible <br>"
14    + "Start the experiment by pressing the space bar.").align("left");
15 awaitkey(' ');                     // Wait until the space bar is pressed
16 
17 for (i = 0; i < stimuli.length; ++i)
18 {
19     text(stimuli[i],300);          // show a word or non-word at 300% size
20     e = awaitkey('s,l',2000);      // Await s or l or 2 s time-out
21     
22     // Wait until either the S-key or the L-key has been pressed. If so...
23     console.log(stimuli[i],e);
24     
25     if (e.type === "timeout")
26     {
27         text("Please, try to respond faster (within 2 seconds)");
28         await(3000);
29     }
30     
31     clear();                     // Clear the screen
32     await(1000);                   // Give 1000 ms pause
33 }
34 
35 text("<h2>Finished!</h2>Thank you for participating");
36 await(3000);

In the final version of this script, you would want at least to save the subject’s responses. Perhaps, you would also want to verify their correctness and log them into the data section of your NeuroTask account, together with their reaction times. We will return to these aspects in the chapter about storing data.

  1. The value returned by a high resolution timer cannot be converted into a time of day because these timers are started at an arbitrary moment, for example, when you open the web page or start your computer.
  2. Some screens use a refresh rate of 70 or 120. This is rare at the moment (March 2015) but may well change in the future.
  3. RAF is an abbreviation of ‘requestAnimationFrame’, this is a signal that the next screen is about to be generated by the computer, which typically happens exactly 60 times per second. See the advanced manual for a more in-depth discussion of this.
  4. You may also encounter an equal operator with two = signs in JavaScript, ==, but we do not recommend using this unless you know exactly what you are doing. For example, the expression ("0" == false) will unexpectedly evaluate to true because "0" is considered so called ‘falsy’ value. This may lead to subtle errors that can trip up even seasoned programmers.
  5. Later, we will see an way around this using the waitfor statement, with which you can combine several await() statements.

3. Screen layout with Box and Block

3.1 Layout issues for the world-wide web

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

Nowadays, subjects online may have screen sizes ranging from very high resolution ultra-wide screen monitors to tiny screens on mobile phones and even (round-screen!) smart watches. New formats are coming out every month. Not only screen size is important because even on a large screen, your experiment may still run in a tiny window.

All of this presents a problem for psychological experiments, as it is necessary to specify with precision the position of your stimuli. How can you be certain your stimulus layout stays exactly the same? What happens when users scroll down or zoom in on your page? Or 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

To deal with the huge range of screen formats (and with variable window sizes), NeuroTask uses a one of two types of layouts: square and fill. Using these layouts greatly simplifies positioning of stimuli on the subject’s screen. The square layout, which we will use throughout this book, limits the area where you can show stimuli to the largest square that can be drawn in the window in which your experiment is running. This presents 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 screen (or window) real-estate. It is up to you when to use this format, for example, when showing instructions or when presenting survey questions. A great disadvantage is that the display (screen or window) in which the experiment is shown can vary from extremely wide to extremely high and narrow. It is nowadays, possible to design web pages that can adjust themselves to such extremes, for example, using a framework like Twitter Bootstrap. This is the styling framework we use for the NeuroTask website and we highly recommend it. For experiments, however, you almost always want to control where screen elements go, rather than leaving it up to the size and shape of the window. For best control over the screen we, therefore, strongly recommend the square layout.

The square layout

This layout automatically determines the largest square that can be drawn in a window. On a wide screen (or wide window), the square will leave areas left and right unused. If the screen (or window) changes, the square will 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 sizeways: within the square, things stay the same, relatively speaking.

Font size

Also, when the subject increases the screen or window, such that the square layout increases in size, the font should increase in size as well. With a square layout the font should increase proportionally to the size of the square. With a fill layout it, this is less straightforward. What do you want to do when a subject stretches a small square window to a wide band triple the width but with the same height? We have chosen to make the font size proportional to the diagonal of the window, a compromise.

3.3 Other cross-browser layout issues

Font types across browsers

Another problem on the web is font type. Different browsers may show the same text in slightly different implementations of a specific font. The text() function will display the text in the Arial font by default. The reason we chose this as the default font is that it is most consistenly 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 you might think. For example, one browser may always show a certain font 15% larger than another browser. We strive to correct such differences, such that only on close inspection you will see any variations between different browsers rendering a given text. It is impossible to have exactly the same text layouts on different browser for the simple reason that even when using the Arial font, the shapes of some letters will vary slightly between browsers and this is impossible to compensate.

If you cannot use the Arial font for some reason, it is possible to use other fonts. In fact, you can do arbitrary styling of your text as will be explained in the chapter on style. You can adjust font type, weight, color, and many other styling aspects. But bear in mind that some of these may show considerable cross-browser variation.

Zooming and text sizing

Another problem is that, ordinarily, users can zoom in or out on web pages. On many browsers, pressing Ctrl-+ makes the whole page larger. On some browsers you can set the text size larger or smaller, while images and other elements on the page stay the same size. NeuroTask overrides the zooming and text sizing options and disables these. This is to guarantee as much as possible that the screen layout of your stimuli was as intended.

Centering text and images

In many experiments, stimuli are shown at the center of the screen (in a large font). As it turns out, on a web page it is suprisingly hard to center text (or images) both vertically and horizontally. At least, it is difficult to do so in a way that works on all browsers. It is not rocket science to center text, but it is somewhat cumbersome and yet another thing that you don’t have to worry about. The text() function already has the necessary code to achieve effective cross-browser centering.

After this fairly long introduction of the issues to be solved, you may wonder, how can you make use of the solutions we developed? The answer is: By using the our concepts of Box and Block to determine precise (relative) screen layouts. We will discuss each of these in turn.

3.4 Box

In most experiments you need only a single Box, which comprises the area where you can show your stimuli. A Box allows you to specify the color of the main stimulus area and of the surrounding unused area. You may also specify a margin around the stimulus area and border of a certain color and thickness. And you can set the display manner as ‘square’ or ‘fill’, which were discussed above.

The default Box gives you a maximally large white (transparent) square area without a border:

// (***) Remove new Box() example and add function addbox() that hides call to new

var main = new Box();
Light grey Box
Light grey Box

This Box will be invisible but can of course still be used to display text, as we will demonstrate below. Often you want a Box with a specific background color, such as light grey. This is done as:

var main = new Box("lightgrey");

This Box is visible as a light grey square.

A ‘subtle’ Box can be obtained with

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

This is a white box with an light grey (#fafafa) background and a black font color. #fafafa is one way of specifying colors, which you can use if none of the predefined colors, like ‘white’ or ‘lightgrey’ are quite right for your purposes. In the chapter on style, we will discuss the various ways in which you can specify shades of color.

In the script fragment, 100 means that the font size is 100%, the 5 means there is border that is 5 pixels wide of a very light grey color, coded as #f3f3f3. Finally, there is 5% padding (internal margin) around the entire box. Such padding is convenient if you don’t want your stimuli ‘touching’ the sides of the window.

3.5 Preset Box called main

By default, there is always one box already present, called main. This is the ‘white’ box you would get with: var main = new Box(). If main’s color scheme and layout is fine for you, you can simply use that. If not, you can declare a new one. For example, if you want a black box on a black background (i.e., all is black) with a white font, you would use:

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

With a Box you define the layout and color scheme of your experiment display. To present your stimuli, NeuroTask uses the Block.

main is default in several Box functions

Given that by default there exists a Box main, several Box methods can be called without a Box. If so, main will be used as a default. In particular, main.addblock(...) may be simplified to addblock(...).

3.6 Block

In many experiments the subject must fixate at a cross in the middle of the screen while a letter may appear either to the left or to the right, for instance, in the Simon Task or Posner Task. Or stimuli may be presented in any of the four quadrants of the screen or in still other configuations, like circular arrangements. To manage such locations, we have created the Block: a square portion of a Box. You can put as many Blocks in a Box as you want. Adding a Block is done with the addblock() function, which is called like this:

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

where main is a Box. This creates a block b, of which the left side starts at 10% of the width of Box main and the top of the block starts at 25% of main’s height. Block b itself has a width of 80% of Box main and a height of 50%. Its (optional) color is “yellow” and has the (optional) text “Display text”.

In the script fragment above, the statement addblock(10,25,80,50) specifies a 80% (width) by 50% (height) block that is centered because that is how the shape of this block is constructed. To facilitate centering, the addblock() method also allows writing “center”, as follows:

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

Now, the NeuroTask Scripting framework will figure you the left and top properties, such that the new Block will indeed appear smack in the middle. Other short-cut arguments are “left” and “right” for the first (left) argument, and “top” or “bottom” for the second (top) argument, for example

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

This would create a small square block that is 10% wide and 10% high and that is positioned in the right-bottom corner of the Box main.

Removing and destroying Blocks

In some experiments you may want to create a bunch of blocks (Block objects), delete them, create some others, and so on. To accomplish this a Box has the function removeblock(), which is called with the block you want to remove. In the following example, we first create a Block b in Box main and then remove it again.

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

// Here you would do useful stuff with the block but then you want to remove it:

main.removeblock(b);

Removing the Block b also destroys it. That is, any contents such as text, images, graphics, or video players, are deleted and their resources (notably memory) freed, so they can be reused by your script.

If you do not want removeblock() to destroy the block, you can call it with an extra parameter true. This is useful if you want to reuse it later or reassign it to another Box. The latter can be done with the pushblock() function. Here, we are removing a block from Box main and adding it to a Box Q.

main.removeblock(b,true);
Q.pushblock(b);

Adding the additional parameter true to removeblock() prevents it from being destroyed. A Block can always be destroyed by calling the destroy() function. For example:

main.removeblock(b,true); // Block b is removed from main but still visible

// Do other stuff and keep Block b handy in case you need it

b.destroy();    // Destroys the block; it will disappear from the screen as well

Example Block layouts

A handy block layout, which we may call centered is:

var centerblock = main.addblock("center","center",90,90);

This layout can be used for instructions and survey questions, shown one at a time, but also for showing images or words in the center of the screen. Making the box 90% of the width and height gives it a margin that looks nicer when you show instructions or other long texts, than when the text appears ‘glued’ to the sides.

Another useful layout is

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

This would give room at the top of the screen for messages and feedback, for stimuli in the middle, and for say a button at the bottom.

As will become apparent, it is quite easy to construct a layout that suits your needs.

Showing text in blocks

Once you have the blocks in place, how do you show text in a particular block? This is most easily done with the ‘.’ construction we encountered above, using the text() method, as follows:

var t = main.addblock("center","top",80,10),
    c = main.addblock("center","center",80,80);

    t.text("This completes the experiment");
    c.text("Thank you for participating!",300);

Notice that we did not create main as this has already been done for us; it is present by default. This script will show the text “This completes the experiment” at the top of the screen, centered, and “Thank you for participating” in the center of the screen, centered and 3 times (300%) the normal size.

For the web experts among you it is important to know that you are not limited to plain text: any type of HTML2 is allowed as text. By default. everything is centered in a block, but also this can be changed by altering the display style, as will be explained in the style chapter.

Showing images in blocks

Images are shown as follows:

var b = main.addblock("center","center",80,80);
b.setimage("cow.png");

Provided you had uploaded an image called “cow.png” to the script, this script fragment would show a cow in the center of the screen. Uploading and the usage of the setimage() function will be explained in the chapter on images.

If you have several blocks, each can contain its own image and these 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 with a word below it, shown in two blocks).

3.7 Using blocks as stimuli

Whereas Block layouts can be used as locations where to show stimuli such as text and images, they can also be used as stimulus configurations themselves. We will show some examples of this usage of Block layouts, starting appropriately 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 taps a number of blocks in a certain order. The subject (or patient) must then copy this, tapping the blocks in the same order. At some point, the subject will start making errors. The longest sequence before that is taken as the ‘span’, which in most people is a sequence of about five to seven blocks.

Traditionally, the blocks are made of wood and glued to a board in a more or less random arrangement. A computerized version will show blocks on the screen, which will ‘light up’ in a certain sequence. In the Appendix, we present a complete script for such a task. Here, we will only study how an arrangement of blocks can be put on the screen, for example, like this:

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
Corsi Blocks display with ten blocks produced by the script fragment

The variable coords specifies the left and top coordinates of the ten blocks used here. We use an array of arrays here:

coords = [[10,10],[20,40],...] 

In JavaScript you can put anything in a array, including other arrays. To get the values in an array of arrays, we must think in two steps: coords[0] equals [10,10]. The 0-th element of [10,10] is 10. Therefore, coords[0][0] is 10. Similarly, coords[1][0] equals 20 and coords[1][1] equals 40.

In the for-loop, when i is 0, the statement

main.addblock(coords[i][0],coords[i][1],
              10,10,"lightgrey");

puts a light grey block on the screen with left at 10% and top at 10%. The sizes of all Blocks are 10% of the width and height of the Box main. The script fragment above gives a display as shown on the right.

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, for example, 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 these. 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
Random dot pattern produced by the script fragment

The code for the ‘bullet’, &bull;, can be found on a w3schools web page where you can find many other symbols. You can use these so called HTML ‘entities’ (symbols) directly in your scripts by copying either the entity name or the entity number. We could also have used an ordinary dot in this script, “.”, but this appears as little a square in the Arial font, which we found less appropriate than a nice round bullet.

In order to make the whole experiment work, we would need to write additional code to distort the patterns, add a presentation phase and a recognition phase. This is shown in the Appendix, where we combine everything and present a number of complete experiment scripts.

3.8 The makebox() convenience function

NeuroTask includes the makebox() ‘convenience’ function for specifying often used Box layouts and color schemes. The first argument determines the number and layout of the Blocks. Right now, makebox() supports a ‘centered’ or ‘threerows’ blocks layout, where ‘centered’ is default. With ‘centered’ there is a single, centered block of 90% width and height. This is the layout used in the preset main Box.

For more complicated cases, the ‘threerows’ layout may be handy.

main = makebox("threerows")

is equal to:

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

Here, the centerblock is only 50% with a header block above and footer block below. All blocks extend the entire width of the Box (i.e., 100%).

The second argument of makebox() is a color scheme, which also allows a choice of a ‘wide’ box that fills the entire window:

‘white’
new Box(); // This is the default
‘black’
new Box('black','black','white');
‘light’
new Box('white','#fafafa','black',100,5,'#f3f3f3',5,"square");

The same color schemes can be made ‘wide’ by writing ‘white-wide’, ‘black-wide’, or ‘light-wide’. In that case, the box is no longer a square but fills the entire window.

So, if you want to show text or images at the center of the screen but with a completely black background (and white text), you can use:

box = makebox("centered","black");

This code has the same effect as writing:

box = new Box('black','black','white');

We may support additional layout and color schemes in the future.

3.9 Deleting the contents of a Box object

When you no longer want the contents of a Box object (i.e., in terms of blocks), you can call:

1 box.clearall();

This removes and destroys all Block objects and other contents, if any. The box can now be re-used and filled with new blocks using addblock().

  1. We may add a format that allows scrolling in the future, but at the moment we see no good application for it in experiments.
  2. HTML is the language in which the structure and markup of webpages is specified. At the HTML Dog web site (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.

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("http://lorempixel.com/400/200/animals/");
await(1000);
hideimage();

This will show a random image of an animal, which is provided (for free) by lorempixel.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 = await("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 
44 log(hits,"hits");                      // Log data into your account
45 log(false_alarms,"false_alarms");
46 
47 text("Thank you for participating!");  // 'debriefing'
48 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 ID of the Invite used (if any, else 0) and the ID Subject (if known, else 0). 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 resonsibility 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: <i>Cloud Icon</i>. 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
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 dialog:

If you hold the Ctrl-Key, you can select multiple-files
If you hold the Ctrl-Key, you can select multiple-files
List of files to upload
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
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
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
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
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 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 inadvertedly 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 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.3 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, which does 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.4 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 folows:

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.5 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.6 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.7 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.

  1. If you look up some style properties, you will notice that especially somewhat older browsers do not support all of them in the same way. As far as possible, NeuroTask Scriptings relies on the extensive and well-tested Dojo Toolkit to correct cross-browser problems.

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. For more complex forms, you may try out LimeSurvey, which is available on the NeuroTask Scripting website as well (go to survey.neurotask.com).

Having said all that, it is possible to build a very sophisticated survey system on the basis of NeuroTask 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. We plan to extend the controls library in the future and offer more different types and better out-of-the-box validation. And we expect that third-party tools will become available either for free in the Share section or for a fee in the Task Store.

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
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
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 middel 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
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
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 ‘benedril’, 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 }

The because you can do arbitrary procesing on the response with JavaScript, it is possible to make arbitrarily complex questionnaires.

Feedback with {name}

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 ‘benedril’, the question would automatically turn into:

input("Enter any side-effects you are experiencing from benedril",
      "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.
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):

1 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
Select drop-down form control

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

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
Select drop-down form control

scale()

The scale() control gives a Likert scale:

1  scale("How well do you sleep at night?",
2        "Very badly","Very well","sleep_quality");
Select drop-down form control
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     ],"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 reponse = 
 2 {
 3     hours_sleep: "7",
 4     instruction_button_1: "click",
 5     medication: "benedril",
 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.

///TODO: Initial values in form controls

6.11 TODO: Add initial values either from response or as arguments to the functions

N.B. The following is not yet implemented!!! But needs to be.

If a form control has a specific variable name, like “sleep1” and if that name has a property with a value in the response object, than that value will be shown automatically. That is also a format for showing values:

response["sleep1"] = "sometimes";
input("How often do you have trouble getting to sleep","sleep1");

This question will now be shown with the value “sometimes”. This method will automatically handle cases where a loop is used for validation:

while (1)
{
    input("Write something","blah");
    if (response["blah"].length > 0)  // Something written
    {
        break;                        // Break out of while loop
    }
    else
    {
        alert("You must enter a value"); // Or use something more subtle than an alert
    }
}

Or add this as an optional third parameter as well?

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

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, invitation, and session.

In most case, 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;
 2 var e, b, i;
 3 
 4 b = addblock().text("Get ready to click with the (left) mouse button...");
 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     await(1500);
19 }
20 
21 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 download 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;
 2 var e, b, i, total, average;
 3 
 4 total = 0;
 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_ip_address 213.93.228.65
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

All 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 ip address, given with ‘nt_ip_address’ variable, is the web address of the computer network on which the participant was taking the experiment. It is always collected by an internet server and may help to identify returning participants, in particular ‘banned’ subjects, which you identified in previous experiments. It is generally not possible to obtain the name or identify from an ip address (at least not without a court order), though there are fairly reliable methods of linking an ip address to a country or area within a country. An ip address, however, is not completely reliable to identify returning subjects, because sometimes many people share one ip address and some internet providers reassign ip addresses unpredictably. It is also possible to deliberately hide your ip address by using special services and browsers, in which case even the country where the participant is doing the experiment may be wrong: you can for example be using a French ip address even if you are actually in the United States.

The remaining 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 Strato corporation’s servers in Germany. The Strato servers are iso 27001 certified. They are protected with very high security standards and are subject to the German 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 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
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’, ‘Invite’, 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. ‘Invite’ is the invitation sent through NeuroTask Scripting by email to invite subjects to take the experiment. If the subject took the experiment through the general URL, this value is 0. If it is not, you can click the number, which links to the original Invite. There, you will be able to see the invite details, like which other subjects where invited and when this occurred. ‘Subject’, finally, contains the ID number of the subject who did the experiment. If the subject used the general script URL or if you invited the subject with an anonymous Invite, a new (anonymous) will have been generated by the NeuroTask Scripting system. If not, that is, if you invited the subject personally through a non-anonymous Invite, the number will be that of the known subject who did the experiment. You can also click on the subject number, which is linked to the subject’s record.

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. The ‘anonymity’ is not shown in the table to prevent adding yet one more column, and it is rarely the case that you have a mix of anonymous and known subjects; most experiments are either anonymous (e.g., via a general URL) or by invitation only (e.g., patients or friends and family).

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, invited by ‘invite’ X, doing an experiment programmed by ‘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’, ‘invite’, ‘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.

What ‘Type’ means in the data table

One could say that ‘session’ is the most basic level at which you can log data in the sense that each logged row of data also contains information about the script (and thus author), invite, and subject. Sometimes, however, you want to log data at a ‘higher’ level. For example, for a particular subject you want to log which experiments he or she did. This can be accomplished by setting the third argument of the log() function to ‘subject’, like this

log("Corsi Block Task","Experiment","subject");

If you include a line of code like this in all experiments, e.g.,

log("Tower of London Task","Experiment","subject");

and perhaps many more in other scripts, each of these would be logged as usual, except that the type is now ‘subject’ and the ‘ID’ column now contains the ID of the subject. This is only useful if the ID of the subject is known, which is only the case with a non-anonymous invite. In all other case, new ‘anon’ IDs will be generated.

It is also possible to log data at the level of the ‘invite’. Suppose, you invite 10 anonymous subjects and you want to keep track of their reaction times. You could add a line of code:

log(average,"average","invite");

where average is as in the Script 7.3 above. If you had sent different invites to different groups of people, this would be a way to get a quick overview of their performance.

Another useful level is the ‘script’. This keeps track of all sessions by all invites and subjects that have been done for a given script. You may suspect there is a time-of-day effect and therefore log the overall performance of each experiment at the level of the ‘script’:

log(total_performance,"Performance","script");

Now, all experiments done will leave one row of data in the table that has type ‘script’ (and the ID of this particular script, e.g., 4201).

We can go one level higher, where we will reach you, the ‘author’. If you log data the ‘author’ type, you can accrue data across several scripts. Suppose, you have several similar scripts with a comparable general performance measure (e.g., reaction time) and you wish to track time-of-day effects on these, then you could simply include a line:

log(total_performance,"Performance","author");

In theory you could go one level higher, ‘global’, which all authors could log and view. This level is currently not available but may be in the future, if we see a good reason for it.

Most of the examples in this section are somewhat contrived, the sense that if you would have written:

log(total_performance,"Performance");    // 'session' is the default type

it would have been recorded just fine. And each data row would have included the script ID anyway (and invite and subject IDs if available). The real power of the different levels becomes apparent with data storage and retrieval, where you can increase and decrease numeric values and update data values, which can later be retrieved. This can make your scripts much more ‘intelligent’ and responsive.

7.4 The main data type selector

If you store data that is not of type ‘session’, you will not immediately see these if you arrived at the page via clicking on the table icon. By default, only the ‘session’ data is shown for a specific script.

If you want to see other types of data, you can select this in the Type drop-down menu all the way on top. If you for example select ‘Subject’, the page then gives you an option to select in the ‘Show only for…’ drop-down. If you would select ‘Script’ here, you are then given the opportunity to select a specific script in the dropdown select list labeled ‘… with ID or name’. For example, you could select script ‘1343: MyExperiment’. The whole selection should be read as: ‘Give me all Subject data for Script 1343: MyExperiment.’

In this way quite a few variations can be made, all which can be downloaded to Excel, as is described below. It is not currently possible, however, to get a mix of say ‘Subject’ and ‘Session’ data in one table. You will have to construct this yourself out of several downloaded tables.

7.5 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 ‘session’, 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(), 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.

As a second argument of retrieve, you could also have specified ‘subject’. The ‘Where’ variable would have been stored at the subject level. For invited (non-anonymous) subjects, this has the advantage that they can in principle continue the experiment on a different computer and also after 48 hours. The default level is ‘session’. Other values are ‘invite’, ‘script’ and ‘author’ (i.e., you).

Let’s extend the technique above to more than two parts and use the ‘subject’ level instead of the ‘session’ level. 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","subject") || 0;
 2 
 3 if (bookmark < 1)
 4 {
 5     text("Press '1'"); // dummy task
 6     awaitkey('1');
 7     store(1,"Where","subject");
 8 }
 9 
10 if (bookmark < 2)
11 {
12     text("Press '2'"); // dummy task
13     awaitkey('2');
14     store(2,"Where","subject");
15 }
16 
17 if (bookmark < 3)
18 {
19     text("Press '3'"); // dummy task
20     awaitkey('3');
21     store(3,"Where","subject");
22 }
23 
24 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","subject") || 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 done. 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.

If a variable does not exist for the level at which retrieve() is called, it will return undefined. So, if you do this: store(3,"Where","subject") and then x = retrieve("Where"), x will be undefined, because retrieve("Where") tries to retrieve (only) ‘session’ variables, whereas you stored ‘Where’ at the ‘subject’ level. But if you do x = retrieve("Where","subject"), x should equal 3.

You can check for null and undefined like this:

var x = retrieve("Where");

if (x === undefined)
{
    // 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.

With retrieve() this is not possible because the 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 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.

increase() and decrease()

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.

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).

7.6 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.7 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
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.

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.8 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.

  1. RAF is an abbreviation of ‘requestAnimationFrame’, this is a signal that the next screen is about to be generated by the computer, which typically happens exactly 60 times per second. See the advanced manual for a more in-depth discussion of this.
  2. Many 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.
  3. SPSS 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.

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 built 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 modern browsers (i.e., those that follow the so called HTML5 standard). The RAF() function halts processing until the browser is about to refresh (and update) the screen. Normally, this happens exactly 60 times per second though this so called ‘refresh rate’ may take other values on certain computers.

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.

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 cuved 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 wether 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.

var a = addblock(“center”,”bottom”,80,20).text(“Press Space bar to start!”), c = addblock(1,1,15,15).icon(‘bug’,350) .style(‘color’,’crimson’), b = addblock(40,40,20,20).icon(‘bullseye’,450) .style(‘color’,’navy’).moveable(), speed = 1, hor = speed, ver = speed;

 1 awaitkey(" ");
 2 a.clear();
 3 
 4 for (var i = 0; i < 2000; i++)
 5 {
 6     RAF();
 7     
 8     c.move(hor,ver);
 9 
10     if (c.inside(b))
11     {
12         a.text("Caught!!!");
13         break;
14     }
15 
16     if (random() < .025)
17     {
18         hor = -hor;
19     }
20 
21     if (random() < .025)
22     {
23         ver = -ver;
24     }
25 
26     if (c.left < 0 || c.left > 85)
27     {
28         hor = -hor;
29     }
30 
31     if (c.top < 0 || c.top > 85)
32     {
33         ver = -ver;
34     }
35 }
36 
37 c.animate('font-size','350%','0%',1000); // fade out the bug
38 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 browsers using a Flash plugin and for recent browers 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.

Playing a sound file is done as follows:

1 play('cow.mp3');
2 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

1 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 the give 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 (taken straight from SoundManager2) are:

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

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. It is possible to play only a fragment with to and from in the options shown above.

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:

1 setvideo('big_buck_bunny');
2 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, awaitkey("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, must first upload it to your account. After 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:

1 var s = {
2     subtitles: "bunny_subtitles.srt",
3     chapters: "bunny_chapters.vtt"
4 }
5 
6 main.addblock("center","center",100,60).setvideo('big_buck_bunny',100,100,s);
7 
8 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 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     },
16     advanced_options = {
17         clickToPlayPause: false,                        
18 		features: ['volume','fullscreen'],
19 		enableKeyboard: false
20     };
21 
22 waitfor
23 {
24     b2.setvideo('big_buck_bunny',100,80,video_options,"bunny",advanced_options)
25       .await('videoended');
26 }   
27 or
28 {
29     b3.await('click');                    
30 }             
31 
32 await(1000);
33 
34 b2.clear();                
35 b3.clear();
36 b1.text("This could have been an instruction video or a scene for eye-witness questions, e\
37 tc.");

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 directory, 
 4             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 player 
 9             on page, and then 2, 3, ...)
10 
11 == options ==
12 
13 options object (all members below are optional; all paths are within the videos directory)
14 
15 hasposter   false by default (this is the image shown at the beginning, before playing)
16 poster      path/name.jpg or .png (default is url.jpg), e.g., animals/grazing_cows_evening\
17 .jpg
18 
19 autoplay    false On true, start playing immediately
20 
21 style       'ted', 'wmp', or 'mejs' (= default). N.B. Must be lower-case.
22 formats     ['mp4','webm'] is default. Also allowed: 'ogv'. N.B. Even with one format, 
23             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 
10 volume      80, initial volume (mejs uses 0.8 here; we remain conisistent with SoundManage\
11 r2)

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','fullscreen'],
21 // Hide controls when playing and mouse is not over the video
22 alwaysShowControls: false,
23 // force iPad's native controls
24 iPadUseNativeControls: false,
25 // force iPhone's native controls
26 iPhoneUseNativeControls: false,	
27 // force Android's native controls
28 AndroidUseNativeControls: false,
29 // forces the hour marker (##:00:00)
30 alwaysShowHours: false,
31 // show framecount in timecode (##:00:00:00)
32 showTimecodeFrameCount: false,
33 // used when showTimecodeFrameCount is set to true
34 framesPerSecond: 25,
35 // turns keyboard support on and off for this instance
36 enableKeyboard: true,
37 // when this player starts, it will pause other players
38 pauseOtherPlayers: true,
39 // array of keyboard commands
40 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     },
16     advanced_options = {
17       clickToPlayPause: false,    // Disable click                    
18 		features: [],               // No controls visible (Play etc.)
19 		pauseOtherPlayers: false,   // Allow two or more players
20 		enableKeyboard: false       // No keyboard shortcuts
21     };
22 
23 b2.setvideo('animated_snake_moving_1',100,100,video_options,"snake1",advanced_options);
24 // no black background
25 query(".mejs-container",b2.node).style("background-color","white"); 
26 query(".mejs-controls",b2.node).style("display","none"); // Hide controls
27 
28 // Make sure the second video player has a different ID, here 'snake2'
29 b3.setvideo('animated_snake_moving_1',100,100,video_options,"snake2",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 playing
36 cover3.style("background-color","transparent"); 
37 
38 await("videoended"); // We finish up as soon as the first player has ended
39 
40 b2.deletevideo(); // Delete the players; we will (re)create new ones all the time
41 b3.deletevideo();
42 b2.clear(); // We clear the block
43 b3.clear();                
44 
45 await(500);
46 
47 b1.text("That's all folks!");
48 
49 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 specifed with the features option, features: [], that no controls should be visible, some browsers still show an empty gray 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 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 ligh 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 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 pivottable(/*zipped or unzipped data*/data,/*string*/rows,
2                     /*string*/columns,/*(optional) object with key:function pairs*/func)  \
3           

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. They 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 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 store() 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:

  • mean
    Average
  • median
    Median
  • stddev
    Standard deviation
  • stderr
    Standard error
  • variance
    Variance
  • sumsqerr
    Sum of squared errors (i.e., sume of squared deviations from mean)
  • min
    Lowest value
  • max
    Highest value
  • range
    Difference between highest and lowest value

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

1 pivottable([{x:1,y:2},{x:1,y:4},{x:2,y:3},{x:2,y:6}],
2            "x","y",{average:mean,n:count}) 

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 stored 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 To be written

Example Scripts

In this chapter, we will demonstrate how to write scripts for a number of 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.

 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 after Posner and Keele (1968), Kolodny, Squire and Knowlton, etc.

  • 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."));