fabric 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,11 +65,6 @@ jobs:
65
65
  - stage: Unit Tests
66
66
  env: LAUNCHER=Node
67
67
  node_js: "12"
68
- - stage: Unit Tests
69
- env: LAUNCHER=Node
70
- node_js: "10"
71
- - stage: Unit Tests
72
- node_js: "8"
73
68
  - stage: Visual Tests
74
69
  env: LAUNCHER=Node
75
70
  node_js: "12"
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "fabric",
3
3
  "description": "Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.",
4
4
  "homepage": "http://fabricjs.com/",
5
- "version": "4.3.0",
5
+ "version": "4.4.0",
6
6
  "author": "Juriy Zaytsev <kangax@gmail.com>",
7
7
  "contributors": [
8
8
  {
@@ -47,7 +47,10 @@
47
47
  "build_with_gestures": "node build.js modules=ALL exclude=accessors",
48
48
  "build_export": "npm run build:fast && npm run export_dist_to_site",
49
49
  "test:single": "qunit test/node_test_setup.js test/lib",
50
- "test": "nyc qunit test/node_test_setup.js test/lib test/unit",
50
+ "test:coverage": "nyc --silent qunit test/node_test_setup.js test/lib test/unit",
51
+ "test:visual:coverage": "nyc --silent --no-clean qunit test/node_test_setup.js test/lib test/visual",
52
+ "coverage:report": "nyc report --reporter=lcov --reporter=text",
53
+ "test": "qunit test/node_test_setup.js test/lib test/unit",
51
54
  "test:visual": "qunit test/node_test_setup.js test/lib test/visual",
52
55
  "test:visual:single": "qunit test/node_test_setup.js test/lib",
53
56
  "test:all": "npm run test && npm run test:visual",
@@ -58,7 +61,6 @@
58
61
  "export_tests_to_site": "cp test/unit/*.js ../fabricjs.com/test/unit && cp -r test/visual/* ../fabricjs.com/test/visual && cp -r test/fixtures/* ../fabricjs.com/test/fixtures && cp -r test/lib/* ../fabricjs.com/test/lib",
59
62
  "all": "npm run build && npm run test && npm run test:visual && npm run lint && npm run lint_tests && npm run export_dist_to_site && npm run export_tests_to_site",
60
63
  "testem": "testem .",
61
- "testem:visual": "testem --file testem-visual.json",
62
64
  "testem:ci": "testem ci"
63
65
  },
64
66
  "optionalDependencies": {
@@ -71,7 +73,7 @@
71
73
  "nyc": "^15.1.0",
72
74
  "onchange": "^3.x.x",
73
75
  "pixelmatch": "^4.0.2",
74
- "qunit": "2.9.2",
76
+ "qunit": "^2.13.0",
75
77
  "testem": "^3.2.0",
76
78
  "uglify-js": "3.3.x"
77
79
  },
@@ -56,6 +56,15 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype
56
56
  */
57
57
  strokeDashArray: null,
58
58
 
59
+ /**
60
+ * When `true`, the free drawing is limited to the whiteboard size. Default to false.
61
+ * @type Boolean
62
+ * @default false
63
+ */
64
+
65
+ limitedToCanvasSize: false,
66
+
67
+
59
68
  /**
60
69
  * Sets brush styles
61
70
  * @private
@@ -120,5 +129,14 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype
120
129
 
121
130
  ctx.shadowColor = '';
122
131
  ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0;
132
+ },
133
+
134
+ /**
135
+ * Check is pointer is outside canvas boundaries
136
+ * @param {Object} pointer
137
+ * @private
138
+ */
139
+ _isOutSideCanvas: function(pointer) {
140
+ return pointer.x < 0 || pointer.x > this.canvas.getWidth() || pointer.y < 0 || pointer.y > this.canvas.getHeight();
123
141
  }
124
142
  });
@@ -70,6 +70,9 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric
70
70
  * @param {Object} pointer
71
71
  */
