Skip to content

Instantly share code, notes, and snippets.

@connorgr
Last active November 16, 2016 20:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save connorgr/0a299fe77d5c7feccd22e02f2ac5d69b to your computer and use it in GitHub Desktop.
Save connorgr/0a299fe77d5c7feccd22e02f2ac5d69b to your computer and use it in GitHub Desktop.
// https://github.com/connorgr/d3-cam02 Version 0.1.0. Copyright 2016 Connor Gramazio.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-color')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-color'], factory) :
(factory((global.d3 = global.d3 || {}),global.d3));
}(this, (function (exports,d3Color) { 'use strict';
var deg2rad = Math.PI / 180;
var rad2deg = 180 / Math.PI;
// Implementation based on Billy Bigg's CIECAM02 implementation in C
// (http://scanline.ca/ciecam02/)
// and information on Wikipedia (https://en.wikipedia.org/wiki/CIECAM02)
//
// IMPORTANT NOTE : uses XYZ [0,100] not [0,1]
//
// When transforming colors into CIECAM02 space we use Luo et al.'s uniform
// color space transform; however, we also provide commented out transform
// coefficients for the long-distance and short-distance CIECAM02 transforms,
// should others desire to use these alternative perceptually uniform
// approximation spaces instead.
//
// Another important note is that we provide the full range of CIECAM02 color
// values in the JCh constructor, but the d3 color object stores only lightness
// (J), chroma (C), and hue (h).
//
// used for brighter and darker functions
// Kn is completely arbitrary and was picked originally by Mike Bostock to make
// the Lab brighter and darker functions behave similarly to the RGB equivalents
// in d3-color. We copy and paste the value directly and encourage others to
// add a more systematically chosen value.
var Kn = 18;
// Conversion functions
function rgb2xyz(r, g, b) {
r = r / 255.0;
g = g / 255.0;
b = b / 255.0;
// assume sRGB
r = r > 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92);
g = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92);
b = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92);
// Convert to XYZ in [0,100] rather than [0,1]
return {
x: ( (r * 0.4124) + (g * 0.3576) + (b * 0.1805) ) * 100.0,
y: ( (r * 0.2126) + (g * 0.7152) + (b * 0.0722) ) * 100.0,
z: ( (r * 0.0193) + (g * 0.1192) + (b * 0.9505) ) * 100.0
};
}
function xyz2rgb(x, y, z) {
x = x / 100.0;
y = y / 100.0;
z = z / 100.0;
var preR = x * 3.2404542 + y * -1.5371385 - z * 0.4985314,
preG = x * -0.9692660 + y * 1.8760108 + z * 0.0415560,
preB = x * 0.0556434 + y * -0.2040259 + z * 1.0572252;
function toRGB(c) {
return 255.0 * (c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055);
}
return {r: toRGB(preR), g: toRGB(preG), b: toRGB(preB)};
}
function xyz2cat02(x,y,z) {
var l = ( 0.7328 * x) + (0.4296 * y) - (0.1624 * z),
m = (-0.7036 * x) + (1.6975 * y) + (0.0061 * z),
s = ( 0.0030 * x) + (0.0136 * y) + (0.9834 * z);
return {l: l, m: m, s: s};
}
function cat022xyz(l, m, s) {
var x = ( 1.096124 * l) - (0.278869 * m) + (0.182745 * s),
y = ( 0.454369 * l) + (0.473533 * m) + (0.072098 * s),
z = (-0.009628 * l) - (0.005698 * m) + (1.015326 * s);
return {x: x, y: y, z:z};
}
function cat022hpe(l,m,s) {
var lh = ( 0.7409792 * l) + (0.2180250 * m) + (0.0410058 * s),
mh = ( 0.2853532 * l) + (0.6242014 * m) + (0.0904454 * s),
sh = (-0.0096280 * l) - (0.0056980 * m) + (1.0153260 * s);
return {lh: lh, mh: mh, sh: sh};
}
function hpe2xyz(l, m, s) {
var x = (1.910197 * l) - (1.112124 * m) + (0.201908 * s),
y = (0.370950 * l) + (0.629054 * m) - (0.000008 * s),
z = s;
return {x:x, y:y, z:z};
}
function nonlinearAdaptation(coneResponse, fl) {
var p = Math.pow( (fl * coneResponse) / 100.0, 0.42 );
return ((400.0 * p) / (27.13 + p)) + 0.1;
}
function inverseNonlinearAdaptation(coneResponse, fl) {
return (100.0 / fl) *
Math.pow((27.13 * Math.abs(coneResponse - 0.1)) /
(400.0 - Math.abs(coneResponse - 0.1)),
1.0 / 0.42);
}
// CIECAM02_VC viewing conditions; assumes average viewing conditions
var CIECAM02_VC = (function() {
var vc = {
D65_X: 95.047, // D65 standard referent
D65_Y: 100.0,
D65_Z: 108.883,
// Viewing conditions
// Note about L_A:
// Billy Bigg's implementation just uses a value of 4 cd/m^2, but
// the colorspacious implementation uses greater precision by calculating
// it with (64 / numpy.pi) / 5
// This is based on Moroney (2000), "Usage guidelines for CIECAM97s" where
// sRGB illuminance is 64 lux. Because of its greater precision we use
// Moroney's alternative definition.
la: (64.0 / Math.PI) / 5.0,
yb: 20.0, // 20% gray
// Surround
f: 1.0, // average; dim: 0.9; dark: 0.8
c: 0.69, // average; dim: 0.59; dark: 0.525
nc: 1.0 // average; dim: 0.95; dark: 0.8
};
vc.D65_LMS = xyz2cat02(vc.D65_X, vc.D65_Y, vc.D65_Z),
vc.n = vc.yb / vc.D65_Y;
vc.z = 1.48 + Math.sqrt(vc.n);
var k = 1.0 / ((5.0 * vc.la) + 1.0);
vc.fl = (0.2 * Math.pow(k, 4.0) * (5.0 * vc.la)) +
0.1 * Math.pow(1.0 - Math.pow(k, 4.0), 2.0) *
Math.pow(5.0 * vc.la, 1.0/3.0);
vc.nbb = 0.725 * Math.pow(1.0 / vc.n, 0.2);
vc.ncb = vc.nbb;
vc.d = vc.f * ( 1.0 - (1.0 / 3.6) * Math.exp((-vc.la - 42.0) / 92.0) );
vc.achromaticResponseToWhite = (function() {
var l = vc.D65_LMS.l,
m = vc.D65_LMS.m,
s = vc.D65_LMS.s;
var lc = l * (((vc.D65_Y * vc.d) / l) + (1.0 - vc.d)),
mc = m * (((vc.D65_Y * vc.d) / m) + (1.0 - vc.d)),
sc = s * (((vc.D65_Y * vc.d) / s) + (1.0 - vc.d));
var hpeTransforms = cat022hpe(lc, mc, sc),
lp = hpeTransforms.lh,
mp = hpeTransforms.mh,
sp = hpeTransforms.sh;
var lpa = nonlinearAdaptation(lp, vc.fl),
mpa = nonlinearAdaptation(mp, vc.fl),
spa = nonlinearAdaptation(sp, vc.fl);
return (2.0 * lpa + mpa + 0.05 * spa - 0.305) * vc.nbb;
})();
return vc;
})(); // end CIECAM02_VC
function cat022cam02(l,m,s) {
var theColor = {};
var D65_CAT02 = xyz2cat02(CIECAM02_VC.D65_X,CIECAM02_VC.D65_Y,CIECAM02_VC.D65_Z);
function cTransform(cone, D65_cone) {
var D65_Y = CIECAM02_VC.D65_Y,
VC_d = CIECAM02_VC.d;
return cone * (((D65_Y * VC_d) / D65_cone) + (1.0 - VC_d));
}
var lc = cTransform(l, D65_CAT02.l),
mc = cTransform(m, D65_CAT02.m),
sc = cTransform(s, D65_CAT02.s);
var hpeTransforms = cat022hpe(lc, mc, sc),
lp = hpeTransforms.lh,
mp = hpeTransforms.mh,
sp = hpeTransforms.sh;
var lpa = nonlinearAdaptation(lp, CIECAM02_VC.fl),
mpa = nonlinearAdaptation(mp, CIECAM02_VC.fl),
spa = nonlinearAdaptation(sp, CIECAM02_VC.fl);
var ca = lpa - ((12.0*mpa) / 11.0) + (spa / 11.0),
cb = (1.0/9.0) * (lpa + mpa - 2.0*spa);
theColor.h = (180.0 / Math.PI) * Math.atan2(cb, ca);
if(theColor.h < 0.0) theColor.h += 360.0;
var temp;
if(theColor.h < 20.14) {
temp = ((theColor.h + 122.47)/1.2) + ((20.14 - theColor.h)/0.8);
theColor.H = 300 + (100*((theColor.h + 122.47)/1.2)) / temp;
} else if(theColor.h < 90.0) {
temp = ((theColor.h - 20.14)/0.8) + ((90.00 - theColor.h)/0.7);
theColor.H = (100*((theColor.h - 20.14)/0.8)) / temp;
} else if(theColor.h < 164.25) {
temp = ((theColor.h - 90.00)/0.7) + ((164.25 - theColor.h)/1.0);
theColor.H = 100 + ((100*((theColor.h - 90.00)/0.7)) / temp);
} else if (theColor.h < 237.53) {
temp = ((theColor.h - 164.25)/1.0) + ((237.53 - theColor.h)/1.2);
theColor.H = 200 + ((100*((theColor.h - 164.25)/1.0)) / temp);
} else {
temp = ((theColor.h - 237.53)/1.2) + ((360 - theColor.h + 20.14)/0.8);
theColor.H = 300 + ((100*((theColor.h - 237.53)/1.2)) / temp);
}
var a = ( 2.0*lpa + mpa + 0.05*spa - 0.305 ) * CIECAM02_VC.nbb;
theColor.J = 100.0 * Math.pow(a / CIECAM02_VC.achromaticResponseToWhite,
CIECAM02_VC.c * CIECAM02_VC.z);
var et = 0.25 * (Math.cos((theColor.h * Math.PI) / 180.0 + 2.0) + 3.8),
t = ((50000.0 / 13.0) * CIECAM02_VC.nc * CIECAM02_VC.ncb * et * Math.sqrt(ca*ca + cb*cb)) /
(lpa + mpa + (21.0/20.0)*spa);
theColor.C = Math.pow(t, 0.9) * Math.sqrt(theColor.J / 100.0) *
Math.pow(1.64 - Math.pow(0.29, CIECAM02_VC.n), 0.73);
theColor.Q = (4.0 / CIECAM02_VC.c) * Math.sqrt(theColor.J / 100.0) *
(CIECAM02_VC.achromaticResponseToWhite + 4.0) * Math.pow(CIECAM02_VC.fl, 0.25);
theColor.M = theColor.C * Math.pow(CIECAM02_VC.fl, 0.25);
theColor.s = 100.0 * Math.sqrt(theColor.M / theColor.Q);
return theColor;
}
function Aab2Cat02LMS(A, aa, bb, nbb) {
var x = (A / nbb) + 0.305;
var l = (0.32787 * x) + (0.32145 * aa) + (0.20527 * bb),
m = (0.32787 * x) - (0.63507 * aa) - (0.18603 * bb),
s = (0.32787 * x) - (0.15681 * aa) - (4.49038 * bb);
return {l:l, m:m, s:s};
}
function cam022rgb(J, C, h) {
// NOTE input is small h not big H, the later of which is corrected
var t = Math.pow(C / (Math.sqrt(J / 100.0) *
Math.pow(1.64-Math.pow(0.29, CIECAM02_VC.n), 0.73)),
(1.0 / 0.9)),
et = 1.0 / 4.0 * (Math.cos(((h * Math.PI) / 180.0) + 2.0) + 3.8);
var a = Math.pow( J / 100.0, 1.0 / (CIECAM02_VC.c * CIECAM02_VC.z) ) *
CIECAM02_VC.achromaticResponseToWhite;
var p1 = ((50000.0 / 13.0) * CIECAM02_VC.nc * CIECAM02_VC.ncb) * et / t,
p2 = (a / CIECAM02_VC.nbb) + 0.305,
p3 = 21.0 / 20.0,
p4, p5, ca, cb;
var hr = (h * Math.PI) / 180.0;
if (Math.abs(Math.sin(hr)) >= Math.abs(Math.cos(hr))) {
p4 = p1 / Math.sin(hr);
cb = (p2 * (2.0 + p3) * (460.0 / 1403.0)) /
(p4 + (2.0 + p3) * (220.0 / 1403.0) *
(Math.cos(hr) / Math.sin(hr)) - (27.0 / 1403.0) +
p3 * (6300.0 / 1403.0));
ca = cb * (Math.cos(hr) / Math.sin(hr));
}
else {
p5 = p1 / Math.cos(hr);
ca = (p2 * (2.0 + p3) * (460.0 / 1403.0)) /
(p5 + (2.0 + p3) * (220.0 / 1403.0) -
((27.0 / 1403.0) - p3 * (6300.0 / 1403.0)) *
(Math.sin(hr) / Math.cos(hr)));
cb = ca * (Math.sin(hr) / Math.cos(hr));
}
var lms_a = Aab2Cat02LMS(a, ca, cb, CIECAM02_VC.nbb),
lpa = lms_a.l,
mpa = lms_a.m,
spa = lms_a.s;
var lp = inverseNonlinearAdaptation(lpa, CIECAM02_VC.fl),
mp = inverseNonlinearAdaptation(mpa, CIECAM02_VC.fl),
sp = inverseNonlinearAdaptation(spa, CIECAM02_VC.fl);
var txyz = hpe2xyz(lp, mp, sp),
lms_c = xyz2cat02(txyz.x, txyz.y, txyz.z);
var D65_CAT02 = xyz2cat02(CIECAM02_VC.D65_X, CIECAM02_VC.D65_Y,
CIECAM02_VC.D65_Z);
var l = lms_c.l / ( ((CIECAM02_VC.D65_Y * CIECAM02_VC.d) / D65_CAT02.l) +
(1.0 - CIECAM02_VC.d) ),
m = lms_c.m / ( ((CIECAM02_VC.D65_Y * CIECAM02_VC.d) / D65_CAT02.m) +
(1.0 - CIECAM02_VC.d) ),
s = lms_c.s / ( ((CIECAM02_VC.D65_Y * CIECAM02_VC.d) / D65_CAT02.s) +
(1.0 - CIECAM02_VC.d) );
var xyz = cat022xyz(l, m, s),
rgb = xyz2rgb(xyz.x, xyz.y, xyz.z);
return rgb;
}
function jchConvert(o) {
if (o instanceof JCh) return new JCh(o.J, o.C, o.h, o.opacity);
if (!(o instanceof d3Color.rgb)) o = d3Color.rgb(o);
var xyz = rgb2xyz(o.r, o.g, o.b),
lmsConeResponses = xyz2cat02(xyz.x,xyz.y,xyz.z),
cam02obj = cat022cam02(lmsConeResponses.l,lmsConeResponses.m,
lmsConeResponses.s);
return new JCh(cam02obj.J, cam02obj.C, cam02obj.h, o.opacity);
}
function jch(J, C, h, opacity) {
return arguments.length === 1 ? jchConvert(J) : new JCh(J, C, h,
opacity == null ? 1 : opacity);
}
function JCh(J, C, h, opacity) {
this.J = +J;
this.C = +C;
this.h = +h;
this.opacity = +opacity;
}
var jchPrototype = JCh.prototype = jch.prototype = Object.create(d3Color.color.prototype);
jchPrototype.constructor = JCh;
jchPrototype.brighter = function(k) {
return new JCh(this.J + Kn * (k === null ? 1 : k), this.C, this.h,
this.opacity);
};
jchPrototype.darker = function(k) {
return new JCh(this.J - Kn * (k === null ? 1 : k), this.C, this.h,
this.opacity);
};
jchPrototype.rgb = function () {
var converted = cam022rgb(this.J, this.C, this.h);
return d3Color.rgb(converted.r, converted.g, converted.b, this.opacity);
};
////////////////////////////////////////////////////////////////////////////////
// Updated attempts at perceptually uniform color spaces
// Formulas and constants taken from
// M.R. Luo and C. Li. "CIECAM02 and Its Recent Developments"
var altCam02Coef = {
lcd: {k_l: 0.77, c1: 0.007, c2:0.0053},
scd: {k_l: 1.24, c1: 0.007, c2:0.0363},
ucs: {k_l: 1.00, c1: 0.007, c2:0.0228}
};
function jabConvert(o) {
if(o instanceof Jab) {
return new Jab(o.J, o.a, o.b, o.opacity);
}
if (!(o instanceof d3Color.rgb)) o = d3Color.rgb(o);
var xyz = rgb2xyz(o.r, o.g, o.b),
lmsConeResponses = xyz2cat02(xyz.x,xyz.y,xyz.z),
cam02 = cat022cam02(lmsConeResponses.l, lmsConeResponses.m, lmsConeResponses.s);
var coefs = altCam02Coef.ucs;
var JPrime = ((1.0 + 100.0*coefs.c1) * cam02.J) / (1.0 + coefs.c1 * cam02.J);
JPrime = JPrime / coefs.k_l;
var MPrime = (1.0/coefs.c2) * Math.log(1.0 + coefs.c2*cam02.M); // log=ln
var a = MPrime * Math.cos(deg2rad*cam02.h),
b = MPrime * Math.sin(deg2rad*cam02.h);
return new Jab(JPrime, a, b, o.opacity);
}
// DE color distance function generator for the three CAM02 perceptually uniform
// models: lcd, scd, and ucs
function cam02de(coefs) {
return function(o) {
if (!(o instanceof Jab)) o = jabConvert(o);
var k_l = coefs.k_l,
diffJ = Math.abs(this.J - o.J),
diffA = Math.abs(this.a - o.a),
diffB = Math.abs(this.b - o.b);
var de = Math.sqrt( (diffJ/k_l)*(diffJ/k_l) + diffA*diffA + diffB*diffB );
return de;
};
}
function jab(J, a, b, opacity) {
opacity = opacity == null ? 1 : opacity;
return arguments.length === 1 ? jabConvert(J) :
new Jab(J, a, b, opacity);
}
function Jab(J, a, b, opacity) {
this.J = J;
this.a = a;
this.b = b;
this.opacity = opacity;
}
var jabPrototype = Jab.prototype = jab.prototype = Object.create(d3Color.color.prototype);
jabPrototype.constructor = JCh;
jabPrototype.brighter = function(k) {
return new Jab(this.J + Kn * (k === null ? 1 : k), this.a, this.b,
this.opacity);
};
jabPrototype.darker = function(k) {
return new Jab(this.J - Kn * (k === null ? 1 : k), this.a, this.b,
this.opacity);
};
jabPrototype.rgb = function() {
var coefs = altCam02Coef.ucs;
var J = this.J, a = this.a, b = this.b;
// Get the new M using trigonomic identities
// MPrime = (1.0/coefs.c2) * Math.log(1.0 + coefs.c2*cam02.M); // log=ln
// var a = MPrime * Math.cos(o.h),
// b = MPrime * Math.sin(o.h);
// x*x = (x*cos(y))*(x(cos(y))) + (x*sin(y))*(x(sin(y)))
var newMPrime = Math.sqrt(a*a + b*b),
newM = (Math.exp(newMPrime * coefs.c2) - 1.0) / coefs.c2;
var newh = rad2deg*Math.atan2(b,a);
if(newh < 0) newh = 360.0 + newh;
// M = C * Math.pow(CIECAM02_VC.fl, 0.25);
// C = M / Math.pow(CIECAM02_VC.fl, 0.25);
var newC = newM / Math.pow(CIECAM02_VC.fl, 0.25);
// Last, derive the new Cam02J
// JPrime = ((1.0 + 100.0*coefs.c1) * cam02.J) / (1.0 + coefs.c1 * cam02.J)
// simplified: var cam02J = JPrime / (1.0 + coefs.c1*(100.0 - JPrime));
// if v = (d*x) / (b + a*x), x = (b*(v/d)) / (1 - a(v/d))
var newCam02J = J / (1.0 + coefs.c1*(100.0 - J));
var converted = cam022rgb(newCam02J, newC, newh);
return d3Color.rgb(converted.r, converted.g, converted.b, this.opacity);
};
jabPrototype.de = cam02de(altCam02Coef.ucs);
function interpolateJab(start, end) {
// constant, linear, and colorInterpolate are taken from d3-interpolate
// the colorInterpolate function is `nogamma` in the d3-interpolate's color.js
function constant(x) { return function() { return x; } }
function linear(a, d) { return function(t) { return a + t * d; }; }
function colorInterpolate(a, b) {
var d = b - a;
return d ? linear(a, d) : constant(isNaN(a) ? b : a);
}
start = jabConvert(start);
end = jabConvert(end);
// TODO import color function from d3-interpolate
var J = colorInterpolate(start.J, end.J),
a = colorInterpolate(start.a, end.a),
b = colorInterpolate(start.b, end.b),
opacity = colorInterpolate(start.opacity, end.opacity);
return function(t) {
start.J = J(t);
start.a = a(t);
start.b = b(t);
start.opacity = opacity(t);
return start + "";
};
}
exports.jch = jch;
exports.jab = jab;
exports.interpolateJab = interpolateJab;
Object.defineProperty(exports, '__esModule', { value: true });
})));
<!DOCTYPE html>
<meta charset="utf-8">
<style>
* {
font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis text {
font: 10px sans-serif;
}
.CIECAM02, .CIECAM02-UCS {
border: 1px solid #ccc;
border-radius: 5px;
display:inline-block;
margin-bottom: 15px;
padding: 0px;
text-align: center;
width: 420px;
}
.channel {
display: block;
}
.channel canvas {
float: left;
margin: 0px 10px 0 10px;
width: 400px;
height: 90px;
}
.channel span {
display: block;
border-bottom: 1px solid #ccc;
margin-bottom: 15px;
padding-bottom: 2px;
width: 100%;
}
</style>
<div class="CIECAM02-UCS">
<strong>CIECAM02-UCS</strong>
<div class="channel" id="J">
<canvas width="400" height="1"></canvas>
<svg width="420" height="20"><g class="axis axis-J" transform="translate(10,.5)"></g></svg>
<span>J* (lightness)</span>
</div>
<div class="channel" id="a">
<canvas width="400" height="1"></canvas>
<svg width="420" height="20"><g class="axis axis-a" transform="translate(10,.5)"></g></svg>
<span><em>a*</em> (greenness-to-redness)</span>
</div>
<div class="channel" id="b">
<canvas width="400" height="1"></canvas>
<svg width="420" height="20"><g class="axis axis-b" transform="translate(10,.5)"></g></svg>
<span>b* (blueness-to-yellowness)</span>
</div>
</div>
<div class="CIECAM02">
<strong>CIECAM02</strong>
<div class="channel" id="J2">
<canvas width="400" height="1"></canvas>
<svg width="420" height="20"><g class="axis axis-J2" transform="translate(10,.5)"></g></svg>
<span>J* (lightness)</span>
</div>
<div class="channel" id="C">
<canvas width="400" height="1"></canvas>
<svg width="420" height="20"><g class="axis axis-C" transform="translate(10,.5)"></g></svg>
<span>C (chroma)</span>
</div>
<div class="channel" id="h">
<canvas width="400" height="1"></canvas>
<svg width="420" height="20"><g class="axis axis-h" transform="translate(10,.5)"></g></svg>
<span>h (hue)</span>
</div>
</div>
<script src="http://d3js.org/d3.v4.js"></script>
<script src="d3-cam02.js"></script>
<script>
var white = d3.rgb("white"),
black = d3.rgb("black"),
width = d3.select("canvas").property("width");
var channels = {
J: {scale: d3.scaleLinear().domain([0, 100]).range([0, width]), x: width / 2},
a: {scale: d3.scaleLinear().domain([-40, 40]).range([0, width]), x: width / 2},
b: {scale: d3.scaleLinear().domain([-40, 40]).range([0, width]), x: width / 2},
J2: {scale: d3.scaleLinear().domain([0, 100]).range([0, width])},
C: {scale: d3.scaleLinear().domain([0, 100]).range([0, width])},
h: {scale: d3.scaleLinear().domain([0, 360]).range([0, width])},
};
var startJCh = d3.jch(d3.jab(channels.J.scale.invert(channels.J.x),
channels.a.scale.invert(channels.J.x),
channels.b.scale.invert(channels.J.x)));
channels.J2.x = channels.J2.scale(startJCh.J);
channels.C.x = channels.C.scale(startJCh.C);
channels.h.x = channels.h.scale(startJCh.h);
var channel = d3.selectAll(".channel")
.data(d3.entries(channels));
channel.select(".axis")
.each(function(d) {
var thisEl = d3.select(this),
axis;
if(thisEl.classed("axis-J")) axis = channels.J.scale;
if(thisEl.classed("axis-a")) axis = channels.a.scale;
if(thisEl.classed("axis-b")) axis = channels.b.scale;
if(thisEl.classed("axis-J2")) axis = channels.J2.scale;
if(thisEl.classed("axis-C")) axis = channels.C.scale;
if(thisEl.classed("axis-h")) axis = channels.h.scale;
d3.select(this)
.call(d3.axisBottom(axis));
});
var canvas = channel.select("canvas")
.each(function() {
var thisCanvas = d3.select(this);
thisCanvas
.call(d3.drag()
.container(function() { return this; })
.on("drag", dragged))
.each(render);
});
function dragged(d) {
d.value.x = Math.max(0, Math.min(this.width - 1, d3.mouse(this)[0]));
if(d.key !== "J" && d.key !== "a" && d.key !== "b") {
var current = d3.jab(d3.jch(
channels.J2.scale.invert(channels.J2.x),
channels.C.scale.invert(channels.C.x),
channels.h.scale.invert(channels.h.x)
));
channels.J.x = channels.J.scale(current.J);
channels.a.x = channels.a.scale(current.a);
channels.b.x = channels.b.scale(current.b);
// }
} else {
var current = d3.jch(d3.jab(
channels.J.scale.invert(channels.J.x),
channels.a.scale.invert(channels.a.x),
channels.b.scale.invert(channels.b.x)
));
channels.J2.x = channels.J2.scale(current.J);
channels.C.x = channels.C.scale(current.C);
channels.h.x = channels.h.scale(current.h);
}
canvas.each(render);
}
function render(d) {
var width = this.width,
context = this.getContext("2d"),
image = context.createImageData(width, 1),
i = -1;
if(d.key !== "J" && d.key !== "a" && d.key !== "b") {
var current = d3.jch(
channels.J2.scale.invert(channels.J2.x),
channels.C.scale.invert(channels.C.x),
channels.h.scale.invert(channels.h.x)
);
} else {
var current = d3.jab(
channels.J.scale.invert(channels.J.x),
channels.a.scale.invert(channels.a.x),
channels.b.scale.invert(channels.b.x)
);
}
for (var x = 0, v, c; x < width; ++x) {
if (x === Math.round(d.value.x)) {
c = white;
} else if (x === Math.round(d.value.x) - 1) {
c = black;
} else {
var idx = d.key === "J2" ? "J" : d.key;
current[idx] = d.value.scale.invert(x);
c = current.rgb();
}
if(c.displayable()) {
image.data[++i] = c.r;
image.data[++i] = c.g;
image.data[++i] = c.b;
image.data[++i] = 255;
} else {
image.data[++i] = 0;
image.data[++i] = 0;
image.data[++i] = 0;
image.data[++i] = 255;
}
}
context.putImageData(image, 0, 0);
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment