mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-04-12 18:31:00 +00:00
708 lines
No EOL
18 KiB
JavaScript
708 lines
No EOL
18 KiB
JavaScript
//= require "object.class"
|
|
|
|
(function(global) {
|
|
|
|
var commandLengths = {
|
|
m: 2,
|
|
l: 2,
|
|
h: 1,
|
|
v: 1,
|
|
c: 6,
|
|
s: 4,
|
|
q: 4,
|
|
t: 2,
|
|
a: 7
|
|
};
|
|
|
|
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;
|
|
|
|
// Copied from Inkscape svgtopdf, thanks!
|
|
function arcToSegments(x, y, rx, ry, large, sweep, rotateX, ox, oy) {
|
|
argsString = _join.call(arguments);
|
|
if (arcToSegmentsCache[argsString]) {
|
|
return arcToSegmentsCache[argsString];
|
|
}
|
|
|
|
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;
|
|
var x0 = a00 * ox + a01 * oy;
|
|
var y0 = a10 * ox + a11 * oy;
|
|
var x1 = a00 * x + a01 * y;
|
|
var y1 = a10 * x + a11 * y;
|
|
|
|
var d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-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;
|
|
var xc = 0.5 * (x0 + x1) - sfactor * (y1-y0);
|
|
var yc = 0.5 * (y0 + y1) + sfactor * (x1-x0);
|
|
|
|
var th0 = Math.atan2(y0-yc, x0-xc);
|
|
var th1 = Math.atan2(y1-yc, x1-xc);
|
|
|
|
var th_arc = th1-th0;
|
|
if (th_arc < 0 && sweep == 1){
|
|
th_arc += 2*Math.PI;
|
|
} 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;
|
|
result[i] = [xc, yc, th2, th3, rx, ry, sin_th, cos_th];
|
|
}
|
|
|
|
return (arcToSegmentsCache[argsString] = result);
|
|
}
|
|
|
|
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);
|
|
|
|
return (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
|
|
]);
|
|
}
|
|
|
|
"use strict";
|
|
|
|
var fabric = global.fabric || (global.fabric = { }),
|
|
min = fabric.util.array.min,
|
|
max = fabric.util.array.max,
|
|
extend = fabric.util.object.extend,
|
|
_toString = Object.prototype.toString;
|
|
|
|
if (fabric.Path) {
|
|
fabric.warn('fabric.Path is already defined');
|
|
return;
|
|
}
|
|
if (!fabric.Object) {
|
|
fabric.warn('fabric.Path requires fabric.Object');
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
function getX(item) {
|
|
if (item[0] === 'H') {
|
|
return item[1];
|
|
}
|
|
return item[item.length - 2];
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
function getY(item) {
|
|
if (item[0] === 'V') {
|
|
return item[1];
|
|
}
|
|
return item[item.length - 1];
|
|
}
|
|
|
|
/**
|
|
* @class Path
|
|
* @extends fabric.Object
|
|
*/
|
|
fabric.Path = fabric.util.createClass(fabric.Object, /** @scope fabric.Path.prototype */ {
|
|
|
|
/**
|
|
* @property
|
|
* @type String
|
|
*/
|
|
type: 'path',
|
|
|
|
/**
|
|
* Constructor
|
|
* @method initialize
|
|
* @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens)
|
|
* @param {Object} [options] Options object
|
|
*/
|
|
initialize: function(path, options) {
|
|
options = options || { };
|
|
|
|
this.setOptions(options);
|
|
|
|
if (!path) {
|
|
throw Error('`path` argument is required');
|
|
}
|
|
|
|
var fromArray = _toString.call(path) === '[object Array]';
|
|
|
|
this.path = fromArray
|
|
? path
|
|
: path.match && path.match(/[a-zA-Z][^a-zA-Z]*/g);
|
|
|
|
if (!this.path) return;
|
|
|
|
// TODO (kangax): rewrite this idiocracy
|
|
if (!fromArray) {
|
|
this._initializeFromArray(options);
|
|
}
|
|
|
|
if (options.sourcePath) {
|
|
this.setSourcePath(options.sourcePath);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _initializeFromArray
|
|
*/
|
|
_initializeFromArray: function(options) {
|
|
var isWidthSet = 'width' in options,
|
|
isHeightSet = 'height' in options;
|
|
|
|
this.path = this._parsePath();
|
|
|
|
if (!isWidthSet || !isHeightSet) {
|
|
extend(this, this._parseDimensions());
|
|
if (isWidthSet) {
|
|
this.width = options.width;
|
|
}
|
|
if (isHeightSet) {
|
|
this.height = options.height;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _render
|
|
*/
|
|
_render: function(ctx) {
|
|
var current, // current instruction
|
|
x = 0, // current x
|
|
y = 0, // current y
|
|
controlX = 0, // current control point x
|
|
controlY = 0, // current control point y
|
|
tempX,
|
|
tempY,
|
|
l = -(this.width / 2),
|
|
t = -(this.height / 2);
|
|
|
|
for (var i = 0, len = this.path.length; i < len; ++i) {
|
|
|
|
current = this.path[i];
|
|
|
|
switch (current[0]) { // first letter
|
|
|
|
case 'l': // lineto, relative
|
|
x += current[1];
|
|
y += current[2];
|
|
ctx.lineTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'L': // lineto, absolute
|
|
x = current[1];
|
|
y = current[2];
|
|
ctx.lineTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'h': // horizontal lineto, relative
|
|
x += current[1];
|
|
ctx.lineTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'H': // horizontal lineto, absolute
|
|
x = current[1];
|
|
ctx.lineTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'v': // vertical lineto, relative
|
|
y += current[1];
|
|
ctx.lineTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'V': // verical lineto, absolute
|
|
y = current[1];
|
|
ctx.lineTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'm': // moveTo, relative
|
|
x += current[1];
|
|
y += current[2];
|
|
ctx.moveTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'M': // moveTo, absolute
|
|
x = current[1];
|
|
y = current[2];
|
|
ctx.moveTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'c': // bezierCurveTo, relative
|
|
tempX = x + current[5];
|
|
tempY = y + current[6];
|
|
controlX = x + current[3];
|
|
controlY = y + current[4];
|
|
ctx.bezierCurveTo(
|
|
x + current[1] + l, // x1
|
|
y + current[2] + t, // y1
|
|
controlX + l, // x2
|
|
controlY + t, // y2
|
|
tempX + l,
|
|
tempY + t
|
|
);
|
|
x = tempX;
|
|
y = tempY;
|
|
break;
|
|
|
|
case 'C': // bezierCurveTo, absolute
|
|
x = current[5];
|
|
y = current[6];
|
|
controlX = current[3];
|
|
controlY = current[4];
|
|
ctx.bezierCurveTo(
|
|
current[1] + l,
|
|
current[2] + t,
|
|
controlX + l,
|
|
controlY + t,
|
|
x + l,
|
|
y + t
|
|
);
|
|
break;
|
|
|
|
case 's': // shorthand cubic bezierCurveTo, relative
|
|
// transform to absolute x,y
|
|
tempX = x + current[3];
|
|
tempY = y + current[4];
|
|
// calculate reflection of previous control points
|
|
controlX = 2 * x - controlX;
|
|
controlY = 2 * y - controlY;
|
|
ctx.bezierCurveTo(
|
|
controlX + l,
|
|
controlY + t,
|
|
x + current[1] + l,
|
|
y + current[2] + t,
|
|
tempX + l,
|
|
tempY + t
|
|
);
|
|
// set control point to 2nd one of this command
|
|
// "... the first control point is assumed to be the reflection of the second control point on the previous command relative to the current point."
|
|
controlX = x + current[1];
|
|
controlY = y + current[2];
|
|
|
|
x = tempX;
|
|
y = tempY;
|
|
break;
|
|
|
|
case 'S': // shorthand cubic bezierCurveTo, absolute
|
|
tempX = current[3];
|
|
tempY = current[4];
|
|
// calculate reflection of previous control points
|
|
controlX = 2*x - controlX;
|
|
controlY = 2*y - controlY;
|
|
ctx.bezierCurveTo(
|
|
controlX + l,
|
|
controlY + t,
|
|
current[1] + l,
|
|
current[2] + t,
|
|
tempX + l,
|
|
tempY + t
|
|
);
|
|
x = tempX;
|
|
y = tempY;
|
|
|
|
// set control point to 2nd one of this command
|
|
// "... the first control point is assumed to be the reflection of the second control point on the previous command relative to the current point."
|
|
controlX = current[1];
|
|
controlY = current[2];
|
|
|
|
break;
|
|
|
|
case 'q': // quadraticCurveTo, relative
|
|
x += current[3];
|
|
y += current[4];
|
|
ctx.quadraticCurveTo(
|
|
current[1] + l,
|
|
current[2] + t,
|
|
x + l,
|
|
y + t
|
|
);
|
|
break;
|
|
|
|
case 'Q': // quadraticCurveTo, absolute
|
|
x = current[3];
|
|
y = current[4];
|
|
controlX = current[1];
|
|
controlY = current[2];
|
|
ctx.quadraticCurveTo(
|
|
controlX + l,
|
|
controlY + t,
|
|
x + l,
|
|
y + t
|
|
);
|
|
break;
|
|
|
|
case 'T':
|
|
tempX = x;
|
|
tempY = y;
|
|
x = current[1];
|
|
y = current[2];
|
|
// calculate reflection of previous control points
|
|
controlX = -controlX + 2 * tempX;
|
|
controlY = -controlY + 2 * tempY;
|
|
ctx.quadraticCurveTo(
|
|
controlX + l,
|
|
controlY + t,
|
|
x + l,
|
|
y + t
|
|
);
|
|
break;
|
|
|
|
case 'a':
|
|
// TODO: optimize this
|
|
drawArc(ctx, x + l, y + t, [
|
|
current[1],
|
|
current[2],
|
|
current[3],
|
|
current[4],
|
|
current[5],
|
|
current[6] + x + l,
|
|
current[7] + y + t
|
|
]);
|
|
x += current[6];
|
|
y += current[7];
|
|
break;
|
|
|
|
case 'A':
|
|
// TODO: optimize this
|
|
drawArc(ctx, x + l, y + t, [
|
|
current[1],
|
|
current[2],
|
|
current[3],
|
|
current[4],
|
|
current[5],
|
|
current[6] + l,
|
|
current[7] + t
|
|
]);
|
|
x = current[6];
|
|
y = current[7];
|
|
break;
|
|
|
|
case 'z':
|
|
case 'Z':
|
|
ctx.closePath();
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Renders path on a specified context
|
|
* @method render
|
|
* @param {CanvasRenderingContext2D} ctx context to render path on
|
|
* @param {Boolean} noTransform When true, context is not transformed
|
|
*/
|
|
render: function(ctx, noTransform) {
|
|
ctx.save();
|
|
var m = this.transformMatrix;
|
|
if (m) {
|
|
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
|
|
}
|
|
if (!noTransform) {
|
|
this.transform(ctx);
|
|
}
|
|
// ctx.globalCompositeOperation = this.fillRule;
|
|
|
|
if (this.overlayFill) {
|
|
ctx.fillStyle = this.overlayFill;
|
|
}
|
|
else if (this.fill) {
|
|
ctx.fillStyle = this.fill;
|
|
}
|
|
|
|
if (this.stroke) {
|
|
ctx.strokeStyle = this.stroke;
|
|
}
|
|
ctx.beginPath();
|
|
|
|
this._render(ctx);
|
|
|
|
if (this.fill) {
|
|
ctx.fill();
|
|
}
|
|
if (this.stroke) {
|
|
ctx.strokeStyle = this.stroke;
|
|
ctx.lineWidth = this.strokeWidth;
|
|
ctx.lineCap = ctx.lineJoin = 'round';
|
|
ctx.stroke();
|
|
}
|
|
if (!noTransform && this.active) {
|
|
this.drawBorders(ctx);
|
|
this.hideCorners || this.drawCorners(ctx);
|
|
}
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* Returns string representation of an instance
|
|
* @method toString
|
|
* @return {String} string representation of an instance
|
|
*/
|
|
toString: function() {
|
|
return '#<fabric.Path (' + this.complexity() +
|
|
'): { "top": ' + this.top + ', "left": ' + this.left + ' }>';
|
|
},
|
|
|
|
/**
|
|
* Returns object representation of an instance
|
|
* @method toObject
|
|
* @return {Object}
|
|
*/
|
|
toObject: function() {
|
|
var o = extend(this.callSuper('toObject'), {
|
|
path: this.path
|
|
});
|
|
if (this.sourcePath) {
|
|
o.sourcePath = this.sourcePath;
|
|
}
|
|
if (this.transformMatrix) {
|
|
o.transformMatrix = this.transformMatrix;
|
|
}
|
|
return o;
|
|
},
|
|
|
|
/**
|
|
* Returns dataless object representation of an instance
|
|
* @method toDatalessObject
|
|
* @return {Object}
|
|
*/
|
|
toDatalessObject: function() {
|
|
var o = this.toObject();
|
|
if (this.sourcePath) {
|
|
o.path = this.sourcePath;
|
|
}
|
|
delete o.sourcePath;
|
|
return o;
|
|
},
|
|
|
|
/**
|
|
* Returns svg representation of an instance
|
|
* @method toSVG
|
|
* @return {string} svg representation of an instance
|
|
*/
|
|
toSVG: function() {
|
|
var chunks = [];
|
|
for (var i = 0, len = this.path.length; i < len; i++) {
|
|
chunks.push(this.path[i].join(' '));
|
|
}
|
|
var path = chunks.join(' ');
|
|
|
|
return [
|
|
'<g transform="', this.getSvgTransform(), '">',
|
|
'<path ',
|
|
'width="', this.width, '" height="', this.height, '" ',
|
|
'd="', path, '" ',
|
|
'style="', this.getSvgStyles(), '" ',
|
|
'transform="translate(', (-this.width / 2), ' ', (-this.height/2), ')" />',
|
|
'</g>'
|
|
].join('');
|
|
},
|
|
|
|
/**
|
|
* Returns number representation of an instance complexity
|
|
* @method complexity
|
|
* @return {Number} complexity
|
|
*/
|
|
complexity: function() {
|
|
return this.path.length;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _parsePath
|
|
*/
|
|
_parsePath: function() {
|
|
var result = [ ],
|
|
currentPath,
|
|
chunks,
|
|
parsed;
|
|
|
|
for (var i = 0, j, chunksParsed, len = this.path.length; i < len; i++) {
|
|
currentPath = this.path[i];
|
|
chunks = currentPath.slice(1).trim().replace(/(\d)-/g, '$1###-').split(/\s|,|###/);
|
|
chunksParsed = [ currentPath.charAt(0) ];
|
|
|
|
for (var j = 0, jlen = chunks.length; j < jlen; j++) {
|
|
parsed = parseFloat(chunks[j]);
|
|
if (!isNaN(parsed)) {
|
|
chunksParsed.push(parsed);
|
|
}
|
|
}
|
|
|
|
var command = chunksParsed[0].toLowerCase(),
|
|
commandLength = commandLengths[command];
|
|
|
|
if (chunksParsed.length - 1 > commandLength) {
|
|
for (var k = 1, klen = chunksParsed.length; k < klen; k += commandLength) {
|
|
result.push([ chunksParsed[0] ].concat(chunksParsed.slice(k, k + commandLength)));
|
|
}
|
|
}
|
|
else {
|
|
result.push(chunksParsed);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* @method _parseDimensions
|
|
*/
|
|
_parseDimensions: function() {
|
|
var aX = [],
|
|
aY = [],
|
|
previousX,
|
|
previousY,
|
|
isLowerCase = false,
|
|
x,
|
|
y;
|
|
|
|
this.path.forEach(function(item, i) {
|
|
if (item[0] !== 'H') {
|
|
previousX = (i === 0) ? getX(item) : getX(this.path[i-1]);
|
|
}
|
|
if (item[0] !== 'V') {
|
|
previousY = (i === 0) ? getY(item) : getY(this.path[i-1]);
|
|
}
|
|
|
|
// lowercased letter denotes relative position;
|
|
// transform to absolute
|
|
if (item[0] === item[0].toLowerCase()) {
|
|
isLowerCase = true;
|
|
}
|
|
|
|
// last 2 items in an array of coordinates are the actualy x/y (except H/V);
|
|
// collect them
|
|
|
|
// TODO (kangax): support relative h/v commands
|
|
|
|
x = isLowerCase
|
|
? previousX + getX(item)
|
|
: item[0] === 'V'
|
|
? previousX
|
|
: getX(item);
|
|
|
|
y = isLowerCase
|
|
? previousY + getY(item)
|
|
: item[0] === 'H'
|
|
? previousY
|
|
: getY(item);
|
|
|
|
var val = parseInt(x, 10);
|
|
if (!isNaN(val)) aX.push(val);
|
|
|
|
val = parseInt(y, 10);
|
|
if (!isNaN(val)) aY.push(val);
|
|
|
|
}, this);
|
|
|
|
var minX = min(aX),
|
|
minY = min(aY),
|
|
deltaX = 0,
|
|
deltaY = 0;
|
|
|
|
var o = {
|
|
top: minY - deltaY,
|
|
left: minX - deltaX,
|
|
bottom: max(aY) - deltaY,
|
|
right: max(aX) - deltaX
|
|
};
|
|
|
|
o.width = o.right - o.left;
|
|
o.height = o.bottom - o.top;
|
|
|
|
return o;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Creates an instance of fabric.Path from an object
|
|
* @static
|
|
* @method fabric.Path.fromObject
|
|
* @return {fabric.Path} Instance of fabric.Path
|
|
*/
|
|
fabric.Path.fromObject = function(object) {
|
|
return new fabric.Path(object.path, object);
|
|
};
|
|
|
|
/**
|
|
* List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`)
|
|
* @static
|
|
* @see http://www.w3.org/TR/SVG/paths.html#PathElement
|
|
*/
|
|
fabric.Path.ATTRIBUTE_NAMES = 'd fill fill-opacity opacity fill-rule stroke stroke-width transform'.split(' ');
|
|
|
|
/**
|
|
* Creates an instance of fabric.Path from an SVG <path> element
|
|
* @static
|
|
* @method fabric.Path.fromElement
|
|
* @param {SVGElement} element to parse
|
|
* @param {Object} options object
|
|
* @return {fabric.Path} Instance of fabric.Path
|
|
*/
|
|
fabric.Path.fromElement = function(element, options) {
|
|
var parsedAttributes = fabric.parseAttributes(element, fabric.Path.ATTRIBUTE_NAMES);
|
|
return new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options));
|
|
};
|
|
|
|
})(typeof exports != 'undefined' ? exports : this); |