24  Lab 11: Face Perception

24.1 Research in Brief: Face Inversion

24.1.1 The Research Area

Perception refers to the cognitive processes through which we interpret and make sense of sensory information from our environment. Face perception represents a specialized domain within this broader system, involving mechanisms that allow us to extract various types of information from faces, including identity, emotion, age, and social characteristics. Our visual system processes faces differently from other objects, reflecting the importance of faces in human social interaction.

Face recognition addresses fundamental questions about how we identify individuals and maintain stable representations of people despite substantial variations in their appearance. How do we recognize someone across different viewing angles, lighting conditions, and expressions? What makes some faces easier to remember than others? Why do we struggle to match photographs of unfamiliar faces but effortlessly recognize people we know? Understanding face recognition has important implications for eyewitness testimony, security systems, and disorders affecting social cognition.

The study of face recognition connects to many practical applications in forensic contexts, passport control, surveillance systems, and understanding conditions like prosopagnosia (face blindness). To investigate these mechanisms systematically, researchers have developed experimental paradigms that can isolate different aspects of face processing, including how faces are represented in memory and what happens when faces are transformed or degraded.

24.1.2 Types of Face Processing

Face processing operates through several distinct systems that extract different types of information. Configural processing involves perceiving faces as integrated wholes rather than collections of separate features, allowing us to detect subtle differences in the spatial relationships between facial features. Featural processing focuses on individual components like eyes, nose, and mouth, though these features are typically processed within the context of the whole face rather than in isolation.

Holistic processing represents a particularly important aspect of face perception that involves integrating all facial features into a unified representation. Unlike processing individual features separately, holistic processing treats the face as a gestalt pattern where the whole is more than the sum of its parts. This system allows us to recognize faces rapidly and accurately, even when individual features might be difficult to identify in isolation.

Research distinguishes between processing of familiar and unfamiliar faces, which show markedly different characteristics. Familiar face recognition is robust across variations in viewpoint, lighting, and expression, while unfamiliar face matching is surprisingly error-prone even when comparing two photographs of the same person. These systems rely on different types of information, with unfamiliar face processing dominated by external features like hairstyle, while familiar face recognition depends more heavily on internal features like the eyes and nose.

24.1.3 The Research Design

The face inversion paradigm developed by Yin (1969) uses a within-subjects experimental design to examine how orientation affects face recognition. Participants recognize faces that are presented either upright or inverted (rotated 180 degrees).

Stimulus Presentation: Participants view photographs of faces in either normal upright orientation or turned upside down. The faces may be familiar (celebrities or personally known individuals) or unfamiliar faces that participants study before testing. The same face images are used in both orientations, controlling for differences in image quality or distinctiveness.

Task Requirements: Participants must identify familiar faces by naming them or matching them to provided names, or they must recognize previously studied unfamiliar faces in a memory test. They respond as accurately as possible, with both accuracy and response time measured to assess recognition performance.

The design manipulates face orientation as the key variable: upright presentation where faces appear in their normal orientation, and inverted presentation where faces are rotated 180 degrees. For comparison, the same inversion manipulation is often applied to other visual stimuli such as houses, objects, or body postures to determine whether faces show a disproportionate inversion effect.

The within-subjects design allows researchers to compare recognition performance for upright versus inverted presentations within the same participants. Each person experiences both upright and inverted faces, acting as their own control. This approach controls for individual differences in overall recognition ability while isolating the specific effects of orientation on face processing.

24.1.4 Key Findings

The within-subjects comparisons have revealed that face recognition is disproportionately impaired by inversion compared to recognition of other visual objects. Upright faces are typically recognized with 95% accuracy, while inverted faces drop to approximately 70% accuracy. This inversion effect is larger for faces than for other categories of objects, suggesting that faces rely on specialized processing mechanisms that are disrupted by inversion.

The inversion effect appears to result from disruption of configural processing. When faces are upright, observers readily perceive the spatial relationships between features and the holistic pattern of the face. Inversion disrupts this configural processing, forcing observers to rely more on individual features. Evidence for this comes from the composite face effect, where the top half of one face combined with the bottom half of another creates a new perceived identity in upright faces, but this effect disappears when faces are inverted.

The magnitude of the inversion effect varies with face familiarity and type of information available. Familiar faces show substantial inversion effects, though they remain more recognizable when inverted than unfamiliar faces in the same orientation. Line drawings of faces show larger benefits from caricaturing when inverted, suggesting that reduced information makes configural processing more difficult. Photographic negatives show even larger impairments than inversion, with inverted negatives recognized only 25% of the time.

24.1.5 Implications

The disproportionate effect of inversion on face recognition provides evidence for specialized face processing mechanisms that rely heavily on configural information. This behavioral evidence is supported by neuroimaging studies showing that face-selective regions like the fusiform face area respond differently to upright versus inverted faces. The inversion effect demonstrates that face recognition depends on perceiving faces as integrated patterns rather than collections of independent features.

These findings support holistic processing theories of face recognition, demonstrating that the spatial arrangement of features within the standard face template is important for recognition. The consistent patterns across participants suggest these represent fundamental properties of human face processing rather than learned strategies. Studies of prosopagnosia further support this interpretation, as individuals with face recognition deficits often show reduced or absent inversion effects, suggesting their impairment involves disrupted configural processing.

The face inversion paradigm exemplifies how simple experimental manipulations can reveal fundamental cognitive mechanisms. Its elegant design allows researchers to isolate configural processing while maintaining experimental control, making it a model for investigating specialized perceptual systems. The paradigm continues to generate insights into face processing, including how expertise develops and how different types of facial information contribute to recognition across the lifespan.

24.1.6 Further Reading

Maurer, D., Le Grand, R., & Mondloch, C. J. (2002). The many faces of configural processing. Trends in Cognitive Sciences, 6, 255-260.

McKone, E., & Yovel, G. (2009). Why does picture-plane inversion sometimes dissociate perception of features and spacing in faces, and sometimes not? Toward a new theory of holistic processing. Psychonomic Bulletin & Review, 16(5), 778-797.

