Clip path parsing (#4786)

* first pass done

* restarted-clippaths

* some changes to element parser

* shared attribute

* done one piece

* cleaned

* mmm going there

* so far so good

* a very first draft

* removed dist

* sovled conflict

* now solved

* now solved

* some improvements

* toObject and fromObject added

* toObject and fromObject added

* more small changes

* added simple tests

* bumpedup qunit

* a test for svg export

* no ist

* more svg exporpt

* fix lint

* make possible to clip canvas

* improved JSOCS

* no builds

* invalidate cache anyway

* changes

* changes

* changes

* mmm working
This commit is contained in:
Andrea Bogazzi 2018-08-23 01:06:07 +02:00 committed by GitHub
parent e7eca140b8
commit 2a476e4277
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 417 additions and 149 deletions

View file

@ -50,13 +50,13 @@ jobs:
packages: # avoid installing packages
- stage: Unit Tests
env: LAUNCHER=Chrome
install: npm install testem@1.18.4 qunit@2.4.1
install: npm install testem@1.18.4 qunit@2.6.1
addons:
apt:
packages: # avoid installing packages
- stage: Unit Tests
env: LAUNCHER=Firefox
install: npm install testem@1.18.4 qunit@2.4.1
install: npm install testem@1.18.4 qunit@2.6.1
addons:
apt:
packages: # avoid installing packages

View file

@ -47,15 +47,15 @@ fabric.isLikelyNode = typeof Buffer !== 'undefined' &&
* @type array
*/
fabric.SHARED_ATTRIBUTES = [
"display",
"transform",
"fill", "fill-opacity", "fill-rule",
"opacity",
"stroke", "stroke-dasharray", "stroke-linecap",
"stroke-linejoin", "stroke-miterlimit",
"stroke-opacity", "stroke-width",
"id", "paint-order",
"instantiated_by_use"
'display',
'transform',
'fill', 'fill-opacity', 'fill-rule',
'opacity',
'stroke', 'stroke-dasharray', 'stroke-linecap',
'stroke-linejoin', 'stroke-miterlimit',
'stroke-opacity', 'stroke-width',
'id', 'paint-order',
'instantiated_by_use', 'clip-path'
];
/* _FROM_SVG_END_ */

View file

@ -67,7 +67,7 @@
"eslint": "4.18.x",
"istanbul": "0.4.x",
"onchange": "^3.x.x",
"qunit": "^2.4.1",
"qunit": "^2.6.1",
"testem": "^1.18.4",
"uglify-js": "3.3.x",
"pixelmatch": "^4.0.2"

View file

@ -8,79 +8,122 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
this.regexUrl = /^url\(['"]?#([^'"]+)['"]?\)/g;
};
fabric.ElementsParser.prototype.parse = function() {
this.instances = new Array(this.elements.length);
this.numElements = this.elements.length;
this.createObjects();
};
fabric.ElementsParser.prototype.createObjects = function() {
for (var i = 0, len = this.elements.length; i < len; i++) {
this.elements[i].setAttribute('svgUid', this.svgUid);
(function(_obj, i) {
setTimeout(function() {
_obj.createObject(_obj.elements[i], i);
}, 0);
})(this, i);
}
};
fabric.ElementsParser.prototype.createObject = function(el, index) {
var klass = fabric[fabric.util.string.capitalize(el.tagName.replace('svg:', ''))];
if (klass && klass.fromElement) {
try {
this._createObject(klass, el, index);
}
catch (err) {
fabric.log(err);
}
}
else {
this.checkIfDone();
}
};
fabric.ElementsParser.prototype._createObject = function(klass, el, index) {
klass.fromElement(el, this.createCallback(index, el), this.options);
};
fabric.ElementsParser.prototype.createCallback = function(index, el) {
var _this = this;
return function(obj) {
var _options;
_this.resolveGradient(obj, 'fill');
_this.resolveGradient(obj, 'stroke');
if (obj instanceof fabric.Image) {
_options = obj.parsePreserveAspectRatioAttribute(el);
}
obj._removeTransformMatrix(_options);
_this.reviver && _this.reviver(el, obj);
_this.instances[index] = obj;
_this.checkIfDone();
(function(proto) {
proto.parse = function() {
this.instances = new Array(this.elements.length);
this.numElements = this.elements.length;
this.createObjects();
};
};
fabric.ElementsParser.prototype.resolveGradient = function(obj, property) {
var instanceFillValue = obj[property];
if (!(/^url\(/).test(instanceFillValue)) {
return;
}
var gradientId = this.regexUrl.exec(instanceFillValue)[1];
this.regexUrl.lastIndex = 0;
if (fabric.gradientDefs[this.svgUid][gradientId]) {
obj.set(property,
fabric.Gradient.fromElement(fabric.gradientDefs[this.svgUid][gradientId], obj));
}
};
fabric.ElementsParser.prototype.checkIfDone = function() {
if (--this.numElements === 0) {
this.instances = this.instances.filter(function(el) {
// eslint-disable-next-line no-eq-null, eqeqeq
return el != null;
proto.createObjects = function() {
var _this = this;
this.elements.forEach(function(element, i) {
element.setAttribute('svgUid', _this.svgUid);
_this.createObject(element, i);
});
this.callback(this.instances, this.elements);
}
};
};
proto.findTag = function(el) {
return fabric[fabric.util.string.capitalize(el.tagName.replace('svg:', ''))];
};
proto.createObject = function(el, index) {
var klass = this.findTag(el);
if (klass && klass.fromElement) {
try {
klass.fromElement(el, this.createCallback(index, el), this.options);
}
catch (err) {
fabric.log(err);
}
}
else {
this.checkIfDone();
}
};
proto.createCallback = function(index, el) {
var _this = this;
return function(obj) {
var _options;
_this.resolveGradient(obj, 'fill');
_this.resolveGradient(obj, 'stroke');
if (obj instanceof fabric.Image) {
_options = obj.parsePreserveAspectRatioAttribute(el);
}
obj._removeTransformMatrix(_options);
_this.resolveClipPath(obj);
_this.reviver && _this.reviver(el, obj);
_this.instances[index] = obj;
_this.checkIfDone();
};
};
proto.extractPropertyDefinition = function(obj, property, storage) {
var value = obj[property];
if (!(/^url\(/).test(value)) {
return;
}
var id = this.regexUrl.exec(value)[1];
this.regexUrl.lastIndex = 0;
return fabric[storage][this.svgUid][id];
};
proto.resolveGradient = function(obj, property) {
var gradientDef = this.extractPropertyDefinition(obj, property, 'gradientDefs');
if (gradientDef) {
obj.set(property, fabric.Gradient.fromElement(gradientDef, obj));
}
};
proto.createClipPathCallback = function(obj, container) {
return function(_newObj) {
_newObj._removeTransformMatrix();
_newObj.fillRule = _newObj.clipRule;
container.push(_newObj);
};
};
proto.resolveClipPath = function(obj) {
var clipPath = this.extractPropertyDefinition(obj, 'clipPath', 'clipPaths'),
element, klass, objTransformInv, container, gTransform, options;
if (clipPath) {
container = [];
objTransformInv = fabric.util.invertTransform(obj.calcTransformMatrix());
for (var i = 0; i < clipPath.length; i++) {
element = clipPath[i];
klass = this.findTag(element);
klass.fromElement(
element,
this.createClipPathCallback(obj, container),
this.options
);
}
clipPath = new fabric.Group(container);
gTransform = fabric.util.multiplyTransformMatrices(
objTransformInv,
clipPath.calcTransformMatrix()
);
var options = fabric.util.qrDecompose(gTransform);
clipPath.flipX = false;
clipPath.flipY = false;
clipPath.set('scaleX', options.scaleX);
clipPath.set('scaleY', options.scaleY);
clipPath.angle = options.angle;
clipPath.skewX = options.skewX;
clipPath.skewY = 0;
clipPath.setPositionByOrigin({ x: options.translateX, y: options.translateY }, 'center', 'center');
obj.clipPath = clipPath;
}
};
proto.checkIfDone = function() {
if (--this.numElements === 0) {
this.instances = this.instances.filter(function(el) {
// eslint-disable-next-line no-eq-null, eqeqeq
return el != null;
});
this.callback(this.instances, this.elements);
}
};
})(fabric.ElementsParser.prototype);

View file

@ -38,7 +38,7 @@
style = filter === '' ? '' : ' style="' + filter + '"',
textDecoration = this.getSvgTextDecoration(this);
markup.push(
'\t<g ', this.getSvgId(), 'transform="', this.getSvgTransform(), this.getSvgTransformMatrix(), '"',
'\t<g ', this.getSvgCommons(), 'transform="', this.getSvgTransform(), this.getSvgTransformMatrix(), '"',
style, '>\n',
textAndBg.textBgRects.join(''),
'\t\t<text xml:space="preserve" ',

View file

@ -119,8 +119,11 @@
* Returns id attribute for svg output
* @return {String}
*/
getSvgId: function() {
return this.id ? 'id="' + this.id + '" ' : '';
getSvgCommons: function() {
return [
this.id ? 'id="' + this.id + '" ' : '',
this.clipPath ? 'clip-path="url(#' + this.clipPath.clipPathId + ')" ' : '',
].join('');
},
/**
@ -196,7 +199,7 @@
* @private
*/
_createBaseSVGMarkup: function() {
var markup = [];
var markup = [], clipPath = this.clipPath;
if (this.fill && this.fill.toLive) {
markup.push(this.fill.toSVG(this, false));
@ -207,6 +210,18 @@
if (this.shadow) {
markup.push(this.shadow.toSVG(this));
}
if (clipPath) {
if (clipPath.clipPathId === undefined) {
clipPath.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++;
}
markup.push(
'<clipPath id="' + clipPath.clipPathId + '" ',
'clipPathUnits="objectBoundingBox" ',
'transform="translate(' + (this.width / 2) + ' , ' + (this.height / 2) + ')" >\n\t',
this.clipPath.toSVG(),
'</clipPath>\n'
);
}
return markup;
},

View file

@ -15,10 +15,10 @@
multiplyTransformMatrices = fabric.util.multiplyTransformMatrices,
svgValidTagNames = ['path', 'circle', 'polygon', 'polyline', 'ellipse', 'rect', 'line',
'image', 'text', 'linearGradient', 'radialGradient', 'stop'],
'image', 'text'],
svgViewBoxElements = ['symbol', 'image', 'marker', 'pattern', 'view', 'svg'],
svgInvalidAncestors = ['pattern', 'defs', 'symbol', 'metadata', 'clipPath', 'mask', 'desc'],
svgValidParents = ['symbol', 'g', 'a', 'svg'],
svgValidParents = ['symbol', 'g', 'a', 'svg', 'clipPath', 'defs'],
attributesMap = {
cx: 'left',
@ -45,7 +45,9 @@
'stroke-width': 'strokeWidth',
'text-decoration': 'textDecoration',
'text-anchor': 'textAnchor',
opacity: 'opacity'
opacity: 'opacity',
'clip-path': 'clipPath',
'clip-rule': 'clipRule',
},
colorAttributes = {
@ -60,6 +62,7 @@
fabric.cssRules = { };
fabric.gradientDefs = { };
fabric.clipPaths = { };
function normalizeAttr(attr) {
// transform attribute names
@ -617,7 +620,7 @@
scaleY + ' ' +
(minX * scaleX + widthDiff) + ' ' +
(minY * scaleY + heightDiff) + ') ';
parsedDim.viewboxTransform = fabric.parseTransformAttribute(matrix);
if (element.nodeName === 'svg') {
el = element.ownerDocument.createElement('g');
// element.firstChild != null
@ -630,7 +633,6 @@
el = element;
matrix = el.getAttribute('transform') + matrix;
}
el.setAttribute('transform', matrix);
return parsedDim;
}
@ -691,13 +693,24 @@
callback && callback([], {});
return;
}
var clipPaths = { };
descendants.filter(function(el) {
return el.nodeName.replace('svg:', '') === 'clipPath';
}).forEach(function(el) {
clipPaths[el.id] = fabric.util.toArray(el.getElementsByTagName('*')).filter(function(el) {
return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', ''));
});
});
fabric.gradientDefs[svgUid] = fabric.getGradientDefs(doc);
fabric.cssRules[svgUid] = fabric.getCSSRules(doc);
fabric.clipPaths[svgUid] = clipPaths;
// Precedence of rules: style > class > attribute
fabric.parseElements(elements, function(instances, elements) {
if (callback) {
callback(instances, options, elements, descendants);
delete fabric.gradientDefs[svgUid];
delete fabric.cssRules[svgUid];
delete fabric.clipPaths[svgUid];
}
}, clone(options), reviver, parsingOptions);
};

View file

@ -89,7 +89,7 @@
if (angle === 0) {
markup.push(
'<circle ', this.getSvgId(),
'<circle ', this.getSvgCommons(),
'cx="' + x + '" cy="' + y + '" ',
'r="', this.radius,
'" style="', this.getSvgStyles(),

View file

@ -111,7 +111,7 @@
toSVG: function(reviver) {
var markup = this._createBaseSVGMarkup();
markup.push(
'<ellipse ', this.getSvgId(),
'<ellipse ', this.getSvgCommons(),
'cx="0" cy="0" ',
'rx="', this.rx,
'" ry="', this.ry,

View file

@ -330,13 +330,14 @@
for (var i = 0, len = this._objects.length; i < len; i++) {
this._objects[i].render(ctx);
}
this._drawClipPath(ctx);
},
/**
* Check if cache is dirty
*/
isCacheDirty: function() {
if (this.callSuper('isCacheDirty')) {
isCacheDirty: function(skipCanvas) {
if (this.callSuper('isCacheDirty', skipCanvas)) {
return true;
}
if (!this.statefullCache) {
@ -520,7 +521,7 @@
toSVG: function(reviver) {
var markup = this._createBaseSVGMarkup();
markup.push(
'<g ', this.getSvgId(), 'transform="',
'<g ', this.getSvgCommons(), 'transform="',
/* avoiding styles intentionally */
this.getSvgTransform(),
this.getSvgTransformMatrix(),

View file

@ -91,15 +91,6 @@
*/
stateProperties: fabric.Object.prototype.stateProperties.concat('cropX', 'cropY'),
/**
* When `true`, object is cached on an additional canvas.
* default to false for images
* since 1.7.0
* @type Boolean
* @default
*/
objectCaching: false,
/**
* key used to retrieve the texture representing this image
* since 2.0.0
@ -311,7 +302,7 @@
clipPath = ' clip-path="url(#imageCrop_' + clipPathId + ')" ';
}
markup.push('<g transform="', this.getSvgTransform(), this.getSvgTransformMatrix(), '">\n');
var imageMarkup = ['\t<image ', this.getSvgId(), 'xlink:href="', this.getSvgSrc(true),
var imageMarkup = ['\t<image ', this.getSvgCommons(), 'xlink:href="', this.getSvgSrc(true),
'" x="', x - this.cropX, '" y="', y - this.cropY,
'" style="', this.getSvgStyles(),
// we're essentially moving origin of transformation from top/left corner to the center of the shape
@ -435,9 +426,7 @@
filters = filters || this.filters || [];
filters = filters.filter(function(filter) { return filter && !filter.isNeutralState(); });
if (this.group) {
this.set('dirty', true);
}
this.set('dirty', true);
// needs to clear out or WEBGL will not resize correctly
this.removeTexture(this.cacheKey + '_filtered');

View file

@ -255,7 +255,7 @@
var markup = this._createBaseSVGMarkup(),
p = this.calcLinePoints();
markup.push(
'<line ', this.getSvgId(),
'<line ', this.getSvgCommons(),
'x1="', p.x1,
'" y1="', p.y1,
'" x2="', p.x2,

View file

@ -603,6 +603,15 @@
' strokeLineCap strokeLineJoin strokeMiterLimit backgroundColor'
).split(' '),
/**
* a fabricObject that, without stroke define a clipping area with their shape. filled in black
* the clipPath object gets used when the object has rendered, and the context is placed in the center
* of the object cacheCanvas.
* If you want 0,0 of a clipPath to align with an object center, use clipPath.originX/Y to 'center'
* @type fabric.Object
*/
clipPath: undefined,
/**
* Constructor
* @param {Object} [options] Options object
@ -675,8 +684,8 @@
* Return the dimension and the zoom level needed to create a cache canvas
* big enough to host the object to be cached.
* @private
* @param {Object} dim.x width of object to be cached
* @param {Object} dim.y height of object to be cached
* @return {Object}.x width of object to be cached
* @return {Object}.y height of object to be cached
* @return {Object}.width width of canvas
* @return {Object}.height height of canvas
* @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache
@ -708,9 +717,10 @@
* @return {Boolean} true if the canvas has been resized
*/
_updateCacheCanvas: function() {
if (this.noScaleCache && this.canvas && this.canvas._currentTransform) {
var target = this.canvas._currentTransform.target,
action = this.canvas._currentTransform.action;
var targetCanvas = this.canvas;
if (this.noScaleCache && targetCanvas && targetCanvas._currentTransform) {
var target = targetCanvas._currentTransform.target,
action = targetCanvas._currentTransform.action;
if (this === target && action.slice && action.slice(0, 5) === 'scale') {
return false;
}
@ -827,9 +837,13 @@
globalCompositeOperation: this.globalCompositeOperation,
transformMatrix: this.transformMatrix ? this.transformMatrix.concat() : null,
skewX: toFixed(this.skewX, NUM_FRACTION_DIGITS),
skewY: toFixed(this.skewY, NUM_FRACTION_DIGITS)
skewY: toFixed(this.skewY, NUM_FRACTION_DIGITS),
};
if (this.clipPath) {
object.clipPath = this.clipPath.toObject(propertiesToInclude);
}
fabric.util.populateWithProperties(this, object, propertiesToInclude);
if (!this.includeDefaultValues) {
object = this._removeDefaultValues(object);
@ -1020,15 +1034,7 @@
}
this.clipTo && fabric.util.clipContext(this, ctx);
if (this.shouldCache()) {
if (!this._cacheCanvas) {
this._createCacheCanvas();
}
if (this.isCacheDirty()) {
this.statefullCache && this.saveState({ propertySet: 'cacheProperties' });
this.drawObject(this._cacheContext);
this.dirty = false;
}
this.renderCache();
this.drawCacheOnCanvas(ctx);
}
else {
@ -1043,6 +1049,18 @@
ctx.restore();
},
renderCache: function(options) {
options = options || {};
if (!this._cacheCanvas) {
this._createCacheCanvas();
}
if (this.isCacheDirty(false)) {
this.statefullCache && this.saveState({ propertySet: 'cacheProperties' });
this.drawObject(this._cacheContext, options.forClipping);
this.dirty = false;
}
},
/**
* Remove cacheCanvas and its dimensions from the objects
*/
@ -1064,6 +1082,9 @@
if (this.paintFirst === 'stroke' && typeof this.shadow === 'object') {
return true;
}
if (this.clipPath) {
return true;
}
return false;
},
@ -1090,15 +1111,52 @@
return !!this.shadow && (this.shadow.offsetX !== 0 || this.shadow.offsetY !== 0);
},
/**
* Execute the drawing operation for an object clipPath
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
drawClipPathOnCache: function(ctx) {
var path = this.clipPath;
ctx.save();
// DEBUG: uncomment this line, comment the following
// ctx.globalAlpha = 0.4
ctx.globalCompositeOperation = 'destination-in';
//ctx.scale(1 / 2, 1 / 2);
path.transform(ctx);
ctx.scale(1 / path.zoomX, 1 / path.zoomY);
ctx.drawImage(path._cacheCanvas, -path.cacheTranslationX, -path.cacheTranslationY);
ctx.restore();
},
/**
* Execute the drawing operation for an object on a specified context
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
drawObject: function(ctx) {
this._renderBackground(ctx);
this._setStrokeStyles(ctx, this);
this._setFillStyles(ctx, this);
drawObject: function(ctx, forClipping) {
if (forClipping) {
this._setClippingProperties(ctx);
}
else {
this._renderBackground(ctx);
this._setStrokeStyles(ctx, this);
this._setFillStyles(ctx, this);
}
this._render(ctx);
this._drawClipPath(ctx);
},
_drawClipPath: function(ctx) {
var path = this.clipPath;
if (!path) { return; }
// needed to setup a couple of variables
// path canvas gets overridden with this one.
// TODO find a better solution?
path.canvas = this.canvas;
path.shouldCache();
path._transformDone = true;
path.renderCache({ forClipping: true });
this.drawClipPathOnCache(ctx);
},
/**
@ -1192,6 +1250,12 @@
}
},
_setClippingProperties: function(ctx) {
ctx.globalAlpha = 1;
ctx.lineWidth = 0;
ctx.fillStyle = 'black';
},
/**
* @private
* Sets line dash
@ -1803,8 +1867,11 @@
if (typeof patterns[1] !== 'undefined') {
object.stroke = patterns[1];
}
var instance = extraParam ? new klass(object[extraParam], object) : new klass(object);
callback && callback(instance);
fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) {
object.clipPath = enlivedProps[0];
var instance = extraParam ? new klass(object[extraParam], object) : new klass(object);
callback && callback(instance);
});
});
};

View file

@ -479,7 +479,7 @@
var path = chunks.join(' ');
addTransform = ' translate(' + (-this.pathOffset.x) + ', ' + (-this.pathOffset.y) + ') ';
markup.push(
'<path ', this.getSvgId(),
'<path ', this.getSvgCommons(),
'd="', path,
'" style="', this.getSvgStyles(),
'" transform="', this.getSvgTransform(), addTransform,

View file

@ -132,7 +132,7 @@
);
}
markup.push(
'<', this.type, ' ', this.getSvgId(),
'<', this.type, ' ', this.getSvgCommons(),
'points="', points.join(''),
'" style="', this.getSvgStyles(),
'" transform="', this.getSvgTransform(),

View file

@ -150,7 +150,7 @@
toSVG: function(reviver) {
var markup = this._createBaseSVGMarkup(), x = -this.width / 2, y = -this.height / 2;
markup.push(
'<rect ', this.getSvgId(),
'<rect ', this.getSvgCommons(),
'x="', x, '" y="', y,
'" rx="', this.get('rx'), '" ry="', this.get('ry'),
'" width="', this.width, '" height="', this.height,

View file

@ -89,7 +89,7 @@
.join(',');
markup.push(
'<polygon ', this.getSvgId(),
'<polygon ', this.getSvgCommons(),
'points="', points,
'" style="', this.getSvgStyles(),
'" transform="', this.getSvgTransform(), '"',

View file

@ -205,6 +205,15 @@
*/
skipOffscreen: true,
/**
* a fabricObject that, without stroke define a clipping area with their shape. filled in black
* the clipPath object gets used when the canvas has rendered, and the context is placed in the
* top left corner of the canvas.
* clipPath will clip away controls, if you do not want this to happen use controlsAboveOverlay = true
* @type fabric.Object
*/
clipPath: undefined,
/**
* @private
* @param {HTMLElement | String} el &lt;canvas> element to initialize instance on
@ -900,11 +909,11 @@
* @chainable
*/
renderCanvas: function(ctx, objects) {
var v = this.viewportTransform;
var v = this.viewportTransform, path = this.clipPath;
this.cancelRequestedRender();
this.calcViewportBoundaries();
this.clearContext(ctx);
this.fire('before:render');
this.fire('before:render', { ctx: ctx, });
if (this.clipTo) {
fabric.util.clipContext(this, ctx);
}
@ -921,11 +930,38 @@
if (this.clipTo) {
ctx.restore();
}
if (path) {
if (path.isCacheDirty()) {
// needed to setup a couple of variables
path.shouldCache();
path.canvas = this;
path._transformDone = true;
path.renderCache({ forClipping: true });
}
this.drawClipPathOnCanvas(ctx);
}
this._renderOverlay(ctx);
if (this.controlsAboveOverlay && this.interactive) {
this.drawControls(ctx);
}
this.fire('after:render');
this.fire('after:render', { ctx: ctx, });
},
/**
* Paint the cached clipPath on the lowerCanvasEl
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
drawClipPathOnCanvas: function(ctx) {
var v = this.viewportTransform, path = this.clipPath;
ctx.save();
ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);
// DEBUG: uncomment this line, comment the following
// ctx.globalAlpha = 0.4
ctx.globalCompositeOperation = 'destination-in';
path.transform(ctx);
ctx.scale(1 / path.zoomX, 1 / path.zoomY);
ctx.drawImage(path._cacheCanvas, -path.cacheTranslationX, -path.cacheTranslationY);
ctx.restore();
},
/**
@ -1122,11 +1158,13 @@
*/
_toObjectMethod: function (methodName, propertiesToInclude) {
var data = {
var clipPath = this.clipPath, data = {
version: fabric.version,
objects: this._toObjects(methodName, propertiesToInclude)
objects: this._toObjects(methodName, propertiesToInclude),
};
if (clipPath) {
clipPath = clipPath.toObject(propertiesToInclude);
}
extend(data, this.__serializeBgOverlay(methodName, propertiesToInclude));
fabric.util.populateWithProperties(this, data, propertiesToInclude);

View file

@ -581,6 +581,20 @@
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.document.createElement('canvas');
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

View file

@ -675,13 +675,13 @@
});
});
QUnit.test('apply filters do not set the image dirty if not in group', function(assert) {
QUnit.test('apply filters set the image dirty', function(assert) {
var done = assert.async();
createImageObject(function(image) {
image.dirty = false;
assert.equal(image.dirty, false, 'false apply filter dirty is false');
image.applyFilters();
assert.equal(image.dirty, false, 'After apply filter dirty is true');
assert.equal(image.dirty, true, 'After apply filter dirty is true');
done();
});
});

View file

@ -0,0 +1,88 @@
(function(){
// var canvas = this.canvas = new fabric.StaticCanvas(null, {enableRetinaScaling: false});
QUnit.module('fabric.Object - clipPath', {
afterEach: function() {
// canvas.clear();
// canvas.calcOffset();
}
});
QUnit.test('constructor & properties', function(assert) {
var cObj = new fabric.Object();
assert.equal(cObj.clipPath, undefined, 'clipPath should not be defined out of the box');
});
QUnit.test('toObject with clipPath', function(assert) {
var emptyObjectRepr = {
'version': fabric.version,
'type': 'object',
'originX': 'left',
'originY': 'top',
'left': 0,
'top': 0,
'width': 0,
'height': 0,
'fill': 'rgb(0,0,0)',
'stroke': null,
'strokeWidth': 1,
'strokeDashArray': null,
'strokeLineCap': 'butt',
'strokeLineJoin': 'miter',
'strokeMiterLimit': 4,
'scaleX': 1,
'scaleY': 1,
'angle': 0,
'flipX': false,
'flipY': false,
'opacity': 1,
'shadow': null,
'visible': true,
'backgroundColor': '',
'clipTo': null,
'fillRule': 'nonzero',
'paintFirst': 'fill',
'globalCompositeOperation': 'source-over',
'skewX': 0,
'skewY': 0,
'transformMatrix': null
};
var cObj = new fabric.Object();
assert.deepEqual(emptyObjectRepr, cObj.toObject());
cObj.clipPath = new fabric.Object();
var expected = fabric.util.object.clone(emptyObjectRepr);
expected.clipPath = emptyObjectRepr;
assert.deepEqual(expected, cObj.toObject());
});
QUnit.test('from object with clipPath', function(assert) {
var done = assert.async();
var rect = new fabric.Rect({ width: 100, height: 100 });
rect.clipPath = new fabric.Circle({ radius: 50 });
var toObject = rect.toObject();
fabric.Rect.fromObject(toObject, function(rect) {
assert.ok(rect.clipPath instanceof fabric.Circle, 'clipPath is enlived');
assert.equal(rect.clipPath.radius, 50, 'radius is restored correctly');
done();
});
});
QUnit.test('from object with clipPath, nested', function(assert) {
var done = assert.async();
var rect = new fabric.Rect({ width: 100, height: 100 });
rect.clipPath = new fabric.Circle({ radius: 50 });
rect.clipPath.clipPath = new fabric.Text('clipPath');
var toObject = rect.toObject();
fabric.Rect.fromObject(toObject, function(rect) {
assert.ok(rect.clipPath instanceof fabric.Circle, 'clipPath is enlived');
assert.equal(rect.clipPath.radius, 50, 'radius is restored correctly');
assert.ok(rect.clipPath.clipPath instanceof fabric.Text, 'neted clipPath is enlived');
assert.equal(rect.clipPath.clipPath.text, 'clipPath', 'instance is restored correctly');
done();
});
});
})();

View file

@ -33,7 +33,7 @@
'rx': 0,
'ry': 0,
'skewX': 0,
'skewY': 0,
'skewY': 0
};
QUnit.module('fabric.Rect');

View file

@ -51,7 +51,7 @@
transformMatrix: null,
charSpacing: 0,
styles: { },
minWidth: 20,
minWidth: 20
};
QUnit.test('constructor', function(assert) {