8 Animation and drag-and-drop

8.1 Animation

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

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

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

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

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

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

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

Blinking other properties with toggle()

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

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

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

Using the animate() function

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

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

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

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

A general approach to animation with RAF()

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

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

Using icons with the icon() function

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

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

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

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

An animation loop

The code for the animation is as follows:

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

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

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

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

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

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

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

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

8.2 Drag-and-drop

Drag-and-drop basics

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

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

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

Dragging a block inside (on top of) another

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

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

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

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

Drag-and-drop with multiple drop targets

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

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

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

8.3 Putting everything together: A simple game

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

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