├── README.md ├── css └── jspsych.css ├── img ├── congruent_left.gif ├── congruent_right.gif ├── incongruent_left.gif └── incongruent_right.gif ├── index.html └── scripts ├── jspsych.js └── plugins ├── jspsych-animation.js ├── jspsych-call-function.js ├── jspsych-categorize-animation.js ├── jspsych-categorize.js ├── jspsych-free-sort.js ├── jspsych-html.js ├── jspsych-palmer.js ├── jspsych-same-different.js ├── jspsych-similarity.js ├── jspsych-single-stim.js ├── jspsych-survey-likert.js ├── jspsych-survey-text.js ├── jspsych-text.js ├── jspsych-visual-search-circle.js ├── jspsych-vsl-animate-occlusion.js ├── jspsych-vsl-grid-scene.js ├── jspsych-xab.js └── template └── jspsych-plugin-template.js /README.md: -------------------------------------------------------------------------------- 1 | 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. 2 | 3 | **This project currently uses jsPsych v3.1** 4 | 5 | Instructions 6 | ------------ 7 | 8 | 1. You will need a GitHub account in order to use the GitHub hosting feature. [Register for free](http://www.github.com). 9 | 10 | 2. After you are logged into your account, click the "fork" button near the top right corner of the page. This will automaticaly copy the repository into your GitHub account. Once the copy is complete, you will be sent directly to your copy of this repository. 11 | 12 | 3. At this point, you must make a small change to the repository to trigger a fresh build of the website. Any change will work. For example, you could change the information in the `` section of index.html to give your experiment a new title. 13 | 14 | 4. After you have made a change, you can try the test experiment by going to http://[your GitHub username].github.io/jsPsych-quickstart/. It can take a few minutes for GitHub to create this page the first time, so you may need to wait up to 10 minutes before this works. 15 | 16 | That's all! Once the website is working, you can edit the files in the repository and the changes will show up immediately at the above URL. 17 | 18 | #### A few more advanced notes 19 | 20 | 1. The repository has two branches, master and gh-pages. Both have the same content, but only changes made to the gh-pages branch will affect the website. The gh-pages branch is marked as the default branch, so you don't need to change any settings if you are editing the page using the GitHub interface. 21 | 22 | 2. Renaming the repository will change the URL. 23 | 24 | Need help? 25 | ---------- 26 | 27 | For questions about using the library, please post to the [jsPsych e-mail list](https://groups.google.com/forum/#!forum/jspsych). This creates a publically available archive of questions and solutions. 28 | -------------------------------------------------------------------------------- /css/jspsych.css: -------------------------------------------------------------------------------- 1 | /* 2 | * CSS for jsPsych experiments. 3 | * 4 | * This stylesheet provides minimal styling to make jsPsych 5 | * experiments look polished without any additional styles. 6 | * 7 | */ 8 | 9 | /* 10 | * 11 | * fonts and type 12 | * 13 | */ 14 | 15 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700); 16 | 17 | html { 18 | font-family: 'Open Sans', 'Arial', sans-serif; 19 | font-size: 18px; 20 | line-height: 1.6em; 21 | } 22 | 23 | p { 24 | clear:both; 25 | } 26 | 27 | .very-small { 28 | font-size: 50%; 29 | } 30 | 31 | .small { 32 | font-size: 75%; 33 | } 34 | 35 | .large { 36 | font-size: 125%; 37 | } 38 | 39 | .very-large { 40 | font-size: 150%; 41 | } 42 | 43 | /* 44 | * 45 | * Classes for changing location of things 46 | * 47 | */ 48 | 49 | .left { 50 | float: left; 51 | } 52 | 53 | .right { 54 | float: right; 55 | } 56 | 57 | .center-content { 58 | text-align: center; 59 | } 60 | 61 | /* 62 | * 63 | * Form elements like input fields and buttons 64 | * 65 | */ 66 | 67 | input[type="text"] { 68 | font-family: 'Open Sans', 'Arial', sans-sefif; 69 | font-size: 14px; 70 | } 71 | 72 | button { 73 | padding: 0.5em; 74 | background-color: #eaeaea; 75 | border: 1px solid #eaeaea; 76 | color: #333; 77 | font-family: 'Open Sans', 'Arial', sans-serif; 78 | font-size: 14px; 79 | cursor: pointer; 80 | } 81 | 82 | button:hover { 83 | border:1px solid #ccc; 84 | } 85 | 86 | /* 87 | * 88 | * Container holding jsPsych content 89 | * 90 | */ 91 | 92 | 93 | .jspsych-display-element { 94 | width: 800px; 95 | margin: 50px auto 50px auto; 96 | } 97 | 98 | /* 99 | * 100 | * PLUGIN: jspsych-single-stim 101 | * 102 | */ 103 | 104 | #jspsych-single-stim-stimulus { 105 | display: block; 106 | margin-left: auto; 107 | margin-right: auto; 108 | } 109 | 110 | /* 111 | * 112 | * PLUGIN: jspsych-survey-text 113 | * 114 | */ 115 | 116 | .jspsych-survey-text { 117 | margin: 0.25em 0em; 118 | } 119 | 120 | .jspsych-survey-text-question { 121 | margin: 2em 0em; 122 | } -------------------------------------------------------------------------------- /img/congruent_left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspsych/jsPsych-quickstart/7809ec26a02b9bcb8c2afa682f385e47e6a4ef61/img/congruent_left.gif -------------------------------------------------------------------------------- /img/congruent_right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspsych/jsPsych-quickstart/7809ec26a02b9bcb8c2afa682f385e47e6a4ef61/img/congruent_right.gif -------------------------------------------------------------------------------- /img/incongruent_left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspsych/jsPsych-quickstart/7809ec26a02b9bcb8c2afa682f385e47e6a4ef61/img/incongruent_left.gif -------------------------------------------------------------------------------- /img/incongruent_right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspsych/jsPsych-quickstart/7809ec26a02b9bcb8c2afa682f385e47e6a4ef61/img/incongruent_right.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My experiment 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 79 | -------------------------------------------------------------------------------- /scripts/jspsych.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jspsych.js 3 | * Josh de Leeuw 4 | * 5 | * documentation: https://github.com/jodeleeuw/jsPsych/wiki 6 | * 7 | **/ 8 | (function($) { 9 | jsPsych = (function() { 10 | 11 | // 12 | // public object 13 | // 14 | var core = {}; 15 | 16 | // 17 | // private class variables 18 | // 19 | 20 | // options 21 | var opts = {}; 22 | // exp structure 23 | var exp_blocks = []; 24 | // flow control 25 | var curr_block = 0; 26 | // everything loaded? 27 | var initialized = false; 28 | // target DOM element 29 | var DOM_target; 30 | // time that the experiment began 31 | var exp_start_time; 32 | 33 | // 34 | // public methods 35 | // 36 | 37 | // core.init creates the experiment and starts running it 38 | // display_element is an HTML element (usually a
) that will display jsPsych content 39 | // options is an object: { 40 | // "experiment_structure": an array of blocks specifying the experiment 41 | // "finish": function to execute when the experiment ends 42 | // } 43 | // 44 | core.init = function(options) { 45 | 46 | // reset the key variables 47 | exp_blocks = []; 48 | opts = {}; 49 | initialized = false; 50 | curr_block = 0; 51 | 52 | // check if there is a body element on the page 53 | var default_display_element = $('body'); 54 | if (default_display_element.length === 0) { 55 | $(document.documentElement).append($('')); 56 | default_display_element = $('body'); 57 | } 58 | 59 | var defaults = { 60 | 'display_element': default_display_element, 61 | 'on_finish': function(data) { 62 | return undefined; 63 | }, 64 | 'on_trial_start': function() { 65 | return undefined; 66 | }, 67 | 'on_trial_finish': function() { 68 | return undefined; 69 | }, 70 | 'on_data_update': function(data) { 71 | return undefined; 72 | } 73 | }; 74 | 75 | // import options 76 | opts = $.extend({}, defaults, options); 77 | 78 | // set target 79 | DOM_target = opts.display_element; 80 | 81 | // add CSS class to DOM_target 82 | DOM_target.addClass('jspsych-display-element'); 83 | 84 | run(); 85 | }; 86 | 87 | // core.data returns all of the data objects for each block as an array 88 | // where core.data[0] = data object from block 0, etc... 89 | // if flatten is true, then the hierarchical structure of the data 90 | // is removed and each array entry will be a single trial. 91 | 92 | core.data = function() { 93 | var all_data = []; 94 | 95 | for (var i = 0; i < exp_blocks.length; i++) { 96 | all_data[i] = exp_blocks[i].data; 97 | } 98 | 99 | return all_data; 100 | }; 101 | 102 | 103 | 104 | // core.progress returns an object with the following properties 105 | // total_blocks: the number of total blocks in the experiment 106 | // total_trials: the number of total trials in the experiment 107 | // current_trial_global: the current trial number in global terms 108 | // i.e. if each block has 20 trials and the experiment is 109 | // currently in block 2 trial 10, this has a value of 30. 110 | // current_trial_local: the current trial number within the block. 111 | // current_block: the current block number. 112 | 113 | core.progress = function() { 114 | 115 | var total_trials = 0; 116 | for (var i = 0; i < exp_blocks.length; i++) { 117 | total_trials += exp_blocks[i].num_trials; 118 | } 119 | 120 | var current_trial_global = 0; 121 | for (var i = 0; i < curr_block; i++) { 122 | current_trial_global += exp_blocks[i].num_trials; 123 | } 124 | current_trial_global += exp_blocks[curr_block].trial_idx; 125 | 126 | var obj = { 127 | "total_blocks": exp_blocks.length, 128 | "total_trials": total_trials, 129 | "current_trial_global": current_trial_global, 130 | "current_trial_local": exp_blocks[curr_block].trial_idx, 131 | "current_block": curr_block 132 | }; 133 | 134 | return obj; 135 | }; 136 | 137 | // core.startTime() returns the Date object which represents the time that the experiment started. 138 | 139 | core.startTime = function() { 140 | return exp_start_time; 141 | }; 142 | 143 | // core.totalTime() returns the length of time in ms since the experiment began 144 | 145 | core.totalTime = function() { 146 | return (new Date()).getTime() - exp_start_time.getTime(); 147 | }; 148 | 149 | // core.preloadImage will load images into the browser cache so that they appear quickly when 150 | // used during a trial. 151 | // images: array of paths to images 152 | // callback_complete: a function with no arguments that calls when loading is complete 153 | // callback_load: a function with a single argument that calls whenever an image is loaded 154 | // argument is the number of images currently loaded. 155 | 156 | core.preloadImages = function(images, callback_complete, callback_load) { 157 | 158 | // flatten the images array 159 | images = flatten(images); 160 | 161 | var n_loaded = 0; 162 | var loadfn = (typeof callback_load === 'undefined') ? function() {} : callback_load; 163 | var finishfn = (typeof callback_complete === 'undefined') ? function() {} : callback_complete; 164 | 165 | for (var i = 0; i < images.length; i++) { 166 | var img = new Image(); 167 | 168 | img.onload = function() { 169 | n_loaded++; 170 | loadfn(n_loaded); 171 | if (n_loaded == images.length) { 172 | finishfn(); 173 | } 174 | }; 175 | 176 | img.src = images[i]; 177 | } 178 | }; 179 | 180 | core.getDisplayElement = function() { 181 | return DOM_target; 182 | } 183 | 184 | // 185 | // private functions // 186 | // 187 | function run() { 188 | // take the experiment structure and dynamically create a set of blocks 189 | exp_blocks = new Array(opts.experiment_structure.length); 190 | 191 | // iterate through block list to create trials 192 | for (var i = 0; i < exp_blocks.length; i++) { 193 | 194 | // check to make sure plugin is loaded 195 | var plugin_name = opts.experiment_structure[i].type; 196 | if (typeof jsPsych[plugin_name] == 'undefined') { 197 | throw new Error("Failed attempt to create trials using plugin type " + plugin_name + ". Is the plugin loaded?"); 198 | } 199 | 200 | var trials = jsPsych[plugin_name]["create"].call(null, opts["experiment_structure"][i]); 201 | 202 | exp_blocks[i] = createBlock(trials); 203 | } 204 | 205 | // record the start time 206 | exp_start_time = new Date(); 207 | 208 | // begin! - run the first block 209 | exp_blocks[0].next(); 210 | } 211 | 212 | function nextBlock() { 213 | curr_block += 1; 214 | if (curr_block == exp_blocks.length) { 215 | finishExperiment(); 216 | } 217 | else { 218 | exp_blocks[curr_block].next(); 219 | } 220 | } 221 | 222 | function createBlock(trial_list) { 223 | var block = { 224 | trial_idx: -1, 225 | 226 | trials: trial_list, 227 | 228 | data: [], 229 | 230 | next: function() { 231 | 232 | // call on_trial_finish() 233 | // if not very first trial 234 | // and not the last call in this block (no trial due to advance in block) 235 | if (typeof this.trials[this.trial_idx + 1] != "undefined" && (curr_block != 0 || this.trial_idx > -1)) { 236 | opts.on_trial_finish(); 237 | }; 238 | 239 | this.trial_idx = this.trial_idx + 1; 240 | 241 | var curr_trial = this.trials[this.trial_idx]; 242 | 243 | if (typeof curr_trial == "undefined") { 244 | return this.done(); 245 | } 246 | 247 | // call on_trial_start() 248 | opts.on_trial_start(); 249 | 250 | do_trial(this, curr_trial); 251 | }, 252 | 253 | writeData: function(data_object) { 254 | this.data[this.trial_idx] = data_object; 255 | opts.on_data_update(data_object); 256 | }, 257 | 258 | 259 | done: nextBlock, 260 | 261 | num_trials: trial_list.length 262 | }; 263 | 264 | return block; 265 | } 266 | 267 | function finishExperiment() { 268 | opts["on_finish"].apply((new Object()), [core.data()]); 269 | } 270 | 271 | function do_trial(block, trial) { 272 | // execute trial method 273 | jsPsych[trial.type]["trial"].call(this, DOM_target, block, trial, 1); 274 | } 275 | 276 | return core; 277 | })(); 278 | 279 | jsPsych.dataAPI = (function() { 280 | 281 | var module = {}; 282 | 283 | // core.dataAsCSV returns a CSV string that contains all of the data 284 | // append_data is an option map object that will append values 285 | // to every row. for example, if append_data = {"subject": 4}, 286 | // then a column called subject will be added to the data and 287 | // it will always have the value 4. 288 | module.dataAsCSV = function(append_data) { 289 | var dataObj = jsPsych.data(); 290 | return JSON2CSV(flattenData(dataObj, append_data)); 291 | }; 292 | 293 | module.localSave = function(filename, format, append_data) { 294 | 295 | var data_string; 296 | 297 | if(format == 'JSON' || format == 'json') { 298 | data_string = JSON.stringify(flattenData(jsPsych.data(), append_data)); 299 | } else if(format == 'CSV' || format == 'csv') { 300 | data_string = module.dataAsCSV(append_data); 301 | } else { 302 | throw new Error('invalid format specified for jsPsych.dataAPI.localSave'); 303 | } 304 | 305 | saveTextToFile(data_string, filename); 306 | }; 307 | 308 | module.getTrialsOfType = function(trial_type){ 309 | var data = jsPsych.data(); 310 | 311 | data = flatten(data); 312 | 313 | var trials = []; 314 | for(var i = 0; i < data.length; i++){ 315 | if(data[i].trial_type == trial_type){ 316 | trials.push(data[i]); 317 | } 318 | } 319 | 320 | return trials; 321 | }; 322 | 323 | module.displayData = function(format) { 324 | format = (typeof format === 'undefined') ? "json" : format.toLowerCase(); 325 | if(format != "json" && format != "csv") { 326 | console.log('Invalid format declared for displayData function. Using json as default.'); 327 | format = "json"; 328 | } 329 | 330 | var data_string; 331 | 332 | if(format == 'json') { 333 | data_string = JSON.stringify(flattenData(jsPsych.data()), undefined, 1); 334 | } else { 335 | data_string = module.dataAsCSV(); 336 | } 337 | 338 | var display_element = jsPsych.getDisplayElement(); 339 | 340 | display_element.append($('
', {
341 |                 html: data_string
342 |             }));
343 |         }
344 |         
345 |         // private function to save text file on local drive
346 |         function saveTextToFile(textstr, filename) {
347 |             var blobToSave = new Blob([textstr], {
348 |                 type: 'text/plain'
349 |             });
350 |             var blobURL = "";
351 |             if (typeof window.webkitURL !== 'undefined') {
352 |                 blobURL = window.webkitURL.createObjectURL(blobToSave);
353 |             }
354 |             else {
355 |                 blobURL = window.URL.createObjectURL(blobToSave);
356 |             }
357 |             
358 |             var display_element = jsPsych.getDisplayElement();
359 |             
360 |             display_element.append($('', {
361 |                 id: 'jspsych-download-as-text-link',
362 |                 href: blobURL,
363 |                 css: {
364 |                     display: 'none'
365 |                 },
366 |                 download: filename,
367 |                 html: 'download file'
368 |             }));
369 |             $('#jspsych-download-as-text-link')[0].click();
370 |         }
371 |         
372 |         //
373 |         // A few helper functions to handle data format conversion
374 |         //
375 |         function flattenData(data_object, append_data) {
376 | 
377 |             append_data = (typeof append_data === undefined) ? {} : append_data;
378 | 
379 |             var trials = [];
380 | 
381 |             // loop through data_object
382 |             for (var i = 0; i < data_object.length; i++) {
383 |                 for (var j = 0; j < data_object[i].length; j++) {
384 |                     var data = $.extend({}, data_object[i][j], append_data);
385 |                     trials.push(data);
386 |                 }
387 |             }
388 | 
389 |             return trials;
390 |         }
391 |         
392 |         // this function based on code suggested by StackOverflow users:
393 |         // http://stackoverflow.com/users/64741/zachary
394 |         // http://stackoverflow.com/users/317/joseph-sturtevant
395 |         function JSON2CSV(objArray) {
396 |             var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
397 |             var line = '';
398 |             var result = '';
399 |             var columns = [];
400 | 
401 |             var i = 0;
402 |             for (var j = 0; j < array.length; j++) {
403 |                 for (var key in array[j]) {
404 |                     var keyString = key + "";
405 |                     keyString = '"' + keyString.replace(/"/g, '""') + '",';
406 |                     if ($.inArray(key, columns) == -1) {
407 |                         columns[i] = key;
408 |                         line += keyString;
409 |                         i++;
410 |                     }
411 |                 }
412 |             }
413 | 
414 |             line = line.slice(0, - 1);
415 |             result += line + '\r\n';
416 | 
417 |             for (var i = 0; i < array.length; i++) {
418 |                 var line = '';
419 |                 for (var j = 0; j < columns.length; j++) {
420 |                     var value = (typeof array[i][columns[j]] === 'undefined') ? '' : array[i][columns[j]];
421 |                     var valueString = value + "";
422 |                     line += '"' + valueString.replace(/"/g, '""') + '",';
423 |                 }
424 | 
425 |                 line = line.slice(0, - 1);
426 |                 result += line + '\r\n';
427 |             }
428 | 
429 |             return result;
430 |         }
431 |         
432 |         return module;
433 |         
434 |     })();
435 |     
436 |     jsPsych.turk = (function() {
437 |         
438 |          // turk info
439 |         var turk_info;
440 |         
441 |         var module = {};
442 |         
443 |         // core.turkInfo gets information relevant to mechanical turk experiments. returns an object
444 |         // containing the workerID, assignmentID, and hitID, and whether or not the HIT is in
445 |         // preview mode, meaning that they haven't accepted the HIT yet.
446 |         module.turkInfo = function(force_refresh) {
447 |             // default value is false
448 |             force_refresh = (typeof force_refresh === 'undefined') ? false : force_refresh;
449 |             // if we already have the turk_info and force_refresh is false
450 |             // then just return the cached version.
451 |             if (typeof turk_info !== 'undefined' && !force_refresh) {
452 |                 return turk_info;
453 |             } else {
454 | 
455 |                 var turk = {};
456 | 
457 |                 var param = function(url, name) {
458 |                     name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
459 |                     var regexS = "[\\?&]" + name + "=([^&#]*)";
460 |                     var regex = new RegExp(regexS);
461 |                     var results = regex.exec(url);
462 |                     return (results == null) ? "" : results[1];
463 |                 };
464 | 
465 |                 var src = param(window.location.href, "assignmentId") ? window.location.href : document.referrer;
466 | 
467 |                 var keys = ["assignmentId", "hitId", "workerId", "turkSubmitTo"];
468 |                 keys.map(
469 | 
470 |                 function(key) {
471 |                     turk[key] = unescape(param(src, key));
472 |                 });
473 | 
474 |                 turk.previewMode = (turk.assignmentId == "ASSIGNMENT_ID_NOT_AVAILABLE");
475 | 
476 |                 turk.outsideTurk = (!turk.previewMode && turk.hitId === "" && turk.assignmentId == "" && turk.workerId == "")
477 | 
478 |                 turk_info = turk;
479 | 
480 |                 return turk;
481 |             }
482 | 
483 |         };
484 | 
485 |         // core.submitToTurk will submit a MechanicalTurk ExternalHIT type
486 | 
487 |         module.submitToTurk = function(data) {
488 | 
489 |             var turkInfo = core.turkInfo();
490 |             var assignmentId = turkInfo.assignmentId;
491 |             var turkSubmitTo = turkInfo.turkSubmitTo;
492 | 
493 |             if (!assignmentId || !turkSubmitTo) return;
494 | 
495 |             var dataString = [];
496 | 
497 |             for (var key in data) {
498 | 
499 |                 if (data.hasOwnProperty(key)) {
500 |                     dataString.push(key + "=" + escape(data[key]));
501 |                 }
502 |             }
503 | 
504 |             dataString.push("assignmentId=" + assignmentId);
505 | 
506 |             var url = turkSubmitTo + "/mturk/externalSubmit?" + dataString.join("&");
507 | 
508 |             window.location.href = url;
509 |         }
510 |         
511 |         return module;
512 |         
513 |     })();
514 |     
515 |     jsPsych.randomization = (function() {
516 |         
517 |         var module = {};
518 |         
519 |         module.repeat = function(array, repetitions, unpack) {
520 |            
521 |             var arr_isArray = Array.isArray(array);
522 |             var rep_isArray = Array.isArray(repetitions);
523 |             
524 |             // if array is not an array, then we just repeat the item
525 |             if(!arr_isArray){
526 |                 if(!rep_isArray) {
527 |                     array = [array];
528 |                     repetitions = [repetitions];
529 |                 } else {
530 |                     repetitions = [repetitions[0]];
531 |                     console.log('Unclear parameters given to randomizeSimpleSample. Multiple set sizes specified, but only one item exists to sample. Proceeding using the first set size.');
532 |                 }
533 |             } else {
534 |                 if(!rep_isArray) {
535 |                     var reps = [];
536 |                     for(var i = 0; i < array.length; i++){
537 |                         reps.push(repetitions);            
538 |                     }
539 |                     repetitions = reps;
540 |                 } else {
541 |                     if(array.length != repetitions.length) {
542 |                         // throw warning if repetitions is too short,
543 |                         // throw warning if too long, and then use the first N
544 |                     }
545 |                 }
546 |             }
547 |             
548 |             // should be clear at this point to assume that array and repetitions are arrays with == length
549 |             var allsamples = [];
550 |             for(var i = 0; i < array.length; i++){
551 |                 for(var j = 0; j < repetitions[i]; j++){
552 |                     allsamples.push(array[i]);
553 |                 }
554 |             }
555 |             
556 |             var out = shuffle(allsamples);
557 |             
558 |             if(unpack) { out = unpackArray(out);  }
559 |             
560 |             return shuffle(out);   
561 |         }
562 |         
563 |         module.factorial = function(factors, repetitions, unpack){
564 |     
565 |             var factorNames = Object.keys(factors);
566 |             
567 |             var factor_combinations = [];
568 |             
569 |             for(var i = 0; i < factors[factorNames[0]].length; i++){
570 |                 factor_combinations.push({});
571 |                 factor_combinations[i][factorNames[0]] = factors[factorNames[0]][i];
572 |             }
573 |             
574 |             for(var i = 1; i< factorNames.length; i++){
575 |                 var toAdd = factors[factorNames[i]];
576 |                 var n = factor_combinations.length;
577 |                 for(var j = 0; j < n; j++){
578 |                     var base = factor_combinations[j];
579 |                     for(var k = 0; k < toAdd.length; k++){
580 |                         var newpiece = {};
581 |                         newpiece[factorNames[i]] = toAdd[k];
582 |                         factor_combinations.push($.extend({}, base, newpiece));
583 |                     }
584 |                 }
585 |                 factor_combinations.splice(0,n);
586 |             }
587 |             
588 |             repetitions = (typeof repetitions === 'undefined') ? 1 : repetitions;
589 |             var with_repetitions = module.repeat(factor_combinations, repetitions, unpack);
590 |             
591 |             return with_repetitions;
592 |         }
593 |         
594 |         function unpackArray(array) {
595 |             
596 |             var out = {};
597 |         
598 |             for(var i = 0; i < array.length; i++){
599 |                 var keys = Object.keys(array[i]);
600 |                 for(var k = 0; k < keys.length; k++){
601 |                     if(typeof out[keys[k]] === 'undefined') {
602 |                         out[keys[k]] = [];
603 |                     }
604 |                     out[keys[k]].push(array[i][keys[k]]);
605 |                 }
606 |             }
607 |             
608 |             return out;
609 |         }
610 |         
611 |         function shuffle(array) {
612 |           var m = array.length, t, i;
613 |         
614 |           // While there remain elements to shuffle…
615 |           while (m) {
616 |         
617 |             // Pick a remaining element…
618 |             i = Math.floor(Math.random() * m--);
619 |         
620 |             // And swap it with the current element.
621 |             t = array[m];
622 |             array[m] = array[i];
623 |             array[i] = t;
624 |           }
625 |         
626 |           return array;
627 |         }
628 |         
629 |         return module;
630 |         
631 |     })();
632 |     
633 |     jsPsych.pluginAPI = (function() {
634 |         
635 |         // keyboard listeners
636 |         var keyboard_listeners = [];
637 |         
638 |         var module = {};
639 |         
640 |         module.getKeyboardResponse = function(callback_function, valid_responses, rt_method, persist) {
641 | 
642 |             rt_method = (typeof rt_method === 'undefined') ? 'date' : rt_method;
643 |             if (rt_method != 'date' && rt_method != 'performance') {
644 |                 console.log('Invalid RT method specified in getKeyboardResponse. Defaulting to "date" method.');
645 |                 rt_method = 'date';
646 |             }
647 | 
648 |             var start_time;
649 |             if (rt_method == 'date') {
650 |                 start_time = (new Date()).getTime();
651 |             }
652 |             if (rt_method == 'performance') {
653 |                 start_time = performance.now();
654 |             }
655 | 
656 |             var listener_id;
657 |             
658 |             var listener_function = function(e) {
659 | 
660 |                 var key_time;
661 |                 if (rt_method == 'date') {
662 |                     key_time = (new Date()).getTime();
663 |                 }
664 |                 if (rt_method == 'performance') {
665 |                     key_time = performance.now();
666 |                 }
667 | 
668 |                 var valid_response = false;
669 |                 if (typeof valid_responses === 'undefined' || valid_responses.length === 0) {
670 |                     valid_response = true;
671 |                 }
672 |                 for (var i = 0; i < valid_responses.length; i++) {
673 |                     if (typeof valid_responses[i] == 'string') {
674 |                         if(typeof keylookup[valid_responses[i]] !== 'undefined'){
675 |                             if(e.which == keylookup[valid_responses[i]]) {
676 |                                 valid_response = true;
677 |                             }
678 |                         } else {
679 |                             throw new Error('Invalid key string specified for getKeyboardResponse');
680 |                         }
681 |                     } else if (e.which == valid_responses[i]) {
682 |                         valid_response = true;
683 |                     }
684 |                 }
685 | 
686 |                 if (valid_response) {
687 |                     
688 |                     var after_up = function(up) {
689 |                         
690 |                         if(up.which == e.which) {
691 |                             $(document).off('keyup', after_up);
692 |                         
693 |                             if($.inArray(listener_id, keyboard_listeners) > -1) {
694 |                                 
695 |                                 if(!persist){
696 |                                     // remove keyboard listener
697 |                                     module.cancelKeyboardResponse(listener_id);
698 |                                 }
699 |                                 
700 |                                 callback_function({
701 |                                     key: e.which,
702 |                                     rt: key_time - start_time
703 |                                 });
704 |                             }
705 |                         }
706 |                     };
707 |                     
708 |                     $(document).keyup(after_up);
709 |                 }
710 |             };
711 | 
712 |             $(document).keydown(listener_function);
713 |             
714 |             // create listener id object
715 |             listener_id = {type: 'keydown', fn: listener_function};
716 |             
717 |             // add this keyboard listener to the list of listeners
718 |             keyboard_listeners.push(listener_id);
719 |             
720 |             return listener_id;
721 |             
722 |         };
723 |         
724 |         module.cancelKeyboardResponse = function(listener) {
725 |             // remove the listener from the doc
726 |             $(document).off(listener.type, listener.fn);
727 |             
728 |             // remove the listener from the list of listeners
729 |             if($.inArray(listener, keyboard_listeners) > -1) {
730 |                 keyboard_listeners.splice($.inArray(listener, keyboard_listeners), 1);
731 |             }
732 |         };
733 |         
734 |         module.cancelAllKeyboardResponses = function() {
735 |             for(var i = 0; i< keyboard_listeners.length; i++){
736 |                 $(document).off(keyboard_listeners[i].type, keyboard_listeners[i].fn);
737 |             }
738 |             keyboard_listeners = [];
739 |         };
740 |         
741 |         // keycode lookup associative array
742 |         var keylookup = {
743 |             'backspace': 8,
744 |             'tab': 9,
745 |             'enter': 13,
746 |             'shift': 16,
747 |             'ctrl': 17,
748 |             'alt': 18,
749 |             'pause': 19,
750 |             'capslock': 20,
751 |             'esc': 27,
752 |             'space':32,
753 |             'spacebar':32,
754 |             ' ':32,
755 |             'pageup': 33,
756 |             'pagedown': 34,
757 |             'end': 35,
758 |             'home': 36,
759 |             'leftarrow': 37,
760 |             'uparrow': 38,
761 |             'rightarrow': 39,
762 |             'downarrow': 40,
763 |             'insert': 45,
764 |             'delete': 46,
765 |             '0': 48,
766 |             '1': 49,
767 |             '2': 50,
768 |             '3': 51,
769 |             '4': 52,
770 |             '5': 53,
771 |             '6': 54,
772 |             '7': 55,
773 |             '8': 56,
774 |             '9': 57,
775 |             'a': 65,
776 |             'b': 66,
777 |             'c': 67,
778 |             'd': 68,
779 |             'e': 69,
780 |             'f': 70,
781 |             'g': 71,
782 |             'h': 72,
783 |             'i': 73,
784 |             'j': 74,
785 |             'k': 75,
786 |             'l': 76,
787 |             'm': 77,
788 |             'n': 78,
789 |             'o': 79,
790 |             'p': 80,
791 |             'q': 81,
792 |             'r': 82,
793 |             's': 83,
794 |             't': 84,
795 |             'u': 85,
796 |             'v': 86,
797 |             'w': 87,
798 |             'x': 88,
799 |             'y': 89,
800 |             'z': 90,
801 |             'A': 65,
802 |             'B': 66,
803 |             'C': 67,
804 |             'D': 68,
805 |             'E': 69,
806 |             'F': 70,
807 |             'G': 71,
808 |             'H': 72,
809 |             'I': 73,
810 |             'J': 74,
811 |             'K': 75,
812 |             'L': 76,
813 |             'M': 77,
814 |             'N': 78,
815 |             'O': 79,
816 |             'P': 80,
817 |             'Q': 81,
818 |             'R': 82,
819 |             'S': 83,
820 |             'T': 84,
821 |             'U': 85,
822 |             'V': 86,
823 |             'W': 87,
824 |             'X': 88,
825 |             'Y': 89,
826 |             'Z': 90,
827 |             '0numpad': 96,
828 |             '1numpad': 97,
829 |             '2numpad': 98,
830 |             '3numpad': 99,
831 |             '4numpad': 100,
832 |             '5numpad': 101,
833 |             '6numpad': 102,
834 |             '7numpad': 103,
835 |             '8numpad': 104,
836 |             '9numpad': 105,
837 |             'multiply': 106,
838 |             'plus': 107,
839 |             'minus': 109,
840 |             'decimal': 110,
841 |             'divide': 111,
842 |             'F1': 112,
843 |             'F2': 113,
844 |             'F3': 114,
845 |             'F4': 115,
846 |             'F5': 116,
847 |             'F6': 117,
848 |             'F7': 118,
849 |             'F8': 119,
850 |             'F9': 120,
851 |             'F10': 121,
852 |             'F11': 122,
853 |             'F12': 123,
854 |             '=': 187,
855 |             ',': 188,
856 |             '.': 190,
857 |             '/': 191,
858 |             '`': 192,
859 |             '[': 219,
860 |             '\\': 220,
861 |             ']': 221
862 |         };
863 |         
864 |         //
865 |         // These are public functions, intended to be used for developing plugins.
866 |         // They aren't considered part of the normal API for the core library.
867 |         //
868 | 
869 |         module.normalizeTrialVariables = function(trial, protect) {
870 | 
871 |             protect = (typeof protect === 'undefined') ? [] : protect;
872 | 
873 |             var keys = getKeys(trial);
874 | 
875 |             var tmp = {};
876 |             for (var i = 0; i < keys.length; i++) {
877 | 
878 |                 var process = true;
879 |                 for (var j = 0; j < protect.length; j++) {
880 |                     if (protect[j] == keys[i]) {
881 |                         process = false;
882 |                         break;
883 |                     }
884 |                 }
885 | 
886 |                 if (typeof trial[keys[i]] == "function" && process) {
887 |                     tmp[keys[i]] = trial[keys[i]].call();
888 |                 }
889 |                 else {
890 |                     tmp[keys[i]] = trial[keys[i]];
891 |                 }
892 | 
893 |             }
894 | 
895 |             return tmp;
896 | 
897 |         };
898 | 
899 |         // if possible_array is not an array, then return a one-element array
900 |         // containing possible_array
901 |         module.enforceArray = function(params, possible_arrays) {
902 | 
903 |             // function to check if something is an array, fallback
904 |             // to string method if browser doesn't support Array.isArray
905 |             var ckArray = Array.isArray || function(a) {
906 |                     return toString.call(a) == '[object Array]';
907 |                 };
908 | 
909 |             for (var i = 0; i < possible_arrays.length; i++) {
910 |                 if(typeof params[possible_arrays[i]] !== 'undefined'){
911 |                     params[possible_arrays[i]] = ckArray(params[possible_arrays[i]]) ? params[possible_arrays[i]] : [params[possible_arrays[i]]];
912 |                 }
913 |             }
914 | 
915 |             return params;
916 |         };
917 |         
918 |         function getKeys(obj) {
919 |             var r = [];
920 |             for (var k in obj) {
921 |                 if (!obj.hasOwnProperty(k)) continue;
922 |                 r.push(k);
923 |             }
924 |             return r;
925 |         }
926 |         
927 |         return module;
928 |     })();
929 |     
930 |     // methods used in multiple modules
931 |     
932 |     // private function to flatten nested arrays
933 |     function flatten(arr, out) {
934 |         out = (typeof out === 'undefined') ? [] : out;
935 |         for (var i = 0; i < arr.length; i++) {
936 |             if (Array.isArray(arr[i])) {
937 |                 flatten(arr[i], out);
938 |             }
939 |             else {
940 |                 out.push(arr[i]);
941 |             }
942 |         }
943 |         return out;
944 |     }
945 | 
946 | })(jQuery);


