Text, Pictures and Keys

Elements

The notion of elements is central in PennController: elements allow you to manipulate content such as text, images, audio or videos, but also to structure trials by inserting pauses and, crucially, collecting the participant’s input.

All PennController trials 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()

You probably understand what it 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, and then it creates an image element using the file 2fishRoundTank.png and prints it onto the screen.

Walking through a trial

You cannot simply copy the script from above and paste it into your file main.js as is, expecting it to show some text and an image on the page. You first need to embed it within PennController( ... ) so as to make it part of a trial. So try to add the following at the end of 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()
)

As you can see, 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!

What happened 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 effectively 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.
The line 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.

Waiting for a key press

So as commands get executed upon runtime in the order in which they appear in your script, you want to have a way to control when to pause the execution, in order to give time to your participants to interact with the page. This is where more interactive elements, such as the key element, come in handy. Try this:

PennController(
    newText("The fish swim in a tank which is perfectly round")
        .print()
    ,
    newImage("2fishRoundTank.png")
        .print()
    ,
    newKey(" ")
        .wait()
)

Up to the second comma, everything proceeds exactly as before. But now, when execution reaches newKey(" "), the trial starts paying attention to any key press on the space bar. The last line, .wait(), relates to this key element and tells 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 invite participants to report 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 " " with "FJ" as the argument of the key element so it now pays attention both to key presses on the F and the J keys.

PennController(
    newText("The fish swim in a tank which is perfectly round")
        .print()
    ,
    newImage("2fishRoundTank.png")
        .print()
    ,
    newImage("1fishSquareTank.png")
        .print()
    ,
    newKey("FJ")
        .wait()
)

Visual Layout

Though we already have something functional, which is conceptually very close to our goal, it would be best if our participants could see smaller renderings of the pictures, side by side.

Resizing elements

The command .settings.size was designed precisely to answer 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()
)

Canvas

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 because we printed them immediately after creating them in the cache, and then we were done manipulating 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. Instead, we add the images to the canvas element, using .settings.add on it, and referring back to the image elements created before using their names in getImage. Then we print the canvas element, which now contains the images, and this is how the images end up appearing onto the screen, side by side.

The rest remained unchanged. In particular, the last command of the trial is still a .wait on the key element, which makes sure that the screen won’t be cleared until the F or the J key is pressed.


Next: Data collection & analysis