jspsych 7.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/JsPsych.d.ts +10 -1
- package/dist/index.browser.js +706 -7
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +1 -1
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +706 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +706 -7
- package/dist/index.js.map +1 -1
- package/dist/migration.d.ts +3 -0
- package/dist/modules/plugin-api/MediaAPI.d.ts +3 -0
- package/dist/modules/plugin-api/SimulationAPI.d.ts +41 -0
- package/dist/modules/plugin-api/index.d.ts +2 -1
- package/dist/modules/randomization.d.ts +28 -1
- package/package.json +6 -4
- package/src/JsPsych.ts +85 -4
- package/src/index.ts +37 -1
- package/src/migration.ts +37 -0
- package/src/modules/plugin-api/MediaAPI.ts +11 -0
- package/src/modules/plugin-api/SimulationAPI.ts +181 -0
- package/src/modules/plugin-api/index.ts +3 -1
- package/src/modules/randomization.ts +72 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export declare class SimulationAPI {
|
|
2
|
+
dispatchEvent(event: Event): void;
|
|
3
|
+
/**
|
|
4
|
+
* Dispatches a `keydown` event for the specified key
|
|
5
|
+
* @param key Character code (`.key` property) for the key to press.
|
|
6
|
+
*/
|
|
7
|
+
keyDown(key: string): void;
|
|
8
|
+
/**
|
|
9
|
+
* Dispatches a `keyup` event for the specified key
|
|
10
|
+
* @param key Character code (`.key` property) for the key to press.
|
|
11
|
+
*/
|
|
12
|
+
keyUp(key: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Dispatches a `keydown` and `keyup` event in sequence to simulate pressing a key.
|
|
15
|
+
* @param key Character code (`.key` property) for the key to press.
|
|
16
|
+
* @param delay Length of time to wait (ms) before executing action
|
|
17
|
+
*/
|
|
18
|
+
pressKey(key: string, delay?: number): void;
|
|
19
|
+
/**
|
|
20
|
+
* Dispatches `mousedown`, `mouseup`, and `click` events on the target element
|
|
21
|
+
* @param target The element to click
|
|
22
|
+
* @param delay Length of time to wait (ms) before executing action
|
|
23
|
+
*/
|
|
24
|
+
clickTarget(target: Element, delay?: number): void;
|
|
25
|
+
/**
|
|
26
|
+
* Sets the value of a target text input
|
|
27
|
+
* @param target A text input element to fill in
|
|
28
|
+
* @param text Text to input
|
|
29
|
+
* @param delay Length of time to wait (ms) before executing action
|
|
30
|
+
*/
|
|
31
|
+
fillTextInput(target: HTMLInputElement, text: string, delay?: number): void;
|
|
32
|
+
/**
|
|
33
|
+
* Picks a valid key from `choices`, taking into account jsPsych-specific
|
|
34
|
+
* identifiers like "NO_KEYS" and "ALL_KEYS".
|
|
35
|
+
* @param choices Which keys are valid.
|
|
36
|
+
* @returns A key selected at random from the valid keys.
|
|
37
|
+
*/
|
|
38
|
+
getValidKey(choices: "NO_KEYS" | "ALL_KEYS" | Array<string> | Array<Array<string>>): any;
|
|
39
|
+
mergeSimulationData(default_data: any, simulation_options: any): any;
|
|
40
|
+
ensureSimulationDataConsistency(trial: any, data: any): void;
|
|
41
|
+
}
|
|
@@ -2,6 +2,7 @@ import { JsPsych } from "../../JsPsych";
|
|
|
2
2
|
import { HardwareAPI } from "./HardwareAPI";
|
|
3
3
|
import { KeyboardListenerAPI } from "./KeyboardListenerAPI";
|
|
4
4
|
import { MediaAPI } from "./MediaAPI";
|
|
5
|
+
import { SimulationAPI } from "./SimulationAPI";
|
|
5
6
|
import { TimeoutAPI } from "./TimeoutAPI";
|
|
6
|
-
export declare function createJointPluginAPIObject(jsPsych: JsPsych): KeyboardListenerAPI & TimeoutAPI & MediaAPI & HardwareAPI;
|
|
7
|
+
export declare function createJointPluginAPIObject(jsPsych: JsPsych): KeyboardListenerAPI & TimeoutAPI & MediaAPI & HardwareAPI & SimulationAPI;
|
|
7
8
|
export declare type PluginAPI = ReturnType<typeof createJointPluginAPIObject>;
|
|
@@ -3,6 +3,33 @@ export declare function shuffle(array: Array<any>): any[];
|
|
|
3
3
|
export declare function shuffleNoRepeats(arr: Array<any>, equalityTest: (a: any, b: any) => boolean): any[];
|
|
4
4
|
export declare function shuffleAlternateGroups(arr_groups: any, random_group_order?: boolean): any[];
|
|
5
5
|
export declare function sampleWithoutReplacement(arr: any, size: any): any[];
|
|
6
|
-
export declare function sampleWithReplacement(arr: any, size: any, weights
|
|
6
|
+
export declare function sampleWithReplacement(arr: any, size: any, weights?: any): any[];
|
|
7
7
|
export declare function factorial(factors: Record<string, any>, repetitions?: number, unpack?: boolean): any;
|
|
8
8
|
export declare function randomID(length?: number): string;
|
|
9
|
+
/**
|
|
10
|
+
* Generate a random integer from `lower` to `upper`, inclusive of both end points.
|
|
11
|
+
* @param lower The lowest value it is possible to generate
|
|
12
|
+
* @param upper The highest value it is possible to generate
|
|
13
|
+
* @returns A random integer
|
|
14
|
+
*/
|
|
15
|
+
export declare function randomInt(lower: number, upper: number): number;
|
|
16
|
+
/**
|
|
17
|
+
* Generates a random sample from a Bernoulli distribution.
|
|
18
|
+
* @param p The probability of sampling 1.
|
|
19
|
+
* @returns 0, with probability 1-p, or 1, with probability p.
|
|
20
|
+
*/
|
|
21
|
+
export declare function sampleBernoulli(p: number): 0 | 1;
|
|
22
|
+
export declare function sampleNormal(mean: number, standard_deviation: number): number;
|
|
23
|
+
export declare function sampleExponential(rate: number): number;
|
|
24
|
+
export declare function sampleExGaussian(mean: number, standard_deviation: number, rate: number, positive?: boolean): number;
|
|
25
|
+
/**
|
|
26
|
+
* Generate one or more random words.
|
|
27
|
+
*
|
|
28
|
+
* This is a wrapper function for the {@link https://www.npmjs.com/package/random-words `random-words` npm package}.
|
|
29
|
+
*
|
|
30
|
+
* @param opts An object with optional properties `min`, `max`, `exactly`,
|
|
31
|
+
* `join`, `maxLength`, `wordsPerString`, `separator`, and `formatter`.
|
|
32
|
+
*
|
|
33
|
+
* @returns An array of words or a single string, depending on parameter choices.
|
|
34
|
+
*/
|
|
35
|
+
export declare function randomWords(opts: any): any;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jspsych",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"description": "Behavioral experiments in a browser",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -39,10 +39,12 @@
|
|
|
39
39
|
},
|
|
40
40
|
"homepage": "https://www.jspsych.org",
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"auto-bind": "^4.0.0"
|
|
42
|
+
"auto-bind": "^4.0.0",
|
|
43
|
+
"random-words": "^1.1.1"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
|
-
"@jspsych/config": "^1.
|
|
46
|
-
"@jspsych/test-utils": "^1.
|
|
46
|
+
"@jspsych/config": "^1.1.0",
|
|
47
|
+
"@jspsych/test-utils": "^1.1.0",
|
|
48
|
+
"@types/dom-mediacapture-record": "^1.0.11"
|
|
47
49
|
}
|
|
48
50
|
}
|
package/src/JsPsych.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import autoBind from "auto-bind";
|
|
2
2
|
|
|
3
3
|
import { version } from "../package.json";
|
|
4
|
+
import { MigrationError } from "./migration";
|
|
4
5
|
import { JsPsychData } from "./modules/data";
|
|
5
6
|
import { PluginAPI, createJointPluginAPIObject } from "./modules/plugin-api";
|
|
6
7
|
import { ParameterType, universalPluginParameters } from "./modules/plugins";
|
|
@@ -71,6 +72,16 @@ export class JsPsych {
|
|
|
71
72
|
private finished: Promise<void>;
|
|
72
73
|
private resolveFinishedPromise: () => void;
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* is the experiment running in `simulate()` mode
|
|
77
|
+
*/
|
|
78
|
+
private simulation_mode: "data-only" | "visual" = null;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* simulation options passed in via `simulate()`
|
|
82
|
+
*/
|
|
83
|
+
private simulation_options;
|
|
84
|
+
|
|
74
85
|
// storing a single webaudio context to prevent problems with multiple inits
|
|
75
86
|
// of jsPsych
|
|
76
87
|
webaudio_context: AudioContext = null;
|
|
@@ -176,6 +187,16 @@ export class JsPsych {
|
|
|
176
187
|
await this.finished;
|
|
177
188
|
}
|
|
178
189
|
|
|
190
|
+
async simulate(
|
|
191
|
+
timeline: any[],
|
|
192
|
+
simulation_mode: "data-only" | "visual",
|
|
193
|
+
simulation_options = {}
|
|
194
|
+
) {
|
|
195
|
+
this.simulation_mode = simulation_mode;
|
|
196
|
+
this.simulation_options = simulation_options;
|
|
197
|
+
await this.run(timeline);
|
|
198
|
+
}
|
|
199
|
+
|
|
179
200
|
getProgress() {
|
|
180
201
|
return {
|
|
181
202
|
total_trials: typeof this.timeline === "undefined" ? undefined : this.timeline.length(),
|
|
@@ -299,12 +320,12 @@ export class JsPsych {
|
|
|
299
320
|
}
|
|
300
321
|
}
|
|
301
322
|
|
|
302
|
-
endExperiment(end_message
|
|
323
|
+
endExperiment(end_message = "", data = {}) {
|
|
303
324
|
this.timeline.end_message = end_message;
|
|
304
325
|
this.timeline.end();
|
|
305
326
|
this.pluginAPI.cancelAllKeyboardResponses();
|
|
306
327
|
this.pluginAPI.clearAllTimeouts();
|
|
307
|
-
this.finishTrial();
|
|
328
|
+
this.finishTrial(data);
|
|
308
329
|
}
|
|
309
330
|
|
|
310
331
|
endCurrentTimeline() {
|
|
@@ -517,6 +538,12 @@ export class JsPsych {
|
|
|
517
538
|
// process all timeline variables for this trial
|
|
518
539
|
this.evaluateTimelineVariables(trial);
|
|
519
540
|
|
|
541
|
+
if (typeof trial.type === "string") {
|
|
542
|
+
throw new MigrationError(
|
|
543
|
+
"A string was provided as the trial's `type` parameter. Since jsPsych v7, the `type` parameter needs to be a plugin object."
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
520
547
|
// instantiate the plugin for this trial
|
|
521
548
|
trial.type = {
|
|
522
549
|
// this is a hack to internally keep the old plugin object structure and prevent touching more
|
|
@@ -579,11 +606,60 @@ export class JsPsych {
|
|
|
579
606
|
}
|
|
580
607
|
};
|
|
581
608
|
|
|
582
|
-
|
|
609
|
+
let trial_complete;
|
|
610
|
+
if (!this.simulation_mode) {
|
|
611
|
+
trial_complete = trial.type.trial(this.DOM_target, trial, load_callback);
|
|
612
|
+
}
|
|
613
|
+
if (this.simulation_mode) {
|
|
614
|
+
// check if the trial supports simulation
|
|
615
|
+
if (trial.type.simulate) {
|
|
616
|
+
let trial_sim_opts;
|
|
617
|
+
if (!trial.simulation_options) {
|
|
618
|
+
trial_sim_opts = this.simulation_options.default;
|
|
619
|
+
}
|
|
620
|
+
if (trial.simulation_options) {
|
|
621
|
+
if (typeof trial.simulation_options == "string") {
|
|
622
|
+
if (this.simulation_options[trial.simulation_options]) {
|
|
623
|
+
trial_sim_opts = this.simulation_options[trial.simulation_options];
|
|
624
|
+
} else if (this.simulation_options.default) {
|
|
625
|
+
console.log(
|
|
626
|
+
`No matching simulation options found for "${trial.simulation_options}". Using "default" options.`
|
|
627
|
+
);
|
|
628
|
+
trial_sim_opts = this.simulation_options.default;
|
|
629
|
+
} else {
|
|
630
|
+
console.log(
|
|
631
|
+
`No matching simulation options found for "${trial.simulation_options}" and no "default" options provided. Using the default values provided by the plugin.`
|
|
632
|
+
);
|
|
633
|
+
trial_sim_opts = {};
|
|
634
|
+
}
|
|
635
|
+
} else {
|
|
636
|
+
trial_sim_opts = trial.simulation_options;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
trial_sim_opts = this.utils.deepCopy(trial_sim_opts);
|
|
640
|
+
trial_sim_opts = this.replaceFunctionsWithValues(trial_sim_opts, null);
|
|
641
|
+
|
|
642
|
+
if (trial_sim_opts?.simulate === false) {
|
|
643
|
+
trial_complete = trial.type.trial(this.DOM_target, trial, load_callback);
|
|
644
|
+
} else {
|
|
645
|
+
trial_complete = trial.type.simulate(
|
|
646
|
+
trial,
|
|
647
|
+
trial_sim_opts?.mode || this.simulation_mode,
|
|
648
|
+
trial_sim_opts,
|
|
649
|
+
load_callback
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
// trial doesn't have a simulate method, so just run as usual
|
|
654
|
+
trial_complete = trial.type.trial(this.DOM_target, trial, load_callback);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
583
658
|
// see if trial_complete is a Promise by looking for .then() function
|
|
584
659
|
const is_promise = trial_complete && typeof trial_complete.then == "function";
|
|
585
660
|
|
|
586
|
-
|
|
661
|
+
// in simulation mode we let the simulate function call the load_callback always.
|
|
662
|
+
if (!is_promise && !this.simulation_mode) {
|
|
587
663
|
load_callback();
|
|
588
664
|
}
|
|
589
665
|
|
|
@@ -727,6 +803,11 @@ export class JsPsych {
|
|
|
727
803
|
}
|
|
728
804
|
|
|
729
805
|
private async checkExclusions(exclusions) {
|
|
806
|
+
if (exclusions.min_width || exclusions.min_height || exclusions.audio) {
|
|
807
|
+
console.warn(
|
|
808
|
+
"The exclusions option in `initJsPsych()` is deprecated and will be removed in a future version. We recommend using the browser-check plugin instead. See https://www.jspsych.org/latest/plugins/browser-check/."
|
|
809
|
+
);
|
|
810
|
+
}
|
|
730
811
|
// MINIMUM SIZE
|
|
731
812
|
if (exclusions.min_width || exclusions.min_height) {
|
|
732
813
|
const mw = exclusions.min_width || 0;
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { JsPsych } from "./JsPsych";
|
|
2
|
+
import { MigrationError } from "./migration";
|
|
2
3
|
|
|
3
4
|
// temporary patch for Safari
|
|
4
5
|
if (
|
|
@@ -20,7 +21,42 @@ if (
|
|
|
20
21
|
* @returns A new JsPsych instance
|
|
21
22
|
*/
|
|
22
23
|
export function initJsPsych(options?) {
|
|
23
|
-
|
|
24
|
+
const jsPsych = new JsPsych(options);
|
|
25
|
+
|
|
26
|
+
// Handle invocations of non-existent v6 methods with migration errors
|
|
27
|
+
const migrationMessages = {
|
|
28
|
+
init: "`jsPsych.init()` was replaced by `initJsPsych()` in jsPsych v7.",
|
|
29
|
+
|
|
30
|
+
ALL_KEYS: 'jsPsych.ALL_KEYS was replaced by the "ALL_KEYS" string in jsPsych v7.',
|
|
31
|
+
NO_KEYS: 'jsPsych.NO_KEYS was replaced by the "NO_KEYS" string in jsPsych v7.',
|
|
32
|
+
|
|
33
|
+
// Getter functions that were renamed
|
|
34
|
+
currentTimelineNodeID:
|
|
35
|
+
"`currentTimelineNodeID()` was renamed to `getCurrentTimelineNodeID()` in jsPsych v7.",
|
|
36
|
+
progress: "`progress()` was renamed to `getProgress()` in jsPsych v7.",
|
|
37
|
+
startTime: "`startTime()` was renamed to `getStartTime()` in jsPsych v7.",
|
|
38
|
+
totalTime: "`totalTime()` was renamed to `getTotalTime()` in jsPsych v7.",
|
|
39
|
+
currentTrial: "`currentTrial()` was renamed to `getCurrentTrial()` in jsPsych v7.",
|
|
40
|
+
initSettings: "`initSettings()` was renamed to `getInitSettings()` in jsPsych v7.",
|
|
41
|
+
allTimelineVariables:
|
|
42
|
+
"`allTimelineVariables()` was renamed to `getAllTimelineVariables()` in jsPsych v7.",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
Object.defineProperties(
|
|
46
|
+
jsPsych,
|
|
47
|
+
Object.fromEntries(
|
|
48
|
+
Object.entries(migrationMessages).map(([key, message]) => [
|
|
49
|
+
key,
|
|
50
|
+
{
|
|
51
|
+
get() {
|
|
52
|
+
throw new MigrationError(message);
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
])
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return jsPsych;
|
|
24
60
|
}
|
|
25
61
|
|
|
26
62
|
export { JsPsych } from "./JsPsych";
|
package/src/migration.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export class MigrationError extends Error {
|
|
2
|
+
constructor(message = "The global `jsPsych` variable is no longer available in jsPsych v7.") {
|
|
3
|
+
super(
|
|
4
|
+
`${message} Please follow the migration guide at https://www.jspsych.org/7.0/support/migration-v7/ to update your experiment.`
|
|
5
|
+
);
|
|
6
|
+
this.name = "MigrationError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Define a global jsPsych object to handle invocations on it with migration errors
|
|
11
|
+
(window as any).jsPsych = {
|
|
12
|
+
get init() {
|
|
13
|
+
throw new MigrationError("`jsPsych.init()` was replaced by `initJsPsych()` in jsPsych v7.");
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
get data() {
|
|
17
|
+
throw new MigrationError();
|
|
18
|
+
},
|
|
19
|
+
get randomization() {
|
|
20
|
+
throw new MigrationError();
|
|
21
|
+
},
|
|
22
|
+
get turk() {
|
|
23
|
+
throw new MigrationError();
|
|
24
|
+
},
|
|
25
|
+
get pluginAPI() {
|
|
26
|
+
throw new MigrationError();
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
get ALL_KEYS() {
|
|
30
|
+
throw new MigrationError(
|
|
31
|
+
'jsPsych.ALL_KEYS was replaced by the "ALL_KEYS" string in jsPsych v7.'
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
get NO_KEYS() {
|
|
35
|
+
throw new MigrationError('jsPsych.NO_KEYS was replaced by the "NO_KEYS" string in jsPsych v7.');
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -323,4 +323,15 @@ export class MediaAPI {
|
|
|
323
323
|
}
|
|
324
324
|
this.preload_requests = [];
|
|
325
325
|
}
|
|
326
|
+
|
|
327
|
+
private microphone_recorder: MediaRecorder = null;
|
|
328
|
+
|
|
329
|
+
initializeMicrophoneRecorder(stream: MediaStream) {
|
|
330
|
+
const recorder = new MediaRecorder(stream);
|
|
331
|
+
this.microphone_recorder = recorder;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
getMicrophoneRecorder(): MediaRecorder {
|
|
335
|
+
return this.microphone_recorder;
|
|
336
|
+
}
|
|
326
337
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
export class SimulationAPI {
|
|
2
|
+
dispatchEvent(event: Event) {
|
|
3
|
+
document.body.dispatchEvent(event);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dispatches a `keydown` event for the specified key
|
|
8
|
+
* @param key Character code (`.key` property) for the key to press.
|
|
9
|
+
*/
|
|
10
|
+
keyDown(key: string) {
|
|
11
|
+
this.dispatchEvent(new KeyboardEvent("keydown", { key }));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Dispatches a `keyup` event for the specified key
|
|
16
|
+
* @param key Character code (`.key` property) for the key to press.
|
|
17
|
+
*/
|
|
18
|
+
keyUp(key: string) {
|
|
19
|
+
this.dispatchEvent(new KeyboardEvent("keyup", { key }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Dispatches a `keydown` and `keyup` event in sequence to simulate pressing a key.
|
|
24
|
+
* @param key Character code (`.key` property) for the key to press.
|
|
25
|
+
* @param delay Length of time to wait (ms) before executing action
|
|
26
|
+
*/
|
|
27
|
+
pressKey(key: string, delay = 0) {
|
|
28
|
+
if (delay > 0) {
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
this.keyDown(key);
|
|
31
|
+
this.keyUp(key);
|
|
32
|
+
}, delay);
|
|
33
|
+
} else {
|
|
34
|
+
this.keyDown(key);
|
|
35
|
+
this.keyUp(key);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Dispatches `mousedown`, `mouseup`, and `click` events on the target element
|
|
41
|
+
* @param target The element to click
|
|
42
|
+
* @param delay Length of time to wait (ms) before executing action
|
|
43
|
+
*/
|
|
44
|
+
clickTarget(target: Element, delay = 0) {
|
|
45
|
+
if (delay > 0) {
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
|
48
|
+
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
|
49
|
+
target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
50
|
+
}, delay);
|
|
51
|
+
} else {
|
|
52
|
+
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
|
53
|
+
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
|
54
|
+
target.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sets the value of a target text input
|
|
60
|
+
* @param target A text input element to fill in
|
|
61
|
+
* @param text Text to input
|
|
62
|
+
* @param delay Length of time to wait (ms) before executing action
|
|
63
|
+
*/
|
|
64
|
+
fillTextInput(target: HTMLInputElement, text: string, delay = 0) {
|
|
65
|
+
if (delay > 0) {
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
target.value = text;
|
|
68
|
+
}, delay);
|
|
69
|
+
} else {
|
|
70
|
+
target.value = text;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Picks a valid key from `choices`, taking into account jsPsych-specific
|
|
76
|
+
* identifiers like "NO_KEYS" and "ALL_KEYS".
|
|
77
|
+
* @param choices Which keys are valid.
|
|
78
|
+
* @returns A key selected at random from the valid keys.
|
|
79
|
+
*/
|
|
80
|
+
getValidKey(choices: "NO_KEYS" | "ALL_KEYS" | Array<string> | Array<Array<string>>) {
|
|
81
|
+
const possible_keys = [
|
|
82
|
+
"a",
|
|
83
|
+
"b",
|
|
84
|
+
"c",
|
|
85
|
+
"d",
|
|
86
|
+
"e",
|
|
87
|
+
"f",
|
|
88
|
+
"g",
|
|
89
|
+
"h",
|
|
90
|
+
"i",
|
|
91
|
+
"j",
|
|
92
|
+
"k",
|
|
93
|
+
"l",
|
|
94
|
+
"m",
|
|
95
|
+
"n",
|
|
96
|
+
"o",
|
|
97
|
+
"p",
|
|
98
|
+
"q",
|
|
99
|
+
"r",
|
|
100
|
+
"s",
|
|
101
|
+
"t",
|
|
102
|
+
"u",
|
|
103
|
+
"v",
|
|
104
|
+
"w",
|
|
105
|
+
"x",
|
|
106
|
+
"y",
|
|
107
|
+
"z",
|
|
108
|
+
"0",
|
|
109
|
+
"1",
|
|
110
|
+
"2",
|
|
111
|
+
"3",
|
|
112
|
+
"4",
|
|
113
|
+
"5",
|
|
114
|
+
"6",
|
|
115
|
+
"7",
|
|
116
|
+
"8",
|
|
117
|
+
"9",
|
|
118
|
+
" ",
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
let key;
|
|
122
|
+
if (choices == "NO_KEYS") {
|
|
123
|
+
key = null;
|
|
124
|
+
} else if (choices == "ALL_KEYS") {
|
|
125
|
+
key = possible_keys[Math.floor(Math.random() * possible_keys.length)];
|
|
126
|
+
} else {
|
|
127
|
+
const flat_choices = choices.flat();
|
|
128
|
+
key = flat_choices[Math.floor(Math.random() * flat_choices.length)];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return key;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
mergeSimulationData(default_data, simulation_options) {
|
|
135
|
+
// override any data with data from simulation object
|
|
136
|
+
return {
|
|
137
|
+
...default_data,
|
|
138
|
+
...simulation_options?.data,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ensureSimulationDataConsistency(trial, data) {
|
|
143
|
+
// All RTs must be rounded
|
|
144
|
+
if (data.rt) {
|
|
145
|
+
data.rt = Math.round(data.rt);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// If a trial_duration and rt exist, make sure that the RT is not longer than the trial.
|
|
149
|
+
if (trial.trial_duration && data.rt && data.rt > trial.trial_duration) {
|
|
150
|
+
data.rt = null;
|
|
151
|
+
if (data.response) {
|
|
152
|
+
data.response = null;
|
|
153
|
+
}
|
|
154
|
+
if (data.correct) {
|
|
155
|
+
data.correct = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// If trial.choices is NO_KEYS make sure that response and RT are null
|
|
160
|
+
if (trial.choices && trial.choices == "NO_KEYS") {
|
|
161
|
+
if (data.rt) {
|
|
162
|
+
data.rt = null;
|
|
163
|
+
}
|
|
164
|
+
if (data.response) {
|
|
165
|
+
data.response = null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// If response is not allowed before stimulus display complete, ensure RT
|
|
170
|
+
// is longer than display time.
|
|
171
|
+
if (trial.allow_response_before_complete) {
|
|
172
|
+
if (trial.sequence_reps && trial.frame_time) {
|
|
173
|
+
const min_time = trial.sequence_reps * trial.frame_time * trial.stimuli.length;
|
|
174
|
+
if (data.rt < min_time) {
|
|
175
|
+
data.rt = null;
|
|
176
|
+
data.response = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -4,6 +4,7 @@ import { JsPsych } from "../../JsPsych";
|
|
|
4
4
|
import { HardwareAPI } from "./HardwareAPI";
|
|
5
5
|
import { KeyboardListenerAPI } from "./KeyboardListenerAPI";
|
|
6
6
|
import { MediaAPI } from "./MediaAPI";
|
|
7
|
+
import { SimulationAPI } from "./SimulationAPI";
|
|
7
8
|
import { TimeoutAPI } from "./TimeoutAPI";
|
|
8
9
|
|
|
9
10
|
export function createJointPluginAPIObject(jsPsych: JsPsych) {
|
|
@@ -19,8 +20,9 @@ export function createJointPluginAPIObject(jsPsych: JsPsych) {
|
|
|
19
20
|
new TimeoutAPI(),
|
|
20
21
|
new MediaAPI(settings.use_webaudio, jsPsych.webaudio_context),
|
|
21
22
|
new HardwareAPI(),
|
|
23
|
+
new SimulationAPI(),
|
|
22
24
|
].map((object) => autoBind(object))
|
|
23
|
-
) as KeyboardListenerAPI & TimeoutAPI & MediaAPI & HardwareAPI;
|
|
25
|
+
) as KeyboardListenerAPI & TimeoutAPI & MediaAPI & HardwareAPI & SimulationAPI;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export type PluginAPI = ReturnType<typeof createJointPluginAPIObject>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import rw from "random-words";
|
|
2
|
+
|
|
1
3
|
export function repeat(array, repetitions, unpack = false) {
|
|
2
4
|
const arr_isArray = Array.isArray(array);
|
|
3
5
|
const rep_isArray = Array.isArray(repetitions);
|
|
@@ -173,7 +175,7 @@ export function sampleWithoutReplacement(arr, size) {
|
|
|
173
175
|
return shuffle(arr).slice(0, size);
|
|
174
176
|
}
|
|
175
177
|
|
|
176
|
-
export function sampleWithReplacement(arr, size, weights) {
|
|
178
|
+
export function sampleWithReplacement(arr, size, weights?) {
|
|
177
179
|
if (!Array.isArray(arr)) {
|
|
178
180
|
console.error("First argument to sampleWithReplacement() must be an array");
|
|
179
181
|
}
|
|
@@ -240,6 +242,75 @@ export function randomID(length = 32) {
|
|
|
240
242
|
return result;
|
|
241
243
|
}
|
|
242
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Generate a random integer from `lower` to `upper`, inclusive of both end points.
|
|
247
|
+
* @param lower The lowest value it is possible to generate
|
|
248
|
+
* @param upper The highest value it is possible to generate
|
|
249
|
+
* @returns A random integer
|
|
250
|
+
*/
|
|
251
|
+
export function randomInt(lower: number, upper: number) {
|
|
252
|
+
if (upper < lower) {
|
|
253
|
+
throw new Error("Upper boundary must be less than or equal to lower boundary");
|
|
254
|
+
}
|
|
255
|
+
return lower + Math.floor(Math.random() * (upper - lower + 1));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Generates a random sample from a Bernoulli distribution.
|
|
260
|
+
* @param p The probability of sampling 1.
|
|
261
|
+
* @returns 0, with probability 1-p, or 1, with probability p.
|
|
262
|
+
*/
|
|
263
|
+
export function sampleBernoulli(p: number) {
|
|
264
|
+
return Math.random() <= p ? 1 : 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function sampleNormal(mean: number, standard_deviation: number) {
|
|
268
|
+
return randn_bm() * standard_deviation + mean;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function sampleExponential(rate: number) {
|
|
272
|
+
return -Math.log(Math.random()) / rate;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function sampleExGaussian(
|
|
276
|
+
mean: number,
|
|
277
|
+
standard_deviation: number,
|
|
278
|
+
rate: number,
|
|
279
|
+
positive = false
|
|
280
|
+
) {
|
|
281
|
+
let s = sampleNormal(mean, standard_deviation) + sampleExponential(rate);
|
|
282
|
+
if (positive) {
|
|
283
|
+
while (s <= 0) {
|
|
284
|
+
s = sampleNormal(mean, standard_deviation) + sampleExponential(rate);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return s;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Generate one or more random words.
|
|
292
|
+
*
|
|
293
|
+
* This is a wrapper function for the {@link https://www.npmjs.com/package/random-words `random-words` npm package}.
|
|
294
|
+
*
|
|
295
|
+
* @param opts An object with optional properties `min`, `max`, `exactly`,
|
|
296
|
+
* `join`, `maxLength`, `wordsPerString`, `separator`, and `formatter`.
|
|
297
|
+
*
|
|
298
|
+
* @returns An array of words or a single string, depending on parameter choices.
|
|
299
|
+
*/
|
|
300
|
+
export function randomWords(opts) {
|
|
301
|
+
return rw(opts);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Box-Muller transformation for a random sample from normal distribution with mean = 0, std = 1
|
|
305
|
+
// https://stackoverflow.com/a/36481059/3726673
|
|
306
|
+
function randn_bm() {
|
|
307
|
+
var u = 0,
|
|
308
|
+
v = 0;
|
|
309
|
+
while (u === 0) u = Math.random(); //Converting [0,1) to (0,1)
|
|
310
|
+
while (v === 0) v = Math.random();
|
|
311
|
+
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
|
312
|
+
}
|
|
313
|
+
|
|
243
314
|
function unpackArray(array) {
|
|
244
315
|
const out = {};
|
|
245
316
|
|