mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-03-16 22:10:32 +00:00
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:
parent
e7eca140b8
commit
2a476e4277
23 changed files with 417 additions and 149 deletions
|
|
@ -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
|
||||
|
|
|
|||
18
HEADER.js
18
HEADER.js
|
|
@ -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_ */
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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" ',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@
|
|||
);
|
||||
}
|
||||
markup.push(
|
||||
'<', this.type, ' ', this.getSvgId(),
|
||||
'<', this.type, ' ', this.getSvgCommons(),
|
||||
'points="', points.join(''),
|
||||
'" style="', this.getSvgStyles(),
|
||||
'" transform="', this.getSvgTransform(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
.join(',');
|
||||
|
||||
markup.push(
|
||||
'<polygon ', this.getSvgId(),
|
||||
'<polygon ', this.getSvgCommons(),
|
||||
'points="', points,
|
||||
'" style="', this.getSvgStyles(),
|
||||
'" transform="', this.getSvgTransform(), '"',
|
||||
|
|
|
|||
|
|
@ -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 <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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
88
test/unit/object_clipPath.js
Normal file
88
test/unit/object_clipPath.js
Normal 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();
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
'rx': 0,
|
||||
'ry': 0,
|
||||
'skewX': 0,
|
||||
'skewY': 0,
|
||||
'skewY': 0
|
||||
};
|
||||
|
||||
QUnit.module('fabric.Rect');
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
transformMatrix: null,
|
||||
charSpacing: 0,
|
||||
styles: { },
|
||||
minWidth: 20,
|
||||
minWidth: 20
|
||||
};
|
||||
|
||||
QUnit.test('constructor', function(assert) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue