/** jspsych-palmer * Josh de Leeuw (October 2013) * * a jspsych plugin for presenting and querying about stimuli modeled after * * Palmer, S. (1977). Hierarchical Structure in Perceptual Representation. Cognitive Psychology, 9, 441. * * and * * Goldstone, R. L., Rogosky, B. J., Pevtzow, R., & Blair, M. (2005). Perceptual and semantic reorganization during category learning. * In H. Cohen & C. Lefebvre (Eds.) Handbook of Categorization in Cognitive Science. (pp. 651-678). Amsterdam: Elsevier. * * NOTE: This plugin requires the Raphaeljs library for manipulating vector graphics (SVG). Download at http://www.raphaeljs.com * * parameters: * configurations: array of arrays. inner most array should be an array of 1s and 0s, where 1s represent the * presence of a line segment, and 0s represent the absence. * editable: set to true if you want the subject to be able to change the configuration by interacting * with the stimulus. (click two circles to toggle the line between them). * show_feedback: set to true to show corrective feedback when trial is editable. * grid_spacing: distance in pixels between the circles. * square_Size: how many circles per row/column. * timing_item: how long to show the stimulus for. (only matters when editable is false) * timing_post_trial: how long to show blank screen after trial. * timing_feedback: how long to show corrective feedback for. * prompt: optional html string to show during stimulus presentation * data: optional data object * */ (function($) { jsPsych.palmer = (function() { var plugin = {}; plugin.create = function(params) { var trials = []; for (var i = 0; i < params.configurations.length; i++) { var trial = { type: "palmer", configurations: params.configurations[i], editable: (typeof params.editable === 'undefined') ? false : params.editable, show_feedback: (typeof params.show_feedback === 'undefined') ? false : params.show_feedback, grid_spacing: params.grid_spacing || 75, square_size: params.square_size || 3, circle_radius: params.circle_radius || 20, timing_item: params.timing_item || 1000, timing_post_trial: (typeof params.timing_post_trial === 'undefined') ? 1000 : params.timing_post_trial, timing_feedback: params.timing_feedback || 1000, prompt: (typeof params.prompt === 'undefined') ? "" : params.prompt, data: (typeof params.data === 'undefined') ? {} : params.data[i] }; trials.push(trial); } return trials; }; plugin.trial = function(display_element, block, trial, part) { // variables to keep track of user interaction var start_circle = -1; var end_circle = -1; var line_started = false; var size = trial.grid_spacing * (trial.square_size + 1); display_element.append($("
" + feedback + "
")); setTimeout(function() { next_trial(); }, trial.timing_feedback); } else { next_trial(); } } function next_trial() { display_element.html(''); // next trial if (trial.timing_post_trial > 0) { setTimeout(function() { block.next(); }, trial.timing_post_trial); } else { block.next(); } } }; // method for drawing palmer stimuli. // returns the string description of svg element containing the stimulus // requires raphaeljs library -> www.raphaeljs.com plugin.generate_stimulus = function(square_size, grid_spacing, circle_radius, configuration) { // create a div to hold the generated svg object var stim_div = $('body').append(''); var size = grid_spacing * (square_size + 1); // create the svg raphael object var paper = Raphael("jspsych-palmer-temp-stim", size, size); // create the circles at the vertices. var circles = []; var node_idx = 0; for (var i = 1; i <= square_size; i++) { for (var j = 1; j <= square_size; j++) { var circle = paper.circle(grid_spacing * j, grid_spacing * i, circle_radius); circle.attr("fill", "#000").attr("stroke-width", "0").attr("stroke", "#000").data("node", node_idx); node_idx++; circles.push(circle); } } // create all possible lines that connect circles var horizontal_lines = []; var vertical_lines = []; var backslash_lines = []; var forwardslash_lines = []; for (var i = 0; i < square_size; i++) { for (var j = 0; j < square_size; j++) { var current_item = (i * square_size) + j; // add horizontal connections if (j < (square_size - 1)) { horizontal_lines.push([current_item, current_item + 1]); } // add vertical connections if (i < (square_size - 1)) { vertical_lines.push([current_item, current_item + square_size]); } // add diagonal backslash connections if (i < (square_size - 1) && j < (square_size - 1)) { backslash_lines.push([current_item, current_item + square_size + 1]); } // add diagonal forwardslash connections if (i < (square_size - 1) && j > 0) { forwardslash_lines.push([current_item, current_item + square_size - 1]); } } } var lines = horizontal_lines.concat(vertical_lines).concat(backslash_lines).concat(forwardslash_lines); // actually draw the lines var lineIsVisible = []; var lineElements = []; for (var i = 0; i < lines.length; i++) { var line = paper.path("M" + circles[lines[i][0]].attr("cx") + " " + circles[lines[i][0]].attr("cy") + "L" + circles[lines[i][1]].attr("cx") + " " + circles[lines[i][1]].attr("cy")).attr("stroke-width", "8").attr("stroke", "#000"); line.hide(); lineElements.push(line); lineIsVisible.push(0); } // define some helper functions to toggle lines on and off // this function turns a line on/off based on the index (the_line) function toggle_line(the_line) { if (the_line > -1) { if (lineIsVisible[the_line] === 0) { lineElements[the_line].show(); lineElements[the_line].toBack(); lineIsVisible[the_line] = 1; } else { lineElements[the_line].hide(); lineElements[the_line].toBack(); lineIsVisible[the_line] = 0; } } } // displays the line wherever there // is a 1 in the array. // showConfiguration(configuration) for (var i = 0; i < configuration.length; i++) { if (configuration[i] == 1) { toggle_line(i); } } var svg = $("#stim_palmer").html(); $('#stim_palmer').remove(); return svg; }; /* * Maybe add this back in later? * public_object.palmer_add_random_connected_element = function(square_size, configuration) { // make sure that configuration is not ALL 1's if ($.inArray(0, configuration) == -1) { return configuration; } // create all possible lines that connect circles var horizontal_lines = []; var vertical_lines = []; var backslash_lines = []; var forwardslash_lines = []; for (var i = 0; i < square_size; i++) { for (var j = 0; j < square_size; j++) { var current_item = (i * square_size) + j; // add horizontal connections if (j < (square_size - 1)) { horizontal_lines.push([current_item, current_item + 1]); } // add vertical connections if (i < (square_size - 1)) { vertical_lines.push([current_item, current_item + square_size]); } // add diagonal backslash connections if (i < (square_size - 1) && j < (square_size - 1)) { backslash_lines.push([current_item, current_item + square_size + 1]); } // add diagonal forwardslash connections if (i < (square_size - 1) && j > 0) { forwardslash_lines.push([current_item, current_item + square_size - 1]); } } } var lines = horizontal_lines.concat(vertical_lines).concat(backslash_lines).concat(forwardslash_lines); function build_possible_lines_list() { // get a list of all the lines that exist in the passed configuration var possible_lines_to_connect_from = []; $.each(configuration, function(i, v) { if (v == 1) { possible_lines_to_connect_from.push(i); } }); // randomly select one of the lines var choice = possible_lines_to_connect_from[Math.floor(Math.random() * possible_lines_to_connect_from.length)]; // randomly select which end of the line to connect to var circles = lines[choice]; // build a list of all possible lines that contain the circles var possible_new_lines = []; for(var i=0;i