@jspsych/plugin-tobii-calibration 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.js ADDED
@@ -0,0 +1,837 @@
1
+ import { ParameterType } from 'jspsych';
2
+
3
+ var version = "0.1.1";
4
+
5
+ class CalibrationDisplay {
6
+ constructor(displayElement, params) {
7
+ this.displayElement = displayElement;
8
+ this.params = params;
9
+ this.currentPoint = null;
10
+ this.progressElement = null;
11
+ this.currentX = 0.5;
12
+ this.currentY = 0.5;
13
+ this.container = this.createContainer();
14
+ this.displayElement.appendChild(this.container);
15
+ if (params.show_progress) {
16
+ this.progressElement = this.createProgressIndicator();
17
+ this.displayElement.appendChild(this.progressElement);
18
+ }
19
+ }
20
+ /**
21
+ * Create container element
22
+ */
23
+ createContainer() {
24
+ const container = document.createElement("div");
25
+ container.className = "tobii-calibration-container";
26
+ return container;
27
+ }
28
+ /**
29
+ * Create progress indicator
30
+ */
31
+ createProgressIndicator() {
32
+ const progress = document.createElement("div");
33
+ progress.className = "tobii-calibration-progress";
34
+ progress.setAttribute("role", "status");
35
+ progress.setAttribute("aria-live", "polite");
36
+ return progress;
37
+ }
38
+ /**
39
+ * Show instructions
40
+ */
41
+ async showInstructions() {
42
+ const wrapper = document.createElement("div");
43
+ wrapper.className = "tobii-calibration-instructions";
44
+ wrapper.setAttribute("role", "dialog");
45
+ wrapper.setAttribute("aria-label", "Eye tracker calibration instructions");
46
+ const content = document.createElement("div");
47
+ content.className = "instructions-content";
48
+ const heading = document.createElement("h2");
49
+ heading.textContent = "Eye Tracker Calibration";
50
+ content.appendChild(heading);
51
+ const paragraph = document.createElement("p");
52
+ paragraph.innerHTML = this.params.instructions || "Look at each point and follow the instructions.";
53
+ content.appendChild(paragraph);
54
+ if (this.params.calibration_mode === "click") {
55
+ const button = document.createElement("button");
56
+ button.className = "calibration-start-btn";
57
+ button.textContent = this.params.button_text || "Start Calibration";
58
+ content.appendChild(button);
59
+ } else {
60
+ const autoMsg = document.createElement("p");
61
+ autoMsg.textContent = "Starting in a moment...";
62
+ content.appendChild(autoMsg);
63
+ }
64
+ wrapper.appendChild(content);
65
+ this.container.appendChild(wrapper);
66
+ if (this.params.calibration_mode === "click") {
67
+ return new Promise((resolve) => {
68
+ const button = wrapper.querySelector("button");
69
+ button?.addEventListener("click", () => {
70
+ wrapper.remove();
71
+ resolve();
72
+ });
73
+ });
74
+ } else {
75
+ await this.delay(this.params.instruction_display_duration || 3e3);
76
+ wrapper.remove();
77
+ }
78
+ }
79
+ /**
80
+ * Initialize the traveling point at screen center
81
+ */
82
+ async initializePoint() {
83
+ if (this.currentPoint)
84
+ return;
85
+ this.currentPoint = document.createElement("div");
86
+ this.currentPoint.className = "tobii-calibration-point";
87
+ this.currentPoint.setAttribute("role", "img");
88
+ this.currentPoint.setAttribute("aria-label", "Calibration target point");
89
+ const x = 0.5 * window.innerWidth;
90
+ const y = 0.5 * window.innerHeight;
91
+ this.currentX = 0.5;
92
+ this.currentY = 0.5;
93
+ Object.assign(this.currentPoint.style, {
94
+ left: `${x}px`,
95
+ top: `${y}px`,
96
+ width: `${this.params.point_size || 20}px`,
97
+ height: `${this.params.point_size || 20}px`,
98
+ backgroundColor: this.params.point_color || "#ff0000",
99
+ transition: "none"
100
+ });
101
+ this.container.appendChild(this.currentPoint);
102
+ await this.delay(this.params.zoom_duration || 300);
103
+ }
104
+ /**
105
+ * Travel to the next point location with smooth animation
106
+ */
107
+ async travelToPoint(point, index, total) {
108
+ if (!this.currentPoint) {
109
+ await this.initializePoint();
110
+ }
111
+ if (this.progressElement) {
112
+ this.progressElement.textContent = `Point ${index + 1} of ${total}`;
113
+ }
114
+ this.currentPoint.setAttribute("aria-label", `Calibration target point ${index + 1} of ${total}`);
115
+ const dx = point.x - this.currentX;
116
+ const dy = point.y - this.currentY;
117
+ const distance = Math.sqrt(dx * dx + dy * dy);
118
+ const travelDuration = Math.max(150, Math.min(400, 150 + distance * 200));
119
+ const x = point.x * window.innerWidth;
120
+ const y = point.y * window.innerHeight;
121
+ this.currentPoint.style.transition = `left ${travelDuration}ms ease-in-out, top ${travelDuration}ms ease-in-out`;
122
+ this.currentPoint.classList.remove("animation-explosion", "animation-shrink", "animation-pulse", "animation-zoom-out", "animation-zoom-in");
123
+ this.currentPoint.style.left = `${x}px`;
124
+ this.currentPoint.style.top = `${y}px`;
125
+ this.currentX = point.x;
126
+ this.currentY = point.y;
127
+ await this.delay(travelDuration);
128
+ }
129
+ /**
130
+ * Play zoom out animation (point grows larger)
131
+ */
132
+ async playZoomOut() {
133
+ if (!this.currentPoint)
134
+ return;
135
+ this.currentPoint.style.transition = "none";
136
+ this.currentPoint.classList.remove("animation-shrink", "animation-pulse", "animation-explosion", "animation-zoom-in");
137
+ this.currentPoint.classList.add("animation-zoom-out");
138
+ await this.delay(this.params.zoom_duration || 300);
139
+ }
140
+ /**
141
+ * Play zoom in animation (point shrinks to fixation size)
142
+ */
143
+ async playZoomIn() {
144
+ if (!this.currentPoint)
145
+ return;
146
+ this.currentPoint.classList.remove("animation-zoom-out");
147
+ this.currentPoint.classList.add("animation-zoom-in");
148
+ await this.delay(this.params.zoom_duration || 300);
149
+ }
150
+ /**
151
+ * Play explosion animation on the current point
152
+ */
153
+ async playExplosion(success) {
154
+ if (!this.currentPoint)
155
+ return;
156
+ this.currentPoint.classList.remove("animation-pulse", "animation-shrink");
157
+ this.currentPoint.classList.add("animation-explosion");
158
+ if (success) {
159
+ this.currentPoint.style.backgroundColor = "#4ade80";
160
+ } else {
161
+ this.currentPoint.style.backgroundColor = "#f87171";
162
+ }
163
+ await this.delay(this.params.explosion_duration || 400);
164
+ }
165
+ /**
166
+ * Hide current calibration point (removes element)
167
+ */
168
+ async hidePoint() {
169
+ if (this.currentPoint) {
170
+ this.currentPoint.remove();
171
+ this.currentPoint = null;
172
+ }
173
+ await this.delay(200);
174
+ }
175
+ /**
176
+ * Reset point state after explosion (keeps element for continued travel)
177
+ */
178
+ async resetPointAfterExplosion() {
179
+ if (!this.currentPoint)
180
+ return;
181
+ this.currentPoint.classList.remove("animation-explosion");
182
+ this.currentPoint.style.transform = "translate(-50%, -50%) scale(1)";
183
+ this.currentPoint.style.opacity = "1";
184
+ this.currentPoint.style.backgroundColor = this.params.point_color || "#ff0000";
185
+ await this.delay(50);
186
+ }
187
+ /**
188
+ * Wait for user click (in click mode)
189
+ */
190
+ waitForClick() {
191
+ return new Promise((resolve) => {
192
+ const handleClick = () => {
193
+ document.removeEventListener("click", handleClick);
194
+ resolve();
195
+ };
196
+ document.addEventListener("click", handleClick);
197
+ });
198
+ }
199
+ /**
200
+ * Show calibration result
201
+ * @param success Whether calibration succeeded
202
+ * @param canRetry Whether a retry button should be shown on failure
203
+ * @returns 'retry' if user chose to retry, 'continue' otherwise
204
+ */
205
+ async showResult(success, canRetry = false) {
206
+ const result = document.createElement("div");
207
+ result.className = "tobii-calibration-result";
208
+ result.setAttribute("role", "alert");
209
+ result.setAttribute("aria-live", "assertive");
210
+ if (success) {
211
+ result.innerHTML = `
212
+ <div class="tobii-calibration-result-content success">
213
+ <h2>Calibration Successful!</h2>
214
+ <p>Continuing automatically...</p>
215
+ </div>
216
+ `;
217
+ this.container.appendChild(result);
218
+ await this.delay(this.params.success_display_duration || 2e3);
219
+ result.remove();
220
+ return "continue";
221
+ }
222
+ const buttonsHTML = canRetry ? `<button class="calibration-retry-btn">Retry</button>
223
+ <button class="calibration-continue-btn" style="margin-left: 10px;">Continue</button>` : `<button class="calibration-continue-btn">Continue</button>`;
224
+ result.innerHTML = `
225
+ <div class="tobii-calibration-result-content error">
226
+ <h2>Calibration Failed</h2>
227
+ <p>Please try again or continue.</p>
228
+ ${buttonsHTML}
229
+ </div>
230
+ `;
231
+ this.container.appendChild(result);
232
+ return new Promise((resolve) => {
233
+ const retryBtn = result.querySelector(".calibration-retry-btn");
234
+ const continueBtn = result.querySelector(".calibration-continue-btn");
235
+ retryBtn?.addEventListener("click", () => {
236
+ result.remove();
237
+ resolve("retry");
238
+ });
239
+ continueBtn?.addEventListener("click", () => {
240
+ result.remove();
241
+ resolve("continue");
242
+ });
243
+ });
244
+ }
245
+ /**
246
+ * Reset display state for a retry attempt
247
+ */
248
+ resetForRetry() {
249
+ this.container.innerHTML = "";
250
+ this.currentPoint = null;
251
+ this.currentX = 0.5;
252
+ this.currentY = 0.5;
253
+ }
254
+ /**
255
+ * Clear display
256
+ */
257
+ clear() {
258
+ this.container.innerHTML = "";
259
+ if (this.progressElement) {
260
+ this.progressElement.textContent = "";
261
+ }
262
+ }
263
+ /**
264
+ * Delay helper
265
+ */
266
+ delay(ms) {
267
+ return new Promise((resolve) => setTimeout(resolve, ms));
268
+ }
269
+ }
270
+
271
+ const info = {
272
+ name: "tobii-calibration",
273
+ version,
274
+ parameters: {
275
+ /** Number of calibration points (5 or 9) */
276
+ calibration_points: {
277
+ type: ParameterType.INT,
278
+ default: 9
279
+ },
280
+ /** Calibration mode: click or view */
281
+ calibration_mode: {
282
+ type: ParameterType.STRING,
283
+ default: "view"
284
+ },
285
+ /** Size of calibration points in pixels */
286
+ point_size: {
287
+ type: ParameterType.INT,
288
+ default: 20
289
+ },
290
+ /** Color of calibration points */
291
+ point_color: {
292
+ type: ParameterType.STRING,
293
+ default: "#ff0000"
294
+ },
295
+ /** Duration to show each point before data collection (ms) - allows user to fixate */
296
+ point_duration: {
297
+ type: ParameterType.INT,
298
+ default: 500
299
+ },
300
+ /** Show progress indicator */
301
+ show_progress: {
302
+ type: ParameterType.BOOL,
303
+ default: true
304
+ },
305
+ /** Custom calibration points */
306
+ custom_points: {
307
+ type: ParameterType.COMPLEX,
308
+ default: null
309
+ },
310
+ /** Instructions text */
311
+ instructions: {
312
+ type: ParameterType.STRING,
313
+ default: "Look at each point as it appears on the screen. Keep your gaze fixed on each point until it disappears."
314
+ },
315
+ /** Button text for click mode */
316
+ button_text: {
317
+ type: ParameterType.STRING,
318
+ default: "Start Calibration"
319
+ },
320
+ /** Background color of the calibration container */
321
+ background_color: {
322
+ type: ParameterType.STRING,
323
+ default: "#808080"
324
+ },
325
+ /** Primary button color */
326
+ button_color: {
327
+ type: ParameterType.STRING,
328
+ default: "#007bff"
329
+ },
330
+ /** Primary button hover color */
331
+ button_hover_color: {
332
+ type: ParameterType.STRING,
333
+ default: "#0056b3"
334
+ },
335
+ /** Retry button color */
336
+ retry_button_color: {
337
+ type: ParameterType.STRING,
338
+ default: "#dc3545"
339
+ },
340
+ /** Retry button hover color */
341
+ retry_button_hover_color: {
342
+ type: ParameterType.STRING,
343
+ default: "#c82333"
344
+ },
345
+ /** Success message color */
346
+ success_color: {
347
+ type: ParameterType.STRING,
348
+ default: "#28a745"
349
+ },
350
+ /** Error message color */
351
+ error_color: {
352
+ type: ParameterType.STRING,
353
+ default: "#dc3545"
354
+ },
355
+ /** Maximum number of retry attempts allowed on calibration failure */
356
+ max_retries: {
357
+ type: ParameterType.INT,
358
+ default: 1
359
+ },
360
+ /** Duration of zoom in/out animations in ms */
361
+ zoom_duration: {
362
+ type: ParameterType.INT,
363
+ default: 300
364
+ },
365
+ /** Duration of explosion animation in ms */
366
+ explosion_duration: {
367
+ type: ParameterType.INT,
368
+ default: 400
369
+ },
370
+ /** Duration to show success result before auto-advancing in ms */
371
+ success_display_duration: {
372
+ type: ParameterType.INT,
373
+ default: 2e3
374
+ },
375
+ /** Duration to show instructions before auto-advancing in view mode in ms */
376
+ instruction_display_duration: {
377
+ type: ParameterType.INT,
378
+ default: 3e3
379
+ }
380
+ },
381
+ data: {
382
+ /** Calibration success status */
383
+ calibration_success: {
384
+ type: ParameterType.BOOL
385
+ },
386
+ /** Number of calibration points used */
387
+ num_points: {
388
+ type: ParameterType.INT
389
+ },
390
+ /** Calibration mode used */
391
+ mode: {
392
+ type: ParameterType.STRING
393
+ },
394
+ /** Full calibration result data */
395
+ calibration_data: {
396
+ type: ParameterType.COMPLEX
397
+ },
398
+ /** Number of calibration attempts made */
399
+ num_attempts: {
400
+ type: ParameterType.INT
401
+ }
402
+ }
403
+ };
404
+ class TobiiCalibrationPlugin {
405
+ constructor(jsPsych) {
406
+ this.jsPsych = jsPsych;
407
+ }
408
+ static {
409
+ this.info = info;
410
+ }
411
+ static removeStyles() {
412
+ const el = document.getElementById("tobii-calibration-styles");
413
+ if (el) {
414
+ el.remove();
415
+ }
416
+ }
417
+ injectStyles(trial) {
418
+ TobiiCalibrationPlugin.removeStyles();
419
+ const css = `
420
+ .tobii-calibration-container {
421
+ position: fixed;
422
+ top: 0;
423
+ left: 0;
424
+ width: 100%;
425
+ height: 100%;
426
+ background-color: ${trial.background_color};
427
+ font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
428
+ z-index: 9999;
429
+ }
430
+
431
+ .tobii-calibration-instructions {
432
+ position: absolute;
433
+ top: 50%;
434
+ left: 50%;
435
+ transform: translate(-50%, -50%);
436
+ background-color: white;
437
+ padding: 40px;
438
+ border-radius: 10px;
439
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
440
+ text-align: center;
441
+ max-width: 600px;
442
+ }
443
+
444
+ .tobii-calibration-instructions h2 {
445
+ margin-top: 0;
446
+ margin-bottom: 20px;
447
+ font-size: 24px;
448
+ color: #333;
449
+ }
450
+
451
+ .tobii-calibration-instructions p {
452
+ margin-bottom: 20px;
453
+ font-size: 16px;
454
+ line-height: 1.5;
455
+ color: #666;
456
+ }
457
+
458
+ .calibration-start-btn,
459
+ .calibration-continue-btn,
460
+ .calibration-retry-btn {
461
+ background-color: ${trial.button_color};
462
+ color: white;
463
+ border: none;
464
+ padding: 12px 30px;
465
+ font-size: 16px;
466
+ border-radius: 5px;
467
+ cursor: pointer;
468
+ transition: background-color 0.3s;
469
+ }
470
+
471
+ .calibration-start-btn:hover,
472
+ .calibration-continue-btn:hover {
473
+ background-color: ${trial.button_hover_color};
474
+ }
475
+
476
+ .calibration-retry-btn {
477
+ background-color: ${trial.retry_button_color};
478
+ }
479
+
480
+ .calibration-retry-btn:hover {
481
+ background-color: ${trial.retry_button_hover_color};
482
+ }
483
+
484
+ .tobii-calibration-point {
485
+ position: absolute;
486
+ border-radius: 50%;
487
+ transform: translate(-50%, -50%);
488
+ cursor: pointer;
489
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
490
+ }
491
+
492
+ .tobii-calibration-point.animation-pulse {
493
+ animation: tobii-calibration-pulse 1s infinite;
494
+ }
495
+
496
+ @keyframes tobii-calibration-pulse {
497
+ 0%, 100% {
498
+ transform: translate(-50%, -50%) scale(1);
499
+ opacity: 1;
500
+ }
501
+ 50% {
502
+ transform: translate(-50%, -50%) scale(1.2);
503
+ opacity: 0.8;
504
+ }
505
+ }
506
+
507
+ .tobii-calibration-point.animation-shrink {
508
+ animation: tobii-calibration-shrink 1s ease-out;
509
+ }
510
+
511
+ @keyframes tobii-calibration-shrink {
512
+ 0% {
513
+ transform: translate(-50%, -50%) scale(3);
514
+ opacity: 0.5;
515
+ }
516
+ 100% {
517
+ transform: translate(-50%, -50%) scale(1);
518
+ opacity: 1;
519
+ }
520
+ }
521
+
522
+ .tobii-calibration-point.animation-explosion {
523
+ animation: tobii-calibration-explosion ${trial.explosion_duration / 1e3}s ease-out forwards;
524
+ }
525
+
526
+ @keyframes tobii-calibration-explosion {
527
+ 0% {
528
+ transform: translate(-50%, -50%) scale(1);
529
+ opacity: 1;
530
+ }
531
+ 50% {
532
+ transform: translate(-50%, -50%) scale(2);
533
+ opacity: 0.8;
534
+ }
535
+ 100% {
536
+ transform: translate(-50%, -50%) scale(0);
537
+ opacity: 0;
538
+ }
539
+ }
540
+
541
+ .tobii-calibration-point.animation-zoom-out {
542
+ animation: tobii-calibration-zoom-out ${trial.zoom_duration / 1e3}s ease-out forwards;
543
+ }
544
+
545
+ @keyframes tobii-calibration-zoom-out {
546
+ 0% {
547
+ transform: translate(-50%, -50%) scale(1);
548
+ }
549
+ 100% {
550
+ transform: translate(-50%, -50%) scale(2.5);
551
+ }
552
+ }
553
+
554
+ .tobii-calibration-point.animation-zoom-in {
555
+ animation: tobii-calibration-zoom-in ${trial.zoom_duration / 1e3}s ease-out forwards;
556
+ }
557
+
558
+ @keyframes tobii-calibration-zoom-in {
559
+ 0% {
560
+ transform: translate(-50%, -50%) scale(2.5);
561
+ }
562
+ 100% {
563
+ transform: translate(-50%, -50%) scale(1);
564
+ }
565
+ }
566
+
567
+ .tobii-calibration-progress {
568
+ position: fixed;
569
+ top: 20px;
570
+ left: 50%;
571
+ transform: translateX(-50%);
572
+ background-color: rgba(0, 0, 0, 0.7);
573
+ color: white;
574
+ padding: 10px 20px;
575
+ border-radius: 5px;
576
+ font-size: 14px;
577
+ z-index: 10000;
578
+ }
579
+
580
+ .tobii-calibration-result {
581
+ position: absolute;
582
+ top: 50%;
583
+ left: 50%;
584
+ transform: translate(-50%, -50%);
585
+ background-color: white;
586
+ padding: 40px;
587
+ border-radius: 10px;
588
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
589
+ text-align: center;
590
+ }
591
+
592
+ .tobii-calibration-result-content h2 {
593
+ margin-top: 0;
594
+ margin-bottom: 20px;
595
+ font-size: 24px;
596
+ }
597
+
598
+ .tobii-calibration-result-content.success h2 {
599
+ color: ${trial.success_color};
600
+ }
601
+
602
+ .tobii-calibration-result-content.error h2 {
603
+ color: ${trial.error_color};
604
+ }
605
+
606
+ .tobii-calibration-result-content p {
607
+ margin-bottom: 20px;
608
+ font-size: 16px;
609
+ color: #666;
610
+ }
611
+ `;
612
+ const styleElement = document.createElement("style");
613
+ styleElement.id = "tobii-calibration-styles";
614
+ styleElement.textContent = css;
615
+ document.head.appendChild(styleElement);
616
+ }
617
+ async trial(display_element, trial) {
618
+ this.injectStyles(trial);
619
+ const tobiiExt = this.jsPsych.extensions.tobii;
620
+ if (!tobiiExt) {
621
+ throw new Error("Tobii extension not initialized");
622
+ }
623
+ if (!tobiiExt.isConnected()) {
624
+ throw new Error("Not connected to Tobii server");
625
+ }
626
+ const calibrationDisplay = new CalibrationDisplay(
627
+ display_element,
628
+ trial
629
+ );
630
+ await calibrationDisplay.showInstructions();
631
+ let points;
632
+ if (trial.custom_points) {
633
+ points = this.validateCustomPoints(trial.custom_points);
634
+ } else {
635
+ points = this.getCalibrationPoints(trial.calibration_points);
636
+ }
637
+ const maxAttempts = 1 + trial.max_retries;
638
+ let attempt = 0;
639
+ let calibrationResult = { success: false };
640
+ try {
641
+ while (attempt < maxAttempts) {
642
+ attempt++;
643
+ const retriesRemaining = maxAttempts - attempt;
644
+ await tobiiExt.startCalibration();
645
+ await calibrationDisplay.initializePoint();
646
+ for (let i = 0; i < points.length; i++) {
647
+ const point = points[i];
648
+ await calibrationDisplay.travelToPoint(point, i, points.length);
649
+ await calibrationDisplay.playZoomOut();
650
+ await calibrationDisplay.playZoomIn();
651
+ if (trial.calibration_mode === "click") {
652
+ await calibrationDisplay.waitForClick();
653
+ } else {
654
+ await this.delay(trial.point_duration);
655
+ }
656
+ const result = await tobiiExt.collectCalibrationPoint(point.x, point.y);
657
+ await calibrationDisplay.playExplosion(result.success);
658
+ if (i < points.length - 1) {
659
+ await calibrationDisplay.resetPointAfterExplosion();
660
+ }
661
+ }
662
+ await calibrationDisplay.hidePoint();
663
+ calibrationResult = await tobiiExt.computeCalibration();
664
+ const userChoice = await calibrationDisplay.showResult(
665
+ calibrationResult.success,
666
+ retriesRemaining > 0
667
+ );
668
+ if (userChoice === "continue") {
669
+ break;
670
+ }
671
+ calibrationDisplay.resetForRetry();
672
+ }
673
+ } finally {
674
+ calibrationDisplay.clear();
675
+ display_element.innerHTML = "";
676
+ TobiiCalibrationPlugin.removeStyles();
677
+ }
678
+ const trial_data = {
679
+ calibration_success: calibrationResult.success,
680
+ num_points: points.length,
681
+ mode: trial.calibration_mode,
682
+ calibration_data: calibrationResult,
683
+ num_attempts: attempt
684
+ };
685
+ this.jsPsych.finishTrial(trial_data);
686
+ }
687
+ /**
688
+ * Validate custom calibration points
689
+ */
690
+ validateCustomPoints(points) {
691
+ if (!Array.isArray(points) || points.length === 0) {
692
+ throw new Error("custom_points must be a non-empty array");
693
+ }
694
+ const validated = [];
695
+ for (let i = 0; i < points.length; i++) {
696
+ const point = points[i];
697
+ if (typeof point !== "object" || point === null) {
698
+ throw new Error(`Invalid calibration point at index ${i}: must have numeric x and y`);
699
+ }
700
+ const p = point;
701
+ if (typeof p.x !== "number" || typeof p.y !== "number") {
702
+ throw new Error(`Invalid calibration point at index ${i}: must have numeric x and y`);
703
+ }
704
+ if (p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1) {
705
+ throw new Error(
706
+ `Calibration point at index ${i} out of range: x and y must be between 0 and 1`
707
+ );
708
+ }
709
+ validated.push({ x: p.x, y: p.y });
710
+ }
711
+ return validated;
712
+ }
713
+ /**
714
+ * Get standard calibration points for the given grid size
715
+ */
716
+ getCalibrationPoints(count) {
717
+ switch (count) {
718
+ case 5:
719
+ return [
720
+ { x: 0.1, y: 0.1 },
721
+ { x: 0.9, y: 0.1 },
722
+ { x: 0.5, y: 0.5 },
723
+ { x: 0.1, y: 0.9 },
724
+ { x: 0.9, y: 0.9 }
725
+ ];
726
+ case 9:
727
+ return [
728
+ { x: 0.1, y: 0.1 },
729
+ { x: 0.5, y: 0.1 },
730
+ { x: 0.9, y: 0.1 },
731
+ { x: 0.1, y: 0.5 },
732
+ { x: 0.5, y: 0.5 },
733
+ { x: 0.9, y: 0.5 },
734
+ { x: 0.1, y: 0.9 },
735
+ { x: 0.5, y: 0.9 },
736
+ { x: 0.9, y: 0.9 }
737
+ ];
738
+ case 13:
739
+ return [
740
+ { x: 0.1, y: 0.1 },
741
+ { x: 0.5, y: 0.1 },
742
+ { x: 0.9, y: 0.1 },
743
+ { x: 0.3, y: 0.3 },
744
+ { x: 0.7, y: 0.3 },
745
+ { x: 0.1, y: 0.5 },
746
+ { x: 0.5, y: 0.5 },
747
+ { x: 0.9, y: 0.5 },
748
+ { x: 0.3, y: 0.7 },
749
+ { x: 0.7, y: 0.7 },
750
+ { x: 0.1, y: 0.9 },
751
+ { x: 0.5, y: 0.9 },
752
+ { x: 0.9, y: 0.9 }
753
+ ];
754
+ case 15:
755
+ return [
756
+ { x: 0.1, y: 0.1 },
757
+ { x: 0.5, y: 0.1 },
758
+ { x: 0.9, y: 0.1 },
759
+ { x: 0.1, y: 0.3 },
760
+ { x: 0.5, y: 0.3 },
761
+ { x: 0.9, y: 0.3 },
762
+ { x: 0.1, y: 0.5 },
763
+ { x: 0.5, y: 0.5 },
764
+ { x: 0.9, y: 0.5 },
765
+ { x: 0.1, y: 0.7 },
766
+ { x: 0.5, y: 0.7 },
767
+ { x: 0.9, y: 0.7 },
768
+ { x: 0.1, y: 0.9 },
769
+ { x: 0.5, y: 0.9 },
770
+ { x: 0.9, y: 0.9 }
771
+ ];
772
+ case 19:
773
+ return [
774
+ { x: 0.1, y: 0.1 },
775
+ { x: 0.5, y: 0.1 },
776
+ { x: 0.9, y: 0.1 },
777
+ { x: 0.1, y: 0.3 },
778
+ { x: 0.3, y: 0.3 },
779
+ { x: 0.5, y: 0.3 },
780
+ { x: 0.7, y: 0.3 },
781
+ { x: 0.9, y: 0.3 },
782
+ { x: 0.1, y: 0.5 },
783
+ { x: 0.5, y: 0.5 },
784
+ { x: 0.9, y: 0.5 },
785
+ { x: 0.1, y: 0.7 },
786
+ { x: 0.3, y: 0.7 },
787
+ { x: 0.5, y: 0.7 },
788
+ { x: 0.7, y: 0.7 },
789
+ { x: 0.9, y: 0.7 },
790
+ { x: 0.1, y: 0.9 },
791
+ { x: 0.5, y: 0.9 },
792
+ { x: 0.9, y: 0.9 }
793
+ ];
794
+ case 25:
795
+ return [
796
+ { x: 0.1, y: 0.1 },
797
+ { x: 0.3, y: 0.1 },
798
+ { x: 0.5, y: 0.1 },
799
+ { x: 0.7, y: 0.1 },
800
+ { x: 0.9, y: 0.1 },
801
+ { x: 0.1, y: 0.3 },
802
+ { x: 0.3, y: 0.3 },
803
+ { x: 0.5, y: 0.3 },
804
+ { x: 0.7, y: 0.3 },
805
+ { x: 0.9, y: 0.3 },
806
+ { x: 0.1, y: 0.5 },
807
+ { x: 0.3, y: 0.5 },
808
+ { x: 0.5, y: 0.5 },
809
+ { x: 0.7, y: 0.5 },
810
+ { x: 0.9, y: 0.5 },
811
+ { x: 0.1, y: 0.7 },
812
+ { x: 0.3, y: 0.7 },
813
+ { x: 0.5, y: 0.7 },
814
+ { x: 0.7, y: 0.7 },
815
+ { x: 0.9, y: 0.7 },
816
+ { x: 0.1, y: 0.9 },
817
+ { x: 0.3, y: 0.9 },
818
+ { x: 0.5, y: 0.9 },
819
+ { x: 0.7, y: 0.9 },
820
+ { x: 0.9, y: 0.9 }
821
+ ];
822
+ default:
823
+ throw new Error(
824
+ `Unsupported calibration_points value: ${count}. Use 5, 9, 13, 15, 19, or 25, or provide custom_points.`
825
+ );
826
+ }
827
+ }
828
+ /**
829
+ * Delay helper
830
+ */
831
+ delay(ms) {
832
+ return new Promise((resolve) => setTimeout(resolve, ms));
833
+ }
834
+ }
835
+
836
+ export { TobiiCalibrationPlugin as default };
837
+ //# sourceMappingURL=index.js.map