Skip to content

Commit

Permalink
Color interpolation (#26)
Browse files Browse the repository at this point in the history
Support a `colorSpace` property on functions that allows
users to choose LAB or HCL color interpolation.
  • Loading branch information
tmcw authored Oct 5, 2016
1 parent 85602e2 commit 111a2b4
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 5 deletions.
32 changes: 32 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,35 @@ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

--------------------------------------------------------------------------------

Contains a portion of d3-color https://github.com/d3/d3-color

Copyright 2010-2016 Mike Bostock
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Neither the name of the author nor the names of contributors may be used to
endorse or promote products derived from this software without specific prior
written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
98 changes: 98 additions & 0 deletions color_spaces.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Constants
var Kn = 18,
Xn = 0.950470, // D65 standard referent
Yn = 1,
Zn = 1.088830,
t0 = 4 / 29,
t1 = 6 / 29,
t2 = 3 * t1 * t1,
t3 = t1 * t1 * t1,
deg2rad = Math.PI / 180,
rad2deg = 180 / Math.PI;

// Utilities
function xyz2lab(t) {
return t > t3 ? Math.pow(t, 1 / 3) : t / t2 + t0;
}

function lab2xyz(t) {
return t > t1 ? t * t * t : t2 * (t - t0);
}

function xyz2rgb(x) {
return 255 * (x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055);
}

function rgb2xyz(x) {
return (x /= 255) <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
}

// LAB
function rgbToLab(rgbColor) {
var b = rgb2xyz(rgbColor[0]),
a = rgb2xyz(rgbColor[1]),
l = rgb2xyz(rgbColor[2]),
x = xyz2lab((0.4124564 * b + 0.3575761 * a + 0.1804375 * l) / Xn),
y = xyz2lab((0.2126729 * b + 0.7151522 * a + 0.0721750 * l) / Yn),
z = xyz2lab((0.0193339 * b + 0.1191920 * a + 0.9503041 * l) / Zn);

return [
116 * y - 16,
500 * (x - y),
200 * (y - z),
rgbColor[3]
];
}

function labToRgb(labColor) {
var y = (labColor[0] + 16) / 116,
x = isNaN(labColor[1]) ? y : y + labColor[1] / 500,
z = isNaN(labColor[2]) ? y : y - labColor[2] / 200;
y = Yn * lab2xyz(y);
x = Xn * lab2xyz(x);
z = Zn * lab2xyz(z);
return [
xyz2rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z), // D65 -> sRGB
xyz2rgb(-0.9692660 * x + 1.8760108 * y + 0.0415560 * z),
xyz2rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z),
labColor[3]
];
}

// HCL
function rgbToHcl(rgbColor) {
var labColor = rgbToLab(rgbColor);
var l = labColor[0],
a = labColor[1],
b = labColor[2];
var h = Math.atan2(b, a) * rad2deg;
return [
h < 0 ? h + 360 : h,
Math.sqrt(a * a + b * b),
l,
rgbColor[3]
];
}

function hclToRgb(hclColor) {
var h = hclColor[0] * deg2rad,
c = hclColor[1],
l = hclColor[2];
return labToRgb([
l,
Math.cos(h) * c,
Math.sin(h) * c,
hclColor[3]
]);
}

module.exports ={
lab: {
forward: rgbToLab,
reverse: labToRgb
},
hcl: {
forward: rgbToHcl,
reverse: hclToRgb
}
};
43 changes: 39 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
'use strict';

var colorSpaces = require('./color_spaces');

function identityFunction(x) {
return x;
}

