├── 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 | 41 | | 42 | 43 | 44 | 45 | 46 | | 47 | 48 | 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 | --------------------------------------------------------------------------------