Rossion, B. (2008). Picture-plane inversion leads to qualitative changes of face perception. Acta Psychologica, 128(2), 274-289.

Yin, R. K. (1969). Looking at upside-down faces. Journal of Experimental Psychology, 81(1), 141-145.

Young, A. W., & Burton, A. M. (2018). Are we face experts? Trends in Cognitive Sciences, 22, 100-110.

24.2 Program a Face Inversion Paradigm

In this lab, we will program the classic face inversion memory task comparing memory for faces to memory for houses. The task is a recognition task, which requires participants to study a list of images, then in a test phase, participants see an old image and a new image, and they have to decide which is the old image.

Half the images will be presented upside down and half rightside up. The orientation for each particular image is repeated in the study and test phases.

In the lab folder, we have the usual boilerplate files in addition to folders with images that we will use for our experiment.

πŸ“‚ L11
β”œβ”€β”€  πŸ“„ index.html
β”œβ”€β”€  πŸ“„ exp.js
β”œβ”€β”€  πŸ“„ style.css
β”œβ”€β”€  πŸ“‚ images
β”‚     β”œβ”€β”€  πŸ“‚ faces
β”‚     β”œβ”€β”€  πŸ“‚ houses
β”‚     └──  πŸ“‚ cats
└──  πŸ“‚ jspsych

24.2.1 Initiate JsPsych

Let’s begin by initializing jsPsych with the basic setup we typically need. In the HTML index page, all necessary files have been linked: the jsPsych core files, our exp.js file, our style.css file, and the plugins required for this experiment.

I’ve also added a welcome screen and our save data trial at the end.

 <!DOCTYPE html>
<html>
<head>
    <title>Lab 11: Face Perception</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-instructions.js"></script>
    <script src="jspsych/plugin-preload.js"></script>
    <script src="jspsych/plugin-html-keyboard-response.js"></script>
    <script src="jspsych/plugin-html-button-response.js"></script>

    <!-- custom CSS -->
    <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 
// ============================================
// Initiate jsPsych
// ============================================

const jsPsych = initJsPsych(); 

// ============================================
// Welcome
// ============================================

let welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: "Welcome to the experiment! Press the space bar to begin.",
  choices: " "
}

// ============================================
// Savd Data Trial
// ============================================

const saveData = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `
    <div style="text-align: center;">
      <p>Experiment complete!</p>
      <p>Click the button below to save your data locally:</p>
      <button id="save-btn" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">
        Click here to save the data locally
      </button>
    </div>
  `,
  choices: "NO_KEYS",
  trial_duration: null,
  on_load: function() {
    document.getElementById("save-btn").addEventListener("click", function() {
      jsPsych.data.get().localSave("csv", "faceInversion_data.csv");
    });
  },
  data: {
    phase: "save data trial"
  }
};

// ============================================
// Run jsPsych 
// ============================================

jsPsych.run([
  welcome,
  saveData
]); 
  
Live JsPsych Demo Click inside the demo to activate demo

24.2.2 Add Study Phase

Let’s construct a short study phase with two images in our timeline variables: a right-side up face and an upside down house.

We’ll need to control the CSS of the images to be able to rotate them and make them upside down. Unfortunately, the image plugins don’t give us enough control for that, so instead, we’ll use the HTML plugin and write a function to display our image in its correct orientation according to the timeline variables.

I’ve also set some other base CSS for the images to ensure they’re not too wide or tall and added a filter to make them all black and white.

 <!DOCTYPE html>
<html>
<head>
    <title>Lab 8: Cognitive Control</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-instructions.js"></script>
    <script src="jspsych/plugin-preload.js"></script>
    <script src="jspsych/plugin-html-keyboard-response.js"></script>
    <script src="jspsych/plugin-html-button-response.js"></script>

    <!-- custom CSS -->
    <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 
// ============================================
// Initiate jsPsych
// ============================================

const jsPsych = initJsPsych(); 

// ============================================
// Welcome
// ============================================

let welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: "Welcome to the experiment! Press the space bar to begin.",
  choices: " "
}


// ============================================
// Study Phase
// ============================================

let study_instructions = {
  type: jsPsychInstructions,
  pages: [
    "Welcome to the study phase.",
    "You will see a series of faces and houses.",
    "Please pay attention to each image and try to memorize them.",
    "Press Next to begin."
  ],
  show_clickable_nav: true,
  key_forward: " "
};

let study_trials = {
  timeline: [
      {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: `<p style="font-size: 60px;">+</p>`,
        choices: "NO_KEYS",
        trial_duration: 500
      },
      {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: function(){
            return `<img class="base_image ${jsPsych.evaluateTimelineVariable("image_orientation")}"   src=${jsPsych.evaluateTimelineVariable("old_image_src")} >`
        },
        choices: "NO_KEYS",
        trial_duration: 1000
      }
  ],
  timeline_variables: [
    {old_image_src: "images/faces/face_1.jpg", image_orientation: "up"},
    {old_image_src: "images/houses/house_1.jpg", image_orientation: "down"}
  ],
  randomize_order: true
}

let study_phase = {
    timeline: [
      study_instructions,
      study_trials
    ]
}

// ============================================
// Savd Data Trial
// ============================================

const saveData = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `
    <div style="text-align: center;">
      <p>Experiment complete!</p>
      <p>Click the button below to save your data locally:</p>
      <button id="save-btn" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">
        Click here to save the data locally
      </button>
    </div>
  `,
  choices: "NO_KEYS",
  trial_duration: null,
  on_load: function() {
    document.getElementById("save-btn").addEventListener("click", function() {
      jsPsych.data.get().localSave("csv", "faceInversion_data.csv");
    });
  },
  data: {
    phase: "save data trial"
  }
};

// ============================================
// Run jsPsych 
// ============================================

