adding likert example

This commit is contained in:
Josh de Leeuw 2013-11-11 13:19:35 -05:00
parent ab4b7985eb
commit 3c5eed2073
33 changed files with 4936 additions and 1 deletions

View File

@ -10,7 +10,6 @@
<!-- style -->
<style>
#jspsych_target { margin: 50px auto 50px auto; width: 600px; font-size:18px; text-align: center;}
#instructions { text-align: left; }
pre { text-align: left; }
</style>
</head>

View File

@ -0,0 +1,62 @@
<!doctype html>
<html>
<head>
<title>jspsych-categorize plugin example</title>
<!-- jQuery -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<!-- this plugin requires the jQuery UI library and stylesheet -->
<!-- these can be loaded from google's servers with the following links -->
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"></script>
<link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/themes/black-tie/jquery-ui.min.css" rel="stylesheet" type="text/css"></link>
<!-- jsPsych -->
<script src="scripts/jspsych.js"></script>
<script src="scripts/plugins/jspsych-survey-likert.js"></script>
<!-- style -->
<style>
#jspsych_target {
margin: 50px auto 50px auto;
width: 600px;
font-size:18px;
text-align: center;
}
#instructions {
text-align: left;
}
pre {
text-align: left;
}
</style>
</head>
<body>
<div id="jspsych_target"></div>
</body>
<script type="text/javascript">
var page_1_questions = ["I like vegetables.", "I hate eggs."];
var page_2_questions = ["I like fruit."];
var scale_1 = ["Strongly Disagree", "Disagree", "Neutral", "Agree", "Strongly Agree"];
var scale_2 = ["Strongly Disagree", "Disagree", "Somewhat Disagree", "Neural", "Somewhat Agree", "Agree", "Strongly Agree"];
var likert_block = {
type: 'survey-likert',
questions: [page_1_questions, page_2_questions],
labels: [[scale_1, scale_2], [scale_1]],
intervals: [[5,7], [9]] // note the the intervals and labels don't necessarily need to match.
}
// launch jspsych experiment
jsPsych.init({
display_element: $('#jspsych_target'),
experiment_structure: [likert_block],
on_finish: function(data) {
$('#jspsych_target').append($("<pre>", {
text: JSON.stringify(data, undefined, 2)
}));
}
});
</script>
</html>

View File

View File

