Published
Edited
May 18, 2019
2 forks
Importers
24 stars
Insert cell
Insert cell
Insert cell
Insert cell
import {srgb_to_xyz, from_hex} from "@jrus/srgb"
Insert cell
viewof user_color = html`<input type=color value=#5588cc>`
Insert cell
user_color_xyz = srgb_to_xyz(from_hex(user_color))
Insert cell
user_color_cam = cam16(user_color_xyz)
Insert cell
Insert cell
user_color_ucs = cam16_ucs(user_color_xyz)
Insert cell
Insert cell
Insert cell
parameters = ({
whitepoint: 'D65',
adapting_luminance: 40, // L_A
background_luminance: 20, // Y_b; relative to Y_w = 100
surround: 'average', // 'dark', 'dim', 'average', or a number from 0 to 2
discounting: false
})
Insert cell
Insert cell
// Fill in any missing values from the parameters cell
params = Object.assign(
{ whitepoint: 'D65', adapting_luminance: 40, background_luminance: 20,
surround: 'average', discounting: false},
parameters)
Insert cell
Insert cell
XYZ_w = standard_whitepoints[params.whitepoint] || params.whitepoint
Insert cell
L_A = params.adapting_luminance
Insert cell
Y_b = params.background_luminance
Insert cell
Y_w = XYZ_w[1] // White point luminance
Insert cell
surround = isnumber(params.surround)
? params.surround
: ['dark', 'dim', 'average'].indexOf(params.surround)
Insert cell
c = surround >= 1
? lerp(0.59, 0.69, surround - 1)
: lerp(0.525, 0.59, surround)
Insert cell
F = c >= 0.59 ? lerp(0.9, 1.0, (c - 0.59)/.1)
: lerp(0.8, 0.9, (c - 0.525)/0.065)
Insert cell
N_c = F
Insert cell
k = 1 / (5*L_A + 1)
Insert cell
F_L = { // Luminance adaptation factor
let k4 = k*k*k*k;
return (k4 * L_A + 0.1 * (1 - k4)*(1 - k4) * pow(5 * L_A, 1/3)) }
Insert cell
F_L_4 = pow(F_L, 0.25)
Insert cell
n = Y_b / Y_w
Insert cell
z = 1.48 + sqrt(n) // Lightness non-linearity exponent (modified by `c`)
Insert cell
N_bb = 0.725 * pow(n, -0.2) // Chromatic induction factors
Insert cell
N_cb = N_bb
Insert cell
// Illuminant discounting (adaptation). Fully adapted = 1
D = !params.discounting
? clip(0, 1, F * (1 - 1 / 3.6 * Math.exp((-L_A - 42)/92)))
: 1
Insert cell
RGB_w = M16(XYZ_w) // Cone responses of the white point
Insert cell
D_RGB = RGB_w.map(C_w => lerp(1, Y_w/C_w, D))
Insert cell
D_RGB_inv = D_RGB.map(D_C => 1 / D_C)
Insert cell
RGB_cw = [
RGB_w[0]*D_RGB[0], RGB_w[1]*D_RGB[1], RGB_w[2]*D_RGB[2]]
Insert cell
adapt = (component) => {
const x = pow(F_L * Math.abs(component) * 0.01, 0.42);
return sgn(component) * 400 * x / (x + 27.13); }
// In the spec there is an additional 0.1 offset at this step;
// This notebook follows Schlömer's adjustments which move the
// offset to another step but do not affect the final results
Insert cell
unadapt = {
const exponent = 1/0.42
const constant = 100 / F_L * pow(27.13, exponent);
return (component) => {
const cabs = Math.abs(component);
return sgn(component) * constant * pow(cabs / (400 - cabs), exponent); }
}
Insert cell
RGB_aw = RGB_cw.map(adapt)
Insert cell
A_w = N_bb * (2*RGB_aw[0] + RGB_aw[1] + 0.05*RGB_aw[2])
Insert cell
Insert cell
Insert cell
cam16 = function cam16(XYZ) {
const
[R_a, G_a, B_a] = elem_mul(M16(XYZ), D_RGB).map(adapt),
a = R_a + (-12*G_a + B_a) / 11, // redness-greenness
b = (R_a + G_a - 2 * B_a) / 9, // yellowness-blueness
h_rad = Math.atan2(b, a), // hue in radians
h = degrees(h_rad), // hue in degrees
e_t = 0.25 * (Math.cos(h_rad + 2) + 3.8),
A = N_bb * (2*R_a + G_a + 0.05*B_a),
J_root = pow(A / A_w, 0.5 * c * z),
J = 100 * J_root*J_root, // lightness
Q = (4/c * J_root * (A_w + 4) * F_L_4), // brightness
t = (5e4 / 13 * N_c * N_cb * e_t * sqrt(a*a + b*b) /
(R_a + G_a + 1.05 * B_a + 0.305)),
alpha = pow(t, 0.9) * pow(1.64 - pow(0.29, n), 0.73),
C = alpha * J_root, // chroma
M = C * F_L_4, // colorfulness
s = 50 * sqrt(c * alpha / (A_w + 4)); // saturation
return {J, C, h, Q, M, s}
}
Insert cell
cam16_inverse = function cam16_inverse({Q, M, J, C, s, h}) {
if (!(exists(h) && (exists(J) + exists(Q) == 1) &&
(exists(M) + exists(C) + exists(s) == 1))) {
throw new Error('Need exactly need exactly one of each of ' +
'{J, Q}, {M, C, s}, {h} as model inputs'); }
if ((J == 0) || (Q == 0)) return [0, 0, 0];
const
h_rad = radians(h),
cos_h = Math.cos(h_rad),
sin_h = Math.sin(h_rad),
J_root = sqrt(J)*0.1 || 0.25 * c * Q / ((A_w + 4) * F_L_4),
alpha = (s == null) ? (C || (M / F_L_4) || 0) / J_root
: 0.0004*s*s*(A_w + 4) / c,
t = pow(alpha * pow(1.64 - pow(0.29, n), -0.73), 10 / 9),
e_t = 0.25 * (Math.cos(h_rad + 2) + 3.8),
A = A_w * pow(J_root, 2 / c / z),
p_1 = 5e4 / 13 * N_c * N_cb * e_t,
p_2 = A / N_bb,
r = 23 * (p_2 + 0.305) * t / (23*p_1 + t * (11*cos_h + 108*sin_h)),
a = r * cos_h,
b = r * sin_h,
denom = 1 / 1403,
RGB_c = [(460*p_2 + 451*a + 288*b) * denom,
(460*p_2 - 891*a - 261*b) * denom,
(460*p_2 - 220*a - 6300*b) * denom].map(unadapt),
XYZ = M16_inv(elem_mul(RGB_c, D_RGB_inv));
return XYZ;
}
Insert cell
Insert cell
cam16_ucs = function cam16_ucs(XYZ) {
let {J, M, h} = cam16(XYZ), h_rad = radians(h);
M = Math.log(1 + 0.0228 * M) / 0.0228;
return {
J: 1.7 * J / (1 + 0.007 * J),
a: M * Math.cos(h_rad),
b: M * Math.sin(h_rad),
M, h };
}
Insert cell
cam16_ucs_inverse = function cam16_ucs_inverse({J, M, h, a, b}) {
const ab = exists(a) && exists(b), Mh = exists(M) && exists(h);
if (ab ^ Mh == 0) { throw new Error(
'Either {a, b} or {M, h} (but not both pairs) are required inputs.'); }
if (ab) {
M = sqrt(a*a + b*b);
h = degrees(Math.atan2(b, a)); }
M = (Math.exp(M * 0.0228) - 1) / 0.0228;
J = J / (1.7 - 0.007 * J);
return cam16_inverse({J, M, h});
}
Insert cell
deltaE = function deltaE(XYZ0, XYZ1) {
const
{J:J0, a:a0, b:b0} = cam16_ucs(XYZ0),
{J:J1, a:a1, b:b1} = cam16_ucs(XYZ1),
dJ = J1 - J0, da = a1 - a0, db = b1 - b0;
return 1.41 * pow(dJ*dJ + da*da + db*db, 0.315);
}
Insert cell
Insert cell
cat16 = function cat16(source_whitepoint, target_whitepoint) {
source_whitepoint = standard_whitepoints[source_whitepoint] || source_whitepoint;
target_whitepoint = standard_whitepoints[target_whitepoint] || target_whitepoint;
const
RGB_w_source = M16(source_whitepoint),
RGB_w_target = M16(target_whitepoint),
D_RGB_source_to_target = [0,1,2].map(i =>
lerp(1, 100/RGB_w_source[i], D) / lerp(1, 100/RGB_w_target[i], D)),
[M00, M10, M20] = M16(elem_mul(M16_inv([1, 0, 0]), D_RGB_source_to_target)),
[M01, M11, M21] = M16(elem_mul(M16_inv([0, 1, 0]), D_RGB_source_to_target)),
[M02, M12, M22] = M16(elem_mul(M16_inv([0, 0, 1]), D_RGB_source_to_target));
return ([X, Y, Z]) => [
M00*X + M01*Y + M02*Z,
M10*X + M11*Y + M12*Z,
M20*X + M21*Y + M22*Z];
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
M16 = ([X, Y, Z]) => [
+ 0.401288*X + 0.650173*Y - 0.051461*Z,
- 0.250268*X + 1.204414*Y + 0.045854*Z,
- 0.002079*X + 0.048952*Y + 0.953127*Z]
Insert cell
Insert cell
M16_inv = ([R, G, B]) => [
+ 1.862067855087233e+0*R - 1.011254630531685e+0*G + 1.491867754444518e-1*B,
+ 3.875265432361372e-1*R + 6.214474419314753e-1*G - 8.973985167612518e-3*B,
- 1.584149884933386e-2*R - 3.412293802851557e-2*G + 1.049964436877850e+0*B]
Insert cell
standard_whitepoints = ({
A: [109.850, 100, 35.585],
B: [ 99.090, 100, 85.324],
C: [ 98.074, 100, 118.232],
E: [100 , 100, 100 ], // equal-energy illuminant
D50: [ 96.422, 100, 82.521],
D55: [ 95.682, 100, 92.149],
D65: [ 95.047, 100, 108.883],
D75: [ 94.972, 100, 122.638],
F2: [ 99.186, 100, 67.393],
F7: [ 95.041, 100, 108.747],
F11: [100.962, 100, 64.350],
})
Insert cell
Insert cell
exists = (obj) =>
typeof obj !== "undefined" && obj !== null
Insert cell
isnumber = (obj) => Object.prototype.toString.call(obj) === '[object Number]'
Insert cell
sqrt = Math.sqrt
Insert cell
pow = Math.pow
Insert cell
sgn = (x) => (x > 0) - (x < 0)
Insert cell
mod = (a, b) => a - b * Math.floor(a/b)
Insert cell
lerp = (a, b, t) => (1 - t) * a + t * b // Linear interpolation
Insert cell
clip = (a, b, t) => Math.min(Math.max(t, a), b)
Insert cell
DEGREES = 180/Math.PI
Insert cell
RADIANS = Math.PI/180
Insert cell
degrees = (angle) => mod(angle * DEGREES, 360)
Insert cell
radians = (angle) => mod(angle, 360) * RADIANS
Insert cell
elem_mul = function elem_mul(v0, v1) {
const n = v0.length, prod = new Array(n);
for (let i = 0; i < n; i++) { prod[i] = v0[i] * v1[i]; }
return prod;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more