jsPsych.run([
  welcome,
  study_phase,
  saveData
]); 
 .base_image {
    max-width: 250px;
    max-height: 250px;
    filter: grayscale(100%);
  }
  
  .up {
    transform: rotate(0deg);
  }
  
  .down {
    transform: rotate(180deg);
  } 
Live JsPsych Demo Click inside the demo to activate demo

24.2.3 Add Test Phase

Let’s again construct a short version of the test phase with only a couple of images. For the test phase, we’ll need to think a bit more carefully about what we need.

  1. We’ll need both the old and new image in our timeline variables.
  2. In the test phase, two images are presented side-by-side and participants indicate which one is the OLD image. We’ll use the HTML button response plugin and construct our HTML for the two images ourselves.
  3. We also don’t want to always put the correct response in the same location on every trial. We’ll need some logic to determine whether the β€˜old’ image goes on the left or the right based on another timeline variable.
 <!DOCTYPE html>
<html>
<head>
    <title>Lab 11: Face Perception</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-instructions.js"></script>
    <script src="jspsych/plugin-preload.js"></script>
    <script src="jspsych/plugin-html-keyboard-response.js"></script>
    <script src="jspsych/plugin-html-button-response.js"></script>

    <!-- custom CSS -->
    <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 
// ============================================
// Initiate jsPsych
// ============================================

const jsPsych = initJsPsych(); 

// ============================================
// Welcome
// ============================================

let welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: "Welcome to the experiment! Press the space bar to begin.",
  choices: " "
}

// ============================================
// Study Phase
// ============================================

let study_instructions = {
  type: jsPsychInstructions,
  pages: [
    "Welcome to the study phase.",
    "You will see a series of faces and houses.",
    "Please pay attention to each image and try to memorize them.",
    "Press Next to begin."
  ],
  show_clickable_nav: true,
  key_forward: " "
};

let study_trials = {
  timeline: [
      {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: `<p style="font-size: 60px;">+</p>`,
        choices: "NO_KEYS",
        trial_duration: 500
      },
      {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: function(){
            return `<img class="base_image ${jsPsych.evaluateTimelineVariable("image_orientation")}"   src=${jsPsych.evaluateTimelineVariable("old_image_src")} >`
        },
        choices: "NO_KEYS",
        trial_duration: 1000
      }
  ],
  timeline_variables: [
    {old_image_src: "images/faces/face_1.jpg", image_orientation: "up"},
    {old_image_src: "images/houses/house_1.jpg", image_orientation: "down"}
  ],
  randomize_order: true
}

let study_phase = {
    timeline: [
      study_instructions,
      study_trials
    ]
}

// ============================================
// Test Phase
// ============================================

let test_instructions = {
    type: jsPsychInstructions,
    pages: [
      "That completes the study phase. You will now complete the memory test phase",
      "You will see two images side-by-side.",
      "One image is from the study phase (OLD) and one is new (NEW).",
      "Your task is to identify the OLD image by clicking either the Left Image or Right Image button.",
      "Press NEXT to begin."
    ],
    show_clickable_nav: true
  };

let test_trials =  {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: `<p style="font-size: 60px;">+</p>`,
      choices: "NO_KEYS",
      trial_duration: 500
    },
    {
      type: jsPsychHtmlButtonResponse,
      stimulus: function() {
        let old_src = jsPsych.evaluateTimelineVariable("old_image_src");
        let new_src = jsPsych.evaluateTimelineVariable("new_image_src");
        let image_orientation = jsPsych.evaluateTimelineVariable("image_orientation");
        let old_location =  jsPsych.evaluateTimelineVariable("correct_location");

        let output
        
        if(old_location == "left"){
          output = `<div style="display: flex; justify-content: center; gap: 50px;">
           <img class="base_image ${image_orientation}" src="${old_src}">
           <img class="base_image ${image_orientation}" src="${new_src}">
          </div>`
        } else {
          output = `<div style="display: flex; justify-content: center; gap: 50px;">
           <img class="base_image ${image_orientation}" src="${new_src}">
           <img class="base_image ${image_orientation}" src="${old_src}">
          </div>`
        }

        return output
      },
      prompt: "Which image is an OLD (studied) image?",
      choices: ["Left Image", "Right Image"]
    }
  ],
  timeline_variables: [
      { 
        old_image_src: "images/faces/face_1.jpg",
        new_image_src: "images/faces/face_2.jpg",
        image_orientation: "up",
        correct_location: "left"
      },
      { 
        old_image_src: "images/houses/house_1.jpg",
        new_image_src: "images/houses/house_2.jpg",
        image_orientation: "down",
        correct_location: "right"
      },
  ],
  randomize_order: true
};

let test_phase = {
    timeline: [
      test_instructions,
      test_trials
    ]
}

// ============================================
// Savd Data Trial
// ============================================

const saveData = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `
    <div style="text-align: center;">
      <p>Experiment complete!</p>
      <p>Click the button below to save your data locally:</p>
      <button id="save-btn" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">
        Click here to save the data locally
      </button>
    </div>
  `,
  choices: "NO_KEYS",
  trial_duration: null,
  on_load: function() {
    document.getElementById("save-btn").addEventListener("click", function() {
      jsPsych.data.get().localSave("csv", "faceInversion_data.csv");
    });
  },
  data: {
    phase: "save data trial"
  }
};

// ============================================
// Run jsPsych 
// ============================================

jsPsych.run([
  welcome,
  study_phase,
  test_phase,
  saveData
]); 
 
  .base_image {
    max-width: 250px;
    max-height: 250px;
    filter: grayscale(100%);
  }
  
  .up {
    transform: rotate(0deg);
  }
  
  .down {
    transform: rotate(180deg);
  }
 
Live JsPsych Demo Click inside the demo to activate demo

24.2.4 Randomize Stimulus Selection and Assignments

