Skip to content

Writing your experiment

Finally, let's stimulate our participant!


Starting the development server

Your ROAR app is ready to go out of the box. Let's start the development server to see what the experiment looks like in the browser.

npm start

This will automatically open a new browser tab with your experiment. It should look something like the experiment hosted here:

A screenshot of the 2AFC ROAR template experiment

However, you're experiment will automatically update when you make changes to your source code.

Adding another block of stimuli

Let's pretend that we want to add a new block to our experiment where we ask the participant to further differentiate between cats and dogs. Let's implement that structure now by first adding images of cats and dogs and then editing the experiment's source code to include those new images.

Adding more images

The first step in creating our new block is hosting the images of dogs and cats that we will use as stimuli. Download these images to your computer

We will demonstrate two different ways to host images: we'll host the cat images along side your experiment and the dog images in a public google cloud storage bucket.

Hosting images alongside your experiment

First download the cat images zip file. Assuming that this file was downloaded to ~/Downloads, do

# Make a folder for the cat images
mkdir -p src/assets
# Navigate to that folder
cd src/assets
# Move the downloaded zip file to this folder
mv ~/Downloads/cat.zip .
# Unzip the file
unzip cat.zip
# And delete the zip file
rm cat.zip
cd ../..

Now edit the src/loadAssets.js file to load these new cat images. You can add individual files simply by importing them as variables and then referencing them in your code. For example

src/loadAssets.js
import jsPsychPreload from '@jspsych/plugin-preload';

import cat1 from './assets/cat/1.jpg';
import cat2 from './assets/cat/2.jpg';
import cat3 from './assets/cat/3.jpg';
import cat4 from './assets/cat/4.jpg';
import cat5 from './assets/cat/5.jpg';

// Reference these files in a new array
const catImages = [cat1, cat2, cat3, cat4, cat5];

// Create arrays of hot dog / not hot dog images
const numFiles = 5;
const hotDogFiles = Array.from(Array(numFiles), (_, i) => i + 1).map(
  (idx) => `https://storage.googleapis.com/roar-hot-dog-images/hotdog/${idx}.jpg`,
);

const notHotDogFiles = Array.from(Array(numFiles), (_, i) => i + 1).map(
  (idx) => `https://storage.googleapis.com/roar-hot-dog-images/nothotdog/${idx}.jpg`,
);

const allFiles = hotDogFiles.concat(notHotDogFiles);
export const allTargets = allFiles.map((url) => ({
  target: `<img src="${url}" width=250 height=250>`,
  isHotDog: !url.includes('nothotdog'),
}));

/* preload images */
export const preloadImages = {
  type: jsPsychPreload,
  images: allFiles,
};

// Preload the cat image
export const preloadCatImages = {
  type: jsPsychPreload,
  images: catImages,
}

And don't forget to commit your changes into git.

git add src/assets/cat
git add -u
git commit -m "Add cat images for block 2"

Hosting images using a cloud storage provider

The above method of hosting image assets alongside your experiment is fine if you only have a few files. But it can become cumbersome to import each file separately if you have a lot of assets. Rather than hosting your files with your website, you can upload them to a cloud storage provider and access your files using a URL. To demonstrate, we have already uploaded the dog images to a Google Cloud Storage bucket. In fact, it is the same bucket that already hosts the hot dog vs. not hot dog images. You can see the images here (1, 2, 3, 4, 5).

Two popular cloud storage service providers are Google Cloud Storage (GCS) and Amazon Simple Storage Service (S3). To upload your own images and make them publicly available, follow these instructions:

Then we can add references to the dog image URLs like so

Edit the src/loadAssets.js file to include the dog image URLs

src/loadAssets.js
import jsPsychPreload from '@jspsych/plugin-preload';

import cat1 from './assets/cat/1.jpg';
import cat2 from './assets/cat/2.jpg';
import cat3 from './assets/cat/3.jpg';
import cat4 from './assets/cat/4.jpg';
import cat5 from './assets/cat/5.jpg';

// Reference these files in a new array
const catImages = [cat1, cat2, cat3, cat4, cat5];

// Create arrays of hot dog / not hot dog images
const numFiles = 5;
const hotDogFiles = Array.from(Array(numFiles), (_, i) => i + 1).map(
  (idx) => `https://storage.googleapis.com/roar-hot-dog-images/hotdog/${idx}.jpg`,
);

const notHotDogFiles = Array.from(Array(numFiles), (_, i) => i + 1).map(
  (idx) => `https://storage.googleapis.com/roar-hot-dog-images/nothotdog/${idx}.jpg`,
);

const dogFiles = Array.from(Array(numFiles), (_, i) => i + 1).map(
  (idx) => `https://storage.googleapis.com/roar-hot-dog-images/dog/${idx}.jpg`,
);

const allFiles = hotDogFiles.concat(notHotDogFiles);
export const allTargets = allFiles.map((url) => ({
  target: `<img src="${url}" width=250 height=250>`,
  isHotDog: !url.includes('nothotdog'),
}));

/* preload images */
export const preloadImages = {
  type: jsPsychPreload,
  images: allFiles,
};

const block2Files = catImages.concat(dogFiles);
export const block2Targets = block2Files.map((url) => ({
  target: `<img src="${url}" width=250 height=250>`,
  isDog: url.includes('dog'),
}));

// Preload the cat/dog images
export const preloadBlock2Images = {
  type: jsPsychPreload,
  images: block2Files,
};

Making sense of the above javascript

If the above line of javascript for dogFiles doesn't make sense to you, let's break it down into its components:

