Selaa lähdekoodia

refactoring: cleanup, methods are exracted, changed rendering settings. added unittest.

Ievgen Naida 5 vuotta sitten
vanhempi
commit
8522fb6c4e

+ 8 - 0
README.md

@@ -134,6 +134,14 @@ to pack JavaScript as a bundle:
   npm run build
 ```
 
+### Debug
+
+VSCode can be used to debug the component with the next extensions:
+* Debugger for Chrome 
+* Live HTML PReviewer.
+
+Also embedded chrome debugger can be used when demo page is running. 
+
 ### Build Tests
 To build TypeScript unittests command should be executed: 
 ```bash

+ 314 - 185
lib/animation-timeline.js

@@ -317,10 +317,14 @@ var TimelineUtils = /*#__PURE__*/function () {
       ctx.moveTo(x1, y1);
       ctx.lineTo(x2, y2);
     }
+    /**
+     * Check is valid number.
+     */
+
   }, {
     key: "isNumber",
     value: function isNumber(val) {
-      if (typeof val === 'number' && !isNaN(val)) {
+      if (typeof val === 'number' && !isNaN(val) && Number.isFinite(val)) {
         return true;
       }
 
@@ -362,6 +366,11 @@ var TimelineUtils = /*#__PURE__*/function () {
     key: "findGoodStep",
     value: function findGoodStep(originalStep) {
       var divisionCheck = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
+
+      if (originalStep <= 0 || isNaN(originalStep) || !Number.isFinite(originalStep)) {
+        return originalStep;
+      }
+
       var step = originalStep;
       var lastDistance = null;
       var pow = TimelineUtils.getPowArgument(originalStep);
@@ -388,6 +397,57 @@ var TimelineUtils = /*#__PURE__*/function () {
 
       return step;
     }
+    /**
+     * Keep value in min, max bounds.
+     */
+
+  }, {
+    key: "keepInBounds",
+    value: function keepInBounds(value) {
+      var min = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
+      var max = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
+
+      if (TimelineUtils.isNumber(value)) {
+        if (TimelineUtils.isNumber(min)) {
+          value = Math.max(value, min);
+        }
+
+        if (TimelineUtils.isNumber(max)) {
+          value = Math.min(value, max);
+        }
+      }
+
+      return value;
+    }
+  }, {
+    key: "setMinMax",
+    value: function setMinMax(to, from) {
+      var shrink = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+
+      if (!from || !to) {
+        return to;
+      }
+
+      var isFromMinNumber = TimelineUtils.isNumber(from.min);
+      var isToMinNumber = TimelineUtils.isNumber(to.min); // get absolute min and max bounds:
+
+      if (isFromMinNumber && isToMinNumber) {
+        to.min = shrink ? Math.min(from.min, to.min) : Math.max(from.min, to.min);
+      } else if (isFromMinNumber) {
+        to.min = from.min;
+      }
+
+      var isFromMaxNumber = TimelineUtils.isNumber(from.max);
+      var isToMaxNumber = TimelineUtils.isNumber(to.max);
+
+      if (isFromMaxNumber && isToMaxNumber) {
+        to.max = shrink ? Math.max(from.max, to.max) : Math.min(from.max, to.max);
+      } else if (isFromMaxNumber) {
+        to.max = from.max;
+      }
+
+      return to;
+    }
   }, {
     key: "isRectOverlap",
     value: function isRectOverlap(rect, rect2) {
@@ -989,12 +1049,6 @@ var defaultTimelineRowStyle = {
   keyframesStyle: defaultTimelineKeyframeStyle
 };
 var defaultTimelineOptions = {
-  /**
-   * Snap the mouse to the values on a timeline.
-   * Value can be from 1 to 60
-   */
-  snapsPerSeconds: 5,
-
   /**
    *  Snap all selected keyframes as a bundle during the drag.
    */
@@ -1010,8 +1064,17 @@ var defaultTimelineOptions = {
    * approximate step for the timeline in pixels for 1 second
    */
   stepPx: 120,
+
+  /**
+   * Number of units that should fit into one stepPx. (1 second by a default)
+   */
+  stepVal: 1000,
   stepSmallPx: 30,
-  smallSteps: 50,
+
+  /**
+   * Snap step in units. from 0 to stepVal
+   */
+  snapStep: 200,
 
   /**
    * additional left margin in pixels to start the line gauge from.
@@ -1042,13 +1105,26 @@ var defaultTimelineOptions = {
    */
   headerHeight: 30,
   font: '11px sans-serif',
-  zoom: 1000,
-  // Zoom speed. Use percent of the screen to set zoom speed.
+
+  /**
+   * Default zoom level = 1. where screen pixels are equals to the corresponding stepVal stepPx.
+   */
+  zoom: 1,
+
+  /**
+   * Default zoom speed.
+   */
   zoomSpeed: 0.1,
-  // Max zoom
-  zoomMin: 80,
-  // Min zoom
-  zoomMax: 8000,
+
+  /**
+   * Max zoom value.
+   */
+  zoomMin: 0.1,
+
+  /**
+   * Min zoom value.
+   */
+  zoomMax: 8,
 
   /**
    * Set this to true in a MAC OS environment: The Meta key will be used instead of the Ctrl key.
@@ -1513,7 +1589,7 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       if (_this._startPos) {
         if (isLeftClicked || isTouch) {
           if (_this._drag && !_this._startedDragWithCtrl) {
-            var convertedVal = _this._mousePosToVal(_this._currentPos.x, true);
+            var convertedVal = _this._currentPos.val;
 
             if (_this._drag.type === TimelineElementType.Timeline) {
               _this._setTimeInternal(convertedVal, TimelineEventSource.User);
@@ -1614,7 +1690,7 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
             _this._zoom(direction, _this._options.zoomSpeed, mousePos);
           } else {
-            _this._performClick(pos, args, _this._drag);
+            _this._performClick(pos, _this._drag);
           }
         } else if (!_this._drag && _this._selectionRect && _this._selectionRectEnabled) {
           if (_this._interactionMode === TimelineInteractionMode.Zoom) {
@@ -1641,7 +1717,7 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       } // Rescale when animation is played out of the bounds.
 
 
-      if (_this.valToPx(_this._val, true) > _this._scrollContainer.scrollWidth) {
+      if (_this.valToPx(_this._val) > _this._scrollContainer.scrollWidth) {
         _this.rescale();
 
         if (!_this._isPanStarted && _this._drag && _this._drag.type !== TimelineElementType.Timeline) {
@@ -1687,10 +1763,23 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
         throw new Error("Element cannot be empty. Should be string or DOM element.");
       }
 
-      var id = options.id;
-      this._options = this._mergeOptions(options);
-      this._currentZoom = this._options.zoom;
+      this._generateContainers(options.id);
+
+      this._options = this._setOptions(options);
 
+      this._subscribeOnEvents();
+
+      this.rescale();
+      this.redraw();
+    }
+    /**
+     * Generate component html.
+     * @param id container.
+     */
+
+  }, {
+    key: "_generateContainers",
+    value: function _generateContainers(id) {
       if (id instanceof HTMLElement) {
         this._container = id;
       } else {
@@ -1713,9 +1802,6 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       this._container.style.position = 'relative'; // Generate size container:
 
       this._canvas.style.cssText = 'image-rendering: -moz-crisp-edges;' + 'image-rendering: -webkit-crisp-edges;' + 'image-rendering: pixelated;' + 'image-rendering: crisp-edges;' + 'user-select: none;' + '-webkit-user-select: none;' + '-khtml-user-select: none;' + '-moz-user-select: none;' + '-o-user-select: none;' + 'user-select: none;' + 'touch-action: none;' + 'position: relative;' + '-webkit-user-drag: none;' + '-khtml-user-drag: none;' + '-moz-user-drag: none;' + '-o-user-drag: none;' + 'user-drag: none;' + 'padding: inherit';
-
-      this._scrollContainer.classList.add(this._options.scrollContainerClass);
-
       this._scrollContainer.style.cssText = 'overflow: scroll;' + 'position: absolute;' + 'width:  100%;' + 'height:  100%;';
       this._scrollContent.style.width = this._scrollContent.style.height = '100%'; // add the text node to the created div
 
@@ -1723,24 +1809,13 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
       this._container.appendChild(this._scrollContainer);
 
-      var scrollBarWidth = this._scrollContainer.offsetWidth - this._scrollContent.clientWidth; // Calculate current browser scroll bar size and add offset for the canvas
+      var scrollBarWidth = this._scrollContainer.offsetWidth - this._scrollContent.clientWidth; // Calculate current browser scrollbar size and add offset for the canvas
 
       this._canvas.style.width = this._canvas.style.height = 'calc(100% -' + (scrollBarWidth || 17) + 'px)';
 
       this._container.appendChild(this._canvas);
 
-      if (this._options.fillColor) {
-        this._scrollContainer.style.background = this._options.fillColor;
-      } // Normalize and validate span per seconds
-
-
-      this._options.snapsPerSeconds = Math.max(0, Math.min(60, this._options.snapsPerSeconds || 0));
       this._ctx = this._canvas.getContext('2d');
-
-      this._subscribeOnEvents();
-
-      this.rescale();
-      this.redraw();
     }
     /**
      * Subscribe current component on the related events.
@@ -1839,21 +1914,14 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
         var deltaSpeed = TimelineUtils.getDistance(this._width() / 2, x) * 0.2;
         x = x + deltaSpeed;
         var diff = this._width() / x;
-        var val = this.pxToVal(this._scrollContainer.scrollLeft + x, false);
-        var zoom = direction * this._currentZoom * speed; //this._options.zoom
-
-        this._currentZoom += zoom;
 
-        if (TimelineUtils.isNumber(this._options.zoomMax) && this._currentZoom > this._options.zoomMax) {
-          this._currentZoom = this._options.zoomMax;
-        }
+        var val = this._fromScreen(x - this._leftMargin());
 
-        if (TimelineUtils.isNumber(this._options.zoomMin) && this._currentZoom < this._options.zoomMin) {
-          this._currentZoom = this._options.zoomMin;
-        } // Get only after zoom is set
+        var zoom = direction * this._currentZoom * speed; //this._options.zoom
 
+        this._currentZoom = this._setZoom(this._currentZoom + zoom); // Get only after zoom is set
 
-        var zoomCenter = this.valToPx(val, true);
+        var zoomCenter = this.valToPx(val);
         var newScrollLeft = Math.round(zoomCenter - this._width() / diff);
 
         if (newScrollLeft <= 0) {
@@ -1894,6 +1962,31 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
       this._zoom(-1, speed, this._scrollContainer.clientWidth / 2);
     }
+    /**
+     * Set direct zoom value.
+     * @param zoom zoom value to set.
+     * @param min min zoom.
+     * @param max max zoom.
+     * @return normalized value.
+     */
+
+  }, {
+    key: "_setZoom",
+    value: function _setZoom(zoom) {
+      var min = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
+      var max = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
+      min = TimelineUtils.isNumber(min) ? min : this._options.zoomMin;
+      max = TimelineUtils.isNumber(max) ? max : this._options.zoomMax;
+
+      if (TimelineUtils.isNumber(zoom)) {
+        zoom = TimelineUtils.keepInBounds(zoom, min, max);
+        zoom = zoom || 1;
+        this._currentZoom = zoom;
+        return zoom;
+      }
+
+      return zoom;
+    }
     /**
      * @param args
      */
@@ -1928,15 +2021,14 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
           min: Number.MIN_SAFE_INTEGER,
           max: Number.MAX_SAFE_INTEGER
         };
-        bounds = this._setMinMax(bounds, this._options);
+        bounds = TimelineUtils.setMinMax(bounds, this._options);
         elements.forEach(function (p) {
           // find allowed bounds for the draggable items.
           // find for each row and keyframe separately.
-          var currentBounds = _this2._setMinMax(_this2._setMinMax({
+          var currentBounds = TimelineUtils.setMinMax(TimelineUtils.setMinMax({
             min: bounds.min,
             max: bounds.max
           }, p.keyframe), p.row);
-
           var expectedKeyframeValue = _this2._options && _this2._options.snapAllKeyframesOnMove ? _this2.snapVal(p.keyframe.val) : p.keyframe.val;
           var newPosition = expectedKeyframeValue + offset;
 
@@ -2035,14 +2127,14 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
     }
   }, {
     key: "_performClick",
-    value: function _performClick(pos, args, drag) {
+    value: function _performClick(pos, drag) {
       var isChanged = false;
 
       if (drag && drag.type === TimelineElementType.Keyframe) {
         var mode = TimelineSelectionMode.Normal;
 
-        if (this._startedDragWithCtrl && this._controlKeyPressed(args) || this._startedDragWithShiftKey && args.shiftKey) {
-          if (this._controlKeyPressed(args)) {
+        if (this._startedDragWithCtrl && this._controlKeyPressed(pos.args) || this._startedDragWithShiftKey && pos.args.shiftKey) {
+          if (this._controlKeyPressed(pos.args)) {
             mode = TimelineSelectionMode.Revert;
           }
         } // Reverse selected keyframe selection by a click:
@@ -2050,19 +2142,16 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
         isChanged = this._selectInternal(this._drag.target.keyframe, mode).selectionChanged || isChanged;
 
-        if (args.shiftKey) {
-          // change timeline pos:
-          var convertedVal = this._mousePosToVal(pos.x, true); // Set current timeline position if it's not a drag or selection rect small or fast click.
-
-
-          isChanged = this._setTimeInternal(convertedVal, TimelineEventSource.User) || isChanged;
+        if (pos.args.shiftKey) {
+          // Set current timeline position if it's not a drag or selection rect small or fast click.
+          isChanged = this._setTimeInternal(pos.val, TimelineEventSource.User) || isChanged;
         }
       } else {
         // deselect keyframes if any:
         isChanged = this._selectInternal(null).selectionChanged || isChanged; // change timeline pos:
         // Set current timeline position if it's not a drag or selection rect small or fast click.
 
-        isChanged = this._setTimeInternal(this._mousePosToVal(pos.x, true), TimelineEventSource.User) || isChanged;
+        isChanged = this._setTimeInternal(pos.val, TimelineEventSource.User) || isChanged;
       }
 
       return isChanged;
@@ -2332,8 +2421,13 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
     value: function _trackMousePos(canvas, mouseArgs) {
       var pos = this._getMousePos(canvas, mouseArgs);
 
-      pos.val = this.pxToVal(pos.x + this._scrollContainer.scrollLeft);
-      pos.snapVal = this.snapVal(pos.val);
+      pos.originalVal = this._mousePosToVal(pos.x, false);
+      pos.snapVal = this._mousePosToVal(pos.x, true);
+      pos.val = pos.originalVal;
+
+      if (this._options && this._options.snapEnabled) {
+        pos.val = pos.snapVal;
+      }
 
       if (this._startPos) {
         if (!this._selectionRect) {
@@ -2341,8 +2435,8 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
         } // get the pos with the virtualization:
 
 
-        var x = Math.floor(this._startPos.x + (this._scrollStartPos.x - this._scrollContainer.scrollLeft));
-        var y = Math.floor(this._startPos.y + (this._scrollStartPos.y - this._scrollContainer.scrollTop));
+        var x = Math.floor(this._startPos.x + (this._scrollStartPos.x - this.getScrollLeft()));
+        var y = Math.floor(this._startPos.y + (this._scrollStartPos.y - this.getScrollTop()));
         this._selectionRect.x = Math.min(x, pos.x);
         this._selectionRect.y = Math.min(y, pos.y);
         this._selectionRect.width = Math.max(x, pos.x) - this._selectionRect.x;
@@ -2488,7 +2582,7 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
         } else if (isRight) {
           // Get normalized speed:
           speedX = TimelineUtils.getDistance(x, this._width() - bounds) * scrollSpeedMultiplier;
-          newWidth = this._scrollContainer.scrollLeft + this._width() + speedX;
+          newWidth = this.getScrollLeft() + this._width() + speedX;
         }
 
         if (isTop) {
@@ -2525,63 +2619,75 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
   }, {
     key: "pxToVal",
-    value: function pxToVal(coords) {
-      var absolute = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+    value: function pxToVal(px) {
+      var steps = this._options.stepVal * this._currentZoom || 1;
+      var val = px / this._options.stepPx * steps;
+      return val;
+    }
+    /**
+     * Convert value to local screen component coordinates.
+     */
 
-      if (!absolute) {
-        coords -= this._options.leftMargin;
-      }
+  }, {
+    key: "_toScreenPx",
+    value: function _toScreenPx(val) {
+      return this.valToPx(val) - this.getScrollLeft() + this._leftMargin();
+    }
+    /**
+     * Convert screen local coordinates to a global value info.
+     */
 
-      var ms = coords / this._options.stepPx * this._currentZoom;
-      return ms;
+  }, {
+    key: "_fromScreen",
+    value: function _fromScreen(px) {
+      return this.pxToVal(this.getScrollLeft() + px);
     }
     /**
-     * Convert area value to screen pixel coordinates.
+     * Convert area value to global screen pixel coordinates.
      */
 
   }, {
     key: "valToPx",
-    value: function valToPx(ms) {
-      var absolute = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
-
-      // Respect current scroll container offset. (virtualization)
-      if (!absolute && this._scrollContainer) {
-        var x = this._scrollContainer.scrollLeft;
-        ms -= this.pxToVal(x);
-      }
-
+    value: function valToPx(val) {
       if (!this._options) {
-        return ms;
+        return val;
       }
 
-      return ms * this._options.stepPx / this._currentZoom;
+      var steps = this._options.stepVal * this._currentZoom || 1;
+      return val * this._options.stepPx / steps;
     }
     /**
-     * Snap a value to a nearest beautiful point.
+     * Snap a value to a nearest grid point.
      */
 
   }, {
     key: "snapVal",
-    value: function snapVal(ms) {
-      // Apply snap to steps if enabled.
-      if (this._options && this._options.snapsPerSeconds && this._options.snapEnabled) {
-        var stopsPerPixel = 1000 / this._options.snapsPerSeconds;
-        var step = ms / stopsPerPixel;
+    value: function snapVal(val) {
+      // Snap a value if configured.
+      if (this._options && this._options.snapEnabled && this._options.snapStep) {
+        var stops = this._options.snapStep;
+        var step = val / stops;
         var stepsFit = Math.round(step);
-        ms = Math.round(stepsFit * stopsPerPixel);
-      }
 
-      if (this._options && TimelineUtils.isNumber(this._options.min) && ms < this._options.min) {
-        ms = 0;
+        var minSteps = Math.abs(this._options.min) / this._options.snapStep;
+
+        var minOffset = TimelineUtils.sign(this._options.min) * (minSteps - Math.floor(minSteps)) * this._options.snapStep;
+
+        val = Math.round(minOffset) + Math.round(stepsFit * stops);
       }
 
-      return ms;
+      val = TimelineUtils.keepInBounds(val, this._options.min, this._options.max);
+      return val;
     }
   }, {
     key: "_mousePosToVal",
     value: function _mousePosToVal(x) {
       var snapEnabled = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
-      var convertedVal = this.pxToVal(this._scrollContainer.scrollLeft + Math.min(x, this._width()));
+
+      var mousePos = Math.min(x, this._width()) - this._leftMargin();
+
+      var convertedVal = this._fromScreen(mousePos);
+
       convertedVal = Math.round(convertedVal);
 
       if (snapEnabled) {
@@ -2598,8 +2704,8 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
      */
 
   }, {
-    key: "_formatLineGaugeText",
-    value: function _formatLineGaugeText(ms) {
+    key: "_formatUnitsText",
+    value: function _formatUnitsText(ms) {
       var isSeconds = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
       // 1- Convert to seconds:
       var seconds = ms / 1000;
@@ -2646,57 +2752,73 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
       return str;
     }
+    /**
+     * Left padding of the timeline.
+     */
+
+  }, {
+    key: "_leftMargin",
+    value: function _leftMargin() {
+      if (!this._options) {
+        return 0;
+      }
+
+      return this._options.leftMargin || 0;
+    }
   }, {
     key: "_renderTicks",
     value: function _renderTicks() {
-      if (!this._ctx || !this._options) {
+      var rulerActive = !!this._ctx && !!this._options && !!this._ctx.canvas && this._ctx.canvas.clientWidth > 0 && this._ctx.canvas.clientHeight > 0 && this._options.stepPx;
+
+      if (!rulerActive) {
         return;
       }
 
-      this._ctx.save();
+      var screenWidth = this._width() - this._leftMargin();
 
-      var areaWidth = this._scrollContainer.scrollWidth - (this._options.leftMargin || 0);
-      var from = this.pxToVal(this._options.min);
-      var to = this.pxToVal(areaWidth);
-      var dist = TimelineUtils.getDistance(from, to);
+      var from = this.pxToVal(this.getScrollLeft());
+      var to = this.pxToVal(this.getScrollLeft() + screenWidth);
 
-      if (dist === 0) {
+      if (isNaN(from) || isNaN(to) || from === to) {
         return;
-      } // normalize step.
-
+      }
 
-      var stepsCanFit = areaWidth / this._options.stepPx;
-      var realStep = dist / stepsCanFit; // Find the nearest 'beautiful' step for a line gauge. This step should be divided by 1/2/5!
-      //let step = realStep;
+      if (to < from) {
+        var wasToVal = to;
+        to = from;
+        from = wasToVal;
+      }
 
-      var step = TimelineUtils.findGoodStep(realStep);
+      var valDistance = TimelineUtils.getDistance(from, to);
 
-      if (step == 0 || isNaN(step) || !isFinite(step)) {
+      if (valDistance <= 0) {
         return;
-      }
+      } // Find the nearest 'beautiful' step for a gauge.
+      // 'beautiful' step should be dividable by 1/2/5/10!
 
-      var goodStepDistancePx = areaWidth / (dist / step);
-      var smallStepsCanFit = goodStepDistancePx / this._options.stepSmallPx;
-      var realSmallStep = step / smallStepsCanFit;
-      var smallStep = TimelineUtils.findGoodStep(realSmallStep, step);
 
-      if (step % smallStep != 0) {
-        smallStep = realSmallStep;
-      } // filter to draw only visible
+      var step = TimelineUtils.findGoodStep(valDistance / (screenWidth / this._options.stepPx));
+      var smallStep = TimelineUtils.findGoodStep(valDistance / (screenWidth / this._options.stepSmallPx)); // Find beautiful start point:
 
+      var fromVal = Math.floor(from / step) * step; // Find a beautiful end point:
 
-      var visibleFrom = this.pxToVal(this._scrollContainer.scrollLeft + this._options.leftMargin || 0);
-      var visibleTo = this.pxToVal(this._scrollContainer.scrollLeft + this._scrollContainer.clientWidth); // Find beautiful start point:
+      var toVal = Math.ceil(to / step) * step + step;
 
-      from = Math.floor(visibleFrom / step) * step; // Find a beautiful end point:
+      if (!TimelineUtils.isNumber(step) || step <= 0 || Math.abs(toVal - fromVal) === 0) {
+        return;
+      }
 
-      to = Math.ceil(visibleTo / step) * step + step;
-      var lastTextX = null;
+      var lastTextStart = 0;
 
-      for (var i = from; i <= to; i += step) {
-        var pos = this.valToPx(i);
+      this._ctx.save();
+
+      var headerHeight = timelineStyleUtils_TimelineStyleUtils.headerHeight(this._options);
+      var tickHeight = headerHeight / 2;
+      var smallTickHeight = headerHeight / 1.3;
 
-        var sharpPos = this._getSharp(Math.round(pos));
+      for (var i = fromVal; i <= toVal; i += step) {
+        // local
+        var sharpPos = this._getSharp(this._toScreenPx(i));
 
         this._ctx.save();
 
@@ -2706,7 +2828,7 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
         this._ctx.lineWidth = 1;
         this._ctx.strokeStyle = this._options.tickColor;
-        TimelineUtils.drawLine(this._ctx, sharpPos, timelineStyleUtils_TimelineStyleUtils.headerHeight(this._options) / 2, sharpPos, this._height());
+        TimelineUtils.drawLine(this._ctx, sharpPos, tickHeight, sharpPos, headerHeight);
 
         this._ctx.stroke();
 
@@ -2716,31 +2838,34 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
           this._ctx.font = this._options.font;
         }
 
-        var text = this._formatLineGaugeText(i);
+        var text = this._formatUnitsText(i);
 
         var textSize = this._ctx.measureText(text);
 
         var textX = sharpPos - textSize.width / 2; // skip text render if there is no space for it.
 
-        if (isNaN(lastTextX) || lastTextX <= textX) {
-          lastTextX = textX + textSize.width;
+        if (isNaN(lastTextStart) || lastTextStart <= textX) {
+          lastTextStart = textX + textSize.width;
 
           this._ctx.fillText(text, textX, 10);
         }
 
-        this._ctx.restore(); // Draw small steps
+        this._ctx.restore();
 
+        if (!TimelineUtils.isNumber(smallStep) || smallStep <= 0) {
+          continue;
+        } // Draw small steps
 
-        for (var x = i + smallStep; x < i + step; x += smallStep) {
-          var nextPos = this.valToPx(x);
 
-          var nextSharpPos = this._getSharp(Math.floor(nextPos));
+        for (var x = i + smallStep; x < i + step; x += smallStep) {
+          // local
+          var nextSharpPos = this._getSharp(this._toScreenPx(x));
 
           this._ctx.beginPath();
 
           this._ctx.lineWidth = this._pixelRatio;
           this._ctx.strokeStyle = this._options.tickColor;
-          TimelineUtils.drawLine(this._ctx, nextSharpPos, timelineStyleUtils_TimelineStyleUtils.headerHeight(this._options) / 1.3, nextSharpPos, timelineStyleUtils_TimelineStyleUtils.headerHeight(this._options));
+          TimelineUtils.drawLine(this._ctx, nextSharpPos, smallTickHeight, nextSharpPos, headerHeight);
 
           this._ctx.stroke();
         }
@@ -2748,35 +2873,6 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
       this._ctx.restore();
     }
-  }, {
-    key: "_setMinMax",
-    value: function _setMinMax(to, from) {
-      var shrink = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
-
-      if (!from || !to) {
-        return to;
-      }
-
-      var isFromMinNumber = TimelineUtils.isNumber(from.min);
-      var isToMinNumber = TimelineUtils.isNumber(to.min); // get absolute min and max bounds:
-
-      if (isFromMinNumber && isToMinNumber) {
-        to.min = shrink ? Math.min(from.min, to.min) : Math.max(from.min, to.min);
-      } else if (isFromMinNumber) {
-        to.min = from.min;
-      }
-
-      var isFromMaxNumber = TimelineUtils.isNumber(from.max);
-      var isToMaxNumber = TimelineUtils.isNumber(to.max);
-
-      if (isFromMaxNumber && isToMaxNumber) {
-        to.max = shrink ? Math.max(from.max, to.max) : Math.min(from.max, to.max);
-      } else if (isFromMaxNumber) {
-        to.max = from.max;
-      }
-
-      return to;
-    }
     /**
      * calculate virtual mode. Determine screen positions for the elements.
      */
@@ -2899,19 +2995,18 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
         calcRow.groups.forEach(function (group) {
           // Extend row min max bounds by a group bounds:
-          _this6._setMinMax(calcRow, group, true); // get group screen coords
-
+          TimelineUtils.setMinMax(calcRow, group, true); // get group screen coords
 
           var groupRect = _this6._getKeyframesGroupSize(row, calcRow.size.y, group.min, group.max);
 
           group.size = groupRect;
         }); // Extend screen bounds by a current calculation:
 
-        _this6._setMinMax(toReturn, calcRow, true);
+        TimelineUtils.setMinMax(toReturn, calcRow, true);
       });
 
       if (TimelineUtils.isNumber(toReturn.max)) {
-        toReturn.size.width = this.valToPx(toReturn.max, true);
+        toReturn.size.width = this.valToPx(toReturn.max);
       }
 
       return toReturn;
@@ -3023,8 +3118,12 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
       var margin = height - groupHeight; // draw keyframes rows.
 
-      var xMin = this.valToPx(minValue);
-      var xMax = this.valToPx(maxValue);
+      var xMin = this._toScreenPx(minValue); // local
+
+
+      var xMax = this._toScreenPx(maxValue); // local
+
+
       return {
         x: xMin,
         y: rowY + Math.floor(margin / 2),
@@ -3063,7 +3162,8 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       if (height > 0) {
         if (!isNaN(val)) {
           var toReturn = {
-            x: Math.floor(this.valToPx(val)),
+            x: Math.floor(this._toScreenPx(val)),
+            // local
             y: Math.floor(y),
             height: height,
             width: width
@@ -3230,7 +3330,7 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
   }, {
     key: "_renderTimeline",
     value: function _renderTimeline() {
-      if (!this._options || !this._options.timelineStyle) {
+      if (!this._ctx || !this._options || !this._options.timelineStyle) {
         return;
       }
 
@@ -3241,7 +3341,7 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       var thickness = style.width || 1;
       this._ctx.lineWidth = thickness * this._pixelRatio;
 
-      var timeLinePos = this._getSharp(Math.round(this.valToPx(this._val)), thickness);
+      var timeLinePos = this._getSharp(this._toScreenPx(this._val), thickness);
 
       this._ctx.strokeStyle = style.strokeColor;
       this._ctx.fillStyle = style.fillColor;
@@ -3359,6 +3459,7 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
     key: "_getSharp",
     value: function _getSharp(pos) {
       var thickness = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
+      pos = Math.round(pos);
 
       if (thickness % 2 == 0) {
         return pos;
@@ -3450,20 +3551,44 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       return this._scrollContainer ? this._scrollContainer.scrollTop : 0;
     }
     /**
-     * Set this._options.
+     * Set options and render the component.
      * Options will be merged with the defaults and control invalidated
      */
 
   }, {
     key: "setOptions",
     value: function setOptions(toSet) {
-      this._options = this._mergeOptions(toSet);
+      this._options = this._setOptions(toSet);
       this.rescale();
       this.redraw(); // Merged options:
 
       return this._options;
     }
   }, {
+    key: "_setOptions",
+    value: function _setOptions(toSet) {
+      this._options = this._mergeOptions(toSet); // Normalize and validate spans per value.
+
+      this._options.snapStep = TimelineUtils.keepInBounds(this._options.snapStep, 0, this._options.stepVal);
+      this._currentZoom = this._setZoom(this._options.zoom, this._options.zoomMin, this._options.zoomMax);
+      this._options.min = TimelineUtils.isNumber(this._options.min) ? this._options.min : 0;
+      this._options.max = TimelineUtils.isNumber(this._options.max) ? this._options.max : Number.MAX_VALUE;
+
+      if (this._scrollContainer) {
+        var classList = this._scrollContainer.classList;
+
+        if (this._options.scrollContainerClass && classList.contains(this._options.scrollContainerClass)) {
+          classList.add(this._options.scrollContainerClass);
+        }
+
+        if (this._options.fillColor) {
+          this._scrollContainer.style.background = this._options.fillColor;
+        }
+      }
+
+      return this._options;
+    }
+  }, {
     key: "getModel",
     value: function getModel() {
       return this._model;
@@ -3511,7 +3636,8 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       return {
         x: x,
         y: y,
-        radius: radius
+        radius: radius,
+        args: e
       };
     }
     /**
@@ -3570,7 +3696,7 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
         var additionalOffset = this._options.stepPx;
         newWidth = newWidth || 0; // not less than current timeline position
 
-        var timelineGlobalPos = this.valToPx(this._val, true);
+        var timelineGlobalPos = this.valToPx(this._val);
         var timelinePos = 0;
 
         if (timelineGlobalPos > this._width()) {
@@ -3581,10 +3707,10 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
           }
         }
 
-        var keyframeW = data.size.width + this._options.leftMargin + additionalOffset;
+        var keyframeW = data.size.width + this._leftMargin() + additionalOffset;
         newWidth = Math.max(newWidth, // keyframes size
         keyframeW, // not less than current scroll position
-        this._scrollContainer.scrollLeft + this._width(), timelinePos);
+        this.getScrollLeft() + this._width(), timelinePos);
         var minWidthPx = Math.floor(newWidth) + 'px';
 
         if (minWidthPx != this._scrollContent.style.minWidth) {
@@ -3693,7 +3819,8 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
       var headerHeight = timelineStyleUtils_TimelineStyleUtils.headerHeight(this._options); // Check whether we can drag timeline.
 
-      var timeLinePos = this.valToPx(this._val);
+      var timeLinePos = this._toScreenPx(this._val);
+
       var width = 0;
 
       if (this._options && this._options.timelineStyle) {
@@ -3709,6 +3836,8 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
         });
       }
 
+      var snap = this._options.snapEnabled;
+
       if (pos.y >= headerHeight && this._options.keyframesDraggable) {
         this._forEachKeyframe(function (calcKeyframe, index, isNextRow) {
           // Check keyframes group overlap
@@ -3717,7 +3846,7 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
             if (rowOverlapped) {
               var row = {
-                val: _this10._mousePosToVal(pos.x, true),
+                val: _this10._mousePosToVal(pos.x, snap),
                 keyframes: calcKeyframe.parentRow.model.keyframes,
                 type: TimelineElementType.Row,
                 row: calcKeyframe.parentRow.model
@@ -3733,7 +3862,7 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
                   var keyframesModels = _this10._mapKeyframes(group.keyframes);
 
                   var groupElement = {
-                    val: _this10._mousePosToVal(pos.x, true),
+                    val: _this10._mousePosToVal(pos.x, snap),
                     type: TimelineElementType.Group,
                     group: group,
                     row: calcKeyframe.parentRow.model,
@@ -3884,8 +4013,8 @@ var timeline_Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
     value: function _emitScrollEvent(args) {
       var scrollEvent = {
         args: args,
-        scrollLeft: this._scrollContainer.scrollLeft,
-        scrollTop: this._scrollContainer.scrollTop,
+        scrollLeft: this.getScrollLeft(),
+        scrollTop: this.getScrollTop(),
         scrollHeight: this._scrollContainer.scrollHeight,
         scrollWidth: this._scrollContainer.scrollWidth
       };

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
lib/animation-timeline.min.js


+ 1 - 1
lib/settings/defaults.js.map

@@ -1 +1 @@
-{"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/settings/defaults.ts"],"names":[],"mappings":";;AACA,8DAA6D;AAG7D,wEAAuE;AAI1D,QAAA,oBAAoB,GAAG;IAClC,KAAK,EAAE,CAAC;IACR,SAAS,EAAE,EAAE;IACb,QAAQ,EAAE,CAAC;IACX,SAAS,EAAE,EAAE;IACb;;OAEG;IACH,OAAO,EAAE,mCAAgB,CAAC,IAAI;IAC9B,WAAW,EAAE,YAAY;IACzB,SAAS,EAAE,YAAY;CACP,CAAC;AAEN,QAAA,4BAA4B,GAAG;IAC1C;;OAEG;IACH,SAAS,EAAE,YAAY;IACvB,KAAK,EAAE,6CAAqB,CAAC,KAAK;IAClC;;OAEG;IACH,iBAAiB,EAAE,KAAK;IACxB,WAAW,EAAE,OAAO;IACpB,mBAAmB,EAAE,OAAO;IAC5B,eAAe,EAAE,GAAG;IACpB,SAAS,EAAE,IAAI;CACS,CAAC;AAEd,QAAA,uBAAuB,GAAG;IACrC;;OAEG;IACH,MAAM,EAAE,EAAE;IACV,YAAY,EAAE,CAAC;IACf,SAAS,EAAE,SAAS;IACpB;;OAEG;IACH,cAAc,EAAE,SAAS;IACzB,WAAW,EAAE,MAAM;IACnB,cAAc,EAAE,oCAA4B;CACzB,CAAC;AAET,QAAA,sBAAsB,GAAG;IACpC;;;OAGG;IACH,eAAe,EAAE,CAAC;IAClB;;OAEG;IACH,sBAAsB,EAAE,KAAK;IAE7B;;OAEG;IACH,WAAW,EAAE,IAAI;IAEjB,aAAa,EAAE,4BAAoB;IACnC;;OAEG;IACH,MAAM,EAAE,GAAG;IACX,WAAW,EAAE,EAAE;IACf,UAAU,EAAE,EAAE;IACd;;OAEG;IACH,UAAU,EAAE,EAAE;IACd,eAAe,EAAE,SAAS;IAC1B,SAAS,EAAE,SAAS;IAEpB,WAAW,EAAE,SAAS;IACtB;;OAEG;IACH,SAAS,EAAE,SAAS;IACpB;;OAEG;IACH,cAAc,EAAE,OAAO;IAEvB;;;OAGG;IACH,SAAS,EAAE,+BAAuB;IAClC;;OAEG;IACH,YAAY,EAAE,EAAE;IAChB,IAAI,EAAE,iBAAiB;IACvB,IAAI,EAAE,IAAI;IACV,2DAA2D;IAC3D,SAAS,EAAE,GAAG;IACd,WAAW;IACX,OAAO,EAAE,EAAE;IACX,WAAW;IACX,OAAO,EAAE,IAAI;IACb;;OAEG;IACH,mBAAmB,EAAE,KAAK;IAC1B;;OAEG;IACH,oBAAoB,EAAE,kBAAkB;IACxC;;OAEG;IACH,eAAe,EAAE,IAAI;IACrB;;OAEG;IACH,kBAAkB,EAAE,IAAI;IACxB,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,MAAM,CAAC,SAAS;CACH,CAAC;AAER,QAAA,qBAAqB,GAAmB;IACnD;;OAEG;IACH,YAAY,EAAE,EAAE;IAChB;;OAEG;IACH,iBAAiB,EAAE,IAAI;IACvB;;OAEG;IACH,gBAAgB,EAAE,GAAG;IACrB;;OAEG;IACH,oBAAoB,EAAE,GAAG;IACzB;;OAEG;IACH,uBAAuB,EAAE,GAAG;IAC5B;;OAEG;IACH,sBAAsB,EAAE,EAAE;IAC1B;;OAEG;IACH,cAAc,EAAE,CAAC;CACA,CAAC"}
+{"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/settings/defaults.ts"],"names":[],"mappings":";;AACA,8DAA6D;AAG7D,wEAAuE;AAI1D,QAAA,oBAAoB,GAAG;IAClC,KAAK,EAAE,CAAC;IACR,SAAS,EAAE,EAAE;IACb,QAAQ,EAAE,CAAC;IACX,SAAS,EAAE,EAAE;IACb;;OAEG;IACH,OAAO,EAAE,mCAAgB,CAAC,IAAI;IAC9B,WAAW,EAAE,YAAY;IACzB,SAAS,EAAE,YAAY;CACP,CAAC;AAEN,QAAA,4BAA4B,GAAG;IAC1C;;OAEG;IACH,SAAS,EAAE,YAAY;IACvB,KAAK,EAAE,6CAAqB,CAAC,KAAK;IAClC;;OAEG;IACH,iBAAiB,EAAE,KAAK;IACxB,WAAW,EAAE,OAAO;IACpB,mBAAmB,EAAE,OAAO;IAC5B,eAAe,EAAE,GAAG;IACpB,SAAS,EAAE,IAAI;CACS,CAAC;AAEd,QAAA,uBAAuB,GAAG;IACrC;;OAEG;IACH,MAAM,EAAE,EAAE;IACV,YAAY,EAAE,CAAC;IACf,SAAS,EAAE,SAAS;IACpB;;OAEG;IACH,cAAc,EAAE,SAAS;IACzB,WAAW,EAAE,MAAM;IACnB,cAAc,EAAE,oCAA4B;CACzB,CAAC;AAET,QAAA,sBAAsB,GAAG;IACpC;;OAEG;IACH,sBAAsB,EAAE,KAAK;IAE7B;;OAEG;IACH,WAAW,EAAE,IAAI;IAEjB,aAAa,EAAE,4BAAoB;IACnC;;OAEG;IACH,MAAM,EAAE,GAAG;IACX;;OAEG;IACH,OAAO,EAAE,IAAI;IACb,WAAW,EAAE,EAAE;IACf;;OAEG;IACH,QAAQ,EAAE,GAAG;IACb;;OAEG;IACH,UAAU,EAAE,EAAE;IACd,eAAe,EAAE,SAAS;IAC1B,SAAS,EAAE,SAAS;IAEpB,WAAW,EAAE,SAAS;IACtB;;OAEG;IACH,SAAS,EAAE,SAAS;IACpB;;OAEG;IACH,cAAc,EAAE,OAAO;IAEvB;;;OAGG;IACH,SAAS,EAAE,+BAAuB;IAClC;;OAEG;IACH,YAAY,EAAE,EAAE;IAChB,IAAI,EAAE,iBAAiB;IACvB;;OAEG;IACH,IAAI,EAAE,CAAC;IACP;;OAEG;IACH,SAAS,EAAE,GAAG;IACd;;OAEG;IACH,OAAO,EAAE,GAAG;IACZ;;OAEG;IACH,OAAO,EAAE,CAAC;IACV;;OAEG;IACH,mBAAmB,EAAE,KAAK;IAC1B;;OAEG;IACH,oBAAoB,EAAE,kBAAkB;IACxC;;OAEG;IACH,eAAe,EAAE,IAAI;IACrB;;OAEG;IACH,kBAAkB,EAAE,IAAI;IACxB,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,MAAM,CAAC,SAAS;CACH,CAAC;AAER,QAAA,qBAAqB,GAAmB;IACnD;;OAEG;IACH,YAAY,EAAE,EAAE;IAChB;;OAEG;IACH,iBAAiB,EAAE,IAAI;IACvB;;OAEG;IACH,gBAAgB,EAAE,GAAG;IACrB;;OAEG;IACH,oBAAoB,EAAE,GAAG;IACzB;;OAEG;IACH,uBAAuB,EAAE,GAAG;IAC5B;;OAEG;IACH,sBAAsB,EAAE,EAAE;IAC1B;;OAEG;IACH,cAAc,EAAE,CAAC;CACA,CAAC"}

+ 18 - 7
lib/settings/timelineOptions.d.ts

@@ -6,11 +6,6 @@ export interface TimelineOptions extends TimelineRanged {
      * Id or HTMLElement of the timeline container.
      */
     id?: string | HTMLElement;
-    /**
-     * Snap the mouse to the values on a timeline.
-     * Value can be from 1 to 60
-     */
-    snapsPerSeconds?: number;
     /**
      * Check whether snapping is enabled.
      */
@@ -23,8 +18,15 @@ export interface TimelineOptions extends TimelineRanged {
      * approximate step for the timeline in pixels for 1 second
      */
     stepPx?: number;
+    /**
+     * Number of points that should fit into the one stepPx.
+     */
+    stepVal: number;
     stepSmallPx?: number;
-    smallSteps?: number;
+    /**
+     * Snap step in units. from 0 to stepVal
+     */
+    snapStep?: number;
     /**
      * additional left margin in pixels to start the line gauge from.
      */
@@ -63,12 +65,21 @@ export interface TimelineOptions extends TimelineRanged {
      * Header ticks font
      */
     font?: string;
+    /**
+     * Default zoom level = 1. where screen pixels are equals to the corresponding stepVal stepPx.
+     */
     zoom?: number;
     /**
-     * Zoom speed. Use percent of the screen to set zoom speed.
+     * Default zoom speed.
      */
     zoomSpeed?: number;
+    /**
+     * Max zoom value.
+     */
     zoomMin?: number;
+    /**
+     * Min zoom value.
+     */
     zoomMax?: number;
     /**
      * Set this to true in a MAC OS environment: The Meta key will be used instead of the Ctrl key.

+ 52 - 16
lib/timeline.d.ts

@@ -17,13 +17,24 @@ import { TimelineEventSource } from './enums/timelineEventSource';
 import { TimelineTimeChangedEvent } from './utils/events/timelineTimeChangedEvent';
 import { TimelineSelectionMode } from './enums/timelineSelectionMode';
 import { TimelineSelectionResults } from './utils/timelineSelectionResults';
-import { TimelineRanged } from './timelineRanged';
-interface MousePoint extends DOMPoint {
-    radius: number;
-}
-interface MouseData extends MousePoint {
+interface MouseData extends DOMPoint {
+    /**
+     * Value to use.
+     */
     val: number;
+    /**
+     * Snapped value.
+     */
     snapVal: number;
+    /**
+     * Unsnapped value.
+     */
+    originalVal: number;
+    /**
+     * Click radius
+     */
+    radius: number;
+    args: TouchEvent | MouseEvent;
 }
 export declare class Timeline extends TimelineEventsEmitter {
     /**
@@ -94,6 +105,11 @@ export declare class Timeline extends TimelineEventsEmitter {
      * @param model Timeline model.
      */
     initialize(options: TimelineOptions | null, model: TimelineModel | null): void;
+    /**
+     * Generate component html.
+     * @param id container.
+     */
+    _generateContainers(id: string | HTMLElement): void;
     /**
      * Subscribe current component on the related events.
      */
@@ -106,7 +122,7 @@ export declare class Timeline extends TimelineEventsEmitter {
     _handleWindowResizeEvent: () => void;
     _clearScrollFinishedTimer(): void;
     _handleScrollEvent: (args: MouseEvent) => void;
-    _controlKeyPressed(e: MouseEvent | KeyboardEvent): boolean;
+    _controlKeyPressed(e: MouseEvent | KeyboardEvent | TouchEvent): boolean;
     _handleWheelEvent: (event: WheelEvent) => void;
     _zoom(direction: number, speed: number, x: number): void;
     /**
@@ -119,6 +135,14 @@ export declare class Timeline extends TimelineEventsEmitter {
      * @param speed value from 0 to 1
      */
     zoomOut(speed?: number): void;
+    /**
+     * Set direct zoom value.
+     * @param zoom zoom value to set.
+     * @param min min zoom.
+     * @param max max zoom.
+     * @return normalized value.
+     */
+    _setZoom(zoom: number, min?: number | undefined, max?: number | undefined): number;
     /**
      * @param args
      */
@@ -150,7 +174,7 @@ export declare class Timeline extends TimelineEventsEmitter {
      * @param screenRect screen coordinates to get keyframes.
      */
     _getKeyframesByRectangle(screenRect: DOMRect): TimelineKeyframe[];
-    _performClick(pos: MouseData, args: MouseEvent, drag: TimelineDraggableData): boolean;
+    _performClick(pos: MouseData, drag: TimelineDraggableData): boolean;
     /**
      * Set keyframe value.
      * @param keyframe
@@ -211,15 +235,23 @@ export declare class Timeline extends TimelineEventsEmitter {
     /**
      * Convert screen pixel to value.
      */
-    pxToVal(coords: number, absolute?: boolean): number;
+    pxToVal(px: number): number;
+    /**
+     * Convert value to local screen component coordinates.
+     */
+    _toScreenPx(val: number): number;
+    /**
+     * Convert screen local coordinates to a global value info.
+     */
+    _fromScreen(px: number): number;
     /**
-     * Convert area value to screen pixel coordinates.
+     * Convert area value to global screen pixel coordinates.
      */
-    valToPx(ms: number, absolute?: boolean): number;
+    valToPx(val: number): number;
     /**
-     * Snap a value to a nearest beautiful point.
+     * Snap a value to a nearest grid point.
      */
-    snapVal(ms: number): number;
+    snapVal(val: number): number;
     _mousePosToVal(x: number, snapEnabled?: boolean): number;
     /**
      * Format line gauge text.
@@ -227,9 +259,12 @@ export declare class Timeline extends TimelineEventsEmitter {
      * @param ms milliseconds to convert.
      * @param isSeconds whether seconds are passed.
      */
-    _formatLineGaugeText(ms: number, isSeconds?: boolean): string;
+    _formatUnitsText(ms: number, isSeconds?: boolean): string;
+    /**
+     * Left padding of the timeline.
+     */
+    _leftMargin(): number;
     _renderTicks(): void;
-    _setMinMax(to: TimelineRanged, from: TimelineRanged, shrink?: boolean): TimelineRanged;
     /**
      * calculate virtual mode. Determine screen positions for the elements.
      */
@@ -287,17 +322,18 @@ export declare class Timeline extends TimelineEventsEmitter {
     getScrollLeft(): number;
     getScrollTop(): number;
     /**
-     * Set this._options.
+     * Set options and render the component.
      * Options will be merged with the defaults and control invalidated
      */
     setOptions(toSet: TimelineOptions): TimelineOptions;
+    _setOptions(toSet: TimelineOptions): TimelineOptions;
     getModel(): TimelineModel;
     /**
      * Set model and redraw application.
      * @param data
      */
     setModel(data: TimelineModel): void;
-    _getMousePos(canvas: HTMLCanvasElement, e: TouchEvent | MouseEvent | any): MousePoint;
+    _getMousePos(canvas: HTMLCanvasElement, e: TouchEvent | MouseEvent | any): MouseData;
     /**
      * Apply container div size to the container on changes detected.
      */

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
lib/timeline.js.map


+ 9 - 0
lib/utils/timelineUtils.d.ts

@@ -1,5 +1,9 @@
+import { TimelineRanged } from '../timelineRanged';
 export declare class TimelineUtils {
     static drawLine(ctx: CanvasRenderingContext2D, x1: number, y1: number, x2: number, y2: number): void;
+    /**
+     * Check is valid number.
+     */
     static isNumber(val?: number): boolean;
     static deleteElement<T>(array: Array<T>, element: T): Array<T>;
     /**
@@ -10,6 +14,11 @@ export declare class TimelineUtils {
      * Find beautiful step for the header line gauge.
      */
     static findGoodStep(originalStep: number, divisionCheck?: number): number;
+    /**
+     * Keep value in min, max bounds.
+     */
+    static keepInBounds(value: number, min?: number | undefined, max?: number | undefined): number;
+    static setMinMax(to: TimelineRanged, from: TimelineRanged, shrink?: boolean): TimelineRanged;
     static isRectOverlap(rect: DOMRect, rect2: DOMRect): boolean;
     static getDistance(x1: number, y1: number, x2?: number, y2?: number): number;
     static sign(p: number): number;

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
lib/utils/timelineUtils.js.map


+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "animation-timeline-js",
-  "version": "2.0.7",
+  "version": "2.1.0",
   "description": "animation timeline control based on the canvas.",
   "main": "lib/animation-timeline.min.js",
   "types": "lib/animation-timeline.d.ts",

+ 23 - 12
src/settings/defaults.ts

@@ -51,11 +51,6 @@ export const defaultTimelineRowStyle = {
 } as TimelineRowStyle;
 
 export const defaultTimelineOptions = {
-  /**
-   * Snap the mouse to the values on a timeline.
-   * Value can be from 1 to 60
-   */
-  snapsPerSeconds: 5,
   /**
    *  Snap all selected keyframes as a bundle during the drag.
    */
@@ -71,8 +66,15 @@ export const defaultTimelineOptions = {
    * approximate step for the timeline in pixels for 1 second
    */
   stepPx: 120,
+  /**
+   * Number of units that should fit into one stepPx. (1 second by a default)
+   */
+  stepVal: 1000,
   stepSmallPx: 30,
-  smallSteps: 50,
+  /**
+   * Snap step in units. from 0 to stepVal
+   */
+  snapStep: 200,
   /**
    * additional left margin in pixels to start the line gauge from.
    */
@@ -100,13 +102,22 @@ export const defaultTimelineOptions = {
    */
   headerHeight: 30,
   font: '11px sans-serif',
-  zoom: 1000,
-  // Zoom speed. Use percent of the screen to set zoom speed.
+  /**
+   * Default zoom level = 1. where screen pixels are equals to the corresponding stepVal stepPx.
+   */
+  zoom: 1,
+  /**
+   * Default zoom speed.
+   */
   zoomSpeed: 0.1,
-  // Max zoom
-  zoomMin: 80,
-  // Min zoom
-  zoomMax: 8000,
+  /**
+   * Max zoom value.
+   */
+  zoomMin: 0.1,
+  /**
+   * Min zoom value.
+   */
+  zoomMax: 8,
   /**
    * Set this to true in a MAC OS environment: The Meta key will be used instead of the Ctrl key.
    */

+ 18 - 9
src/settings/timelineOptions.ts

@@ -7,11 +7,6 @@ export interface TimelineOptions extends TimelineRanged {
    * Id or HTMLElement of the timeline container.
    */
   id?: string | HTMLElement;
-  /**
-   * Snap the mouse to the values on a timeline.
-   * Value can be from 1 to 60
-   */
-  snapsPerSeconds?: number;
   /**
    * Check whether snapping is enabled.
    */
@@ -24,8 +19,15 @@ export interface TimelineOptions extends TimelineRanged {
    * approximate step for the timeline in pixels for 1 second
    */
   stepPx?: number;
+  /**
+   * Number of points that should fit into the one stepPx.
+   */
+  stepVal: number;
   stepSmallPx?: number;
-  smallSteps?: number;
+  /**
+   * Snap step in units. from 0 to stepVal
+   */
+  snapStep?: number;
   /**
    * additional left margin in pixels to start the line gauge from.
    */
@@ -65,14 +67,21 @@ export interface TimelineOptions extends TimelineRanged {
    * Header ticks font
    */
   font?: string;
+  /**
+   * Default zoom level = 1. where screen pixels are equals to the corresponding stepVal stepPx.
+   */
   zoom?: number;
   /**
-   * Zoom speed. Use percent of the screen to set zoom speed.
+   * Default zoom speed.
    */
   zoomSpeed?: number;
-  // Max zoom
+  /**
+   * Max zoom value.
+   */
   zoomMin?: number;
-  // Min zoom
+  /**
+   * Min zoom value.
+   */
   zoomMax?: number;
   /**
    * Set this to true in a MAC OS environment: The Meta key will be used instead of the Ctrl key.

+ 205 - 163
src/timeline.ts

@@ -28,12 +28,24 @@ import { TimelineSelectionMode } from './enums/timelineSelectionMode';
 import { TimelineSelectionResults } from './utils/timelineSelectionResults';
 import { TimelineRanged } from './timelineRanged';
 
-interface MousePoint extends DOMPoint {
-  radius: number;
-}
-interface MouseData extends MousePoint {
+interface MouseData extends DOMPoint {
+  /**
+   * Value to use.
+   */
   val: number;
+  /**
+   * Snapped value.
+   */
   snapVal: number;
+  /**
+   * Unsnapped value.
+   */
+  originalVal: number;
+  /**
+   * Click radius
+   */
+  radius: number;
+  args: TouchEvent | MouseEvent;
 }
 
 export class Timeline extends TimelineEventsEmitter {
@@ -119,9 +131,18 @@ export class Timeline extends TimelineEventsEmitter {
       throw new Error(`Element cannot be empty. Should be string or DOM element.`);
     }
 
-    const id = options.id;
-    this._options = this._mergeOptions(options);
-    this._currentZoom = this._options.zoom;
+    this._generateContainers(options.id);
+    this._options = this._setOptions(options);
+    this._subscribeOnEvents();
+    this.rescale();
+    this.redraw();
+  }
+
+  /**
+   * Generate component html.
+   * @param id container.
+   */
+  _generateContainers(id: string | HTMLElement): void {
     if (id instanceof HTMLElement) {
       this._container = id as HTMLElement;
     } else {
@@ -163,7 +184,6 @@ export class Timeline extends TimelineEventsEmitter {
       'user-drag: none;' +
       'padding: inherit';
 
-    this._scrollContainer.classList.add(this._options.scrollContainerClass);
     this._scrollContainer.style.cssText = 'overflow: scroll;' + 'position: absolute;' + 'width:  100%;' + 'height:  100%;';
 
     this._scrollContent.style.width = this._scrollContent.style.height = '100%';
@@ -172,24 +192,12 @@ export class Timeline extends TimelineEventsEmitter {
     this._scrollContainer.appendChild(this._scrollContent);
     this._container.appendChild(this._scrollContainer);
     const scrollBarWidth = this._scrollContainer.offsetWidth - this._scrollContent.clientWidth;
-    // Calculate current browser scroll bar size and add offset for the canvas
+    // Calculate current browser scrollbar size and add offset for the canvas
     this._canvas.style.width = this._canvas.style.height = 'calc(100% -' + (scrollBarWidth || 17) + 'px)';
 
     this._container.appendChild(this._canvas);
-
-    if (this._options.fillColor) {
-      this._scrollContainer.style.background = this._options.fillColor;
-    }
-
-    // Normalize and validate span per seconds
-    this._options.snapsPerSeconds = Math.max(0, Math.min(60, this._options.snapsPerSeconds || 0));
-
     this._ctx = this._canvas.getContext('2d');
-    this._subscribeOnEvents();
-    this.rescale();
-    this.redraw();
   }
-
   /**
    * Subscribe current component on the related events.
    */
@@ -292,7 +300,7 @@ export class Timeline extends TimelineEventsEmitter {
     this.redraw();
     this._emitScrollEvent(args);
   };
-  _controlKeyPressed(e: MouseEvent | KeyboardEvent): boolean {
+  _controlKeyPressed(e: MouseEvent | KeyboardEvent | TouchEvent): boolean {
     if (!this._options || this._options.controlKeyIsMetaKey === undefined) {
       return e.metaKey || e.ctrlKey;
     }
@@ -313,18 +321,12 @@ export class Timeline extends TimelineEventsEmitter {
       const deltaSpeed = TimelineUtils.getDistance(this._width() / 2, x) * 0.2;
       x = x + deltaSpeed;
       const diff = this._width() / x;
-      const val = this.pxToVal(this._scrollContainer.scrollLeft + x, false);
+      const val = this._fromScreen(x - this._leftMargin());
       const zoom = direction * this._currentZoom * speed;
       //this._options.zoom
-      this._currentZoom += zoom;
-      if (TimelineUtils.isNumber(this._options.zoomMax) && this._currentZoom > this._options.zoomMax) {
-        this._currentZoom = this._options.zoomMax;
-      }
-      if (TimelineUtils.isNumber(this._options.zoomMin) && this._currentZoom < this._options.zoomMin) {
-        this._currentZoom = this._options.zoomMin;
-      }
+      this._currentZoom = this._setZoom(this._currentZoom + zoom);
       // Get only after zoom is set
-      const zoomCenter = this.valToPx(val, true);
+      const zoomCenter = this.valToPx(val);
       let newScrollLeft = Math.round(zoomCenter - this._width() / diff);
       if (newScrollLeft <= 0) {
         newScrollLeft = 0;
@@ -353,6 +355,26 @@ export class Timeline extends TimelineEventsEmitter {
   public zoomOut(speed = this._options.zoomSpeed): void {
     this._zoom(-1, speed, this._scrollContainer.clientWidth / 2);
   }
+  /**
+   * Set direct zoom value.
+   * @param zoom zoom value to set.
+   * @param min min zoom.
+   * @param max max zoom.
+   * @return normalized value.
+   */
+  _setZoom(zoom: number, min: number | undefined = null, max: number | undefined = null): number {
+    min = TimelineUtils.isNumber(min) ? min : this._options.zoomMin;
+    max = TimelineUtils.isNumber(max) ? max : this._options.zoomMax;
+    if (TimelineUtils.isNumber(zoom)) {
+      zoom = TimelineUtils.keepInBounds(zoom, min, max);
+      zoom = zoom || 1;
+      this._currentZoom = zoom;
+      return zoom;
+    }
+
+    return zoom;
+  }
+
   /**
    * @param args
    */
@@ -468,7 +490,7 @@ export class Timeline extends TimelineEventsEmitter {
     if (this._startPos) {
       if (isLeftClicked || isTouch) {
         if (this._drag && !this._startedDragWithCtrl) {
-          const convertedVal = this._mousePosToVal(this._currentPos.x, true);
+          const convertedVal = this._currentPos.val;
           if (this._drag.type === TimelineElementType.Timeline) {
             this._setTimeInternal(convertedVal, TimelineEventSource.User);
           } else if ((this._drag.type == TimelineElementType.Keyframe || this._drag.type == TimelineElementType.Group) && this._drag.elements) {
@@ -559,11 +581,11 @@ export class Timeline extends TimelineEventsEmitter {
     if (Math.abs(offset) > 0) {
       // Find drag min and max bounds:
       let bounds = { min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER } as TimelineRanged;
-      bounds = this._setMinMax(bounds, this._options);
+      bounds = TimelineUtils.setMinMax(bounds, this._options);
       elements.forEach((p) => {
         // find allowed bounds for the draggable items.
         // find for each row and keyframe separately.
-        const currentBounds = this._setMinMax(this._setMinMax({ min: bounds.min, max: bounds.max }, p.keyframe), p.row);
+        const currentBounds = TimelineUtils.setMinMax(TimelineUtils.setMinMax({ min: bounds.min, max: bounds.max }, p.keyframe), p.row);
         const expectedKeyframeValue = this._options && this._options.snapAllKeyframesOnMove ? this.snapVal(p.keyframe.val) : p.keyframe.val;
         const newPosition = expectedKeyframeValue + offset;
         if (TimelineUtils.isNumber(currentBounds.min) && newPosition < currentBounds.min) {
@@ -606,7 +628,7 @@ export class Timeline extends TimelineEventsEmitter {
           const mousePos = Math.max(0, this._getMousePos(this._canvas, args).x || 0);
           this._zoom(direction, this._options.zoomSpeed, mousePos);
         } else {
-          this._performClick(pos, args, this._drag);
+          this._performClick(pos, this._drag);
         }
       } else if (!this._drag && this._selectionRect && this._selectionRectEnabled) {
         if (this._interactionMode === TimelineInteractionMode.Zoom) {
@@ -672,23 +694,21 @@ export class Timeline extends TimelineEventsEmitter {
     return keyframesModels;
   }
 
-  _performClick(pos: MouseData, args: MouseEvent, drag: TimelineDraggableData): boolean {
+  _performClick(pos: MouseData, drag: TimelineDraggableData): boolean {
     let isChanged = false;
     if (drag && drag.type === TimelineElementType.Keyframe) {
       let mode = TimelineSelectionMode.Normal;
-      if ((this._startedDragWithCtrl && this._controlKeyPressed(args)) || (this._startedDragWithShiftKey && args.shiftKey)) {
-        if (this._controlKeyPressed(args)) {
+      if ((this._startedDragWithCtrl && this._controlKeyPressed(pos.args)) || (this._startedDragWithShiftKey && pos.args.shiftKey)) {
+        if (this._controlKeyPressed(pos.args)) {
           mode = TimelineSelectionMode.Revert;
         }
       }
       // Reverse selected keyframe selection by a click:
       isChanged = this._selectInternal(this._drag.target.keyframe, mode).selectionChanged || isChanged;
 
-      if (args.shiftKey) {
-        // change timeline pos:
-        const convertedVal = this._mousePosToVal(pos.x, true);
+      if (pos.args.shiftKey) {
         // Set current timeline position if it's not a drag or selection rect small or fast click.
-        isChanged = this._setTimeInternal(convertedVal, TimelineEventSource.User) || isChanged;
+        isChanged = this._setTimeInternal(pos.val, TimelineEventSource.User) || isChanged;
       }
     } else {
       // deselect keyframes if any:
@@ -696,7 +716,7 @@ export class Timeline extends TimelineEventsEmitter {
 
       // change timeline pos:
       // Set current timeline position if it's not a drag or selection rect small or fast click.
-      isChanged = this._setTimeInternal(this._mousePosToVal(pos.x, true), TimelineEventSource.User) || isChanged;
+      isChanged = this._setTimeInternal(pos.val, TimelineEventSource.User) || isChanged;
     }
 
     return isChanged;
@@ -915,16 +935,21 @@ export class Timeline extends TimelineEventsEmitter {
 
   _trackMousePos(canvas: HTMLCanvasElement, mouseArgs: MouseEvent | TouchEvent): MouseData {
     const pos = this._getMousePos(canvas, mouseArgs) as MouseData;
-    pos.val = this.pxToVal(pos.x + this._scrollContainer.scrollLeft);
-    pos.snapVal = this.snapVal(pos.val);
+    pos.originalVal = this._mousePosToVal(pos.x, false);
+    pos.snapVal = this._mousePosToVal(pos.x, true);
+    pos.val = pos.originalVal;
+    if (this._options && this._options.snapEnabled) {
+      pos.val = pos.snapVal;
+    }
+
     if (this._startPos) {
       if (!this._selectionRect) {
         this._selectionRect = {} as DOMRect;
       }
 
       // get the pos with the virtualization:
-      const x = Math.floor(this._startPos.x + (this._scrollStartPos.x - this._scrollContainer.scrollLeft));
-      const y = Math.floor(this._startPos.y + (this._scrollStartPos.y - this._scrollContainer.scrollTop));
+      const x = Math.floor(this._startPos.x + (this._scrollStartPos.x - this.getScrollLeft()));
+      const y = Math.floor(this._startPos.y + (this._scrollStartPos.y - this.getScrollTop()));
       this._selectionRect.x = Math.min(x, pos.x);
       this._selectionRect.y = Math.min(y, pos.y);
       this._selectionRect.width = Math.max(x, pos.x) - this._selectionRect.x;
@@ -1052,7 +1077,7 @@ export class Timeline extends TimelineEventsEmitter {
       } else if (isRight) {
         // Get normalized speed:
         speedX = TimelineUtils.getDistance(x, this._width() - bounds) * scrollSpeedMultiplier;
-        newWidth = this._scrollContainer.scrollLeft + this._width() + speedX;
+        newWidth = this.getScrollLeft() + this._width() + speedX;
       }
 
       if (isTop) {
@@ -1087,51 +1112,56 @@ export class Timeline extends TimelineEventsEmitter {
   /**
    * Convert screen pixel to value.
    */
-  public pxToVal(coords: number, absolute = false): number {
-    if (!absolute) {
-      coords -= this._options.leftMargin;
-    }
-    const ms = (coords / this._options.stepPx) * this._currentZoom;
-    return ms;
+  public pxToVal(px: number): number {
+    const steps = this._options.stepVal * this._currentZoom || 1;
+    const val = (px / this._options.stepPx) * steps;
+    return val;
   }
 
   /**
-   * Convert area value to screen pixel coordinates.
+   * Convert value to local screen component coordinates.
    */
-  public valToPx(ms: number, absolute = false): number {
-    // Respect current scroll container offset. (virtualization)
-    if (!absolute && this._scrollContainer) {
-      const x = this._scrollContainer.scrollLeft;
-      ms -= this.pxToVal(x);
-    }
-
+  _toScreenPx(val: number): number {
+    return this.valToPx(val) - this.getScrollLeft() + this._leftMargin();
+  }
+  /**
+   * Convert screen local coordinates to a global value info.
+   */
+  _fromScreen(px: number): number {
+    return this.pxToVal(this.getScrollLeft() + px);
+  }
+  /**
+   * Convert area value to global screen pixel coordinates.
+   */
+  public valToPx(val: number): number {
     if (!this._options) {
-      return ms;
+      return val;
     }
-    return (ms * this._options.stepPx) / this._currentZoom;
+    const steps = this._options.stepVal * this._currentZoom || 1;
+    return (val * this._options.stepPx) / steps;
   }
 
   /**
-   * Snap a value to a nearest beautiful point.
+   * Snap a value to a nearest grid point.
    */
-  public snapVal(ms: number): number {
-    // Apply snap to steps if enabled.
-    if (this._options && this._options.snapsPerSeconds && this._options.snapEnabled) {
-      const stopsPerPixel = 1000 / this._options.snapsPerSeconds;
-      const step = ms / stopsPerPixel;
+  public snapVal(val: number): number {
+    // Snap a value if configured.
+    if (this._options && this._options.snapEnabled && this._options.snapStep) {
+      const stops = this._options.snapStep;
+      const step = val / stops;
       const stepsFit = Math.round(step);
-      ms = Math.round(stepsFit * stopsPerPixel);
-    }
-
-    if (this._options && TimelineUtils.isNumber(this._options.min) && ms < this._options.min) {
-      ms = 0;
+      const minSteps = Math.abs(this._options.min) / this._options.snapStep;
+      const minOffset = TimelineUtils.sign(this._options.min) * (minSteps - Math.floor(minSteps)) * this._options.snapStep;
+      val = Math.round(minOffset) + Math.round(stepsFit * stops);
     }
 
-    return ms;
+    val = TimelineUtils.keepInBounds(val, this._options.min, this._options.max);
+    return val;
   }
 
   _mousePosToVal(x: number, snapEnabled = false): number {
-    let convertedVal = this.pxToVal(this._scrollContainer.scrollLeft + Math.min(x, this._width()));
+    const mousePos = Math.min(x, this._width()) - this._leftMargin();
+    let convertedVal = this._fromScreen(mousePos);
     convertedVal = Math.round(convertedVal);
     if (snapEnabled) {
       convertedVal = this.snapVal(convertedVal);
@@ -1146,7 +1176,7 @@ export class Timeline extends TimelineEventsEmitter {
    * @param ms milliseconds to convert.
    * @param isSeconds whether seconds are passed.
    */
-  _formatLineGaugeText(ms: number, isSeconds = false): string {
+  _formatUnitsText(ms: number, isSeconds = false): string {
     // 1- Convert to seconds:
     let seconds = ms / 1000;
     if (isSeconds) {
@@ -1189,55 +1219,67 @@ export class Timeline extends TimelineEventsEmitter {
 
     return str;
   }
-
+  /**
+   * Left padding of the timeline.
+   */
+  _leftMargin(): number {
+    if (!this._options) {
+      return 0;
+    }
+    return this._options.leftMargin || 0;
+  }
   _renderTicks(): void {
-    if (!this._ctx || !this._options) {
+    const rulerActive = !!this._ctx && !!this._options && !!this._ctx.canvas && this._ctx.canvas.clientWidth > 0 && this._ctx.canvas.clientHeight > 0 && this._options.stepPx;
+    if (!rulerActive) {
       return;
     }
-    this._ctx.save();
-
-    const areaWidth = this._scrollContainer.scrollWidth - (this._options.leftMargin || 0);
-    let from = this.pxToVal(this._options.min);
-    let to = this.pxToVal(areaWidth);
-    const dist = TimelineUtils.getDistance(from, to);
-    if (dist === 0) {
+    const screenWidth = this._width() - this._leftMargin();
+    let from = this.pxToVal(this.getScrollLeft());
+    let to = this.pxToVal(this.getScrollLeft() + screenWidth);
+    if (isNaN(from) || isNaN(to) || from === to) {
       return;
     }
-    // normalize step.
-    const stepsCanFit = areaWidth / this._options.stepPx;
-    const realStep = dist / stepsCanFit;
-    // Find the nearest 'beautiful' step for a line gauge. This step should be divided by 1/2/5!
-    //let step = realStep;
-    const step = TimelineUtils.findGoodStep(realStep);
-    if (step == 0 || isNaN(step) || !isFinite(step)) {
-      return;
+
+    if (to < from) {
+      const wasToVal = to;
+      to = from;
+      from = wasToVal;
     }
-    const goodStepDistancePx = areaWidth / (dist / step);
-    const smallStepsCanFit = goodStepDistancePx / this._options.stepSmallPx;
-    const realSmallStep = step / smallStepsCanFit;
-    let smallStep = TimelineUtils.findGoodStep(realSmallStep, step);
-    if (step % smallStep != 0) {
-      smallStep = realSmallStep;
+
+    const valDistance = TimelineUtils.getDistance(from, to);
+    if (valDistance <= 0) {
+      return;
     }
-    // filter to draw only visible
-    const visibleFrom = this.pxToVal(this._scrollContainer.scrollLeft + this._options.leftMargin || 0);
-    const visibleTo = this.pxToVal(this._scrollContainer.scrollLeft + this._scrollContainer.clientWidth);
+
+    // Find the nearest 'beautiful' step for a gauge.
+    // 'beautiful' step should be dividable by 1/2/5/10!
+    const step = TimelineUtils.findGoodStep(valDistance / (screenWidth / this._options.stepPx));
+    const smallStep = TimelineUtils.findGoodStep(valDistance / (screenWidth / this._options.stepSmallPx));
+
     // Find beautiful start point:
-    from = Math.floor(visibleFrom / step) * step;
+    const fromVal = Math.floor(from / step) * step;
 
     // Find a beautiful end point:
-    to = Math.ceil(visibleTo / step) * step + step;
+    const toVal = Math.ceil(to / step) * step + step;
+
+    if (!TimelineUtils.isNumber(step) || step <= 0 || Math.abs(toVal - fromVal) === 0) {
+      return;
+    }
 
-    let lastTextX: number | null = null;
-    for (let i = from; i <= to; i += step) {
-      const pos = this.valToPx(i);
-      const sharpPos = this._getSharp(Math.round(pos));
+    let lastTextStart = 0;
+    this._ctx.save();
+    const headerHeight = TimelineStyleUtils.headerHeight(this._options);
+    const tickHeight = headerHeight / 2;
+    const smallTickHeight = headerHeight / 1.3;
+    for (let i = fromVal; i <= toVal; i += step) {
+      // local
+      const sharpPos = this._getSharp(this._toScreenPx(i));
       this._ctx.save();
       this._ctx.beginPath();
       this._ctx.setLineDash([4]);
       this._ctx.lineWidth = 1;
       this._ctx.strokeStyle = this._options.tickColor;
-      TimelineUtils.drawLine(this._ctx, sharpPos, TimelineStyleUtils.headerHeight(this._options) / 2, sharpPos, this._height());
+      TimelineUtils.drawLine(this._ctx, sharpPos, tickHeight, sharpPos, headerHeight);
       this._ctx.stroke();
 
       this._ctx.fillStyle = this._options.labelsColor;
@@ -1245,25 +1287,28 @@ export class Timeline extends TimelineEventsEmitter {
         this._ctx.font = this._options.font;
       }
 
-      const text = this._formatLineGaugeText(i);
+      const text = this._formatUnitsText(i);
       const textSize = this._ctx.measureText(text);
 
       const textX = sharpPos - textSize.width / 2;
       // skip text render if there is no space for it.
-      if (isNaN(lastTextX) || lastTextX <= textX) {
-        lastTextX = textX + textSize.width;
+      if (isNaN(lastTextStart) || lastTextStart <= textX) {
+        lastTextStart = textX + textSize.width;
         this._ctx.fillText(text, textX, 10);
       }
 
       this._ctx.restore();
+      if (!TimelineUtils.isNumber(smallStep) || smallStep <= 0) {
+        continue;
+      }
       // Draw small steps
       for (let x = i + smallStep; x < i + step; x += smallStep) {
-        const nextPos = this.valToPx(x);
-        const nextSharpPos = this._getSharp(Math.floor(nextPos));
+        // local
+        const nextSharpPos = this._getSharp(this._toScreenPx(x));
         this._ctx.beginPath();
         this._ctx.lineWidth = this._pixelRatio;
         this._ctx.strokeStyle = this._options.tickColor;
-        TimelineUtils.drawLine(this._ctx, nextSharpPos, TimelineStyleUtils.headerHeight(this._options) / 1.3, nextSharpPos, TimelineStyleUtils.headerHeight(this._options));
+        TimelineUtils.drawLine(this._ctx, nextSharpPos, smallTickHeight, nextSharpPos, headerHeight);
         this._ctx.stroke();
       }
     }
@@ -1271,29 +1316,6 @@ export class Timeline extends TimelineEventsEmitter {
     this._ctx.restore();
   }
 
-  _setMinMax(to: TimelineRanged, from: TimelineRanged, shrink = false): TimelineRanged {
-    if (!from || !to) {
-      return to;
-    }
-    const isFromMinNumber = TimelineUtils.isNumber(from.min);
-    const isToMinNumber = TimelineUtils.isNumber(to.min);
-    // get absolute min and max bounds:
-    if (isFromMinNumber && isToMinNumber) {
-      to.min = shrink ? Math.min(from.min, to.min) : Math.max(from.min, to.min);
-    } else if (isFromMinNumber) {
-      to.min = from.min;
-    }
-    const isFromMaxNumber = TimelineUtils.isNumber(from.max);
-    const isToMaxNumber = TimelineUtils.isNumber(to.max);
-    if (isFromMaxNumber && isToMaxNumber) {
-      to.max = shrink ? Math.max(from.max, to.max) : Math.min(from.max, to.max);
-    } else if (isFromMaxNumber) {
-      to.max = from.max;
-    }
-
-    return to;
-  }
-
   /**
    * calculate virtual mode. Determine screen positions for the elements.
    */
@@ -1396,17 +1418,17 @@ export class Timeline extends TimelineEventsEmitter {
 
       calcRow.groups.forEach((group) => {
         // Extend row min max bounds by a group bounds:
-        this._setMinMax(calcRow, group, true);
+        TimelineUtils.setMinMax(calcRow, group, true);
         // get group screen coords
         const groupRect = this._getKeyframesGroupSize(row, calcRow.size.y, group.min, group.max);
         group.size = groupRect;
       });
 
       // Extend screen bounds by a current calculation:
-      this._setMinMax(toReturn, calcRow, true);
+      TimelineUtils.setMinMax(toReturn, calcRow, true);
     });
     if (TimelineUtils.isNumber(toReturn.max)) {
-      toReturn.size.width = this.valToPx(toReturn.max, true);
+      toReturn.size.width = this.valToPx(toReturn.max);
     }
     return toReturn;
   }
@@ -1503,10 +1525,9 @@ export class Timeline extends TimelineEventsEmitter {
     }
 
     const margin = height - (groupHeight as number);
-
     // draw keyframes rows.
-    const xMin = this.valToPx(minValue);
-    const xMax = this.valToPx(maxValue);
+    const xMin = this._toScreenPx(minValue); // local
+    const xMax = this._toScreenPx(maxValue); // local
 
     return {
       x: xMin,
@@ -1543,7 +1564,7 @@ export class Timeline extends TimelineEventsEmitter {
     if (height > 0) {
       if (!isNaN(val)) {
         const toReturn = {
-          x: Math.floor(this.valToPx(val)),
+          x: Math.floor(this._toScreenPx(val)), // local
           y: Math.floor(y),
           height: height,
           width: width,
@@ -1674,14 +1695,14 @@ export class Timeline extends TimelineEventsEmitter {
   }
 
   _renderTimeline(): void {
-    if (!this._options || !this._options.timelineStyle) {
+    if (!this._ctx || !this._options || !this._options.timelineStyle) {
       return;
     }
     const style = this._options.timelineStyle;
     this._ctx.save();
     const thickness = style.width || 1;
     this._ctx.lineWidth = thickness * this._pixelRatio;
-    const timeLinePos = this._getSharp(Math.round(this.valToPx(this._val)), thickness);
+    const timeLinePos = this._getSharp(this._toScreenPx(this._val), thickness);
     this._ctx.strokeStyle = style.strokeColor;
     this._ctx.fillStyle = style.fillColor;
     const y = style.marginTop;
@@ -1754,7 +1775,7 @@ export class Timeline extends TimelineEventsEmitter {
       return;
     }
     // Rescale when animation is played out of the bounds.
-    if (this.valToPx(this._val, true) > this._scrollContainer.scrollWidth) {
+    if (this.valToPx(this._val) > this._scrollContainer.scrollWidth) {
       this.rescale();
       if (!this._isPanStarted && this._drag && this._drag.type !== TimelineElementType.Timeline) {
         this.scrollLeft();
@@ -1792,6 +1813,7 @@ export class Timeline extends TimelineEventsEmitter {
    * Find sharp pixel position
    */
   _getSharp(pos: number, thickness = 1): number {
+    pos = Math.round(pos);
     if (thickness % 2 == 0) {
       return pos;
     }
@@ -1865,17 +1887,36 @@ export class Timeline extends TimelineEventsEmitter {
   }
 
   /**
-   * Set this._options.
+   * Set options and render the component.
    * Options will be merged with the defaults and control invalidated
    */
   public setOptions(toSet: TimelineOptions): TimelineOptions {
-    this._options = this._mergeOptions(toSet);
+    this._options = this._setOptions(toSet);
     this.rescale();
     this.redraw();
     // Merged options:
     return this._options;
   }
 
+  _setOptions(toSet: TimelineOptions): TimelineOptions {
+    this._options = this._mergeOptions(toSet);
+    // Normalize and validate spans per value.
+    this._options.snapStep = TimelineUtils.keepInBounds(this._options.snapStep, 0, this._options.stepVal);
+    this._currentZoom = this._setZoom(this._options.zoom, this._options.zoomMin, this._options.zoomMax);
+    this._options.min = TimelineUtils.isNumber(this._options.min) ? this._options.min : 0;
+    this._options.max = TimelineUtils.isNumber(this._options.max) ? this._options.max : Number.MAX_VALUE;
+    if (this._scrollContainer) {
+      const classList = this._scrollContainer.classList;
+      if (this._options.scrollContainerClass && classList.contains(this._options.scrollContainerClass)) {
+        classList.add(this._options.scrollContainerClass);
+      }
+      if (this._options.fillColor) {
+        this._scrollContainer.style.background = this._options.fillColor;
+      }
+    }
+    return this._options;
+  }
+
   public getModel(): TimelineModel {
     return this._model;
   }
@@ -1891,7 +1932,7 @@ export class Timeline extends TimelineEventsEmitter {
   }
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  _getMousePos(canvas: HTMLCanvasElement, e: TouchEvent | MouseEvent | any): MousePoint {
+  _getMousePos(canvas: HTMLCanvasElement, e: TouchEvent | MouseEvent | any): MouseData {
     let radius = 1;
     let clientX = 0;
     let clientY = 0;
@@ -1917,7 +1958,8 @@ export class Timeline extends TimelineEventsEmitter {
       x: x,
       y: y,
       radius,
-    } as MousePoint;
+      args: e,
+    } as MouseData;
   }
 
   /**
@@ -1961,7 +2003,7 @@ export class Timeline extends TimelineEventsEmitter {
       const additionalOffset = this._options.stepPx;
       newWidth = newWidth || 0;
       // not less than current timeline position
-      const timelineGlobalPos = this.valToPx(this._val, true);
+      const timelineGlobalPos = this.valToPx(this._val);
       let timelinePos = 0;
       if (timelineGlobalPos > this._width()) {
         if (scrollMode == 'scrollBySelection') {
@@ -1970,14 +2012,14 @@ export class Timeline extends TimelineEventsEmitter {
           timelinePos = Math.floor(timelineGlobalPos + this._width() / 1.5);
         }
       }
-      const keyframeW = data.size.width + this._options.leftMargin + additionalOffset;
+      const keyframeW = data.size.width + this._leftMargin() + additionalOffset;
 
       newWidth = Math.max(
         newWidth,
         // keyframes size
         keyframeW,
         // not less than current scroll position
-        this._scrollContainer.scrollLeft + this._width(),
+        this.getScrollLeft() + this._width(),
         timelinePos,
       );
 
@@ -2070,7 +2112,7 @@ export class Timeline extends TimelineEventsEmitter {
 
     const headerHeight = TimelineStyleUtils.headerHeight(this._options);
     // Check whether we can drag timeline.
-    const timeLinePos = this.valToPx(this._val);
+    const timeLinePos = this._toScreenPx(this._val);
     let width = 0;
     if (this._options && this._options.timelineStyle) {
       const timelineStyle = this._options.timelineStyle;
@@ -2083,7 +2125,7 @@ export class Timeline extends TimelineEventsEmitter {
         type: TimelineElementType.Timeline,
       } as TimelineElement);
     }
-
+    const snap = this._options.snapEnabled;
     if (pos.y >= headerHeight && this._options.keyframesDraggable) {
       this._forEachKeyframe((calcKeyframe, index, isNextRow): void => {
         // Check keyframes group overlap
@@ -2091,7 +2133,7 @@ export class Timeline extends TimelineEventsEmitter {
           const rowOverlapped = TimelineUtils.isOverlap(pos.x, pos.y, calcKeyframe.parentRow.size);
           if (rowOverlapped) {
             const row = {
-              val: this._mousePosToVal(pos.x, true),
+              val: this._mousePosToVal(pos.x, snap),
               keyframes: calcKeyframe.parentRow.model.keyframes,
               type: TimelineElementType.Row,
               row: calcKeyframe.parentRow.model,
@@ -2104,7 +2146,7 @@ export class Timeline extends TimelineEventsEmitter {
               if (keyframesGroupOverlapped) {
                 const keyframesModels = this._mapKeyframes(group.keyframes);
                 const groupElement = {
-                  val: this._mousePosToVal(pos.x, true),
+                  val: this._mousePosToVal(pos.x, snap),
                   type: TimelineElementType.Group,
                   group: group,
                   row: calcKeyframe.parentRow.model,
@@ -2224,8 +2266,8 @@ export class Timeline extends TimelineEventsEmitter {
   _emitScrollEvent(args: MouseEvent | null): TimelineScrollEvent {
     const scrollEvent = {
       args: args,
-      scrollLeft: this._scrollContainer.scrollLeft,
-      scrollTop: this._scrollContainer.scrollTop,
+      scrollLeft: this.getScrollLeft(),
+      scrollTop: this.getScrollTop(),
       scrollHeight: this._scrollContainer.scrollHeight,
       scrollWidth: this._scrollContainer.scrollWidth,
     } as TimelineScrollEvent;

+ 45 - 1
src/utils/timelineUtils.ts

@@ -1,11 +1,16 @@
+import { TimelineRanged } from '../timelineRanged';
+
 const denominators = [1, 2, 5, 10];
 export class TimelineUtils {
   static drawLine(ctx: CanvasRenderingContext2D, x1: number, y1: number, x2: number, y2: number): void {
     ctx.moveTo(x1, y1);
     ctx.lineTo(x2, y2);
   }
+  /**
+   * Check is valid number.
+   */
   static isNumber(val?: number): boolean {
-    if (typeof val === 'number' && !isNaN(val)) {
+    if (typeof val === 'number' && !isNaN(val) && Number.isFinite(val)) {
       return true;
     }
 
@@ -37,6 +42,9 @@ export class TimelineUtils {
    * Find beautiful step for the header line gauge.
    */
   static findGoodStep(originalStep: number, divisionCheck = 0): number {
+    if (originalStep <= 0 || isNaN(originalStep) || !Number.isFinite(originalStep)) {
+      return originalStep;
+    }
     let step = originalStep;
     let lastDistance = null;
     const pow = TimelineUtils.getPowArgument(originalStep);
@@ -60,7 +68,43 @@ export class TimelineUtils {
 
     return step;
   }
+  /**
+   * Keep value in min, max bounds.
+   */
+  static keepInBounds(value: number, min: number | undefined = null, max: number | undefined = null): number {
+    if (TimelineUtils.isNumber(value)) {
+      if (TimelineUtils.isNumber(min)) {
+        value = Math.max(value, min);
+      }
+      if (TimelineUtils.isNumber(max)) {
+        value = Math.min(value, max);
+      }
+    }
 
+    return value;
+  }
+  static setMinMax(to: TimelineRanged, from: TimelineRanged, shrink = false): TimelineRanged {
+    if (!from || !to) {
+      return to;
+    }
+    const isFromMinNumber = TimelineUtils.isNumber(from.min);
+    const isToMinNumber = TimelineUtils.isNumber(to.min);
+    // get absolute min and max bounds:
+    if (isFromMinNumber && isToMinNumber) {
+      to.min = shrink ? Math.min(from.min, to.min) : Math.max(from.min, to.min);
+    } else if (isFromMinNumber) {
+      to.min = from.min;
+    }
+    const isFromMaxNumber = TimelineUtils.isNumber(from.max);
+    const isToMaxNumber = TimelineUtils.isNumber(to.max);
+    if (isFromMaxNumber && isToMaxNumber) {
+      to.max = shrink ? Math.max(from.max, to.max) : Math.min(from.max, to.max);
+    } else if (isFromMaxNumber) {
+      to.max = from.max;
+    }
+
+    return to;
+  }
   static isRectOverlap(rect: DOMRect, rect2: DOMRect): boolean {
     if (!rect || !rect2) {
       console.log('Rectangles cannot be empty');

+ 6 - 6
tests/js/settingsTests.js

@@ -7,11 +7,11 @@ describe('_mergeOptions', function () {
     it('Top level options are merged', function () {
         var timeline = new animation_timeline_1.Timeline();
         var defOptions = animation_timeline_1.defaultTimelineOptions;
-        var options = { id: 'new id', snapsPerSeconds: 10, snapEnabled: true };
+        var options = { id: 'new id', snapStep: 10, snapEnabled: true };
         var merged = timeline._mergeOptions(options);
         asserts_1.assert.equal(merged.id, options.id);
         asserts_1.assert.equal(merged.snapEnabled, options.snapEnabled);
-        asserts_1.assert.equal(merged.snapsPerSeconds, options.snapsPerSeconds);
+        asserts_1.assert.equal(merged.snapStep, options.snapStep);
         asserts_1.assert.equal(merged.labelsColor, defOptions.labelsColor);
         asserts_1.assert.equal(merged.leftMargin, defOptions.leftMargin);
         asserts_1.assert.equal(merged.selectionColor, defOptions.selectionColor);
@@ -19,7 +19,7 @@ describe('_mergeOptions', function () {
     });
     it('Default styles are merged', function () {
         var timeline = new animation_timeline_1.Timeline();
-        var options = { id: 'new id', snapsPerSeconds: 10, snapEnabled: true };
+        var options = { id: 'new id', snapStep: 10, snapEnabled: true };
         var merged = timeline._mergeOptions(options);
         asserts_1.assert.equal(merged.id, options.id);
         asserts_1.assert.equal(!!merged.rowsStyle, true, 'Row style cannot be null');
@@ -29,7 +29,7 @@ describe('_mergeOptions', function () {
         var timeline = new animation_timeline_1.Timeline();
         var options = {
             id: 'new id',
-            snapsPerSeconds: 10,
+            snapStep: 10,
             headerHeight: 44,
             snapEnabled: true,
             rowsStyle: {
@@ -53,11 +53,11 @@ describe('_mergeOptions', function () {
         var timeline = new animation_timeline_1.Timeline();
         var options = {
             id: 'new id',
-            snapsPerSeconds: 10,
+            snapStep: 10,
         };
         var merged = timeline._mergeOptions(options);
         asserts_1.assert.equal(merged.id, 'new id');
-        asserts_1.assert.equal(merged.snapsPerSeconds, 10);
+        asserts_1.assert.equal(merged.snapStep, 10);
         asserts_1.assert.equal(options.headerHeight === undefined, true);
     });
 });

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
tests/js/settingsTests.js.map


+ 105 - 0
tests/js/timelineTests.js

@@ -376,6 +376,111 @@ describe('Timeline', function () {
             });
         });
     });
+    describe('Coordinates', function () {
+        it('Coordinates', function () {
+            var timeline = new animation_timeline_1.Timeline();
+            timeline._setOptions({
+                stepVal: 100,
+                stepPx: 50,
+                zoom: 1,
+            });
+            asserts_1.assert.equal(timeline.valToPx(0), 0);
+            asserts_1.assert.equal(timeline.valToPx(100), 50);
+            asserts_1.assert.equal(timeline.valToPx(200), 100);
+            asserts_1.assert.equal(timeline.pxToVal(0), 0);
+            asserts_1.assert.equal(timeline.pxToVal(50), 100);
+            asserts_1.assert.equal(timeline.pxToVal(100), 200);
+        });
+        it('Zoom is respected', function () {
+            var timeline = new animation_timeline_1.Timeline();
+            timeline._setOptions({
+                stepVal: 100,
+                stepPx: 50,
+                zoom: 1,
+            });
+            asserts_1.assert.equal(timeline.valToPx(0), 0);
+            asserts_1.assert.equal(timeline.valToPx(100), 50);
+            asserts_1.assert.equal(timeline.valToPx(200), 100);
+            timeline._setZoom(2);
+            asserts_1.assert.equal(timeline.valToPx(0), 0);
+            asserts_1.assert.equal(timeline.valToPx(100), 25);
+            asserts_1.assert.equal(timeline.valToPx(200), 50);
+            asserts_1.assert.equal(timeline.pxToVal(0), 0);
+            asserts_1.assert.equal(timeline.pxToVal(25), 100);
+            asserts_1.assert.equal(timeline.pxToVal(50), 200);
+        });
+    });
+    describe('Snapping', function () {
+        it('Snapping', function () {
+            var timeline = new animation_timeline_1.Timeline();
+            timeline._setOptions({
+                stepVal: 100,
+                stepPx: 50,
+                snapStep: 25,
+                zoom: 1,
+            });
+            asserts_1.assert.equal(timeline.snapVal(0), 0);
+            asserts_1.assert.equal(timeline.snapVal(10), 0);
+            asserts_1.assert.equal(timeline.snapVal(26), 25);
+            asserts_1.assert.equal(timeline.snapVal(48), 50);
+            asserts_1.assert.equal(timeline.snapVal(58), 50);
+        });
+        it('Snapping. min is defined', function () {
+            var timeline = new animation_timeline_1.Timeline();
+            timeline._setOptions({
+                stepVal: 100,
+                stepPx: 50,
+                snapStep: 25,
+                min: 5,
+                zoom: 1,
+            });
+            asserts_1.assert.equal(timeline.snapVal(0), 5);
+            asserts_1.assert.equal(timeline.snapVal(10), 5);
+            asserts_1.assert.equal(timeline.snapVal(26), 30);
+            asserts_1.assert.equal(timeline.snapVal(48), 55);
+            asserts_1.assert.equal(timeline.snapVal(58), 55);
+            // Don't overlap the limit.
+            asserts_1.assert.equal(timeline.snapVal(-100), 5);
+        });
+        it('Snapping. negative min is defined', function () {
+            var timeline = new animation_timeline_1.Timeline();
+            timeline._setOptions({
+                stepVal: 100,
+                stepPx: 50,
+                snapStep: 25,
+                min: -55,
+                zoom: 1,
+            });
+            asserts_1.assert.equal(timeline.snapVal(0), -5);
+            asserts_1.assert.equal(timeline.snapVal(10), -5);
+            asserts_1.assert.equal(timeline.snapVal(26), 20);
+            asserts_1.assert.equal(timeline.snapVal(48), 45);
+            asserts_1.assert.equal(timeline.snapVal(58), 45);
+            asserts_1.assert.equal(timeline.snapVal(-1), -5);
+            asserts_1.assert.equal(timeline.snapVal(-10), -5);
+            asserts_1.assert.equal(timeline.snapVal(-26), -30);
+            asserts_1.assert.equal(timeline.snapVal(-48), -55);
+            asserts_1.assert.equal(timeline.snapVal(-58), -55);
+            // Don't overlap the limit.
+            asserts_1.assert.equal(timeline.snapVal(-100), -55);
+        });
+        it('Snapping. negative min (-25) is defined', function () {
+            var timeline = new animation_timeline_1.Timeline();
+            timeline._setOptions({
+                stepVal: 100,
+                stepPx: 50,
+                snapStep: 25,
+                min: -25,
+                zoom: 1,
+            });
+            asserts_1.assert.equal(timeline.snapVal(-1), 0);
+            asserts_1.assert.equal(timeline.snapVal(-10), 0);
+            asserts_1.assert.equal(timeline.snapVal(10), 0);
+            asserts_1.assert.equal(timeline.snapVal(26), 25);
+            asserts_1.assert.equal(timeline.snapVal(50), 50);
+            asserts_1.assert.equal(timeline.snapVal(-58), -25);
+        });
+    });
     describe('Move Keyframes', function () {
         it('move left', function () {
             var timeline = new animation_timeline_1.Timeline();

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
tests/js/timelineTests.js.map


+ 6 - 6
tests/settingsTests.ts

@@ -5,11 +5,11 @@ describe('_mergeOptions', function () {
   it('Top level options are merged', function () {
     const timeline = new Timeline();
     const defOptions = defaultTimelineOptions as TimelineOptions;
-    const options = { id: 'new id', snapsPerSeconds: 10, snapEnabled: true } as TimelineOptions;
+    const options = { id: 'new id', snapStep: 10, snapEnabled: true } as TimelineOptions;
     const merged = timeline._mergeOptions(options);
     assert.equal(merged.id, options.id);
     assert.equal(merged.snapEnabled, options.snapEnabled);
-    assert.equal(merged.snapsPerSeconds, options.snapsPerSeconds);
+    assert.equal(merged.snapStep, options.snapStep);
     assert.equal(merged.labelsColor, defOptions.labelsColor);
     assert.equal(merged.leftMargin, defOptions.leftMargin);
     assert.equal(merged.selectionColor, defOptions.selectionColor);
@@ -19,7 +19,7 @@ describe('_mergeOptions', function () {
 
   it('Default styles are merged', function () {
     const timeline = new Timeline();
-    const options = { id: 'new id', snapsPerSeconds: 10, snapEnabled: true } as TimelineOptions;
+    const options = { id: 'new id', snapStep: 10, snapEnabled: true } as TimelineOptions;
     const merged = timeline._mergeOptions(options);
     assert.equal(merged.id, options.id);
     assert.equal(!!merged.rowsStyle, true, 'Row style cannot be null');
@@ -30,7 +30,7 @@ describe('_mergeOptions', function () {
     const timeline = new Timeline();
     const options = {
       id: 'new id',
-      snapsPerSeconds: 10,
+      snapStep: 10,
       headerHeight: 44,
       snapEnabled: true,
       rowsStyle: {
@@ -54,11 +54,11 @@ describe('_mergeOptions', function () {
     const timeline = new Timeline();
     const options = {
       id: 'new id',
-      snapsPerSeconds: 10,
+      snapStep: 10,
     } as TimelineOptions;
     const merged = timeline._mergeOptions(options);
     assert.equal(merged.id, 'new id');
-    assert.equal(merged.snapsPerSeconds, 10);
+    assert.equal(merged.snapStep, 10);
     assert.equal(options.headerHeight === undefined, true);
   });
 });

+ 117 - 0
tests/timelineTests.ts

@@ -396,6 +396,123 @@ describe('Timeline', function () {
       });
     });
   });
+  describe('Coordinates', function () {
+    it('Coordinates', function () {
+      const timeline = new Timeline();
+      timeline._setOptions({
+        stepVal: 100,
+        stepPx: 50,
+        zoom: 1,
+      } as TimelineOptions);
+
+      assert.equal(timeline.valToPx(0), 0);
+      assert.equal(timeline.valToPx(100), 50);
+      assert.equal(timeline.valToPx(200), 100);
+
+      assert.equal(timeline.pxToVal(0), 0);
+      assert.equal(timeline.pxToVal(50), 100);
+      assert.equal(timeline.pxToVal(100), 200);
+    });
+
+    it('Zoom is respected', function () {
+      const timeline = new Timeline();
+      timeline._setOptions({
+        stepVal: 100,
+        stepPx: 50,
+        zoom: 1,
+      } as TimelineOptions);
+
+      assert.equal(timeline.valToPx(0), 0);
+      assert.equal(timeline.valToPx(100), 50);
+      assert.equal(timeline.valToPx(200), 100);
+      timeline._setZoom(2);
+      assert.equal(timeline.valToPx(0), 0);
+      assert.equal(timeline.valToPx(100), 25);
+      assert.equal(timeline.valToPx(200), 50);
+
+      assert.equal(timeline.pxToVal(0), 0);
+      assert.equal(timeline.pxToVal(25), 100);
+      assert.equal(timeline.pxToVal(50), 200);
+    });
+  });
+  describe('Snapping', function () {
+    it('Snapping', function () {
+      const timeline = new Timeline();
+      timeline._setOptions({
+        stepVal: 100,
+        stepPx: 50,
+        snapStep: 25,
+        zoom: 1,
+      } as TimelineOptions);
+
+      assert.equal(timeline.snapVal(0), 0);
+      assert.equal(timeline.snapVal(10), 0);
+      assert.equal(timeline.snapVal(26), 25);
+      assert.equal(timeline.snapVal(48), 50);
+      assert.equal(timeline.snapVal(58), 50);
+    });
+    it('Snapping. min is defined', function () {
+      const timeline = new Timeline();
+      timeline._setOptions({
+        stepVal: 100,
+        stepPx: 50,
+        snapStep: 25,
+        min: 5,
+        zoom: 1,
+      } as TimelineOptions);
+
+      assert.equal(timeline.snapVal(0), 5);
+      assert.equal(timeline.snapVal(10), 5);
+      assert.equal(timeline.snapVal(26), 30);
+      assert.equal(timeline.snapVal(48), 55);
+      assert.equal(timeline.snapVal(58), 55);
+
+      // Don't overlap the limit.
+      assert.equal(timeline.snapVal(-100), 5);
+    });
+    it('Snapping. negative min is defined', function () {
+      const timeline = new Timeline();
+      timeline._setOptions({
+        stepVal: 100,
+        stepPx: 50,
+        snapStep: 25,
+        min: -55,
+        zoom: 1,
+      } as TimelineOptions);
+
+      assert.equal(timeline.snapVal(0), -5);
+      assert.equal(timeline.snapVal(10), -5);
+      assert.equal(timeline.snapVal(26), 20);
+      assert.equal(timeline.snapVal(48), 45);
+      assert.equal(timeline.snapVal(58), 45);
+
+      assert.equal(timeline.snapVal(-1), -5);
+      assert.equal(timeline.snapVal(-10), -5);
+      assert.equal(timeline.snapVal(-26), -30);
+      assert.equal(timeline.snapVal(-48), -55);
+      assert.equal(timeline.snapVal(-58), -55);
+
+      // Don't overlap the limit.
+      assert.equal(timeline.snapVal(-100), -55);
+    });
+    it('Snapping. negative min (-25) is defined', function () {
+      const timeline = new Timeline();
+      timeline._setOptions({
+        stepVal: 100,
+        stepPx: 50,
+        snapStep: 25,
+        min: -25,
+        zoom: 1,
+      } as TimelineOptions);
+
+      assert.equal(timeline.snapVal(-1), 0);
+      assert.equal(timeline.snapVal(-10), 0);
+      assert.equal(timeline.snapVal(10), 0);
+      assert.equal(timeline.snapVal(26), 25);
+      assert.equal(timeline.snapVal(50), 50);
+      assert.equal(timeline.snapVal(-58), -25);
+    });
+  });
   describe('Move Keyframes', function () {
     it('move left', function () {
       const timeline = new Timeline();

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä