Browse Source

Added groups stroke and border radius support. BUG FIX: group is rendered for one keyframe.

Ievgen Naida 1 year ago
parent
commit
0aa60d3fac

+ 2 - 0
CHANGELOG.md

@@ -9,6 +9,8 @@
 - Added context menu event.
 - Extended demo to show different styling options.
 - Updated dev packages to the latest versions.
+- Added groups stroke and border radius support.
+- BUG FIX: group is rendered for one keyframe.
 
 ## [2.3.2] - 10.04.2024
 

+ 91 - 14
demo/demo.js

@@ -7,6 +7,7 @@
 var outlineContainer = document.getElementById('outline-container');
 
 function generateModel() {
+    /** @type {import('../lib/animation-timeline').TimelineGroupStyle} */
     const groupA = {
         style: {
             fillColor: '#6B9080',
@@ -16,11 +17,57 @@ function generateModel() {
             shape: 'rect',
         },
     };
+    /** @type {import('../lib/animation-timeline').TimelineGroupStyle} */
     const groupB = {
         style: {
             marginTop: 6,
         },
     };
+    /** @type {import('../lib/animation-timeline').TimelineGroupStyle} */
+    const groupC = {
+        style: {
+            strokeColor: "white",
+            strokeThickness: 1,
+        }, keyframesStyle: {
+            shape: 'none',
+        },
+    };
+    /** @type {import('../lib/animation-timeline').TimelineGroupStyle} */
+    const groupD = {
+        style: {
+            fillColor: "transparent",
+            strokeColor: "gray",
+            strokeThickness: 2,
+            radii: 3,
+            keyframesStyle: {
+                shape: "none"
+            }
+        },
+    };
+    /** @type {import('../lib/animation-timeline').TimelineGroupStyle} */
+    const groupWhite1 = {
+        style: {
+            fillColor: "white",
+            strokeColor: "white",
+            strokeThickness: 2,
+            radii: 3,
+            keyframesStyle: {
+                shape: "none"
+            }
+        },
+    };
+    /** @type {import('../lib/animation-timeline').TimelineGroupStyle} */
+    const groupWhite2 = {
+        style: {
+            fillColor: "white",
+            strokeColor: "white",
+            strokeThickness: 2,
+            radii: 3,
+            keyframesStyle: {
+                shape: "none"
+            }
+        },
+    };
     /** @type {import('../lib/animation-timeline').TimelineModel} */
     let timelineModel = {
         rows: [
@@ -193,27 +240,61 @@ function generateModel() {
                     },
                     {
                         min: 900,
-                        max: 3400,
-                        val: 1900,
+                        max: 1400,
+                        val: 900,
                         group: groupB,
                     },
                     {
-                        val: 4000,
+                        val: 2000,
                         group: groupB,
                     },
+                    {
+                        val: 2100,
+                        group: groupC,
+                    },
+                    {
+                        val: 5000,
+                        group: groupC,
+                    },
                 ],
             },
             {
+                title: 'Groups Overlap',
+                keyframesDraggable: false,
+                groupsDraggable: false,
                 keyframes: [
                     {
                         val: 100,
+                        group: groupD,
+                    },
+                    {
+                        val: 4000,
+                        group: groupD,
                     },
                     {
-                        val: 3410,
+                        min: 100,
+                        max: 4000,
+                        val: 500,
+                        group: groupWhite1,
                     },
                     {
-                        val: 2000,
+                        min: 100,
+                        max: 4000,
+                        val: 1500,
+                        group: groupWhite1,
                     },
+                    {
+                        min: 100,
+                        max: 4000,
+                        val: 2500,
+                        group: groupWhite2,
+                    },
+                    {
+                        min: 100,
+                        max: 4000,
+                        val: 3600,
+                        group: groupWhite2,
+                    }
                 ],
             },
             {
@@ -264,8 +345,6 @@ function generateModel() {
                     },
                 ],
             },
-            {},
-            {},
             {
                 title: 'Custom Height',
                 style: {
@@ -372,16 +451,14 @@ timeline.onContextMenu(function (obj) {
     logDraggingMessage(obj, 'addKeyframe');
 
     obj.elements.forEach(p => {
-        if (p.type === "row") {
-            if (!p.keyframes) {
-                p.keyframes = []
+        if (p.type === "row" && p.row) {
+            if (!p.row?.keyframes) {
+                p.row.keyframes = []
             }
-            p.keyframes?.push({ val: obj.point?.val || 0 });
+            p.row?.keyframes?.push({ val: obj.point?.val || 0 });
         }
     })
-    timeline.redraw();
-
-
+    timeline.redraw()
 });
 
 timeline.onMouseDown(function (obj) {

+ 1 - 0
lib/animation-timeline.d.ts

@@ -9,6 +9,7 @@ export * from './settings/timelineOptions';
 export * from './settings/styles/timelineKeyframeStyle';
 export * from './settings/styles/timelineRowStyle';
 export * from './settings/styles/timelineStyle';
+export * from './settings/styles/timelineGroupStyle';
 export * from './utils/timelineStyleUtils';
 export * from './utils/timelineUtils';
 export * from './utils/timelineElement';

+ 101 - 43
lib/animation-timeline.js

@@ -797,6 +797,39 @@ var TimelineStyleUtils = /*#__PURE__*/function () {
       rowStyle === null || rowStyle === void 0 || (_rowStyle$groupsStyle11 = rowStyle.groupsStyle) === null || _rowStyle$groupsStyle11 === void 0 ? void 0 : _rowStyle$groupsStyle11.fillColor, // global styles
       options === null || options === void 0 || (_options$rowsStyle19 = options.rowsStyle) === null || _options$rowsStyle19 === void 0 || (_options$rowsStyle19 = _options$rowsStyle19.groupsStyle) === null || _options$rowsStyle19 === void 0 ? void 0 : _options$rowsStyle19.fillColor);
     }
+  }, {
+    key: "groupStrokeColor",
+    value: function groupStrokeColor(options, group, rowStyle) {
+      var _TimelineStyleUtils$g13, _rowStyle$groupsStyle12, _options$rowsStyle20;
+      return TimelineStyleUtils.getFirstSet(
+      // default value
+      defaultGroupStyle.strokeColor || '', // exact style
+      (_TimelineStyleUtils$g13 = TimelineStyleUtils.getGroupStyle(group)) === null || _TimelineStyleUtils$g13 === void 0 ? void 0 : _TimelineStyleUtils$g13.strokeColor, // Row row style
+      rowStyle === null || rowStyle === void 0 || (_rowStyle$groupsStyle12 = rowStyle.groupsStyle) === null || _rowStyle$groupsStyle12 === void 0 ? void 0 : _rowStyle$groupsStyle12.strokeColor, // global styles
+      options === null || options === void 0 || (_options$rowsStyle20 = options.rowsStyle) === null || _options$rowsStyle20 === void 0 || (_options$rowsStyle20 = _options$rowsStyle20.groupsStyle) === null || _options$rowsStyle20 === void 0 ? void 0 : _options$rowsStyle20.strokeColor);
+    }
+  }, {
+    key: "groupStrokeThickness",
+    value: function groupStrokeThickness(options, group, rowStyle) {
+      var _TimelineStyleUtils$g14, _rowStyle$groupsStyle13, _options$rowsStyle21;
+      return TimelineStyleUtils.getFirstSet(
+      // default value
+      defaultGroupStyle.strokeThickness || '', // exact style
+      (_TimelineStyleUtils$g14 = TimelineStyleUtils.getGroupStyle(group)) === null || _TimelineStyleUtils$g14 === void 0 ? void 0 : _TimelineStyleUtils$g14.strokeThickness, // Row row style
+      rowStyle === null || rowStyle === void 0 || (_rowStyle$groupsStyle13 = rowStyle.groupsStyle) === null || _rowStyle$groupsStyle13 === void 0 ? void 0 : _rowStyle$groupsStyle13.strokeThickness, // global styles
+      options === null || options === void 0 || (_options$rowsStyle21 = options.rowsStyle) === null || _options$rowsStyle21 === void 0 || (_options$rowsStyle21 = _options$rowsStyle21.groupsStyle) === null || _options$rowsStyle21 === void 0 ? void 0 : _options$rowsStyle21.strokeThickness) || 0;
+    }
+  }, {
+    key: "groupsRadii",
+    value: function groupsRadii(options, group, rowStyle) {
+      var _TimelineStyleUtils$g15, _rowStyle$groupsStyle14, _options$rowsStyle22;
+      return TimelineStyleUtils.getFirstSet(
+      // default value
+      defaultGroupStyle.radii || '', // exact style
+      (_TimelineStyleUtils$g15 = TimelineStyleUtils.getGroupStyle(group)) === null || _TimelineStyleUtils$g15 === void 0 ? void 0 : _TimelineStyleUtils$g15.radii, // Row row style
+      rowStyle === null || rowStyle === void 0 || (_rowStyle$groupsStyle14 = rowStyle.groupsStyle) === null || _rowStyle$groupsStyle14 === void 0 ? void 0 : _rowStyle$groupsStyle14.radii, // global styles
+      options === null || options === void 0 || (_options$rowsStyle22 = options.rowsStyle) === null || _options$rowsStyle22 === void 0 || (_options$rowsStyle22 = _options$rowsStyle22.groupsStyle) === null || _options$rowsStyle22 === void 0 ? void 0 : _options$rowsStyle22.radii) || 0;
+    }
 
     /**
      * Get current row height from styles
@@ -804,35 +837,35 @@ var TimelineStyleUtils = /*#__PURE__*/function () {
   }, {
     key: "getRowHeight",
     value: function getRowHeight(rowStyle, options) {
-      var _options$rowsStyle20;
+      var _options$rowsStyle23;
       var defaultValue = defaultTimelineRowStyle.height || 0;
       return TimelineStyleUtils.getFirstSet(
       // default value
       defaultValue, // exact style
       rowStyle === null || rowStyle === void 0 ? void 0 : rowStyle.height, // Style set by global options
-      options === null || options === void 0 || (_options$rowsStyle20 = options.rowsStyle) === null || _options$rowsStyle20 === void 0 ? void 0 : _options$rowsStyle20.height);
+      options === null || options === void 0 || (_options$rowsStyle23 = options.rowsStyle) === null || _options$rowsStyle23 === void 0 ? void 0 : _options$rowsStyle23.height);
     }
   }, {
     key: "getRowMarginBottom",
     value: function getRowMarginBottom(rowStyle, options) {
-      var _options$rowsStyle21;
+      var _options$rowsStyle24;
       var defaultValue = defaultTimelineRowStyle.marginBottom || 0;
       return TimelineStyleUtils.getFirstSet(
       // default value
       defaultValue, // exact style
       rowStyle === null || rowStyle === void 0 ? void 0 : rowStyle.marginBottom, // Style set by global options
-      options === null || options === void 0 || (_options$rowsStyle21 = options.rowsStyle) === null || _options$rowsStyle21 === void 0 ? void 0 : _options$rowsStyle21.marginBottom);
+      options === null || options === void 0 || (_options$rowsStyle24 = options.rowsStyle) === null || _options$rowsStyle24 === void 0 ? void 0 : _options$rowsStyle24.marginBottom);
     }
   }, {
     key: "getRowFillColor",
     value: function getRowFillColor(rowStyle, options) {
-      var _options$rowsStyle22;
+      var _options$rowsStyle25;
       var defaultValue = defaultTimelineRowStyle.fillColor || '';
       return TimelineStyleUtils.getFirstSet(
       // default value
       defaultValue, // exact style
       rowStyle === null || rowStyle === void 0 ? void 0 : rowStyle.fillColor, // Style set by global options
-      options === null || options === void 0 || (_options$rowsStyle22 = options.rowsStyle) === null || _options$rowsStyle22 === void 0 ? void 0 : _options$rowsStyle22.fillColor);
+      options === null || options === void 0 || (_options$rowsStyle25 = options.rowsStyle) === null || _options$rowsStyle25 === void 0 ? void 0 : _options$rowsStyle25.fillColor);
     }
   }, {
     key: "headerHeight",
@@ -843,12 +876,12 @@ var TimelineStyleUtils = /*#__PURE__*/function () {
   }, {
     key: "keyframeDraggable",
     value: function keyframeDraggable(keyframe, group, row, options) {
-      var _TimelineStyleUtils$g13;
+      var _TimelineStyleUtils$g16;
       var defaultValue = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
       var findFirstNegativeBool = true;
       var boolResult = TimelineStyleUtils.getValue(defaultValue, findFirstNegativeBool, // Keyframe settings
       keyframe === null || keyframe === void 0 ? void 0 : keyframe.draggable, // Group settings
-      (_TimelineStyleUtils$g13 = TimelineStyleUtils.getGroup(group)) === null || _TimelineStyleUtils$g13 === void 0 ? void 0 : _TimelineStyleUtils$g13.keyframesDraggable, // Row settings
+      (_TimelineStyleUtils$g16 = TimelineStyleUtils.getGroup(group)) === null || _TimelineStyleUtils$g16 === void 0 ? void 0 : _TimelineStyleUtils$g16.keyframesDraggable, // Row settings
       row === null || row === void 0 ? void 0 : row.keyframesDraggable, // Start from global settings first.
       options === null || options === void 0 ? void 0 : options.keyframesDraggable);
       return boolResult;
@@ -856,10 +889,10 @@ var TimelineStyleUtils = /*#__PURE__*/function () {
   }, {
     key: "groupDraggable",
     value: function groupDraggable(group, row, options) {
-      var _TimelineStyleUtils$g14;
+      var _TimelineStyleUtils$g17;
       var findFirstNegativeBool = true;
       var boolResult = TimelineStyleUtils.getValue(true, findFirstNegativeBool, // Group settings
-      (_TimelineStyleUtils$g14 = TimelineStyleUtils.getGroup(group)) === null || _TimelineStyleUtils$g14 === void 0 ? void 0 : _TimelineStyleUtils$g14.draggable, // Row settings
+      (_TimelineStyleUtils$g17 = TimelineStyleUtils.getGroup(group)) === null || _TimelineStyleUtils$g17 === void 0 ? void 0 : _TimelineStyleUtils$g17.draggable, // Row settings
       row === null || row === void 0 ? void 0 : row.groupsDraggable, // Start from global settings first.
       options === null || options === void 0 ? void 0 : options.groupsDraggable);
       return boolResult;
@@ -2815,25 +2848,45 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
         return;
       }
       rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$groupsV2 = rowViewModel.groupsViewModels) === null || _rowViewModel$groupsV2 === void 0 || _rowViewModel$groupsV2.forEach(function (groupsViewModels) {
-        var _rowViewModel$model;
+        var _groupsViewModels$key, _rowViewModel$model, _rowViewModel$model2, _rowViewModel$model3, _rowViewModel$model4;
         if (!_this._ctx) {
           return;
         }
-        var keyframeLaneColor = TimelineStyleUtils.groupFillColor(_this._options, groupsViewModels.groupModel, rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$model = rowViewModel.model) === null || _rowViewModel$model === void 0 ? void 0 : _rowViewModel$model.style);
-        if (!keyframeLaneColor) {
+        if (((groupsViewModels === null || groupsViewModels === void 0 || (_groupsViewModels$key = groupsViewModels.keyframesViewModels) === null || _groupsViewModels$key === void 0 ? void 0 : _groupsViewModels$key.length) || 0) <= 1) {
           return;
         }
+        var groupFillColor = TimelineStyleUtils.groupFillColor(_this._options, groupsViewModels.groupModel, rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$model = rowViewModel.model) === null || _rowViewModel$model === void 0 ? void 0 : _rowViewModel$model.style);
+        var strokeColor = TimelineStyleUtils.groupStrokeColor(_this._options, groupsViewModels.groupModel, rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$model2 = rowViewModel.model) === null || _rowViewModel$model2 === void 0 ? void 0 : _rowViewModel$model2.style);
+        var groupStrokeThickness = TimelineStyleUtils.groupStrokeThickness(_this._options, groupsViewModels.groupModel, rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$model3 = rowViewModel.model) === null || _rowViewModel$model3 === void 0 ? void 0 : _rowViewModel$model3.style);
+        var groupsRadii = TimelineStyleUtils.groupsRadii(_this._options, groupsViewModels.groupModel, rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$model4 = rowViewModel.model) === null || _rowViewModel$model4 === void 0 ? void 0 : _rowViewModel$model4.style);
         if (!groupsViewModels.size) {
           console.log('Size of the group cannot be calculated');
           return;
         }
+        try {
+          _this._ctx.save();
 
-        // get the bounds on a canvas
-        var rectBounds = _this._cutBounds(groupsViewModels.size);
-        if (rectBounds !== null && rectBounds !== void 0 && rectBounds.rect) {
-          _this._ctx.fillStyle = keyframeLaneColor;
-          var rect = rectBounds.rect;
-          _this._ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
+          // get the bounds on a canvas
+          var rectBounds = _this._cutBounds(groupsViewModels.size);
+          if (rectBounds !== null && rectBounds !== void 0 && rectBounds.rect) {
+            var rect = rectBounds.rect;
+            if (!strokeColor) {
+              groupStrokeThickness = 0;
+            }
+            // Manipulate it again
+            _this._ctx.strokeStyle = groupStrokeThickness > 0 ? strokeColor : '';
+            _this._ctx.fillStyle = groupFillColor;
+            _this._ctx.lineWidth = groupStrokeThickness;
+            // Different radii for each corner, top-left clockwise to bottom-left
+            _this._ctx.beginPath();
+            _this._ctx.roundRect(rect.x + groupStrokeThickness, rect.y + groupStrokeThickness, rect.width - groupStrokeThickness, rect.height - groupStrokeThickness, groupsRadii);
+            _this._ctx.fill();
+            if (groupStrokeThickness > 0) {
+              _this._ctx.stroke();
+            }
+          }
+        } finally {
+          _this._ctx.restore();
         }
       });
     });
@@ -2888,12 +2941,12 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
      * @param rowY row screen coords y position
      */
     timeline_defineProperty(_this, "_getKeyframesGroupSize", function (groupViewModel, rowViewModel) {
-      var _rowViewModel$model2, _rowViewModel$model3;
+      var _rowViewModel$model5, _rowViewModel$model6;
       var rowY = rowViewModel.size.y;
       var rowHeight = rowViewModel.size.height;
       var groupModel = groupViewModel.groupModel || null;
-      var groupHeight = TimelineStyleUtils.groupHeight(_this._options, groupModel, rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$model2 = rowViewModel.model) === null || _rowViewModel$model2 === void 0 ? void 0 : _rowViewModel$model2.style);
-      var marginTop = TimelineStyleUtils.groupMarginTop(_this._options, groupModel, rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$model3 = rowViewModel.model) === null || _rowViewModel$model3 === void 0 ? void 0 : _rowViewModel$model3.style);
+      var groupHeight = TimelineStyleUtils.groupHeight(_this._options, groupModel, rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$model5 = rowViewModel.model) === null || _rowViewModel$model5 === void 0 ? void 0 : _rowViewModel$model5.style);
+      var marginTop = TimelineStyleUtils.groupMarginTop(_this._options, groupModel, rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$model6 = rowViewModel.model) === null || _rowViewModel$model6 === void 0 ? void 0 : _rowViewModel$model6.style);
       var isAutoHeight = groupHeight === 'auto';
       if (!groupHeight || isAutoHeight) {
         groupHeight = Math.floor(rowHeight);
@@ -2928,7 +2981,7 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       };
     });
     timeline_defineProperty(_this, "_getKeyframePosition", function (keyframe, groupViewModel, rowViewModel, keyframeShape) {
-      var _rowViewModel$model4, _rowViewModel$model5;
+      var _rowViewModel$model7, _rowViewModel$model8;
       if (!keyframe) {
         console.log('keyframe should be defined.');
         return null;
@@ -2941,8 +2994,8 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       // get center of the lane:
       var y = rowSize.y + rowSize.height / 2;
       var groupModel = (groupViewModel === null || groupViewModel === void 0 ? void 0 : groupViewModel.groupModel) || null;
-      var height = TimelineStyleUtils.keyframeHeight(keyframe, groupModel, rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$model4 = rowViewModel.model) === null || _rowViewModel$model4 === void 0 ? void 0 : _rowViewModel$model4.style, _this._options);
-      var width = TimelineStyleUtils.keyframeWidth(keyframe, groupModel, rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$model5 = rowViewModel.model) === null || _rowViewModel$model5 === void 0 ? void 0 : _rowViewModel$model5.style, _this._options);
+      var height = TimelineStyleUtils.keyframeHeight(keyframe, groupModel, rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$model7 = rowViewModel.model) === null || _rowViewModel$model7 === void 0 ? void 0 : _rowViewModel$model7.style, _this._options);
+      var width = TimelineStyleUtils.keyframeWidth(keyframe, groupModel, rowViewModel === null || rowViewModel === void 0 || (_rowViewModel$model8 = rowViewModel.model) === null || _rowViewModel$model8 === void 0 ? void 0 : _rowViewModel$model8.style, _this._options);
       if (height === 'auto') {
         height = rowSize.height / 3;
       }
