UNPKG

@mapbox/mapbox-gl-style-spec

Version:

a specification for mapbox gl styles

312 lines (275 loc) 9.28 kB
import {parseCSSColor} from 'csscolorparser'; import {number as lerp} from './interpolate'; import type {LUT} from '../types/lut'; /** * An RGBA color value. Create instances from color strings using the static * method `Color.parse`. The constructor accepts RGB channel values in the range * `[0, 1]`, premultiplied by A. * * @param {number} r The red channel. * @param {number} g The green channel. * @param {number} b The blue channel. * @param {number} a The alpha channel. * @private */ class Color { r: number; g: number; b: number; a: number; constructor(r: number, g: number, b: number, a: number = 1) { this.r = r; this.g = g; this.b = b; this.a = a; } static black: Color; static white: Color; static transparent: Color; static red: Color; static blue: Color; /** * Parses valid CSS color strings and returns a `Color` instance. * @returns A `Color` instance, or `undefined` if the input is not a valid color string. */ static parse(input?: string | Color | null): Color | undefined { if (!input) { return undefined; } if (input instanceof Color) { return input; } if (typeof input !== 'string') { return undefined; } const rgba = parseCSSColor(input); if (!rgba) { return undefined; } return new Color( rgba[0] / 255 * rgba[3], rgba[1] / 255 * rgba[3], rgba[2] / 255 * rgba[3], rgba[3] ); } /** * Returns an RGBA string representing the color value. * * @returns An RGBA string. * @example * var purple = new Color.parse('purple'); * purple.toString; // = "rgba(128,0,128,1)" * var translucentGreen = new Color.parse('rgba(26, 207, 26, .73)'); * translucentGreen.toString(); // = "rgba(26,207,26,0.73)" */ toStringPremultipliedAlpha(): string { const [r, g, b, a] = this.a === 0 ? [0, 0, 0, 0] : [ this.r * 255 / this.a, this.g * 255 / this.a, this.b * 255 / this.a, this.a ]; return `rgba(${Math.round(r)},${Math.round(g)},${Math.round(b)},${a})`; } toString(): string { const [r, g, b, a] = [ this.r, this.g, this.b, this.a ]; return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`; } toRenderColor(lut: LUT | null): RenderColor { const {r, g, b, a} = this; return new RenderColor(lut, r, g, b, a); } clone(): Color { return new Color(this.r, this.g, this.b, this.a); } } /** * Renderable color created from a Color and an optional LUT value */ export class RenderColor { r: number; g: number; b: number; a: number; constructor(lut: LUT | null, r: number, g: number, b: number, a: number) { if (!lut) { this.r = r; this.g = g; this.b = b; this.a = a; } else { const N = lut.image.height; const N2 = N * N; // Normalize to cube dimensions. r = a === 0 ? 0 : (r / a) * (N - 1); g = a === 0 ? 0 : (g / a) * (N - 1); b = a === 0 ? 0 : (b / a) * (N - 1); // Determine boundary values for the cube the color is in. const r0 = Math.floor(r); const g0 = Math.floor(g); const b0 = Math.floor(b); const r1 = Math.ceil(r); const g1 = Math.ceil(g); const b1 = Math.ceil(b); // Determine weights within the cube. const rw = r - r0; const gw = g - g0; const bw = b - b0; const data = lut.image.data; const i0 = (r0 + g0 * N2 + b0 * N) * 4; const i1 = (r0 + g0 * N2 + b1 * N) * 4; const i2 = (r0 + g1 * N2 + b0 * N) * 4; const i3 = (r0 + g1 * N2 + b1 * N) * 4; const i4 = (r1 + g0 * N2 + b0 * N) * 4; const i5 = (r1 + g0 * N2 + b1 * N) * 4; const i6 = (r1 + g1 * N2 + b0 * N) * 4; const i7 = (r1 + g1 * N2 + b1 * N) * 4; if (i0 < 0 || i7 >= data.length) { throw new Error("out of range"); } // Trilinear interpolation. this.r = lerp( lerp( lerp(data[i0], data[i1], bw), lerp(data[i2], data[i3], bw), gw), lerp( lerp(data[i4], data[i5], bw), lerp(data[i6], data[i7], bw), gw), rw) / 255 * a; this.g = lerp( lerp( lerp(data[i0 + 1], data[i1 + 1], bw), lerp(data[i2 + 1], data[i3 + 1], bw), gw), lerp( lerp(data[i4 + 1], data[i5 + 1], bw), lerp(data[i6 + 1], data[i7 + 1], bw), gw), rw) / 255 * a; this.b = lerp( lerp( lerp(data[i0 + 2], data[i1 + 2], bw), lerp(data[i2 + 2], data[i3 + 2], bw), gw), lerp( lerp(data[i4 + 2], data[i5 + 2], bw), lerp(data[i6 + 2], data[i7 + 2], bw), gw), rw) / 255 * a; this.a = a; } } /** * Returns an RGBA array of values representing the color, unpremultiplied by A. * * @returns An array of RGBA color values in the range [0, 255]. */ toArray(): [number, number, number, number] { const {r, g, b, a} = this; return a === 0 ? [0, 0, 0, 0] : [ r * 255 / a, g * 255 / a, b * 255 / a, a ]; } /** * Returns an HSLA array of values representing the color, unpremultiplied by A. * * @returns An array of HSLA color values. */ toHslaArray(): [number, number, number, number] { if (this.a === 0) { return [0, 0, 0, 0]; } const {r, g, b, a} = this; const red = Math.min(Math.max(r / a, 0.0), 1.0); const green = Math.min(Math.max(g / a, 0.0), 1.0); const blue = Math.min(Math.max(b / a, 0.0), 1.0); const min = Math.min(red, green, blue); const max = Math.max(red, green, blue); const l = (min + max) / 2; if (min === max) { return [0, 0, l * 100, a]; } const delta = max - min; const s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min); let h = 0; if (max === red) { h = (green - blue) / delta + (green < blue ? 6 : 0); } else if (max === green) { h = (blue - red) / delta + 2; } else if (max === blue) { h = (red - green) / delta + 4; } h *= 60; return [ Math.min(Math.max(h, 0), 360), Math.min(Math.max(s * 100, 0), 100), Math.min(Math.max(l * 100, 0), 100), a ]; } /** * Returns a RGBA array of float values representing the color, unpremultiplied by A. * * @returns An array of RGBA color values in the range [0, 1]. */ toArray01(): [number, number, number, number] { const {r, g, b, a} = this; return a === 0 ? [0, 0, 0, 0] : [ r / a, g / a, b / a, a ]; } /** * Returns an RGB array of values representing the color, unpremultiplied by A and multiplied by a scalar. * * @param {number} scale A scale to apply to the unpremultiplied-alpha values. * @returns An array of RGB color values in the range [0, 1]. */ toArray01Scaled(scale: number): [number, number, number] { const {r, g, b, a} = this; return a === 0 ? [0, 0, 0] : [ (r / a) * scale, (g / a) * scale, (b / a) * scale ]; } /** * Returns an RGBA array of values representing the color, premultiplied by A. * * @returns An array of RGBA color values in the range [0, 1]. */ toArray01PremultipliedAlpha(): [number, number, number, number] { const {r, g, b, a} = this; return [ r, g, b, a ]; } /** * Returns an RGBA array of values representing the color, unpremultiplied by A, and converted to linear color space. * The color is defined by sRGB primaries, but the sRGB transfer function is reversed to obtain linear energy. * * @returns An array of RGBA color values in the range [0, 1]. */ toArray01Linear(): [number, number, number, number] { const {r, g, b, a} = this; return a === 0 ? [0, 0, 0, 0] : [ Math.pow((r / a), 2.2), Math.pow((g / a), 2.2), Math.pow((b / a), 2.2), a ]; } } Color.black = new Color(0, 0, 0, 1); Color.white = new Color(1, 1, 1, 1); Color.transparent = new Color(0, 0, 0, 0); Color.red = new Color(1, 0, 0, 1); Color.blue = new Color(0, 0, 1, 1); export default Color;