Before deciding how to randomize our stimulus assignments, we need to have a good idea about what we want to end up with.

  1. We have 50 face and 50 house images. We only 20 of each, 10 for the old images and 10 for the new images. Therefore, we need to randomly select 20 images from the full 50.
  2. Images need to be randomly split into an OLD set and a NEW set.
  3. OLD and NEW Images need to be randomly split into upside down or right-side up.
  4. The OLD and NEW images are presented together during the test phase, so they need to be paired together
  5. When we present them together, we need to know which one goes on the left versus right side, to know which is the correct one and to make sure the old image isn’t always on the same side.

Let’s make a mock single timeline variable that contains all these labels:

// what type of image is it?
image_type: "face" // or "house"

// Two images paired together, with a src label 
// these images need to be randomly assigned
old_image_src: "images/faces/face_1.jpg",
new_image_src: "images/faces/face_2.jpg"

// Orientation needs to be labelled too
// Half the images need to be up, half down
image_orientation: "up" // or "down"

// The location of the old image during test
// This also needs to be randomly assigned
correct_location: "left" // or "right

Now, we’ll need to construct our timeline_variables array so that each object inside it contains all those labels, but with proper randomization:

let stims = [
  {
    image_type: "face", 
    old_image_src: "images/faces/face_1.jpg",
    new_image_src: "images/faces/face_2.jpg"
    image_orientation: "up", 
    correct_location: "left" 
  },
  
  {
    image_type: "face",
    old_image_src: "images/faces/face_3.jpg",
    new_image_src: "images/faces/face_4.jpg"
    image_orientation: "down", 
    correct_location: "left" 
  },

]

Although we have two phases: study phase and a test phase, we can actually use the same timeline variables array for both since it contains all the info we need for both trials.

Before we put the code into our exp.js file, I’m going to walk through, step-by-step how I randomized the image assignments.

1. Setup our variables

First, we need our list of images. Since the filenames are numbered (e.g., faces_1, faces_2, faces_3 …), we can just use the numbers 1 to 50 randomly select images. We’ll still need to make our list of numbers though.

I’ll also create an empty array for the timeline variables called stims

// ============================================
// Select & Assign Images
// ============================================

// the images are numbered 1 to 50 (e.g., faces_34.jpg)
// create a list of numbers 1 to 50
let face_numbers = []
let house_numbers = []
for (let i = 1; i <= 50; i++) {
  face_numbers.push(i);
  house_numbers.push(i)
}

// variable for our timeline_variables array
let stims = []

2. Randomly select 20 images split into conditions

Now that I have a list of numbers from 1 to 50, I can randomize the order and select out 5 per condition. Using slice and selecting 0-5, 6-10, etc. will make sure I use each number only once.

// ============================================
// Select & Assign Images
// ============================================

// the images are numbered 1 to 50 (e.g., faces_34.jpg)
// create a list of numbers 1 to 50
let face_numbers = []
let house_numbers = []
for (let i = 1; i <= 50; i++) {
  face_numbers.push(i);
  house_numbers.push(i)
}

// variable for our timeline_variables array
let stims = []

// Face Images
// 1. Randomize the order
face_numbers = jsPsych.randomization.shuffle(face_numbers)

// 2. Split into four sets
let old_faces_down = face_numbers.slice(0,5)
let old_faces_up = face_numbers.slice(5,10)
let new_faces_down = face_numbers.slice(10,15)
let new_faces_up = face_numbers.slice(15,20)

3. Pair the old/new images together and add to an array

Now we loop through the old_faces_down and old_faces_up arrays and create an object that has an old image and a new image with proper labels.

I also need the image source location with the filename. I’m constructing that from the number and pasting the text together: "images/faces/face_" + old_faces_up[i] + ".jpg".

Remember that old_faces_[0] will be whatever number is in the first position of the array, old_faces_[1] will be whatever number is in the second position and so on. Since we randomize the order it can be any number from 1 to 50.

// ============================================
// Select & Assign Images
// ============================================

// the images are numbered 1 to 50 (e.g., faces_34.jpg)
// create a list of numbers 1 to 50
let face_numbers = []
let house_numbers = []
for (let i = 1; i <= 50; i++) {
  face_numbers.push(i);
  house_numbers.push(i)
}

// variable for our timeline_variables array
let stims = []

// Face Images
// 1. Randomize the order
face_numbers = jsPsych.randomization.shuffle(face_numbers)

// 2. Split into four sets
let old_faces_down = face_numbers.slice(0,5)
let old_faces_up = face_numbers.slice(5,10)
let new_faces_down = face_numbers.slice(10,15)
let new_faces_up = face_numbers.slice(15,20)

// 3. Create object with image pair and add to stims
for (let i = 0; i < old_faces_up.length; i++) {
  stims.push(
      {
        old_image_number: old_faces_up[i],
        old_image_src: "images/faces/face_" + old_faces_up[i] + ".jpg",
        new_image_number:  new_faces_up[i],
        new_image_src: "images/faces/face_" + new_faces_up[i] + ".jpg",
        image_type: "face",
        image_orientation: "up"
      }
  );
}
  
for (let i = 0; i < old_faces_down.length; i++) {
  stims.push(
      {
        old_image_number: old_faces_down[i],
        old_image_src: "images/faces/face_" + old_faces_down[i] + ".jpg",
        new_image_number:  new_faces_down[i],
        new_image_src: "images/faces/face_" + new_faces_down[i] + ".jpg",
        image_type: "face",
        image_orientation: "down"
      }
  );
}

4. Randomly select left or right as the old image location

Finally, I need to set the test locations for the images. I’m just going to randomly select the old image location using a jsPsych function.

// ============================================
// Select & Assign Images
// ============================================

// the images are numbered 1 to 50 (e.g., faces_34.jpg)
// create a list of numbers 1 to 50
let face_numbers = []
let house_numbers = []
for (let i = 1; i <= 50; i++) {
  face_numbers.push(i);
  house_numbers.push(i)
}

// variable for our timeline_variables array
let stims = []

// Face Images
// 1. Randomize the order
face_numbers = jsPsych.randomization.shuffle(face_numbers)

