From 1d70335b1aac5d5498604a4fdd578bc7b532c1d0 Mon Sep 17 00:00:00 2001 From: jos Date: Mon, 25 Aug 2014 11:10:48 +0200 Subject: [PATCH] Implemented animated range change for functions `fit`, `focus`, `setSelection`, and `setWindow`. --- HISTORY.md | 2 ++ docs/timeline.html | 26 ++++++++++---- lib/timeline/Core.js | 26 ++++++++++---- lib/timeline/Range.js | 76 ++++++++++++++++++++++++++++++++++------ lib/timeline/Timeline.js | 26 ++++++++++---- lib/util.js | 17 +++++++++ 6 files changed, 144 insertions(+), 29 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 40214d13..c3e656ae 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -15,6 +15,8 @@ http://visjs.org on screen. - Implemented an option `focus` for `setSelection(ids, options)`, to immediately focus selected nodes. +- Implemented animated range change for functions `fit`, `focus`, `setSelection`, + and `setWindow`. ### Network diff --git a/docs/timeline.html b/docs/timeline.html index aa0b89f3..93fc27fa 100644 --- a/docs/timeline.html +++ b/docs/timeline.html @@ -737,16 +737,23 @@ timeline.clear({options: true}); // clear options only - fit() + fit([options]) none Adjust the visible window such that it fits all items. See also function focus(id). + Available options: + - focus(id | ids) + focus(id | ids [, options]) none - Adjust the visible window such that the selected item (or multiple items) are centered on screen. See also function fit(). + Adjust the visible window such that the selected item (or multiple items) are centered on screen. See also function fit(). Available options: + @@ -829,19 +836,24 @@ timeline.clear({options: true}); // clear options only - setSelection([ids [, options]]) + setSelection(id | ids [, options]) none Select one or multiple items by their id. The currently selected items will be unselected. To unselect all selected items, call `setSelection([])`. Available options: - setWindow(start, end) + setWindow(start, end [, options]) none - Set the current visible window. The parameters start and end can be a Date, Number, or String. If the parameter value of start or end is null, the parameter will be left unchanged. + Set the current visible window. The parameters start and end can be a Date, Number, or String. If the parameter value of start or end is null, the parameter will be left unchanged. Available options: + + diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js index 7c33be50..2db38778 100644 --- a/lib/timeline/Core.js +++ b/lib/timeline/Core.js @@ -325,8 +325,14 @@ Core.prototype.clear = function(what) { /** * Set Core window such that it fits all items + * @param {Object} [options] Available options: + * `animate: boolean | number` + * If true (default), the range is animated + * smoothly to the new window. + * If a number, the number is taken as duration + * for the animation. Default duration is 500 ms. */ -Core.prototype.fit = function() { +Core.prototype.fit = function(options) { // apply the data range as range var dataRange = this.getItemRange(); @@ -348,7 +354,8 @@ Core.prototype.fit = function() { return; } - this.range.setRange(start, end); + var animate = (options && options.animate !== undefined) ? options.animate : true; + this.range.setRange(start, end, animate); }; @@ -363,15 +370,22 @@ Core.prototype.fit = function() { * object with properties start and end. * * @param {Date | Number | String | Object} [start] Start date of visible window - * @param {Date | Number | String} [end] End date of visible window + * @param {Date | Number | String} [end] End date of visible window + * @param {Object} [options] Available options: + * `animate: boolean | number` + * If true (default), the range is animated + * smoothly to the new window. + * If a number, the number is taken as duration + * for the animation. Default duration is 500 ms. */ -Core.prototype.setWindow = function(start, end) { +Core.prototype.setWindow = function(start, end, options) { + var animate = (options && options.animate !== undefined) ? options.animate : true; if (arguments.length == 1) { var range = arguments[0]; - this.range.setRange(range.start, range.end); + this.range.setRange(range.start, range.end, animate); } else { - this.range.setRange(start, end); + this.range.setRange(start, end, animate); } }; diff --git a/lib/timeline/Range.js b/lib/timeline/Range.js index 20e9abff..98349baa 100644 --- a/lib/timeline/Range.js +++ b/lib/timeline/Range.js @@ -35,6 +35,7 @@ function Range(body, options) { this.props = { touch: {} }; + this.animateTimer = null; // drag listeners for dragging this.body.emitter.on('dragstart', this._onDragStart.bind(this)); @@ -76,7 +77,7 @@ Range.prototype = new Component(); Range.prototype.setOptions = function (options) { if (options) { // copy the options that we know - var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable']; + var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', 'activate']; util.selectiveExtend(fields, this.options, options); if ('start' in options || 'end' in options) { @@ -101,16 +102,69 @@ function validateDirection (direction) { * Set a new start and end range * @param {Number} [start] * @param {Number} [end] + * @param {boolean | number} [animate=false] If true, the range is animated + * smoothly to the new window. + * If animate is a number, the + * number is taken as duration + * Default duration is 500 ms. + * */ -Range.prototype.setRange = function(start, end) { - var changed = this._applyRange(start, end); - if (changed) { - var params = { - start: new Date(this.start), - end: new Date(this.end) - }; - this.body.emitter.emit('rangechange', params); - this.body.emitter.emit('rangechanged', params); +Range.prototype.setRange = function(start, end, animate) { + this._cancelAnimation(); + + if (animate) { + var me = this; + var initStart = this.start; + var initEnd = this.end; + var duration = typeof animate === 'number' ? animate : 500; + var initTime = new Date().valueOf(); + var anyChanged = false; + function next() { + if (!me.props.touch.dragging) { + var now = new Date().valueOf(); + var time = now - initTime; + var s = util.easeInOutQuad(time, initStart, start, duration); + var e = util.easeInOutQuad(time, initEnd, end, duration); + changed = me._applyRange(s, e); + anyChanged = anyChanged || changed; + if (changed) { + me.body.emitter.emit('rangechange', {start: new Date(s), end: new Date(e)}); + } + + if (time <= duration) { + // animate with as high as possible frame rate, leave 20 ms in between + // each to prevent the browser from blocking + me.animateTimer = setTimeout(next, 20); + } + else { + // done + if (anyChanged) { + me.body.emitter.emit('rangechanged', {start: new Date(me.start), end: new Date(me.end)}); + } + } + } + } + + return next(); + } + else { + var changed = this._applyRange(start, end); + if (changed) { + var params = {start: new Date(this.start), end: new Date(this.end)}; + this.body.emitter.emit('rangechange', params); + this.body.emitter.emit('rangechanged', params); + } + } +}; + +/** + * Stop an animation + * @private + */ +Range.prototype._cancelAnimation = function () { + if (this.animateTimer) { + clearTimeout(this.animateTimer); + this.animateTimer = null; } }; @@ -284,6 +338,7 @@ Range.prototype._onDragStart = function(event) { this.props.touch.start = this.start; this.props.touch.end = this.end; + this.props.touch.dragging = true; if (this.body.dom.root) { this.body.dom.root.style.cursor = 'move'; @@ -327,6 +382,7 @@ Range.prototype._onDragEnd = function (event) { // when releasing the fingers in opposite order from the touch screen if (!this.props.touch.allowDragging) return; + this.props.touch.dragging = false; if (this.body.dom.root) { this.body.dom.root.style.cursor = 'auto'; } diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js index adb93774..a95c6f90 100644 --- a/lib/timeline/Timeline.js +++ b/lib/timeline/Timeline.js @@ -139,7 +139,7 @@ Timeline.prototype.setItems = function(items) { var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null; var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null; - this.setWindow(start, end); + this.setWindow(start, end, false); } }; @@ -172,14 +172,20 @@ Timeline.prototype.setGroups = function(groups) { * selected. If ids is an empty array, all items will be * unselected. * @param {Object} [options] Available options: - * `focus: boolean` If true, focus will be set - * to the selected item(s) + * `focus: boolean` + * If true, focus will be set to the selected item(s) + * `animate: boolean | number` + * If true (default), the range is animated + * smoothly to the new window. + * If a number, the number is taken as duration + * for the animation. Default duration is 500 ms. + * Only applicable when option focus is true. */ Timeline.prototype.setSelection = function(ids, options) { this.itemSet && this.itemSet.setSelection(ids); if (options && options.focus) { - this.focus(ids); + this.focus(ids, options); } }; @@ -195,8 +201,15 @@ Timeline.prototype.getSelection = function() { * Adjust the visible window such that the selected item (or multiple items) * are centered on screen. * @param {String | String[]} id An item id or array with item ids + * @param {Object} [options] Available options: + * `animate: boolean | number` + * If true (default), the range is animated + * smoothly to the new window. + * If a number, the number is taken as duration + * for the animation. Default duration is 500 ms. + * Only applicable when option focus is true */ -Timeline.prototype.focus = function(id) { +Timeline.prototype.focus = function(id, options) { if (!this.itemsData || id == undefined) return; var ids = Array.isArray(id) ? id : [id]; @@ -229,7 +242,8 @@ Timeline.prototype.focus = function(id) { var middle = (start + end) / 2; var interval = Math.max((this.range.end - this.range.start), (end - start) * 1.1); - this.range.setRange(middle - interval / 2, middle + interval / 2); + var animate = (options && options.animate !== undefined) ? options.animate : true; + this.range.setRange(middle - interval / 2, middle + interval / 2, animate); }; /** diff --git a/lib/util.js b/lib/util.js index f2a5fd4c..03fcbe2c 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1243,4 +1243,21 @@ exports.binarySearchGeneric = function(orderedItems, target, field, sidePreferen } } return guess; +}; + +/** + * Quadratic ease-in-out + * http://gizma.com/easing/ + * @param {number} t Current time + * @param {number} start Start value + * @param {number} end End value + * @param {number} duration Duration + * @returns {number} Value corresponding with current time + */ +exports.easeInOutQuad = function (t, start, end, duration) { + var change = end - start; + t /= duration/2; + if (t < 1) return change/2*t*t + start; + t--; + return -change/2 * (t*(t-2) - 1) + start; }; \ No newline at end of file