fabric.js/src/util/misc.js

587 lines
17 KiB
JavaScript
Raw Normal View History

(function(global) {
var sqrt = Math.sqrt,
atan2 = Math.atan2;
/**
* @namespace fabric.util
*/
fabric.util = { };
2010-06-09 22:34:55 +00:00
/**
* Removes value from an array.
2010-06-09 22:34:55 +00:00
* Presence of value (and its position in an array) is determined via `Array.prototype.indexOf`
* @static
* @memberOf fabric.util
2010-06-09 22:34:55 +00:00
* @param {Array} array
* @param {Any} value
* @return {Array} original array
*/
function removeFromArray(array, value) {
var idx = array.indexOf(value);
if (idx !== -1) {
array.splice(idx, 1);
}
return array;
}
2010-06-09 22:34:55 +00:00
/**
* Returns random number between 2 specified ones.
2010-06-09 22:34:55 +00:00
* @static
* @memberOf fabric.util
2010-06-09 22:34:55 +00:00
* @param {Number} min lower limit
* @param {Number} max upper limit
* @return {Number} random value (between min and max)
*/
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var PiBy180 = Math.PI / 180;
2010-06-09 22:34:55 +00:00
/**
* Transforms degrees to radians.
* @static
* @memberOf fabric.util
2010-06-09 22:34:55 +00:00
* @param {Number} degrees value in degrees
* @return {Number} value in radians
*/
function degreesToRadians(degrees) {
return degrees * PiBy180;
}
/**
* Transforms radians to degrees.
* @static
* @memberOf fabric.util
* @param {Number} radians value in radians
* @return {Number} value in degrees
*/
function radiansToDegrees(radians) {
return radians / PiBy180;
}
/**
* Rotates `point` around `origin` with `radians`
* @static
* @memberOf fabric.util
* @param {fabric.Point} The point to rotate
* @param {fabric.Point} The origin of the rotation
* @param {Number} The radians of the angle for the rotation
* @return {fabric.Point} The new rotated point
*/
function rotatePoint(point, origin, radians) {
var sin = Math.sin(radians),
cos = Math.cos(radians);
point.subtractEquals(origin);
var rx = point.x * cos - point.y * sin;
var ry = point.x * sin + point.y * cos;
return new fabric.Point(rx, ry).addEquals(origin);
}
2010-06-09 22:34:55 +00:00
/**
* A wrapper around Number#toFixed, which contrary to native method returns number, not string.
* @static
* @memberOf fabric.util
2010-06-09 22:34:55 +00:00
* @param {Number | String} number number to operate on
* @param {Number} fractionDigits number of fraction digits to "leave"
* @return {Number}
*/
function toFixed(number, fractionDigits) {
return parseFloat(Number(number).toFixed(fractionDigits));
}
/**
* Function which always returns `false`.
* @static
* @memberOf fabric.util
* @return {Boolean}
*/
function falseFunction() {
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"
*/
function getKlass(type, namespace) {
// capitalize first letter only
type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1));
return 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)
*/
function resolveNamespace(namespace) {
2013-08-17 18:18:06 +00:00
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
*/
function loadImage(url, callback, context, crossOrigin) {
if (url) {
var img = fabric.util.createImage();
/** @ignore */
img.onload = function () {
callback && callback.call(context, img);
2013-10-27 12:12:48 +00:00
img = img.onload = img.onerror = null;
};
/** @ignore */
img.onerror = function() {
fabric.log('Error loading ' + img.src);
callback && callback.call(context, null, true);
2013-10-27 12:12:48 +00:00
img = img.onload = img.onerror = null;
};
2013-11-06 20:16:44 +00:00
// 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) {
img.crossOrigin = crossOrigin || '';
}
img.src = url;
}
else {
callback && callback.call(context, url);
}
}
2012-12-18 10:46:51 +00:00
/**
* 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} [reviver] Method for further parsing of object elements, called after each fabric object created.
2012-12-18 10:46:51 +00:00
*/
function enlivenObjects(objects, callback, namespace, reviver) {
objects = objects || [ ];
2012-09-02 14:39:07 +00:00
function onLoaded() {
if (++numLoadedObjects === numTotalObjects) {
callback && callback(enlivenedObjects);
2012-09-02 14:39:07 +00:00
}
}
2012-09-02 14:39:07 +00:00
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) {
2013-09-24 16:34:32 +00:00
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]);
}
2012-09-02 14:39:07 +00:00
onLoaded();
});
}
else {
enlivenedObjects[index] = klass.fromObject(o);
reviver && reviver(o, enlivenedObjects[index]);
2012-09-02 14:39:07 +00:00
onLoaded();
}
});
}
/**
* Groups SVG elements (usually those retrieved from SVG document)
* @static
* @memberOf fabric.util
* @param {Array} elements SVG elements to group
2012-12-13 14:36:43 +00:00
* @param {Object} [options] Options object
* @return {fabric.Object|fabric.PathGroup}
*/
function groupSVGElements(elements, options, path) {
var object;
if (elements.length > 1) {
2013-06-22 17:26:32 +00:00
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
*/
function populateWithProperties(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 ctx {Canvas} context
* @param x {Number} start x coordinate
* @param y {Number} start y coordinate
* @param x2 {Number} end x coordinate
* @param y2 {Number} end y coordinate
* @param da {Array} dash array pattern
*/
function drawDashedLine(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
*/
function createCanvasElement(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
*/
function createImage() {
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
*/
function createAccessors(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
*/
function clipContext(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
*/
function multiplyTransformMatrices(matrixA, matrixB) {
// Matrix multiply matrixA * matrixB
var a = [
[matrixA[0], matrixA[2], matrixA[4]],
[matrixA[1], matrixA[3], matrixA[5]],
[0 , 0 , 1 ]
];
var b = [
[matrixB[0], matrixB[2], matrixB[4]],
[matrixB[1], matrixB[3], matrixB[5]],
[0 , 0 , 1 ]
];
var 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]
];
}
function getFunctionBody(fn) {
return (String(fn).match(/function[^{]*\{([\s\S]*)\}/) || {})[1];
}
function drawArc(ctx, x, y, coords) {
var rx = coords[0];
var ry = coords[1];
var rot = coords[2];
var large = coords[3];
var sweep = coords[4];
var ex = coords[5];
var ey = coords[6];
var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y);
for (var i=0; i<segs.length; i++) {
var bez = segmentToBezier.apply(this, segs[i]);
ctx.bezierCurveTo.apply(ctx, bez);
}
}
var arcToSegmentsCache = { },
segmentToBezierCache = { },
_join = Array.prototype.join,
argsString;
// Generous contribution by Raph Levien, from libsvg-0.1.0.tar.gz
function arcToSegments(x, y, rx, ry, large, sweep, rotateX, ox, oy) {
2013-11-10 16:13:01 +00:00
argsString = _join.call(arguments);
2013-11-10 16:13:01 +00:00
if (arcToSegmentsCache[argsString]) {
return arcToSegmentsCache[argsString];
}
2013-11-10 16:13:01 +00:00
var coords = getXYCoords(rotateX, rx, ry, ox, oy, x, y);
2013-11-10 16:13:01 +00:00
var d = (coords.x1-coords.x0) * (coords.x1-coords.x0) + (coords.y1-coords.y0) * (coords.y1-coords.y0);
var sfactor_sq = 1 / d - 0.25;
if (sfactor_sq < 0) sfactor_sq = 0;
var sfactor = Math.sqrt(sfactor_sq);
if (sweep === large) sfactor = -sfactor;
2013-11-10 16:13:01 +00:00
var xc = 0.5 * (coords.x0 + coords.x1) - sfactor * (coords.y1-coords.y0);
var yc = 0.5 * (coords.y0 + coords.y1) + sfactor * (coords.x1-coords.x0);
2013-11-10 16:13:01 +00:00
var th0 = Math.atan2(coords.y0-yc, coords.x0-xc);
var th1 = Math.atan2(coords.y1-yc, coords.x1-xc);
var th_arc = th1-th0;
2013-11-10 16:13:01 +00:00
if (th_arc < 0 && sweep === 1) {
th_arc += 2*Math.PI;
2013-11-10 16:13:01 +00:00
}
else if (th_arc > 0 && sweep === 0) {
th_arc -= 2 * Math.PI;
}
var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001)));
var result = [];
for (var i=0; i<segments; i++) {
var th2 = th0 + i * th_arc / segments;
var th3 = th0 + (i+1) * th_arc / segments;
2013-11-10 16:13:01 +00:00
result[i] = [xc, yc, th2, th3, rx, ry, coords.sin_th, coords.cos_th];
}
arcToSegmentsCache[argsString] = result;
return result;
}
2013-11-10 16:13:01 +00:00
function getXYCoords(rotateX, rx, ry, ox, oy, x, y) {
var th = rotateX * (Math.PI/180);
var sin_th = Math.sin(th);
var cos_th = Math.cos(th);
rx = Math.abs(rx);
ry = Math.abs(ry);
var px = cos_th * (ox - x) * 0.5 + sin_th * (oy - y) * 0.5;
var py = cos_th * (oy - y) * 0.5 - sin_th * (ox - x) * 0.5;
var pl = (px*px) / (rx*rx) + (py*py) / (ry*ry);
if (pl > 1) {
pl = Math.sqrt(pl);
rx *= pl;
ry *= pl;
}
var a00 = cos_th / rx;
var a01 = sin_th / rx;
var a10 = (-sin_th) / ry;
var a11 = (cos_th) / ry;
return {
x0: a00 * ox + a01 * oy,
y0: a10 * ox + a11 * oy,
x1: a00 * x + a01 * y,
y1: a10 * x + a11 * y,
sin_th: sin_th,
cos_th: cos_th
};
}
function segmentToBezier(cx, cy, th0, th1, rx, ry, sin_th, cos_th) {
argsString = _join.call(arguments);
if (segmentToBezierCache[argsString]) {
return segmentToBezierCache[argsString];
}
var a00 = cos_th * rx;
var a01 = -sin_th * ry;
var a10 = sin_th * rx;
var a11 = cos_th * ry;
var th_half = 0.5 * (th1 - th0);
var t = (8/3) * Math.sin(th_half * 0.5) * Math.sin(th_half * 0.5) / Math.sin(th_half);
var x1 = cx + Math.cos(th0) - t * Math.sin(th0);
var y1 = cy + Math.sin(th0) + t * Math.cos(th0);
var x3 = cx + Math.cos(th1);
var y3 = cy + Math.sin(th1);
var x2 = x3 + t * Math.sin(th1);
var y2 = y3 - t * Math.cos(th1);
segmentToBezierCache[argsString] = [
a00 * x1 + a01 * y1, a10 * x1 + a11 * y1,
a00 * x2 + a01 * y2, a10 * x2 + a11 * y2,
a00 * x3 + a01 * y3, a10 * x3 + a11 * y3
];
return segmentToBezierCache[argsString];
}
function normalizePoints(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;
}
}
fabric.util.removeFromArray = removeFromArray;
fabric.util.degreesToRadians = degreesToRadians;
fabric.util.radiansToDegrees = radiansToDegrees;
fabric.util.rotatePoint = rotatePoint;
fabric.util.toFixed = toFixed;
fabric.util.getRandomInt = getRandomInt;
fabric.util.falseFunction = falseFunction;
fabric.util.getKlass = getKlass;
fabric.util.resolveNamespace = resolveNamespace;
fabric.util.loadImage = loadImage;
fabric.util.enlivenObjects = enlivenObjects;
fabric.util.groupSVGElements = groupSVGElements;
fabric.util.populateWithProperties = populateWithProperties;
fabric.util.drawDashedLine = drawDashedLine;
fabric.util.createCanvasElement = createCanvasElement;
fabric.util.createImage = createImage;
fabric.util.createAccessors = createAccessors;
fabric.util.clipContext = clipContext;
fabric.util.multiplyTransformMatrices = multiplyTransformMatrices;
fabric.util.getFunctionBody = getFunctionBody;
fabric.util.drawArc = drawArc;
fabric.util.normalizePoints = normalizePoints;
})(typeof exports !== 'undefined' ? exports : this);