Diff
checker
テキスト
テキスト
画像
ドキュメント
Excel
フォルダ
Legal
Enterprise
デスクトップ
料金
ログイン
Diffchecker デスクトップのダウンロード
テキスト比較
2 つのテキスト ファイルの違いを見つける
ツール
履歴
ライブエディター
空白の変更を非表示
未変更行を折りたたむ
折り返しなし
レイアウト
分割
統合
比較精度
スマート
単語
文字
テキストスタイル
外観を変更
シンタックスハイライト
構文を選択
無視
テキスト変換
最初の差分へ移動
入力を編集
Diffchecker Desktop
Diffcheckerを実行する最も安全な方法。Diffchecker Desktopアプリを入手:あなたの差分はコンピューターから出ることはありません!
Desktopを入手
arabic_demo
作成日
5 年前
差分は期限切れになりません
クリア
エクスポート
共有
説明
19 削除
行
合計
削除
文字
合計
削除
この機能を引き続き使用するには、アップグレードしてください
Diff
checker
Pro
価格を見る
843 行
すべてコピー
18 追加
行
合計
追加
文字
合計
追加
この機能を引き続き使用するには、アップグレードしてください
Diff
checker
Pro
価格を見る
840 行
すべてコピー
/*****************
/*****************
* 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" };
コピー
コピー済み
コピー
コピー済み
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;
コピー
コピー済み
コピー
コピー済み
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) {
コピー
コピー済み
コピー
コピー済み
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);
コピー
コピー済み
コピー
コピー済み
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();
コピー
コピー済み
コピー
コピー済み
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
保存された差分
原文
ファイルを開く
/***************** * 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 ); } }
変更されたテキスト
ファイルを開く
/***************** * 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 ); } }
違いを見つける