1 define([ 2 'jquery', 3 'underscore', 4 'view', 5 'viewcontroller', 6 'animationdirector', 7 'draw', 8 'color-editor', 9 'colorviewcontroller' 10 ], function($, _, DecompositionView, ViewControllers, AnimationDirector, 11 draw, Color, ColorViewController) { 12 var EmperorViewController = ViewControllers.EmperorViewController; 13 var drawTrajectoryLineStatic = draw.drawTrajectoryLineStatic; 14 var drawTrajectoryLineDynamic = draw.drawTrajectoryLineDynamic; 15 var disposeTrajectoryLineStatic = draw.disposeTrajectoryLineStatic; 16 var disposeTrajectoryLineDynamic = draw.disposeTrajectoryLineDynamic; 17 var updateStaticTrajectoryDrawRange = draw.updateStaticTrajectoryDrawRange; 18 var ColorEditor = Color.ColorEditor, ColorFormatter = Color.ColorFormatter; 19 20 /** 21 * @class AnimationsController 22 * 23 * Controls the axes that are displayed on screen as well as their 24 * orientation. 25 * 26 * @param {UIState} uiState The shared state 27 * @param {Node} container Container node to create the controller in. 28 * @param {Object} decompViewDict This is object is keyed by unique 29 * identifiers and the values are DecompositionView objects referring to a 30 * set of objects presented on screen. This dictionary will usually be shared 31 * by all the tabs in the application. This argument is passed by reference. 32 * 33 * @return {AnimationsController} 34 * @constructs AnimationsController 35 * @extends EmperorViewController 36 */ 37 function AnimationsController(uiState, container, decompViewDict) { 38 var helpmenu = 'Animate trajectories connecting samples in your data'; 39 var title = 'Animations'; 40 var scope = this, dm, label, gradientTooltip, trajectoryTooltip; 41 EmperorViewController.call(this, uiState, container, title, helpmenu, 42 decompViewDict); 43 44 trajectoryTooltip = 'Category to group samples'; 45 gradientTooltip = 'Category to sort samples'; 46 47 dm = this.getView().decomp; 48 49 this.$gradientSelect = $("<select class='emperor-tab-drop-down'>"); 50 this.$trajectorySelect = $("<select class='emperor-tab-drop-down'>"); 51 52 // http://stackoverflow.com/a/6602002 53 // prepend an empty string so dropdown shows the "tooltip string" 54 _.each([''].concat(dm.md_headers), function(header) { 55 scope.$gradientSelect.append( 56 $('<option>').attr('value', header).text(header)); 57 scope.$trajectorySelect.append( 58 $('<option>').attr('value', header).text(header)); 59 }); 60 61 // add a label to the chosen drop downs 62 label = $('<label>').text('Gradient').append(this.$gradientSelect); 63 label.attr('title', gradientTooltip); 64 this.$header.append(label); 65 66 label = $('<label>').text('Trajectory').append(this.$trajectorySelect); 67 label.attr('title', trajectoryTooltip); 68 this.$header.append(label); 69 70 // container of the sliders and buttons 71 this._$mediaContainer = $('<div name="media-controls-container"></div>'); 72 this._$mediaContainer.css({'padding-top': '10px', 73 'width': 'inherit', 74 'text-align': 'center'}); 75 this.$body.append(this._$mediaContainer); 76 77 this.$rewind = $('<button></button>'); 78 this._$mediaContainer.append(this.$rewind); 79 80 this.$play = $('<button></button>'); 81 this._$mediaContainer.append(this.$play); 82 83 this.$pause = $('<button></button>'); 84 this._$mediaContainer.append(this.$pause); 85 86 this._colors = {}; 87 this._currentFrame = 0; 88 89 // make the buttons squared 90 this._$mediaContainer.find('button').css({'width': '30px', 91 'height': '30px', 92 'margin': '0 auto', 93 'margin-left': '10px', 94 'margin-right': '10px'}); 95 96 this._$mediaContainer.append($('<hr>')); 97 98 this._$speedLabel = $('<text name="speed">Speed: 1x</text>'); 99 this._$speedLabel.attr('title', 'Speed at which the traces animate'); 100 this._$mediaContainer.append(this._$speedLabel); 101 102 this.$speed = $('<div></div>').css('margin-top', '10px'); 103 this.$speed.attr('title', 'Speed at which the traces animate'); 104 this._$mediaContainer.append(this.$speed); 105 106 this._$radiusLabel = $('<text name="radius">Radius: 1</text>'); 107 this._$radiusLabel.attr('title', 'Radius of the traces'); 108 this._$mediaContainer.append(this._$radiusLabel); 109 110 this.$radius = $('<div></div>').css('margin-top', '10px'); 111 this.$radius.attr('title', 'Radius of the animated traces'); 112 this._$mediaContainer.append(this.$radius); 113 114 this.$gridDiv = $('<div name="emperor-grid-div"></div>'); 115 this.$gridDiv.css('margin', '0 auto'); 116 this.$gridDiv.css('width', 'inherit'); 117 this.$gridDiv.css('height', '100%'); 118 this.$gridDiv.attr('title', 'Change the color of the animated traces.'); 119 this.$body.append(this.$gridDiv); 120 121 this.director = null; 122 this.playing = false; 123 124 /** 125 * @type {Slick.Grid} 126 * Container that lists the trajectories and their colors 127 */ 128 this._grid = null; 129 130 // initialize interface elements here 131 $(this).ready(function() { 132 scope.$speed.slider({'min': 0.01, 133 'max': 10, 134 'step': 0.05, 135 'value': 1, 136 'range': 'max', 137 'slide': function(event, ui) { 138 scope._$speedLabel.text('Speed: ' + ui.value + 139 'x'); 140 }, 141 'change': function(event, ui) { 142 scope._$speedLabel.text('Speed: ' + ui.value + 143 'x'); 144 }}); 145 scope.$speed.css('background', '#70caff'); 146 147 scope.$radius.slider({'min': 0.01, 148 'max': 10, 149 'step': 0.05, 150 'value': 1, 151 'range': 'max', 152 'slide': function(event, ui) { 153 scope._$radiusLabel.text('Radius: ' + ui.value); 154 }, 155 'change': function(event, ui) { 156 scope._$radiusLabel.text('Radius: ' + ui.value); 157 }}); 158 scope.$radius.css('background', '#70caff'); 159 160 // once this element is ready, it is safe to execute the "ready" callback 161 // if a subclass needs to wait on other elements, this attribute should 162 // be changed to null so this callback is effectively cancelled, for an 163 // example see the constructor of ColorViewController 164 scope.$trajectorySelect.on('chosen:ready', function() { 165 if (scope.ready !== null) { 166 scope.ready(); 167 } 168 }); 169 170 // setup chosen 171 scope.$gradientSelect.chosen({ 172 width: '100%', 173 search_contains: true, 174 placeholder_text_single: gradientTooltip 175 }); 176 scope.$trajectorySelect.chosen({ 177 width: '100%', 178 search_contains: true, 179 placeholder_text_single: trajectoryTooltip 180 }); 181 182 scope.$gradientSelect.chosen().change(function(e, p) { 183 scope._gradientChanged(e, p); 184 }); 185 scope.$trajectorySelect.chosen().change(function(e, p) { 186 scope._trajectoryChanged(e, p); 187 }); 188 189 scope.$rewind.button({icons: {primary: 'ui-icon-seek-first'}}); 190 scope.$rewind.attr('title', 'Restart the animation'); 191 scope.$rewind.on('click', function() { 192 scope._rewindButtonClicked(); 193 }); 194 195 scope.$play.button({icons: {primary: 'ui-icon-play'}}); 196 scope.$play.attr('title', 'Start the animation'); 197 scope.$play.on('click', function() { 198 scope._playButtonClicked(); 199 }); 200 201 scope.$pause.button({icons: {primary: 'ui-icon-pause'}}); 202 scope.$pause.attr('title', 'Pause the animation'); 203 scope.$pause.on('click', function() { 204 scope._pauseButtonClicked(); 205 }); 206 207 scope._buildGrid(); 208 209 scope.setEnabled(false); 210 211 //Note that we can't do this before the buttons are ready. 212 scope.UIState.registerProperty('view.viewType', 213 scope._viewTypeChanged.bind(scope)); 214 }); 215 216 return this; 217 } 218 AnimationsController.prototype = Object.create( 219 EmperorViewController.prototype); 220 AnimationsController.prototype.constructor = EmperorViewController; 221 222 /** 223 * Get the colors for the trajectories 224 * 225 * @return {Object} Returns the object mapping trajectories to colors. 226 */ 227 AnimationsController.prototype.getColors = function() { 228 return this._colors; 229 }; 230 231 /** 232 * Set the colors of the trajectories 233 * 234 * @param {Object} colors Mapping between trajectories and colors. 235 */ 236 AnimationsController.prototype.setColors = function(colors) { 237 this._colors = colors; 238 239 var data = []; 240 for (var value in colors) { 241 data.push({category: value, value: colors[value]}); 242 } 243 244 this._grid.setData(data); 245 this._grid.invalidate(); 246 this._grid.render(); 247 }; 248 249 /** 250 * Callback when a row's color changes 251 * 252 * See _buildGrid for information about the arguments. 253 * 254 * @private 255 */ 256 AnimationsController.prototype._colorChanged = function(e, args) { 257 this._colors[args.item.category] = args.item.value; 258 }; 259 260 /** 261 * Helper method to create a grid and set it up for the traces 262 * 263 * @private 264 */ 265 AnimationsController.prototype._buildGrid = function() { 266 var scope = this, columns, gridOptions; 267 268 columns = [ 269 {id: 'title', name: '', field: 'value', sortable: false, maxWidth: 25, 270 minWidth: 25, editor: ColorEditor, formatter: ColorFormatter}, 271 {id: 'field1', name: '', field: 'category'} 272 ]; 273 274 // autoEdit enables one-click editor trigger on the entire grid, instead 275 // of requiring users to click twice on a widget. 276 gridOptions = {editable: true, enableAddRow: false, autoEdit: true, 277 enableCellNavigation: true, forceFitColumns: true, 278 enableColumnReorder: false}; 279 280 this._grid = new Slick.Grid(this.$gridDiv, [], columns, gridOptions); 281 282 // hide the header row of the grid 283 // http://stackoverflow.com/a/29827664/379593 284 this.$body.find('.slick-header').css('display', 'none'); 285 286 // subscribe to events when a cell is changed 287 this._grid.onCellChange.subscribe(function(e, args) { 288 scope._colorChanged(e, args); 289 }); 290 }; 291 292 /** 293 * Sets whether or not the tab can be modified or accessed. 294 * 295 * @param {boolean} trulse option to enable tab. 296 */ 297 AnimationsController.prototype.setEnabled = function(trulse) { 298 EmperorViewController.prototype.setEnabled.call(this, trulse); 299 this._updateButtons(); 300 }; 301 302 /** 303 * Resizes the container and the individual elements. 304 * 305 * Note, the consumer of this class, likely the main controller should call 306 * the resize function any time a resizing event happens. 307 * 308 * @param {Float} width the container width. 309 * @param {Float} height the container height. 310 */ 311 AnimationsController.prototype.resize = function(width, height) { 312 // call super, most of the header and body resizing logic is done there 313 EmperorViewController.prototype.resize.call(this, width, height); 314 315 this.$body.height(this.$canvas.height() - this.$header.height()); 316 this.$body.width(this.$canvas.width()); 317 318 var grid = this.$canvas.height(); 319 grid -= this.$header.height() + this._$mediaContainer.height(); 320 this.$gridDiv.height(grid); 321 322 // the whole code is asynchronous, so there may be situations where 323 // _grid doesn't exist yet, so check before trying to modify the object 324 if (this._grid !== null) { 325 // make the columns fit the available space whenever the window resizes 326 // http://stackoverflow.com/a/29835739 327 this._grid.setColumns(this._grid.getColumns()); 328 // Resize the slickgrid canvas for the new body size. 329 this._grid.resizeCanvas(); 330 } 331 }; 332 333 /** 334 * 335 * Helper method to update what media buttons should be enabled 336 * 337 * @private 338 */ 339 AnimationsController.prototype._updateButtons = function() { 340 var play, pause, speed, rewind; 341 342 /* 343 * 344 * The behavior of the media buttons is a bit complicated. It is explained 345 * by the following truth table where the variables are "director", 346 * "playing" and "enabled". Each output's value determines if the button 347 * should be enabled. Note that we negate the values when we make the 348 * assignment because jQuery only has a "disabled" method. 349 * 350 * ||----------|---------|---------||-------|-------|-------|--------| 351 * || director | playing | enabled || Play | Speed | Pause | Rewind | 352 * || | | || | Radius| | | 353 * || | | || | Colors| | | 354 * ||----------|---------|---------||-------|-------|-------|--------| 355 * || FALSE | FALSE | FALSE || FALSE | FALSE | FALSE | FALSE | 356 * || FALSE | FALSE | TRUE || TRUE | TRUE | FALSE | FALSE | 357 * || FALSE | TRUE | FALSE || FALSE | FALSE | FALSE | FALSE | 358 * || FALSE | TRUE | TRUE || FALSE | FALSE | FALSE | FALSE | 359 * || TRUE | FALSE | FALSE || FALSE | FALSE | FALSE | FALSE | 360 * || TRUE | FALSE | TRUE || TRUE | FALSE | FALSE | TRUE | 361 * || TRUE | TRUE | FALSE || FALSE | FALSE | FALSE | FALSE | 362 * || TRUE | TRUE | TRUE || FALSE | FALSE | TRUE | TRUE | 363 * ||----------|---------|---------||-------|-------|-------|--------| 364 * 365 */ 366 play = ((this.enabled && this.director === null && !this.playing) || 367 (this.enabled && this.director !== null && !this.playing)); 368 369 pause = this.director !== null && this.enabled && this.playing; 370 371 speed = this.director === null && !this.playing && this.enabled; 372 373 rewind = this.director !== null && this.enabled; 374 375 this.$speed.slider('option', 'disabled', !speed); 376 this.$radius.slider('option', 'disabled', !speed); 377 378 // jquery ui requires a manual refresh of to the UI after state changes 379 this.$play.prop('disabled', !play).button('refresh'); 380 this.$pause.prop('disabled', !pause).button('refresh'); 381 this.$rewind.prop('disabled', !rewind).button('refresh'); 382 383 this._grid.setOptions({editable: speed}); 384 }; 385 386 /** 387 * 388 * Helper method to update a grid. 389 * 390 * @private 391 */ 392 AnimationsController.prototype._updateGrid = function() { 393 var category = this.getTrajectoryCategory(), colors, values; 394 395 values = this.getView().decomp.getUniqueValuesByCategory(category); 396 colors = ColorViewController.getColorList(values, 397 'discrete-coloring-qiime', 398 true, false)[0]; 399 400 this.setColors(colors); 401 this.resize(); 402 }; 403 404 /** 405 * 406 * Callback method executed when the Gradient menu changes. 407 * 408 * @private 409 */ 410 AnimationsController.prototype._gradientChanged = function(evt, params) { 411 if (this.getGradientCategory() !== '' && 412 this.getTrajectoryCategory() !== '' && 413 this.UIState['view.viewType'] === 'scatter') { 414 this.setEnabled(true); 415 this._updateGrid(); 416 } 417 else if (this.getGradientCategory() === '' || 418 this.getTrajectoryCategory() === '' || 419 this.UIState['view.viewType'] !== 'scatter') { 420 this.setEnabled(false); 421 } 422 }; 423 424 /** 425 * 426 * Callback method executed when the Trajectory menu changes. 427 * 428 * @private 429 */ 430 AnimationsController.prototype._trajectoryChanged = function(evt, params) { 431 if (this.getGradientCategory() !== '' && 432 this.getTrajectoryCategory() !== '' && 433 this.UIState['view.viewType'] === 'scatter') { 434 this.setEnabled(true); 435 this._updateGrid(); 436 } 437 else if (this.getGradientCategory() === '' || 438 this.getTrajectoryCategory() === '' || 439 this.UIState['view.viewType'] !== 'scatter') { 440 this.setColors({}); 441 this.setEnabled(false); 442 } 443 }; 444 445 /** 446 * 447 * Callback method executed when the UIState view.viewType changes. 448 * 449 * @private 450 */ 451 AnimationsController.prototype._viewTypeChanged = function(evt) { 452 if (this.getGradientCategory() !== '' && 453 this.getTrajectoryCategory() !== '' && 454 this.UIState['view.viewType'] === 'scatter') { 455 this.setEnabled(true); 456 this._updateGrid(); 457 } 458 else if (this.getGradientCategory() === '' || 459 this.getTrajectoryCategory() === '' || 460 this.UIState['view.viewType'] !== 'scatter') { 461 this.setEnabled(false); 462 } 463 }; 464 465 /** 466 * 467 * Callback method executed when the Rewind button is clicked. 468 * 469 * @private 470 */ 471 AnimationsController.prototype._rewindButtonClicked = function(evt, params) { 472 var view = this.getView(); 473 474 this.playing = false; 475 this.director = null; 476 477 view.staticTubes.forEach(function(tube) { 478 if (tube !== null && tube.parent !== null) { 479 tube.parent.remove(tube); 480 disposeTrajectoryLineStatic(tube); 481 } 482 }); 483 view.dynamicTubes.forEach(function(tube) { 484 if (tube !== null && tube.parent !== null) { 485 tube.parent.remove(tube); 486 disposeTrajectoryLineDynamic(tube); 487 } 488 }); 489 490 view.staticTubes = []; 491 view.dynamicTubes = []; 492 493 view.needsUpdate = true; 494 495 this._updateButtons(); 496 497 this.dispatchEvent({type: 'animation-cancelled', message: { 498 gradient: this.getGradientCategory(), 499 trajectory: this.getTrajectoryCategory(), 500 controller: this 501 }}); 502 }; 503 504 /** 505 * 506 * Callback method when the Pause button is clicked. 507 * 508 * @private 509 */ 510 AnimationsController.prototype._pauseButtonClicked = function(evt, params) { 511 if (this.playing) { 512 this.playing = false; 513 } 514 this._updateButtons(); 515 516 this.dispatchEvent({type: 'animation-paused', message: { 517 gradient: this.getGradientCategory(), 518 trajectory: this.getTrajectoryCategory(), 519 controller: this 520 }}); 521 522 }; 523 524 /** 525 * 526 * Callback method when the Play button is clicked. 527 * 528 * @private 529 */ 530 AnimationsController.prototype._playButtonClicked = function(evt, params) { 531 532 if (this.playing === false && this.director !== null) { 533 this.playing = true; 534 this._updateButtons(); 535 return; 536 } 537 538 var headers, data = {}, positions = {}, gradient, trajectory, decomp, p; 539 var view, marker, pos, speed; 540 541 view = this.getView(); 542 decomp = this.getView().decomp; 543 headers = decomp.md_headers; 544 545 // get the current visible dimensions 546 var x = view.visibleDimensions[0], y = view.visibleDimensions[1], 547 z = view.visibleDimensions[2]; 548 var is2D = (z === null || z === undefined); 549 550 gradient = this.$gradientSelect.val(); 551 trajectory = this.$trajectorySelect.val(); 552 553 speed = this.getSpeed(); 554 555 for (var i = 0; i < decomp.plottable.length; i++) { 556 p = decomp.plottable[i]; 557 558 data[p.name] = p.metadata; 559 560 // get the view's position, not the metadata's position 561 positions[p.name] = { 562 'name': p.name, 'color': 0, 563 'x': p.coordinates[x] * view.axesOrientation[0], 564 'y': p.coordinates[y] * view.axesOrientation[1], 565 'z': is2D ? 0 : (p.coordinates[z] * view.axesOrientation[2]) 566 }; 567 } 568 569 this.director = new AnimationDirector(headers, data, positions, gradient, 570 trajectory, speed); 571 572 this.director.updateFrame(); 573 this._currentFrame = 0; 574 575 this.playing = true; 576 this._updateButtons(); 577 578 this.dispatchEvent({type: 'animation-started', message: { 579 gradient: this.getGradientCategory(), 580 trajectory: this.getTrajectoryCategory(), 581 controller: this 582 }}); 583 }; 584 585 /** 586 * 587 * Update the portion of the trajectory that needs to be drawn. 588 * 589 * If the animation is not playing (because it was paused or it has finished) 590 * or a director hasn't been instantiated, no action is taken. Otherwise, 591 * trajectories are updated on screen. 592 * 593 */ 594 AnimationsController.prototype.drawFrame = function() { 595 if (this.director === null || this.director.animationCycleFinished() || 596 !this.playing) { 597 return; 598 } 599 600 var view = this.getView(), tube, scope = this, color; 601 602 var radius = view.getGeometryFactor(); 603 radius *= 0.45 * this.getRadius(); 604 605 for (var i = 0; i < this.director.trajectories.length; i++) { 606 var trajectory = this.director.trajectories[i]; 607 608 //Ensure static tubes are constructed 609 if (view.staticTubes[i] === null || view.staticTubes[i] === undefined) 610 { 611 var color = this._colors[trajectory.metadataCategoryName] || 'red'; 612 view.staticTubes[i] = drawTrajectoryLineStatic(trajectory, 613 color, 614 radius); 615 } 616 617 //Ensure static tube draw ranges are set to visible segment 618 updateStaticTrajectoryDrawRange(trajectory, this.director.currentFrame, 619 view.staticTubes[i]); 620 } 621 622 //Remove any old dynamic tubes from the scene 623 view.dynamicTubes.forEach(function(tube) { 624 if (tube === undefined || tube === null) { 625 return; 626 } 627 if (tube.parent !== null) { 628 tube.parent.remove(tube); 629 disposeTrajectoryLineDynamic(tube); 630 } 631 }); 632 633 if (this.UIState['view.viewType'] !== 'parallel-plot') { 634 //Construct new dynamic tubes containing necessary 635 //interpolated segment for the current frame 636 view.dynamicTubes = this.director.trajectories.map(function(trajectory) { 637 var color = scope._colors[trajectory.metadataCategoryName] || 'red'; 638 var tube = drawTrajectoryLineDynamic(trajectory, 639 scope.director.currentFrame, 640 color, 641 radius); 642 return tube; 643 }); 644 } 645 646 view.needsUpdate = true; 647 648 if (this.director.currentFrameIsGradientPoint()) { 649 this.dispatchEvent({type: 'animation-new-frame-started', message: { 650 frame: this._currentFrame, 651 gradientPoint: this.director.gradientPoints[this._currentFrame], 652 controller: this 653 }}); 654 655 this._currentFrame += 1; 656 } 657 658 659 this.director.updateFrame(); 660 661 if (this.director.animationCycleFinished()) { 662 this.director = null; 663 this.playing = false; 664 665 // When the animation cycle finishes, update the state of the media 666 // buttons and re-enable the rewind button so users can clear the 667 // screen. 668 this._updateButtons(); 669 this.$rewind.prop('disabled', false).button('refresh'); 670 671 this.dispatchEvent({type: 'animation-ended', message: { 672 gradient: this.getGradientCategory(), 673 trajectory: this.getTrajectoryCategory(), 674 controller: this 675 }}); 676 677 } 678 }; 679 680 /** 681 * 682 * Setter for the gradient category 683 * 684 * Represents how samples are ordered in each trajectory. 685 * 686 * @param {String} category The name of the category to set in the menu. 687 */ 688 AnimationsController.prototype.setGradientCategory = function(category) { 689 if (!this.hasMetadataField(category)) { 690 category = ''; 691 } 692 693 this.$gradientSelect.val(category); 694 this.$gradientSelect.trigger('chosen:updated'); 695 this.$gradientSelect.change(); 696 }; 697 698 /** 699 * 700 * Getter for the gradient category 701 * 702 * Represents how samples are ordered in each trajectory. 703 * 704 * @return {String} The name of the gradient category in the menu. 705 */ 706 AnimationsController.prototype.getGradientCategory = function() { 707 return this.$gradientSelect.val(); 708 }; 709 710 /** 711 * 712 * Setter for the trajectory category 713 * 714 * Represents how samples are grouped together. 715 * 716 * @param {String} category The name of the category to set in the menu. 717 */ 718 AnimationsController.prototype.setTrajectoryCategory = function(category) { 719 if (!this.hasMetadataField(category)) { 720 category = ''; 721 } 722 723 this.$trajectorySelect.val(category); 724 this.$trajectorySelect.trigger('chosen:updated'); 725 this.$trajectorySelect.change(); 726 }; 727 728 /** 729 * 730 * Getter for the trajectory category 731 * 732 * Represents how samples are grouped together. 733 * 734 * @return {String} The name of the trajectory category in the menu. 735 */ 736 AnimationsController.prototype.getTrajectoryCategory = function() { 737 return this.$trajectorySelect.val(); 738 }; 739 740 /** 741 * 742 * Setter for the speed of the animation. 743 * 744 * @param {Float} speed Speed at which the animation is played. 745 * @throws {Error} If the radius value is lesser than or equal to 0 or 746 * greater than 10. 747 */ 748 AnimationsController.prototype.setSpeed = function(speed) { 749 if (speed <= 0 || speed > 10) { 750 throw new Error('The speed must be greater than 0 and lesser than 10'); 751 } 752 this.$speed.slider('option', 'value', speed); 753 }; 754 755 /** 756 * 757 * Getter for the speed of the animation. 758 * 759 * @return {Float} Speed at which the animation is played. 760 */ 761 AnimationsController.prototype.getSpeed = function() { 762 return this.$speed.slider('option', 'value'); 763 }; 764 765 /** 766 * 767 * Setter for the radius of the animation. 768 * 769 * @param {Float} radius Radius of the traces in the animations. 770 * @throws {Error} If the radius value is lesser than or equal to 0 or 771 * greater than 10. 772 */ 773 AnimationsController.prototype.setRadius = function(radius) { 774 if (radius <= 0 || radius > 10) { 775 throw new Error('The radius must be greater than 0 and lesser than 10'); 776 } 777 this.$radius.slider('option', 'value', radius); 778 }; 779 780 /** 781 * 782 * Getter for the radius of the traces in the animation. 783 * 784 * @return {Float} Radius of the traces in the animation 785 */ 786 AnimationsController.prototype.getRadius = function() { 787 return this.$radius.slider('option', 'value'); 788 }; 789 790 /** 791 * Converts the current instance into a JSON string. 792 * 793 * @return {Object} JSON ready representation of self. 794 */ 795 AnimationsController.prototype.toJSON = function() { 796 var json = {}; 797 798 json.gradientCategory = this.getGradientCategory(); 799 json.trajectoryCategory = this.getTrajectoryCategory(); 800 json.speed = this.getSpeed(); 801 json.radius = this.getRadius(); 802 json.colors = this.getColors(); 803 804 return json; 805 }; 806 807 /** 808 * Decodes JSON string and modifies its own instance variables accordingly. 809 * 810 * @param {Object} Parsed JSON string representation of self. 811 */ 812 AnimationsController.prototype.fromJSON = function(json) { 813 this._rewindButtonClicked(); 814 815 this.setGradientCategory(json.gradientCategory); 816 this.setTrajectoryCategory(json.trajectoryCategory); 817 818 this.setSpeed(json.speed); 819 this.setRadius(json.radius); 820 821 this.setColors(json.colors); 822 }; 823 824 return AnimationsController; 825 }); 826