mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-05-11 07:13:09 +00:00
605 lines
18 KiB
JavaScript
605 lines
18 KiB
JavaScript
(function(global) {
|
|
|
|
var sqrt = Math.sqrt,
|
|
atan2 = Math.atan2,
|
|
atan = Math.atan,
|
|
pow = Math.pow,
|
|
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) {
|
|
point.subtractEquals(origin);
|
|
var sin = Math.sin(radians),
|
|
cos = Math.cos(radians),
|
|
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[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 = Math.abs(minX - maxX),
|
|
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 = Math.abs(minY - maxY);
|
|
|
|
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
|
|
* @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 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 {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);
|
|
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;
|
|
|
|
object = new fabric.PathGroup(elements, options);
|
|
|
|
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'));
|
|
//jscs:disable requireCamelCaseOrUpperCaseIdentifiers
|
|
if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') {
|
|
G_vmlCanvasManager.initElement(canvasEl);
|
|
}
|
|
//jscs:enable requireCamelCaseOrUpperCaseIdentifiers
|
|
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} 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]
|
|
];
|
|
},
|
|
|
|
/**
|
|
* Decompone standard 2x2 matrix into transform componentes
|
|
* @static
|
|
* @memberOf fabric.util
|
|
* @param {Array} a transformMatrix
|
|
* @return {Object} Components of transform
|
|
*/
|
|
qrDecompone: function(a) {
|
|
var angle = atan(a[0] / a[1]),
|
|
denom = pow(a[0]) + pow(a[1]),
|
|
scaleX = sqrt(denom),
|
|
scaleY = (a[0] * a[3] - a[2] * a [1]) / scaleX,
|
|
skewX = atan((a[0] * a[2] + a[1] * a [3]) / denom);
|
|
return {
|
|
angle: angle / PiBy180,
|
|
scaleX: scaleX,
|
|
scaleY: scaleY,
|
|
skewX: skewX / PiBy180,
|
|
skewY: 0
|
|
};
|
|
},
|
|
|
|
/**
|
|
* 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,
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
};
|
|
}
|
|
};
|
|
|
|
})(typeof exports !== 'undefined' ? exports : this);
|