Array(numFiles)
// Yields [ <5 empty items> ] since numFiles = 5

Array.from(Array(numFiles), (_, i) => i + 1)
// Yields [ 1, 2, 3, 4, 5 ]

// And Finally
Array.from(Array(numFiles), (_, i) => i + 1).map(
  (idx) => `https://storage.googleapis.com/roar-hot-dog-images/dog/${idx}.jpg`,
);
// Yields
// [
//   'https://storage.googleapis.com/roar-hot-dog-images/dog/1.jpg',
//   'https://storage.googleapis.com/roar-hot-dog-images/dog/2.jpg',
//   'https://storage.googleapis.com/roar-hot-dog-images/dog/3.jpg',
//   'https://storage.googleapis.com/roar-hot-dog-images/dog/4.jpg',
//   'https://storage.googleapis.com/roar-hot-dog-images/dog/5.jpg'
// ]

By using the map method, we were able to write the URL pattern just once, following the DRY principle. We use the map method again to convert the raw URLs in the dogImages array to HTML image tags in the block2Targets array.

Adding a block of stimuli to index.js

Now that we have established references to our new cat vs. dog images, let's create the new block of stimuli. We'll introduce this first by adding only one single stimulus. and then we'll use the same code to add an entire block of stimuli using jsPsych's timeline variables.

Adding a single stimulus

We will add the first stimulus in the block2Targets array.

Edit the src/index.js file to include a new instruction set and the new stimuli.

At the top of the file, add the following imports

src/index.js
// Local modules
import { initConfig, initRoarJsPsych, initRoarTimeline } from './config';

import { allTargets, preloadImages, block2Targets, preloadBlock2Images } from './loadAssets';

Then, a little bit later in the file, add

src/index.js
timeline.push(hotDogTrials);

const block2Instructions = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `
    <h3>Great Job!</h3>
    <p>
      Now press the right arrow key if the displayed image is of a dog.
      Press the left arrow key if the displayed image is of a cat.
    </p>
    <p>Press any key to continue.</p>
    `,
};

timeline.push(preloadBlock2Images);
timeline.push(block2Instructions);

const catDogTrials = {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: `<div style="font-size: 60px;">+</div>`,
      choices: 'NO_KEYS',
      trial_duration: 500,
    },
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: block2Targets[0].target,
      choices: ['ArrowLeft', 'ArrowRight'],
      prompt: `
        <p>Is this a cat or a dog?</p>
        <p>If cat, press the left arrow key.</p>
        <p>If dog, press the right arrow key.</p>
        `
      data: {
        // Here is where we specify that we should save the trial to Firestore
        save_trial: true,
        // Here we can also specify additional information that we would like stored
        // in this trial in ROAR's Firestore database.
      },
    }
  ]
};

timeline.push(catDogTrials);

Adding a block of stimuli

We just added one single stimulus. It would be really annoying to have to write all that code over and over just to add the next nine stimuli for this block. Luckily, jsPsych has timeline variables to make this easier. In fact, the hot dog vs. not hot dog block already uses this technology. Let's add the other dog vs. cat stimuli using timeline variables with random sampling.

Edit the catVsDogTrials in the src/index.js file so that it reads:

src/index.js
const catDogTrials = {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: `<div style="font-size: 60px;">+</div>`,
      choices: 'NO_KEYS',
      trial_duration: 500,
    },
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable('target'),
      choices: ['ArrowLeft', 'ArrowRight'],
      prompt: `
        <p>Is this a cat or a dog?</p>
        <p>If cat, press the left arrow key.</p>
        <p>If dog, press the right arrow key.</p>
        `
      data: {
        // Here is where we specify that this trial is a test response trial
        task: 'test_response',
        // Here we can also specify additional information that we would like stored
        // in this trial in ROAR's Firestore database. For example,
        start_time: config.startTime.toLocaleString('PST'),
        start_time_unix: config.startTime.getTime(),
        timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
      }
    }
  ],
  timeline_variables: block2Targets,
  sample: {
    type: 'without-replacement',
    size: 10,
  },
};

Ending the experiment

We've added the second block of stimuli. Right now, the experiment abruptly ends after the last stimulus. It's a good idea to let your participants know that they've finished the experiment. Let's add one last trial telling the participant that they are done.

Add one more trial and push it to the timeline before the exit_fullscreen trial.

src/index.js
const endTrial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: '<p>Great job! Press any key to finish the assessment.</p>',
  choices: 'ALL_KEYS',
  response_ends_trial: true,
};

timeline.push(endTrial);

const exit_fullscreen = {
  type: jsPsychFullScreen,
  fullscreen_mode: false,
  delay_after: 0,
};

timeline.push(exit_fullscreen);

How to properly end your assessment

Be sure to give your participant concrete instructions for how to end the assessment. In this case, we told them to "press any key to finish the assessment." If you don't, then the participant might think that they are done and simply close the browser tab. Why is this bad? Although all of the trial information will be saved in the database, the assessment will not be counted as finished (either in the database or in the participant dashboard) because the jsPsych timeline did not complete.

If you want to use any of jsPsych's audio plugins (e.g., audio-button-response or audio-keyboard-response) to end the assessment be sure to specify trial_ends_after_audio: true so that the experiment automatically ends after the last audio file is played. Likewise, if you want to use any video plugins (e.g., video-button-response or video-keyboard-response), be sure to specify trial_ends_after_video: true so that the experiment automatically ends after the last video.

The guiding principle here is to ensure that the jsPsych timeline ends before the participant closes their browser tab.

Advice for asset files

Please see Common Issues > Advice For Asset Files.