diff --git a/.eslintrc_tests b/.eslintrc_tests index 7b7d9f95..39d48ab8 100644 --- a/.eslintrc_tests +++ b/.eslintrc_tests @@ -6,7 +6,8 @@ "globals": { "fabric": true, "QUnit": true, - "assert": true + "assert": true, + "pixelmatch": true }, "rules": { "eqeqeq": 0, diff --git a/.travis.yml b/.travis.yml index a5dd827f..5301fcd0 100644 --- a/.travis.yml +++ b/.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 diff --git a/package.json b/package.json index c13da032..476f4e9a 100644 --- a/package.json +++ b/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" diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 6f06947a..803c9f54 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -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'; }, /** diff --git a/test/lib/visualCallbackQunit.js b/test/lib/visualCallbackQunit.js new file mode 100644 index 00000000..b987a46b --- /dev/null +++ b/test/lib/visualCallbackQunit.js @@ -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); diff --git a/test/node_test_setup.js b/test/node_test_setup.js index 1d2a50e5..7542ca7e 100644 --- a/test/node_test_setup.js +++ b/test/node_test_setup.js @@ -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; diff --git a/test/unit/object_clipPath.js b/test/unit/object_clipPath.js index 26146caa..a434e22b 100644 --- a/test/unit/object_clipPath.js +++ b/test/unit/object_clipPath.js @@ -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'); + }); })(); diff --git a/test/visual/assets/svg_linear_8.svg b/test/visual/assets/svg_linear_8.svg index fe21f47f..d8affc9d 100644 --- a/test/visual/assets/svg_linear_8.svg +++ b/test/visual/assets/svg_linear_8.svg @@ -8,6 +8,6 @@ - - \ No newline at end of file + diff --git a/test/visual/clippath.js b/test/visual/clippath.js new file mode 100644 index 00000000..0c5482e8 --- /dev/null +++ b/test/visual/clippath.js @@ -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(); + }); + }); + }); + }); +})(); diff --git a/test/visual/golden/clipping0.png b/test/visual/golden/clipping0.png new file mode 100644 index 00000000..bcf570be Binary files /dev/null and b/test/visual/golden/clipping0.png differ diff --git a/test/visual/golden/clipping01.png b/test/visual/golden/clipping01.png new file mode 100644 index 00000000..af577e42 Binary files /dev/null and b/test/visual/golden/clipping01.png differ diff --git a/test/visual/golden/clipping1.png b/test/visual/golden/clipping1.png new file mode 100644 index 00000000..22ad3f0f Binary files /dev/null and b/test/visual/golden/clipping1.png differ diff --git a/test/visual/golden/clipping2.png b/test/visual/golden/clipping2.png new file mode 100644 index 00000000..99c8eab2 Binary files /dev/null and b/test/visual/golden/clipping2.png differ diff --git a/test/visual/golden/clipping3.png b/test/visual/golden/clipping3.png new file mode 100644 index 00000000..e62010e4 Binary files /dev/null and b/test/visual/golden/clipping3.png differ diff --git a/test/visual/golden/clipping4.png b/test/visual/golden/clipping4.png new file mode 100644 index 00000000..3e28b489 Binary files /dev/null and b/test/visual/golden/clipping4.png differ diff --git a/test/visual/golden/clipping5.png b/test/visual/golden/clipping5.png new file mode 100644 index 00000000..a4c1ad6e Binary files /dev/null and b/test/visual/golden/clipping5.png differ diff --git a/test/visual/golden/clipping6.png b/test/visual/golden/clipping6.png new file mode 100644 index 00000000..4a363c29 Binary files /dev/null and b/test/visual/golden/clipping6.png differ diff --git a/test/visual/golden/clipping7.png b/test/visual/golden/clipping7.png new file mode 100644 index 00000000..da7f253e Binary files /dev/null and b/test/visual/golden/clipping7.png differ diff --git a/test/visual/golden/clipping8.png b/test/visual/golden/clipping8.png new file mode 100644 index 00000000..16b56938 Binary files /dev/null and b/test/visual/golden/clipping8.png differ diff --git a/test/visual/golden/clipping9.png b/test/visual/golden/clipping9.png new file mode 100644 index 00000000..48ad5b81 Binary files /dev/null and b/test/visual/golden/clipping9.png differ diff --git a/test/visual/golden/svg_linear_8.png b/test/visual/golden/svg_linear_8.png index 8f6aec88..f6a988a8 100644 Binary files a/test/visual/golden/svg_linear_8.png and b/test/visual/golden/svg_linear_8.png differ diff --git a/test/visual/resize_filter.js b/test/visual/resize_filter.js index 01df6b52..b7e1682b 100644 --- a/test/visual/resize_filter.js +++ b/test/visual/resize_filter.js @@ -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(); }); }); diff --git a/test/visual/svg_import.js b/test/visual/svg_import.js index 7c5c60ab..3195ac4c 100644 --- a/test/visual/svg_import.js +++ b/test/visual/svg_import.js @@ -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), '%'); }); }); }); diff --git a/testem-visual.json b/testem-visual.json index a971ca9c..a54e77d4 100644 --- a/testem-visual.json +++ b/testem-visual.json @@ -3,6 +3,7 @@ "serve_files": [ "dist/fabric.js", "test/lib/pixelmatch.js", + "test/lib/visualCallbackQunit.js", "test/visual/*.js" ], "routes": {