remote-calibrator 0.2.1 → 0.2.2-beta.0
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +16 -0
- package/README.md +2 -0
- package/homepage/example.js +11 -13
- package/lib/RemoteCalibrator.min.js +1 -1
- package/lib/RemoteCalibrator.min.js.LICENSE.txt +1 -1
- package/lib/RemoteCalibrator.min.js.map +1 -1
- package/package.json +8 -8
- package/src/WebGazer4RC/.gitattributes +10 -0
- package/src/WebGazer4RC/LICENSE.md +15 -0
- package/src/WebGazer4RC/README.md +142 -0
- package/src/WebGazer4RC/gnu-lgpl-v3.0.md +163 -0
- package/src/WebGazer4RC/gplv3.md +636 -0
- package/src/WebGazer4RC/package-lock.json +1133 -0
- package/src/WebGazer4RC/package.json +28 -0
- package/src/WebGazer4RC/src/dom_util.mjs +27 -0
- package/src/WebGazer4RC/src/facemesh.mjs +150 -0
- package/src/WebGazer4RC/src/index.mjs +1213 -0
- package/src/WebGazer4RC/src/mat.mjs +301 -0
- package/src/WebGazer4RC/src/params.mjs +29 -0
- package/src/WebGazer4RC/src/pupil.mjs +109 -0
- package/src/WebGazer4RC/src/ridgeReg.mjs +104 -0
- package/src/WebGazer4RC/src/ridgeRegThreaded.mjs +161 -0
- package/src/WebGazer4RC/src/ridgeWeightedReg.mjs +125 -0
- package/src/WebGazer4RC/src/ridgeWorker.mjs +135 -0
- package/src/WebGazer4RC/src/util.mjs +348 -0
- package/src/WebGazer4RC/src/util_regression.mjs +240 -0
- package/src/WebGazer4RC/src/worker_scripts/mat.js +306 -0
- package/src/WebGazer4RC/src/worker_scripts/util.js +398 -0
- package/src/WebGazer4RC/test/regression_test.js +182 -0
- package/src/WebGazer4RC/test/run_tests_and_server.sh +24 -0
- package/src/WebGazer4RC/test/util_test.js +60 -0
- package/src/WebGazer4RC/test/webgazerExtract_test.js +40 -0
- package/src/WebGazer4RC/test/webgazer_test.js +160 -0
- package/src/WebGazer4RC/test/www_page_test.js +41 -0
- package/src/components/onCanvas.js +1 -2
- package/src/core.js +18 -0
- package/src/css/distance.scss +1 -0
- package/src/distance/distance.js +4 -1
- package/src/distance/distanceTrack.js +2 -2
- package/src/panel/panel.js +14 -5
- package/src/text.json +4 -2
@@ -0,0 +1,40 @@
|
|
1
|
+
const puppeteer = require('puppeteer');
|
2
|
+
const { assert } = require('chai');
|
3
|
+
const TFFaceMesh = require('@tensorflow-models/facemesh');
|
4
|
+
|
5
|
+
describe('webgazerExtract functions', async () => {
|
6
|
+
let browser,page;
|
7
|
+
before(async () => {
|
8
|
+
browser = await puppeteer.launch()
|
9
|
+
page = await browser.newPage();
|
10
|
+
await page.goto('http://localhost:8000/webgazerExtractClient.html');
|
11
|
+
})
|
12
|
+
after(async () => {
|
13
|
+
await browser.close();
|
14
|
+
})
|
15
|
+
it('should load elements', async() =>{
|
16
|
+
const elements = await page.evaluate(() => {
|
17
|
+
return {myMouse:document.getElementById('myMouse'),
|
18
|
+
tobiiGP:document.getElementById('tobiiGP'),
|
19
|
+
wsCanvas: document.getElementById('wsCanvas'),
|
20
|
+
screencap: document.getElementById('screencap'),
|
21
|
+
showScreenCap: document.getElementById('showScreenCap'),
|
22
|
+
scTimeOffsetDiv: document.getElementById('scTimeOffsetDiv'),
|
23
|
+
diagDiv: document.getElementById('diagDiv')}
|
24
|
+
})
|
25
|
+
for(const [k,v] of Object.entries(elements)){
|
26
|
+
assert.equal((Object.keys(v).length === 0
|
27
|
+
&& v.constructor === Object), true)
|
28
|
+
}
|
29
|
+
});
|
30
|
+
|
31
|
+
it('webgazer properties should be set correctly', async() =>{
|
32
|
+
await page.waitForSelector('#overlay');
|
33
|
+
let model = await page.evaluate(async() => {
|
34
|
+
let tracker = webgazer.getTracker();
|
35
|
+
return tracker.name
|
36
|
+
})
|
37
|
+
assert.equal(model,'TFFaceMesh')
|
38
|
+
})
|
39
|
+
});
|
40
|
+
|
@@ -0,0 +1,160 @@
|
|
1
|
+
const puppeteer = require('puppeteer');
|
2
|
+
const { assert } = require('chai');
|
3
|
+
const TFFaceMesh = require('@tensorflow-models/facemesh');
|
4
|
+
|
5
|
+
before(async () => {
|
6
|
+
const parent_dir = __dirname.substring(0,__dirname.length-4)
|
7
|
+
let my_y4m_video = parent_dir + 'www/data/src/P_01/dot.y4m'
|
8
|
+
browser = await puppeteer.launch({args:['--use-file-for-fake-video-capture='+my_y4m_video,
|
9
|
+
'--allow-file-access', '--use-fake-device-for-media-stream','--use-fake-ui-for-media-stream',
|
10
|
+
'--no-sandbox','--disable-setuid-sandbox',
|
11
|
+
]
|
12
|
+
//,devtools:true //enable for debugging
|
13
|
+
});
|
14
|
+
page = await browser.newPage();
|
15
|
+
await page.goto('http://localhost:3000/calibration.html?');
|
16
|
+
page.coverage.startJSCoverage();
|
17
|
+
await page.goto('http://localhost:3000/calibration.html?');
|
18
|
+
await page.waitFor(1500)
|
19
|
+
await page.waitForSelector('#start_calibration')
|
20
|
+
//calibration button is not immediately clickable due to css transition
|
21
|
+
await page.waitFor(2500)
|
22
|
+
|
23
|
+
await page.evaluate(async() => {
|
24
|
+
document.querySelector("#start_calibration").click()
|
25
|
+
})
|
26
|
+
await page.waitFor(1500)
|
27
|
+
await page.evaluate(async() =>{
|
28
|
+
document.querySelector("body > div.swal-overlay.swal-overlay--show-modal > div > div.swal-footer > div > button").click()
|
29
|
+
})
|
30
|
+
})
|
31
|
+
describe('webgazer function', async() => {
|
32
|
+
after(async () => {
|
33
|
+
const jsCoverage = await page.coverage.stopJSCoverage();
|
34
|
+
let usedBytes = 0;
|
35
|
+
let webgazer_coverage;
|
36
|
+
jsCoverage.forEach(item => {if (item.url == "http://localhost:3000/webgazer.js")
|
37
|
+
{webgazer_coverage = item}
|
38
|
+
})
|
39
|
+
webgazer_coverage.ranges.forEach(range => (usedBytes += range.end - range.start - 1));
|
40
|
+
console.log((100*usedBytes/webgazer_coverage.text.length).toFixed(4), "% Code Coverage on webgazer.js")
|
41
|
+
await browser.close();
|
42
|
+
})
|
43
|
+
describe('top level functions', async() =>{
|
44
|
+
it('should be able to recognize video input', async() =>{
|
45
|
+
const videoAvailable = await page.evaluate(async() => {
|
46
|
+
return await webgazer.params.showFaceFeedbackBox;
|
47
|
+
});
|
48
|
+
const isReady = await page.evaluate(async() => {
|
49
|
+
return await webgazer.isReady()
|
50
|
+
});
|
51
|
+
assert.equal(videoAvailable,true);
|
52
|
+
assert.equal(isReady,true);
|
53
|
+
});
|
54
|
+
//modifying visibility params
|
55
|
+
it('webgazerVideoFeed should display', async() => {
|
56
|
+
let video_display = await page.evaluate(async() => {
|
57
|
+
return document.getElementById('webgazerVideoFeed').style.display
|
58
|
+
})
|
59
|
+
assert.notEqual(video_display,"none");
|
60
|
+
})
|
61
|
+
it('webgazerFaceFeedbackBox should display', async() => {
|
62
|
+
await page.waitForSelector('#webgazerFaceFeedbackBox')
|
63
|
+
let face_overlay = await page.evaluate(async() => {
|
64
|
+
return document.getElementById('webgazerFaceFeedbackBox').style.display
|
65
|
+
})
|
66
|
+
assert.notEqual(face_overlay,"none");
|
67
|
+
})
|
68
|
+
it('webgazerGazeDot should display', async() => {
|
69
|
+
let webgazer_gazedot = await page.evaluate(async() => {
|
70
|
+
return document.getElementById('webgazerGazeDot').style.display
|
71
|
+
})
|
72
|
+
assert.notEqual(webgazer_gazedot,"none");
|
73
|
+
})
|
74
|
+
it('faceoverlay should hide when showFaceOverlay is false', async() => {
|
75
|
+
face_overlay = await page.evaluate(async() => {
|
76
|
+
await webgazer.showFaceFeedbackBox(false)
|
77
|
+
return document.getElementById('webgazerFaceFeedbackBox').style.display
|
78
|
+
})
|
79
|
+
assert.equal(face_overlay,"none");
|
80
|
+
})
|
81
|
+
it('webgazerGazeDot should hide when showPredictionPoints is false', async() =>{
|
82
|
+
let webgazer_gazedot = await page.evaluate(async() => {
|
83
|
+
await webgazer.showPredictionPoints(false)
|
84
|
+
return document.getElementById('webgazerGazeDot').style.display
|
85
|
+
})
|
86
|
+
assert.equal(webgazer_gazedot,"none");
|
87
|
+
})
|
88
|
+
it('webgazerVideoFeed should hide when showVideo is false', async() => {
|
89
|
+
video_display = await page.evaluate(async() => {
|
90
|
+
await webgazer.showVideo(false)
|
91
|
+
return document.getElementById('webgazerVideoFeed').style.display
|
92
|
+
});
|
93
|
+
assert.equal(video_display,"none");
|
94
|
+
})
|
95
|
+
it('getVideoElementCanvas should exist and be a canvas element', async() => {
|
96
|
+
let video_element_canvas_type = await page.evaluate(async() => {
|
97
|
+
return await webgazer.getVideoElementCanvas().nodeName
|
98
|
+
})
|
99
|
+
assert.equal(video_element_canvas_type,'CANVAS')
|
100
|
+
})
|
101
|
+
it('preview to camera resolution ratio should be [0.5,0.5]', async() =>{
|
102
|
+
let preview_to_camera_resolution_ratio = await page.evaluate(async() => {
|
103
|
+
return await webgazer.getVideoPreviewToCameraResolutionRatio()
|
104
|
+
})
|
105
|
+
assert.equal(preview_to_camera_resolution_ratio[0],0.5)
|
106
|
+
assert.equal(preview_to_camera_resolution_ratio[1],0.5)
|
107
|
+
})
|
108
|
+
it('should be able to change video viewer size', async()=>{
|
109
|
+
const video_dimensions = await page.evaluate(async()=>{
|
110
|
+
return [webgazer.params.videoViewerWidth,webgazer.params.videoViewerHeight]
|
111
|
+
})
|
112
|
+
const new_dimensions = [video_dimensions[0],video_dimensions[1]]
|
113
|
+
const new_video_dimensions = await page.evaluate(async(new_dimensions)=>{
|
114
|
+
await webgazer.setVideoViewerSize(new_dimensions[0],new_dimensions[1])
|
115
|
+
return [webgazer.params.videoViewerWidth,webgazer.params.videoViewerHeight]
|
116
|
+
},new_dimensions)
|
117
|
+
assert.equal(new_video_dimensions[0],new_dimensions[0])
|
118
|
+
assert.equal(new_video_dimensions[1],new_dimensions[1])
|
119
|
+
})
|
120
|
+
it('top level, non-video no arguments webgazer functions should work', async() =>{
|
121
|
+
let basic_functions = await page.evaluate(async() => {
|
122
|
+
return {getCurrentPrediction: JSON.stringify(await webgazer.getCurrentPrediction()),
|
123
|
+
addMouseEventListeners: JSON.stringify(await webgazer.addMouseEventListeners()),
|
124
|
+
getStoredPoints:JSON.stringify(await webgazer.getStoredPoints()),
|
125
|
+
removeMouseEventListeners:JSON.stringify(await webgazer.removeMouseEventListeners()),
|
126
|
+
isReady:JSON.stringify(await webgazer.isReady()),
|
127
|
+
detectCompatibility:JSON.stringify(await webgazer.detectCompatibility()),
|
128
|
+
clearGazeListener:JSON.stringify(await webgazer.clearGazeListener()),
|
129
|
+
getRegression:JSON.stringify(await webgazer.getRegression()),
|
130
|
+
getStoredPoints:JSON.stringify(await webgazer.getStoredPoints()),
|
131
|
+
pause:JSON.stringify(await webgazer.pause())
|
132
|
+
}
|
133
|
+
})
|
134
|
+
|
135
|
+
|
136
|
+
for(const [k,v] of Object.entries(basic_functions)){
|
137
|
+
assert.notEqual(Object.keys(v),null)
|
138
|
+
assert.notEqual(Object.keys(v),{})
|
139
|
+
}
|
140
|
+
|
141
|
+
assert.equal(basic_functions.isReady,"true")
|
142
|
+
assert.equal(basic_functions.detectCompatibility,"true")
|
143
|
+
})
|
144
|
+
it('can record screen position, set tracker and regression and set static video', async() =>{
|
145
|
+
const screen_functions = page.evaluate(async() => {
|
146
|
+
return {setStaticVideo: await webgazer.setStaticVideo('../www/data/src/P_02/1491487691210_2_-study-dot_test_instructions.webm'),
|
147
|
+
setTracker: await webgazer.setTracker('TFFacemesh'),
|
148
|
+
setRegression: await webgazer.setRegression('ridge')}
|
149
|
+
})
|
150
|
+
for(const [k,v] of Object.entries(screen_functions)){
|
151
|
+
assert.notEqual(Object.keys(v),null)
|
152
|
+
assert.notEqual(Object.keys(v),{})
|
153
|
+
}
|
154
|
+
})
|
155
|
+
//checkEyesInValidationBox exists in code but the comment above says it's wrong and it returns nothing
|
156
|
+
})
|
157
|
+
require('./regression_test')
|
158
|
+
require('./util_test')
|
159
|
+
|
160
|
+
})
|
@@ -0,0 +1,41 @@
|
|
1
|
+
const puppeteer = require('puppeteer');
|
2
|
+
const { assert } = require('chai');
|
3
|
+
|
4
|
+
describe('Main Page Basics', async () => {
|
5
|
+
let browser,page,response;
|
6
|
+
before(async () => {
|
7
|
+
browser = await puppeteer.launch();
|
8
|
+
page = await browser.newPage();
|
9
|
+
response = await page.goto('http://localhost:3000');
|
10
|
+
})
|
11
|
+
|
12
|
+
after(async () => {
|
13
|
+
await browser.close();
|
14
|
+
})
|
15
|
+
|
16
|
+
it('Page response should be 200', async() =>{
|
17
|
+
assert.equal(response.status(),200)
|
18
|
+
})
|
19
|
+
it('clicking the collision button should send you to new page' , async() =>{
|
20
|
+
const collision_button = "#collision_button";
|
21
|
+
const [response] = await Promise.all([
|
22
|
+
page.waitForNavigation(),
|
23
|
+
page.click(collision_button),
|
24
|
+
]);
|
25
|
+
assert.equal(page.url(),'http://localhost:3000/collision.html?')
|
26
|
+
assert.equal(response.status(),200)
|
27
|
+
});
|
28
|
+
it('clicking the calibration button should send you to new page' , async() =>{
|
29
|
+
await page.goto('http://localhost:3000');
|
30
|
+
const calibration_button = "#calibration_button";
|
31
|
+
const [response] = await Promise.all([
|
32
|
+
page.waitForNavigation(),
|
33
|
+
page.click(calibration_button),
|
34
|
+
]);
|
35
|
+
assert.equal(page.url(),'http://localhost:3000/calibration.html?')
|
36
|
+
assert.equal(response.status(),200)
|
37
|
+
});
|
38
|
+
});
|
39
|
+
|
40
|
+
|
41
|
+
|
@@ -5,12 +5,11 @@ import { colorDarkRed } from '../constants'
|
|
5
5
|
|
6
6
|
// CROSS
|
7
7
|
const crossLW = 32 // Width of a line of the middle cross
|
8
|
-
const crossLH =
|
8
|
+
const crossLH = 3
|
9
9
|
export const _getCrossX = (eyeSide, tX) => {
|
10
10
|
return eyeSide === 'left' ? tX * 0.1 : tX * 0.9
|
11
11
|
}
|
12
12
|
export function _cross(ctx, cX, mY) {
|
13
|
-
// Draw a cross at the middle of the canvas
|
14
13
|
ctx.fillStyle = '#000'
|
15
14
|
ctx.fillRect(cX - (crossLW >> 1), mY - (crossLH >> 1), crossLW, crossLH)
|
16
15
|
ctx.fillRect(cX - (crossLH >> 1), mY - (crossLW >> 1), crossLH, crossLW)
|
package/src/core.js
CHANGED
@@ -533,6 +533,24 @@ RemoteCalibrator.prototype._constructFloatInstructionElement = function (
|
|
533
533
|
return (this._background.instructionElement = instP)
|
534
534
|
}
|
535
535
|
|
536
|
+
RemoteCalibrator.prototype._setFloatInstructionElementPos = function (side) {
|
537
|
+
const r = this.instructionElement.getBoundingClientRect()
|
538
|
+
if (side === 'left') {
|
539
|
+
this.instructionElement.style.left = '10%'
|
540
|
+
this.instructionElement.style.right = 'unset'
|
541
|
+
this.instructionElement.style.transform = `translate(${-r.width / 2}px, 0)`
|
542
|
+
} else if (side === 'right') {
|
543
|
+
this.instructionElement.style.right = '10%'
|
544
|
+
this.instructionElement.style.left = 'unset'
|
545
|
+
this.instructionElement.style.transform = `translate(${r.width / 2}px, 0)`
|
546
|
+
} else {
|
547
|
+
// Reset to center
|
548
|
+
this.instructionElement.style.left = '50%'
|
549
|
+
this.instructionElement.style.right = 'unset'
|
550
|
+
this.instructionElement.style.transform = 'translate(-50%, 0)'
|
551
|
+
}
|
552
|
+
}
|
553
|
+
|
536
554
|
RemoteCalibrator.prototype._removeFloatInstructionElement = function () {
|
537
555
|
if (this.instructionElement) {
|
538
556
|
this.background.removeChild(this.instructionElement)
|
package/src/css/distance.scss
CHANGED
package/src/distance/distance.js
CHANGED
@@ -39,7 +39,7 @@ export function blindSpotTest(RC, options, toTrackDistance = false, callback) {
|
|
39
39
|
RC.background.appendChild(blindSpotDiv)
|
40
40
|
RC._constructFloatInstructionElement(
|
41
41
|
'blind-spot-instruction',
|
42
|
-
`
|
42
|
+
`Close <span id="eye-side"></span> eye.`
|
43
43
|
)
|
44
44
|
|
45
45
|
// Get HTML elements
|
@@ -48,6 +48,7 @@ export function blindSpotTest(RC, options, toTrackDistance = false, callback) {
|
|
48
48
|
|
49
49
|
const eyeSideEle = document.getElementById('eye-side')
|
50
50
|
let eyeSide = (eyeSideEle.innerText = 'LEFT').toLocaleLowerCase()
|
51
|
+
RC._setFloatInstructionElementPos(eyeSide)
|
51
52
|
let crossX = _getCrossX(eyeSide, c.width)
|
52
53
|
|
53
54
|
let circleBounds
|
@@ -113,6 +114,8 @@ export function blindSpotTest(RC, options, toTrackDistance = false, callback) {
|
|
113
114
|
if (eyeSide === 'left')
|
114
115
|
eyeSide = (eyeSideEle.innerText = 'RIGHT').toLocaleLowerCase()
|
115
116
|
else eyeSide = (eyeSideEle.innerText = 'LEFT').toLocaleLowerCase()
|
117
|
+
RC._setFloatInstructionElementPos(eyeSide)
|
118
|
+
|
116
119
|
circleBounds = _getCircleBounds(eyeSide, crossX, c.width)
|
117
120
|
circleX = circleBounds[eyeSide === 'left' ? 0 : 1]
|
118
121
|
v = eyeSide === 'left' ? 1 : -1
|
@@ -138,8 +138,8 @@ RemoteCalibrator.prototype.trackDistance = function (
|
|
138
138
|
|
139
139
|
const startTrackingPupils = async (RC, beforeCallbackTrack, callbackTrack) => {
|
140
140
|
RC.gazeTracker.beginVideo({ pipWidthPx: trackingOptions.pipWidthPx }, () => {
|
141
|
-
beforeCallbackTrack()
|
142
141
|
RC._removeFloatInstructionElement()
|
142
|
+
beforeCallbackTrack()
|
143
143
|
_tracking(RC, trackingOptions, callbackTrack)
|
144
144
|
})
|
145
145
|
}
|
@@ -269,7 +269,7 @@ const _tracking = async (RC, trackingOptions, callbackTrack) => {
|
|
269
269
|
callbackTrack({
|
270
270
|
value: {
|
271
271
|
viewingDistanceCm: data.value,
|
272
|
-
nearPointCm: nPData ? nPData.value : null,
|
272
|
+
nearPointCm: nPData ? nPData.value : [null, null],
|
273
273
|
},
|
274
274
|
timestamp: timestamp,
|
275
275
|
method: 'Facemesh Predict',
|
package/src/panel/panel.js
CHANGED
@@ -88,9 +88,11 @@ RemoteCalibrator.prototype.panel = async function (
|
|
88
88
|
{
|
89
89
|
headline: text.panel.headline,
|
90
90
|
description: text.panel.description,
|
91
|
+
nextHeadline: text.panel.nextHeadline,
|
92
|
+
nextDescription: text.panel.nextDescription,
|
91
93
|
nextButton: text.panel.nextButton,
|
92
94
|
color: '#3490de',
|
93
|
-
_demoActivateAll: false, // !
|
95
|
+
_demoActivateAll: false, // ! Private
|
94
96
|
},
|
95
97
|
options
|
96
98
|
)
|
@@ -116,10 +118,8 @@ RemoteCalibrator.prototype.panel = async function (
|
|
116
118
|
|
117
119
|
const panel = document.createElement('div')
|
118
120
|
panel.className = panel.id = 'rc-panel'
|
119
|
-
panel.innerHTML = `<h1 class="rc-panel-title">${options.headline}</h1>`
|
120
|
-
panel.innerHTML += options.description
|
121
|
-
? `<p class="rc-panel-description">${options.description}</p>`
|
122
|
-
: ''
|
121
|
+
panel.innerHTML = `<h1 class="rc-panel-title" id="rc-panel-title">${options.headline}</h1>`
|
122
|
+
panel.innerHTML += `<p class="rc-panel-description" id="rc-panel-description">${options.description}</p>`
|
123
123
|
panel.innerHTML += '<div class="rc-panel-steps" id="rc-panel-steps"></div>'
|
124
124
|
|
125
125
|
if (!_reset) parentElement.appendChild(panel)
|
@@ -300,6 +300,15 @@ const _activateStepAt = (RC, current, tasks, options, finalCallback) => {
|
|
300
300
|
_activateStepAt(RC, current, tasks, options, finalCallback)
|
301
301
|
}
|
302
302
|
} else {
|
303
|
+
// Change headline and description
|
304
|
+
const { headline, nextHeadline, description, nextDescription } =
|
305
|
+
options
|
306
|
+
if (headline !== nextHeadline)
|
307
|
+
document.querySelector('#rc-panel-title').innerHTML = nextHeadline
|
308
|
+
if (description !== nextDescription)
|
309
|
+
document.querySelector('#rc-panel-description').innerHTML =
|
310
|
+
nextDescription
|
311
|
+
|
303
312
|
e.onclick = () => {
|
304
313
|
RC._panelFinished = true
|
305
314
|
if (finalCallback && typeof finalCallback === 'function')
|
package/src/text.json
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
},
|
6
6
|
"measureDistance": {
|
7
7
|
"headline": "📏 Measure Viewing Distance",
|
8
|
-
"description": "We'll measure your viewing distance. To do this, we will perform a blind spot test. Close/Cover one of your eyes (as instructed below) and focus on the black
|
8
|
+
"description": "We'll measure your viewing distance. To do this, we will perform a blind spot test. Close/Cover one of your eyes (as instructed below) and focus on the black crosshair. Press <b>SPACE</b> or click <b>OK</b> when the red circle disappears. If it doesn't disappear, try moving closer to the screen."
|
9
9
|
},
|
10
10
|
"trackDistance": {
|
11
11
|
"headline": "🙂 Set up for Head Tracking",
|
12
|
-
"description": "Now we'll set up head tracking, to monitor viewing distance. When asked, please grant permission to access your camera. We'll perform a blind spot test first. Close/Cover one of your eyes (as instructed below) and focus on the black
|
12
|
+
"description": "Now we'll set up head tracking, to monitor viewing distance. When asked, please grant permission to access your camera. We'll perform a blind spot test first. Close/Cover one of your eyes (as instructed below) and focus on the black crosshair. Please press <b>SPACE</b> or click <b>OK</b> when the red dot disappears. If it doesn't disappear, try moving closer to the screen. You'll do this twice with each eye. Once you're done, head tracking will begin."
|
13
13
|
},
|
14
14
|
"calibrateGaze": {
|
15
15
|
"headline": "👀 Set up for Gaze Tracking",
|
@@ -22,6 +22,8 @@
|
|
22
22
|
"panel": {
|
23
23
|
"headline": "To calibrate, press a button!",
|
24
24
|
"description": "",
|
25
|
+
"nextHeadline": "Thanks for calibrating. Hit the button to continue.",
|
26
|
+
"nextDescription": "",
|
25
27
|
"nextButton": "Done"
|
26
28
|
},
|
27
29
|
"measurePD": {
|