diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f8fad28139..de6430d29c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
- fix(fabric.Text): improve compatibility with transformed gradients [#6669](https://github.com/fabricjs/fabric.js/pull/6669)
- feat(fabric.Text): Add ability to put text on paths BETA [#6543](https://github.com/fabricjs/fabric.js/pull/6543)
- fix(fabric.Canvas): rotation handle should take origin into account [#6686](https://github.com/fabricjs/fabric.js/pull/6686)
+- fix(fabric.Text): Text on path, fix non linear distance of chars over path [#6671](https://github.com/fabricjs/fabric.js/pull/6671)
## [4.2.0]
diff --git a/HEADER.js b/HEADER.js
index a8262923a60..46728498b13 100644
--- a/HEADER.js
+++ b/HEADER.js
@@ -1,6 +1,6 @@
/*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */
-var fabric = fabric || { version: '4.2.0' };
+var fabric = fabric || { version: '4.3.0' };
if (typeof exports !== 'undefined') {
exports.fabric = fabric;
}
@@ -73,7 +73,7 @@ fabric.SHARED_ATTRIBUTES = [
*/
fabric.DPI = 96;
fabric.reNum = '(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:[eE][-+]?\\d+)?)';
-fabric.commaWsp = '(?:\\s+,?\\s*|,\\s*)'
+fabric.commaWsp = '(?:\\s+,?\\s*|,\\s*)';
fabric.rePathCommand = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:[eE][-+]?\d+)?)/ig;
fabric.reNonWord = /[ \n\.,;!\?\-]/;
fabric.fontPaths = { };
diff --git a/dist/fabric.js b/dist/fabric.js
index 1239303750e..f01375e349a 100644
--- a/dist/fabric.js
+++ b/dist/fabric.js
@@ -1,7 +1,7 @@
/* build: `node build.js modules=ALL exclude=gestures,accessors requirejs minifier=uglifyjs` */
/*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */
-var fabric = fabric || { version: '4.2.0' };
+var fabric = fabric || { version: '4.3.0' };
if (typeof exports !== 'undefined') {
exports.fabric = fabric;
}
@@ -74,7 +74,7 @@ fabric.SHARED_ATTRIBUTES = [
*/
fabric.DPI = 96;
fabric.reNum = '(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:[eE][-+]?\\d+)?)';
-fabric.commaWsp = '(?:\\s+,?\\s*|,\\s*)'
+fabric.commaWsp = '(?:\\s+,?\\s*|,\\s*)';
fabric.rePathCommand = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:[eE][-+]?\d+)?)/ig;
fabric.reNonWord = /[ \n\.,;!\?\-]/;
fabric.fontPaths = { };
@@ -163,7 +163,7 @@ fabric.devicePixelRatio = fabric.window.devicePixelRatio ||
fabric.browserShadowBlurConstant = 1;
/**
- * This object contains the result of arc to beizer conversion for faster retrieving if the same arc needs to be converted again.
+ * This object contains the result of arc to bezier conversion for faster retrieving if the same arc needs to be converted again.
* It was an internal variable, is accessible since version 2.3.4
*/
fabric.arcToSegmentsCache = { };
@@ -1551,7 +1551,7 @@ fabric.CommonMethods = {
/**
* given an array of 6 number returns something like `"matrix(...numbers)"`
* @memberOf fabric.util
- * @param {Array} trasnform an array with 6 numbers
+ * @param {Array} transform an array with 6 numbers
* @return {String} transform matrix for svg
* @return {Object.y} Limited dimensions by Y
*/
@@ -1724,7 +1724,7 @@ fabric.CommonMethods = {
* @param {Number} y1
* @param {Number} x2 secondo control point
* @param {Number} y2
- * @param {Number} x3 end of beizer
+ * @param {Number} x3 end of bezier
* @param {Number} y3
*/
// taken from http://jsbin.com/ivomiq/56/edit no credits available for that.
@@ -1812,12 +1812,12 @@ fabric.CommonMethods = {
}
/**
- * Converts arc to a bunch of beizer curves
+ * Converts arc to a bunch of bezier curves
* @param {Number} fx starting point x
* @param {Number} fy starting point y
* @param {Array} coords Arc command
*/
- function fromArcToBeizers(fx, fy, coords) {
+ function fromArcToBeziers(fx, fy, coords) {
var rx = coords[1],
ry = coords[2],
rot = coords[3],
@@ -1838,7 +1838,13 @@ fabric.CommonMethods = {
return segsNorm;
};
-
+ /**
+ * This function take a parsed SVG path and make it simpler for fabricJS logic.
+ * simplification consist of: only UPPERCASE absolute commands ( relative converted to absolute )
+ * S converted in C, T converted in Q, A converted in C.
+ * @param {Array} path the array of commands of a parsed svg path for fabric.Path
+ * @return {Array} the simplified array of commands of a parsed svg path for fabric.Path
+ */
function makePathSimpler(path) {
// x and y represent the last point of the path. the previous command point.
// we add them to each relative command to make it an absolute comment.
@@ -1986,7 +1992,7 @@ fabric.CommonMethods = {
// falls through
case 'A':
converted = true;
- destinationPath = destinationPath.concat(fromArcToBeizers(x, y, current));
+ destinationPath = destinationPath.concat(fromArcToBeziers(x, y, current));
x = current[6];
y = current[7];
break;
@@ -2064,6 +2070,8 @@ fabric.CommonMethods = {
};
}
+ // this will run over a path segment ( a cubic or quadratic segment) and approximate it
+ // with 100 segemnts. This will good enough to calculate the length of the curve
function pathIterator(iterator, x1, y1) {
var tempP = { x: x1, y: y1 }, p, tmpLen = 0, perc;
for (perc = 0.01; perc <= 1; perc += 0.01) {
@@ -2074,7 +2082,43 @@ fabric.CommonMethods = {
return tmpLen;
}
- //measures the length of a pre-simplified path
+ /**
+ * Given a pathInfo, and a distance in pixels, find the percentage from 0 to 1
+ * that correspond to that pixels run over the path.
+ * The percentage will be then used to find the correct point on the canvas for the path.
+ * @param {Array} segInfo fabricJS collection of information on a parsed path
+ * @param {Number} distance from starting point, in pixels.
+ * @return {Number} length of segment
+ */
+ function findPercentageForDistance(segInfo, distance) {
+ var perc = 0, tmpLen = 0, iterator = segInfo.iterator, tempP = { x: segInfo.x, y: segInfo.y },
+ p, nextLen, nextStep = 0.01;
+ // nextStep > 0.0001 covers 0.00015625 that 1/64th of 1/100
+ // the path
+ while (tmpLen < distance && perc <= 1 && nextStep > 0.0001) {
+ p = iterator(perc);
+ nextLen = calcLineLength(tempP.x, tempP.y, p.x, p.y);
+ // compare tmpLen each cycle with distance, decide next perc to test.
+ if ((nextLen + tmpLen) > distance) {
+ // we discard this step and we make smaller steps.
+ nextStep /= 2;
+ perc -= nextStep;
+ }
+ else {
+ tempP = p;
+ perc += nextStep;
+ tmpLen += nextLen;
+ }
+ }
+ return p;
+ }
+
+ /**
+ * Run over a parsed and simplifed path and extrac some informations.
+ * informations are length of each command and starting point
+ * @param {Array} path fabricJS parsed path commands
+ * @return {Array} path commands informations
+ */
function getPathSegmentsInfo(path) {
var totalLength = 0, len = path.length, current,
//x2 and y2 are the coords of segment start
@@ -2109,6 +2153,7 @@ fabric.CommonMethods = {
current[5],
current[6]
);
+ tempInfo.iterator = iterator;
tempInfo.length = pathIterator(iterator, x1, y1);
x1 = current[5];
y1 = current[6];
@@ -2122,6 +2167,7 @@ fabric.CommonMethods = {
current[3],
current[4]
);
+ tempInfo.iterator = iterator;
tempInfo.length = pathIterator(iterator, x1, y1);
x1 = current[3];
y1 = current[4];
@@ -2143,53 +2189,37 @@ fabric.CommonMethods = {
return info;
}
- function getPointOnPath(path, perc, infos) {
+ function getPointOnPath(path, distance, infos) {
if (!infos) {
infos = getPathSegmentsInfo(path);
}
- var distance = infos[infos.length - 1] * perc, i = 0;
- while ((distance - infos[i] > 0) && i < infos.length) {
- distance -= infos[i];
+ // var distance = infos[infos.length - 1] * perc;
+ var i = 0;
+ while ((distance - infos[i].length > 0) && i < (infos.length - 2)) {
+ distance -= infos[i].length;
i++;
}
var segInfo = infos[i], segPercent = distance / segInfo.length,
- command = segInfo.length, segment = path[i];
+ command = segInfo.command, segment = path[i];
+
switch (command) {
+ case 'M':
+ return { x: segInfo.x, y: segInfo.y };
case 'Z':
case 'z':
return new fabric.Point(segInfo.x, segInfo.y).lerp(
new fabric.Point(segInfo.destX, segInfo.destY),
segPercent
);
- break;
case 'L':
return new fabric.Point(segInfo.x, segInfo.y).lerp(
new fabric.Point(segment[1], segment[2]),
segPercent
);
- break;
case 'C':
- return getPointOnCubicBezierIterator(
- segInfo.x,
- segInfo.y,
- segment[1],
- segment[2],
- segment[3],
- segment[4],
- segment[5],
- segment[6]
- )(segPercent);
- break;
+ return findPercentageForDistance(segInfo, distance);
case 'Q':
- return getPointOnQuadraticBezierIterator(
- segInfo.x,
- segInfo.y,
- segment[1],
- segment[2],
- segment[3],
- segment[4]
- )(segPercent);
- break;
+ return findPercentageForDistance(segInfo, distance);
}
}
@@ -2291,6 +2321,7 @@ fabric.CommonMethods = {
/**
* Draws arc
+ * @deprecated
* @param {CanvasRenderingContext2D} ctx
* @param {Number} fx
* @param {Number} fy
@@ -2298,16 +2329,22 @@ fabric.CommonMethods = {
*/
function drawArc(ctx, fx, fy, coords) {
coords = coords.slice(0).unshift('X'); // command A or a does not matter
- var beizers = fromArcToBeizers(fx, fy, coords);
- beizers.forEach(function(beizer) {
- ctx.bezierCurveTo.apply(ctx, beizer.slice(1));
+ var beziers = fromArcToBeziers(fx, fy, coords);
+ beziers.forEach(function(bezier) {
+ ctx.bezierCurveTo.apply(ctx, bezier.slice(1));
});
};
fabric.util.parsePath = parsePath;
fabric.util.makePathSimpler = makePathSimpler;
fabric.util.getPathSegmentsInfo = getPathSegmentsInfo;
- fabric.util.fromArcToBeizers = fromArcToBeizers;
+ fabric.util.fromArcToBeziers = fromArcToBeziers;
+ /**
+ * Typo of `fromArcToBeziers` kept for not breaking the api once corrected.
+ * Will be removed in fabric 5.0
+ * @deprecated
+ */
+ fabric.util.fromArcToBeizers = fromArcToBeziers;
fabric.util.getBoundsOfCurve = getBoundsOfCurve;
fabric.util.getPointOnPath = getPointOnPath;
// kept because we do not want to make breaking changes.
@@ -3924,7 +3961,7 @@ fabric.warn = console.warn;
return value;
}
else if (attr === 'imageSmoothing') {
- return (value === 'optimizeQuality' ? true : false);
+ return (value === 'optimizeQuality');
}
else {
parsed = isArray ? value.map(parseUnit) : parseUnit(value, fontSize);
@@ -4768,8 +4805,7 @@ fabric.warn = console.warn;
// very crude parsing of style contents
for (i = 0, len = styles.length; i < len; i++) {
- // could produce `undefined`, covering this case with ''
- var styleContents = styles[i].textContent || '';
+ var styleContents = styles[i].textContent;
// remove comments
styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, '');
@@ -6200,7 +6236,8 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
function fireEvent(eventName, options) {
var target = options.transform.target,
canvas = target.canvas,
- canvasOptions = Object.assign({}, options, { target: target });
+ canvasOptions = fabric.util.object.clone(options);
+ canvasOptions.target = target;
canvas && canvas.fire('object:' + eventName, canvasOptions);
target.fire(eventName, options);
}
@@ -6859,6 +6896,29 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
return hasResized;
}
+ /**
+ * Action handler
+ * @private
+ * @param {Event} eventData javascript event that is doing the transform
+ * @param {Object} transform javascript object containing a series of information around the current transform
+ * @param {number} x current mouse x position, canvas normalized
+ * @param {number} y current mouse y position, canvas normalized
+ * @return {Boolean} true if the translation occurred
+ */
+ function dragHandler(eventData, transform, x, y) {
+ var target = transform.target,
+ newLeft = x - transform.offsetX,
+ newTop = y - transform.offsetY,
+ moveX = !target.get('lockMovementX') && target.left !== newLeft,
+ moveY = !target.get('lockMovementY') && target.top !== newTop;
+ moveX && target.set('left', newLeft);
+ moveY && target.set('top', newTop);
+ if (moveX || moveY) {
+ fireEvent('moving', commonEventInfo(eventData, transform, x, y));
+ }
+ return moveX || moveY;
+ }
+
controls.scaleCursorStyleHandler = scaleCursorStyleHandler;
controls.skewCursorStyleHandler = skewCursorStyleHandler;
controls.scaleSkewCursorStyleHandler = scaleSkewCursorStyleHandler;
@@ -6871,6 +6931,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
controls.changeWidth = wrapWithFixedAnchor(changeWidth);
controls.skewHandlerX = skewHandlerX;
controls.skewHandlerY = skewHandlerY;
+ controls.dragHandler = dragHandler;
controls.scaleOrSkewActionName = scaleOrSkewActionName;
controls.rotationStyleHandler = rotationStyleHandler;
controls.fireEvent = fireEvent;
@@ -6902,18 +6963,35 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
*/
function renderCircleControl (ctx, left, top, styleOverride, fabricObject) {
styleOverride = styleOverride || {};
- var size = styleOverride.cornerSize || fabricObject.cornerSize,
+ var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize,
+ ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize,
transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ?
styleOverride.transparentCorners : this.transparentCorners,
methodName = transparentCorners ? 'stroke' : 'fill',
- stroke = !transparentCorners && (styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor);
+ stroke = !transparentCorners && (styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor),
+ myLeft = left,
+ myTop = top, size;
ctx.save();
ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor;
ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor;
+ // as soon as fabric react v5, remove ie11, use proper ellipse code.
+ if (xSize > ySize) {
+ size = xSize;
+ ctx.scale(1.0, ySize / xSize);
+ myTop = top * xSize / ySize;
+ }
+ else if (ySize > xSize) {
+ size = ySize;
+ ctx.scale(xSize / ySize, 1.0);
+ myLeft = left * ySize / xSize;
+ }
+ else {
+ size = xSize;
+ }
// this is still wrong
ctx.lineWidth = 1;
ctx.beginPath();
- ctx.arc(left, top, size / 2, 0, 2 * Math.PI, false);
+ ctx.arc(myLeft, myTop, size / 2, 0, 2 * Math.PI, false);
ctx[methodName]();
if (stroke) {
ctx.stroke();
@@ -6934,13 +7012,14 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
*/
function renderSquareControl(ctx, left, top, styleOverride, fabricObject) {
styleOverride = styleOverride || {};
- var size = styleOverride.cornerSize || fabricObject.cornerSize,
+ var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize,
+ ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize,
transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ?
styleOverride.transparentCorners : fabricObject.transparentCorners,
methodName = transparentCorners ? 'stroke' : 'fill',
stroke = !transparentCorners && (
styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor
- ), sizeBy2 = size / 2;
+ ), xSizeBy2 = xSize / 2, ySizeBy2 = ySize / 2;
ctx.save();
ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor;
ctx.strokeStyle = styleOverride.strokeCornerColor || fabricObject.strokeCornerColor;
@@ -6950,10 +7029,10 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
ctx.rotate(degreesToRadians(fabricObject.angle));
// this does not work, and fixed with ( && ) does not make sense.
// to have real transparent corners we need the controls on upperCanvas
- // transparentCorners || ctx.clearRect(-sizeBy2, -sizeBy2, size, size);
- ctx[methodName + 'Rect'](-sizeBy2, -sizeBy2, size, size);
+ // transparentCorners || ctx.clearRect(-xSizeBy2, -ySizeBy2, xSize, ySize);
+ ctx[methodName + 'Rect'](-xSizeBy2, -ySizeBy2, xSize, ySize);
if (stroke) {
- ctx.strokeRect(-sizeBy2, -sizeBy2, size, size);
+ ctx.strokeRect(-xSizeBy2, -ySizeBy2, xSize, ySize);
}
ctx.restore();
}
@@ -6991,7 +7070,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
visible: true,
/**
- * Name of the action that the controll will likely execute.
+ * Name of the action that the control will likely execute.
* This is optional. FabricJS uses to identify what the user is doing for some
* extra optimizations. If you are writing a custom control and you want to know
* somewhere else in the code what is going on, you can use this string here.
@@ -7035,7 +7114,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
* Positive offset moves the control to the right, negative to the left.
* It used when you want to have position of control that does not scale with
* the bounding box. Example: rotation control is placed at x:0, y: 0.5 on
- * the boundindbox, with an offset of 30 pixels vertivally. Those 30 pixels will
+ * the boundindbox, with an offset of 30 pixels vertically. Those 30 pixels will
* stay 30 pixels no matter how the object is big. Another example is having 2
* controls in the corner, that stay in the same position when the object scale.
* of the bounding box.
@@ -7052,6 +7131,38 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
*/
offsetY: 0,
+ /**
+ * Sets the length of the control. If null, defaults to object's cornerSize.
+ * Expects both sizeX and sizeY to be set when set.
+ * @type {?Number}
+ * @default null
+ */
+ sizeX: null,
+
+ /**
+ * Sets the height of the control. If null, defaults to object's cornerSize.
+ * Expects both sizeX and sizeY to be set when set.
+ * @type {?Number}
+ * @default null
+ */
+ sizeY: null,
+
+ /**
+ * Sets the length of the touch area of the control. If null, defaults to object's touchCornerSize.
+ * Expects both touchSizeX and touchSizeY to be set when set.
+ * @type {?Number}
+ * @default null
+ */
+ touchSizeX: null,
+
+ /**
+ * Sets the height of the touch area of the control. If null, defaults to object's touchCornerSize.
+ * Expects both touchSizeX and touchSizeY to be set when set.
+ * @type {?Number}
+ * @default null
+ */
+ touchSizeY: null,
+
/**
* Css cursor style to display when the control is hovered.
* if the method `cursorStyleHandler` is provided, this property is ignored.
@@ -7072,46 +7183,49 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
* The control actionHandler, provide one to handle action ( control being moved )
* @param {Event} eventData the native mouse event
* @param {Object} transformData properties of the current transform
- * @param {fabric.Object} object on which the control is displayed
- * @return {Function}
+ * @param {Number} x x position of the cursor
+ * @param {Number} y y position of the cursor
+ * @return {Boolean} true if the action/event modified the object
*/
- actionHandler: function(/* eventData, transformData, fabricObject */) { },
+ actionHandler: function(/* eventData, transformData, x, y */) { },
/**
* The control handler for mouse down, provide one to handle mouse down on control
* @param {Event} eventData the native mouse event
* @param {Object} transformData properties of the current transform
- * @param {fabric.Object} object on which the control is displayed
- * @return {Function}
+ * @param {Number} x x position of the cursor
+ * @param {Number} y y position of the cursor
+ * @return {Boolean} true if the action/event modified the object
*/
- mouseDownHandler: function(/* eventData, transformData, fabricObject */) { },
+ mouseDownHandler: function(/* eventData, transformData, x, y */) { },
/**
* The control mouseUpHandler, provide one to handle an effect on mouse up.
* @param {Event} eventData the native mouse event
* @param {Object} transformData properties of the current transform
- * @param {fabric.Object} object on which the control is displayed
- * @return {Function}
+ * @param {Number} x x position of the cursor
+ * @param {Number} y y position of the cursor
+ * @return {Boolean} true if the action/event modified the object
*/
- mouseUpHandler: function(/* eventData, transformData, fabricObject */) { },
+ mouseUpHandler: function(/* eventData, transformData, x, y */) { },
/**
* Returns control actionHandler
* @param {Event} eventData the native mouse event
- * @param {Object} transformData properties of the current transform
- * @param {fabric.Object} object on which the control is displayed
- * @return {Function}
+ * @param {fabric.Object} fabricObject on which the control is displayed
+ * @param {fabric.Control} control control for which the action handler is being asked
+ * @return {Function} the action handler
*/
- getActionHandler: function(/* eventData, transformData, fabricObject */) {
+ getActionHandler: function(/* eventData, fabricObject, control */) {
return this.actionHandler;
},
/**
* Returns control mouseDown handler
* @param {Event} eventData the native mouse event
- * @param {Object} transformData properties of the current transform
- * @param {fabric.Object} object on which the control is displayed
- * @return {Function}
+ * @param {fabric.Object} fabricObject on which the control is displayed
+ * @param {fabric.Control} control control for which the action handler is being asked
+ * @return {Function} the action handler
*/
getMouseDownHandler: function(/* eventData, fabricObject, control */) {
return this.mouseDownHandler;
@@ -7120,9 +7234,9 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
/**
* Returns control mouseUp handler
* @param {Event} eventData the native mouse event
- * @param {Object} transformData properties of the current transform
- * @param {fabric.Object} object on which the control is displayed
- * @return {Function}
+ * @param {fabric.Object} fabricObject on which the control is displayed
+ * @param {fabric.Control} control control for which the action handler is being asked
+ * @return {Function} the action handler
*/
getMouseUpHandler: function(/* eventData, fabricObject, control */) {
return this.mouseUpHandler;
@@ -7183,6 +7297,66 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
return point;
},
+ /**
+ * Returns the coords for this control based on object values.
+ * @param {Number} objectAngle angle from the fabric object holding the control
+ * @param {Number} objectCornerSize cornerSize from the fabric object holding the control (or touchCornerSize if
+ * isTouch is true)
+ * @param {Number} centerX x coordinate where the control center should be
+ * @param {Number} centerY y coordinate where the control center should be
+ * @param {boolean} isTouch true if touch corner, false if normal corner
+ */
+ calcCornerCoords: function(objectAngle, objectCornerSize, centerX, centerY, isTouch) {
+ var cosHalfOffset,
+ sinHalfOffset,
+ cosHalfOffsetComp,
+ sinHalfOffsetComp,
+ xSize = (isTouch) ? this.touchSizeX : this.sizeX,
+ ySize = (isTouch) ? this.touchSizeY : this.sizeY;
+ if (xSize && ySize && xSize !== ySize) {
+ // handle rectangular corners
+ var controlTriangleAngle = Math.atan2(ySize, xSize);
+ var cornerHypotenuse = Math.sqrt(xSize * xSize + ySize * ySize) / 2;
+ var newTheta = controlTriangleAngle - fabric.util.degreesToRadians(objectAngle);
+ var newThetaComp = Math.PI / 2 - controlTriangleAngle - fabric.util.degreesToRadians(objectAngle);
+ cosHalfOffset = cornerHypotenuse * fabric.util.cos(newTheta);
+ sinHalfOffset = cornerHypotenuse * fabric.util.sin(newTheta);
+ // use complementary angle for two corners
+ cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newThetaComp);
+ sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newThetaComp);
+ }
+ else {
+ // handle square corners
+ // use default object corner size unless size is defined
+ var cornerSize = (xSize && ySize) ? xSize : objectCornerSize;
+ /* 0.7071067812 stands for sqrt(2)/2 */
+ cornerHypotenuse = cornerSize * 0.7071067812;
+ // complementary angles are equal since they're both 45 degrees
+ var newTheta = fabric.util.degreesToRadians(45 - objectAngle);
+ cosHalfOffset = cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newTheta);
+ sinHalfOffset = sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newTheta);
+ }
+
+ return {
+ tl: {
+ x: centerX - sinHalfOffsetComp,
+ y: centerY - cosHalfOffsetComp,
+ },
+ tr: {
+ x: centerX + cosHalfOffset,
+ y: centerY - sinHalfOffset,
+ },
+ bl: {
+ x: centerX - cosHalfOffset,
+ y: centerY + sinHalfOffset,
+ },
+ br: {
+ x: centerX + sinHalfOffsetComp,
+ y: centerY + cosHalfOffsetComp,
+ },
+ };
+ },
+
/**
* Render function for the control.
* When this function runs the context is unscaled. unrotate. Just retina scaled.
@@ -7450,7 +7624,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
offsetX += object.width / 2;
offsetY += object.height / 2;
}
- if (object.type === 'path') {
+ if (object.type === 'path' && this.gradientUnits !== 'percentage') {
offsetX -= object.pathOffset.x;
offsetY -= object.pathOffset.y;
}
@@ -7572,7 +7746,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
* @param {SVGGradientElement} el SVG gradient element
* @param {fabric.Object} instance
* @param {String} opacityAttr A fill-opacity or stroke-opacity attribute to multiply to each stop's opacity.
- * @param {Object} svgOptions an object containing the size of the SVG in order to parse correctly graidents
+ * @param {Object} svgOptions an object containing the size of the SVG in order to parse correctly gradients
* that uses gradientUnits as 'userSpaceOnUse' and percentages.
* @param {Object.number} viewBoxWidth width part of the viewBox attribute on svg
* @param {Object.number} viewBoxHeight height part of the viewBox attribute on svg
@@ -8521,7 +8695,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
* @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage}
* or {@link fabric.StaticCanvas#overlayImage|overlayImage})
* @param {(fabric.Image|String|null)} image fabric.Image instance, URL of an image or null to set background or overlay to
- * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay. The first argument is the created image, the second argument is a flag indicating whether an error occured or not.
+ * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay. The first argument is the created image, the second argument is a flag indicating whether an error occurred or not.
* @param {Object} [options] Optional options to set for the {@link fabric.Image|image}.
*/
__setBgOverlayImage: function(property, image, callback, options) {
@@ -8783,7 +8957,10 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
},
/**
- * Sets zoom level of this canvas instance, zoom centered around point
+ * Sets zoom level of this canvas instance, the zoom centered around point
+ * meaning that following zoom to point with the same point will have the visual
+ * effect of the zoom originating from that point. The point won't move.
+ * It has nothing to do with canvas center or visual center of the viewport.
* @param {fabric.Point} point to zoom with respect to
* @param {Number} value to set zoom to, less than 1 zooms out
* @return {fabric.Canvas} instance
@@ -9204,7 +9381,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
},
/**
- * Returs dataless JSON representation of canvas
+ * Returns dataless JSON representation of canvas
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {String} json string
*/
@@ -9320,7 +9497,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
* @param {Object} [options] Options object for SVG output
* @param {Boolean} [options.suppressPreamble=false] If true xml tag is not included
* @param {Object} [options.viewBox] SVG viewbox object
- * @param {Number} [options.viewBox.x] x-cooridnate of viewbox
+ * @param {Number} [options.viewBox.x] x-coordinate of viewbox
* @param {Number} [options.viewBox.y] y-coordinate of viewbox
* @param {Number} [options.viewBox.width] Width of viewbox
* @param {Number} [options.viewBox.height] Height of viewbox
@@ -9660,7 +9837,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
/**
* Moves an object or a selection down in stack of drawn objects
- * An optional paramter, intersecting allowes to move the object in behind
+ * An optional parameter, intersecting allows to move the object in behind
* the first intersecting object. Where intersection is calculated with
* bounding box. If no intersection is found, there will not be change in the
* stack.
@@ -9733,7 +9910,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
/**
* Moves an object or a selection up in stack of drawn objects
- * An optional paramter, intersecting allowes to move the object in front
+ * An optional parameter, intersecting allows to move the object in front
* of the first intersecting object. Where intersection is calculated with
* bounding box. If no intersection is found, there will not be change in the
* stack.
@@ -9903,10 +10080,13 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
});
/**
- * Returns JSON representation of canvas
+ * Returns Object representation of canvas
+ * this alias is provided because if you call JSON.stringify on an instance,
+ * the toJSON object will be invoked if it exists.
+ * Having a toJSON method means you can do JSON.stringify(myCanvas)
* @function
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
- * @return {String} JSON string
+ * @return {Object} JSON compatible object
* @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization}
* @see {@link http://jsfiddle.net/fabricjs/pec86/|jsFiddle demo}
* @example
JSON without additional properties
@@ -10094,7 +10274,7 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype
},
/**
- * Inovoked on mouse down
+ * Invoked on mouse down
* @param {Object} pointer
*/
onMouseDown: function(pointer, options) {
@@ -10109,7 +10289,7 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype
},
/**
- * Inovoked on mouse move
+ * Invoked on mouse move
* @param {Object} pointer
*/
onMouseMove: function(pointer, options) {
@@ -10294,7 +10474,7 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype
},
/**
- * Decimate poins array with the decimate value
+ * Decimate points array with the decimate value
*/
decimatePoints: function(points, distance) {
if (points.length <= 2) {
@@ -10792,10 +10972,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab
* @see {@link fabric.Canvas#initialize} for constructor definition
*
* @fires object:modified at the end of a transform or any change when statefull is true
- * @fires object:rotated at the end of a rotation transform
- * @fires object:scaled at the end of a scale transform
- * @fires object:moved at the end of translation transform
- * @fires object:skewed at the end of a skew transform
* @fires object:rotating while an object is being rotated from the control
* @fires object:scaling while an object is being scaled by controls
* @fires object:moving while an object is being dragged
@@ -10825,6 +11001,11 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab
* @fires after:render at the end of the render process, receives the context in the callback
* @fires before:render at start the render process, receives the context in the callback
*
+ * the following events are deprecated:
+ * @fires object:rotated at the end of a rotation transform
+ * @fires object:scaled at the end of a scale transform
+ * @fires object:moved at the end of translation transform
+ * @fires object:skewed at the end of a skew transform
*/
fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ {
@@ -11289,7 +11470,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab
},
/**
- * takes an event and determins if selection key has been pressed
+ * takes an event and determines if selection key has been pressed
* @private
* @param {Event} e Event object
*/
@@ -11384,10 +11565,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab
else if (corner === 'bl' || corner === 'mb' || corner === 'br') {
origin.y = 'top';
}
- else if (corner === 'mtr') {
- origin.x = 'center';
- origin.y = 'center';
- }
return origin;
},
@@ -11417,7 +11594,9 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab
}
var pointer = this.getPointer(e), corner = target.__corner,
- actionHandler = !!corner && target.controls[corner].getActionHandler(),
+ control = target.controls[corner],
+ actionHandler = (alreadySelected && corner) ?
+ control.getActionHandler(e, target, control) : fabric.controlsUtils.dragHandler,
action = this._getActionFromCorner(alreadySelected, corner, e, target),
origin = this._getOriginFromCorner(target, corner),
altKey = e[this.centeredKey],
@@ -11460,26 +11639,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab
this._beforeTransform(e);
},
- /**
- * Translates object by "setting" its left/top
- * @private
- * @param {Number} x pointer's x coordinate
- * @param {Number} y pointer's y coordinate
- * @return {Boolean} true if the translation occurred
- */
- _translateObject: function (x, y) {
- var transform = this._currentTransform,
- target = transform.target,
- newLeft = x - transform.offsetX,
- newTop = y - transform.offsetY,
- moveX = !target.get('lockMovementX') && target.left !== newLeft,
- moveY = !target.get('lockMovementY') && target.top !== newTop;
-
- moveX && target.set('left', newLeft);
- moveY && target.set('top', newTop);
- return moveX || moveY;
- },
-
/**
* Set the cursor type of the canvas element
* @param {String} value Cursor type of the canvas element.
@@ -12290,7 +12449,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab
// as a short term fix we are not firing this if we are currently transforming.
// as a long term fix we need to separate the action of finding a target with the
// side effects we added to it.
- if (!this.currentTransform && !this.findTarget(e)) {
+ if (!this._currentTransform && !this.findTarget(e)) {
this.fire('mouse:over', { target: null, e: e });
this._hoveredTarget = null;
this._hoveredTargets = [];
@@ -12575,7 +12734,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab
var control = target.controls[corner],
mouseUpHandler = control && control.getMouseUpHandler(e, target, control);
if (mouseUpHandler) {
- mouseUpHandler(e, target, control);
+ var pointer = this.getPointer(e);
+ mouseUpHandler(e, transform, pointer.x, pointer.y);
}
target.isMoving = false;
}
@@ -12665,6 +12825,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab
e: e,
target: target,
transform: transform,
+ action: transform.action,
};
if (target._scaling) {
@@ -12675,6 +12836,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab
if (transform.actionPerformed || (this.stateful && target.hasStateChanged())) {
if (transform.actionPerformed) {
+ // this is not friendly to the new control api.
+ // is deprecated.
eventName = this._addEventOptions(options, transform);
this._fire(eventName, options);
}
@@ -12685,6 +12848,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab
/**
* Mutate option object in order to add by property and give back the event name.
* @private
+ * @deprecated since 4.2.0
* @param {Object} options to mutate
* @param {Object} transform to inspect action from
*/
@@ -12836,12 +13000,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab
);
target.__corner = corner;
if (target === this._activeObject && (corner || !shouldGroup)) {
+ this._setupCurrentTransform(e, target, alreadySelected);
var control = target.controls[corner],
+ pointer = this.getPointer(e),
mouseDownHandler = control && control.getMouseDownHandler(e, target, control);
if (mouseDownHandler) {
- mouseDownHandler(e, target, control);
+ mouseDownHandler(e, this._currentTransform, pointer.x, pointer.y);
}
- this._setupCurrentTransform(e, target, alreadySelected);
}
}
this._handleEvent(e, 'down');
@@ -13051,24 +13216,15 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab
y = pointer.y,
action = transform.action,
actionPerformed = false,
- actionHandler = transform.actionHandler,
+ actionHandler = transform.actionHandler;
// this object could be created from the function in the control handlers
- options = {
- target: transform.target,
- e: e,
- transform: transform,
- pointer: pointer
- };
- if (action === 'drag') {
- actionPerformed = this._translateObject(x, y);
- if (actionPerformed) {
- this._fire('moving', options);
- this.setCursor(options.target.moveCursor || this.moveCursor);
- }
+
+ if (actionHandler) {
+ actionPerformed = actionHandler(e, transform, x, y);
}
- else if (actionHandler) {
- (actionPerformed = actionHandler(e, transform, x, y)) && this._fire(action, options);
+ if (action === 'drag' && actionPerformed) {
+ this.setCursor(transform.target.moveCursor || this.moveCursor);
}
transform.actionPerformed = transform.actionPerformed || actionPerformed;
},
@@ -13978,7 +14134,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
strokeLineCap: 'butt',
/**
- * Corner style of an object's stroke (one of "bevil", "round", "miter")
+ * Corner style of an object's stroke (one of "bevel", "round", "miter")
* @type String
* @default
*/
@@ -14008,7 +14164,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
/**
* Scale factor of object's controlling borders
* bigger number will make a thicker border
- * border is 1, so this is basically a border tickness
+ * border is 1, so this is basically a border thickness
* since there is no way to change the border itself.
* @type Number
* @default
@@ -14722,7 +14878,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
/**
* return true if the object will draw a stroke
* Does not consider text styles. This is just a shortcut used at rendering time
- * We want it to be an aproximation and be fast.
+ * We want it to be an approximation and be fast.
* wrote to avoid extra caching, it has to return true when stroke happens,
* can guess when it will not happen at 100% chance, does not matter if it misses
* some use case where the stroke is invisible.
@@ -14736,7 +14892,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
/**
* return true if the object will draw a fill
* Does not consider text styles. This is just a shortcut used at rendering time
- * We want it to be an aproximation and be fast.
+ * We want it to be an approximation and be fast.
* wrote to avoid extra caching, it has to return true when fill happens,
* can guess when it will not happen at 100% chance, does not matter if it misses
* some use case where the fill is invisible.
@@ -14831,8 +14987,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
}
else {
this._renderBackground(ctx);
- this._setStrokeStyles(ctx, this);
- this._setFillStyles(ctx, this);
}
this._render(ctx);
this._drawClipPath(ctx);
@@ -14928,23 +15082,44 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
},
_setStrokeStyles: function(ctx, decl) {
- if (decl.stroke) {
+ var stroke = decl.stroke;
+ if (stroke) {
ctx.lineWidth = decl.strokeWidth;
ctx.lineCap = decl.strokeLineCap;
ctx.lineDashOffset = decl.strokeDashOffset;
ctx.lineJoin = decl.strokeLineJoin;
ctx.miterLimit = decl.strokeMiterLimit;
- ctx.strokeStyle = decl.stroke.toLive
- ? decl.stroke.toLive(ctx, this)
- : decl.stroke;
+ if (stroke.toLive) {
+ if (stroke.gradientUnits === 'percentage' || stroke.gradientTrasnform || stroke.patternTransform) {
+ // need to transform gradient in a pattern.
+ // this is a slow process. If you are hitting this codepath, and the object
+ // is not using caching, you should consider switching it on.
+ // we need a canvas as big as the current object caching canvas.
+ this._applyPatternForTransformedGradient(ctx, stroke);
+ }
+ else {
+ // is a simple gradient or pattern
+ ctx.strokeStyle = stroke.toLive(ctx, this);
+ this._applyPatternGradientTransform(ctx, stroke);
+ }
+ }
+ else {
+ // is a color
+ ctx.strokeStyle = decl.stroke;
+ }
}
},
_setFillStyles: function(ctx, decl) {
- if (decl.fill) {
- ctx.fillStyle = decl.fill.toLive
- ? decl.fill.toLive(ctx, this)
- : decl.fill;
+ var fill = decl.fill;
+ if (fill) {
+ if (fill.toLive) {
+ ctx.fillStyle = fill.toLive(ctx, this);
+ this._applyPatternGradientTransform(ctx, decl.fill);
+ }
+ else {
+ ctx.fillStyle = fill;
+ }
}
},
@@ -15114,7 +15289,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
}
ctx.save();
- this._applyPatternGradientTransform(ctx, this.fill);
+ this._setFillStyles(ctx, this);
if (this.fillRule === 'evenodd') {
ctx.fill('evenodd');
}
@@ -15146,16 +15321,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
ctx.scale(1 / this.scaleX, 1 / this.scaleY);
}
this._setLineDash(ctx, this.strokeDashArray, this._renderDashedStroke);
- if (this.stroke.toLive && this.stroke.gradientUnits === 'percentage') {
- // need to transform gradient in a pattern.
- // this is a slow process. If you are hitting this codepath, and the object
- // is not using caching, you should consider switching it on.
- // we need a canvas as big as the current object caching canvas.
- this._applyPatternForTransformedGradient(ctx, this.stroke);
- }
- else {
- this._applyPatternGradientTransform(ctx, this.stroke);
- }
+ this._setStrokeStyles(ctx, this);
ctx.stroke();
ctx.restore();
},
@@ -15270,11 +15436,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
/**
* Creates an instance of fabric.Image out of an object
- * could make use of both toDataUrl or toCanvasElement.
+ * makes use of toCanvasElement.
+ * Once this method was based on toDataUrl and loadImage, so it also had a quality
+ * and format option. toCanvasElement is faster and produce no loss of quality.
+ * If you need to get a real Jpeg or Png from an object, using toDataURL is the right way to do it.
+ * toCanvasElement and then toBlob from the obtained canvas is also a good option.
+ * This method is sync now, but still support the callback because we did not want to break.
+ * When fabricJS 5.0 will be planned, this will probably be changed to not have a callback.
* @param {Function} callback callback, invoked with an instance as a first argument
* @param {Object} [options] for clone as image, passed to toDataURL
- * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png"
- * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg.
* @param {Number} [options.multiplier=1] Multiplier to scale by
* @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14
* @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14
@@ -16357,7 +16527,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
},
/**
- * Sets corner position coordinates based on current angle, width and height.
+ * Sets corner and controls position coordinates based on current angle, width and height, left and top.
* oCoords are used to find the corners
* aCoords are used to quickly find an object on the canvas
* lineCoords are used to quickly find object during pointer events.
@@ -17040,8 +17210,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot
}
lines = this._getImageLines(forTouch ? this.oCoords[i].touchCorner : this.oCoords[i].corner);
- // debugging
-
+ // // debugging
+ //
// this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2);
// this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2);
//
@@ -17082,59 +17252,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot
* @private
*/
_setCornerCoords: function() {
- var coords = this.oCoords,
- newTheta = degreesToRadians(45 - this.angle),
- cosTheta = fabric.util.cos(newTheta),
- sinTheta = fabric.util.sin(newTheta),
- /* Math.sqrt(2 * Math.pow(this.cornerSize, 2)) / 2, */
- /* 0.707106 stands for sqrt(2)/2 */
- cornerHypotenuse = this.cornerSize * 0.707106,
- touchHypotenuse = this.touchCornerSize * 0.707106,
- cosHalfOffset = cornerHypotenuse * cosTheta,
- sinHalfOffset = cornerHypotenuse * sinTheta,
- touchCosHalfOffset = touchHypotenuse * cosTheta,
- touchSinHalfOffset = touchHypotenuse * sinTheta,
- x, y;
+ var coords = this.oCoords;
for (var control in coords) {
- x = coords[control].x;
- y = coords[control].y;
- coords[control].corner = {
- tl: {
- x: x - sinHalfOffset,
- y: y - cosHalfOffset
- },
- tr: {
- x: x + cosHalfOffset,
- y: y - sinHalfOffset
- },
- bl: {
- x: x - cosHalfOffset,
- y: y + sinHalfOffset
- },
- br: {
- x: x + sinHalfOffset,
- y: y + cosHalfOffset
- }
- };
- coords[control].touchCorner = {
- tl: {
- x: x - touchSinHalfOffset,
- y: y - touchCosHalfOffset
- },
- tr: {
- x: x + touchCosHalfOffset,
- y: y - touchSinHalfOffset
- },
- bl: {
- x: x - touchCosHalfOffset,
- y: y + touchSinHalfOffset
- },
- br: {
- x: x + touchSinHalfOffset,
- y: y + touchCosHalfOffset
- }
- };
+ var controlObject = this.controls[control];
+ coords[control].corner = controlObject.calcCornerCoords(
+ this.angle, this.cornerSize, coords[control].x, coords[control].y, false);
+ coords[control].touchCorner = controlObject.calcCornerCoords(
+ this.angle, this.touchCornerSize, coords[control].x, coords[control].y, true);
}
},
@@ -17797,7 +17922,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot
/**
* Returns object representation of an instance
- * @methd toObject
+ * @method toObject
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} object representation of an instance
*/
@@ -17973,7 +18098,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot
/**
* Start angle of the circle, moving clockwise
- * deprectated type, this should be in degree, this was an oversight.
+ * deprecated type, this should be in degree, this was an oversight.
* probably will change to degrees in next major version
* @type Number
* @default 0
@@ -17982,7 +18107,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot
/**
* End angle of the circle
- * deprectated type, this should be in degree, this was an oversight.
+ * deprecated type, this should be in degree, this was an oversight.
* probably will change to degrees in next major version
* @type Number
* @default 2Pi
@@ -18738,7 +18863,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot
/**
* Calculate the polygon min and max point from points array,
- * returning an object with left, top, widht, height to measure the
+ * returning an object with left, top, width, height to measure the
* polygon size
* @return {Object} object.left X coordinate of the polygon leftmost point
* @return {Object} object.top Y coordinate of the polygon topmost point
@@ -19729,7 +19854,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot
},
/**
- * Retores original state of each of group objects (original state is that which was before group was created).
+ * Restores original state of each of group objects (original state is that which was before group was created).
* @private
* @return {fabric.Group} thisArg
* @chainable
@@ -20291,7 +20416,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot
}
// resizeFilters work on the already filtered copy.
// we need to apply resizeFilters AFTER normal filters.
- // applyResizeFilters is run more often than normal fiters
+ // applyResizeFilters is run more often than normal filters
// and is triggered by user interactions rather than dev code
if (this.resizeFilter) {
this.applyResizeFilters();
@@ -20410,7 +20535,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot
},
/**
- * Returns true if an image has crop applied, inspecting values of cropX,cropY,width,hight.
+ * Returns true if an image has crop applied, inspecting values of cropX,cropY,width,height.
* @return {Boolean}
*/
hasCrop: function() {
@@ -20858,7 +20983,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot
* Creates an instance of fabric.Image from an URL string
* @static
* @param {String} url URL to create an image from
- * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument). Second argument is a boolean indicating if an error occured or not.
+ * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument). Second argument is a boolean indicating if an error occurred or not.
* @param {Object} [imgOptions] Options object
*/
fabric.Image.fromURL = function(url, callback, imgOptions) {
@@ -21053,7 +21178,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
/**
* Experimental. This object is a sort of repository of help layers used to avoid
* of recreating them during frequent filtering. If you are previewing a filter with
- * a slider you problably do not want to create help layers every filter step.
+ * a slider you probably do not want to create help layers every filter step.
* in this object there will be appended some canvases, created once, resized sometimes
* cleared never. Clearing is left to the developer.
**/
@@ -21230,7 +21355,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
/**
* Create a WebGL texture object.
*
- * Accepts specific dimensions to initialize the textuer to or a source image.
+ * Accepts specific dimensions to initialize the texture to or a source image.
*
* @param {WebGLRenderingContext} gl The GL context to use for creating the texture.
* @param {Number} width The width to initialize the texture at.
@@ -21643,7 +21768,7 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag
* Generic isNeutral implementation for one parameter based filters.
* Used only in image applyFilters to discard filters that will not have an effect
* on the image
- * Other filters may need their own verison ( ColorMatrix, HueRotation, gamma, ComposedFilter )
+ * Other filters may need their own version ( ColorMatrix, HueRotation, gamma, ComposedFilter )
* @param {Object} options
**/
isNeutralState: function(/* options */) {
@@ -24534,7 +24659,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
/**
* blur value, in percentage of image dimensions.
* specific to keep the image blur constant at different resolutions
- * range bewteen 0 and 1.
+ * range between 0 and 1.
*/
blur: 0,
@@ -25010,6 +25135,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
return;
}
+ var styleProps =
+ 'fontFamily fontWeight fontSize text underline overline linethrough' +
+ ' textAlign fontStyle lineHeight textBackgroundColor charSpacing styles path'.split(' ');
+
/**
* Text class
* @class fabric.Text
@@ -25035,6 +25164,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
'charSpacing',
'textAlign',
'styles',
+ 'path'
],
/**
@@ -25167,37 +25297,13 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
* as well as for history (undo/redo) purposes
* @type Array
*/
- stateProperties: fabric.Object.prototype.stateProperties.concat('fontFamily',
- 'fontWeight',
- 'fontSize',
- 'text',
- 'underline',
- 'overline',
- 'linethrough',
- 'textAlign',
- 'fontStyle',
- 'lineHeight',
- 'textBackgroundColor',
- 'charSpacing',
- 'styles'),
+ stateProperties: fabric.Object.prototype.stateProperties.concat(styleProps),
/**
* List of properties to consider when checking if cache needs refresh
* @type Array
*/
- cacheProperties: fabric.Object.prototype.cacheProperties.concat('fontFamily',
- 'fontWeight',
- 'fontSize',
- 'text',
- 'underline',
- 'overline',
- 'linethrough',
- 'textAlign',
- 'fontStyle',
- 'lineHeight',
- 'textBackgroundColor',
- 'charSpacing',
- 'styles'),
+ cacheProperties: fabric.Object.prototype.cacheProperties.concat(styleProps),
/**
* When defined, an object is rendered via stroke and this property specifies its color.
@@ -25246,7 +25352,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
/**
* Object containing character styles - top-level properties -> line numbers,
- * 2nd-level properties - charater numbers
+ * 2nd-level properties - character numbers
* @type Object
* @default
*/
@@ -25255,7 +25361,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
/**
* Reference to a context to measure text char or couple of chars
* the cacheContext of the canvas will be used or a freshly created one if the object is not on canvas
- * once created it will be referenced on fabric._measuringContext to avoide creating a canvas for every
+ * once created it will be referenced on fabric._measuringContext to avoid creating a canvas for every
* text object created.
* @type {CanvasRenderingContext2D}
* @default
@@ -25263,7 +25369,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
_measuringContext: null,
/**
- * Baseline shift, stlyes only, keep at 0 for the main text object
+ * Baseline shift, styles only, keep at 0 for the main text object
* @type {Number}
* @default
*/
@@ -25321,6 +25427,9 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
this.text = text;
this.__skipDimension = true;
this.callSuper('initialize', options);
+ if (this.path) {
+ this.setPathInfo();
+ }
this.__skipDimension = false;
this.initDimensions();
this.setCoords();
@@ -25328,7 +25437,19 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
},
/**
- * Return a contex for measurement of text string.
+ * If text has a path, it will add the extra information needed
+ * for path and text calculations
+ * @return {fabric.Text} thisArg
+ */
+ setPathInfo: function() {
+ var path = this.path;
+ if (path) {
+ path.segmentsInfo = fabric.util.getPathSegmentsInfo(path.path);
+ }
+ },
+
+ /**
+ * Return a context for measurement of text string.
* if created it gets stored for reuse
* @param {String} text Text string
* @param {Object} [options] Options object
@@ -25367,12 +25488,18 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
}
this._splitText();
this._clearCache();
- this.width = this.calcTextWidth() || this.cursorWidth || this.MIN_TEXT_WIDTH;
+ if (this.path) {
+ this.width = this.path.width;
+ this.height = this.path.height;
+ }
+ else {
+ this.width = this.calcTextWidth() || this.cursorWidth || this.MIN_TEXT_WIDTH;
+ this.height = this.calcTextHeight();
+ }
if (this.textAlign.indexOf('justify') !== -1) {
// once text is measured we need to make space fatter to make justified text.
this.enlargeSpaces();
}
- this.height = this.calcTextHeight();
this.saveState({ propertySet: '_dimensionAffectingProps' });
},
@@ -25537,12 +25664,12 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor')) {
return;
}
- var lineTopOffset = 0, heightOfLine,
+ var heightOfLine,
lineLeftOffset, originalFill = ctx.fillStyle,
line, lastColor,
leftOffset = this._getLeftOffset(),
- topOffset = this._getTopOffset(),
- boxStart = 0, boxWidth = 0, charBox, currentColor;
+ lineTopOffset = this._getTopOffset(),
+ boxStart = 0, boxWidth = 0, charBox, currentColor, path = this.path;
for (var i = 0, len = this._textLines.length; i < len; i++) {
heightOfLine = this.getHeightOfLine(i);
@@ -25558,11 +25685,24 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
for (var j = 0, jlen = line.length; j < jlen; j++) {
charBox = this.__charBounds[i][j];
currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor');
- if (currentColor !== lastColor) {
+ if (path) {
+ ctx.save();
+ ctx.translate(charBox.renderLeft, charBox.renderTop);
+ ctx.rotate(charBox.angle);
+ ctx.fillStyle = currentColor;
+ currentColor && ctx.fillRect(
+ -charBox.width / 2,
+ -heightOfLine / this.lineHeight * (1 - this._fontSizeFraction),
+ charBox.width,
+ heightOfLine / this.lineHeight
+ );
+ ctx.restore();
+ }
+ else if (currentColor !== lastColor) {
ctx.fillStyle = lastColor;
lastColor && ctx.fillRect(
leftOffset + lineLeftOffset + boxStart,
- topOffset + lineTopOffset,
+ lineTopOffset,
boxWidth,
heightOfLine / this.lineHeight
);
@@ -25574,11 +25714,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
boxWidth += charBox.kernedWidth;
}
}
- if (currentColor) {
+ if (currentColor && !path) {
ctx.fillStyle = currentColor;
ctx.fillRect(
leftOffset + lineLeftOffset + boxStart,
- topOffset + lineTopOffset,
+ lineTopOffset,
boxWidth,
heightOfLine / this.lineHeight
);
@@ -25612,22 +25752,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
return cache[cacheProp];
},
- /**
- * apply all the character style to canvas for rendering
- * @private
- * @param {String} _char
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @param {Object} [decl]
- */
- _applyCharStyles: function(method, ctx, lineIndex, charIndex, styleDeclaration) {
-
- this._setFillStyles(ctx, styleDeclaration);
- this._setStrokeStyles(ctx, styleDeclaration);
-
- ctx.font = this._getFontDeclaration(styleDeclaration);
- },
-
/**
* measure and return the width of a single character.
* possibly overridden to accommodate different measure logic or
@@ -25711,14 +25835,28 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
*/
_measureLine: function(lineIndex) {
var width = 0, i, grapheme, line = this._textLines[lineIndex], prevGrapheme,
- graphemeInfo, numOfSpaces = 0, lineBounds = new Array(line.length);
+ graphemeInfo, numOfSpaces = 0, lineBounds = new Array(line.length),
+ positionInPath = 0, startingPoint, totalPathLength, path = this.path;
this.__charBounds[lineIndex] = lineBounds;
+ if (path) {
+ startingPoint = fabric.util.getPointOnPath(path.path, 0, path.segmentsInfo);
+ totalPathLength = path.segmentsInfo[path.segmentsInfo.length - 1].length;
+ startingPoint.x += path.pathOffset.x;
+ startingPoint.y += path.pathOffset.y;
+ }
for (i = 0; i < line.length; i++) {
grapheme = line[i];
graphemeInfo = this._getGraphemeBox(grapheme, lineIndex, i, prevGrapheme);
+ if (path) {
+ if (positionInPath > totalPathLength) {
+ positionInPath %= totalPathLength;
+ }
+ this._setGraphemeOnPath(positionInPath, graphemeInfo, startingPoint);
+ }
lineBounds[i] = graphemeInfo;
width += graphemeInfo.kernedWidth;
+ positionInPath += graphemeInfo.kernedWidth;
prevGrapheme = grapheme;
}
// this latest bound box represent the last character of the line
@@ -25732,6 +25870,26 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
return { width: width, numOfSpaces: numOfSpaces };
},
+ /**
+ * Calculate the angle and the left,top position of the char that follow a path.
+ * It appends it to graphemeInfo to be reused later at rendering
+ * @private
+ * @param {Number} positionInPath to be measured
+ * @param {Object} graphemeInfo current grapheme box information
+ * @param {Object} startingPoint position of the point
+ */
+ _setGraphemeOnPath: function(positionInPath, graphemeInfo, startingPoint) {
+ var centerPosition = positionInPath + graphemeInfo.kernedWidth / 2,
+ path = this.path;
+
+ // we are at currentPositionOnPath. we want to know what point on the path is.
+ var p1 = fabric.util.getPointOnPath(path.path, centerPosition - 0.1, path.segmentsInfo),
+ p2 = fabric.util.getPointOnPath(path.path, centerPosition + 0.1, path.segmentsInfo);
+ graphemeInfo.renderLeft = p1.x - startingPoint.x;
+ graphemeInfo.renderTop = p1.y - startingPoint.y;
+ graphemeInfo.angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
+ },
+
/**
* Measure and return the info of a single grapheme.
* needs the the info of previous graphemes already filled
@@ -25824,8 +25982,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
*/
_renderTextCommon: function(ctx, method) {
ctx.save();
- var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset(),
- offsets = this._applyPatternGradientTransform(ctx, method === 'fillText' ? this.fill : this.stroke);
+ var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset();
for (var i = 0, len = this._textLines.length; i < len; i++) {
var heightOfLine = this.getHeightOfLine(i),
maxHeight = heightOfLine / this.lineHeight,
@@ -25834,8 +25991,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
method,
ctx,
this._textLines[i],
- left + leftOffset - offsets.offsetX,
- top + lineHeights + maxHeight - offsets.offsetY,
+ left + leftOffset,
+ top + lineHeights + maxHeight,
i
);
lineHeights += heightOfLine;
@@ -25895,7 +26052,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
charBox,
boxWidth = 0,
timeToRender,
- shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex);
+ path = this.path,
+ shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path;
ctx.save();
top -= lineHeight * this._fontSizeFraction / this.lineHeight;
@@ -25906,7 +26064,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
return;
}
for (var i = 0, len = line.length - 1; i <= len; i++) {
- timeToRender = i === len || this.charSpacing;
+ timeToRender = i === len || this.charSpacing || path;
charsToRender += line[i];
charBox = this.__charBounds[lineIndex][i];
if (boxWidth === 0) {
@@ -25928,7 +26086,16 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
timeToRender = this._hasStyleChanged(actualStyle, nextStyle);
}
if (timeToRender) {
- this._renderChar(method, ctx, lineIndex, i, charsToRender, left, top, lineHeight);
+ if (path) {
+ ctx.save();
+ ctx.translate(charBox.renderLeft, charBox.renderTop);
+ ctx.rotate(charBox.angle);
+ this._renderChar(method, ctx, lineIndex, i, charsToRender, -boxWidth / 2, 0, lineHeight);
+ ctx.restore();
+ }
+ else {
+ this._renderChar(method, ctx, lineIndex, i, charsToRender, left, top, lineHeight);
+ }
charsToRender = '';
actualStyle = nextStyle;
left += boxWidth;
@@ -25938,6 +26105,73 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
ctx.restore();
},
+ /**
+ * This function try to patch the missing gradientTransform on canvas gradients.
+ * transforming a context to transform the gradient, is going to transform the stroke too.
+ * we want to transform the gradient but not the stroke operation, so we create
+ * a transformed gradient on a pattern and then we use the pattern instead of the gradient.
+ * this method has drawbacks: is slow, is in low resolution, needs a patch for when the size
+ * is limited.
+ * @private
+ * @param {fabric.Gradient} filler a fabric gradient instance
+ * @return {CanvasPattern} a pattern to use as fill/stroke style
+ */
+ _applyPatternGradientTransformText: function(filler) {
+ var pCanvas = fabric.util.createCanvasElement(), pCtx,
+ // TODO: verify compatibility with strokeUniform
+ width = this.width + this.strokeWidth, height = this.height + this.strokeWidth;
+ pCanvas.width = width;
+ pCanvas.height = height;
+ pCtx = pCanvas.getContext('2d');
+ pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height);
+ pCtx.lineTo(0, height); pCtx.closePath();
+ pCtx.translate(width / 2, height / 2);
+ pCtx.fillStyle = filler.toLive(pCtx);
+ this._applyPatternGradientTransform(pCtx, filler);
+ pCtx.fill();
+ return pCtx.createPattern(pCanvas, 'no-repeat');
+ },
+
+ handleFiller: function(ctx, property, filler) {
+ var offsetX, offsetY;
+ if (filler.toLive) {
+ if (filler.gradientUnits === 'percentage' || filler.gradientTrasnform || filler.patternTransform) {
+ // need to transform gradient in a pattern.
+ // this is a slow process. If you are hitting this codepath, and the object
+ // is not using caching, you should consider switching it on.
+ // we need a canvas as big as the current object caching canvas.
+ offsetX = -this.width / 2;
+ offsetY = -this.height / 2;
+ ctx.translate(offsetX, offsetY);
+ ctx[property] = this._applyPatternGradientTransformText(filler);
+ return { offsetX: offsetX, offsetY: offsetY };
+ }
+ else {
+ // is a simple gradient or pattern
+ ctx[property] = filler.toLive(ctx, this);
+ return this._applyPatternGradientTransform(ctx, filler);
+ }
+ }
+ else {
+ // is a color
+ ctx[property] = filler;
+ }
+ return { offsetX: 0, offsetY: 0 };
+ },
+
+ _setStrokeStyles: function(ctx, decl) {
+ ctx.lineWidth = decl.strokeWidth;
+ ctx.lineCap = this.strokeLineCap;
+ ctx.lineDashOffset = this.strokeDashOffset;
+ ctx.lineJoin = this.strokeLineJoin;
+ ctx.miterLimit = this.strokeMiterLimit;
+ return this.handleFiller(ctx, 'strokeStyle', decl.stroke);
+ },
+
+ _setFillStyles: function(ctx, decl) {
+ return this.handleFiller(ctx, 'fillStyle', decl.fill);
+ },
+
/**
* @private
* @param {String} method
@@ -25953,14 +26187,19 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
var decl = this._getStyleDeclaration(lineIndex, charIndex),
fullDecl = this.getCompleteStyleDeclaration(lineIndex, charIndex),
shouldFill = method === 'fillText' && fullDecl.fill,
- shouldStroke = method === 'strokeText' && fullDecl.stroke && fullDecl.strokeWidth;
+ shouldStroke = method === 'strokeText' && fullDecl.stroke && fullDecl.strokeWidth,
+ fillOffsets, strokeOffsets;
if (!shouldStroke && !shouldFill) {
return;
}
- decl && ctx.save();
+ ctx.save();
+
+ shouldFill && (fillOffsets = this._setFillStyles(ctx, fullDecl));
+ shouldStroke && (strokeOffsets = this._setStrokeStyles(ctx, fullDecl));
+
+ ctx.font = this._getFontDeclaration(fullDecl);
- this._applyCharStyles(method, ctx, lineIndex, charIndex, fullDecl);
if (decl && decl.textBackgroundColor) {
this._removeShadow(ctx);
@@ -25968,10 +26207,9 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
if (decl && decl.deltaY) {
top += decl.deltaY;
}
-
- shouldFill && ctx.fillText(_char, left, top);
- shouldStroke && ctx.strokeText(_char, left, top);
- decl && ctx.restore();
+ shouldFill && ctx.fillText(_char, left - fillOffsets.offsetX, top - fillOffsets.offsetY);
+ shouldStroke && ctx.strokeText(_char, left - strokeOffsets.offsetX, top - strokeOffsets.offsetY);
+ ctx.restore();
},
/**
@@ -26121,7 +26359,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
/**
* Retrieves the value of property at given character position
* @param {Number} lineIndex the line number
- * @param {Number} charIndex the charater number
+ * @param {Number} charIndex the character number
* @param {String} property the property name
* @returns the value of 'property'
*/
@@ -26147,7 +26385,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
leftOffset = this._getLeftOffset(),
topOffset = this._getTopOffset(), top,
boxStart, boxWidth, charBox, currentDecoration,
- maxHeight, currentFill, lastFill,
+ maxHeight, currentFill, lastFill, path = this.path,
charSpacing = this._getWidthOfCharSpacing();
for (var i = 0, len = this._textLines.length; i < len; i++) {
@@ -26172,15 +26410,30 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
currentFill = this.getValueOfPropertyAt(i, j, 'fill');
_size = this.getHeightOfChar(i, j);
_dy = this.getValueOfPropertyAt(i, j, 'deltaY');
- if ((currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy) &&
- boxWidth > 0) {
+ if (path && currentDecoration && currentFill) {
+ ctx.save();
ctx.fillStyle = lastFill;
- lastDecoration && lastFill && ctx.fillRect(
- leftOffset + lineLeftOffset + boxStart,
- top + this.offsets[type] * size + dy,
- boxWidth,
+ ctx.translate(charBox.renderLeft, charBox.renderTop);
+ ctx.rotate(charBox.angle);
+ ctx.fillRect(
+ -charBox.kernedWidth / 2,
+ this.offsets[type] * _size + _dy,
+ charBox.kernedWidth,
this.fontSize / 15
);
+ ctx.restore();
+ }
+ else if (
+ (currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy)
+ && boxWidth > 0
+ ) {
+ lastDecoration && lastFill &&
+ ctx.fillRect(
+ leftOffset + lineLeftOffset + boxStart,
+ top + this.offsets[type] * size + dy,
+ boxWidth,
+ this.fontSize / 15
+ );
boxStart = charBox.left;
boxWidth = charBox.width;
lastDecoration = currentDecoration;
@@ -26299,13 +26552,22 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
set: function(key, value) {
this.callSuper('set', key, value);
var needsDims = false;
+ var isAddingPath = false;
if (typeof key === 'object') {
for (var _key in key) {
+ if (_key === 'path') {
+ this.setPathInfo();
+ }
needsDims = needsDims || this._dimensionAffectingProps.indexOf(_key) !== -1;
+ isAddingPath = isAddingPath || _key === 'path';
}
}
else {
needsDims = this._dimensionAffectingProps.indexOf(key) !== -1;
+ isAddingPath = key === 'path';
+ }
+ if (isAddingPath) {
+ this.setPathInfo();
}
if (needsDims) {
this.initDimensions();
@@ -26635,7 +26897,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) {
/**
* Gets style of a current selection/cursor (at the start position)
- * if startIndex or endIndex are not provided, slectionStart or selectionEnd will be used.
+ * if startIndex or endIndex are not provided, selectionStart or selectionEnd will be used.
* @param {Number} [startIndex] Start index to get styles at
* @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1
* @param {Boolean} [complete] get full style or not
@@ -28367,7 +28629,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
},
/**
- * standard hander for mouse up, overridable
+ * standard handler for mouse up, overridable
* @private
*/
mouseUpHandler: function(options) {
@@ -28695,7 +28957,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
removeTo = selectionEnd;
}
else if (backDelete) {
- // detect differencies between forwardDelete and backDelete
+ // detect differences between forwardDelete and backDelete
removeFrom = selectionEnd - removedText.length;
removeTo = selectionEnd;
}
@@ -29485,7 +29747,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
/**
* Use this boolean property in order to split strings that have no white space concept.
- * this is a cheap way to help with chinese/japaense
+ * this is a cheap way to help with chinese/japanese
* @type Boolean
* @since 2.6.0
*/
@@ -29725,7 +29987,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
splitByGrapheme = this.splitByGrapheme,
graphemeLines = [],
line = [],
- // spaces in different languges?
+ // spaces in different languages?
words = splitByGrapheme ? fabric.util.string.graphemeSplit(_line) : _line.split(this._wordJoiners),
word = '',
offset = 0,
@@ -29748,7 +30010,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
offset += word.length;
lineWidth += infixWidth + wordWidth - additionalSpace;
- if (lineWidth >= desiredWidth && !lineJustStarted) {
+ if (lineWidth > desiredWidth && !lineJustStarted) {
graphemeLines.push(line);
line = [];
lineWidth = wordWidth;
diff --git a/dist/fabric.min.js b/dist/fabric.min.js
index e48825287ae..d59d16d8a87 100644
--- a/dist/fabric.min.js
+++ b/dist/fabric.min.js
@@ -1 +1 @@
-var fabric=fabric||{version:"4.2.0"};if("undefined"!=typeof exports?exports.fabric=fabric:"function"==typeof define&&define.amd&&define([],function(){return fabric}),"undefined"!=typeof document&&"undefined"!=typeof window)document instanceof("undefined"!=typeof HTMLDocument?HTMLDocument:Document)?fabric.document=document:fabric.document=document.implementation.createHTMLDocument(""),fabric.window=window;else{var jsdom=require("jsdom"),virtualWindow=new jsdom.JSDOM(decodeURIComponent("%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E"),{features:{FetchExternalResources:["img"]},resources:"usable"}).window;fabric.document=virtualWindow.document,fabric.jsdomImplForWrapper=require("jsdom/lib/jsdom/living/generated/utils").implForWrapper,fabric.nodeCanvas=require("jsdom/lib/jsdom/utils").Canvas,fabric.window=virtualWindow,DOMParser=fabric.window.DOMParser}function resizeCanvasIfNeeded(t){var e=t.targetCanvas,i=e.width,r=e.height,n=t.destinationWidth,s=t.destinationHeight;i===n&&r===s||(e.width=n,e.height=s)}function copyGLTo2DDrawImage(t,e){var i=t.canvas,r=e.targetCanvas,n=r.getContext("2d");n.translate(0,r.height),n.scale(1,-1);var s=i.height-r.height;n.drawImage(i,0,s,r.width,r.height,0,0,r.width,r.height)}function copyGLTo2DPutImageData(t,e){var i=e.targetCanvas.getContext("2d"),r=e.destinationWidth,n=e.destinationHeight,s=r*n*4,o=new Uint8Array(this.imageBuffer,0,s),a=new Uint8ClampedArray(this.imageBuffer,0,s);t.readPixels(0,0,r,n,t.RGBA,t.UNSIGNED_BYTE,o);var c=new ImageData(a,r,n);i.putImageData(c,0,0)}fabric.isTouchSupported="ontouchstart"in fabric.window||"ontouchstart"in fabric.document||fabric.window&&fabric.window.navigator&&0y)for(var C=1,S=d.length;C/g,">")},graphemeSplit:function(t){var e,i=0,r=[];for(i=0;it.x&&this.y>t.y},gte:function(t){return this.x>=t.x&&this.y>=t.y},lerp:function(t,e){return void 0===e&&(e=.5),e=Math.max(Math.min(1,e),0),new i(this.x+(t.x-this.x)*e,this.y+(t.y-this.y)*e)},distanceFrom:function(t){var e=this.x-t.x,i=this.y-t.y;return Math.sqrt(e*e+i*i)},midPointFrom:function(t){return this.lerp(t)},min:function(t){return new i(Math.min(this.x,t.x),Math.min(this.y,t.y))},max:function(t){return new i(Math.max(this.x,t.x),Math.max(this.y,t.y))},toString:function(){return this.x+","+this.y},setXY:function(t,e){return this.x=t,this.y=e,this},setX:function(t){return this.x=t,this},setY:function(t){return this.y=t,this},setFromPoint:function(t){return this.x=t.x,this.y=t.y,this},swap:function(t){var e=this.x,i=this.y;this.x=t.x,this.y=t.y,t.x=e,t.y=i},clone:function(){return new i(this.x,this.y)}}}("undefined"!=typeof exports?exports:this),function(t){"use strict";var f=t.fabric||(t.fabric={});function d(t){this.status=t,this.points=[]}f.Intersection?f.warn("fabric.Intersection is already defined"):(f.Intersection=d,f.Intersection.prototype={constructor:d,appendPoint:function(t){return this.points.push(t),this},appendPoints:function(t){return this.points=this.points.concat(t),this}},f.Intersection.intersectLineLine=function(t,e,i,r){var n,s=(r.x-i.x)*(t.y-i.y)-(r.y-i.y)*(t.x-i.x),o=(e.x-t.x)*(t.y-i.y)-(e.y-t.y)*(t.x-i.x),a=(r.y-i.y)*(e.x-t.x)-(r.x-i.x)*(e.y-t.y);if(0!==a){var c=s/a,h=o/a;0<=c&&c<=1&&0<=h&&h<=1?(n=new d("Intersection")).appendPoint(new f.Point(t.x+c*(e.x-t.x),t.y+c*(e.y-t.y))):n=new d}else n=new d(0===s||0===o?"Coincident":"Parallel");return n},f.Intersection.intersectLinePolygon=function(t,e,i){var r,n,s,o,a=new d,c=i.length;for(o=0;o=c&&(h.x-=c),h.x<=-c&&(h.x+=c),h.y>=c&&(h.y-=c),h.y<=c&&(h.y+=c),h.x-=o.offsetX,h.y-=o.offsetY,h}function m(t){return t.flipX&&!t.flipY||!t.flipX&&t.flipY}function b(t,e,i,r,n){if(0!==t[e]){var s=n/t._getTransformedDimensions()[r]*t[i];t.set(i,s)}}function u(t,e,i,r){var n,s=e.target,o=s._getTransformedDimensions(0,s.skewY),a=A(e,e.originX,e.originY,i,r),c=Math.abs(2*a.x)-o.x,h=s.skewX;c<2?n=0:(n=v(Math.atan2(c/s.scaleX,o.y/s.scaleY)),e.originX===f&&e.originY===p&&(n=-n),e.originX===g&&e.originY===d&&(n=-n),m(s)&&(n=-n));var l=h!==n;if(l){var u=s._getTransformedDimensions().y;s.set("skewX",n),b(s,"skewY","scaleY","y",u),O("skewing",E(t,e,i,r))}return l}function _(t,e,i,r){var n,s=e.target,o=s._getTransformedDimensions(s.skewX,0),a=A(e,e.originX,e.originY,i,r),c=Math.abs(2*a.y)-o.y,h=s.skewY;c<2?n=0:(n=v(Math.atan2(c/s.scaleY,o.x/s.scaleX)),e.originX===f&&e.originY===p&&(n=-n),e.originX===g&&e.originY===d&&(n=-n),m(s)&&(n=-n));var l=h!==n;if(l){var u=s._getTransformedDimensions().x;s.set("skewY",n),b(s,"skewX","scaleX","x",u),O("skewing",E(t,e,i,r))}return l}function y(t,e,i,r,n){n=n||{};var s,o,a,c,h,l,u=e.target,f=u.lockScalingX,d=u.lockScalingY,g=n.by,p=k(t,u),v=P(u,g,p),m=e.gestureScale;if(v)return!1;if(m)o=e.scaleX*m,a=e.scaleY*m;else{if(s=A(e,e.originX,e.originY,i,r),h="y"!==g?T(s.x):1,l="x"!==g?T(s.y):1,e.signX||(e.signX=h),e.signY||(e.signY=l),u.lockScalingFlip&&(e.signX!==h||e.signY!==l))return!1;if(c=u._getTransformedDimensions(),p&&!g){var b,_=Math.abs(s.x)+Math.abs(s.y),y=e.original,x=_/(Math.abs(c.x*y.scaleX/u.scaleX)+Math.abs(c.y*y.scaleY/u.scaleY));o=y.scaleX*x,a=y.scaleY*x}else o=Math.abs(s.x*u.scaleX/c.x),a=Math.abs(s.y*u.scaleY/c.y);D(e)&&(o*=2,a*=2),e.signX!==h&&"y"!==g&&(e.originX=w[e.originX],o*=-1,e.signX=h),e.signY!==l&&"x"!==g&&(e.originY=w[e.originY],a*=-1,e.signY=l)}var C=u.scaleX,S=u.scaleY;return g?("x"===g&&u.set("scaleX",o),"y"===g&&u.set("scaleY",a)):(!f&&u.set("scaleX",o),!d&&u.set("scaleY",a)),(b=C!==u.scaleX||S!==u.scaleY)&&O("scaling",E(t,e,i,r)),b}n.scaleCursorStyleHandler=function(t,e,i){var r=k(t,i),n="";if(0!==e.x&&0===e.y?n="x":0===e.x&&0!==e.y&&(n="y"),P(i,n,r))return"not-allowed";var s=a(i,e);return o[s]+"-resize"},n.skewCursorStyleHandler=function(t,e,i){var r="not-allowed";if(0!==e.x&&i.lockSkewingY)return r;if(0!==e.y&&i.lockSkewingX)return r;var n=a(i,e)%4;return s[n]+"-resize"},n.scaleSkewCursorStyleHandler=function(t,e,i){return t[i.canvas.altActionKey]?n.skewCursorStyleHandler(t,e,i):n.scaleCursorStyleHandler(t,e,i)},n.rotationWithSnapping=h(function(t,e,i,r){var n=e,s=n.target,o=s.translateToOriginPoint(s.getCenterPoint(),n.originX,n.originY);if(s.lockRotation)return!1;var a,c=Math.atan2(n.ey-o.y,n.ex-o.x),h=Math.atan2(r-o.y,i-o.x),l=v(h-c+n.theta);if(0o.r2,h=this.gradientTransform?this.gradientTransform.concat():fabric.iMatrix.concat(),l=-this.offsetX,u=-this.offsetY,f=!!e.additionalTransform,d="pixels"===this.gradientUnits?"userSpaceOnUse":"objectBoundingBox";if(a.sort(function(t,e){return t.offset-e.offset}),"objectBoundingBox"===d?(l/=t.width,u/=t.height):(l+=t.width/2,u+=t.height/2),"path"===t.type&&(l-=t.pathOffset.x,u-=t.pathOffset.y),h[4]-=l,h[5]-=u,s='id="SVGID_'+this.id+'" gradientUnits="'+d+'"',s+=' gradientTransform="'+(f?e.additionalTransform+" ":"")+fabric.util.matrixToSVG(h)+'" ',"linear"===this.type?n=["\n']:"radial"===this.type&&(n=["\n']),"radial"===this.type){if(c)for((a=a.concat()).reverse(),i=0,r=a.length;i\n')}return n.push("linear"===this.type?"\n":"\n"),n.join("")},toLive:function(t){var e,i,r,n=fabric.util.object.clone(this.coords);if(this.type){for("linear"===this.type?e=t.createLinearGradient(n.x1,n.y1,n.x2,n.y2):"radial"===this.type&&(e=t.createRadialGradient(n.x1,n.y1,n.r1,n.x2,n.y2,n.r2)),i=0,r=this.colorStops.length;i\n\n\n'},setOptions:function(t){for(var e in t)this[e]=t[e]},toLive:function(t){var e=this.source;if(!e)return"";if(void 0!==e.src){if(!e.complete)return"";if(0===e.naturalWidth||0===e.naturalHeight)return""}return t.createPattern(e,this.repeat)}})}(),function(t){"use strict";var o=t.fabric||(t.fabric={}),a=o.util.toFixed;o.Shadow?o.warn("fabric.Shadow is already defined."):(o.Shadow=o.util.createClass({color:"rgb(0,0,0)",blur:0,offsetX:0,offsetY:0,affectStroke:!1,includeDefaultValues:!0,nonScaling:!1,initialize:function(t){for(var e in"string"==typeof t&&(t=this._parseShadow(t)),t)this[e]=t[e];this.id=o.Object.__uid++},_parseShadow:function(t){var e=t.trim(),i=o.Shadow.reOffsetsAndBlur.exec(e)||[];return{color:(e.replace(o.Shadow.reOffsetsAndBlur,"")||"rgb(0,0,0)").trim(),offsetX:parseInt(i[1],10)||0,offsetY:parseInt(i[2],10)||0,blur:parseInt(i[3],10)||0}},toString:function(){return[this.offsetX,this.offsetY,this.blur,this.color].join("px ")},toSVG:function(t){var e=40,i=40,r=o.Object.NUM_FRACTION_DIGITS,n=o.util.rotateVector({x:this.offsetX,y:this.offsetY},o.util.degreesToRadians(-t.angle)),s=new o.Color(this.color);return t.width&&t.height&&(e=100*a((Math.abs(n.x)+this.blur)/t.width,r)+20,i=100*a((Math.abs(n.y)+this.blur)/t.height,r)+20),t.flipX&&(n.x*=-1),t.flipY&&(n.y*=-1),'\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\n\t\n\n'},toObject:function(){if(this.includeDefaultValues)return{color:this.color,blur:this.blur,offsetX:this.offsetX,offsetY:this.offsetY,affectStroke:this.affectStroke,nonScaling:this.nonScaling};var e={},i=o.Shadow.prototype;return["color","blur","offsetX","offsetY","affectStroke","nonScaling"].forEach(function(t){this[t]!==i[t]&&(e[t]=this[t])},this),e}}),o.Shadow.reOffsetsAndBlur=/(?:\s|^)(-?\d+(?:px)?(?:\s?|$))?(-?\d+(?:px)?(?:\s?|$))?(\d+(?:px)?)?(?:\s?|$)(?:$|\s)/)}("undefined"!=typeof exports?exports:this),function(){"use strict";if(fabric.StaticCanvas)fabric.warn("fabric.StaticCanvas is already defined.");else{var n=fabric.util.object.extend,t=fabric.util.getElementOffset,h=fabric.util.removeFromArray,a=fabric.util.toFixed,s=fabric.util.transformPoint,o=fabric.util.invertTransform,i=fabric.util.getNodeCanvas,r=fabric.util.createCanvasElement,e=new Error("Could not initialize `canvas` element");fabric.StaticCanvas=fabric.util.createClass(fabric.CommonMethods,{initialize:function(t,e){e||(e={}),this.renderAndResetBound=this.renderAndReset.bind(this),this.requestRenderAllBound=this.requestRenderAll.bind(this),this._initStatic(t,e)},backgroundColor:"",backgroundImage:null,overlayColor:"",overlayImage:null,includeDefaultValues:!0,stateful:!1,renderOnAddRemove:!0,controlsAboveOverlay:!1,allowTouchScrolling:!1,imageSmoothingEnabled:!0,viewportTransform:fabric.iMatrix.concat(),backgroundVpt:!0,overlayVpt:!0,enableRetinaScaling:!0,vptCoords:{},skipOffscreen:!0,clipPath:void 0,_initStatic:function(t,e){var i=this.requestRenderAllBound;this._objects=[],this._createLowerCanvas(t),this._initOptions(e),this.interactive||this._initRetinaScaling(),e.overlayImage&&this.setOverlayImage(e.overlayImage,i),e.backgroundImage&&this.setBackgroundImage(e.backgroundImage,i),e.backgroundColor&&this.setBackgroundColor(e.backgroundColor,i),e.overlayColor&&this.setOverlayColor(e.overlayColor,i),this.calcOffset()},_isRetinaScaling:function(){return 1!==fabric.devicePixelRatio&&this.enableRetinaScaling},getRetinaScaling:function(){return this._isRetinaScaling()?fabric.devicePixelRatio:1},_initRetinaScaling:function(){if(this._isRetinaScaling()){var t=fabric.devicePixelRatio;this.__initRetinaScaling(t,this.lowerCanvasEl,this.contextContainer),this.upperCanvasEl&&this.__initRetinaScaling(t,this.upperCanvasEl,this.contextTop)}},__initRetinaScaling:function(t,e,i){e.setAttribute("width",this.width*t),e.setAttribute("height",this.height*t),i.scale(t,t)},calcOffset:function(){return this._offset=t(this.lowerCanvasEl),this},setOverlayImage:function(t,e,i){return this.__setBgOverlayImage("overlayImage",t,e,i)},setBackgroundImage:function(t,e,i){return this.__setBgOverlayImage("backgroundImage",t,e,i)},setOverlayColor:function(t,e){return this.__setBgOverlayColor("overlayColor",t,e)},setBackgroundColor:function(t,e){return this.__setBgOverlayColor("backgroundColor",t,e)},__setBgOverlayImage:function(r,t,n,s){return"string"==typeof t?fabric.util.loadImage(t,function(t,e){if(t){var i=new fabric.Image(t,s);(this[r]=i).canvas=this}n&&n(t,e)},this,s&&s.crossOrigin):(s&&t.setOptions(s),(this[r]=t)&&(t.canvas=this),n&&n(t,!1)),this},__setBgOverlayColor:function(t,e,i){return this[t]=e,this._initGradient(e,t),this._initPattern(e,t,i),this},_createCanvasElement:function(){var t=r();if(!t)throw e;if(t.style||(t.style={}),void 0===t.getContext)throw e;return t},_initOptions:function(t){var e=this.lowerCanvasEl;this._setOptions(t),this.width=this.width||parseInt(e.width,10)||0,this.height=this.height||parseInt(e.height,10)||0,this.lowerCanvasEl.style&&(e.width=this.width,e.height=this.height,e.style.width=this.width+"px",e.style.height=this.height+"px",this.viewportTransform=this.viewportTransform.slice())},_createLowerCanvas:function(t){t&&t.getContext?this.lowerCanvasEl=t:this.lowerCanvasEl=fabric.util.getById(t)||this._createCanvasElement(),fabric.util.addClass(this.lowerCanvasEl,"lower-canvas"),this.interactive&&this._applyCanvasStyle(this.lowerCanvasEl),this.contextContainer=this.lowerCanvasEl.getContext("2d")},getWidth:function(){return this.width},getHeight:function(){return this.height},setWidth:function(t,e){return this.setDimensions({width:t},e)},setHeight:function(t,e){return this.setDimensions({height:t},e)},setDimensions:function(t,e){var i;for(var r in e=e||{},t)i=t[r],e.cssOnly||(this._setBackstoreDimension(r,t[r]),i+="px",this.hasLostContext=!0),e.backstoreOnly||this._setCssDimension(r,i);return this._isCurrentlyDrawing&&this.freeDrawingBrush&&this.freeDrawingBrush._setBrushStyles(),this._initRetinaScaling(),this.calcOffset(),e.cssOnly||this.requestRenderAll(),this},_setBackstoreDimension:function(t,e){return this.lowerCanvasEl[t]=e,this.upperCanvasEl&&(this.upperCanvasEl[t]=e),this.cacheCanvasEl&&(this.cacheCanvasEl[t]=e),this[t]=e,this},_setCssDimension:function(t,e){return this.lowerCanvasEl.style[t]=e,this.upperCanvasEl&&(this.upperCanvasEl.style[t]=e),this.wrapperEl&&(this.wrapperEl.style[t]=e),this},getZoom:function(){return this.viewportTransform[0]},setViewportTransform:function(t){var e,i,r,n=this._activeObject;for(this.viewportTransform=t,i=0,r=this._objects.length;i\n'),this._setSVGBgOverlayColor(i,"background"),this._setSVGBgOverlayImage(i,"backgroundImage",e),this._setSVGObjects(i,e),this.clipPath&&i.push("\n"),this._setSVGBgOverlayColor(i,"overlay"),this._setSVGBgOverlayImage(i,"overlayImage",e),i.push(""),i.join("")},_setSVGPreamble:function(t,e){e.suppressPreamble||t.push('\n','\n')},_setSVGHeader:function(t,e){var i,r=e.width||this.width,n=e.height||this.height,s='viewBox="0 0 '+this.width+" "+this.height+'" ',o=fabric.Object.NUM_FRACTION_DIGITS;e.viewBox?s='viewBox="'+e.viewBox.x+" "+e.viewBox.y+" "+e.viewBox.width+" "+e.viewBox.height+'" ':this.svgViewportTransformation&&(i=this.viewportTransform,s='viewBox="'+a(-i[4]/i[0],o)+" "+a(-i[5]/i[3],o)+" "+a(this.width/i[0],o)+" "+a(this.height/i[3],o)+'" '),t.push(""),i.join("")},_setSVGPreamble:function(t,e){e.suppressPreamble||t.push('\n','\n')},_setSVGHeader:function(t,e){var i,r=e.width||this.width,n=e.height||this.height,s='viewBox="0 0 '+this.width+" "+this.height+'" ',o=fabric.Object.NUM_FRACTION_DIGITS;e.viewBox?s='viewBox="'+e.viewBox.x+" "+e.viewBox.y+" "+e.viewBox.width+" "+e.viewBox.height+'" ':this.svgViewportTransformation&&(i=this.viewportTransform,s='viewBox="'+a(-i[4]/i[0],o)+" "+a(-i[5]/i[3],o)+" "+a(this.width/i[0],o)+" "+a(this.height/i[3],o)+'" '),t.push("