remote-calibrator 0.3.0 → 0.5.0-beta.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +29 -19
  3. package/homepage/example.js +9 -3
  4. package/i18n/fetch-languages-sheets.js +5 -4
  5. package/lib/RemoteCalibrator.min.js +1 -1
  6. package/lib/RemoteCalibrator.min.js.LICENSE.txt +1 -1
  7. package/lib/RemoteCalibrator.min.js.map +1 -1
  8. package/package.json +15 -15
  9. package/src/WebGazer4RC/.gitattributes +10 -0
  10. package/src/WebGazer4RC/LICENSE.md +15 -0
  11. package/src/WebGazer4RC/README.md +142 -0
  12. package/src/WebGazer4RC/gnu-lgpl-v3.0.md +163 -0
  13. package/src/WebGazer4RC/gplv3.md +636 -0
  14. package/src/WebGazer4RC/package-lock.json +1133 -0
  15. package/src/WebGazer4RC/package.json +28 -0
  16. package/src/WebGazer4RC/src/dom_util.mjs +27 -0
  17. package/src/WebGazer4RC/src/facemesh.mjs +150 -0
  18. package/src/WebGazer4RC/src/index.mjs +1235 -0
  19. package/src/WebGazer4RC/src/mat.mjs +301 -0
  20. package/src/WebGazer4RC/src/params.mjs +29 -0
  21. package/src/WebGazer4RC/src/pupil.mjs +109 -0
  22. package/src/WebGazer4RC/src/ridgeReg.mjs +104 -0
  23. package/src/WebGazer4RC/src/ridgeRegThreaded.mjs +161 -0
  24. package/src/WebGazer4RC/src/ridgeWeightedReg.mjs +125 -0
  25. package/src/WebGazer4RC/src/ridgeWorker.mjs +135 -0
  26. package/src/WebGazer4RC/src/util.mjs +348 -0
  27. package/src/WebGazer4RC/src/util_regression.mjs +240 -0
  28. package/src/WebGazer4RC/src/worker_scripts/mat.js +306 -0
  29. package/src/WebGazer4RC/src/worker_scripts/util.js +398 -0
  30. package/src/WebGazer4RC/test/regression_test.js +182 -0
  31. package/src/WebGazer4RC/test/run_tests_and_server.sh +24 -0
  32. package/src/WebGazer4RC/test/util_test.js +60 -0
  33. package/src/WebGazer4RC/test/webgazerExtract_test.js +40 -0
  34. package/src/WebGazer4RC/test/webgazer_test.js +160 -0
  35. package/src/WebGazer4RC/test/www_page_test.js +41 -0
  36. package/src/const.js +3 -0
  37. package/src/core.js +8 -0
  38. package/src/css/distance.scss +40 -0
  39. package/src/css/panel.scss +32 -1
  40. package/src/distance/distance.js +4 -4
  41. package/src/distance/distanceCheck.js +115 -0
  42. package/src/distance/distanceTrack.js +99 -41
  43. package/src/{interpupillaryDistance.js → distance/interPupillaryDistance.js} +14 -12
  44. package/src/gaze/gazeTracker.js +16 -1
  45. package/src/i18n.js +1 -1
  46. package/src/index.js +2 -1
  47. package/src/panel.js +32 -3
  48. package/webpack.config.js +4 -4
@@ -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
+
package/src/const.js CHANGED
@@ -12,6 +12,8 @@ RemoteCalibrator.prototype._CONST = Object.freeze({
12
12
  MARGIN: 10,
13
13
  BORDER: 8,
14
14
  },
15
+ PPI_DONT_USE: 127.7,
16
+ PD_DONT_USE: 6.4,
15
17
  },
16
18
  S: {
17
19
  AUTO: 'AUTO',
@@ -31,4 +33,5 @@ RemoteCalibrator.prototype._CONST = Object.freeze({
31
33
  B: 'BlindSpot',
32
34
  F: 'FaceMesh',
33
35
  },
36
+ IN_TO_CM: 2.54,
34
37
  })
package/src/core.js CHANGED
@@ -44,10 +44,18 @@ class RemoteCalibrator {
44
44
  panelResolve: null,
45
45
  }
46
46
 
47
+ // Are we calibrating for setting up gaze or distance tracking?
47
48
  this._trackingSetupFinishedStatus = {
48
49
  gaze: true,
49
50
  distance: true,
50
51
  }
52
+ this._trackingStatus = {
53
+ distanceCorrecting: null, // setInterval
54
+ }
55
+ this._tackingVideoFrameTimestamps = {
56
+ gaze: 0,
57
+ distance: 0,
58
+ }
51
59
 
52
60
  this._environmentData = []
53
61
 
@@ -87,3 +87,43 @@
87
87
  height: 16px;
88
88
  }
89
89
  }
