Skip to content

Commit 513233b

Browse files
authored
Making clipPath absolute positionable (fabricjs#5199)
Added visual tests Added property absolutePositioned and inverse
1 parent 2a476e4 commit 513233b

24 files changed

+663
-64
lines changed

.eslintrc_tests

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"globals": {
77
"fabric": true,
88
"QUnit": true,
9-
"assert": true
9+
"assert": true,
10+
"pixelmatch": true
1011
},
1112
"rules": {
1213
"eqeqeq": 0,

.travis.yml

+7-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ addons:
1313
- libpng-dev
1414
- libpango1.0-dev
1515
- libjpeg-dev
16+
- librsvg2-dev
1617
# libcairo2-dev is preinstalled
1718
stages:
1819
- Linting and Building
@@ -50,12 +51,14 @@ jobs:
5051
packages: # avoid installing packages
5152
- stage: Unit Tests
5253
env: LAUNCHER=Chrome
54+
script: npm run build:fast && testem ci --port 8080 -f testem.json -l $LAUNCHER
5355
5456
addons:
5557
apt:
5658
packages: # avoid installing packages
5759
- stage: Unit Tests
5860
env: LAUNCHER=Firefox
61+
script: npm run build:fast && testem ci --port 8080 -f testem.json -l $LAUNCHER
5962
6063
addons:
6164
apt:
@@ -72,17 +75,18 @@ jobs:
7275
- stage: Unit Tests
7376
node_js: "4"
7477
- stage: Visual Tests
78+
env: LAUNCHER=Node CANFAIL=TRUE
7579
node_js: "8"
7680
script: npm run build:fast && npm run test:visual
7781
- stage: Visual Tests
7882
env: LAUNCHER=Chrome
79-
install: npm install [email protected] qunit@2.4.1
83+
install: npm install [email protected] qunit@2.6.1
8084
script: npm run build:fast && testem ci --port 8080 -f testem-visual.json -l $LAUNCHER
8185
- stage: Visual Tests
8286
env: LAUNCHER=Firefox
83-
install: npm install [email protected] qunit@2.4.1
87+
install: npm install [email protected] qunit@2.6.1
8488
script: npm run build:fast && testem ci --port 8080 -f testem-visual.json -l $LAUNCHER
8589

86-
script: 'npm run build:fast && testem ci --port 8080 -f testem.json -l $LAUNCHER'
90+
script: npm run build:fast && npm run test
8791

8892
dist: trusty

package.json

+6-4
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@
4848
"test:single": "./node_modules/qunit/bin/qunit test/node_test_setup.js test/lib",
4949
"test": "istanbul cover ./node_modules/qunit/bin/qunit test/node_test_setup.js test/lib test/unit",
5050
"test:visual": "./node_modules/qunit/bin/qunit test/node_test_setup.js test/lib test/visual",
51+
"test:visual:single": "./node_modules/qunit/bin/qunit test/node_test_setup.js test/lib",
5152
"test:all": "npm run test && npm run test:visual",
5253
"lint": "eslint --config .eslintrc.json src",
53-
"lint_tests": "eslint test/unit --config .eslintrc_tests",
54+
"lint_tests": "eslint test/unit --config .eslintrc_tests && eslint test/visual --config .eslintrc_tests",
5455
"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/",
5556
"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",
5657
"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 @@
5960
"testem:ci": "testem ci"
6061
},
6162
"optionalDependencies": {
62-
"canvas": "1.6.x",
63-
"jsdom": "9.x.x",
63+
"canvas": "^1.6.12",
64+
"jsdom": "^9.12.0",
6465
"xmldom": "0.1.x"
6566
},
6667
"devDependencies": {
@@ -70,7 +71,8 @@
7071
"qunit": "^2.6.1",
7172
"testem": "^1.18.4",
7273
"uglify-js": "3.3.x",
73-
"pixelmatch": "^4.0.2"
74+
"pixelmatch": "^4.0.2",
75+
"chalk": "^2.4.1"
7476
},
7577
"engines": {
7678
"node": ">=4.0.0"

src/shapes/object.class.js

+39-4
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,27 @@
612612
*/
613613
clipPath: undefined,
614614

615+
/**
616+
* Meaningfull ONLY when the object is used as clipPath.
617+
* if true, the clipPath will make the object clip to the outside of the clipPath
618+
* since 2.4.0
619+
* @type boolean
620+
* @default false
621+
*/
622+
inverted: false,
623+
624+
/**
625+
* Meaningfull ONLY when the object is used as clipPath.
626+
* if true, the clipPath will have its top and left relative to canvas, and will
627+
* not be influenced by the object transform. This will make the clipPath relative
628+
* to the canvas, but clipping just a particular object.
629+
* WARNING this is beta, this feature may change or be renamed.
630+
* since 2.4.0
631+
* @type boolean
632+
* @default false
633+
*/
634+
absolutePositioned: false,
635+
615636
/**
616637
* Constructor
617638
* @param {Object} [options] Options object
@@ -842,6 +863,8 @@
842863

843864
if (this.clipPath) {
844865
object.clipPath = this.clipPath.toObject(propertiesToInclude);
866+
object.clipPath.inverted = this.clipPath.inverted;
867+
object.clipPath.absolutePositioned = this.clipPath.absolutePositioned;
845868
}
846869

847870
fabric.util.populateWithProperties(this, object, propertiesToInclude);
@@ -1120,8 +1143,17 @@
11201143
ctx.save();
11211144
// DEBUG: uncomment this line, comment the following
11221145
// ctx.globalAlpha = 0.4
1123-
ctx.globalCompositeOperation = 'destination-in';
1146+
if (path.inverted) {
1147+
ctx.globalCompositeOperation = 'destination-out';
1148+
}
1149+
else {
1150+
ctx.globalCompositeOperation = 'destination-in';
1151+
}
11241152
//ctx.scale(1 / 2, 1 / 2);
1153+
if (path.absolutePositioned) {
1154+
var m = fabric.util.invertTransform(this.calcTransformMatrix());
1155+
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
1156+
}
11251157
path.transform(ctx);
11261158
ctx.scale(1 / path.zoomX, 1 / path.zoomY);
11271159
ctx.drawImage(path._cacheCanvas, -path.cacheTranslationX, -path.cacheTranslationY);
@@ -1182,7 +1214,10 @@
11821214
return true;
11831215
}
11841216
else {
1185-
if (this.dirty || (this.statefullCache && this.hasStateChanged('cacheProperties'))) {
1217+
if (this.dirty ||
1218+
(this.clipPath && this.clipPath.absolutePositioned) ||
1219+
(this.statefullCache && this.hasStateChanged('cacheProperties'))
1220+
) {
11861221
if (this._cacheCanvas && !skipCanvas) {
11871222
var width = this.cacheWidth / this.zoomX;
11881223
var height = this.cacheHeight / this.zoomY;
@@ -1252,8 +1287,8 @@
12521287

12531288
_setClippingProperties: function(ctx) {
12541289
ctx.globalAlpha = 1;
1255-
ctx.lineWidth = 0;
1256-
ctx.fillStyle = 'black';
1290+
ctx.strokeStyle = 'transparent';
1291+
ctx.fillStyle = '#000000';
12571292
},
12581293

12591294
/**

test/lib/visualCallbackQunit.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
(function(window) {
2+
function visualCallback() {
3+
this.currentArgs = {};
4+
}
5+
6+
visualCallback.prototype.addArguments = function(argumentObj) {
7+
this.currentArgs = {
8+
enabled: true,
9+
fabric: argumentObj.fabric,
10+
golden: argumentObj.golden,
11+
diff: argumentObj.diff,
12+
};
13+
};
14+
15+
visualCallback.prototype.testDone = function(details) {
16+
if (window && document && this.currentArgs.enabled) {
17+
var fabricCanvas = this.currentArgs.fabric;
18+
var ouputImageDataRef = this.currentArgs.diff;
19+
var goldenCanvasRef = this.currentArgs.golden;
20+
var id = 'qunit-test-output-' + details.testId;
21+
var node = document.getElementById(id);
22+
var fabricCopy = document.createElement('canvas');
23+
var diff = document.createElement('canvas');
24+
diff.width = fabricCopy.width = fabricCanvas.width;
25+
diff.height = fabricCopy.height = fabricCanvas.height;
26+
diff.getContext('2d').putImageData(ouputImageDataRef, 0, 0);
27+
fabricCopy.getContext('2d').drawImage(fabricCanvas, 0, 0);
28+
var _div = document.createElement('div');
29+
_div.appendChild(goldenCanvasRef);
30+
_div.appendChild(fabricCopy);
31+
_div.appendChild(diff);
32+
node.appendChild(_div);
33+
// after one run, disable
34+
this.currentArgs.enabled = false;
35+
}
36+
};
37+
38+
if (window) {
39+
window.visualCallback = new visualCallback();
40+
}
41+
})(this);

test/node_test_setup.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
11
// set the fabric famework as a global for tests
2+
var chalk = require('chalk');
23
global.fabric = require('../dist/fabric').fabric;
34
global.pixelmatch = require('pixelmatch');
45
global.fs = require('fs');
5-
6+
global.visualCallback = {
7+
addArguments: function() {},
8+
};
9+
global.imageDataToChalk = function(imageData) {
10+
// actually this does not work on travis-ci, so commenting it out
11+
return '';
12+
var block = String.fromCharCode(9608)
13+
var data = imageData.data;
14+
var width = imageData.width;
15+
var height = imageData.height;
16+
var outputString = '';
17+
var cp = 0;
18+
for (var i = 0; i < height; i++) {
19+
outputString += '\n';
20+
for (var j = 0; j < width; j++) {
21+
cp = (i * width + j) * 4;
22+
outputString += chalk.rgb(data[cp], data[cp + 1], data[cp + 2])(block);
23+
}
24+
}
25+
return outputString;
26+
};
627
QUnit.config.testTimeout = 15000;
728
QUnit.config.noglobals = true;
829
QUnit.config.hidePassed = true;

test/unit/object_clipPath.js

+80-33
Original file line numberDiff line numberDiff line change
@@ -16,46 +16,50 @@
1616

1717
QUnit.test('toObject with clipPath', function(assert) {
1818
var emptyObjectRepr = {
19-
'version': fabric.version,
20-
'type': 'object',
21-
'originX': 'left',
22-
'originY': 'top',
23-
'left': 0,
24-
'top': 0,
25-
'width': 0,
26-
'height': 0,
27-
'fill': 'rgb(0,0,0)',
28-
'stroke': null,
29-
'strokeWidth': 1,
30-
'strokeDashArray': null,
31-
'strokeLineCap': 'butt',
32-
'strokeLineJoin': 'miter',
33-
'strokeMiterLimit': 4,
34-
'scaleX': 1,
35-
'scaleY': 1,
36-
'angle': 0,
37-
'flipX': false,
38-
'flipY': false,
39-
'opacity': 1,
40-
'shadow': null,
41-
'visible': true,
42-
'backgroundColor': '',
43-
'clipTo': null,
44-
'fillRule': 'nonzero',
45-
'paintFirst': 'fill',
46-
'globalCompositeOperation': 'source-over',
47-
'skewX': 0,
48-
'skewY': 0,
49-
'transformMatrix': null
19+
version: fabric.version,
20+
type: 'object',
21+
originX: 'left',
22+
originY: 'top',
23+
left: 0,
24+
top: 0,
25+
width: 0,
26+
height: 0,
27+
fill: 'rgb(0,0,0)',
28+
stroke: null,
29+
strokeWidth: 1,
30+
strokeDashArray: null,
31+
strokeLineCap: 'butt',
32+
strokeLineJoin: 'miter',
33+
strokeMiterLimit: 4,
34+
scaleX: 1,
35+
scaleY: 1,
36+
angle: 0,
37+
flipX: false,
38+
flipY: false,
39+
opacity: 1,
40+
shadow: null,
41+
visible: true,
42+
backgroundColor: '',
43+
clipTo: null,
44+
fillRule: 'nonzero',
45+
paintFirst: 'fill',
46+
globalCompositeOperation: 'source-over',
47+
skewX: 0,
48+
skewY: 0,
49+
transformMatrix: null,
5050
};
5151

5252
var cObj = new fabric.Object();
5353
assert.deepEqual(emptyObjectRepr, cObj.toObject());
5454

5555
cObj.clipPath = new fabric.Object();
56-
5756
var expected = fabric.util.object.clone(emptyObjectRepr);
58-
expected.clipPath = emptyObjectRepr;
57+
var expectedClipPath = fabric.util.object.clone(emptyObjectRepr);
58+
expectedClipPath = fabric.util.object.extend(expectedClipPath, {
59+
inverted: cObj.clipPath.inverted,
60+
absolutePositioned: cObj.clipPath.absolutePositioned,
61+
});
62+
expected.clipPath = expectedClipPath;
5963
assert.deepEqual(expected, cObj.toObject());
6064
});
6165

@@ -71,6 +75,20 @@
7175
});
7276
});
7377

78+
QUnit.test('from object with clipPath inverted, absolutePositioned', function(assert) {
79+
var done = assert.async();
80+
var rect = new fabric.Rect({ width: 100, height: 100 });
81+
rect.clipPath = new fabric.Circle({ radius: 50, inverted: true, absolutePositioned: true });
82+
var toObject = rect.toObject();
83+
fabric.Rect.fromObject(toObject, function(rect) {
84+
assert.ok(rect.clipPath instanceof fabric.Circle, 'clipPath is enlived');
85+
assert.equal(rect.clipPath.radius, 50, 'radius is restored correctly');
86+
assert.equal(rect.clipPath.inverted, true, 'inverted is restored correctly');
87+
assert.equal(rect.clipPath.absolutePositioned, true, 'absolutePositioned is restored correctly');
88+
done();
89+
});
90+
});
91+
7492
QUnit.test('from object with clipPath, nested', function(assert) {
7593
var done = assert.async();
7694
var rect = new fabric.Rect({ width: 100, height: 100 });
@@ -85,4 +103,33 @@
85103
done();
86104
});
87105
});
106+
107+
QUnit.test('from object with clipPath, nested inverted, absolutePositioned', function(assert) {
108+
var done = assert.async();
109+
var rect = new fabric.Rect({ width: 100, height: 100 });
110+
rect.clipPath = new fabric.Circle({ radius: 50 });
111+
rect.clipPath.clipPath = new fabric.Text('clipPath', { inverted: true, absolutePositioned: true});
112+
var toObject = rect.toObject();
113+
fabric.Rect.fromObject(toObject, function(rect) {
114+
assert.ok(rect.clipPath instanceof fabric.Circle, 'clipPath is enlived');
115+
assert.equal(rect.clipPath.radius, 50, 'radius is restored correctly');
116+
assert.ok(rect.clipPath.clipPath instanceof fabric.Text, 'neted clipPath is enlived');
117+
assert.equal(rect.clipPath.clipPath.text, 'clipPath', 'instance is restored correctly');
118+
assert.equal(rect.clipPath.clipPath.inverted, true, 'instance inverted is restored correctly');
119+
assert.equal(rect.clipPath.clipPath.absolutePositioned, true, 'instance absolutePositioned is restored correctly');
120+
done();
121+
});
122+
});
123+
124+
QUnit.test('_setClippingProperties fix the context props', function(assert) {
125+
var canvas = new fabric.Canvas();
126+
var rect = new fabric.Rect({ width: 100, height: 100 });
127+
canvas.contextContainer.fillStyle = 'red';
128+
canvas.contextContainer.strokeStyle = 'blue';
129+
canvas.contextContainer.globalAlpha = 0.3;
130+
rect._setClippingProperties(canvas.contextContainer);
131+
assert.equal(canvas.contextContainer.fillStyle, '#000000', 'fillStyle is reset');
132+
assert.equal(new fabric.Color(canvas.contextContainer.strokeStyle).getAlpha(), 0, 'stroke style is reset');
133+
assert.equal(canvas.contextContainer.globalAlpha, 1, 'globalAlpha is reset');
134+
});
88135
})();

test/visual/assets/svg_linear_8.svg

+2-2
Loading

0 commit comments

Comments
 (0)