remote-calibrator 0.3.0 → 0.5.0-beta.3

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.
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
+ }