From 421bb9a89e1a81fc43fb5c93ada9b212e775fb24 Mon Sep 17 00:00:00 2001 From: Alexander Rose <alexander.rose@weirdbyte.de> Date: Thu, 30 May 2019 21:25:25 -0700 Subject: [PATCH] added lab and hcl color spaces --- src/mol-util/color/color.ts | 26 ++++- src/mol-util/color/spaces/hcl.ts | 107 ++++++++++++++++++++ src/mol-util/color/spaces/lab.ts | 164 +++++++++++++++++++++++++++++++ 3 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 src/mol-util/color/spaces/hcl.ts create mode 100644 src/mol-util/color/spaces/lab.ts diff --git a/src/mol-util/color/color.ts b/src/mol-util/color/color.ts index 60260c745..94da227c9 100644 --- a/src/mol-util/color/color.ts +++ b/src/mol-util/color/color.ts @@ -1,11 +1,13 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { NumberArray } from 'mol-util/type-helpers'; import { Vec3 } from 'mol-math/linear-algebra'; +import { Hcl } from './spaces/hcl'; +import { Lab } from './spaces/lab'; /** RGB color triplet expressed as a single number */ export type Color = { readonly '@type': 'color' } & number @@ -81,7 +83,7 @@ export namespace Color { return out } - /** Linear interpolation between two colors */ + /** Linear interpolation between two colors in rgb space */ export function interpolate(c1: Color, c2: Color, t: number): Color { const r1 = c1 >> 16 & 255 const g1 = c1 >> 8 & 255 @@ -96,6 +98,26 @@ export namespace Color { return ((r << 16) | (g << 8) | b) as Color } + + const tmpSaturateHcl = [0, 0, 0] as Hcl + export function saturate(c: Color, amount: number): Color { + Hcl.fromColor(tmpSaturateHcl, c) + return Hcl.toColor(Hcl.saturate(tmpSaturateHcl, tmpSaturateHcl, amount)) + } + + export function desaturate(c: Color, amount: number): Color { + return saturate(c, -amount) + } + + const tmpDarkenLab = [0, 0, 0] as Lab + export function darken(c: Color, amount: number): Color { + Lab.fromColor(tmpDarkenLab, c) + return Lab.toColor(Lab.darken(tmpDarkenLab, tmpDarkenLab, amount)) + } + + export function lighten(c: Color, amount: number): Color { + return darken(c, -amount) +} } export type ColorTable<T extends { [k: string]: number[] }> = { [k in keyof T]: Color[] } diff --git a/src/mol-util/color/spaces/hcl.ts b/src/mol-util/color/spaces/hcl.ts new file mode 100644 index 000000000..cfffa7589 --- /dev/null +++ b/src/mol-util/color/spaces/hcl.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + * + * Color conversion code adapted from chroma.js (https://github.com/gka/chroma.js) + * Copyright (c) 2011-2018, Gregor Aisch, BSD license + */ + +import { Color } from '../color'; +import { degToRad } from 'mol-math/misc'; +import { Lab } from './lab'; + +export { Hcl } + +interface Hcl extends Array<number> { [d: number]: number, '@type': 'hcl', length: 3 } + +/** + * CIE HCL (Hue-Chroma-Luminance) color + * + * - H [0..360] + * - C [0..100] + * - L [0..100] + * + * Cylindrical representation of CIELUV (see https://en.wikipedia.org/wiki/CIELUV) + */ +function Hcl() { + return Hcl.zero() +} + +namespace Hcl { + export function zero(): Hcl { + const out = [0.1, 0.0, 0.0] + out[0] = 0 + return out as Hcl; + } + + export function create(h: number, c: number, l: number): Hcl { + const out = zero() + out[0] = h + out[1] = c + out[2] = l + return out + } + + const tmpFromColorLab = [0, 0, 0] as Lab + export function fromColor(out: Hcl, color: Color): Hcl { + return Lab.toHcl(out, Lab.fromColor(tmpFromColorLab, color)) + } + + export function fromLab(hcl: Hcl, lab: Lab): Hcl { + return Lab.toHcl(hcl, lab) + } + + const tmpToColorLab = [0, 0, 0] as Lab + export function toColor(hcl: Hcl): Color { + return Lab.toColor(toLab(tmpToColorLab, hcl)) + } + + /** + * Convert from a qualitative parameter h and a quantitative parameter l to a 24-bit pixel. + * + * These formulas were invented by David Dalrymple to obtain maximum contrast without going + * out of gamut if the parameters are in the range 0-1. + * A saturation multiplier was added by Gregor Aisch + */ + export function toLab(out: Lab, hcl: Hcl): Lab { + let [h, c, l] = hcl + if (isNaN(h)) h = 0 + h = degToRad(h) + out[0] = l + out[1] = Math.cos(h) * c + out[2] = Math.sin(h) * c + return out + } + + export function copy(out: Hcl, c: Hcl): Hcl { + out[0] = c[0] + out[1] = c[1] + out[2] = c[2] + return out + } + + export function saturate(out: Hcl, c: Hcl, amount: number): Hcl { + out[0] = c[0] + out[1] = Math.max(0, c[1] + Kn * amount) + out[2] = c[2] + return out + } + + export function desaturate(out: Hcl, c: Hcl, amount: number): Hcl { + return saturate(out, c, -amount) + } + + const tmpDarkenLab = [0, 0, 0] as Lab + export function darken(out: Hcl, c: Hcl, amount: number): Hcl { + toLab(tmpDarkenLab, c) + return Lab.toHcl(out, Lab.darken(tmpDarkenLab, tmpDarkenLab, amount)) + } + + export function lighten(out: Hcl, c: Hcl, amount: number): Hcl { + return darken(out, c, -amount) + } + + // Corresponds roughly to RGB brighter/darker + const Kn = 18 +} \ No newline at end of file diff --git a/src/mol-util/color/spaces/lab.ts b/src/mol-util/color/spaces/lab.ts new file mode 100644 index 000000000..4e05f2367 --- /dev/null +++ b/src/mol-util/color/spaces/lab.ts @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + * + * Color conversion code adapted from chroma.js (https://github.com/gka/chroma.js) + * Copyright (c) 2011-2018, Gregor Aisch, BSD license + */ + +import { Color } from '../color'; +import { Hcl } from './hcl'; +import { radToDeg } from 'mol-math/misc'; +import { clamp } from 'mol-math/interpolate'; + +export { Lab } + +interface Lab extends Array<number> { [d: number]: number, '@type': 'lab', length: 3 } + +/** + * CIE LAB color + * + * - L* [0..100] - lightness from black to white + * - a [-100..100] - green (-) to red (+) + * - b [-100..100] - blue (-) to yellow (+) + * + * see https://en.wikipedia.org/wiki/CIELAB_color_space + */ +function Lab() { + return Lab.zero() +} + +namespace Lab { + export function zero(): Lab { + const out = [0.1, 0.0, 0.0] + out[0] = 0 + return out as Lab; + } + + export function create(l: number, a: number, b: number): Lab { + const out = zero() + out[0] = l + out[1] = a + out[2] = b + return out + } + + export function fromColor(out: Lab, color: Color): Lab { + const [r, g, b] = Color.toRgb(color) + const [x, y, z] = rgbToXyz(r, g, b) + const l = 116 * y - 16 + out[0] = l < 0 ? 0 : l + out[1] = 500 * (x - y) + out[2] = 200 * (y - z) + return out + } + + export function fromHcl(out: Lab, hcl: Hcl): Lab { + return Hcl.toLab(out, hcl) + } + + export function toColor(lab: Lab): Color { + let y = (lab[0] + 16) / 116 + let x = isNaN(lab[1]) ? y : y + lab[1] / 500 + let z = isNaN(lab[2]) ? y : y - lab[2] / 200 + + y = Yn * lab_xyz(y) + x = Xn * lab_xyz(x) + z = Zn * lab_xyz(z) + + const r = xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z) // D65 -> sRGB + const g = xyz_rgb(-0.9692660 * x + 1.8760108 * y + 0.0415560 * z) + const b = xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z) + + return Color.fromRgb( + Math.round(clamp(r, 0, 255)), + Math.round(clamp(g, 0, 255)), + Math.round(clamp(b, 0, 255)) + ) + } + + export function toHcl(out: Hcl, lab: Lab): Hcl { + const [l, a, b] = lab + const c = Math.sqrt(a * a + b * b) + let h = (radToDeg(Math.atan2(b, a)) + 360) % 360 + if (Math.round(c * 10000) === 0) h = Number.NaN + out[0] = h + out[1] = c + out[2] = l + return out + } + + export function copy(out: Lab, c: Lab): Lab { + out[0] = c[0] + out[1] = c[1] + out[2] = c[2] + return out + } + + export function darken(out: Lab, c: Lab, amount: number): Lab { + out[0] = c[0] - Kn * amount + out[1] = c[1] + out[2] = c[2] + return out + } + + export function lighten(out: Lab, c: Lab, amount: number): Lab { + return darken(out, c, -amount) + } + + const tmpSaturateHcl = [0, 0, 0] as Hcl + export function saturate(out: Lab, c: Lab, amount: number): Lab { + toHcl(tmpSaturateHcl, c) + return Hcl.toLab(out, Hcl.saturate(tmpSaturateHcl, tmpSaturateHcl, amount)) + } + + export function desaturate(out: Lab, c: Lab, amount: number): Lab { + return saturate(out, c, -amount) + } + + // Corresponds roughly to RGB brighter/darker + const Kn = 18 + + /** D65 standard referent */ + const Xn = 0.950470 + const Yn = 1 + const Zn = 1.088830 + + const T0 = 0.137931034 // 4 / 29 + const T1 = 0.206896552 // 6 / 29 + const T2 = 0.12841855 // 3 * t1 * t1 + const T3 = 0.008856452 // t1 * t1 * t1 + + /** convert component from xyz to rgb */ + function xyz_rgb(c: number) { + return 255 * (c <= 0.00304 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055) + } + + /** convert component from lab to xyz */ + function lab_xyz(t: number) { + return t > T1 ? t * t * t : T2 * (t - T0) + } + + /** convert component from rgb to xyz */ + function rgb_xyz(c: number) { + if ((c /= 255) <= 0.04045) return c / 12.92 + return Math.pow((c + 0.055) / 1.055, 2.4) + } + + /** convert component from xyz to lab */ + function xyz_lab(t: number) { + if (t > T3) return Math.pow(t, 1 / 3) + return t / T2 + T0 + } + + function rgbToXyz(r: number, g: number, b: number) { + r = rgb_xyz(r) + g = rgb_xyz(g) + b = rgb_xyz(b) + const x = xyz_lab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / Xn) + const y = xyz_lab((0.2126729 * r + 0.7151522 * g + 0.0721750 * b) / Yn) + const z = xyz_lab((0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / Zn) + return [x, y, z] + } +} \ No newline at end of file -- GitLab