mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-04-08 08:21:00 +00:00
572 lines
16 KiB
JavaScript
572 lines
16 KiB
JavaScript
(function(global) {
|
|
|
|
var sqrt = Math.sqrt,
|
|
atan2 = Math.atan2,
|
|
PiBy180 = Math.PI / 180;
|
|
|
|
/**
|
|
* @namespace fabric.util
|
|
*/
|
|
fabric.util = {
|
|
|
|
/**
|
|
* 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 {Any} 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) {
|
|
var sin = Math.sin(radians),
|
|
cos = Math.cos(radians);
|
|
|
|
point.subtractEquals(origin);
|
|
|
|
var rx = point.x * cos - point.y * sin,
|
|
ry = point.x * sin + point.y * cos;
|
|
|
|
return new fabric.Point(rx, ry).addEquals(origin);
|
|
},
|
|
|
|
/**
|
|
* 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[1] * p.y,
|
|
t[2] * p.x + t[3] * p.y
|
|
);
|
|
}
|
|
return new fabric.Point(
|
|
t[0] * p.x + t[1] * p.y + t[4],
|
|
t[2] * p.x + t[3] * p.y + t[5]
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Invert transformation t
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Array} t The transform
|
|
* @return {Array} The inverted transform
|
|
*/
|
|
invertTransform: function(t) {
|
|
var r = t.slice(),
|
|
a = 1 / (t[0] * t[3] - t[1] * t[2]);
|
|
r = [a * t[3], -a * t[1], -a * t[2], a * t[0], 0, 0];
|
|
var o = fabric.util.transformPoint({ x: t[4], y: t[5] }, r);
|
|
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} number to operate on
|
|
* @return {Number | String}
|
|
*/
|
|
parseUnit: function(value) {
|
|
var unit = /\D{0,2}$/.exec(value),
|
|
number = parseFloat(value);
|
|
switch (unit[0]) {
|
|
case 'mm':
|
|
return number * fabric.DPI / 25.4;
|
|
break;
|
|
case 'cm':
|
|
return number * fabric.DPI / 2.54;
|
|
break;
|
|
case 'in':
|
|
return number * fabric.DPI;
|
|
break;
|
|
case 'pt':
|
|
return number * fabric.DPI / 72; // or * 4 / 3
|
|
break;
|
|
case 'pc':
|
|
return number * fabric.DPI / 72 * 12; // or * 16
|
|
break;
|
|
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 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,
|
|
obj = global || fabric.window;
|
|
|
|
for (var 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 {Any} [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 */
|
|
img.onload = function () {
|
|
callback && callback.call(context, img);
|
|
img = img.onload = img.onerror = null;
|
|
};
|
|
|
|
/** @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 && typeof crossOrigin !== 'undefined') {
|
|
img.crossOrigin = crossOrigin;
|
|
}
|
|
|
|
img.src = url;
|
|
},
|
|
|
|
/**
|
|
* 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 {Function} 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);
|
|
if (klass.async) {
|
|
klass.fromObject(o, function (obj, error) {
|
|
if (!error) {
|
|
enlivenedObjects[index] = obj;
|
|
reviver && reviver(o, enlivenedObjects[index]);
|
|
}
|
|
onLoaded();
|
|
});
|
|
}
|
|
else {
|
|
enlivenedObjects[index] = klass.fromObject(o);
|
|
reviver && reviver(o, enlivenedObjects[index]);
|
|
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
|
|
* @return {fabric.Object|fabric.PathGroup}
|
|
*/
|
|
groupSVGElements: function(elements, options, path) {
|
|
var object;
|
|
|
|
if (elements.length > 1) {
|
|
object = new fabric.PathGroup(elements, options);
|
|
}
|
|
else {
|
|
object = elements[0];
|
|
}
|
|
|
|
if (typeof path !== 'undefined') {
|
|
object.setSourcePath(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 Propertie 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 and initializes it via excanvas if necessary
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {CanvasElement} [canvasEl] optional canvas element to initialize;
|
|
* when not given, element is created implicitly
|
|
* @return {CanvasElement} initialized canvas element
|
|
*/
|
|
createCanvasElement: function(canvasEl) {
|
|
canvasEl || (canvasEl = fabric.document.createElement('canvas'));
|
|
if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') {
|
|
G_vmlCanvasManager.initElement(canvasEl);
|
|
}
|
|
return canvasEl;
|
|
},
|
|
|
|
/**
|
|
* Creates image element (works on client and node)
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @return {HTMLImageElement} HTML image element
|
|
*/
|
|
createImage: function() {
|
|
return fabric.isLikelyNode
|
|
? new (require('canvas').Image)()
|
|
: fabric.document.createElement('img');
|
|
},
|
|
|
|
/**
|
|
* Creates accessors (getXXX, setXXX) for a "class", based on "stateProperties" array
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Object} klass "Class" to create accessors for
|
|
*/
|
|
createAccessors: function(klass) {
|
|
var proto = klass.prototype;
|
|
|
|
for (var i = proto.stateProperties.length; i--; ) {
|
|
|
|
var propName = proto.stateProperties[i],
|
|
capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1),
|
|
setterName = 'set' + capitalizedPropName,
|
|
getterName = 'get' + capitalizedPropName;
|
|
|
|
// using `new Function` for better introspection
|
|
if (!proto[getterName]) {
|
|
proto[getterName] = (function(property) {
|
|
return new Function('return this.get("' + property + '")');
|
|
})(propName);
|
|
}
|
|
if (!proto[setterName]) {
|
|
proto[setterName] = (function(property) {
|
|
return new Function('value', 'return this.set("' + property + '", value)');
|
|
})(propName);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @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} matrixA First transformMatrix
|
|
* @param {Array} matrixB Second transformMatrix
|
|
* @return {Array} The product of the two transform matrices
|
|
*/
|
|
multiplyTransformMatrices: function(matrixA, matrixB) {
|
|
// Matrix multiply matrixA * matrixB
|
|
var a = [
|
|
[matrixA[0], matrixA[2], matrixA[4]],
|
|
[matrixA[1], matrixA[3], matrixA[5]],
|
|
[0, 0, 1 ]
|
|
],
|
|
|
|
b = [
|
|
[matrixB[0], matrixB[2], matrixB[4]],
|
|
[matrixB[1], matrixB[3], matrixB[5]],
|
|
[0, 0, 1 ]
|
|
],
|
|
|
|
result = [];
|
|
|
|
for (var r = 0; r < 3; r++) {
|
|
result[r] = [];
|
|
for (var c = 0; c < 3; c++) {
|
|
var sum = 0;
|
|
for (var k = 0; k < 3; k++) {
|
|
sum += a[r][k] * b[k][c];
|
|
}
|
|
|
|
result[r][c] = sum;
|
|
}
|
|
}
|
|
|
|
return [
|
|
result[0][0],
|
|
result[1][0],
|
|
result[0][1],
|
|
result[1][1],
|
|
result[0][2],
|
|
result[1][2]
|
|
];
|
|
},
|
|
|
|
/**
|
|
* 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];
|
|
},
|
|
|
|
/**
|
|
* Normalizes polygon/polyline points according to their dimensions
|
|
* @param {Array} points
|
|
* @param {Object} options
|
|
*/
|
|
normalizePoints: function(points, options) {
|
|
var minX = fabric.util.array.min(points, 'x'),
|
|
minY = fabric.util.array.min(points, 'y');
|
|
|
|
minX = minX < 0 ? minX : 0;
|
|
minY = minX < 0 ? minY : 0;
|
|
|
|
for (var i = 0, len = points.length; i < len; i++) {
|
|
// normalize coordinates, according to containing box
|
|
// (dimensions of which are passed via `options`)
|
|
points[i].x -= (options.width / 2 + minX) || 0;
|
|
points[i].y -= (options.height / 2 + minY) || 0;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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,
|
|
imageData = ctx.getImageData(x, y, (tolerance * 2) || 1, (tolerance * 2) || 1);
|
|
|
|
// Split image data - for tolerance > 1, pixelDataSize = 4;
|
|
for (var i = 3, l = imageData.data.length; i < l; i += 4) {
|
|
var temp = imageData.data[i];
|
|
_isTransparent = temp <= 0;
|
|
if (_isTransparent === false) break; // Stop if colour found
|
|
}
|
|
|
|
imageData = null;
|
|
|
|
return _isTransparent;
|
|
}
|
|
};
|
|
|
|
})(typeof exports !== 'undefined' ? exports : this);
|