UNPKG

s2maps-gpu

Version:

S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.

461 lines (460 loc) 13.7 kB
import { colorBlindAdjust } from './colorBlindAdjust.js'; import { colorParser } from './colorParser.js'; export * from './colorBlindAdjust.js'; export * from './colorGenerators.js'; export * from './colorParser.js'; /** * # Color manager * * ### Description * Color class to handle color conversions and adjustments. Supports RGB, HSV, HSL, and LCH. * * ### Example Usage * * ex. name string * ```ts * const color = new Color('red'); * const rgb = color.getRGB(); * ``` * * ex. Hex string * ```ts * const color = new Color('#ff0000'); * const rgb = color.getRGB(); * ``` * * ex. String input array * ```ts * const color = new Color('hsv(180, 0.9, 0.7843137254901961)'); * const rgb = color.getRGB(); * ``` * * ex. RGBA array * ```ts * const color = new Color(255, 0, 0, 1, 'rgb'); * const rgb = color.getRGB(); * ``` * * ### COLOR INTERPOLATION: we support the use of the LCH color space: * - [interpolation guide here.](https://www.alanzucconi.com/2016/01/06/colour-interpolation/4/) * - use [chroma.js](https://github.com/gka/chroma.js) as a guide to create best interpolation * hsv is a good secondary. Saved for posterity. * * ### MORE INFORMATION ON COLOR BLIND ADJUST: * - [Link one](https://www.nature.com/articles/nmeth.1618), * - [Link two](http://www.daltonize.org/), * - [Link three](https://galactic.ink/labs/Color-Vision/Javascript/Color.Vision.Daltonize.js) */ export class Color { val; type = 'rgb'; /** * @param x - either an input color, string defining a color, or first color value * @param y - second color value * @param z - third color value * @param a - alpha value * @param type - color type */ constructor(x = 0, y = 0, z = 0, a = 1, type = 'rgb') { if (x instanceof Color) { this.val = x.val; this.type = x.type; } else if (typeof x === 'string') { const [type, val] = colorParser(x); this.type = type; this.val = val; } else { // x is a number this.val = [x, y, z, a]; this.type = type; } } /** @returns a clone the color */ copy() { return new Color(...this.val, this.type); } /** * Get the rgb values of the color * @param normalize - whether to normalize the values * @param cbAdjust - colorblind adjustment * @returns Color as [r, g, b, a] */ getRGB(normalize = true, cbAdjust) { this.toRGB(); const color = cbAdjust !== undefined ? colorBlindAdjust(this.val, cbAdjust) : this.val.map((n) => n); return (normalize ? color.map((n, i) => (i < 3 ? n / 255 : n)) : color); } /** @returns the lch values of the color */ getLCH() { // now convert to lch this.toLCH(); return this.val; } /** @returns the rgb values of the color */ toRGB() { if (this.type === 'rgb') return this; // potentially swing back if (this.type === 'hsv') this.HSV2RGB(); // lch goes to midpoint lab if (this.type === 'lch') this.LCH2LAB(); // lab goes straight to rgb if (this.type === 'lab') this.LAB2RGB(); // hsl goes straight to rgb if (this.type === 'hsl') this.HSL2RGB(); return this; } /** @returns the hsv values of the color */ toHSV() { if (this.type === 'hsv') return this; // potentially swing back if (this.type === 'lch') this.LCH2LAB(); if (this.type === 'lab') this.LAB2RGB(); // now ready for convert if (this.type === 'rgb') this.RGB2HSV(); return this; } /** @returns the lch values of the color */ toLCH() { if (this.type === 'lch') return this; // if outside variables, bring them back to a starting point if (this.type === 'hsv') this.HSV2RGB(); // step 1: rgb to lab if (this.type === 'rgb') this.RGB2LAB(); // step 2: lab to lch if (this.type === 'lab') this.LAB2LCH(); return this; } /** Convert rgb to lab */ RGB2LAB() { this.type = 'lab'; const [r, g, b, a] = this.val; const [x, y, z] = rgb2xyz(r, g, b); const l = 116 * y - 16; this.val = [l < 0 ? 0 : l, 500 * (x - y), 200 * (y - z), a]; } /** Convert lab to lch */ LAB2LCH() { this.type = 'lch'; const [l, a, b, alpha] = this.val; const c = Math.sqrt(a * a + b * b); let h = (Math.atan2(b, a) * (180 / Math.PI) + 360) % 360; if (Math.round(c * 10000) === 0) h = 0; this.val = [l, c, h, alpha]; } /** Convert lch to lab */ LCH2LAB() { this.type = 'lab'; const [l, c, h_o, alpha] = this.val; const h = h_o * (Math.PI / 180); this.val = [l, Math.cos(h) * c, Math.sin(h) * c, alpha]; } /** Convert lab to rgb */ LAB2RGB() { this.type = 'rgb'; const [l, a, b, alpha] = this.val; let x, y, z, r, g, b_; // prep move to xyz y = (l + 16) / 116; x = isNaN(a) ? y : y + a / 500; z = isNaN(b) ? y : y - b / 200; // solve x, y, z y = labXyz(y); x = 0.95047 * labXyz(x); z = 1.08883 * labXyz(z); // xyz to rgb r = xyzRgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z); g = xyzRgb(-0.969266 * x + 1.8760108 * y + 0.041556 * z); b_ = xyzRgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z); // clip space from 0 to 255 if (r < 0) r = 0; else if (r > 255) r = 255; if (g < 0) g = 0; else if (g > 255) g = 255; if (b_ < 0) b_ = 0; else if (b_ > 255) b_ = 255; // set new value this.val = [r, g, b_, alpha]; } /** Convert rgb to hsv */ RGB2HSV() { this.type = 'hsv'; const [r, g, b, a] = this.val; const min = Math.min(r, g, b); const max = Math.max(r, g, b); const delta = max - min; let h, s; const v = max / 255.0; if (max === 0) { h = 0; s = 0; } else { s = delta / max; if (delta === 0) { h = 0; } else { if (r === max) h = (g - b) / delta; if (g === max) h = 2 + (b - r) / delta; if (b === max) h = 4 + (r - g) / delta; if (h === undefined) h = 0; h *= 60; if (h < 0) h += 360; } } this.val = [h, s, v, a]; } /** Convert hsv to rgb */ HSV2RGB() { this.type = 'rgb'; let [h, s, v, a] = this.val; v *= 255; if (s === 0) { this.val = [v, v, v, a]; } else { if (h === 360) h = 0; else if (h > 360) h -= 360; else if (h < 0) h += 360; h /= 60; const i = Math.floor(h); const f = h - i; const p = v * (1 - s); const q = v * (1 - s * f); const t = v * (1 - s * (1 - f)); switch (i) { case 0: this.val = [v, t, p, a]; break; case 1: this.val = [q, v, p, a]; break; case 2: this.val = [p, v, t, a]; break; case 3: this.val = [p, q, v, a]; break; case 4: this.val = [t, p, v, a]; break; case 5: this.val = [v, p, q, a]; break; default: this.val = [v, t, p, a]; break; } } } /** Convert hsl to rgb */ HSL2RGB() { const { round } = Math; this.type = 'rgb'; const [h, s, l, a] = this.val; let r, g, b; if (s === undefined) { r = g = b = l * 255; } else { const t3 = [0, 0, 0]; const c = [0, 0, 0]; const t2 = l < 0.5 ? l * (1 + s) : l + s - l * s; const t1 = 2 * l - t2; const h_ = h / 360; t3[0] = h_ + 1 / 3; t3[1] = h_; t3[2] = h_ - 1 / 3; for (let i = 0; i < 3; i++) { if (t3[i] < 0) t3[i] += 1; if (t3[i] > 1) t3[i] -= 1; if (6 * t3[i] < 1) c[i] = t1 + (t2 - t1) * 6 * t3[i]; else if (2 * t3[i] < 1) c[i] = t2; else if (3 * t3[i] < 2) c[i] = t1 + (t2 - t1) * (2 / 3 - t3[i]) * 6; else c[i] = t1; } r = round(c[0] * 255); g = round(c[1] * 255); b = round(c[2] * 255); } this.val = [r, g, b, a]; } /** * Get a sinebow color via interpolation input value * @param t - input value * @returns sinebow color at t */ static sinebow(t) { const { sin, cos, floor, max, PI } = Math; let rad = t * (2 * PI) * (5 / 6); rad *= 0.75; // increase frequency to 2/3 cycle per rad const s = sin(rad); const c = cos(rad); const r = floor(max(0, -c) * 255); const g = floor(max(s, 0) * 255); const b = floor(max(c, 0, -s) * 255); return new Color(r, g, b, 1, 'rgb'); } /** * Get a white color via interpolation * @param t - input value * @returns white color at t */ static fadeToWhite(t) { return interpolate(this.sinebow(1), new Color(255, 255, 255, 1, 'rgb'), t); } /** * Get a sinebow extended color via interpolation * @param t - input value * @returns sinebow extended color at t */ static sinebowExtended(t) { return t <= 0.45 ? this.sinebow(t / 0.45) : this.fadeToWhite((t - 0.45) / (1 - 0.45)); } } // everything below this was taken from chroma.js /** * Convert rgb to xyz * @param r - red/green/blue value * @returns xyz value */ function rgbXyz(r) { if ((r /= 255) <= 0.04045) return r / 12.92; return Math.pow((r + 0.055) / 1.055, 2.4); } /** * Convert xyz to lab * @param t - xyz value * @returns lab value */ function xyzLab(t) { if (t > 0.008856452) return Math.pow(t, 1 / 3); return t / 0.12841855 + 0.137931034; } /** * Convert rgb to xyz * @param r - red/green/blue value * @returns xyz value */ function xyzRgb(r) { return 255 * (r <= 0.00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - 0.055); } /** * Convert lab to xyz * @param t - lab value * @returns xyz value */ function labXyz(t) { return t > 0.206896552 ? t * t * t : 0.12841855 * (t - 0.137931034); } /** * Convert rgb to xyz * @param r - red value * @param g - green value * @param b - blue value * @returns xyz value */ function rgb2xyz(r, g, b) { r = rgbXyz(r); g = rgbXyz(g); b = rgbXyz(b); const x = xyzLab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / 0.95047); const y = xyzLab((0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 1); const z = xyzLab((0.0193339 * r + 0.119192 * g + 0.9503041 * b) / 1.08883); return [x, y, z]; } /** * Given two colors, interpolate between them using a t value between 0 and 1. * 0 returns color1, 1 returns color2, and anything in between returns a mixture. * @param color1 - first color * @param color2 - second color * @param t - t value between 0 and 1 * @returns interpolated color */ export function interpolate(color1, color2, t) { if (color1.type !== color2.type) return new Color(color1.val[0], color1.val[1], color1.val[2], color1.val[3], color1.type); if (color1.type === 'rgb') { const [r1, g1, b1, a1] = color1.val; const [r2, g2, b2, a2] = color2.val; const r = r1 + (r2 - r1) * t; const g = g1 + (g2 - g1) * t; const b = b1 + (b2 - b1) * t; const a = a1 + (a2 - a1) * t; return new Color(r, g, b, a, 'rgb'); } // prep variables let sat, hue, dh; const [hue0, sat0, lbv0, alpha0] = color1.val; const [hue1, sat1, lbv1, alpha1] = color2.val; // first manage hue if (!isNaN(hue0) && !isNaN(hue1)) { if (hue1 > hue0 && hue1 - hue0 > 180) dh = hue1 - (hue0 + 360); else if (hue1 < hue0 && hue0 - hue1 > 180) dh = hue1 + 360 - hue0; else dh = hue1 - hue0; hue = hue0 + t * dh; } else if (!isNaN(hue0)) { hue = hue0; if (lbv1 === 1 || lbv1 === 0) sat = sat0; } else if (!isNaN(hue1)) { hue = hue1; if (lbv0 === 1 || lbv0 === 0) sat = sat1; } else { hue = 0; } // saturation if (sat === undefined) sat = sat0 + t * (sat1 - sat0); // luminosity const lbv = lbv0 + t * (lbv1 - lbv0); // alpha const alpha = alpha0 + t * (alpha1 - alpha0); // create the new color return new Color(hue, sat, lbv, alpha, color1.type); }