<!DOCTYPE html> <html> <!-- _ _ _ ___| |_ ___ _ __ (_) |_ / __| __/ _ \| '_ \ _____| | __| \__ \ || (_) | |_) |_____| | |_ |___/\__\___/| .__/ |_|\__| |_| AUTHORS: Luc Vermeylen & Frederick Verbruggen (2019) DESCRIPTION: This is the jsPsych version of the stop-signal task --> <head> <!-- import the jsPsych core library, specific plugins, jquery and some other scripts--> <title>Stop Signal Task</title> <!-- defines a title in the browser tab --> <script src="js/jspsych-6.0.5/jspsych.js"></script> <!-- jsPsych core library --> <script src="js/jspsych-6.0.5/plugins/jspsych-instructions.js"></script> <!-- plugins define specific tasks, e.g., presenting instructions --> <script src="js/jspsych-6.0.5/plugins/jspsych-html-keyboard-response.js"></script> <script src="js/jspsych-6.0.5/plugins/jspsych-survey-text-beta-6.1.js"></script> <!-- beta 6.1 version has the 'input required' function for text fields --> <script src="js/jspsych-6.0.5/plugins/jspsych-survey-multi-choice.js"></script> <script src="js/jspsych-6.0.5/plugins/jspsych-call-function.js"></script> <script src="js/jspsych-6.0.5/plugins/jspsych-fullscreen.js"></script> <script src="js/jquery-1.7.1.min.js"></script> <!-- the jquery library is used to communicate with the server (to store the data) through "AJAX" and PHP --> <script src="js/bowser.js"></script> <!-- a browser and operating system detector --> <script src="js/sprintf.js"></script> <!-- format variables in a string, used for customizable feedback strings in which the variables are not yet declared --> <script src="js/custom-stop-signal-plugin.js"></script> <!-- custom plugin for the main stop-signal trial based on the image-keyboard-response plugin --> <script src="js/jspsych-detect-held-down-keys.js"></script> <!-- custom plugin for detecting if a key is being held down --> <script src="configuration/experiment_variables.js"></script> <!-- parameters to configure the experiment --> <script src="configuration/text_variables.js"></script> <!-- holds all the text variables for easy modification/translation --> <link href="js/jspsych-6.0.5/css/jspsych.css" rel="stylesheet" type="text/css"></link> <!-- standard jsPsych css stylesheet --> </head> <body></body> <script> /* ######################################################################### Initialize variables ######################################################################### */ // Initialize some important variables var timeline = []; // this array stores the events we want to run in the experiment var trial_ind = 1; // trial indexing variable starts at 1 for convenience var block_ind = 0; // block indexing variables: block 0 is considered to be the practice block var focus = 'focus'; // tracks if the current tab/window is the active tab/window, initially the current tab should be focused var fullscr_ON = 'no'; // tracks fullscreen activity, initially not activated var redirect_timeout = 1500; // set this so that data is saved before redirect! // is the experiment running from a server or not? (this determines if data is saved on server or offline) if (document.location.host) { // returns your host or null online = true; } else { online = false; }; // detect visitor variables with the bowser js library (/js/bowser.js) jsPsych.data.addProperties({ // add these variables to all rows of the datafile browser_name: bowser.name, browser_version: bowser.version, os_name: bowser.osname, os_version: bowser.osversion, tablet: String(bowser.tablet), mobile: String(bowser.mobile), // convert explicitly to string so that "undefined" (no response) does not lead to empty cells in the datafile screen_resolution: screen.width + ' x ' + screen.height, window_resolution: window.innerWidth + ' x ' + window.innerHeight, // this will be updated throughout the experiment }); // define the images to be loaded, the actual preloading occurs in the jsPsych.init function at the bottom var pre_load_stimuli = [fix_stim, go_stim1, go_stim2, stop_stim1, stop_stim2]; /* ######################################################################### Create the design based on the input from 'experiment_variables.js' ######################################################################### */ // Since we have two stimuli, the number of trials of the basic design = 2 * nstim // This design will later be repeated a few times for each block // (number of repetitions is also defined in 'experiment_variables.js') var ngostop = 1/nprop // covert proportion to trial numbers. E.g. 1/5 = 1 stop signal and 4 go var ntrials = ngostop * 2 // total number of trials in basic design (2 two choice stimuli x ngostop) var signalArray = Array(ngostop-1).fill('go'); // no-signal trials signalArray[ngostop-1] = ('stop'); // stop-signal trials // create factorial design from choices(2) and signal(nstim) var factors = { stim: [choice_stim1, choice_stim2], signal: signalArray, }; var design = jsPsych.randomization.factorial(factors, 1); // modify the design to make it compatible with the custom stop signal plugin // - set a first/second stimulus property. // on no-signal trials, only one image will be used (i.e. the go image/stimulus) // on stop-signal trials, two images will be used (i.e. the go and stop images/stimuli) // - set a data property with additional attributes for identifying the type of trial for (var i = 0; i < design.length; i++) { design[i].data = {} if ((design[i].stim == choice_stim1) && (design[i].signal == 'go')) { design[i].fixation = fix_stim; design[i].first_stimulus = go_stim1; design[i].second_stimulus = go_stim1; design[i].data.stim = choice_stim1; design[i].data.correct_response = cresp_stim1; design[i].data.signal = "no"; } else if ((design[i].stim == choice_stim2) && (design[i].signal == 'go')) { design[i].fixation = fix_stim; design[i].first_stimulus = go_stim2; design[i].second_stimulus = go_stim2; design[i].data.stim = choice_stim2; design[i].data.correct_response = cresp_stim2; design[i].data.signal = "no"; } else if ((design[i].stim == choice_stim1) && (design[i].signal == 'stop')) { design[i].fixation = fix_stim; design[i].first_stimulus = go_stim1; design[i].second_stimulus = stop_stim1; design[i].data.stim = choice_stim1; design[i].data.correct_response = "undefined"; design[i].data.signal = "yes"; } else if ((design[i].stim == choice_stim2) && (design[i].signal == 'stop')) { design[i].fixation = fix_stim; design[i].first_stimulus = go_stim2; design[i].second_stimulus = stop_stim2; design[i].data.stim = choice_stim2; design[i].data.correct_response = "undefined"; design[i].data.signal = "yes"; } delete design[i].signal; delete design[i].stim; }; //console.log(design); // uncomment to print the design in the browser's console /* ######################################################################### Define the individual events/trials that make up the experiment ######################################################################### */ // welcome message trial. Also: end the experiment if browser is not Chrome or Firefox var welcome = { type: "instructions", pages: welcome_message, show_clickable_nav: true, allow_backward: false, button_label_next: label_next_button, on_start: function(trial){ if (bowser.name == 'Firefox' || bowser.name == 'Chrome'){ trial.pages = welcome_message; } else { trial.pages = not_supported_message; setTimeout(function(){location.href="html/not_supported.html"}, 2000); } } }; // these events turn fullscreen mode on in the beginning and off at the end, if enabled (see experiment_variables.js) var fullscr = { type: 'fullscreen', fullscreen_mode: true, message: full_screen_message, button_label: label_next_button, }; var fullscr_off = { type: 'fullscreen', fullscreen_mode: false, button_label: label_next_button, }; // informed consent trial. The informed_consent_text variable comes from /configuration/text_variables.js var consent = { type: "instructions", pages: [informed_consent_text], show_clickable_nav: true, button_label_next: label_consent_button, allow_backward: false }; // if enabled below, get participant's id from participant and add it to the datafile. // the prompt is declared in the configuration/text_variables.js file var participant_id = { type: 'survey-text', questions: [{ prompt: subjID_instructions, required: true }, ], button_label: label_next_button, on_finish: function(data) { var responses = JSON.parse(data.responses); var code = responses.Q0; jsPsych.data.addProperties({ participantID: code }); } }; // get participant's age and add it to the datafile // the prompt is declared in the configuration/text_variables.js file var age = { type: 'survey-text', questions: [{ prompt: age_instructions, required: true }, ], button_label: label_next_button, on_finish: function(data) { var responses = JSON.parse(data.responses); var code = responses.Q0; jsPsych.data.addProperties({ age: code }); } }; // get participant's gender and add it to the datafile // the prompt and options are declared in the configuration/text_variables.js file var gender = { type: 'survey-multi-choice', questions: [{ prompt: gender_instructions, options: gender_options, required: true }, ], button_label: label_next_button, on_finish: function(data) { var responses = JSON.parse(data.responses); var code = responses.Q0; jsPsych.data.addProperties({ gender: code }); } }; // instruction trial // the instructions are declared in the configuration/text_variables.js file var instructions = { type: "instructions", pages: [page1, page2], show_clickable_nav: true , button_label_previous: label_previous_button, button_label_next: label_next_button, }; // start of each block // the start message is declared in the configuration/text_variables.js file var block_start = { type: 'html-keyboard-response', stimulus: text_at_start_block, choices: ['space'] }; // get ready for beginning of block // the get ready message is declared in the configuration/text_variables.js file var block_get_ready = { type: 'html-keyboard-response', stimulus: get_ready_message, choices: jsPsych.NO_KEYS, trial_duration: 2000, }; // blank inter-trial interval var blank_ITI = { type: 'jspsych-detect-held-down-keys', // this enables the detection of held down keys stimulus: "", // blank trial_duration: ITI/2, response_ends_trial: false, }; // now put the trial in a node that loops (if response is registered) var held_down_node = { timeline: [blank_ITI], loop_function: function(data){ if(data.values()[0].key_press != null){ return true; // keep looping when a response is registered } else { return false; // break out of loop when no response is registered } } }; // the main stimulus // use custom-stop-signal-plugin.js to show three consecutive stimuli within one trial // (fixation -> first stimulus -> second stimulus, with variable inter-stimuli-intervals) var stimulus = { type: 'custom-stop-signal-plugin', fixation: jsPsych.timelineVariable('fixation'), fixation_duration: FIX, stimulus1: jsPsych.timelineVariable('first_stimulus'), stimulus2: jsPsych.timelineVariable('second_stimulus'), trial_duration: MAXRT, // this is the max duration of the actual stimulus (excluding fixation time) // inter stimulus interval between first and second stimulus = stop signal delay (SSD) ISI: function() { var duration = SSD; return duration }, response_ends_trial: true, choices: [cresp_stim1, cresp_stim2], data: jsPsych.timelineVariable('data'), // was the response correct? adapt SSD accordingly on_finish: function(data) { // check if the response was correct data.response = jsPsych.pluginAPI.convertKeyCodeToKeyCharacter(data.key_press); // keys are stored in keycodes not in character, so convert for convenience data.response = String(data.response); // convert explicitly to string so that "undefined" (no response) does not lead to empty cells in the datafile data.correct = data.response == data.correct_response; // if no response was made, the reaction time should not be -250 but null if (data.rt == -250) { data.rt = null }; // on go trials, reaction times on the fixation (below zero) are always wrong if (data.signal == 'no' && data.rt < 0){ data.correct = false; }; // set and adapt stop signal delay (SSD) data.SSD = SSD; data.trial_i = trial_ind; data.block_i = block_ind; trial_ind = trial_ind + 1; if (data.signal == 'yes') { if (data.correct) { SSD = SSD + SSDstep; if (SSD >= MAXRT) { SSD = MAXRT - SSDstep }; } else { SSD = SSD - SSDstep; if (SSD <= SSDstep) { SSD = SSDstep }; } } } }; // trial-by-trial feedback // messages are defined in the configuration/text_variables.js file var trial_feedback = { type: 'html-keyboard-response', choices: jsPsych.NO_KEYS, trial_duration: iFBT, stimulus: function() { var last_trial_data = jsPsych.data.get().last(1).values()[0]; if (last_trial_data['signal'] === 'no') { // go trials if (last_trial_data['correct']) { return correct_msg } else { if (last_trial_data['response'] === "undefined") { // no response previous trial return too_slow_msg } else { if (last_trial_data['rt'] >= 0) { return incorrect_msg } else { return too_fast_msg } } } } else { // stop trials if (last_trial_data['correct']) { return correct_stop_msg } else { if (last_trial_data['rt'] >= 0) { return incorrect_stop_msg } else { return too_fast_msg } } } } }; // at the end of the block, give feedback on performance var block_feedback = { type: 'html-keyboard-response', trial_duration: bFBT, choices: function() { if (block_ind == NexpBL){ return ['p','space'] } else { return ['p'] // 'p' can be used to skip the feedback, useful for debugging } }, stimulus: function() { // calculate performance measures var ns_trials = jsPsych.data.get().filter({ trial_type: 'custom-stop-signal-plugin', block_i: block_ind, signal: 'no' }); var avg_nsRT = Math.round(ns_trials.select('rt').subset(function(x){ return x > 0; }).mean()); var prop_ns_Correct = Math.round(ns_trials.filter({ correct: true }).count() / ns_trials.count() * 1000) / 1000; // unhandy multiplying and dividing by 1000 necessary to round to two decimals var prop_ns_Missed = Math.round(ns_trials.filter({ key_press: null }).count() / ns_trials.count() * 1000) / 1000; var prop_ns_Incorrect = Math.round((1 - (prop_ns_Correct + prop_ns_Missed)) * 1000) / 1000; var ss_trials = jsPsych.data.get().filter({ trial_type: 'custom-stop-signal-plugin', block_i: block_ind, signal: 'yes' }); var prop_ss_Correct = Math.round(ss_trials.filter({ correct: true }).count() / ss_trials.count() * 1000) / 1000; // in the last block, we should not say that there will be a next block if (block_ind == NexpBL){ var next_block_text = final_block_msg } else { // make a countdown timer var count=(bFBT/1000); var counter; clearInterval(counter); counter=setInterval(timer, 1000); //1000 will run it every 1 second function timer(){ count=count-1; if (count <= 0){ clearInterval(counter); } document.getElementById("timer").innerHTML = count ; } var next_block_text = next_block_msg // insert countdown timer } // the final text to present. Can also show correct and incorrect proportions if requested. return [ no_signal_header + sprintf(avg_rt_msg,avg_nsRT) + sprintf(prop_miss_msg,prop_ns_Missed) + stop_signal_header + sprintf(prop_corr_msg,prop_ss_Correct) + next_block_text ] }, on_finish: function() { trial_ind = 1; // reset trial counter block_ind = block_ind + 1; // next block } }; var evaluate_end_if_practice = { type: 'call-function', func: function() { if (block_ind == 0) { // this limits the amount of trials in the practice block if (trial_ind > NdesignReps_practice * ntrials) { jsPsych.endCurrentTimeline(); } } } }; // end trial and save the data var goodbye = { type: "html-keyboard-response", stimulus: end_message, on_start: function(data) { var subjID = jsPsych.data.get().last(1).values()[0]['participantID']; var full_data = jsPsych.data.get(); var ignore_columns = ['raw_rt','trial_type','first_stimulus','second_stimulus','onset_of_first_stimulus', 'onset_of_second_stimulus','key_press','correct_response','trial_index','internal_node_id']; var rows = {trial_type: 'custom-stop-signal-plugin'}; // we are only interested in our main stimulus, not fixation, feedback etc. var selected_data = jsPsych.data.get().filter(rows).ignore(ignore_columns); // the next piece of codes orders the columns of the data file var d = selected_data.values() // get the data values // make an array that specifies the order of the object properties var arr = ['participantID','age','gender','block_i','trial_i','stim','signal','SSD','response','rt','correct','focus','Fullscreen', 'time_elapsed','browser_name','browser_version','os_name','os_version','tablet','mobile','screen_resolution','window_resolution']; new_arr = [] // we will fill this array with the ordered data function myFunction(item) { // this is function is called in the arr.forEach call below new_obj[item] = obj[item] return new_obj } // do it for the whole data array for (i = 0; i < d.length; i++) { obj = d[i]; // get one row of data new_obj = {}; arr.forEach(myFunction) // for each element in the array run my function selected_data.values()[i] = new_obj; // insert the ordered values back in the jsPsych.data object } if (!online) { selected_data.localSave('csv', 'SST_data_' + subjID + '.csv'); } } }; /* ######################################################################### combine trials in procedures (create nested timeline) ######################################################################### */ // only ask for participant id if 'id' = 'particpant' (experiment_variables.js) // if 'id' = 'url', get it from url; otherwise, generate random value // only go into fullscreen mode if 'fullscreen' is true if (id == "participant"){ if (fullscreen){ var start_timeline = [welcome, consent, participant_id, age, gender, fullscr, instructions] } else { var start_timeline = [welcome, consent, participant_id, age, gender, instructions] } } else { if (id == "url"){ var urlvar = jsPsych.data.urlVariables(); var code = urlvar.subject; jsPsych.data.addProperties({participantID: code}); } else { var code = jsPsych.randomization.randomID(); jsPsych.data.addProperties({participantID: code}); } if (fullscreen) { var start_timeline = [welcome, consent, age, gender, fullscr, instructions] } else { var start_timeline = [welcome, consent, age, gender, instructions] } } // start the experiment with the previously defined start_timeline trials var start_procedure = { timeline: start_timeline, }; // put trial_feedback in its own timeline to make it conditional (only to be shown during the practice block) var feedback_node = { timeline: [trial_feedback], conditional_function: function() { var last_trial_data = jsPsych.data.get().last(1).values()[0]; var current_block = block_ind; if (current_block == 0) { // this was previously set to provide feedback only on incorrect trials by adding: && last_trial_data['correct']==false return true; } else { return false; } } }; // timeline_variables determine the stimuli in the 'stimulus' trial var trial_procedure = { timeline: [blank_ITI, held_down_node, stimulus, feedback_node, evaluate_end_if_practice], timeline_variables: design, randomize_order: true, repetitions: NdesignReps_exp, }; // again: combine the following screen in one timeline, which constitues of the procedure of one block var block_procedure = { timeline: [block_start, block_get_ready, trial_procedure, block_feedback], randomize_order: false, repetitions: NexpBL+1, // add one because the first block is the practice block }; // end of the experiment if (fullscreen){ end_timeline = [fullscr_off, goodbye] } else { end_timeline = [goodbye] } var end_procedure = { timeline: end_timeline, // here, you could add questionnaire trials etc... }; // finally, push all the procedures to the overall timeline timeline.push(start_procedure, block_procedure, end_procedure) /* ######################################################################### the functions that save the data and initiates the experiment ######################################################################### */ // function that appends data to an existing file (or creates the file if it does not exist) function appendData(filename, filedata) { $.ajax({ // make sure jquery-1.7.1.min.js is loaded in the html header for this to work type: 'post', cache: false, url: 'php/save_data_append.php', // IMPORTANT: change the php script to link to the directory of your server where you want to store the data! data: { filename: filename, filedata: filedata }, }); }; // run the experiment! jsPsych.init({ timeline: timeline, preload_images: pre_load_stimuli, on_data_update: function(data) { // each time the data is updated: // write the current window resolution to the data data.window_resolution = window.innerWidth + ' x ' + window.innerHeight; // is the experiment window the active window? (focus = yes, blur = no) data.focus = focus; data.Fullscreen = fullscr_ON; // append a subset of the data each time a go or stop stimulus is shown (the custom-stop-signal-plugin) id_index = 2; // point in experiment when particpant id is manually entered. see 'start_timeline' if (online){ var subjID = jsPsych.data.get().last(1).values()[0]['participantID']; if (data.trial_index == id_index){ // write header data_row = "participantID,age,gender,block_i,trial_i,stim,signal,SSD,response,rt,correct," + "focus,Fullscreen,time_elapsed,browser_name,browser_version,os_name,os_version," + "tablet,mobile,screen_resolution,window_resolution\n" appendData('SST_data_'+ subjID +'.csv',data_row) } else if (data.trial_type == 'custom-stop-signal-plugin'){ // append data each stimulus data_row = data.participantID + ',' + data.age + ',' + data.gender + ',' + data.block_i + ',' + data.trial_i + ',' + data.stim + ',' + data.signal + ',' + data.SSD + ',' + data.response + ',' + data.rt + ',' + data.correct + ',' + data.focus + ',' + data.Fullscreen + ',' + data.time_elapsed + ',' + data.browser_name + ',' + data.browser_version + ',' + data.os_name + ',' + data.os_version + ',' + data.tablet + ',' + data.mobile + ',' + data.screen_resolution + ',' + data.window_resolution + '\n' appendData('SST_data_'+ subjID +'.csv',data_row) } } }, on_interaction_data_update: function(data) { //interaction data logs if participants leaves the browser window or exits full screen mode interaction = data.event; if (interaction.includes("fullscreen")){ // some unhandy coding to circumvent a bug in jspsych that logs fullscreenexit when actually entering if (fullscr_ON == 'no') {fullscr_ON = 'yes'; return fullscr_ON} else if (fullscr_ON == 'yes') {fullscr_ON = 'no'; return fullscr_ON} } else if (interaction == 'blur' || interaction == 'focus'){ focus = interaction; return focus; } }, exclusions: { // browser window needs to have these dimensions, if not, participants get the chance to maximize their window, if they don't support this resolution when maximized they can't particiate. min_width: minWidth, min_height: minHeight }, on_finish: function() { if (redirect_onCompletion){ setTimeout("location.href = '" + redirect_link + "';",redirect_timeout); // redirect to another URL with a certain delay, only when redirect_onCompletion is set to 'true' } }, }) </script> </html>