/** * jspsych.js * Josh de Leeuw * * documentation: docs.jspsych.org * **/ (function($) { jsPsych = (function() { var core = {}; // // private variables // // options var opts = {}; // exp structure var root_chunk; // flow control var curr_chunk = 0; var global_trial_index = 0; var current_trial = {}; // target DOM element var DOM_target; // time that the experiment began var exp_start_time; // // public methods // core.init = function(options) { // reset variables root_chunk = {}; opts = {}; curr_chunk = 0; // check if there is a body element on the page var default_display_element = $('body'); if (default_display_element.length === 0) { $(document.documentElement).append($('
')); default_display_element = $('body'); } var defaults = { 'display_element': default_display_element, 'on_finish': function(data) { return undefined; }, 'on_trial_start': function() { return undefined; }, 'on_trial_finish': function() { return undefined; }, 'on_data_update': function(data) { return undefined; }, 'show_progress_bar': false, 'max_load_time': 30000, 'skip_load_check': false, 'fullscreen': false }; // override default options if user specifies an option opts = $.extend({}, defaults, options); // set target DOM_target = opts.display_element; // add CSS class to DOM_target DOM_target.addClass('jspsych-display-element'); // create experiment structure root_chunk = parseExpStructure(opts.experiment_structure); // wait for everything to load if (opts.skip_load_check) { startExperiment(opts.fullscreen); } else { allLoaded(startExperiment, opts.max_load_time, opts.fullscreen); } }; core.progress = function() { var obj = { "total_trials": root_chunk.length(), "current_trial_global": global_trial_index, "current_trial_local": root_chunk.currentTrialLocalIndex(), "total_chunks": root_chunk.timeline.length, "current_chunk": root_chunk.currentTimelineLocation }; return obj; }; core.startTime = function() { return exp_start_time; }; core.totalTime = function() { return (new Date()).getTime() - exp_start_time.getTime(); }; core.preloadImages = function(images, callback_complete, callback_load) { // flatten the images array images = flatten(images); var n_loaded = 0; var loadfn = (typeof callback_load === 'undefined') ? function() {} : callback_load; var finishfn = (typeof callback_complete === 'undefined') ? function() {} : callback_complete; for (var i = 0; i < images.length; i++) { var img = new Image(); img.onload = function() { n_loaded++; loadfn(n_loaded); if (n_loaded == images.length) { finishfn(); } }; img.src = images[i]; } }; core.getDisplayElement = function() { return DOM_target; }; core.finishTrial = function() { // logic to advance to next trial? // handle callback at plugin level if (typeof current_trial.on_finish === 'function') { var trial_data = jsPsych.data.getDataByTrialIndex(global_trial_index); current_trial.on_finish(trial_data); } // handle callback at whole-experiment level opts.on_trial_finish(); if (current_trial.timing_post_trial > 0) { setTimeout(next_trial, current_trial.timing_post_trial); } else { next_trial(); } function next_trial() { global_trial_index++; // advance chunk root_chunk.advance(); // update progress bar if shown if (opts.show_progress_bar === true) { updateProgressBar(); } // check if experiment is over if (root_chunk.isComplete()) { finishExperiment(); return; } doTrial(root_chunk.next()); } }; core.endExperiment = function() { root_chunk.end(); } core.endCurrentChunk = function() { root_chunk.endCurrentChunk(); } core.currentTrial = function() { return current_trial; }; core.initSettings = function() { return opts; }; core.currentChunkID = function() { return root_chunk.activeChunkID(); }; function allLoaded(callback, max_wait, fullscreen) { var refresh_rate = 1000; var max_wait = max_wait || 30000; var start = (new Date()).getTime(); var interval = setInterval(function() { if (jsPsych.pluginAPI.audioLoaded()) { clearInterval(interval); callback(fullscreen); } else if ((new Date()).getTime() - max_wait > start) { console.error('Experiment failed to load all resouces in time alloted'); } }, refresh_rate); } function parseExpStructure(experiment_structure) { if (!Array.isArray(experiment_structure)) { throw new Error("Invalid experiment structure. Experiment structure must be an array"); } return createExperimentChunk({ chunk_type: 'root', timeline: experiment_structure }); } function createExperimentChunk(chunk_definition, parent_chunk, relative_id) { var chunk = {}; chunk.timeline = parseChunkDefinition(chunk_definition.timeline); chunk.parentChunk = parent_chunk; chunk.relID = relative_id; chunk.type = chunk_definition.chunk_type; // root, linear, while, if chunk.currentTimelineLocation = 0; // this is the current trial since the last time the chunk was reset chunk.currentTrialInTimeline = 0; // this is the current trial since the chunk started (incl. resets) chunk.currentTrialInChunk = 0; // flag that indicates the chunk is done; overrides loops and ifs chunk.done = false; chunk.iteration = 0; chunk.length = function() { // this will recursively get the number of trials on this chunk's timeline var n = 0; for (var i = 0; i < this.timeline.length; i++) { n += this.timeline[i].length; } return n; }; chunk.activeChunkID = function() { if (this.timeline[this.currentTimelineLocation].type === 'block') { return this.chunkID(); } else { return this.timeline[this.currentTimelineLocation].activeChunkID(); } }; chunk.endCurrentChunk = function() { if (this.timeline[this.currentTimelineLocation].type === 'block') { this.end(); } else { this.timeline[this.currentTimelineLocation].endCurrentChunk(); } } chunk.chunkID = function() { if (typeof this.parentChunk === 'undefined') { return 0 + "-" + this.iteration; } else { return this.parentChunk.chunkID() + "." + this.relID + "-" + this.iteration; } }; chunk.next = function() { // return the next trial in the block to be run // if chunks might need their conditional_function evaluated if (this.type == 'if' && this.currentTimelineLocation == 0) { if (!chunk_definition.conditional_function()) { this.end(); this.parentChunk.advance(); return this.parentChunk.next(); } } return this.timeline[this.currentTimelineLocation].next(); }; chunk.end = function() { // end the chunk no matter what chunk.done = true; } chunk.advance = function() { // increment the current trial in the chunk this.timeline[this.currentTimelineLocation].advance(); while (this.currentTimelineLocation < this.timeline.length && this.timeline[this.currentTimelineLocation].isComplete()) { this.currentTimelineLocation++; } this.currentTrialInTimeline++; this.currentTrialInChunk++; }; chunk.isComplete = function() { // return true if the chunk is done running trials // return false otherwise // if done flag is set, then we're done no matter what if (this.done) { return true; } // linear chunks just go through the timeline in order and are // done when each trial has been completed once // the root chunk is a special case of the linear chunk if (this.type == 'linear' || this.type == 'root' || this.type == 'if') { if (this.currentTimelineLocation >= this.timeline.length) { return true; } else { return false; } } // while chunks play the block again as long as the continue_function // returns true else if (this.type == 'while') { if (this.currentTimelineLocation >= this.timeline.length) { if (chunk_definition.continue_function(this.generatedData())) { this.reset(); return false; } else { return true; } } else { return false; } } /*else if(this.type == 'if'){ if(this.currentTimelineLocation >= this.timeline.length){ return true; } if(this.currentTimelineLocation == 0){ if(chunk_definition.conditional_function()){ return false; } else { return true; } } else { return false; } }*/ }; chunk.currentTrialLocalIndex = function() { if (this.currentTimelineLocation >= this.timeline.length) { return -1; } if (this.timeline[this.currentTimelineLocation].type == 'block') { return this.timeline[this.currentTimelineLocation].trial_idx; } else { return this.timeline[this.currentTimelineLocation].currentTrialLocalIndex(); } }; chunk.generatedData = function() { // return an array containing all of the data generated by this chunk for this iteration var d = jsPsych.data.getTrialsFromChunk(this.chunkID()); return d; }; chunk.reset = function() { this.currentTimelineLocation = 0; this.currentTrialInTimeline = 0; this.done = false; this.iteration++; for (var i = 0; i < this.timeline.length; i++) { this.timeline[i].reset(); } }; function parseChunkDefinition(chunk_timeline) { var timeline = []; for (var i = 0; i < chunk_timeline.length; i++) { var ct = chunk_timeline[i].chunk_type; if (typeof ct !== 'undefined') { if ($.inArray(ct, ["linear", "while", "if"]) > -1) { timeline.push(createExperimentChunk(chunk_timeline[i], chunk, i)); } else { throw new Error('Invalid experiment structure definition. Element of the experiment_structure array has an invalid chunk_type property'); } } else { // create a terminal block ... // check to make sure plugin is loaded var plugin_name = chunk_timeline[i].type; if (typeof chunk_timeline[i].type === 'undefined') { throw new Error("Invalid experiment structure definition. One or more trials is missing a 'type' parameter."); } if (typeof jsPsych[plugin_name] === 'undefined') { throw new Error("Failed attempt to create trials using plugin type " + plugin_name + ". Is the plugin loaded?"); } var trials = jsPsych[plugin_name].create(chunk_timeline[i]); // add chunk level data to all trials if (typeof chunk_definition.data !== 'undefined') { for (t in trials) { trials[t].data = chunk_definition.data; } } // add block/trial level data to all trials trials = addParamToTrialsArr(trials, chunk_timeline[i].data, 'data', undefined, true); // add options that are generic to all plugins trials = addGenericTrialOptions(trials, chunk_timeline[i]); // setting default values for repetitions and randomize_order var randomize_order = (typeof chunk_timeline[i].randomize_order === 'undefined') ? false : chunk_timeline[i].randomize_order; var repetitions = (typeof chunk_timeline[i].repetitions === 'undefined') ? 1 : chunk_timeline[i].repetitions; for (var j = 0; j < repetitions; j++) { timeline.push(createBlock(trials, randomize_order)); } } } return timeline; } return chunk; } function createBlock(trial_list, randomize_order) { var block = { trial_idx: 0, trials: trial_list, type: 'block', randomize_order: randomize_order, next: function() { // stuff that happens when the block is running from the start if (this.trial_idx === 0) { if (this.randomize_order) { this.trials = jsPsych.randomization.repeat(this.trials, 1, false); } } var curr_trial = this.trials[this.trial_idx]; return curr_trial; }, isComplete: function() { if (this.trial_idx >= this.trials.length) { return true; } else { return false; } }, advance: function() { this.trial_idx++; }, reset: function() { this.trial_idx = 0; }, length: trial_list.length }; return block; } function startExperiment(fullscreen) { // fullscreen setup if (fullscreen) { // check if keys are allowed in fullscreen mode var keyboardNotAllowed = typeof Element !== 'undefined' && 'ALLOW_KEYBOARD_INPUT' in Element; if (keyboardNotAllowed) { go(); } else { DOM_target.append('The experiment will launch in fullscreen mode when you click the button below.