UNPKG

jspsych

Version:

Behavioral experiments in a browser

1,688 lines (1,594 loc) 143 kB
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