// 2. Split into four sets
let old_faces_down = face_numbers.slice(0,5)
let old_faces_up = face_numbers.slice(5,10)
let new_faces_down = face_numbers.slice(10,15)
let new_faces_up = face_numbers.slice(15,20)

// 3. Create object with image pair and add to stims
for (let i = 0; i < old_faces_up.length; i++) {
  stims.push(
      {
        old_image_number: old_faces_up[i],
        old_image_src: "images/faces/face_" + old_faces_up[i] + ".jpg",
        new_image_number:  new_faces_up[i],
        new_image_src: "images/faces/face_" + new_faces_up[i] + ".jpg",
        image_type: "face",
        image_orientation: "up",
        correct_location:    jsPsych.randomization.sampleWithoutReplacement(["left", "right"], 1)[0]
      }
  );
}
  
for (let i = 0; i < old_faces_down.length; i++) {
  stims.push(
      {
        old_image_number: old_faces_down[i],
        old_image_src: "images/faces/face_" + old_faces_down[i] + ".jpg",
        new_image_number:  new_faces_down[i],
        new_image_src: "images/faces/face_" + new_faces_down[i] + ".jpg",
        image_type: "face",
        image_orientation: "down",
        correct_location:    jsPsych.randomization.sampleWithoutReplacement(["left", "right"], 1)[0]
      }
  );
}

5. Add to our code

Now let’s add that code to our experiment and replace our timeline_variables with stims.

 <!DOCTYPE html>
<html>
<head>
    <title>Lab 11: Face Perception</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-instructions.js"></script>
    <script src="jspsych/plugin-preload.js"></script>
    <script src="jspsych/plugin-html-keyboard-response.js"></script>
    <script src="jspsych/plugin-html-button-response.js"></script>

    <!-- custom CSS -->
    <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 
// ============================================
// Initiate jsPsych
// ============================================

const jsPsych = initJsPsych(); 

// ============================================
// Welcome
// ============================================

let welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: "Welcome to the experiment! Press the space bar to begin.",
  choices: " "
}

// ============================================
// Select & Assign Images
// ============================================

// the images are numbered 1 to 50 (e.g., faces_34.jpg)
// create a list of numbers 1 to 50
let face_numbers = []
let house_numbers = []
for (let i = 1; i <= 50; i++) {
  face_numbers.push(i);
  house_numbers.push(i)
}

// variable for our timeline_variables array
let stims = []

// Face Images
// 1. Randomize the order
face_numbers = jsPsych.randomization.shuffle(face_numbers)

// 2. Split into four sets
let old_faces_down = face_numbers.slice(0,5)
let old_faces_up = face_numbers.slice(5,10)
let new_faces_down = face_numbers.slice(10,15)
let new_faces_up = face_numbers.slice(15,20)

// 3. Create object with image pair and add to stims
for (let i = 0; i < old_faces_up.length; i++) {
  stims.push(
    {
    old_image_number: old_faces_up[i],
    old_image_src: "images/faces/face_" + old_faces_up[i] + ".jpg",
    new_image_number:  new_faces_up[i],
    new_image_src: "images/faces/face_" + new_faces_up[i] + ".jpg",
    image_type: "face",
    image_orientation: "up",
    correct_location: jsPsych.randomization.sampleWithoutReplacement(["left", "right"], 1)[0]
    }
  );
}

for (let i = 0; i < old_faces_down.length; i++) {
  stims.push({
    old_image_number: old_faces_down[i],
    old_image_src: "images/faces/face_" + old_faces_down[i] + ".jpg",
    new_image_number:  new_faces_down[i],
    new_image_src: "images/faces/face_" + new_faces_down[i] + ".jpg",
    image_type: "face",
    image_orientation: "down",
    correct_location: jsPsych.randomization.sampleWithoutReplacement(["left", "right"], 1)[0]
  });
}

// ============================================
// Study Phase
// ============================================

let study_instructions = {
  type: jsPsychInstructions,
  pages: [
    "Welcome to the study phase.",
    "You will see a series of faces and houses.",
    "Please pay attention to each image and try to memorize them.",
    "Press Next to begin."
  ],
  show_clickable_nav: true,
  key_forward: " "
};

let study_trials = {
  timeline: [
      {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: `<p style="font-size: 60px;">+</p>`,
        choices: "NO_KEYS",
        trial_duration: 500
      },
      {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: function(){
            return `<img class="base_image ${jsPsych.evaluateTimelineVariable("image_orientation")}"   src=${jsPsych.evaluateTimelineVariable("old_image_src")} >`
        },
        choices: "NO_KEYS",
        trial_duration: 1000
      }
  ],
  timeline_variables: stims,
  randomize_order: true
}

let study_phase = {
    timeline: [
      study_instructions,
      study_trials
    ]
}

// ============================================
// Test Phase
// ============================================

let test_instructions = {
    type: jsPsychInstructions,
    pages: [
      "That completes the study phase. You will now complete the memory test phase",
      "You will see two images side-by-side.",
      "One image is from the study phase (OLD) and one is new (NEW).",
      "Your task is to identify the OLD image by clicking either the Left Image or Right Image button.",
      "Press NEXT to begin."
    ],
    show_clickable_nav: true
  };

let test_trials =  {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: `<p style="font-size: 60px;">+</p>`,
      choices: "NO_KEYS",
      trial_duration: 500
    },
    {
      type: jsPsychHtmlButtonResponse,
      stimulus: function() {
        let old_src = jsPsych.evaluateTimelineVariable("old_image_src");
        let new_src = jsPsych.evaluateTimelineVariable("new_image_src");
        let image_orientation = jsPsych.evaluateTimelineVariable("image_orientation");
        let old_location =  jsPsych.evaluateTimelineVariable("correct_location");

        let output
        
        if(old_location == "left"){
          output = `<div style="display: flex; justify-content: center; gap: 50px;">
           <img class="base_image ${image_orientation}" src="${old_src}">
           <img class="base_image ${image_orientation}" src="${new_src}">
          </div>`
        } else {
          output = `<div style="display: flex; justify-content: center; gap: 50px;">
           <img class="base_image ${image_orientation}" src="${new_src}">
           <img class="base_image ${image_orientation}" src="${old_src}">
          </div>`
        }

        return output
      },
      prompt: "Which image is an OLD (studied) image?",
      choices: ["Left Image", "Right Image"]
    }
  ],
  timeline_variables: stims,
  randomize_order: true
};

