├── README.md
├── LICENSE
├── style.css
├── test.html
├── index.html
├── data.js
└── main.js
/README.md:
--------------------------------------------------------------------------------
1 | # Eye Movement Visualization
2 | This project visualizes eye movement data in reading.
3 |
4 | ## TODO
5 | - ~~Use different color for regressions.~~
6 | - ~~circles to front, lines to back.~~
7 | - ~~Use bootstrap.~~
8 | - ~~Switch to new d3.js~~
9 | - Monitor animation time.
10 | - Restart, pause, resume.
11 | - Heatmap.
12 | - Filter (e.g. show fixations above given threshold)
13 | - Show pop-up on fixations
14 | - Customize bootstrap.
15 | - Add credits section.
16 | - ~~Alternative Visualizations.~~
17 | - ~~Implement eyelink data reader and converter.~~
18 | - Create test page for monospace font overlapping.
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Mustafa İlhan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | /* Web site */
2 |
3 | body {
4 | font-family: 'Roboto Mono', 'Courier New', Courier, monospace;
5 | font-size: 12;
6 | color: #a73b8f;
7 | background: #fff;
8 | overflow: hidden;
9 | }
10 | ::selection {
11 | background: #a93790;
12 | color: #fff;
13 | }
14 | h1 {
15 | color: #3c1357;
16 | }
17 | .lead {
18 | color: #61208d;
19 | }
20 | p {
21 | color: #a73b8f;
22 | }
23 | .btn-default {
24 | color: #e8638b;
25 | background-color: #fff;
26 | border-color: #e8638b;
27 | }
28 | .btn-default:hover,
29 | .btn-default:focus,
30 | .btn-default.focus,
31 | .btn-default:active,
32 | .btn-default.active,
33 | .btn-default:active:hover,
34 | .btn-default.active:hover,
35 | .btn-default:active:focus,
36 | .btn-default.active:focus,
37 | .btn-default:active.focus,
38 | .btn-default.active.focus {
39 | color: #fff;
40 | background-color: #e8638b;
41 | border-color: #e8638b;
42 | }
43 | a, .btn-link {
44 | color: #f4aea3;
45 | }
46 | a:hover,
47 | a:focus,
48 | a:active,
49 | .btn-link:hover,
50 | .btn-link:focus,
51 | .btn-link:active {
52 | color: #a73b8f;
53 | text-decoration: underline;
54 | }
55 | .project-info {
56 | position: absolute;
57 | top: 20px;
58 | bottom: 20px;
59 | left: 20px;
60 | right: 20px;
61 | }
62 | .upload-area {
63 | display: none;
64 | }
65 | .animation-canvas {
66 | display: none;
67 | position: absolute;
68 | top: 0;
69 | bottom: 0;
70 | left: 0;
71 | right: 0;
72 | }
73 | footer {
74 | position: absolute;
75 | bottom: 0;
76 | color: #f4aea3;
77 | }
78 | .read-text {
79 | display: none;
80 | position: absolute;
81 | color: #a73b8f;
82 | font-size: 14px;
83 | opacity: .4;
84 | }
85 | .controls {
86 | /*padding: 0 20px;*/
87 | }
88 |
--------------------------------------------------------------------------------
/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | test
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Eye Movement Visualization
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
Eye Movement Visualization alpha
27 |
This project visualizes eye movements data recorded via eye trackers during reading.
28 |
Eye tracking is a hot research method to understand how humans read. It provides measurable data. During reading texts there are two main eye movements: saccades and fixations.
29 |
View Sample
30 |
Visualize Your Data
31 |
35 |
36 |
37 |
38 |
39 |
40 | Back
41 | |
42 | Resume
43 | Pause
44 | Stop
45 | Restart
46 | |
47 | Tips
48 | How it works
49 |
50 |
51 |
52 |
53 |
54 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/data.js:
--------------------------------------------------------------------------------
1 | var customData = {},
2 | demoData = {
3 | /*windowWidth: 1024,*/
4 | /*windowHeight: 768,*/
5 | sentenceLine1: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
6 | sentenceLine2: '',
7 | sentenceX: 20,
8 | sentenceY: 180,
9 | fontSize: 14,
10 | /*sentenceUrl: '',*/
11 | xFix: -60,
12 | yFix: -50,
13 | trials: [
14 | /******************
15 | * trial 1
16 | ******************/
17 | [{
18 | duration: 200,
19 | x: 100,
20 | y: 200,
21 | type: 'fixation'
22 | // word: 'Lorem'
23 | }, {
24 | duration: 50,
25 | x1: 100,
26 | x2: 200,
27 | y1: 200,
28 | y2: 200,
29 | type: 'saccade'
30 | },
31 | /*{
32 | duration: 200,
33 | x: 100,
34 | y: 200,
35 | type: 'fixation'
36 | // word: 'ipsum'
37 | }, {
38 | duration: 50,
39 | x1: 100,
40 | x2: 200,
41 | y1: 200,
42 | y2: 200,
43 | type: 'saccade'
44 | }, */
45 | {
46 | duration: 240,
47 | x: 200,
48 | y: 200,
49 | type: 'fixation'
50 | // word: 'dolor'
51 | }, {
52 | duration: 28,
53 | x1: 200,
54 | x2: 248,
55 | y1: 200,
56 | y2: 200,
57 | type: 'saccade'
58 | }, {
59 | duration: 220,
60 | x: 248,
61 | y: 200,
62 | type: 'fixation'
63 | // word: 'sit'
64 | }, {
65 | duration: 45,
66 | x1: 248,
67 | x2: 280,
68 | y1: 200,
69 | y2: 200,
70 | type: 'saccade'
71 | }, {
72 | duration: 234,
73 | x: 280,
74 | y: 200,
75 | type: 'fixation'
76 | // word: 'amet'
77 | }, {
78 | duration: 20,
79 | x1: 280,
80 | x2: 342,
81 | y1: 200,
82 | y2: 200,
83 | type: 'saccade'
84 | }, {
85 | duration: 340,
86 | x: 342,
87 | y: 200,
88 | type: 'fixation'
89 | // word: 'consectetur'
90 | }, {
91 | duration: 40,
92 | x1: 342,
93 | x2: 405,
94 | y1: 200,
95 | y2: 200,
96 | type: 'saccade'
97 | }, {
98 | duration: 285,
99 | x: 405,
100 | y: 200,
101 | type: 'fixation'
102 | // word: 'consectetur'
103 | }, {
104 | duration: 41,
105 | x1: 405,
106 | x2: 450,
107 | y1: 200,
108 | y2: 200,
109 | type: 'saccade'
110 | }, {
111 | duration: 232,
112 | x: 450,
113 | y: 200,
114 | type: 'fixation'
115 | // word: 'adipiscing'
116 | }, {
117 | duration: 26,
118 | x1: 450,
119 | x2: 490,
120 | y1: 200,
121 | y2: 200,
122 | type: 'saccade'
123 | }, {
124 | duration: 340,
125 | x: 490,
126 | y: 200,
127 | type: 'fixation'
128 | // word: 'adipiscing'
129 | }, {
130 | duration: 43,
131 | x1: 490,
132 | x2: 532,
133 | y1: 200,
134 | y2: 200,
135 | type: 'saccade'
136 | }, {
137 | duration: 296,
138 | x: 532,
139 | y: 200,
140 | type: 'fixation'
141 | // word: 'elit'
142 | }
143 | ],
144 | /******************
145 | * trial 2
146 | ******************/
147 | [{
148 | duration: 219,
149 | x: 95,
150 | y: 200,
151 | type: 'fixation'
152 | // word: 'Lorem'
153 | }, {
154 | duration: 50,
155 | x1: 95,
156 | x2: 145,
157 | y1: 200,
158 | y2: 200,
159 | type: 'saccade'
160 | }, {
161 | duration: 200,
162 | x: 145,
163 | y: 200,
164 | type: 'fixation'
165 | // word: 'ipsum'
166 | }, {
167 | duration: 50,
168 | x1: 145,
169 | x2: 209.1,
170 | y1: 200,
171 | y2: 200,
172 | type: 'saccade'
173 | }, {
174 | duration: 245,
175 | x: 209.1,
176 | y: 200,
177 | type: 'fixation'
178 | // word: 'dolor'
179 | }, {
180 | duration: 32,
181 | x1: 209.1,
182 | x2: 244,
183 | y1: 200,
184 | y2: 200,
185 | type: 'saccade'
186 | }, {
187 | duration: 229,
188 | x: 244,
189 | y: 200,
190 | type: 'fixation'
191 | // word: 'sit'
192 | }, {
193 | duration: 43,
194 | x1: 244,
195 | x2: 286,
196 | y1: 200,
197 | y2: 200,
198 | type: 'saccade'
199 | }, {
200 | duration: 254,
201 | x: 286,
202 | y: 200,
203 | type: 'fixation'
204 | // word: 'amet'
205 | }, {
206 | duration: 22,
207 | x1: 286,
208 | x2: 342,
209 | y1: 200,
210 | y2: 200,
211 | type: 'saccade'
212 | }, {
213 | duration: 349,
214 | x: 342,
215 | y: 200,
216 | type: 'fixation'
217 | // word: 'consectetur'
218 | }, {
219 | duration: 42,
220 | x1: 342,
221 | x2: 401.2,
222 | y1: 200,
223 | y2: 200,
224 | type: 'saccade'
225 | }, {
226 | duration: 284,
227 | x: 401.2,
228 | y: 200,
229 | type: 'fixation'
230 | // word: 'consectetur'
231 | }, {
232 | duration: 41,
233 | x1: 401.2,
234 | x2: 444,
235 | y1: 200,
236 | y2: 200,
237 | type: 'saccade'
238 | }, {
239 | duration: 303,
240 | x: 444,
241 | y: 200,
242 | type: 'fixation'
243 | // word: 'adipiscing'
244 | }, {
245 | duration: 26,
246 | x1: 444,
247 | x2: 487,
248 | y1: 200,
249 | y2: 200,
250 | type: 'saccade'
251 | }, {
252 | duration: 450,
253 | x: 487,
254 | y: 200,
255 | type: 'fixation'
256 | // word: 'adipiscing'
257 | }, {
258 | duration: 43,
259 | x1: 487,
260 | x2: 535,
261 | y1: 200,
262 | y2: 200,
263 | type: 'saccade'
264 | }, {
265 | duration: 312,
266 | x: 535,
267 | y: 200,
268 | type: 'fixation'
269 | // word: 'elit'
270 | }],
271 | /******************
272 | * trial 3
273 | ******************/
274 | [{
275 | duration: 179,
276 | x: 102,
277 | y: 200,
278 | type: 'fixation'
279 | // word: 'Lorem'
280 | }, {
281 | duration: 34,
282 | x1: 102,
283 | x2: 148.9,
284 | y1: 200,
285 | y2: 200,
286 | type: 'saccade'
287 | }, {
288 | duration: 165,
289 | x: 148.9,
290 | y: 200,
291 | type: 'fixation'
292 | // word: 'ipsum'
293 | }, {
294 | duration: 50,
295 | x1: 148.9,
296 | x2: 211,
297 | y1: 200,
298 | y2: 200,
299 | type: 'saccade'
300 | }, {
301 | duration: 245,
302 | x: 211,
303 | y: 200,
304 | type: 'fixation'
305 | // word: 'dolor'
306 | }, {
307 | duration: 32,
308 | x1: 211,
309 | x2: 248.9,
310 | y1: 200,
311 | y2: 200,
312 | type: 'saccade'
313 | }, {
314 | duration: 230,
315 | x: 248.9,
316 | y: 200,
317 | type: 'fixation'
318 | // word: 'sit'
319 | }, {
320 | duration: 43,
321 | x1: 248.9,
322 | x2: 289.8,
323 | y1: 200,
324 | y2: 200,
325 | type: 'saccade'
326 | }, {
327 | duration: 274,
328 | x: 289.8,
329 | y: 200,
330 | type: 'fixation'
331 | // word: 'amet'
332 | }, {
333 | duration: 22,
334 | x1: 289.8,
335 | x2: 344.3,
336 | y1: 200,
337 | y2: 200,
338 | type: 'saccade'
339 | }, {
340 | duration: 378.7,
341 | x: 344.3,
342 | y: 200,
343 | type: 'fixation'
344 | // word: 'consectetur'
345 | }, {
346 | duration: 42,
347 | x1: 344.3,
348 | x2: 398.4,
349 | y1: 200,
350 | y2: 200,
351 | type: 'saccade'
352 | }, {
353 | duration: 260,
354 | x: 398.4,
355 | y: 200,
356 | type: 'fixation'
357 | // word: 'consectetur'
358 | }, {
359 | duration: 48.9,
360 | x1: 398.4,
361 | x2: 462,
362 | y1: 200,
363 | y2: 200,
364 | type: 'saccade'
365 | }, {
366 | duration: 382.3,
367 | x: 462,
368 | y: 200,
369 | type: 'fixation'
370 | // word: 'adipiscing'
371 | },
372 | /* {
373 | duration: 26,
374 | x1: 444,
375 | x2: 487,
376 | y1: 200, y2: 200,
377 | type: 'saccade'
378 | }, {
379 | duration: 450,
380 | x: 487,
381 | y: 200,
382 | type: 'fixation'
383 | // word: 'adipiscing'
384 | },*/
385 | {
386 | duration: 43,
387 | x1: 462,
388 | x2: 530.9,
389 | y1: 200,
390 | y2: 200,
391 | type: 'saccade'
392 | }, {
393 | duration: 343,
394 | x: 530.9,
395 | y: 200,
396 | type: 'fixation'
397 | // word: 'elit'
398 | }
399 | ]
400 |
401 | ]
402 | };
403 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | window.onload = function() {
2 |
3 | /**************************************************
4 | * Global Variables *
5 | **************************************************/
6 |
7 | var width = $(document).width(),
8 | height = $(document).height();
9 |
10 | var colors = ['#3c1357', '#61208d', '#a73b8f', '#e8638b', '#f4aea3'];
11 |
12 | var saccadeColor = '#ff00ff',
13 | /*'#f4aea3',*/
14 | regressionColor = '#00ffff',
15 | /*'#f4aea3',*/
16 | fixationColor = '#3c1357';
17 |
18 | var paused = false,
19 | finished = false,
20 | started = false,
21 | stopped = false;
22 |
23 | var debugEnabled = true,
24 | playBackAnimationEnabled = true,
25 | timelineAnimationEnabled = true,
26 | densityAnimationEnabled = true;
27 |
28 | var timelineOffset = 30,
29 | densityOffset = -30;
30 |
31 | var svg,
32 | densityGroup,
33 | timelineGroup,
34 | playBackGroup,
35 | playBackLineGroup,
36 | playBackCircleGroup;
37 |
38 | function debug(funcName) {
39 | printLog(performance.now() + ' ' + funcName);
40 | }
41 |
42 | function printLog(msg) {
43 | if (debugEnabled) {
44 | console.log(msg);
45 | }
46 | }
47 |
48 | /**************************************************
49 | * Animation Stuff *
50 | **************************************************/
51 |
52 | function init() {
53 | timelineOffset = 30;
54 | densityOffset = -30;
55 |
56 | svg = d3.select('#animationCanvas').append('svg').attr('width', width).attr('height', height);
57 | densityGroup = svg.append('g').attr('id', 'densityGroup');
58 | timelineGroup = svg.append('g').attr('id', 'timelineGroup');
59 | playBackGroup = svg.append('g').attr('id', 'playBackGroup');
60 | playBackLineGroup = playBackGroup.append('g').attr('id', 'playBackLineGroup');
61 | playBackCircleGroup = playBackGroup.append('g').attr('id', 'playBackCircleGroup');
62 |
63 | stopped = false;
64 | }
65 |
66 | function resetForNextTrial() {
67 | playBackGroup.remove();
68 | playBackGroup = svg.append('g').attr('id', 'playBackGroup');
69 | playBackLineGroup = playBackGroup.append('g').attr('id', 'playBackLineGroup');
70 | playBackCircleGroup = playBackGroup.append('g').attr('id', 'playBackCircleGroup');
71 |
72 | timelineOffset = 30;
73 | densityOffset = -30;
74 | }
75 |
76 | function startAnimation() {
77 | printLog('started');
78 | init();
79 | started = true;
80 | finished = false;
81 | updateControls();
82 | addText(selectData());
83 | }
84 |
85 | function endAnimation() {
86 | printLog('finished');
87 | finished = true;
88 | started = false;
89 | updateControls();
90 | // Clean playback area.
91 | setTimeout(resetForNextTrial, 4000);
92 | }
93 |
94 | function selectData() {
95 | //printLog(window.location.hash);
96 | return window.location.hash === '#custom' ? customData : demoData;
97 | }
98 |
99 | function updateControls() {
100 | // TODO Update controls
101 | }
102 |
103 | function addText(data) {
104 | // TODO Automate sentence line append. (It is hard-coded now.)
105 | $('#readText').css({
106 | top: data.sentenceY,
107 | left: data.sentenceX,
108 | fontSize: data.fontSize + 'px'
109 | }).append('' + data.sentenceLine1 + ' ')
110 | .append('' + data.sentenceLine2 + ' ')
111 | .fadeIn(function() {
112 | drawTrials(data, 0);
113 | });
114 |
115 | if (typeof data.sentenceLine2 != 'undefined' && data.sentenceLine2 != '') {
116 | // Get first line width and set.
117 | data.sentenceLine2StartOffset = $('#line1').width();
118 | printLog('sentenceLine2StartOffset: ' + data.sentenceLine2StartOffset);
119 | }
120 | calibrate(data);
121 | }
122 |
123 | function drawTrials(data, i) {
124 | //debug('drawTrials');
125 | // TODO Show current running trial as message. (e.g. trial1, trial2)
126 | return drawMovements(data.trials[i], i, 0, function() {
127 | if (stopped) {
128 | return;
129 | }
130 | if (i + 1 < data.trials.length) {
131 | setTimeout(function() {
132 | // Clean playback area.
133 | resetForNextTrial();
134 | // Call next trial.
135 | setTimeout(function() {
136 | drawTrials(data, i + 1);
137 | }, 1000);
138 | }, 4000);
139 | } else {
140 | endAnimation();
141 | }
142 | });
143 | }
144 |
145 | function drawMovements(trial, i, j, callback) {
146 | //debug('drawMovements');
147 | return animateAll(trial[j], function() {
148 |
149 | if (stopped) {
150 | return;
151 | }
152 |
153 | if (j + 1 < trial.length) {
154 | drawMovements(trial, i, j + 1, callback);
155 | } else {
156 | callback();
157 | }
158 | });
159 | }
160 |
161 | function drawLine(obj) {
162 | return obj.group.append('path')
163 | .attr('d', obj.path)
164 | .attr('stroke', obj.color)
165 | .attr('stroke-width', '1')
166 | .attr('fill', 'none');
167 | }
168 |
169 | function drawCircle(obj) {
170 | return obj.group.append('circle')
171 | .attr('cx', obj.x)
172 | .attr('cy', obj.y)
173 | .attr('fill', obj.color)
174 | .attr('r', 0)
175 | .attr('opacity', .6);
176 | }
177 |
178 | function drawRectangle(obj) {
179 | return obj.group.append('rect')
180 | .attr('x', obj.x)
181 | .attr('y', obj.y)
182 | .attr('width', obj.width)
183 | .attr('height', 0)
184 | .attr('fill', obj.color)
185 | .attr('opacity', .1);
186 | }
187 |
188 | function animateLine(obj, callback) {
189 | var path = drawLine(obj),
190 | totalLength = path.node().getTotalLength();
191 | return path
192 | .attr('stroke-dasharray', totalLength + ' ' + totalLength)
193 | .attr('stroke-dashoffset', totalLength)
194 | .transition()
195 | .duration(obj.duration)
196 | .attr('stroke-dashoffset', 0)
197 | .on('end', callback);
198 | }
199 |
200 | function animateCircle(obj, callback) {
201 | return drawCircle(obj)
202 | .transition()
203 | .duration(obj.duration)
204 | .attr('r', obj.r)
205 | .on('end', callback);
206 | }
207 |
208 | function animateRectangle(obj, callback) {
209 | return drawRectangle(obj)
210 | .transition()
211 | .duration(obj.duration)
212 | .attr('y', obj.y - obj.duration / 5)
213 | .attr('height', obj.duration / 5)
214 | .on('end', callback);
215 | }
216 |
217 | function animateAll(movement, callback) {
218 | playBackAnimation(movement);
219 | timelineAnimation(movement);
220 | densityAnimation(movement);
221 | setTimeout(callback, movement.duration);
222 | }
223 |
224 | /**
225 | *
226 | */
227 | function playBackAnimation(movement, callback) {
228 | function getSaccadeData() {
229 | return {
230 | group: playBackLineGroup,
231 | path: 'M ' + movement.x1 + ' ' + movement.y1 + ' Q ' + (movement.x1 + (movement.x2 - movement.x1) / 2) + ' ' + (movement.y1 - 30) + ' ' + movement.x2 + ' ' + movement.y2,
232 | color: movement.x2 - movement.x1 < 0 ? regressionColor : saccadeColor,
233 | duration: movement.duration
234 | }
235 | }
236 |
237 | function getFixationData() {
238 | return {
239 | group: playBackCircleGroup,
240 | x: movement.x,
241 | y: movement.y,
242 | r: movement.duration / 30,
243 | color: fixationColor,
244 | duration: movement.duration
245 | }
246 | }
247 |
248 | if (playBackAnimationEnabled) {
249 | if (movement.type === 'saccade') {
250 | return animateLine(getSaccadeData(movement), callback);
251 | } else if (movement.type === 'fixation') {
252 | return animateCircle(getFixationData(movement), callback);
253 | }
254 | return;
255 | }
256 | }
257 |
258 | /**
259 | *
260 | */
261 | function timelineAnimation(movement, callback) {
262 | function getSaccadeData() {
263 | return {
264 | group: timelineGroup,
265 | path: 'M ' + movement.x1 + ' ' + (movement.y1 + timelineOffset) + ' H ' + movement.x2,
266 | color: movement.x2 - movement.x1 < 0 ? regressionColor : saccadeColor,
267 | duration: movement.duration
268 | }
269 | }
270 |
271 | function getFixationData() {
272 | var tOffset = timelineOffset;
273 | timelineOffset += movement.duration / 10;
274 | return {
275 | group: timelineGroup,
276 | path: 'M ' + movement.x + ' ' + (movement.y + tOffset) + ' V ' + (movement.y + timelineOffset),
277 | color: fixationColor,
278 | duration: movement.duration
279 | }
280 | }
281 |
282 | if (timelineAnimationEnabled) {
283 | if (movement.type === 'saccade') {
284 | // draw horizontal line
285 | return animateLine(getSaccadeData(movement), callback);
286 | } else if (movement.type === 'fixation') {
287 | // draw vertical line
288 | return animateLine(getFixationData(movement), callback);
289 | }
290 | return;
291 | }
292 | }
293 |
294 | /**
295 | *
296 | */
297 | function densityAnimation(movement, callback) {
298 | function getFixationData(movement) {
299 | return {
300 | group: densityGroup,
301 | x: (movement.x - 5),
302 | y: (movement.y + densityOffset),
303 | width: 10,
304 | height: movement.duration / 5,
305 | color: fixationColor,
306 | duration: movement.duration
307 | }
308 | }
309 |
310 | if (densityAnimationEnabled) {
311 | if (movement.type === 'fixation') {
312 | // draw rectangle
313 | return animateRectangle(getFixationData(movement), callback);
314 | }
315 | return;
316 | }
317 | }
318 |
319 | /**************************************************
320 | * UI Bindings *
321 | **************************************************/
322 |
323 | // Init tooltip
324 | $(function() {
325 | $('[data-toggle="tooltip"]').tooltip()
326 | })
327 |
328 | /* Start Animation */
329 | $('#demo').click(function() {
330 | $('#projectInfo').fadeOut(function() {
331 | $('#animationCanvas').fadeIn(function() {
332 | startAnimation();
333 | });
334 | });
335 | });
336 |
337 | /* Back */
338 | $('#back').click(function() {
339 | // FIXME Stop animation.
340 | stopped = true;
341 |
342 | // Clear all svg.
343 | d3.select("svg").remove();
344 |
345 | // Fade out text.
346 | $('#readText').html('').fadeOut();
347 |
348 | // Remove #demo from url.
349 | if (window.history.pushState) {
350 | window.history.pushState('', '/', window.location.pathname)
351 | } else {
352 | window.location.hash = '';
353 | }
354 |
355 | // Reset custom data.
356 | customData = {};
357 |
358 | $('#animationCanvas').fadeOut(function() {
359 | $('#projectInfo').fadeIn();
360 | });
361 | });
362 |
363 | /* Resume */
364 | $('#resume').click(function() {
365 | // TODO Resume animation.
366 | });
367 |
368 | /* Pause */
369 | $('#pause').click(function() {
370 | // TODO Pause animation.
371 | });
372 |
373 | /* Stop */
374 | $('#stop').click(function() {
375 | // TODO Stop animation.
376 | });
377 |
378 | /* Restart */
379 | $('#restart').click(function() {
380 | // TODO Restart animation.
381 | });
382 |
383 | $('#visualize').click(function() {
384 |
385 | try {
386 | // Read inputs.
387 | customData.userTrials = JSON.parse($('#trialData').val()) || [];
388 | customData.sentenceLine1 = $('#sentenceLine1').val() || '';
389 | customData.sentenceLine2 = $('#sentenceLine2').val() || '';
390 | customData.fontSize = parseInt($('#fontSize').val(), 10) || 14;
391 | customData.sentenceX = parseInt($('#sentenceX').val(), 10) || 20;
392 | customData.sentenceY = parseInt($('#sentenceY').val(), 10) || 180;
393 | customData.xFix = parseInt($('#xFix').val(), 10) || 0;
394 | customData.yFix = parseInt($('#yFix').val(), 10) || 0;
395 |
396 | console.log(customData);
397 |
398 | // Convert user input.
399 | convertCustomData();
400 |
401 | console.log(customData);
402 |
403 | } catch (e) {
404 | console.error(e);
405 | // TODO Show error messages to user.
406 | return;
407 | }
408 |
409 | window.location.hash = 'custom';
410 |
411 | // Close modal panel.
412 | $('#customVisModal').modal('hide');
413 |
414 | // Start animation.
415 | $('#projectInfo').fadeOut(function() {
416 | $('#animationCanvas').fadeIn(function() {
417 | startAnimation();
418 | });
419 | });
420 |
421 | });
422 |
423 | /**************************************************
424 | * Convert custom data *
425 | **************************************************/
426 |
427 | function convertCustomData() {
428 | var trials = [],
429 | trial,
430 | trialIndex = -1,
431 | previousEvent;
432 |
433 | customData.userTrials.forEach(function(data) {
434 | // Check new trial
435 | if (data.TRIAL_INDEX != trialIndex) {
436 | if (typeof trial != 'undefined') {
437 | // Push previous trial
438 | trials.push(trial);
439 | }
440 | // Create new trial
441 | trial = new Array();
442 | trialIndex = data.TRIAL_INDEX;
443 | }
444 |
445 | if (typeof previousEvent != 'undefined') {
446 | // Insert saccade
447 | trial.push({
448 | type: 'saccade',
449 | duration: previousEvent.NEXT_SAC_END_TIME - previousEvent.NEXT_SAC_START_TIME,
450 | x1: previousEvent.CURRENT_FIX_X,
451 | x2: data.CURRENT_FIX_X,
452 | y1: previousEvent.CURRENT_FIX_Y,
453 | y2: data.CURRENT_FIX_Y
454 | });
455 | }
456 |
457 | // Insert fixation event.
458 | trial.push({
459 | type: 'fixation',
460 | duration: data.CURRENT_FIX_DURATION,
461 | x: data.CURRENT_FIX_X,
462 | y: data.CURRENT_FIX_Y
463 | });
464 | previousEvent = data;
465 | });
466 |
467 | // Append last trial.
468 | trials.push(trial);
469 |
470 | // Set trials to customData
471 | customData.trials = trials;
472 |
473 | // Remove user input.
474 | //customData.userTrials = 'undefined';
475 | }
476 |
477 | // Calibrate data
478 | function calibrate(data) {
479 | if (data.calibrated) {
480 | return;
481 | }
482 |
483 | data.trials.forEach(function(trial) {
484 | trial.forEach(function(movement) {
485 | if (movement.type === 'fixation') {
486 | movement.x += data.xFix;
487 | movement.y += data.yFix;
488 | } else if (movement.type === 'saccade') {
489 | movement.x1 += data.xFix;
490 | movement.x2 += data.xFix;
491 | movement.y1 += data.yFix;
492 | movement.y2 += data.yFix;
493 | }
494 | });
495 | });
496 |
497 | // line2 fix
498 | if (typeof data.sentenceLine2StartOffset != 'undefined') {
499 | // For saccade, x1-x2 and y1-y2 must be negative, and difference must be bigger than the others.
500 | printLog('applying line2 fix');
501 |
502 | data.trials.forEach(function(trial) {
503 | var shift = false;
504 | trial.forEach(function(movement) {
505 | if (shift) {
506 | if (movement.type === 'fixation') {
507 | movement.x += data.sentenceLine2StartOffset /*+ data.xFix*/ ;
508 | } else if (movement.type === 'saccade') {
509 | movement.x1 += data.sentenceLine2StartOffset /*+ data.xFix*/ ;
510 | movement.x2 += data.sentenceLine2StartOffset /*+ data.xFix*/ ;
511 | }
512 | }
513 | if (movement.type === 'saccade') {
514 | if (movement.y1 - movement.y2 < 0 && movement.x1 - movement.x2 > 0 && movement.x1 - movement.x2 > 0.7 * data.sentenceLine2StartOffset) {
515 | printLog('shift started diff: ' + (movement.x1 - movement.x2));
516 | shift = true;
517 | movement.x2 += data.sentenceLine2StartOffset;
518 | }
519 | }
520 | });
521 | });
522 | }
523 |
524 | // Stabilize all y positions.
525 | data.trials.forEach(function(trial) {
526 | trial.forEach(function(movement) {
527 | if (movement.type === 'fixation') {
528 | movement.y = data.sentenceY - 30;
529 | } else if (movement.type === 'saccade') {
530 | movement.y1 = data.sentenceY - 30;
531 | movement.y2 = data.sentenceY - 30;
532 | }
533 | });
534 | });
535 |
536 | data.calibrated = true;
537 |
538 | console.log(data);
539 | }
540 |
541 | }
542 |
--------------------------------------------------------------------------------