////////////////////////////////////////////////////
////////////////////////////////////////////////////
//// UTILITIES
////////////////////////////////////////////////////
////////////////////////////////////////////////////
/**
* Shuffles an array using a random number generator.
*
* @param {Array} array The array to be shuffled.
* @returns {Array} Returns shuffled array.
* @link https://www.geeksforgeeks.org/how-to-shuffle-an-array-using-javascript/
*/
function shuffleArray(array) {
for (var i = array.length - 1; i > 0; i--) {
// Generate random number
var j = Math.floor(Math.random() * (i + 1));
var temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
/**
* Gets the html content of the given file paths and performs replacements for given strings.
*
* @param {Object.<key, string>} slides_names A mapping of slides to html file paths.
* @param {Object.<string, string>} replacements Strings to be replaced in all html content and
* their replacements.
* @returns {Object.<key, string>} A mapping of slide names to html content.
*/
function prepare_html(slides_names, replacements) {
// Load the html files and replace placeholders
let slides_content = {};
Object.keys(slides_names).forEach((key) => {
const req = new XMLHttpRequest();
req.open("GET", slides_names[key], false);
req.send();
text = req.response;
Object.keys(replacements).forEach((key) => {
text =
'<div class="canvas">' +
text.replaceAll("$" + key + "$", replacements[key]) +
"</div>";
});
slides_content[key] = text;
});
return slides_content;
}
////////////////////////////////////////////////////
////////////////////////////////////////////////////
//// PRACTICE test
////////////////////////////////////////////////////
////////////////////////////////////////////////////
/**
* Creates a jsPsych timeline for subjects to practice the instructions.
*
* This timeline includes the following trials:
*
* -comboPracticeTrial
* -cumulativeResponsePercentage
*
* IF comboPracticeTrial FAILED : -another comboPracticeTrial
* -secondFeedback
*
* @returns {timeline} A jsPsych timeline containing the mentioned trials
*/
function practiceFeedBack() {
/**
* A practice trial for the subject to check if instructions are clear.
* @type {timeline}
*/
var comboPracticeTrial = {
timeline: [
{
type: jsPsychMootPlugin,
moot_parameters: {
doggelgaenger_col: condition.doppelgaenger_color_white
? "1,1,1"
: "0,0,0",
doggelgaenger_loc: condition.doppelgaenger_up
? "20,0,-10"
: "-20,0,-10",
doppelgaenger_target: condition.targetpattern_squares ? "SQ" : "TR",
go_notarget_left: jsPsych.timelineVariable('go_notarget_left'),
nogo_type1_notarget_left: jsPsych.timelineVariable('nogo_type1_notarget_left'),
go_target_left: jsPsych.timelineVariable("go_target_left"),
nogo_type1_notarget_right: jsPsych.timelineVariable("nogo_type1_notarget_right"),
go_notarget_right: jsPsych.timelineVariable("go_notarget_right"),
nogo_type2_notarget_left: jsPsych.timelineVariable('nogo_type2_notarget_left'),
go_target_right: jsPsych.timelineVariable("go_target_right"),
nogo_type2_notarget_right: jsPsych.timelineVariable('nogo_type2_notarget_right'),
go_target_right: jsPsych.timelineVariable("go_target_right"),
nogo_type1_target_left: jsPsych.timelineVariable("nogo_type1_target_left"),
go_notarget_right: jsPsych.timelineVariable("go_notarget_right"),
nogo_type1_target_right: jsPsych.timelineVariable("nogo_type1_target_right"),
go_target_left: jsPsych.timelineVariable("go_target_left"),
nogo_type2_target_left: jsPsych.timelineVariable('nogo_type2_target_left'),
go_notarget_left: jsPsych.timelineVariable('go_notarget_left'),
nogo_type2_target_right: jsPsych.timelineVariable('nogo_type2_target_right'),
doppelgaenger_anim_rec:experiment_config.record_doppelgaenger_animation,
other_anim_rec: experiment_config.record_other_animation,
},
extensions: [
{ type: jsPsychUnityExtension },
{
type: jsPsychExtensionMediapipeFacemesh,
params: {
record: false,
},
},
],
},
{
type: jsPsychHtmlSliderResponse,
stimulus:
"<p>What should you have done in response to the last video?</p>",
step: 33,
labels: [
"Turn my head left",
"Turn my head right",
"Do nothing",
"Press the space bar",
],
//prompt: "<p>Press right arrow key to move forward</p>",
on_finish: function (data) {
if (data.response == jsPsych.timelineVariable("correctAnswer")) {
data.correct = 1;
} else {
data.correct = 0;
}
},
},
{
type: jsPsychHtmlKeyboardResponse,
stimulus: function () {
var last_trial_correct = jsPsych.data
.get()
.last(1)
.values()[0].correct;
if (last_trial_correct) {
return "<p>Correct</p>"; // the parameter value has to be returned from the function
} else {
return "<p>Wrong</p>"; // the parameter value has to be returned from the function
}
},
trial_duration: 1000,
},
],
timeline_variables: [
{ go_notarget_left: 1, correctAnswer: 66 },
{ nogo_type1_notarget_left: 1, correctAnswer: 99 },
{ go_target_left: 1, correctAnswer: 0 },
{ nogo_type1_notarget_right: 1, correctAnswer: 99 },
{ go_notarget_right: 1, correctAnswer: 66 },
{ nogo_type2_notarget_left: 1, correctAnswer: 99 },
{ go_target_right: 1, correctAnswer: 33 },
{ nogo_type2_notarget_right: 1, correctAnswer: 99 },
{ go_target_right: 1, correctAnswer: 33 },
{ nogo_type1_target_left: 1, correctAnswer: 99 },
{ go_notarget_right: 1, correctAnswer: 66 },
{ nogo_type1_target_right: 1, correctAnswer: 99 },
{ go_target_left: 1, correctAnswer: 0 },
{ nogo_type2_target_left: 1, correctAnswer: 99 },
{ go_notarget_left: 1, correctAnswer: 66 },
{ nogo_type2_target_right: 1, correctAnswer: 99 },
],
};
/**
* Calculates the subjects score on the previous comboPracticeTrial and displays whether the subject has passed.
* @type {timeline}
*/
var cumulativeResponsePercentage = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function () {
var trial_correct = jsPsych.data.get().filter({ correct: 1 }).count();
console.log("trial correct",trial_correct)
if ((trial_correct / 16) * 100 >= 85) {
return "<p>You have passed the test</p>"; // the parameter value has to be returned from the function
jsPsych.data.addProperties({ subject: 1, condition: "control" });
}
if ((trial_correct / 16) * 100 < 85) {
return "<p>You have failed the test and it will be repeated again</p>"; // the parameter value has to be returned from the function
}
},
choices: ["ArrowRight"],
prompt: '<img alt="" src="images/right_arrow.png" width="75" height="50"/>',
on_finish: function (data) {
var last_trial_correct = jsPsych.data
.get()
.filter({ correct: 1 })
.count();
},
};
/**
* jsPsych trial that gets displayed when the subject has failed the ComboPracticeTrial for the second time.
*
* Calculates the comboPracticeTrial score for the last trial and displays whether the subject has passed.
* @type {timeline}
*/
var secondFeedback = {
type: jsPsychHtmlKeyboardResponse,
stimulus: function () {
var trial_correct = jsPsych.data
.get()
.last(48)
.filter({ correct: 1 })
.count();
if ((trial_correct / 16) * 100 < 85) {
return "<p>You have failed the test and it will not be repeated again. You will have the chance to review instructions again</p>"; // the parameter value has to be returned from the function
}
if ((trial_correct / 16) * 100 >= 85) {
return "<p>You have passed the test</p>"; // the parameter value has to be returned from the function
}
},
//trial_duration: 1000,
choices: ["ArrowRight"],
prompt: '<img alt="" src="images/right_arrow.png" width="75" height="50"/>',
on_finish: function (data) {
var trial_correct = jsPsych.data
.get()
.last(48)
.filter({ correct: 1 })
.count();
console.log("correct", trial_correct)
},
};
/**
* Conditional timeline that adds another comboPracticeTrial and secondFeedback to the timeline if the subject has
* failed the first comboPracticeTrial.
* @type {timeline}
*/
var if_node = {
timeline: [comboPracticeTrial, secondFeedback],
conditional_function: function () {
// get the data from the previous trial,
// and check which key was pressed
var last_trial_stimulus = jsPsych.data.get().last(1).values()[0].stimulus;
var trial_correct = jsPsych.data.get().filter({ correct: 1 }).count();
if ((trial_correct / 16) * 100 >= 85) {
return false;
}
if ((trial_correct / 16) * 100 < 85) {
return true;
}
},
};
var practiceTest = [].concat(comboPracticeTrial);
practiceTest.push(cumulativeResponsePercentage);
practiceTest.push(if_node);
return practiceTest;
}
////////////////////////////////////////////////////
////////////////////////////////////////////////////
//// PRACTICE
////////////////////////////////////////////////////
////////////////////////////////////////////////////
/**
* Creates a jsPsych trial for a practice moot block similar to the original experiment.
*
* @returns {timeline} A practice moot trial
*/
function createPracticeTimeline() {
practiceMoot = {
type: jsPsychMootPlugin,
moot_parameters: {
doggelgaenger_col: condition.doppelgaenger_color_white
? "1,1,1"
: "0,0,0",
doggelgaenger_loc: condition.doppelgaenger_up ? "20,0,-10" : "-20,0,-10",
doppelgaenger_target: condition.targetpattern_squares ? "SQ" : "TR",
go_notarget_left: 4,
go_notarget_right: 4,
go_target_left: 4,
go_target_right: 4,
nogo_type1_target_left: 1,
nogo_type1_target_right: 1,
nogo_type1_notarget_left: 1,
nogo_type1_notarget_right: 1,
nogo_type2_target_left: 1,
nogo_type2_target_right: 1,
nogo_type2_notarget_left: 1,
nogo_type2_notarget_right: 1,
doppelgaenger_anim_rec: experiment_config.record_doppelgaenger_animation,
other_anim_rec: experiment_config.record_other_animation,
},
extensions: [
{ type: jsPsychUnityExtension },
{ type: jsPsychExtensionRecordVideo },
{
type: jsPsychExtensionMediapipeFacemesh,
params: {
record: false,
},
},
],
};
return [practiceMoot];
}
////////////////////////////////////////////////////
////////////////////////////////////////////////////
//// MOOT
////////////////////////////////////////////////////
////////////////////////////////////////////////////
/**
* Creates a random combination of moot and mimicry blocks.
* This is for the main part of the experiment.
*
* @returns {timeline} A timeline containing 16 randomized moot blocks, followed by mimicry blocks.
*/
function createMootTimeline() {
// Create the moot blocks V1,...,V4
const blocks = {
1: {
doggelgaenger_col: condition.doppelgaenger_color_white
? "1,1,1"
: "0,0,0",
doggelgaenger_loc: condition.doppelgaenger_up ? "20,0,-10" : "-20,0,-10",
doppelgaenger_target: condition.targetpattern_squares ? "SQ" : "TR",
go_notarget_left: 3,
go_notarget_right: 3,
go_target_left: 3,
go_target_right: 3,
nogo_type1_target_left: 1,
nogo_type1_target_right: 1,
doppelgaenger_anim_rec: experiment_config.record_doppelgaenger_animation,
other_anim_rec: experiment_config.record_other_animation,
},
2: {
doggelgaenger_col: condition.doppelgaenger_color_white
? "1,1,1"
: "0,0,0",
doggelgaenger_loc: condition.doppelgaenger_up ? "20,0,-10" : "-20,0,-10",
doppelgaenger_target: condition.targetpattern_squares ? "SQ" : "TR",
go_notarget_left: 2,
go_notarget_right: 2,
go_target_left: 2,
go_target_right: 2,
nogo_type1_target_left: 1,
nogo_type1_target_right: 1,
doppelgaenger_anim_rec: experiment_config.record_doppelgaenger_animation,
other_anim_rec: experiment_config.record_other_animation,
},
3: {
doggelgaenger_col: condition.doppelgaenger_color_white
? "1,1,1"
: "0,0,0",
doggelgaenger_loc: condition.doppelgaenger_up ? "20,0,-10" : "-20,0,-10",
doppelgaenger_target: condition.targetpattern_squares ? "SQ" : "TR",
go_notarget_left: 4,
go_notarget_right: 4,
go_target_left: 4,
go_target_right: 4,
nogo_type1_target_left: 1,
nogo_type1_target_right: 1,
doppelgaenger_anim_rec: experiment_config.record_doppelgaenger_animation,
other_anim_rec: experiment_config.record_other_animation,
},
4: {
doggelgaenger_col: condition.doppelgaenger_color_white
? "1,1,1"
: "0,0,0",
doggelgaenger_loc: condition.doppelgaenger_up ? "20,0,-10" : "-20,0,-10",
doppelgaenger_target: condition.targetpattern_squares ? "SQ" : "TR",
go_notarget_left: 5,
go_notarget_right: 5,
go_target_left: 5,
go_target_right: 5,
nogo_type1_target_left: 1,
nogo_type1_target_right: 1,
doppelgaenger_anim_rec: experiment_config.record_doppelgaenger_animation,
other_anim_rec: experiment_config.record_other_animation,
},
};
let mimicry_blocks = [
{ mimicry_first_target_left: 1, mimicry_second_notarget_left: 1 },
{ mimicry_first_target_left: 1, mimicry_second_notarget_right: 1 },
{ mimicry_first_target_right: 1, mimicry_second_notarget_left: 1 },
{ mimicry_first_target_right: 1, mimicry_second_notarget_right: 1 },
{ mimicry_first_notarget_left: 1, mimicry_second_target_left: 1 },
{ mimicry_first_notarget_left: 1, mimicry_second_target_right: 1 },
{ mimicry_first_notarget_right: 1, mimicry_second_target_left: 1 },
{ mimicry_first_notarget_right: 1, mimicry_second_target_right: 1 },
];
mimicry_blocks = shuffleArray(mimicry_blocks.concat(mimicry_blocks));
let trials_moot_blocks = [];
shuffleArray(new Array(4).fill([1, 2, 3, 4]).flat()).forEach((idx, i) => {
const moot_parameters = Object.assign({}, blocks[idx], mimicry_blocks[i]);
trials_moot_blocks.push({
type: jsPsychMootPlugin,
moot_parameters: moot_parameters,
extensions: [
{ type: jsPsychUnityExtension },
{ type: jsPsychExtensionRecordVideo },
{
type: jsPsychExtensionMediapipeFacemesh,
params: {
record: false,
},
},
],
});
});
return trials_moot_blocks;
}
/**
* Creates a warmup timeline to get the subject to be more comfortable showing expressions.
*
* The timeline consists of multiple jsPsychHtmlKeyboardResponse trials which show a stimulus and then prompt the
* subject to press ArrowRight to continue.
*
* @returns {timeline} A warmup timeline.
*/
function createWarmupTimeline() {
let warmup_block = [];
warmup_block.push({
type: jsPsychHtmlKeyboardResponse,
stimulus: slides_content["Slide_7"],
choices: ["ArrowRight"],
trial_ends_after_video: false,
prompt: '<img alt="" src="images/right_arrow.png" width="75" height="50"/>',
});
for (i=0;i< warmup_videos_names.length; i++){
warmup_block.push({
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p>Please imitate the expression you will see during the upcoming video.</p>',
choices: ["ArrowRight"],
prompt:
'<p>Press right arrow key to start the video.</p><img alt="" src="images/right_arrow.png" width="75" height="50"/>',
});
warmup_block.push({
type: jsPsychVideoKeyboardResponse,
stimulus: [warmup_videos_names[i].video],
response_ends_trial:false,
trial_duration: 11000,
extensions: [
{type: jsPsychExtensionRecordVideo}
]
});
warmup_block.push({
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p>You can stop imitating the expression now.</p>',
trial_duration:2000,
});
}
warmup_block.push({
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p>The warmup phase is finished, now we will give further instructions for the experiment.</p>',
trial_duration:4000,
});
return warmup_block;
}
/**
* Creates a timeline to ask for consent and upload recorded video data.
*
* The subject is asked for consent to upload the collected video data using jsPsychHtmlButtonResponse.
*
* If they agree the data upload is defined using a jsPsychNextcloudFiledropPlugin.
*
* If they decline, displays a jsPsychSurveyText block.
*
* @returns {timeline} A consent and upload timeline.
*/
function createConsentAndUpload() {
var pipeline = [];
pipeline.push({
type: jsPsychHtmlButtonResponse,
stimulus: "<p>Do you give consent to upload the video data collected during the experiment?</p>",
choices: ['<p>Yes, upload with video</p>', '<p>No, upload without video</p>'],
prompt: slides_content['upload_consent'],
});
const stripVideo = function (data) {
Object.keys(data.trials).forEach((key) => {
let counter = 0;
if (data.trials[key]['record_video_data']) {
data.trials[key]['record_video_data'] = "REMOVED";
counter++;
}
return (counter);
});
}
// Strip video data depending on the response of the participant
pipeline.push({
timeline: [
{
type: jsPsychSurveyText,
questions: [
{
prompt: '<p>Please take a moment to describe in the textbox below why did you declined uploading your data.</p>',
rows: 7
}
]
},
{
type: jsPsychCallFunction,
func: function () { stripVideo(jsPsych.data.get()) }
}
],
conditional_function: function () {
// get the data from the previous trial,
// and check which key was pressed
var data = jsPsych.data.get().last(1).values()[0];
return (data.response == 1)
}
});
pipeline.push(
{
type: jsPsychNextcloudFiledropPlugin,
url: 'https://owncloud.csl.uni-bremen.de',
// If changing this, please also change the link for rescue-upload in
// slides/upload_failed.html
folder: 'MQqCYwmy6YBdS9d',
filename: function (){
return subject_id + '.zip';
},
generate_download_url_on_error: true
});
// Check for error during upload
pipeline.push({
timeline: [
{
type: jsPsychHtmlKeyboardResponse,
stimulus: slides_content['upload_failed'],
prompt: "<p>Press right arrow key after upload has finished.</p>",
choices: ['ArrowRight'],
},
{
type: jsPsychCallFunction,
func: function () {
// console.log("REVOKE", jsPsych.data.get().last(2).values()[0].url);
// Remove ObjectURL to prevent mem leakage
URL.revokeObjectURL(jsPsych.data.get().last(2).values()[0].url);
}
}
],
conditional_function: function () {
var data = jsPsych.data.get().last(1).values()[0];
return (data.error)
}
});
return (pipeline)
}
/**
* Creates a trial to check if the subject is using one of the listed browsers, otherwise lets them know what browser
* to use.
*
* Usable browsers include:
*
* -Chrome
* -Firefox
* -Edge Chromium
*
* @returns {trial?} A trial that checks for correct browser. !TODO
*/
function checkBrowser(){
var browserCondition = {
type: jsPsychBrowserCheck,
inclusion_function: (data) => {
return ['chrome', 'firefox','edge-chromium'].includes(data.browser) && data.mobile === false;
},
exclusion_message: (data) => {
if(data.mobile){
return '<p>You must use a desktop/laptop computer to participate in this experiment.</p>';
} else if (data.browser !== 'chrome' || data.browser !== 'firefox' || data.browser !== 'edge-chromium') {
return '<p>You must use Chrome or Firefox or Edge to complete this experiment.</p>'
}
},
};
return browserCondition;
}
/**
* Creates a timeline containing the generalInstructions trial.
* Displays a sequence of slides containing general instructions. The subject has to manually continue by pressing arrowright.
*
* @returns {timeline} A timeline containing the generalInstructions trial.
*/
function createGeneralInstructions(){
var generalInstructions = []
for (let i = 0; i < 6; i++) {
generalInstructions.push(allSlides[i]);
}
generalInstructions[0].prompt = '<p>Press right arrow key for further instructions</p> <img alt="" src="images/right_arrow.png" width="75" height="50"/>';
for (let i = 1; i < generalInstructions.length; i++) {
generalInstructions[i].prompt = '<img alt="" src="images/right_arrow.png" width="75" height="50"/>';
}
return generalInstructions;
}
/**
* Creates a timeline containing the mainInstructions trial.
* Displays a sequence of slides containing the main instructions. The subject has to manually continue by pressing arrowright.
*
* @returns {timeline} A timeline containing the mainInstructions trial.
*/
function createMainInstructions(){
var mainInstructions = [];
for (let i = 9; i < 15; i++) {
mainInstructions.push(allSlides[i]);
}
for (let i = 0; i < mainInstructions.length; i++) {
mainInstructions[i].prompt = '<img alt="" src="images/right_arrow.png" width="75" height="50"/>';
}
return mainInstructions;
}
/**
* Creates a timeline containing the countDown trial.
* Displays a sequence of slides one after another, waiting a set amount of time between each slide.
*
* @returns {timeline} A timeline containing the countDown trial.
*/
function createCountDown(){ //TODO delete choices and prompts does nothing, can still be skipped by pressing buttons
var countDown = [];
for (let i = 25; i < 30; i++) {
countDown.push(allSlides[i]);
}
for (let i = 0; i < countDown.length; i++) {
delete countDown[i].choices;
delete countDown[i].prompt;
countDown[i].trial_duration = 1500;
}
return countDown;
}
/**
* Creates a timeline containing the summaryInstruction trial.
* Disaplys a slide containing a summary of the instructions and a prompt.
*
* @returns {timeline} A timeline containing the summaryInstruction trial.
*/
function createSummaryInstruction(){
var summaryInstruction = allSlides[23];
summaryInstruction.prompt = '<img alt="" src="images/right_arrow.png" width="75" height="50"/>';
return [summaryInstruction];
}
/**
* Creates a timeline containing the practicePhaseStartInstruction trial.
* Disaplys a slide containing start instructions for the practice phase and removes prompts and choices from it.
*
* @returns {timeline} A timeline containing the practicePhaseStartInstruction trial.
*/
function createPracticePhaseStartInstruction(){
var practicePhaseStartInstruction = allSlides[24];
delete practicePhaseStartInstruction.choices;
delete practicePhaseStartInstruction.prompt;
practicePhaseStartInstruction.trial_duration = 1500;
return [practicePhaseStartInstruction];
}
/**
* Creates a timeline containing the practicePhaseEndInstruction trial.
* Displays a slide containing instructions to be displayed after the practice phase and removes prompts and choices from it.
*
* @returns {timeline} A timeline containing the practicePhaseEndInstruction trial.
*/
function createPracticePhaseEndInstruction(){
var practicePhaseEndInstruction = allSlides[30];
delete practicePhaseEndInstruction.choices;
delete practicePhaseEndInstruction.prompt;
practicePhaseEndInstruction.trial_duration = 1500;
return [practicePhaseEndInstruction];
}
/**
* Creates a timeline containing the MOOTPhaseStartInstruction trial.
* Displays a slide containing instructions about the moot phase and removes prompts and choices from it.
*
* @returns {timeline} A timeline containing the MOOTPhaseStartInstruction trial.
*/
function createMOOTPhaseStartInstruction(){
var MOOTPhaseStartInstruction = allSlides[33];
delete MOOTPhaseStartInstruction.choices;
delete MOOTPhaseStartInstruction.prompt;
MOOTPhaseStartInstruction.trial_duration = 1500;
return [MOOTPhaseStartInstruction];
}
/**
* Creates a timeline containing repeatInstructionChoiceFirst trial.
* Displays a slide and prompts the subject to press left or right.
*
* @returns {timeline} A timeline containing repeatInstructionChoiceFirst trial.
*/
function createRepeatInstructionChoiceFirst(){
var repeatInstructionChoiceFirst = allSlides[15];
delete repeatInstructionChoiceFirst.prompt;
repeatInstructionChoiceFirst.choices = ['ArrowLeft', 'ArrowRight'];
return repeatInstructionChoiceFirst;
}
/**
* Creates a timeline containing repeatInstructionChoiceSecond trial.
* Same as createRepeatInstructionChoiceFirst but displays a different slide.
*
* @returns {timeline} A timeline containing repeatInstructionChoiceSecond trial.
*/
function createRepeatInstructionChoiceSecond (){
var repeatInstructionChoiceSecond = allSlides[31];
delete repeatInstructionChoiceSecond.prompt;
repeatInstructionChoiceSecond.choices = ['ArrowLeft', 'ArrowRight'];
return [repeatInstructionChoiceSecond];
}
/**
* Creates an instruction_if_node trial that contains a timeline based on what the subject pressed in the previous trial.
* If the subject pressed arrowright, no timeline element is returned.
* If the subject pressed arrowleft, the mainInstructions timeline (except for the first element) is returned.
*
* @returns {timeline} A timeline containing either a trial or nothing.
*/
function createInstruction_if_node(){
var instruction_if_node = {
timeline: mainInstructions.slice(1),
conditional_function: function () {
// get the data from the previous trial,
// and check which key was pressed
var last_trial_choice = jsPsych.data.get().last(1).values()[0].response;
if (last_trial_choice == 'arrowright') {
return false;
} if (last_trial_choice == 'arrowleft') {
return true;
}
}
}
return instruction_if_node;
}
/**
* Creates a timeline containing the practiceInstruction trial.
* Displays a slide with a prompt.
*
* @returns {timeline} A timeline containing the practiceInstruction trial.
*/
function createPracticeInstruction(){
const practiceInstruction = allSlides[16];
practiceInstruction.prompt = '<img alt="" src="images/right_arrow.png" width="75" height="50"/>';
return [practiceInstruction]
}
/**
* Creates a timeline containing questions as jsPsychHtmlSliderResponses.
* Displays questions from allSlides with a slider for response.
*
* @returns {timeline} A timeline containing multiple jsPsychHtmlSliderResponse trials.
*/
function createQuestionsAgencyOwnership(){
var questionnaire = allSlides.slice(40, 49);
var questionsAgencyOwnership = [];
for (i = 0; i < questionnaire.length-1; i++) {
html_1 = questionnaire[i].stimulus;
html_2 = questionnaire[i].stimulus;
if (condition.doppelgaenger_color_white) {
html_2 = html_2.replaceAll("white", "black");
} else {
html_2 = html_2.replaceAll("black", "white");
}
if (condition.questionnaire_doppelgaenger_first) {
// do nothing
} else {
[html_1, html_2] = [html_2, html_1];
}
questionsAgencyOwnership.push({
type: jsPsychHtmlSliderResponse,
stimulus: html_1,
step: 1,
labels: ["Fully agree", "Fully disagree"],
});
questionsAgencyOwnership.push({
type: jsPsychHtmlSliderResponse,
stimulus: html_2,
step: 1,
labels: ["Fully agree", "Fully disagree"],
});
}
return questionsAgencyOwnership;
}
/**
* Creates a timeline containing questions as jsPsychHtmlSliderResponses.
* Displays questions from allSlides with a slider for response. Same as createQuestionsAgencyOwnership but with different sliders.
*
* @returns {timeline} A timeline containing a questionnaire instructions prompt and multiple jsPsychHtmlSliderResponse trials.
*/
function createQuestionsIOS(){
const questionnaireInstruction = allSlides[39];
questionnaireInstruction.prompt = '<img alt="" src="images/right_arrow.png" width="75" height="50"/>';
var questionnaire = allSlides.slice(40, 49);
var questionsIOS = [];
for (i = questionnaire.length-1; i < questionnaire.length; i++) {
html_1 = questionnaire[i].stimulus;
html_2 = questionnaire[i].stimulus;
if (condition.doppelgaenger_color_white) {
html_2 = html_2.replaceAll("white", "black");
} else {
html_2 = html_2.replaceAll("black", "white");
}
if (condition.questionnaire_doppelgaenger_first) {
// do nothing
} else {
[html_1, html_2] = [html_2, html_1];
}
questionsIOS.push({
type: jsPsychHtmlSliderResponse,
stimulus: html_1,
step:16.66 ,
labels: ["1", "2","3","4","5","6","7"],
});
questionsIOS.push({
type: jsPsychHtmlSliderResponse,
stimulus: html_2,
step: 16.66,
labels: ["1", "2","3","4","5","6","7"],
});
}
questionsIOS.unshift(questionnaireInstruction)
return questionsIOS;
}
/**
* Creates a slice of elements from allSlides containing usabilityQuestions.
* Like allSlides this should be used to access the contained trials and should not be used as a timeline on its own.
*
* @returns {timeline} A timeline containing a slice of trials from allSlides.
*/
function createUsabilityQuestionnaire(){
var usabilityQuestionnaire = allSlides.slice(50, 54);
return usabilityQuestionnaire;
}
/**
* Creates a slice of elements from allSlides containing usabilityQuestionsPlus.
* Like allSlides this should be used to access the contained trials and should not be used as a timeline on its own.
*
* @returns {timeline} A slice of trials from allSlides.
*/
function createUsabilityQuestionnairePlus(){
var usabilityQuestionnairePlus = allSlides.slice(55, 66);
return usabilityQuestionnairePlus;
}
/**
* Creates a timeline containing usability questions from usabilityQuestionnaire as usabilityQuestionnaires.
* Displays questions with a slider for response. Also replaces stimulus to fit the experiment
* based on doppelgaenger color.
*
* @returns {timeline} A timeline containing a prompt for usability instructions and the multiple jsPsychHtmlSliderResponse trials.
*/
function createUsabilityQuestions(){
const usabilityInstruction = allSlides[49];
usabilityInstruction.prompt = '<img alt="" src="images/right_arrow.png" width="75" height="50"/>';
var usabilityQuestions = [];
for (i = 0; i < usabilityQuestionnaire.length; i++) {
html_1 = usabilityQuestionnaire[i].stimulus;
html_2 = usabilityQuestionnaire[i].stimulus;
if (condition.doppelgaenger_color_white) {
html_2 = html_2.replaceAll("white", "black");
} else {
html_2 = html_2.replaceAll("black", "white");
}
if (condition.questionnaire_doppelgaenger_first) {
// do nothing
} else {
[html_1, html_2] = [html_2, html_1];
}
usabilityQuestions.push({
type: jsPsychHtmlSliderResponse,
stimulus: html_1,
step: 1,
labels: ["Not at all", "Very much"],
});
usabilityQuestions.push({
type: jsPsychHtmlSliderResponse,
stimulus: html_2,
step: 1,
labels: ["Not at all", "Very much"],
});
}
usabilityQuestionnaire.unshift(usabilityInstruction);
return usabilityQuestions;
}
/**
* Creates a timeline containing the usabilityQuestionnaireLatency trial.
* Displays a slide with a slider for response
*
* @returns {timeline} A timeline containing the usabilityQuestionnaireLatency trial.
*/
function createUsabilityQuestionnaireLatency(){
var usabilityQuestionnaireLatency = {
type: jsPsychHtmlSliderResponse,
stimulus: allSlides[67].stimulus,
step: 1,
labels: ["Not at all", "Very much"],
}
return [usabilityQuestionnaireLatency];
}
/**
* Creates a timeline for usabilityQuestionnaireExtend containing multiple questionnaire slides
* as jsPsychHtmlSliderResponses.
* Displays questions from usabilityQuestionnairePlus with a slider for response.
*
* @returns {timeline} A timeline containing multiple usabilityQuestionnaire trials.
*/
function createUsabilityQuestionnaireExtend(){
var usabilityQuestionnaireExtend = [];
for (i = 0; i < usabilityQuestionnairePlus.length - 2; i++) {
html = usabilityQuestionnairePlus[i].stimulus;
usabilityQuestionnaireExtend.push({
type: jsPsychHtmlSliderResponse,
stimulus: html,
step: 1,
labels: ["Fully agree", "Fully disagree"],
});
}
return usabilityQuestionnaireExtend;
}
/**
* Creates a timeline containing two usabilityQuestionnaireTextBoxes as jsPsychSurveyTexts.
* Displays a questionnaire prompt out of usabilityQuestionnairePlus with a free response text field.
*
* @returns {timeline} A timeline containing two usabilityQuestionnaire trials.
*/
function createUsabilityQuestionnaireTextBox(){
var usabilityQuestionnaireTextBox = [];
usabilityQuestionnaireTextBox.push({
type: jsPsychSurveyText,
questions: [
{
prompt: usabilityQuestionnairePlus[usabilityQuestionnairePlus.length - 2].stimulus.replace(/<[^>]+>/g, ''),
required: true,
placeholder: 'Your text here',
rows: 3,
columns: 40,
}
]
})
usabilityQuestionnaireTextBox.push({
type: jsPsychSurveyText,
questions: [
{
prompt: usabilityQuestionnairePlus[usabilityQuestionnairePlus.length - 1].stimulus.replace(/<[^>]+>/g, ''),
required: true,
placeholder: 'Your text here',
rows: 3,
columns: 40,
},
],
})
return usabilityQuestionnaireTextBox;
}
/**
* Creates a timeline containing the thankYou trial.
* Displays a "Thank you" message as a jsPsychHtmlKeyboardResponse and returns the subject to the prolific website
* with the given link.
*
* @param {String} link The prolific link the subject should return to after the experiment.
* Can be found on the prolific website
*
* @returns {timeline} A timeline containing the thankYou trial.
*/
function createThankYou(link){
var thankYou = {
type: jsPsychHtmlKeyboardResponse,
stimulus: () => {
let html = `<h1>Thank you for your participation!</h1>`;
return html;
},
choices: "NO_KEYS",
trial_duration: 3000,
on_finish: function(){
window.location = link;
}
}
return [thankYou];
}
/**
* Creates a timeline containing the closeUnity trial.
* Closes the Unity application.
*
* @returns {timeline} A timeline containing the closeUnity trial.
*/
function createCloseUnity(){
var closeUnity = {
type: jsPsychCloseUnityPlugin,
}
return [closeUnity];
}
/**
* Creates a timeline containing the cameraCalibration trial.
* Displays instructions for camera calibration.
*
* @returns {timeline} A timeline containing the cameraCalibration trial.
*/
function createCameraCalibration(){
var cameraCalibration = {
type: jsPsychCalibration,
prompt: '<p>It is important that you keep your head in the elipse throughout the experiment. Furthermore, the camera should face you frontally, if needed adjust your camera accordingly.</p>',
}
return [cameraCalibration];
}
/**
* Creates a timeline containing the consentBeforeCameraInit trial.
* Displays the consent_before_camera slide as a jsPsychHtmlButtonResponse
*
* @returns {timeline} A timeline containing the consentBeforeCameraInit trial.
*/
function createConsentBeforeCameraInit(){
const consentBeforeCameraInit = {
type: jsPsychHtmlButtonResponse,
stimulus: slides_content['consent_before_camera'],
choices: ['Next']
}
return [consentBeforeCameraInit];
}
/**
* Creates a timeline containing the startConsent trial.
* Displays the start_consent slide as a jsPsychHtmlButtonResponse
*
* @returns {timeline} A timeline containing the startConsent trial.
*/
function createStartConsent(){
const startConsent = {
type: jsPsychHtmlButtonResponse,
stimulus: slides_content['start_consent'],
choices: ['I Agree'],
}
return [startConsent];
}
/**
* Creates a timeline containing the preloadMedia trial.
* Preloads all existing media to avoid loading times during the experiment.
*
* @returns {timeline} A timeline containing the preloadMedia trial.
*/
function createPreloadMedia(){
const preloadMedia = {
type: jsPsychPreload,
message: "<p>Loading videos and images.</p>",
//auto_preload: true,
images: images_filenames,
videos: video_filenames,
show_detailed_errors: true
}
return [preloadMedia];
}
/**
* Creates a timeline containing the initCamera trial.
* Initializes the subjects camera.
*
* @returns {timeline} A timeline containing the initCamera trial.
*/
function createInitCamera(){
const initCamera = {
type: jsPsychInitializeCamera,
include_audio: false
}
return [initCamera];
}
/**
* Creates a timeline containing the enterFullscreen trial.
* Enters fullscreen mode on a page.
*
* @returns {timeline} A timeline containing the enterFullscreen trial.
*/
function createEnterFullscreen(){
const enterFullscreen = {
type: jsPsychFullscreen,
fullscreen_mode: true,
message: '<p>Move the browser on the screen where the camera is above the screen. The experiment will switch to full screen mode when you press the button below, please leave it in full screen for the entire experiment</p>',
}
return [enterFullscreen];
}
/**
* Creates a timeline containing the exitFullscreen trial.
* Exits fullscreen mode on a page.
*
* @returns {timeline} A timeline containing the exitFullscreen trial.
*/
function createExitFullscreen(){
const exitFullscreen = {
type: jsPsychFullscreen,
fullscreen_mode: false,
delay_after: 0
}
return [exitFullscreen];
}
/**
* ?
*
* @returns {timeline} A timeline containing a single debug helper trial.
*/
function createDebug_condition(){
const debug_condition = {
type: jsPsychHtmlKeyboardResponse,
stimulus: "<div><p>Condition</p><p>" + JSON.stringify(condition, null, 2) + "</p></div>",
//trial_duration: 1000,
choice: ['ArrowRight'],
prompt: "<p>press right arrow key to continue</p>"
}
return [debug_condition];
}
/**
* Creates a jsPsychHtmlKeyboardResponse containing the content of each slide as stimulus for every slide.
* Should be used to access the contained trials, not as a timeline on its own.
*
* @param {Object.<key, string>} slides_content A mapping of slide names to html content.
*
* @returns {timeline} A timeline containing multiple jsPsychHtmlKeyboardResponse objects.
*/
function createAllSlides(slides_content){
var allSlides = [];
Object.keys(slides_content).forEach(key => {
html = slides_content[key];
allSlides.push({
type: jsPsychHtmlKeyboardResponse,
stimulus: html,
prompt: "<p>Press right arrow key for further instructions</p>",
choices: ['ArrowRight'],
});
});
return allSlides;
}