diff --git a/examples/jspsych-call-function.html b/examples/jspsych-call-function.html index 355e7732..52c1eac8 100644 --- a/examples/jspsych-call-function.html +++ b/examples/jspsych-call-function.html @@ -10,7 +10,6 @@ diff --git a/examples/jspsych-survey-likert.html b/examples/jspsych-survey-likert.html new file mode 100644 index 00000000..0d7327f6 --- /dev/null +++ b/examples/jspsych-survey-likert.html @@ -0,0 +1,62 @@ + + + + + jspsych-categorize plugin example + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/examples/tutorial/download.html b/examples/tutorial/download.html new file mode 100644 index 00000000..e69de29b diff --git a/examples/tutorial/experiment.css b/examples/tutorial/experiment.css new file mode 100644 index 00000000..91afaeaa --- /dev/null +++ b/examples/tutorial/experiment.css @@ -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; } + diff --git a/examples/tutorial/index.html b/examples/tutorial/index.html new file mode 100644 index 00000000..6edd7092 --- /dev/null +++ b/examples/tutorial/index.html @@ -0,0 +1,81 @@ + + + + + My experiment + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/examples/tutorial/scripts/jspsych.js b/examples/tutorial/scripts/jspsych.js new file mode 100644 index 00000000..8e702a4f --- /dev/null +++ b/examples/tutorial/scripts/jspsych.js @@ -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
) 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($('')); + 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($('', { + 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); diff --git a/examples/tutorial/scripts/plugins/dev/jspsych-active-match.js b/examples/tutorial/scripts/plugins/dev/jspsych-active-match.js new file mode 100644 index 00000000..cbc0b815 --- /dev/null +++ b/examples/tutorial/scripts/plugins/dev/jspsych-active-match.js @@ -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($('', { + "src": trial.stimuli[trial.target_idx], + "class": '', + "id": 'am_target' + })); + + // show manipulate image + display_element.append($('', { + "src": trial.stimuli[trial.start_idx], + "class": '', + "id": 'am_manipulate' + })); + + // append a div for showing messages + display_element.append($('
', { + "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("

Minimum value reached. Go the other direction.

"); + } + 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("

Maximum value reached. Go the other direction.

"); + } + 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 -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 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=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 = '

You need to categorize at least '+min_percent_correct+'% of the items correctly in each round in order to make progress in training.

' + if(stop_criteria > -1) { + var remaining_blocks = stop_criteria - this.blocks_under_thresh; + if(remaining_blocks >= 1){ + completed += '

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.

' + } else { + completed += '

Training will now stop because your accuracy was below '+min_percent_correct+'% for '+stop_criteria+' consecutive rounds.

' + } + } + + } else { + if(remaining_objects == 0) + { + completed = '

Congratulations! You have completed training.

'; + } + else if(completed_objects > 0) + { + completed = '

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.

'; + } + else + { + completed = '

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.

'; + } + } + + display_element.html( + '

You correctly categorized '+percent_correct+'% of the items in that round.

'+completed+'

Press ENTER to continue.

' + ); + + 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($('', { + "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', { + "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); diff --git a/examples/tutorial/scripts/plugins/dev/jspsych-ballistic-match-2.js b/examples/tutorial/scripts/plugins/dev/jspsych-ballistic-match-2.js new file mode 100644 index 00000000..eede0a79 --- /dev/null +++ b/examples/tutorial/scripts/plugins/dev/jspsych-ballistic-match-2.js @@ -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($('', { + "src": trial.stimuli[trial.start_idx], + "class": 'bm_img', + "id": 'bm_manipulate' + })); + + // show target image + display_element.append($('', { + "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($('
',{ + "id":"bm_feedback", + })); + + if(change>0) { + $("#bm_feedback").html('

You said increase.

'); + } else { + $("#bm_feedback").html('

You said decrease.

'); + } + + 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($('
',{ + "id":"bm_feedback", + })); + if(correct){ + $("#bm_feedback").html('

Correct!

'); + } else { + $("#bm_feedback").html('

Wrong.

'); + } + 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($('',{ + "src": moving_img, + "id": "moving_image" + })); + + // show the linear trajectory below + + display_element.append($('
', { + "id": "trajectory"})); + + $("#trajectory").append($('
', { + "id": "line"})); + + // display the images on the trajectory + $("#trajectory").append($('
', { + "id": "target_flag"})); + + $("#target_flag").append($('
', { + "id": "target_dot"})); + + $("#target_flag").append($('
', { + "id": "target_words"})); + + $("#target_words").html("

Target Cell

"); + + $("#trajectory").append($('
', { + "id": "marker_flag"})); + + $("#marker_flag").append($('
', { + "id": "marker_dot"})); + + $("#marker_flag").append($('
', { + "id": "marker_words"})); + + $("#marker_words").html("

Above Cell

"); + + // label the trajectory line + $("#trajectory").append($('', { + "id": "left_label"})); + + $("#trajectory").append($('', { + "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); \ No newline at end of file diff --git a/examples/tutorial/scripts/plugins/dev/jspsych-ballistic-match.js b/examples/tutorial/scripts/plugins/dev/jspsych-ballistic-match.js new file mode 100644 index 00000000..8bd3f6ea --- /dev/null +++ b/examples/tutorial/scripts/plugins/dev/jspsych-ballistic-match.js @@ -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($('', { + "src": trial.stimuli[trial.start_idx], + "class": 'bm_img', + "id": 'bm_manipulate' + })); + + // show target image + display_element.append($('', { + "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($('
',{ + "id":"bm_feedback", + })); + + if(change>0) { + $("#bm_feedback").html('

You said increase.

'); + } else { + $("#bm_feedback").html('

You said decrease.

'); + } + + 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($('
',{ + "id":"bm_feedback", + })); + if(correct){ + $("#bm_feedback").html('

Correct!

'); + } else { + $("#bm_feedback").html('

Wrong.

'); + } + 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($('
', { + "id": "message_holder"})); + $("#message_holder").append($('

Less Chemical X

')); + $("#message_holder").append($('')); + + $("#message_holder").append($('',{ + "src":"img/400arrow.gif", + "id":"arrow" + })); + // display the images on the trajectory + display_element.append($('
',{ + "id": "bm_trajectory", + "css": { + "position":"relative" + } + })); + + $("#bm_trajectory").append($('',{ + "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($('',{ + "src":moving_img, + "id": "bm_moving", + "css": { + "position":"absolute", + "left": moving_left, + "top": 0 + } + })); + + $("#bm_trajectory").append($( + '
', + { + "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('

TARGET

'); + } + + return plugin; + })(); +})(jQuery); \ No newline at end of file diff --git a/examples/tutorial/scripts/plugins/dev/jspsych-categorize-animation.js b/examples/tutorial/scripts/plugins/dev/jspsych-categorize-animation.js new file mode 100644 index 00000000..36efd2d5 --- /dev/null +++ b/examples/tutorial/scripts/plugins/dev/jspsych-categorize-animation.js @@ -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($('', { + "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', { + "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', { + "id": "cm_"+i + })); + // for each INDIVIDUAL choice + for(var j = 0; j < trial.choices[i].length; j++) + { + // add a RADIO button + $('#cm_'+i).append($('', { + "type": "radio", + "name": "category_"+i, + "value": trial.choices[i][j], + "id": "cat_"+i+"_"+j + })); + + $('#cm_'+i).append(''); + + } + } + + // add a button to hit when done. + display_element.append($('')); + $('#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($('
')); + $("#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("

" + feedback + "

")); + + 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); \ No newline at end of file diff --git a/examples/tutorial/scripts/plugins/jspsych-same-different.js b/examples/tutorial/scripts/plugins/jspsych-same-different.js new file mode 100644 index 00000000..7156bb7b --- /dev/null +++ b/examples/tutorial/scripts/plugins/jspsych-same-different.js @@ -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($('', { + src: trial.a_path, + "class": 'sd' + })); + } + else { + display_element.append($('
', { + 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($('', { + src: trial.a_path, + "class": 'sd', + id: 'jspsych_sd_second_image' + })); + } + else { + display_element.append($('
', { + 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); \ No newline at end of file diff --git a/examples/tutorial/scripts/plugins/jspsych-similarity.js b/examples/tutorial/scripts/plugins/jspsych-similarity.js new file mode 100644 index 00000000..40c55241 --- /dev/null +++ b/examples/tutorial/scripts/plugins/jspsych-similarity.js @@ -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($('', { + "src": trial.a_path, + "class": 'sim' + })); + } + else { + display_element.append($('
', { + "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($('', { + "src": trial.b_path, + "class": 'sim', + "id": 'jspsych_sim_second_image' + })); + } + else { + display_element.append($('
', { + "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($('
', { + "id": 'slider', + "class": 'sim' + })); + $("#slider").slider({ + value: 50, + min: 0, + max: 100, + step: 1, + }); + + + // create labels for slider + display_element.append($('
', { + "id": 'slider_labels', + "class": 'sim' + })); + + $('#slider_labels').append($('

' + trial.label_low + '

')); + $('#slider_labels').append($('

' + trial.label_high + '

')); + + // create button + display_element.append($('