mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-03-16 22:10:32 +00:00
842 lines
25 KiB
JavaScript
842 lines
25 KiB
JavaScript
(function(global) {
|
|
|
|
var sqrt = Math.sqrt,
|
|
atan2 = Math.atan2,
|
|
pow = Math.pow,
|
|
abs = Math.abs,
|
|
PiBy180 = Math.PI / 180,
|
|
PiBy2 = Math.PI / 2;
|
|
|
|
/**
|
|
* @namespace fabric.util
|
|
*/
|
|
fabric.util = {
|
|
|
|
/**
|
|
* Calculate the cos of an angle, avoiding returning floats for known results
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Number} angle the angle in radians or in degree
|
|
* @return {Number}
|
|
*/
|
|
cos: function(angle) {
|
|
if (angle === 0) { return 1; }
|
|
if (angle < 0) {
|
|
// cos(a) = cos(-a)
|
|
angle = -angle;
|
|
}
|
|
var angleSlice = angle / PiBy2;
|
|
switch (angleSlice) {
|
|
case 1: case 3: return 0;
|
|
case 2: return -1;
|
|
}
|
|
return Math.cos(angle);
|
|
},
|
|
|
|
/**
|
|
* Calculate the sin of an angle, avoiding returning floats for known results
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Number} angle the angle in radians or in degree
|
|
* @return {Number}
|
|
*/
|
|
sin: function(angle) {
|
|
if (angle === 0) { return 0; }
|
|
var angleSlice = angle / PiBy2, sign = 1;
|
|
if (angle < 0) {
|
|
// sin(-a) = -sin(a)
|
|
sign = -1;
|
|
}
|
|
switch (angleSlice) {
|
|
case 1: return sign;
|
|
case 2: return 0;
|
|
case 3: return -sign;
|
|
}
|
|
return Math.sin(angle);
|
|
},
|
|
|
|
/**
|
|
* Removes value from an array.
|
|
* Presence of value (and its position in an array) is determined via `Array.prototype.indexOf`
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Array} array
|
|
* @param {*} value
|
|
* @return {Array} original array
|
|
*/
|
|
removeFromArray: function(array, value) {
|
|
var idx = array.indexOf(value);
|
|
if (idx !== -1) {
|
|
array.splice(idx, 1);
|
|
}
|
|
return array;
|
|
},
|
|
|
|
/**
|
|
* Returns random number between 2 specified ones.
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Number} min lower limit
|
|
* @param {Number} max upper limit
|
|
* @return {Number} random value (between min and max)
|
|
*/
|
|
getRandomInt: function(min, max) {
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
},
|
|
|
|
/**
|
|
* Transforms degrees to radians.
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Number} degrees value in degrees
|
|
* @return {Number} value in radians
|
|
*/
|
|
degreesToRadians: function(degrees) {
|
|
return degrees * PiBy180;
|
|
},
|
|
|
|
/**
|
|
* Transforms radians to degrees.
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Number} radians value in radians
|
|
* @return {Number} value in degrees
|
|
*/
|
|
radiansToDegrees: function(radians) {
|
|
return radians / PiBy180;
|
|
},
|
|
|
|
/**
|
|
* Rotates `point` around `origin` with `radians`
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {fabric.Point} point The point to rotate
|
|
* @param {fabric.Point} origin The origin of the rotation
|
|
* @param {Number} radians The radians of the angle for the rotation
|
|
* @return {fabric.Point} The new rotated point
|
|
*/
|
|
rotatePoint: function(point, origin, radians) {
|
|
point.subtractEquals(origin);
|
|
var v = fabric.util.rotateVector(point, radians);
|
|
return new fabric.Point(v.x, v.y).addEquals(origin);
|
|
},
|
|
|
|
/**
|
|
* Rotates `vector` with `radians`
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Object} vector The vector to rotate (x and y)
|
|
* @param {Number} radians The radians of the angle for the rotation
|
|
* @return {Object} The new rotated point
|
|
*/
|
|
rotateVector: function(vector, radians) {
|
|
var sin = fabric.util.sin(radians),
|
|
cos = fabric.util.cos(radians),
|
|
rx = vector.x * cos - vector.y * sin,
|
|
ry = vector.x * sin + vector.y * cos;
|
|
return {
|
|
x: rx,
|
|
y: ry
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Apply transform t to point p
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {fabric.Point} p The point to transform
|
|
* @param {Array} t The transform
|
|
* @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied
|
|
* @return {fabric.Point} The transformed point
|
|
*/
|
|
transformPoint: function(p, t, ignoreOffset) {
|
|
if (ignoreOffset) {
|
|
return new fabric.Point(
|
|
t[0] * p.x + t[2] * p.y,
|
|
t[1] * p.x + t[3] * p.y
|
|
);
|
|
}
|
|
return new fabric.Point(
|
|
t[0] * p.x + t[2] * p.y + t[4],
|
|
t[1] * p.x + t[3] * p.y + t[5]
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Returns coordinates of points's bounding rectangle (left, top, width, height)
|
|
* @param {Array} points 4 points array
|
|
* @return {Object} Object with left, top, width, height properties
|
|
*/
|
|
makeBoundingBoxFromPoints: function(points) {
|
|
var xPoints = [points[0].x, points[1].x, points[2].x, points[3].x],
|
|
minX = fabric.util.array.min(xPoints),
|
|
maxX = fabric.util.array.max(xPoints),
|
|
width = maxX - minX,
|
|
yPoints = [points[0].y, points[1].y, points[2].y, points[3].y],
|
|
minY = fabric.util.array.min(yPoints),
|
|
maxY = fabric.util.array.max(yPoints),
|
|
height = maxY - minY;
|
|
|
|
return {
|
|
left: minX,
|
|
top: minY,
|
|
width: width,
|
|
height: height
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Invert transformation t
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Array} t The transform
|
|
* @return {Array} The inverted transform
|
|
*/
|
|
invertTransform: function(t) {
|
|
var a = 1 / (t[0] * t[3] - t[1] * t[2]),
|
|
r = [a * t[3], -a * t[1], -a * t[2], a * t[0]],
|
|
o = fabric.util.transformPoint({ x: t[4], y: t[5] }, r, true);
|
|
r[4] = -o.x;
|
|
r[5] = -o.y;
|
|
return r;
|
|
},
|
|
|
|
/**
|
|
* A wrapper around Number#toFixed, which contrary to native method returns number, not string.
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Number|String} number number to operate on
|
|
* @param {Number} fractionDigits number of fraction digits to "leave"
|
|
* @return {Number}
|
|
*/
|
|
toFixed: function(number, fractionDigits) {
|
|
return parseFloat(Number(number).toFixed(fractionDigits));
|
|
},
|
|
|
|
/**
|
|
* Converts from attribute value to pixel value if applicable.
|
|
* Returns converted pixels or original value not converted.
|
|
* @param {Number|String} value number to operate on
|
|
* @param {Number} fontSize
|
|
* @return {Number|String}
|
|
*/
|
|
parseUnit: function(value, fontSize) {
|
|
var unit = /\D{0,2}$/.exec(value),
|
|
number = parseFloat(value);
|
|
if (!fontSize) {
|
|
fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE;
|
|
}
|
|
switch (unit[0]) {
|
|
case 'mm':
|
|
return number * fabric.DPI / 25.4;
|
|
|
|
case 'cm':
|
|
return number * fabric.DPI / 2.54;
|
|
|
|
case 'in':
|
|
return number * fabric.DPI;
|
|
|
|
case 'pt':
|
|
return number * fabric.DPI / 72; // or * 4 / 3
|
|
|
|
case 'pc':
|
|
return number * fabric.DPI / 72 * 12; // or * 16
|
|
|
|
case 'em':
|
|
return number * fontSize;
|
|
|
|
default:
|
|
return number;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Function which always returns `false`.
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @return {Boolean}
|
|
*/
|
|
falseFunction: function() {
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Returns klass "Class" object of given namespace
|
|
* @memberOf fabric.util
|
|
* @param {String} type Type of object (eg. 'circle')
|
|
* @param {String} namespace Namespace to get klass "Class" object from
|
|
* @return {Object} klass "Class"
|
|
*/
|
|
getKlass: function(type, namespace) {
|
|
// capitalize first letter only
|
|
type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1));
|
|
return fabric.util.resolveNamespace(namespace)[type];
|
|
},
|
|
|
|
/**
|
|
* Returns array of attributes for given svg that fabric parses
|
|
* @memberOf fabric.util
|
|
* @param {String} type Type of svg element (eg. 'circle')
|
|
* @return {Array} string names of supported attributes
|
|
*/
|
|
getSvgAttributes: function(type) {
|
|
var attributes = [
|
|
'instantiated_by_use',
|
|
'style',
|
|
'id',
|
|
'class'
|
|
];
|
|
switch (type) {
|
|
case 'linearGradient':
|
|
attributes = attributes.concat(['x1', 'y1', 'x2', 'y2', 'gradientUnits', 'gradientTransform']);
|
|
break;
|
|
case 'radialGradient':
|
|
attributes = attributes.concat(['gradientUnits', 'gradientTransform', 'cx', 'cy', 'r', 'fx', 'fy', 'fr']);
|
|
break;
|
|
case 'stop':
|
|
attributes = attributes.concat(['offset', 'stop-color', 'stop-opacity']);
|
|
break;
|
|
}
|
|
return attributes;
|
|
},
|
|
|
|
/**
|
|
* Returns object of given namespace
|
|
* @memberOf fabric.util
|
|
* @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric'
|
|
* @return {Object} Object for given namespace (default fabric)
|
|
*/
|
|
resolveNamespace: function(namespace) {
|
|
if (!namespace) {
|
|
return fabric;
|
|
}
|
|
|
|
var parts = namespace.split('.'),
|
|
len = parts.length, i,
|
|
obj = global || fabric.window;
|
|
|
|
for (i = 0; i < len; ++i) {
|
|
obj = obj[parts[i]];
|
|
}
|
|
|
|
return obj;
|
|
},
|
|
|
|
/**
|
|
* Loads image element from given url and passes it to a callback
|
|
* @memberOf fabric.util
|
|
* @param {String} url URL representing an image
|
|
* @param {Function} callback Callback; invoked with loaded image
|
|
* @param {*} [context] Context to invoke callback in
|
|
* @param {Object} [crossOrigin] crossOrigin value to set image element to
|
|
*/
|
|
loadImage: function(url, callback, context, crossOrigin) {
|
|
if (!url) {
|
|
callback && callback.call(context, url);
|
|
return;
|
|
}
|
|
|
|
var img = fabric.util.createImage();
|
|
|
|
/** @ignore */
|
|
var onLoadCallback = function () {
|
|
callback && callback.call(context, img);
|
|
img = img.onload = img.onerror = null;
|
|
};
|
|
|
|
img.onload = onLoadCallback;
|
|
/** @ignore */
|
|
img.onerror = function() {
|
|
fabric.log('Error loading ' + img.src);
|
|
callback && callback.call(context, null, true);
|
|
img = img.onload = img.onerror = null;
|
|
};
|
|
|
|
// data-urls appear to be buggy with crossOrigin
|
|
// https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767
|
|
// see https://code.google.com/p/chromium/issues/detail?id=315152
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=935069
|
|
if (url.indexOf('data') !== 0 && crossOrigin) {
|
|
img.crossOrigin = crossOrigin;
|
|
}
|
|
|
|
// IE10 / IE11-Fix: SVG contents from data: URI
|
|
// will only be available if the IMG is present
|
|
// in the DOM (and visible)
|
|
if (url.substring(0,14) === 'data:image/svg') {
|
|
img.onload = null;
|
|
fabric.util.loadImageInDom(img, onLoadCallback);
|
|
}
|
|
|
|
img.src = url;
|
|
},
|
|
|
|
/**
|
|
* Attaches SVG image with data: URL to the dom
|
|
* @memberOf fabric.util
|
|
* @param {Object} img Image object with data:image/svg src
|
|
* @param {Function} callback Callback; invoked with loaded image
|
|
* @return {Object} DOM element (div containing the SVG image)
|
|
*/
|
|
loadImageInDom: function(img, onLoadCallback) {
|
|
var div = fabric.document.createElement('div');
|
|
div.style.width = div.style.height = '1px';
|
|
div.style.left = div.style.top = '-100%';
|
|
div.style.position = 'absolute';
|
|
div.appendChild(img);
|
|
fabric.document.querySelector('body').appendChild(div);
|
|
/**
|
|
* Wrap in function to:
|
|
* 1. Call existing callback
|
|
* 2. Cleanup DOM
|
|
*/
|
|
img.onload = function () {
|
|
onLoadCallback();
|
|
div.parentNode.removeChild(div);
|
|
div = null;
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Creates corresponding fabric instances from their object representations
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Array} objects Objects to enliven
|
|
* @param {Function} callback Callback to invoke when all objects are created
|
|
* @param {String} namespace Namespace to get klass "Class" object from
|
|
* @param {Function} reviver Method for further parsing of object elements,
|
|
* called after each fabric object created.
|
|
*/
|
|
enlivenObjects: function(objects, callback, namespace, reviver) {
|
|
objects = objects || [];
|
|
|
|
function onLoaded() {
|
|
if (++numLoadedObjects === numTotalObjects) {
|
|
callback && callback(enlivenedObjects);
|
|
}
|
|
}
|
|
|
|
var enlivenedObjects = [],
|
|
numLoadedObjects = 0,
|
|
numTotalObjects = objects.length;
|
|
|
|
if (!numTotalObjects) {
|
|
callback && callback(enlivenedObjects);
|
|
return;
|
|
}
|
|
|
|
objects.forEach(function (o, index) {
|
|
// if sparse array
|
|
if (!o || !o.type) {
|
|
onLoaded();
|
|
return;
|
|
}
|
|
var klass = fabric.util.getKlass(o.type, namespace);
|
|
klass.fromObject(o, function (obj, error) {
|
|
error || (enlivenedObjects[index] = obj);
|
|
reviver && reviver(o, obj, error);
|
|
onLoaded();
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Create and wait for loading of patterns
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Array} patterns Objects to enliven
|
|
* @param {Function} callback Callback to invoke when all objects are created
|
|
* called after each fabric object created.
|
|
*/
|
|
enlivenPatterns: function(patterns, callback) {
|
|
patterns = patterns || [];
|
|
|
|
function onLoaded() {
|
|
if (++numLoadedPatterns === numPatterns) {
|
|
callback && callback(enlivenedPatterns);
|
|
}
|
|
}
|
|
|
|
var enlivenedPatterns = [],
|
|
numLoadedPatterns = 0,
|
|
numPatterns = patterns.length;
|
|
|
|
if (!numPatterns) {
|
|
callback && callback(enlivenedPatterns);
|
|
return;
|
|
}
|
|
|
|
patterns.forEach(function (p, index) {
|
|
if (p && p.source) {
|
|
new fabric.Pattern(p, function(pattern) {
|
|
enlivenedPatterns[index] = pattern;
|
|
onLoaded();
|
|
});
|
|
}
|
|
else {
|
|
enlivenedPatterns[index] = p;
|
|
onLoaded();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Groups SVG elements (usually those retrieved from SVG document)
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Array} elements SVG elements to group
|
|
* @param {Object} [options] Options object
|
|
* @param {String} path Value to set sourcePath to
|
|
* @return {fabric.Object|fabric.Group}
|
|
*/
|
|
groupSVGElements: function(elements, options, path) {
|
|
var object;
|
|
if (elements && elements.length === 1) {
|
|
return elements[0];
|
|
}
|
|
if (options) {
|
|
if (options.width && options.height) {
|
|
options.centerPoint = {
|
|
x: options.width / 2,
|
|
y: options.height / 2
|
|
};
|
|
}
|
|
else {
|
|
delete options.width;
|
|
delete options.height;
|
|
}
|
|
}
|
|
object = new fabric.Group(elements, options);
|
|
if (typeof path !== 'undefined') {
|
|
object.sourcePath = path;
|
|
}
|
|
return object;
|
|
},
|
|
|
|
/**
|
|
* Populates an object with properties of another object
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Object} source Source object
|
|
* @param {Object} destination Destination object
|
|
* @return {Array} properties Properties names to include
|
|
*/
|
|
populateWithProperties: function(source, destination, properties) {
|
|
if (properties && Object.prototype.toString.call(properties) === '[object Array]') {
|
|
for (var i = 0, len = properties.length; i < len; i++) {
|
|
if (properties[i] in source) {
|
|
destination[properties[i]] = source[properties[i]];
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Draws a dashed line between two points
|
|
*
|
|
* This method is used to draw dashed line around selection area.
|
|
* See <a href="http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas">dotted stroke in canvas</a>
|
|
*
|
|
* @param {CanvasRenderingContext2D} ctx context
|
|
* @param {Number} x start x coordinate
|
|
* @param {Number} y start y coordinate
|
|
* @param {Number} x2 end x coordinate
|
|
* @param {Number} y2 end y coordinate
|
|
* @param {Array} da dash array pattern
|
|
*/
|
|
drawDashedLine: function(ctx, x, y, x2, y2, da) {
|
|
var dx = x2 - x,
|
|
dy = y2 - y,
|
|
len = sqrt(dx * dx + dy * dy),
|
|
rot = atan2(dy, dx),
|
|
dc = da.length,
|
|
di = 0,
|
|
draw = true;
|
|
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
ctx.moveTo(0, 0);
|
|
ctx.rotate(rot);
|
|
|
|
x = 0;
|
|
while (len > x) {
|
|
x += da[di++ % dc];
|
|
if (x > len) {
|
|
x = len;
|
|
}
|
|
ctx[draw ? 'lineTo' : 'moveTo'](x, 0);
|
|
draw = !draw;
|
|
}
|
|
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* Creates canvas element
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @return {CanvasElement} initialized canvas element
|
|
*/
|
|
createCanvasElement: function() {
|
|
return fabric.document.createElement('canvas');
|
|
},
|
|
|
|
/**
|
|
* Creates a canvas element that is a copy of another and is also painted
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @return {CanvasElement} initialized canvas element
|
|
*/
|
|
copyCanvasElement: function(canvas) {
|
|
var newCanvas = fabric.util.createCanvasElement();
|
|
newCanvas.width = canvas.width;
|
|
newCanvas.height = canvas.height;
|
|
newCanvas.getContext('2d').drawImage(canvas, 0, 0);
|
|
return newCanvas;
|
|
},
|
|
|
|
/**
|
|
* Creates image element (works on client and node)
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @return {HTMLImageElement} HTML image element
|
|
*/
|
|
createImage: function() {
|
|
return fabric.document.createElement('img');
|
|
},
|
|
|
|
/**
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @deprecated since 2.0.0
|
|
* @param {fabric.Object} receiver Object implementing `clipTo` method
|
|
* @param {CanvasRenderingContext2D} ctx Context to clip
|
|
*/
|
|
clipContext: function(receiver, ctx) {
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
receiver.clipTo(ctx);
|
|
ctx.clip();
|
|
},
|
|
|
|
/**
|
|
* Multiply matrix A by matrix B to nest transformations
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Array} a First transformMatrix
|
|
* @param {Array} b Second transformMatrix
|
|
* @param {Boolean} is2x2 flag to multiply matrices as 2x2 matrices
|
|
* @return {Array} The product of the two transform matrices
|
|
*/
|
|
multiplyTransformMatrices: function(a, b, is2x2) {
|
|
// Matrix multiply a * b
|
|
return [
|
|
a[0] * b[0] + a[2] * b[1],
|
|
a[1] * b[0] + a[3] * b[1],
|
|
a[0] * b[2] + a[2] * b[3],
|
|
a[1] * b[2] + a[3] * b[3],
|
|
is2x2 ? 0 : a[0] * b[4] + a[2] * b[5] + a[4],
|
|
is2x2 ? 0 : a[1] * b[4] + a[3] * b[5] + a[5]
|
|
];
|
|
},
|
|
|
|
/**
|
|
* Decomposes standard 2x2 matrix into transform componentes
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Array} a transformMatrix
|
|
* @return {Object} Components of transform
|
|
*/
|
|
qrDecompose: function(a) {
|
|
var angle = atan2(a[1], a[0]),
|
|
denom = pow(a[0], 2) + pow(a[1], 2),
|
|
scaleX = sqrt(denom),
|
|
scaleY = (a[0] * a[3] - a[2] * a [1]) / scaleX,
|
|
skewX = atan2(a[0] * a[2] + a[1] * a [3], denom);
|
|
return {
|
|
angle: angle / PiBy180,
|
|
scaleX: scaleX,
|
|
scaleY: scaleY,
|
|
skewX: skewX / PiBy180,
|
|
skewY: 0,
|
|
translateX: a[4],
|
|
translateY: a[5]
|
|
};
|
|
},
|
|
|
|
customTransformMatrix: function(scaleX, scaleY, skewX) {
|
|
var skewMatrixX = [1, 0, abs(Math.tan(skewX * PiBy180)), 1],
|
|
scaleMatrix = [abs(scaleX), 0, 0, abs(scaleY)];
|
|
return fabric.util.multiplyTransformMatrices(scaleMatrix, skewMatrixX, true);
|
|
},
|
|
|
|
/**
|
|
* reset an object transform state to neutral. Top and left are not accounted for
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {fabric.Object} target object to transform
|
|
*/
|
|
resetObjectTransform: function (target) {
|
|
target.scaleX = 1;
|
|
target.scaleY = 1;
|
|
target.skewX = 0;
|
|
target.skewY = 0;
|
|
target.flipX = false;
|
|
target.flipY = false;
|
|
target.rotate(0);
|
|
},
|
|
|
|
/**
|
|
* Extract Object transform values
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {fabric.Object} target object to read from
|
|
* @return {Object} Components of transform
|
|
*/
|
|
saveObjectTransform: function (target) {
|
|
return {
|
|
scaleX: target.scaleX,
|
|
scaleY: target.scaleY,
|
|
skewX: target.skewX,
|
|
skewY: target.skewY,
|
|
angle: target.angle,
|
|
left: target.left,
|
|
flipX: target.flipX,
|
|
flipY: target.flipY,
|
|
top: target.top
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Returns string representation of function body
|
|
* @param {Function} fn Function to get body of
|
|
* @return {String} Function body
|
|
*/
|
|
getFunctionBody: function(fn) {
|
|
return (String(fn).match(/function[^{]*\{([\s\S]*)\}/) || {})[1];
|
|
},
|
|
|
|
/**
|
|
* Returns true if context has transparent pixel
|
|
* at specified location (taking tolerance into account)
|
|
* @param {CanvasRenderingContext2D} ctx context
|
|
* @param {Number} x x coordinate
|
|
* @param {Number} y y coordinate
|
|
* @param {Number} tolerance Tolerance
|
|
*/
|
|
isTransparent: function(ctx, x, y, tolerance) {
|
|
|
|
// If tolerance is > 0 adjust start coords to take into account.
|
|
// If moves off Canvas fix to 0
|
|
if (tolerance > 0) {
|
|
if (x > tolerance) {
|
|
x -= tolerance;
|
|
}
|
|
else {
|
|
x = 0;
|
|
}
|
|
if (y > tolerance) {
|
|
y -= tolerance;
|
|
}
|
|
else {
|
|
y = 0;
|
|
}
|
|
}
|
|
|
|
var _isTransparent = true, i, temp,
|
|
imageData = ctx.getImageData(x, y, (tolerance * 2) || 1, (tolerance * 2) || 1),
|
|
l = imageData.data.length;
|
|
|
|
// Split image data - for tolerance > 1, pixelDataSize = 4;
|
|
for (i = 3; i < l; i += 4) {
|
|
temp = imageData.data[i];
|
|
_isTransparent = temp <= 0;
|
|
if (_isTransparent === false) {
|
|
break; // Stop if colour found
|
|
}
|
|
}
|
|
|
|
imageData = null;
|
|
|
|
return _isTransparent;
|
|
},
|
|
|
|
/**
|
|
* Parse preserveAspectRatio attribute from element
|
|
* @param {string} attribute to be parsed
|
|
* @return {Object} an object containing align and meetOrSlice attribute
|
|
*/
|
|
parsePreserveAspectRatioAttribute: function(attribute) {
|
|
var meetOrSlice = 'meet', alignX = 'Mid', alignY = 'Mid',
|
|
aspectRatioAttrs = attribute.split(' '), align;
|
|
|
|
if (aspectRatioAttrs && aspectRatioAttrs.length) {
|
|
meetOrSlice = aspectRatioAttrs.pop();
|
|
if (meetOrSlice !== 'meet' && meetOrSlice !== 'slice') {
|
|
align = meetOrSlice;
|
|
meetOrSlice = 'meet';
|
|
}
|
|
else if (aspectRatioAttrs.length) {
|
|
align = aspectRatioAttrs.pop();
|
|
}
|
|
}
|
|
//divide align in alignX and alignY
|
|
alignX = align !== 'none' ? align.slice(1, 4) : 'none';
|
|
alignY = align !== 'none' ? align.slice(5, 8) : 'none';
|
|
return {
|
|
meetOrSlice: meetOrSlice,
|
|
alignX: alignX,
|
|
alignY: alignY
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Clear char widths cache for the given font family or all the cache if no
|
|
* fontFamily is specified.
|
|
* Use it if you know you are loading fonts in a lazy way and you are not waiting
|
|
* for custom fonts to load properly when adding text objects to the canvas.
|
|
* If a text object is added when its own font is not loaded yet, you will get wrong
|
|
* measurement and so wrong bounding boxes.
|
|
* After the font cache is cleared, either change the textObject text content or call
|
|
* initDimensions() to trigger a recalculation
|
|
* @memberOf fabric.util
|
|
* @param {String} [fontFamily] font family to clear
|
|
*/
|
|
clearFabricFontCache: function(fontFamily) {
|
|
fontFamily = (fontFamily || '').toLowerCase();
|
|
if (!fontFamily) {
|
|
fabric.charWidthsCache = { };
|
|
}
|
|
else if (fabric.charWidthsCache[fontFamily]) {
|
|
delete fabric.charWidthsCache[fontFamily];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Given current aspect ratio, determines the max width and height that can
|
|
* respect the total allowed area for the cache.
|
|
* @memberOf fabric.util
|
|
* @param {Number} ar aspect ratio
|
|
* @param {Number} maximumArea Maximum area you want to achieve
|
|
* @return {Object.x} Limited dimensions by X
|
|
* @return {Object.y} Limited dimensions by Y
|
|
*/
|
|
limitDimsByArea: function(ar, maximumArea) {
|
|
var roughWidth = Math.sqrt(maximumArea * ar),
|
|
perfLimitSizeY = Math.floor(maximumArea / roughWidth);
|
|
return { x: Math.floor(roughWidth), y: perfLimitSizeY };
|
|
},
|
|
|
|
capValue: function(min, value, max) {
|
|
return Math.max(min, Math.min(value, max));
|
|
},
|
|
|
|
findScaleToFit: function(source, destination) {
|
|
return Math.min(destination.width / source.width, destination.height / source.height);
|
|
},
|
|
|
|
findScaleToCover: function(source, destination) {
|
|
return Math.max(destination.width / source.width, destination.height / source.height);
|
|
}
|
|
};
|
|
})(typeof exports !== 'undefined' ? exports : this);
|