diff --git a/README.md b/README.md index 89eae08..968c2f9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ This repository is intended to be cloned/forked in order to set up a working jsPsych experiment via GitHub pages. This makes it simple to start exploring jsPsych. -**This project currently uses jsPsych v3.1** +**This project currently uses jsPsych v6.3.0** Instructions ------------ diff --git a/css/jspsych.css b/css/jspsych.css index dad8776..9a07da4 100644 --- a/css/jspsych.css +++ b/css/jspsych.css @@ -1,122 +1,206 @@ -/* +/* * CSS for jsPsych experiments. * - * This stylesheet provides minimal styling to make jsPsych + * This stylesheet provides minimal styling to make jsPsych * experiments look polished without any additional styles. - * */ -/* - * - * fonts and type - * - */ - -@import url(https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700); + @import url(https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700); -html { - font-family: 'Open Sans', 'Arial', sans-serif; - font-size: 18px; - line-height: 1.6em; -} +/* Container holding jsPsych content */ -p { - clear:both; -} + .jspsych-display-element { + display: flex; + flex-direction: column; + overflow-y: auto; + } -.very-small { - font-size: 50%; -} + .jspsych-display-element:focus { + outline: none; + } -.small { - font-size: 75%; -} + .jspsych-content-wrapper { + display: flex; + margin: auto; + flex: 1 1 100%; + width: 100%; + } -.large { - font-size: 125%; -} + .jspsych-content { + max-width: 95%; /* this is mainly an IE 10-11 fix */ + text-align: center; + margin: auto; /* this is for overflowing content */ + } -.very-large { - font-size: 150%; -} + .jspsych-top { + align-items: flex-start; + } -/* - * - * Classes for changing location of things - * - */ - -.left { - float: left; -} + .jspsych-middle { + align-items: center; + } -.right { - float: right; -} +/* fonts and type */ -.center-content { - text-align: center; +.jspsych-display-element { + font-family: 'Open Sans', 'Arial', sans-serif; + font-size: 18px; + line-height: 1.6em; } -/* - * - * Form elements like input fields and buttons - * - */ +/* Form elements like input fields and buttons */ -input[type="text"] { - font-family: 'Open Sans', 'Arial', sans-sefif; - font-size: 14px; +.jspsych-display-element input[type="text"] { + font-family: 'Open Sans', 'Arial', sans-serif; + font-size: 14px; } -button { - padding: 0.5em; - background-color: #eaeaea; - border: 1px solid #eaeaea; - color: #333; - font-family: 'Open Sans', 'Arial', sans-serif; - font-size: 14px; - cursor: pointer; +/* borrowing Bootstrap style for btn elements, but combining styles a bit */ +.jspsych-btn { + display: inline-block; + padding: 6px 12px; + margin: 0px; + font-size: 14px; + font-weight: 400; + font-family: 'Open Sans', 'Arial', sans-serif; + cursor: pointer; + line-height: 1.4; + text-align: center; + white-space: nowrap; + vertical-align: middle; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; + color: #333; + background-color: #fff; + border-color: #ccc; } -button:hover { - border:1px solid #ccc; +/* only apply the hover style on devices with a mouse/pointer that can hover - issue #977 */ +@media (hover: hover) { + .jspsych-btn:hover { + background-color: #ddd; + border-color: #aaa; + } } -/* - * - * Container holding jsPsych content - * - */ +.jspsych-btn:active { + background-color: #ddd; + border-color:#000000; +} +.jspsych-btn:disabled { + background-color: #eee; + color: #aaa; + border-color: #ccc; + cursor: not-allowed; +} -.jspsych-display-element { - width: 800px; - margin: 50px auto 50px auto; +/* custom style for input[type="range] (slider) to improve alignment between positions and labels */ + +.jspsych-slider { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + width: 100%; + background: transparent; +} +.jspsych-slider:focus { + outline: none; +} +/* track */ +.jspsych-slider::-webkit-slider-runnable-track { + appearance: none; + -webkit-appearance: none; + width: 100%; + height: 8px; + cursor: pointer; + background: #eee; + box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; + border-radius: 2px; + border: 1px solid #aaa; +} +.jspsych-slider::-moz-range-track { + appearance: none; + width: 100%; + height: 8px; + cursor: pointer; + background: #eee; + box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; + border-radius: 2px; + border: 1px solid #aaa; +} +.jspsych-slider::-ms-track { + appearance: none; + width: 99%; + height: 14px; + cursor: pointer; + background: #eee; + box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; + border-radius: 2px; + border: 1px solid #aaa; +} +/* thumb */ +.jspsych-slider::-webkit-slider-thumb { + border: 1px solid #666; + height: 24px; + width: 15px; + border-radius: 5px; + background: #ffffff; + cursor: pointer; + -webkit-appearance: none; + margin-top: -9px; +} +.jspsych-slider::-moz-range-thumb { + border: 1px solid #666; + height: 24px; + width: 15px; + border-radius: 5px; + background: #ffffff; + cursor: pointer; +} +.jspsych-slider::-ms-thumb { + border: 1px solid #666; + height: 20px; + width: 15px; + border-radius: 5px; + background: #ffffff; + cursor: pointer; + margin-top: -2px; } -/* - * - * PLUGIN: jspsych-single-stim - * - */ - -#jspsych-single-stim-stimulus { - display: block; - margin-left: auto; - margin-right: auto; +/* jsPsych progress bar */ + +#jspsych-progressbar-container { + color: #555; + border-bottom: 1px solid #dedede; + background-color: #f9f9f9; + margin-bottom: 1em; + text-align: center; + padding: 8px 0px; + width: 100%; + line-height: 1em; +} +#jspsych-progressbar-container span { + font-size: 14px; + padding-right: 14px; +} +#jspsych-progressbar-outer { + background-color: #eee; + width: 50%; + margin: auto; + height: 14px; + display: inline-block; + vertical-align: middle; + box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); +} +#jspsych-progressbar-inner { + background-color: #aaa; + width: 0%; + height: 100%; } -/* - * - * PLUGIN: jspsych-survey-text - * - */ - - .jspsych-survey-text { - margin: 0.25em 0em; - } - - .jspsych-survey-text-question { - margin: 2em 0em; - } \ No newline at end of file +/* Control appearance of jsPsych.data.displayData() */ +#jspsych-data-display { + text-align: left; +} diff --git a/img/blue.png b/img/blue.png new file mode 100644 index 0000000..820bdce Binary files /dev/null and b/img/blue.png differ diff --git a/img/congruent_left.gif b/img/congruent_left.gif deleted file mode 100644 index 4edfbb5..0000000 Binary files a/img/congruent_left.gif and /dev/null differ diff --git a/img/congruent_right.gif b/img/congruent_right.gif deleted file mode 100644 index 0c4855e..0000000 Binary files a/img/congruent_right.gif and /dev/null differ diff --git a/img/incongruent_left.gif b/img/incongruent_left.gif deleted file mode 100644 index 922503c..0000000 Binary files a/img/incongruent_left.gif and /dev/null differ diff --git a/img/incongruent_right.gif b/img/incongruent_right.gif deleted file mode 100644 index 43b6113..0000000 Binary files a/img/incongruent_right.gif and /dev/null differ diff --git a/img/orange.png b/img/orange.png new file mode 100644 index 0000000..108e6e5 Binary files /dev/null and b/img/orange.png differ diff --git a/index.html b/index.html index b7eab9a..896a8c6 100644 --- a/index.html +++ b/index.html @@ -1,79 +1,120 @@ - + - -
-The experiment failed to load.
'; + loadfail = true; + DOM_target.innerHTML = message; + } - // flatten the images array - images = flatten(images); + core.getSafeModeStatus = function() { + return file_protocol; + } - var n_loaded = 0; - var loadfn = (typeof callback_load === 'undefined') ? function() {} : callback_load; - var finishfn = (typeof callback_complete === 'undefined') ? function() {} : callback_complete; + function TimelineNode(parameters, parent, relativeID) { - for (var i = 0; i < images.length; i++) { - var img = new Image(); + // a unique ID for this node, relative to the parent + var relative_id; - img.onload = function() { - n_loaded++; - loadfn(n_loaded); - if (n_loaded == images.length) { - finishfn(); - } - }; + // store the parent for this node + var parent_node; - img.src = images[i]; - } - }; - - core.getDisplayElement = function() { - return DOM_target; + // parameters for the trial if the node contains a trial + var trial_parameters; + + // parameters for nodes that contain timelines + var timeline_parameters; + + // stores trial information on a node that contains a timeline + // used for adding new trials + var node_trial_data; + + // track progress through the node + var progress = { + current_location: -1, // where on the timeline (which timelinenode) + current_variable_set: 0, // which set of variables to use from timeline_variables + current_repetition: 0, // how many times through the variable set on this run of the node + current_iteration: 0, // how many times this node has been revisited + done: false + } + + // reference to self + var self = this; + + // recursively get the next trial to run. + // if this node is a leaf (trial), then return the trial. + // otherwise, recursively find the next trial in the child timeline. + this.trial = function() { + if (typeof timeline_parameters == 'undefined') { + // returns a clone of the trial_parameters to + // protect functions. + return jsPsych.utils.deepCopy(trial_parameters); + } else { + if (progress.current_location >= timeline_parameters.timeline.length) { + return null; + } else { + return timeline_parameters.timeline[progress.current_location].trial(); } + } + } + + this.markCurrentTrialComplete = function() { + if(typeof timeline_parameters == 'undefined'){ + progress.done = true; + } else { + timeline_parameters.timeline[progress.current_location].markCurrentTrialComplete(); + } + } - // - // private functions // - // - function run() { - // take the experiment structure and dynamically create a set of blocks - exp_blocks = new Array(opts.experiment_structure.length); + this.nextRepetiton = function() { + this.setTimelineVariablesOrder(); + progress.current_location = -1; + progress.current_variable_set = 0; + progress.current_repetition++; + for (var i = 0; i < timeline_parameters.timeline.length; i++) { + timeline_parameters.timeline[i].reset(); + } + } - // iterate through block list to create trials - for (var i = 0; i < exp_blocks.length; i++) { + // set the order for going through the timeline variables array + this.setTimelineVariablesOrder = function() { + + // check to make sure this node has variables + if(typeof timeline_parameters === 'undefined' || typeof timeline_parameters.timeline_variables === 'undefined'){ + return; + } + + var order = []; + for(var i=0; iThe minimum width is '+mw+'px. Your current width is '+w+'px.
'+ + 'The minimum height is '+mh+'px. Your current height is '+h+'px.
'; + core.getDisplayElement().innerHTML = msg; + } else { + clearInterval(interval); + core.getDisplayElement().innerHTML = ''; + checkExclusions(exclusions, success, fail); + } + }, 100); + return; // prevents checking other exclusions while this is being fixed + } + } - var start_time; - if (rt_method == 'date') { - start_time = (new Date()).getTime(); - } - if (rt_method == 'performance') { - start_time = performance.now(); - } + // WEB AUDIO API + if(typeof exclusions.audio !== 'undefined' && exclusions.audio) { + if(window.hasOwnProperty('AudioContext') || window.hasOwnProperty('webkitAudioContext')){ + // clear + } else { + clear = false; + var msg = 'Your browser does not support the WebAudio API, which means that you will not '+ + 'be able to complete the experiment.
Browsers that support the WebAudio API include '+ + 'Chrome, Firefox, Safari, and Edge.
'; + core.getDisplayElement().innerHTML = msg; + fail(); + return; + } + } - var listener_id; - - var listener_function = function(e) { + // GO? + if(clear){ success(); } + } + + function drawProgressBar(msg) { + document.querySelector('.jspsych-display-element').insertAdjacentHTML('afterbegin', + 'Correct
", + description: 'String to show when correct answer is given.' + }, + incorrect_text: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Incorrect text', + default: "Incorrect
", + description: 'String to show when incorrect answer is given.' + }, + prompt: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Prompt', + default: null, + description: 'Any content here will be displayed below the stimulus.' + }, + force_correct_button_press: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Force correct button press', + default: false, + description: 'If set to true, then the subject must press the correct response key after feedback in order to advance to next trial.' + }, + show_stim_with_feedback: { + type: jsPsych.plugins.parameterType.BOOL, + default: true, + no_function: false, + description: '' + }, + show_feedback_on_timeout: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Show feedback on timeout', + default: false, + description: 'If true, stimulus will be shown during feedback. If false, only the text feedback will be displayed during feedback.' + }, + timeout_message: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Timeout message', + default: "Please respond faster.
", + description: 'The message displayed on a timeout non-response.' + }, + stimulus_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Stimulus duration', + default: null, + description: 'How long to hide stimulus.' + }, + trial_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Trial duration', + default: null, + description: 'How long to show trial' + }, + feedback_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Feedback duration', + default: 2000, + description: 'How long to show feedback.' + } + } + } + + plugin.trial = function(display_element, trial) { + + display_element.innerHTML = 'Correct
", + description: 'String to show when correct answer is given.' + }, + incorrect_text: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Incorrect text', + default: "Incorrect
", + description: 'String to show when incorrect answer is given.' + }, + prompt: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Prompt', + default: null, + description: 'Any content here will be displayed below the stimulus.' + }, + force_correct_button_press: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Force correct button press', + default: false, + description: 'If set to true, then the subject must press the correct response key after feedback in order to advance to next trial.' + }, + show_stim_with_feedback: { + type: jsPsych.plugins.parameterType.BOOL, + default: true, + no_function: false, + description: '' + }, + show_feedback_on_timeout: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Show feedback on timeout', + default: false, + description: 'If true, stimulus will be shown during feedback. If false, only the text feedback will be displayed during feedback.' + }, + timeout_message: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Timeout message', + default: "Please respond faster.
", + description: 'The message displayed on a timeout non-response.' + }, + stimulus_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Stimulus duration', + default: null, + description: 'How long to hide stimulus.' + }, + trial_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Trial duration', + default: null, + description: 'How long to show trial' + }, + feedback_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Feedback duration', + default: 2000, + description: 'How long to show feedback.' + } + } + } + + plugin.trial = function(display_element, trial) { + + display_element.innerHTML = ''; + + // hide image after time if the timing parameter is set + if (trial.stimulus_duration !== null) { + jsPsych.pluginAPI.setTimeout(function() { + display_element.querySelector('#jspsych-categorize-image-stimulus').style.visibility = 'hidden'; + }, trial.stimulus_duration); + } + + // if prompt is set, show prompt + if (trial.prompt !== null) { + display_element.innerHTML += trial.prompt; + } + + var trial_data = {}; + + // create response function + var after_response = function(info) { + + // kill any remaining setTimeout handlers + jsPsych.pluginAPI.clearAllTimeouts(); + + // clear keyboard listener + jsPsych.pluginAPI.cancelAllKeyboardResponses(); + + var correct = false; + if (jsPsych.pluginAPI.compareKeys(trial.key_answer, info.key)) { + correct = true; + } + + // save data + trial_data = { + rt: info.rt, + correct: correct, + stimulus: trial.stimulus, + response: info.key + }; + + display_element.innerHTML = ''; + + var timeout = info.rt == null; + doFeedback(correct, timeout); + } + + jsPsych.pluginAPI.getKeyboardResponse({ + callback_function: after_response, + valid_responses: trial.choices, + rt_method: 'performance', + persist: false, + allow_held_key: false + }); + + if (trial.trial_duration !== null) { + jsPsych.pluginAPI.setTimeout(function() { + after_response({ + key: null, + rt: null + }); + }, trial.trial_duration); + } + + function doFeedback(correct, timeout) { + + if (timeout && !trial.show_feedback_on_timeout) { + display_element.innerHTML += trial.timeout_message; + } else { + // show image during feedback if flag is set + if (trial.show_stim_with_feedback) { + display_element.innerHTML = ''; + } + + // substitute answer in feedback string. + var atext = ""; + if (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.innerHTML += atext; + } + // check if force correct button press is set + if (trial.force_correct_button_press && correct === false && ((timeout && trial.show_feedback_on_timeout) || !timeout)) { + + var after_forced_response = function(info) { + endTrial(); + } + + jsPsych.pluginAPI.getKeyboardResponse({ + callback_function: after_forced_response, + valid_responses: [trial.key_answer], + rt_method: 'performance', + persist: false, + allow_held_key: false + }); + + } else { + jsPsych.pluginAPI.setTimeout(function() { + endTrial(); + }, trial.feedback_duration); + } + + } + + function endTrial() { + display_element.innerHTML = ''; + jsPsych.finishTrial(trial_data); + } + + }; + + return plugin; +})(); diff --git a/scripts/plugins/jspsych-categorize.js b/scripts/plugins/jspsych-categorize.js deleted file mode 100644 index 17ee77b..0000000 --- a/scripts/plugins/jspsych-categorize.js +++ /dev/null @@ -1,180 +0,0 @@ -/** - * jspsych plugin for categorization trials with feedback - * Josh de Leeuw - * - * documentation: https://github.com/jodeleeuw/jsPsych/wiki/jspsych-categorize -**/ - -(function($) { - jsPsych.categorize = (function() { - - var plugin = {}; - - plugin.create = function(params) { - - params = jsPsych.pluginAPI.enforceArray(params, ['choices', 'stimuli', 'key_answer', 'text_answer', 'data']); - - 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') ? "Correct
" : params.correct_text; - trials[i].incorrect_text = (typeof params.incorrect_text === 'undefined') ? "Incorrect
" : 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 = (typeof params.timing_post_trial === 'undefined') ? 1000 : params.timing_post_trial; - // 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) { - - // if any trial variables are functions - // this evaluates the function and replaces - // it with the output of the function - trial = jsPsych.pluginAPI.normalizeTrialVariables(trial); - - switch (part) { - case 1: - // set finish flag - cat_trial_complete = false; - - if (!trial.is_html) { - // add image to display - display_element.append($('', { - "src": trial.a_path, - "class": 'jspsych-categorize-stimulus', - "id": 'jspsych-categorize-stimulus' - })); - } - else { - display_element.append($(''+get_counter_text(trial.stimuli.length)+'
Press " + trial.right_category_key + " for:
" +
+ trial.right_category_label[0].bold() + '
Press " + trial.right_category_key + " for:
" +
+ trial.right_category_label[0].bold() + "
" + "or
" +
+ trial.right_category_label[1].bold() + "
"+trial.html_when_wrong+"
If you press the wrong key, a red X will appear. Press any key to continue.
', + description: 'Instructions shown at the bottom of the page.' + }, + force_correct_key_press: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Force correct key press', + default: false, + description: 'If true, in order to advance to the next trial after a wrong key press the user will be forced to press the correct key.' + }, + stim_key_association: { + type: jsPsych.plugins.parameterType.HTML_STRING, + pretty_name: 'Stimulus key association', + options: ['left', 'right'], + default: undefined, + description: 'Stimulus will be associated with either "left" or "right".' + }, + response_ends_trial: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response ends trial', + default: true, + description: 'If true, trial will end when user makes a response.' + }, + trial_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Trial duration', + default: null, + description: 'How long to show the trial.' + }, + } + } + + + plugin.trial = function(display_element, trial) { + + var html_str = ""; + + html_str += ""; + + html_str += "Press " + trial.left_category_key + " for:
" +
+ trial.left_category_label[0].bold() + "
Press " + trial.left_category_key + " for:
" +
+ trial.left_category_label[0].bold() + "
" + "or
" +
+ trial.left_category_label[1].bold() + "
Press " + trial.right_category_key + " for:
" +
+ trial.right_category_label[0].bold() + '
Press " + trial.right_category_key + " for:
" +
+ trial.right_category_label[0].bold() + "
" + "or
" +
+ trial.right_category_label[1].bold() + "
"+trial.html_when_wrong+"
" + feedback + "
")); - - setTimeout(function() { - next_trial(); - }, trial.timing_feedback); - - } - else { - next_trial(); - } - } - - function next_trial() { - - display_element.html(''); - - // next trial - if (trial.timing_post_trial > 0) { - setTimeout(function() { - block.next(); - }, trial.timing_post_trial); - } - else { - block.next(); - } - - } - - - }; - - // method for drawing palmer stimuli. - // returns the string description of svg element containing the stimulus - // requires raphaeljs library -> www.raphaeljs.com - - plugin.generate_stimulus = function(square_size, grid_spacing, circle_radius, configuration) { - - // create a div to hold the generated svg object - var stim_div = $('body').append(''); - - var size = grid_spacing * (square_size + 1); - - // create the svg raphael object - var paper = Raphael("jspsych-palmer-temp-stim", size, size); - - // create the circles at the vertices. - var circles = []; - var node_idx = 0; - for (var i = 1; i <= square_size; i++) { - for (var j = 1; j <= square_size; j++) { - var circle = paper.circle(grid_spacing * j, grid_spacing * i, circle_radius); - circle.attr("fill", "#000").attr("stroke-width", "0").attr("stroke", "#000").data("node", node_idx); - node_idx++; - circles.push(circle); - } - } - - // create all possible lines that connect circles - var horizontal_lines = []; - var vertical_lines = []; - var backslash_lines = []; - var forwardslash_lines = []; - - for (var i = 0; i < square_size; i++) { - for (var j = 0; j < square_size; j++) { - var current_item = (i * square_size) + j; - // add horizontal connections - if (j < (square_size - 1)) { - horizontal_lines.push([current_item, current_item + 1]); - } - // add vertical connections - if (i < (square_size - 1)) { - vertical_lines.push([current_item, current_item + square_size]); - } - // add diagonal backslash connections - if (i < (square_size - 1) && j < (square_size - 1)) { - backslash_lines.push([current_item, current_item + square_size + 1]); - } - // add diagonal forwardslash connections - if (i < (square_size - 1) && j > 0) { - forwardslash_lines.push([current_item, current_item + 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 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; - } - } - } - - // displays the line wherever there - // is a 1 in the array. - // showConfiguration(configuration) - for (var i = 0; i < configuration.length; i++) { - if (configuration[i] == 1) { - toggle_line(i); - } - } - - - var svg = $("#jspsych-palmer-temp-stim").html(); - - $('#jspsych-palmer-temp-stim').remove(); - - return svg; - }; - - return plugin; - })(); -})(jQuery); diff --git a/scripts/plugins/jspsych-preload.js b/scripts/plugins/jspsych-preload.js new file mode 100644 index 0000000..8a8e1c0 --- /dev/null +++ b/scripts/plugins/jspsych-preload.js @@ -0,0 +1,345 @@ +/** + * jspsych-preload + * documentation: docs.jspsych.org + **/ + +jsPsych.plugins['preload'] = (function() { + + var plugin = {}; + + plugin.info = { + name: 'preload', + description: '', + parameters: { + auto_preload: { + type: jsPsych.plugins.parameterType.BOOL, + default: false, + description: 'Whether or not to automatically preload any media files based on the timeline passed to jsPsych.init.' + }, + trials: { + type: jsPsych.plugins.parameterType.TIMELINE, + default: [], + description: 'Array with a timeline of trials to automatically preload. If one or more trial objects is provided, '+ + 'then the plugin will attempt to preload the media files used in the trial(s).' + }, + images: { + type: jsPsych.plugins.parameterType.STRING, + default: [], + description: 'Array with one or more image files to load. This parameter is often used in cases where media files cannot '+ + 'be automatically preloaded based on the timeline, e.g. because the media files are passed into an image plugin/parameter with '+ + 'timeline variables or dynamic parameters, or because the image is embedded in an HTML string.' + }, + audio: { + type: jsPsych.plugins.parameterType.STRING, + default: [], + description: 'Array with one or more audio files to load. This parameter is often used in cases where media files cannot '+ + 'be automatically preloaded based on the timeline, e.g. because the media files are passed into an audio plugin/parameter with '+ + 'timeline variables or dynamic parameters, or because the audio is embedded in an HTML string.' + }, + video: { + type: jsPsych.plugins.parameterType.STRING, + default: [], + description: 'Array with one or more video files to load. This parameter is often used in cases where media files cannot '+ + 'be automatically preloaded based on the timeline, e.g. because the media files are passed into a video plugin/parameter with '+ + 'timeline variables or dynamic parameters, or because the video is embedded in an HTML string.' + }, + message: { + type: jsPsych.plugins.parameterType.HTML_STRING, + default: null, + description: 'HTML-formatted message to be shown above the progress bar while the files are loading.' + }, + show_progress_bar: { + type: jsPsych.plugins.parameterType.BOOL, + default: true, + description: 'Whether or not to show the loading progress bar.' + }, + continue_after_error: { + type: jsPsych.plugins.parameterType.BOOL, + default: false, + description: 'Whether or not to continue with the experiment if a loading error occurs. If false, then if a loading error occurs, '+ + 'the error_message will be shown on the page and the trial will not end. If true, then if if a loading error occurs, the trial will end '+ + 'and preloading failure will be logged in the trial data.' + }, + error_message: { + type: jsPsych.plugins.parameterType.HTML_STRING, + default: 'The experiment failed to load.', + description: 'Error message to show on the page in case of any loading errors. This parameter is only relevant when continue_after_error is false.' + }, + show_detailed_errors: { + type: jsPsych.plugins.parameterType.BOOL, + default: false, + description: 'Whether or not to show a detailed error message on the page. If true, then detailed error messages will be shown on the '+ + 'page for all files that failed to load, along with the general error_message. This parameter is only relevant when continue_after_error is false.' + }, + max_load_time: { + type: jsPsych.plugins.parameterType.INT, + default: null, + description: 'The maximum amount of time that the plugin should wait before stopping the preload and either ending the trial '+ + '(if continue_after_error is true) or stopping the experiment with an error message (if continue_after_error is false). '+ + 'If null, the plugin will wait indefintely for the files to load.' + }, + on_error: { + type: jsPsych.plugins.parameterType.FUNCTION, + default: null, + description: 'Function to be called after a file fails to load. The function takes the file name as its only argument.' + }, + on_success: { + type: jsPsych.plugins.parameterType.FUNCTION, + default: null, + description: 'Function to be called after a file loads successfully. The function takes the file name as its only argument.' + } + } + } + + plugin.trial = function(display_element, trial) { + + var success = null; + var timeout = false; + var failed_images = []; + var failed_audio = []; + var failed_video = []; + var detailed_errors = []; + var in_safe_mode = jsPsych.getSafeModeStatus(); + + // create list of media to preload // + + var images = []; + var audio = []; + var video = []; + + if(trial.auto_preload){ + var auto_preload = jsPsych.pluginAPI.getAutoPreloadList(); + images = images.concat(auto_preload.images); + audio = audio.concat(auto_preload.audio); + video = video.concat(auto_preload.video); + } + + if(trial.trials.length > 0){ + var trial_preloads = jsPsych.pluginAPI.getAutoPreloadList(trial.trials); + images = images.concat(trial_preloads.images); + audio = audio.concat(trial_preloads.audio); + video = video.concat(trial_preloads.video); + } + + images = images.concat(trial.images); + audio = audio.concat(trial.audio); + video = video.concat(trial.video); + + images = jsPsych.utils.unique(jsPsych.utils.flatten(images)); + audio = jsPsych.utils.unique(jsPsych.utils.flatten(audio)); + video = jsPsych.utils.unique(jsPsych.utils.flatten(video)); + + if (in_safe_mode) { + // don't preload video if in safe mode (experiment is running via file protocol) + video = []; + } + + // render display of message and progress bar + + var html = ''; + + if(trial.message !== null){ + html += trial.message; + } + + if(trial.show_progress_bar){ + html += ` +Error loading file: '+source+'
';
+ if (e.error.statusText) {
+ err_msg += 'File request response status: '+e.error.statusText+'
';
+ }
+ if (e.error == "404") {
+ err_msg += '404 - file not found.
';
+ }
+ if (typeof e.error.loaded !== 'undefined' && e.error.loaded !== null && e.error.loaded !== 0) {
+ err_msg += e.error.loaded+' bytes transferred.';
+ } else {
+ err_msg += 'File did not begin loading. Check that file path is correct and reachable by the browser,
'+
+ 'and that loading is not blocked by cross-origin resource sharing (CORS) errors.';
+ }
+ err_msg += '
Loading timed out.
'+
+ 'Consider compressing your stimuli files, loading your files in smaller batches,
'+
+ 'and/or increasing the max_load_time parameter.
Error details:
'; + detailed_errors.forEach(function(e) { + display_element.innerHTML += e; + }); + } + } + + function after_error(source) { + // call on_error function and pass file name + if (trial.on_error !== null) { + trial.on_error(source); + } + } + function after_success(source) { + // call on_success function and pass file name + if (trial.on_success !== null) { + trial.on_success(source); + } + } + + function end_trial(){ + // clear timeout again when end_trial is called, to handle race condition with max_load_time + jsPsych.pluginAPI.clearAllTimeouts(); + var trial_data = { + success: success, + timeout: timeout, + failed_images: failed_images, + failed_audio: failed_audio, + failed_video: failed_video + }; + // clear the display + display_element.innerHTML = ''; + jsPsych.finishTrial(trial_data); + } + }; + + return plugin; + })(); + \ No newline at end of file diff --git a/scripts/plugins/jspsych-rdk.js b/scripts/plugins/jspsych-rdk.js new file mode 100644 index 0000000..80674f1 --- /dev/null +++ b/scripts/plugins/jspsych-rdk.js @@ -0,0 +1,1373 @@ +/* + + RDK plugin for JsPsych + ---------------------- + + This code was created in the Consciousness and Metacognition Lab at UCLA, + under the supervision of Brian Odegaard and Hakwan Lau + + We would appreciate it if you cited this paper when you use the RDK: + Rajananda, S., Lau, H. & Odegaard, B., (2018). A Random-Dot Kinematogram for Web-Based Vision Research. Journal of Open Research Software. 6(1), p.6. DOI: [http://doi.org/10.5334/jors.194] + + ---------------------- + + Copyright (C) 2017 Sivananda Rajananda + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see' + trial.questions[i] + '
'); - - // create slider - $("#jspsych-survey-likert-" + i).append($('' + question.prompt + '
'; + + // create option check boxes + for (var j = 0; j < question.options.length; j++) { + var option_id_name = _join(plugin_id_name, "option", question_id, j); + + // add check box container + display_element.querySelector(question_selector).innerHTML += ''; + + // add label and question text + var form = document.getElementById(option_id_name) + var input_name = _join(plugin_id_name, 'response', question_id); + var input_id = _join(plugin_id_name, 'response', question_id, j); + var label = document.createElement('label'); + label.setAttribute('class', plugin_id_name+'-text'); + label.innerHTML = question.options[j]; + label.setAttribute('for', input_id) + + // create checkboxes + var input = document.createElement('input'); + input.setAttribute('type', "checkbox"); + input.setAttribute('name', input_name); + input.setAttribute('id', input_id); + input.setAttribute('value', question.options[j]) + form.appendChild(label) + label.insertBefore(input, label.firstChild) + } + } + // add submit button + trial_form.innerHTML += '' + trial_form.innerHTML += '' + trial.questions[i] + '
'); - - // add text box - $("#jspsych-survey-text-" + i).append(''); - } - - // add submit button - display_element.append($('Click and drag the lower right corner of the image until it is the same size as a credit card held up to the screen.
+You can use any card that is the same size as a credit card, like a membership card or driver's license.
+If you do not have access to a real card you can use a ruler to measure the image width to 3.37 inches or 85.6 mm.
+Now we will quickly measure how far away you are sitting.
+Press the space bar when you are ready to begin.
+ `, + description: "HTML-formatted prompt to be shown on the screen during blindspot estimates." + }, + // blindspot_start_prompt: { + // type: jsPsych.plugins.parameterType.HTML_STRING, + // pretty_name: "Blindspot start prompt", + // default: "Start", + // description: "Content of the start button for the blindspot tasks.", + // }, + blindspot_measurements_prompt: { + type: jsPsych.plugins.parameterType.HTML_STRING, + pretty_name: "Blindspot measurements prompt", + default: "Remaining measurements: ", + description: "Text accompanying the remaining measures counter", + }, + viewing_distance_report: { + type: jsPsych.plugins.parameterType.HTML_STRING, + pretty_name: "Viewing distance report", + default: "Based on your responses, you are sitting about from the screen.
Does that seem about right?
", + description: + 'If "none" is given, viewing distance will not be reported to the participant', + }, + redo_measurement_button_label: { + type: jsPsych.plugins.parameterType.HTML_STRING, + pretty_name: "Re-do measurement button label", + default: 'No, that is not close. Try again.', + description: "Label for the button that can be clicked on the viewing distance report screen to re-do the blindspot estimate(s)." + }, + blindspot_done_prompt: { + type: jsPsych.plugins.parameterType.HTML_STRING, + pretty_name: "Blindspot done prompt", + default: "Yes", + description: "Label for the button that can be clicked on the viewing distance report screen to accept the viewing distance estimate.", + }, + }, + }; + + plugin.trial = function (display_element, trial) { + /* check parameter compatibility */ + if (!(trial.blindspot_reps > 0) && (trial.resize_units == "deg" || trial.resize_units == "degrees")) { + console.error("Blindspot repetitions set to 0, so resizing to degrees of visual angle is not possible!"); + return; + } + + /* some additional parameter configuration */ + let trial_data = { + item_width_mm: trial.item_width_mm, + item_height_mm: trial.item_height_mm, //card dimension: 85.60 × 53.98 mm (3.370 × 2.125 in) + }; + + let blindspot_config_data = { + ball_pos: [], + slider_clck: false, + }; + + let aspect_ratio = trial.item_width_mm / trial.item_height_mm; + + const start_div_height = + aspect_ratio < 1 + ? trial.item_init_size + : Math.round(trial.item_init_size / aspect_ratio); + const start_div_width = + aspect_ratio < 1 + ? Math.round(trial.item_init_size * aspect_ratio) + : trial.item_init_size; + const adjust_size = Math.round(start_div_width * 0.1); + + /* create content for first screen, resizing card */ + let pagesize_content = ` +', {
- id: 'jspsych-vsl-grid-scene-table-' + row + '-' + col,
- css: {
- padding: image_size[1] / 10 + "px " + image_size[0] / 10 + "px",
- border: '1px solid #555'
- }
- }));
- $('#jspsych-vsl-grid-scene-table-' + row + '-' + col).append($(' ', {
- id: 'jspsych-vsl-grid-scene-table-cell-' + row + '-' + col,
- css: {
- width: image_size[0] + "px",
- height: image_size[1] + "px"
- }
- }));
- }
- }
-
-
- for (var row = 0; row < nrows; row++) {
- for (var col = 0; col < ncols; col++) {
- if (pattern[row][col] !== 0) {
- $('#jspsych-vsl-grid-scene-table-cell-' + row + '-' + col).append($('', {
- src: pattern[row][col],
- css: {
- width: image_size[0] + "px",
- height: image_size[1] + "px",
- }
- }));
- }
- }
- }
-
- var html_out = $('#jspsych-vsl-grid-scene-dummy').html();
- $('#jspsych-vsl-grid-scene-dummy').remove();
-
- return html_out;
-
- };
-
- return plugin;
- })();
-})(jQuery);
+jsPsych.plugins['vsl-grid-scene'] = (function() {
+
+ var plugin = {};
+
+ jsPsych.pluginAPI.registerPreload('vsl-grid-scene', 'stimuli', 'image');
+
+ plugin.info = {
+ name: 'vsl-grid-scene',
+ description: '',
+ parameters: {
+ stimuli: {
+ type: jsPsych.plugins.parameterType.IMAGE,
+ pretty_name: 'Stimuli',
+ array: true,
+ default: undefined,
+ description: 'An array that defines a grid.'
+ },
+ image_size: {
+ type: jsPsych.plugins.parameterType.INT,
+ pretty_name: 'Image size',
+ array: true,
+ default: [100,100],
+ description: 'Array specifying the width and height of the images to show.'
+ },
+ trial_duration: {
+ type: jsPsych.plugins.parameterType.INT,
+ pretty_name: 'Trial duration',
+ default: 2000,
+ description: 'How long to show the stimulus for in milliseconds.'
+ }
+ }
+ }
+
+ plugin.trial = function(display_element, trial) {
+
+ display_element.innerHTML = plugin.generate_stimulus(trial.stimuli, trial.image_size);
+
+ jsPsych.pluginAPI.setTimeout(function() {
+ endTrial();
+ }, trial.trial_duration);
+
+ function endTrial() {
+
+ display_element.innerHTML = '';
+
+ var trial_data = {
+ stimulus: trial.stimuli
+ };
+
+ jsPsych.finishTrial(trial_data);
+ }
+ };
+
+ plugin.generate_stimulus = function(pattern, image_size) {
+ var nrows = pattern.length;
+ var ncols = pattern[0].length;
+
+ // create blank element to hold code that we generate
+ var html = ' ';
+
+ // create table
+ html += ' ';
+
+ return html;
+
+ };
+
+ return plugin;
+})();
diff --git a/scripts/plugins/jspsych-webgazer-calibrate.js b/scripts/plugins/jspsych-webgazer-calibrate.js
new file mode 100644
index 0000000..54afe7e
--- /dev/null
+++ b/scripts/plugins/jspsych-webgazer-calibrate.js
@@ -0,0 +1,166 @@
+/**
+ * jspsych-webgazer-calibrate
+ * Josh de Leeuw
+ **/
+
+jsPsych.plugins["webgazer-calibrate"] = (function() {
+
+ var plugin = {};
+
+ plugin.info = {
+ name: 'webgazer-calibrate',
+ description: '',
+ parameters: {
+ calibration_points: {
+ type: jsPsych.plugins.parameterType.INT,
+ default: [[10,10], [10,50], [10,90], [50,10], [50,50], [50,90], [90,10], [90,50], [90,90]]
+ },
+ calibration_mode: {
+ type: jsPsych.plugins.parameterType.STRING,
+ default: 'click', // options: 'click', 'view'
+ },
+ repetitions_per_point: {
+ type: jsPsych.plugins.parameterType.INT,
+ default: 1
+ },
+ randomize_calibration_order: {
+ type: jsPsych.plugins.parameterType.BOOL,
+ default: false
+ },
+ time_to_saccade: {
+ type: jsPsych.plugins.parameterType.INT,
+ default: 1000
+ },
+ time_per_point: {
+ type: jsPsych.plugins.parameterType.STRING,
+ default: 1000
+ }
+ }
+ }
+
+ // provide options for calibration routines?
+ // dot clicks?
+ // track a dot with mouse?
+
+ // then a validation phase of staring at the dot in different locations?
+
+ plugin.trial = function(display_element, trial) {
+
+ var html = `
+
+ `
+
+ display_element.innerHTML = html;
+
+ jsPsych.extensions['webgazer'].resume();
+
+ var wg_container = display_element.querySelector('#webgazer-calibrate-container');
+
+ var reps_completed = 0;
+ var points_completed = -1;
+ var cal_points = null;
+
+ calibrate();
+
+
+ function calibrate(){
+ jsPsych.extensions['webgazer'].resume();
+ if(trial.calibration_mode == 'click'){
+ jsPsych.extensions['webgazer'].startMouseCalibration();
+ }
+ next_calibration_round();
+ }
+
+ function next_calibration_round(){
+ if(trial.randomize_calibration_order){
+ cal_points = jsPsych.randomization.shuffle(trial.calibration_points);
+ } else {
+ cal_points = trial.calibration_points;
+ }
+ points_completed = -1;
+ next_calibration_point();
+ }
+
+ function next_calibration_point(){
+ points_completed++;
+ if(points_completed == cal_points.length){
+ reps_completed++;
+ if(reps_completed == trial.repetitions_per_point){
+ calibration_done();
+ } else {
+ next_calibration_round();
+ }
+ } else {
+ var pt = cal_points[points_completed];
+ calibration_display_gaze_only(pt);
+ }
+ }
+
+ function calibration_display_gaze_only(pt){
+ var pt_html = ''
+ wg_container.innerHTML = pt_html;
+
+ var pt_dom = wg_container.querySelector('#calibration-point');
+
+ if(trial.calibration_mode == 'click'){
+ pt_dom.style.cursor = 'pointer';
+ pt_dom.addEventListener('click', function(){
+ next_calibration_point();
+ })
+ }
+
+ if(trial.calibration_mode == 'view'){
+ var br = pt_dom.getBoundingClientRect();
+ var x = br.left + br.width / 2;
+ var y = br.top + br.height / 2;
+
+ var pt_start_cal = performance.now() + trial.time_to_saccade;
+ var pt_finish = performance.now() + trial.time_to_saccade + trial.time_per_point;
+
+ requestAnimationFrame(function watch_dot(){
+
+ if(performance.now() > pt_start_cal){
+ jsPsych.extensions['webgazer'].calibratePoint(x,y,'click');
+ }
+ if(performance.now() < pt_finish){
+ requestAnimationFrame(watch_dot);
+ } else {
+ next_calibration_point();
+ }
+ })
+ }
+ }
+
+ function calibration_done(){
+ if(trial.calibration_mode == 'click'){
+ jsPsych.extensions['webgazer'].stopMouseCalibration();
+ }
+ wg_container.innerHTML = "";
+ end_trial();
+ }
+
+ // function to end trial when it is time
+ function end_trial() {
+ jsPsych.extensions['webgazer'].pause();
+ jsPsych.extensions['webgazer'].hidePredictions();
+ jsPsych.extensions['webgazer'].hideVideo();
+
+ // kill any remaining setTimeout handlers
+ jsPsych.pluginAPI.clearAllTimeouts();
+
+ // gather the data to store for the trial
+ var trial_data = {
+
+ };
+
+ // clear the display
+ display_element.innerHTML = '';
+
+ // move on to the next trial
+ jsPsych.finishTrial(trial_data);
+ };
+
+ };
+
+ return plugin;
+ })();
\ No newline at end of file
diff --git a/scripts/plugins/jspsych-webgazer-init-camera.js b/scripts/plugins/jspsych-webgazer-init-camera.js
new file mode 100644
index 0000000..952a906
--- /dev/null
+++ b/scripts/plugins/jspsych-webgazer-init-camera.js
@@ -0,0 +1,95 @@
+/**
+ * jspsych-webgazer-init-camera
+ * Josh de Leeuw
+ **/
+
+jsPsych.plugins["webgazer-init-camera"] = (function() {
+
+ var plugin = {};
+
+ plugin.info = {
+ name: 'webgazer-init-camera',
+ description: '',
+ parameters: {
+ instructions: {
+ type: jsPsych.plugins.parameterType.HTML_STRING,
+ default: `
+ Position your head so that the webcam has a good view of your eyes. +Use the video in the upper-left corner as a guide. Center your face in the box and look directly towards the camera. +It is important that you try and keep your head reasonably still throughout the experiment, so please take a moment to adjust your setup as needed. +When your face is centered in the box and the box turns green, you can click to continue. ` + }, + button_text: { + type: jsPsych.plugins.parameterType.STRING, + default: 'Continue' + } + } + } + + plugin.trial = function(display_element, trial) { + + var html = ` +
+ `
+
+ display_element.innerHTML = html;
+
+ jsPsych.extensions['webgazer'].showVideo();
+ jsPsych.extensions['webgazer'].resume();
+
+ var wg_container = display_element.querySelector('#webgazer-init-container');
+
+
+ wg_container.innerHTML = `
+
+ ${trial.instructions}
+ `
+
+ var observer = new MutationObserver(face_detect_event_observer);
+ observer.observe(document, {
+ attributes: true,
+ attributeFilter: ['style'],
+ subtree: true
+ });
+
+ document.querySelector('#jspsych-wg-cont').addEventListener('click', function(){
+ observer.disconnect();
+ end_trial();
+ });
+
+ function face_detect_event_observer(mutationsList, observer){
+ if(mutationsList[0].target == document.querySelector('#webgazerFaceFeedbackBox')){
+ if(mutationsList[0].type == 'attributes' && mutationsList[0].target.style.borderColor == "green"){
+ document.querySelector('#jspsych-wg-cont').disabled = false;
+ }
+ if(mutationsList[0].type == 'attributes' && mutationsList[0].target.style.borderColor == "red"){
+ document.querySelector('#jspsych-wg-cont').disabled = true;
+ }
+ }
+ }
+
+ // function to end trial when it is time
+ function end_trial() {
+ jsPsych.extensions['webgazer'].pause();
+ jsPsych.extensions['webgazer'].hideVideo();
+
+ // kill any remaining setTimeout handlers
+ jsPsych.pluginAPI.clearAllTimeouts();
+
+ // gather the data to store for the trial
+ var trial_data = {
+
+ };
+
+ // clear the display
+ display_element.innerHTML = '';
+
+ // move on to the next trial
+ jsPsych.finishTrial(trial_data);
+ };
+
+ };
+
+ return plugin;
+ })();
\ No newline at end of file
diff --git a/scripts/plugins/jspsych-webgazer-validate.js b/scripts/plugins/jspsych-webgazer-validate.js
new file mode 100644
index 0000000..87f191a
--- /dev/null
+++ b/scripts/plugins/jspsych-webgazer-validate.js
@@ -0,0 +1,304 @@
+/**
+ * jspsych-webgazer-validate
+ * Josh de Leeuw
+ **/
+
+jsPsych.plugins["webgazer-validate"] = (function() {
+
+ var plugin = {};
+
+ plugin.info = {
+ name: 'webgazer-validate',
+ description: '',
+ parameters: {
+ validation_points: {
+ type: jsPsych.plugins.parameterType.INT,
+ default: [[10,10], [10,50], [10,90], [50,10], [50,50], [50,90], [90,10], [90,50], [90,90]]
+ },
+ validation_point_coordinates: {
+ type: jsPsych.plugins.parameterType.STRING,
+ default: 'percent' // options: 'percent', 'center-offset-pixels'
+ },
+ roi_radius: {
+ type: jsPsych.plugins.parameterType.INT,
+ default: 200
+ },
+ randomize_validation_order: {
+ type: jsPsych.plugins.parameterType.BOOL,
+ default: false
+ },
+ time_to_saccade: {
+ type: jsPsych.plugins.parameterType.INT,
+ default: 1000
+ },
+ validation_duration: {
+ type: jsPsych.plugins.parameterType.INT,
+ default: 2000
+ },
+ point_size:{
+ type: jsPsych.plugins.parameterType.INT,
+ default: 10
+ },
+ show_validation_data: {
+ type: jsPsych.plugins.parameterType.BOOL,
+ default: false
+ }
+ }
+ }
+
+ plugin.trial = function(display_element, trial) {
+
+ var trial_data = {}
+ trial_data.raw_gaze = [];
+ trial_data.percent_in_roi = [];
+ trial_data.average_offset = [];
+
+ var html = `
+
+ `
+
+ display_element.innerHTML = html;
+
+ var wg_container = display_element.querySelector('#webgazer-validate-container');
+
+ var points_completed = -1;
+ var val_points = null;
+ var start = performance.now();
+
+ validate();
+
+ function validate(){
+
+ if(trial.randomize_validation_order){
+ val_points = jsPsych.randomization.shuffle(trial.validation_points);
+ } else {
+ val_points = trial.validation_points;
+ }
+ points_completed = -1;
+ jsPsych.extensions['webgazer'].resume();
+ //jsPsych.extensions.webgazer.showPredictions();
+ next_validation_point();
+ }
+
+ function next_validation_point(){
+ points_completed++;
+ if(points_completed == val_points.length){
+ validation_done();
+ } else {
+ var pt = val_points[points_completed];
+ validation_display(pt);
+ }
+ }
+
+ function validation_display(pt){
+ var pt_html = drawValidationPoint(pt[0], pt[1]);
+ wg_container.innerHTML = pt_html;
+
+ var pt_dom = wg_container.querySelector('.validation-point');
+
+ var br = pt_dom.getBoundingClientRect();
+ var x = br.left + br.width / 2;
+ var y = br.top + br.height / 2;
+
+ var pt_start_val = performance.now() + trial.time_to_saccade;
+ var pt_finish = pt_start_val + trial.validation_duration;
+
+ var pt_data = [];
+
+ requestAnimationFrame(function watch_dot(){
+
+ if(performance.now() > pt_start_val){
+ jsPsych.extensions['webgazer'].getCurrentPrediction().then(function(prediction){
+ pt_data.push({dx: prediction.x - x, dy: prediction.y - y, t: Math.round(performance.now()-start)});
+ });
+ }
+ if(performance.now() < pt_finish){
+ requestAnimationFrame(watch_dot);
+ } else {
+ trial_data.raw_gaze.push(pt_data);
+ next_validation_point();
+ }
+ });
+
+ }
+
+ function drawValidationPoint(x,y){
+ if(trial.validation_point_coordinates == 'percent'){
+ return drawValidationPoint_PercentMode(x,y);
+ }
+ if(trial.validation_point_coordinates == 'center-offset-pixels'){
+ return drawValidationPoint_CenterOffsetMode(x,y);
+ }
+ }
+
+ function drawValidationPoint_PercentMode(x,y){
+ return ``
+ }
+
+ function drawValidationPoint_CenterOffsetMode(x,y){
+ return ``
+ }
+
+ function drawCircle(target_x, target_y, dx, dy, r){
+ if(trial.validation_point_coordinates == 'percent'){
+ return drawCircle_PercentMode(target_x, target_y, dx, dy, r);
+ }
+ if(trial.validation_point_coordinates == 'center-offset-pixels'){
+ return drawCircle_CenterOffsetMode(target_x, target_y, dx, dy, r);
+ }
+ }
+
+ function drawCircle_PercentMode(target_x, target_y, dx, dy, r){
+ var html = `
+
+ `
+ return html;
+ }
+
+ function drawCircle_CenterOffsetMode(target_x, target_y, dx, dy, r){
+ var html = `
+
+ `
+ return html;
+ }
+
+ function drawRawDataPoint(target_x, target_y, dx, dy, ){
+ if(trial.validation_point_coordinates == 'percent'){
+ return drawRawDataPoint_PercentMode(target_x, target_y, dx, dy);
+ }
+ if(trial.validation_point_coordinates == 'center-offset-pixels'){
+ return drawRawDataPoint_CenterOffsetMode(target_x, target_y, dx, dy);
+ }
+ }
+
+ function drawRawDataPoint_PercentMode(target_x, target_y, dx, dy){
+ var color = Math.sqrt(dx*dx + dy*dy) <= trial.roi_radius ? '#afa' : '#faa';
+ return ``
+ }
+
+ function drawRawDataPoint_CenterOffsetMode(target_x, target_y, dx, dy){
+ var color = Math.sqrt(dx*dx + dy*dy) <= trial.roi_radius ? '#afa' : '#faa';
+ return ``
+ }
+
+ function median(arr){
+ var mid = Math.floor(arr.length/2);
+ var sorted_arr = arr.sort((a,b) => a-b);
+ if(arr.length % 2 == 0){
+ return sorted_arr[mid-1] + sorted_arr[mid] / 2;
+ } else {
+ return sorted_arr[mid];
+ }
+ }
+
+ function calculateGazeCentroid(gazeData){
+
+ var x_diff_m = gazeData.reduce(function(accumulator, currentValue, index){
+ accumulator += currentValue.dx;
+ if(index == gazeData.length-1){
+ return accumulator / gazeData.length;
+ } else {
+ return accumulator;
+ }
+ }, 0);
+
+ var y_diff_m = gazeData.reduce(function(accumulator, currentValue, index){
+ accumulator += currentValue.dy;
+ if(index == gazeData.length-1){
+ return accumulator / gazeData.length;
+ } else {
+ return accumulator;
+ }
+ }, 0);
+
+ var median_distance = median(gazeData.map(function(x){ return(Math.sqrt(Math.pow(x.dx-x_diff_m,2) + Math.pow(x.dy-y_diff_m,2)))}));
+
+ return {
+ x: x_diff_m,
+ y: y_diff_m,
+ r: median_distance
+ }
+ }
+
+ function calculatePercentInROI(gazeData){
+ var distances = gazeData.map(function(p){
+ return(Math.sqrt(Math.pow(p.dx,2) + Math.pow(p.dy,2)))
+ });
+ var sum_in_roi = distances.reduce(function(accumulator, currentValue){
+ if(currentValue <= trial.roi_radius){
+ accumulator++;
+ }
+ return accumulator;
+ }, 0);
+ var percent = sum_in_roi / gazeData.length * 100;
+ return percent;
+ }
+
+ function calculateSampleRate(gazeData){
+ var mean_diff = [];
+ for(var i=0; i', {
- "class": 'jspsych-xab-stimulus',
- 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
- $('.jspsych-xab-stimulus').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($('', {
- "src": images[0],
- "class": 'jspsych-xab-stimulus left'
- }));
- display_element.append($('', {
- "src": images[1],
- "class": 'jspsych-xab-stimulus right'
- }));
- }
- else {
- display_element.append($(' ', {
- "class": 'jspsych-xab-stimulus left',
- html: images[0]
- }));
- display_element.append($(' ', {
- "class": 'jspsych-xab-stimulus right',
- html: images[1]
- }));
- }
-
- if (trial.prompt !== "") {
- display_element.append(trial.prompt);
- }
-
- // 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) {
- $('.jspsych-xab-stimulus').css('visibility', 'hidden');
- }
- }, trial.timing_ab);
- }
-
- // create the function that triggers when a key is pressed.
- var after_response = function(info) {
-
- var correct = false; // true when the correct response is chosen
-
- if (info.key == trial.left_key) // 'q' key by default
- {
- if (target_left) {
- correct = true;
- }
- }
- else if (info.key == trial.right_key) // 'p' key by default
- {
- if (!target_left) {
- correct = true;
- }
- }
-
-
- // create object to store data from trial
- var trial_data = {
- "trial_type": "xab",
- "trial_index": block.trial_idx,
- "rt": info.rt,
- "correct": correct,
- "stimulus_x": trial.x_path,
- "stimulus_a": trial.a_path,
- "stimulus_b": trial.b_path,
- "key_press": info.key
- };
- block.writeData($.extend({}, trial_data, trial.data));
-
- display_element.html(''); // remove all
-
- xab_trial_complete = true;
-
- // move on to the next trial after timing_post_trial milliseconds
- if(trial.timing_post_trial > 0) {
- setTimeout(function() {
- block.next();
- }, trial.timing_post_trial);
- } else {
- block.next();
- }
-
- };
-
- jsPsych.pluginAPI.getKeyboardResponse(after_response, [trial.left_key, trial.right_key], 'date', false);
-
- break;
- }
- };
-
- return plugin;
- })();
-})(jQuery);
diff --git a/scripts/plugins/template/jspsych-plugin-template.js b/scripts/plugins/template/jspsych-plugin-template.js
index 22e3430..db0956f 100644
--- a/scripts/plugins/template/jspsych-plugin-template.js
+++ b/scripts/plugins/template/jspsych-plugin-template.js
@@ -1,65 +1,35 @@
-/**
- * Josh de Leeuw
- * November 2013
- *
- * This is a basic template for a jsPsych plugin. Use it to start creating your
- * own plugin. There is more information about how to create a plugin on the
- * jsPsych wiki (https://github.com/jodeleeuw/jsPsych/wiki/Create-a-Plugin).
- *
- *
+/*
+ * Example plugin template
*/
-
-(function( $ ) {
- jsPsych["PLUGIN-NAME"] = (function(){
- var plugin = {};
+jsPsych.plugins["PLUGIN-NAME"] = (function() {
- plugin.create = function(params) {
- var trials = new Array(NUMBER_OF_TRIALS);
- for(var i = 0; i < NUMBER_OF_TRIALS; i++)
- {
- trials[i] = {};
- trials[i].type = "PLUGIN-NAME";
- // other information needed for the trial method can be added here
-
- // supporting the generic data object with the following line
- // is always a good idea. it allows people to pass in the data
- // parameter, but if they don't it gracefully adds an empty object
- // in it's place.
- trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
- }
- return trials;
- };
+ var plugin = {};
- plugin.trial = function(display_element, block, trial, part) {
- // code for running the trial goes here
-
- // allow variables as functions
- // this allows any trial variable to be specified as a function
- // that will be evaluated when the trial runs. this allows users
- // to dynamically adjust the contents of a trial as a result
- // of other trials, among other uses. you can leave this out,
- // but in general it should be included
- trial = jsPsych.normalizeTrialVariables(trial);
-
- // data saving
- // this is technically optional, but virtually every plugin will
- // need to do it. it is good practice to include the type and
- // trial_index fields for all plugins.
- var trial_data = {
- type: trial.type,
- trial_index: block.trial_idx,
- // other values to save go here
- };
-
- // this line merges together the trial_data object and the generic
- // data object (trial.data), and then stores them.
- block.writeData($.extend({}, trial_data, trial.data));
-
- // this method must be called at the end of the trial
- block.next();
- };
+ plugin.info = {
+ name: "PLUGIN-NAME",
+ parameters: {
+ parameter_name: {
+ type: jsPsych.plugins.parameterType.INT, // BOOL, STRING, INT, FLOAT, FUNCTION, KEY, SELECT, HTML_STRING, IMAGE, AUDIO, VIDEO, OBJECT, COMPLEX
+ default: undefined
+ },
+ parameter_name: {
+ type: jsPsych.plugins.parameterType.IMAGE,
+ default: undefined
+ }
+ }
+ }
- return plugin;
- })();
-}) (jQuery);
\ No newline at end of file
+ plugin.trial = function(display_element, trial) {
+
+ // data saving
+ var trial_data = {
+ parameter_name: 'parameter value'
+ };
+
+ // end trial
+ jsPsych.finishTrial(trial_data);
+ };
+
+ return plugin;
+})();
|