Making clipPath absolute positionable (#5199)
Added visual tests Added property absolutePositioned and inverse
|
|
@ -6,7 +6,8 @@
|
|||
"globals": {
|
||||
"fabric": true,
|
||||
"QUnit": true,
|
||||
"assert": true
|
||||
"assert": true,
|
||||
"pixelmatch": true
|
||||
},
|
||||
"rules": {
|
||||
"eqeqeq": 0,
|
||||
|
|
|
|||
10
.travis.yml
|
|
@ -13,6 +13,7 @@ addons:
|
|||
- libpng-dev
|
||||
- libpango1.0-dev
|
||||
- libjpeg-dev
|
||||
- librsvg2-dev
|
||||
# libcairo2-dev is preinstalled
|
||||
stages:
|
||||
- Linting and Building
|
||||
|
|
@ -50,12 +51,14 @@ jobs:
|
|||
packages: # avoid installing packages
|
||||
- stage: Unit Tests
|
||||
env: LAUNCHER=Chrome
|
||||
script: npm run build:fast && testem ci --port 8080 -f testem.json -l $LAUNCHER
|
||||
install: npm install testem@1.18.4 qunit@2.6.1
|
||||
addons:
|
||||
apt:
|
||||
packages: # avoid installing packages
|
||||
- stage: Unit Tests
|
||||
env: LAUNCHER=Firefox
|
||||
script: npm run build:fast && testem ci --port 8080 -f testem.json -l $LAUNCHER
|
||||
install: npm install testem@1.18.4 qunit@2.6.1
|
||||
addons:
|
||||
apt:
|
||||
|
|
@ -72,17 +75,18 @@ jobs:
|
|||
- stage: Unit Tests
|
||||
node_js: "4"
|
||||
- stage: Visual Tests
|
||||
env: LAUNCHER=Node CANFAIL=TRUE
|
||||
node_js: "8"
|
||||
script: npm run build:fast && npm run test:visual
|
||||
- stage: Visual 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
|
||||
script: npm run build:fast && testem ci --port 8080 -f testem-visual.json -l $LAUNCHER
|
||||
- stage: Visual 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
|
||||
script: npm run build:fast && testem ci --port 8080 -f testem-visual.json -l $LAUNCHER
|
||||
|
||||
script: 'npm run build:fast && testem ci --port 8080 -f testem.json -l $LAUNCHER'
|
||||
script: npm run build:fast && npm run test
|
||||
|
||||
dist: trusty
|
||||
|
|
|
|||
10
package.json
|
|
@ -48,9 +48,10 @@
|
|||
"test:single": "./node_modules/qunit/bin/qunit test/node_test_setup.js test/lib",
|
||||
"test": "istanbul cover ./node_modules/qunit/bin/qunit test/node_test_setup.js test/lib test/unit",
|
||||
"test:visual": "./node_modules/qunit/bin/qunit test/node_test_setup.js test/lib test/visual",
|
||||
"test:visual:single": "./node_modules/qunit/bin/qunit test/node_test_setup.js test/lib",
|
||||
"test:all": "npm run test && npm run test:visual",
|
||||
"lint": "eslint --config .eslintrc.json src",
|
||||
"lint_tests": "eslint test/unit --config .eslintrc_tests",
|
||||
"lint_tests": "eslint test/unit --config .eslintrc_tests && eslint test/visual --config .eslintrc_tests",
|
||||
"export_dist_to_site": "cp dist/fabric.js ../fabricjs.com/lib/fabric.js && cp package.json ../fabricjs.com/lib/package.json && cp -r src HEADER.js lib ../fabricjs.com/build/files/",
|
||||
"export_tests_to_site": "cp test/unit/*.js ../fabricjs.com/test/unit && cp -r test/visual/* ../fabricjs.com/test/visual && cp -r test/fixtures/* ../fabricjs.com/test/fixtures",
|
||||
"all": "npm run build && npm run test && npm run test:visual && npm run lint && npm run lint_tests && npm run export_dist_to_site && npm run export_tests_to_site",
|
||||
|
|
@ -59,8 +60,8 @@
|
|||
"testem:ci": "testem ci"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvas": "1.6.x",
|
||||
"jsdom": "9.x.x",
|
||||
"canvas": "^1.6.12",
|
||||
"jsdom": "^9.12.0",
|
||||
"xmldom": "0.1.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -70,7 +71,8 @@
|
|||
"qunit": "^2.6.1",
|
||||
"testem": "^1.18.4",
|
||||
"uglify-js": "3.3.x",
|
||||
"pixelmatch": "^4.0.2"
|
||||
"pixelmatch": "^4.0.2",
|
||||
"chalk": "^2.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
|
|
|
|||
|
|
@ -612,6 +612,27 @@
|
|||
*/
|
||||
clipPath: undefined,
|
||||
|
||||
/**
|
||||
* Meaningfull ONLY when the object is used as clipPath.
|
||||
* if true, the clipPath will make the object clip to the outside of the clipPath
|
||||
* since 2.4.0
|
||||
* @type boolean
|
||||
* @default false
|
||||
*/
|
||||
inverted: false,
|
||||
|
||||
/**
|
||||
* Meaningfull ONLY when the object is used as clipPath.
|
||||
* if true, the clipPath will have its top and left relative to canvas, and will
|
||||
* not be influenced by the object transform. This will make the clipPath relative
|
||||
* to the canvas, but clipping just a particular object.
|
||||
* WARNING this is beta, this feature may change or be renamed.
|
||||
* since 2.4.0
|
||||
* @type boolean
|
||||
* @default false
|
||||
*/
|
||||
absolutePositioned: false,
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} [options] Options object
|
||||
|
|
@ -842,6 +863,8 @@
|
|||
|
||||
if (this.clipPath) {
|
||||
object.clipPath = this.clipPath.toObject(propertiesToInclude);
|
||||
object.clipPath.inverted = this.clipPath.inverted;
|
||||
object.clipPath.absolutePositioned = this.clipPath.absolutePositioned;
|
||||
}
|
||||
|
||||
fabric.util.populateWithProperties(this, object, propertiesToInclude);
|
||||
|
|
@ -1120,8 +1143,17 @@
|
|||
ctx.save();
|
||||
// DEBUG: uncomment this line, comment the following
|
||||
// ctx.globalAlpha = 0.4
|
||||
ctx.globalCompositeOperation = 'destination-in';
|
||||
if (path.inverted) {
|
||||
ctx.globalCompositeOperation = 'destination-out';
|
||||
}
|
||||
else {
|
||||
ctx.globalCompositeOperation = 'destination-in';
|
||||
}
|
||||
//ctx.scale(1 / 2, 1 / 2);
|
||||
if (path.absolutePositioned) {
|
||||
var m = fabric.util.invertTransform(this.calcTransformMatrix());
|
||||
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
|
||||
}
|
||||
path.transform(ctx);
|
||||
ctx.scale(1 / path.zoomX, 1 / path.zoomY);
|
||||
ctx.drawImage(path._cacheCanvas, -path.cacheTranslationX, -path.cacheTranslationY);
|
||||
|
|
@ -1182,7 +1214,10 @@
|
|||
return true;
|
||||
}
|
||||
else {
|
||||
if (this.dirty || (this.statefullCache && this.hasStateChanged('cacheProperties'))) {
|
||||
if (this.dirty ||
|
||||
(this.clipPath && this.clipPath.absolutePositioned) ||
|
||||
(this.statefullCache && this.hasStateChanged('cacheProperties'))
|
||||
) {
|
||||
if (this._cacheCanvas && !skipCanvas) {
|
||||
var width = this.cacheWidth / this.zoomX;
|
||||
var height = this.cacheHeight / this.zoomY;
|
||||
|
|
@ -1252,8 +1287,8 @@
|
|||
|
||||
_setClippingProperties: function(ctx) {
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.lineWidth = 0;
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.strokeStyle = 'transparent';
|
||||
ctx.fillStyle = '#000000';
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
41
test/lib/visualCallbackQunit.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
(function(window) {
|
||||
function visualCallback() {
|
||||
this.currentArgs = {};
|
||||
}
|
||||
|
||||
visualCallback.prototype.addArguments = function(argumentObj) {
|
||||
this.currentArgs = {
|
||||
enabled: true,
|
||||
fabric: argumentObj.fabric,
|
||||
golden: argumentObj.golden,
|
||||
diff: argumentObj.diff,
|
||||
};
|
||||
};
|
||||
|
||||
visualCallback.prototype.testDone = function(details) {
|
||||
if (window && document && this.currentArgs.enabled) {
|
||||
var fabricCanvas = this.currentArgs.fabric;
|
||||
var ouputImageDataRef = this.currentArgs.diff;
|
||||
var goldenCanvasRef = this.currentArgs.golden;
|
||||
var id = 'qunit-test-output-' + details.testId;
|
||||
var node = document.getElementById(id);
|
||||
var fabricCopy = document.createElement('canvas');
|
||||
var diff = document.createElement('canvas');
|
||||
diff.width = fabricCopy.width = fabricCanvas.width;
|
||||
diff.height = fabricCopy.height = fabricCanvas.height;
|
||||
diff.getContext('2d').putImageData(ouputImageDataRef, 0, 0);
|
||||
fabricCopy.getContext('2d').drawImage(fabricCanvas, 0, 0);
|
||||
var _div = document.createElement('div');
|
||||
_div.appendChild(goldenCanvasRef);
|
||||
_div.appendChild(fabricCopy);
|
||||
_div.appendChild(diff);
|
||||
node.appendChild(_div);
|
||||
// after one run, disable
|
||||
this.currentArgs.enabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
if (window) {
|
||||
window.visualCallback = new visualCallback();
|
||||
}
|
||||
})(this);
|
||||
|
|
@ -1,8 +1,29 @@
|
|||
// set the fabric famework as a global for tests
|
||||
var chalk = require('chalk');
|
||||
global.fabric = require('../dist/fabric').fabric;
|
||||
global.pixelmatch = require('pixelmatch');
|
||||
global.fs = require('fs');
|
||||
|
||||
global.visualCallback = {
|
||||
addArguments: function() {},
|
||||
};
|
||||
global.imageDataToChalk = function(imageData) {
|
||||
// actually this does not work on travis-ci, so commenting it out
|
||||
return '';
|
||||
var block = String.fromCharCode(9608)
|
||||
var data = imageData.data;
|
||||
var width = imageData.width;
|
||||
var height = imageData.height;
|
||||
var outputString = '';
|
||||
var cp = 0;
|
||||
for (var i = 0; i < height; i++) {
|
||||
outputString += '\n';
|
||||
for (var j = 0; j < width; j++) {
|
||||
cp = (i * width + j) * 4;
|
||||
outputString += chalk.rgb(data[cp], data[cp + 1], data[cp + 2])(block);
|
||||
}
|
||||
}
|
||||
return outputString;
|
||||
};
|
||||
QUnit.config.testTimeout = 15000;
|
||||
QUnit.config.noglobals = true;
|
||||
QUnit.config.hidePassed = true;
|
||||
|
|
|
|||
|
|
@ -16,46 +16,50 @@
|
|||
|
||||
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
|
||||
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;
|
||||
var expectedClipPath = fabric.util.object.clone(emptyObjectRepr);
|
||||
expectedClipPath = fabric.util.object.extend(expectedClipPath, {
|
||||
inverted: cObj.clipPath.inverted,
|
||||
absolutePositioned: cObj.clipPath.absolutePositioned,
|
||||
});
|
||||
expected.clipPath = expectedClipPath;
|
||||
assert.deepEqual(expected, cObj.toObject());
|
||||
});
|
||||
|
||||
|
|
@ -71,6 +75,20 @@
|
|||
});
|
||||
});
|
||||
|
||||
QUnit.test('from object with clipPath inverted, absolutePositioned', function(assert) {
|
||||
var done = assert.async();
|
||||
var rect = new fabric.Rect({ width: 100, height: 100 });
|
||||
rect.clipPath = new fabric.Circle({ radius: 50, inverted: true, absolutePositioned: true });
|
||||
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.equal(rect.clipPath.inverted, true, 'inverted is restored correctly');
|
||||
assert.equal(rect.clipPath.absolutePositioned, true, 'absolutePositioned 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 });
|
||||
|
|
@ -85,4 +103,33 @@
|
|||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('from object with clipPath, nested inverted, absolutePositioned', 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', { inverted: true, absolutePositioned: true});
|
||||
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');
|
||||
assert.equal(rect.clipPath.clipPath.inverted, true, 'instance inverted is restored correctly');
|
||||
assert.equal(rect.clipPath.clipPath.absolutePositioned, true, 'instance absolutePositioned is restored correctly');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('_setClippingProperties fix the context props', function(assert) {
|
||||
var canvas = new fabric.Canvas();
|
||||
var rect = new fabric.Rect({ width: 100, height: 100 });
|
||||
canvas.contextContainer.fillStyle = 'red';
|
||||
canvas.contextContainer.strokeStyle = 'blue';
|
||||
canvas.contextContainer.globalAlpha = 0.3;
|
||||
rect._setClippingProperties(canvas.contextContainer);
|
||||
assert.equal(canvas.contextContainer.fillStyle, '#000000', 'fillStyle is reset');
|
||||
assert.equal(new fabric.Color(canvas.contextContainer.strokeStyle).getAlpha(), 0, 'stroke style is reset');
|
||||
assert.equal(canvas.contextContainer.globalAlpha, 1, 'globalAlpha is reset');
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@
|
|||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect x="0" y="0" width="200" height="100"
|
||||
<rect x="1" y="1" width="200" height="100" stroke-width="2"
|
||||
style="fill: url(#three_stops); stroke: black;"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 427 B After Width: | Height: | Size: 445 B |
418
test/visual/clippath.js
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
(function() {
|
||||
fabric.enableGLFiltering = false;
|
||||
fabric.isWebglSupported = false;
|
||||
fabric.Object.prototype.objectCaching = true;
|
||||
var _pixelMatch;
|
||||
var visualCallback;
|
||||
var fs;
|
||||
var imageDataToChalk;
|
||||
if (fabric.isLikelyNode) {
|
||||
fs = global.fs;
|
||||
_pixelMatch = global.pixelmatch;
|
||||
visualCallback = global.visualCallback;
|
||||
imageDataToChalk = global.imageDataToChalk;
|
||||
}
|
||||
else {
|
||||
_pixelMatch = pixelmatch;
|
||||
if (window) {
|
||||
visualCallback = window.visualCallback;
|
||||
}
|
||||
imageDataToChalk = function() { return ''; };
|
||||
}
|
||||
var fabricCanvas = this.canvas = new fabric.Canvas(null, {
|
||||
enableRetinaScaling: false, renderOnAddRemove: false, width: 200, height: 200,
|
||||
});
|
||||
var pixelmatchOptions = {
|
||||
includeAA: false,
|
||||
threshold: 0.095
|
||||
};
|
||||
|
||||
function getAbsolutePath(path) {
|
||||
var isAbsolute = /^https?:/.test(path);
|
||||
if (isAbsolute) { return path; };
|
||||
var imgEl = fabric.document.createElement('img');
|
||||
imgEl.src = path;
|
||||
var src = imgEl.src;
|
||||
imgEl = null;
|
||||
return src;
|
||||
}
|
||||
|
||||
// function getFixtureName(filename) {
|
||||
// var finalName = '/fixtures/' + filename;
|
||||
// return fabric.isLikelyNode ? (__dirname + '/..' + finalName) : getAbsolutePath('/test' + finalName);
|
||||
// }
|
||||
|
||||
function getGoldeName(filename) {
|
||||
var finalName = '/golden/' + filename;
|
||||
return fabric.isLikelyNode ? (__dirname + finalName) : getAbsolutePath('/test/visual' + finalName);
|
||||
}
|
||||
|
||||
function getImage(filename, original, callback) {
|
||||
if (fabric.isLikelyNode && original) {
|
||||
try {
|
||||
fs.statSync(filename);
|
||||
}
|
||||
catch (err) {
|
||||
var dataUrl = original.toDataURL().split(',')[1];
|
||||
console.log('creating original for ', filename);
|
||||
fs.writeFileSync(filename, dataUrl, { encoding: 'base64' });
|
||||
}
|
||||
}
|
||||
var img = fabric.document.createElement('img');
|
||||
img.onload = function() {
|
||||
img.onload = null;
|
||||
callback(img);
|
||||
};
|
||||
img.onerror = function(err) {
|
||||
img.onerror = null;
|
||||
callback(img);
|
||||
console.log('Image loading errored', err);
|
||||
};
|
||||
img.src = filename;
|
||||
}
|
||||
|
||||
function beforeEachHandler() {
|
||||
fabricCanvas.clipPath = null;
|
||||
fabricCanvas.viewportTransform = [1, 0, 0, 1, 0, 0];
|
||||
fabricCanvas.clear();
|
||||
fabricCanvas.renderAll();
|
||||
}
|
||||
|
||||
var tests = [];
|
||||
|
||||
function clipping0(canvas, callback) {
|
||||
var clipPath = new fabric.Circle({ radius: 100, strokeWidth: 0, top: -10, left: -10 });
|
||||
var obj = new fabric.Rect({ top: 0, left: 0, strokeWidth: 0, width: 200, height: 200, fill: 'rgba(0,255,0,0.5)'});
|
||||
obj.clipPath = clipPath;
|
||||
canvas.add(obj);
|
||||
canvas.renderAll();
|
||||
callback(canvas.lowerCanvasEl);
|
||||
}
|
||||
|
||||
tests.push({
|
||||
test: 'Clip a rect with a circle, no zoom',
|
||||
code: clipping0,
|
||||
golden: 'clipping0.png',
|
||||
newModule: 'Clipping shapes',
|
||||
percentage: 0.06,
|
||||
});
|
||||
|
||||
function clipping01(canvas, callback) {
|
||||
var clipPath = new fabric.Circle({ radius: 50, strokeWidth: 40, top: -50, left: -50, fill: 'transparent' });
|
||||
var obj = new fabric.Rect({ top: 0, left: 0, strokeWidth: 0, width: 200, height: 200, fill: 'rgba(0,255,0,0.5)'});
|
||||
obj.clipPath = clipPath;
|
||||
canvas.add(obj);
|
||||
canvas.renderAll();
|
||||
callback(canvas.lowerCanvasEl);
|
||||
}
|
||||
|
||||
tests.push({
|
||||
test: 'A clippath ignores fill and stroke for drawing, not positioning',
|
||||
code: clipping01,
|
||||
golden: 'clipping01.png',
|
||||
newModule: 'Clipping shapes',
|
||||
percentage: 0.06,
|
||||
});
|
||||
|
||||
function clipping1(canvas, callback) {
|
||||
var zoom = 20;
|
||||
canvas.setZoom(zoom);
|
||||
var clipPath = new fabric.Circle({ radius: 5, strokeWidth: 0, top: -2, left: -2 });
|
||||
var obj = new fabric.Rect({ top: 0, left: 0, strokeWidth: 0, width: 10, height: 10, fill: 'rgba(255,0,0,0.5)'});
|
||||
obj.clipPath = clipPath;
|
||||
canvas.add(obj);
|
||||
canvas.renderAll();
|
||||
callback(canvas.lowerCanvasEl);
|
||||
}
|
||||
|
||||
tests.push({
|
||||
test: 'Clip a rect with a circle, with zoom',
|
||||
code: clipping1,
|
||||
golden: 'clipping1.png',
|
||||
percentage: 0.06,
|
||||
});
|
||||
|
||||
function clipping2(canvas, callback) {
|
||||
var clipPath = new fabric.Circle({
|
||||
radius: 100,
|
||||
top: -100,
|
||||
left: -100
|
||||
});
|
||||
var group = new fabric.Group([
|
||||
new fabric.Rect({ strokeWidth: 0, width: 100, height: 100, fill: 'red' }),
|
||||
new fabric.Rect({ strokeWidth: 0, width: 100, height: 100, fill: 'yellow', left: 100 }),
|
||||
new fabric.Rect({ strokeWidth: 0, width: 100, height: 100, fill: 'blue', top: 100 }),
|
||||
new fabric.Rect({ strokeWidth: 0, width: 100, height: 100, fill: 'green', left: 100, top: 100 })
|
||||
], { strokeWidth: 0 });
|
||||
group.clipPath = clipPath;
|
||||
canvas.add(group);
|
||||
canvas.renderAll();
|
||||
callback(canvas.lowerCanvasEl);
|
||||
}
|
||||
|
||||
tests.push({
|
||||
test: 'Clip a group with a circle',
|
||||
code: clipping2,
|
||||
golden: 'clipping2.png',
|
||||
percentage: 0.06,
|
||||
});
|
||||
|
||||
// function clipping3(canvas, callback) {
|
||||
// var clipPath = new fabric.Circle({ radius: 100, top: -100, left: -100 });
|
||||
// var small = new fabric.Circle({ radius: 50, top: -50, left: -50 });
|
||||
// var small2 = new fabric.Rect({ width: 30, height: 30, top: -50, left: -50 });
|
||||
// var group = new fabric.Group([
|
||||
// new fabric.Rect({ strokeWidth: 0, width: 100, height: 100, fill: 'red', clipPath: small }),
|
||||
// new fabric.Rect({ strokeWidth: 0, width: 100, height: 100, fill: 'yellow', left: 100 }),
|
||||
// new fabric.Rect({ strokeWidth: 0, width: 100, height: 100, fill: 'blue', top: 100, clipPath: small2 }),
|
||||
// new fabric.Rect({ strokeWidth: 0, width: 100, height: 100, fill: 'green', left: 100, top: 100 })
|
||||
// ], { strokeWidth: 0 });
|
||||
// group.clipPath = clipPath;
|
||||
// canvas.add(group);
|
||||
// canvas.renderAll();
|
||||
// callback(canvas.lowerCanvasEl);
|
||||
// }
|
||||
|
||||
// FIX ON NODE
|
||||
// tests.push({
|
||||
// test: 'Isolation of clipPath of group and inner objects',
|
||||
// code: clipping3,
|
||||
// golden: 'clipping3.png',
|
||||
// percentage: 0.06,
|
||||
// });
|
||||
|
||||
function clipping4(canvas, callback) {
|
||||
var clipPath = new fabric.Circle({ radius: 20, strokeWidth: 0, top: -10, left: -10, scaleX: 2, skewY: 45 });
|
||||
var obj = new fabric.Rect({ top: 0, left: 0, strokeWidth: 0, width: 200, height: 200, fill: 'rgba(0,255,0,0.5)'});
|
||||
obj.fill = new fabric.Gradient({
|
||||
type: 'linear',
|
||||
coords: {
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
x2: 200,
|
||||
y2: 200,
|
||||
},
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'blue',
|
||||
}
|
||||
]
|
||||
});
|
||||
obj.clipPath = clipPath;
|
||||
canvas.add(obj);
|
||||
canvas.renderAll();
|
||||
callback(canvas.lowerCanvasEl);
|
||||
}
|
||||
|
||||
tests.push({
|
||||
test: 'ClipPath can be transformed',
|
||||
code: clipping4,
|
||||
golden: 'clipping4.png',
|
||||
percentage: 0.06,
|
||||
});
|
||||
|
||||
function clipping5(canvas, callback) {
|
||||
var clipPath = new fabric.Circle({ radius: 20, strokeWidth: 0, top: -10, left: -10, scaleX: 2, skewY: 45 });
|
||||
var clipPath1 = new fabric.Circle({ radius: 15, rotate: 45, strokeWidth: 0, top: -100, left: -50, scaleX: 2, skewY: 45 });
|
||||
var clipPath2 = new fabric.Circle({ radius: 10, strokeWidth: 0, top: -20, left: -20, scaleY: 2, skewX: 45 });
|
||||
var group = new fabric.Group([clipPath, clipPath1, clipPath2]);
|
||||
var obj = new fabric.Rect({ top: 0, left: 0, strokeWidth: 0, width: 200, height: 200, fill: 'rgba(0,255,0,0.5)'});
|
||||
obj.fill = new fabric.Gradient({
|
||||
type: 'linear',
|
||||
coords: {
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
x2: 200,
|
||||
y2: 200,
|
||||
},
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'blue',
|
||||
}
|
||||
]
|
||||
});
|
||||
obj.clipPath = group;
|
||||
canvas.add(obj);
|
||||
canvas.renderAll();
|
||||
callback(canvas.lowerCanvasEl);
|
||||
}
|
||||
|
||||
tests.push({
|
||||
test: 'ClipPath can be a group with many objects',
|
||||
code: clipping5,
|
||||
golden: 'clipping5.png',
|
||||
percentage: 0.06,
|
||||
});
|
||||
|
||||
function clipping6(canvas, callback) {
|
||||
var clipPath = new fabric.Circle({ radius: 20, strokeWidth: 0, top: -10, left: -10, scaleX: 2, skewY: 45 });
|
||||
var clipPath1 = new fabric.Circle({ radius: 15, rotate: 45, strokeWidth: 0, top: -100, left: -50, scaleX: 2, skewY: 45 });
|
||||
var clipPath2 = new fabric.Circle({ radius: 10, strokeWidth: 0, top: -20, left: -20, scaleY: 2, skewX: 45 });
|
||||
var group = new fabric.Group([clipPath, clipPath1, clipPath2]);
|
||||
var obj = new fabric.Rect({ top: 0, left: 0, strokeWidth: 0, width: 200, height: 200, fill: 'rgba(0,255,0,0.5)'});
|
||||
obj.fill = new fabric.Gradient({
|
||||
type: 'linear',
|
||||
coords: {
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
x2: 200,
|
||||
y2: 200,
|
||||
},
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'blue',
|
||||
}
|
||||
]
|
||||
});
|
||||
obj.clipPath = group;
|
||||
group.inverted = true;
|
||||
canvas.add(obj);
|
||||
canvas.renderAll();
|
||||
callback(canvas.lowerCanvasEl);
|
||||
}
|
||||
|
||||
tests.push({
|
||||
test: 'ClipPath can be inverted, it will clip what is outside the clipPath',
|
||||
code: clipping6,
|
||||
golden: 'clipping6.png',
|
||||
percentage: 0.06,
|
||||
});
|
||||
|
||||
// function clipping7(canvas, callback) {
|
||||
// var clipPath = new fabric.Circle({ radius: 30, strokeWidth: 0, top: -30, left: -30, skewY: 45 });
|
||||
// var obj1 = new fabric.Rect({ top: 0, left: 100, strokeWidth: 0, width: 100, height: 100, fill: 'rgba(0,255,0,0.8)'});
|
||||
// var obj2 = new fabric.Rect({ top: 0, left: 0, strokeWidth: 0, width: 100, height: 100, fill: 'rgba(255,255,0,0.8)'});
|
||||
// var obj3 = new fabric.Rect({ top: 100, left: 0, strokeWidth: 0, width: 100, height: 100, fill: 'rgba(0,255,255,0.8)'});
|
||||
// var obj4 = new fabric.Rect({ top: 100, left: 100, strokeWidth: 0, width: 100, height: 100, fill: 'rgba(255,0,0,0.8)'});
|
||||
// obj1.clipPath = clipPath;
|
||||
// obj2.clipPath = clipPath;
|
||||
// obj3.clipPath = clipPath;
|
||||
// obj4.clipPath = clipPath;
|
||||
// canvas.add(obj1);
|
||||
// canvas.add(obj2);
|
||||
// canvas.add(obj3);
|
||||
// canvas.add(obj4);
|
||||
// canvas.renderAll();
|
||||
// callback(canvas.lowerCanvasEl);
|
||||
// }
|
||||
|
||||
// FIX ON NODE
|
||||
// tests.push({
|
||||
// test: 'Many Objects can share the same clipPath',
|
||||
// code: clipping7,
|
||||
// golden: 'clipping7.png',
|
||||
// percentage: 0.06,
|
||||
// });
|
||||
|
||||
function clipping8(canvas, callback) {
|
||||
var clipPath = new fabric.Circle({ radius: 60, strokeWidth: 0, top: 40, left: 40, absolutePositioned: true });
|
||||
var obj1 = new fabric.Rect({ top: 0, left: 100, strokeWidth: 0, width: 100, height: 100, fill: 'rgba(0,255,0,0.8)'});
|
||||
var obj2 = new fabric.Rect({ top: 0, left: 0, strokeWidth: 0, width: 100, height: 100, fill: 'rgba(255,255,0,0.8)'});
|
||||
var obj3 = new fabric.Rect({ top: 100, left: 0, strokeWidth: 0, width: 100, height: 100, fill: 'rgba(0,255,255,0.8)'});
|
||||
var obj4 = new fabric.Rect({ top: 100, left: 100, strokeWidth: 0, width: 100, height: 100, fill: 'rgba(255,0,0,0.8)'});
|
||||
obj1.clipPath = clipPath;
|
||||
obj2.clipPath = clipPath;
|
||||
obj3.clipPath = clipPath;
|
||||
canvas.add(obj1);
|
||||
canvas.add(obj2);
|
||||
canvas.add(obj3);
|
||||
canvas.add(obj4);
|
||||
canvas.renderAll();
|
||||
callback(canvas.lowerCanvasEl);
|
||||
}
|
||||
|
||||
tests.push({
|
||||
test: 'an absolute positioned clipPath, shared',
|
||||
code: clipping8,
|
||||
golden: 'clipping8.png',
|
||||
percentage: 0.06,
|
||||
});
|
||||
|
||||
function clipping9(canvas, callback) {
|
||||
var clipPath = new fabric.Circle({ radius: 60, strokeWidth: 0, top: 10, left: 10 });
|
||||
var obj1 = new fabric.Rect({ top: 0, left: 100, strokeWidth: 0, width: 100, height: 100, fill: 'rgba(0,255,0,0.8)'});
|
||||
var obj2 = new fabric.Rect({ top: 0, left: 0, strokeWidth: 0, width: 100, height: 100, fill: 'rgba(255,255,0,0.8)'});
|
||||
var obj3 = new fabric.Rect({ top: 100, left: 0, strokeWidth: 0, width: 100, height: 100, fill: 'rgba(0,255,255,0.8)'});
|
||||
var obj4 = new fabric.Rect({ top: 100, left: 100, strokeWidth: 0, width: 100, height: 100, fill: 'rgba(255,0,0,0.8)'});
|
||||
canvas.add(obj1);
|
||||
canvas.add(obj2);
|
||||
canvas.add(obj3);
|
||||
canvas.add(obj4);
|
||||
canvas.clipPath = clipPath;
|
||||
canvas.renderAll();
|
||||
callback(canvas.lowerCanvasEl);
|
||||
}
|
||||
|
||||
tests.push({
|
||||
test: 'a clipPath on the canvas',
|
||||
code: clipping9,
|
||||
golden: 'clipping9.png',
|
||||
percentage: 0.06,
|
||||
});
|
||||
|
||||
|
||||
tests.forEach(function(testObj) {
|
||||
var testName = testObj.test;
|
||||
var code = testObj.code;
|
||||
var percentage = testObj.percentage;
|
||||
var golden = testObj.golden;
|
||||
var newModule = testObj.newModule;
|
||||
if (newModule) {
|
||||
QUnit.module(newModule, {
|
||||
beforeEach: beforeEachHandler,
|
||||
});
|
||||
}
|
||||
QUnit.test(testName, function(assert) {
|
||||
var done = assert.async();
|
||||
code(fabricCanvas, function(renderedCanvas) {
|
||||
var width = renderedCanvas.width;
|
||||
var height = renderedCanvas.height;
|
||||
var totalPixels = width * height;
|
||||
var imageDataCanvas = renderedCanvas.getContext('2d').getImageData(0, 0, width, height).data;
|
||||
var canvas = fabric.document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
var ctx = canvas.getContext('2d');
|
||||
var output = ctx.getImageData(0, 0, width, height);
|
||||
getImage(getGoldeName(golden), renderedCanvas, function(goldenImage) {
|
||||
ctx.drawImage(goldenImage, 0, 0);
|
||||
visualCallback.addArguments({
|
||||
enabled: true,
|
||||
golden: canvas,
|
||||
fabric: renderedCanvas,
|
||||
diff: output
|
||||
});
|
||||
var imageDataGolden = ctx.getImageData(0, 0, width, height).data;
|
||||
var differentPixels = _pixelMatch(imageDataCanvas, imageDataGolden, output.data, width, height, pixelmatchOptions);
|
||||
var percDiff = differentPixels / totalPixels * 100;
|
||||
var okDiff = totalPixels * percentage;
|
||||
var isOK = differentPixels < okDiff;
|
||||
assert.ok(
|
||||
isOK,
|
||||
testName + ' has too many different pixels ' + differentPixels + '(' + okDiff + ') representing ' + percDiff + '%'
|
||||
);
|
||||
if (!isOK) {
|
||||
var stringa = imageDataToChalk(output);
|
||||
console.log(stringa);
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
BIN
test/visual/golden/clipping0.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
test/visual/golden/clipping01.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
test/visual/golden/clipping1.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
test/visual/golden/clipping2.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
test/visual/golden/clipping3.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
test/visual/golden/clipping4.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
test/visual/golden/clipping5.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
test/visual/golden/clipping6.png
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
test/visual/golden/clipping7.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
test/visual/golden/clipping8.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
test/visual/golden/clipping9.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2 KiB |
|
|
@ -1,10 +1,19 @@
|
|||
(function() {
|
||||
fabric.enableGLFiltering = false;
|
||||
fabric.isWebglSupported = false;
|
||||
var _pixelMatch = pixelmatch;
|
||||
var _pixelMatch;
|
||||
var visualCallback;
|
||||
var fs;
|
||||
if (fabric.isLikelyNode) {
|
||||
var fs = global.fs;
|
||||
fs = global.fs;
|
||||
_pixelMatch = global.pixelmatch;
|
||||
visualCallback = global.visualCallback;
|
||||
}
|
||||
else {
|
||||
_pixelMatch = pixelmatch;
|
||||
if (window) {
|
||||
visualCallback = window.visualCallback;
|
||||
}
|
||||
}
|
||||
var fabricCanvas = this.canvas = new fabric.Canvas(null, {enableRetinaScaling: false, renderOnAddRemove: false});
|
||||
var pixelmatchOptions = {
|
||||
|
|
@ -56,7 +65,7 @@
|
|||
img.src = filename;
|
||||
}
|
||||
|
||||
function afterEachHandler() {
|
||||
function beforeEachHandler() {
|
||||
fabricCanvas.setZoom(1);
|
||||
fabricCanvas.setDimensions({
|
||||
width: 300,
|
||||
|
|
@ -205,7 +214,7 @@
|
|||
var newModule = testObj.newModule;
|
||||
if (newModule) {
|
||||
QUnit.module(newModule, {
|
||||
afterEach: afterEachHandler,
|
||||
beforeEach: beforeEachHandler,
|
||||
});
|
||||
}
|
||||
QUnit.test(testName, function(assert) {
|
||||
|
|
@ -219,18 +228,23 @@
|
|||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
var ctx = canvas.getContext('2d');
|
||||
var output = ctx.getImageData(0, 0, width, height).data;
|
||||
var output = ctx.getImageData(0, 0, width, height);
|
||||
getImage(getGoldeName(golden), renderedCanvas, function(goldenImage) {
|
||||
ctx.drawImage(goldenImage, 0, 0);
|
||||
visualCallback.addArguments({
|
||||
enabled: true,
|
||||
golden: canvas,
|
||||
fabric: renderedCanvas,
|
||||
diff: output
|
||||
});
|
||||
var imageDataGolden = ctx.getImageData(0, 0, width, height).data;
|
||||
var differentPixels = _pixelMatch(imageDataCanvas, imageDataGolden, output, width, height, pixelmatchOptions);
|
||||
var differentPixels = _pixelMatch(imageDataCanvas, imageDataGolden, output.data, width, height, pixelmatchOptions);
|
||||
var percDiff = differentPixels / totalPixels * 100;
|
||||
var okDiff = totalPixels * percentage;
|
||||
assert.ok(
|
||||
differentPixels < okDiff,
|
||||
testName + ' has too many different pixels ' + differentPixels + '(' + okDiff + ') representing ' + percDiff + '%'
|
||||
);
|
||||
console.log('Different pixels:', differentPixels, '/', totalPixels, ' diff:', percDiff.toFixed(3), '%');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,19 @@
|
|||
(function() {
|
||||
var _pixelMatch = pixelmatch;
|
||||
fabric.enableGLFiltering = false;
|
||||
fabric.isWebglSupported = false;
|
||||
var _pixelMatch;
|
||||
var visualCallback;
|
||||
var fs;
|
||||
if (fabric.isLikelyNode) {
|
||||
var fs = global.fs;
|
||||
fs = global.fs;
|
||||
_pixelMatch = global.pixelmatch;
|
||||
visualCallback = global.visualCallback;
|
||||
}
|
||||
else {
|
||||
_pixelMatch = pixelmatch;
|
||||
if (window) {
|
||||
visualCallback = window.visualCallback;
|
||||
}
|
||||
}
|
||||
var fabricCanvas = this.canvas = new fabric.Canvas(null, {enableRetinaScaling: false, renderOnAddRemove: false});
|
||||
var pixelmatchOptions = {
|
||||
|
|
@ -63,23 +74,28 @@
|
|||
height: height
|
||||
});
|
||||
var ctx = canvas.getContext('2d');
|
||||
var outputImageData = ctx.getImageData(0, 0, width, height);
|
||||
ctx.drawImage(img, 0, 0);
|
||||
var goldenImageData = ctx.getImageData(0, 0, width, height).data;
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
var outputImageData = ctx.getImageData(0, 0, width, height).data;
|
||||
var goldenImageData = ctx.getImageData(0, 0, width, height);
|
||||
getAsset(filename, function(err, string) {
|
||||
fabric.loadSVGFromString(string, function(objects) {
|
||||
fabricCanvas.add.apply(fabricCanvas, objects);
|
||||
fabricCanvas.renderAll();
|
||||
visualCallback.addArguments({
|
||||
enabled: true,
|
||||
golden: canvas,
|
||||
fabric: fabricCanvas.lowerCanvasEl,
|
||||
diff: outputImageData
|
||||
});
|
||||
var fabricImageData = fabricCanvas.contextContainer.getImageData(0, 0, width, height).data;
|
||||
callback(fabricImageData, goldenImageData, width, height, outputImageData);
|
||||
callback(fabricImageData, goldenImageData.data, width, height, outputImageData.data);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.module('Simple svg import test', {
|
||||
afterEach: function() {
|
||||
beforeEach: function() {
|
||||
fabricCanvas.clear();
|
||||
fabricCanvas.renderAll();
|
||||
}
|
||||
|
|
@ -116,7 +132,7 @@
|
|||
['svg_radial_13', 4],
|
||||
].forEach(function(filenameArray) {
|
||||
var filename = filenameArray[0];
|
||||
var expectedPixels = filenameArray[1];
|
||||
// var expectedPixels = filenameArray[1];
|
||||
QUnit.test('Import test for file ' + filename, function(assert) {
|
||||
var done = assert.async();
|
||||
loadAndPrepareCanvasFor(filename, function(imageDataCanvas, imageDataGolden, width, height, output) {
|
||||
|
|
@ -126,7 +142,6 @@
|
|||
var percDiff = differentPixels / totalPixels * 100;
|
||||
assert.ok(differentPixels < totalPixels * percentage, 'Image ' + filename + ' has too many different pixels ' + differentPixels + ' representing ' + percDiff + '%');
|
||||
done();
|
||||
console.log('Different pixels for', filename, ':', differentPixels, '/', totalPixels, 'expected:', expectedPixels, ' diff:', percDiff.toFixed(3), '%');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
"serve_files": [
|
||||
"dist/fabric.js",
|
||||
"test/lib/pixelmatch.js",
|
||||
"test/lib/visualCallbackQunit.js",
|
||||
"test/visual/*.js"
|
||||
],
|
||||
"routes": {
|
||||
|
|
|
|||