Diff
checker
Text
Text
Bilder
Dokumente
Excel
Ordner
Legal
Enterprise
Desktop-App
Preise
Einloggen
Diffchecker Desktop herunterladen
Texte vergleichen
Finde den Unterschied zwischen zwei Textdateien
Werkzeuge
Verlauf
Live-Editor
Leerzeichen ausblenden
Gleiches ausblenden
Zeilenumbruch aus
Ansicht
Zweispaltig
Einspaltig
Vergleichsgenauigkeit
Intelligent
Wort
Zeichen
Textstile
Darstellung ändern
Syntaxhervorhebung
Syntax auswählen
Ignorieren
Text umwandeln
Zur ersten Änderung
Eingabe bearbeiten
Diffchecker Desktop
Der sicherste Weg, Diffchecker zu nutzen. Hol dir die Desktop-App: Deine Diffs verlassen nie deinen Computer!
Desktop holen
arabic_demo
Erstellt
vor 5 Jahren
Diff läuft nie ab
Löschen
Exportieren
Teilen
Erklären
19 Entfernungen
Zeilen
Gesamt
Entfernt
Zeichen
Gesamt
Entfernt
Um diese Funktion weiterhin zu nutzen, aktualisiere auf
Diff
checker
Pro
Preise anzeigen
843 Zeilen
Kopieren
18 Hinzufügungen
Zeilen
Gesamt
Hinzugefügt
Zeichen
Gesamt
Hinzugefügt
Um diese Funktion weiterhin zu nutzen, aktualisiere auf
Diff
checker
Pro
Preise anzeigen
840 Zeilen
Kopieren
/*****************
/*****************
* Crowding Test *
* Crowding Test *
*****************/
*****************/
const debug = false;
const debug = false;
import { core, data, util, visual } from "./psychojs/out/psychojs-2021.3.0.js";
import { core, data, util, visual } from "./psychojs/out/psychojs-2021.3.0.js";
const { PsychoJS } = core;
const { PsychoJS } = core;
const { TrialHandler, MultiStairHandler } = data;
const { TrialHandler, MultiStairHandler } = data;
const { Scheduler } = util;
const { Scheduler } = util;
////
////
import * as jsQUEST from "./lib/jsQUEST.module.js";
import * as jsQUEST from "./lib/jsQUEST.module.js";
////
////
/* ------------------------------- Components ------------------------------- */
/* ------------------------------- Components ------------------------------- */
import { hideCursor, showCursor, shuffle } from "./components/utils.js";
import { hideCursor, showCursor, shuffle } from "./components/utils.js";
import {
import {
addBeepButton,
addBeepButton,
instructionsText,
instructionsText,
removeBeepButton,
removeBeepButton,
} from "./components/instructions.js";
} from "./components/instructions.js";
import { calculateBlockWithTrialIndex } from "./components/trialCounter.js";
import { calculateBlockWithTrialIndex } from "./components/trialCounter.js";
import {
import {
getCorrectSynth,
getCorrectSynth,
getWrongSynth,
getWrongSynth,
getPurrSynth,
getPurrSynth,
} from "./components/sound.js";
} from "./components/sound.js";
import {
import {
removeClickableAlphabet,
removeClickableAlphabet,
setupClickableAlphabet,
setupClickableAlphabet,
} from "./components/showAlphabet.js";
} from "./components/showAlphabet.js";
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
window.jsQUEST = jsQUEST;
window.jsQUEST = jsQUEST;
var conditionTrials;
var conditionTrials;
var levelLeft, levelRight;
var levelLeft, levelRight;
let correctAns;
let correctAns;
// For development purposes, toggle RC off for testing speed
// For development purposes, toggle RC off for testing speed
const useRC = !debug;
const useRC = !debug;
const rc = RemoteCalibrator;
const rc = RemoteCalibrator;
rc.init();
rc.init();
// store info about the experiment session:
// store info about the experiment session:
let expName = "Threshold"; // from the Builder filename that created this script
let expName = "Threshold"; // from the Builder filename that created this script
let expInfo = { participant: debug ? rc.id.value : "", session: "001" };
let expInfo = { participant: debug ? rc.id.value : "", session: "001" };
Kopieren
Kopiert
Kopieren
Kopiert
const fontsRequired =
new Set()
;
const fontsRequired =
{}
;
////
////
// blockCount is just a file telling the program how many blocks in total
// blockCount is just a file telling the program how many blocks in total
Papa.parse("conditions/blockCount.csv", {
Papa.parse("conditions/blockCount.csv", {
download: true,
download: true,
complete: function (results) {
complete: function (results) {
const blockCount = results.data.length - 2; // TODO Make this calculation robust
const blockCount = results.data.length - 2; // TODO Make this calculation robust
loadBlockFiles(blockCount, () => {
loadBlockFiles(blockCount, () => {
if (useRC) {
if (useRC) {
rc.panel(
rc.panel(
[
[
{
{
name: "screenSize",
name: "screenSize",
},
},
{
{
name: "trackDistance",
name: "trackDistance",
options: {
options: {
nearPoint: false,
nearPoint: false,
showVideo: false,
showVideo: false,
},
},
},
},
],
],
"body",
"body",
{},
{},
() => {
() => {
rc.removePanel();
rc.removePanel();
// ! Start actual experiment
// ! Start actual experiment
experiment(blockCount);
experiment(blockCount);
}
}
);
);
} else {
} else {
// NO RC
// NO RC
experiment(blockCount);
experiment(blockCount);
}
}
});
});
},
},
});
});
const blockFiles = {};
const blockFiles = {};
const loadBlockFiles = (count, callback) => {
const loadBlockFiles = (count, callback) => {
if (count === 0) {
if (count === 0) {
callback();
callback();
return;
return;
}
}
Papa.parse(`conditions/block_${count}.csv`, {
Papa.parse(`conditions/block_${count}.csv`, {
download: true,
download: true,
header: true,
header: true,
skipEmptyLines: true,
skipEmptyLines: true,
dynamicTyping: true,
dynamicTyping: true,
complete: function (results) {
complete: function (results) {
blockFiles[count] = results.data;
blockFiles[count] = results.data;
if (debug) console.log("Block " + count + ": ", results.data);
if (debug) console.log("Block " + count + ": ", results.data);
Object.values(results.data).forEach((row) => {
Object.values(results.data).forEach((row) => {
let fontFamily = row["targetFont"];
let fontFamily = row["targetFont"];
let fontTestString = "12px " + fontFamily;
let fontTestString = "12px " + fontFamily;
Kopieren
Kopiert
Kopieren
Kopiert
let fontPath = "fonts/" + fontFamily + ".woff
";
let fontPath = "fonts/" + fontFamily + ".woff
2
";
if (debug) console.log("fontTestString: ", fontTestString);
if (debug) console.log("fontTestString: ", fontTestString);
let response = fetch(fontPath).then((response) => {
let response = fetch(fontPath).then((response) => {
if (response.ok) {
if (response.ok) {
Kopieren
Kopiert
Kopieren
Kopiert
fontsRequired
.add(row["targetFont"])
;
// let f = new FontFace(fontFamily, `url(${response.url})`);
// f.load()
// .then((loadedFontFace) => {
// document.fonts.add(loadedFontFace);
// })
// .catch((err) => {
// console.error(err);
// });
fontsRequired
[fontFamily] = fontPath
;
} else {
} else {
console.log(
console.log(
"Does the browser consider this font supported?",
"Does the browser consider this font supported?",
document.fonts.check(fontTestString)
document.fonts.check(fontTestString)
);
);
console.log(
console.log(
"Uh oh, unable to find the font file for: " +
"Uh oh, unable to find the font file for: " +
fontFamily +
fontFamily +
"\n" +
"\n" +
"If this font is already supported by the browser then it should display correctly. " +
"If this font is already supported by the browser then it should display correctly. " +
"\n" +
"\n" +
"If not, however, a different fallback font will be chosen by the browser, and your stimulus will not be displayed as intended. " +
"If not, however, a different fallback font will be chosen by the browser, and your stimulus will not be displayed as intended. " +
"\n" +
"\n" +
"Please verify for yourself that " +
"Please verify for yourself that " +
fontFamily +
fontFamily +
" is being correctly represented in your experiment."
" is being correctly represented in your experiment."
);
);
}
}
});
});
});
});
loadBlockFiles(count - 1, callback);
loadBlockFiles(count - 1, callback);
},
},
});
});
};
};
var totalTrialConfig = {
var totalTrialConfig = {
initialVal: 1,
initialVal: 1,
fontSize: 20,
fontSize: 20,
x: window.innerWidth / 2,
x: window.innerWidth / 2,
y: -window.innerHeight / 2,
y: -window.innerHeight / 2,
fontName: "Arial",
fontName: "Arial",
alignHoriz: "right",
alignHoriz: "right",
alignVert: "bottom",
alignVert: "bottom",
};
};
var totalTrial, // TextSim object
var totalTrial, // TextSim object
totalTrialIndex = totalTrialConfig.initialVal, // numerical value of totalTrialIndex
totalTrialIndex = totalTrialConfig.initialVal, // numerical value of totalTrialIndex
totalTrialCount = 0;
totalTrialCount = 0;
var totalBlockConfig = {
var totalBlockConfig = {
initialVal: 0,
initialVal: 0,
};
};
var totalBlockIndex = totalBlockConfig.initialVal,
var totalBlockIndex = totalBlockConfig.initialVal,
totalBlockTrialList = [],
totalBlockTrialList = [],
totalBlockCount = 0;
totalBlockCount = 0;
const experiment = (blockCount) => {
const experiment = (blockCount) => {
////
////
// Resources
// Resources
const _resources = [];
const _resources = [];
for (let i = 1; i <= blockCount; i++) {
for (let i = 1; i <= blockCount; i++) {
_resources.push({
_resources.push({
name: `conditions/block_${i}.csv`,
name: `conditions/block_${i}.csv`,
path: `conditions/block_${i}.csv`,
path: `conditions/block_${i}.csv`,
});
});
}
}
if (debug) console.log("fontsRequired: ", fontsRequired);
if (debug) console.log("fontsRequired: ", fontsRequired);
Kopieren
Kopiert
Kopieren
Kopiert
fontsRequired
.forEach((fontFamily) => {
for (let i in
fontsRequired
) {
_resources.push({ name:
fontFamily
, path:
fontPath
});
if (debug) console.log(i, fontsRequired[i]);
}
);
_resources.push({ name:
i
, path:
fontsRequired[i]
});
}
// Start code blocks for 'Before Experiment'
// Start code blocks for 'Before Experiment'
// init psychoJS:
// init psychoJS:
const psychoJS = new PsychoJS({
const psychoJS = new PsychoJS({
debug: debug,
debug: debug,
});
});
/* ---------------------------------- Sound --------------------------------- */
/* ---------------------------------- Sound --------------------------------- */
const correctSynth = getCorrectSynth(psychoJS);
const correctSynth = getCorrectSynth(psychoJS);
const wrongSynth = getWrongSynth(psychoJS);
const wrongSynth = getWrongSynth(psychoJS);
const purrSynth = getPurrSynth(psychoJS);
const purrSynth = getPurrSynth(psychoJS);
// open window:
// open window:
psychoJS.openWindow({
psychoJS.openWindow({
fullscr: !debug,
fullscr: !debug,
color: new util.Color([0.9, 0.9, 0.9]),
color: new util.Color([0.9, 0.9, 0.9]),
units: "height", // TODO change to pix
units: "height", // TODO change to pix
waitBlanking: true,
waitBlanking: true,
});
});
// schedule the experiment:
// schedule the experiment:
psychoJS.schedule(
psychoJS.schedule(
psychoJS.gui.DlgFromDict({
psychoJS.gui.DlgFromDict({
dictionary: expInfo,
dictionary: expInfo,
title: expName,
title: expName,
})
})
);
);
const flowScheduler = new Scheduler(psychoJS);
const flowScheduler = new Scheduler(psychoJS);
const dialogCancelScheduler = new Scheduler(psychoJS);
const dialogCancelScheduler = new Scheduler(psychoJS);
psychoJS.scheduleCondition(
psychoJS.scheduleCondition(
function () {
function () {
return psychoJS.gui.dialogComponent.button === "OK";
return psychoJS.gui.dialogComponent.button === "OK";
},
},
flowScheduler,
flowScheduler,
dialogCancelScheduler
dialogCancelScheduler
);
);
// flowScheduler gets run if the participants presses OK
// flowScheduler gets run if the participants presses OK
flowScheduler.add(updateInfo); // add timeStamp
flowScheduler.add(updateInfo); // add timeStamp
flowScheduler.add(experimentInit);
flowScheduler.add(experimentInit);
flowScheduler.add(fileRoutineBegin());
flowScheduler.add(fileRoutineBegin());
flowScheduler.add(fileRoutineEachFrame());
flowScheduler.add(fileRoutineEachFrame());
flowScheduler.add(fileRoutineEnd());
flowScheduler.add(fileRoutineEnd());
// flowScheduler.add(initInstructionRoutineBegin());
// flowScheduler.add(initInstructionRoutineBegin());
// flowScheduler.add(initInstructionRoutineEachFrame());
// flowScheduler.add(initInstructionRoutineEachFrame());
// flowScheduler.add(initInstructionRoutineEnd());
// flowScheduler.add(initInstructionRoutineEnd());
const blocksLoopScheduler = new Scheduler(psychoJS);
const blocksLoopScheduler = new Scheduler(psychoJS);
flowScheduler.add(blocksLoopBegin(blocksLoopScheduler));
flowScheduler.add(blocksLoopBegin(blocksLoopScheduler));
flowScheduler.add(blocksLoopScheduler);
flowScheduler.add(blocksLoopScheduler);
flowScheduler.add(blocksLoopEnd);
flowScheduler.add(blocksLoopEnd);
flowScheduler.add(quitPsychoJS, "", true);
flowScheduler.add(quitPsychoJS, "", true);
// quit if user presses Cancel in dialog box:
// quit if user presses Cancel in dialog box:
dialogCancelScheduler.add(quitPsychoJS, "", false);
dialogCancelScheduler.add(quitPsychoJS, "", false);
if (useRC) {
if (useRC) {
expInfo["participant"] = rc.id.value;
expInfo["participant"] = rc.id.value;
}
}
if (debug) console.log("_resources: ", _resources);
if (debug) console.log("_resources: ", _resources);
psychoJS.start({
psychoJS.start({
expName: expName,
expName: expName,
expInfo: expInfo,
expInfo: expInfo,
resources: [
resources: [
{ name: "conditions/blockCount.csv", path: "conditions/blockCount.csv" },
{ name: "conditions/blockCount.csv", path: "conditions/blockCount.csv" },
..._resources,
..._resources,
],
],
});
});
psychoJS.experimentLogger.setLevel(core.Logger.ServerLevel.EXP);
psychoJS.experimentLogger.setLevel(core.Logger.ServerLevel.EXP);
var frameDur;
var frameDur;
async function updateInfo() {
async function updateInfo() {
expInfo["date"] = util.MonotonicClock.getDateStr(); // add a simple timestamp
expInfo["date"] = util.MonotonicClock.getDateStr(); // add a simple timestamp
expInfo["expName"] = expName;
expInfo["expName"] = expName;
expInfo["psychopyVersion"] = "2021.3.1";
expInfo["psychopyVersion"] = "2021.3.1";
expInfo["OS"] = rc.systemFamily.value;
expInfo["OS"] = rc.systemFamily.value;
// store frame rate of monitor if we can measure it successfully
// store frame rate of monitor if we can measure it successfully
expInfo["frameRate"] = psychoJS.window.getActualFrameRate();
expInfo["frameRate"] = psychoJS.window.getActualFrameRate();
if (typeof expInfo["frameRate"] !== "undefined")
if (typeof expInfo["frameRate"] !== "undefined")
frameDur = 1.0 / Math.round(expInfo["frameRate"]);
frameDur = 1.0 / Math.round(expInfo["frameRate"]);
else frameDur = 1.0 / 60.0; // couldn't get a reliable measure so guess
else frameDur = 1.0 / 60.0; // couldn't get a reliable measure so guess
// add info from the URL:
// add info from the URL:
util.addInfoFromUrl(expInfo);
util.addInfoFromUrl(expInfo);
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
}
}
var fileClock;
var fileClock;
var filterClock;
var filterClock;
var instructionsClock;
var instructionsClock;
var thisLoopNumber; // ! BLOCK COUNTER
var thisLoopNumber; // ! BLOCK COUNTER
var thisConditionsFile;
var thisConditionsFile;
var trialClock;
var trialClock;
// var targetBoundingPoly; // Target Bounding Box
// var targetBoundingPoly; // Target Bounding Box
var instructions;
var instructions;
var key_resp;
var key_resp;
var fixation; ////
var fixation; ////
var flanker1;
var flanker1;
var target;
var target;
var flanker2;
var flanker2;
var showAlphabet;
var showAlphabet;
var globalClock;
var globalClock;
var routineTimer;
var routineTimer;
async function experimentInit() {
async function experimentInit() {
// Initialize components for Routine "file"
// Initialize components for Routine "file"
fileClock = new util.Clock();
fileClock = new util.Clock();
// Initialize components for Routine "filter"
// Initialize components for Routine "filter"
filterClock = new util.Clock();
filterClock = new util.Clock();
instructionsClock = new util.Clock();
instructionsClock = new util.Clock();
thisLoopNumber = 0;
thisLoopNumber = 0;
thisConditionsFile = "./conditions/block_1.csv";
thisConditionsFile = "./conditions/block_1.csv";
// Initialize components for Routine "trial"
// Initialize components for Routine "trial"
trialClock = new util.Clock();
trialClock = new util.Clock();
// Target Bounding Box
// Target Bounding Box
// targetBoundingPoly = new visual.Rect ({
// targetBoundingPoly = new visual.Rect ({
// win: psychoJS.window, name: 'targetBoundingPoly', units : 'pix',
// win: psychoJS.window, name: 'targetBoundingPoly', units : 'pix',
// width: [1.0, 1.0][0], height: [1.0, 1.0][1],
// width: [1.0, 1.0][0], height: [1.0, 1.0][1],
// ori: 0.0, pos: [0, 0],
// ori: 0.0, pos: [0, 0],
// lineWidth: 1.0, lineColor: new util.Color('pink'),
// lineWidth: 1.0, lineColor: new util.Color('pink'),
// // fillColor: new util.Color('pink'),
// // fillColor: new util.Color('pink'),
// fillColor: undefined,
// fillColor: undefined,
// opacity: undefined, depth: -10, interpolate: true,
// opacity: undefined, depth: -10, interpolate: true,
// });
// });
key_resp = new core.Keyboard({
key_resp = new core.Keyboard({
psychoJS: psychoJS,
psychoJS: psychoJS,
clock: new util.Clock(),
clock: new util.Clock(),
waitForStart: true,
waitForStart: true,
});
});
fixation = new visual.TextStim({
fixation = new visual.TextStim({
win: psychoJS.window,
win: psychoJS.window,
name: "fixation",
name: "fixation",
text: "+",
text: "+",
font: "Open Sans",
font: "Open Sans",
units: "pix",
units: "pix",
pos: [0, 0],
pos: [0, 0],
height: 1.0,
height: 1.0,
wrapWidth: undefined,
wrapWidth: undefined,
ori: 0.0,
ori: 0.0,
color: new util.Color("black"),
color: new util.Color("black"),
opacity: undefined,
opacity: undefined,
depth: -6.0,
depth: -6.0,
});
});
flanker1 = new visual.TextStim({
flanker1 = new visual.TextStim({
win: psychoJS.window,
win: psychoJS.window,
name: "flanker1",
name: "flanker1",
text: "",
text: "",
font: "Arial",
font: "Arial",
units: "pix",
units: "pix",
pos: [0, 0],
pos: [0, 0],
height: 1.0,
height: 1.0,
wrapWidth: undefined,
wrapWidth: undefined,
ori: 0.0,
ori: 0.0,
color: new util.Color("black"),
color: new util.Color("black"),
opacity: 1.0,
opacity: 1.0,
depth: -7.0,
depth: -7.0,
});
});
target = new visual.TextStim({
target = new visual.TextStim({
win: psychoJS.window,
win: psychoJS.window,
name: "target",
name: "target",
text: "",
text: "",
font: "Arial",
font: "Arial",
units: "pix",
units: "pix",
pos: [0, 0],
pos: [0, 0],
height: 1.0,
height: 1.0,
wrapWidth: undefined,
wrapWidth: undefined,
ori: 0.0,
ori: 0.0,
color: new util.Color("black"),
color: new util.Color("black"),
opacity: 1.0,
opacity: 1.0,
depth: -8.0,
depth: -8.0,
});
});
flanker2 = new visual.TextStim({
flanker2 = new visual.TextStim({
win: psychoJS.window,
win: psychoJS.window,
name: "flanker2",
name: "flanker2",
text: "",
text: "",
font: "Arial",
font: "Arial",
units: "pix",
units: "pix",
pos: [0, 0],
pos: [0, 0],
height: 1.0,
height: 1.0,
wrapWidth: undefined,
wrapWidth: undefined,
ori: 0.0,
ori: 0.0,
color: new util.Color("black"),
color: new util.Color("black"),
opacity: 1.0,
opacity: 1.0,
depth: -9.0,
depth: -9.0,
});
});
showAlphabet = new visual.TextStim({
showAlphabet = new visual.TextStim({
win: psychoJS.window,
win: psychoJS.window,
name: "showAlphabet",
name: "showAlphabet",
text: "",
text: "",
font: "Arial",
font: "Arial",
units: "pix",
units: "pix",
pos: [0, 0],
pos: [0, 0],
height: 1.0,
height: 1.0,
wrapWidth: window.innerWidth,
wrapWidth: window.innerWidth,
ori: 0.0,
ori: 0.0,
color: new util.Color("black"),
color: new util.Color("black"),
opacity: 1.0,
opacity: 1.0,
depth: -5.0,
depth: -5.0,
});
});
totalTrial = new visual.TextStim({
totalTrial = new visual.TextStim({
win: psychoJS.window,
win: psychoJS.window,
name: "totalTrial",
name: "totalTrial",
text: "",
text: "",
font: totalTrialConfig.fontName,
font: totalTrialConfig.fontName,
units: "pix",
units: "pix",
pos: [totalTrialConfig.x, totalTrialConfig.y],
pos: [totalTrialConfig.x, totalTrialConfig.y],
alignHoriz: totalTrialConfig.alignHoriz,
alignHoriz: totalTrialConfig.alignHoriz,
alignVert: totalTrialConfig.alignVert,
alignVert: totalTrialConfig.alignVert,
height: 1.0,
height: 1.0,
wrapWidth: undefined,
wrapWidth: undefined,
ori: 0.0,
ori: 0.0,
color: new util.Color("black"),
color: new util.Color("black"),
opacity: 1.0,
opacity: 1.0,
depth: -20.0,
depth: -20.0,
});
});
instructions = new visual.TextStim({
instructions = new visual.TextStim({
win: psychoJS.window,
win: psychoJS.window,
name: "instructions",
name: "instructions",
text: "",
text: "",
font: "Arial",
font: "Arial",
units: "pix",
units: "pix",
pos: [-window.innerWidth * 0.4, window.innerHeight * 0.4],
pos: [-window.innerWidth * 0.4, window.innerHeight * 0.4],
height: 32.0,
height: 32.0,
wrapWidth: window.innerWidth * 0.8,
wrapWidth: window.innerWidth * 0.8,
ori: 0.0,
ori: 0.0,
color: new util.Color("black"),
color: new util.Color("black"),
opacity: 1.0,
opacity: 1.0,
depth: -12.0,
depth: -12.0,
alignHoriz: "left",
alignHoriz: "left",
alignVert: "top",
alignVert: "top",
});
});
// Create some handy timers
// Create some handy timers
globalClock = new util.Clock(); // to track the time since experiment started
globalClock = new util.Clock(); // to track the time since experiment started
routineTimer = new util.CountdownTimer(); // to track time remaining of each (non-slip) routine
routineTimer = new util.CountdownTimer(); // to track time remaining of each (non-slip) routine
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
}
}
var t;
var t;
var frameN;
var frameN;
var continueRoutine;
var continueRoutine;
var fileComponents;
var fileComponents;
var clickedContinue;
var clickedContinue;
// TODO Read from config
// TODO Read from config
var responseType = 2;
var responseType = 2;
function fileRoutineBegin(snapshot) {
function fileRoutineBegin(snapshot) {
return async function () {
return async function () {
TrialHandler.fromSnapshot(snapshot); // ensure that .thisN vals are up to date
TrialHandler.fromSnapshot(snapshot); // ensure that .thisN vals are up to date
//------Prepare to start Routine 'file'-------
//------Prepare to start Routine 'file'-------
t = 0;
t = 0;
fileClock.reset(); // clock
fileClock.reset(); // clock
frameN = -1;
frameN = -1;
continueRoutine = true; // until we're told otherwise
continueRoutine = true; // until we're told otherwise
// update component parameters for each repeat
// update component parameters for each repeat
// keep track of which components have finished
// keep track of which components have finished
fileComponents = [];
fileComponents = [];
for (const thisComponent of fileComponents)
for (const thisComponent of fileComponents)
if ("status" in thisComponent)
if ("status" in thisComponent)
thisComponent.status = PsychoJS.Status.NOT_STARTED;
thisComponent.status = PsychoJS.Status.NOT_STARTED;
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
};
};
}
}
function fileRoutineEachFrame() {
function fileRoutineEachFrame() {
return async function () {
return async function () {
//------Loop for each frame of Routine 'file'-------
//------Loop for each frame of Routine 'file'-------
// get current time
// get current time
t = fileClock.getTime();
t = fileClock.getTime();
frameN = frameN + 1; // number of completed frames (so 0 is the first frame)
frameN = frameN + 1; // number of completed frames (so 0 is the first frame)
// update/draw components on each frame
// update/draw components on each frame
// check for quit (typically the Esc key)
// check for quit (typically the Esc key)
if (
if (
psychoJS.experiment.experimentEnded ||
psychoJS.experiment.experimentEnded ||
psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0
psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0
) {
) {
return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false);
return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false);
}
}
// check if the Routine should terminate
// check if the Routine should terminate
if (!continueRoutine) {
if (!continueRoutine) {
// a component has requested a forced-end of Routine
// a component has requested a forced-end of Routine
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
}
}
continueRoutine = false; // reverts to True if at least one component still running
continueRoutine = false; // reverts to True if at least one component still running
for (const thisComponent of fileComponents)
for (const thisComponent of fileComponents)
if (
if (
"status" in thisComponent &&
"status" in thisComponent &&
thisComponent.status !== PsychoJS.Status.FINISHED
thisComponent.status !== PsychoJS.Status.FINISHED
) {
) {
continueRoutine = true;
continueRoutine = true;
break;
break;
}
}
// refresh the screen if continuing
// refresh the screen if continuing
if (continueRoutine) {
if (continueRoutine) {
return Scheduler.Event.FLIP_REPEAT;
return Scheduler.Event.FLIP_REPEAT;
} else {
} else {
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
}
}
};
};
}
}
function fileRoutineEnd() {
function fileRoutineEnd() {
return async function () {
return async function () {
//------Ending Routine 'file'-------
//------Ending Routine 'file'-------
for (const thisComponent of fileComponents) {
for (const thisComponent of fileComponents) {
if (typeof thisComponent.setAutoDraw === "function") {
if (typeof thisComponent.setAutoDraw === "function") {
thisComponent.setAutoDraw(false);
thisComponent.setAutoDraw(false);
}
}
}
}
// the Routine "file" was not non-slip safe, so reset the non-slip timer
// the Routine "file" was not non-slip safe, so reset the non-slip timer
routineTimer.reset();
routineTimer.reset();
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
};
};
}
}
var _beepButton;
var _beepButton;
function _instructionSetup(text) {
function _instructionSetup(text) {
t = 0;
t = 0;
instructionsClock.reset(); // clock
instructionsClock.reset(); // clock
frameN = -1;
frameN = -1;
continueRoutine = true;
continueRoutine = true;
instructions.setWrapWidth(window.innerWidth * 0.8);
instructions.setWrapWidth(window.innerWidth * 0.8);
instructions.setPos([-window.innerWidth * 0.4, window.innerHeight * 0.4]);
instructions.setPos([-window.innerWidth * 0.4, window.innerHeight * 0.4]);
instructions.setText(text);
instructions.setText(text);
instructions.setAutoDraw(true);
instructions.setAutoDraw(true);
}
}
function _clickContinue(e) {
function _clickContinue(e) {
if (e.target.id !== "threshold-beep-button") clickedContinue = true;
if (e.target.id !== "threshold-beep-button") clickedContinue = true;
}
}
async function _instructionRoutineEachFrame() {
async function _instructionRoutineEachFrame() {
t = instructionsClock.getTime();
t = instructionsClock.getTime();
frameN = frameN + 1;
frameN = frameN + 1;
if (
if (
psychoJS.experiment.experimentEnded ||
psychoJS.experiment.experimentEnded ||
psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0
psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0
) {
) {
document.removeEventListener("click", _clickContinue);
document.removeEventListener("click", _clickContinue);
document.removeEventListener("touchend", _clickContinue);
document.removeEventListener("touchend", _clickContinue);
removeBeepButton(_beepButton);
removeBeepButton(_beepButton);
return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false);
return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false);
}
}
if (!continueRoutine) {
if (!continueRoutine) {
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
}
}
continueRoutine = true;
continueRoutine = true;
if (psychoJS.eventManager.getKeys({ keyList: ["return"] }).length > 0) {
if (psychoJS.eventManager.getKeys({ keyList: ["return"] }).length > 0) {
continueRoutine = false;
continueRoutine = false;
}
}
if (continueRoutine && !clickedContinue) {
if (continueRoutine && !clickedContinue) {
return Scheduler.Event.FLIP_REPEAT;
return Scheduler.Event.FLIP_REPEAT;
} else {
} else {
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
}
}
}
}
async function _instructionRoutineEnd() {
async function _instructionRoutineEnd() {
instructions.setAutoDraw(false);
instructions.setAutoDraw(false);
routineTimer.reset();
routineTimer.reset();
document.removeEventListener("click", _clickContinue);
document.removeEventListener("click", _clickContinue);
document.removeEventListener("touchend", _clickContinue);
document.removeEventListener("touchend", _clickContinue);
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
}
}
var blocks;
var blocks;
var currentLoop;
var currentLoop;
function blocksLoopBegin(blocksLoopScheduler, snapshot) {
function blocksLoopBegin(blocksLoopScheduler, snapshot) {
return async function () {
return async function () {
TrialHandler.fromSnapshot(snapshot); // update internal variables (.thisN etc) of the loop
TrialHandler.fromSnapshot(snapshot); // update internal variables (.thisN etc) of the loop
// set up handler to look after randomisation of conditions etc
// set up handler to look after randomisation of conditions etc
blocks = new TrialHandler({
blocks = new TrialHandler({
psychoJS: psychoJS,
psychoJS: psychoJS,
nReps: 1,
nReps: 1,
method: TrialHandler.Method.SEQUENTIAL,
method: TrialHandler.Method.SEQUENTIAL,
extraInfo: expInfo,
extraInfo: expInfo,
originPath: undefined,
originPath: undefined,
trialList: "conditions/blockCount.csv",
trialList: "conditions/blockCount.csv",
seed: undefined,
seed: undefined,
name: "blocks",
name: "blocks",
});
});
psychoJS.experiment.addLoop(blocks); // add the loop to the experiment
psychoJS.experiment.addLoop(blocks); // add the loop to the experiment
currentLoop = blocks; // we're now the current loop
currentLoop = blocks; // we're now the current loop
// Schedule all the trials in the trialList:
// Schedule all the trials in the trialList:
for (const thisBlock of blocks) {
for (const thisBlock of blocks) {
const snapshot = blocks.getSnapshot();
const snapshot = blocks.getSnapshot();
blocksLoopScheduler.add(importConditions(snapshot));
blocksLoopScheduler.add(importConditions(snapshot));
blocksLoopScheduler.add(filterRoutineBegin(snapshot));
blocksLoopScheduler.add(filterRoutineBegin(snapshot));
blocksLoopScheduler.add(filterRoutineEachFrame());
blocksLoopScheduler.add(filterRoutineEachFrame());
blocksLoopScheduler.add(filterRoutineEnd());
blocksLoopScheduler.add(filterRoutineEnd());
blocksLoopScheduler.add(initInstructionRoutineBegin(snapshot));
blocksLoopScheduler.add(initInstructionRoutineBegin(snapshot));
blocksLoopScheduler.add(initInstructionRoutineEachFrame());
blocksLoopScheduler.add(initInstructionRoutineEachFrame());
blocksLoopScheduler.add(initInstructionRoutineEnd());
blocksLoopScheduler.add(initInstructionRoutineEnd());
const trialsLoopScheduler = new Scheduler(psychoJS);
const trialsLoopScheduler = new Scheduler(psychoJS);
blocksLoopScheduler.add(trialsLoopBegin(trialsLoopScheduler, snapshot));
blocksLoopScheduler.add(trialsLoopBegin(trialsLoopScheduler, snapshot));
blocksLoopScheduler.add(trialsLoopScheduler);
blocksLoopScheduler.add(trialsLoopScheduler);
blocksLoopScheduler.add(trialsLoopEnd);
blocksLoopScheduler.add(trialsLoopEnd);
blocksLoopScheduler.add(
blocksLoopScheduler.add(
endLoopIteration(blocksLoopScheduler, snapshot)
endLoopIteration(blocksLoopScheduler, snapshot)
);
);
}
}
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
};
};
}
}
var trialsConditions;
var trialsConditions;
var trials;
var trials;
function trialsLoopBegin(trialsLoopScheduler, snapshot) {
function trialsLoopBegin(trialsLoopScheduler, snapshot) {
return async function () {
return async function () {
// setup a MultiStairTrialHandler
// setup a MultiStairTrialHandler
trialsConditions = TrialHandler.importConditions(
trialsConditions = TrialHandler.importConditions(
psychoJS.serverManager,
psychoJS.serverManager,
thisConditionsFile
thisConditionsFile
);
);
trials = new data.MultiStairHandler({
trials = new data.MultiStairHandler({
stairType: MultiStairHandler.StaircaseType.QUEST,
stairType: MultiStairHandler.StaircaseType.QUEST,
psychoJS: psychoJS,
psychoJS: psychoJS,
name: "trials",
name: "trials",
varName: "trialsVal",
varName: "trialsVal",
nTrials: conditionTrials,
nTrials: conditionTrials,
conditions: trialsConditions,
conditions: trialsConditions,
method: TrialHandler.Method.FULLRANDOM,
method: TrialHandler.Method.FULLRANDOM,
});
});
psychoJS.experiment.addLoop(trials); // add the loop to the experiment
psychoJS.experiment.addLoop(trials); // add the loop to the experiment
currentLoop = trials; // we're now the current loop
currentLoop = trials; // we're now the current loop
// Schedule all the trials in the trialList:
// Schedule all the trials in the trialList:
for (const thisQuestLoop of trials) {
for (const thisQuestLoop of trials) {
const snapshot = trials.getSnapshot();
const snapshot = trials.getSnapshot();
trialsLoopScheduler.add(importConditions(snapshot));
trialsLoopScheduler.add(importConditions(snapshot));
trialsLoopScheduler.add(trialInstructionRoutineBegin(snapshot));
trialsLoopScheduler.add(trialInstructionRoutineBegin(snapshot));
trialsLoopScheduler.add(trialInstructionRoutineEachFrame());
trialsLoopScheduler.add(trialInstructionRoutineEachFrame());
trialsLoopScheduler.add(trialInstructionRoutineEnd());
trialsLoopScheduler.add(trialInstructionRoutineEnd());
trialsLoopScheduler.add(trialRoutineBegin(snapshot));
trialsLoopScheduler.add(trialRoutineBegin(snapshot));
trialsLoopScheduler.add(trialRoutineEachFrame());
trialsLoopScheduler.add(trialRoutineEachFrame());
trialsLoopScheduler.add(trialRoutineEnd());
trialsLoopScheduler.add(trialRoutineEnd());
trialsLoopScheduler.add(
trialsLoopScheduler.add(
endLoopIteration(trialsLoopScheduler, snapshot)
endLoopIteration(trialsLoopScheduler, snapshot)
);
);
}
}
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
};
};
}
}
async function trialsLoopEnd() {
async function trialsLoopEnd() {
psychoJS.experiment.addData(
psychoJS.experiment.addData(
"staircaseName",
"staircaseName",
currentLoop._currentStaircase._name
currentLoop._currentStaircase._name
);
);
psychoJS.experiment.addData(
psychoJS.experiment.addData(
"questMeanAtEndOfTrialsLoop",
"questMeanAtEndOfTrialsLoop",
currentLoop._currentStaircase.mean()
currentLoop._currentStaircase.mean()
);
);
psychoJS.experiment.addData(
psychoJS.experiment.addData(
"questSDAtEndOfTrialsLoop",
"questSDAtEndOfTrialsLoop",
currentLoop._currentStaircase.sd()
currentLoop._currentStaircase.sd()
);
);
psychoJS.experiment.addData(
psychoJS.experiment.addData(
"questQuantileOfQuantileOrderAtEndOfTrialsLoop",
"questQuantileOfQuantileOrderAtEndOfTrialsLoop",
currentLoop._currentStaircase.quantile(
currentLoop._currentStaircase.quantile(
currentLoop._currentStaircase._jsQuest.quantileOrder
currentLoop._currentStaircase._jsQuest.quantileOrder
)
)
);
);
// terminate loop
// terminate loop
psychoJS.experiment.removeLoop(trials);
psychoJS.experiment.removeLoop(trials);
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
}
}
async function blocksLoopEnd() {
async function blocksLoopEnd() {
psychoJS.experiment.removeLoop(blocks);
psychoJS.experiment.removeLoop(blocks);
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
}
}
var filterComponents;
var filterComponents;
function filterRoutineBegin(snapshot) {
function filterRoutineBegin(snapshot) {
return async function () {
return async function () {
TrialHandler.fromSnapshot(snapshot); // ensure that .thisN vals are up to date
TrialHandler.fromSnapshot(snapshot); // ensure that .thisN vals are up to date
//------Prepare to start Routine 'filter'-------
//------Prepare to start Routine 'filter'-------
t = 0;
t = 0;
filterClock.reset(); // clock
filterClock.reset(); // clock
frameN = -1;
frameN = -1;
continueRoutine = true; // until we're told otherwise
continueRoutine = true; // until we're told otherwise
// update component parameters for each repeat
// update component parameters for each repeat
thisLoopNumber += 1;
thisLoopNumber += 1;
thisConditionsFile = `conditions/block_${thisLoopNumber}.csv`;
thisConditionsFile = `conditions/block_${thisLoopNumber}.csv`;
const possibleTrials = [];
const possibleTrials = [];
const thisBlockFileData = blockFiles[thisLoopNumber];
const thisBlockFileData = blockFiles[thisLoopNumber];
if (debug) console.log("thisBlockFileData: ", thisBlockFileData);
if (debug) console.log("thisBlockFileData: ", thisBlockFileData);
for (let rowKey in thisBlockFileData) {
for (let rowKey in thisBlockFileData) {
let rowIndex = parseInt(rowKey);
let rowIndex = parseInt(rowKey);
if (Object.keys(thisBlockFileData[rowIndex]).length > 1) {
if (Object.keys(thisBlockFileData[rowIndex]).length > 1) {
if (debug)
if (debug)
console.log(
console.log(
"condition trials this row of block: ",
"condition trials this row of block: ",
parseInt(thisBlockFileData[rowIndex]["conditionTrials"])
parseInt(thisBlockFileData[rowIndex]["conditionTrials"])
);
);
possibleTrials.push(
possibleTrials.push(
parseInt(thisBlockFileData[rowIndex]["conditionTrials"])
parseInt(thisBlockFileData[rowIndex]["conditionTrials"])
);
);
}
}
}
}
if (debug) console.log("possibleTrials: ", possibleTrials);
if (debug) console.log("possibleTrials: ", possibleTrials);
totalTrialCount = possibleTrials.reduce((a, b) => a + b, 0); // sum of possible trials
totalTrialCount = possibleTrials.reduce((a, b) => a + b, 0); // sum of possible trials
totalBlockCount = Object.keys(blockFiles).length;
totalBlockCount = Object.keys(blockFiles).length;
totalBlockTrialList = [...possibleTrials];
totalBlockTrialList = [...possibleTrials];
// console.log('totalBlockTrialList', totalBlockTrialList)
// console.log('totalBlockTrialList', totalBlockTrialList)
// totalBlockCount = blockFiles.length;
// totalBlockCount = blockFiles.length;
// TODO Remove this constraint to allow different # of trials for each condition
// TODO Remove this constraint to allow different # of trials for each condition
if (!possibleTrials.every((a) => a === possibleTrials[0]))
if (!possibleTrials.every((a) => a === possibleTrials[0]))
throw "Number of trials for each condition within one block has to be equal. (Will be updated soon.)";
throw "Number of trials for each condition within one block has to be equal. (Will be updated soon.)";
conditionTrials = possibleTrials[0];
conditionTrials = possibleTrials[0];
// keep track of which components have finished
// keep track of which components have finished
filterComponents = [];
filterComponents = [];
for (const thisComponent of filterComponents)
for (const thisComponent of filterComponents)
if ("status" in thisComponent)
if ("status" in thisComponent)
thisComponent.status = PsychoJS.Status.NOT_STARTED;
thisComponent.status = PsychoJS.Status.NOT_STARTED;
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
};
};
}
}
function filterRoutineEachFrame() {
function filterRoutineEachFrame() {
return async function () {
return async function () {
//------Loop for each frame of Routine 'filter'-------
//------Loop for each frame of Routine 'filter'-------
// get current time
// get current time
t = filterClock.getTime();
t = filterClock.getTime();
frameN = frameN + 1; // number of completed frames (so 0 is the first frame)
frameN = frameN + 1; // number of completed frames (so 0 is the first frame)
// update/draw components on each frame
// update/draw components on each frame
// check for quit (typically the Esc key)
// check for quit (typically the Esc key)
if (
if (
psychoJS.experiment.experimentEnded ||
psychoJS.experiment.experimentEnded ||
psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0
psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0
) {
) {
return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false);
return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false);
}
}
// check if the Routine should terminate
// check if the Routine should terminate
if (!continueRoutine) {
if (!continueRoutine) {
// a component has requested a forced-end of Routine
// a component has requested a forced-end of Routine
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
}
}
continueRoutine = false; // reverts to True if at least one component still running
continueRoutine = false; // reverts to True if at least one component still running
for (const thisComponent of filterComponents)
for (const thisComponent of filterComponents)
if (
if (
"status" in thisComponent &&
"status" in thisComponent &&
thisComponent.status !== PsychoJS.Status.FINISHED
thisComponent.status !== PsychoJS.Status.FINISHED
) {
) {
continueRoutine = true;
continueRoutine = true;
break;
break;
}
}
// refresh the screen if continuing
// refresh the screen if continuing
if (continueRoutine) {
if (continueRoutine) {
return Scheduler.Event.FLIP_REPEAT;
return Scheduler.Event.FLIP_REPEAT;
} else {
} else {
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
}
}
};
};
}
}
function filterRoutineEnd() {
function filterRoutineEnd() {
return async function () {
return async function () {
//------Ending Routine 'filter'-------
//------Ending Routine 'filter'-------
for (const thisComponent of filterComponents) {
for (const thisComponent of filterComponents) {
if (typeof thisComponent.setAutoDraw === "function") {
if (typeof thisComponent.setAutoDraw === "function") {
thisComponent.setAutoDraw(false);
thisComponent.setAutoDraw(false);
}
}
}
}
// the Routine "filter" was not non-slip safe, so reset the non-slip timer
// the Routine "filter" was not non-slip safe, so reset the non-slip timer
routineTimer.reset();
routineTimer.reset();
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
};
};
}
}
function initInstructionRoutineBegin(snapshot) {
function initInstructionRoutineBegin(snapshot) {
return async function () {
return async function () {
TrialHandler.fromSnapshot(snapshot);
TrialHandler.fromSnapshot(snapshot);
_instructionSetup(
_instructionSetup(
instructionsText.initial(expInfo.participant) +
instructionsText.initial(expInfo.participant) +
instructionsText.initialByThresholdParameter["spacing"](
instructionsText.initialByThresholdParameter["spacing"](
responseType,
responseType,
totalTrialCount
totalTrialCount
) +
) +
instructionsText.initialEnd(responseType)
instructionsText.initialEnd(responseType)
);
);
clickedContinue = false;
clickedContinue = false;
document.addEventListener("click", _clickContinue);
document.addEventListener("click", _clickContinue);
document.addEventListener("touchend", _clickContinue);
document.addEventListener("touchend", _clickContinue);
_beepButton = addBeepButton(correctSynth);
_beepButton = addBeepButton(correctSynth);
psychoJS.eventManager.clearKeys();
psychoJS.eventManager.clearKeys();
return Scheduler.Event.NEXT;
return Scheduler.Event.NEXT;
};
};
}
}
function initInstructionRoutineEachFrame() {
function initInstructionRoutineEachFrame() {
return _instructionRoutineEachFrame;
return _instructionRoutineEachFrame;
}
}
function initInstructionRoutineEnd() {
function initInstructionRoutineEnd() {
return async function () {
return async function () {
instructions.setAutoDraw(false);
instructions.setAutoDraw(false);
routineTimer.reset();
routineTimer.reset();
Kopieren
Kopiert
Kopieren
Kopiert
document.removeEventListener("click", _
clickContinue);
document.removeEventListener("click", _
clickC
document.removeEventListener("touchend", _clickContinue);
removeBeepButton(_beepButton);
return Scheduler.Event.NEXT;
};
}
function blockInstructionRoutineBegin(snapshot) {
return async function () {
TrialHandler.fromSnapshot(snapshot);
_instructionSetup(instructionsText.block(snapshot.b
Gespeicherte Diffs
Originaltext
Datei öffnen
/***************** * Crowding Test * *****************/ const debug = false; import { core, data, util, visual } from "./psychojs/out/psychojs-2021.3.0.js"; const { PsychoJS } = core; const { TrialHandler, MultiStairHandler } = data; const { Scheduler } = util; //// import * as jsQUEST from "./lib/jsQUEST.module.js"; //// /* ------------------------------- Components ------------------------------- */ import { hideCursor, showCursor, shuffle } from "./components/utils.js"; import { addBeepButton, instructionsText, removeBeepButton, } from "./components/instructions.js"; import { calculateBlockWithTrialIndex } from "./components/trialCounter.js"; import { getCorrectSynth, getWrongSynth, getPurrSynth, } from "./components/sound.js"; import { removeClickableAlphabet, setupClickableAlphabet, } from "./components/showAlphabet.js"; /* -------------------------------------------------------------------------- */ window.jsQUEST = jsQUEST; var conditionTrials; var levelLeft, levelRight; let correctAns; // For development purposes, toggle RC off for testing speed const useRC = !debug; const rc = RemoteCalibrator; rc.init(); // store info about the experiment session: let expName = "Threshold"; // from the Builder filename that created this script let expInfo = { participant: debug ? rc.id.value : "", session: "001" }; const fontsRequired = new Set(); //// // blockCount is just a file telling the program how many blocks in total Papa.parse("conditions/blockCount.csv", { download: true, complete: function (results) { const blockCount = results.data.length - 2; // TODO Make this calculation robust loadBlockFiles(blockCount, () => { if (useRC) { rc.panel( [ { name: "screenSize", }, { name: "trackDistance", options: { nearPoint: false, showVideo: false, }, }, ], "body", {}, () => { rc.removePanel(); // ! Start actual experiment experiment(blockCount); } ); } else { // NO RC experiment(blockCount); } }); }, }); const blockFiles = {}; const loadBlockFiles = (count, callback) => { if (count === 0) { callback(); return; } Papa.parse(`conditions/block_${count}.csv`, { download: true, header: true, skipEmptyLines: true, dynamicTyping: true, complete: function (results) { blockFiles[count] = results.data; if (debug) console.log("Block " + count + ": ", results.data); Object.values(results.data).forEach((row) => { let fontFamily = row["targetFont"]; let fontTestString = "12px " + fontFamily; let fontPath = "fonts/" + fontFamily + ".woff"; if (debug) console.log("fontTestString: ", fontTestString); let response = fetch(fontPath).then((response) => { if (response.ok) { fontsRequired.add(row["targetFont"]); } else { console.log( "Does the browser consider this font supported?", document.fonts.check(fontTestString) ); console.log( "Uh oh, unable to find the font file for: " + fontFamily + "\n" + "If this font is already supported by the browser then it should display correctly. " + "\n" + "If not, however, a different fallback font will be chosen by the browser, and your stimulus will not be displayed as intended. " + "\n" + "Please verify for yourself that " + fontFamily + " is being correctly represented in your experiment." ); } }); }); loadBlockFiles(count - 1, callback); }, }); }; var totalTrialConfig = { initialVal: 1, fontSize: 20, x: window.innerWidth / 2, y: -window.innerHeight / 2, fontName: "Arial", alignHoriz: "right", alignVert: "bottom", }; var totalTrial, // TextSim object totalTrialIndex = totalTrialConfig.initialVal, // numerical value of totalTrialIndex totalTrialCount = 0; var totalBlockConfig = { initialVal: 0, }; var totalBlockIndex = totalBlockConfig.initialVal, totalBlockTrialList = [], totalBlockCount = 0; const experiment = (blockCount) => { //// // Resources const _resources = []; for (let i = 1; i <= blockCount; i++) { _resources.push({ name: `conditions/block_${i}.csv`, path: `conditions/block_${i}.csv`, }); } if (debug) console.log("fontsRequired: ", fontsRequired); fontsRequired.forEach((fontFamily) => { _resources.push({ name: fontFamily, path: fontPath }); }); // Start code blocks for 'Before Experiment' // init psychoJS: const psychoJS = new PsychoJS({ debug: debug, }); /* ---------------------------------- Sound --------------------------------- */ const correctSynth = getCorrectSynth(psychoJS); const wrongSynth = getWrongSynth(psychoJS); const purrSynth = getPurrSynth(psychoJS); // open window: psychoJS.openWindow({ fullscr: !debug, color: new util.Color([0.9, 0.9, 0.9]), units: "height", // TODO change to pix waitBlanking: true, }); // schedule the experiment: psychoJS.schedule( psychoJS.gui.DlgFromDict({ dictionary: expInfo, title: expName, }) ); const flowScheduler = new Scheduler(psychoJS); const dialogCancelScheduler = new Scheduler(psychoJS); psychoJS.scheduleCondition( function () { return psychoJS.gui.dialogComponent.button === "OK"; }, flowScheduler, dialogCancelScheduler ); // flowScheduler gets run if the participants presses OK flowScheduler.add(updateInfo); // add timeStamp flowScheduler.add(experimentInit); flowScheduler.add(fileRoutineBegin()); flowScheduler.add(fileRoutineEachFrame()); flowScheduler.add(fileRoutineEnd()); // flowScheduler.add(initInstructionRoutineBegin()); // flowScheduler.add(initInstructionRoutineEachFrame()); // flowScheduler.add(initInstructionRoutineEnd()); const blocksLoopScheduler = new Scheduler(psychoJS); flowScheduler.add(blocksLoopBegin(blocksLoopScheduler)); flowScheduler.add(blocksLoopScheduler); flowScheduler.add(blocksLoopEnd); flowScheduler.add(quitPsychoJS, "", true); // quit if user presses Cancel in dialog box: dialogCancelScheduler.add(quitPsychoJS, "", false); if (useRC) { expInfo["participant"] = rc.id.value; } if (debug) console.log("_resources: ", _resources); psychoJS.start({ expName: expName, expInfo: expInfo, resources: [ { name: "conditions/blockCount.csv", path: "conditions/blockCount.csv" }, ..._resources, ], }); psychoJS.experimentLogger.setLevel(core.Logger.ServerLevel.EXP); var frameDur; async function updateInfo() { expInfo["date"] = util.MonotonicClock.getDateStr(); // add a simple timestamp expInfo["expName"] = expName; expInfo["psychopyVersion"] = "2021.3.1"; expInfo["OS"] = rc.systemFamily.value; // store frame rate of monitor if we can measure it successfully expInfo["frameRate"] = psychoJS.window.getActualFrameRate(); if (typeof expInfo["frameRate"] !== "undefined") frameDur = 1.0 / Math.round(expInfo["frameRate"]); else frameDur = 1.0 / 60.0; // couldn't get a reliable measure so guess // add info from the URL: util.addInfoFromUrl(expInfo); return Scheduler.Event.NEXT; } var fileClock; var filterClock; var instructionsClock; var thisLoopNumber; // ! BLOCK COUNTER var thisConditionsFile; var trialClock; // var targetBoundingPoly; // Target Bounding Box var instructions; var key_resp; var fixation; //// var flanker1; var target; var flanker2; var showAlphabet; var globalClock; var routineTimer; async function experimentInit() { // Initialize components for Routine "file" fileClock = new util.Clock(); // Initialize components for Routine "filter" filterClock = new util.Clock(); instructionsClock = new util.Clock(); thisLoopNumber = 0; thisConditionsFile = "./conditions/block_1.csv"; // Initialize components for Routine "trial" trialClock = new util.Clock(); // Target Bounding Box // targetBoundingPoly = new visual.Rect ({ // win: psychoJS.window, name: 'targetBoundingPoly', units : 'pix', // width: [1.0, 1.0][0], height: [1.0, 1.0][1], // ori: 0.0, pos: [0, 0], // lineWidth: 1.0, lineColor: new util.Color('pink'), // // fillColor: new util.Color('pink'), // fillColor: undefined, // opacity: undefined, depth: -10, interpolate: true, // }); key_resp = new core.Keyboard({ psychoJS: psychoJS, clock: new util.Clock(), waitForStart: true, }); fixation = new visual.TextStim({ win: psychoJS.window, name: "fixation", text: "+", font: "Open Sans", units: "pix", pos: [0, 0], height: 1.0, wrapWidth: undefined, ori: 0.0, color: new util.Color("black"), opacity: undefined, depth: -6.0, }); flanker1 = new visual.TextStim({ win: psychoJS.window, name: "flanker1", text: "", font: "Arial", units: "pix", pos: [0, 0], height: 1.0, wrapWidth: undefined, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -7.0, }); target = new visual.TextStim({ win: psychoJS.window, name: "target", text: "", font: "Arial", units: "pix", pos: [0, 0], height: 1.0, wrapWidth: undefined, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -8.0, }); flanker2 = new visual.TextStim({ win: psychoJS.window, name: "flanker2", text: "", font: "Arial", units: "pix", pos: [0, 0], height: 1.0, wrapWidth: undefined, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -9.0, }); showAlphabet = new visual.TextStim({ win: psychoJS.window, name: "showAlphabet", text: "", font: "Arial", units: "pix", pos: [0, 0], height: 1.0, wrapWidth: window.innerWidth, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -5.0, }); totalTrial = new visual.TextStim({ win: psychoJS.window, name: "totalTrial", text: "", font: totalTrialConfig.fontName, units: "pix", pos: [totalTrialConfig.x, totalTrialConfig.y], alignHoriz: totalTrialConfig.alignHoriz, alignVert: totalTrialConfig.alignVert, height: 1.0, wrapWidth: undefined, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -20.0, }); instructions = new visual.TextStim({ win: psychoJS.window, name: "instructions", text: "", font: "Arial", units: "pix", pos: [-window.innerWidth * 0.4, window.innerHeight * 0.4], height: 32.0, wrapWidth: window.innerWidth * 0.8, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -12.0, alignHoriz: "left", alignVert: "top", }); // Create some handy timers globalClock = new util.Clock(); // to track the time since experiment started routineTimer = new util.CountdownTimer(); // to track time remaining of each (non-slip) routine return Scheduler.Event.NEXT; } var t; var frameN; var continueRoutine; var fileComponents; var clickedContinue; // TODO Read from config var responseType = 2; function fileRoutineBegin(snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); // ensure that .thisN vals are up to date //------Prepare to start Routine 'file'------- t = 0; fileClock.reset(); // clock frameN = -1; continueRoutine = true; // until we're told otherwise // update component parameters for each repeat // keep track of which components have finished fileComponents = []; for (const thisComponent of fileComponents) if ("status" in thisComponent) thisComponent.status = PsychoJS.Status.NOT_STARTED; return Scheduler.Event.NEXT; }; } function fileRoutineEachFrame() { return async function () { //------Loop for each frame of Routine 'file'------- // get current time t = fileClock.getTime(); frameN = frameN + 1; // number of completed frames (so 0 is the first frame) // update/draw components on each frame // check for quit (typically the Esc key) if ( psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0 ) { return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false); } // check if the Routine should terminate if (!continueRoutine) { // a component has requested a forced-end of Routine return Scheduler.Event.NEXT; } continueRoutine = false; // reverts to True if at least one component still running for (const thisComponent of fileComponents) if ( "status" in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED ) { continueRoutine = true; break; } // refresh the screen if continuing if (continueRoutine) { return Scheduler.Event.FLIP_REPEAT; } else { return Scheduler.Event.NEXT; } }; } function fileRoutineEnd() { return async function () { //------Ending Routine 'file'------- for (const thisComponent of fileComponents) { if (typeof thisComponent.setAutoDraw === "function") { thisComponent.setAutoDraw(false); } } // the Routine "file" was not non-slip safe, so reset the non-slip timer routineTimer.reset(); return Scheduler.Event.NEXT; }; } var _beepButton; function _instructionSetup(text) { t = 0; instructionsClock.reset(); // clock frameN = -1; continueRoutine = true; instructions.setWrapWidth(window.innerWidth * 0.8); instructions.setPos([-window.innerWidth * 0.4, window.innerHeight * 0.4]); instructions.setText(text); instructions.setAutoDraw(true); } function _clickContinue(e) { if (e.target.id !== "threshold-beep-button") clickedContinue = true; } async function _instructionRoutineEachFrame() { t = instructionsClock.getTime(); frameN = frameN + 1; if ( psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0 ) { document.removeEventListener("click", _clickContinue); document.removeEventListener("touchend", _clickContinue); removeBeepButton(_beepButton); return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false); } if (!continueRoutine) { return Scheduler.Event.NEXT; } continueRoutine = true; if (psychoJS.eventManager.getKeys({ keyList: ["return"] }).length > 0) { continueRoutine = false; } if (continueRoutine && !clickedContinue) { return Scheduler.Event.FLIP_REPEAT; } else { return Scheduler.Event.NEXT; } } async function _instructionRoutineEnd() { instructions.setAutoDraw(false); routineTimer.reset(); document.removeEventListener("click", _clickContinue); document.removeEventListener("touchend", _clickContinue); return Scheduler.Event.NEXT; } var blocks; var currentLoop; function blocksLoopBegin(blocksLoopScheduler, snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); // update internal variables (.thisN etc) of the loop // set up handler to look after randomisation of conditions etc blocks = new TrialHandler({ psychoJS: psychoJS, nReps: 1, method: TrialHandler.Method.SEQUENTIAL, extraInfo: expInfo, originPath: undefined, trialList: "conditions/blockCount.csv", seed: undefined, name: "blocks", }); psychoJS.experiment.addLoop(blocks); // add the loop to the experiment currentLoop = blocks; // we're now the current loop // Schedule all the trials in the trialList: for (const thisBlock of blocks) { const snapshot = blocks.getSnapshot(); blocksLoopScheduler.add(importConditions(snapshot)); blocksLoopScheduler.add(filterRoutineBegin(snapshot)); blocksLoopScheduler.add(filterRoutineEachFrame()); blocksLoopScheduler.add(filterRoutineEnd()); blocksLoopScheduler.add(initInstructionRoutineBegin(snapshot)); blocksLoopScheduler.add(initInstructionRoutineEachFrame()); blocksLoopScheduler.add(initInstructionRoutineEnd()); const trialsLoopScheduler = new Scheduler(psychoJS); blocksLoopScheduler.add(trialsLoopBegin(trialsLoopScheduler, snapshot)); blocksLoopScheduler.add(trialsLoopScheduler); blocksLoopScheduler.add(trialsLoopEnd); blocksLoopScheduler.add( endLoopIteration(blocksLoopScheduler, snapshot) ); } return Scheduler.Event.NEXT; }; } var trialsConditions; var trials; function trialsLoopBegin(trialsLoopScheduler, snapshot) { return async function () { // setup a MultiStairTrialHandler trialsConditions = TrialHandler.importConditions( psychoJS.serverManager, thisConditionsFile ); trials = new data.MultiStairHandler({ stairType: MultiStairHandler.StaircaseType.QUEST, psychoJS: psychoJS, name: "trials", varName: "trialsVal", nTrials: conditionTrials, conditions: trialsConditions, method: TrialHandler.Method.FULLRANDOM, }); psychoJS.experiment.addLoop(trials); // add the loop to the experiment currentLoop = trials; // we're now the current loop // Schedule all the trials in the trialList: for (const thisQuestLoop of trials) { const snapshot = trials.getSnapshot(); trialsLoopScheduler.add(importConditions(snapshot)); trialsLoopScheduler.add(trialInstructionRoutineBegin(snapshot)); trialsLoopScheduler.add(trialInstructionRoutineEachFrame()); trialsLoopScheduler.add(trialInstructionRoutineEnd()); trialsLoopScheduler.add(trialRoutineBegin(snapshot)); trialsLoopScheduler.add(trialRoutineEachFrame()); trialsLoopScheduler.add(trialRoutineEnd()); trialsLoopScheduler.add( endLoopIteration(trialsLoopScheduler, snapshot) ); } return Scheduler.Event.NEXT; }; } async function trialsLoopEnd() { psychoJS.experiment.addData( "staircaseName", currentLoop._currentStaircase._name ); psychoJS.experiment.addData( "questMeanAtEndOfTrialsLoop", currentLoop._currentStaircase.mean() ); psychoJS.experiment.addData( "questSDAtEndOfTrialsLoop", currentLoop._currentStaircase.sd() ); psychoJS.experiment.addData( "questQuantileOfQuantileOrderAtEndOfTrialsLoop", currentLoop._currentStaircase.quantile( currentLoop._currentStaircase._jsQuest.quantileOrder ) ); // terminate loop psychoJS.experiment.removeLoop(trials); return Scheduler.Event.NEXT; } async function blocksLoopEnd() { psychoJS.experiment.removeLoop(blocks); return Scheduler.Event.NEXT; } var filterComponents; function filterRoutineBegin(snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); // ensure that .thisN vals are up to date //------Prepare to start Routine 'filter'------- t = 0; filterClock.reset(); // clock frameN = -1; continueRoutine = true; // until we're told otherwise // update component parameters for each repeat thisLoopNumber += 1; thisConditionsFile = `conditions/block_${thisLoopNumber}.csv`; const possibleTrials = []; const thisBlockFileData = blockFiles[thisLoopNumber]; if (debug) console.log("thisBlockFileData: ", thisBlockFileData); for (let rowKey in thisBlockFileData) { let rowIndex = parseInt(rowKey); if (Object.keys(thisBlockFileData[rowIndex]).length > 1) { if (debug) console.log( "condition trials this row of block: ", parseInt(thisBlockFileData[rowIndex]["conditionTrials"]) ); possibleTrials.push( parseInt(thisBlockFileData[rowIndex]["conditionTrials"]) ); } } if (debug) console.log("possibleTrials: ", possibleTrials); totalTrialCount = possibleTrials.reduce((a, b) => a + b, 0); // sum of possible trials totalBlockCount = Object.keys(blockFiles).length; totalBlockTrialList = [...possibleTrials]; // console.log('totalBlockTrialList', totalBlockTrialList) // totalBlockCount = blockFiles.length; // TODO Remove this constraint to allow different # of trials for each condition if (!possibleTrials.every((a) => a === possibleTrials[0])) throw "Number of trials for each condition within one block has to be equal. (Will be updated soon.)"; conditionTrials = possibleTrials[0]; // keep track of which components have finished filterComponents = []; for (const thisComponent of filterComponents) if ("status" in thisComponent) thisComponent.status = PsychoJS.Status.NOT_STARTED; return Scheduler.Event.NEXT; }; } function filterRoutineEachFrame() { return async function () { //------Loop for each frame of Routine 'filter'------- // get current time t = filterClock.getTime(); frameN = frameN + 1; // number of completed frames (so 0 is the first frame) // update/draw components on each frame // check for quit (typically the Esc key) if ( psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0 ) { return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false); } // check if the Routine should terminate if (!continueRoutine) { // a component has requested a forced-end of Routine return Scheduler.Event.NEXT; } continueRoutine = false; // reverts to True if at least one component still running for (const thisComponent of filterComponents) if ( "status" in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED ) { continueRoutine = true; break; } // refresh the screen if continuing if (continueRoutine) { return Scheduler.Event.FLIP_REPEAT; } else { return Scheduler.Event.NEXT; } }; } function filterRoutineEnd() { return async function () { //------Ending Routine 'filter'------- for (const thisComponent of filterComponents) { if (typeof thisComponent.setAutoDraw === "function") { thisComponent.setAutoDraw(false); } } // the Routine "filter" was not non-slip safe, so reset the non-slip timer routineTimer.reset(); return Scheduler.Event.NEXT; }; } function initInstructionRoutineBegin(snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); _instructionSetup( instructionsText.initial(expInfo.participant) + instructionsText.initialByThresholdParameter["spacing"]( responseType, totalTrialCount ) + instructionsText.initialEnd(responseType) ); clickedContinue = false; document.addEventListener("click", _clickContinue); document.addEventListener("touchend", _clickContinue); _beepButton = addBeepButton(correctSynth); psychoJS.eventManager.clearKeys(); return Scheduler.Event.NEXT; }; } function initInstructionRoutineEachFrame() { return _instructionRoutineEachFrame; } function initInstructionRoutineEnd() { return async function () { instructions.setAutoDraw(false); routineTimer.reset(); document.removeEventListener("click", _clickContinue); document.removeEventListener("touchend", _clickContinue); removeBeepButton(_beepButton); return Scheduler.Event.NEXT; }; } function blockInstructionRoutineBegin(snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); _instructionSetup(instructionsText.block(snapshot.block + 1)); clickedContinue = false; document.addEventListener("click", _clickContinue); document.addEventListener("touchend", _clickContinue); return Scheduler.Event.NEXT; }; } function blockInstructionRoutineEachFrame() { return _instructionRoutineEachFrame; } function blockInstructionRoutineEnd() { return _instructionRoutineEnd; } const _takeFixationClick = (e) => { let cX, cY; if (e.clientX) { cX = e.clientX; cY = e.clientY; } else { const t = e.changedTouches[0]; if (t.clientX) { cX = t.clientX; cY = t.clientY; } else { clickedContinue = false; return; } } if ( Math.hypot( cX - (window.innerWidth >> 1), cY - (window.innerHeight >> 1) ) < fixationSize ) { // Clicked on fixation hideCursor(); setTimeout(() => { clickedContinue = true; }, 17); } else { // wrongSynth.play(); clickedContinue = false; } }; function trialInstructionRoutineBegin(snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); _instructionSetup(instructionsText.trial.fixate["spacing"](responseType)); fixation.setHeight(fixationSize); fixation.setPos(fixationXYPx); fixation.tStart = t; fixation.frameNStart = frameN; fixation.setAutoDraw(true); totalTrial.setAutoDraw(true); clickedContinue = false; document.addEventListener("click", _takeFixationClick); document.addEventListener("touchend", _takeFixationClick); psychoJS.eventManager.clearKeys(); return Scheduler.Event.NEXT; }; } function trialInstructionRoutineEachFrame() { return async function () { t = instructionsClock.getTime(); frameN = frameN + 1; if ( psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0 ) { return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false); } if (!continueRoutine) { return Scheduler.Event.NEXT; } continueRoutine = true; if (psychoJS.eventManager.getKeys({ keyList: ["space"] }).length > 0) { continueRoutine = false; } if (continueRoutine && !clickedContinue) { return Scheduler.Event.FLIP_REPEAT; } else { return Scheduler.Event.NEXT; } }; } function trialInstructionRoutineEnd() { return async function () { document.removeEventListener("click", _takeFixationClick); document.removeEventListener("touchend", _takeFixationClick); instructions.setAutoDraw(false); routineTimer.reset(); return Scheduler.Event.NEXT; }; } var level; var windowWidthCm; var windowWidthPx; var pixPerCm; var viewingDistanceDesiredCm; var viewingDistanceCm; var fixationXYPx = [0, 0]; var fixationSize = 45; // TODO Set on block begins var showFixation = true; var block; var spacingDirection; var targetFont; var targetAlphabet; var validAns; var showAlphabetWhere; var showAlphabetElement; var showCounterBool; var showViewingDistanceBool; const showAlphabetResponse = { current: null, onsetTime: 0, clickTime: 0 }; var targetDurationSec; var targetMinimumPix; var spacingOverSizeRatio; var targetEccentricityXDeg; var targetEccentricityYDeg; var targetEccentricityXYDeg; var trackGazeYes; var trackHeadYes; var wirelessKeyboardNeededYes; var _key_resp_allKeys; var trialComponents; function trialRoutineBegin(snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); // ensure that .thisN vals are up to date hideCursor(); //// if (debug) console.log( `Level: ${snapshot.getCurrentTrial().trialsVal}, Index: ${ snapshot.thisIndex }` ); let condition; for (let c of snapshot.handler.getConditions()) { if (c.label === trials._currentStaircase._name) { condition = c; } } if (debug) console.log("condition: ", condition); let proposedLevel = currentLoop._currentStaircase.getQuestValue(); if (debug) console.log("level from getQuestValue(): ", proposedLevel); psychoJS.experiment.addData("levelProposedByQUEST", proposedLevel); // TODO Find a real way of estimating the max size proposedLevel = Math.min(proposedLevel, 1.75); psychoJS.experiment.addData("levelRoughlyLimited", proposedLevel); psychoJS.experiment.addData("conditionName", condition["label"]); psychoJS.experiment.addData( "flankerOrientation", condition["spacingDirection"] ); psychoJS.experiment.addData("targetFont", condition["targetFont"]); // TODO add a data field that is unique to this staircase (ie differentiate staircases within the same block, if they have equivalent parameters) // TODO set QUEST // ! // ! //------Prepare to start Routine 'trial'------- t = 0; trialClock.reset(); // clock frameN = -1; continueRoutine = true; // until we're told otherwise // update component parameters for each repeat windowWidthCm = rc.screenWidthCm ? rc.screenWidthCm.value : 30; windowWidthPx = rc.displayWidthPx.value; pixPerCm = windowWidthPx / windowWidthCm; if (!rc.screenWidthCm) console.warn("[Screen Width] Using arbitrary screen width. Enable RC."); viewingDistanceDesiredCm = condition["viewingDistanceDesiredCm"]; viewingDistanceCm = rc.viewingDistanceCm ? rc.viewingDistanceCm.value : viewingDistanceDesiredCm; if (!rc.viewingDistanceCm) console.warn( "[Viewing Distance] Using arbitrary viewing distance. Enable RC." ); // TODO // ! Very inefficient to read params very trial as they do not change in a block // ! Move this to a block-level routine and store the values fixationXYPx = [0, 0]; block = condition["blockOrder"]; spacingDirection = condition["spacingDirection"]; targetFont = condition["targetFont"].toLowerCase(); targetAlphabet = String(condition["targetAlphabet"]).split(""); validAns = String(condition["targetAlphabet"]).toLowerCase().split(""); showAlphabetWhere = condition["showAlphabetWhere"] || "bottom"; showViewingDistanceBool = condition["showViewingDistanceBool"] !== "FALSE"; showCounterBool = condition["showCounterBool"] !== "FALSE"; conditionTrials = condition["conditionTrials"]; targetDurationSec = condition["targetDurationSec"]; fixationSize = 45; // TODO use .csv parameters, ie draw as 2 lines, not one letter showFixation = condition["markTheFixationBool"] === "True"; targetMinimumPix = condition["targetMinimumPix"]; spacingOverSizeRatio = condition["spacingOverSizeRatio"]; targetEccentricityXDeg = condition["targetEccentricityXDeg"]; psychoJS.experiment.addData( "targetEccentricityXDeg", targetEccentricityXDeg ); targetEccentricityYDeg = condition["targetEccentricityYDeg"]; psychoJS.experiment.addData( "targetEccentricityYDeg", targetEccentricityYDeg ); targetEccentricityXYDeg = [ targetEccentricityXDeg, targetEccentricityYDeg, ]; trackGazeYes = condition["trackGazeYes"] === "True"; trackHeadYes = condition["trackHeadYes"] === "True"; wirelessKeyboardNeededYes = condition["wirelessKeyboardNeededYes"] === "True"; var alphabet = targetAlphabet; /* ------------------------------ Pick triplets ----------------------------- */ const tempAlphabet = shuffle(shuffle(alphabet)); var firstFlankerCharacter = tempAlphabet[0]; var targetCharacter = tempAlphabet[1]; var secondFlankerCharacter = tempAlphabet[2]; if (debug) console.log( firstFlankerCharacter, targetCharacter, secondFlankerCharacter ); correctAns = targetCharacter.toLowerCase(); /* -------------------------------------------------------------------------- */ var heightPx; var pos1XYDeg, pos1XYPx, pos2XYDeg, pos2XYPx, pos3XYDeg, pos3XYPx; var spacingDeg, spacingPx; //// // ! // TODO use actual nearPoint, from RC const nearPointXYDeg = { x: 0, y: 0 }; // TEMP const nearPointXYPix = { x: 0, y: 0 }; // TEMP const displayOptions = { pixPerCm: pixPerCm, viewingDistanceCm: viewingDistanceCm, nearPointXYDeg: nearPointXYDeg, nearPointXYPix: nearPointXYPix, spacingOverSizeRatio: spacingOverSizeRatio, minimumHeight: targetMinimumPix, fontFamily: targetFont, window: psychoJS.window, }; const [targetXYPix] = XYPixOfXYDeg( [targetEccentricityXYDeg], displayOptions ); level = await awaitMaxPresentableLevel( proposedLevel, targetXYPix, fixationXYPx, spacingDirection, displayOptions ); psychoJS.experiment.addData("levelUsed", level); if (debug) console.log("New level: ", level); spacingDeg = Math.pow(10, level); psychoJS.experiment.addData("spacingDeg", spacingDeg); if (debug) console.log("targetEccentricityXYDeg: ", targetEccentricityXYDeg); [pos1XYDeg, pos3XYDeg] = getFlankerLocations( targetEccentricityXYDeg, fixationXYPx, spacingDirection, spacingDeg ); if (debug) console.log("flanker locations: ", [pos1XYDeg, pos3XYDeg]); psychoJS.experiment.addData("flankerLocationsDeg", [ pos1XYDeg, pos3XYDeg, ]); pos2XYDeg = targetEccentricityXYDeg; [pos1XYPx, pos2XYPx, pos3XYPx] = XYPixOfXYDeg( [pos1XYDeg, pos2XYDeg, pos3XYDeg], displayOptions ); psychoJS.experiment.addData("targetLocationsPix", pos2XYPx); psychoJS.experiment.addData("flankerLocationsPix", [pos1XYPx, pos3XYPx]); spacingPx = Math.abs( degreesToPixels(spacingDeg, { pixPerCm: pixPerCm, viewingDistanceCm: viewingDistanceCm, }) ); psychoJS.experiment.addData("spacingPx", spacingPx); if (debug) console.log("spacingPx: ", spacingPx); if (debug) console.log( "spacing/spacingOverSizeRation: ", spacingPx / spacingOverSizeRatio ); if (debug) console.log("targetMinimumPix: ", targetMinimumPix); heightPx = Math.max(spacingPx / spacingOverSizeRatio, targetMinimumPix); key_resp.keys = undefined; key_resp.rt = undefined; _key_resp_allKeys = []; //// heightPx = Math.round(heightPx); pos1XYPx = pos1XYPx.map((x) => Math.round(x)); pos2XYPx = pos2XYPx.map((x) => Math.round(x)); pos3XYPx = pos3XYPx.map((x) => Math.round(x)); fixation.setPos(fixationXYPx); fixation.setHeight(fixationSize); flanker1.setPos(pos1XYPx); flanker1.setText(firstFlankerCharacter); flanker1.setFont(targetFont); flanker1.setHeight(heightPx); target.setPos(pos2XYPx); target.setText(targetCharacter); target.setFont(targetFont); target.setHeight(heightPx); flanker2.setPos(pos3XYPx); flanker2.setText(secondFlankerCharacter); flanker2.setFont(targetFont); flanker2.setHeight(heightPx); showAlphabet.setPos([0, 0]); showAlphabet.setText(""); // showAlphabet.setText(getAlphabetShowText(validAns)) instructions.setText( instructionsText.trial.respond["spacing"](responseType) ); // totalTrial.setPos([totalTrialConfig.x, totalTrialConfig.y]); // totalTrial.setAlignHoriz('right'); // totalTrial.setAlignVert('bottom'); totalBlockIndex = calculateBlockWithTrialIndex( totalBlockTrialList, totalTrialIndex ); let trialInfoStr = ""; if (showCounterBool) trialInfoStr = `Block ${totalBlockIndex} of ${totalBlockCount}. Trial ${totalTrialIndex} of ${totalTrialCount}.`; if (showViewingDistanceBool) trialInfoStr += ` At ${viewingDistanceCm} cm.`; totalTrial.setText(trialInfoStr); totalTrial.setFont(totalTrialConfig.fontName); totalTrial.setHeight(totalTrialConfig.fontSize); totalTrial.setPos([window.innerWidth / 2, -window.innerHeight / 2]); // keep track of which components have finished trialComponents = []; trialComponents.push(key_resp); // trialComponents.push(targetBoundingPoly); // Target Bounding Box trialComponents.push(fixation); trialComponents.push(flanker1); trialComponents.push(target); trialComponents.push(flanker2); trialComponents.push(showAlphabet); trialComponents.push(totalTrial); for (const thisComponent of trialComponents) if ("status" in thisComponent) thisComponent.status = PsychoJS.Status.NOT_STARTED; // update trial index totalTrialIndex = totalTrialIndex + 1; return Scheduler.Event.NEXT; }; } var frameRemains; function trialRoutineEachFrame() { return async function () { //------Loop for each frame of Routine 'trial'------- // get current time t = trialClock.getTime(); frameN = frameN + 1; // number of completed frames (so 0 is the first frame) // update/draw components on each frame // Target Bounding Box // // *targetBoundingPoly* updates // if (t >= 0.0 && targetBoundingPoly.status === PsychoJS.Status.NOT_STARTED) { // // keep track of start time/frame for later // targetBoundingPoly.tStart = t; // (not accounting for frame time here) // targetBoundingPoly.frameNStart = frameN; // exact frame index // targetBoundingPoly.setAutoDraw(true); // } // if (targetBoundingPoly.status === PsychoJS.Status.STARTED){ // only update if being drawn // const tightBoundingBox = target.getBoundingBox(true); // targetBoundingPoly.setPos([tightBoundingBox.left, tightBoundingBox.top]); // targetBoundingPoly.setSize([tightBoundingBox.width, tightBoundingBox.height]); // } const uniDelay = 0.5; // *key_resp* updates if (t >= uniDelay && key_resp.status === PsychoJS.Status.NOT_STARTED) { // keep track of start time/frame for later key_resp.tStart = t; // (not accounting for frame time here) key_resp.frameNStart = frameN; // exact frame index // TODO Use PsychoJS clock if possible // Reset together with PsychoJS showAlphabetResponse.onsetTime = performance.now(); // keyboard checking is just starting psychoJS.window.callOnFlip(function () { key_resp.clock.reset(); }); // t=0 on next screen flip psychoJS.window.callOnFlip(function () { key_resp.start(); }); // start on screen flip psychoJS.window.callOnFlip(function () { key_resp.clearEvents(); }); } if (key_resp.status === PsychoJS.Status.STARTED) { let theseKeys = key_resp.getKeys({ keyList: validAns, waitRelease: false, }); _key_resp_allKeys = _key_resp_allKeys.concat(theseKeys); if (_key_resp_allKeys.length > 0) { key_resp.keys = _key_resp_allKeys[_key_resp_allKeys.length - 1].name; // just the last key pressed key_resp.rt = _key_resp_allKeys[_key_resp_allKeys.length - 1].rt; // was this correct? if (key_resp.keys == correctAns) { // Play correct audio correctSynth.play(); key_resp.corr = 1; } else { // Play wrong audio key_resp.corr = 0; } // a response ends the routine continueRoutine = false; } } // *showAlphabetResponse* updates if (showAlphabetResponse.current) { key_resp.keys = showAlphabetResponse.current; key_resp.rt = (showAlphabetResponse.clickTime - showAlphabetResponse.onsetTime) / 1000; if (showAlphabetResponse.current == correctAns) { // Play correct audio correctSynth.play(); key_resp.corr = 1; } else { // Play wrong audio key_resp.corr = 0; } showAlphabetResponse.current = null; removeClickableAlphabet(); continueRoutine = false; } // *fixation* updates if ( t >= 0.0 && fixation.status === PsychoJS.Status.NOT_STARTED && showFixation ) { // keep track of start time/frame for later fixation.tStart = t; // (not accounting for frame time here) fixation.frameNStart = frameN; // exact frame index fixation.setAutoDraw(true); } // *totalTrial* updates if (t >= 0.0 && totalTrial.status === PsychoJS.Status.NOT_STARTED) { // keep track of start time/frame for later totalTrial.tStart = t; // (not accounting for frame time here) totalTrial.frameNStart = frameN; // exact frame index totalTrial.setAutoDraw(true); } // *flanker1* updates if (t >= uniDelay && flanker1.status === PsychoJS.Status.NOT_STARTED) { // keep track of start time/frame for later flanker1.tStart = t; // (not accounting for frame time here) flanker1.frameNStart = frameN; // exact frame index flanker1.setAutoDraw(true); } frameRemains = uniDelay + targetDurationSec - psychoJS.window.monitorFramePeriod * 0.75; // most of one frame period left if (flanker1.status === PsychoJS.Status.STARTED && t >= frameRemains) { flanker1.setAutoDraw(false); } // *target* updates if (t >= uniDelay && target.status === PsychoJS.Status.NOT_STARTED) { // keep track of start time/frame for later target.tStart = t; // (not accounting for frame time here) target.frameNStart = frameN; // exact frame index target.setAutoDraw(true); } frameRemains = uniDelay + targetDurationSec - psychoJS.window.monitorFramePeriod * 0.75; // most of one frame period left if (target.status === PsychoJS.Status.STARTED && t >= frameRemains) { target.setAutoDraw(false); // Play purr sound // Wait until next frame to play setTimeout(() => { purrSynth.play(); }, 17); setTimeout(() => { showCursor(); }, 500); } // *flanker2* updates if (t >= uniDelay && flanker2.status === PsychoJS.Status.NOT_STARTED) { // keep track of start time/frame for later flanker2.tStart = t; // (not accounting for frame time here) flanker2.frameNStart = frameN; // exact frame index flanker2.setAutoDraw(true); } frameRemains = uniDelay + targetDurationSec - psychoJS.window.monitorFramePeriod * 0.75; // most of one frame period left if (flanker2.status === PsychoJS.Status.STARTED && t >= frameRemains) { flanker2.setAutoDraw(false); } // check for quit (typically the Esc key) if ( psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0 ) { return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false); } /* -------------------------------------------------------------------------- */ // *showAlphabet* updates if ( t >= uniDelay + targetDurationSec && showAlphabet.status === PsychoJS.Status.NOT_STARTED ) { // keep track of start time/frame for later showAlphabet.tStart = t; // (not accounting for frame time here) showAlphabet.frameNStart = frameN; // exact frame index showAlphabet.setAutoDraw(true); showAlphabetElement = setupClickableAlphabet( targetAlphabet, targetFont, showAlphabetWhere, showAlphabetResponse ); instructions.tSTart = t; instructions.frameNStart = frameN; instructions.setAutoDraw(true); } /* -------------------------------------------------------------------------- */ // check if the Routine should terminate if (!continueRoutine) { // a component has requested a forced-end of Routine removeClickableAlphabet(); return Scheduler.Event.NEXT; } continueRoutine = false; // reverts to True if at least one component still running for (const thisComponent of trialComponents) if ( "status" in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED ) { continueRoutine = true; break; } // refresh the screen if continuing if (continueRoutine) { return Scheduler.Event.FLIP_REPEAT; } else { return Scheduler.Event.NEXT; } }; } function trialRoutineEnd() { return async function () { //------Ending Routine 'trial'------- for (const thisComponent of trialComponents) { if (typeof thisComponent.setAutoDraw === "function") { thisComponent.setAutoDraw(false); } } // was no response the correct answer?! if (key_resp.keys === undefined) { console.error("[key_resp.keys] No response error."); } // store data for psychoJS.experiment (ExperimentHandler) // update the trial handler if (currentLoop instanceof MultiStairHandler) { currentLoop.addResponse(key_resp.corr, level); if (debug) console.log("level passed to addResponse: ", level); } psychoJS.experiment.addData("key_resp.keys", key_resp.keys); psychoJS.experiment.addData("key_resp.corr", key_resp.corr); if (typeof key_resp.keys !== "undefined") { // we had a response psychoJS.experiment.addData("key_resp.rt", key_resp.rt); routineTimer.reset(); } key_resp.stop(); // the Routine "trial" was not non-slip safe, so reset the non-slip timer routineTimer.reset(); psychoJS.experiment.addData( "staircaseName", currentLoop._currentStaircase._name ); psychoJS.experiment.addData( "questMeanAtEndOfTrial", currentLoop._currentStaircase.mean() ); psychoJS.experiment.addData( "questSDAtEndOfTrial", currentLoop._currentStaircase.sd() ); psychoJS.experiment.addData( "questQuantileOfQuantileOrderAtEndOfTrial", currentLoop._currentStaircase.quantile( currentLoop._currentStaircase._jsQuest.quantileOrder ) ); return Scheduler.Event.NEXT; }; } function endLoopIteration(scheduler, snapshot) { // ------Prepare for next entry------ return async function () { if (typeof snapshot !== "undefined") { // ------Check if user ended loop early------ if (snapshot.finished) { // Check for and save orphaned data if (psychoJS.experiment.isEntryEmpty()) { psychoJS.experiment.nextEntry(snapshot); } scheduler.stop(); } else { const thisTrial = snapshot.getCurrentTrial(); if ( typeof thisTrial === "undefined" || !("isTrials" in thisTrial) || thisTrial.isTrials ) { psychoJS.experiment.nextEntry(snapshot); } } return Scheduler.Event.NEXT; } }; } function importConditions(currentLoop) { return async function () { psychoJS.importAttributes(currentLoop.getCurrentTrial()); return Scheduler.Event.NEXT; }; } async function quitPsychoJS(message, isCompleted) { // Check for and save orphaned data if (psychoJS.experiment.isEntryEmpty()) { psychoJS.experiment.nextEntry(); } psychoJS.window.close(); psychoJS.quit({ message: message, isCompleted: isCompleted }); return Scheduler.Event.QUIT; } }; /* Utilities */ /** * Convert a number of visual degrees to pixels VERIFY * @param {Number} degrees Scalar, in degrees * @param {Object} displayOptions Parameters about the stimulus presentation * @param {Number} displayOptions.pixPerCm Pixels per centimeter on screen * @param {Number} displayOptions.viewingDistanceCm Distance (in cm) of participant from screen * @returns {Number} */ function degreesToPixels(degrees, displayOptions) { const radians = degrees * (Math.PI / 180); const pixels = displayOptions.pixPerCm * displayOptions.viewingDistanceCm * Math.tan(radians); return pixels; } /** * Translation of MATLAB function of the same name * by Prof Denis Pelli, XYPixOfXYDeg.m * @param {Array} xyDeg List of [x,y] pairs, representing points x degrees right, and y degrees up, of fixation * @param {Object} displayOptions Parameters about the stimulus presentation * @param {Number} displayOptions.pixPerCm Pixels per centimeter on screen * @param {Number} displayOptions.viewingDistanceCm Distance (in cm) of participant from screen * @param {Object} displayOptions.nearPointXYDeg Near-point on screen, in degrees relative to fixation(?) * @param {Number} displayOptions.nearPointXYDeg.x Degrees along x-axis of near-point from fixation * @param {Number} displayOptions.nearPointXYDeg.y Degrees along y-axis of near-point from fixation * @param {Object} displayOptions.nearPointXYPix Near-point on screen, in pixels relative to origin(?) * @param {Number} displayOptions.nearPointXYPix.x Pixels along x-axis of near-point from origin * @param {Number} displayOptions.nearPointXYPix.y Pixels along y-axis of near-point from origin * @returns {Number[][]} Array of length=2 arrays of numbers, representing the same points in Pixel space */ function XYPixOfXYDeg(xyDeg, displayOptions) { if (xyDeg.length == 0) { return; } // Return if no points to transform // TODO verify displayOptions has the correct parameters const xyPix = []; xyDeg.forEach((position) => { position[0] = position[0] - displayOptions.nearPointXYDeg.x; position[1] = position[1] - displayOptions.nearPointXYDeg.y; const rDeg = Math.sqrt(position[0] ** 2 + position[1] ** 2); const rPix = degreesToPixels(rDeg, displayOptions); let pixelPosition = []; if (rDeg > 0) { pixelPosition = [ (position[0] * rPix) / rDeg, (position[1] * rPix) / rDeg, ]; } else { pixelPosition = [0, 0]; } pixelPosition[0] = pixelPosition[0] + displayOptions.nearPointXYPix.x; pixelPosition[1] = pixelPosition[1] + displayOptions.nearPointXYPix.x; xyPix.push(pixelPosition); }); return xyPix; } /** * Given a spacing value (in pixels), estimate a (non-tight) bounding box * @param {Number} spacing Spacing which will be used to place flanker * @param {Number} spacingOverSizeRatio Specified ratio of distance between flanker&target to letter height * @param {Number} minimumHeight Smallest allowable letter height for flanker * @param {String} font Font-family in which the stimuli will be presented * @param {PsychoJS.window} window PsychoJS window, used to create a stimulus to be measured * @returns */ function boundingBoxFromSpacing( spacing, spacingOverSizeRatio, minimumHeight, font, window ) { const height = Math.max(spacing / spacingOverSizeRatio, minimumHeight); try { const testTextStim = new visual.TextStim({ win: window, name: "testTextStim", text: "H", // TEMP font: font, units: "pix", // ASSUMES that parameters are in pixel units pos: [0, 0], height: height, wrapWidth: undefined, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -7.0, autoDraw: false, autoLog: false, }); const estimatedBoundingBox = testTextStim._boundingBox; return estimatedBoundingBox; } catch (error) { console.error( "Error estimating bounding box of flanker. Likely due to too large a `proposedLevel` value being tested.", error ); return error; } } /** * Calculate the (2D) coordinates of two tangential flankers, linearly symmetrical around a target at targetPosition * @todo Add parameter/support for log-symmetric spacing * @param {Number[]} targetPosition [x,y] position of the target * @param {Number[]} fixationPosition [x,y] position of the fixation point * @param {Number} spacing How far the flankers are to be from the target (in the same units as the target & fixation positions) * @returns {Number[][]} Array containing two Arrays which represent the positions of Flanker 1 and Flanker 2 */ function tangentialFlankerPositions(targetPosition, fixationPosition, spacing) { let x, i; // Variables for anonymous fn's // Vector representing the line between target and fixation const v = [ fixationPosition[0] - targetPosition[0], fixationPosition[1] - targetPosition[1], ]; // Get the vector perpendicular to v const p = [v[1], -v[0]]; // SEE https://gamedev.stackexchange.com/questions/70075/how-can-i-find-the-perpendicular-to-a-2d-vector // Find the point that is `spacing` far from `targetPosition` along p // SEE https://math.stackexchange.com/questions/175896/finding-a-point-along-a-line-a-certain-distance-away-from-another-point /// Find the length of `p` const llpll = Math.sqrt( p.map((x) => x ** 2).reduce((previous, current) => previous + current) ); /// Normalize `p` const u = p.map((x) => x / llpll); /// Find our two new points, `spacing` distance away from targetPosition along line `p` const flankerPositions = [ targetPosition.map((x, i) => x + spacing * u[i]), targetPosition.map((x, i) => x - spacing * u[i]), ]; return flankerPositions; } /** * Calculate the (2D) coordinates of two radial flankers, linearly symmetrical around a target at targetPosition * @todo Add parameter/support for log-symmetric spacing * @param {Number[]} targetPosition [x,y] position of the target * @param {Number[]} fixationPosition [x,y] position of the fixation point * @param {Number} spacing How far the flankers are to be from the target (in the same units as the target & fixation positions) * @returns {Number[][]} Array containing two Arrays, which represent the positions of Flanker 1 and Flanker 2 */ function radialFlankerPositions(targetPosition, fixationPosition, spacing) { // SEE https://math.stackexchange.com/questions/175896/finding-a-point-along-a-line-a-certain-distance-away-from-another-point // Vector representing the line between target and fixation const v = [ fixationPosition[0] - targetPosition[0], fixationPosition[1] - targetPosition[1], ]; /// Find the length of v const llvll = Math.sqrt( v.map((x) => x ** 2).reduce((previous, current) => previous + current) ); /// Normalize v const u = v.map((x) => x / llvll); /// Find our two new points, `spacing` distance away from targetPosition along line v const flankerPositions = [ targetPosition.map((x, i) => x + spacing * u[i]), targetPosition.map((x, i) => x - spacing * u[i]), ]; return flankerPositions; } /** * Return the coordinates of the two flankers around a given target. * @param {Number[]} targetPosition [x,y] position of the target stimuli * @param {Number[]} fixationPosition [x,y] position of the fixation stimuli * @param {("radial"|"tangential")} flankerOrientation String specifying the position of the flankers relative to the line between fixation and the target * @param {Number} spacing Distance between the target and one flanker * @returns {Number[][]} Array containing two [x,y] arrays, each representing the location of one flanker */ function getFlankerLocations( targetPosition, fixationPosition, flankerOrientation, spacing ) { switch (flankerOrientation) { case "radial": return radialFlankerPositions(targetPosition, fixationPosition, spacing); case "tangential": return tangentialFlankerPositions( targetPosition, fixationPosition, spacing ); default: console.error( "Unknown flankerOrientation specified, ", flankerOrientation ); } } /** * Return the extreme points of a rectangle bounding the pair of flankers * @param {Number} level Suggested level from QUEST * @param {Number[]} targetPosition [x,y] position of the target stimulus * @param {Number[]} fixationPosition [x,y] position of the fixation stimulus * @param {("radial"|"tangential")} flankerOrientation Arrangement of the flankers relative to the line between fixation and target * @param {Object} sizingParameters Parameters for drawing stimuli * @param {Number} sizingParameters.spacingOverSizeRatio Ratio of distance between flanker&target to stimuli letter height * @param {Number} sizingParameters.minimumHeight Minimum stimulus letter height (in same units as other parameters) * @param {String} sizingParameters.fontFamily Name of the fontFamily in which the stimuli will be drawn * @param {Number} sizingParameters.pixPerCm Pixel/cm ratio of the display * @param {Number} sizingParameters.viewingDistanceCm Distance (in cm) of the observer from the near-point * @param {PsychoJS.window} sizingParameters.window Window object, used for creating a mock stimuli for measurement * @returns {Number[][]} [[x_min, y_min], [x_max, y_max]] Array of defining points of the area over which flankers extend */ function flankersExtent( level, targetPosition, fixationPosition, flankerOrientation, sizingParameters ) { if (debug) console.log("window: ", sizingParameters.window); const spacingDegrees = Math.pow(10, level); const spacingPixels = Math.abs( degreesToPixels(spacingDegrees, { pixPerCm: sizingParameters.pixPerCm, viewingDistanceCm: sizingParameters.viewingDistanceCm, }) ); const flankerLocations = getFlankerLocations( targetPosition, fixationPosition, flankerOrientation, spacingPixels ); try { const flankerBoxDimensions = boundingBoxFromSpacing( spacingPixels, sizingParameters.spacingOverSizeRatio, sizingParameters.minimumHeight, sizingParameters.fontFamily, sizingParameters.window ); const boundingPoints = []; flankerLocations.forEach((flankerPosition, i) => { const boundingPoint = []; if (targetPosition[0] < 0) { boundingPoint.push( flankerPosition[0] - (i === 0 ? -1 : 1) * (flankerBoxDimensions.width / 2) ); } else { boundingPoint.push( flankerPosition[0] + (i === 0 ? -1 : 1) * (flankerBoxDimensions.width / 2) ); } if (targetPosition[1] < 0) { boundingPoint.push( flankerPosition[1] - (i === 0 ? -1 : 1) * (flankerBoxDimensions.height / 2) ); } else { boundingPoint.push( flankerPosition[1] + (i === 0 ? -1 : 1) * (flankerBoxDimensions.height / 2) ); } boundingPoints.push(boundingPoint); }); return boundingPoints; } catch (error) { console.error("Error estimating flankers extent.", error); return error; } } /** * Determine whether a given point lies inside a given rectangle * @param {Number[][]} rectangle Array of two [x,y] points, which define an area * @param {Number[]} point [x,y] coordinate of a point which may be within rectangle * @returns {Boolean} */ function rectangleContainsPoint(rectangle, point) { const leftX = Math.min(rectangle[0][0], rectangle[1][0]); const rightX = Math.max(rectangle[0][0], rectangle[1][0]); const lowerY = Math.min(rectangle[0][1], rectangle[1][1]); const upperY = Math.max(rectangle[0][1], rectangle[1][1]); const xIsIn = point[0] >= leftX && point[0] <= rightX; const yIsIn = point[1] >= lowerY && point[1] <= upperY; if (debug) { console.log("flanker rectangle: ", rectangle); console.log("xIsIn: ", xIsIn); console.log("yIsIn: ", yIsIn); } return xIsIn && yIsIn; } /** * Determines whether any part of a given rectangle will extend beyond the screen * @param {Number[][]} rectangle Array of two [x,y] points, defining a rectangle * @param {Object} screenDimensions * @param {Number} screenDimensions.width Width of the screen * @param {Number} screenDimensions.height Height of the screen * @returns {Boolean} */ function rectangleOffscreen(rectangle, screenDimensions) { const pointOffScreen = (point) => Math.abs(point[0]) > screenDimensions.width / 2 || Math.abs(point[1]) > screenDimensions.height / 2; return rectangle.some(pointOffScreen); // VERIFY this logic is correct } /** * Tests whether these proposed parameters for presentation would draw improperly, eg extend beyond the extent of the screen * @todo Test whether the flankers interfer with eachother * @param {Number} proposedLevel Level to be tested, as provided by QUEST * @param {Number[]} targetXYPix [x,y] position of the target (in pixels) * @param {Number[]} fixationXYPix [x,y] position of the fixation (in pixels) * @param {"radial"|"tangential"} spacingDirection Orientation of flankers relative to fixation-target * @param {Object} displayOptions Set of parameters for the specifics of presentation * @todo Specify necessary members of `displayOptions` * @returns {Boolean} */ function unacceptableStimuli( proposedLevel, targetXYPix, fixationXYPix, spacingDirection, displayOptions ) { const areaFlankersCover = flankersExtent( proposedLevel, targetXYPix, fixationXYPix, spacingDirection, displayOptions ); // TODO take the size of fixation into account const fixationInfringed = rectangleContainsPoint( areaFlankersCover, fixationXYPix ); const stimuliExtendOffscreen = rectangleOffscreen(areaFlankersCover, { width: screen.width, height: screen.height, }); const badPresentation = fixationInfringed || stimuliExtendOffscreen; if (debug) { console.log("areaFlankersCover: ", areaFlankersCover); console.log("fixationInfringed: ", fixationInfringed); console.log("stimuliExtendOffscreen: ", stimuliExtendOffscreen); console.log("badPresentation: ", badPresentation); } return badPresentation; } /** * Estimate the largest `level` value which will still present correctly * @param {Number} proposedLevel Level to be tested, as provided by QUEST * @param {Number[]} targetXYPix [x,y] position of the target (in pixels) * @param {Number[]} fixationXYPix [x,y] position of the fixation (in pixels) * @param {"radial"|"tangential"} spacingDirection Orientation of flankers relative to fixation-target * @param {Object} displayOptions Set of parameters for the specifics of presentation * @todo Specify necessary members of `displayOptions` * @returns {Number} */ function getMaxPresentableLevel( proposedLevel, targetXYPix, fixationXYPix, spacingDirection, displayOptions ) { const granularityOfChange = 0.05; if ( !unacceptableStimuli( proposedLevel, targetXYPix, fixationXYPix, spacingDirection, displayOptions ) ) { if (debug) console.log("acceptable level found: ", proposedLevel); return proposedLevel; } else { if (debug) console.log("unacceptable level: ", proposedLevel); return getMaxPresentableLevel( proposedLevel - granularityOfChange, targetXYPix, fixationXYPix, spacingDirection, displayOptions ); } } /** * Promise-based equivalent to `getMaxPresentableLevel` * @param {Number} proposedLevel Level to be tested, as provided by QUEST * @param {Number[]} targetXYPix [x,y] position of the target (in pixels) * @param {Number[]} fixationXYPix [x,y] position of the fixation (in pixels) * @param {("radial"|"tangential")} spacingDirection Orientation of flankers relative to fixation-target * @param {Object} displayOptions Set of parameters for the specifics of presentation * @todo Specify necessary members of `displayOptions` * @returns {Number} */ function awaitMaxPresentableLevel( proposedLevel, targetXYPix, fixationXYPix, spacingDirection, displayOptions ) { const granularityOfChange = 0.05; if ( unacceptableStimuli( granularityOfChange, targetXYPix, fixationXYPix, spacingDirection, displayOptions ) ) { console.error( "Unpresentable stimuli, even at level=" + String(granularityOfChange) ); return new Promise((resolve) => resolve(granularityOfChange)); } if ( !unacceptableStimuli( proposedLevel, targetXYPix, fixationXYPix, spacingDirection, displayOptions ) ) { if (debug) console.log("acceptable level found: ", proposedLevel); return new Promise((resolve) => resolve(proposedLevel)); } else { if (debug) console.log("unacceptable level: ", proposedLevel); return awaitMaxPresentableLevel( proposedLevel - granularityOfChange, targetXYPix, fixationXYPix, spacingDirection, displayOptions ); } }
Bearbeitung
Datei öffnen
/***************** * Crowding Test * *****************/ const debug = false; import { core, data, util, visual } from "./psychojs/out/psychojs-2021.3.0.js"; const { PsychoJS } = core; const { TrialHandler, MultiStairHandler } = data; const { Scheduler } = util; //// import * as jsQUEST from "./lib/jsQUEST.module.js"; //// /* ------------------------------- Components ------------------------------- */ import { hideCursor, showCursor, shuffle } from "./components/utils.js"; import { addBeepButton, instructionsText, removeBeepButton, } from "./components/instructions.js"; import { calculateBlockWithTrialIndex } from "./components/trialCounter.js"; import { getCorrectSynth, getWrongSynth, getPurrSynth, } from "./components/sound.js"; import { removeClickableAlphabet, setupClickableAlphabet, } from "./components/showAlphabet.js"; /* -------------------------------------------------------------------------- */ window.jsQUEST = jsQUEST; var conditionTrials; var levelLeft, levelRight; let correctAns; // For development purposes, toggle RC off for testing speed const useRC = !debug; const rc = RemoteCalibrator; rc.init(); // store info about the experiment session: let expName = "Threshold"; // from the Builder filename that created this script let expInfo = { participant: debug ? rc.id.value : "", session: "001" }; const fontsRequired = {}; //// // blockCount is just a file telling the program how many blocks in total Papa.parse("conditions/blockCount.csv", { download: true, complete: function (results) { const blockCount = results.data.length - 2; // TODO Make this calculation robust loadBlockFiles(blockCount, () => { if (useRC) { rc.panel( [ { name: "screenSize", }, { name: "trackDistance", options: { nearPoint: false, showVideo: false, }, }, ], "body", {}, () => { rc.removePanel(); // ! Start actual experiment experiment(blockCount); } ); } else { // NO RC experiment(blockCount); } }); }, }); const blockFiles = {}; const loadBlockFiles = (count, callback) => { if (count === 0) { callback(); return; } Papa.parse(`conditions/block_${count}.csv`, { download: true, header: true, skipEmptyLines: true, dynamicTyping: true, complete: function (results) { blockFiles[count] = results.data; if (debug) console.log("Block " + count + ": ", results.data); Object.values(results.data).forEach((row) => { let fontFamily = row["targetFont"]; let fontTestString = "12px " + fontFamily; let fontPath = "fonts/" + fontFamily + ".woff2"; if (debug) console.log("fontTestString: ", fontTestString); let response = fetch(fontPath).then((response) => { if (response.ok) { // let f = new FontFace(fontFamily, `url(${response.url})`); // f.load() // .then((loadedFontFace) => { // document.fonts.add(loadedFontFace); // }) // .catch((err) => { // console.error(err); // }); fontsRequired[fontFamily] = fontPath; } else { console.log( "Does the browser consider this font supported?", document.fonts.check(fontTestString) ); console.log( "Uh oh, unable to find the font file for: " + fontFamily + "\n" + "If this font is already supported by the browser then it should display correctly. " + "\n" + "If not, however, a different fallback font will be chosen by the browser, and your stimulus will not be displayed as intended. " + "\n" + "Please verify for yourself that " + fontFamily + " is being correctly represented in your experiment." ); } }); }); loadBlockFiles(count - 1, callback); }, }); }; var totalTrialConfig = { initialVal: 1, fontSize: 20, x: window.innerWidth / 2, y: -window.innerHeight / 2, fontName: "Arial", alignHoriz: "right", alignVert: "bottom", }; var totalTrial, // TextSim object totalTrialIndex = totalTrialConfig.initialVal, // numerical value of totalTrialIndex totalTrialCount = 0; var totalBlockConfig = { initialVal: 0, }; var totalBlockIndex = totalBlockConfig.initialVal, totalBlockTrialList = [], totalBlockCount = 0; const experiment = (blockCount) => { //// // Resources const _resources = []; for (let i = 1; i <= blockCount; i++) { _resources.push({ name: `conditions/block_${i}.csv`, path: `conditions/block_${i}.csv`, }); } if (debug) console.log("fontsRequired: ", fontsRequired); for (let i in fontsRequired) { if (debug) console.log(i, fontsRequired[i]); _resources.push({ name: i, path: fontsRequired[i] }); } // Start code blocks for 'Before Experiment' // init psychoJS: const psychoJS = new PsychoJS({ debug: debug, }); /* ---------------------------------- Sound --------------------------------- */ const correctSynth = getCorrectSynth(psychoJS); const wrongSynth = getWrongSynth(psychoJS); const purrSynth = getPurrSynth(psychoJS); // open window: psychoJS.openWindow({ fullscr: !debug, color: new util.Color([0.9, 0.9, 0.9]), units: "height", // TODO change to pix waitBlanking: true, }); // schedule the experiment: psychoJS.schedule( psychoJS.gui.DlgFromDict({ dictionary: expInfo, title: expName, }) ); const flowScheduler = new Scheduler(psychoJS); const dialogCancelScheduler = new Scheduler(psychoJS); psychoJS.scheduleCondition( function () { return psychoJS.gui.dialogComponent.button === "OK"; }, flowScheduler, dialogCancelScheduler ); // flowScheduler gets run if the participants presses OK flowScheduler.add(updateInfo); // add timeStamp flowScheduler.add(experimentInit); flowScheduler.add(fileRoutineBegin()); flowScheduler.add(fileRoutineEachFrame()); flowScheduler.add(fileRoutineEnd()); // flowScheduler.add(initInstructionRoutineBegin()); // flowScheduler.add(initInstructionRoutineEachFrame()); // flowScheduler.add(initInstructionRoutineEnd()); const blocksLoopScheduler = new Scheduler(psychoJS); flowScheduler.add(blocksLoopBegin(blocksLoopScheduler)); flowScheduler.add(blocksLoopScheduler); flowScheduler.add(blocksLoopEnd); flowScheduler.add(quitPsychoJS, "", true); // quit if user presses Cancel in dialog box: dialogCancelScheduler.add(quitPsychoJS, "", false); if (useRC) { expInfo["participant"] = rc.id.value; } if (debug) console.log("_resources: ", _resources); psychoJS.start({ expName: expName, expInfo: expInfo, resources: [ { name: "conditions/blockCount.csv", path: "conditions/blockCount.csv" }, ..._resources, ], }); psychoJS.experimentLogger.setLevel(core.Logger.ServerLevel.EXP); var frameDur; async function updateInfo() { expInfo["date"] = util.MonotonicClock.getDateStr(); // add a simple timestamp expInfo["expName"] = expName; expInfo["psychopyVersion"] = "2021.3.1"; expInfo["OS"] = rc.systemFamily.value; // store frame rate of monitor if we can measure it successfully expInfo["frameRate"] = psychoJS.window.getActualFrameRate(); if (typeof expInfo["frameRate"] !== "undefined") frameDur = 1.0 / Math.round(expInfo["frameRate"]); else frameDur = 1.0 / 60.0; // couldn't get a reliable measure so guess // add info from the URL: util.addInfoFromUrl(expInfo); return Scheduler.Event.NEXT; } var fileClock; var filterClock; var instructionsClock; var thisLoopNumber; // ! BLOCK COUNTER var thisConditionsFile; var trialClock; // var targetBoundingPoly; // Target Bounding Box var instructions; var key_resp; var fixation; //// var flanker1; var target; var flanker2; var showAlphabet; var globalClock; var routineTimer; async function experimentInit() { // Initialize components for Routine "file" fileClock = new util.Clock(); // Initialize components for Routine "filter" filterClock = new util.Clock(); instructionsClock = new util.Clock(); thisLoopNumber = 0; thisConditionsFile = "./conditions/block_1.csv"; // Initialize components for Routine "trial" trialClock = new util.Clock(); // Target Bounding Box // targetBoundingPoly = new visual.Rect ({ // win: psychoJS.window, name: 'targetBoundingPoly', units : 'pix', // width: [1.0, 1.0][0], height: [1.0, 1.0][1], // ori: 0.0, pos: [0, 0], // lineWidth: 1.0, lineColor: new util.Color('pink'), // // fillColor: new util.Color('pink'), // fillColor: undefined, // opacity: undefined, depth: -10, interpolate: true, // }); key_resp = new core.Keyboard({ psychoJS: psychoJS, clock: new util.Clock(), waitForStart: true, }); fixation = new visual.TextStim({ win: psychoJS.window, name: "fixation", text: "+", font: "Open Sans", units: "pix", pos: [0, 0], height: 1.0, wrapWidth: undefined, ori: 0.0, color: new util.Color("black"), opacity: undefined, depth: -6.0, }); flanker1 = new visual.TextStim({ win: psychoJS.window, name: "flanker1", text: "", font: "Arial", units: "pix", pos: [0, 0], height: 1.0, wrapWidth: undefined, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -7.0, }); target = new visual.TextStim({ win: psychoJS.window, name: "target", text: "", font: "Arial", units: "pix", pos: [0, 0], height: 1.0, wrapWidth: undefined, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -8.0, }); flanker2 = new visual.TextStim({ win: psychoJS.window, name: "flanker2", text: "", font: "Arial", units: "pix", pos: [0, 0], height: 1.0, wrapWidth: undefined, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -9.0, }); showAlphabet = new visual.TextStim({ win: psychoJS.window, name: "showAlphabet", text: "", font: "Arial", units: "pix", pos: [0, 0], height: 1.0, wrapWidth: window.innerWidth, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -5.0, }); totalTrial = new visual.TextStim({ win: psychoJS.window, name: "totalTrial", text: "", font: totalTrialConfig.fontName, units: "pix", pos: [totalTrialConfig.x, totalTrialConfig.y], alignHoriz: totalTrialConfig.alignHoriz, alignVert: totalTrialConfig.alignVert, height: 1.0, wrapWidth: undefined, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -20.0, }); instructions = new visual.TextStim({ win: psychoJS.window, name: "instructions", text: "", font: "Arial", units: "pix", pos: [-window.innerWidth * 0.4, window.innerHeight * 0.4], height: 32.0, wrapWidth: window.innerWidth * 0.8, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -12.0, alignHoriz: "left", alignVert: "top", }); // Create some handy timers globalClock = new util.Clock(); // to track the time since experiment started routineTimer = new util.CountdownTimer(); // to track time remaining of each (non-slip) routine return Scheduler.Event.NEXT; } var t; var frameN; var continueRoutine; var fileComponents; var clickedContinue; // TODO Read from config var responseType = 2; function fileRoutineBegin(snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); // ensure that .thisN vals are up to date //------Prepare to start Routine 'file'------- t = 0; fileClock.reset(); // clock frameN = -1; continueRoutine = true; // until we're told otherwise // update component parameters for each repeat // keep track of which components have finished fileComponents = []; for (const thisComponent of fileComponents) if ("status" in thisComponent) thisComponent.status = PsychoJS.Status.NOT_STARTED; return Scheduler.Event.NEXT; }; } function fileRoutineEachFrame() { return async function () { //------Loop for each frame of Routine 'file'------- // get current time t = fileClock.getTime(); frameN = frameN + 1; // number of completed frames (so 0 is the first frame) // update/draw components on each frame // check for quit (typically the Esc key) if ( psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0 ) { return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false); } // check if the Routine should terminate if (!continueRoutine) { // a component has requested a forced-end of Routine return Scheduler.Event.NEXT; } continueRoutine = false; // reverts to True if at least one component still running for (const thisComponent of fileComponents) if ( "status" in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED ) { continueRoutine = true; break; } // refresh the screen if continuing if (continueRoutine) { return Scheduler.Event.FLIP_REPEAT; } else { return Scheduler.Event.NEXT; } }; } function fileRoutineEnd() { return async function () { //------Ending Routine 'file'------- for (const thisComponent of fileComponents) { if (typeof thisComponent.setAutoDraw === "function") { thisComponent.setAutoDraw(false); } } // the Routine "file" was not non-slip safe, so reset the non-slip timer routineTimer.reset(); return Scheduler.Event.NEXT; }; } var _beepButton; function _instructionSetup(text) { t = 0; instructionsClock.reset(); // clock frameN = -1; continueRoutine = true; instructions.setWrapWidth(window.innerWidth * 0.8); instructions.setPos([-window.innerWidth * 0.4, window.innerHeight * 0.4]); instructions.setText(text); instructions.setAutoDraw(true); } function _clickContinue(e) { if (e.target.id !== "threshold-beep-button") clickedContinue = true; } async function _instructionRoutineEachFrame() { t = instructionsClock.getTime(); frameN = frameN + 1; if ( psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0 ) { document.removeEventListener("click", _clickContinue); document.removeEventListener("touchend", _clickContinue); removeBeepButton(_beepButton); return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false); } if (!continueRoutine) { return Scheduler.Event.NEXT; } continueRoutine = true; if (psychoJS.eventManager.getKeys({ keyList: ["return"] }).length > 0) { continueRoutine = false; } if (continueRoutine && !clickedContinue) { return Scheduler.Event.FLIP_REPEAT; } else { return Scheduler.Event.NEXT; } } async function _instructionRoutineEnd() { instructions.setAutoDraw(false); routineTimer.reset(); document.removeEventListener("click", _clickContinue); document.removeEventListener("touchend", _clickContinue); return Scheduler.Event.NEXT; } var blocks; var currentLoop; function blocksLoopBegin(blocksLoopScheduler, snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); // update internal variables (.thisN etc) of the loop // set up handler to look after randomisation of conditions etc blocks = new TrialHandler({ psychoJS: psychoJS, nReps: 1, method: TrialHandler.Method.SEQUENTIAL, extraInfo: expInfo, originPath: undefined, trialList: "conditions/blockCount.csv", seed: undefined, name: "blocks", }); psychoJS.experiment.addLoop(blocks); // add the loop to the experiment currentLoop = blocks; // we're now the current loop // Schedule all the trials in the trialList: for (const thisBlock of blocks) { const snapshot = blocks.getSnapshot(); blocksLoopScheduler.add(importConditions(snapshot)); blocksLoopScheduler.add(filterRoutineBegin(snapshot)); blocksLoopScheduler.add(filterRoutineEachFrame()); blocksLoopScheduler.add(filterRoutineEnd()); blocksLoopScheduler.add(initInstructionRoutineBegin(snapshot)); blocksLoopScheduler.add(initInstructionRoutineEachFrame()); blocksLoopScheduler.add(initInstructionRoutineEnd()); const trialsLoopScheduler = new Scheduler(psychoJS); blocksLoopScheduler.add(trialsLoopBegin(trialsLoopScheduler, snapshot)); blocksLoopScheduler.add(trialsLoopScheduler); blocksLoopScheduler.add(trialsLoopEnd); blocksLoopScheduler.add( endLoopIteration(blocksLoopScheduler, snapshot) ); } return Scheduler.Event.NEXT; }; } var trialsConditions; var trials; function trialsLoopBegin(trialsLoopScheduler, snapshot) { return async function () { // setup a MultiStairTrialHandler trialsConditions = TrialHandler.importConditions( psychoJS.serverManager, thisConditionsFile ); trials = new data.MultiStairHandler({ stairType: MultiStairHandler.StaircaseType.QUEST, psychoJS: psychoJS, name: "trials", varName: "trialsVal", nTrials: conditionTrials, conditions: trialsConditions, method: TrialHandler.Method.FULLRANDOM, }); psychoJS.experiment.addLoop(trials); // add the loop to the experiment currentLoop = trials; // we're now the current loop // Schedule all the trials in the trialList: for (const thisQuestLoop of trials) { const snapshot = trials.getSnapshot(); trialsLoopScheduler.add(importConditions(snapshot)); trialsLoopScheduler.add(trialInstructionRoutineBegin(snapshot)); trialsLoopScheduler.add(trialInstructionRoutineEachFrame()); trialsLoopScheduler.add(trialInstructionRoutineEnd()); trialsLoopScheduler.add(trialRoutineBegin(snapshot)); trialsLoopScheduler.add(trialRoutineEachFrame()); trialsLoopScheduler.add(trialRoutineEnd()); trialsLoopScheduler.add( endLoopIteration(trialsLoopScheduler, snapshot) ); } return Scheduler.Event.NEXT; }; } async function trialsLoopEnd() { psychoJS.experiment.addData( "staircaseName", currentLoop._currentStaircase._name ); psychoJS.experiment.addData( "questMeanAtEndOfTrialsLoop", currentLoop._currentStaircase.mean() ); psychoJS.experiment.addData( "questSDAtEndOfTrialsLoop", currentLoop._currentStaircase.sd() ); psychoJS.experiment.addData( "questQuantileOfQuantileOrderAtEndOfTrialsLoop", currentLoop._currentStaircase.quantile( currentLoop._currentStaircase._jsQuest.quantileOrder ) ); // terminate loop psychoJS.experiment.removeLoop(trials); return Scheduler.Event.NEXT; } async function blocksLoopEnd() { psychoJS.experiment.removeLoop(blocks); return Scheduler.Event.NEXT; } var filterComponents; function filterRoutineBegin(snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); // ensure that .thisN vals are up to date //------Prepare to start Routine 'filter'------- t = 0; filterClock.reset(); // clock frameN = -1; continueRoutine = true; // until we're told otherwise // update component parameters for each repeat thisLoopNumber += 1; thisConditionsFile = `conditions/block_${thisLoopNumber}.csv`; const possibleTrials = []; const thisBlockFileData = blockFiles[thisLoopNumber]; if (debug) console.log("thisBlockFileData: ", thisBlockFileData); for (let rowKey in thisBlockFileData) { let rowIndex = parseInt(rowKey); if (Object.keys(thisBlockFileData[rowIndex]).length > 1) { if (debug) console.log( "condition trials this row of block: ", parseInt(thisBlockFileData[rowIndex]["conditionTrials"]) ); possibleTrials.push( parseInt(thisBlockFileData[rowIndex]["conditionTrials"]) ); } } if (debug) console.log("possibleTrials: ", possibleTrials); totalTrialCount = possibleTrials.reduce((a, b) => a + b, 0); // sum of possible trials totalBlockCount = Object.keys(blockFiles).length; totalBlockTrialList = [...possibleTrials]; // console.log('totalBlockTrialList', totalBlockTrialList) // totalBlockCount = blockFiles.length; // TODO Remove this constraint to allow different # of trials for each condition if (!possibleTrials.every((a) => a === possibleTrials[0])) throw "Number of trials for each condition within one block has to be equal. (Will be updated soon.)"; conditionTrials = possibleTrials[0]; // keep track of which components have finished filterComponents = []; for (const thisComponent of filterComponents) if ("status" in thisComponent) thisComponent.status = PsychoJS.Status.NOT_STARTED; return Scheduler.Event.NEXT; }; } function filterRoutineEachFrame() { return async function () { //------Loop for each frame of Routine 'filter'------- // get current time t = filterClock.getTime(); frameN = frameN + 1; // number of completed frames (so 0 is the first frame) // update/draw components on each frame // check for quit (typically the Esc key) if ( psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0 ) { return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false); } // check if the Routine should terminate if (!continueRoutine) { // a component has requested a forced-end of Routine return Scheduler.Event.NEXT; } continueRoutine = false; // reverts to True if at least one component still running for (const thisComponent of filterComponents) if ( "status" in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED ) { continueRoutine = true; break; } // refresh the screen if continuing if (continueRoutine) { return Scheduler.Event.FLIP_REPEAT; } else { return Scheduler.Event.NEXT; } }; } function filterRoutineEnd() { return async function () { //------Ending Routine 'filter'------- for (const thisComponent of filterComponents) { if (typeof thisComponent.setAutoDraw === "function") { thisComponent.setAutoDraw(false); } } // the Routine "filter" was not non-slip safe, so reset the non-slip timer routineTimer.reset(); return Scheduler.Event.NEXT; }; } function initInstructionRoutineBegin(snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); _instructionSetup( instructionsText.initial(expInfo.participant) + instructionsText.initialByThresholdParameter["spacing"]( responseType, totalTrialCount ) + instructionsText.initialEnd(responseType) ); clickedContinue = false; document.addEventListener("click", _clickContinue); document.addEventListener("touchend", _clickContinue); _beepButton = addBeepButton(correctSynth); psychoJS.eventManager.clearKeys(); return Scheduler.Event.NEXT; }; } function initInstructionRoutineEachFrame() { return _instructionRoutineEachFrame; } function initInstructionRoutineEnd() { return async function () { instructions.setAutoDraw(false); routineTimer.reset(); document.removeEventListener("click", _clickContinue); document.removeEventListener("touchend", _clickContinue); removeBeepButton(_beepButton); return Scheduler.Event.NEXT; }; } function blockInstructionRoutineBegin(snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); _instructionSetup(instructionsText.block(snapshot.block + 1)); clickedContinue = false; document.addEventListener("click", _clickContinue); document.addEventListener("touchend", _clickContinue); return Scheduler.Event.NEXT; }; } function blockInstructionRoutineEachFrame() { return _instructionRoutineEachFrame; } function blockInstructionRoutineEnd() { return _instructionRoutineEnd; } const _takeFixationClick = (e) => { let cX, cY; if (e.clientX) { cX = e.clientX; cY = e.clientY; } else { const t = e.changedTouches[0]; if (t.clientX) { cX = t.clientX; cY = t.clientY; } else { clickedContinue = false; return; } } if ( Math.hypot( cX - (window.innerWidth >> 1), cY - (window.innerHeight >> 1) ) < fixationSize ) { // Clicked on fixation hideCursor(); setTimeout(() => { clickedContinue = true; }, 17); } else { // wrongSynth.play(); clickedContinue = false; } }; function trialInstructionRoutineBegin(snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); _instructionSetup(instructionsText.trial.fixate["spacing"](responseType)); fixation.setHeight(fixationSize); fixation.setPos(fixationXYPx); fixation.tStart = t; fixation.frameNStart = frameN; fixation.setAutoDraw(true); totalTrial.setAutoDraw(true); clickedContinue = false; document.addEventListener("click", _takeFixationClick); document.addEventListener("touchend", _takeFixationClick); psychoJS.eventManager.clearKeys(); return Scheduler.Event.NEXT; }; } function trialInstructionRoutineEachFrame() { return async function () { t = instructionsClock.getTime(); frameN = frameN + 1; if ( psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0 ) { return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false); } if (!continueRoutine) { return Scheduler.Event.NEXT; } continueRoutine = true; if (psychoJS.eventManager.getKeys({ keyList: ["space"] }).length > 0) { continueRoutine = false; } if (continueRoutine && !clickedContinue) { return Scheduler.Event.FLIP_REPEAT; } else { return Scheduler.Event.NEXT; } }; } function trialInstructionRoutineEnd() { return async function () { document.removeEventListener("click", _takeFixationClick); document.removeEventListener("touchend", _takeFixationClick); instructions.setAutoDraw(false); routineTimer.reset(); return Scheduler.Event.NEXT; }; } var level; var windowWidthCm; var windowWidthPx; var pixPerCm; var viewingDistanceDesiredCm; var viewingDistanceCm; var fixationXYPx = [0, 0]; var fixationSize = 45; // TODO Set on block begins var showFixation = true; var block; var spacingDirection; var targetFont; var targetAlphabet; var validAns; var showAlphabetWhere; var showAlphabetElement; var showCounterBool; var showViewingDistanceBool; const showAlphabetResponse = { current: null, onsetTime: 0, clickTime: 0 }; var targetDurationSec; var targetMinimumPix; var spacingOverSizeRatio; var targetEccentricityXDeg; var targetEccentricityYDeg; var targetEccentricityXYDeg; var trackGazeYes; var trackHeadYes; var wirelessKeyboardNeededYes; var _key_resp_allKeys; var trialComponents; function trialRoutineBegin(snapshot) { return async function () { TrialHandler.fromSnapshot(snapshot); // ensure that .thisN vals are up to date hideCursor(); //// if (debug) console.log( `Level: ${snapshot.getCurrentTrial().trialsVal}, Index: ${ snapshot.thisIndex }` ); let condition; for (let c of snapshot.handler.getConditions()) { if (c.label === trials._currentStaircase._name) { condition = c; } } if (debug) console.log("condition: ", condition); let proposedLevel = currentLoop._currentStaircase.getQuestValue(); if (debug) console.log("level from getQuestValue(): ", proposedLevel); psychoJS.experiment.addData("levelProposedByQUEST", proposedLevel); // TODO Find a real way of estimating the max size proposedLevel = Math.min(proposedLevel, 1.75); psychoJS.experiment.addData("levelRoughlyLimited", proposedLevel); psychoJS.experiment.addData("conditionName", condition["label"]); psychoJS.experiment.addData( "flankerOrientation", condition["spacingDirection"] ); psychoJS.experiment.addData("targetFont", condition["targetFont"]); // TODO add a data field that is unique to this staircase (ie differentiate staircases within the same block, if they have equivalent parameters) // TODO set QUEST // ! // ! //------Prepare to start Routine 'trial'------- t = 0; trialClock.reset(); // clock frameN = -1; continueRoutine = true; // until we're told otherwise // update component parameters for each repeat windowWidthCm = rc.screenWidthCm ? rc.screenWidthCm.value : 30; windowWidthPx = rc.displayWidthPx.value; pixPerCm = windowWidthPx / windowWidthCm; if (!rc.screenWidthCm) console.warn("[Screen Width] Using arbitrary screen width. Enable RC."); viewingDistanceDesiredCm = condition["viewingDistanceDesiredCm"]; viewingDistanceCm = rc.viewingDistanceCm ? rc.viewingDistanceCm.value : viewingDistanceDesiredCm; if (!rc.viewingDistanceCm) console.warn( "[Viewing Distance] Using arbitrary viewing distance. Enable RC." ); // TODO // ! Very inefficient to read params very trial as they do not change in a block // ! Move this to a block-level routine and store the values fixationXYPx = [0, 0]; block = condition["blockOrder"]; spacingDirection = condition["spacingDirection"]; targetFont = condition["targetFont"]; targetAlphabet = String(condition["targetAlphabet"]).split(""); validAns = String(condition["targetAlphabet"]).toLowerCase().split(""); showAlphabetWhere = condition["showAlphabetWhere"] || "bottom"; showViewingDistanceBool = condition["showViewingDistanceBool"] !== "FALSE"; showCounterBool = condition["showCounterBool"] !== "FALSE"; conditionTrials = condition["conditionTrials"]; targetDurationSec = condition["targetDurationSec"]; fixationSize = 45; // TODO use .csv parameters, ie draw as 2 lines, not one letter showFixation = condition["markTheFixationBool"] === "True"; targetMinimumPix = condition["targetMinimumPix"]; spacingOverSizeRatio = condition["spacingOverSizeRatio"]; targetEccentricityXDeg = condition["targetEccentricityXDeg"]; psychoJS.experiment.addData( "targetEccentricityXDeg", targetEccentricityXDeg ); targetEccentricityYDeg = condition["targetEccentricityYDeg"]; psychoJS.experiment.addData( "targetEccentricityYDeg", targetEccentricityYDeg ); targetEccentricityXYDeg = [ targetEccentricityXDeg, targetEccentricityYDeg, ]; trackGazeYes = condition["trackGazeYes"] === "True"; trackHeadYes = condition["trackHeadYes"] === "True"; wirelessKeyboardNeededYes = condition["wirelessKeyboardNeededYes"] === "True"; var alphabet = targetAlphabet; /* ------------------------------ Pick triplets ----------------------------- */ const tempAlphabet = shuffle(shuffle(alphabet)); var firstFlankerCharacter = tempAlphabet[0]; var targetCharacter = tempAlphabet[1]; var secondFlankerCharacter = tempAlphabet[2]; if (debug) console.log( firstFlankerCharacter, targetCharacter, secondFlankerCharacter ); correctAns = targetCharacter.toLowerCase(); /* -------------------------------------------------------------------------- */ var heightPx; var pos1XYDeg, pos1XYPx, pos2XYDeg, pos2XYPx, pos3XYDeg, pos3XYPx; var spacingDeg, spacingPx; //// // ! // TODO use actual nearPoint, from RC const nearPointXYDeg = { x: 0, y: 0 }; // TEMP const nearPointXYPix = { x: 0, y: 0 }; // TEMP const displayOptions = { pixPerCm: pixPerCm, viewingDistanceCm: viewingDistanceCm, nearPointXYDeg: nearPointXYDeg, nearPointXYPix: nearPointXYPix, spacingOverSizeRatio: spacingOverSizeRatio, minimumHeight: targetMinimumPix, fontFamily: targetFont, window: psychoJS.window, }; const [targetXYPix] = XYPixOfXYDeg( [targetEccentricityXYDeg], displayOptions ); level = await awaitMaxPresentableLevel( proposedLevel, targetXYPix, fixationXYPx, spacingDirection, displayOptions ); psychoJS.experiment.addData("levelUsed", level); if (debug) console.log("New level: ", level); spacingDeg = Math.pow(10, level); psychoJS.experiment.addData("spacingDeg", spacingDeg); if (debug) console.log("targetEccentricityXYDeg: ", targetEccentricityXYDeg); [pos1XYDeg, pos3XYDeg] = getFlankerLocations( targetEccentricityXYDeg, fixationXYPx, spacingDirection, spacingDeg ); if (debug) console.log("flanker locations: ", [pos1XYDeg, pos3XYDeg]); psychoJS.experiment.addData("flankerLocationsDeg", [ pos1XYDeg, pos3XYDeg, ]); pos2XYDeg = targetEccentricityXYDeg; [pos1XYPx, pos2XYPx, pos3XYPx] = XYPixOfXYDeg( [pos1XYDeg, pos2XYDeg, pos3XYDeg], displayOptions ); psychoJS.experiment.addData("targetLocationsPix", pos2XYPx); psychoJS.experiment.addData("flankerLocationsPix", [pos1XYPx, pos3XYPx]); spacingPx = Math.abs( degreesToPixels(spacingDeg, { pixPerCm: pixPerCm, viewingDistanceCm: viewingDistanceCm, }) ); psychoJS.experiment.addData("spacingPx", spacingPx); if (debug) console.log("spacingPx: ", spacingPx); if (debug) console.log( "spacing/spacingOverSizeRation: ", spacingPx / spacingOverSizeRatio ); if (debug) console.log("targetMinimumPix: ", targetMinimumPix); heightPx = Math.max(spacingPx / spacingOverSizeRatio, targetMinimumPix); key_resp.keys = undefined; key_resp.rt = undefined; _key_resp_allKeys = []; //// heightPx = Math.round(heightPx); pos1XYPx = pos1XYPx.map((x) => Math.round(x)); pos2XYPx = pos2XYPx.map((x) => Math.round(x)); pos3XYPx = pos3XYPx.map((x) => Math.round(x)); fixation.setPos(fixationXYPx); fixation.setHeight(fixationSize); flanker1.setPos(pos1XYPx); flanker1.setText(firstFlankerCharacter); flanker1.setFont(targetFont); flanker1.setHeight(heightPx); target.setPos(pos2XYPx); target.setText(targetCharacter); target.setFont(targetFont); target.setHeight(heightPx); flanker2.setPos(pos3XYPx); flanker2.setText(secondFlankerCharacter); flanker2.setFont(targetFont); flanker2.setHeight(heightPx); showAlphabet.setPos([0, 0]); showAlphabet.setText(""); // showAlphabet.setText(getAlphabetShowText(validAns)) instructions.setText( instructionsText.trial.respond["spacing"](responseType) ); // totalTrial.setPos([totalTrialConfig.x, totalTrialConfig.y]); // totalTrial.setAlignHoriz('right'); // totalTrial.setAlignVert('bottom'); totalBlockIndex = calculateBlockWithTrialIndex( totalBlockTrialList, totalTrialIndex ); let trialInfoStr = ""; if (showCounterBool) trialInfoStr = `Block ${totalBlockIndex} of ${totalBlockCount}. Trial ${totalTrialIndex} of ${totalTrialCount}.`; if (showViewingDistanceBool) trialInfoStr += ` At ${viewingDistanceCm} cm.`; totalTrial.setText(trialInfoStr); totalTrial.setFont(totalTrialConfig.fontName); totalTrial.setHeight(totalTrialConfig.fontSize); totalTrial.setPos([window.innerWidth / 2, -window.innerHeight / 2]); // keep track of which components have finished trialComponents = []; trialComponents.push(key_resp); // trialComponents.push(targetBoundingPoly); // Target Bounding Box trialComponents.push(fixation); trialComponents.push(flanker1); trialComponents.push(target); trialComponents.push(flanker2); trialComponents.push(showAlphabet); trialComponents.push(totalTrial); for (const thisComponent of trialComponents) if ("status" in thisComponent) thisComponent.status = PsychoJS.Status.NOT_STARTED; // update trial index totalTrialIndex = totalTrialIndex + 1; return Scheduler.Event.NEXT; }; } var frameRemains; function trialRoutineEachFrame() { return async function () { //------Loop for each frame of Routine 'trial'------- // get current time t = trialClock.getTime(); frameN = frameN + 1; // number of completed frames (so 0 is the first frame) // update/draw components on each frame // Target Bounding Box // // *targetBoundingPoly* updates // if (t >= 0.0 && targetBoundingPoly.status === PsychoJS.Status.NOT_STARTED) { // // keep track of start time/frame for later // targetBoundingPoly.tStart = t; // (not accounting for frame time here) // targetBoundingPoly.frameNStart = frameN; // exact frame index // targetBoundingPoly.setAutoDraw(true); // } // if (targetBoundingPoly.status === PsychoJS.Status.STARTED){ // only update if being drawn // const tightBoundingBox = target.getBoundingBox(true); // targetBoundingPoly.setPos([tightBoundingBox.left, tightBoundingBox.top]); // targetBoundingPoly.setSize([tightBoundingBox.width, tightBoundingBox.height]); // } const uniDelay = 0.5; // *key_resp* updates if (t >= uniDelay && key_resp.status === PsychoJS.Status.NOT_STARTED) { // keep track of start time/frame for later key_resp.tStart = t; // (not accounting for frame time here) key_resp.frameNStart = frameN; // exact frame index // TODO Use PsychoJS clock if possible // Reset together with PsychoJS showAlphabetResponse.onsetTime = performance.now(); // keyboard checking is just starting psychoJS.window.callOnFlip(function () { key_resp.clock.reset(); }); // t=0 on next screen flip psychoJS.window.callOnFlip(function () { key_resp.start(); }); // start on screen flip psychoJS.window.callOnFlip(function () { key_resp.clearEvents(); }); } if (key_resp.status === PsychoJS.Status.STARTED) { let theseKeys = key_resp.getKeys({ keyList: validAns, waitRelease: false, }); _key_resp_allKeys = _key_resp_allKeys.concat(theseKeys); if (_key_resp_allKeys.length > 0) { key_resp.keys = _key_resp_allKeys[_key_resp_allKeys.length - 1].name; // just the last key pressed key_resp.rt = _key_resp_allKeys[_key_resp_allKeys.length - 1].rt; // was this correct? if (key_resp.keys == correctAns) { // Play correct audio correctSynth.play(); key_resp.corr = 1; } else { // Play wrong audio key_resp.corr = 0; } // a response ends the routine continueRoutine = false; } } // *showAlphabetResponse* updates if (showAlphabetResponse.current) { key_resp.keys = showAlphabetResponse.current; key_resp.rt = (showAlphabetResponse.clickTime - showAlphabetResponse.onsetTime) / 1000; if (showAlphabetResponse.current == correctAns) { // Play correct audio correctSynth.play(); key_resp.corr = 1; } else { // Play wrong audio key_resp.corr = 0; } showAlphabetResponse.current = null; removeClickableAlphabet(); continueRoutine = false; } // *fixation* updates if ( t >= 0.0 && fixation.status === PsychoJS.Status.NOT_STARTED && showFixation ) { // keep track of start time/frame for later fixation.tStart = t; // (not accounting for frame time here) fixation.frameNStart = frameN; // exact frame index fixation.setAutoDraw(true); } // *totalTrial* updates if (t >= 0.0 && totalTrial.status === PsychoJS.Status.NOT_STARTED) { // keep track of start time/frame for later totalTrial.tStart = t; // (not accounting for frame time here) totalTrial.frameNStart = frameN; // exact frame index totalTrial.setAutoDraw(true); } // *flanker1* updates if (t >= uniDelay && flanker1.status === PsychoJS.Status.NOT_STARTED) { // keep track of start time/frame for later flanker1.tStart = t; // (not accounting for frame time here) flanker1.frameNStart = frameN; // exact frame index flanker1.setAutoDraw(true); } frameRemains = uniDelay + targetDurationSec - psychoJS.window.monitorFramePeriod * 0.75; // most of one frame period left if (flanker1.status === PsychoJS.Status.STARTED && t >= frameRemains) { flanker1.setAutoDraw(false); } // *target* updates if (t >= uniDelay && target.status === PsychoJS.Status.NOT_STARTED) { // keep track of start time/frame for later target.tStart = t; // (not accounting for frame time here) target.frameNStart = frameN; // exact frame index target.setAutoDraw(true); } frameRemains = uniDelay + targetDurationSec - psychoJS.window.monitorFramePeriod * 0.75; // most of one frame period left if (target.status === PsychoJS.Status.STARTED && t >= frameRemains) { target.setAutoDraw(false); // Play purr sound // Wait until next frame to play setTimeout(() => { purrSynth.play(); }, 17); setTimeout(() => { showCursor(); }, 500); } // *flanker2* updates if (t >= uniDelay && flanker2.status === PsychoJS.Status.NOT_STARTED) { // keep track of start time/frame for later flanker2.tStart = t; // (not accounting for frame time here) flanker2.frameNStart = frameN; // exact frame index flanker2.setAutoDraw(true); } frameRemains = uniDelay + targetDurationSec - psychoJS.window.monitorFramePeriod * 0.75; // most of one frame period left if (flanker2.status === PsychoJS.Status.STARTED && t >= frameRemains) { flanker2.setAutoDraw(false); } // check for quit (typically the Esc key) if ( psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({ keyList: ["escape"] }).length > 0 ) { return quitPsychoJS("The [Escape] key was pressed. Goodbye!", false); } /* -------------------------------------------------------------------------- */ // *showAlphabet* updates if ( t >= uniDelay + targetDurationSec && showAlphabet.status === PsychoJS.Status.NOT_STARTED ) { // keep track of start time/frame for later showAlphabet.tStart = t; // (not accounting for frame time here) showAlphabet.frameNStart = frameN; // exact frame index showAlphabet.setAutoDraw(true); showAlphabetElement = setupClickableAlphabet( targetAlphabet, targetFont, showAlphabetWhere, showAlphabetResponse ); instructions.tSTart = t; instructions.frameNStart = frameN; instructions.setAutoDraw(true); } /* -------------------------------------------------------------------------- */ // check if the Routine should terminate if (!continueRoutine) { // a component has requested a forced-end of Routine removeClickableAlphabet(); return Scheduler.Event.NEXT; } continueRoutine = false; // reverts to True if at least one component still running for (const thisComponent of trialComponents) if ( "status" in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED ) { continueRoutine = true; break; } // refresh the screen if continuing if (continueRoutine) { return Scheduler.Event.FLIP_REPEAT; } else { return Scheduler.Event.NEXT; } }; } function trialRoutineEnd() { return async function () { //------Ending Routine 'trial'------- for (const thisComponent of trialComponents) { if (typeof thisComponent.setAutoDraw === "function") { thisComponent.setAutoDraw(false); } } // was no response the correct answer?! if (key_resp.keys === undefined) { console.error("[key_resp.keys] No response error."); } // store data for psychoJS.experiment (ExperimentHandler) // update the trial handler if (currentLoop instanceof MultiStairHandler) { currentLoop.addResponse(key_resp.corr, level); if (debug) console.log("level passed to addResponse: ", level); } psychoJS.experiment.addData("key_resp.keys", key_resp.keys); psychoJS.experiment.addData("key_resp.corr", key_resp.corr); if (typeof key_resp.keys !== "undefined") { // we had a response psychoJS.experiment.addData("key_resp.rt", key_resp.rt); routineTimer.reset(); } key_resp.stop(); // the Routine "trial" was not non-slip safe, so reset the non-slip timer routineTimer.reset(); psychoJS.experiment.addData( "staircaseName", currentLoop._currentStaircase._name ); psychoJS.experiment.addData( "questMeanAtEndOfTrial", currentLoop._currentStaircase.mean() ); psychoJS.experiment.addData( "questSDAtEndOfTrial", currentLoop._currentStaircase.sd() ); psychoJS.experiment.addData( "questQuantileOfQuantileOrderAtEndOfTrial", currentLoop._currentStaircase.quantile( currentLoop._currentStaircase._jsQuest.quantileOrder ) ); return Scheduler.Event.NEXT; }; } function endLoopIteration(scheduler, snapshot) { // ------Prepare for next entry------ return async function () { if (typeof snapshot !== "undefined") { // ------Check if user ended loop early------ if (snapshot.finished) { // Check for and save orphaned data if (psychoJS.experiment.isEntryEmpty()) { psychoJS.experiment.nextEntry(snapshot); } scheduler.stop(); } else { const thisTrial = snapshot.getCurrentTrial(); if ( typeof thisTrial === "undefined" || !("isTrials" in thisTrial) || thisTrial.isTrials ) { psychoJS.experiment.nextEntry(snapshot); } } return Scheduler.Event.NEXT; } }; } function importConditions(currentLoop) { return async function () { psychoJS.importAttributes(currentLoop.getCurrentTrial()); return Scheduler.Event.NEXT; }; } async function quitPsychoJS(message, isCompleted) { // Check for and save orphaned data if (psychoJS.experiment.isEntryEmpty()) { psychoJS.experiment.nextEntry(); } psychoJS.window.close(); psychoJS.quit({ message: message, isCompleted: isCompleted }); return Scheduler.Event.QUIT; } }; /* Utilities */ /** * Convert a number of visual degrees to pixels VERIFY * @param {Number} degrees Scalar, in degrees * @param {Object} displayOptions Parameters about the stimulus presentation * @param {Number} displayOptions.pixPerCm Pixels per centimeter on screen * @param {Number} displayOptions.viewingDistanceCm Distance (in cm) of participant from screen * @returns {Number} */ function degreesToPixels(degrees, displayOptions) { const radians = degrees * (Math.PI / 180); const pixels = displayOptions.pixPerCm * displayOptions.viewingDistanceCm * Math.tan(radians); return pixels; } /** * Translation of MATLAB function of the same name * by Prof Denis Pelli, XYPixOfXYDeg.m * @param {Array} xyDeg List of [x,y] pairs, representing points x degrees right, and y degrees up, of fixation * @param {Object} displayOptions Parameters about the stimulus presentation * @param {Number} displayOptions.pixPerCm Pixels per centimeter on screen * @param {Number} displayOptions.viewingDistanceCm Distance (in cm) of participant from screen * @param {Object} displayOptions.nearPointXYDeg Near-point on screen, in degrees relative to fixation(?) * @param {Number} displayOptions.nearPointXYDeg.x Degrees along x-axis of near-point from fixation * @param {Number} displayOptions.nearPointXYDeg.y Degrees along y-axis of near-point from fixation * @param {Object} displayOptions.nearPointXYPix Near-point on screen, in pixels relative to origin(?) * @param {Number} displayOptions.nearPointXYPix.x Pixels along x-axis of near-point from origin * @param {Number} displayOptions.nearPointXYPix.y Pixels along y-axis of near-point from origin * @returns {Number[][]} Array of length=2 arrays of numbers, representing the same points in Pixel space */ function XYPixOfXYDeg(xyDeg, displayOptions) { if (xyDeg.length == 0) { return; } // Return if no points to transform // TODO verify displayOptions has the correct parameters const xyPix = []; xyDeg.forEach((position) => { position[0] = position[0] - displayOptions.nearPointXYDeg.x; position[1] = position[1] - displayOptions.nearPointXYDeg.y; const rDeg = Math.sqrt(position[0] ** 2 + position[1] ** 2); const rPix = degreesToPixels(rDeg, displayOptions); let pixelPosition = []; if (rDeg > 0) { pixelPosition = [ (position[0] * rPix) / rDeg, (position[1] * rPix) / rDeg, ]; } else { pixelPosition = [0, 0]; } pixelPosition[0] = pixelPosition[0] + displayOptions.nearPointXYPix.x; pixelPosition[1] = pixelPosition[1] + displayOptions.nearPointXYPix.x; xyPix.push(pixelPosition); }); return xyPix; } /** * Given a spacing value (in pixels), estimate a (non-tight) bounding box * @param {Number} spacing Spacing which will be used to place flanker * @param {Number} spacingOverSizeRatio Specified ratio of distance between flanker&target to letter height * @param {Number} minimumHeight Smallest allowable letter height for flanker * @param {String} font Font-family in which the stimuli will be presented * @param {PsychoJS.window} window PsychoJS window, used to create a stimulus to be measured * @returns */ function boundingBoxFromSpacing( spacing, spacingOverSizeRatio, minimumHeight, font, window ) { const height = Math.max(spacing / spacingOverSizeRatio, minimumHeight); try { const testTextStim = new visual.TextStim({ win: window, name: "testTextStim", text: "H", // TEMP font: font, units: "pix", // ASSUMES that parameters are in pixel units pos: [0, 0], height: height, wrapWidth: undefined, ori: 0.0, color: new util.Color("black"), opacity: 1.0, depth: -7.0, autoDraw: false, autoLog: false, }); const estimatedBoundingBox = testTextStim._boundingBox; return estimatedBoundingBox; } catch (error) { console.error( "Error estimating bounding box of flanker. Likely due to too large a `proposedLevel` value being tested.", error ); return error; } } /** * Calculate the (2D) coordinates of two tangential flankers, linearly symmetrical around a target at targetPosition * @todo Add parameter/support for log-symmetric spacing * @param {Number[]} targetPosition [x,y] position of the target * @param {Number[]} fixationPosition [x,y] position of the fixation point * @param {Number} spacing How far the flankers are to be from the target (in the same units as the target & fixation positions) * @returns {Number[][]} Array containing two Arrays which represent the positions of Flanker 1 and Flanker 2 */ function tangentialFlankerPositions(targetPosition, fixationPosition, spacing) { let x, i; // Variables for anonymous fn's // Vector representing the line between target and fixation const v = [ fixationPosition[0] - targetPosition[0], fixationPosition[1] - targetPosition[1], ]; // Get the vector perpendicular to v const p = [v[1], -v[0]]; // SEE https://gamedev.stackexchange.com/questions/70075/how-can-i-find-the-perpendicular-to-a-2d-vector // Find the point that is `spacing` far from `targetPosition` along p // SEE https://math.stackexchange.com/questions/175896/finding-a-point-along-a-line-a-certain-distance-away-from-another-point /// Find the length of `p` const llpll = Math.sqrt( p.map((x) => x ** 2).reduce((previous, current) => previous + current) ); /// Normalize `p` const u = p.map((x) => x / llpll); /// Find our two new points, `spacing` distance away from targetPosition along line `p` const flankerPositions = [ targetPosition.map((x, i) => x + spacing * u[i]), targetPosition.map((x, i) => x - spacing * u[i]), ]; return flankerPositions; } /** * Calculate the (2D) coordinates of two radial flankers, linearly symmetrical around a target at targetPosition * @todo Add parameter/support for log-symmetric spacing * @param {Number[]} targetPosition [x,y] position of the target * @param {Number[]} fixationPosition [x,y] position of the fixation point * @param {Number} spacing How far the flankers are to be from the target (in the same units as the target & fixation positions) * @returns {Number[][]} Array containing two Arrays, which represent the positions of Flanker 1 and Flanker 2 */ function radialFlankerPositions(targetPosition, fixationPosition, spacing) { // SEE https://math.stackexchange.com/questions/175896/finding-a-point-along-a-line-a-certain-distance-away-from-another-point // Vector representing the line between target and fixation const v = [ fixationPosition[0] - targetPosition[0], fixationPosition[1] - targetPosition[1], ]; /// Find the length of v const llvll = Math.sqrt( v.map((x) => x ** 2).reduce((previous, current) => previous + current) ); /// Normalize v const u = v.map((x) => x / llvll); /// Find our two new points, `spacing` distance away from targetPosition along line v const flankerPositions = [ targetPosition.map((x, i) => x + spacing * u[i]), targetPosition.map((x, i) => x - spacing * u[i]), ]; return flankerPositions; } /** * Return the coordinates of the two flankers around a given target. * @param {Number[]} targetPosition [x,y] position of the target stimuli * @param {Number[]} fixationPosition [x,y] position of the fixation stimuli * @param {("radial"|"tangential")} flankerOrientation String specifying the position of the flankers relative to the line between fixation and the target * @param {Number} spacing Distance between the target and one flanker * @returns {Number[][]} Array containing two [x,y] arrays, each representing the location of one flanker */ function getFlankerLocations( targetPosition, fixationPosition, flankerOrientation, spacing ) { switch (flankerOrientation) { case "radial": return radialFlankerPositions(targetPosition, fixationPosition, spacing); case "tangential": return tangentialFlankerPositions( targetPosition, fixationPosition, spacing ); default: console.error( "Unknown flankerOrientation specified, ", flankerOrientation ); } } /** * Return the extreme points of a rectangle bounding the pair of flankers * @param {Number} level Suggested level from QUEST * @param {Number[]} targetPosition [x,y] position of the target stimulus * @param {Number[]} fixationPosition [x,y] position of the fixation stimulus * @param {("radial"|"tangential")} flankerOrientation Arrangement of the flankers relative to the line between fixation and target * @param {Object} sizingParameters Parameters for drawing stimuli * @param {Number} sizingParameters.spacingOverSizeRatio Ratio of distance between flanker&target to stimuli letter height * @param {Number} sizingParameters.minimumHeight Minimum stimulus letter height (in same units as other parameters) * @param {String} sizingParameters.fontFamily Name of the fontFamily in which the stimuli will be drawn * @param {Number} sizingParameters.pixPerCm Pixel/cm ratio of the display * @param {Number} sizingParameters.viewingDistanceCm Distance (in cm) of the observer from the near-point * @param {PsychoJS.window} sizingParameters.window Window object, used for creating a mock stimuli for measurement * @returns {Number[][]} [[x_min, y_min], [x_max, y_max]] Array of defining points of the area over which flankers extend */ function flankersExtent( level, targetPosition, fixationPosition, flankerOrientation, sizingParameters ) { if (debug) console.log("window: ", sizingParameters.window); const spacingDegrees = Math.pow(10, level); const spacingPixels = Math.abs( degreesToPixels(spacingDegrees, { pixPerCm: sizingParameters.pixPerCm, viewingDistanceCm: sizingParameters.viewingDistanceCm, }) ); const flankerLocations = getFlankerLocations( targetPosition, fixationPosition, flankerOrientation, spacingPixels ); try { const flankerBoxDimensions = boundingBoxFromSpacing( spacingPixels, sizingParameters.spacingOverSizeRatio, sizingParameters.minimumHeight, sizingParameters.fontFamily, sizingParameters.window ); const boundingPoints = []; flankerLocations.forEach((flankerPosition, i) => { const boundingPoint = []; if (targetPosition[0] < 0) { boundingPoint.push( flankerPosition[0] - (i === 0 ? -1 : 1) * (flankerBoxDimensions.width / 2) ); } else { boundingPoint.push( flankerPosition[0] + (i === 0 ? -1 : 1) * (flankerBoxDimensions.width / 2) ); } if (targetPosition[1] < 0) { boundingPoint.push( flankerPosition[1] - (i === 0 ? -1 : 1) * (flankerBoxDimensions.height / 2) ); } else { boundingPoint.push( flankerPosition[1] + (i === 0 ? -1 : 1) * (flankerBoxDimensions.height / 2) ); } boundingPoints.push(boundingPoint); }); return boundingPoints; } catch (error) { console.error("Error estimating flankers extent.", error); return error; } } /** * Determine whether a given point lies inside a given rectangle * @param {Number[][]} rectangle Array of two [x,y] points, which define an area * @param {Number[]} point [x,y] coordinate of a point which may be within rectangle * @returns {Boolean} */ function rectangleContainsPoint(rectangle, point) { const leftX = Math.min(rectangle[0][0], rectangle[1][0]); const rightX = Math.max(rectangle[0][0], rectangle[1][0]); const lowerY = Math.min(rectangle[0][1], rectangle[1][1]); const upperY = Math.max(rectangle[0][1], rectangle[1][1]); const xIsIn = point[0] >= leftX && point[0] <= rightX; const yIsIn = point[1] >= lowerY && point[1] <= upperY; if (debug) { console.log("flanker rectangle: ", rectangle); console.log("xIsIn: ", xIsIn); console.log("yIsIn: ", yIsIn); } return xIsIn && yIsIn; } /** * Determines whether any part of a given rectangle will extend beyond the screen * @param {Number[][]} rectangle Array of two [x,y] points, defining a rectangle * @param {Object} screenDimensions * @param {Number} screenDimensions.width Width of the screen * @param {Number} screenDimensions.height Height of the screen * @returns {Boolean} */ function rectangleOffscreen(rectangle, screenDimensions) { const pointOffScreen = (point) => Math.abs(point[0]) > screenDimensions.width / 2 || Math.abs(point[1]) > screenDimensions.height / 2; return rectangle.some(pointOffScreen); // VERIFY this logic is correct } /** * Tests whether these proposed parameters for presentation would draw improperly, eg extend beyond the extent of the screen * @todo Test whether the flankers interfer with eachother * @param {Number} proposedLevel Level to be tested, as provided by QUEST * @param {Number[]} targetXYPix [x,y] position of the target (in pixels) * @param {Number[]} fixationXYPix [x,y] position of the fixation (in pixels) * @param {"radial"|"tangential"} spacingDirection Orientation of flankers relative to fixation-target * @param {Object} displayOptions Set of parameters for the specifics of presentation * @todo Specify necessary members of `displayOptions` * @returns {Boolean} */ function unacceptableStimuli( proposedLevel, targetXYPix, fixationXYPix, spacingDirection, displayOptions ) { const areaFlankersCover = flankersExtent( proposedLevel, targetXYPix, fixationXYPix, spacingDirection, displayOptions ); // TODO take the size of fixation into account const fixationInfringed = rectangleContainsPoint( areaFlankersCover, fixationXYPix ); const stimuliExtendOffscreen = rectangleOffscreen(areaFlankersCover, { width: screen.width, height: screen.height, }); const badPresentation = fixationInfringed || stimuliExtendOffscreen; if (debug) { console.log("areaFlankersCover: ", areaFlankersCover); console.log("fixationInfringed: ", fixationInfringed); console.log("stimuliExtendOffscreen: ", stimuliExtendOffscreen); console.log("badPresentation: ", badPresentation); } return badPresentation; } /** * Estimate the largest `level` value which will still present correctly * @param {Number} proposedLevel Level to be tested, as provided by QUEST * @param {Number[]} targetXYPix [x,y] position of the target (in pixels) * @param {Number[]} fixationXYPix [x,y] position of the fixation (in pixels) * @param {"radial"|"tangential"} spacingDirection Orientation of flankers relative to fixation-target * @param {Object} displayOptions Set of parameters for the specifics of presentation * @todo Specify necessary members of `displayOptions` * @returns {Number} */ function getMaxPresentableLevel( proposedLevel, targetXYPix, fixationXYPix, spacingDirection, displayOptions ) { const granularityOfChange = 0.05; if ( !unacceptableStimuli( proposedLevel, targetXYPix, fixationXYPix, spacingDirection, displayOptions ) ) { if (debug) console.log("acceptable level found: ", proposedLevel); return proposedLevel; } else { if (debug) console.log("unacceptable level: ", proposedLevel); return getMaxPresentableLevel( proposedLevel - granularityOfChange, targetXYPix, fixationXYPix, spacingDirection, displayOptions ); } } /** * Promise-based equivalent to `getMaxPresentableLevel` * @param {Number} proposedLevel Level to be tested, as provided by QUEST * @param {Number[]} targetXYPix [x,y] position of the target (in pixels) * @param {Number[]} fixationXYPix [x,y] position of the fixation (in pixels) * @param {("radial"|"tangential")} spacingDirection Orientation of flankers relative to fixation-target * @param {Object} displayOptions Set of parameters for the specifics of presentation * @todo Specify necessary members of `displayOptions` * @returns {Number} */ function awaitMaxPresentableLevel( proposedLevel, targetXYPix, fixationXYPix, spacingDirection, displayOptions ) { const granularityOfChange = 0.05; if ( unacceptableStimuli( granularityOfChange, targetXYPix, fixationXYPix, spacingDirection, displayOptions ) ) { console.error( "Unpresentable stimuli, even at level=" + String(granularityOfChange) ); return new Promise((resolve) => resolve(granularityOfChange)); } if ( !unacceptableStimuli( proposedLevel, targetXYPix, fixationXYPix, spacingDirection, displayOptions ) ) { if (debug) console.log("acceptable level found: ", proposedLevel); return new Promise((resolve) => resolve(proposedLevel)); } else { if (debug) console.log("unacceptable level: ", proposedLevel); return awaitMaxPresentableLevel( proposedLevel - granularityOfChange, targetXYPix, fixationXYPix, spacingDirection, displayOptions ); } }
Unterschied finden