Elements are the core pieces that PennController works with: they allow you to manipulate and present content such as text, images, audio or videos, but also to structure trials by inserting pauses and, crucially, collecting the participant’s input.
The main parts of PennController trial scripts therefore consist of sequences of commands relating to an element. When you want to show some text, you create a text element and use a command to print the text onto the screen. If you want to add a picture below it, you create an image element and again use a command to print it onto the screen.
With this in mind, consider the following code:
newText("The fish swim in a tank which is perfectly round") .print() , newImage("2fishRoundTank.png") .print()
It’s probably pretty transparent what this does: it creates a text element with the content The fish swim in a tank which is perfectly round and prints it onto the screen. After the comma, an image element is created using the file 2fishRoundTank.png (which, like all the other files we’ll use here, was uploaded under Resources during the initial project updated from github) and printed onto the screen as well.
Walking through a trial
For integrating this into an experiment on the PCIbex Farm, just copying the script from above and pasting it into your file main.js isn’t quite enough. You need to embed this code within the PennController( ... ) environment, to make it part of a trial created with PennController. So try adding the following to your script, then click Save and test and see what happens:
PennController( newText("The fish swim in a tank which is perfectly round") .print() , newImage("2fishRoundTank.png") .print() )
You’ll find that this doesn’t work as intended yet. As soon as you open the page, you immediately reach the end of the experiment, with a message saying The results were successfully sent to the server. Thanks!
(Note: This problem doesn’t arise if you use the Try it environment, because it is configured slightly differently for sake of simple illustration.)
This illustrates an important aspect of how scripts are executed: they carry on plowing through the sequence of commands until reaching the end. So what happened here is:
- the trial started by executing the command on the very first line inside PennController(...):
newText("The fish swim in a tank which is perfectly round"). It created a text element in the cache of the trial. Any command after this line relates to the newly created text element, until we reach a separating comma.
- The second line,
.print(), thus relates to the very same text element, and even though you couldn’t see it when you tested your script, the text was actually added to the page.
- Execution went on to the third line of the trial, where it encountered a comma indicating that a new series of commands was coming.
newImage("2fishRoundTank.png")created an image element in the cache of the trial from the file 2fishRoundTank.png. Any command after this line relates to the newly created image element.
- The next and last line of the trial,
.print(), thus relates to the image element we just created with newImage. It adds the image to the page below the text element that was previously added.
And that was it, there were no commands left to be executed for the trial, so the trial ended, and it was the end of the experiment. All of that happened within a few milliseconds.
Holding the press: Waiting for a key press
If we want to pause this series of commands getting rapidly executed upon runtime in the order in which they appear in your script, so that your participants can interact with the page, we have to add a command that will halt things. This is where more interactive elements, such as the key element, come in handy. Try adding the lines prefaced with a ‘+’ to your script:
PennController( newText("The fish swim in a tank which is perfectly round") .print() , newImage("2fishRoundTank.png") .print() , newKey(" ") .wait() )
Up to the second comma (i.e., up to printing the image), everything proceeds exactly as before. But now, when execution of the script reaches
newKey(" "), the trial starts listening for a key press on the space bar. The last line,
.wait(), relates to this key element and causes the execution to pause until the space bar is pressed. As a result, execution will not reach the end of the trial until your participants press the space bar, which will give them time to see what is on the page.
Two images, two keys
We now have all we need to ask participants to indicate which of two pictures matches a description by pressing one of two keys. We simply create a second image element in the cache using 1fishSquareTank.png that we print below the first one, and we replace the string " " in the argument of the key element with "FJ", so that it now pays attention to key presses on both the F and the J keys. (Note that the string of keys included here specifies a set of keys that cause the execution of the script to resume.)
PennController( newText("The fish swim in a tank which is perfectly round") .print() , newImage("2fishRoundTank.png") .print() , newImage("1fishSquareTank.png") .print() , newKey("FJ") .wait() )
Although we already have a basic functional version of what we’re aiming for, it would be best if our participants could see smaller renderings of the pictures, side by side.
The command .settings.size makes it possible to address the first issue:
PennController( newText("The fish swim in a tank which is perfectly round") .print() , newImage("2fishRoundTank.png") .settings.size(200,200) .print() , newImage("1fishSquareTank.png") .settings.size(200,200) .print() , newKey("FJ") .wait() )
Now how about positioning the images side by side? A very simple and yet powerful element is the Canvas element: it defines a transparent rectangle surface on the page onto which you can add whatever element (even other canvas) wherever you want (even over other elements on the Canvas, like images). In our case, we simply want a 450x200px surface so we can place the images side by side while leaving a 50px gap between them:
PennController( newText("The fish swim in a tank which is perfectly round") .print() , newImage("two", "2fishRoundTank.png") .settings.size(200,200) // .print() , newImage("one", "1fishSquareTank.png") .settings.size(200,200) // .print() , newCanvas(450,200) .settings.add( 0 , 0 , getImage("two") ) .settings.add( 250 , 0 , getImage("one") ) .print() , newKey("FJ") .wait() )
We brought several changes at once here.
- For one, we now named the image elements: so far, we had only used them anonymously, as it were, because any commands operating on them, such as printing them, were executed immediately after creating them. But now, we need to give them names so we can refer back to them within a command that relates to another element, namely a canvas element.
- We also no longer use .print() commands on the image elements directly. Instead, we add the images to the canvas element, using .settings.add, and refer back to the previously created image elements using their names withing getImage (a first example of a family of getX commands which serve to retrieve already created elements). Then we print the canvas element as a whole, which now contains the images, and this is how the images end up appearing onto the screen, side by side in the positions defined in the pixel parameter in the .settings.add commands.
The rest remained unchanged. In particular, the last command of the trial is still a .wait on the key element, which ensures that the screen won’t be cleared until the F or the J key is pressed.
Next: Data collection basics