Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modified Visual Search Circle Plugin #1954

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions contributors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand All @@ -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
}
```
2 changes: 1 addition & 1 deletion docs/plugins/list-of-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<script src="js/snap.svg-min.js"></script>
<script src="../jspsych.js"></script>
<script src="../plugins/jspsych-html-keyboard-response.js"></script>
<script src="../plugins/jspsych-visual-search-circle.js"></script>
<script src="../plugins/jspsych-visual-search.js"></script>
<script src="../plugins/jspsych-preload.js"></script>
<link rel="stylesheet" href="../css/jspsych.css">
</head>
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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',
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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 += '<div id="jspsych-visual-search-circle-container" style="position: relative; width:' + paper_size + 'px; height:' + paper_size + 'px"></div>';
var paper = display_element.querySelector("#jspsych-visual-search-circle-container");
display_element.innerHTML += '<div id="jspsych-visual-search-container" style="position: relative; width:' + paper_size + 'px; height:' + paper_size + 'px"></div>';
var paper = display_element.querySelector("#jspsych-visual-search-container");

// check distractors - array?
if(!Array.isArray(trial.foil)){
Expand All @@ -152,6 +205,7 @@ jsPsych.plugins["visual-search-circle"] = (function() {
jsPsych.pluginAPI.setTimeout(function() {
// after wait is over
show_search_array();
paper.innerHTML += "<img src='"+trial.fixation_image+"' style='position: absolute; top:"+fix_loc[0]+"px; left:"+fix_loc[1]+"px; width:"+trial.fixation_size[0]+"px; height:"+trial.fixation_size[1]+"px;'></img>";
}, trial.fixation_duration);
}

Expand Down Expand Up @@ -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;
})();
16 changes: 0 additions & 16 deletions tests/plugins/plugin-visual-search-circle.test.js

This file was deleted.

16 changes: 16 additions & 0 deletions tests/plugins/plugin-visual-search.test.js
Original file line number Diff line number Diff line change
@@ -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');
});

});