浏览代码

New rule: similar colors

Gaël Métais 9 年之前
父节点
当前提交
d0257fea8f

+ 11 - 0
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;

+ 13 - 0
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;

+ 4 - 0
front/src/views/rule.html

@@ -96,6 +96,10 @@
                         </span>
                     </div>
 
+                    <div ng-if="policyName === 'similarColors'">
+                        <div class="similarColors checker"><div ng-style="{'background-color': offender.color1, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color1}}</div><div ng-style="{'background-color': offender.color2, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color2}}</div></div>
+                    </div>
+
                     <div ng-if="policyName === 'cssParsingErrors'">
                         <b>{{offender.error}}</b>
                         <file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>

+ 17 - 2
lib/metadata/policies.js

@@ -463,7 +463,7 @@ var policies = {
     },
     "cssColors": {
         "tool": "phantomas",
-        "label": "Different colors",
+        "label": "Colors count",
         "message": "<p>This is the number of different colors defined in CSS.</p><p>Your CSS project will be easier to maintain if you keep a small color set.</p>",
         "isOkThreshold": 30,
         "isBadThreshold": 150,
@@ -507,6 +507,21 @@ var policies = {
             };
         }
     },
+    "similarColors": {
+        "tool": "colorDiff",
+        "label": "Similar colors",
+        "message": "<p>This is the list of colors found in the stylesheets, that are very close to each other. The eye can barely see the difference.</p><p>Use this list to reduce the number of colors in your palette, it will be easier to maintain.</p>",
+        "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": "<p>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.</p><p>Trackers are an exception, you'd better hide them.</p>",
-        "isOkThreshold": 3,
+        "isOkThreshold": 1,
         "isBadThreshold": 12,
         "isAbnormalThreshold": 30,
         "hasOffenders": true

+ 2 - 1
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": {

+ 4 - 0
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);
 

+ 90 - 0
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();

+ 2 - 0
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",

+ 92 - 0
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
+            }
+        ]);
+    });
+
+});