jspsych
Version:
Behavioral experiments in a browser
1,688 lines (1,594 loc) • 143 kB
JavaScript
var jsPsychModule = (function (exports) {
'use strict';
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
// Gets all non-builtin properties up the prototype chain
const getAllProperties = object => {
const properties = new Set();
do {
for (const key of Reflect.ownKeys(object)) {
properties.add([object, key]);
}
} while ((object = Reflect.getPrototypeOf(object)) && object !== Object.prototype);
return properties;
};
var autoBind = (self, {include, exclude} = {}) => {
const filter = key => {
const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key);
if (include) {
return include.some(match);
}
if (exclude) {
return !exclude.some(match);
}
return true;
};
for (const [object, key] of getAllProperties(self.constructor.prototype)) {
if (key === 'constructor' || !filter(key)) {
continue;
}
const descriptor = Reflect.getOwnPropertyDescriptor(object, key);
if (descriptor && typeof descriptor.value === 'function') {
self[key] = self[key].bind(self);
}
}
return self;
};
var autoBind$1 = /*@__PURE__*/getDefaultExportFromCjs(autoBind);
var version = "8.2.2";
class ExtensionManager {
constructor(dependencies, extensionsConfiguration) {
this.dependencies = dependencies;
this.extensionsConfiguration = extensionsConfiguration;
this.extensions = Object.fromEntries(
extensionsConfiguration.map((extension) => [
ExtensionManager.getExtensionNameByClass(extension.type),
this.dependencies.instantiateExtension(extension.type)
])
);
}
static getExtensionNameByClass(extensionClass) {
return extensionClass["info"].name;
}
getExtensionInstanceByClass(extensionClass) {
return this.extensions[ExtensionManager.getExtensionNameByClass(extensionClass)];
}
async initializeExtensions() {
await Promise.all(
this.extensionsConfiguration.map(({ type, params = {} }) => {
this.getExtensionInstanceByClass(type).initialize(params);
const extensionInfo = type["info"];
if (!("version" in extensionInfo) && !("data" in extensionInfo)) {
console.warn(
extensionInfo["name"],
"is missing the 'version' and 'data' fields. Please update extension as 'version' and 'data' will be required in v9. See https://www.jspsych.org/latest/developers/extension-development/ for more details."
);
} else if (!("version" in extensionInfo)) {
console.warn(
extensionInfo["name"],
"is missing the 'version' field. Please update extension as 'version' will be required in v9. See https://www.jspsych.org/latest/developers/extension-development/ for more details."
);
} else if (!("data" in extensionInfo)) {
console.warn(
extensionInfo["name"],
"is missing the 'data' field. Please update extension as 'data' will be required in v9. See https://www.jspsych.org/latest/developers/extension-development/ for more details."
);
}
})
);
}
onStart(trialExtensionsConfiguration = []) {
for (const { type, params } of trialExtensionsConfiguration) {
this.getExtensionInstanceByClass(type)?.on_start(params);
}
}
onLoad(trialExtensionsConfiguration = []) {
for (const { type, params } of trialExtensionsConfiguration) {
this.getExtensionInstanceByClass(type)?.on_load(params);
}
}
async onFinish(trialExtensionsConfiguration = []) {
const results = await Promise.all(
trialExtensionsConfiguration.map(
({ type, params }) => Promise.resolve(this.getExtensionInstanceByClass(type)?.on_finish(params))
)
);
const extensionInfos = trialExtensionsConfiguration.length ? {
extension_type: trialExtensionsConfiguration.map(({ type }) => type["info"].name),
extension_version: trialExtensionsConfiguration.map(({ type }) => type["info"].version)
} : {};
results.unshift(extensionInfos);
return Object.assign({}, ...results);
}
}
function unique(arr) {
return [...new Set(arr)];
}
function deepCopy(obj) {
if (!obj) return obj;
let out;
if (Array.isArray(obj)) {
out = [];
for (const x of obj) {
out.push(deepCopy(x));
}
return out;
} else if (typeof obj === "object" && obj !== null) {
out = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
out[key] = deepCopy(obj[key]);
}
}
return out;
} else {
return obj;
}
}
function deepMerge(obj1, obj2) {
let merged = {};
for (const key in obj1) {
if (obj1.hasOwnProperty(key)) {
if (typeof obj1[key] === "object" && obj2.hasOwnProperty(key)) {
merged[key] = deepMerge(obj1[key], obj2[key]);
} else {
merged[key] = obj1[key];
}
}
}
for (const key in obj2) {
if (obj2.hasOwnProperty(key)) {
if (!merged.hasOwnProperty(key)) {
merged[key] = obj2[key];
} else if (typeof obj2[key] === "object") {
merged[key] = deepMerge(merged[key], obj2[key]);
} else {
merged[key] = obj2[key];
}
}
}
return merged;
}
var utils = /*#__PURE__*/Object.freeze({
__proto__: null,
deepCopy: deepCopy,
deepMerge: deepMerge,
unique: unique
});
class DataColumn {
constructor(values = []) {
this.values = values;
}
sum() {
let s = 0;
for (const v of this.values) {
s += v;
}
return s;
}
mean() {
let sum = 0;
let count = 0;
for (const value of this.values) {
if (typeof value !== "undefined" && value !== null) {
sum += value;
count++;
}
}
if (count === 0) {
return void 0;
}
return sum / count;
}
median() {
if (this.values.length === 0) {
return void 0;
}
const numbers = this.values.slice(0).sort(function(a, b) {
return a - b;
});
const middle = Math.floor(numbers.length / 2);
const isEven = numbers.length % 2 === 0;
return isEven ? (numbers[middle] + numbers[middle - 1]) / 2 : numbers[middle];
}
min() {
return Math.min.apply(null, this.values);
}
max() {
return Math.max.apply(null, this.values);
}
count() {
return this.values.length;
}
variance() {
const mean = this.mean();
let sum_square_error = 0;
for (const x of this.values) {
sum_square_error += Math.pow(x - mean, 2);
}
const mse = sum_square_error / (this.values.length - 1);
return mse;
}
sd() {
const mse = this.variance();
const rmse = Math.sqrt(mse);
return rmse;
}
frequencies() {
const unique = {};
for (const x of this.values) {
if (typeof unique[x] === "undefined") {
unique[x] = 1;
} else {
unique[x]++;
}
}
return unique;
}
all(eval_fn) {
for (const x of this.values) {
if (!eval_fn(x)) {
return false;
}
}
return true;
}
subset(eval_fn) {
const out = [];
for (const x of this.values) {
if (eval_fn(x)) {
out.push(x);
}
}
return new DataColumn(out);
}
}
function saveTextToFile(textstr, filename) {
const blobToSave = new Blob([textstr], {
type: "text/plain"
});
let blobURL = "";
if (typeof window.webkitURL !== "undefined") {
blobURL = window.webkitURL.createObjectURL(blobToSave);
} else {
blobURL = window.URL.createObjectURL(blobToSave);
}
const link = document.createElement("a");
link.id = "jspsych-download-as-text-link";
link.style.display = "none";
link.download = filename;
link.href = blobURL;
link.click();
}
function JSON2CSV(objArray) {
const array = typeof objArray != "object" ? JSON.parse(objArray) : objArray;
let line = "";
let result = "";
const columns = [];
for (const row of array) {
for (const key in row) {
let keyString = key + "";
keyString = '"' + keyString.replace(/"/g, '""') + '",';
if (!columns.includes(key)) {
columns.push(key);
line += keyString;
}
}
}
line = line.slice(0, -1);
result += line + "\r\n";
for (const row of array) {
line = "";
for (const col of columns) {
let value = typeof row[col] === "undefined" ? "" : row[col];
if (typeof value == "object") {
value = JSON.stringify(value);
}
const valueString = value + "";
line += '"' + valueString.replace(/"/g, '""') + '",';
}
line = line.slice(0, -1);
result += line + "\r\n";
}
return result;
}
function getQueryString() {
const a = window.location.search.substr(1).split("&");
const b = {};
for (let i = 0; i < a.length; ++i) {
const p = a[i].split("=", 2);
if (p.length == 1) b[p[0]] = "";
else b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
}
return b;
}
class DataCollection {
constructor(data = []) {
this.trials = data;
}
push(new_data) {
this.trials.push(new_data);
return this;
}
join(other_data_collection) {
this.trials = this.trials.concat(other_data_collection.values());
return this;
}
top() {
if (this.trials.length <= 1) {
return this;
} else {
return new DataCollection([this.trials[this.trials.length - 1]]);
}
}
/**
* Queries the first n elements in a collection of trials.
*
* @param n A positive integer of elements to return. A value of
* n that is less than 1 will throw an error.
*
* @return First n objects of a collection of trials. If fewer than
* n trials are available, the trials.length elements will
* be returned.
*
*/
first(n = 1) {
if (n < 1) {
throw `You must query with a positive nonzero integer. Please use a
different value for n.`;
}
if (this.trials.length === 0) return new DataCollection();
if (n > this.trials.length) n = this.trials.length;
return new DataCollection(this.trials.slice(0, n));
}
/**
* Queries the last n elements in a collection of trials.
*
* @param n A positive integer of elements to return. A value of
* n that is less than 1 will throw an error.
*
* @return Last n objects of a collection of trials. If fewer than
* n trials are available, the trials.length elements will
* be returned.
*
*/
last(n = 1) {
if (n < 1) {
throw `You must query with a positive nonzero integer. Please use a
different value for n.`;
}
if (this.trials.length === 0) return new DataCollection();
if (n > this.trials.length) n = this.trials.length;
return new DataCollection(this.trials.slice(this.trials.length - n, this.trials.length));
}
values() {
return this.trials;
}
count() {
return this.trials.length;
}
readOnly() {
return new DataCollection(deepCopy(this.trials));
}
addToAll(properties) {
for (const trial of this.trials) {
Object.assign(trial, properties);
}
return this;
}
addToLast(properties) {
if (this.trials.length > 0) {
Object.assign(this.trials[this.trials.length - 1], properties);
}
return this;
}
filter(filters) {
let f;
if (!Array.isArray(filters)) {
f = deepCopy([filters]);
} else {
f = deepCopy(filters);
}
const filtered_data = [];
for (const trial of this.trials) {
let keep = false;
for (const filter of f) {
let match = true;
for (const key of Object.keys(filter)) {
if (typeof trial[key] !== "undefined" && trial[key] === filter[key]) ; else {
match = false;
}
}
if (match) {
keep = true;
break;
}
}
if (keep) {
filtered_data.push(trial);
}
}
return new DataCollection(filtered_data);
}
filterCustom(fn) {
return new DataCollection(this.trials.filter(fn));
}
filterColumns(columns) {
return new DataCollection(
this.trials.map(
(trial) => Object.fromEntries(columns.filter((key) => key in trial).map((key) => [key, trial[key]]))
)
);
}
select(column) {
const values = [];
for (const trial of this.trials) {
if (typeof trial[column] !== "undefined") {
values.push(trial[column]);
}
}
return new DataColumn(values);
}
ignore(columns) {
if (!Array.isArray(columns)) {
columns = [columns];
}
const o = deepCopy(this.trials);
for (const trial of o) {
for (const delete_key of columns) {
delete trial[delete_key];
}
}
return new DataCollection(o);
}
uniqueNames() {
const names = [];
for (const trial of this.trials) {
for (const key of Object.keys(trial)) {
if (!names.includes(key)) {
names.push(key);
}
}
}
return names;
}
csv() {
return JSON2CSV(this.trials);
}
json(pretty = false) {
if (pretty) {
return JSON.stringify(this.trials, null, " ");
}
return JSON.stringify(this.trials);
}
localSave(format, filename) {
format = format.toLowerCase();
let data_string;
if (format === "json") {
data_string = this.json();
} else if (format === "csv") {
data_string = this.csv();
} else {
throw new Error('Invalid format specified for localSave. Must be "json" or "csv".');
}
saveTextToFile(data_string, filename);
}
}
class JsPsychData {
constructor(dependencies) {
this.dependencies = dependencies;
/** Data properties for all trials */
this.dataProperties = {};
this.interactionListeners = {
blur: () => {
this.addInteractionRecord("blur");
},
focus: () => {
this.addInteractionRecord("focus");
},
fullscreenchange: () => {
this.addInteractionRecord(
// @ts-expect-error
document.isFullScreen || // @ts-expect-error
document.webkitIsFullScreen || // @ts-expect-error
document.mozIsFullScreen || document.fullscreenElement ? "fullscreenenter" : "fullscreenexit"
);
}
};
this.reset();
}
reset() {
this.results = new DataCollection();
this.resultToTrialMap = /* @__PURE__ */ new WeakMap();
this.interactionRecords = new DataCollection();
}
get() {
return this.results;
}
getInteractionData() {
return this.interactionRecords;
}
write(trial) {
const result = trial.getResult();
Object.assign(result, this.dataProperties);
this.results.push(result);
this.resultToTrialMap.set(result, trial);
}
addProperties(properties) {
this.results.addToAll(properties);
this.dataProperties = Object.assign({}, this.dataProperties, properties);
}
addDataToLastTrial(data) {
this.results.addToLast(data);
}
getLastTrialData() {
return this.results.top();
}
getLastTimelineData() {
const lastResult = this.getLastTrialData().values()[0];
return new DataCollection(
lastResult ? this.resultToTrialMap.get(lastResult).parent.getResults() : []
);
}
displayData(format = "json") {
format = format.toLowerCase();
if (format !== "json" && format !== "csv") {
console.log("Invalid format declared for displayData function. Using json as default.");
format = "json";
}
const dataContainer = document.createElement("pre");
dataContainer.id = "jspsych-data-display";
dataContainer.textContent = format === "json" ? this.results.json(true) : this.results.csv();
this.dependencies.getDisplayElement().replaceChildren(dataContainer);
}
urlVariables() {
if (typeof this.query_string == "undefined") {
this.query_string = getQueryString();
}
return this.query_string;
}
getURLVariable(whichvar) {
return this.urlVariables()[whichvar];
}
addInteractionRecord(event) {
const record = { event, ...this.dependencies.getProgress() };
this.interactionRecords.push(record);
this.dependencies.onInteractionRecordAdded(record);
}
createInteractionListeners() {
window.addEventListener("blur", this.interactionListeners.blur);
window.addEventListener("focus", this.interactionListeners.focus);
document.addEventListener("fullscreenchange", this.interactionListeners.fullscreenchange);
document.addEventListener("mozfullscreenchange", this.interactionListeners.fullscreenchange);
document.addEventListener("webkitfullscreenchange", this.interactionListeners.fullscreenchange);
}
removeInteractionListeners() {
window.removeEventListener("blur", this.interactionListeners.blur);
window.removeEventListener("focus", this.interactionListeners.focus);
document.removeEventListener("fullscreenchange", this.interactionListeners.fullscreenchange);
document.removeEventListener("mozfullscreenchange", this.interactionListeners.fullscreenchange);
document.removeEventListener(
"webkitfullscreenchange",
this.interactionListeners.fullscreenchange
);
}
}
class KeyboardListenerAPI {
constructor(getRootElement, areResponsesCaseSensitive = false, minimumValidRt = 0) {
this.getRootElement = getRootElement;
this.areResponsesCaseSensitive = areResponsesCaseSensitive;
this.minimumValidRt = minimumValidRt;
this.listeners = /* @__PURE__ */ new Set();
this.heldKeys = /* @__PURE__ */ new Set();
this.areRootListenersRegistered = false;
autoBind$1(this);
this.registerRootListeners();
}
/**
* If not previously done and `this.getRootElement()` returns an element, adds the root key
* listeners to that element.
*/
registerRootListeners() {
if (!this.areRootListenersRegistered) {
const rootElement = this.getRootElement();
if (rootElement) {
rootElement.addEventListener("keydown", this.rootKeydownListener);
rootElement.addEventListener("keyup", this.rootKeyupListener);
this.areRootListenersRegistered = true;
}
}
}
rootKeydownListener(e) {
for (const listener of [...this.listeners]) {
listener(e);
}
this.heldKeys.add(this.toLowerCaseIfInsensitive(e.key));
}
toLowerCaseIfInsensitive(string) {
return this.areResponsesCaseSensitive ? string : string.toLowerCase();
}
rootKeyupListener(e) {
this.heldKeys.delete(this.toLowerCaseIfInsensitive(e.key));
}
isResponseValid(validResponses, allowHeldKey, key) {
if (!allowHeldKey && this.heldKeys.has(key)) {
return false;
}
if (validResponses === "ALL_KEYS") {
return true;
}
if (validResponses === "NO_KEYS") {
return false;
}
return validResponses.includes(key);
}
getKeyboardResponse({
callback_function,
valid_responses = "ALL_KEYS",
rt_method = "performance",
persist,
audio_context,
audio_context_start_time,
allow_held_key = false,
minimum_valid_rt = this.minimumValidRt
}) {
if (rt_method !== "performance" && rt_method !== "audio") {
console.log(
'Invalid RT method specified in getKeyboardResponse. Defaulting to "performance" method.'
);
rt_method = "performance";
}
const usePerformanceRt = rt_method === "performance";
const startTime = usePerformanceRt ? performance.now() : audio_context_start_time * 1e3;
this.registerRootListeners();
if (!this.areResponsesCaseSensitive && typeof valid_responses !== "string") {
valid_responses = valid_responses.map((r) => r.toLowerCase());
}
const listener = (e) => {
const rt = Math.round(
(rt_method == "performance" ? performance.now() : audio_context.currentTime * 1e3) - startTime
);
if (rt < minimum_valid_rt) {
return;
}
const key = this.toLowerCaseIfInsensitive(e.key);
if (this.isResponseValid(valid_responses, allow_held_key, key)) {
e.preventDefault();
if (!persist) {
this.cancelKeyboardResponse(listener);
}
callback_function({ key: e.key, rt });
}
};
this.listeners.add(listener);
return listener;
}
cancelKeyboardResponse(listener) {
this.listeners.delete(listener);
}
cancelAllKeyboardResponses() {
this.listeners.clear();
}
compareKeys(key1, key2) {
if (typeof key1 !== "string" && key1 !== null || typeof key2 !== "string" && key2 !== null) {
console.error(
"Error in jsPsych.pluginAPI.compareKeys: arguments must be key strings or null."
);
return void 0;
}
if (typeof key1 === "string" && typeof key2 === "string") {
return this.areResponsesCaseSensitive ? key1 === key2 : key1.toLowerCase() === key2.toLowerCase();
}
return key1 === null && key2 === null;
}
}
var ParameterType = /* @__PURE__ */ ((ParameterType2) => {
ParameterType2[ParameterType2["BOOL"] = 0] = "BOOL";
ParameterType2[ParameterType2["STRING"] = 1] = "STRING";
ParameterType2[ParameterType2["INT"] = 2] = "INT";
ParameterType2[ParameterType2["FLOAT"] = 3] = "FLOAT";
ParameterType2[ParameterType2["FUNCTION"] = 4] = "FUNCTION";
ParameterType2[ParameterType2["KEY"] = 5] = "KEY";
ParameterType2[ParameterType2["KEYS"] = 6] = "KEYS";
ParameterType2[ParameterType2["SELECT"] = 7] = "SELECT";
ParameterType2[ParameterType2["HTML_STRING"] = 8] = "HTML_STRING";
ParameterType2[ParameterType2["IMAGE"] = 9] = "IMAGE";
ParameterType2[ParameterType2["AUDIO"] = 10] = "AUDIO";
ParameterType2[ParameterType2["VIDEO"] = 11] = "VIDEO";
ParameterType2[ParameterType2["OBJECT"] = 12] = "OBJECT";
ParameterType2[ParameterType2["COMPLEX"] = 13] = "COMPLEX";
ParameterType2[ParameterType2["TIMELINE"] = 14] = "TIMELINE";
return ParameterType2;
})(ParameterType || {});
class AudioPlayer {
constructor(src, options = { useWebAudio: false }) {
this.src = src;
this.useWebAudio = options.useWebAudio;
this.audioContext = options.audioContext || null;
}
async load() {
if (this.useWebAudio) {
this.webAudioBuffer = await this.preloadWebAudio(this.src);
} else {
this.audio = await this.preloadHTMLAudio(this.src);
}
}
play() {
if (this.audio instanceof HTMLAudioElement) {
this.audio.play();
} else {
if (!this.audio) this.audio = this.getAudioSourceNode(this.webAudioBuffer);
this.audio.start();
}
}
stop() {
if (this.audio instanceof HTMLAudioElement) {
this.audio.pause();
this.audio.currentTime = 0;
} else {
this.audio.stop();
this.audio = this.getAudioSourceNode(this.webAudioBuffer);
}
}
addEventListener(eventName, callback) {
if (!this.audio && this.webAudioBuffer)
this.audio = this.getAudioSourceNode(this.webAudioBuffer);
this.audio.addEventListener(eventName, callback);
}
removeEventListener(eventName, callback) {
if (!this.audio && this.webAudioBuffer)
this.audio = this.getAudioSourceNode(this.webAudioBuffer);
this.audio.removeEventListener(eventName, callback);
}
getAudioSourceNode(audioBuffer) {
const source = this.audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.audioContext.destination);
return source;
}
async preloadWebAudio(src) {
const buffer = await fetch(src);
const arrayBuffer = await buffer.arrayBuffer();
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
const source = this.audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.audioContext.destination);
return audioBuffer;
}
async preloadHTMLAudio(src) {
return new Promise((resolve, reject) => {
const audio = new Audio(src);
audio.addEventListener("canplaythrough", () => {
resolve(audio);
});
audio.addEventListener("error", (err) => {
reject(err);
});
audio.addEventListener("abort", (err) => {
reject(err);
});
});
}
}
const preloadParameterTypes = [
ParameterType.AUDIO,
ParameterType.IMAGE,
ParameterType.VIDEO
];
class MediaAPI {
constructor(useWebaudio) {
this.useWebaudio = useWebaudio;
// video //
this.video_buffers = {};
// audio //
this.context = null;
this.audio_buffers = [];
// preloading stimuli //
this.preload_requests = [];
this.img_cache = {};
this.preloadMap = /* @__PURE__ */ new Map();
this.microphone_recorder = null;
this.camera_stream = null;
this.camera_recorder = null;
if (this.useWebaudio && typeof window !== "undefined" && typeof window.AudioContext !== "undefined") {
this.context = new AudioContext();
}
}
getVideoBuffer(videoID) {
if (videoID.startsWith("blob:")) {
this.video_buffers[videoID] = videoID;
}
return this.video_buffers[videoID];
}
audioContext() {
if (this.context && this.context.state !== "running") {
this.context.resume();
}
return this.context;
}
async getAudioPlayer(audioID) {
if (this.audio_buffers[audioID] instanceof AudioPlayer) {
return this.audio_buffers[audioID];
} else {
this.audio_buffers[audioID] = new AudioPlayer(audioID, {
useWebAudio: this.useWebaudio,
audioContext: this.context
});
await this.audio_buffers[audioID].load();
return this.audio_buffers[audioID];
}
}
preloadAudio(files, callback_complete = () => {
}, callback_load = (filepath) => {
}, callback_error = (error) => {
}) {
files = unique(files.flat());
let n_loaded = 0;
if (files.length == 0) {
callback_complete();
return;
}
for (const file of files) {
if (this.audio_buffers[file] instanceof AudioPlayer) {
n_loaded++;
callback_load(file);
if (n_loaded == files.length) {
callback_complete();
}
} else {
this.audio_buffers[file] = new AudioPlayer(file, {
useWebAudio: this.useWebaudio,
audioContext: this.context
});
this.audio_buffers[file].load().then(() => {
n_loaded++;
callback_load(file);
if (n_loaded == files.length) {
callback_complete();
}
}).catch((e) => {
callback_error(e);
});
}
}
}
preloadImages(images, callback_complete = () => {
}, callback_load = (filepath) => {
}, callback_error = (error_msg) => {
}) {
images = unique(images.flat());
var n_loaded = 0;
if (images.length === 0) {
callback_complete();
return;
}
for (let i = 0; i < images.length; i++) {
const img = new Image();
const src = images[i];
img.onload = () => {
n_loaded++;
callback_load(src);
if (n_loaded === images.length) {
callback_complete();
}
};
img.onerror = (e) => {
callback_error({ source: src, error: e });
};
img.src = src;
this.img_cache[src] = img;
this.preload_requests.push(img);
}
}
preloadVideo(videos, callback_complete = () => {
}, callback_load = (filepath) => {
}, callback_error = (error_msg) => {
}) {
videos = unique(videos.flat());
let n_loaded = 0;
if (videos.length === 0) {
callback_complete();
return;
}
for (const video of videos) {
const video_buffers = this.video_buffers;
const request = new XMLHttpRequest();
request.open("GET", video, true);
request.responseType = "blob";
request.onload = () => {
if (request.status === 200 || request.status === 0) {
const videoBlob = request.response;
video_buffers[video] = URL.createObjectURL(videoBlob);
n_loaded++;
callback_load(video);
if (n_loaded === videos.length) {
callback_complete();
}
}
};
request.onerror = (e) => {
let err = e;
if (request.status == 404) {
err = "404";
}
callback_error({ source: video, error: err });
};
request.onloadend = (e) => {
if (request.status == 404) {
callback_error({ source: video, error: "404" });
}
};
request.send();
this.preload_requests.push(request);
}
}
getAutoPreloadList(timeline_description) {
const preloadPaths = Object.fromEntries(
preloadParameterTypes.map((type) => [type, /* @__PURE__ */ new Set()])
);
const traverseTimeline = (node, inheritedTrialType) => {
const isTimeline = typeof node.timeline !== "undefined";
if (isTimeline) {
for (const childNode of node.timeline) {
traverseTimeline(childNode, node.type ?? inheritedTrialType);
}
} else if ((node.type ?? inheritedTrialType)?.info) {
const { name: pluginName, parameters } = (node.type ?? inheritedTrialType).info;
if (!this.preloadMap.has(pluginName)) {
this.preloadMap.set(
pluginName,
Object.fromEntries(
Object.entries(parameters).filter(
([_name, { type, preload }]) => preloadParameterTypes.includes(type) && (preload ?? true)
).map(([name, { type }]) => [name, type])
)
);
}
for (const [parameterName, parameterType] of Object.entries(
this.preloadMap.get(pluginName)
)) {
const parameterValue = node[parameterName];
const elements = preloadPaths[parameterType];
if (typeof parameterValue === "string") {
elements.add(parameterValue);
} else if (Array.isArray(parameterValue)) {
for (const element of parameterValue.flat()) {
if (typeof element === "string") {
elements.add(element);
}
}
}
}
}
};
traverseTimeline({ timeline: timeline_description });
return {
images: [...preloadPaths[ParameterType.IMAGE]],
audio: [...preloadPaths[ParameterType.AUDIO]],
video: [...preloadPaths[ParameterType.VIDEO]]
};
}
cancelPreloads() {
for (const request of this.preload_requests) {
request.onload = () => {
};
request.onerror = () => {
};
request.oncanplaythrough = () => {
};
request.onabort = () => {
};
}
this.preload_requests = [];
}
initializeMicrophoneRecorder(stream) {
const recorder = new MediaRecorder(stream);
this.microphone_recorder = recorder;
}
getMicrophoneRecorder() {
return this.microphone_recorder;
}
initializeCameraRecorder(stream, opts) {
let mimeType = this.getCompatibleMimeType() || "video/webm";
const recorderOptions = {
...opts,
mimeType
};
this.camera_stream = stream;
const recorder = new MediaRecorder(stream, recorderOptions);
this.camera_recorder = recorder;
}
// mimetype checking code adapted from https://github.com/lookit/lookit-jspsych/blob/develop/packages/record/src/videoConfig.ts#L673-L699
/** returns a compatible mimetype string, or null if none from the array are supported. */
getCompatibleMimeType() {
const types = [
// chrome firefox edge
"video/webm;codecs=vp9,opus",
"video/webm;codecs=vp8,opus",
// general
"video/mp4;codecs=avc1.42E01E,mp4a.40.2",
// safari
"video/mp4;codecs=h264,aac",
"video/mp4;codecs=hevc,aac"
];
for (const mimeType of types) {
if (MediaRecorder.isTypeSupported(mimeType)) {
return mimeType;
}
}
return null;
}
getCameraStream() {
return this.camera_stream;
}
getCameraRecorder() {
return this.camera_recorder;
}
}
class SimulationAPI {
constructor(getDisplayContainerElement, setJsPsychTimeout) {
this.getDisplayContainerElement = getDisplayContainerElement;
this.setJsPsychTimeout = setJsPsychTimeout;
}
dispatchEvent(event) {
this.getDisplayContainerElement().dispatchEvent(event);
}
/**
* Dispatches a `keydown` event for the specified key
* @param key Character code (`.key` property) for the key to press.
*/
keyDown(key) {
this.dispatchEvent(new KeyboardEvent("keydown", { key }));
}
/**
* Dispatches a `keyup` event for the specified key
* @param key Character code (`.key` property) for the key to press.
*/
keyUp(key) {
this.dispatchEvent(new KeyboardEvent("keyup", { key }));
}
/**
* Dispatches a `keydown` and `keyup` event in sequence to simulate pressing a key.
* @param key Character code (`.key` property) for the key to press.
* @param delay Length of time to wait (ms) before executing action
*/
pressKey(key, delay = 0) {
if (delay > 0) {
this.setJsPsychTimeout(() => {
this.keyDown(key);
this.keyUp(key);
}, delay);
} else {
this.keyDown(key);
this.keyUp(key);
}
}
/**
* Dispatches `mousedown`, `mouseup`, and `click` events on the target element
* @param target The element to click
* @param delay Length of time to wait (ms) before executing action
*/
clickTarget(target, delay = 0) {
if (delay > 0) {
this.setJsPsychTimeout(() => {
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}, delay);
} else {
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}
}
/**
* Sets the value of a target text input
* @param target A text input element to fill in
* @param text Text to input
* @param delay Length of time to wait (ms) before executing action
*/
fillTextInput(target, text, delay = 0) {
if (delay > 0) {
this.setJsPsychTimeout(() => {
target.value = text;
}, delay);
} else {
target.value = text;
}
}
/**
* Picks a valid key from `choices`, taking into account jsPsych-specific
* identifiers like "NO_KEYS" and "ALL_KEYS".
* @param choices Which keys are valid.
* @returns A key selected at random from the valid keys.
*/
getValidKey(choices) {
const possible_keys = [
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
" "
];
let key;
if (choices == "NO_KEYS") {
key = null;
} else if (choices == "ALL_KEYS") {
key = possible_keys[Math.floor(Math.random() * possible_keys.length)];
} else {
const flat_choices = choices.flat();
key = flat_choices[Math.floor(Math.random() * flat_choices.length)];
}
return key;
}
mergeSimulationData(default_data, simulation_options) {
return {
...default_data,
...simulation_options?.data
};
}
ensureSimulationDataConsistency(trial, data) {
if (data.rt) {
data.rt = Math.round(data.rt);
}
if (trial.trial_duration && data.rt && data.rt > trial.trial_duration) {
data.rt = null;
if (data.response) {
data.response = null;
}
if (data.correct) {
data.correct = false;
}
}
if (trial.choices && trial.choices == "NO_KEYS") {
if (data.rt) {
data.rt = null;
}
if (data.response) {
data.response = null;
}
}
if (trial.allow_response_before_complete) {
if (trial.sequence_reps && trial.frame_time) {
const min_time = trial.sequence_reps * trial.frame_time * trial.stimuli.length;
if (data.rt < min_time) {
data.rt = null;
data.response = null;
}
}
}
}
}
class TimeoutAPI {
constructor() {
this.timeout_handlers = [];
}
/**
* Calls a function after a specified delay, in milliseconds.
* @param callback The function to call after the delay.
* @param delay The number of milliseconds to wait before calling the function.
* @returns A handle that can be used to clear the timeout with clearTimeout.
*/
setTimeout(callback, delay) {
const handle = window.setTimeout(callback, delay);
this.timeout_handlers.push(handle);
return handle;
}
/**
* Clears all timeouts that have been created with setTimeout.
*/
clearAllTimeouts() {
for (const handler of this.timeout_handlers) {
clearTimeout(handler);
}
this.timeout_handlers = [];
}
}
function createJointPluginAPIObject(jsPsych) {
const settings = jsPsych.getInitSettings();
const keyboardListenerAPI = new KeyboardListenerAPI(
jsPsych.getDisplayContainerElement,
settings.case_sensitive_responses,
settings.minimum_valid_rt
);
const timeoutAPI = new TimeoutAPI();
const mediaAPI = new MediaAPI(settings.use_webaudio);
const simulationAPI = new SimulationAPI(
jsPsych.getDisplayContainerElement,
timeoutAPI.setTimeout.bind(timeoutAPI)
);
return Object.assign(
{},
...[keyboardListenerAPI, timeoutAPI, mediaAPI, simulationAPI].map((object) => autoBind$1(object))
);
}
var alea$1 = {exports: {}};
alea$1.exports;
(function (module) {
// A port of an algorithm by Johannes Baagøe <baagoe@baagoe.com>, 2010
// http://baagoe.com/en/RandomMusings/javascript/
// https://github.com/nquinlan/better-random-numbers-for-javascript-mirror
// Original work is under MIT license -
// Copyright (C) 2010 by Johannes Baagøe <baagoe@baagoe.org>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
(function(global, module, define) {
function Alea(seed) {
var me = this, mash = Mash();
me.next = function() {
var t = 2091639 * me.s0 + me.c * 2.3283064365386963e-10; // 2^-32
me.s0 = me.s1;
me.s1 = me.s2;
return me.s2 = t - (me.c = t | 0);
};
// Apply the seeding algorithm from Baagoe.
me.c = 1;
me.s0 = mash(' ');
me.s1 = mash(' ');
me.s2 = mash(' ');
me.s0 -= mash(seed);
if (me.s0 < 0) { me.s0 += 1; }
me.s1 -= mash(seed);
if (me.s1 < 0) { me.s1 += 1; }
me.s2 -= mash(seed);
if (me.s2 < 0) { me.s2 += 1; }
mash = null;
}
function copy(f, t) {
t.c = f.c;
t.s0 = f.s0;
t.s1 = f.s1;
t.s2 = f.s2;
return t;
}
function impl(seed, opts) {
var xg = new Alea(seed),
state = opts && opts.state,
prng = xg.next;
prng.int32 = function() { return (xg.next() * 0x100000000) | 0; };
prng.double = function() {
return prng() + (prng() * 0x200000 | 0) * 1.1102230246251565e-16; // 2^-53
};
prng.quick = prng;
if (state) {
if (typeof(state) == 'object') copy(state, xg);
prng.state = function() { return copy(xg, {}); };
}
return prng;
}
function Mash() {
var n = 0xefc8249d;
var mash = function(data) {
data = String(data);
for (var i = 0; i < data.length; i++) {
n += data.charCodeAt(i);
var h = 0.02519603282416938 * n;
n = h >>> 0;
h -= n;
h *= n;
n = h >>> 0;
h -= n;
n += h * 0x100000000; // 2^32
}
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
};
return mash;
}
if (module && module.exports) {
module.exports = impl;
} else {
this.alea = impl;
}
})(
commonjsGlobal,
module);
} (alea$1));
var aleaExports = alea$1.exports;
var seedrandom$3 = /*@__PURE__*/getDefaultExportFromCjs(aleaExports);
var xor128$1 = {exports: {}};
xor128$1.exports;
(function (module) {
// A Javascript implementaion of the "xor128" prng algorithm by
// George Marsaglia. See http://www.jstatsoft.org/v08/i14/paper
(function(global, module, define) {
function XorGen(seed) {
var me = this, strseed = '';
me.x = 0;
me.y = 0;
me.z = 0;
me.w = 0;
// Set up generator function.
me.next = function() {
var t = me.x ^ (me.x << 11);
me.x = me.y;
me.y = me.z;
me.z = me.w;
return me.w ^= (me.w >>> 19) ^ t ^ (t >>> 8);
};
if (seed === (seed | 0)) {
// Integer seed.
me.x = seed;
} else {
// String seed.
strseed += seed;
}
// Mix in string seed, then discard an initial batch of 64 values.
for (var k = 0; k < strseed.length + 64; k++) {
me.x ^= strseed.charCodeAt(k) | 0;
me.next();
}
}
function copy(f, t) {
t.x = f.x;
t.y = f.y;
t.z = f.z;
t.w = f.w;
return t;
}
function impl(seed, opts) {
var xg = new XorGen(seed),
state = opts && opts.state,
prng = function() { return (xg.next() >>> 0) / 0x100000000; };
prng.double = function() {
do {
var top = xg.next() >>> 11,
bot = (xg.next() >>> 0) / 0x100000000,
result = (top + bot) / (1 << 21);
} while (result === 0);
return result;
};
prng.int32 = xg.next;
prng.quick = prng;
if (state) {
if (typeof(state) == 'object') copy(state, xg);
prng.state = function() { return copy(xg, {}); };
}
return prng;
}
if (module && module.exports) {
module.exports = impl;
} else {
this.xor128 = impl;
}
})(
commonjsGlobal,
module);
} (xor128$1));
var xor128Exports = xor128$1.exports;
var xorwow$1 = {exports: {}};
xorwow$1.exports;
(function (module) {
// A Javascript implementaion of the "xorwow" prng algorithm by
// George Marsaglia. See http://www.jstatsoft.org/v08/i14/paper
(function(global, module, define) {
function XorGen(seed) {
var me = this, strseed = '';
// Set up generator function.
me.next = function() {
var t = (me.x ^ (me.x >>> 2));
me.x = me.y; me.y = me.z; me.z = me.w; me.w = me.v;
return (me.d = (me.d + 362437 | 0)) +
(me.v = (me.v ^ (me.v << 4)) ^ (t ^ (t << 1))) | 0;
};
me.x = 0;
me.y = 0;
me.z = 0;
me.w = 0;
me.v = 0;
if (seed === (seed | 0)) {
// Integer seed.
me.x = seed;
} else {
// String seed.
strseed += seed;
}
// Mix in string seed, then discard an initial batch of 64 values.
for (var k = 0; k < strseed.length + 64; k++) {
me.x ^= strseed.charCodeAt(k) | 0;
if (k == strseed.length) {
me.d = me.x << 10 ^ me.x >>> 4;
}
me.next();
}
}
function copy(f, t) {
t.x = f.x;
t.y = f.y;
t.z = f.z;
t.w = f.w;
t.v = f.v;
t.d = f.d;
return t;
}
function impl(seed, opts) {
var xg = new XorGen(seed),
state = opts && opts.state,
prng = function() { return (xg.next() >>> 0) / 0x100000000; };
prng.double = function() {
do {
var top = xg.next() >>> 11,
bot = (xg.next() >>> 0) / 0x100000000,
result = (top + bot) / (1 << 21);
} while (result === 0);
return result;
};
prng.int32 = xg.next;
prng.quick = prng;
if (state) {
if (typeof(state) == 'object') copy(state, xg);
prng.state = function() { return copy(xg, {}); };
}
return prng;
}
if (module && module.exports) {
module.exports = impl;
} else {
this.xorwow = impl;
}
})(
commonjsGlobal,
module);
} (xorwow$1));
var xorwowExports = xorwow$1.exports;
var xorshift7$1 = {exports: {}};
xorshift7$1.exports;
(function (module) {
// A Javascript implementaion of the "xorshift7" algorithm by
// François Panneton and Pierre L'ecuyer:
// "On the Xorgshift Random Number Generators"
// http://saluc.engr.uconn.edu/refs/crypto/rng/panneton05onthexorshift.pdf
(function(global, module, define) {
function XorGen(seed) {
var me = this;
// Set up generator function.
me.next = function() {
// Update xor generator.
var X = me.x, i = me.i, t, v;
t = X[i]; t ^= (t >>> 7); v = t ^ (t << 24);
t = X[(i + 1) & 7]; v ^= t ^ (t >>> 10);
t = X[(i + 3) & 7]; v ^= t ^ (t >>> 3);
t = X[(i + 4) & 7]; v ^= t ^ (t << 7);
t = X[(i + 7) & 7]; t = t ^ (t << 13); v ^= t ^ (t << 9);
X[i] = v;
me.i = (i + 1) & 7;
return v;
};
function init(me, seed) {
var j, X = [];
if (seed === (seed | 0)) {
// Seed state array using a 32-bit integer.
X[0] = seed;
} else {
// Seed state using a string.
seed = '' + seed;
for (j = 0; j < seed.length; ++j) {
X[j & 7] = (X[j & 7] << 15) ^
(seed.charCodeAt(j) + X[(j + 1) & 7] << 13);
}
}
// Enforce an array length of 8, not all zeroes.
while (X.length < 8) X.push(0);
for (j = 0; j < 8 && X[j] === 0; ++j);
if (j == 8) X[7] = -1; else X[j];
me.x = X;
me.i = 0;
// Discard an initial 256 values.
for (j = 256; j > 0; --j) {
me.next();
}
}
init(me, seed);
}
function copy(f, t) {
t.x = f.x.slice();
t.i = f.i;
return t;
}
function impl(seed, opts) {
if (seed == null) seed = +(new Date);
var xg = new XorGen(seed),
state = opts && opts.state,
prng = function() { return (xg.next() >>> 0) / 0x100000000; };
prng.double = function() {
do {
var top = xg.next() >>> 11,
bot = (xg.next() >>> 0) / 0x100000000,
result = (top + bot) / (1 << 21);
} while (result === 0);
return result;
};
prng.int32 = xg.next;
prng.quick = prng;
if (state) {
if (state.x) copy(state, xg);
prng.state = function() { return copy(xg, {}); };
}
return prng;
}
if (module && module.exports) {
module.exports = impl;
} else {
this.xorshift7 = impl;
}
})(
commonjsGlobal,
module);
} (xorshift7$1));
var xorshift7Exports = xorshift7$1.exports;
var xor4096$1 = {exports: {}};
xor4096$1.exports;
(function (module) {
// A Javascript implementaion of Richard Brent's Xorgens xor4096 algorithm.
//
// This fast non-cryptographic random number generator is designed for
// use in Monte-Carlo algorithms. It combines a long-period xorshift
// generator with a Weyl generator, and it passes all common batteries
// of stasticial tests for randomness while consuming only a few nanoseconds
// for each prng generated. For background on the generator, see Brent's
// paper: "Some long-period random number generators using shifts and xors."
// http://arxiv.org/pdf/1004.3115v1.pdf
//
// Usage:
//
// var xor4096 = require('xor4096');
// random = xor4096(1); // Seed with int32 or string.
// assert.equal(random(), 0.1520436450538547); // (0, 1) range, 53 bits.
// assert.equal(random.int32(), 1806534897); // signed int32, 32 bits.
//
// For nonzero numeric keys, this impelementation provides a sequence
// identical to that by Brent's xorgens 3 implementaion in C. This
// implementation also provides for initalizing the generator with
// string seeds, or for saving and restoring the state of the generator.
//
// On Chrome, this prng benchmarks about 2.1 times slower than
// Javascript's b