--------------------------------------------------------------------------------
/scripts/plugins/jspsych-animation.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * jsPsych plugin for showing animations and recording keyboard responses
  3 |  * Josh de Leeuw
  4 |  * 
  5 |  * documentation: https://github.com/jodeleeuw/jsPsych/wiki/jspsych-animation
  6 |  */
  7 | 
  8 | (function($) {
  9 |     jsPsych.animation = (function() {
 10 | 
 11 |         var plugin = {};
 12 | 
 13 |         plugin.create = function(params) {
 14 |             
 15 |             params = jsPsych.pluginAPI.enforceArray(params, ['choices', 'data']);
 16 | 
 17 |             var trials = new Array(params.stimuli.length);
 18 |             for (var i = 0; i < trials.length; i++) {
 19 |                 trials[i] = {};
 20 |                 trials[i].type = "animation";
 21 |                 trials[i].stims = params.stimuli[i];
 22 |                 trials[i].frame_time = params.frame_time || 250;
 23 |                 trials[i].frame_isi = params.frame_isi || 0;
 24 |                 trials[i].repetitions = params.repetitions || 1;
 25 |                 trials[i].choices = params.choices || [];
 26 |                 trials[i].timing_post_trial = (typeof params.timing_post_trial === 'undefined') ? 1000 : params.timing_post_trial;
 27 |                 trials[i].prompt = (typeof params.prompt === 'undefined') ? "" : params.prompt;
 28 |                 trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
 29 |             }
 30 |             return trials;
 31 |         };
 32 | 
 33 |         plugin.trial = function(display_element, block, trial, part) {
 34 |             
 35 |             // if any trial variables are functions
 36 |             // this evaluates the function and replaces
 37 |             // it with the output of the function
 38 |             trial = jsPsych.pluginAPI.normalizeTrialVariables(trial);
 39 |             
 40 |             var interval_time = trial.frame_time + trial.frame_isi;
 41 |             var animate_frame = -1;
 42 |             var reps = 0;
 43 |             var startTime = (new Date()).getTime();
 44 |             var animation_sequence = [];
 45 |             var responses = [];
 46 |             var current_stim = "";
 47 | 
 48 |             var animate_interval = setInterval(function() {
 49 |                 var showImage = true;
 50 |                 display_element.html(""); // clear everything
 51 |                 animate_frame++;
 52 |                 if (animate_frame == trial.stims.length) {
 53 |                     animate_frame = 0;
 54 |                     reps++;
 55 |                     if (reps >= trial.repetitions) {
 56 |                         endTrial();
 57 |                         clearInterval(animate_interval);
 58 |                         showImage = false;
 59 |                     }
 60 |                 }
 61 |                 if (showImage) {
 62 |                     show_next_frame();
 63 |                 }
 64 |             }, interval_time);
 65 | 
 66 |             function show_next_frame() {
 67 |                 // show image
 68 |                 display_element.append($('', {
 69 |                     "src": trial.stims[animate_frame],
 70 |                     "id": 'jspsych-animation-image'
 71 |                 }));
 72 |                 
 73 |                 current_stim = trial.stims[animate_frame];
 74 | 
 75 |                 // record when image was shown
 76 |                 animation_sequence.push({
 77 |                     "stimulus": current_stim,
 78 |                     "time": (new Date()).getTime() - startTime
 79 |                 });
 80 | 
 81 |                 if (trial.prompt !== "") {
 82 |                     display_element.append(trial.prompt);
 83 |                 }
 84 | 
 85 |                 if (trial.frame_isi > 0) {
 86 |                     setTimeout(function() {
 87 |                         $('#jspsych-animation-image').css('visibility', 'hidden');
 88 |                         current_stim = 'blank';
 89 |                         // record when blank image was shown
 90 |                         animation_sequence.push({
 91 |                             "stimulus": 'blank',
 92 |                             "time": (new Date()).getTime() - startTime
 93 |                         });
 94 |                     }, trial.frame_time);
 95 |                 }
 96 |             }
 97 |             
 98 |             var after_response = function(info) {
 99 |                 
100 |                 responses.push({
101 |                     key_press: info.key,
102 |                     rt: info.rt,
103 |                     stimulus: current_stim
104 |                 });
105 |                 
106 |                 // after a valid response, the stimulus will have the CSS class 'responded'
107 |                 // which can be used to provide visual feedback that a response was recorded
108 |                 $("#jspsych-animation-image").addClass('responded');
109 |             }
110 | 
111 |             // hold the jspsych response listener object in memory
112 |             // so that we can turn off the response collection when
113 |             // the trial ends
114 |             var response_listener = jsPsych.pluginAPI.getKeyboardResponse(after_response, trial.choices, 'date', true);
115 | 
116 |             function endTrial() {
117 |                 
118 |                 jsPsych.pluginAPI.cancelKeyboardResponse(response_listener);
119 |                 
120 |                 block.writeData($.extend({}, {
121 |                     "trial_type": "animation",
122 |                     "trial_index": block.trial_idx,
123 |                     "animation_sequence": JSON.stringify(animation_sequence),
124 |                     "responses": JSON.stringify(responses)
125 |                 }, trial.data));
126 | 
127 |                 if(trial.timing_post_trial > 0){
128 |                     setTimeout(function() {
129 |                         block.next();
130 |                     }, trial.timing_post_trial);
131 |                 } else {
132 |                     block.next();
133 |                 }
134 |             }
135 |         };
136 | 
137 |         return plugin;
138 |     })();
139 | })(jQuery);
140 | 