@@ -3547,16 +3600,6 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
           var rowViewModel = keyframeViewModel.rowViewModel;
           // Check keyframes group overlap
           if (isNextRow) {
-            var rowOverlapped = TimelineUtils.isOverlap(pos.x, pos.y, rowViewModel.size);
-            if (rowOverlapped) {
-              var row = {
-                val: _this._mousePosToVal(pos.x, snap),
-                keyframes: rowViewModel.model.keyframes,
-                type: TimelineElementType.Row,
-                row: rowViewModel.model
-              };
-              toReturn.push(row);
-            }
             if (rowViewModel.groupsViewModels) {
               rowViewModel.groupsViewModels.forEach(function (groupViewModel) {
                 if (!(groupViewModel !== null && groupViewModel !== void 0 && groupViewModel.size)) {
@@ -3608,6 +3651,17 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
               });
             }
           }
+        }, function (rowViewModel) {
+          var rowOverlapped = TimelineUtils.isOverlap(pos.x, pos.y, rowViewModel.size);
+          if (rowOverlapped) {
+            var row = {
+              val: _this._mousePosToVal(pos.x, snap),
+              keyframes: rowViewModel.model.keyframes,
+              type: TimelineElementType.Row,
+              row: rowViewModel.model
+            };
+            toReturn.push(row);
+          }
         });
       }
       if (!onlyTypes || onlyTypes.length === 0) {
@@ -3876,9 +3930,9 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
     /**
      * foreach visible keyframe.
      */
-    function _forEachKeyframe(callback) {
+    function _forEachKeyframe(callback, onRowCallback) {
       var _calculatedModel$rows;
-      if (!callback) {
+      if (!callback && !onRowCallback) {
         return;
       }
       if (!this._model) {
@@ -3892,13 +3946,16 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
         if (!rowViewModel) {
           return;
         }
+        onRowCallback && onRowCallback(rowViewModel);
         var nextRow = true;
-        rowViewModel.keyframesViewModels.forEach(function (keyframeViewModel, keyframeIndex) {
-          if (keyframeViewModel) {
-            callback(keyframeViewModel, keyframeIndex, nextRow);
-          }
-          nextRow = false;
-        });
+        if (callback) {
+          rowViewModel.keyframesViewModels.forEach(function (keyframeViewModel, keyframeIndex) {
+            if (keyframeViewModel) {
+              callback(keyframeViewModel, keyframeIndex, nextRow);
+            }
+            nextRow = false;
+          });
+        }
       });
     }
 
@@ -4181,6 +4238,7 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
 
 
+
 // @private helper containers.
 
 

File diff suppressed because it is too large
+ 0 - 0
lib/animation-timeline.js.map


File diff suppressed because it is too large
+ 0 - 0
lib/animation-timeline.min.js


+ 11 - 3
lib/settings/styles/timelineGroupStyle.d.ts

@@ -12,11 +12,19 @@ export interface TimelineGroupStyle {
     /**
      * Group stroke color.
      */
-    strokeColor: string;
+    strokeColor?: string;
+    /**
+     * Group stroke thickness.
+     */
+    strokeThickness?: number | null;
+    /**
+     * Group border radius. See canvas roundRect official documentation.
+     */
+    radii?: number | DOMPointInit | Iterable<number | DOMPointInit>;
     /**
      * Group fill color.
      */
-    fillColor: string;
+    fillColor?: string;
     /**
      * Group mouse over cursor style.
      */
@@ -28,5 +36,5 @@ export interface TimelineGroupStyle {
     /**
      * Style for all the keyframes in the current group.
      */
-    keyframesStyle: TimelineKeyframeStyle;
+    keyframesStyle?: TimelineKeyframeStyle;
 }

+ 1 - 1
lib/timeline.d.ts

@@ -303,7 +303,7 @@ export declare class Timeline extends TimelineEventsEmitter {
     /**
      * foreach visible keyframe.
      */
-    _forEachKeyframe(callback: (keyframe: TimelineKeyframeViewModel, index?: number, newRow?: boolean) => void): void;
+    _forEachKeyframe(callback: (keyframe: TimelineKeyframeViewModel, index?: number, newRow?: boolean) => void, onRowCallback?: (rowViewModel: TimelineRowViewModel) => void): void;
     /**
      * Private.
      * Create extended mouse position and calculate size of the selection rectangle.

+ 3 - 0
lib/utils/timelineStyleUtils.d.ts

@@ -29,6 +29,9 @@ export declare class TimelineStyleUtils {
     static groupHeight(options: TimelineOptions | null | undefined, group: TimelineGroup | string | null | undefined, rowStyle: TimelineRowStyle | null | undefined): number | string;
     static groupMarginTop(options: TimelineOptions | null | undefined, group: TimelineGroup | string | null | undefined, rowStyle: TimelineRowStyle | null | undefined): number | string;
     static groupFillColor(options: TimelineOptions | null | undefined, group: TimelineGroup | string | null | undefined, rowStyle: TimelineRowStyle | null | undefined): string;
+    static groupStrokeColor(options: TimelineOptions | null | undefined, group: TimelineGroup | string | null | undefined, rowStyle: TimelineRowStyle | null | undefined): string;
+    static groupStrokeThickness(options: TimelineOptions | null | undefined, group: TimelineGroup | string | null | undefined, rowStyle: TimelineRowStyle | null | undefined): number;
+    static groupsRadii(options: TimelineOptions | null | undefined, group: TimelineGroup | string | null | undefined, rowStyle: TimelineRowStyle | null | undefined): number | DOMPointInit | Iterable<number | DOMPointInit>;
     /**
      * Get current row height from styles
      */

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "animation-timeline-js",
-  "version": "2.3.2",
+  "version": "2.3.3",
   "description": "animation timeline control based on the canvas.",
   "main": "lib/animation-timeline.min.js",
   "types": "lib/animation-timeline.d.ts",
@@ -71,4 +71,4 @@
     "url": "https://github.com/ievgennaida/js-animation-timeline-control/issues"
   },
   "homepage": "https://ievgennaida.github.io/animation-timeline-control/index.html"
-}
+}

+ 13 - 3
src/settings/styles/timelineGroupStyle.ts

@@ -13,11 +13,21 @@ export interface TimelineGroupStyle {
   /**
    * Group stroke color.
    */
-  strokeColor: string;
+  strokeColor?: string;
+  /**
+   * Group stroke thickness.
+   */
+  strokeThickness?: number | null;
+
+  /**
+   * Group border radius. See canvas roundRect official documentation.
+   */
+  radii?: number | DOMPointInit | Iterable<number | DOMPointInit>;
+
   /**
    * Group fill color.
    */
-  fillColor: string;
+  fillColor?: string;
   /**
    * Group mouse over cursor style.
    */
@@ -29,5 +39,5 @@ export interface TimelineGroupStyle {
   /**
    * Style for all the keyframes in the current group.
    */
-  keyframesStyle: TimelineKeyframeStyle;
+  keyframesStyle?: TimelineKeyframeStyle;
 }

+ 95 - 68
src/timeline.ts

@@ -1165,8 +1165,8 @@ export class Timeline extends TimelineEventsEmitter {
   /**
    * foreach visible keyframe.
    */
-  _forEachKeyframe(callback: (keyframe: TimelineKeyframeViewModel, index?: number, newRow?: boolean) => void): void {
-    if (!callback) {
+  _forEachKeyframe(callback: (keyframe: TimelineKeyframeViewModel, index?: number, newRow?: boolean) => void, onRowCallback?: (rowViewModel: TimelineRowViewModel) => void): void {
+    if (!callback && !onRowCallback) {
       return;
     }
     if (!this._model) {
@@ -1182,15 +1182,17 @@ export class Timeline extends TimelineEventsEmitter {
       if (!rowViewModel) {
         return;
       }
-
+      onRowCallback && onRowCallback(rowViewModel);
       let nextRow = true;
-      rowViewModel.keyframesViewModels.forEach((keyframeViewModel, keyframeIndex) => {
-        if (keyframeViewModel) {
-          callback(keyframeViewModel, keyframeIndex, nextRow);
-        }
+      if (callback) {
+        rowViewModel.keyframesViewModels.forEach((keyframeViewModel, keyframeIndex) => {
+          if (keyframeViewModel) {
+            callback(keyframeViewModel, keyframeIndex, nextRow);
+          }
 
-        nextRow = false;
-      });
+          nextRow = false;
+        });
+      }
     });
   }
 
@@ -1808,21 +1810,42 @@ export class Timeline extends TimelineEventsEmitter {
       if (!this._ctx) {
         return;
       }
-      const keyframeLaneColor = TimelineStyleUtils.groupFillColor(this._options, groupsViewModels.groupModel, rowViewModel?.model?.style);
-      if (!keyframeLaneColor) {
+      if ((groupsViewModels?.keyframesViewModels?.length || 0) <= 1) {
         return;
       }
+      const groupFillColor = TimelineStyleUtils.groupFillColor(this._options, groupsViewModels.groupModel, rowViewModel?.model?.style);
+      const strokeColor = TimelineStyleUtils.groupStrokeColor(this._options, groupsViewModels.groupModel, rowViewModel?.model?.style);
+      let groupStrokeThickness = TimelineStyleUtils.groupStrokeThickness(this._options, groupsViewModels.groupModel, rowViewModel?.model?.style);
+      const groupsRadii = TimelineStyleUtils.groupsRadii(this._options, groupsViewModels.groupModel, rowViewModel?.model?.style);
       if (!groupsViewModels.size) {
         console.log('Size of the group cannot be calculated');
         return;
       }
 
-      // get the bounds on a canvas
-      const rectBounds = this._cutBounds(groupsViewModels.size);
-      if (rectBounds?.rect) {
-        this._ctx.fillStyle = keyframeLaneColor;
-        const rect = rectBounds.rect;
-        this._ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
+      try {
+        this._ctx.save();
+
+        // get the bounds on a canvas
+        const rectBounds = this._cutBounds(groupsViewModels.size);
+        if (rectBounds?.rect) {
+          const rect = rectBounds.rect;
+          if (!strokeColor) {
+            groupStrokeThickness = 0;
+          }
+          // Manipulate it again
+          this._ctx.strokeStyle = groupStrokeThickness > 0 ? strokeColor : '';
+          this._ctx.fillStyle = groupFillColor;
+          this._ctx.lineWidth = groupStrokeThickness;
+          // Different radii for each corner, top-left clockwise to bottom-left
+          this._ctx.beginPath();
+          this._ctx.roundRect(rect.x + groupStrokeThickness, rect.y + groupStrokeThickness, rect.width - groupStrokeThickness, rect.height - groupStrokeThickness, groupsRadii);
+          this._ctx.fill();
+          if (groupStrokeThickness > 0) {
+            this._ctx.stroke();
+          }
+        }
+      } finally {
+        this._ctx.restore();
       }
     });
   };
@@ -2596,10 +2619,59 @@ export class Timeline extends TimelineEventsEmitter {
     }
     const snap = this._options.snapEnabled;
     if (pos.y >= headerHeight && this._options.keyframesDraggable) {
-      this._forEachKeyframe((keyframeViewModel, _, isNextRow): void => {
-        const rowViewModel = keyframeViewModel.rowViewModel;
-        // Check keyframes group overlap
-        if (isNextRow) {
+      this._forEachKeyframe(
+        (keyframeViewModel, _, isNextRow): void => {
+          const rowViewModel = keyframeViewModel.rowViewModel;
+          // Check keyframes group overlap
+          if (isNextRow) {
+            if (rowViewModel.groupsViewModels) {
+              rowViewModel.groupsViewModels.forEach((groupViewModel) => {
+                if (!groupViewModel?.size) {
+                  return;
+                }
+                const keyframesGroupOverlapped = TimelineUtils.isOverlap(pos.x, pos.y, groupViewModel.size);
+                if (keyframesGroupOverlapped) {
+                  const keyframesModels = groupViewModel?.keyframesViewModels.map((p) => p.model) || [];
+                  const groupElement = {
+                    // TODO:
+                    val: this._mousePosToVal(pos.x, snap),
+                    type: TimelineElementType.Group,
+                    group: groupViewModel.groupModel,
+                    row: rowViewModel.model,
+                    keyframes: keyframesModels,
+                  } as TimelineElement;
+
+                  const snapped = this.snapVal(groupViewModel.min);
+                  // get snapped mouse pos based on a min value.
+                  groupElement.val += groupViewModel.min - snapped;
+                  toReturn.push(groupElement);
+                }
+              });
+            }
+          }
+
+          const keyframePosRect = keyframeViewModel.size;
+          if (keyframePosRect) {
+            let isMouseOver = false;
+            if (keyframeViewModel.shape === TimelineKeyframeShape.Rect) {
+              const extendedMouseRect = TimelineUtils.shrinkSelf({ x: pos.x, y: pos.y, height: clickRadius, width: clickRadius } as DOMRect, clickRadius);
+              isMouseOver = TimelineUtils.isRectIntersects(extendedMouseRect, keyframePosRect, true);
+            } else {
+              const dist = TimelineUtils.getDistance(keyframePosRect.x, keyframePosRect.y, pos.x, pos.y);
+              isMouseOver = dist <= keyframePosRect.height + clickRadius;
+            }
+            if (isMouseOver) {
+              toReturn.push({
+                keyframe: keyframeViewModel.model,
+                keyframes: [keyframeViewModel.model],
+                val: keyframeViewModel.model.val,
+                row: keyframeViewModel.rowViewModel.model,
+                type: TimelineElementType.Keyframe,
+              } as TimelineElement);
+            }
+          }
+        },
+        (rowViewModel) => {
           const rowOverlapped = TimelineUtils.isOverlap(pos.x, pos.y, rowViewModel.size);
           if (rowOverlapped) {
             const row = {
@@ -2610,53 +2682,8 @@ export class Timeline extends TimelineEventsEmitter {
             } as TimelineElement;
             toReturn.push(row);
           }
-          if (rowViewModel.groupsViewModels) {
-            rowViewModel.groupsViewModels.forEach((groupViewModel) => {
-              if (!groupViewModel?.size) {
-                return;
-              }
-              const keyframesGroupOverlapped = TimelineUtils.isOverlap(pos.x, pos.y, groupViewModel.size);
-              if (keyframesGroupOverlapped) {
-                const keyframesModels = groupViewModel?.keyframesViewModels.map((p) => p.model) || [];
-                const groupElement = {
-                  // TODO:
-                  val: this._mousePosToVal(pos.x, snap),
-                  type: TimelineElementType.Group,
-                  group: groupViewModel.groupModel,
-                  row: rowViewModel.model,
-                  keyframes: keyframesModels,
-                } as TimelineElement;
-
-                const snapped = this.snapVal(groupViewModel.min);
-                // get snapped mouse pos based on a min value.
-                groupElement.val += groupViewModel.min - snapped;
-                toReturn.push(groupElement);
-              }
-            });
-          }
-        }
-
-        const keyframePosRect = keyframeViewModel.size;
-        if (keyframePosRect) {
-          let isMouseOver = false;
-          if (keyframeViewModel.shape === TimelineKeyframeShape.Rect) {
-            const extendedMouseRect = TimelineUtils.shrinkSelf({ x: pos.x, y: pos.y, height: clickRadius, width: clickRadius } as DOMRect, clickRadius);
-            isMouseOver = TimelineUtils.isRectIntersects(extendedMouseRect, keyframePosRect, true);
-          } else {
-            const dist = TimelineUtils.getDistance(keyframePosRect.x, keyframePosRect.y, pos.x, pos.y);
-            isMouseOver = dist <= keyframePosRect.height + clickRadius;
-          }
-          if (isMouseOver) {
-            toReturn.push({
-              keyframe: keyframeViewModel.model,
-              keyframes: [keyframeViewModel.model],
-              val: keyframeViewModel.model.val,
-              row: keyframeViewModel.rowViewModel.model,
-              type: TimelineElementType.Keyframe,
-            } as TimelineElement);
-          }
-        }
-      });
+        },
+      );
     }
 
     if (!onlyTypes || onlyTypes.length === 0) {

+ 46 - 0
src/utils/timelineStyleUtils.ts

@@ -278,6 +278,52 @@ export class TimelineStyleUtils {
       options?.rowsStyle?.groupsStyle?.fillColor,
     );
   }
+  static groupStrokeColor(options: TimelineOptions | null | undefined, group: TimelineGroup | string | null | undefined, rowStyle: TimelineRowStyle | null | undefined): string {
+    return TimelineStyleUtils.getFirstSet(
+      // default value
+      defaultGroupStyle.strokeColor || '',
+      // exact style
+      TimelineStyleUtils.getGroupStyle(group)?.strokeColor,
+      // Row row style
+      rowStyle?.groupsStyle?.strokeColor,
+      // global styles
+      options?.rowsStyle?.groupsStyle?.strokeColor,
+    );
+  }
+
+  static groupStrokeThickness(options: TimelineOptions | null | undefined, group: TimelineGroup | string | null | undefined, rowStyle: TimelineRowStyle | null | undefined): number {
+    return (
+      TimelineStyleUtils.getFirstSet(
+        // default value
+        defaultGroupStyle.strokeThickness || '',
+        // exact style
+        TimelineStyleUtils.getGroupStyle(group)?.strokeThickness,
+        // Row row style
+        rowStyle?.groupsStyle?.strokeThickness,
+        // global styles
+        options?.rowsStyle?.groupsStyle?.strokeThickness,
+      ) || 0
+    );
+  }
+
+  static groupsRadii(
+    options: TimelineOptions | null | undefined,
+    group: TimelineGroup | string | null | undefined,
+    rowStyle: TimelineRowStyle | null | undefined,
+  ): number | DOMPointInit | Iterable<number | DOMPointInit> {
+    return (
+      TimelineStyleUtils.getFirstSet(
+        // default value
+        defaultGroupStyle.radii || '',
+        // exact style
+        TimelineStyleUtils.getGroupStyle(group)?.radii,
+        // Row row style
+        rowStyle?.groupsStyle?.radii,
+        // global styles
+        options?.rowsStyle?.groupsStyle?.radii,
+      ) || 0
+    );
+  }
 
   /**
    * Get current row height from styles

Some files were not shown because too many files changed in this diff