diff --git a/front/src/css/rule.css b/front/src/css/rule.css index 24137f4..b9b3014 100644 --- a/front/src/css/rule.css +++ b/front/src/css/rule.css @@ -214,6 +214,17 @@ z-index: 2; border: 0.2em solid #f1c40f; } +.similarColors { + margin: 1em; + width: 20em; + height: 6em; +} +.similarColors > div { + display: inline-block; + width: 10em; + height: 3.5em; + padding-top: 2.5em; +} .totalWeightPie { max-width: 39em; margin: 2em auto 4em; diff --git a/front/src/less/rule.less b/front/src/less/rule.less index 6f1fe61..c3da1e9 100644 --- a/front/src/less/rule.less +++ b/front/src/less/rule.less @@ -234,6 +234,19 @@ } } +.similarColors { + margin: 1em; + width: 20em; + height: 6em; + + > div { + display: inline-block; + width: 10em; + height: 3.5em; + padding-top: 2.5em; + } +} + .totalWeightPie { max-width: 39em; margin: 2em auto 4em; diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 0ca11f6..001b0f7 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -96,6 +96,10 @@ +
+
{{offender.color1}}
{{offender.color2}}
+
+
{{offender.error}} diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index fc6bbf0..ae59880 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -463,7 +463,7 @@ var policies = { }, "cssColors": { "tool": "phantomas", - "label": "Different colors", + "label": "Colors count", "message": "

This is the number of different colors defined in CSS.

Your CSS project will be easier to maintain if you keep a small color set.

", "isOkThreshold": 30, "isBadThreshold": 150, @@ -507,6 +507,21 @@ var policies = { }; } }, + "similarColors": { + "tool": "colorDiff", + "label": "Similar colors", + "message": "

This is the list of colors found in the stylesheets, that are very close to each other. The eye can barely see the difference.

Use this list to reduce the number of colors in your palette, it will be easier to maintain.

", + "isOkThreshold": 0, + "isBadThreshold": 40, + "isAbnormalThreshold": 80, + "hasOffenders": true, + "offendersTransformFn": function(offenders) { + return { + count: offenders.length, + list: offenders + }; + } + }, "cssImports": { "tool": "phantomas", "label": "Uses of @import", @@ -970,7 +985,7 @@ var policies = { "tool": "phantomas", "label": "Hidden images", "message": "

List of all images that have a display:none property, or one of their parents. These images are loaded by the browser even if they're not visible. You might be able to find a way to lazy-load them, only when they get visible.

Trackers are an exception, you'd better hide them.

", - "isOkThreshold": 3, + "isOkThreshold": 1, "isBadThreshold": 12, "isAbnormalThreshold": 30, "hasOffenders": true diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index 8dd9670..30230ec 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -70,7 +70,8 @@ "cssRules": 2, "cssComplexSelectors": 2, "cssComplexSelectorsByAttribute": 1.5, - "cssColors": 0.5 + "cssColors": 0.5, + "similarColors": 0.5 } }, "badCSS": { diff --git a/lib/runner.js b/lib/runner.js index 786286d..f35879d 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -3,6 +3,7 @@ var debug = require('debug')('ylt:runner'); var phantomasWrapper = require('./tools/phantomas/phantomasWrapper'); var jsExecutionTransformer = require('./tools/jsExecutionTransformer'); +var colorDiff = require('./tools/colorDiff'); var weightChecker = require('./tools/weightChecker/weightChecker'); var rulesChecker = require('./rulesChecker'); var scoreCalculator = require('./scoreCalculator'); @@ -28,6 +29,9 @@ var Runner = function(params) { // Treat the JS Execution Tree from offenders data = jsExecutionTransformer.transform(data); + // Compare colors + data = colorDiff.compareAllColors(data); + // Redownload every file return weightChecker.recheckAllFiles(data); diff --git a/lib/tools/colorDiff.js b/lib/tools/colorDiff.js new file mode 100644 index 0000000..bf9cdae --- /dev/null +++ b/lib/tools/colorDiff.js @@ -0,0 +1,90 @@ +var debug = require('debug')('ylt:colorDiff'); +var diff = require('color-diff'); +var parse = require('parse-color'); + + +var colorDiff = function() { + + var COLOR_DIFF_THRESHOLD = 0.9; + + this.compareAllColors = function(data) { + debug('Starting to compare all colors...'); + + var offenders = data.toolsResults.phantomas.offenders.cssColors; + var colors = (offenders) ? this.parseAllColors(offenders) : []; + var result = []; + + // Compare colors to each other + for (var i = 0; i < colors.length - 1 ; i++) { + for (var j = i + 1; j < colors.length; j++) { + debug('Comparing color %s with color %s', colors[i].original, colors[j].original); + if (this.compareTwoColors(colors[i], colors[j])) { + debug('Colors are similar!'); + result.push({ + color1: colors[i].original, + color2: colors[j].original, + isDark: (colors[i].Lab.L < 60) + }); + } + } + } + + data.toolsResults.colorDiff = { + metrics: { + similarColors: result.length + }, + offenders: { + similarColors: result + } + }; + + return data; + }; + + this.parseAllColors = function(offenders) { + var parsedOffenders = offenders.map(this.parseOffender); + var deduplicatedColors = {}; + + parsedOffenders.forEach(function(color) { + deduplicatedColors[color] = color; + }); + + return Object.keys(deduplicatedColors).map(this.parseColor); + }; + + this.parseOffender = function(offender) { + var regexResult = /^(.*) \(\d+ times\)$/.exec(offender); + return regexResult[1]; + }; + + this.parseColor = function(color) { + var colorArray = parse(color).rgba; + + var obj = { + R: colorArray[0], + G: colorArray[1], + B: colorArray[2], + A: colorArray[3] + }; + + obj.Lab = diff.rgb_to_lab(obj); + obj.original = color; + + return obj; + }; + + this.compareTwoColors = function(color1, color2) { + if (color1.A !== color2.A) { + debug('Comparison not possible, because the alpha channel is different'); + return false; + } + + var delta = diff.diff(color1.Lab, color2.Lab); + debug('Delta is %d', delta); + + return delta <= COLOR_DIFF_THRESHOLD; + }; + +}; + +module.exports = new colorDiff(); \ No newline at end of file diff --git a/package.json b/package.json index 3a792b5..ea62935 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "body-parser": "1.13.3", "chart.js": "1.0.2", "clean-css": "3.4.2", + "color-diff": "0.1.7", "compression": "1.5.2", "cors": "2.7.1", "debug": "2.2.0", @@ -38,6 +39,7 @@ "lwip": "0.0.7", "meow": "3.3.0", "minimize": "1.7.1", + "parse-color": "1.0.0", "phantomas": "1.12.0", "ps-node": "0.0.4", "q": "1.4.1", diff --git a/test/core/colorDiffTest.js b/test/core/colorDiffTest.js new file mode 100644 index 0000000..d26aad1 --- /dev/null +++ b/test/core/colorDiffTest.js @@ -0,0 +1,92 @@ +var should = require('chai').should(); +var colorDiff = require('../../lib/tools/colorDiff'); + +describe('colorDiff', function() { + + it('should parse offenders correctly', function() { + colorDiff.parseOffender('#000 (1 times)').should.equal('#000'); + colorDiff.parseOffender('#5bc0de (2 times)').should.equal('#5bc0de'); + colorDiff.parseOffender('rgba(0,0,0,0.075) (100 times)').should.equal('rgba(0,0,0,0.075)'); + colorDiff.parseOffender('rgb(91,192,222) (1000 times)').should.equal('rgb(91,192,222)'); + }); + + it('should parse colors correctly', function() { + colorDiff.parseColor('#000').should.deep.equal({R: 0, G: 0, B: 0, A: 1, Lab: {L: 0, a: 0, b: 0}, original: '#000'}); + colorDiff.parseColor('rgba(255,255,255,0.5)').should.deep.equal({R: 255, G: 255, B: 255, A: 0.5, Lab: {L: 100, a: 0.00526049995830391, b: -0.010408184525267927}, original: 'rgba(255,255,255,0.5)'}); + }); + + it('should not compare colors with different alpha', function() { + var color1 = colorDiff.parseColor("rgba(0, 0, 0, 1)"); + var color2 = colorDiff.parseColor("rgba(0, 0, 0, 0.5)"); + + colorDiff.compareTwoColors(color1, color2).should.equal(false); + }); + + it('should find that two colors are similar', function() { + var color1 = colorDiff.parseColor("rgba(0, 0, 0, 1)"); + var color2 = colorDiff.parseColor("rgba(0, 0, 1, 1)"); + + colorDiff.compareTwoColors(color1, color2).should.equal(true); + }); + + it('should find that two colors are similar even with alpha', function() { + var color1 = colorDiff.parseColor("rgba(0, 0, 0, 0.3)"); + var color2 = colorDiff.parseColor("rgba(0, 0, 2, 0.3)"); + + colorDiff.compareTwoColors(color1, color2).should.equal(true); + }); + + it('should find that two colors are different', function() { + var color1 = colorDiff.parseColor("rgba(0, 0, 0, 1)"); + var color2 = colorDiff.parseColor("rgba(99, 99, 99, 1)"); + + colorDiff.compareTwoColors(color1, color2).should.equal(false); + }); + + it('should compare all colors to each other', function() { + var colors = [ + '#000 (1 times)', + '#5bc0de (2 times)', + 'rgba(0,0,0,0.075) (100 times)', + 'rgb(91,192,222) (1000 times)', + 'rgba(0,0,2,1) (1 times)', + 'rgba(99,99,99,1) (1 times)', + 'rgba(100,100,100,1) (1 times)' + ]; + + var data = { + toolsResults: { + phantomas: { + offenders: { + cssColors: colors + } + } + } + }; + + newData = colorDiff.compareAllColors(data); + + newData.toolsResults.should.have.a.property('colorDiff'); + newData.toolsResults.colorDiff.should.have.a.property('metrics'); + newData.toolsResults.colorDiff.metrics.should.have.a.property('similarColors').that.equals(3); + newData.toolsResults.colorDiff.should.have.a.property('offenders'); + newData.toolsResults.colorDiff.offenders.should.have.a.property('similarColors').that.deep.equals([ + { + color1: '#000', + color2: 'rgba(0,0,2,1)', + isDark: true + }, + { + color1: '#5bc0de', + color2: 'rgb(91,192,222)', + isDark: false + }, + { + color1: 'rgba(99,99,99,1)', + color2: 'rgba(100,100,100,1)', + isDark: true + } + ]); + }); + +});