--------------------------------------------------------------------------------
/scripts/plugins/jspsych-call-function.js:
--------------------------------------------------------------------------------
 1 | /** 
 2 |  * jspsych-call-function
 3 |  * plugin for calling an arbitrary function during a jspsych experiment
 4 |  * Josh de Leeuw
 5 |  * 
 6 |  * documentation: https://github.com/jodeleeuw/jsPsych/wiki/jspsych-call-function
 7 |  * 
 8 | **/
 9 | 
10 | (function($) {
11 |     jsPsych['call-function'] = (function() {
12 | 
13 |         var plugin = {};
14 | 
15 |         plugin.create = function(params) {
16 |             var trials = new Array(1);
17 |             trials[0] = {
18 |                 "type": "call-function",
19 |                 "func": params.func,
20 |                 "args": params.args || [],
21 |                 "data": (typeof params.data === 'undefined') ? {} : params.data
22 |             };
23 |             return trials;
24 |         };
25 | 
26 |         plugin.trial = function(display_element, block, trial, part) {
27 |             var return_val = trial.func.apply({}, [trial.args]);
28 |             if (typeof return_val !== 'undefined') {
29 |                 block.writeData($.extend({},{
30 |                     trial_type: "call-function",
31 |                     trial_index: block.trial_idx,
32 |                     value: return_val
33 |                 },trial.data));
34 |             }
35 | 
36 |             block.next();
37 |         };
38 | 
39 |         return plugin;
40 |     })();
41 | })(jQuery);
42 | 


