An easy-to-use platform for EEG experimentation in the classroom
1/* eslint-disable */
2
3// Define study
4const experimentObject = {
5 title: 'root',
6 type: 'lab.flow.Sequence',
7 parameters: {},
8 plugins: [],
9 metadata: {},
10 files: {},
11 responses: {},
12 content: [
13 {
14 type: 'lab.flow.Sequence',
15 files: {},
16 parameters: {},
17 responses: {},
18 messageHandlers: {},
19 title: 'The face-house task',
20 content: [
21 {
22 type: 'lab.html.Screen',
23 files: {},
24 parameters: {},
25 responses: {
26 'keypress(Space)': 'continue',
27 'keypress(q)': 'skipPractice',
28 },
29 messageHandlers: {},
30 title: 'Instruction',
31 content:
32 '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003E${this.parameters.title || "The face-house task"}\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n\n \u003Cp\u003E\n ${this.parameters.intro}\n \u003C\u002Fp\u003E\n \n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E',
33 },
34 {
35 type: 'lab.flow.Loop',
36 files: {},
37 parameters: {},
38 templateParameters: [],
39 sample: {
40 mode: 'draw-shuffle',
41 n: '',
42 },
43 responses: {},
44 messageHandlers: {
45 'before:prepare': function anonymous() {
46 let initParameters = [...this.parameters.stimuli] || [];
47 initParameters =
48 initParameters.filter((t) => t.phase === 'practice') || [];
49 let numberTrials = this.parameters.nbPracticeTrials;
50 if (initParameters.length === 0) {
51 numberTrials = 0;
52 }
53 const randomize = this.parameters.randomize;
54 const trialsLength = initParameters.length;
55 if (numberTrials > trialsLength) {
56 const append = [...initParameters];
57 const multiply = Math.ceil(numberTrials / trialsLength);
58 for (let i = 0; i < multiply; i++) {
59 initParameters = initParameters.concat(append);
60 }
61 }
62
63 function shuffle(a) {
64 let j, x, i;
65 for (i = a.length - 1; i > 0; i--) {
66 j = Math.floor(Math.random() * (i + 1));
67 x = a[i];
68 a[i] = a[j];
69 a[j] = x;
70 }
71 return a;
72 }
73
74 if (randomize === 'random') {
75 shuffle(initParameters);
76 }
77
78 const trialConstructor = (file) => ({
79 condition: file.condition,
80 image: `${file.dir}/${file.filename}`,
81 correctResponse: file.response,
82 phase: 'practice',
83 name: file.name,
84 type: file.type,
85 });
86
87 // balance design across conditions
88 const conditions = Array.from(
89 new Set(initParameters.map((p) => p.condition))
90 );
91 const conditionsParameters = {};
92 for (const c of conditions) {
93 conditionsParameters[c] = initParameters.filter(
94 (p) => p.condition == c
95 );
96 }
97 const numberConditionsTrials = Math.ceil(
98 numberTrials / conditions.length
99 );
100 let balancedParameters = [];
101 for (let i = 0; i < numberConditionsTrials; i++) {
102 for (const c of conditions) {
103 balancedParameters = balancedParameters.concat(
104 conditionsParameters[c][i % conditionsParameters[c].length]
105 );
106 }
107 }
108 initParameters = [...balancedParameters.slice(0, numberTrials)];
109
110 let practiceParameters = [];
111 for (let i = 0; i < numberTrials; i++) {
112 practiceParameters = practiceParameters.concat(
113 trialConstructor(initParameters[i])
114 );
115 }
116
117 // assign options values to parameters of this task
118 this.options.templateParameters = practiceParameters;
119 if (randomize === 'random') {
120 this.options.shuffle = true;
121 } else {
122 this.options.shuffle = false;
123 }
124 },
125 },
126 title: 'Practice loop',
127 shuffleGroups: [],
128 template: {
129 type: 'lab.flow.Sequence',
130 files: {},
131 parameters: {},
132 responses: {},
133 messageHandlers: {},
134 title: 'Trial',
135 content: [
136 {
137 type: 'lab.canvas.Screen',
138 content: [
139 {
140 type: 'rect',
141 left: 0,
142 top: 0,
143 angle: 0,
144 width: 10,
145 height: '50',
146 stroke: null,
147 strokeWidth: 1,
148 fill: 'black',
149 },
150 {
151 type: 'rect',
152 left: 0,
153 top: 0,
154 angle: 90,
155 width: 10,
156 height: '50',
157 stroke: null,
158 strokeWidth: 1,
159 fill: 'black',
160 },
161 ],
162 files: {},
163 parameters: {},
164 responses: {},
165 messageHandlers: {},
166 viewport: [800, 600],
167 title: 'Fixation cross',
168 timeout: '${parameters.iti}',
169 },
170 {
171 type: 'lab.html.Screen',
172 files: {},
173 responses: {},
174 parameters: {},
175 messageHandlers: {
176 'before:prepare': function anonymous() {
177 // This code registers an event listener for this screen.
178 // We have a timeout for this screen, but we also want to record responses.
179 // On a keydown event, we record the key and the time of response.
180 // We also record whether the response was correct (by comparing the pressed key with the correct response which is defined inside the Experiment loop).
181 // "this" in the code means the lab.js experiment.
182 const responses = [
183 ...new Set(
184 this.parameters.stimuli.map((e) => e.response)
185 ),
186 ];
187 this.data.trial_number =
188 1 +
189 parseInt(
190 this.options.id.split('_')[
191 this.options.id.split('_').length - 2
192 ]
193 );
194 this.data.response_given = 'no';
195
196 this.options.events = {
197 keydown: (event) => {
198 if (responses.includes(event.key)) {
199 this.data.reaction_time = this.timer;
200 if (this.parameters.phase === 'task')
201 this.data.response_given = 'yes';
202 this.data.response = event.key;
203 if (
204 this.data.response ==
205 this.parameters.correctResponse
206 ) {
207 this.data.correct_response = true;
208 } else {
209 this.data.correct_response = false;
210 }
211 this.end();
212 }
213 },
214 };
215 },
216 run: function anonymous() {
217 this.parameters.callbackForEEG(this.parameters.type);
218 },
219 },
220 title: 'Stimulus',
221 timeout:
222 "${parameters.selfPaced ? '3600000' : parameters.presentationTime}",
223 content:
224 '\u003Cmain class="content-horizontal-center content-vertical-center"\u003E\n \u003Cdiv\u003E\n \u003Cimg src=${ this.files[this.parameters.image] } height=${ this.parameters.imageHeight } \u002F\u003E\n \u003C\u002Fdiv\u003E\n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n ${this.parameters.taskHelp} \n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E',
225 },
226 {
227 type: 'lab.canvas.Screen',
228 content: [
229 {
230 type: 'i-text',
231 left: 0,
232 top: 0,
233 angle: 0,
234 width: 895.3,
235 height: 36.16,
236 stroke: null,
237 strokeWidth: 1,
238 fill: "${ state.correct_response ? 'green' : 'red' }",
239 text: "${ state.correct_response ? 'Well done!' : 'Please respond accurately' }",
240 fontStyle: 'normal',
241 fontWeight: 'bold',
242 fontSize: '52',
243 fontFamily: 'sans-serif',
244 lineHeight: 1.16,
245 textAlign: 'center',
246 },
247 ],
248 files: {},
249 parameters: {},
250 responses: {},
251 messageHandlers: {
252 end: function anonymous() {
253 this.data.correct_response = false;
254 },
255 },
256 viewport: [800, 600],
257 title: 'Feedback',
258 tardy: true,
259 timeout: '1000',
260 skip: "${ parameters.phase === 'task' }",
261 },
262 ],
263 },
264 },
265 {
266 type: 'lab.html.Screen',
267 files: {},
268 parameters: {},
269 responses: {
270 'keypress(Space)': 'continue',
271 },
272 messageHandlers: {},
273 title: 'Main task',
274 content:
275 '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EReady for the real data collection?\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\u003Cmain\u003E\n\n \u003Cp\u003E\n Press the the space bar to start the main task.\n \u003C\u002Fp\u003E\n\n\u003C\u002Fmain\u003E\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E',
276 },
277 {
278 type: 'lab.flow.Loop',
279 files: {},
280 parameters: {},
281 templateParameters: [],
282 sample: {
283 mode: 'draw-shuffle',
284 n: '',
285 },
286 responses: {},
287 messageHandlers: {
288 'before:prepare': function anonymous() {
289 let initialParameters = [...this.parameters.stimuli] || [];
290 initialParameters =
291 initialParameters.filter((t) => t.phase === 'main') || [];
292 let numberTrials = this.parameters.nbTrials;
293 if (initialParameters.length === 0) {
294 numberTrials = 0;
295 }
296 const randomize = this.parameters.randomize;
297 const trialsLength = initialParameters.length;
298 if (numberTrials > trialsLength) {
299 const append = [...initialParameters];
300 const multiply = Math.ceil(numberTrials / trialsLength);
301 for (let i = 0; i < multiply; i++) {
302 initialParameters = initialParameters.concat(append);
303 }
304 }
305
306 function shuffle(a) {
307 let j, x, i;
308 for (i = a.length - 1; i > 0; i--) {
309 j = Math.floor(Math.random() * (i + 1));
310 x = a[i];
311 a[i] = a[j];
312 a[j] = x;
313 }
314 return a;
315 }
316
317 if (randomize === 'random') {
318 shuffle(initialParameters);
319 }
320
321 const trialConstructor = (file) => ({
322 condition: file.condition,
323 image: `${file.dir}/${file.filename}`,
324 correctResponse: file.response,
325 phase: 'task',
326 name: file.name,
327 type: file.type,
328 });
329 // balance design across conditions
330 const conditions = Array.from(
331 new Set(initialParameters.map((p) => p.condition))
332 );
333 const conditionsParameters = {};
334 for (const c of conditions) {
335 conditionsParameters[c] = initialParameters.filter(
336 (p) => p.condition == c
337 );
338 }
339 const numberConditionsTrials = Math.ceil(
340 numberTrials / conditions.length
341 );
342 let balancedParameters = [];
343 for (let i = 0; i < numberConditionsTrials; i++) {
344 for (const c of conditions) {
345 balancedParameters = balancedParameters.concat(
346 conditionsParameters[c][i % conditionsParameters[c].length]
347 );
348 }
349 }
350 initialParameters = [
351 ...balancedParameters.slice(0, numberTrials),
352 ];
353
354 let trialParameters = [];
355 for (let i = 0; i < numberTrials; i++) {
356 trialParameters = [
357 ...trialParameters.concat(
358 trialConstructor(initialParameters[i])
359 ),
360 ];
361 }
362 // assign options values to parameters of this task
363 this.options.templateParameters = trialParameters;
364 if (randomize === 'random') {
365 this.options.shuffle = true;
366 } else {
367 this.options.shuffle = false;
368 }
369 },
370 },
371 title: 'Experiment loop',
372 shuffleGroups: [],
373 template: {
374 type: 'lab.flow.Sequence',
375 files: {},
376 parameters: {},
377 responses: {},
378 messageHandlers: {},
379 title: 'Trial',
380 content: [
381 {
382 type: 'lab.canvas.Screen',
383 content: [
384 {
385 type: 'rect',
386 left: 0,
387 top: 0,
388 angle: 0,
389 width: 10,
390 height: '50',
391 stroke: null,
392 strokeWidth: 1,
393 fill: 'black',
394 },
395 {
396 type: 'rect',
397 left: 0,
398 top: 0,
399 angle: 90,
400 width: 10,
401 height: '50',
402 stroke: null,
403 strokeWidth: 1,
404 fill: 'black',
405 },
406 ],
407 files: {},
408 parameters: {},
409 responses: {},
410 messageHandlers: {},
411 viewport: [800, 600],
412 title: 'Fixation cross',
413 timeout: '${parameters.iti}',
414 },
415 {
416 type: 'lab.html.Screen',
417 files: {},
418 responses: {},
419 parameters: {},
420 messageHandlers: {
421 'before:prepare': function anonymous() {
422 // This code registers an event listener for this screen.
423 // We have a timeout for this screen, but we also want to record responses.
424 // On a keydown event, we record the key and the time of response.
425 // We also record whether the response was correct (by comparing the pressed key with the correct response which is defined inside the Experiment loop).
426 // "this" in the code means the lab.js experiment.
427 const responses = [
428 ...new Set(
429 this.parameters.stimuli.map((e) => e.response)
430 ),
431 ];
432 this.data.trial_number =
433 1 +
434 parseInt(
435 this.options.id.split('_')[
436 this.options.id.split('_').length - 2
437 ]
438 );
439 this.data.response_given = 'no';
440
441 this.options.events = {
442 keydown: (event) => {
443 if (responses.includes(event.key)) {
444 this.data.reaction_time = this.timer;
445 if (this.parameters.phase === 'task')
446 this.data.response_given = 'yes';
447 this.data.response = event.key;
448 if (
449 this.data.response ==
450 this.parameters.correctResponse
451 ) {
452 this.data.correct_response = true;
453 } else {
454 this.data.correct_response = false;
455 }
456 this.end();
457 }
458 },
459 };
460 },
461 run: function anonymous() {
462 this.parameters.callbackForEEG(this.parameters.type);
463 },
464 },
465 title: 'Stimulus',
466 timeout:
467 "${parameters.selfPaced ? '3600000' : parameters.presentationTime}",
468 timeout:
469 "${parameters.selfPaced ? '3600000' : parameters.presentationTime}",
470 content:
471 '\u003Cmain class="content-horizontal-center content-vertical-center"\u003E\n \u003Cdiv\u003E\n \u003Cimg src=${ this.files[this.parameters.image] } height=${ this.parameters.imageHeight } \u002F\u003E\n \u003C\u002Fdiv\u003E\n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n ${this.parameters.taskHelp} \n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E',
472 },
473 {
474 type: 'lab.canvas.Screen',
475 content: [
476 {
477 type: 'i-text',
478 left: 0,
479 top: 0,
480 angle: 0,
481 width: 895.3,
482 height: 36.16,
483 stroke: null,
484 strokeWidth: 1,
485 fill: "${ state.correct_response ? 'green' : 'red' }",
486 text: "${ state.correct_response ? 'Well done!' : 'Please respond accurately' }",
487 fontStyle: 'normal',
488 fontWeight: 'bold',
489 fontSize: '52',
490 fontFamily: 'sans-serif',
491 lineHeight: 1.16,
492 textAlign: 'center',
493 },
494 ],
495 files: {},
496 parameters: {},
497 responses: {},
498 messageHandlers: {},
499 viewport: [800, 600],
500 title: 'Feedback',
501 tardy: true,
502 timeout: '1000',
503 skip: "${ parameters.phase === 'task' }",
504 },
505 ],
506 },
507 },
508 {
509 type: 'lab.html.Screen',
510 files: {},
511 parameters: {},
512 responses: {
513 'keypress(Space)': 'end',
514 },
515 messageHandlers: {},
516 title: 'End',
517 content:
518 '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \u003Ch1\u003E\n Thank you!\n \u003C\u002Fh1\u003E\n \u003Ch1\u003E\n Press the space bar to finish the task.\n \u003C\u002Fh1\u003E\n\u003C\u002Fmain\u003E\n\n',
519 },
520 ],
521 },
522 ],
523};
524
525// export
526export default experimentObject;