let test_phase = {
    timeline: [
      test_instructions,
      test_trials
    ]
}

// ============================================
// Savd Data Trial
// ============================================

const saveData = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `
    <div style="text-align: center;">
      <p>Experiment complete!</p>
      <p>Click the button below to save your data locally:</p>
      <button id="save-btn" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">
        Click here to save the data locally
      </button>
    </div>
  `,
  choices: "NO_KEYS",
  trial_duration: null,
  on_load: function() {
    document.getElementById("save-btn").addEventListener("click", function() {
      jsPsych.data.get().localSave("csv", "faceInversion_data.csv");
    });
  },
  data: {
    phase: "save data trial"
  }
};

// ============================================
// Run jsPsych 
// ============================================

jsPsych.run([
  welcome,
  study_phase,
  test_phase,
  saveData
]); 
 
  .base_image {
    max-width: 250px;
    max-height: 250px;
    filter: grayscale(100%);
  }
  
  .up {
    transform: rotate(0deg);
  }
  
  .down {
    transform: rotate(180deg);
  }
 
Live JsPsych Demo Click inside the demo to activate demo

24.2.5 Repeat the randomization for house images

We can copy-paste our face code to repeat it for the house images. In fact, everything stays exactly the same we just replace β€˜face’ with β€˜house’! And since the code adds objects to our stims array, it will just add the list of randomized house images to the list of face images.

 <!DOCTYPE html>
<html>
<head>
    <title>Lab 11: Face Perception</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-instructions.js"></script>
    <script src="jspsych/plugin-preload.js"></script>
    <script src="jspsych/plugin-html-keyboard-response.js"></script>
    <script src="jspsych/plugin-html-button-response.js"></script>

    <!-- custom CSS -->
    <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 
// ============================================
// Initiate jsPsych
// ============================================

const jsPsych = initJsPsych(); 

// ============================================
// Welcome
// ============================================

let welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: "Welcome to the experiment! Press the space bar to begin.",
  choices: " "
}

// ============================================
// Select & Assign Images
// ============================================

// the images are numbered 1 to 50 (e.g., faces_34.jpg)
// create a list of numbers 1 to 50
let face_numbers = []
let house_numbers = []
for (let i = 1; i <= 50; i++) {
  face_numbers.push(i);
  house_numbers.push(i)
}

// variable for our timeline_variables array
let stims = []

// Face Images
// 1. Randomize the order
face_numbers = jsPsych.randomization.shuffle(face_numbers)

// 2. Split into four sets
let old_faces_down = face_numbers.slice(0,5)
let old_faces_up = face_numbers.slice(5,10)
let new_faces_down = face_numbers.slice(10,15)
let new_faces_up = face_numbers.slice(15,20)

// 3. Create object with image pair and add to stims
for (let i = 0; i < old_faces_up.length; i++) {
  stims.push(
    {
    old_image_number: old_faces_up[i],
    old_image_src: "images/faces/face_" + old_faces_up[i] + ".jpg",
    new_image_number:  new_faces_up[i],
    new_image_src: "images/faces/face_" + new_faces_up[i] + ".jpg",
    image_type: "face",
    image_orientation: "up",
    correct_location:    jsPsych.randomization.sampleWithoutReplacement(["left", "right"], 1)[0]
    }
  );
}

for (let i = 0; i < old_faces_down.length; i++) {
  stims.push({
    old_image_number: old_faces_down[i],
    old_image_src: "images/faces/face_" + old_faces_down[i] + ".jpg",
    new_image_number:  new_faces_down[i],
    new_image_src: "images/faces/face_" + new_faces_down[i] + ".jpg",
    image_type: "face",
    image_orientation: "down",
    correct_location:    jsPsych.randomization.sampleWithoutReplacement(["left", "right"], 1)[0]
  });
}

// House Images
// 1. randomize number order
house_numbers = jsPsych.randomization.shuffle(house_numbers)

// 2. split into four sets
let old_houses_down = face_numbers.slice(0,5)
let old_houses_up = face_numbers.slice(5,10)
let new_houses_down = face_numbers.slice(10,15)
let new_houses_up = face_numbers.slice(15,20)

// 3. Create object with image pair and add to stims
for (let i = 0; i < old_houses_up.length; i++) {
  stims.push(
    {
      old_image_number: old_houses_up[i],
      old_image_src: "images/houses/house_" + old_houses_up[i] + ".jpg",
      new_image_number:  new_houses_up[i],
      new_image_src: "images/houses/house_" + new_houses_up[i] + ".jpg",
      image_type: "house",
      image_orientation: "up",
      correct_location:    jsPsych.randomization.sampleWithoutReplacement(["left", "right"], 1)[0]
    }
  );
}

for (let i = 0; i < old_houses_down.length; i++) {
  stims.push(
    {
      old_image_number: old_houses_down[i],
      old_image_src: "images/houses/house_" + old_houses_down[i] + ".jpg",
      new_image_number:  new_houses_down[i],
      new_image_src: "images/houses/house_" + new_houses_down[i] + ".jpg",
      image_type: "house",
      image_orientation: "down",
      correct_location:    jsPsych.randomization.sampleWithoutReplacement(["left", "right"], 1)[0]
    }
  );
}

// ============================================
// Study Phase
// ============================================

let study_instructions = {
  type: jsPsychInstructions,
  pages: [
    "Welcome to the study phase.",
    "You will see a series of faces and houses.",
    "Please pay attention to each image and try to memorize them.",
    "Press Next to begin."
  ],
  show_clickable_nav: true,
  key_forward: " "
};