--------------------------------------------------------------------------------
/scripts/plugins/jspsych-categorize-animation.js:
--------------------------------------------------------------------------------
  1 | /** 
  2 |  * jspsych plugin for categorization trials with feedback and animated stimuli
  3 |  * Josh de Leeuw
  4 |  * 
  5 |  * documentation: https://github.com/jodeleeuw/jsPsych/wiki/jspsych-categorize-animation
  6 |  **/
  7 | 
  8 | (function($) {
  9 |     jsPsych["categorize-animation"] = (function() {
 10 | 
 11 |         var plugin = {};
 12 | 
 13 |         plugin.create = function(params) {
 14 |             
 15 |             params = jsPsych.pluginAPI.enforceArray(params, ['key_answer','text_answer','choices','data']);
 16 |             
 17 |             var trials = new Array(params.stimuli.length);
 18 |             for (var i = 0; i < trials.length; i++) {
 19 |                 trials[i] = {};
 20 |                 trials[i].type = "categorize-animation";
 21 |                 trials[i].stims = params.stimuli[i];
 22 |                 trials[i].reps = params.reps || 1;
 23 |                 trials[i].key_answer = params.key_answer[i];
 24 |                 trials[i].text_answer = (typeof params.text_answer === 'undefined') ? "" : params.text_answer[i];
 25 |                 trials[i].choices = params.choices;
 26 |                 trials[i].correct_text = params.correct_text || "Correct.";
 27 |                 trials[i].incorrect_text = params.incorrect_text || "Wrong.";
 28 |                 trials[i].allow_response_before_complete = params.allow_response_before_complete || false;
 29 |                 trials[i].frame_time = params.frame_time || 500;
 30 |                 trials[i].timing_feedback_duration = params.timing_feedback_duration || 2000;
 31 |                 trials[i].timing_post_trial = (typeof params.timing_post_trial === 'undefined') ? 1000 : params.timing_post_trial;
 32 |                 trials[i].prompt = (typeof params.prompt === 'undefined') ? '' : params.prompt;
 33 |                 trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
 34 |             }
 35 |             return trials;
 36 |         };
 37 | 
 38 |         plugin.trial = function(display_element, block, trial, part) {
 39 |             
 40 |             // if any trial variables are functions
 41 |             // this evaluates the function and replaces
 42 |             // it with the output of the function
 43 |             trial = jsPsych.pluginAPI.normalizeTrialVariables(trial);
 44 |             
 45 |             var animate_frame = -1;
 46 |             var reps = 0;
 47 | 
 48 |             var showAnimation = true;
 49 | 
 50 |             var responded = false;
 51 |             var timeoutSet = false;
 52 | 
 53 | 
 54 |             var startTime = (new Date()).getTime();
 55 | 
 56 |             // show animation
 57 |             var animate_interval = setInterval(function() {
 58 |                 display_element.html(""); // clear everything
 59 |                 animate_frame++;
 60 |                 if (animate_frame == trial.stims.length) {
 61 |                     animate_frame = 0;
 62 |                     reps++;
 63 |                     // check if reps complete //
 64 |                     if (trial.reps != -1 && reps >= trial.reps) {
 65 |                         // done with animation
 66 |                         showAnimation = false;
 67 |                     }
 68 |                 }
 69 | 
 70 |                 if (showAnimation) {
 71 |                     display_element.append($('', {
 72 |                         "src": trial.stims[animate_frame],
 73 |                         "class": 'jspsych-categorize-animation-stimulus'
 74 |                     }));
 75 |                 }
 76 | 
 77 |                 if (!responded && trial.allow_response_before_complete) {
 78 |                     // in here if the user can respond before the animation is done
 79 |                     if (trial.prompt !== "") {
 80 |                         display_element.append(trial.prompt);
 81 |                     }
 82 |                 }
 83 |                 else if (!responded) {
 84 |                     // in here if the user has to wait to respond until animation is done.
 85 |                     // if this is the case, don't show the prompt until the animation is over.
 86 |                     if (!showAnimation) {
 87 |                         if (trial.prompt !== "") {
 88 |                             display_element.append(trial.prompt);
 89 |                         }
 90 |                     }
 91 |                 }
 92 |                 else {
 93 |                     // user has responded if we get here.
 94 | 
 95 |                     // show feedback
 96 |                     var feedback_text = "";
 97 |                     if (block.data[block.trial_idx].correct) {
 98 |                         feedback_text = trial.correct_text.replace("%ANS%", trial.text_answer);
 99 |                     }
100 |                     else {
101 |                         feedback_text = trial.incorrect_text.replace("%ANS%", trial.text_answer);
102 |                     }
103 |                     display_element.append(feedback_text);
104 | 
105 |                     // set timeout to clear feedback
106 |                     if (!timeoutSet) {
107 |                         timeoutSet = true;
108 |                         setTimeout(function() {
109 |                             endTrial();
110 |                         }, trial.timing_feedback_duration);
111 |                     }
112 |                 }
113 | 
114 | 
115 |             }, trial.frame_time);
116 | 
117 |             
118 |             var keyboard_listener;
119 |             
120 |             var after_response = function(info){
121 |                 // ignore the response if animation is playing and subject
122 |                 // not allowed to respond before it is complete
123 |                 if (!trial.allow_response_before_complete && showAnimation) {
124 |                     return false;
125 |                 }
126 |                 
127 |                 var correct = false;
128 |                 if(trial.key_answer == info.key) {
129 |                     correct = true;
130 |                 }
131 |                 
132 |                 responded = true;
133 | 
134 |                 var trial_data = {
135 |                     "trial_type": trial.type,
136 |                     "trial_index": block.trial_idx,
137 |                     "stimulus": trial.stims[0],
138 |                     "rt": info.rt,
139 |                     "correct": correct,
140 |                     "key_press": info.key
141 |                 };
142 |                 
143 |                 block.writeData($.extend({}, trial_data, trial.data));
144 |                 
145 |                 jsPsych.pluginAPI.cancelKeyboardResponse(keyboard_listener);
146 |                 
147 |             }
148 |             
149 |             jsPsych.pluginAPI.getKeyboardResponse(after_response, trial.choices, 'date', true);
150 | 
151 |             function endTrial() {
152 |                 clearInterval(animate_interval); // stop animation!
153 |                 display_element.html(''); // clear everything
154 |                 if(trial.timing_post_trial > 0){
155 |                     setTimeout(function() {
156 |                         block.next();
157 |                     }, trial.timing_post_trial);
158 |                 } else {
159 |                     block.next();
160 |                 }
161 |             }
162 |         };
163 | 
164 |         return plugin;
165 |     })();
166 | })(jQuery);
167 | 