@ -0,0 +1,7 @@
body { background: #fff; margin:0; padding:0; font-size: 18px; font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; }
#jspsych_target { width: 1000px; margin-left: auto; margin-right: auto; margin-top: 50px; text-align: center; }
#jspsych_target pre { text-align: left; }
#instructions {width: 500px; margin-left: auto; margin-right: auto; text-align: left; }

View File

@ -0,0 +1,81 @@
<!doctype html>
<html>
<head>
<title>My experiment</title>
<!-- Load jQuery -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<!-- Load the jspsych library and plugins -->
<script src="scripts/jspsych.js"></script>
<script src="scripts/plugins/jspsych-text.js"></script>
<script src="scripts/plugins/jspsych-single-stim.js"></script>
<!-- Load the stylesheet -->
<link href="experiment.css" type="text/css" rel="stylesheet"></link>
</head>
<body>
<div id="jspsych_target"></div>
</body>
<script type="text/javascript">
// Experiment parameters
var n_trials = 2;
var stimuli = ["img/congruent_left.gif", "img/congruent_right.gif", "img/incongruent_left.gif", "img/incongruent_right.gif"];
var stimuli_types = ["congruent", "congruent", "incongruent", "incongruent"];
// Experiment Instructions
var welcome_message = '<div id="instructions"><p>Welcome to the experiment. Press enter to begin.</p></div>';
var instructions = '<div id="instructions"><p>You will see a series of images that look similar to this:</p>\
<p><img src="img/incongruent_right.gif"></p><p>Press the arrow key that corresponds to the direction that\
the middle arrow is pointing. For example, in this case you would press the right arrow key.</p>\
<p>Press enter to start.</p>';
var debrief = '<div id="instructions"><p>Thank you for participating! Press enter to see the data.</p></div>';
// Generating Random Order for Stimuli
var stimuli_random_order = [];
var opt_data = [];
for (var i = 0; i < n_trials; i++) {
var random_choice = Math.floor(Math.random() * stimuli.length);
stimuli_random_order.push(stimuli[random_choice]);
opt_data.push({
"stimulus_type": stimuli_types[random_choice]
});
}
// Define experiment blocks
var instruction_block = {
type: "text",
text: [welcome_message, instructions],
timing_post_trial: 25
};
var test_block = {
type: "single-stim",
stimuli: stimuli_random_order,
choices: [37, 39],
data: opt_data
};
var debrief_block = {
type: "text",
text: [debrief]
};
jsPsych.init({
display_element: $('#jspsych_target'),
experiment_structure: [instruction_block, test_block, debrief_block],
on_finish: function(data) {
$("#jspsych_target").append($('<pre>', {
html: jsPsych.dataAsCSV()
}));
jsPsych.saveCSVdata("data.csv");
}
});
</script>
</html>

View File

@ -0,0 +1,317 @@
// jspsych.js
//
// Josh de Leeuw
// Percepts and Concepts Lab, Indiana University
//
//
(function($) {
jsPsych = (function() {
//
// public object
//
var core = {};
//
// private class variables
//
// options
var opts = {};
// exp structure
var exp_blocks = [];
// flow control
var curr_block = 0;
// everything loaded?
var initialized = false;
// target DOM element
var DOM_target;
// time that the experiment began
var exp_start_time;
//
// public methods
//
// core.init creates the experiment and starts running it
// display_element is an HTML element (usually a <div>) that will display jsPsych content
// options is an object: {
// "experiment_structure": an array of blocks specifying the experiment
// "finish": function to execute when the experiment ends
// }
//
core.init = function(options) {
// reset the key variables
exp_blocks = [];
opts = {};
initialized = false;
curr_block = 0;
// check if there is a body element on the page
var default_display_element = $('body');
if (default_display_element.length === 0) {
$(document.documentElement).append($('<body>'));
default_display_element = $('body');
}
var defaults = {
'display_element': default_display_element,
'on_finish': function(data) {
return undefined;
},
'on_trial_start': function() {
return undefined;
},
'on_trial_finish': function() {
return undefined;
},
'on_data_update': function(data) {
return undefined;
}
};
// import options
opts = $.extend({}, defaults, options);
// set target
DOM_target = opts.display_element;
run();
};
// core.data returns all of the data objects for each block as an array
// where core.data[0] = data object from block 0, etc...
core.data = function() {
var all_data = [];
for (var i = 0; i < exp_blocks.length; i++) {
all_data[i] = exp_blocks[i].data;
}
return all_data;
};
// core.dataAsCSV returns a CSV string that contains all of the data
// append_data is an option map object that will append values
// to every row. for example, if append_data = {"subject": 4},
// then a column called subject will be added to the data and
// it will always have the value 4.
core.dataAsCSV = function(append_data) {
var dataObj = core.data();
return JSON2CSV(flattenData(dataObj, append_data));
};
core.saveCSVdata = function(filename, append_data) {
var data_string = core.dataAsCSV(append_data);
saveTextToFile(data_string, filename);
};
// core.progress returns an object with the following properties
// total_blocks: the number of total blocks in the experiment
// total_trials: the number of total trials in the experiment
// current_trial_global: the current trial number in global terms
// i.e. if each block has 20 trials and the experiment is
// currently in block 2 trial 10, this has a value of 30.
// current_trial_local: the current trial number within the block.
// current_block: the current block number.
core.progress = function() {
var total_trials = 0;
for (var i = 0; i < exp_blocks.length; i++) {
total_trials += exp_blocks[i].num_trials;
}
var current_trial_global = 0;
for (var i = 0; i < curr_block; i++) {
current_trial_global += exp_blocks[i].num_trials;
}
current_trial_global += exp_blocks[curr_block].trial_idx;
var obj = {
"total_blocks": exp_blocks.length,
"total_trials": total_trials,
"current_trial_global": current_trial_global,
"current_trial_local": exp_blocks[curr_block].trial_idx,
"current_block": curr_block
};
return obj;
};
// core.startTime() returns the Date object which represents the time that the experiment started.
core.startTime = function() {
return exp_start_time;
};
//
// private functions //
//
function run() {
// take the experiment structure and dynamically create a set of blocks
exp_blocks = new Array(opts.experiment_structure.length);
// iterate through block list to create trials
for (var i = 0; i < exp_blocks.length; i++) {
var trials = jsPsych[opts["experiment_structure"][i]["type"]]["create"].call(null, opts["experiment_structure"][i]);
exp_blocks[i] = createBlock(trials);
}
// record the start time
exp_start_time = new Date();
// begin! - run the first block
exp_blocks[0].next();
}
function nextBlock() {
curr_block += 1;
if (curr_block == exp_blocks.length) {
finishExperiment();
}
else {
exp_blocks[curr_block].next();
}
}
function createBlock(trial_list) {
var block = {
trial_idx: -1,
trials: trial_list,
data: [],
next: function() {
// call on_trial_finish()
// if not very first trial
// and not the last call in this block (no trial due to advance in block)
if (typeof this.trials[this.trial_idx + 1] != "undefined" && (curr_block != 0 || this.trial_idx > -1)) {
opts.on_trial_finish();
};
this.trial_idx = this.trial_idx + 1;
var curr_trial = this.trials[this.trial_idx];
if (typeof curr_trial == "undefined") {
return this.done();
}
// call on_trial_start()
opts.on_trial_start();
do_trial(this, curr_trial);
},
writeData: function(data_object) {
this.data[this.trial_idx] = data_object;
opts.on_data_update(data_object);
},
done: nextBlock,
num_trials: trial_list.length
};
return block;
}
function finishExperiment() {
opts["on_finish"].apply((new Object()), [core.data()]);
}
function do_trial(block, trial) {
// execute trial method
jsPsych[trial.type]["trial"].call(this, DOM_target, block, trial, 1);
}
//
// A few helper functions to handle data format conversion
//
function flattenData(data_object, append_data) {
append_data = (typeof append_data === undefined) ? {} : append_data;
var trials = [];
// loop through data_object
for (var i = 0; i < data_object.length; i++) {
for (var j = 0; j < data_object[i].length; j++) {
var data = $.extend({}, data_object[i][j], append_data);
trials.push(data);
}
}
return trials;
}
// this function based on code suggested by StackOverflow users:
// http://stackoverflow.com/users/64741/zachary
// http://stackoverflow.com/users/317/joseph-sturtevant
function JSON2CSV(objArray) {
var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
var line = '';
var result = '';
var columns = [];
var i = 0;
for (var j = 0; j < array.length; j++) {
for (var key in array[j]) {
var keyString = key + "";
keyString = '"' + keyString.replace(/"/g, '""') + '",';
if ($.inArray(key,columns) == -1) {
columns[i] = key;
line += keyString;
i++;
}
}
}
line = line.slice(0, - 1);
result += line + '\r\n';
for (var i = 0; i < array.length; i++) {
var line = '';
for (var j = 0; j < columns.length; j++) {
var value = (typeof array[i][columns[j]] === 'undefined') ? '' : array[i][columns[j]];
var valueString = value + "";
line += '"' + valueString.replace(/"/g, '""') + '",';
}
line = line.slice(0, - 1);
result += line + '\r\n';
}
return result;
}
function saveTextToFile(textstr, filename) {
var blobToSave = new Blob([textstr], {
type: 'text/plain'
});
var blobURL = "";
if (typeof window.webkitURL !== 'undefined') {
blobURL = window.webkitURL.createObjectURL(blobToSave);
}
else {
blobURL = window.URL.createObjectURL(blobToSave);
}
DOM_target.append($('<a>', {
id: 'jspsych-download-as-text-link',
href: blobURL,
css: {
display: 'none'
},
download: filename,
html: 'download file'
}));
$('#jspsych-download-as-text-link')[0].click();
}
return core;
})();
})(jQuery);

View File

@ -0,0 +1,174 @@
(function( $ ) {
jsPsych.active_match = (function(){
var plugin = {};
plugin.create = function(params) {
stims = params["stimuli"];
trials = new Array(stims.length);
for(var i = 0; i < trials.length; i++)
{
trials[i] = {};
trials[i]["type"] = "active_match";
trials[i]["target_idx"] = params["target_idx"][i];
trials[i]["start_idx"] = params["start_idx"][i];
trials[i]["stimuli"] = params["stimuli"][i];
trials[i]["timing"] = params["timing"];
trials[i]["key_dec"] = params["key_dec"];
trials[i]["key_inc"] = params["key_inc"];
if(params["prompt"] != undefined){
trials[i]["prompt"] = params["prompt"];
}
if(params["data"]!=undefined){
trials[i]["data"] = params["data"][i];
}
}
return trials;
}
// data to keep track of
var responses = [];
var last_response_time = 0;
var start_time = 0;
var direction_changes = 0;
var last_response = -1;
plugin.trial = function(display_element, block, trial, part)
{
switch(part){
case 1:
// reset response variables
responses = [];
last_response_time = 0;
start_time = 0;
direction_changes = 0;
last_response = -1;
// starting new trial
start_time = (new Date()).getTime();
last_response_time = start_time;
current_idx = trial.start_idx;
// show target image
display_element.append($('<img>', {
"src": trial.stimuli[trial.target_idx],
"class": '',
"id": 'am_target'
}));
// show manipulate image
display_element.append($('<img>', {
"src": trial.stimuli[trial.start_idx],
"class": '',
"id": 'am_manipulate'
}));
// append a div for showing messages
display_element.append($('<div>', {
"id": 'am_message_box'
}));
if(trial.prompt)
{
display_element.append(trial.prompt);
}
// add function on keypress to control manipulate image
// pressing key_dec will move the index down
// pressing key_inc will move the index up
var resp_func = function(e) {
var change = 0;
var valid_response = false;
if(e.which == trial.key_dec)
{
change = -1;
valid_response = true;
} else if (e.which == trial.key_inc)
{
change = 1;
valid_response = true;
}
if(valid_response){
var resp_time = (new Date()).getTime();
var response = {"key": e.which, "rt": (resp_time-last_response_time)};
responses.push(response);
if(e.which != last_response && last_response != -1)
{
direction_changes++;
}
last_response = e.which;
last_response_time = resp_time;
var next_idx = current_idx + change;
if(next_idx < 0) {
// can't do this
if($('#am_message_box').children().length == 0)
{
$('#am_message_box').append("<p id='prompt'>Minimum value reached. Go the other direction.</p>");
}
next_idx = 0;
} else if(next_idx == trial.stimuli.length) {
// can't do this
if($('#am_message_box').children().length == 0)
{
$('#am_message_box').append("<p id='prompt'>Maximum value reached. Go the other direction.</p>");
}
next_idx = current_idx;
} else {
// update current_idx
current_idx = next_idx;
$("#am_message_box").html('');
// change the image
$("#am_manipulate").attr("src",trial.stimuli[current_idx]);
}
if(current_idx == trial.target_idx)
{
// unbind response function to prevent further change
$(document).unbind('keyup',resp_func);
// match!
plugin.trial(display_element, block, trial, part + 1);
}
}
}
$(document).keyup(resp_func);
break;
case 2:
$("#am_target").addClass('matched');
$("#am_manipulate").addClass('matched');
var key_responses_string = "";
var rt_responses_string = "";
for(var i=0;i<responses.length; i++)
{
key_responses_string = key_responses_string + responses[i].key +",";
rt_responses_string = rt_responses_string + responses[i].rt +",";
}
var trial_data = {"key_responses": key_responses_string, "rt_responses": rt_responses_string, "num_responses": responses.length, "direction_changes": direction_changes, "start_idx":trial.start_idx, "target_idx": trial.target_idx};
block.data[block.trial_idx] = $.extend({},trial_data,trial.data);
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing[1]);
break;
case 3:
display_element.html('');
setTimeout(function(){block.next();}, trial.timing[0]);
break;
}
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,355 @@
/* jspsych-adaptive-category-train.js
* Josh de Leeuw, Nov. 2012
*
* Train subjects on a category task adaptively.
* Stimuli are presented in blocks, and after a particular stimulus has been identified correctly in X
* consecutive blocks, it is removed from the training list. Training ends after all stimuli have been
* correctly identified in X consecutive blocks.
*
*/
(function($) {
jsPsych.adaptive_category_train = (function() {
var plugin = {};
plugin.create = function(params) {
trials = []; // everything is a single trial, since it is unknown how many presentations it will take.
trials[0] = {};
trials[0].type = "adaptive_category_train";
trials[0].items = params["items"]; // array of all the stimuli to learn
trials[0].correct_key = params["correct_key"]; // array of all the correct key responses
trials[0].text_answer = params["text_answer"]; // the labels of the category members
trials[0].choices = params["choices"]; // valid key responses
trials[0].correct_text = params["correct_text"]; // feedback text for correct answers.
trials[0].incorrect_text = params["incorrect_text"]; // feedback text for incorrect answers
trials[0].consecutive_correct_min = params["consecutive_correct_min"]; // how many times they have to get the correct answer in a row for a stim.
trials[0].min_percent_correct = params["min_percent_correct"] || 60; // percent correct needed to have adaptive training kick in
trials[0].min_items_per_block = params["min_items_per_block"] || 5; // when remaining items is less than this #, completed items are added back in.
trials[0].stop_training_criteria = params["stop_training_criteria"] || -1; // after this number of rounds below min_percent_correct, stop training. -1 = continue indefinitely.
// timing
trials[0].timing_display_feedback = params["timing_display_feedback"] || 1500; // default 1000ms
trials[0].timing_post_trial = params["timing_post_trial"] || 1000; // default 1000ms between trials.
// display progress ?
trials[0].show_progress = params["show_progress"] || true;
// optional parameters
if(params["data"]!=undefined){
trials[0].data = params["data"];
}
if(params["prompt"]!=undefined){
trials[0].prompt = params["prompt"];
}
return trials;
}
plugin.trial = function(display_element, block, trial, part)
{
// create a tally for each item
var all_items = [];
for(var i=0; i<trial.items.length; i++)
{
all_items.push({
"a_path": trial.items[i],
"consecutive_correct_responses": 0,
"correct_key": trial.correct_key[i],
"text_answer": trial.text_answer[i],
"choices":trial.choices,
"correct_text": trial.correct_text,
"incorrect_text": trial.incorrect_text,
"min_percent_correct": trial.min_percent_correct,
"timing_post_trial": trial.timing_post_trial,
"timing_display_feedback": trial.timing_display_feedback,
"prompt": trial.prompt,
"complete": false
});
};
// create the training controller
var controller = new TrainingControl(all_items, trial.consecutive_correct_min, trial.min_items_per_block,
trial.min_percent_correct, display_element, block, trial.show_progress, trial.timing_post_trial,
trial.stop_training_criteria);
controller.next_round(); // when this finishes, block.next() is called.
}
function TrainingControl(items, min_correct, min_per_block, min_percent_correct, display_element, block, show_progress, timing_post_trial, stop_criteria){
this.total_items = items.length;
this.remaining_items = items;
this.timing_post_trial = timing_post_trial;
this.complete_items = [];
this.curr_block = 0;
this.blocks_under_thresh = 0;
this.next_round = function() {
if(stop_criteria > -1 && this.blocks_under_thresh >=stop_criteria)
{
// end training due to failure to learn
block.next();
} else {
if(this.remaining_items.length > 0)
{
if(this.remaining_items.length < min_per_block)
{
shuffle(this.complete_items);
for( var i = 0; this.remaining_items.length < min_per_block; i++)
{
this.remaining_items.push(this.complete_items[i]);
}
}
// present remaining items in random order
shuffle(this.remaining_items);
var iterator = new TrialIterator(display_element, this.remaining_items, this, block, this.curr_block);
this.curr_block++;
var updated_trials = iterator.next(); // when this finishes, all trials are complete.
// updated_trials will have the updated consecutive correct responses
} else {
// end training
block.next();
}
}
}
this.round_complete = function(trials)
{
// check items for threshold and remove items where consecutive responses has been reached
var cont_trials = [];
this.remaining_items = trials;
var score_denominator = this.remaining_items.length;
var score_numerator = 0;
for(var i=0; i<this.remaining_items.length; i++)
{
if(this.remaining_items[i].consecutive_correct_responses > 0)
{
score_numerator++;
}
}
var percent_correct = Math.round((score_numerator / score_denominator)*100);
if(percent_correct < min_percent_correct){
this.blocks_under_thresh++;
} else {
this.blocks_under_thresh = 0;
}
for(var i=0; i<this.remaining_items.length; i++)
{
if(this.remaining_items[i].complete == false)
{
if(this.remaining_items[i].consecutive_correct_responses < min_correct)
{
cont_trials.push(this.remaining_items[i]);
} else {
if(percent_correct>=min_percent_correct){
// newly completed item
this.remaining_items[i].complete = true;
this.complete_items.push(this.remaining_items[i]);
} else {
cont_trials.push(this.remaining_items[i]);
}
}
}
}
this.remaining_items = cont_trials;
var remaining_objects = this.remaining_items.length;
var completed_objects = this.total_items - remaining_objects;
if(show_progress)
{
this.display_progress(completed_objects, remaining_objects, score_numerator, score_denominator);
} else {
// call next round
this.next_round();
}
}
this.display_progress = function(completed_objects, remaining_objects, score_numerator, score_denominator, blocks_under_criteria)
{
var completed = '';
var percent_correct = Math.round((score_numerator / score_denominator)*100);
if(percent_correct < min_percent_correct)
{
completed = '<p>You need to categorize at least '+min_percent_correct+'% of the items correctly in each round in order to make progress in training.</p>'
if(stop_criteria > -1) {
var remaining_blocks = stop_criteria - this.blocks_under_thresh;
if(remaining_blocks >= 1){
completed += '<p>If you continue to have an accuracy below '+min_percent_correct+'% for '+remaining_blocks+' more round(s) of training, then training will stop and you will not be eligible for the bonus payment.</p>'
} else {
completed += '<p>Training will now stop because your accuracy was below '+min_percent_correct+'% for '+stop_criteria+' consecutive rounds.</p>'
}
}
} else {
if(remaining_objects == 0)
{
completed = '<p>Congratulations! You have completed training.</p>';
}
else if(completed_objects > 0)
{
completed = '<p>You have correctly categorized '+completed_objects+' item(s) in '+min_correct+' consecutive rounds. You need to correctly categorize '+remaining_objects+
' more item(s) in '+min_correct+' consecutive rounds to complete training. Items that you have correctly identified in '+min_correct+' consecutive rounds will not be shown as frequently.</p>';
}
else
{
completed = '<p>Good job! You need to categorize an item correctly in '+min_correct+' consecutive rounds to finish training for that item. Once you have finished training for all items the next part of the experiment will begin.</p>';
}
}
display_element.html(
'<div id="adaptive_category_progress"><p>You correctly categorized '+percent_correct+'% of the items in that round.</p>'+completed+'<p>Press ENTER to continue.</p></div>'
);
var controller = this;
var key_listener = function(e) {
if(e.which=='13')
{
$(document).unbind('keyup',key_listener); // remove the response function, so that it doesn't get triggered again.
display_element.html(''); // clear the display
setTimeout(function(){controller.next_round();}, this.timing_post_trial); // call block.next() to advance the experiment after a delay.
}
}
$(document).keyup(key_listener);
}
}
function TrialIterator(display_element, trials, controller, block, block_idx){
this.trials = trials;
this.curr_trial = 0;
this.curr_block = block_idx;
this.next = function() {
if(this.curr_trial >= this.trials.length)
{
// call function in the controller
controller.round_complete(trials);
} else {
this.do_trial(this.trials[this.curr_trial], this.curr_trial, this.curr_block);
}
}
this.do_trial = function(trial, t_idx, b_idx)
{
// do the trial!
// show the image
display_element.append($('<img>', {
"src": trial.a_path,
"class": 'cf'
}));
display_element.append(trial.prompt);
startTime = (new Date()).getTime();
// get response
var resp_func = function(e) {
var flag = false;
var correct = false;
if(e.which==trial.correct_key) // correct category
{
flag = true;
correct = true;
}
else
{
// check if the key is any of the options, or if it is an accidental keystroke
for(var i=0;i<trial.choices.length;i++)
{
if(e.which==trial.choices[i])
{
flag = true;
correct = false;
}
}
}
if(flag)
{
// get response time
endTime = (new Date()).getTime();
rt = (endTime-startTime);
// update the consecutive correct responses
if(correct){
trial.consecutive_correct_responses++;
} else {
trial.consecutive_correct_responses = 0;
}
// store data
var trial_data = {"block": b_idx, "trial_idx": t_idx, "rt": rt, "correct": correct, "a_path": trial.a_path, "key_press": e.which, "trial_type":"adaptive_train"}
if(trial.data!="undefined"){
block.data.push($.extend({},trial_data,trial.data));
} else {
block.data.push(trial_data);
}
$(document).unbind('keyup',resp_func);
display_element.html('');
show_feedback(correct, e.data.iterator_object);
}
}
$(document).keyup({"iterator_object":this},resp_func);
// provide feedback
function show_feedback(is_correct, iterator_object){
display_element.append($('<img>', {
"src": trial.a_path,
"class": 'cf'
}));
// give feedback
var atext = "";
if(is_correct)
{
atext = trial.correct_text.replace("&ANS&", trial.text_answer);
} else {
atext = trial.incorrect_text.replace("&ANS&", trial.text_answer);
}
display_element.append(atext);
setTimeout(function(){finish_trial(iterator_object);}, trial.timing_display_feedback);
}
function finish_trial(iterator_object){
display_element.html('');
setTimeout(function(){
iterator_object.curr_trial++;
iterator_object.next();
}, trial.timing_post_trial);
}
}
}
function shuffle(array) {
var tmp, current, top = array.length;
if(top) while(--top) {
current = Math.floor(Math.random() * (top + 1));
tmp = array[current];
array[current] = array[top];
array[top] = tmp;
}
return array;
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,223 @@
(function( $ ) {
jsPsych.ballistic_match = (function(){
var plugin = {};
plugin.create = function(params) {
stims = params["stimuli"];
trials = new Array(stims.length);
for(var i = 0; i < trials.length; i++)
{
trials[i] = {};
trials[i]["type"] = "ballistic_match";
trials[i]["target_idx"] = params["target_idx"][i];
trials[i]["start_idx"] = params["start_idx"][i];
trials[i]["stimuli"] = params["stimuli"][i];
trials[i]["timing"] = params["timing"];
trials[i]["key_dec"] = params["key_dec"];
trials[i]["key_inc"] = params["key_inc"];
trials[i]["animate_frame_time"] = params["animate_frame_time"] || 100;
if(params["prompt"] != undefined){
trials[i]["prompt"] = params["prompt"];
}
if(params["data"]!=undefined){
trials[i]["data"] = params["data"][i];
}
}
return trials;
}
var change = 0; // which direction they indicated the stim should move.
var start_time;
var end_time;
plugin.trial = function(display_element, block, trial, part)
{
switch(part){
case 1:
// starting new trial
start_time = (new Date()).getTime();
change = 0;
// show manipulate image
display_element.append($('<img>', {
"src": trial.stimuli[trial.start_idx],
"class": 'bm_img',
"id": 'bm_manipulate'
}));
// show target image
display_element.append($('<img>', {
"src": trial.stimuli[trial.target_idx],
"class": 'bm_img',
"id": 'bm_target'
}));
if(trial.prompt)
{
display_element.append(trial.prompt);
}
// categorize the image.
var resp_func = function(e) {
var valid_response = false;
if(e.which == trial.key_dec)
{
change = -1;
valid_response = true;
} else if (e.which == trial.key_inc)
{
change = 1;
valid_response = true;
}
if(valid_response){
end_time = (new Date()).getTime();
plugin.trial(display_element,block,trial,part+1);
$(document).unbind('keyup', resp_func);
}
}
$(document).keyup(resp_func);
break;
case 2:
// clear everything
display_element.html('');
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing[1]);
break;
case 3:
// draw trajectory
draw_trajectory(display_element,
trial.stimuli[trial.target_idx],
trial.stimuli[trial.start_idx],
trial.target_idx/(trial.stimuli.length-1),
trial.start_idx/(trial.stimuli.length-1));
display_element.append($('<div>',{
"id":"bm_feedback",
}));
if(change>0) {
$("#bm_feedback").html('<p>You said increase.</p>');
} else {
$("#bm_feedback").html('<p>You said decrease.</p>');
}
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing[1]*3);
break;
case 4:
var curr_loc = trial.start_idx
animate_interval = setInterval(function(){
// clear everything
display_element.html('');
// draw trajectory
draw_trajectory(display_element,
trial.stimuli[trial.target_idx],
trial.stimuli[curr_loc],
trial.target_idx/(trial.stimuli.length-1),
curr_loc/(trial.stimuli.length-1));
curr_loc += change;
if(curr_loc - change == trial.target_idx || curr_loc < 0 || curr_loc == trial.stimuli.length)
{
clearInterval(animate_interval);
var correct = false;
if(change > 0 && trial.start_idx < trial.target_idx) { correct = true; }
if(change < 0 && trial.start_idx > trial.target_idx) { correct = true; }
display_element.append($('<div>',{
"id":"bm_feedback",
}));
if(correct){
$("#bm_feedback").html('<p>Correct!</p>');
} else {
$("#bm_feedback").html('<p>Wrong.</p>');
}
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing[1]*3);
}
}, trial.animate_frame_time);
break;
case 5:
display_element.html('');
var correct = false;
if(change > 0 && trial.start_idx < trial.target_idx) { correct = true; }
if(change < 0 && trial.start_idx > trial.target_idx) { correct = true; }
var trial_data = {"start_idx":trial.start_idx, "target_idx": trial.target_idx, "correct": correct, "rt": (end_time-start_time)};
block.data[block.trial_idx] = $.extend({},trial_data,trial.data);
setTimeout(function(){block.next();}, trial.timing[0]);
break;
}
}
function draw_trajectory(display_element,target_img, moving_img, target_loc_percent, marker_loc_percent)
{
// display the image as it morphs
display_element.append($('<img>',{
"src": moving_img,
"id": "moving_image"
}));
// show the linear trajectory below
display_element.append($('<div>', {
"id": "trajectory"}));
$("#trajectory").append($('<div>', {
"id": "line"}));
// display the images on the trajectory
$("#trajectory").append($('<div>', {
"id": "target_flag"}));
$("#target_flag").append($('<div>', {
"id": "target_dot"}));
$("#target_flag").append($('<div>', {
"id": "target_words"}));
$("#target_words").html("<p>Target Cell</p>");
$("#trajectory").append($('<div>', {
"id": "marker_flag"}));
$("#marker_flag").append($('<div>', {
"id": "marker_dot"}));
$("#marker_flag").append($('<div>', {
"id": "marker_words"}));
$("#marker_words").html("<p>Above Cell</p>");
// label the trajectory line
$("#trajectory").append($('<span>', {
"id": "left_label"}));
$("#trajectory").append($('<span>', {
"id": "right_label"}));
$("#left_label").html("Less Chemical X");
$("#right_label").html("More Chemical X");
// set the location of the flags on the line
var dot_width = parseInt($("#marker_dot").css('width'));
var line_width = parseInt($("#line").css('width'));
var target_flag_left = (line_width- dot_width) * target_loc_percent;
var marker_flag_left = (line_width- dot_width) * marker_loc_percent;
$("#marker_flag").css('left', marker_flag_left);
$("#target_flag").css('left', target_flag_left);
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,227 @@
(function( $ ) {
jsPsych.ballistic_match = (function(){
var plugin = {};
plugin.create = function(params) {
stims = params["stimuli"];
trials = new Array(stims.length);
for(var i = 0; i < trials.length; i++)
{
trials[i] = {};
trials[i]["type"] = "ballistic_match";
trials[i]["target_idx"] = params["target_idx"][i];
trials[i]["start_idx"] = params["start_idx"][i];
trials[i]["stimuli"] = params["stimuli"][i];
trials[i]["timing"] = params["timing"];
trials[i]["key_dec"] = params["key_dec"];
trials[i]["key_inc"] = params["key_inc"];
trials[i]["animate_frame_time"] = params["animate_frame_time"] || 100;
if(params["prompt"] != undefined){
trials[i]["prompt"] = params["prompt"];
}
if(params["data"]!=undefined){
trials[i]["data"] = params["data"][i];
}
}
return trials;
}
var change = 0; // which direction they indicated the stim should move.
var start_time;
var end_time;
plugin.trial = function(display_element, block, trial, part)
{
switch(part){
case 1:
// starting new trial
start_time = (new Date()).getTime();
change = 0;
// show manipulate image
display_element.append($('<img>', {
"src": trial.stimuli[trial.start_idx],
"class": 'bm_img',
"id": 'bm_manipulate'
}));
// show target image
display_element.append($('<img>', {
"src": trial.stimuli[trial.target_idx],
"class": 'bm_img',
"id": 'bm_target'
}));
if(trial.prompt)
{
display_element.append(trial.prompt);
}
// categorize the image.
var resp_func = function(e) {
var valid_response = false;
if(e.which == trial.key_dec)
{
change = -1;
valid_response = true;
} else if (e.which == trial.key_inc)
{
change = 1;
valid_response = true;
}
if(valid_response){
end_time = (new Date()).getTime();
plugin.trial(display_element,block,trial,part+1);
$(document).unbind('keyup', resp_func);
}
}
$(document).keyup(resp_func);
break;
case 2:
// clear everything
display_element.html('');
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing[1]);
break;
case 3:
// draw trajectory
draw_trajectory(display_element,
trial.stimuli[trial.target_idx],
trial.stimuli[trial.start_idx],
trial.target_idx/(trial.stimuli.length-1),
trial.start_idx/(trial.stimuli.length-1));
display_element.append($('<div>',{
"id":"bm_feedback",
}));
if(change>0) {
$("#bm_feedback").html('<p>You said increase.</p>');
} else {
$("#bm_feedback").html('<p>You said decrease.</p>');
}
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing[1]*3);
break;
case 4:
var curr_loc = trial.start_idx
animate_interval = setInterval(function(){
// clear everything
display_element.html('');
// draw trajectory
draw_trajectory(display_element,
trial.stimuli[trial.target_idx],
trial.stimuli[curr_loc],
trial.target_idx/(trial.stimuli.length-1),
curr_loc/(trial.stimuli.length-1));
curr_loc += change;
if(curr_loc - change == trial.target_idx || curr_loc < 0 || curr_loc == trial.stimuli.length)
{
clearInterval(animate_interval);
var correct = false;
if(change > 0 && trial.start_idx < trial.target_idx) { correct = true; }
if(change < 0 && trial.start_idx > trial.target_idx) { correct = true; }
display_element.append($('<div>',{
"id":"bm_feedback",
}));
if(correct){
$("#bm_feedback").html('<p>Correct!</p>');
} else {
$("#bm_feedback").html('<p>Wrong.</p>');
}
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing[1]*3);
}
}, trial.animate_frame_time);
break;
case 5:
display_element.html('');
var correct = false;
if(change > 0 && trial.start_idx < trial.target_idx) { correct = true; }
if(change < 0 && trial.start_idx > trial.target_idx) { correct = true; }
var trial_data = {"start_idx":trial.start_idx, "target_idx": trial.target_idx, "correct": correct, "rt": (end_time-start_time)};
block.data[block.trial_idx] = $.extend({},trial_data,trial.data);
setTimeout(function(){block.next();}, trial.timing[0]);
break;
}
}
function draw_trajectory(display_element,target_img, moving_img, target_loc_percent, moving_loc_percent)
{
display_element.append($('<div>', {
"id": "message_holder"}));
$("#message_holder").append($('<p id="left">Less Chemical X</p>'));
$("#message_holder").append($('<p id="right">More Chemical X</p>'));
$("#message_holder").append($('<img>',{
"src":"img/400arrow.gif",
"id":"arrow"
}));
// display the images on the trajectory
display_element.append($('<div>',{
"id": "bm_trajectory",
"css": {
"position":"relative"
}
}));
$("#bm_trajectory").append($('<img>',{
"src":target_img,
"id": "bm_target",
"css": {
"position":"absolute",
}
}));
var image_width = parseInt($("#bm_target").css('width'));
var image_height = parseInt($("#bm_target").css('height'));
var container_width = parseInt($("#bm_trajectory").css('width'));
var target_left = (container_width - image_width) * target_loc_percent;
var moving_left = (container_width - image_width) * moving_loc_percent;
$("#bm_target").css('left', target_left);
$("#bm_target").css('top', image_height);
$("#bm_trajectory").append($('<img>',{
"src":moving_img,
"id": "bm_moving",
"css": {
"position":"absolute",
"left": moving_left,
"top": 0
}
}));
$("#bm_trajectory").append($(
'<div>',
{
"id": "target_flag",
"css": {
"position":"absolute",
"left": target_left+(image_width/2)-40,
"bottom": "-10px",
"background-color": "#cccccc",
"border": "1px solid #999999",
"width": 80,
"height": 20
}
}
));
$("#target_flag").html('<p>TARGET</p>');
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,160 @@
// jsPsych plugin for showing animations
// Josh de Leeuw
(function( $ ) {
jsPsych.categorize_animation = (function(){
var plugin = {};
plugin.create = function(params) {
stims = params["stimuli"];
trials = new Array(stims.length);
for(var i = 0; i < trials.length; i++)
{
trials[i] = {};
trials[i]["type"] = "categorize_animation";
trials[i]["stims"] = stims[i];
trials[i]["frame_time"] = params["frame_time"];
trials[i]["timing"] = params["timing"];
trials[i]["key_answer"] = params["key_answer"][i];
trials[i]["text_answer"] = params["text_answer"][i];
trials[i]["choices"] = params["choices"];
trials[i]["correct_text"] = params["correct_text"];
trials[i]["incorrect_text"] = params["incorrect_text"];
trials[i]["allow_response_before_complete"] = params["allow_response_before_complete"] || false;
trials[i]["reps"] = params["reps"] || -1; // default of -1, which allows indefinitely
if(params["prompt"] != undefined){
trials[i]["prompt"] = params["prompt"];
}
if(params["data"]!=undefined){
trials[i]["data"] = params["data"][i];
}
}
return trials;
}
plugin.trial = function(display_element, block, trial, part)
{
var animate_frame = -1;
var reps = 0;
var showAnimation = true;
var responded = false;
var timeoutSet = false;
switch(part)
{
case 1:
var startTime = (new Date()).getTime();
// show animation
animate_interval = setInterval(function(){
display_element.html(""); // clear everything
animate_frame++;
if(animate_frame == trial.stims.length)
{
animate_frame = 0;
reps++;
// check if reps complete //
if(trial.reps != -1 && reps >= trial.reps) {
// done with animation
showAnimation = false;
}
}
if( showAnimation ) {
display_element.append($('<img>', {
"src": trial.stims[animate_frame],
"class": 'animate'
}));
}
if(!responded && trial.allow_response_before_complete) {
// in here if the user can respond before the animation is done
if(trial.prompt != undefined) { display_element.append(trial.prompt); }
} else if(!responded) {
// in here if the user has to wait to respond until animation is done.
// if this is the case, don't show the prompt until the animation is over.
if( !showAnimation )
{
if(trial.prompt != undefined) { display_element.append(trial.prompt); }
}
} else {
// user has responded if we get here.
// show feedback
var feedback_text = "";
if(block.data[block.trial_idx]["correct"])
{
feedback_text = trial.correct_text.replace("&ANS&", trial.text_answer);
} else {
feedback_text = trial.incorrect_text.replace("&ANS&", trial.text_answer);
}
display_element.append(feedback_text);
// set timeout to clear feedback
if(!timeoutSet)
{
timeoutSet = true;
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing[0]);
}
}
}, trial.frame_time);
// attach response function
var resp_func = function(e) {
if(!trial.allow_response_before_complete && showAnimation)
{
return false;
}
var flag = false; // valid keystroke?
var correct = false; // correct answer?
if(e.which==trial.key_answer) // correct category
{
flag = true;
correct = true;
}
else
{
// check if the key is any of the options, or if it is an accidental keystroke
for(var i=0;i<trial.choices.length;i++)
{
if(e.which==trial.choices[i])
{
flag = true;
correct = false;
}
}
}
if(flag) // if keystroke is one of the choices
{
responded = true;
endTime = (new Date()).getTime();
rt = (endTime-startTime);
var trial_data = {"rt": rt, "correct": correct, "key_press": e.which}
block.data[block.trial_idx] = $.extend({},trial_data,trial.data);
$(document).unbind('keyup',resp_func);
}
}
$(document).keyup(resp_func);
break;
case 2:
clearInterval(animate_interval); // stop animation!
display_element.html(''); // clear everything
setTimeout(function(){ block.next(); }, trial.timing[1]);
break;
}
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,182 @@
// timing parameters: [length to show feedback, intertrial gap, optional length to display target]
// if optional length to display target is missing, then target is displayed until subject responds.
//TODO
// option to keep stim on screen during feedback
// way to provide corrective feedback
(function( $ ) {
jsPsych.categorize_multi = (function(){
var plugin = {};
plugin.create = function(params) {
cf_stims = params["stimuli"];
trials = new Array(cf_stims.length);
for(var i = 0; i < trials.length; i++)
{
trials[i] = {};
trials[i]["type"] = "categorize_multi";
trials[i]["a_path"] = cf_stims[i];
trials[i]["choices"] = params["choices"];
trials[i]["answer_idx"] = params["answer_idx"][i];
trials[i]["text_answer"] = params["text_answer"][i];
trials[i]["correct_text"] = params["correct_text"];
trials[i]["incorrect_text"] = params["incorrect_text"];
trials[i]["show_stim_feedback"] = params["show_stim_feedback"] || true;
// timing params
trials[i]["timing_length_of_feedback"] = params["timing_length_of_feedback"] || 2000;
// opt params
if(params["prompt"] != undefined){
trials[i]["prompt"] = params["prompt"];
}
if(params["data"]!=undefined){
trials[i]["data"] = params["data"][i];
}
}
return trials;
}
// to save correct_answers between iterations of trial method...
var correct_answers = [];
plugin.trial = function(display_element, block, trial, part)
{
switch(part){
case 1:
// show image
display_element.append($('<img>', {
"src": trial.a_path,
"class": 'cm'
}));
// hide image if the timing param is set.
if(trial.timing_show_image > 0)
{
setTimeout(function(){
$('.cm').css('visibility', 'hidden');
}, trial.timing_show_image);
}
// show prompt
display_element.append(trial.prompt);
// start recording for RT
startTime = (new Date()).getTime();
// display button choices
// for each SET of choices
for(var i = 0; i<trial.choices.length; i++)
{
// add a DIV
display_element.append($('<div>', {
"id": "cm_"+i
}));
// for each INDIVIDUAL choice
for(var j = 0; j < trial.choices[i].length; j++)
{
// add a RADIO button
$('#cm_'+i).append($('<input>', {
"type": "radio",
"name": "category_"+i,
"value": trial.choices[i][j],
"id": "cat_"+i+"_"+j
}));
$('#cm_'+i).append('<label>'+trial.choices[i][j]+'</label>');
}
}
// add a button to hit when done.
display_element.append($('<button>', {
"type": "button",
"value": "done",
"name": "Next",
"id": "nextBtn",
"html": "Submit Answer"
}));
// add response function to the button.
$('#nextBtn').click(function(){
var correct_overall = true;
var string_answers = "";
correct_answers = [];
for(var i=0; i<trial.answer_idx.length; i++)
{
var corr_choice = trial.answer_idx[i];
if($('#cat_'+i+'_'+corr_choice).is(':checked'))
{
correct_answers.push(true);
string_answers = string_answers + "1";
} else {
correct_answers.push(false);
correct_overall = false;
string_answers = string_answers + "0";
}
}
// measure RT
endTime = (new Date()).getTime();
rt = (endTime-startTime);
// save data
var trial_data = {"rt": rt, "correct": correct_overall, "a_path": trial.a_path, "cat_answers": string_answers}
block.data[block.trial_idx] = $.extend({},trial_data,trial.data);
// clear everything
display_element.html('');
plugin.trial(display_element, block, trial, part + 1);
});
break;
case 2:
// show image
display_element.append($('<img>', {
"src": trial.a_path,
"class": 'cm'
}));
// show prompt
display_element.append(trial.prompt);
// give feedback
var atext = "";
for(var i=0; i<correct_answers.length; i++)
{
// add a DIV
display_element.append($('<div>', {
"id": "cm_"+i
}));
var text_to_add = "";
if(correct_answers[i])
{
text_to_add = trial.correct_text.replace("&ANS&", trial.text_answer[i]);
$('#cm_'+i).addClass('correct');
} else {
text_to_add = trial.incorrect_text.replace("&ANS&", trial.text_answer[i]);
$('#cm_'+i).addClass('incorrect');
}
$('#cm_'+i).append(text_to_add);
}
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing_length_of_feedback); // fix timing?
break;
case 3:
display_element.html('');
setTimeout(function(){block.next()},1000);
break;
}
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,186 @@
(function( $ ) {
jsPsych.paint = (function(){
var plugin = {};
// private functions
function colorPixel(ctx, x, y, fresh)
{
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "rgba(0,0,0, 1.0)";
//ctx.fillRect(x,y,10,10);
//ctx.arc(x,y,10,0,Math.Pi*2,true);
//ctx.fill();
ctx.lineWidth = 30;
ctx.lineCap = ctx.lineJoin = 'round';
ctx.strokeStyle = "rgba(0,0,0,1.0)";
if(fresh){
ctx.beginPath();
ctx.moveTo(x+0.01,y);
}
ctx.lineTo(x,y);
ctx.stroke();
}
plugin.create = function(params) {
stims = params["stimuli"];
trials = new Array(stims.length);
for(var i = 0; i < trials.length; i++)
{
trials[i] = {};
trials[i]["type"] = "paint";
trials[i]["a_path"] = stims[i];
trials[i]["height"] = params["height"];
trials[i]["width"] = params["width"];
trials[i]["timing"] = params["timing"];
if(params["data"]!=undefined){
trials[i]["data"] = params["data"][i];
}
if(params["prompt"]!=undefined){
trials[i]["prompt"] = params["prompt"];
}
}
return trials;
}
plugin.trial = function(display_element, block, trial, part)
{
switch(part){
case 1:
p1_time = (new Date()).getTime();
display_element.append($('<div>', {
"id": 'paintbox',
"class": 'paint'}));
$('#paintbox').css({
'position': 'relative',
'width': trial.width,
'height': trial.height});
$('#paintbox').append($('<img>', {
"src": trial.a_path,
"class": 'paint',
"css": {
'width':trial.width,
'height':trial.height,
'position': 'absolute',
'top': '0',
'left': '0',
'z-index': '0'
}
}));
$('#paintbox').append($('<canvas>', {
"id": "paintcanvas",
"class": "paint",
"css": {
'position': 'absolute',
'top': '0',
'left': '0',
'z-index': '1',
'cursor': 'crosshair'
}
}));
$('#paintcanvas').attr('width',trial.width);
$('#paintcanvas').attr('height',trial.height);
var canvas = document.getElementById('paintcanvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = "rgba(0,0,0,0.8)";
ctx.fillRect(0,0,trial.width,trial.height);
var paint = false;
document.onselectstart = function(){
return false;
}
$('#paintcanvas').mousedown(function(e){
var mouseX = e.pageX - $('#paintbox').offset().left;
var mouseY = e.pageY - $('#paintbox').offset().top;
paint = true;
colorPixel(ctx, mouseX, mouseY, true);
});
$('#paintcanvas').mousemove(function(e){
var mouseX = e.pageX - $('#paintbox').offset().left;
var mouseY = e.pageY - $('#paintbox').offset().top;
if(paint){
colorPixel(ctx, mouseX, mouseY, false);
}
});
$(document).mouseup(function(e){
paint = false;
});
display_element.append($('<button>',{
'id':'done',
'class':'paint'}));
$('#done').html('Done');
$('#done').click(function(){
plugin.trial(display_element,block,trial,part+1);
});
display_element.append($('<button>',{
'id':'reset',
'class':'paint'}));
$('#reset').html('Reset');
$('#reset').click(function(){
ctx.clearRect(0,0,trial.width,trial.height);
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = "rgba(0,0,0,0.8)";
ctx.fillRect(0,0,trial.width,trial.height);
});
if(trial.prompt){
display_element.append(trial.prompt);
}
break;
case 2:
var canvas = document.getElementById('paintcanvas');
var ctx = canvas.getContext('2d');
var img = ctx.getImageData(0,0,trial.width,trial.height);
var pix = img.data;
var tdata = new Array(trial.width*trial.height);
var data_string = "";
//tdata[0] = new Array();
for(var i=0, j=0, n=pix.length; i<n; i+=4, j++)
{
tdata[j] = (pix[i+3] == 0)
if(pix[i+3] == 0)
{
data_string = data_string+"1";
} else {
data_string = data_string+"0";
}
}
block.data[block.trial_idx] = $.extend({},{"a_path": trial.a_path,"pixels": data_string},trial.data);
display_element.html('');
setTimeout(function(){block.next();}, trial.timing[0]);
break;
}
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,144 @@
// Josh de Leeuw
// Nov. 2012
(function( $ ) {
jsPsych.samedifferentloc = (function(){
var plugin = {};
plugin.create = function(params) {
var stims = params["stimuli"];
trials = new Array(stims.length);
for(var i = 0; i < trials.length; i++)
{
trials[i] = {};
trials[i]["type"] = "samedifferentloc";
trials[i]["a_path"] = sd_stims[i][0];
trials[i]["b_path"] = sd_stims[i][1];
trials[i]["mask_path"] = params["mask_path"];
trials[i]["a_x_offset"] = params["a_x_offset"][i];
trials[i]["a_y_offset"] = params["a_y_offset"][i];
trials[i]["b_x_offset"] = params["b_x_offset"][i];
trials[i]["b_y_offset"] = params["b_y_offset"][i];
trials[i]["answer"] = params["answer"][i];
trials[i]["same_key"] = params["same_key"] || 80;
trials[i]["different_key"] = params["different_key"] || 81;
// timing parameters
trials[i]["timing_first_img"] = params["timing_first_img"] || 1500;
trials[i]["timing_mask"] = params["timing_mask"] || 500;
trials[i]["timing_second_img"] = params["timing_second_img"] || 1000;
trials[i]["timing_fixation"] = params["timing_fixation"] || 3000;
// optional parameters
if(params["prompt"] != undefined){
trials[i]["prompt"] = params["prompt"];
}
if(params["data"]!=undefined){
trials[i]["data"] = params["data"][i];
}
}
return trials;
}
plugin.trial = function(display_element, block, trial, part)
{
switch(part){
case 1:
display_element.append($('<div>', {
"id": 'sdl_img_container',
"css": {
"position": 'relative'
}
}));
$("#sdl_img_container").append($('<img>', {
"src": trial.a_path,
"class": 'sdl',
"css": {
"position": 'absolute',
"top": trial.a_y_offset,
"left": trial.a_x_offset
}
}));
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing_first_img);
break;
case 2:
$('.sd1').remove();
$("#sdl_img_container").append($('<img>', {
"src": trial.mask_path,
"class": 'sdl'
}));
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing_mask);
break;
case 3:
$('.sd1').remove();
$("#sdl_img_container").append($('<img>', {
"src": trial.b_path,
"class": 'sdl',
"css": {
"position": 'absolute',
"top": trial.b_y_offset,
"left": trial.b_x_offset
}
}));
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing_second_img);
break;
case 4:
$('.sd1').remove();
$("#sdl_img_container").append($('<img>', {
"src": trial.fixation_path,
"class": 'sdl'
}));
startTime = (new Date()).getTime();
var correct = false;
var response = false;
var resp_func = function(e) {
var flag = false;
if(e.which==trial.same_key)
{
flag = true;
if(trial.answer == "same") { correct = true; }
} else if(e.which==trial.different_key)
{
flag = true;
if(trial.answer == "different"){ correct = true; }
}
if(flag)
{
endTime = (new Date()).getTime();
rt = (endTime-startTime);
$(document).unbind('keyup',resp_func);
response = true;
}
}
$(document).keyup(resp_func);
var finish_func = function() {
if(!response)
{
rt = -1;
}
var trial_data = {"rt": rt, "correct": correct, "a_path": trial.a_path, "b_path": trial.b_path, "key_press": e.which,
"a_x_loc": trial.a_x_offset,"a_y_loc": trial.a_y_offset,"b_x_loc": trial.b_x_offset, "b_y_loc": trial.b_y_offset };
block.data[block.trial_idx] = $.extend({},trial_data,trial.data);
display_element.html('');
block.next();
}
setTimeout(finish_func, trial.timing_fixation);
break;
}
}
return plugin;
})();
}) (jQuery);

View File

@ -0,0 +1,248 @@
/* jspsych-staircase-categorize.js
* Josh de Leeuw, Sep. 2013
*
*
*
*/
(function($) {
jsPsych.staircase_categorize = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = []; // everything is a single trial, since it is unknown how many presentations it will take.
trials[0] = {};
trials[0].type = "staircase_categorize";
// items structure is four nested arrays. top level array is separate staircases that should be interleaved.
// second level array is difficulty. third arrays are sets of options. inner most array are stimuli of equivalent
// difficulty and category.
trials[0].items = params.items;
// two level nested array. top level is separate staircases.
// second (inner) level is list of key responses in order that stimuli are ordered for third level of items array.
trials[0].choices = params.choices;
trials[0].n_up = params.n_up || 1;
trials[0].n_down = params.n_down || 1;
trials[0].n_turns = params.n_turns || 10;
trials[0].n_turns_average = params.n_turns_average || 6;
// timing
trials[0].timing_post_trial = params.timing_post_trial || 1000; // default 1000ms between trials.
trials[0].timing_item = params.timing_item || 1000;
// optional parameters
if (params.data !== undefined) {
trials[0].data = params.data;
}
if (params.prompt !== undefined) {
trials[0].prompt = params.prompt;
}
return trials;
};
plugin.trial = function(display_element, block, trial, part) {
var interleaver = new StaircaseInterleaver(display_element, trial, block);
interleaver.start();
};
function StaircaseInterleaver(display_element, trial, block) {
this.staircases = [];
for(var i=0; i<trial.items.length; i++)
{
var s_trial = $.extend(true, {}, trial);
s_trial.items = s_trial.items[i];
s_trial.choices = s_trial.choices[i];
var staircase = new StaircaseController(display_element, s_trial, this, block);
this.staircases.push(staircase);
}
this.start = function() {
this.next_trial();
};
this.next_trial = function() {
var eligible_staircases = [];
for (var i = 0; i < this.staircases.length; i++) {
if (this.staircases[i].is_complete === false) {
eligible_staircases.push(this.staircases[i]);
}
}
if (eligible_staircases.length === 0) {
this.finish();
}
else {
var which_staircase = Math.floor(Math.random() * eligible_staircases.length);
eligible_staircases[which_staircase].next_trial();
}
};
this.finish = function()
{
// end the whole staircase process
block.next();
}
}
function StaircaseController(display_element, trial, interleaver, block) {
this.current_item = 0;
this.total_turns = 0;
this.current_direction = 1;
this.turns = [];
this.consecutive_correct = 0;
this.consecutive_incorrect = 0;
this.items = trial.items;
this.is_complete = false;
this.interleaver = interleaver;
this.next_trial = function() {
var item_set = this.items[this.current_item];
// pick which category to show randomly
var which_category = Math.floor(Math.random()*item_set.length);
// pick which exemplar to show randomly
var which_exemplar = Math.floor(Math.random()*item_set[which_category].length);
var this_trial = {
a_path: item_set[which_category][which_exemplar],
choices: trial.choices,
correct_response: trial.choices[which_category]
};
this.do_trial(this_trial);
};
this.finish_trial = function(correct) {
// change the counters
if (correct) {
this.consecutive_incorrect = 0;
this.consecutive_correct++;
}
else {
this.consecutive_incorrect++;
this.consecutive_correct = 0;
}
var new_direction;
if (correct) {
if (this.consecutive_correct >= trial.n_up) {
new_direction = 1; // make it harder
this.consecutive_correct = 0; // reset this because we changed difficulty.
}
else {
// do the same
new_direction = 0;
}
}
else {
if (this.consecutive_incorrect >= trial.n_down) {
// make it easier
new_direction = -1;
this.consecutive_incorrect = 0; // reset because we changed difficulty
}
else {
// do the same
new_direction = 0;
}
}
// if there is a turn, add it.
if (new_direction !== 0) {
if (this.current_direction != new_direction) {
this.turns.push(this.current_item);
}
this.current_direction = new_direction;
}
// check if we are done
if (this.turns.length >= trial.n_turns ){
this.is_complete = true;
}
// now figure out what the next item is
this.current_item = this.current_item + new_direction;
if (this.current_item < 0) {
this.current_item = 0;
}
if (this.current_item >= trial.items.length) {
this.current_item = trial.items.length - 1;
}
// tell the interleaver that we are done
this.interleaver.next_trial();
};
this.do_trial = function(this_trial) {
var trial_complete = false;
// show image
display_element.append($('<img>', {
"src": this_trial.a_path,
"class": 'staircase-categorize'
}));
if (trial.prompt) {
display_element.append(trial.prompt);
}
var startTime = (new Date()).getTime();
// set timer to remove image
if (trial.timing_item > 0) {
setTimeout(function() {
if (!trial_complete) {
$('.staircase-categorize').css('visibility', 'hidden');
}
}, trial.timing_item);
}
var resp_func = function(e) {
var flag = false;
var correct = false;
if ($.inArray(e.which, this_trial.choices) > -1) {
flag = true;
correct = (e.which == this_trial.correct_response);
}
if (flag) {
var endTime = (new Date()).getTime();
var rt = (endTime - startTime);
var t_idx = block.data.length;
var trial_data = {
"trial_type": "staircase-categorize",
"trial_index": t_idx,
"rt": rt,
"correct": correct,
"a_path": this_trial.a_path,
"staircase_index": e.data.iterator_object.current_item,
"staircase_direction": e.data.iterator_object.current_direction,
"key_press": e.which
};
block.data.push($.extend({},trial_data,trial.data));
$(document).unbind('keyup', resp_func);
display_element.html(''); // remove all
trial_complete = true;
setTimeout(function() {
e.data.iterator_object.finish_trial(correct);
}, trial.timing_post_trial);
}
}
$(document).keyup({iterator_object: this}, resp_func);
}
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,223 @@
/* jspsych-staircase.js
* Josh de Leeuw, Sep. 2013
*
*
*
*/
(function($) {
jsPsych.staircase_xab = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = []; // everything is a single trial, since it is unknown how many presentations it will take.
trials[0] = {};
trials[0].type = "staircase_xab";
trials[0].items = params.items;
trials[0].foils = params.foils;
trials[0].left_key = params.left_key || 81; // array of all the correct key responses
trials[0].right_key = params.right_key || 80; // valid key responses
trials[0].n_up = params.n_up || 1;
trials[0].n_down = params.n_down || 1;
trials[0].n_turns = params.n_turns || 10;
trials[0].n_turns_average = params.n_turns_average || 6;
// timing
trials[0].timing_post_trial = params.timing_post_trial || 1000; // default 1000ms between trials.
trials[0].timing_x = params.timing_x || 1000;
trials[0].timing_xab_gap = params.timing_xab_gap || 1000;
trials[0].timing_ab = params.timing_ab || -1; // default is indefinitely.
// optional parameters
if (params.data !== undefined) {
trials[0].data = params.data;
}
if (params.prompt !== undefined) {
trials[0].prompt = params.prompt;
}
return trials;
};
plugin.trial = function(display_element, block, trial, part) {
var current_item = 0; // start with the easiest item
var total_turns = 0; // counter for keeping track of how many turns
var current_direction = 1; // which way are we currently moving in item set; +1 = harder; -1 = easier
var turns = []; // store the items where we turned
var consecutive_correct = 0;
var consecutive_incorrect = 0;
// do a trial, check if we've hit total_turns, if yes then end, if not do another trial.
var staircase_xab_trial_complete = false;
function next_trial() {
if (total_turns < trial.n_turns) {
// do trial
var this_trial = { };
do_trial(display_element, block, this )
}
else {
// end run
}
}
function finish_trial(correct) {
// change the counters
if (correct) {
consecutive_incorrect = 0;
consecutive_correct++;
}
else {
consecutive_incorrect++;
consecutive_correct = 0;
}
var new_direction;
if (correct) {
if (consecutive_correct >= trial.n_up) {
new_direction = 1; // make it harder
consecutive_correct = 0; // reset this because we changed difficulty.
}
else {
// do the same
new_direction = 0;
}
}
else {
if (consecutive_incorrect >= trial.n_down) {
// make it easier
new_direction = -1;
consecutive_incorrect = 0; // reset because we changed difficulty
}
else {
// do the same
new_direction = 0;
}
}
// if there is a turn, add it.
if (new_direction !== 0) {
if (current_direction != new_direction) {
turns.push(current_item);
}
current_direction = new_direction;
}
// now figure out what the next item is
current_item = current_item + new_direction;
if (current_item < 0) {
current_item = 0;
}
if (current_item >= trial.items.length) {
current_item = trial.items.length - 1;
}
}
function do_trial(display_element, block, trial, part) {
switch (part) {
case 1:
staircase_xab_trial_complete = false;
display_element.append($('<img>', {
"src": trial.a_path,
"class": 'staircase-xab'
}));
setTimeout(function() {
do_trial(display_element, block, trial, part + 1)
}, trial.timing_x);
break;
case 2:
$('.staircase-xab').remove();
setTimeout(function() {
do_trial(display_element, block, trial, part + 1)
}, trial.timing_xab_gap);
break;
case 3:
var startTime = (new Date()).getTime();
var images = [trial.a_path, trial.b_path];
var target_left = (Math.floor(Math.random() * 2) == 0); // 50% chance target is on left.
if (!target_left) {
images = [trial.b_path, trial.a_path];
}
// show the images
display_element.append($('<img>', {
"src": images[0],
"class": 'staircase-xab'
}));
display_element.append($('<img>', {
"src": images[1],
"class": 'staircase-xab'
}));
if (trial.prompt) {
display_element.append(trial.prompt);
}
if (trial.timing_ab > 0) {
setTimeout(function() {
if (!staircase_xab_trial_complete) {
$('.staircase-xab').css('visibility', 'hidden');
}
}, trial.timing_ab);
}
var resp_func = function(e) {
var flag = false;
var correct = false;
if (e.which == trial.left_key) {
flag = true;
if (target_left) {
correct = true;
}
}
else if (e.which == trial.right_key) {
flag = true;
if (!target_left) {
correct = true;
}
}
if (flag) {
var endTime = (new Date()).getTime();
var rt = (endTime - startTime);
var trial_data = {
"trial_type": "staircase-xab",
"trial_index": block.trial_idx,
"rt": rt,
"correct": correct,
"x_path": trial.x_path,
"a_path": trial.a_path,
"b_path": trial.b_path,
"key_press": e.which
}
//block.data[block.trial_idx] = $.extend({},trial_data,trial.data);
$(document).unbind('keyup', resp_func);
display_element.html(''); // remove all
staircase_xab_trial_complete = true;
setTimeout(function() {
block.next();
}, trial.timing_post_trial);
}
}
$(document).keyup(resp_func);
break;
}
}
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,82 @@
(function( $ ) {
jsPsych.storybook = (function(){
var plugin = {};
plugin.create = function(params) {
stims = params["stimuli"];
trials = new Array(stims.length);
for(var i = 0; i < trials.length; i++)
{
trials[i] = {};
trials[i]["type"] = "storybook";
trials[i]["a_path"] = stims[i];
trials[i]["click_num"] = params["click_num"];
trials[i]["timing"] = params["timing"];
if(params["data"]!=undefined){
trials[i]["data"] = params["data"][i];
}
}
return trials;
}
plugin.trial = function(display_element, block, trial, part)
{
switch(part){
case 1:
p1_time = (new Date()).getTime();
display_element.append($('<img>', {
"src": trial.a_path,
"class": 'storybook'
}));
var click_count = 0;
var click_locations = [];
var click_times = [];
var touchfunction = function(e) {
e.originalEvent.preventDefault();
var rt = (new Date()).getTime() - p1_time;
var x = e.originalEvent.touches[0].pageX;
var y = e.originalEvent.touches[0].pageY;
click_count = click_count + 1;
console.log("click event "+x+" "+y+". click count "+click_count+". click num "+trial.click_num);
//save location
click_locations.push([x,y]);
click_times.push(rt);
//save response time
if(click_count == trial.click_num)
{
var click_loc_data = {"click_locations": click_locations};
var click_time_data = {"click_times": click_times};
var img = {"img": trial.a_path };
// save data
block.data[block.trial_idx] = $.extend({}, img, click_loc_data, click_time_data, trial.data);
plugin.trial(display_element, block, trial, part + 1);
}
};
$('.storybook').click(function(){ void(0); })
//$('.storybook').mousedown(function(e){ touchfunction(e);});
$('.storybook').bind("touchstart", function(e){touchfunction(e);});
break;
case 2:
$('.storybook').remove();
setTimeout(function(){block.next();}, trial.timing[0]);
break;
}
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,89 @@
(function( $ ) {
jsPsych.touch_freepick = (function(){
var plugin = {};
plugin.create = function(params) {
stims = params["stims"];
trials = new Array(stims.length);
for(var i = 0; i < trials.length; i++)
{
trials[i] = {};
trials[i]["type"] = "touch_freepick";
trials[i]["stims"] = stims[i];
trials[i]["answer_text"] = params["answer_text"][i] || "";
trials[i]["timing"] = params["timing"];
trials[i]["force_correct_pick"] = params["force_correct_pick"] || false;
if(params["data"]!=undefined){
trials[i]["data"] = params["data"][i];
}
}
return trials;
}
plugin.trial = function(display_element, block, trial, part)
{
switch(part){
case 1:
p1_time = (new Date()).getTime();
var order = [];
for(var i=0;i<trial.stims.length;i++)
{
order.push(i);
}
order = shuffle(order);
// add images
for(var i=0;i<trial.stims.length;i++){
display_element.append($('<img>', {
"src": trial.stims[order[i]],
"class": 'freepick',
"id":'fp'+order[i]
}));
}
// need to pick the right one!
if(trial.force_correct_pick) {
$("#fp0").click(
function(){
// clear everything
display_element.html('');
// add only target
display_element.append($('<img>', {
"src": trial.stims[0],
"class": 'freepick',
"id":'fp_answer'
}));
display_element.append($('<p class="answer">'+trial.answer_text+'</p>'));
setTimeout(function(){plugin.trial(display_element,block,trial,part+1)}, trial.timing[0]);
}
);
}
break;
case 2:
display_element.html('');
setTimeout(function(){block.next();}, trial.timing[1]);
break;
}
}
function shuffle(array) {
var tmp, current, top = array.length;
if(top) while(--top) {
current = Math.floor(Math.random() * (top + 1));
tmp = array[current];
array[current] = array[top];
array[top] = tmp;
}
return array;
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,110 @@
// Josh de Leeuw
// Nov. 2012
// This plugin is for presenting two images in sequence and collecting a key response.
(function( $ ) {
jsPsych.twoimage_keyresponse = (function(){
var plugin = {};
plugin.create = function(params) {
var stims = params["stimuli"];
trials = new Array(stims.length);
for(var i = 0; i < trials.length; i++)
{
trials[i] = {};
trials[i]["type"] = "twoimage_keyresponse";
trials[i]["a_path"] = stims[i][0];
trials[i]["b_path"] = stims[i][1];
trials[i]["choices"] = params["choices"];
// timing parameters
trials[i]["timing_first_stim"] = params["timing_first_stim"] || 1000;
trials[i]["timing_gap"] = params["timing_gap"] || 500;
trials[i]["timing_second_stim"] = params["timing_second_stim"]; // if undefined, then show indefinitely
trials[i]["timing_post_trial"] = params["timing_post_trial"] || 1000;
// optional parameters
if(params["prompt"] != undefined){
trials[i]["prompt"] = params["prompt"];
}
if(params["data"]!=undefined){
trials[i]["data"] = params["data"][i];
}
}
return trials;
}
var tikr_trial_complete = false;
plugin.trial = function(display_element, block, trial, part)
{
switch(part){
case 1:
tikr_trial_complete = false;
display_element.append($('<img>', {
"src": trial.a_path,
"id": 'tikr_a_img'
}));
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1)}, trial.timing_first_stim);
break;
case 2:
$('#tikr_a_img').remove();
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1)}, trial.timing_gap);
break;
case 3:
startTime = (new Date()).getTime();
display_element.append($('<img>', {
"src": trial.b_path,
"id": 'tikr_b_img'
}));
//show prompt here
if(trial.prompt != undefined){
display_element.append(trial.prompt);
}
// hide image if timing is set
if(trial.timing_second_stim != undefined){
setTimeout(function(){
if(!tikr_trial_complete){
$('#tikr_b_img').css('visibility','hidden');
}
}, trial.timing_second_stim);
}
var resp_func = function(e) {
var flag = false;
// check if the key is any of the options, or if it is an accidental keystroke
for(var i=0;i<trial.choices.length;i++)
{
if(e.which==trial.choices[i])
{
flag = true;
}
}
if(flag)
{
endTime = (new Date()).getTime();
rt = (endTime-startTime);
var trial_data = {"trial_type":"twoimage_keyresponse", "rt": rt, "a_path": trial.a_path, "b_path": trial.b_path, "key_press": e.which}
block.data[block.trial_idx] = $.extend({},trial_data,trial.data);
$(document).unbind('keyup',resp_func);
display_element.html('');
tikr_trial_complete = true;
setTimeout(function(){block.next();}, trial.timing_post_trial);
}
}
$(document).keyup(resp_func);
break;
}
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,96 @@
(function( $ ) {
jsPsych.xab_touch = (function(){
var plugin = {};
plugin.create = function(params) {
//xab_stims = shuffle(xab_stims);
xab_stims = params["stimuli"];
trials = new Array(xab_stims.length);
for(var i = 0; i < trials.length; i++)
{
trials[i] = {};
trials[i]["type"] = "xab_touch";
trials[i]["a_path"] = xab_stims[i][0];
trials[i]["b_path"] = xab_stims[i][1];
trials[i]["timing"] = params["timing"];
if(params["data"]!=undefined){
trials[i]["data"] = params["data"][i];
}
}
return trials;
}
plugin.trial = function(display_element, block, trial, part)
{
switch(part){
case 1:
p1_time = (new Date()).getTime();
display_element.append($('<img>', {
"src": trial.a_path,
"class": 'xab_touch'
}));
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing[0]);
break;
case 2:
p2_time = (new Date()).getTime();
$('.xab_touch').remove();
setTimeout(function(){plugin.trial(display_element, block, trial, part + 1);}, trial.timing[1]);
break;
case 3:
p3_time = (new Date()).getTime();
startTime = (new Date()).getTime();
var images = [trial.a_path, trial.b_path];
var target_left = (Math.floor(Math.random()*2)==0); // binary true/false choice
if(!target_left){
images = [trial.b_path, trial.a_path];
}
//$.fn.jsPsych.showImages(display_element, images, 'xab');
var correct=false;
display_element.append($('<img>', {
"src": images[0],
"class": 'xab_touch',
"id": "left_img"
}));
$("#left_img").click(function() {
if(target_left) { correct = true; }
endTime = (new Date()).getTime();
rt = (endTime-startTime);
stim1_time = (p2_time-p1_time);
isi_time = (p3_time-p2_time);
var trial_data = {"rt": rt, "correct": correct, "a_path": trial.a_path, "b_path": trial.b_path, "stim1_time": stim1_time, "isi_time":isi_time}
block.data[block.trial_idx] = $.extend({},trial_data,trial.data);
$('.xab_touch').remove();
setTimeout(function(){block.next();}, trial.timing[2]);
});
display_element.append($('<img>', {
"src": images[1],
"class": 'xab_touch',
"id": "right_img"
}));
$("#right_img").click(function() {
if(!target_left) { correct = true; }
endTime = (new Date()).getTime();
rt = (endTime-startTime);
stim1_time = (p2_time-p1_time);
isi_time = (p3_time-p2_time);
var trial_data = {"rt": rt, "correct": correct, "a_path": trial.a_path, "b_path": trial.b_path, "stim1_time": stim1_time, "isi_time":isi_time}
block.data[block.trial_idx] = $.extend({},trial_data,trial.data);
$('.xab_touch').remove();
setTimeout(function(){block.next();}, trial.timing[2]);
});
//TODO: CHECK IF IMAGE SHOULD DISAPPEAR
//based on timings
break;
}
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,86 @@
/**
* jsPsych plugin for showing animations
* Josh de Leeuw
* updated October 2013
*
* shows a sequence of images at a fixed frame rate.
* no data is collected from the subject, but it does record the path of the first image
* in each sequence, and allows for optional data tagging as well.
*
* parameters:
* stimuli: array of arrays. inner arrays should consist of all the frames of the animation sequence. each inner array
* corresponds to a single trial
* frame_time: how long to display each frame in ms.
* repetitions: how many times to show the animation sequence.
* timing_post_trial: how long to show a blank screen after the trial in ms.
* prompt: optional HTML string to display while the animation is playing
* data: optional data object
*/
(function($) {
jsPsych.animation = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = new Array(params.stimuli.length);
for (var i = 0; i < trials.length; i++) {
trials[i] = {};
trials[i].type = "animation";
trials[i].stims = params.stimuli[i];
trials[i].frame_time = params.frame_time || 250;
trials[i].repetitions = params.repetitions || 1;
trials[i].timing_post_trial = params.timing_post_trial || 1000;
trials[i].prompt = (typeof params.prompt === 'undefined') ? "" : params.prompt;
trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
}
return trials;
};
plugin.trial = function(display_element, block, trial, part) {
var animate_frame = -1;
var reps = 0;
switch (part) {
case 1:
var animate_interval = setInterval(function() {
var showImage = true;
display_element.html(""); // clear everything
animate_frame++;
if (animate_frame == trial.stims.length) {
animate_frame = 0;
reps++;
if (reps >= trial.repetitions) {
plugin.trial(display_element, block, trial, part + 1);
clearInterval(animate_interval);
showImage = false;
}
}
if (showImage) {
display_element.append($('<img>', {
"src": trial.stims[animate_frame],
"class": 'animate'
}));
if (trial.prompt !== "") {
display_element.append(trial.prompt);
}
}
}, trial.frame_time);
break;
case 2:
block.writeData($.extend({}, {
"trial_type": "animation",
"trial_index": block.trial_idx,
"stimulus": trial.stims[0]
}, trial.data));
setTimeout(function() {
block.next();
}, trial.timing_post_trial);
break;
}
};
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,48 @@
/**
* Josh de Leeuw
* Updated October 2013
This plugin gives the user the ability to execute an arbitrary function
during an experiment.
Params:
"type" is "call_function"
"func" is the function that will be called
"args" is an array of arguments to pass to the function. (optional)
Data:
The return value of the function will be stored in the data.
**/
(function($) {
jsPsych['call-function'] = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = new Array(1);
trials[0] = {
"type": "call-function",
"func": params.func,
"args": params.args || [],
"data": (typeof params.data === 'undefined') ? {} : params.data
};
return trials;
};
plugin.trial = function(display_element, block, trial, part) {
var return_val = trial.func.apply({}, [trial.args]);
if (typeof return_val !== 'undefined') {
block.writeData($.extend({},{
trial_type: "call-function",
trial_index: block.trial_idx,
value: return_val
},trial.data));
}
block.next();
};
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,206 @@
/**
* jspsych plugin for categorization trials with feedback
* Josh de Leeuw
* updated October 2013
*
* display an image or HTML object and then give corrective feedback based on the subject's response
*
* parameters:
* stimuli: array of stimuli. array elements can be paths to images or strings of HTML.
* key_answer: array of key codes representing the correct answer for each stimulus.
* text_answer: array of strings representing the label associated with each stimulus. optional.
* choices: array of key codes representing valid choices that can be made. other key responses will be ignored.
* correct_text: HTML string to show when correct answer is given.
* incorrect_text: HTML string to show when incorrect answer is given.
* NOTE: for both of the above, the special string %ANS% can be used. The text_answer associated with
* the trial will be substituted for %ANS%.
* timing_stim: how long to show the stimulus for. -1 will show until response is given.
* timing_feedback_duration: how long to show the feedback for.
* timing_post_trial: how long to show a blank screen before the next trial.
* show_stim_with_feedback: if true, the stimulus will remain on the screen while feedback is given.
* is_html: must set to true if the stimulus is HTML code.
* force_correct_button_press: if true, then the user must press the correct key after feedback is given.
* prompt: HTML string to show when the subject is viewing the stimulus and making a categorization decision.
* data: the optional data object
**/
(function($) {
jsPsych.categorize = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = [];
for (var i = 0; i < params.stimuli.length; i++) {
trials.push({});
trials[i].type = "categorize";
trials[i].a_path = params.stimuli[i];
trials[i].key_answer = params.key_answer[i];
trials[i].text_answer = (typeof params.text_answer === 'undefined') ? "" : params.text_answer[i];
trials[i].choices = params.choices;
trials[i].correct_text = (typeof params.correct_text === 'undefined') ? "<p class='feedback'>Correct</p>" : params.correct_text;
trials[i].incorrect_text = (typeof params.incorrect_text === 'undefined') ? "<p class='feedback'>Incorrect</p>" : params.incorrect_text;
// timing params
trials[i].timing_stim = params.timing_stim || -1; // default is to show image until response
trials[i].timing_feedback_duration = params.timing_feedback_duration || 2000;
trials[i].timing_post_trial = params.timing_post_trial || 1000;
// optional params
trials[i].show_stim_with_feedback = (typeof params.show_stim_with_feedback === 'undefined') ? true : params.show_stim_with_feedback;
trials[i].is_html = (typeof params.is_html === 'undefined') ? false : params.is_html;
trials[i].force_correct_button_press = (typeof params.force_correct_button_press === 'undefined') ? false : params.force_correct_button_press;
trials[i].prompt = (typeof params.prompt === 'undefined') ? '' : params.prompt;
trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
}
return trials;
};
var cat_trial_complete = false;
plugin.trial = function(display_element, block, trial, part) {
switch (part) {
case 1:
// set finish flag
cat_trial_complete = false;
if (!trial.is_html) {
// add image to display
display_element.append($('<img>', {
"src": trial.a_path,
"class": 'cat',
"id": 'jspsych_cat_image'
}));
}
else {
display_element.append($('<div>', {
id: 'jspsych_cat_image',
"class": 'cat',
html: trial.a_path
}));
}
// hide image after time if the timing parameter is set
if (trial.timing_stim > 0) {
setTimeout(function() {
if (!cat_trial_complete) {
$('#jspsych_cat_image').css('visibility', 'hidden');
}
}, trial.timing_stim);
}
// if prompt is set, show prompt
if (trial.prompt !== "") {
display_element.append(trial.prompt);
}
// start measuring RT
var startTime = (new Date()).getTime();
// create response function
var resp_func = function(e) {
var flag = false;
var correct = false;
if (e.which == trial.key_answer) // correct category
{
flag = true;
correct = true;
}
else {
// check if the key is any of the options, or if it is an accidental keystroke
for (var i = 0; i < trial.choices.length; i++) {
if (e.which == trial.choices[i]) {
flag = true;
correct = false;
}
}
}
if (flag) {
cat_trial_complete = true;
// measure response time
var endTime = (new Date()).getTime();
var rt = (endTime - startTime);
// save data
var trial_data = {
"trial_type": "categorize",
"trial_index": block.trial_idx,
"rt": rt,
"correct": correct,
"stimulus": trial.a_path,
"key_press": e.which
};
block.writeData($.extend({}, trial_data, trial.data));
// clear function
$(document).unbind('keyup', resp_func);
display_element.html('');
plugin.trial(display_element, block, trial, part + 1);
}
};
// add event listener
$(document).keyup(resp_func);
break;
case 2:
// show image during feedback if flag is set
if (trial.show_stim_with_feedback) {
if (!trial.is_html) {
// add image to display
display_element.append($('<img>', {
"src": trial.a_path,
"class": 'cat',
"id": 'jspsych_cat_image'
}));
}
else {
display_element.append($('<div>', {
id: 'jspsych_cat_image',
"class": 'cat',
html: trial.a_path
}));
}
}
// substitute answer in feedback string.
var atext = "";
if (block.data[block.trial_idx].correct) {
atext = trial.correct_text.replace("%ANS%", trial.text_answer);
}
else {
atext = trial.incorrect_text.replace("%ANS%", trial.text_answer);
}
// show the feedback
display_element.append(atext);
// check if force correct button press is set
if (trial.force_correct_button_press && block.data[block.trial_idx].correct === false) {
var resp_func_corr_key = function(e) {
if (e.which == trial.key_answer) // correct category
{
$(document).unbind('keyup', resp_func_corr_key);
plugin.trial(display_element, block, trial, part + 1);
}
};
$(document).keyup(resp_func_corr_key);
}
else {
setTimeout(function() {
plugin.trial(display_element, block, trial, part + 1);
}, trial.timing_feedback_duration);
}
break;
case 3:
display_element.html("");
setTimeout(function() {
block.next();
}, trial.timing_post_trial);
break;
}
};
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,158 @@
/**
* Josh de Leeuw
* Updated October 2013
*
* This plugin displays a set of images on the screen and allows the user to drag them around.
* The location of each object and the moves that the subject performs are recorded.
*
* parameters:
* stimuli: array of arrays. inner most arrays are collections of image paths that will be displayed in a trial. outer
* arrays are trials.
* stim_height: the height of the images to sort in pixels.
* stim_width: the width of the images to sort in pixels.
* timing_post_trial: how long to show a blank screen after the trial in ms.
* prompt: optional html string to display while sorting happens.
* prompt_location: either 'above' or 'below'; changes location of prompt
* sort_area_width: width of the area used to sort the images
* sort_area_height: height of the area used to sort the images
* data: optional data object
*
*/
(function($) {
jsPsych['free-sort'] = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = new Array(params.stimuli.length);
for (var i = 0; i < trials.length; i++) {
trials[i] = {
"type": "free-sort",
"images": params.stimuli[i], // array of images to display
"stim_height": params.stim_height || 100,
"stim_width": params.stim_width || 100,
"timing_post_trial": params.timing_post_trial || 1000,
"prompt": (typeof params.prompt === 'undefined') ? '' : params.prompt,
"prompt_location": params.prompt_location || "above",
"sort_area_width": params.sort_area_width || 800,
"sort_area_height": params.sort_area_height || 800,
"data": (typeof params.data === 'undefined') ? {} : params.data[i]
};
}
return trials;
};
plugin.trial = function(display_element, block, trial, part) {
var start_time = (new Date()).getTime();
// check if there is a prompt and if it is shown above
if (trial.prompt && trial.prompt_location == "above") {
display_element.append(trial.prompt);
}
display_element.append($('<div>', {
"id": "sort_arena",
"class": "sort",
"css": {
"position": "relative",
"width": trial.sort_area_width,
"height": trial.sort_area_height
}
}));
// check if prompt exists and if it is shown below
if (trial.prompt && trial.prompt_location == "below") {
display_element.append(trial.prompt);
}
// store initial location data
var init_locations = [];
for (var i = 0; i < trial.images.length; i++) {
var coords = random_coordinate(trial.sort_area_width - trial.stim_width, trial.sort_area_height - trial.stim_height);
$("#sort_arena").append($('<img>', {
"src": trial.images[i],
"class": "draggable_stim sort",
"css": {
"position": "absolute",
"top": coords.y,
"left": coords.x
}
}));
init_locations.push({
"src": trial.images[i],
"x": coords.x,
"y": coords.y
});
}
var moves = [];
$('.draggable_stim').draggable({
containment: "#sort_arena",
scroll: false,
stack: ".draggable_stim",
stop: function(event, ui) {
moves.push({
"src": event.target.src.split("/").slice(-1)[0],
"x": ui.position.left,
"y": ui.position.top
});
}
});
display_element.append($('<button>', {
"id": "done_btn",
"class": "sort",
"html": "Done",
"click": function() {
var end_time = (new Date()).getTime();
var rt = end_time - start_time;
// gather data
// get final position of all objects
var final_locations = [];
$('.draggable_stim').each(function() {
final_locations.push({
"src": $(this).attr('src'),
"x": $(this).css('left'),
"y": $(this).css('top')
});
});
block.writeData($.extend({}, {
"init_locations": JSON.stringify(init_locations),
"moves": JSON.stringify(moves),
"final_locations": JSON.stringify(final_locations),
"rt": rt
}, trial.data));
// advance to next part
$('.sort').remove();
setTimeout(function() {
block.next();
}, trial.timing_post_trial);
}
}));
};
// helper functions
function random_coordinate(max_width, max_height) {
var rnd_x = Math.floor(Math.random() * (max_width - 1));
var rnd_y = Math.floor(Math.random() * (max_height - 1));
return {
x: rnd_x,
y: rnd_y
};
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,98 @@
/** (July 2012, Erik Weitnauer)
The html-plugin will load and display an arbitrary number of html pages. To proceed to the next, the
user might either press a button on the page or a specific key. Afterwards, the page get hidden and
the plugin will wait of a specified time before it proceeds.
Parameters:
pages: array of
url: url of the html-page to display (mandatory)
cont_key: keycode of the key to continue (optional)
cont_btn: id of a button (or any element on the page) that can be clicked to continue (optional)
note that the button / element has to be included in the page that is loaded, already
timing: number of ms to wait after hiding the page and before proceeding (optional)
check_fn: called with display_element as argument when subject attempts to proceed; only proceeds if this
returns true; (optional)
cont_key: this setting is used for all pages that don't define it themself (optional)
cont_btn: this setting is used for all pages that don't define it themself (optional)
timing: this setting is used for all pages that don't define it themself (optional)
force_refresh: set to true if you want to force the plugin to grab the newest version of the html document
Data:
array of
url: the url of the page
rt: duration the user looked at the page in ms
Example Usage:
jsPsych.init(
{experiment_structure: [
{type: "html", pages:[{url: "intro.html", cont_btn: "start"}]}
]
});
*/
(function($) {
jsPsych.html = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = [];
for (var i = 0; i < params.pages.length; i++) {
trials.push({
type: "html",
url: params.pages[i].url,
cont_key: params.pages[i].cont_key || params.cont_key,
cont_btn: params.pages[i].cont_btn || params.cont_btn,
timing_post_trial: params.pages[i].timing_post_trial || params.timing_post_trial || 1000,
check_fn: params.pages[i].check_fn,
force_refresh: (typeof params.force_refresh === 'undefined') ? false : params.force_refresh
});
}
return trials;
};
plugin.trial = function(display_element, block, trial, part) {
var url = trial.url;
if (trial.force_refresh) {
url = trial.url + "?time=" + (new Date().getTime());
}
display_element.load(trial.url, function() {
var t0 = (new Date()).getTime();
var finish = function() {
if (trial.check_fn && !trial.check_fn(display_element)) return;
if (trial.cont_key) $(document).unbind('keyup', key_listener);
block.writeData({
trial_type: "html",
trial_index: block.trial_idx,
rt: (new Date()).getTime() - t0,
url: trial.url
});
if (trial.timing) {
// hide display_element, since it could have a border and we want a blank screen during timing
display_element.hide();
setTimeout(function() {
display_element.empty();
display_element.show();
block.next();
}, trial.timing);
}
else {
display_element.empty();
block.next();
}
};
if (trial.cont_btn) $('#' + trial.cont_btn).click(finish);
if (trial.cont_key) {
var key_listener = function(e) {
if (e.which == trial.cont_key) finish();
};
$(document).keyup(key_listener);
}
});
};
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,324 @@
/** jspsych-palmer
* Josh de Leeuw (October 2013)
*
* a jspsych plugin for presenting and querying about stimuli modeled after
*
* Palmer, S. (1977). Hierarchical Structure in Perceptual Representation. Cognitive Psychology, 9, 441.
*
* and
*
* Goldstone, R. L., Rogosky, B. J., Pevtzow, R., & Blair, M. (2005). Perceptual and semantic reorganization during category learning.
* In H. Cohen & C. Lefebvre (Eds.) Handbook of Categorization in Cognitive Science. (pp. 651-678). Amsterdam: Elsevier.
*
* NOTE: This plugin requires the Raphaeljs library for manipulating vector graphics (SVG). Download at http://www.raphaeljs.com
*
* parameters:
* configurations: array of arrays. inner most array should be an array of 1s and 0s, where 1s represent the
* presence of a line segment, and 0s represent the absence.
* editable: set to true if you want the subject to be able to change the configuration by interacting
* with the stimulus. (click two circles to toggle the line between them).
* show_feedback: set to true to show corrective feedback when trial is editable.
* grid_spacing: distance in pixels between the circles.
* square_Size: how many circles per row/column.
* timing_item: how long to show the stimulus for. (only matters when editable is false)
* timing_post_trial: how long to show blank screen after trial.
* timing_feedback: how long to show corrective feedback for.
* prompt: optional html string to show during stimulus presentation
* data: optional data object
*
*/
(function($) {
jsPsych.palmer = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = [];
for (var i = 0; i < params.configurations.length; i++) {
var trial = {
type: "palmer",
configurations: params.configurations[i],
editable: (typeof params.editable === 'undefined') ? false : params.editable,
show_feedback: (typeof params.show_feedback === 'undefined') ? false : params.show_feedback,
grid_spacing: params.grid_spacing || 75,
square_size: params.square_size || 3,
circle_radius: params.circle_radius || 20,
timing_item: params.timing_item || 1000,
timing_post_trial: params.timing_post_trial || 1000,
timing_feedback: params.timing_feedback || 1000,
prompt: (typeof params.prompt === 'undefined') ? "" : params.prompt,
data: (typeof params.data === 'undefined') ? {} : params.data[i]
};
trials.push(trial);
}
return trials;
};
plugin.trial = function(display_element, block, trial, part) {
// variables to keep track of user interaction
var start_circle = -1;
var end_circle = -1;
var line_started = false;
var size = trial.grid_spacing * (trial.square_size + 1);
display_element.append($("<div id='raphaelCanvas'>", {
css: {
width: size + "px",
height: size + "px"
}
}));
var paper = Raphael("raphaelCanvas", size, size);
// create the circles at the vertices.
var circles = [];
var node_idx = 0;
for (var i = 1; i <= trial.square_size; i++) {
for (var j = 1; j <= trial.square_size; j++) {
var circle = paper.circle(trial.grid_spacing * j, trial.grid_spacing * i, trial.circle_radius);
circle.attr("fill", "#000").attr("stroke-width", "0").attr("stroke", "#000").data("node", node_idx);
if (trial.editable) {
circle.hover(
function() {
this.attr("stroke-width", "2");
//this.attr("stroke", "#000");
},
function() {
this.attr("stroke-width", "0");
//this.attr("stroke", "#fff")
}).click(
function() {
if (!line_started) {
line_started = true;
start_circle = this.data("node");
this.attr("fill", "#777").attr("stroke", "#777");
}
else {
end_circle = this.data("node");
draw_connection(start_circle, end_circle);
}
});
}
node_idx++;
circles.push(circle);
}
}
function draw_connection(start_circle, end_circle) {
var the_line = getLineIndex(start_circle, end_circle);
if (the_line > -1) {
toggle_line(the_line);
}
// reset highlighting on circles
for (var i = 0; i < circles.length; i++) {
circles[i].attr("fill", "#000").attr("stroke", "#000");
}
// cleanup the variables
line_started = false;
start_circle = -1;
end_circle = -1;
}
// create all possible lines that connect circles
var horizontal_lines = [];
var vertical_lines = [];
var backslash_lines = [];
var forwardslash_lines = [];
for (var i = 0; i < trial.square_size; i++) {
for (var j = 0; j < trial.square_size; j++) {
var current_item = (i * trial.square_size) + j;
// add horizontal connections
if (j < (trial.square_size - 1)) {
horizontal_lines.push([current_item, current_item + 1]);
}
// add vertical connections
if (i < (trial.square_size - 1)) {
vertical_lines.push([current_item, current_item + trial.square_size]);
}
// add diagonal backslash connections
if (i < (trial.square_size - 1) && j < (trial.square_size - 1)) {
backslash_lines.push([current_item, current_item + trial.square_size + 1]);
}
// add diagonal forwardslash connections
if (i < (trial.square_size - 1) && j > 0) {
forwardslash_lines.push([current_item, current_item + trial.square_size - 1]);
}
}
}
var lines = horizontal_lines.concat(vertical_lines).concat(backslash_lines).concat(forwardslash_lines);
// actually draw the lines
var lineIsVisible = [];
var lineElements = [];
for (var i = 0; i < lines.length; i++) {
var line = paper.path("M" + circles[lines[i][0]].attr("cx") + " " + circles[lines[i][0]].attr("cy") + "L" + circles[lines[i][1]].attr("cx") + " " + circles[lines[i][1]].attr("cy")).attr("stroke-width", "8").attr("stroke", "#000");
line.hide();
lineElements.push(line);
lineIsVisible.push(0);
}
// define some helper functions to toggle lines on and off
// this function gets the index of a line based on the two circles it connects
function getLineIndex(start_circle, end_circle) {
var the_line = -1;
for (var i = 0; i < lines.length; i++) {
if ((start_circle == lines[i][0] && end_circle == lines[i][1]) || (start_circle == lines[i][1] && end_circle == lines[i][0])) {
the_line = i;
break;
}
}
return the_line;
}
// this function turns a line on/off based on the index (the_line)
function toggle_line(the_line) {
if (the_line > -1) {
if (lineIsVisible[the_line] === 0) {
lineElements[the_line].show();
lineElements[the_line].toBack();
lineIsVisible[the_line] = 1;
}
else {
lineElements[the_line].hide();
lineElements[the_line].toBack();
lineIsVisible[the_line] = 0;
}
}
}
// this function takes an array of length = num lines, and displays the line whereever there
// is a 1 in the array.
function showConfiguration(configuration) {
for (var i = 0; i < configuration.length; i++) {
if (configuration[i] != lineIsVisible[i]) {
toggle_line(i);
}
}
}
// highlight a line
function highlightLine(line) {
lineElements[line].attr("stroke", "#f00");
}
// start recording the time
var startTime = (new Date()).getTime();
// what kind of trial are we doing?
// if trial.editable is true, then we will let the user interact with the stimulus to create
// something, e.g. for a reconstruction probe.
// need a way for the user to submit when they are done in that case...
if (trial.editable) {
display_element.append($('<button id="submitButton" type="button">Submit Answer</button>'));
$('#submitButton').click(function() {
save_data();
});
}
// if trial.editable is false, then we are just showing a pre-determined configuration.
// for now, the only option will be to display for a fixed amount of time.
// future ideas: allow for key response, to enable things like n-back, same/different, etc..
if (!trial.editable) {
showConfiguration(trial.configurations);
setTimeout(function() {
save_data();
}, trial.timing_item);
}
if (trial.prompt !== "") {
display_element.append($('<div id="palmer_prompt">'));
$("#palmer_prompt").html(trial.prompt);
}
function arrayDifferences(arr1, arr2) {
var n_diff = 0;
for (var i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
n_diff++;
}
}
return n_diff;
}
// save data
function save_data() {
// measure RT
var endTime = (new Date()).getTime();
var response_time = endTime - startTime;
// check if configuration is correct
// this is meaningless for trials where the user can't edit
var n_diff = arrayDifferences(trial.configurations, lineIsVisible);
var correct = (n_diff === 0);
block.writeData($.extend({}, {
"trial_type": "palmer",
"trial_index": block.trial_idx,
"configuration": JSON.stringify(lineIsVisible),
"target_configuration": JSON.stringify(trial.configurations),
"rt": response_time,
"correct": correct,
"num_wrong": n_diff,
}, trial.data));
if (trial.editable && trial.show_feedback) {
// hide the button
$('#submitButton').hide();
$('#palmer_prompt').hide();
showConfiguration(trial.configurations);
var feedback = "";
if (correct) {
feedback = "Correct!";
}
else {
if (n_diff > 1) {
feedback = "You missed " + n_diff + " lines. The correct symbol is shown above.";
}
else {
feedback = "You missed 1 line. The correct symbol is shown above.";
}
}
display_element.append($.parseHTML("<p id='palmer_feedback'>" + feedback + "</p>"));
setTimeout(function() {
next_trial();
}, trial.timing_feedback);
}
else {
next_trial();
}
}
function next_trial() {
display_element.html('');
// next trial
setTimeout(function() {
block.next();
}, trial.timing_post_trial);
}
};
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,150 @@
/**
* jspsych-same-different
* Josh de Leeuw (Updated Oct 2013)
*
* plugin for showing two stimuli sequentially and getting a same / different judgment
*
* parameters:
* stimuli: array of arrays. inner most array should have two elements, corresponding to the two items that will be shown.
* items can be image paths or HTML strings. each inner array is a trial.
* answer: array of strings. acceptable values are "same" and "different". represents the correct answer for each trial.
* same_key: which key to press to indicate a 'same' response.
* different_key: which key to press to indicate a 'different' response.
* timing_first_stim: how long to show the first stimulus
* timing_second_stim: how long to show the second stim. can be -1, which means to show until a response is given.
* timing_gap: how long to show a blank screen in between the two stimuli.
* timing_post_trial: how long to show a blank screen after the trial ends.
* is_html: must set to true if the stimulus is HTML code.
* prompt: HTML string to show when the subject is viewing the stimulus and making a categorization decision.
* data: the optional data object
*
*
*/
jsPsych['same-different'] = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = new Array(params.stimuli.length);
for (var i = 0; i < trials.length; i++) {
trials[i] = {};
trials[i].type = "same-different";
trials[i].a_path = params.stimuli[i][0];
trials[i].b_path = params.stimuli[i][1];
trials[i].answer = params.answer[i];
trials[i].same_key = params.same_key || 81; // default is 'q'
trials[i].different_key = params.different_key || 80; // default is 'p'
// timing parameters
trials[i].timing_first_stim = params.timing_first_stim || 1000;
trials[i].timing_second_stim = params.timing_second_stim || 1000; // if -1, then second stim is shown until response.
trials[i].timing_gap = params.timing_gap || 500;
trials[i].timing_post_trial = params.timing_post_trial || 1000;
// optional parameters
trials[i].is_html = (typeof params.is_html === 'undefined') ? false : true;
trials[i].prompt = (typeof params.prompt === 'undefined') ? "" : params.prompt;
trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
}
return trials;
};
var sd_trial_complete = false;
plugin.trial = function(display_element, block, trial, part) {
switch (part) {
case 1:
sd_trial_complete = false;
// show image
if (!trial.is_html) {
display_element.append($('<img>', {
src: trial.a_path,
"class": 'sd'
}));
}
else {
display_element.append($('<div>', {
html: trial.a_path,
"class": 'sd'
}));
}
setTimeout(function() {
plugin.trial(display_element, block, trial, part + 1);
}, trial.timing_first_stim);
break;
case 2:
$('.sd').remove();
setTimeout(function() {
plugin.trial(display_element, block, trial, part + 1);
}, trial.timing_gap);
break;
case 3:
if (!trial.is_html) {
display_element.append($('<img>', {
src: trial.a_path,
"class": 'sd',
id: 'jspsych_sd_second_image'
}));
}
else {
display_element.append($('<div>', {
html: trial.a_path,
"class": 'sd',
id: 'jspsych_sd_second_image'
}));
}
if (trial.timing_second_stim > 0) {
setTimeout(function() {
if (!sd_trial_complete) {
$("#jspsych_sd_second_image").css('visibility', 'hidden');
}
}, trial.timing_second_stim);
}
var startTime = (new Date()).getTime();
var resp_func = function(e) {
var flag = false;
var correct = false;
if (e.which == trial.same_key) {
flag = true;
if (trial.answer == "same") {
correct = true;
}
}
else if (e.which == trial.different_key) {
flag = true;
if (trial.answer == "different") {
correct = true;
}
}
if (flag) {
var endTime = (new Date()).getTime();
var rt = (endTime - startTime);
var trial_data = {
"trial_type": "same-different",
"trial_index": block.trial_idx,
"rt": rt,
"correct": correct,
"a_path": trial.a_path,
"b_path": trial.b_path,
"key_press": e.which
};
block.data[block.trial_idx] = $.extend({}, trial_data, trial.data);
$(document).unbind('keyup', resp_func);
display_element.html('');
setTimeout(function() {
block.next();
}, trial.timing_post_trial);
}
};
$(document).keyup(resp_func);
break;
}
};
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,170 @@
/**
* jspsych-similarity.js
* Josh de Leeuw
*
* This plugin create a trial where two images are shown sequentially, and the subject rates their similarity using a slider controlled with the mouse.
*
* parameters:
* stimuli: array of arrays. inner arrays are two stimuli. stimuli can be image paths or html strings. each inner array is one trial.
* label_low: label to display at the left end of the similarity slider scale.
* label_high: label to display at the right end of the similiarity slider scale.
* timing_first_stim: how long to show the first stimulus.
* timing_second_stim: how long to show the second stimulus. can be -1 to show until a response is given.
* timing_image_gap: how long to show a blank screen between the two stimuli.
* timing_post_trial: how long to show a blank screen after the trial is over.
* is_html: must set to true when using HTML strings as the stimuli.
* prompt: optional HTML string to display with the stimulus.
* data: the optional data object
*
*/
(function($) {
jsPsych.similarity = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = new Array(params.stimuli.length);
for (var i = 0; i < trials.length; i++) {
trials[i] = {};
trials[i].type = "similarity";
trials[i].a_path = params.stimuli[i][0];
trials[i].b_path = params.stimuli[i][1];
trials[i].label_low = params.label_low || "Not at all similar";
trials[i].label_high = params.label_high || "Identical";
trials[i].timing_first_stim = params.timing_first_stim || 1000; // default 1000ms
trials[i].timing_second_stim = params.timing_second_stim || -1; // -1 = inf time; positive numbers = msec to display second image.
trials[i].timing_image_gap = params.timing_image_gap || 1000; // default 1000ms
trials[i].timing_post_trial = params.timing_post_trial || 1000; // default 1000ms
trials[i].is_html = (typeof params.is_html === 'undefined') ? false : params.is_html;
trials[i].prompt = (typeof params.prompt === 'undefined') ? '' : params.prompt;
trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
}
return trials;
};
var sim_trial_complete = false;
plugin.trial = function(display_element, block, trial, part) {
switch (part) {
case 1:
sim_trial_complete = false;
// show the images
if (!trial.is_html) {
display_element.append($('<img>', {
"src": trial.a_path,
"class": 'sim'
}));
}
else {
display_element.append($('<div>', {
"html": trial.a_path,
"class": 'sim'
}));
}
setTimeout(function() {
plugin.trial(display_element, block, trial, part + 1);
}, trial.timing_first_stim);
break;
case 2:
$('.sim').remove();
setTimeout(function() {
plugin.trial(display_element, block, trial, part + 1);
}, trial.timing_image_gap);
break;
case 3:
if (!trial.is_html) {
display_element.append($('<img>', {
"src": trial.b_path,
"class": 'sim',
"id": 'jspsych_sim_second_image'
}));
}
else {
display_element.append($('<div>', {
"html": trial.b_path,
"class": 'sim',
"id": 'jspsych_sim_second_image'
}));
}
if (trial.timing_second_stim > 0) {
setTimeout(function() {
if (!sim_trial_complete) {
$("#jspsych_sim_second_image").css('visibility', 'hidden');
}
}, trial.timing_second_stim);
}
// create slider
display_element.append($('<div>', {
"id": 'slider',
"class": 'sim'
}));
$("#slider").slider({
value: 50,
min: 0,
max: 100,
step: 1,
});
// create labels for slider
display_element.append($('<div>', {
"id": 'slider_labels',
"class": 'sim'
}));
$('#slider_labels').append($('<p class="slider_left sim">' + trial.label_low + '</p>'));
$('#slider_labels').append($('<p class="slider_right sim">' + trial.label_high + '</p>'));
// create button
display_element.append($('<button>', {
'id': 'next',
'class': 'sim',
'html': 'Submit Answer'
}));
// if prompt is set, show prompt
if (trial.prompt !== "") {
display_element.append(trial.prompt);
}
$("#next").click(function() {
var endTime = (new Date()).getTime();
var response_time = endTime - startTime;
sim_trial_complete = true;
var score = $("#slider").slider("value");
block.writeData($.extend({}, {
"sim_score": score,
"rt": response_time,
"stimulus": trial.a_path,
"stimulus_2": trial.b_path,
"trial_type": "similarity",
"trial_index": block.trial_idx
}, trial.data));
// goto next trial in block
display_element.html('');
setTimeout(function() {
block.next();
}, trial.timing_post_trial);
});
var startTime = (new Date()).getTime();
break;
}
};
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,150 @@
// Josh de Leeuw
// Updated October 2013
//
// This plugin is for presenting a single image and collecting a key response.
// It can be used for categorizing images (without feedback), collecting yes/no responses, etc...
//
// parameters
// stimuli: array of stimuli to present. elements of the array can be either paths to images
// or HTML strings
// choices: array of key codes that represent valid responses. other key codes will be ignored
// continue_after_response: when true, the trial will end as soon as the user gives a response.
// if false, then the trial will continue until timing_response is reached
// timing_stim: how long to show the stimulus for. -1 will show indefinitely.
// timing_response: how long to wait for a response. this timer starts at the same time as the
// timer for the stimulus presentation. if the timer is reached without a response
// given, then the user's response will be recorded as a "-1" for the trial, and the
// trial will end.
// timing_post_trial: how long to show a blank screen after the trial ends.
// is_html: must set to true when using HTML strings as the stimuli.
// prompt: optional HTML string to display with the stimulus.
// data: the optional data object
(function($) {
jsPsych["single-stim"] = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = new Array(params.stimuli.length);
for (var i = 0; i < trials.length; i++) {
trials[i] = {};
trials[i].type = "single-stim";
trials[i].a_path = params.stimuli[i];
trials[i].choices = params.choices;
// option to show image for fixed time interval, ignoring key responses
// true = image will keep displaying after response
// false = trial will immediately advance when response is recorded
trials[i].continue_after_response = params.continue_after_response || true;
// timing parameters
trials[i].timing_stim = params.timing_stim || -1; // if -1, then show indefinitely
trials[i].timing_response = params.timing_response || -1; // if -1, then wait for response forever
trials[i].timing_post_trial = params.timing_post_trial || 1000;
// optional parameters
trials[i].is_html = (typeof params.is_html === 'undefined') ? false : params.is_html;
trials[i].prompt = (typeof params.prompt === 'undefined') ? "" : params.prompt;
trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
}
return trials;
};
plugin.trial = function(display_element, block, trial, part) {
var trial_complete = false;
var startTime = (new Date()).getTime();
var key_press = -1;
if (!trial.is_html) {
display_element.append($('<img>', {
src: trial.a_path,
id: 'ss'
}));
}
else {
display_element.append($('<div>', {
html: trial.a_path,
id: 'ss'
}));
}
//show prompt here
if (trial.prompt !== "") {
display_element.append(trial.prompt);
}
var cont_function = function() {
var endTime = (new Date()).getTime();
var rt = (endTime - startTime);
trial_complete = true;
var trial_data = {
"trial_type": "single-stim",
"trial_index": block.trial_idx,
"rt": rt,
"stimulus": trial.a_path,
"key_press": key_press
};
block.writeData($.extend({}, trial_data, trial.data));
$(document).unbind('keyup', resp_func);
display_element.html('');
setTimeout(function() {
block.next();
}, trial.timing_post_trial);
};
var resp_func = function(e) {
var flag = false;
// check if the key is any of the options, or if it is an accidental keystroke
for (var i = 0; i < trial.choices.length; i++) {
if (e.which == trial.choices[i]) {
flag = true;
}
}
if (flag) {
key_press = e.which;
// after a valid response, the stimulus will have the CSS class 'responded'
// which can be used to provide visual feedback that a response was recorded
$("#ss").addClass('responded');
if (trial.continue_after_response) {
// response triggers the next trial in this case.
// if hide_image_after_response is true, then next
// trial should be triggered by timeout function below.
cont_function();
}
}
};
$(document).keyup(resp_func);
// hide image if timing is set
if (trial.timing_stim > 0) {
setTimeout(function() {
if (!trial_complete) {
$('#ss').css('visibility', 'hidden');
}
}, trial.timing_stim);
}
// end trial if time limit is set
if (trial.timing_response > 0) {
setTimeout(function() {
if (!trial_complete) {
cont_function();
}
}, trial.timing_response);
}
};
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,164 @@
/**
* jspsych-likert
* a jspsych plugin for measuring items on a likert scale
*
* Josh de Leeuw (March 2013)
* Updated October 2013
*
* parameters:
* questions: array of arrays. inner arrays are arrays of strings, where each string represents a prompt
* for the user to respond to.
* labels: array of arrays of arrays. inner most arrays are label markers for the slider, e.g. ["Strongly Disagree", "Neutral", "Strongly Agree"].
* need one inner array for every question that is part of the trial. middle arrays group questions together.
* intervals: array of arrays. inner arrays are how many different responses the user can select from, e.g. 5, one for each question.
* show_ticks: graphically show tick marks on the slider bar to indicate the response levels.
* data: optional data object
*
*/
(function($) {
jsPsych.likert = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = [];
for (var i = 0; i < params.questions.length; i++) {
trials.push({
type: "likert",
questions: params.questions[i],
labels: params.labels[i],
intervals: params.intervals[i],
show_ticks: (typeof params.show_ticks === 'undefined') ? true : params.show_ticks,
data: (typeof params.data === 'undefined') ? {} : params.data[i]
});
}
return trials;
};
plugin.trial = function(display_element, block, trial, part) {
// add likert scale questions
for (var i = 0; i < trial.questions.length; i++) {
// create div
display_element.append($('<div>', {
"id": 'likert' + i,
"class": 'likertquestion'
}));
// add question text
$("#likert" + i).append('<p class="likerttext likert">' + trial.questions[i] + '</p>');
// create slider
$("#likert" + i).append($('<div>', {
"id": 'slider' + i,
"class": 'slider likert'
}));
$("#slider" + i).slider({
value: Math.ceil(trial.intervals[i] / 2),
min: 1,
max: trial.intervals[i],
step: 1
});
// show tick marks
if (trial.show_ticks) {
$("#likert" + i).append($('<div>', {
"id": 'sliderticks' + i,
"class": 'sliderticks likert',
"css": {
"position": 'relative'
}
}));
for (var j = 1; j < trial.intervals[i] - 1; j++) {
$('#slider' + i).append('<div class="slidertickmark"></div>');
}
$('#slider' + i + ' .slidertickmark').each(function(index) {
var left = (index + 1) * (100 / (trial.intervals[i] - 1));
$(this).css({
'position': 'absolute',
'left': left + '%',
'width': '1px',
'height': '100%',
'background-color': '#222222'
});
});
}
// create labels for slider
$("#likert" + i).append($('<ul>', {
"id": "sliderlabels" + i,
"class": 'sliderlabels likert',
"css": {
"width": "100%",
"margin": "10px 0px 0px 0px",
"padding": "0px",
"display": "block",
"position": "relative"
}
}));
for (var j = 0; j < trial.labels[i].length; j++) {
$("#sliderlabels" + i).append('<li>' + trial.labels[i][j] + '</li>');
}
// position labels to match slider intervals
var slider_width = $("#slider" + i).width();
var num_items = trial.labels[i].length;
var item_width = slider_width / num_items;
var spacing_interval = slider_width / (num_items - 1);
$("#sliderlabels" + i + " li").each(function(index) {
$(this).css({
'display': 'inline-block',
'width': item_width + 'px',
'margin': '0px',
'padding': '0px',
'text-align': 'center',
'position': 'absolute',
'left': (spacing_interval * index) - (item_width / 2)
});
});
}
// add submit button
display_element.append($('<button>', {
'id': 'next',
'class': 'likert'
}));
$("#next").html('Submit Answers');
$("#next").click(function() {
// measure response time
var endTime = (new Date()).getTime();
var response_time = endTime - startTime;
// create object to hold responses
var question_data = {};
$("div.slider").each(function(index) {
var id = "Q" + index;
var val = $(this).slider("value");
var obje = {};
obje[id] = val;
$.extend(question_data, obje);
});
// save data
block.writeData($.extend({}, {
"trial_type": "survey-likert",
"trial_index": block.trial_idx,
"rt": response_time
}, question_data, trial.data));
display_element.html('');
// next trial
block.next();
});
var startTime = (new Date()).getTime();
}
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,88 @@
/**
* jspsych-survey-text
* a jspsych plugin for free response survey questions
*
* Josh de Leeuw (March 2013)
* Updated October 2013
*
* parameters:
* questions: array of arrays. inner arrays are arrays of strings, where each string represents a prompt
* for the user to respond to.
* data: optional data object
*
*/
(function($) {
jsPsych['survey-text'] = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = [];
for (var i = 0; i < params.questions.length; i++) {
trials.push({
type: "survey-text",
questions: params.questions[i],
data: (typeof params.data === 'undefined') ? {} : params.data[i]
});
}
return trials;
};
plugin.trial = function(display_element, block, trial, part) {
// add likert scale questions
for (var i = 0; i < trial.questions.length; i++) {
// create div
display_element.append($('<div>', {
"id": 'surveytext' + i,
"class": 'surveyquestion'
}));
// add question text
$("#surveytext" + i).append('<p class="surveytext">' + trial.questions[i] + '</p>');
// add text box
$("#surveytext" + i).append('<input type="text" name="surveyresponse' + i + '"></input>');
}
// add submit button
display_element.append($('<button>', {
'id': 'next',
'class': 'surveytext'
}));
$("#next").html('Submit Answers');
$("#next").click(function() {
// measure response time
var endTime = (new Date()).getTime();
var response_time = endTime - startTime;
// create object to hold responses
var question_data = {};
$("div.surveyquestion").each(function(index) {
var id = "Q" + index;
var val = $(this).children('input').val();
var obje = {};
obje[id] = val;
$.extend(question_data, obje);
});
// save data
block.writeData($.extend({}, {
"trial_type": "survey-text",
"trial_index": block.trial_idx,
"rt": response_time
}, question_data, trial.data));
display_element.html('');
// next trial
block.next();
});
var startTime = (new Date()).getTime();
};
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,121 @@
/* jspsych-text.js
* Josh de Leeuw
*
* This plugin displays text (including HTML formatted strings) during the experiment.
* Use it to show instructions, provide performance feedback, etc...
*
* No data is currently collected with this plugin. Do not use it for situations in which
* data collection is important.
*
* Parameters:
* type: "text"
* text: an array of strings. Each element in the array will be displayed on a separate screen.
* cont_key: the keycode of the key the user should press to advance to the next screen. Default is '13' which is ENTER. May specify mouse click
* by listing the key as 'mouse'
* timing_post_trial: an array with a single element representing the time in milliseconds to delay on a blank screen after the continue key is pressed. Default is no delay.
* variables: see variables section below.
* data: optional data object
*
* Optional Variables: If you want to display dynamic information that is updated at the moment the text is rendered on the screen,
* such calculating an accuracy score to tell a subject how many trials they got right, you can use the optional variables parameter.
* The variables are specified as an array of arrays. The outer level array indexes for which block of text you are showing, and the
* outer array should have the same length as the text array. Each element of the outer level array is also an array, with one element
* for each variable that you are specifying. Variables are specified as functions which return the string that you want to display.
* To indicate where a variable should be placed in the text, use the special string "%v" in the text. This will be replaced by the
* return value of the function that is in the variables array. Each "%v" string will be replaced in the order that they appear in the
* text, and only one replacement will be made per function call. Therefore, you should have the same number of "%v" strings as you have
* elements in the inner arrays of the "variables" array.
*
* Example:
* var goodbye_func = function(){return "goodbye";}
* var hello_func = function(){return "hello";}
* "text": ["%v, %v.", "I don't know why you say %v, I say %v."]
* "variables":[[hello_func, hello_func],[goodbye_func, hello_func]]
*
* When the above parameters are loaded into the text plugin, the first screen would show
* "hello, hello." and the second screen would show "I don't know why you say goodbye, I say hello."
*
*/
(function($) {
jsPsych.text = (function() {
var plugin = {};
plugin.create = function(params) {
var trials = new Array(params.text.length);
for (var i = 0; i < trials.length; i++) {
trials[i] = {};
trials[i].type = "text"; // must match plugin name
trials[i].text = params.text[i]; // text of all trials
trials[i].cont_key = params.cont_key || '13'; // keycode to press to advance screen, default is ENTER.
trials[i].timing_post_trial = params.timing_post_trial || 0; // how long to delay between screens, default is no delay.
trials[i].variables = (typeof params.variables === 'undefined') ? undefined : params.variables[i];
trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
}
return trials;
};
plugin.trial = function(display_element, block, trial, part) {
// the text for the trial is in trial.text, but we need to replace any variables that are in the text.
var replaced_text = trial.text;
// check to see if there are any variables defined.
if (typeof trial.variables != 'undefined') {
for (var i = 0; i < trial.variables.length; i++) {
// loop through the array of variables and call each variable function
// to get the actual text that should be substituted in.
var variable_text = trial.variables[i]();
// replace the "%v" with the return value of the function.
replaced_text = replaced_text.replace("%v", variable_text);
}
}
// set the HTML of the display target to replaced_text.
display_element.html(replaced_text);
var startTime = (new Date()).getTime();
// define a function that will advance to the next trial when the user presses
// the continue key.
var key_listener = function(e) {
if (e.which == trial.cont_key) {
save_data();
$(document).unbind('keyup', key_listener); // remove the response function, so that it doesn't get triggered again.
display_element.html(''); // clear the display
setTimeout(function() {
block.next();
}, trial.timing_post_trial); // call block.next() to advance the experiment after a delay.
}
};
var mouse_listener = function(e) {
save_data();
display_element.unbind('click', mouse_listener); // remove the response function, so that it doesn't get triggered again.
display_element.html(''); // clear the display
setTimeout(function() {
block.next();
}, trial.timing_post_trial); // call block.next() to advance the experiment after a delay.
};
// check if key is 'mouse'
if (trial.cont_key == 'mouse') {
display_element.click(mouse_listener);
}
else {
// attach the response function to the html document.
$(document).keyup(key_listener);
}
var save_data = function() {
var rt = (new Date()).getTime() - startTime;
block.writeData($.extend({}, {
"trial_type": "text",
"trial_index": block.trial_idx,
"rt": rt
}, trial.data));
};
};
return plugin;
})();
})(jQuery);

View File

@ -0,0 +1,207 @@
/* jspsych-xab.js
* Josh de Leeuw
* updated Oct 2013
*
* This plugin runs a single XAB trial, where X is an image presented in isolation, and A and B are choices, with A or B being equal to X.
* The subject's goal is to identify whether A or B is identical to X.
*
* parameters:
* stimuli: array of arrays. each interior array represents the stimuli for a single trial.
* each interior array can be two or three elements. if two elements, then the plugin
* will show the first one as the target (X). if three elements, then the first is X
* the second is A and the third is B. the second is considered the target and the third
* is the foil. this is useful if X and A are not identical, but A is still the correct
* choice (e.g. a categorization experiment where the goal is to pick the item that is
* in the same category). stimuli can be paths to images, or html strings.
* left_key: key code for response associated with image on the left side of the screen.
* right_key: key code for right side
* timing_x: how long to display X for in ms
* timing_xab_gap: how long to show a blank screen in between X and AB in ms.
* timing_ab: how long to show the screen with AB in ms. -1 will display until a response is given.
* timing_post_trial: how long to show a blank screen after the trial is complete.
* is_html: must set to TRUE if the stimuli are HTML strings instead of images.
* prompt: an HTML string to display under the AB stimuli, e.g. to remind the subject which keys to use
* data: the optional data object
*
*/
(function($) {
jsPsych.xab = (function() {
var plugin = {};
plugin.create = function(params) {
// the number of trials is determined by how many entries the params.stimuli array has
var trials = new Array(params.stimuli.length);
for (var i = 0; i < trials.length; i++) {
trials[i] = {};
trials[i].type = "xab";
trials[i].x_path = params.stimuli[i][0];
// if there is only a pair of stimuli, then the first is the target and is shown twice.
// if there is a triplet, then the first is X, the second is the target, and the third is foil (useful for non-exact-match XAB).
if (params.stimuli[i].length == 2) {
trials[i].a_path = params.stimuli[i][0];
trials[i].b_path = params.stimuli[i][1];
}
else {
trials[i].a_path = params.stimuli[i][1];
trials[i].b_path = params.stimuli[i][2];
}
trials[i].left_key = params.left_key || 81; // defaults to 'q'
trials[i].right_key = params.right_key || 80; // defaults to 'p'
// timing parameters
trials[i].timing_x = params.timing_x || 1000; // defaults to 1000msec.
trials[i].timing_xab_gap = params.timing_xab_gap || 1000; // defaults to 1000msec.
trials[i].timing_ab = params.timing_ab || -1; // defaults to -1, meaning infinite time on AB. If a positive number is used, then AB will only be displayed for that length.
trials[i].timing_post_trial = params.timing_post_trial || 1000; // defaults to 1000msec.
// optional parameters
trials[i].is_html = (typeof params.is_html === 'undefined') ? false : params.is_html;
trials[i].prompt = (typeof params.prompt === 'undefined') ? "" : params.prompt;
trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
}
return trials;
};
var xab_trial_complete = false;
plugin.trial = function(display_element, block, trial, part) {
switch (part) {
// the first part of the trial is to show the X stimulus.
case 1:
// reset this variable to false
xab_trial_complete = false;
// how we display the content depends on whether the content is
// HTML code or an image path.
if (!trial.is_html) {
display_element.append($('<img>', {
src: trial.x_path,
"class": 'xab'
}));
}
else {
display_element.append($('<div>', {
"class": 'xab',
html: trial.x_path
}));
}
// start a timer of length trial.timing_x to move to the next part of the trial
setTimeout(function() {
plugin.trial(display_element, block, trial, part + 1);
}, trial.timing_x);
break;
// the second part of the trial is the gap between X and AB.
case 2:
// remove the x stimulus
$('.xab').remove();
// start timer
setTimeout(function() {
plugin.trial(display_element, block, trial, part + 1);
}, trial.timing_xab_gap);
break;
// the third part of the trial is to display A and B, and get the subject's response
case 3:
// randomize whether the target is on the left or the right
var images = [trial.a_path, trial.b_path];
var target_left = (Math.floor(Math.random() * 2) === 0); // 50% chance target is on left.
if (!target_left) {
images = [trial.b_path, trial.a_path];
}
// show the options
if (!trial.is_html) {
display_element.append($('<img>', {
"src": images[0],
"class": 'xab'
}));
display_element.append($('<img>', {
"src": images[1],
"class": 'xab'
}));
}
else {
display_element.append($('<div>', {
"class": 'xab',
html: images[0]
}));
display_element.append($('<div>', {
"class": 'xab',
html: images[1]
}));
}
if (trial.prompt !== "") {
display_element.append(trial.prompt);
}
// start measuring response time
var startTime = (new Date()).getTime();
// if timing_ab is > 0, then we hide the stimuli after timing_ab milliseconds
if (trial.timing_ab > 0) {
setTimeout(function() {
if (!xab_trial_complete) {
$('.xab').css('visibility', 'hidden');
}
}, trial.timing_ab);
}
// create the function that triggers when a key is pressed.
var resp_func = function(e) {
var flag = false; // true when a valid key is chosen
var correct = false; // true when the correct response is chosen
if (e.which == trial.left_key) // 'q' key by default
{
flag = true;
if (target_left) {
correct = true;
}
}
else if (e.which == trial.right_key) // 'p' key by default
{
flag = true;
if (!target_left) {
correct = true;
}
}
if (flag) {
var endTime = (new Date()).getTime();
var rt = (endTime - startTime);
// create object to store data from trial
var trial_data = {
"trial_type": "xab",
"trial_index": block.trial_idx,
"rt": rt,
"correct": correct,
"stimulus_x": trial.x_path,
"stimulus_a": trial.a_path,
"stimulus_b": trial.b_path,
"key_press": e.which
};
block.writeData($.extend({}, trial_data, trial.data));
$(document).unbind('keyup', resp_func); // remove response function from keys
display_element.html(''); // remove all
xab_trial_complete = true;
// move on to the next trial after timing_post_trial milliseconds
setTimeout(function() {
block.next();
}, trial.timing_post_trial);
}
};
$(document).keyup(resp_func);
break;
}
};
return plugin;
})();
})(jQuery);