function createFunction(parameters, defaultType) {
var fun;

Expand Down Expand Up @@ -27,10 +33,35 @@ function createFunction(parameters, defaultType) {
throw new Error('Unknown function type "' + type + '"');
}

var outputFunction;

// If we're interpolating colors in a color system other than RGBA,
// first translate all stop values to that color system, then interpolate
// arrays as usual. The `outputFunction` option lets us then translate
// the result of that interpolation back into RGBA.
if (parameters.colorSpace && parameters.colorSpace !== 'rgb') {
if (colorSpaces[parameters.colorSpace]) {
var colorspace = colorSpaces[parameters.colorSpace];
// Avoid mutating the parameters value
parameters = JSON.parse(JSON.stringify(parameters));
for (var s = 0; s < parameters.stops.length; s++) {
parameters.stops[s] = [
parameters.stops[s][0],
colorspace.forward(parameters.stops[s][1])
];
}
outputFunction = colorspace.reverse;
} else {
throw new Error('Unknown color space: ' + parameters.colorSpace);
}
} else {
outputFunction = identityFunction;
}

if (zoomAndFeatureDependent) {
var featureFunctions = {};
var featureFunctionStops = [];
for (var s = 0; s < parameters.stops.length; s++) {
for (s = 0; s < parameters.stops.length; s++) {
var stop = parameters.stops[s];
if (featureFunctions[stop[0].zoom] === undefined) {
featureFunctions[stop[0].zoom] = {
Expand All @@ -47,20 +78,24 @@ function createFunction(parameters, defaultType) {
featureFunctionStops.push([featureFunctions[z].zoom, createFunction(featureFunctions[z])]);
}
fun = function(zoom, feature) {
return evaluateExponentialFunction({ stops: featureFunctionStops, base: parameters.base }, zoom)(zoom, feature);
return outputFunction(evaluateExponentialFunction({
stops: featureFunctionStops,
base: parameters.base
}, zoom)(zoom, feature));
};
fun.isFeatureConstant = false;
fun.isZoomConstant = false;

} else if (zoomDependent) {
fun = function(zoom) {
return innerFun(parameters, zoom);
return outputFunction(innerFun(parameters, zoom));
};
fun.isFeatureConstant = true;
fun.isZoomConstant = false;
} else {
fun = function(zoom, feature) {
return innerFun(parameters, feature[parameters.property]);
return outputFunction(
innerFun(parameters, feature[parameters.property]));
};
fun.isFeatureConstant = false;
fun.isZoomConstant = true;
Expand Down
55 changes: 54 additions & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ var MapboxGLFunction = require('../').interpolated;

test('function types', function(t) {

t.test('contant', function(t) {
t.test('constant', function(t) {

t.test('range types', function(t) {

Expand Down Expand Up @@ -107,6 +107,59 @@ test('function types', function(t) {
t.end();
});

t.test('three elements', function(t) {
var f = MapboxGLFunction({
type: 'exponential',
colorSpace: 'lab',
stops: [[1, [0, 0, 0, 1]], [10, [0, 1, 1, 1]]]
});

t.deepEqual(f(0), [0, 0, 0, 1]);
t.deepEqual(f(5).map(function (n) {
return parseFloat(n.toFixed(3));
}), [0, 0.444, 0.444, 1]);

t.end();
});

t.test('rgb colorspace', function(t) {
var f = MapboxGLFunction({
type: 'exponential',
colorSpace: 'rgb',
stops: [[0, [0, 0, 0, 1]], [10, [1, 1, 1, 1]]]
});

t.deepEqual(f(5).map(function (n) {
return parseFloat(n.toFixed(3));
}), [0.5, 0.5, 0.5, 1]);

t.end();
});

t.test('unknown color spaces', function(t) {
t.throws(function () {
MapboxGLFunction({
type: 'exponential',
colorSpace: 'unknown',
stops: [[1, [0, 0, 0, 1]], [10, [0, 1, 1, 1]]]
});
}, 'Unknown color space: unknown');

t.end();
});

t.test('interpolation mutation avoidance', function(t) {
var params = {
type: 'exponential',
colorSpace: 'lab',
stops: [[1, [0, 0, 0, 1]], [10, [0, 1, 1, 1]]]
};
var paramsCopy = JSON.parse(JSON.stringify(params));
MapboxGLFunction(params);
t.deepEqual(params, paramsCopy);
t.end();
});

});

t.test('zoom + data stops', function(t) {
Expand Down

0 comments on commit 111a2b4

Please sign in to comment.