Skip to content
This repository has been archived by the owner on Jan 12, 2024. It is now read-only.

Perceptual color support #422

Merged
merged 5 commits into from
Mar 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions docs-generator/index._
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Below is a list of values and an explanation of any expression that can be appli

### Color

CartoCSS accepts a variety of syntaxes for colors - HTML-style hex values, rgb, rgba, hsl, and hsla. It also supports the predefined HTML colors names, like `yellow` and `blue`.
CartoCSS accepts a variety of syntaxes for colors - HTML-style hex values, rgb, rgba, hsl, hsla, husl, and husla. It also supports the predefined HTML colors names, like `yellow` and `blue`.

``` css
#line {
Expand All @@ -19,11 +19,13 @@ line-color: rgb(255, 255, 0);
line-color: rgba(255, 255, 0, 1);
line-color: hsl(100, 50%, 50%);
line-color: hsla(100, 50%, 50%, 1);
line-color: husl(100, 50%, 50%); // same values yield different color than HSL
line-color: husla(100, 50%, 50%, 1);
line-color: yellow;
}
```

Especially of note is the support for hsl, which can be [easier to reason about than rgb()](http://mothereffinghsl.com/). Carto also includes several color operation functions [borrowed from less](http://lesscss.org/functions/#color-operations):
Especially of note is the support for hsl and husl, which can be [easier to reason about than rgb()](http://mothereffinghsl.com/). Carto also includes several color operation functions [borrowed from less](http://lesscss.org/functions/#color-operations):

``` css
// lighten and darken colors
Expand All @@ -43,9 +45,16 @@ spin(#ff00ff, 10);

// mix generates a color in between two other colors.
mix(#fff, #000, 50%);

// get color components
hue(#ff00ff);
saturation(#ff00ff);
lightness(#ff00ff);
alpha(hsla(100, 50%, 50%, 0.5));
```

These functions all take arguments which can be color variables, literal colors, or the results of other functions operating on colors.
These functions all take arguments which can be color variables, literal colors, or the results of other functions operating on colors. All the above mentioned functions also come in
a `functionp`-variant (e.g. `lightenp`), which force a given color into perceptual color space.

### Float

Expand Down
251 changes: 176 additions & 75 deletions lib/carto/functions.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
var chroma = require('chroma-js');

(function (tree) {

tree.functions = {
Expand All @@ -8,7 +10,17 @@ tree.functions = {
var rgb = [r, g, b].map(function (c) { return number(c); });
a = number(a);
if (rgb.some(isNaN) || isNaN(a)) return null;
return new tree.Color(rgb, a);

this.rgb[0] = Math.max(0, Math.min(this.rgb[0], 255));
this.rgb[1] = Math.max(0, Math.min(this.rgb[1], 255));
this.rgb[2] = Math.max(0, Math.min(this.rgb[2], 255));

var hsl = chroma(rgb).hsl();
if (isNaN(hsl[0])) {
hsl[0] = 0;
}

return new tree.Color(hsl, a);
},
// Only require val
stop: function (val) {
Expand All @@ -33,98 +45,170 @@ tree.functions = {
return this.hsla(h, s, l, 1.0);
},
hsla: function (h, s, l, a) {
h = (number(h) % 360) / 360;
s = number(s); l = number(l); a = number(a);
if ([h, s, l, a].some(isNaN)) return null;

var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s,
m1 = l * 2 - m2;

return this.rgba(hue(h + 1/3) * 255,
hue(h) * 255,
hue(h - 1/3) * 255,
a);

function hue(h) {
h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h);
if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
else if (h * 2 < 1) return m2;
else if (h * 3 < 2) return m1 + (m2 - m1) * (2/3 - h) * 6;
else return m1;
}
var hsl = [h, s, l].map(function (c) { return number(c); });
a = number(a);
if (hsl.some(isNaN) || isNaN(a)) return null;

return new tree.Color(hsl, a, false);
},
husl: function (h, s, l) {
return this.husla(h, s, l, 1.0);
},
husla: function (h, s, l, a) {
var hsl = [h, s, l].map(function (c) { return number(c); });
a = number(a);
if (hsl.some(isNaN) || isNaN(a)) return null;

return new tree.Color(hsl, a, true);
},
hue: function (color) {
if (!('toHSL' in color)) return null;
return new tree.Dimension(Math.round(color.toHSL().h));
if (!('getComponents' in color)) return null;
var comp = color.getComponents();
if (comp.perceptual) {
if (!('toStandard' in color)) return null;
color = color.toStandard();
comp = color.getComponents();
}
return new tree.Dimension(Math.round(comp.h));
},
huep: function (color) {
if (!('getComponents' in color)) return null;
var comp = color.getComponents();
if (!comp.perceptual) {
if (!('toPerceptual' in color)) return null;
color = color.toPerceptual();
comp = color.getComponents();
}
return new tree.Dimension(Math.round(comp.h));
},
saturation: function (color) {
if (!('toHSL' in color)) return null;
return new tree.Dimension(Math.round(color.toHSL().s * 100), '%');
if (!('getComponents' in color)) return null;
var comp = color.getComponents();
if (comp.perceptual) {
if (!('toStandard' in color)) return null;
color = color.toStandard();
comp = color.getComponents();
}
return new tree.Dimension(Math.round(comp.s * 100), '%');
},
saturationp: function (color) {
if (!('getComponents' in color)) return null;
var comp = color.getComponents();
if (!comp.perceptual) {
if (!('toPerceptual' in color)) return null;
color = color.toPerceptual();
comp = color.getComponents();
}
return new tree.Dimension(Math.round(comp.s * 100), '%');
},
lightness: function (color) {
if (!('toHSL' in color)) return null;
return new tree.Dimension(Math.round(color.toHSL().l * 100), '%');
if (!('getComponents' in color)) return null;
var comp = color.getComponents();
if (comp.perceptual) {
if (!('toStandard' in color)) return null;
color = color.toStandard();
comp = color.getComponents();
}
return new tree.Dimension(Math.round(comp.l * 100), '%');
},
lightnessp: function (color) {
if (!('getComponents' in color)) return null;
var comp = color.getComponents();
if (!comp.perceptual) {
if (!('toPerceptual' in color)) return null;
color = color.toPerceptual();
comp = color.getComponents();
}
return new tree.Dimension(Math.round(comp.l * 100), '%');
},
alpha: function (color) {
if (!('toHSL' in color)) return null;
return new tree.Dimension(color.toHSL().a);
if (!('getComponents' in color)) return null;
var comp = color.getComponents();
return new tree.Dimension(comp.a);
},
saturate: function (color, amount) {
if (!('toHSL' in color)) return null;
var hsl = color.toHSL();
if (!('getComponents' in color)) return null;
var comp = color.getComponents();

hsl.s += amount.value / 100;
hsl.s = clamp(hsl.s);
return hsla(hsl);
comp.s += amount.value / 100;
comp.s = clamp(comp.s);
return new tree.Color([comp.h, comp.s, comp.l], comp.a, comp.perceptual);
},
saturatep: function (color, amount) {
if (!('toPerceptual' in color)) return null;
return this.saturate(color.toPerceptual(), amount);
},
desaturate: function (color, amount) {
if (!('toHSL' in color)) return null;
var hsl = color.toHSL();
if (!('getComponents' in color)) return null;
var comp = color.getComponents();

hsl.s -= amount.value / 100;
hsl.s = clamp(hsl.s);
return hsla(hsl);
comp.s -= amount.value / 100;
comp.s = clamp(comp.s);
return new tree.Color([comp.h, comp.s, comp.l], comp.a, comp.perceptual);
},
desaturatep: function (color, amount) {
if (!('toPerceptual' in color)) return null;
return this.desaturate(color.toPerceptual(), amount);
},
lighten: function (color, amount) {
if (!('toHSL' in color)) return null;
var hsl = color.toHSL();
if (!('getComponents' in color)) return null;
var comp = color.getComponents();

hsl.l += amount.value / 100;
hsl.l = clamp(hsl.l);
return hsla(hsl);
comp.l += amount.value / 100;
comp.l = clamp(comp.l);
return new tree.Color([comp.h, comp.s, comp.l], comp.a, comp.perceptual);
},
lightenp: function (color, amount) {
if (!('toPerceptual' in color)) return null;
return this.lighten(color.toPerceptual(), amount);
},
darken: function (color, amount) {
if (!('toHSL' in color)) return null;
var hsl = color.toHSL();
if (!('getComponents' in color)) return null;
var comp = color.getComponents();

hsl.l -= amount.value / 100;
hsl.l = clamp(hsl.l);
return hsla(hsl);
comp.l -= amount.value / 100;
comp.l = clamp(comp.l);
return new tree.Color([comp.h, comp.s, comp.l], comp.a, comp.perceptual);
},
darkenp: function (color, amount) {
if (!('toPerceptual' in color)) return null;
return this.darken(color.toPerceptual(), amount);
},
fadein: function (color, amount) {
if (!('toHSL' in color)) return null;
var hsl = color.toHSL();
if (!('getComponents' in color)) return null;
var comp = color.getComponents();

hsl.a += amount.value / 100;
hsl.a = clamp(hsl.a);
return hsla(hsl);
comp.a += amount.value / 100;
comp.a = clamp(comp.a);
return new tree.Color([comp.h, comp.s, comp.l], comp.a, comp.perceptual);
},
fadeinp: function (color, amount) {
if (!('toPerceptual' in color)) return null;
return this.fadein(color.toPerceptual(), amount);
},
fadeout: function (color, amount) {
if (!('toHSL' in color)) return null;
var hsl = color.toHSL();
if (!('getComponents' in color)) return null;
var comp = color.getComponents();

hsl.a -= amount.value / 100;
hsl.a = clamp(hsl.a);
return hsla(hsl);
comp.a -= amount.value / 100;
comp.a = clamp(comp.a);
return new tree.Color([comp.h, comp.s, comp.l], comp.a, comp.perceptual);
},
fadeoutp: function (color, amount) {
if (!('toPerceptual' in color)) return null;
return this.fadeout(color.toPerceptual(), amount);
},
spin: function (color, amount) {
if (!('toHSL' in color)) return null;
var hsl = color.toHSL();
var hue = (hsl.h + amount.value) % 360;

hsl.h = hue < 0 ? 360 + hue : hue;
if (!('getComponents' in color)) return null;
var comp = color.getComponents();

return hsla(hsl);
var hue = (comp.h + amount.value) % 360;
comp.h = hue < 0 ? 360 + hue : hue;
return new tree.Color([comp.h, comp.s, comp.l], comp.a, comp.perceptual);
},
spinp: function (color, amount) {
if (!('toPerceptual' in color)) return null;
return this.spin(color.toPerceptual(), amount);
},
replace: function (entity, a, b) {
if (entity.is === 'field') {
Expand All @@ -138,23 +222,44 @@ tree.functions = {
// http://sass-lang.com
//
mix: function (color1, color2, weight) {
if (!('getComponents' in color1) || !('getComponents' in color2)) return null;

var p = weight.value / 100.0;
var w = p * 2 - 1;
var a = color1.toHSL().a - color2.toHSL().a;
var comp1 = color1.getComponents();
var comp2 = color2.getComponents();
var perceptual = comp1.perceptual || comp2.perceptual;
var a = comp1.a - comp2.a;

if (comp1.perceptual && !comp2.perceptual) {
comp2 = color2.toPerceptual().getComponents();
}
else if (!comp1.perceptual && comp2.perceptual) {
comp1 = color1.toPerceptual().getComponents();
}

var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;
var w2 = 1 - w1;

var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2,
color1.rgb[1] * w1 + color2.rgb[1] * w2,
color1.rgb[2] * w1 + color2.rgb[2] * w2];
var hsl = [comp1.h * w1 + comp2.h * w2,
comp1.s * w1 + comp2.s * w2,
comp1.l * w1 + comp2.l * w2];

var alpha = color1.alpha * p + color2.alpha * (1 - p);
var alpha = comp1.a * p + comp2.a * (1 - p);

return new tree.Color(rgb, alpha);
return new tree.Color(hsl, alpha, perceptual);
},
greyscale: function (color) {
return this.desaturate(color, new tree.Dimension(100));
if (!('getComponents' in color)) return null;
var comp = color.getComponents();

comp.s -= 1;
comp.s = clamp(comp.s);
return new tree.Color([comp.h, comp.s, comp.l], comp.a, comp.perceptual);
},
greyscalep: function (color) {
if (!('toPerceptual' in color)) return null;
return this.greyscale(color.toPerceptual());
},
'%': function (quoted /* arg, arg, ...*/) {
var args = Array.prototype.slice.call(arguments, 1),
Expand Down Expand Up @@ -190,10 +295,6 @@ tree.functions['scale-hsla'] = function(h0,h1,s0,s1,l0,l1,a0,a1) {
return new tree.ImageFilter('scale-hsla', [h0,h1,s0,s1,l0,l1,a0,a1]);
};

function hsla(h) {
return tree.functions.hsla(h.h, h.s, h.l, h.a);
}

function number(n) {
if (n instanceof tree.Dimension) {
return parseFloat(n.unit == '%' ? n.value / 100 : n.value);
Expand Down
14 changes: 11 additions & 3 deletions lib/carto/parser.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var carto = exports,
tree = require('./tree'),
_ = require('lodash');
_ = require('lodash'),
chroma = require('chroma-js');

// Token matching is done with the `$` function, which either takes
// a terminal string or regexp, or a non-terminal function to call.
Expand Down Expand Up @@ -452,14 +453,21 @@ carto.Parser = function Parser(env) {
hexcolor: function() {
var rgb;
if (input.charAt(i) === '#' && (rgb = $(/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/))) {
return new tree.Color(rgb[1]);
var hsl = chroma(rgb[0]).hsl();
return new tree.Color(hsl, 1, false);
}
},

keywordcolor: function() {
var rgb = chunks[j].match(/^[a-z]+/);
if (rgb && rgb[0] in tree.Reference.data.colors) {
return new tree.Color(tree.Reference.data.colors[$(/^[a-z]+/)]);
var data = tree.Reference.data.colors[$(/^[a-z]+/)];
var a = 1;
if (data.length > 3) {
a = data[3];
}
var hsl = chroma(data.slice(0, 3)).hsl();
return new tree.Color(hsl, a, false);
}
},

Expand Down
Loading