--------------------------------------------------------------------------------
/scripts/plugins/jspsych-categorize.js:
--------------------------------------------------------------------------------
  1 | /** 
  2 |  * jspsych plugin for categorization trials with feedback
  3 |  * Josh de Leeuw
  4 |  * 
  5 |  * documentation: https://github.com/jodeleeuw/jsPsych/wiki/jspsych-categorize
  6 | **/
  7 | 
  8 | (function($) {
  9 |     jsPsych.categorize = (function() {
 10 | 
 11 |         var plugin = {};
 12 | 
 13 |         plugin.create = function(params) {
 14 |             
 15 |             params = jsPsych.pluginAPI.enforceArray(params, ['choices', 'stimuli', 'key_answer', 'text_answer', 'data']);
 16 |             
 17 |             var trials = [];
 18 |             for (var i = 0; i < params.stimuli.length; i++) {
 19 |                 trials.push({});
 20 |                 trials[i].type = "categorize";
 21 |                 trials[i].a_path = params.stimuli[i];
 22 |                 trials[i].key_answer = params.key_answer[i];
 23 |                 trials[i].text_answer = (typeof params.text_answer === 'undefined') ? "" : params.text_answer[i];
 24 |                 trials[i].choices = params.choices;
 25 |                 trials[i].correct_text = (typeof params.correct_text === 'undefined') ? "" : params.correct_text;
 26 |                 trials[i].incorrect_text = (typeof params.incorrect_text === 'undefined') ? "" : params.incorrect_text;
 27 |                 // timing params
 28 |                 trials[i].timing_stim = params.timing_stim || -1; // default is to show image until response
 29 |                 trials[i].timing_feedback_duration = params.timing_feedback_duration || 2000;
 30 |                 trials[i].timing_post_trial = (typeof params.timing_post_trial === 'undefined') ? 1000 : params.timing_post_trial;
 31 |                 // optional params
 32 |                 trials[i].show_stim_with_feedback = (typeof params.show_stim_with_feedback === 'undefined') ? true : params.show_stim_with_feedback;
 33 |                 trials[i].is_html = (typeof params.is_html === 'undefined') ? false : params.is_html;
 34 |                 trials[i].force_correct_button_press = (typeof params.force_correct_button_press === 'undefined') ? false : params.force_correct_button_press;
 35 |                 trials[i].prompt = (typeof params.prompt === 'undefined') ? '' : params.prompt;
 36 |                 trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
 37 |             }
 38 |             return trials;
 39 |         };
 40 | 
 41 |         var cat_trial_complete = false;
 42 | 
 43 |         plugin.trial = function(display_element, block, trial, part) {
 44 |             
 45 |             // if any trial variables are functions
 46 |             // this evaluates the function and replaces
 47 |             // it with the output of the function
 48 |             trial = jsPsych.pluginAPI.normalizeTrialVariables(trial);
 49 |             
 50 |             switch (part) {
 51 |             case 1:
 52 |                 // set finish flag
 53 |                 cat_trial_complete = false;
 54 | 
 55 |                 if (!trial.is_html) {
 56 |                     // add image to display
 57 |                     display_element.append($('', {
 58 |                         "src": trial.a_path,
 59 |                         "class": 'jspsych-categorize-stimulus',
 60 |                         "id": 'jspsych-categorize-stimulus'
 61 |                     }));
 62 |                 }
 63 |                 else {
 64 |                     display_element.append($('
', { 65 | "id": 'jspsych-categorize-stimulus', 66 | "class": 'jspsych-categorize-stimulus', 67 | "html": trial.a_path 68 | })); 69 | } 70 | 71 | // hide image after time if the timing parameter is set 72 | if (trial.timing_stim > 0) { 73 | setTimeout(function() { 74 | if (!cat_trial_complete) { 75 | $('#jspsych-categorize-stimulus').css('visibility', 'hidden'); 76 | } 77 | }, trial.timing_stim); 78 | } 79 | 80 | // if prompt is set, show prompt 81 | if (trial.prompt !== "") { 82 | display_element.append(trial.prompt); 83 | } 84 | 85 | // start measuring RT 86 | var startTime = (new Date()).getTime(); 87 | 88 | // create response function 89 | var after_response = function(info) { 90 | 91 | var correct = false; 92 | if(trial.key_answer == info.key) { correct = true; } 93 | 94 | cat_trial_complete = true; 95 | 96 | // save data 97 | var trial_data = { 98 | "trial_type": "categorize", 99 | "trial_index": block.trial_idx, 100 | "rt": info.rt, 101 | "correct": correct, 102 | "stimulus": trial.a_path, 103 | "key_press": info.key 104 | }; 105 | 106 | block.writeData($.extend({}, trial_data, trial.data)); 107 | 108 | display_element.html(''); 109 | 110 | plugin.trial(display_element, block, trial, part + 1); 111 | } 112 | 113 | jsPsych.pluginAPI.getKeyboardResponse(after_response, trial.choices, 'date', false); 114 | 115 | break; 116 | 117 | case 2: 118 | // show image during feedback if flag is set 119 | if (trial.show_stim_with_feedback) { 120 | if (!trial.is_html) { 121 | // add image to display 122 | display_element.append($('', { 123 | "src": trial.a_path, 124 | "class": 'jspsych-categorize-stimulus', 125 | "id": 'jspsych-categorize-stimulus' 126 | })); 127 | } 128 | else { 129 | display_element.append($('
', { 130 | "id": 'jspsych-categorize-stimulus', 131 | "class": 'jspsych-categorize-stimulus', 132 | "html": trial.a_path 133 | })); 134 | } 135 | } 136 | 137 | // substitute answer in feedback string. 138 | var atext = ""; 139 | if (block.data[block.trial_idx].correct) { 140 | atext = trial.correct_text.replace("%ANS%", trial.text_answer); 141 | } 142 | else { 143 | atext = trial.incorrect_text.replace("%ANS%", trial.text_answer); 144 | } 145 | 146 | // show the feedback 147 | display_element.append(atext); 148 | 149 | // check if force correct button press is set 150 | if (trial.force_correct_button_press && block.data[block.trial_idx].correct === false) { 151 | 152 | var after_forced_response = function(info) { 153 | plugin.trial(display_element, block, trial, part + 1); 154 | } 155 | 156 | jsPsych.pluginAPI.getKeyboardResponse(after_forced_response, trial.key_answer, 'date', false); 157 | 158 | } 159 | else { 160 | setTimeout(function() { 161 | plugin.trial(display_element, block, trial, part + 1); 162 | }, trial.timing_feedback_duration); 163 | } 164 | break; 165 | case 3: 166 | display_element.html(""); 167 | if(trial.timing_post_trial > 0){ 168 | setTimeout(function() { 169 | block.next(); 170 | }, trial.timing_post_trial); 171 | } else { 172 | block.next(); 173 | } 174 | break; 175 | } 176 | }; 177 | 178 | return plugin; 179 | })(); 180 | })(jQuery); 181 | -------------------------------------------------------------------------------- /scripts/plugins/jspsych-free-sort.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jspsych-free-sort 3 | * plugin for drag-and-drop sorting of a collection of images 4 | * Josh de Leeuw 5 | * 6 | * documentation: https://github.com/jodeleeuw/jsPsych/wiki/jspsych-free-sort 7 | */ 8 | 9 | (function($) { 10 | jsPsych['free-sort'] = (function() { 11 | 12 | var plugin = {}; 13 | 14 | plugin.create = function(params) { 15 | 16 | params = jsPsych.pluginAPI.enforceArray(params, ['data']); 17 | 18 | var trials = new Array(params.stimuli.length); 19 | for (var i = 0; i < trials.length; i++) { 20 | trials[i] = { 21 | "type": "free-sort", 22 | "images": params.stimuli[i], // array of images to display 23 | "stim_height": params.stim_height || 100, 24 | "stim_width": params.stim_width || 100, 25 | "timing_post_trial": (typeof params.timing_post_trial === 'undefined') ? 1000 : params.timing_post_trial, 26 | "prompt": (typeof params.prompt === 'undefined') ? '' : params.prompt, 27 | "prompt_location": params.prompt_location || "above", 28 | "sort_area_width": params.sort_area_width || 800, 29 | "sort_area_height": params.sort_area_height || 800, 30 | "data": (typeof params.data === 'undefined') ? {} : params.data[i] 31 | }; 32 | } 33 | return trials; 34 | }; 35 | 36 | plugin.trial = function(display_element, block, trial, part) { 37 | 38 | // if any trial variables are functions 39 | // this evaluates the function and replaces 40 | // it with the output of the function 41 | trial = jsPsych.pluginAPI.normalizeTrialVariables(trial); 42 | 43 | var start_time = (new Date()).getTime(); 44 | 45 | // check if there is a prompt and if it is shown above 46 | if (trial.prompt && trial.prompt_location == "above") { 47 | display_element.append(trial.prompt); 48 | } 49 | 50 | display_element.append($('
', { 51 | "id": "jspsych-free-sort-arena", 52 | "class": "jspsych-free-sort-arena", 53 | "css": { 54 | "position": "relative", 55 | "width": trial.sort_area_width, 56 | "height": trial.sort_area_height 57 | } 58 | })); 59 | 60 | // check if prompt exists and if it is shown below 61 | if (trial.prompt && trial.prompt_location == "below") { 62 | display_element.append(trial.prompt); 63 | } 64 | 65 | // store initial location data 66 | var init_locations = []; 67 | 68 | for (var i = 0; i < trial.images.length; i++) { 69 | var coords = random_coordinate(trial.sort_area_width - trial.stim_width, trial.sort_area_height - trial.stim_height); 70 | 71 | $("#jspsych-free-sort-arena").append($('', { 72 | "src": trial.images[i], 73 | "class": "jspsych-free-sort-draggable", 74 | "css": { 75 | "position": "absolute", 76 | "top": coords.y, 77 | "left": coords.x 78 | } 79 | })); 80 | 81 | init_locations.push({ 82 | "src": trial.images[i], 83 | "x": coords.x, 84 | "y": coords.y 85 | }); 86 | } 87 | 88 | var moves = []; 89 | 90 | $('.jspsych-free-sort-draggable').draggable({ 91 | containment: "#jspsych-free-sort-arena", 92 | scroll: false, 93 | stack: ".jspsych-free-sort-draggable", 94 | stop: function(event, ui) { 95 | moves.push({ 96 | "src": event.target.src.split("/").slice(-1)[0], 97 | "x": ui.position.left, 98 | "y": ui.position.top 99 | }); 100 | } 101 | }); 102 | 103 | display_element.append($('')); 220 | $('#jspsych-palmer-submitButton').click(function() { 221 | save_data(); 222 | }); 223 | } 224 | 225 | // if trial.editable is false, then we are just showing a pre-determined configuration. 226 | // for now, the only option will be to display for a fixed amount of time. 227 | // future ideas: allow for key response, to enable things like n-back, same/different, etc.. 228 | if (!trial.editable) { 229 | showConfiguration(trial.configurations); 230 | 231 | setTimeout(function() { 232 | save_data(); 233 | }, trial.timing_item); 234 | } 235 | 236 | if (trial.prompt !== "") { 237 | display_element.append($('
')); 238 | $("#jspsych-palmer-prompt").html(trial.prompt); 239 | } 240 | 241 | function arrayDifferences(arr1, arr2) { 242 | var n_diff = 0; 243 | for (var i = 0; i < arr1.length; i++) { 244 | if (arr1[i] != arr2[i]) { 245 | n_diff++; 246 | } 247 | } 248 | return n_diff; 249 | } 250 | 251 | // save data 252 | function save_data() { 253 | 254 | // measure RT 255 | var endTime = (new Date()).getTime(); 256 | var response_time = endTime - startTime; 257 | 258 | // check if configuration is correct 259 | // this is meaningless for trials where the user can't edit 260 | var n_diff = arrayDifferences(trial.configurations, lineIsVisible); 261 | var correct = (n_diff === 0); 262 | 263 | block.writeData($.extend({}, { 264 | "trial_type": "palmer", 265 | "trial_index": block.trial_idx, 266 | "configuration": JSON.stringify(lineIsVisible), 267 | "target_configuration": JSON.stringify(trial.configurations), 268 | "rt": response_time, 269 | "correct": correct, 270 | "num_wrong": n_diff, 271 | }, trial.data)); 272 | 273 | if (trial.editable && trial.show_feedback) { 274 | // hide the button 275 | $('#jspsych-palmer-submitButton').hide(); 276 | $('#jspsych-palmer-prompt').hide(); 277 | 278 | showConfiguration(trial.configurations); 279 | var feedback = ""; 280 | if (correct) { 281 | feedback = "Correct!"; 282 | } 283 | else { 284 | if (n_diff > 1) { 285 | feedback = "You missed " + n_diff + " lines. The correct symbol is shown above."; 286 | } 287 | else { 288 | feedback = "You missed 1 line. The correct symbol is shown above."; 289 | } 290 | } 291 | display_element.append($.parseHTML("

" + feedback + "

")); 292 | 293 | setTimeout(function() { 294 | next_trial(); 295 | }, trial.timing_feedback); 296 | 297 | } 298 | else { 299 | next_trial(); 300 | } 301 | } 302 | 303 | function next_trial() { 304 | 305 | display_element.html(''); 306 | 307 | // next trial 308 | if (trial.timing_post_trial > 0) { 309 | setTimeout(function() { 310 | block.next(); 311 | }, trial.timing_post_trial); 312 | } 313 | else { 314 | block.next(); 315 | } 316 | 317 | } 318 | 319 | 320 | }; 321 | 322 | // method for drawing palmer stimuli. 323 | // returns the string description of svg element containing the stimulus 324 | // requires raphaeljs library -> www.raphaeljs.com 325 | 326 | plugin.generate_stimulus = function(square_size, grid_spacing, circle_radius, configuration) { 327 | 328 | // create a div to hold the generated svg object 329 | var stim_div = $('body').append('
'); 330 | 331 | var size = grid_spacing * (square_size + 1); 332 | 333 | // create the svg raphael object 334 | var paper = Raphael("jspsych-palmer-temp-stim", size, size); 335 | 336 | // create the circles at the vertices. 337 | var circles = []; 338 | var node_idx = 0; 339 | for (var i = 1; i <= square_size; i++) { 340 | for (var j = 1; j <= square_size; j++) { 341 | var circle = paper.circle(grid_spacing * j, grid_spacing * i, circle_radius); 342 | circle.attr("fill", "#000").attr("stroke-width", "0").attr("stroke", "#000").data("node", node_idx); 343 | node_idx++; 344 | circles.push(circle); 345 | } 346 | } 347 | 348 | // create all possible lines that connect circles 349 | var horizontal_lines = []; 350 | var vertical_lines = []; 351 | var backslash_lines = []; 352 | var forwardslash_lines = []; 353 | 354 | for (var i = 0; i < square_size; i++) { 355 | for (var j = 0; j < square_size; j++) { 356 | var current_item = (i * square_size) + j; 357 | // add horizontal connections 358 | if (j < (square_size - 1)) { 359 | horizontal_lines.push([current_item, current_item + 1]); 360 | } 361 | // add vertical connections 362 | if (i < (square_size - 1)) { 363 | vertical_lines.push([current_item, current_item + square_size]); 364 | } 365 | // add diagonal backslash connections 366 | if (i < (square_size - 1) && j < (square_size - 1)) { 367 | backslash_lines.push([current_item, current_item + square_size + 1]); 368 | } 369 | // add diagonal forwardslash connections 370 | if (i < (square_size - 1) && j > 0) { 371 | forwardslash_lines.push([current_item, current_item + square_size - 1]); 372 | } 373 | } 374 | } 375 | 376 | var lines = horizontal_lines.concat(vertical_lines).concat(backslash_lines).concat(forwardslash_lines); 377 | 378 | // actually draw the lines 379 | var lineIsVisible = []; 380 | var lineElements = []; 381 | 382 | for (var i = 0; i < lines.length; i++) { 383 | 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"); 384 | line.hide(); 385 | lineElements.push(line); 386 | lineIsVisible.push(0); 387 | } 388 | 389 | // define some helper functions to toggle lines on and off 390 | 391 | // this function turns a line on/off based on the index (the_line) 392 | function toggle_line(the_line) { 393 | if (the_line > -1) { 394 | if (lineIsVisible[the_line] === 0) { 395 | lineElements[the_line].show(); 396 | lineElements[the_line].toBack(); 397 | lineIsVisible[the_line] = 1; 398 | } 399 | else { 400 | lineElements[the_line].hide(); 401 | lineElements[the_line].toBack(); 402 | lineIsVisible[the_line] = 0; 403 | } 404 | } 405 | } 406 | 407 | // displays the line wherever there 408 | // is a 1 in the array. 409 | // showConfiguration(configuration) 410 | for (var i = 0; i < configuration.length; i++) { 411 | if (configuration[i] == 1) { 412 | toggle_line(i); 413 | } 414 | } 415 | 416 | 417 | var svg = $("#jspsych-palmer-temp-stim").html(); 418 | 419 | $('#jspsych-palmer-temp-stim').remove(); 420 | 421 | return svg; 422 | }; 423 | 424 | return plugin; 425 | })(); 426 | })(jQuery); 427 | -------------------------------------------------------------------------------- /scripts/plugins/jspsych-same-different.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jspsych-same-different 3 | * Josh de Leeuw 4 | * 5 | * plugin for showing two stimuli sequentially and getting a same / different judgment 6 | * 7 | * documentation: https://github.com/jodeleeuw/jsPsych/wiki/jspsych-same-different 8 | * 9 | */ 10 | (function($) { 11 | jsPsych['same-different'] = (function() { 12 | 13 | var plugin = {}; 14 | 15 | plugin.create = function(params) { 16 | 17 | params = jsPsych.pluginAPI.enforceArray(params, ['data','answer']) 18 | 19 | var trials = new Array(params.stimuli.length); 20 | for (var i = 0; i < trials.length; i++) { 21 | trials[i] = {}; 22 | trials[i].type = "same-different"; 23 | trials[i].a_path = params.stimuli[i][0]; 24 | trials[i].b_path = params.stimuli[i][1]; 25 | trials[i].answer = params.answer[i]; 26 | trials[i].same_key = params.same_key || 81; // default is 'q' 27 | trials[i].different_key = params.different_key || 80; // default is 'p' 28 | // timing parameters 29 | trials[i].timing_first_stim = params.timing_first_stim || 1000; 30 | trials[i].timing_second_stim = params.timing_second_stim || 1000; // if -1, then second stim is shown until response. 31 | trials[i].timing_gap = params.timing_gap || 500; 32 | trials[i].timing_post_trial = (typeof params.timing_post_trial === 'undefined') ? 1000 : params.timing_post_trial; 33 | // optional parameters 34 | trials[i].is_html = (typeof params.is_html === 'undefined') ? false : true; 35 | trials[i].prompt = (typeof params.prompt === 'undefined') ? "" : params.prompt; 36 | trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i]; 37 | } 38 | return trials; 39 | }; 40 | 41 | var sd_trial_complete = false; 42 | 43 | plugin.trial = function(display_element, block, trial, part) { 44 | 45 | // if any trial variables are functions 46 | // this evaluates the function and replaces 47 | // it with the output of the function 48 | trial = jsPsych.pluginAPI.normalizeTrialVariables(trial); 49 | 50 | 51 | switch (part) { 52 | case 1: 53 | sd_trial_complete = false; 54 | // show image 55 | if (!trial.is_html) { 56 | display_element.append($('', { 57 | src: trial.a_path, 58 | "class": 'jspsych-same-different-stimulus' 59 | })); 60 | } 61 | else { 62 | display_element.append($('
', { 63 | html: trial.a_path, 64 | "class": 'jspsych-same-different-stimulus' 65 | })); 66 | } 67 | setTimeout(function() { 68 | plugin.trial(display_element, block, trial, part + 1); 69 | }, trial.timing_first_stim); 70 | break; 71 | case 2: 72 | $('.jspsych-same-different-stimulus').remove(); 73 | setTimeout(function() { 74 | plugin.trial(display_element, block, trial, part + 1); 75 | }, trial.timing_gap); 76 | break; 77 | case 3: 78 | if (!trial.is_html) { 79 | display_element.append($('', { 80 | src: trial.b_path, 81 | "class": 'jspsych-same-different-stimulus', 82 | id: 'jspsych-same-different-second-stimulus' 83 | })); 84 | } 85 | else { 86 | display_element.append($('
', { 87 | html: trial.b_path, 88 | "class": 'jspsych-same-different-stimulus', 89 | id: 'jspsych-same-different-second-stimulus' 90 | })); 91 | } 92 | 93 | if (trial.timing_second_stim > 0) { 94 | setTimeout(function() { 95 | if (!sd_trial_complete) { 96 | $("#jspsych-same-different-second-stimulus").css('visibility', 'hidden'); 97 | } 98 | }, trial.timing_second_stim); 99 | } 100 | 101 | //show prompt here 102 | if (trial.prompt !== "") { 103 | display_element.append(trial.prompt); 104 | } 105 | 106 | var after_response = function(info){ 107 | 108 | var correct = false; 109 | 110 | if(info.key == trial.same_key && trial.answer == 'same'){ 111 | correct = true; 112 | } 113 | 114 | if(info.key == trial.different_key && trial.answer == 'different'){ 115 | correct = true; 116 | } 117 | 118 | var trial_data = { 119 | "trial_type": "same-different", 120 | "trial_index": block.trial_idx, 121 | "rt": info.rt, 122 | "correct": correct, 123 | "stimulus": trial.a_path, 124 | "stimulus_2": trial.b_path, 125 | "key_press": info.key 126 | }; 127 | block.writeData($.extend({}, trial_data, trial.data)); 128 | 129 | display_element.html(''); 130 | 131 | if(trial.timing_post_trial > 0) { 132 | setTimeout(function() { 133 | block.next(); 134 | }, trial.timing_post_trial); 135 | } else { 136 | block.next(); 137 | } 138 | } 139 | 140 | jsPsych.pluginAPI.getKeyboardResponse(after_response, [trial.same_key, trial.different_key], 'date', false); 141 | 142 | break; 143 | } 144 | }; 145 | 146 | return plugin; 147 | })(); 148 | })(jQuery); 149 | -------------------------------------------------------------------------------- /scripts/plugins/jspsych-similarity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jspsych-similarity.js 3 | * Josh de Leeuw 4 | * 5 | * This plugin create a trial where two images are shown sequentially, and the subject rates their similarity using a slider controlled with the mouse. 6 | * 7 | * documentation: https://github.com/jodeleeuw/jsPsych/wiki/jspsych-similarity 8 | * 9 | */ 10 | 11 | (function($) { 12 | jsPsych.similarity = (function() { 13 | 14 | var plugin = {}; 15 | 16 | plugin.create = function(params) { 17 | 18 | jsPsych.pluginAPI.enforceArray(params, ['data']); 19 | 20 | var trials = new Array(params.stimuli.length); 21 | for (var i = 0; i < trials.length; i++) { 22 | trials[i] = {}; 23 | trials[i].type = "similarity"; 24 | trials[i].a_path = params.stimuli[i][0]; 25 | trials[i].b_path = params.stimuli[i][1]; 26 | trials[i].labels = (typeof params.labels === 'undefined') ? ["Not at all similar", "Identical"] : params.labels; 27 | trials[i].intervals = params.intervals || 100; 28 | trials[i].show_ticks = (typeof params.show_ticks === 'undefined') ? false : params.show_ticks; 29 | 30 | trials[i].show_response = params.show_response || "SECOND_STIMULUS"; 31 | 32 | trials[i].timing_first_stim = params.timing_first_stim || 1000; // default 1000ms 33 | trials[i].timing_second_stim = params.timing_second_stim || -1; // -1 = inf time; positive numbers = msec to display second image. 34 | trials[i].timing_image_gap = params.timing_image_gap || 1000; // default 1000ms 35 | trials[i].timing_post_trial = (typeof params.timing_post_trial === 'undefined') ? 1000 : params.timing_post_trial; // default 1000ms 36 | 37 | trials[i].is_html = (typeof params.is_html === 'undefined') ? false : params.is_html; 38 | trials[i].prompt = (typeof params.prompt === 'undefined') ? '' : params.prompt; 39 | trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i]; 40 | } 41 | return trials; 42 | }; 43 | 44 | var sim_trial_complete = false; 45 | 46 | plugin.trial = function(display_element, block, trial, part) { 47 | 48 | // if any trial variables are functions 49 | // this evaluates the function and replaces 50 | // it with the output of the function 51 | trial = jsPsych.pluginAPI.normalizeTrialVariables(trial); 52 | 53 | switch (part) { 54 | case 1: 55 | sim_trial_complete = false; 56 | // show the images 57 | if (!trial.is_html) { 58 | display_element.append($('', { 59 | "src": trial.a_path, 60 | "id": 'jspsych_sim_stim' 61 | })); 62 | } 63 | else { 64 | display_element.append($('
', { 65 | "html": trial.a_path, 66 | "id": 'jspsych_sim_stim' 67 | })); 68 | } 69 | 70 | if (trial.show_response == "FIRST_STIMULUS") { 71 | show_response_slider(display_element, trial, block); 72 | } 73 | 74 | setTimeout(function() { 75 | plugin.trial(display_element, block, trial, part + 1); 76 | }, trial.timing_first_stim); 77 | break; 78 | 79 | case 2: 80 | 81 | $('#jspsych_sim_stim').css('visibility', 'hidden'); 82 | 83 | setTimeout(function() { 84 | plugin.trial(display_element, block, trial, part + 1); 85 | }, trial.timing_image_gap); 86 | break; 87 | 88 | case 3: 89 | 90 | if (!trial.is_html) { 91 | $('#jspsych_sim_stim').attr('src', trial.b_path); 92 | } 93 | else { 94 | $('#jspsych_sim_stim').html(trial.b_path); 95 | } 96 | 97 | $('#jspsych_sim_stim').css('visibility', 'visible'); 98 | 99 | if (trial.show_response == "SECOND_STIMULUS") { 100 | show_response_slider(display_element, trial, block); 101 | } 102 | 103 | if (trial.timing_second_stim > 0) { 104 | setTimeout(function() { 105 | if (!sim_trial_complete) { 106 | $("#jspsych_sim_stim").css('visibility', 'hidden'); 107 | if (trial.show_response == "POST_STIMULUS") { 108 | show_response_slider(display_element, trial, block); 109 | } 110 | } 111 | }, trial.timing_second_stim); 112 | } 113 | 114 | break; 115 | } 116 | }; 117 | 118 | function show_response_slider(display_element, trial, block) { 119 | 120 | var startTime = (new Date()).getTime(); 121 | 122 | // create slider 123 | display_element.append($('
', { 124 | "id": 'slider', 125 | "class": 'sim' 126 | })); 127 | 128 | $("#slider").slider({ 129 | value: Math.ceil(trial.intervals / 2), 130 | min: 1, 131 | max: trial.intervals, 132 | step: 1, 133 | }); 134 | 135 | // show tick marks 136 | if (trial.show_ticks) { 137 | for (var j = 1; j < trial.intervals - 1; j++) { 138 | $('#slider').append('
'); 139 | } 140 | 141 | $('#slider .slidertickmark').each(function(index) { 142 | var left = (index + 1) * (100 / (trial.intervals - 1)); 143 | $(this).css({ 144 | 'position': 'absolute', 145 | 'left': left + '%', 146 | 'width': '1px', 147 | 'height': '100%', 148 | 'background-color': '#222222' 149 | }); 150 | }); 151 | } 152 | 153 | // create labels for slider 154 | display_element.append($('
    ', { 155 | "id": "sliderlabels", 156 | "class": 'sliderlabels', 157 | "css": { 158 | "width": "100%", 159 | "height": "3em", 160 | "margin": "10px 0px 0px 0px", 161 | "padding": "0px", 162 | "display": "block", 163 | "position": "relative" 164 | } 165 | })); 166 | 167 | for (var j = 0; j < trial.labels.length; j++) { 168 | $("#sliderlabels").append('
  • ' + trial.labels[j] + '
  • '); 169 | } 170 | 171 | // position labels to match slider intervals 172 | var slider_width = $("#slider").width(); 173 | var num_items = trial.labels.length; 174 | var item_width = slider_width / num_items; 175 | var spacing_interval = slider_width / (num_items - 1); 176 | 177 | $("#sliderlabels li").each(function(index) { 178 | $(this).css({ 179 | 'display': 'inline-block', 180 | 'width': item_width + 'px', 181 | 'margin': '0px', 182 | 'padding': '0px', 183 | 'text-align': 'center', 184 | 'position': 'absolute', 185 | 'left': (spacing_interval * index) - (item_width / 2) 186 | }); 187 | }); 188 | 189 | // create button 190 | display_element.append($('