", {
+ css: {
+ width: size + "px",
+ height: size + "px"
+ }
+ }));
+
+ var paper = Raphael("raphaelCanvas", size, size);
+
+ // create the circles at the vertices.
+ var circles = [];
+ var node_idx = 0;
+ for (var i = 1; i <= trial.square_size; i++) {
+ for (var j = 1; j <= trial.square_size; j++) {
+ var circle = paper.circle(trial.grid_spacing * j, trial.grid_spacing * i, trial.circle_radius);
+ circle.attr("fill", "#000").attr("stroke-width", "0").attr("stroke", "#000").data("node", node_idx);
+
+ if (trial.editable) {
+ circle.hover(
+
+ function() {
+ this.attr("stroke-width", "2");
+ //this.attr("stroke", "#000");
+ },
+
+ function() {
+ this.attr("stroke-width", "0");
+ //this.attr("stroke", "#fff")
+ }).click(
+
+ function() {
+ if (!line_started) {
+ line_started = true;
+ start_circle = this.data("node");
+ this.attr("fill", "#777").attr("stroke", "#777");
+ }
+ else {
+ end_circle = this.data("node");
+ draw_connection(start_circle, end_circle);
+ }
+ });
+ }
+ node_idx++;
+ circles.push(circle);
+ }
+ }
+
+ function draw_connection(start_circle, end_circle) {
+ var the_line = getLineIndex(start_circle, end_circle);
+ if (the_line > -1) {
+ toggle_line(the_line);
+ }
+ // reset highlighting on circles
+ for (var i = 0; i < circles.length; i++) {
+ circles[i].attr("fill", "#000").attr("stroke", "#000");
+ }
+ // cleanup the variables
+ line_started = false;
+ start_circle = -1;
+ end_circle = -1;
+ }
+
+ // create all possible lines that connect circles
+ var horizontal_lines = [];
+ var vertical_lines = [];
+ var backslash_lines = [];
+ var forwardslash_lines = [];
+
+ for (var i = 0; i < trial.square_size; i++) {
+ for (var j = 0; j < trial.square_size; j++) {
+ var current_item = (i * trial.square_size) + j;
+ // add horizontal connections
+ if (j < (trial.square_size - 1)) {
+ horizontal_lines.push([current_item, current_item + 1]);
+ }
+ // add vertical connections
+ if (i < (trial.square_size - 1)) {
+ vertical_lines.push([current_item, current_item + trial.square_size]);
+ }
+ // add diagonal backslash connections
+ if (i < (trial.square_size - 1) && j < (trial.square_size - 1)) {
+ backslash_lines.push([current_item, current_item + trial.square_size + 1]);
+ }
+ // add diagonal forwardslash connections
+ if (i < (trial.square_size - 1) && j > 0) {
+ forwardslash_lines.push([current_item, current_item + trial.square_size - 1]);
+ }
+ }
+ }
+
+ var lines = horizontal_lines.concat(vertical_lines).concat(backslash_lines).concat(forwardslash_lines);
+
+ // actually draw the lines
+ var lineIsVisible = [];
+ var lineElements = [];
+
+ for (var i = 0; i < lines.length; i++) {
+ var line = paper.path("M" + circles[lines[i][0]].attr("cx") + " " + circles[lines[i][0]].attr("cy") + "L" + circles[lines[i][1]].attr("cx") + " " + circles[lines[i][1]].attr("cy")).attr("stroke-width", "8").attr("stroke", "#000");
+ line.hide();
+ lineElements.push(line);
+ lineIsVisible.push(0);
+ }
+
+ // define some helper functions to toggle lines on and off
+
+ // this function gets the index of a line based on the two circles it connects
+ function getLineIndex(start_circle, end_circle) {
+ var the_line = -1;
+ for (var i = 0; i < lines.length; i++) {
+ if ((start_circle == lines[i][0] && end_circle == lines[i][1]) || (start_circle == lines[i][1] && end_circle == lines[i][0])) {
+ the_line = i;
+ break;
+ }
+ }
+ return the_line;
+ }
+
+ // this function turns a line on/off based on the index (the_line)
+ function toggle_line(the_line) {
+ if (the_line > -1) {
+ if (lineIsVisible[the_line] === 0) {
+ lineElements[the_line].show();
+ lineElements[the_line].toBack();
+ lineIsVisible[the_line] = 1;
+ }
+ else {
+ lineElements[the_line].hide();
+ lineElements[the_line].toBack();
+ lineIsVisible[the_line] = 0;
+ }
+ }
+ }
+
+ // this function takes an array of length = num lines, and displays the line whereever there
+ // is a 1 in the array.
+ function showConfiguration(configuration) {
+ for (var i = 0; i < configuration.length; i++) {
+ if (configuration[i] != lineIsVisible[i]) {
+ toggle_line(i);
+ }
+ }
+ }
+
+ // highlight a line
+ function highlightLine(line) {
+ lineElements[line].attr("stroke", "#f00");
+ }
+
+ // start recording the time
+ var startTime = (new Date()).getTime();
+
+ // what kind of trial are we doing?
+ // if trial.editable is true, then we will let the user interact with the stimulus to create
+ // something, e.g. for a reconstruction probe.
+ // need a way for the user to submit when they are done in that case...
+ if (trial.editable) {
+ display_element.append($('
'));
+ $('#submitButton').click(function() {
+ save_data();
+ });
+ }
+
+ // if trial.editable is false, then we are just showing a pre-determined configuration.
+ // for now, the only option will be to display for a fixed amount of time.
+ // future ideas: allow for key response, to enable things like n-back, same/different, etc..
+ if (!trial.editable) {
+ showConfiguration(trial.configurations);
+
+ setTimeout(function() {
+ save_data();
+ }, trial.timing_item);
+ }
+
+ if (trial.prompt !== "") {
+ display_element.append($('
'));
+ $("#palmer_prompt").html(trial.prompt);
+ }
+
+ function arrayDifferences(arr1, arr2) {
+ var n_diff = 0;
+ for (var i = 0; i < arr1.length; i++) {
+ if (arr1[i] != arr2[i]) {
+ n_diff++;
+ }
+ }
+ return n_diff;
+ }
+
+ // save data
+ function save_data() {
+
+ // measure RT
+ var endTime = (new Date()).getTime();
+ var response_time = endTime - startTime;
+
+ // check if configuration is correct
+ // this is meaningless for trials where the user can't edit
+ var n_diff = arrayDifferences(trial.configurations, lineIsVisible);
+ var correct = (n_diff === 0);
+
+ block.writeData($.extend({}, {
+ "trial_type": "palmer",
+ "trial_index": block.trial_idx,
+ "configuration": JSON.stringify(lineIsVisible),
+ "target_configuration": JSON.stringify(trial.configurations),
+ "rt": response_time,
+ "correct": correct,
+ "num_wrong": n_diff,
+ }, trial.data));
+
+ if (trial.editable && trial.show_feedback) {
+ // hide the button
+ $('#submitButton').hide();
+ $('#palmer_prompt').hide();
+
+ showConfiguration(trial.configurations);
+ var feedback = "";
+ if (correct) {
+ feedback = "Correct!";
+ }
+ else {
+ if (n_diff > 1) {
+ feedback = "You missed " + n_diff + " lines. The correct symbol is shown above.";
+ }
+ else {
+ feedback = "You missed 1 line. The correct symbol is shown above.";
+ }
+ }
+ display_element.append($.parseHTML("
" + feedback + "
"));
+
+ setTimeout(function() {
+ next_trial();
+ }, trial.timing_feedback);
+
+ }
+ else {
+ next_trial();
+ }
+ }
+
+ function next_trial() {
+
+ display_element.html('');
+
+ // next trial
+ setTimeout(function() {
+ block.next();
+ }, trial.timing_post_trial);
+
+ }
+
+
+ };
+
+ return plugin;
+ })();
+})(jQuery);
\ No newline at end of file
diff --git a/examples/tutorial/scripts/plugins/jspsych-same-different.js b/examples/tutorial/scripts/plugins/jspsych-same-different.js
new file mode 100644
index 00000000..7156bb7b
--- /dev/null
+++ b/examples/tutorial/scripts/plugins/jspsych-same-different.js
@@ -0,0 +1,150 @@
+/**
+ * jspsych-same-different
+ * Josh de Leeuw (Updated Oct 2013)
+ *
+ * plugin for showing two stimuli sequentially and getting a same / different judgment
+ *
+ * parameters:
+ * stimuli: array of arrays. inner most array should have two elements, corresponding to the two items that will be shown.
+ * items can be image paths or HTML strings. each inner array is a trial.
+ * answer: array of strings. acceptable values are "same" and "different". represents the correct answer for each trial.
+ * same_key: which key to press to indicate a 'same' response.
+ * different_key: which key to press to indicate a 'different' response.
+ * timing_first_stim: how long to show the first stimulus
+ * timing_second_stim: how long to show the second stim. can be -1, which means to show until a response is given.
+ * timing_gap: how long to show a blank screen in between the two stimuli.
+ * timing_post_trial: how long to show a blank screen after the trial ends.
+ * is_html: must set to true if the stimulus is HTML code.
+ * prompt: HTML string to show when the subject is viewing the stimulus and making a categorization decision.
+ * data: the optional data object
+ *
+ *
+ */
+
+ jsPsych['same-different'] = (function() {
+
+ var plugin = {};
+
+ plugin.create = function(params) {
+ var trials = new Array(params.stimuli.length);
+ for (var i = 0; i < trials.length; i++) {
+ trials[i] = {};
+ trials[i].type = "same-different";
+ trials[i].a_path = params.stimuli[i][0];
+ trials[i].b_path = params.stimuli[i][1];
+ trials[i].answer = params.answer[i];
+ trials[i].same_key = params.same_key || 81; // default is 'q'
+ trials[i].different_key = params.different_key || 80; // default is 'p'
+ // timing parameters
+ trials[i].timing_first_stim = params.timing_first_stim || 1000;
+ trials[i].timing_second_stim = params.timing_second_stim || 1000; // if -1, then second stim is shown until response.
+ trials[i].timing_gap = params.timing_gap || 500;
+ trials[i].timing_post_trial = params.timing_post_trial || 1000;
+ // optional parameters
+ trials[i].is_html = (typeof params.is_html === 'undefined') ? false : true;
+ trials[i].prompt = (typeof params.prompt === 'undefined') ? "" : params.prompt;
+ trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
+ }
+ return trials;
+ };
+
+ var sd_trial_complete = false;
+
+ plugin.trial = function(display_element, block, trial, part) {
+ switch (part) {
+ case 1:
+ sd_trial_complete = false;
+ // show image
+ if (!trial.is_html) {
+ display_element.append($('
![]()
', {
+ src: trial.a_path,
+ "class": 'sd'
+ }));
+ }
+ else {
+ display_element.append($('
', {
+ html: trial.a_path,
+ "class": 'sd'
+ }));
+ }
+ setTimeout(function() {
+ plugin.trial(display_element, block, trial, part + 1);
+ }, trial.timing_first_stim);
+ break;
+ case 2:
+ $('.sd').remove();
+ setTimeout(function() {
+ plugin.trial(display_element, block, trial, part + 1);
+ }, trial.timing_gap);
+ break;
+ case 3:
+ if (!trial.is_html) {
+ display_element.append($('
![]()
', {
+ src: trial.a_path,
+ "class": 'sd',
+ id: 'jspsych_sd_second_image'
+ }));
+ }
+ else {
+ display_element.append($('
', {
+ html: trial.a_path,
+ "class": 'sd',
+ id: 'jspsych_sd_second_image'
+ }));
+ }
+
+ if (trial.timing_second_stim > 0) {
+ setTimeout(function() {
+ if (!sd_trial_complete) {
+ $("#jspsych_sd_second_image").css('visibility', 'hidden');
+ }
+ }, trial.timing_second_stim);
+ }
+
+ var startTime = (new Date()).getTime();
+
+ var resp_func = function(e) {
+ var flag = false;
+ var correct = false;
+ if (e.which == trial.same_key) {
+ flag = true;
+ if (trial.answer == "same") {
+ correct = true;
+ }
+ }
+ else if (e.which == trial.different_key) {
+ flag = true;
+ if (trial.answer == "different") {
+ correct = true;
+ }
+ }
+ if (flag) {
+ var endTime = (new Date()).getTime();
+ var rt = (endTime - startTime);
+
+ var trial_data = {
+ "trial_type": "same-different",
+ "trial_index": block.trial_idx,
+ "rt": rt,
+ "correct": correct,
+ "a_path": trial.a_path,
+ "b_path": trial.b_path,
+ "key_press": e.which
+ };
+ block.data[block.trial_idx] = $.extend({}, trial_data, trial.data);
+ $(document).unbind('keyup', resp_func);
+
+ display_element.html('');
+ setTimeout(function() {
+ block.next();
+ }, trial.timing_post_trial);
+ }
+ };
+ $(document).keyup(resp_func);
+ break;
+ }
+ };
+
+ return plugin;
+ })();
+})(jQuery);
\ No newline at end of file
diff --git a/examples/tutorial/scripts/plugins/jspsych-similarity.js b/examples/tutorial/scripts/plugins/jspsych-similarity.js
new file mode 100644
index 00000000..40c55241
--- /dev/null
+++ b/examples/tutorial/scripts/plugins/jspsych-similarity.js
@@ -0,0 +1,170 @@
+/**
+ * jspsych-similarity.js
+ * Josh de Leeuw
+ *
+ * This plugin create a trial where two images are shown sequentially, and the subject rates their similarity using a slider controlled with the mouse.
+ *
+ * parameters:
+ * stimuli: array of arrays. inner arrays are two stimuli. stimuli can be image paths or html strings. each inner array is one trial.
+ * label_low: label to display at the left end of the similarity slider scale.
+ * label_high: label to display at the right end of the similiarity slider scale.
+ * timing_first_stim: how long to show the first stimulus.
+ * timing_second_stim: how long to show the second stimulus. can be -1 to show until a response is given.
+ * timing_image_gap: how long to show a blank screen between the two stimuli.
+ * timing_post_trial: how long to show a blank screen after the trial is over.
+ * is_html: must set to true when using HTML strings as the stimuli.
+ * prompt: optional HTML string to display with the stimulus.
+ * data: the optional data object
+ *
+ */
+
+(function($) {
+ jsPsych.similarity = (function() {
+
+ var plugin = {};
+
+ plugin.create = function(params) {
+ var trials = new Array(params.stimuli.length);
+ for (var i = 0; i < trials.length; i++) {
+ trials[i] = {};
+ trials[i].type = "similarity";
+ trials[i].a_path = params.stimuli[i][0];
+ trials[i].b_path = params.stimuli[i][1];
+
+ trials[i].label_low = params.label_low || "Not at all similar";
+ trials[i].label_high = params.label_high || "Identical";
+
+ trials[i].timing_first_stim = params.timing_first_stim || 1000; // default 1000ms
+ trials[i].timing_second_stim = params.timing_second_stim || -1; // -1 = inf time; positive numbers = msec to display second image.
+ trials[i].timing_image_gap = params.timing_image_gap || 1000; // default 1000ms
+ trials[i].timing_post_trial = params.timing_post_trial || 1000; // default 1000ms
+
+ trials[i].is_html = (typeof params.is_html === 'undefined') ? false : params.is_html;
+ trials[i].prompt = (typeof params.prompt === 'undefined') ? '' : params.prompt;
+ trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
+ }
+ return trials;
+ };
+
+ var sim_trial_complete = false;
+
+ plugin.trial = function(display_element, block, trial, part) {
+ switch (part) {
+ case 1:
+ sim_trial_complete = false;
+ // show the images
+ if (!trial.is_html) {
+ display_element.append($('
![]()
', {
+ "src": trial.a_path,
+ "class": 'sim'
+ }));
+ }
+ else {
+ display_element.append($('
', {
+ "html": trial.a_path,
+ "class": 'sim'
+ }));
+ }
+
+ setTimeout(function() {
+ plugin.trial(display_element, block, trial, part + 1);
+ }, trial.timing_first_stim);
+ break;
+
+ case 2:
+
+ $('.sim').remove();
+
+ setTimeout(function() {
+ plugin.trial(display_element, block, trial, part + 1);
+ }, trial.timing_image_gap);
+ break;
+ case 3:
+
+ if (!trial.is_html) {
+ display_element.append($('
![]()
', {
+ "src": trial.b_path,
+ "class": 'sim',
+ "id": 'jspsych_sim_second_image'
+ }));
+ }
+ else {
+ display_element.append($('
', {
+ "html": trial.b_path,
+ "class": 'sim',
+ "id": 'jspsych_sim_second_image'
+ }));
+ }
+
+ if (trial.timing_second_stim > 0) {
+ setTimeout(function() {
+ if (!sim_trial_complete) {
+ $("#jspsych_sim_second_image").css('visibility', 'hidden');
+ }
+ }, trial.timing_second_stim);
+ }
+
+ // create slider
+ display_element.append($('
', {
+ "id": 'slider',
+ "class": 'sim'
+ }));
+ $("#slider").slider({
+ value: 50,
+ min: 0,
+ max: 100,
+ step: 1,
+ });
+
+
+ // create labels for slider
+ display_element.append($('
', {
+ "id": 'slider_labels',
+ "class": 'sim'
+ }));
+
+ $('#slider_labels').append($('
' + trial.label_low + '
'));
+ $('#slider_labels').append($('
' + trial.label_high + '
'));
+
+ // create button
+ display_element.append($('