xxxxxxxxxx
374
/*
TODO:
experiment mode:
- fix canvas size
- fix image ordering system? it works but it's a mess and probably really hard for students to understand/make changes to
*/
/*
Visual search task using a 'target' from a 'scene'. The target is first shown in a random location (randomX, randomY), and after a mouse click the scene is shown and the target can be selected with the cross (can also be changed not a circle/ellipse if needed).
To use this code:
1. upload image(s)
2. write filenames in code
3. run in setup mode
4. copy all the numbers they get into array
5. take out of setup mode
*/
// !! Change this to you own project's name in order to save your experiments data.
// The data can be found on the google drive
const SAVE_DATA_SHEET_NAME = "visual_search_test";
// change this to false to execute the experiment
const setupMode = false;
const filenames = ['vatican.jpeg', 'ajaxmadrid.jpg'];
// change this to the copied coordinates
const targetCoords = [[[181,173,34,47], [392,5,68,41], [502,246,33,57]], [[260,4,95,56], [72,176,45,112], [404,153,48,26]]]
// set this to false to show targets in the selected order
const randomOrder = true;
// set this to false to show images in the order of filenames
const imgRandomOrder = true;
// real code
let max_width = 0, max_height = 0;
//setup mode vars
let selectionCount = 0;
let imgSelectionCount = 0;
let targetSelection = [];
let res;
let setupText = 'Press enter to go to the next image. <br>Copy the following into the code: <br>';
let targetsText = "[]";
//experiment vars
let images = [];
let clicks=0;
var randomX,randomY;
var reactionTime = 0;
var startTime = 0;
var targets =[];
let presentationOrder = [], imgPresentationOrder = [];
let trial = 0, imgtrial = 0;
function preload(){
for(i=0;i<filenames.length;i++){
images[i] = loadImage('images/' + filenames[i]);
imgPresentationOrder.push(i);
// for setup mode
targetSelection.push([]);
}
}
function setup() {
// determine which image has the biggest width/height
for (let i = 0; i < images.length; i++) {
if (images[i].width > max_width) {
max_width = images[i].width;
}
if (images[i].height > max_height) {
max_height = images[i].height;
}
}
//use the image size for the canvas, SO MIND THAT YOU USE A SMALL ENOUGH IMAGE!
cnv = createCanvas(max_width, max_height);
if(setupMode){
res = select('#setuptext');
res.html(setupText);
cnv.mousePressed(startSelect);
cnv.mouseReleased(endSelect);
cnv.parent("sketch");
document.getElementById('sketch').setAttribute("style","height:" + cnv.height + "px");
}
else{
cnv.mouseClicked(clickOnCanvas);
//first random location for the target
randomX=random(100,width-100);
randomY=random(100,height-100);
for(j=0;j<targetCoords.length;j++){
image(images[j],0,0);
presentationOrder.push([]);
targets.push([]);
for(i=0;i<targetCoords[j].length;i++){
targets[j].push(get(targetCoords[j][i][0], targetCoords[j][i][1], targetCoords[j][i][2], targetCoords[j][i][3]));
presentationOrder[j][i] = i;
}
background(220);
}
if(randomOrder){
for(i=0;i<presentationOrder.length;i++){
shuffle(presentationOrder[i], true);
}
}
if(imgRandomOrder){shuffle(imgPresentationOrder, true);}
data = new p5.Table();
data.columns = ['image', 'target','reaction time (ms)','hit/distance','x','y'];
}
}
function draw() {
//this code is to draw the rectangles
if(setupMode){
background(0);
image(images[imgSelectionCount],0,0);
strokeWeight(1);
textSize(16);
for(i=0;i<targetSelection[imgSelectionCount].length;i++){
let xs,ys,ws,hs;
if(i == selectionCount){
rectfill = color(0, 200, 200);
rectfill.setAlpha(100);
fill(rectfill);
stroke(0,255,255);
if(mouseX>targetSelection[imgSelectionCount][i][0]){
xs = targetSelection[imgSelectionCount][i][0];
ws = mouseX - xs;
}else{
xs = mouseX;
ws = targetSelection[imgSelectionCount][i][0] - xs;
}
if(mouseY>targetSelection[imgSelectionCount][i][1]){
ys = targetSelection[imgSelectionCount][i][1];
hs = mouseY - ys;
}else{
ys = mouseY;
hs = targetSelection[imgSelectionCount][i][1] - ys;
}
}
else{
rectfill = color(255, 0, 255);
rectfill.setAlpha(100);
fill(rectfill);
stroke(255,0,255);
xs = targetSelection[imgSelectionCount][i][0];
ys = targetSelection[imgSelectionCount][i][1];
ws = targetSelection[imgSelectionCount][i][2];
hs = targetSelection[imgSelectionCount][i][3];
text(i+1,xs,ys);
}
rect(xs,ys,ws,hs);
}
}
//this is the regular experiment
else{
background(220);
//search scene & arrow are shown anyway but...
image(images[imgPresentationOrder[imgtrial]],0,0);
//... with no mouseclicks, a white background occludes the scene and target is shown...
if(clicks==0){
showTarget(trial);
}
}
}
//called when mouse is pressed in setup mode
function startSelect(){
targetSelection[imgSelectionCount].push([mouseX,mouseY]);
}
//called when mouse is released in setup mode
function endSelect(){
//sort numbers so they're in "x,y,w,h" format
let xs, ys, ws, hs;
if(mouseX>targetSelection[imgSelectionCount][selectionCount][0]){
xs = targetSelection[imgSelectionCount][selectionCount][0];
ws = mouseX - xs;
}else{
xs = mouseX;
ws = targetSelection[imgSelectionCount][selectionCount][0] - xs;
}
if(mouseY>targetSelection[imgSelectionCount][selectionCount][1]){
ys = targetSelection[imgSelectionCount][selectionCount][1];
hs = mouseY - ys;
}else{
ys = mouseY;
hs = targetSelection[imgSelectionCount][selectionCount][1] - ys;
}
targetSelection[imgSelectionCount][selectionCount] = [int(xs), int(ys), int(ws), int(hs)];
//translates targetSelection to a string (targetsText)
targetsText = '[';
for(i=0;i<targetSelection.length;i++){
for(j=0;j<targetSelection[i].length;j++){
if(j==0){
targetsText += '[[' + targetSelection[i][j] + ']';
}
else{
targetsText += ', [' + targetSelection[i][j] + ']';
}
}
if(i<targetSelection.length - 1){
targetsText += '], ';
} else{
targetsText += ']]';
}
}
res.html(setupText + targetsText);
selectionCount++;
}
// Here we keep track of reaction time and how many times has been clicked:
function clickOnCanvas() {
if(clicks==0){
startTime=millis();
clicks++;
randomX=random(100,width-100);
randomY=random(100,height-100);
}
else if(clicks==1){
// check accuracy
let imgnum = imgPresentationOrder[imgtrial];
let target = targetCoords[imgnum][presentationOrder[imgnum][trial]];
let d;
if(mouseX>=target[0] && mouseX<=target[0] + target[2] && mouseY>=target[1] && mouseY<=target[1] + target[3]){
d = 0;
} else{
var dx = Math.max(target[0] - mouseX, 0, mouseX - (target[0] + target[2]));
var dy = Math.max(target[1] - mouseY, 0, mouseY - (target[1] + target[3]));
d = sqrt(dx*dx + dy*dy)
}
// write results to the table
let newRow = data.addRow();
newRow.setString('image', filenames[imgnum]);
newRow.setNum('target', presentationOrder[imgnum][trial]);
newRow.setNum('reaction time (ms)', millis()-startTime);
newRow.setNum('hit/distance', int(d));
newRow.setNum('x', int(mouseX));
newRow.setNum('y', int(mouseY));
if(trial + 1 == targetCoords[imgnum].length){
if(imgtrial + 1 == targetCoords.length){
// Translate the p5 Table to a two dimensional array and send it to Google sheets
dataExport = data.getArray();
dataExport.unshift(data.columns);
saveToSheet(SAVE_DATA_SHEET_NAME, dataExport);
//lock the sketch
clicks = 2;
} else{
imgtrial++;
trial = 0;
clicks = 0;
}
}else{
trial++;
clicks = 0;
}
}
}
function keyPressed(){
if(keyCode==ENTER && imgSelectionCount < filenames.length-1){
imgSelectionCount++;
selectionCount = 0;
}
}
function showTarget(i){
background(255);
noStroke();
textSize(16);
text('Try to find the target a.s.a.p.',30,40);
text('Click to start the task',30,60);
image(targets[imgPresentationOrder[imgtrial]][presentationOrder[imgPresentationOrder[imgtrial]][trial]],randomX,randomY);
}
/**
* This function saves the data to a google sheet of the given name.
* Make sure that the name is unique for you project!
* If your name is not unique then you will share a google sheet with someone else.
*
*
* @param {string} sheetName - The name of you sheet. Make sure it is unique to your project.
* @param {Array<Array>} data - A two dimensional array representing the data.
* Data example:
* data = [
* (row1:) [col1, col2, col3],
* (row2:) [col1, col2, col3]
* ]
* @param {string} [tabName] - (OPTIONAL) The name of the tab/worksheet that this instance of data will be in.
* If you leave this empty it will fall back to using the current time of data submission as tab name
* IMPORTANT! if you are using custom tab names make sure that every name is unique.
* If a name already exists saving wil result in an error.
*/
async function saveToSheet(sheetName, data, tabName) {
// Safety check
if (!sheetName) {
return console.error("The name for the sheet was not valid!")
}
if (!data || data.length === 0) {
return console.error("Cannot save empty data!")
}
// Building save request
const requestBody = {
sheetName,
data
}
// Add tabName if it exists
if (tabName && typeof tabName === "string") {
requestBody.tabName = tabName
}
try {
// Send the save request
const response = await fetch("https://node.bykrijgsman.com/save-data", {
method: "POST",
mode: "cors",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
const result = await response.text()
console.log(result)
} catch (e) {
// There was an error while saving if we get here!
console.error(e)
}
}