@jspsych/plugin-tobii-user-position 0.1.1

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/index.cjs ADDED
@@ -0,0 +1,689 @@
1
+ 'use strict';
2
+
3
+ var jspsych = require('jspsych');
4
+
5
+ var version = "0.1.1";
6
+
7
+ class PositionDisplay {
8
+ constructor(container, options) {
9
+ this.BOX_WIDTH = 400;
10
+ this.BOX_HEIGHT = 300;
11
+ this.MIN_FACE_SCALE = 0.4;
12
+ this.MAX_FACE_SCALE = 1.6;
13
+ this.OPTIMAL_FACE_SCALE = 1;
14
+ this.container = container;
15
+ this.options = options;
16
+ this.createDisplay();
17
+ }
18
+ createDisplay() {
19
+ this.container.innerHTML = "";
20
+ this.messageElement = document.createElement("div");
21
+ this.messageElement.className = "tobii-user-position-message";
22
+ this.messageElement.textContent = this.options.message;
23
+ this.container.appendChild(this.messageElement);
24
+ const guideContainer = document.createElement("div");
25
+ guideContainer.className = "tobii-user-position-guide";
26
+ this.container.appendChild(guideContainer);
27
+ this.trackingBoxElement = document.createElement("div");
28
+ this.trackingBoxElement.className = "tobii-tracking-box";
29
+ this.trackingBoxElement.setAttribute("role", "img");
30
+ this.trackingBoxElement.setAttribute("aria-label", "Head position tracking display");
31
+ this.trackingBoxElement.style.cssText = `
32
+ position: relative;
33
+ width: ${this.BOX_WIDTH}px;
34
+ height: ${this.BOX_HEIGHT}px;
35
+ border: 3px solid #666;
36
+ border-radius: 12px;
37
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
38
+ overflow: hidden;
39
+ box-shadow: inset 0 0 30px rgba(0,0,0,0.5);
40
+ `;
41
+ guideContainer.appendChild(this.trackingBoxElement);
42
+ const optimalZone = document.createElement("div");
43
+ optimalZone.style.cssText = `
44
+ position: absolute;
45
+ top: 50%;
46
+ left: 50%;
47
+ transform: translate(-50%, -50%);
48
+ width: 60%;
49
+ height: 70%;
50
+ border: 2px dashed rgba(255,255,255,0.2);
51
+ border-radius: 8px;
52
+ `;
53
+ this.trackingBoxElement.appendChild(optimalZone);
54
+ this.faceOutlineElement = document.createElement("div");
55
+ this.faceOutlineElement.className = "tobii-face-outline";
56
+ this.faceOutlineElement.setAttribute("aria-hidden", "true");
57
+ this.faceOutlineElement.style.cssText = `
58
+ position: absolute;
59
+ top: 50%;
60
+ left: 50%;
61
+ transform: translate(-50%, -50%);
62
+ transition: all 0.1s ease-out;
63
+ `;
64
+ this.trackingBoxElement.appendChild(this.faceOutlineElement);
65
+ this.createFaceSVG();
66
+ if (this.options.showDistanceFeedback) {
67
+ this.distanceBarContainer = document.createElement("div");
68
+ this.distanceBarContainer.style.cssText = `
69
+ position: absolute;
70
+ right: -40px;
71
+ top: 10%;
72
+ width: 20px;
73
+ height: 80%;
74
+ background: rgba(0,0,0,0.3);
75
+ border-radius: 10px;
76
+ border: 2px solid #444;
77
+ overflow: hidden;
78
+ `;
79
+ guideContainer.appendChild(this.distanceBarContainer);
80
+ const closeLabel = document.createElement("div");
81
+ closeLabel.textContent = "Close";
82
+ closeLabel.style.cssText = `
83
+ position: absolute;
84
+ right: -70px;
85
+ top: 0;
86
+ font-size: 10px;
87
+ color: #888;
88
+ `;
89
+ guideContainer.appendChild(closeLabel);
90
+ const farLabel = document.createElement("div");
91
+ farLabel.textContent = "Far";
92
+ farLabel.style.cssText = `
93
+ position: absolute;
94
+ right: -55px;
95
+ bottom: 10%;
96
+ font-size: 10px;
97
+ color: #888;
98
+ `;
99
+ guideContainer.appendChild(farLabel);
100
+ this.distanceBarFill = document.createElement("div");
101
+ this.distanceBarFill.style.cssText = `
102
+ position: absolute;
103
+ bottom: 0;
104
+ left: 0;
105
+ width: 100%;
106
+ height: 50%;
107
+ background: ${this.options.goodColor};
108
+ border-radius: 8px;
109
+ transition: all 0.1s ease-out;
110
+ `;
111
+ this.distanceBarContainer.appendChild(this.distanceBarFill);
112
+ const optimalMarker = document.createElement("div");
113
+ optimalMarker.style.cssText = `
114
+ position: absolute;
115
+ left: -2px;
116
+ right: -2px;
117
+ top: 40%;
118
+ height: 20%;
119
+ border: 2px solid rgba(255,255,255,0.5);
120
+ border-radius: 4px;
121
+ pointer-events: none;
122
+ `;
123
+ this.distanceBarContainer.appendChild(optimalMarker);
124
+ }
125
+ this.feedbackElement = document.createElement("div");
126
+ this.feedbackElement.className = "tobii-position-feedback";
127
+ this.feedbackElement.setAttribute("role", "status");
128
+ this.feedbackElement.setAttribute("aria-live", "polite");
129
+ this.feedbackElement.style.cssText = `
130
+ margin-top: 20px;
131
+ font-size: 1.1em;
132
+ font-weight: 600;
133
+ text-align: center;
134
+ `;
135
+ this.container.appendChild(this.feedbackElement);
136
+ }
137
+ createFaceSVG() {
138
+ const baseWidth = 120;
139
+ const baseHeight = 150;
140
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
141
+ svg.setAttribute("width", `${baseWidth}`);
142
+ svg.setAttribute("height", `${baseHeight}`);
143
+ svg.setAttribute("viewBox", `0 0 ${baseWidth} ${baseHeight}`);
144
+ svg.style.cssText = "overflow: visible;";
145
+ const faceOutline = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
146
+ faceOutline.setAttribute("cx", "60");
147
+ faceOutline.setAttribute("cy", "80");
148
+ faceOutline.setAttribute("rx", "50");
149
+ faceOutline.setAttribute("ry", "65");
150
+ faceOutline.setAttribute("fill", "none");
151
+ faceOutline.setAttribute("stroke", "#888");
152
+ faceOutline.setAttribute("stroke-width", "3");
153
+ faceOutline.setAttribute("stroke-dasharray", "8,4");
154
+ svg.appendChild(faceOutline);
155
+ this.leftEyeElement = document.createElementNS("http://www.w3.org/2000/svg", "circle");
156
+ this.leftEyeElement.setAttribute("cx", "40");
157
+ this.leftEyeElement.setAttribute("cy", "65");
158
+ this.leftEyeElement.setAttribute("r", "12");
159
+ this.leftEyeElement.setAttribute("fill", this.options.poorColor);
160
+ this.leftEyeElement.setAttribute("stroke", "#fff");
161
+ this.leftEyeElement.setAttribute("stroke-width", "2");
162
+ svg.appendChild(this.leftEyeElement);
163
+ this.rightEyeElement = document.createElementNS("http://www.w3.org/2000/svg", "circle");
164
+ this.rightEyeElement.setAttribute("cx", "80");
165
+ this.rightEyeElement.setAttribute("cy", "65");
166
+ this.rightEyeElement.setAttribute("r", "12");
167
+ this.rightEyeElement.setAttribute("fill", this.options.poorColor);
168
+ this.rightEyeElement.setAttribute("stroke", "#fff");
169
+ this.rightEyeElement.setAttribute("stroke-width", "2");
170
+ svg.appendChild(this.rightEyeElement);
171
+ const nose = document.createElementNS("http://www.w3.org/2000/svg", "path");
172
+ nose.setAttribute("d", "M60,75 L55,95 L65,95 Z");
173
+ nose.setAttribute("fill", "none");
174
+ nose.setAttribute("stroke", "#666");
175
+ nose.setAttribute("stroke-width", "2");
176
+ svg.appendChild(nose);
177
+ this.faceOutlineElement.appendChild(svg);
178
+ }
179
+ /**
180
+ * Update the display with new position data
181
+ */
182
+ updatePosition(positionData) {
183
+ if (!positionData) {
184
+ this.showNoData();
185
+ return;
186
+ }
187
+ const avgX = this.getAveragePosition(positionData.leftX, positionData.rightX, positionData.leftValid, positionData.rightValid);
188
+ const avgY = this.getAveragePosition(positionData.leftY, positionData.rightY, positionData.leftValid, positionData.rightValid);
189
+ const avgZ = this.getAveragePosition(positionData.leftZ, positionData.rightZ, positionData.leftValid, positionData.rightValid);
190
+ this.updateFaceDisplay(avgX, avgY, avgZ, positionData);
191
+ if (this.options.showDistanceFeedback && avgZ !== null) {
192
+ this.updateDistanceBar(avgZ);
193
+ }
194
+ this.updateEyeIndicators(positionData);
195
+ this.updateTextualFeedback(avgX, avgY, avgZ);
196
+ }
197
+ getAveragePosition(left, right, leftValid, rightValid) {
198
+ if (leftValid && rightValid && left !== null && right !== null) {
199
+ return (left + right) / 2;
200
+ } else if (leftValid && left !== null) {
201
+ return left;
202
+ } else if (rightValid && right !== null) {
203
+ return right;
204
+ }
205
+ return null;
206
+ }
207
+ updateFaceDisplay(x, y, z, _positionData) {
208
+ if (x === null || y === null) {
209
+ this.faceOutlineElement.style.opacity = "0.3";
210
+ return;
211
+ }
212
+ this.faceOutlineElement.style.opacity = "1";
213
+ const offsetX = (x - 0.5) * this.BOX_WIDTH * 0.8;
214
+ const offsetY = (0.5 - y) * this.BOX_HEIGHT * 0.8;
215
+ let scale = this.OPTIMAL_FACE_SCALE;
216
+ if (z !== null) {
217
+ scale = this.MIN_FACE_SCALE + z * (this.MAX_FACE_SCALE - this.MIN_FACE_SCALE);
218
+ scale = Math.max(this.MIN_FACE_SCALE, Math.min(this.MAX_FACE_SCALE, scale));
219
+ }
220
+ this.faceOutlineElement.style.transform = `translate(calc(-50% + ${offsetX}px), calc(-50% + ${offsetY}px)) scale(${scale})`;
221
+ }
222
+ updateDistanceBar(z) {
223
+ if (!this.distanceBarFill)
224
+ return;
225
+ const fillPercent = z * 100;
226
+ this.distanceBarFill.style.height = `${fillPercent}%`;
227
+ const goodMin = 0.5 - this.options.distanceThresholdGood;
228
+ const goodMax = 0.5 + this.options.distanceThresholdGood;
229
+ const fairMin = 0.5 - this.options.distanceThresholdFair;
230
+ const fairMax = 0.5 + this.options.distanceThresholdFair;
231
+ let color;
232
+ if (z >= goodMin && z <= goodMax) {
233
+ color = this.options.goodColor;
234
+ } else if (z >= fairMin && z <= fairMax) {
235
+ color = this.options.fairColor;
236
+ } else {
237
+ color = this.options.poorColor;
238
+ }
239
+ this.distanceBarFill.style.background = color;
240
+ }
241
+ updateEyeIndicators(positionData) {
242
+ if (this.leftEyeElement) {
243
+ const leftColor = positionData.leftValid ? this.options.goodColor : this.options.poorColor;
244
+ this.leftEyeElement.setAttribute("fill", leftColor);
245
+ }
246
+ if (this.rightEyeElement) {
247
+ const rightColor = positionData.rightValid ? this.options.goodColor : this.options.poorColor;
248
+ this.rightEyeElement.setAttribute("fill", rightColor);
249
+ }
250
+ }
251
+ updateTextualFeedback(x, y, z) {
252
+ if (!this.feedbackElement)
253
+ return;
254
+ if (x === null || y === null || z === null) {
255
+ this.feedbackElement.textContent = "Eyes not detected - please position yourself in front of the tracker";
256
+ this.feedbackElement.style.color = this.options.poorColor;
257
+ return;
258
+ }
259
+ const quality = this.assessPositionQuality(x, y, z);
260
+ let feedback;
261
+ let color;
262
+ if (quality.isGoodPosition) {
263
+ feedback = "\u2713 Position is good";
264
+ color = this.options.goodColor;
265
+ } else {
266
+ const issues = [];
267
+ const posFairThreshold = this.options.positionThresholdFair;
268
+ if (x < 0.5 - posFairThreshold)
269
+ issues.push("move right");
270
+ else if (x > 0.5 + posFairThreshold)
271
+ issues.push("move left");
272
+ if (y < 0.5 - posFairThreshold)
273
+ issues.push("move up");
274
+ else if (y > 0.5 + posFairThreshold)
275
+ issues.push("move down");
276
+ const distFairThreshold = this.options.distanceThresholdFair;
277
+ if (z > 0.5 + distFairThreshold)
278
+ issues.push("move back");
279
+ else if (z < 0.5 - distFairThreshold)
280
+ issues.push("move closer");
281
+ if (issues.length > 0) {
282
+ feedback = `Please ${issues.join(" and ")}`;
283
+ } else {
284
+ feedback = "Position: Almost there...";
285
+ }
286
+ color = quality.distanceStatus === "poor" || quality.horizontalStatus === "poor" || quality.verticalStatus === "poor" ? this.options.poorColor : this.options.fairColor;
287
+ }
288
+ this.feedbackElement.textContent = feedback;
289
+ this.feedbackElement.style.color = color;
290
+ }
291
+ assessPositionQuality(x, y, z) {
292
+ const xOffset = Math.abs(x - 0.5);
293
+ let horizontalStatus;
294
+ if (xOffset < this.options.positionThresholdGood)
295
+ horizontalStatus = "good";
296
+ else if (xOffset < this.options.positionThresholdFair)
297
+ horizontalStatus = "fair";
298
+ else
299
+ horizontalStatus = "poor";
300
+ const yOffset = Math.abs(y - 0.5);
301
+ let verticalStatus;
302
+ if (yOffset < this.options.positionThresholdGood)
303
+ verticalStatus = "good";
304
+ else if (yOffset < this.options.positionThresholdFair)
305
+ verticalStatus = "fair";
306
+ else
307
+ verticalStatus = "poor";
308
+ const zOffset = Math.abs(z - 0.5);
309
+ let distanceStatus;
310
+ if (zOffset < this.options.distanceThresholdGood)
311
+ distanceStatus = "good";
312
+ else if (zOffset < this.options.distanceThresholdFair)
313
+ distanceStatus = "fair";
314
+ else
315
+ distanceStatus = "poor";
316
+ const isGoodPosition = horizontalStatus === "good" && verticalStatus === "good" && distanceStatus === "good";
317
+ return {
318
+ isGoodPosition,
319
+ horizontalStatus,
320
+ verticalStatus,
321
+ distanceStatus,
322
+ averageX: x,
323
+ averageY: y,
324
+ averageZ: z
325
+ };
326
+ }
327
+ showNoData() {
328
+ if (this.feedbackElement) {
329
+ this.feedbackElement.textContent = "Waiting for tracker data...";
330
+ this.feedbackElement.style.color = this.options.poorColor;
331
+ }
332
+ this.faceOutlineElement.style.opacity = "0.3";
333
+ }
334
+ /**
335
+ * Get current position quality
336
+ */
337
+ getCurrentQuality(positionData) {
338
+ if (!positionData) {
339
+ return {
340
+ isGoodPosition: false,
341
+ horizontalStatus: "poor",
342
+ verticalStatus: "poor",
343
+ distanceStatus: "poor",
344
+ averageX: null,
345
+ averageY: null,
346
+ averageZ: null
347
+ };
348
+ }
349
+ const avgX = this.getAveragePosition(positionData.leftX, positionData.rightX, positionData.leftValid, positionData.rightValid);
350
+ const avgY = this.getAveragePosition(positionData.leftY, positionData.rightY, positionData.leftValid, positionData.rightValid);
351
+ const avgZ = this.getAveragePosition(positionData.leftZ, positionData.rightZ, positionData.leftValid, positionData.rightValid);
352
+ if (avgX === null || avgY === null || avgZ === null) {
353
+ return {
354
+ isGoodPosition: false,
355
+ horizontalStatus: "poor",
356
+ verticalStatus: "poor",
357
+ distanceStatus: "poor",
358
+ averageX: avgX,
359
+ averageY: avgY,
360
+ averageZ: avgZ
361
+ };
362
+ }
363
+ return this.assessPositionQuality(avgX, avgY, avgZ);
364
+ }
365
+ /**
366
+ * Remove the display
367
+ */
368
+ destroy() {
369
+ this.container.innerHTML = "";
370
+ }
371
+ }
372
+
373
+ const info = {
374
+ name: "tobii-user-position",
375
+ version,
376
+ parameters: {
377
+ /** Duration to show the position guide (ms), null for manual */
378
+ duration: {
379
+ type: jspsych.ParameterType.INT,
380
+ default: null
381
+ },
382
+ /** Message to display */
383
+ message: {
384
+ type: jspsych.ParameterType.STRING,
385
+ default: "Please position yourself so the indicators are green"
386
+ },
387
+ /** Update interval (ms) */
388
+ update_interval: {
389
+ type: jspsych.ParameterType.INT,
390
+ default: 100
391
+ },
392
+ /** Show distance feedback */
393
+ show_distance_feedback: {
394
+ type: jspsych.ParameterType.BOOL,
395
+ default: true
396
+ },
397
+ /** Show position feedback */
398
+ show_position_feedback: {
399
+ type: jspsych.ParameterType.BOOL,
400
+ default: true
401
+ },
402
+ /** Button text for manual continuation */
403
+ button_text: {
404
+ type: jspsych.ParameterType.STRING,
405
+ default: "Continue"
406
+ },
407
+ /** Only show button when position is good */
408
+ require_good_position: {
409
+ type: jspsych.ParameterType.BOOL,
410
+ default: false
411
+ },
412
+ /** Background color */
413
+ background_color: {
414
+ type: jspsych.ParameterType.STRING,
415
+ default: "#f0f0f0"
416
+ },
417
+ /** Good position color */
418
+ good_color: {
419
+ type: jspsych.ParameterType.STRING,
420
+ default: "#28a745"
421
+ },
422
+ /** Fair position color */
423
+ fair_color: {
424
+ type: jspsych.ParameterType.STRING,
425
+ default: "#ffc107"
426
+ },
427
+ /** Poor position color */
428
+ poor_color: {
429
+ type: jspsych.ParameterType.STRING,
430
+ default: "#dc3545"
431
+ },
432
+ /** Button color */
433
+ button_color: {
434
+ type: jspsych.ParameterType.STRING,
435
+ default: "#007bff"
436
+ },
437
+ /** Button hover color */
438
+ button_hover_color: {
439
+ type: jspsych.ParameterType.STRING,
440
+ default: "#0056b3"
441
+ },
442
+ /** Font size */
443
+ font_size: {
444
+ type: jspsych.ParameterType.STRING,
445
+ default: "18px"
446
+ },
447
+ /** Position offset threshold for "good" status (normalized, default 0.15) */
448
+ position_threshold_good: {
449
+ type: jspsych.ParameterType.FLOAT,
450
+ default: 0.15
451
+ },
452
+ /** Position offset threshold for "fair" status (normalized, default 0.25) */
453
+ position_threshold_fair: {
454
+ type: jspsych.ParameterType.FLOAT,
455
+ default: 0.25
456
+ },
457
+ /** Distance offset threshold for "good" status (normalized, default 0.1) */
458
+ distance_threshold_good: {
459
+ type: jspsych.ParameterType.FLOAT,
460
+ default: 0.1
461
+ },
462
+ /** Distance offset threshold for "fair" status (normalized, default 0.2) */
463
+ distance_threshold_fair: {
464
+ type: jspsych.ParameterType.FLOAT,
465
+ default: 0.2
466
+ }
467
+ },
468
+ data: {
469
+ /** Average X position during trial */
470
+ average_x: {
471
+ type: jspsych.ParameterType.FLOAT
472
+ },
473
+ /** Average Y position during trial */
474
+ average_y: {
475
+ type: jspsych.ParameterType.FLOAT
476
+ },
477
+ /** Average Z position (distance) during trial */
478
+ average_z: {
479
+ type: jspsych.ParameterType.FLOAT
480
+ },
481
+ /** Whether position was good at end */
482
+ position_good: {
483
+ type: jspsych.ParameterType.BOOL
484
+ },
485
+ /** Horizontal position status */
486
+ horizontal_status: {
487
+ type: jspsych.ParameterType.STRING
488
+ },
489
+ /** Vertical position status */
490
+ vertical_status: {
491
+ type: jspsych.ParameterType.STRING
492
+ },
493
+ /** Distance status */
494
+ distance_status: {
495
+ type: jspsych.ParameterType.STRING
496
+ },
497
+ /** Duration of trial */
498
+ rt: {
499
+ type: jspsych.ParameterType.INT
500
+ }
501
+ }
502
+ };
503
+ class TobiiUserPositionPlugin {
504
+ constructor(jsPsych) {
505
+ this.jsPsych = jsPsych;
506
+ }
507
+ static {
508
+ this.info = info;
509
+ }
510
+ static removeStyles() {
511
+ const el = document.getElementById("tobii-user-position-styles");
512
+ if (el) {
513
+ el.remove();
514
+ }
515
+ }
516
+ injectStyles(trial) {
517
+ TobiiUserPositionPlugin.removeStyles();
518
+ const css = `
519
+ .tobii-user-position-container {
520
+ display: flex;
521
+ flex-direction: column;
522
+ align-items: center;
523
+ justify-content: center;
524
+ width: 100%;
525
+ height: 100vh;
526
+ font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
527
+ font-size: ${trial.font_size};
528
+ }
529
+
530
+ .tobii-user-position-message {
531
+ margin-bottom: 40px;
532
+ text-align: center;
533
+ font-weight: 500;
534
+ color: #333;
535
+ }
536
+
537
+ .tobii-user-position-guide {
538
+ position: relative;
539
+ margin-bottom: 40px;
540
+ }
541
+
542
+ .tobii-position-feedback {
543
+ text-align: center;
544
+ margin-bottom: 30px;
545
+ font-weight: 600;
546
+ font-size: 1.1em;
547
+ }
548
+
549
+ .tobii-user-position-button {
550
+ padding: 12px 32px;
551
+ font-size: 16px;
552
+ font-weight: 500;
553
+ border: none;
554
+ border-radius: 6px;
555
+ background-color: ${trial.button_color};
556
+ color: white;
557
+ cursor: pointer;
558
+ transition: all 0.2s ease;
559
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
560
+ }
561
+
562
+ .tobii-user-position-button:hover:not(:disabled) {
563
+ background-color: ${trial.button_hover_color};
564
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
565
+ transform: translateY(-1px);
566
+ }
567
+
568
+ .tobii-user-position-button:disabled {
569
+ background-color: #ccc;
570
+ cursor: not-allowed;
571
+ opacity: 0.6;
572
+ }
573
+
574
+ .tobii-center-marker {
575
+ position: absolute;
576
+ top: 50%;
577
+ left: 50%;
578
+ transform: translate(-50%, -50%);
579
+ width: 30px;
580
+ height: 30px;
581
+ border: 2px dashed #666;
582
+ border-radius: 50%;
583
+ opacity: 0.5;
584
+ }
585
+ `;
586
+ const styleElement = document.createElement("style");
587
+ styleElement.id = "tobii-user-position-styles";
588
+ styleElement.textContent = css;
589
+ document.head.appendChild(styleElement);
590
+ }
591
+ trial(display_element, trial) {
592
+ return new Promise((resolve) => {
593
+ this.injectStyles(trial);
594
+ const tobiiExtension = this.jsPsych.extensions.tobii;
595
+ if (!tobiiExtension) {
596
+ throw new Error("Tobii extension not loaded");
597
+ }
598
+ display_element.innerHTML = `
599
+ <div class="tobii-user-position-container">
600
+ </div>
601
+ `;
602
+ const container = display_element.querySelector(
603
+ ".tobii-user-position-container"
604
+ );
605
+ const positionDisplay = new PositionDisplay(container, {
606
+ message: trial.message,
607
+ showDistanceFeedback: trial.show_distance_feedback,
608
+ showPositionFeedback: trial.show_position_feedback,
609
+ backgroundColor: trial.background_color,
610
+ goodColor: trial.good_color,
611
+ fairColor: trial.fair_color,
612
+ poorColor: trial.poor_color,
613
+ fontSize: trial.font_size,
614
+ positionThresholdGood: trial.position_threshold_good,
615
+ positionThresholdFair: trial.position_threshold_fair,
616
+ distanceThresholdGood: trial.distance_threshold_good,
617
+ distanceThresholdFair: trial.distance_threshold_fair
618
+ });
619
+ let continueButton = null;
620
+ if (trial.duration === null) {
621
+ continueButton = document.createElement("button");
622
+ continueButton.className = "tobii-user-position-button";
623
+ continueButton.textContent = trial.button_text;
624
+ if (trial.require_good_position) {
625
+ continueButton.disabled = true;
626
+ }
627
+ container.appendChild(continueButton);
628
+ }
629
+ const positionSamples = [];
630
+ const startTime = performance.now();
631
+ const updateInterval = setInterval(async () => {
632
+ try {
633
+ const positionData = await tobiiExtension.getUserPosition();
634
+ positionDisplay.updatePosition(positionData);
635
+ const quality = positionDisplay.getCurrentQuality(positionData);
636
+ positionSamples.push(quality);
637
+ if (continueButton && trial.require_good_position) {
638
+ continueButton.disabled = !quality.isGoodPosition;
639
+ }
640
+ } catch (error) {
641
+ console.error("Error updating user position:", error);
642
+ }
643
+ }, trial.update_interval);
644
+ const cleanup = () => {
645
+ clearInterval(updateInterval);
646
+ positionDisplay.destroy();
647
+ display_element.innerHTML = "";
648
+ TobiiUserPositionPlugin.removeStyles();
649
+ };
650
+ const endTrial = () => {
651
+ const validSamples = positionSamples.filter(
652
+ (s) => s.averageX !== null && s.averageY !== null && s.averageZ !== null
653
+ );
654
+ let averageX = null;
655
+ let averageY = null;
656
+ let averageZ = null;
657
+ let finalQuality = null;
658
+ if (validSamples.length > 0) {
659
+ averageX = validSamples.reduce((sum, s) => sum + s.averageX, 0) / validSamples.length;
660
+ averageY = validSamples.reduce((sum, s) => sum + s.averageY, 0) / validSamples.length;
661
+ averageZ = validSamples.reduce((sum, s) => sum + s.averageZ, 0) / validSamples.length;
662
+ finalQuality = positionSamples[positionSamples.length - 1];
663
+ }
664
+ const trialData = {
665
+ average_x: averageX,
666
+ average_y: averageY,
667
+ average_z: averageZ,
668
+ position_good: finalQuality?.isGoodPosition ?? false,
669
+ horizontal_status: finalQuality?.horizontalStatus ?? "poor",
670
+ vertical_status: finalQuality?.verticalStatus ?? "poor",
671
+ distance_status: finalQuality?.distanceStatus ?? "poor",
672
+ rt: Math.round(performance.now() - startTime)
673
+ };
674
+ cleanup();
675
+ this.jsPsych.finishTrial(trialData);
676
+ resolve();
677
+ };
678
+ if (continueButton) {
679
+ continueButton.addEventListener("click", endTrial);
680
+ }
681
+ if (trial.duration != null) {
682
+ this.jsPsych.pluginAPI.setTimeout(endTrial, trial.duration);
683
+ }
684
+ });
685
+ }
686
+ }
687
+
688
+ module.exports = TobiiUserPositionPlugin;
689
+ //# sourceMappingURL=index.cjs.map