@mapbox/mapbox-gl-style-spec
Version:
a specification for mapbox gl styles
328 lines (280 loc) • 9.57 kB
text/typescript
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[1] / 255,
rgba[2] / 255,
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)"
*/
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})`;
}
toNonPremultipliedRenderColor(lut: LUT | null): NonPremultipliedRenderColor {
const {r, g, b, a} = this;
return new NonPremultipliedRenderColor(lut, r, g, b, a);
}
toPremultipliedRenderColor(lut: LUT | null): NonPremultipliedRenderColor {
const {r, g, b, a} = this;
return new PremultipliedRenderColor(lut, r * a, g * a, b * a, a);
}
clone(): Color {
return new Color(this.r, this.g, this.b, this.a);
}
}
export abstract class RenderColor {
premultiplied: boolean = false;
r: number;
g: number;
b: number;
a: number;
constructor(lut: LUT | null, r: number, g: number, b: number, a: number, premultiplied: boolean = false) {
this.premultiplied = premultiplied;
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.
if (this.premultiplied) {
r = a === 0 ? 0 : (r / a) * (N - 1);
g = a === 0 ? 0 : (g / a) * (N - 1);
b = a === 0 ? 0 : (b / a) * (N - 1);
} else {
r = r * (N - 1);
g = g * (N - 1);
b = b * (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 * (this.premultiplied ? a : 1);
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 * (this.premultiplied ? a : 1);
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 * (this.premultiplied ? a : 1);
this.a = a;
}
}
/**
* Returns an RGBA array of values representing the color.
* @returns An array of RGBA color values in the range [0, 255].
*/
toArray(): [number, number, number, number] {
const {r, g, b, a} = this;
return [
r * 255,
g * 255,
b * 255,
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] {
let {r, g, b, a} = this;
if (this.premultiplied) {
if (a === 0) return [0, 0, 0, 0];
r /= a;
g /= a;
b /= a;
}
const red = Math.min(Math.max(r, 0.0), 1.0);
const green = Math.min(Math.max(g, 0.0), 1.0);
const blue = Math.min(Math.max(b, 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.
*
* @returns An array of RGBA color values in the range [0, 1].
*/
toArray01(): [number, number, number, number] {
const {r, g, b, a} = this;
return [
r,
g,
b,
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} = this;
return [
r * scale,
g * scale,
b * scale
];
}
/**
* Returns an RGBA array of values representing the color 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 [
Math.pow(r, 2.2),
Math.pow(g, 2.2),
Math.pow(b, 2.2),
a
];
}
}
/**
* Renderable color created from a Color and an optional LUT value.
* Represent a color value with non-premultiplied alpha.
*/
export class NonPremultipliedRenderColor extends RenderColor {
constructor(lut: LUT | null, r: number, g: number, b: number, a: number) {
super(lut, r, g, b, a, false);
}
}
/**
* Renderable color created from a Color and an optional LUT value.
* Represent a color value with premultiplied alpha.
*/
export class PremultipliedRenderColor extends RenderColor {
constructor(lut: LUT | null, r: number, g: number, b: number, a: number) {
super(lut, r, g, b, a, true);
}
}
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;