90
+
91
+ /* ---------------------------------- Check --------------------------------- */
92
+
93
+ #rc-distance-correct {
94
+ text-align: center;
95
+ width: 100%;
96
+ margin: 3rem auto auto auto;
97
+ padding: 2rem;
98
+ overflow-wrap: break-word;
99
+
100
+ #rc-distance-correct-instruction {
101
+ font-weight: 700;
102
+ font-size: 7rem;
103
+ margin: 2rem auto;
104
+ }
105
+
106
+ #rc-distance-correct-guide {
107
+ font-weight: 500;
108
+ font-size: 3rem;
109
+ line-height: 150%;
110
+
111
+ .rc-distance-num {
112
+ padding: 0.5rem;
113
+ border-radius: 7px !important;
114
+ font-weight: 700;
115
+ font-family: monospace !important;
116
+ vertical-align: middle;
117
+ }
118
+
119
+ .rc-distance-now {
120
+ border: 2px solid #ff9a00;
121
+ background-color: #ff9a0066;
122
+ }
123
+
124
+ .rc-distance-desired {
125
+ border: 2px solid #3490de;
126
+ background-color: #3490de66;
127
+ }
128
+ }
129
+ }
@@ -29,6 +29,7 @@
29
29
  user-select: none;
30
30
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
31
31
  Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
32
+ scrollbar-width: none;
32
33
  }
33
34
 
34
35
  .rc-panel-title {
@@ -45,8 +46,27 @@
45
46
  font-weight: 500 !important;
46
47
  }
47
48
 
