mirror of
https://github.com/jspsych/jsPsych.git
synced 2025-05-10 11:10:54 +00:00
Merge pull request #1018 from kohler-visual-neuroscience-lab/master
updates to free-sort plugin, new functionality
This commit is contained in:
commit
175eaab10d
@ -8,6 +8,7 @@ The following people have contributed to the development of jsPsych by writing c
|
||||
* Gustavo Juantorena - https://github.com/GEJ1
|
||||
* Chris Jungerius - https://github.com/cjungerius
|
||||
* Jana Klaus - https://github.com/janakl4us
|
||||
* Peter Jes Kohler - https://github.com/pjkohler
|
||||
* Jonas Lambers
|
||||
* Shane Martin - https://github.com/shamrt
|
||||
* Adrian Oesch - https://github.com/adrianoesch
|
||||
|
@ -15,13 +15,14 @@ Parameter | Type | Default Value | Description
|
||||
stimuli | array | *undefined* | Each element of this array is an image path.
|
||||
stim_height | numeric | 100 | The height of the images in pixels.
|
||||
stim_width | numeric | 100 | The width of the images in pixels.
|
||||
scale_factor | numeric | 1.5 | How much larger to make the stimulus while moving (1 = no scaling).
|
||||
sort_area_height | numeric | 800 | The height of the container that subjects can move the stimuli in. Stimuli will be constrained to this area.
|
||||
sort_area_width | numeric | 800 | The width of the container that subjects can move the stimuli in. Stimuli will be constrained to this area.
|
||||
sort_area_shape | string | "ellipse" | The shape of the sorting area, can be "ellipse" or "square".
|
||||
prompt | string | null | This string can contain HTML markup. The intention is that it can be used to provide a reminder about the action the subject is supposed to take (e.g., which key to press).
|
||||
prompt_location | string | "above" | Indicates whether to show the prompt `"above"` or `"below"` the sorting area.
|
||||
button_label | string | 'Continue' | The text that appears on the button to continue to the next trial.
|
||||
|
||||
|
||||
## Data Generated
|
||||
|
||||
In addition to the [default data collected by all plugins](overview#data-collected-by-plugins), this plugin collects the following data for each trial.
|
||||
|
@ -10,7 +10,12 @@
|
||||
|
||||
var trials = {
|
||||
type: 'free-sort',
|
||||
stimuli: ['img/happy_face_1.jpg','img/happy_face_2.jpg','img/happy_face_3.jpg','img/happy_face_4.jpg']
|
||||
stimuli: ['img/happy_face_1.jpg','img/happy_face_2.jpg','img/happy_face_3.jpg','img/happy_face_4.jpg'],
|
||||
stim_height: 150,
|
||||
stim_width: 200,
|
||||
scale_factor: 1.3,
|
||||
sort_area_shape: "ellipse",
|
||||
prompt_location: "below"
|
||||
};
|
||||
|
||||
|
||||
|
@ -22,19 +22,25 @@ jsPsych.plugins['free-sort'] = (function() {
|
||||
pretty_name: 'Stimuli',
|
||||
default: undefined,
|
||||
array: true,
|
||||
description: 'Images to be displayed.'
|
||||
description: 'items to be displayed.'
|
||||
},
|
||||
stim_height: {
|
||||
type: jsPsych.plugins.parameterType.INT,
|
||||
pretty_name: 'Stimulus height',
|
||||
default: 100,
|
||||
description: 'Height of images in pixels.'
|
||||
description: 'Height of items in pixels.'
|
||||
},
|
||||
stim_width: {
|
||||
type: jsPsych.plugins.parameterType.INT,
|
||||
pretty_name: 'Stimulus width',
|
||||
default: 100,
|
||||
description: 'Width of images in pixels'
|
||||
description: 'Width of items in pixels'
|
||||
},
|
||||
scale_factor: {
|
||||
type: jsPsych.plugins.parameterType.FLOAT,
|
||||
pretty_name: 'Stimulus scaling factor',
|
||||
default: 1.5,
|
||||
description: 'How much larger to make the stimulus while moving (1 = no scaling)'
|
||||
},
|
||||
sort_area_height: {
|
||||
type: jsPsych.plugins.parameterType.INT,
|
||||
@ -48,10 +54,17 @@ jsPsych.plugins['free-sort'] = (function() {
|
||||
default: 800,
|
||||
description: 'The width of the container that subjects can move the stimuli in.'
|
||||
},
|
||||
sort_area_shape: {
|
||||
type: jsPsych.plugins.parameterType.STRING,
|
||||
pretty_name: 'Sort area shape',
|
||||
options: ['square','ellipse'],
|
||||
default: 'ellipse',
|
||||
description: 'The shape of the sorting area'
|
||||
},
|
||||
prompt: {
|
||||
type: jsPsych.plugins.parameterType.STRING,
|
||||
pretty_name: 'Prompt',
|
||||
default: null,
|
||||
default: '',
|
||||
description: 'It can be used to provide a reminder about the action the subject is supposed to take.'
|
||||
},
|
||||
prompt_location: {
|
||||
@ -64,7 +77,7 @@ jsPsych.plugins['free-sort'] = (function() {
|
||||
button_label: {
|
||||
type: jsPsych.plugins.parameterType.STRING,
|
||||
pretty_name: 'Button label',
|
||||
default: 'Continue',
|
||||
default: 'continue',
|
||||
description: 'The text that appears on the button to continue to the next trial.'
|
||||
}
|
||||
}
|
||||
@ -74,36 +87,96 @@ jsPsych.plugins['free-sort'] = (function() {
|
||||
|
||||
var start_time = performance.now();
|
||||
|
||||
var html = "";
|
||||
// check if there is a prompt and if it is shown above
|
||||
if (trial.prompt !== null && trial.prompt_location == "above") {
|
||||
html += trial.prompt;
|
||||
}
|
||||
|
||||
html += '<div '+
|
||||
let html =
|
||||
'<div '+
|
||||
'id="jspsych-free-sort-arena" '+
|
||||
'class="jspsych-free-sort-arena" '+
|
||||
'style="position: relative; width:'+trial.sort_area_width+'px; height:'+trial.sort_area_height+'px; border:2px solid #444;"'+
|
||||
'></div>';
|
||||
'style="position: relative; width:'+trial.sort_area_width+'px; height:'+trial.sort_area_height+'px; margin: auto; line-height: 0em"</div>';
|
||||
|
||||
// check if prompt exists and if it is shown below
|
||||
if (trial.prompt !== null && trial.prompt_location == "below") {
|
||||
html += trial.prompt;
|
||||
// another div for border
|
||||
html += '<div '+
|
||||
'id="jspsych-free-sort-border" '+
|
||||
'class="jspsych-free-sort-border" '+
|
||||
'style="position: relative; width:'+trial.sort_area_width*.94+'px; height:'+trial.sort_area_height*.94+'px; border:'+trial.sort_area_height*.03+'px solid #fc9272; margin: auto; line-height: 0em; ';
|
||||
|
||||
if ( trial.sort_area_shape == "ellipse") {
|
||||
html += 'webkit-border-radius: 50%; moz-border-radius: 50%; border-radius: 50%"></div>'
|
||||
} else {
|
||||
html += 'webkit-border-radius: 0%; moz-border-radius: 0%; border-radius: 0%"></div>'
|
||||
}
|
||||
|
||||
if ( trial.prompt ) {
|
||||
trial.prompt = '<br>'
|
||||
}
|
||||
else {
|
||||
trial.prompt = ''
|
||||
}
|
||||
|
||||
// variable that has the prompt text, counter and button
|
||||
const html_text = '<div style="line-height: 0em">' + trial.prompt +
|
||||
'<p id="jspsych-free-sort-counter" style="display: inline-block; line-height: 1em">You still need to place ' + trial.stimuli.length + ' items inside the arena.</p>'+
|
||||
'<button id="jspsych-free-sort-done-btn" class="jspsych-btn" '+
|
||||
'style="display: none; margin: 5px; padding: 5px; text-align: center; font-weight: bold; font-size: 18px; vertical-align:baseline; line-height: 1em">' + trial.button_label+'</button></div>'
|
||||
|
||||
// position prompt above or below
|
||||
if (trial.prompt_location == "below") {
|
||||
html += html_text
|
||||
} else {
|
||||
html = html_text + html
|
||||
}
|
||||
|
||||
console.log(html)
|
||||
display_element.innerHTML = html;
|
||||
|
||||
// store initial location data
|
||||
var init_locations = [];
|
||||
let init_locations = [];
|
||||
|
||||
for (var i = 0; i < trial.stimuli.length; i++) {
|
||||
var coords = random_coordinate(trial.sort_area_width - trial.stim_width, trial.sort_area_height - trial.stim_height);
|
||||
// determine number of rows and colums, must be a even number
|
||||
let num_rows = Math.ceil(Math.sqrt(trial.stimuli.length))
|
||||
if ( num_rows % 2 != 0) {
|
||||
num_rows = num_rows + 1
|
||||
}
|
||||
|
||||
// compute coords for left and right side of arena
|
||||
let r_coords = [];
|
||||
let l_coords = [];
|
||||
for (const x of make_arr(0, trial.sort_area_width - trial.stim_width, num_rows) ) {
|
||||
for (const y of make_arr(0, trial.sort_area_height - trial.stim_height, num_rows) ) {
|
||||
if ( x > ( (trial.sort_area_width - trial.stim_width) * .5 ) ) {
|
||||
//r_coords.push({ x:x, y:y } )
|
||||
r_coords.push({ x:x + (trial.sort_area_width) * .5 , y:y });
|
||||
} else {
|
||||
l_coords.push({ x:x - (trial.sort_area_width) * .5 , y:y });
|
||||
//l_coords.push({ x:x, y:y } )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// repeat coordinates until you have enough coords (may be obsolete)
|
||||
while ( ( r_coords.length + l_coords.length ) < trial.stimuli.length ) {
|
||||
r_coords = r_coords.concat(r_coords)
|
||||
l_coords = l_coords.concat(l_coords)
|
||||
}
|
||||
// reverse left coords, so that coords closest to arena is used first
|
||||
l_coords = l_coords.reverse()
|
||||
|
||||
// shuffle stimuli, so that starting positions are random
|
||||
trial.stimuli = shuffle(trial.stimuli);
|
||||
|
||||
let inside = []
|
||||
for (let i = 0; i < trial.stimuli.length; i++) {
|
||||
let coords = []
|
||||
if ( (i % 2) == 0 ) {
|
||||
coords = r_coords[Math.floor(i * .5)];
|
||||
} else {
|
||||
coords = l_coords[Math.floor(i * .5)];
|
||||
}
|
||||
display_element.querySelector("#jspsych-free-sort-arena").innerHTML += '<img '+
|
||||
'src="'+trial.stimuli[i]+'" '+
|
||||
'data-src="'+trial.stimuli[i]+'" '+
|
||||
'class="jspsych-free-sort-draggable" '+
|
||||
'draggable="false" '+
|
||||
'id="'+i+'" '+
|
||||
'style="position: absolute; cursor: move; width:'+trial.stim_width+'px; height:'+trial.stim_height+'px; top:'+coords.y+'px; left:'+coords.x+'px;">'+
|
||||
'</img>';
|
||||
|
||||
@ -112,31 +185,74 @@ jsPsych.plugins['free-sort'] = (function() {
|
||||
"x": coords.x,
|
||||
"y": coords.y
|
||||
});
|
||||
inside.push(false);
|
||||
}
|
||||
// moves within a trial
|
||||
let moves = [];
|
||||
|
||||
display_element.innerHTML += '<button id="jspsych-free-sort-done-btn" class="jspsych-btn">'+trial.button_label+'</button>';
|
||||
// are objects currently inside
|
||||
let cur_in = false
|
||||
|
||||
var maxz = 1;
|
||||
// draggable items
|
||||
const draggables = display_element.querySelectorAll('.jspsych-free-sort-draggable');
|
||||
|
||||
var moves = [];
|
||||
// button (will show when all items are inside) and border (will change color)
|
||||
const border = display_element.querySelector("#jspsych-free-sort-border")
|
||||
const button = display_element.querySelector('#jspsych-free-sort-done-btn')
|
||||
|
||||
var draggables = display_element.querySelectorAll('.jspsych-free-sort-draggable');
|
||||
|
||||
for(var i=0;i<draggables.length; i++){
|
||||
for(let i=0; i<draggables.length; i++){
|
||||
draggables[i].addEventListener('mousedown', function(event){
|
||||
var x = event.pageX - event.currentTarget.offsetLeft;
|
||||
var y = event.pageY - event.currentTarget.offsetTop - window.scrollY;
|
||||
var elem = event.currentTarget;
|
||||
elem.style.zIndex = ++maxz;
|
||||
let x = event.pageX - event.currentTarget.offsetLeft;
|
||||
let y = event.pageY - event.currentTarget.offsetTop - window.scrollY;
|
||||
let elem = event.currentTarget;
|
||||
elem.style.transform = "scale(" + trial.scale_factor + "," + trial.scale_factor + ")";
|
||||
let mousemoveevent = function(e){
|
||||
cur_in = inside_ellipse(e.clientX - x, e.clientY - y,
|
||||
trial.sort_area_width*.5 - trial.stim_width*.5, trial.sort_area_height*.5 - trial.stim_height*.5,
|
||||
trial.sort_area_width*.5, trial.sort_area_height*.5,
|
||||
trial.sort_area_shape == "square");
|
||||
elem.style.top = Math.min(trial.sort_area_height - trial.stim_height*.5, Math.max(- trial.stim_height*.5, (e.clientY - y))) + 'px';
|
||||
elem.style.left = Math.min(trial.sort_area_width*1.5 - trial.stim_width, Math.max(-trial.sort_area_width*.5, (e.clientX - x)))+ 'px';
|
||||
|
||||
// modify border while items is being moved
|
||||
if (cur_in) {
|
||||
border.style.borderColor = "#a1d99b";
|
||||
border.style.background = "None";
|
||||
} else {
|
||||
border.style.borderColor = "#fc9272";
|
||||
border.style.background = "None";
|
||||
}
|
||||
|
||||
// replace in overall array, grab idx from item id
|
||||
inside.splice(elem.id, true, cur_in)
|
||||
|
||||
var mousemoveevent = function(e){
|
||||
elem.style.top = Math.min(trial.sort_area_height - trial.stim_height, Math.max(0,(e.clientY - y))) + 'px';
|
||||
elem.style.left = Math.min(trial.sort_area_width - trial.stim_width, Math.max(0,(e.clientX - x))) + 'px';
|
||||
// modify text and background if all items are inside
|
||||
if (inside.every(Boolean)) {
|
||||
border.style.background = "#a1d99b";
|
||||
button.style.display = "inline-block";
|
||||
display_element.querySelector("#jspsych-free-sort-counter").innerHTML = "All items placed. Feel free to reposition any item if necessary. Otherwise, click here to "
|
||||
} else {
|
||||
border.style.background = "none";
|
||||
button.style.display = "none";
|
||||
if ( (inside.length - inside.filter(Boolean).length) > 1 ) {
|
||||
display_element.querySelector("#jspsych-free-sort-counter").innerHTML = "You still need to place " + (inside.length - inside.filter(Boolean).length) + " items inside the arena."
|
||||
} else {
|
||||
display_element.querySelector("#jspsych-free-sort-counter").innerHTML = "You still need to place " + (inside.length - inside.filter(Boolean).length) + " item inside the arena."
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousemove', mousemoveevent);
|
||||
|
||||
var mouseupevent = function(e){
|
||||
document.removeEventListener('mousemove', mousemoveevent);
|
||||
elem.style.transform = "scale(1, 1)";
|
||||
if (inside.every(Boolean)) {
|
||||
border.style.background = "#a1d99b";
|
||||
border.style.borderColor = "#a1d99b";
|
||||
} else {
|
||||
border.style.background = "none";
|
||||
border.style.borderColor = "#fc9272";
|
||||
}
|
||||
moves.push({
|
||||
"src": elem.dataset.src,
|
||||
"x": elem.offsetLeft,
|
||||
@ -148,47 +264,86 @@ jsPsych.plugins['free-sort'] = (function() {
|
||||
});
|
||||
}
|
||||
|
||||
display_element.querySelector('#jspsych-free-sort-done-btn').addEventListener('click', function(){
|
||||
display_element.querySelector('#jspsych-free-sort-done-btn').addEventListener('click', function(){
|
||||
if (inside.every(Boolean)) {
|
||||
const end_time = performance.now();
|
||||
const rt = end_time - start_time;
|
||||
// gather data
|
||||
const items = display_element.querySelectorAll('.jspsych-free-sort-draggable');
|
||||
// get final position of all items
|
||||
let final_locations = [];
|
||||
for(let i=0; i<items.length; i++){
|
||||
final_locations.push({
|
||||
"src": items[i].dataset.src,
|
||||
"x": parseInt(items[i].style.left),
|
||||
"y": parseInt(items[i].style.top)
|
||||
});
|
||||
}
|
||||
|
||||
var end_time = performance.now();
|
||||
var rt = end_time - start_time;
|
||||
// gather data
|
||||
// get final position of all objects
|
||||
var final_locations = [];
|
||||
var matches = display_element.querySelectorAll('.jspsych-free-sort-draggable');
|
||||
for(var i=0; i<matches.length; i++){
|
||||
final_locations.push({
|
||||
"src": matches[i].dataset.src,
|
||||
"x": parseInt(matches[i].style.left),
|
||||
"y": parseInt(matches[i].style.top)
|
||||
});
|
||||
const trial_data = {
|
||||
"init_locations": JSON.stringify(init_locations),
|
||||
"moves": JSON.stringify(moves),
|
||||
"final_locations": JSON.stringify(final_locations),
|
||||
"rt": rt
|
||||
};
|
||||
|
||||
// advance to next part
|
||||
display_element.innerHTML = '';
|
||||
jsPsych.finishTrial(trial_data);
|
||||
}
|
||||
|
||||
var trial_data = {
|
||||
"init_locations": JSON.stringify(init_locations),
|
||||
"moves": JSON.stringify(moves),
|
||||
"final_locations": JSON.stringify(final_locations),
|
||||
"rt": rt
|
||||
};
|
||||
|
||||
// advance to next part
|
||||
display_element.innerHTML = '';
|
||||
jsPsych.finishTrial(trial_data);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// helper functions
|
||||
|
||||
function random_coordinate(max_width, max_height) {
|
||||
var rnd_x = Math.floor(Math.random() * (max_width - 1));
|
||||
var rnd_y = Math.floor(Math.random() * (max_height - 1));
|
||||
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;
|
||||
}
|
||||
|
||||
function make_arr(startValue, stopValue, cardinality) {
|
||||
const step = (stopValue - startValue) / (cardinality - 1);
|
||||
let arr = [];
|
||||
for (let i = 0; i < cardinality; i++) {
|
||||
arr.push(startValue + (step * i));
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function inside_ellipse(x, y, x0, y0, rx, ry, square=false) {
|
||||
const results = [];
|
||||
if (square) {
|
||||
result = ( Math.abs(x - x0) <= rx ) && ( Math.abs(y - y0) <= ry )
|
||||
} else {
|
||||
result = (( x - x0 ) * ( x - x0 )) * (ry * ry) + ((y - y0) * ( y - y0 )) * ( rx * rx ) <= ( (rx * rx) * (ry * ry) )
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/*
|
||||
un-used functions (that might be useful for something else)
|
||||
|
||||
function random_coordinate(max_width, max_height) {
|
||||
const rnd_x = Math.floor(Math.random() * (max_width - 1));
|
||||
const rnd_y = Math.floor(Math.random() * (max_height - 1));
|
||||
return {
|
||||
x: rnd_x,
|
||||
y: rnd_y
|
||||
};
|
||||
}
|
||||
|
||||
*/
|
||||
return plugin;
|
||||
})();
|
||||
|
Loading…
Reference in New Issue
Block a user