let study_trials = {
  timeline: [
      {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: `<p style="font-size: 60px;">+</p>`,
        choices: "NO_KEYS",
        trial_duration: 500
      },
      {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: function(){
            return `<img class="base_image ${jsPsych.evaluateTimelineVariable("image_orientation")}"   src=${jsPsych.evaluateTimelineVariable("old_image_src")} >`
        },
        choices: "NO_KEYS",
        trial_duration: 1000
      }
  ],
  timeline_variables: stims,
  randomize_order: true
}

let study_phase = {
    timeline: [
      study_instructions,
      study_trials
    ]
}

// ============================================
// Test Phase
// ============================================

let test_instructions = {
    type: jsPsychInstructions,
    pages: [
      "That completes the study phase. You will now complete the memory test phase",
      "You will see two images side-by-side.",
      "One image is from the study phase (OLD) and one is new (NEW).",
      "Your task is to identify the OLD image by clicking either the Left Image or Right Image button.",
      "Press NEXT to begin."
    ],
    show_clickable_nav: true
  };

let test_trials =  {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: `<p style="font-size: 60px;">+</p>`,
      choices: "NO_KEYS",
      trial_duration: 500
    },
    {
      type: jsPsychHtmlButtonResponse,
      stimulus: function() {
        let old_src = jsPsych.evaluateTimelineVariable("old_image_src");
        let new_src = jsPsych.evaluateTimelineVariable("new_image_src");
        let image_orientation = jsPsych.evaluateTimelineVariable("image_orientation");
        let old_location =  jsPsych.evaluateTimelineVariable("correct_location");

        let output
        
        if(old_location == "left"){
          output = `<div style="display: flex; justify-content: center; gap: 50px;">
           <img class="base_image ${image_orientation}" src="${old_src}">
           <img class="base_image ${image_orientation}" src="${new_src}">
          </div>`
        } else {
          output = `<div style="display: flex; justify-content: center; gap: 50px;">
           <img class="base_image ${image_orientation}" src="${new_src}">
           <img class="base_image ${image_orientation}" src="${old_src}">
          </div>`
        }

        return output
      },
      prompt: "Which image is an OLD (studied) image?",
      choices: ["Left Image", "Right Image"]
    }
  ],
  timeline_variables: stims,
  randomize_order: true
};

let test_phase = {
    timeline: [
      test_instructions,
      test_trials
    ]
}

// ============================================
// Savd Data Trial
// ============================================

const saveData = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `
    <div style="text-align: center;">
      <p>Experiment complete!</p>
      <p>Click the button below to save your data locally:</p>
      <button id="save-btn" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">
        Click here to save the data locally
      </button>
    </div>
  `,
  choices: "NO_KEYS",
  trial_duration: null,
  on_load: function() {
    document.getElementById("save-btn").addEventListener("click", function() {
      jsPsych.data.get().localSave("csv", "faceInversion_data.csv");
    });
  },
  data: {
    phase: "save data trial"
  }
};

// ============================================
// Run jsPsych 
// ============================================

jsPsych.run([
  welcome,
  study_phase,
  test_phase,
  saveData
]); 
 
  .base_image {
    max-width: 250px;
    max-height: 250px;
    filter: grayscale(100%);
  }
  
  .up {
    transform: rotate(0deg);
  }
  
  .down {
    transform: rotate(180deg);
  }
 
Live JsPsych Demo Click inside the demo to activate demo

24.2.6 Add Preload

Finally, we’ll need to preload our images. We’ll also do this in a programmatic way, looping through our stims array and copying the image locations to a new array. That way we’re only preloading the images we’re using and not any of the other 50 images.

 <!DOCTYPE html>
<html>
<head>
    <title>Lab 11: Face Perception</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-instructions.js"></script>
    <script src="jspsych/plugin-preload.js"></script>
    <script src="jspsych/plugin-html-keyboard-response.js"></script>
    <script src="jspsych/plugin-html-button-response.js"></script>

    <!-- custom CSS -->
    <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 
// ============================================
// Initiate jsPsych
// ============================================

const jsPsych = initJsPsych(); 

// ============================================
// Welcome
// ============================================

let welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: "Welcome to the experiment! Press the space bar to begin.",
  choices: " "
}

// ============================================
// Select & Assign Images
// ============================================

// the images are numbered 1 to 50 (e.g., faces_34.jpg)
// create a list of numbers 1 to 50
let face_numbers = []
let house_numbers = []
for (let i = 1; i <= 50; i++) {
  face_numbers.push(i);
  house_numbers.push(i)
}

// variable for our timeline_variables array
let stims = []

// Face Images
// 1. Randomize the order
face_numbers = jsPsych.randomization.shuffle(face_numbers)

// 2. Split into four sets
let old_faces_down = face_numbers.slice(0,5)
let old_faces_up = face_numbers.slice(5,10)
let new_faces_down = face_numbers.slice(10,15)
let new_faces_up = face_numbers.slice(15,20)

// 3. Create object with image pair and add to stims
for (let i = 0; i < old_faces_up.length; i++) {
  stims.push(
    {
    old_image_number: old_faces_up[i],
    old_image_src: "images/faces/face_" + old_faces_up[i] + ".jpg",
    new_image_number:  new_faces_up[i],
    new_image_src: "images/faces/face_" + new_faces_up[i] + ".jpg",
    image_type: "face",
    image_orientation: "up",
    correct_location:    jsPsych.randomization.sampleWithoutReplacement(["left", "right"], 1)[0]
    }
  );
}

for (let i = 0; i < old_faces_down.length; i++) {
  stims.push({
    old_image_number: old_faces_down[i],
    old_image_src: "images/faces/face_" + old_faces_down[i] + ".jpg",
    new_image_number:  new_faces_down[i],
    new_image_src: "images/faces/face_" + new_faces_down[i] + ".jpg",
    image_type: "face",
    image_orientation: "down",
    correct_location:    jsPsych.randomization.sampleWithoutReplacement(["left", "right"], 1)[0]
  });
}