49
+ #rc-panel-language-parent {
50
+ display: flex;
51
+ flex-direction: row-reverse;
52
+ margin: 0;
53
+ padding: 0;
54
+
55
+ #rc-panel-lang-picker {
56
+ display: block;
57
+ background-color: #ffffffcc !important;
58
+ border: none !important;
59
+ outline: none !important;
60
+ margin: 0.25rem 0.25rem 0 0.25rem !important;
61
+ padding: 0.25rem !important;
62
+ font-size: 1rem !important;
63
+ font-weight: 500 !important;
64
+ border-radius: 5px !important;
65
+ }
66
+ }
67
+
48
68
  .rc-panel-steps {
49
- margin: 1.5rem 0 0 0 !important;
69
+ margin: 1rem 0 0 0 !important;
50
70
 
51
71
  &.rc-panel-steps-l,
52
72
  &.rc-panel-steps-s {
@@ -90,6 +110,8 @@
90
110
 
91
111
  &.rc-panel-steps-l {
92
112
  flex-flow: row nowrap;
113
+ max-width: 100%;
114
+ overflow-x: scroll;
93
115
 
94
116
  .rc-panel-step-name {
95
117
  margin: 1.5rem 0.5rem !important;
@@ -99,6 +121,10 @@
99
121
  &.rc-panel-steps-s {
100
122
  flex-flow: column nowrap;
101
123
 
124
+ .rc-panel-step {
125
+ overflow-x: hidden;
126
+ }
127
+
102
128
  .rc-panel-step-name {
103
129
  margin: 1.2rem 1.5rem !important;
104
130
  }
@@ -186,4 +212,9 @@
186
212
  cursor: pointer;
187
213
  }
188
214
  }
215
+
216
+ ::-webkit-scrollbar {
217
+ width: 0;
218
+ display: none;
219
+ }
189
220
  }
@@ -24,7 +24,7 @@ const blindSpotHTML = `<canvas id="blind-spot-canvas"></canvas>`
24
24
  /* -------------------------------------------------------------------------- */
25
25
 
26
26
  export function blindSpotTest(RC, options, toTrackDistance = false, callback) {
27
- let ppi = 108 // Dangerous! Arbitrary value
27
+ let ppi = RC._CONST.N.PPI_DONT_USE // Dangerous! Arbitrary value
28
28
  if (RC.screenPpi) ppi = RC.screenPpi.value
29
29
  else
30
30
  console.error(
@@ -41,7 +41,7 @@ export function blindSpotTest(RC, options, toTrackDistance = false, callback) {
41
41
  RC.background.appendChild(blindSpotDiv)
42
42
  RC._constructFloatInstructionElement(
43
43
  'blind-spot-instruction',
44
- phrases.RC_headTrackingCloseL[RC.L]
44
+ phrases.RC_distanceTrackingCloseL[RC.L]
45
45
  )
46
46
  RC._addCreditOnBackground(phrases.RC_viewingBlindSpotCredit[RC.L])
47
47
 
@@ -125,10 +125,10 @@ export function blindSpotTest(RC, options, toTrackDistance = false, callback) {
125
125
  if (eyeSide === 'left') {
126
126
  // Change to RIGHT
127
127
  eyeSide = 'right'
128
- eyeSideEle.innerHTML = phrases.RC_headTrackingCloseR[RC.L]
128
+ eyeSideEle.innerHTML = phrases.RC_distanceTrackingCloseR[RC.L]
129
129
  } else {
130
130
  eyeSide = 'left'
131
- eyeSideEle.innerHTML = phrases.RC_headTrackingCloseL[RC.L]
131
+ eyeSideEle.innerHTML = phrases.RC_distanceTrackingCloseL[RC.L]
132
132
  }
133
133
  RC._setFloatInstructionElementPos(eyeSide, 16)
134
134
 
@@ -0,0 +1,115 @@
1
+ import RemoteCalibrator from '../core'
2
+ import { bindKeys, unbindKeys } from '../components/keyBinder'
3
+ import { phrases } from '../i18n'
4
+ import { addButtons } from '../components/buttons'
5
+
6
+ RemoteCalibrator.prototype.checkDistance = function (
7
+ desiredCm,
8
+ errorTolerance
9
+ ) {
10
+ ////
11
+ if (!this.checkInitialized()) return
12
+ ////
13
+
14
+ if (!desiredCm) return
15
+
16
+ if (
17
+ this.viewingDistanceCm &&
18
+ this.viewingDistanceCm.method === this._CONST.VIEW_METHOD.F
19
+ ) {
20
+ if (!withinRange(this.viewingDistanceCm.value, desiredCm, errorTolerance)) {
21
+ // ! Out of range
22
+ if (this._trackingStatus.distanceCorrecting === null) {
23
+ const breakFunction = () => {
24
+ this._removeBackground()
25
+ clearInterval(this._trackingStatus.distanceCorrecting)
26
+ this._trackingStatus.distanceCorrecting = null
27
+
28
+ unbindKeys(bindKeysFunction)
29
+ }
30
+
31
+ // Bind keys
32
+ const bindKeysFunction = bindKeys({
33
+ Escape: breakFunction,
34
+ })
35
+
36
+ // ! Start
37
+ const [moveElement, guideNumNow, guideNumDesired] =
38
+ startCorrecting(this)
39
+
40
+ addButtons(
41
+ this.L,
42
+ this.background,
43
+ {
44
+ cancel: breakFunction,
45
+ },
46
+ this.params.showCancelButton
47
+ )
48
+
49
+ const _update = () => {
50
+ moveElement.innerHTML = getMoveInner(
51
+ this,
52
+ this.viewingDistanceCm.value,
53
+ desiredCm
54
+ )
55
+ guideNumNow.innerHTML = Math.round(this.viewingDistanceCm.value)
56
+ guideNumDesired.innerHTML = Math.round(desiredCm)
57
+ }
58
+ _update()
59
+
60
+ this._trackingStatus.distanceCorrecting = setInterval(() => {
61
+ _update()
62
+
63
+ // Check again
64
+ if (
65
+ withinRange(this.viewingDistanceCm.value, desiredCm, errorTolerance)
66
+ ) {
67
+ breakFunction()
68
+ unbindKeys(bindKeysFunction)
69
+ }
70
+ }, 250)
71
+ }
72
+ return false
73
+ } else {
74
+ // ! In range
75
+ return true
76
+ }
77
+ } else {
78
+ console.error(
79
+ 'You need to start tracking viewing distance before checking it.'
80
+ )
81
+ return false
82
+ }
83
+ }
84
+
85
+ const withinRange = (value, target, tolerance) => {
86
+ tolerance = Math.max(Math.min(Number(tolerance), 1), 0.1)
87
+ return value <= target * (1 + tolerance) && value >= target * (1 - tolerance)
88
+ }
89
+
90
+ const startCorrecting = RC => {
91
+ RC._addBackground(`<div id="rc-distance-correct">
92
+ <p id="rc-distance-correct-instruction"></p>
93
+ <p id="rc-distance-correct-guide">${phrases.RC_distanceTrackingGuide[RC.L]
94
+ .replace(
95
+ 'xx1',
96
+ `<span class="rc-distance-num rc-distance-now" id="rc-distance-now"></span>`
97
+ )
98
+ .replace(
99
+ 'xx2',
100
+ `<span class="rc-distance-num rc-distance-desired" id="rc-distance-desired"></span>`
101
+ )}</p>
102
+ </div>
103
+ `)
104
+
105
+ return [
106
+ document.querySelector('#rc-distance-correct-instruction'),
107
+ document.querySelector('#rc-distance-now'),
108
+ document.querySelector('#rc-distance-desired'),
109
+ ]
110
+ }
111
+
112
+ const getMoveInner = (RC, value, target) => {
113
+ if (value >= target) return phrases.RC_distanceTrackingMoveCloser[RC.L]
114
+ else return phrases.RC_distanceTrackingMoveFurther[RC.L]
115
+ }