diff --git a/contributors.md b/contributors.md index fe53d59517..57d6719bef 100644 --- a/contributors.md +++ b/contributors.md @@ -38,6 +38,7 @@ The following people have contributed to the development of jsPsych by writing c * Vijay Marupudi - https://github.com/vijaymarupudi * Adrian Oesch - https://github.com/adrianoesch * Benjamin Ooghe-Tabanou - https://github.com/boogheta +* Keshav Pabbi - https://github.com/keshavpabbi * Nikolay B Petrov - https://github.com/nikbpetrov * Dillon Plunkett - https://github.com/dillonplunkett * Junyan Qi - https://github.com/GavinQ1 diff --git a/docs/plugins/jspsych-visual-search-circle.md b/docs/plugins/jspsych-visual-search.md similarity index 76% rename from docs/plugins/jspsych-visual-search-circle.md rename to docs/plugins/jspsych-visual-search.md index bb71ee6925..6bb85d47a2 100644 --- a/docs/plugins/jspsych-visual-search-circle.md +++ b/docs/plugins/jspsych-visual-search.md @@ -1,8 +1,6 @@ -# jspsych-visual-search-circle plugin +# jspsych-visual-search plugin -This plugin presents a customizable visual-search task modelled after [Wang, Cavanagh, & Green (1994)](http://dx.doi.org/10.3758/BF03206946). The subject indicates whether or not a target is present among a set of distractors. The stimuli are displayed in a circle, evenly-spaced, equidistant from a fixation point. Here is an example using normal and backward Ns: - -![Sample Visual Search Stimulus](/img/visual_search_example.jpg) +This plugin presents a customizable visual-search task modelled after Treisman and Gelade (1980). The subject indicates whether or not a target is present among a set of distractors. The stimuli can be displayed in a circle or grid format. The stimuli can also be jittered as a ratio of image size ## Parameters @@ -17,11 +15,13 @@ In addition to the [parameters available in all plugins](/overview/plugins#param | fixation_image | string | *undefined* | Path to image file that is a fixation target. | | target_size | array | `[50, 50]` | Two element array indicating the height and width of the search array element images. | | fixation_size | array | `[16, 16]` | Two element array indicating the height and width of the fixation image. | -| circle_diameter | numeric | 250 | The diameter of the search array circle in pixels. | +| circle_diameter | numeric | 500 | The diameter of the search array circle in pixels. | | target_present_key | string | 'j' | The key to press if the target is present in the search array. | | target_absent_key | string | 'f' | The key to press if the target is not present in the search array. | | trial_duration | numeric | null | The maximum amount of time the subject is allowed to search before the trial will continue. A value of null will allow the subject to search indefinitely. | | fixation_duration | numeric | 1000 | How long to show the fixation image for before the search array (in milliseconds). | +| usegrid | boolean | false | Are we using a grid for the visual search task? | +| jitter_ratio | numeric | 0.0 | The distance to jitter the image as ratio of image size (average of x and y). | ## Data Generated @@ -36,17 +36,32 @@ In addition to the [default data collected by all plugins](/overview/plugins#dat | target_present | boolean | True if the target is present in the search array | | locations | array | Array where each element is the pixel value of the center of an image in the search array. If the target is present, then the first element will represent the location of the target. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. | -## Example +## Examples + +#### Search for the green T in the grid, no jitter + +```javascript +var trial_1 = { + type: 'visual-search', + target: 'img/greenT.png', + foil: 'img/greenL.png', + fixation_image: 'img/fixation.png', + target_present: true, + set_size: 4 + usegrid: true +} +``` -#### Search for the backward N +#### Search for the green T in the circle, jitter ```javascript var trial_1 = { - type: 'visual-search-circle', - target: 'img/backwardN.gif', - foil: 'img/normalN.gif', - fixation_image: 'img/fixation.gif', + type: 'visual-search', + target: 'img/greenT.png', + foil: 'img/greenL.png', + fixation_image: 'img/fixation.png', target_present: true, set_size: 4 + jitter_ratio: 1.0 } ``` diff --git a/docs/plugins/list-of-plugins.md b/docs/plugins/list-of-plugins.md index 4259eeedd6..3e8e47969b 100644 --- a/docs/plugins/list-of-plugins.md +++ b/docs/plugins/list-of-plugins.md @@ -46,7 +46,7 @@ Plugin | Description [jspsych‑video‑keyboard‑response](/plugins/jspsych-video-keyboard-response) | Displays a video file with many options for customizing playback. Subject responds to the video by pressing a key. [jspsych‑video‑slider‑response](/plugins/jspsych-video-slider-response) | Displays a video file with many options for customizing playback. Subject responds to the video by moving a slider. [jspsych‑virtual‑chinrest](/plugins/jspsych-virtual-chinrest) | An implementation of the "virutal chinrest" procedure developed by [Li, Joo, Yeatman, and Reinecke (2020)](https://doi.org/10.1038/s41598-019-57204-1). Calibrates the monitor to display items at a known physical size by having participants scale an image to be the same size as a physical credit card. Then uses a blind spot task to estimate the distance between the participant and the display. -[jspsych‑visual‑search‑circle](/plugins/jspsych-visual-search-circle) | A customizable visual-search task modelled after [Wang, Cavanagh, & Green (1994)](http://dx.doi.org/10.3758/BF03206946). The subject indicates whether or not a target is present among a set of distractors. The stimuli are displayed in a circle, evenly-spaced, equidistant from a fixation point. +[jspsych‑visual‑search‑](/plugins/jspsych-visual-search) | This plugin presents a customizable visual-search task modelled after [Treisman and Gelade (1980)](https://doi.org/10.1016/0010-0285(80)90005-5). The subject indicates whether or not a target is present among a set of distractors. The stimuli can be displayed in a circle or grid format. The stimuli can also be jittered as a ratio of image size. [jspsych‑vsl‑animate‑occlusion](/plugins/jspsych-vsl-animate-occlusion) | A visual statistical learning paradigm based on [Fiser & Aslin (2002)](http://dx.doi.org/10.1037//0278-7393.28.3.458). A sequence of stimuli are shown in an oscillatory motion. An occluding rectangle is in the center of the display, and the stimuli change when they are behind the rectangle. [jspsych‑vsl‑grid‑scene](/plugins/jspsych-vsl-grid-scene) | A visual statistical learning paradigm based on [Fiser & Aslin (2001)](http://dx.doi.org/10.1111/1467-9280.00392). A scene made up of individual stimuli arranged in a grid is shown. This plugin can also generate the HTML code to render the stimuli for use in other plugins. [jspsych‑webgazer‑calibrate](/plugins/jspsych-webgazer-calibrate) | Calibrates the WebGazer extension for eye tracking. diff --git a/examples/jspsych-visual-search-circle.html b/examples/jspsych-visual-search.html similarity index 66% rename from examples/jspsych-visual-search-circle.html rename to examples/jspsych-visual-search.html index b5af50e210..af51911fef 100644 --- a/examples/jspsych-visual-search-circle.html +++ b/examples/jspsych-visual-search.html @@ -6,7 +6,7 @@ - + @@ -44,17 +44,32 @@ set_size: 3 } + var trial_5 = { + usegrid: true, + target_present: false, + foil: ['img/1.gif', 'img/2.gif', 'img/3.gif'], // example of using multiple foils. + set_size: 3 + } + + var trials_circle = { + type: 'visual-search', + target: 'img/backwardN.gif', + foil: 'img/normalN.gif', + fixation_image: 'img/fixation.gif', + timeline: [trial_1, trial_2, trial_3, trial_4, trial_5] + }; - var trials = { - type: 'visual-search-circle', + var trials_grid = { + usegrid: true, + type: 'visual-search', target: 'img/backwardN.gif', foil: 'img/normalN.gif', fixation_image: 'img/fixation.gif', - timeline: [trial_1, trial_2, trial_3, trial_4] + timeline: [trial_1, trial_2, trial_3, trial_4, trial_5] }; jsPsych.init({ - timeline: [preload_images, intro, trials], + timeline: [preload_images, intro, trials_circle, trials_grid], on_finish: function() { jsPsych.data.displayData(); } diff --git a/plugins/jspsych-visual-search-circle.js b/plugins/jspsych-visual-search.js similarity index 66% rename from plugins/jspsych-visual-search-circle.js rename to plugins/jspsych-visual-search.js index 85cf5d18b9..7d7d963d06 100644 --- a/plugins/jspsych-visual-search-circle.js +++ b/plugins/jspsych-visual-search.js @@ -1,6 +1,6 @@ /** * - * jspsych-visual-search-circle + * jspsych-visual-search * Josh de Leeuw * * display a set of objects, with or without a target, equidistant from fixation @@ -12,18 +12,30 @@ * **/ -jsPsych.plugins["visual-search-circle"] = (function() { +jsPsych.plugins["visual-search"] = (function() { var plugin = {}; - jsPsych.pluginAPI.registerPreload('visual-search-circle', 'target', 'image'); - jsPsych.pluginAPI.registerPreload('visual-search-circle', 'foil', 'image'); - jsPsych.pluginAPI.registerPreload('visual-search-circle', 'fixation_image', 'image'); + jsPsych.pluginAPI.registerPreload('visual-search', 'target', 'image'); + jsPsych.pluginAPI.registerPreload('visual-search', 'foil', 'image'); + jsPsych.pluginAPI.registerPreload('visual-search', 'fixation_image', 'image'); plugin.info = { - name: 'visual-search-circle', + name: 'visual-search', description: '', parameters: { + usegrid: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Use grid', + default: false, + description: 'Place items on a grid?' + }, + jitter_ratio: { + type: jsPsych.plugins.parameterType.FLOAT, + pretty_name: 'jitter', + default: 0.0, + description: 'The distance to jitter the image as ratio of image size (average of x and y).' + }, target: { type: jsPsych.plugins.parameterType.IMAGE, pretty_name: 'Target', @@ -71,7 +83,7 @@ jsPsych.plugins["visual-search-circle"] = (function() { circle_diameter: { type: jsPsych.plugins.parameterType.INT, pretty_name: 'Circle diameter', - default: 250, + default: 500, description: 'The diameter of the search array circle in pixels.' }, target_present_key: { @@ -111,26 +123,67 @@ jsPsych.plugins["visual-search-circle"] = (function() { // stimuli width, height var stimh = trial.target_size[0]; var stimw = trial.target_size[1]; + const jitter = ((stimh + stimw) / 2) * trial.jitter_ratio var hstimh = stimh / 2; var hstimw = stimw / 2; // fixation location var fix_loc = [Math.floor(paper_size / 2 - trial.fixation_size[0] / 2), Math.floor(paper_size / 2 - trial.fixation_size[1] / 2)]; + + var possible_display_locs = trial.set_size; + + let display_locs = []; + + if (trial.usegrid) { + + const num_pos = Math.ceil(Math.sqrt(possible_display_locs)) + //square root the number of possible display locations + + const step = 2 / (num_pos - 1) + //distance between each location + console.log(step, num_pos) + + let full_locs = [] + for (let x = -1; x <= 1; x+=step) { + //x is between -1 and 1, increase by value of step each time + for (let y = -1; y <= 1; y+=step) { + //y is between -1 and 1, increase by value of step each time + full_locs.push([ + Math.floor(paper_size / 2 + (x * radi) - hstimw), + //x coord + Math.floor(paper_size / 2 + (y * radi) - hstimh) + //y coord + ]); + } + } + + full_locs = shuffle(full_locs) + display_locs = full_locs.slice(0, possible_display_locs); - // possible stimulus locations on the circle - var display_locs = []; - var possible_display_locs = trial.set_size; - var random_offset = Math.floor(Math.random() * 360); - for (var i = 0; i < possible_display_locs; i++) { - display_locs.push([ - Math.floor(paper_size / 2 + (cosd(random_offset + (i * (360 / possible_display_locs))) * radi) - hstimw), - Math.floor(paper_size / 2 - (sind(random_offset + (i * (360 / possible_display_locs))) * radi) - hstimh) - ]); + for (let i=0; i < display_locs.length; i++) { + let random_angle = Math.floor(Math.random() * 360); + let rand_x = Math.floor(cosd(random_angle) * jitter) + let rand_y = Math.floor(sind(random_angle) * jitter) + display_locs[i][0] += rand_x + display_locs[i][1] += rand_y + } + + } else { + + // possible stimulus locations on the circle + var random_offset = Math.floor(Math.random() * 360); + for (var i = 0; i < possible_display_locs; i++) { + let vec_jitter = Math.sign(Math.random() * 2 - 1) * jitter + display_locs.push([ + Math.floor(paper_size / 2 + (cosd(random_offset + (i * (360 / possible_display_locs))) * (radi + vec_jitter)) - hstimw), + Math.floor(paper_size / 2 - (sind(random_offset + (i * (360 / possible_display_locs))) * (radi + vec_jitter)) - hstimh) + ]); + } } // get target to draw on - display_element.innerHTML += '
'; - var paper = display_element.querySelector("#jspsych-visual-search-circle-container"); + display_element.innerHTML += '
'; + var paper = display_element.querySelector("#jspsych-visual-search-container"); // check distractors - array? if(!Array.isArray(trial.foil)){ @@ -152,6 +205,7 @@ jsPsych.plugins["visual-search-circle"] = (function() { jsPsych.pluginAPI.setTimeout(function() { // after wait is over show_search_array(); + paper.innerHTML += ""; }, trial.fixation_duration); } @@ -255,5 +309,24 @@ jsPsych.plugins["visual-search-circle"] = (function() { return Math.sin(num / 180 * Math.PI); } + // shuffle any input array + function shuffle(array) { + // define three variables + let cur_idx = array.length, tmp_val, rand_idx; + + // While there remain elements to shuffle... + while (0 !== cur_idx) { + // Pick a remaining element... + rand_idx = Math.floor(Math.random() * cur_idx); + cur_idx -= 1; + + // And swap it with the current element. + tmp_val = array[cur_idx]; + array[cur_idx] = array[rand_idx]; + array[rand_idx] = tmp_val; + } + return array; + } + return plugin; })(); diff --git a/tests/plugins/plugin-visual-search-circle.test.js b/tests/plugins/plugin-visual-search-circle.test.js deleted file mode 100644 index 9f3ac0624d..0000000000 --- a/tests/plugins/plugin-visual-search-circle.test.js +++ /dev/null @@ -1,16 +0,0 @@ -const root = '../../'; - -jest.useFakeTimers(); - -describe('visual-search-circle plugin', function(){ - - beforeEach(function(){ - require(root + 'jspsych.js'); - require(root + 'plugins/jspsych-visual-search-circle.js'); - }); - - test('loads correctly', function(){ - expect(typeof window.jsPsych.plugins['visual-search-circle']).not.toBe('undefined'); - }); - -}); diff --git a/tests/plugins/plugin-visual-search.test.js b/tests/plugins/plugin-visual-search.test.js new file mode 100644 index 0000000000..9ebc8ff66d --- /dev/null +++ b/tests/plugins/plugin-visual-search.test.js @@ -0,0 +1,16 @@ +const root = '../../'; + +jest.useFakeTimers(); + +describe('visual-search plugin', function(){ + + beforeEach(function(){ + require(root + 'jspsych.js'); + require(root + 'plugins/jspsych-visual-search.js'); + }); + + test('loads correctly', function(){ + expect(typeof window.jsPsych.plugins['visual-search']).not.toBe('undefined'); + }); + +});