-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathexperiment.js
480 lines (419 loc) · 19.5 KB
/
experiment.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
/* experiment variables */
var username;
var numPracticeTrials = 5;
var numRepeatsPerMiniblock = 25;
var numMiniblocksPerCondition = 4;
var availableShapes = ['cross', 'diamond', 'circle', 'triangle', 'arrow', 'flag'];
// parameters for ensuring consistent display sizes.
// use original px per millimeter for measurements to ensure no issue when the pxPerMillimeter is updated later, given a separate function handles resizing of the canvas
var originalPxPerMillimeter = 6.522;
var pxPerMillimeter = 6.522;
var locationAngles = [];
for (let theta = 0; theta < 2 * Math.PI; theta += 2 * Math.PI / 8) {
locationAngles.push(theta);
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
var debug = false;
var platform = "lab";
/* create timeline */
var timeline = [];
function preloadImages() {
var preload = {
type: jsPsychPreload,
auto_preload: true,
images: [
'media/instructions/wm/instruction_b1_s1.png',
'media/instructions/wm/instruction_b1_s2.png',
'media/instructions/wm/instruction_b1_s3.png',
'media/instructions/wm/instruction_b1_s4.png',
'media/instructions/wm/instruction_b1_s5.png',
'media/instructions/wm/instruction_b1_s6.png',
'media/instructions/wm/instruction_b2_s1.png',
'media/instructions/wm/instruction_b2_s2.png',
'media/instructions/wm/instruction_b2_s3.png',
'media/instructions/wm/instruction_b2_s4.png',
'media/instructions/wm/instruction_b2_s5.png'
],
audio: [
'media/audio/test.mp3'
],
video: [
'media/instructions/wm/wm_task_video.mp4'
],
show_detailed_errors: true,
show_progress_bar: true,
loading_message: '<div>The experiment is being loaded ...</div>',
}
timeline.push(preload);
}
function initialiseExperiment() {
var welcomeScreen = {
type: jsPsychHtmlButtonResponse,
stimulus: function() { return '<div style="font-size:25px;"><b>Your anonymous ID is: </b> "' + jsPsych.data.getURLVariable('participant') + '"?<br><br>Press [Next] to continue.<br><br></div>' },
choices: ['Next'],
stimulus_duration: null,
response_ends_trial: true,
on_finish: function() {
username = jsPsych.data.getURLVariable('participant');
jsPsych.data.addProperties({ ppid: username });
}
};
timeline.push(welcomeScreen)
}
function performCalibration() {
var preCalibration = {
type: jsPsychHtmlButtonResponse,
stimulus: '<div style="font-size:25px;">The next few screens will calibrate the experiment.<br><br>Please press [Next] to continue.<br><br></div>',
choices: ['Next'],
stimulus_duration: null,
response_ends_trial: true
};
timeline.push(preCalibration)
/* measure credit card */
var creditCard = {
type: jsPsychResize,
item_width: 86.60,
item_height: 53.98,
prompt: '<div style="font-size:25px;"><br><br>To try and approximate the size of your screen in "real world" coordinates, please put a standard size credit/debit card on the screen. Then using your mouse, click and drag the bottom right corner to resize the box until it is the same size as the credit/debit card. Please be as accurate as possible.<br><br><div>',
pixels_per_unit: 6.522,
on_finish: function() {
pxPerMillimeter = jsPsych.data.get().last(1).values()[0]["final_width_px"] / 86.60;
jsPsych.data.addProperties({ px_per_mm: pxPerMillimeter });
console.log(pxPerMillimeter);
stimuliDisplay.resize(pxPerMillimeter);
}
};
timeline.push(creditCard);
/* get permission to use microphone and record a clip to calibrate audio volume */
if (!debug) {
var speakerCheck = {
type: jsPsychAudioButtonResponse,
stimulus: "media/audio/test.mp3",
choices: ['Continue'],
prompt: '<div style="font-size:25px;"><br><br>Can you hear the sound of a river?<br><br>Make sure your speakers are on, and adjust your speaker volume until the audio is at a comfortable hearing level. When you can hear the sound of the river comfortably, press [Continue].<br><br></div>',
response_ends_trial: true
}
timeline.push(speakerCheck);
}
var initMic = {
type: jsPsychInitializeMicrophone
};
timeline.push(initMic);
var preMic = {
type: jsPsychHtmlButtonResponse,
stimulus: '<div style="font-size:25px;">On the next page, you will be asked to record yourself saying "hello" to calibrate the volume of the microphone.<br><br>When you have recorded it, listen to the playback.<br>If you can hear yourself, click [continue].<br>If you can\'t hear yourself, increase the volume of your microphone and click [Record again]<br><br></div>',
choices: ['Next'],
stimulus_duration: null,
response_ends_trial: true
};
timeline.push(preMic)
var calibrateMic = {
type: jsPsychHtmlAudioResponse,
stimulus: '<div style="font-size:25px;"><br>Please say "hello" now, and click [Finished] after.<br></div>',
allow_playback: true,
recording_duration: 10000,
show_done_button: true,
done_button_label: "Finished",
playback_text: "Please press the play button to listen to the recording you just made. <br>If you can hear yourself in the recording, click [Finished]. <br>If you can't hear yourself, increase your microphone volume, and then click [Record again] to make the recording again."
};
timeline.push(calibrateMic);
var postMic = {
type: jsPsychHtmlKeyboardResponse,
stimulus: '<div style="font-size:25px;">Calibration Complete!</div>',
choices: "NO_KEYS",
trial_duration: 2000
};
timeline.push(postMic)
}
/* define instructions */
function generateFirstBlockInstructions() {
return {
type: jsPsychInstructions,
pages: [
'<img src="media/instructions/wm/instruction_b1_s1.png" width="900" height="630"></img>' + '<div style="font-size:25px;"><br></div>',
'<img src="media/instructions/wm/instruction_b1_s2.png" width="900" height="630"></img>' + '<div style="font-size:25px;"><br></div>',
'<img src="media/instructions/wm/instruction_b1_s3.png" width="900" height="630"></img>' + '<div style="font-size:25px;"><br></div>',
'<img src="media/instructions/wm/instruction_b1_s4.png" width="900" height="630"></img>' + '<div style="font-size:25px;"><br></div>',
'<img src="media/instructions/wm/instruction_b1_s5.png" width="900" height="630"></img>' + '<div style="font-size:25px;"><br></div>',
'<img src="media/instructions/wm/instruction_b1_s6.png" width="900" height="630"></img>' + '<div style="font-size:25px;"><br></div>',
'Please watch the video below for an example of a trial and the speed at which you should repeat the number.<br><br><video width="900" height="630" controls> <source src="media/instructions/wm/wm_task_video.mp4" type="video/mp4"></video>' + '<div style="font-size:25px;"><br></div>'
],
show_clickable_nav: true
}
}
function generateSecondBlockInstructions() {
return {
type: jsPsychInstructions,
pages: [
'<img src="media/instructions/wm/instruction_b2_s1.png" width="900" height="630"></img>' + '<div style="font-size:25px;"><br></div>',
'<img src="media/instructions/wm/instruction_b2_s2.png" width="900" height="630"></img>' + '<div style="font-size:25px;"><br></div>',
'<img src="media/instructions/wm/instruction_b2_s3.png" width="900" height="630"></img>' + '<div style="font-size:25px;"><br></div>',
'<img src="media/instructions/wm/instruction_b2_s4.png" width="900" height="630"></img>' + '<div style="font-size:25px;"><br></div>',
'<img src="media/instructions/wm/instruction_b2_s5.png" width="900" height="630"></img>' + '<div style="font-size:25px;"><br></div>'
],
show_clickable_nav: true
}
}
function generateAllTrials() {
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getRandomJitter() {
return [
(Math.random() * 2) - 1,
(Math.random() * 2) - 1
]
}
function generateRandomAngles(num) {
function checkAngleDifference(angle1, angle2) {
return 180 - Math.abs(Math.abs(angle1 - angle2) - 180)
}
let random_angles = [];
let unallowable_range = 20;
for (let i = 0; i < num; i++) {
while (true) {
temp_angle = getRandomInt(0, 360);
let too_close = false;
for (let j = 0; j < random_angles.length; j++) {
let angle_difference = checkAngleDifference(temp_angle, random_angles[j]);
if (angle_difference < unallowable_range) {
too_close = true;
break;
}
}
if (!too_close) {
random_angles.push(temp_angle);
break;
}
}
}
for (let i = 0; i < random_angles.length; i++) {
random_angles[i] = random_angles[i] * Math.PI / 180;
}
return random_angles;
}
var spaceWhenReady = {
type: jsPsychHtmlKeyboardResponse,
stimulus: '<div style="font-size:25px;">Press Spacebar When Ready</div>',
choices: " ",
response_ends_trial: true
};
var pretrial = {
type: jsPsychPretrialResponse,
shapes: function() { return jsPsych.timelineVariable('shapes') },
colors: function() { return jsPsych.timelineVariable('colors') },
locationAngles: function() { return jsPsych.timelineVariable('location_angles') },
jitters: function() { return jsPsych.timelineVariable('jitters') },
data: {
test_part: 'test'
}
};
var colorwheel = {
type: jsPsychColorwheelResponse,
shapes: function() { return jsPsych.timelineVariable('shapes') },
target_angle: function() { return jsPsych.timelineVariable('angles')[jsPsych.timelineVariable('probe')] },
locationAngles: function() { return jsPsych.timelineVariable('location_angles') },
jitters: function() { return jsPsych.timelineVariable('jitters') },
probe: function() { return jsPsych.timelineVariable('probe') },
probeByLocation: function() { return jsPsych.timelineVariable('probe_by_location') },
data: {
test_part: 'test'
}
}
function generateTrial(trialShapes, trialLocationAngles, trialProbe, trialCondition, experimentPart, probeByLocation) {
let currentShapes = trialShapes;
let currentLocationAngles = trialLocationAngles;
let currentProbe = trialProbe;
let currentCondition = trialCondition;
let currentAngles = generateRandomAngles(trialShapes.length);
let currentColors = [];
for (const angle of currentAngles) {
currentColors.push(cieColors.angleToRGB(angle));
}
let currentJitters = Array.from({length: trialShapes.length}, (() => getRandomJitter()));
let currentProbeByLocation = probeByLocation;
let testProcedure = {
timeline: [spaceWhenReady, pretrial, colorwheel],
repetitions: 1,
timeline_variables: [{
condition: currentCondition,
experiment_part: experimentPart,
shapes: currentShapes,
angles: currentAngles,
colors: currentColors,
probe: currentProbe,
location_angles: currentLocationAngles,
probe_by_location: currentProbeByLocation,
jitters: currentJitters
}],
data: {
condition: function() { return jsPsych.timelineVariable('condition') },
experiment_part: function() { return jsPsych.timelineVariable('experiment_part') },
shapes: function() { return jsPsych.timelineVariable('shapes') },
angles: function() { return jsPsych.timelineVariable('angles') },
colors: function() { return jsPsych.timelineVariable('colors') },
probe: function() { return jsPsych.timelineVariable('probe') },
locationAngles: function() { return jsPsych.timelineVariable('location_angles') },
probeByLocation: function() { return jsPsych.timelineVariable('probe_by_location') },
jitters: function() { return jsPsych.timelineVariable("jitters") }
},
randomize_order: false
}
return testProcedure;
}
// make unequal value conditions
function generateBlock(numTrials, experimentPart, probeByLocation, nItems) {
currentBlock = []
// make equal value conditions
for (let i = 0; i < numTrials; i++) {
let probe = Array.from({length: nItems}, (v, i) => i);
shuffleArray(probe);
probe = probe.slice(0, 1);
let trialShapes = Array(nItems).fill("square");
if (!probeByLocation)
{
trialShapes = availableShapes.slice()
shuffleArray(trialShapes);
trialShapes = trialShapes.slice(0, nItems);
}
let trialLocationAngles = locationAngles.slice()
shuffleArray(trialLocationAngles);
trialLocationAngles = trialLocationAngles.slice(0, nItems);
currentBlock.push(generateTrial(trialShapes, trialLocationAngles, probe, 'EqualValue', experimentPart, probeByLocation));
}
shuffleArray(currentBlock);
currentBlock = currentBlock.slice(0, numTrials);
return currentBlock;
}
// generate all trials //
// generate test trials
var breaks = {
type: jsPsychHtmlButtonResponse,
stimulus: "You are currently in a break!\nPress the [Ready] button to continue.",
choices: ['Ready'],
stimulus_duration: null,
response_ends_trial: true
};
function generateBeforePracticeMessage(numItems, probeByLocation) {
return {
type: jsPsychHtmlButtonResponse,
stimulus: `<div style="font-size:25px;">Please remember to repeat the two digit number aloud at a speed of two repetitions per second until you are asked to report the remembered colour. Compliance with this instruction will be checked. <br><br>In this block of trials, there will be ${numItems} items shown. <br><br>First, let\'s do some practice trials. Press [Next] to start.<br><br></div>`,
choices: ['Next'],
response_ends_trial: true
};
}
function generateBeforeBlockMessage(numItems, probeByLocation) {
return {
type: jsPsychHtmlButtonResponse,
stimulus: `<div style="font-size:25px;">You will complete the test trials now<br><br> Please remember to repeat the two digit number aloud at a speed of two repetitions per second until you are asked to report the remembered colour. Compliance with this instruction will be checked. <br><br>In this block of trials, there will ${numItems} items shown.<br><br></div>`,
choices: ['Next'],
response_ends_trial: true
};
}
function generateCondition(numTrials, experimentPart, probeByLocation, numItems, numRepeats) {
let trials = []
for (let i = 0; i < numRepeats; i++) {
trials.push(generateBlock(numTrials, experimentPart, probeByLocation, numItems));
trials.push(breaks);
}
return trials;
}
var trials = [
generateFirstBlockInstructions(),
generateBlock(numPracticeTrials, "practice", true, 4),
generateBeforeBlockMessage(4, true),
generateCondition(numRepeatsPerMiniblock, "test", true, 4, numMiniblocksPerCondition),
generateSecondBlockInstructions(),
generateBlock(numPracticeTrials, "practice", false, 4),
generateBeforeBlockMessage(4, false),
generateCondition(numRepeatsPerMiniblock, "test", false, 4, numMiniblocksPerCondition)
];
trials = trials.flat(Infinity);
trials.pop(); // remove last break
for (var trial of trials) {
timeline.push(trial);
}
}
// generate survey
function generatePostExperimentSurvey() {
var survey = {
type: jsPsychSurveyText,
preamble: 'Thank you for completing the experiments! Please complete this questionnaire to finish.',
questions: [{
prompt: "Did you use any stategies to perform the task and maximise your score? Please type them in the box below.",
placeholder: "Enter any strategies used here ...",
required: true
}],
data: {
test_part: 'survey'
}
};
timeline.push(survey);
}
// generate debrief screen
function generateDebriefScreen() {
var debrief = {
type: jsPsychHtmlButtonResponse,
stimulus: '<div style="font-size:25px;">Thank you for your participation.<br><br>Press [Next] to complete the experiment.<br><br></div>',
choices: ['Next'],
stimulus_duration: null,
response_ends_trial: true
};
timeline.push(debrief)
}
function finaliseExperiment() {
var saveData = {
type: jsPsychPipe,
action: "save",
experiment_id: "biRtBLGfGkY8",
filename: function() { return `${username}.csv` },
data_string: ()=>jsPsych.data.get().csv()
};
timeline.push(saveData);
if (platform == "prolific") {
var finished = {
type: jsPsychHtmlKeyboardResponse,
stimulus: `<p>You've finished the last task. Thanks for participating!</p>
<p>This study aims to investigate the involvement of a brain region called the Hippocampus in Working Memory, our ability to remember information over short durations. Your data will be used to set the task difficulty to compare healthy 'controls' against individuals with Hippocampal damage.</p>
<p><a href="https://app.prolific.com/submissions/complete?cc=77E7D184">Click here to return to Prolific and finalise your submission.</a>. If the link does not work, the completion code is 77E7D184.</p>`,
choices: "NO_KEYS"
}
timeline.push(finished);
} else {
var downloadData = {
type: jsPsychHtmlButtonResponse,
stimulus: function() { return '<div style="font-size:25px;"><b>Please notify the experimenter you have finished the experiment.<br><br></div>' },
choices: ['Download data'],
stimulus_duration: null,
response_ends_trial: true,
on_finish: function() {
jsPsych.data.get().localSave('csv', `${username}.csv`);
}
}
timeline.push(downloadData);
var finished = {
type: jsPsychHtmlKeyboardResponse,
stimulus: '<div style="font-size:25px;">You can close the experiment now.</div>',
choices: "NO_KEYS"
};
timeline.push(finished);
}
}
// set up
initialiseExperiment();
preloadImages();
performCalibration();
// generate the WM experiment
generateAllTrials();
// post experiment survey
generatePostExperimentSurvey();
// end experiment
generateDebriefScreen();
finaliseExperiment();