72
72
  onMouseMove: function(pointer) {
73
+ if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) {
74
+ return;
75
+ }
73
76
  if (this.needsFullRender()) {
74
77
  this.canvas.clearContext(this.canvas.contextTop);
75
78
  this.addPoint(pointer);
@@ -56,6 +56,9 @@
56
56
  if (!this.canvas._isMainEvent(options.e)) {
57
57
  return;
58
58
  }
59
+ if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) {
60
+ return;
61
+ }
59
62
  if (this._captureDrawingPath(pointer) && this._points.length > 1) {
60
63
  if (this.needsFullRender()) {
61
64
  // redraw curve
@@ -74,6 +74,9 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric
74
74
  * @param {Object} pointer
75
75
  */
76
76
  onMouseMove: function(pointer) {
77
+ if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) {
78
+ return;
79
+ }
77
80
  this.addSprayChunk(pointer);
78
81
  this.render(this.sprayChunkPoints);
79
82
  },
@@ -497,13 +497,6 @@
497
497
  target.render(ctx);
498
498
  ctx.restore();
499
499
 
500
- target === this._activeObject && target._renderControls(ctx, {
501
- hasBorders: false,
502
- transparentCorners: false
503
- }, {
504
- hasBorders: false,
505
- });
506
-
507
500
  target.selectionBackgroundColor = originalColor;
508
501
 
509
502
  var isTransparent = fabric.util.isTransparent(
@@ -765,18 +758,19 @@
765
758
  activeObject = this._activeObject,
766
759
  aObjects = this.getActiveObjects(),
767
760
  activeTarget, activeTargetSubs,
768
- isTouch = isTouchEvent(e);
761
+ isTouch = isTouchEvent(e),
762
+ shouldLookForActive = (aObjects.length > 1 && !skipGroup) || aObjects.length === 1;
769
763
 
770
764
  // first check current group (if one exists)
771
765
  // active group does not check sub targets like normal groups.
772
766
  // if active group just exits.
773
767
  this.targets = [];
774
768
 
775
- if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) {
769
+ // if we hit the corner of an activeObject, let's return that.
770
+ if (shouldLookForActive && activeObject._findTargetCorner(pointer, isTouch)) {
776
771
  return activeObject;
777
772
  }
778
- // if we hit the corner of an activeObject, let's return that.
779
- if (aObjects.length === 1 && activeObject._findTargetCorner(pointer, isTouch)) {
773
+ if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) {
780
774
  return activeObject;
781
775
  }
782
776
  if (aObjects.length === 1 &&
@@ -812,7 +806,7 @@
812
806
  obj.evented &&
813
807
  // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html
814
808
  // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html
815
- (obj.containsPoint(pointer) || !!obj._findTargetCorner(pointer))
809
+ obj.containsPoint(pointer)
816
810
  ) {
817
811
  if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) {
818
812
  var isTransparent = this.isTargetTransparent(obj, globalPointer.x, globalPointer.y);
@@ -1088,38 +1082,50 @@
1088
1082
  */
1089
1083
  _fireSelectionEvents: function(oldObjects, e) {
1090
1084
  var somethingChanged = false, objects = this.getActiveObjects(),
1091
- added = [], removed = [], opt = { e: e };
1085
+ added = [], removed = [];
1092
1086
  oldObjects.forEach(function(oldObject) {
1093
1087
  if (objects.indexOf(oldObject) === -1) {
1094
1088
  somethingChanged = true;
1095
- oldObject.fire('deselected', opt);
1089
+ oldObject.fire('deselected', {
1090
+ e: e,
1091
+ target: oldObject
1092
+ });
1096
1093
  removed.push(oldObject);
1097
1094
  }
1098
1095
  });
1099
1096
  objects.forEach(function(object) {
1100
1097
  if (oldObjects.indexOf(object) === -1) {
1101
1098
  somethingChanged = true;
1102
- object.fire('selected', opt);
1099
+ object.fire('selected', {
1100
+ e: e,
1101
+ target: object
1102
+ });
1103
1103
  added.push(object);
1104
1104
  }
1105
1105
  });
1106
1106
  if (oldObjects.length > 0 && objects.length > 0) {
1107
- opt.selected = added;
1108
- opt.deselected = removed;
1109
- // added for backward compatibility
1110
- opt.updated = added[0] || removed[0];
1111
- opt.target = this._activeObject;
1112
- somethingChanged && this.fire('selection:updated', opt);
1107
+ somethingChanged && this.fire('selection:updated', {
1108
+ e: e,
1109
+ selected: added,
1110
+ deselected: removed,
1111
+ // added for backward compatibility
1112
+ // deprecated
1113
+ updated: added[0] || removed[0],
1114
+ target: this._activeObject,
1115
+ });
1113
1116
  }
1114
1117
  else if (objects.length > 0) {
1115
- opt.selected = added;
1116
- // added for backward compatibility
1117
- opt.target = this._activeObject;
1118
- this.fire('selection:created', opt);
1118
+ this.fire('selection:created', {
1119
+ e: e,
1120
+ selected: added,
1121
+ target: this._activeObject,
1122
+ });
1119
1123
  }
1120
1124
  else if (oldObjects.length > 0) {
1121
- opt.deselected = removed;
1122
- this.fire('selection:cleared', opt);
1125
+ this.fire('selection:cleared', {
1126
+ e: e,
1127
+ deselected: removed,
1128
+ });
1123
1129
  }
1124
1130
  },
1125
1131
 
@@ -1138,6 +1144,10 @@
1138
1144
  },
1139
1145
 
1140
1146
  /**
1147
+ * This is a private method for now.
1148
+ * This is supposed to be equivalent to setActiveObject but without firing
1149
+ * any event. There is commitment to have this stay this way.
1150
+ * This is the functional part of setActiveObject.
1141
1151
  * @private
1142
1152
  * @param {Object} object to set as active
1143
1153
  * @param {Event} [e] Event (passed along when firing "object:selected")
@@ -1158,6 +1168,13 @@
1158
1168
  },
1159
1169
 
1160
1170
  /**
1171
+ * This is a private method for now.
1172
+ * This is supposed to be equivalent to discardActiveObject but without firing
1173
+ * any events. There is commitment to have this stay this way.
1174
+ * This is the functional part of discardActiveObject.
1175
+ * @param {Event} [e] Event (passed along when firing "object:deselected")
1176
+ * @param {Object} object to set as active
1177
+ * @return {Boolean} true if the selection happened
1161
1178
  * @private
1162
1179
  */
1163
1180
  _discardActiveObject: function(e, object) {
@@ -1268,7 +1285,7 @@
1268
1285
  layoutProps.forEach(function(prop) {
1269
1286
  originalValues[prop] = instance[prop];
1270
1287
  });
1271
- this._activeObject.realizeTransform(instance);
1288
+ fabric.util.addTransformToObject(instance, this._activeObject.calcOwnMatrix());
1272
1289
  return originalValues;
1273
1290
  }
1274
1291
  else {
@@ -240,7 +240,7 @@
240
240
  * @return {Boolean} true if one flip, but not two.
241
241
  */
242
242
  function targetHasOneFlip(target) {
243
- return (target.flipX && !target.flipY) || (!target.flipX && target.flipY);
243
+ return target.flipX !== target.flipY;
244
244
  }
245
245
 
246
246
  /**
@@ -78,7 +78,7 @@
78
78
  ), xSizeBy2 = xSize / 2, ySizeBy2 = ySize / 2;
79
79
  ctx.save();
80
80
  ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor;
81
- ctx.strokeStyle = styleOverride.strokeCornerColor || fabricObject.strokeCornerColor;
81
+ ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor;
82
82
  // this is still wrong
83
83
  ctx.lineWidth = 1;
84
84
  ctx.translate(left, top);
@@ -451,15 +451,21 @@
451
451
  }
452
452
  }
453
453
  if (target) {
454
- var corner = target._findTargetCorner(
455
- this.getPointer(e, true),
456
- fabric.util.isTouchEvent(e)
457
- );
458
- var control = target.controls[corner],
459
- mouseUpHandler = control && control.getMouseUpHandler(e, target, control);
460
- if (mouseUpHandler) {
461
- var pointer = this.getPointer(e);
462
- mouseUpHandler(e, transform, pointer.x, pointer.y);
454
+ if (target.selectable && target !== this._activeObject && target.activeOn === 'up') {
455
+ this.setActiveObject(target, e);
456
+ shouldRender = true;
457
+ }
458
+ else {
459
+ var corner = target._findTargetCorner(
460
+ this.getPointer(e, true),
461
+ fabric.util.isTouchEvent(e)
462
+ );
463
+ var control = target.controls[corner],
464
+ mouseUpHandler = control && control.getMouseUpHandler(e, target, control);
465
+ if (mouseUpHandler) {
466
+ var pointer = this.getPointer(e);
467
+ mouseUpHandler(e, transform, pointer.x, pointer.y);
468
+ }
463
469
  }
464
470
  target.isMoving = false;
465
471
  }
@@ -715,7 +721,7 @@
715
721
 
716
722
  if (target) {
717
723
  var alreadySelected = target === this._activeObject;
718
- if (target.selectable) {
724
+ if (target.selectable && target.activeOn === 'down') {
719
725
  this.setActiveObject(target, e);
720
726
  }
721
727
  var corner = target._findTargetCorner(
@@ -924,7 +930,6 @@
924
930
  transform = this._currentTransform;
925
931
 
926
932
  transform.reset = false;
927
- transform.target.isMoving = true;
928
933
  transform.shiftKey = e.shiftKey;
929
934
  transform.altKey = e[this.centeredKey];
930
935
 
@@ -948,6 +953,7 @@
948
953
  actionPerformed = actionHandler(e, transform, x, y);
949
954
  }
950
955
  if (action === 'drag' && actionPerformed) {
956
+ transform.target.isMoving = true;
951
957
  this.setCursor(transform.target.moveCursor || this.moveCursor);
952
958
  }
953
959
  transform.actionPerformed = transform.actionPerformed || actionPerformed;
@@ -156,17 +156,27 @@
156
156
  * @chainable
157
157
  */
158
158
  addWithUpdate: function(object) {
159
+ var nested = !!this.group;
159
160
  this._restoreObjectsState();
160
161
  fabric.util.resetObjectTransform(this);
161
162
  if (object) {
163
+ if (nested) {
164
+ // if this group is inside another group, we need to pre transform the object
165
+ fabric.util.removeTransformFromObject(object, this.group.calcTransformMatrix());
166
+ }
162
167
  this._objects.push(object);
163
168
  object.group = this;
164
169
  object._set('canvas', this.canvas);
165
170
  }
166
171
  this._calcBounds();
167
172
  this._updateObjectsCoords();
168
- this.setCoords();
169
173
  this.dirty = true;
174
+ if (nested) {
175
+ this.group.addWithUpdate();
176
+ }
177
+ else {
178
+ this.setCoords();
179
+ }
170
180
  return this;
171
181
  },
172
182
 
@@ -357,12 +367,21 @@
357
367
 
358
368
  /**
359
369
  * Restores original state of each of group objects (original state is that which was before group was created).
370
+ * if the nested boolean is true, the original state will be restored just for the
371
+ * first group and not for all the group chain
360
372
  * @private
373
+ * @param {Boolean} nested tell the function to restore object state up to the parent group and not more
361
374
  * @return {fabric.Group} thisArg
362
375
  * @chainable
363
376
  */
364
377
  _restoreObjectsState: function() {
365
- this._objects.forEach(this._restoreObjectState, this);
378
+ var groupMatrix = this.calcOwnMatrix();
379
+ this._objects.forEach(function(object) {
380
+ // instead of using _this = this;
381
+ fabric.util.addTransformToObject(object, groupMatrix);
382
+ delete object.group;
383
+ object.setCoords();
384
+ });
366
385
  return this;
367
386
  },
368
387
 
@@ -371,37 +390,20 @@
371
390
  * i.e. it tells you what would happen if the supplied object was in
372
391
  * the group, and then the group was destroyed. It mutates the supplied
373
392
  * object.
393
+ * Warning: this method is not useful anymore, it has been kept to no break the api.
394
+ * is not used in the fabricJS codebase
395
+ * this method will be reduced to using the utility.
396
+ * @private
397
+ * @deprecated
374
398
  * @param {fabric.Object} object
399
+ * @param {Array} parentMatrix parent transformation
375
400
  * @return {fabric.Object} transformedObject
376
401
  */
377
- realizeTransform: function(object) {
378
- var matrix = object.calcTransformMatrix(),
379
- options = fabric.util.qrDecompose(matrix),
380
- center = new fabric.Point(options.translateX, options.translateY);
381
- object.flipX = false;
382
- object.flipY = false;
383
- object.set('scaleX', options.scaleX);
384
- object.set('scaleY', options.scaleY);
385
- object.skewX = options.skewX;
386
- object.skewY = options.skewY;
387
- object.angle = options.angle;
388
- object.setPositionByOrigin(center, 'center', 'center');
402
+ realizeTransform: function(object, parentMatrix) {
403
+ fabric.util.addTransformToObject(object, parentMatrix);
389
404
  return object;
390
405
  },
391
406
 
392
- /**
393
- * Restores original state of a specified object in group
394
- * @private
395
- * @param {fabric.Object} object
396
- * @return {fabric.Group} thisArg
397
- */
398
- _restoreObjectState: function(object) {
399
- this.realizeTransform(object);
400
- delete object.group;
401
- object.setCoords();
402
- return this;
403
- },
404
-
405
407
  /**
406
408
  * Destroys a group (restoring state of its objects)
407
409
  * @return {fabric.Group} thisArg
@@ -474,19 +476,20 @@
474
476
  _calcBounds: function(onlyWidthHeight) {
475
477
  var aX = [],
476
478
  aY = [],
477
- o, prop,
479
+ o, prop, coords,
478
480
  props = ['tr', 'br', 'bl', 'tl'],
479
481
  i = 0, iLen = this._objects.length,
480
482
  j, jLen = props.length;
481
483
 
482
484
  for ( ; i < iLen; ++i) {
483
485
  o = this._objects[i];
484
- o.aCoords = o.calcACoords();
486
+ coords = o.calcACoords();
485
487
  for (j = 0; j < jLen; j++) {
486
488
  prop = props[j];
487
- aX.push(o.aCoords[prop].x);
488
- aY.push(o.aCoords[prop].y);
489
+ aX.push(coords[prop].x);
490
+ aY.push(coords[prop].y);
489
491
  }
492
+ o.aCoords = coords;
490
493
  }
491
494
 
492
495
  this._getBounds(aX, aY, onlyWidthHeight);
@@ -92,6 +92,15 @@
92
92
  */
93
93
  stateProperties: fabric.Object.prototype.stateProperties.concat('cropX', 'cropY'),
94
94
 
95
+ /**
96
+ * List of properties to consider when checking if cache needs refresh
97
+ * Those properties are checked by statefullCache ON ( or lazy mode if we want ) or from single
98
+ * calls to Object.set(key, value). If the key is in this list, the object is marked as dirty
99
+ * and refreshed at the next render
100
+ * @type Array
101
+ */
102
+ cacheProperties: fabric.Object.prototype.cacheProperties.concat('cropX', 'cropY'),
103
+
95
104
  /**
96
105
  * key used to retrieve the texture representing this image
97
106
  * @since 2.0.0
@@ -127,7 +136,11 @@
127
136
 
128
137
  /**
129
138
  * Constructor
130
- * @param {HTMLImageElement | String} element Image element
139
+ * Image can be initialized with any canvas drawable or a string.
140
+ * The string should be a url and will be loaded as an image.
141
+ * Canvas and Image element work out of the box, while videos require extra code to work.
142
+ * Please check video element events for seeking.
143
+ * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | String} element Image element
131
144
  * @param {Object} [options] Options object
132
145
  * @param {function} [callback] callback function to call after eventual filters applied.
133
146
  * @return {fabric.Image} thisArg
@@ -552,7 +565,7 @@
552
565
  sH = min(h * scaleY, elHeight - sY),
553
566
  x = -w / 2, y = -h / 2,
554
567
  maxDestW = min(w, elWidth / scaleX - cropX),
555
- maxDestH = min(h, elHeight / scaleX - cropY);
568
+ maxDestH = min(h, elHeight / scaleY - cropY);
556
569
 
557
570
  elementToDraw && ctx.drawImage(elementToDraw, sX, sY, sW, sH, x, y, maxDestW, maxDestH);
558
571
  },
@@ -471,8 +471,9 @@
471
471
  * High level function to know the color of the cursor.
472
472
  * the currentChar is the one that precedes the cursor
473
473
  * Returns color (fill) of char at the current cursor
474
- * Unused from the library, is for the end user
475
- * @return {String} Character color (fill)
474
+ * if the text object has a pattern or gradient for filler, it will return that.
475
+ * Unused by the library, is for the end user
476
+ * @return {String | fabric.Gradient | fabric.Pattern} Character color (fill)
476
477
  */
477
478
  getCurrentCharColor: function() {
478
479
  var cp = this._getCurrentCharIndex();
@@ -571,6 +571,17 @@
571
571
  */
572
572
  paintFirst: 'fill',
573
573
 
574
+ /**
575
+ * When 'down', object is set to active on mousedown/touchstart
576
+ * When 'up', object is set to active on mouseup/touchend
577
+ * Experimental. Let's see if this breaks anything before supporting officially
578
+ * @private
579
+ * since 4.4.0
580
+ * @type String
581
+ * @default 'down'
582
+ */
583
+ activeOn: 'down',
584
+
574
585
  /**
575
586
  * List of properties to consider when checking if state
576
587
  * of an object is changed (fabric.Object#hasStateChanged)
@@ -839,7 +850,7 @@
839
850
  strokeLineCap: this.strokeLineCap,
840
851
  strokeDashOffset: this.strokeDashOffset,
841
852
  strokeLineJoin: this.strokeLineJoin,
842
- // strokeUniform: this.strokeUniform,
853
+ strokeUniform: this.strokeUniform,
843
854
  strokeMiterLimit: toFixed(this.strokeMiterLimit, NUM_FRACTION_DIGITS),
844
855
  scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS),
845
856
  scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS),
@@ -991,7 +1002,6 @@
991
1002
  this.group.set('dirty', true);
992
1003
  }
993
1004
  }
994
-
995
1005
  return this;
996
1006
  },
997
1007
 
@@ -10,9 +10,9 @@
10
10
  return;
11
11
  }
12
12
 
13
- var styleProps =
14
- 'fontFamily fontWeight fontSize text underline overline linethrough' +
15
- ' textAlign fontStyle lineHeight textBackgroundColor charSpacing styles path'.split(' ');
13
+ var additionalProps =
14
+ ('fontFamily fontWeight fontSize text underline overline linethrough' +
15
+ ' textAlign fontStyle lineHeight textBackgroundColor charSpacing styles path').split(' ');
16
16
 
17
17
  /**
18
18
  * Text class
@@ -172,13 +172,13 @@
172
172
  * as well as for history (undo/redo) purposes
173
173
  * @type Array
174
174
  */
175
- stateProperties: fabric.Object.prototype.stateProperties.concat(styleProps),
175
+ stateProperties: fabric.Object.prototype.stateProperties.concat(additionalProps),
176
176
 
177
177
  /**
178
178
  * List of properties to consider when checking if cache needs refresh
179
179
  * @type Array
180
180
  */
181
- cacheProperties: fabric.Object.prototype.cacheProperties.concat(styleProps),
181
+ cacheProperties: fabric.Object.prototype.cacheProperties.concat(additionalProps),
182
182
 
183
183
  /**
184
184
  * When defined, an object is rendered via stroke and this property specifies its color.
@@ -196,6 +196,14 @@
196
196
  */
197
197
  shadow: null,
198
198
 
199
+ /**
200
+ * fabric.Path that the text can follow.
201
+ * This feature is in BETA.
202
+ * @type fabric.Path
203
+ * @default
204
+ */
205
+ path: null,
206
+
199
207
  /**
200
208
  * @private
201
209
  */
@@ -727,6 +735,8 @@
727
735
  if (positionInPath > totalPathLength) {
728
736
  positionInPath %= totalPathLength;
729
737
  }
738
+ // it would probably much fater to send all the grapheme position for a line
739
+ // and calculate path position/angle at once.
730
740
  this._setGraphemeOnPath(positionInPath, graphemeInfo, startingPoint);
731
741
  }
732
742
  lineBounds[i] = graphemeInfo;
@@ -758,11 +768,10 @@
758
768
  path = this.path;
759
769
 
760
770
  // we are at currentPositionOnPath. we want to know what point on the path is.
761
- var p1 = fabric.util.getPointOnPath(path.path, centerPosition - 0.1, path.segmentsInfo),
762
- p2 = fabric.util.getPointOnPath(path.path, centerPosition + 0.1, path.segmentsInfo);
763
- graphemeInfo.renderLeft = p1.x - startingPoint.x;
764
- graphemeInfo.renderTop = p1.y - startingPoint.y;
765
- graphemeInfo.angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
771
+ var info = fabric.util.getPointOnPath(path.path, centerPosition, path.segmentsInfo);
772
+ graphemeInfo.renderLeft = info.x - startingPoint.x;
773
+ graphemeInfo.renderTop = info.y - startingPoint.y;
774
+ graphemeInfo.angle = info.angle;
766
775
  },
767
776
 
768
777
  /**
@@ -1302,13 +1311,15 @@
1302
1311
  (currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy)
1303
1312
  && boxWidth > 0
1304
1313
  ) {
1305
- lastDecoration && lastFill &&
1314
+ if (lastDecoration && lastFill) {
1315
+ ctx.fillStyle = lastFill;
1306
1316
  ctx.fillRect(
1307
1317
  leftOffset + lineLeftOffset + boxStart,
1308
1318
  top + this.offsets[type] * size + dy,
1309
1319
  boxWidth,
1310
1320
  this.fontSize / 15
1311
1321
  );
1322
+ }
1312
1323
  boxStart = charBox.left;
1313
1324
  boxWidth = charBox.width;
1314
1325
  lastDecoration = currentDecoration;
@@ -1411,9 +1422,11 @@
1411
1422
  'textAlign',
1412
1423
  'textBackgroundColor',
1413
1424
  'charSpacing',
1425
+ 'path'
1414
1426
  ].concat(propertiesToInclude);
1415
1427
  var obj = this.callSuper('toObject', additionalProperties);
1416
1428
  obj.styles = clone(this.styles, true);
1429
+ obj.path = this.path && this.path.toObject();
1417
1430
  return obj;
1418
1431
  },
1419
1432
 
@@ -1569,11 +1582,23 @@
1569
1582
  * Returns fabric.Text instance from an object representation
1570
1583
  * @static
1571
1584
  * @memberOf fabric.Text
1572
- * @param {Object} object Object to create an instance from
1585
+ * @param {Object} object plain js Object to create an instance from
1573
1586
  * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created
1574
1587
  */
1575
1588
  fabric.Text.fromObject = function(object, callback) {
1576
- return fabric.Object._fromObject('Text', object, callback, 'text');
1589
+ var objectCopy = clone(object), path = object.path;
1590
+ delete objectCopy.path;
1591
+ return fabric.Object._fromObject('Text', objectCopy, function(textInstance) {
1592
+ if (path) {
1593
+ fabric.Object._fromObject('Path', path, function(pathInstance) {
1594
+ textInstance.set('path', pathInstance);
1595
+ callback(textInstance);
1596
+ }, 'path');
1597
+ }
1598
+ else {
1599
+ callback(textInstance);
1600
+ }
1601
+ }, 'text');
1577
1602
  };
1578
1603
 
1579
1604
  fabric.Text.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace'];