// House Images
// 1. randomize number order
house_numbers = jsPsych.randomization.shuffle(house_numbers)

// 2. split into four sets
let old_houses_down = face_numbers.slice(0,5)
let old_houses_up = face_numbers.slice(5,10)
let new_houses_down = face_numbers.slice(10,15)
let new_houses_up = face_numbers.slice(15,20)

// 3. Create object with image pair and add to stims
for (let i = 0; i < old_houses_up.length; i++) {
  stims.push(
    {
      old_image_number: old_houses_up[i],
      old_image_src: "images/houses/house_" + old_houses_up[i] + ".jpg",
      new_image_number:  new_houses_up[i],
      new_image_src: "images/houses/house_" + new_houses_up[i] + ".jpg",
      image_type: "house",
      image_orientation: "up",
      correct_location:    jsPsych.randomization.sampleWithoutReplacement(["left", "right"], 1)[0]
    }
  );
}

for (let i = 0; i < old_houses_down.length; i++) {
  stims.push({
    old_image_number: old_houses_down[i],
    old_image_src: "images/houses/house_" + old_houses_down[i] + ".jpg",
    new_image_number:  new_houses_down[i],
    new_image_src: "images/houses/house_" + new_houses_down[i] + ".jpg",
    image_type: "house",
    image_orientation: "down",
    correct_location:    jsPsych.randomization.sampleWithoutReplacement(["left", "right"], 1)[0]
  });
}

// ============================================
// Preload
// ============================================

// variable for list of images to preload
let image_list = []

// add only the images we are using from stims
for(let i = 0; i < stims.length; i++){
  image_list.push(stims[i].old_image_src)
  image_list.push(stims[i].new_image_src)
}

let preload = {
  type: jsPsychPreload,
  images: image_list,
  show_progress_bar: true,
  continue_after_error: false
}

// ============================================
// Study Phase
// ============================================

let study_instructions = {
  type: jsPsychInstructions,
  pages: [
    "Welcome to the study phase.",
    "You will see a series of faces and houses.",
    "Please pay attention to each image and try to memorize them.",
    "Press Next to begin."
  ],
  show_clickable_nav: true,
  key_forward: " "
};


let study_trials = {
  timeline: [
      {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: `<p style="font-size: 60px;">+</p>`,
        choices: "NO_KEYS",
        trial_duration: 500
      },
      {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: function(){
            return `<img class="base_image ${jsPsych.evaluateTimelineVariable("image_orientation")}"   src=${jsPsych.evaluateTimelineVariable("old_image_src")} >`
        },
        choices: "NO_KEYS",
        trial_duration: 1000
      }
  ],
  timeline_variables: stims,
  randomize_order: true
}

let study_phase = {
    timeline: [
      study_instructions,
      study_trials
    ]
}

// ============================================
// Test Phase
// ============================================

let test_instructions = {
    type: jsPsychInstructions,
    pages: [
      "That completes the study phase. You will now complete the memory test phase",
      "You will see two images side-by-side.",
      "One image is from the study phase (OLD) and one is new (NEW).",
      "Your task is to identify the OLD image by clicking either the Left Image or Right Image button.",
      "Press NEXT to begin."
    ],
    show_clickable_nav: true
  };


let test_trials =  {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: `<p style="font-size: 60px;">+</p>`,
      choices: "NO_KEYS",
      trial_duration: 500
    },
    {
      type: jsPsychHtmlButtonResponse,
      stimulus: function() {
        let old_src = jsPsych.evaluateTimelineVariable("old_image_src");
        let new_src = jsPsych.evaluateTimelineVariable("new_image_src");
        let image_orientation = jsPsych.evaluateTimelineVariable("image_orientation");
        let old_location =  jsPsych.evaluateTimelineVariable("correct_location");

        let output
        
        if(old_location == "left"){
          output = `<div style="display: flex; justify-content: center; gap: 50px;">
           <img class="base_image ${image_orientation}" src="${old_src}">
           <img class="base_image ${image_orientation}" src="${new_src}">
          </div>`
        } else {
          output = `<div style="display: flex; justify-content: center; gap: 50px;">
           <img class="base_image ${image_orientation}" src="${new_src}">
           <img class="base_image ${image_orientation}" src="${old_src}">
          </div>`
        }

        return output
      },
      prompt: "Which image is an OLD (studied) image?",
      choices: ["Left Image", "Right Image"]
    }
  ],
  timeline_variables: stims,
  randomize_order: true
};


let test_phase = {
    timeline: [
      test_instructions,
      test_trials
    ]
}

// ============================================
// Savd Data Trial
// ============================================

const saveData = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `
    <div style="text-align: center;">
      <p>Experiment complete!</p>
      <p>Click the button below to save your data locally:</p>
      <button id="save-btn" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">
        Click here to save the data locally
      </button>
    </div>
  `,
  choices: "NO_KEYS",
  trial_duration: null,
  on_load: function() {
    document.getElementById("save-btn").addEventListener("click", function() {
      jsPsych.data.get().localSave("csv", "faceInversion_data.csv");
    });
  },
  data: {
    phase: "save data trial"
  }
};

// ============================================
// Run jsPsych 
// ============================================

jsPsych.run([
  welcome,
  preload,
  study_phase,
  test_phase,
  saveData
]); 
 
  .base_image {
    max-width: 250px;
    max-height: 250px;
    filter: grayscale(100%);
  }
  
  .up {
    transform: rotate(0deg);
  }
  
  .down {
    transform: rotate(180deg);
  }
 
Live JsPsych Demo Click inside the demo to activate demo

24.3 Stretch Goals

24.3.1 Add data labels

Our experiment is missing proper data labels. Let’s make sure we’ve added:

  1. all of our timeline_variables
  2. phase: instructions/study/test/save_data
  3. trial_part: fixation/image

24.3.2 Add cat faces

Let’s add another condition to our experiment: cat faces! Add another list of cat faces, randomly selected and paired together just like the face and house images. The cat faces are in a folder called images/cats/ and follow the same numbering scheme.