@itwin/core-common
Version:
iTwin.js components common to frontend and backend
567 lines • 26.4 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Symbology
*/
import { Geometry } from "@itwin/core-geometry";
import { ColorByName } from "./ColorByName";
import { HSLColor } from "./HSLColor";
import { HSVColor, HSVConstants } from "./HSVColor";
// cspell: ignore ttbbggrr bbggrr rrggbb aabbggrr abgr rrggbb hsla lerp torgb dhue dsaturation dvalue intpart fractpart cyanish
// portions adapted from Three.js Copyright © 2010-2024 three.js authors
const scratchBytes = new Uint8Array(4);
const scratchUInt32 = new Uint32Array(scratchBytes.buffer);
/** An immutable integer representation of a color.
*
* A color consists of 4 components: Red, Blue, Green, and Transparency. Each component is an 8-bit unsigned integer in the range [0..255]. A value of zero means that component contributes nothing
* to the color: e.g., a color with Red=0 contains no shade of red, and a color with Transparency=0 is fully opaque. A value of 255 means that component contributes its maximum
* value to the color: e.g., a color with Red=255 is as red as it is possible to be, and a color with Transparency=255 is fully transparent.
*
* Internally, these 4 components are combined into a single 32-bit unsigned integer as represented by [[ColorDefProps]]. This representation can result in some confusion regarding:
* 1. The ordering of the individual components; and
* 2. Whether to specify transparency or opacity (sometimes referred to as "alpha").
*
* ColorDef uses `0xTTBBGGRR` internally, which uses Transparency and puts Red in the low byte and Transparency in the high byte. It can be converted to `0xRRGGBB` format (blue in the low byte)
* using [[getRgb]] and `0xAABBGGRRx format (red in the low byte, using opacity instead of transparency) using [[getAbgr]].
*
* A ColorDef can be created from a numeric [[ColorDefProps]], from a string in one of the common HTML formats (e.g., [[fromString]]), or by specifying values for the individual components
* (e.g., [[from]]).
*
* ColorDef is **immutable**. To obtain a modified copy of a ColorDef, use methods like [[adjustedForContrast]], [[inverse]], or [[withTransparency]]. For example:
* ```ts
* const semiTransparentBlue = ColorDef.blue.withTransparency(100);
* ```
* @public
* @extensions
*/
export class ColorDef {
_tbgr;
constructor(tbgr) {
scratchUInt32[0] = tbgr; // Force to be a 32-bit unsigned integer
this._tbgr = scratchUInt32[0];
}
/**
* Create a new ColorDef.
* @param val value to use.
* If a number, it is interpreted as a 0xTTBBGGRR (Red in the low byte, high byte is transparency 0==fully opaque) value.
* If a string, it must be in one of the forms supported by [[fromString]] - any unrecognized string will produce [[black]].
*/
static create(val) {
return this.fromTbgr(this.computeTbgr(val));
}
/** Compute the 0xTTBBGGRR value corresponding to the specified representation of a color.
* @see [[fromString]] for a description of valid string representations.
*/
static computeTbgr(val) {
switch (typeof val) {
case "number":
return val;
case "string":
return this.computeTbgrFromString(val);
default:
return 0;
}
}
/** Convert this ColorDef to a 32 bit number representing the 0xTTBBGGRR value */
toJSON() { return this._tbgr; }
/** Create a new ColorDef from a json object. If the json object is a number, it is assumed to be a 0xTTBBGGRR value. */
static fromJSON(json) {
return this.create(json);
}
/** Create a ColorDef from Red, Green, Blue, Transparency values. All inputs should be integers between 0-255. */
static from(red, green, blue, transparency) {
return this.fromTbgr(this.computeTbgrFromComponents(red, green, blue, transparency));
}
/** Compute the 0xTTBBGGRR value corresponding to the specified Red, Green, Blue, Transparency components. All inputs should be integers between 0-255. */
static computeTbgrFromComponents(red, green, blue, transparency) {
scratchBytes[0] = red;
scratchBytes[1] = green;
scratchBytes[2] = blue;
scratchBytes[3] = transparency || 0;
return scratchUInt32[0];
}
/** Create a ColorDef from its 0xTTBBGGRR representation. */
static fromTbgr(tbgr) {
switch (tbgr) {
case ColorByName.black:
return this.black;
case ColorByName.white:
return this.white;
case ColorByName.red:
return this.red;
case ColorByName.green:
return this.green;
case ColorByName.blue:
return this.blue;
default:
return new ColorDef(tbgr);
}
}
/** Create a ColorDef from its 0xAABBGGRR representation. */
static fromAbgr(abgr) {
return this.fromTbgr(this.getAbgr(abgr));
}
/** Create a ColorDef from a string representation. The following representations are supported:
* *"rgb(255,0,0)"*
* *"rgba(255,0,0,.2)"*
* *"rgb(100%,0%,0%)"*
* *"hsl(120,50%,50%)"*
* *"#rrbbgg"*
* *"blanchedAlmond"* (see possible values from [[ColorByName]]). Case-insensitive.
*
* If `val` is not a valid color string, this function returns [[black]].
* @see [[isValidColor]] to determine if `val` is a valid color string.
*/
static fromString(val) {
return this.fromTbgr(this.computeTbgrFromString(val));
}
/** Determine whether the input is a valid representation of a ColorDef.
* @see [[fromString]] for the definition of a valid string representation.
* @see [[ColorDefProps]] for the definition of a valid numeric representation.
*/
static isValidColor(val) {
if (typeof val === "number")
return val >= 0 && val <= 0xffffffff && Math.floor(val) === val;
return undefined !== this.tryComputeTbgrFromString(val);
}
/** Compute the 0xTTBBGGRR value corresponding to a string representation of a color.
* If `val` is not a valid color string, this function returns 0 (black).
* @see [[fromString]] for the definition of a valid color string.
* @see [[tryComputeTbgrFromString]] to determine if `val` is a valid color string.
*/
static computeTbgrFromString(val) {
return this.tryComputeTbgrFromString(val) ?? 0;
}
/** Try to compute the 0xTTBBGGRR value corresponding to a string representation of a ColorDef.
* @returns the corresponding numeric representation, or `undefined` if the input does not represent a color.
* @see [[fromString]] for the definition of a valid color string.
*/
static tryComputeTbgrFromString(val) {
if (typeof val !== "string")
return undefined;
val = val.toLowerCase();
let m = /^((?:rgb|hsl)a?)\(\s*([^\)]*)\)/.exec(val);
if (m) { // rgb / hsl
let color;
const name = m[1];
const components = m[2];
const hasPercent = (str) => str[str.length - 1] === "%";
const floatOrPercent = (str) => {
const v = parseFloat(str);
return 255 * Geometry.clamp(hasPercent(str) ? v / 100 : v, 0, 1);
};
const intOrPercent = (str) => {
const v = hasPercent(str) ? (parseFloat(str) / 100) * 255 : parseInt(str, 10);
return Geometry.clamp(v, 0, 255);
};
switch (name) {
case "rgb":
case "rgba":
color = /^(\d+%*)\s*[, ]\s*(\d+%*)\s*[, ]\s*(\d+%*)\s*([,\/]\s*([0-9]*\.?[0-9]+%*)\s*)?$/.exec(components);
if (color) { // rgb(255,0,0) rgba(255,0,0,0.5)
return this.computeTbgrFromComponents(intOrPercent(color[1]), intOrPercent(color[2]), intOrPercent(color[3]), typeof color[5] === "string" ? 255 - floatOrPercent(color[5]) : 0);
}
break;
case "hsl":
case "hsla":
color = /^([0-9]*\.?[0-9]+)\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(,\s*([0-9]*\.?[0-9]+)\s*)?$/.exec(components);
if (color) { // hsl(120,50%,50%) hsla(120,50%,50%,0.5)
const h = parseFloat(color[1]) / 360;
const s = parseInt(color[2], 10) / 100;
const l = parseInt(color[3], 10) / 100;
const t = typeof color[5] === "string" ? 255 - floatOrPercent(color[5]) : 0;
return this.computeTbgrFromHSL(h, s, l, t);
}
break;
}
}
else if (m = /^\#([a-f0-9]+)$/.exec(val)) { // hex color
const hex = m[1];
const size = hex.length;
if (size === 3) { // #ff0
return this.computeTbgrFromComponents(parseInt(hex.charAt(0) + hex.charAt(0), 16), parseInt(hex.charAt(1) + hex.charAt(1), 16), parseInt(hex.charAt(2) + hex.charAt(2), 16), 0);
}
if (size === 6) { // #ff0000
return this.computeTbgrFromComponents(parseInt(hex.charAt(0) + hex.charAt(1), 16), parseInt(hex.charAt(2) + hex.charAt(3), 16), parseInt(hex.charAt(4) + hex.charAt(5), 16), 0);
}
}
if (val && val.length > 0) { // ColorRgb value
for (const [key, value] of Object.entries(ColorByName))
if (key.toLowerCase() === val)
return value;
}
return undefined;
}
/** Get the red, green, blue, and transparency values from this ColorDef. Values will be integers between 0-255. */
get colors() {
return ColorDef.getColors(this._tbgr);
}
/** Get the r,g,b,t values encoded in an 0xTTBBGGRR value. Values will be integers between 0-255. */
static getColors(tbgr) {
scratchUInt32[0] = tbgr;
return {
b: scratchBytes[2],
g: scratchBytes[1],
r: scratchBytes[0],
t: scratchBytes[3],
};
}
/** The color value of this ColorDef as an integer in the form 0xTTBBGGRR (red in the low byte) */
get tbgr() { return this._tbgr; }
/** Get the value of the color as a number in 0xAABBGGRR format (i.e. red is in low byte). Transparency (0==fully opaque) converted to alpha (0==fully transparent). */
getAbgr() {
return ColorDef.getAbgr(this._tbgr);
}
/** Get the value of a 0xTTBBGGRR color as a number in 0xAABBGGRR format (i.e. red is in low byte). Transparency (0==fully opaque) converted to alpha (0==fully transparent). */
static getAbgr(tbgr) {
scratchUInt32[0] = tbgr;
scratchBytes[3] = 255 - scratchBytes[3];
return scratchUInt32[0];
}
/** Get the RGB value of the color as a number in 0xRRGGBB format (i.e blue is in the low byte). Transparency is ignored. Value will be from 0 to 2^24 */
getRgb() {
return ColorDef.getRgb(this._tbgr);
}
/** Get the RGB value of the 0xTTBBGGRR color as a number in 0xRRGGBB format (i.e blue is in the low byte). Transparency is ignored. Value will be from 0 to 2^24 */
static getRgb(tbgr) {
scratchUInt32[0] = tbgr;
return (scratchBytes[0] << 16) + (scratchBytes[1] << 8) + scratchBytes[2];
}
/** Return a copy of this ColorDef with the specified alpha component.
* @param alpha the new alpha value as an integer between 0-255.
* @returns A ColorDef with equivalent red, green, and blue components to this one but with the specified alpha.
*/
withAlpha(alpha) {
const tbgr = ColorDef.withAlpha(this._tbgr, alpha);
return tbgr === this._tbgr ? this : ColorDef.fromTbgr(tbgr);
}
/** Return a color equivalent to the specified 0xTTBBGGRR but with modified alpha component.
* @param alpha the new alpha value as an integer between 0-255.
* @returns The 0xTTBBGGRR value equivalent to `tbgr` but with the specified alpha.
*/
static withAlpha(tbgr, alpha) {
scratchUInt32[0] = tbgr;
scratchBytes[3] = 255 - (alpha | 0);
return scratchUInt32[0];
}
/** Get the alpha value for this ColorDef. Will be between 0-255 */
getAlpha() {
return ColorDef.getAlpha(this._tbgr);
}
/** Extract the alpha value from a 0xTTBBGGRR color. */
static getAlpha(tbgr) {
scratchUInt32[0] = tbgr;
return 255 - scratchBytes[3];
}
/** True if this ColorDef is fully opaque. */
get isOpaque() {
return ColorDef.isOpaque(this._tbgr);
}
/** True if the specified 0xTTBBGGRR color is fully opaque. */
static isOpaque(tbgr) {
return 255 === this.getAlpha(tbgr);
}
/** Get the transparency value for this ColorDef (inverse of alpha). Will be between 0-255. */
getTransparency() {
return ColorDef.getTransparency(this._tbgr);
}
/** Extract the transparency component from a 0xTTBBGGRR color as an integer between 0-255.. */
static getTransparency(tbgr) {
scratchUInt32[0] = tbgr;
return scratchBytes[3];
}
/** Create a copy of this ColorDef with the specified transparency.
* @param transparency the new transparency value. Must be between 0-255, where 0 means 'fully opaque' and 255 means 'fully transparent'.
* @returns a new ColorDef with the same color as this one and the specified transparency.
*/
withTransparency(transparency) {
const tbgr = ColorDef.withTransparency(this._tbgr, transparency);
return tbgr === this._tbgr ? this : ColorDef.fromTbgr(tbgr);
}
/** Compute the 0xTTBBGGRR value of the specified color and transparency.
* @param transparency the new transparency as an integer between 0-255.
* @returns The 0xTTBBGGRR value equivalent to `tbgr` but with the specified transparency.
*/
static withTransparency(tbgr, transparency) {
return this.withAlpha(tbgr, 255 - transparency);
}
/** The "known name" for this ColorDef. Will be undefined if color value is not in [[ColorByName]] list */
get name() {
return ColorDef.getName(this.tbgr);
}
/** Obtain the name of the color in the [[ColorByName]] list associated with the specified 0xTTBBGGRR value, or undefined if no such named color exists.
* @note A handful of colors (like "aqua" and "cyan") have identical tbgr values; in such cases the first match will be returned.
*/
static getName(tbgr) {
for (const [key, value] of Object.entries(ColorByName))
if (value === tbgr)
return key;
return undefined;
}
/** Convert this ColorDef to a string in the form "#rrggbb" where values are hex digits of the respective colors */
toHexString() {
return ColorDef.toHexString(this.tbgr);
}
/** Convert the 0xTTBBGGRR value to a string in the form "#rrggbb". */
static toHexString(tbgr) {
return `#${(`000000${this.getRgb(tbgr).toString(16)}`).slice(-6)}`;
}
static getColorsString(tbgr) {
const c = this.getColors(tbgr);
return `${c.r},${c.g},${c.b}`;
}
/** Convert this ColorDef to a string in the form "rgb(r,g,b)" where values are decimal digits of the respective colors. */
toRgbString() {
return ColorDef.toRgbString(this.tbgr);
}
/** Convert the 0xTTBBGGRR color to a string in the form "rgb(r,g,b)" where each component is specified in decimal. */
static toRgbString(tbgr) {
return `rgb(${this.getColorsString(tbgr)})`;
}
/** Convert this ColorDef to a string in the form "rgba(r,g,b,a)" where color values are decimal digits and a is a fraction */
toRgbaString() {
return ColorDef.toRgbaString(this.tbgr);
}
/** Convert the 0xTTBBGGRR color to a string of the form "rgba(r,g,b,a)" where the color components are specified in decimal and the alpha component is a fraction. */
static toRgbaString(tbgr) {
return `rgba(${this.getColorsString(tbgr)},${this.getAlpha(tbgr) / 255.})`;
}
/** Create a ColorDef that is the linear interpolation of this ColorDef and another ColorDef, using a weighting factor.
* @param color2 The other color
* @param weight The weighting factor for color2. 0.0 = this color, 1.0 = color2.
* @param result Optional ColorDef to hold result. If undefined, a new ColorDef is created.
*/
lerp(color2, weight) {
return ColorDef.fromTbgr(ColorDef.lerp(this.tbgr, color2.tbgr, weight));
}
/** Interpolate between two 0xTTBBGGRR colors using a weighting factor.
* @param tbgr1 The first color
* @param tbgr2 The other color
* @param weight The weighting factor in [0..1]. A value of 0.0 selects `tbgr1`; 1.0 selects `tbgr2`; 0.5 mixes them evenly; etc.
* @returns The linear interpolation between `tbgr1` and `tbgr2` using the specified weight.
*/
static lerp(tbgr1, tbgr2, weight) {
const c = this.getColors(tbgr1);
const color = this.getColors(tbgr2);
c.r += (color.r - c.r) * weight;
c.g += (color.g - c.g) * weight;
c.b += (color.b - c.b) * weight;
return this.computeTbgrFromComponents(c.r, c.g, c.b, c.t);
}
/** Create a new ColorDef that is the inverse (all colors set to 255 - this) of this color. Ignores transparency - result has 0 transparency. */
inverse() {
return ColorDef.fromTbgr(ColorDef.inverse(this.tbgr));
}
/** Return a 0xTTBBGGRR color whose color components are the inverse of the input color. The result has 0 transparency. */
static inverse(tbgr) {
const colors = this.getColors(tbgr);
return this.computeTbgrFromComponents(255 - colors.r, 255 - colors.g, 255 - colors.b);
}
/** Create a ColorDef from hue, saturation, lightness values */
static fromHSL(h, s, l, transparency = 0) {
return this.fromTbgr(this.computeTbgrFromHSL(h, s, l, transparency));
}
/** Compute the 0xTTBBGGRR color corresponding to the specified hue, saturation, lightness values. */
static computeTbgrFromHSL(h, s, l, transparency = 0) {
const torgb = (p1, q1, t) => {
if (t < 0)
t += 1;
if (t > 1)
t -= 1;
if (t < 1 / 6)
return p1 + (q1 - p1) * 6 * t;
if (t < 1 / 2)
return q1;
if (t < 2 / 3)
return p1 + (q1 - p1) * 6 * (2 / 3 - t);
return p1;
};
const hue2rgb = (p1, q1, t) => Math.round(torgb(p1, q1, t) * 255);
const modulo = (n, m) => ((n % m) + m) % m;
// h,s,l ranges are in 0.0 - 1.0
h = modulo(h, 1);
s = Geometry.clamp(s, 0, 1);
l = Geometry.clamp(l, 0, 1);
if (s === 0) {
l *= 255;
return this.computeTbgrFromComponents(l, l, l, transparency);
}
const p = l <= 0.5 ? l * (1 + s) : l + s - (l * s);
const q = (2 * l) - p;
return this.computeTbgrFromComponents(hue2rgb(q, p, h + 1 / 3), hue2rgb(q, p, h), hue2rgb(q, p, h - 1 / 3), transparency);
}
/** Create an [[HSLColor]] from this ColorDef */
toHSL() {
// h,s,l ranges are in 0.0 - 1.0
const col = this.colors;
col.r /= 255;
col.g /= 255;
col.b /= 255;
const max = Math.max(col.r, col.g, col.b);
const min = Math.min(col.r, col.g, col.b);
let hue = 0;
let saturation;
const lightness = (min + max) / 2.0;
if (min === max) {
saturation = 0;
}
else {
const delta = max - min;
saturation = lightness <= 0.5 ? delta / (max + min) : delta / (2 - max - min);
switch (max) {
case col.r:
hue = (col.g - col.b) / delta + (col.g < col.b ? 6 : 0);
break;
case col.g:
hue = (col.b - col.r) / delta + 2;
break;
case col.b:
hue = (col.r - col.g) / delta + 4;
break;
}
hue /= 6;
}
return new HSLColor(hue, saturation, lightness);
}
/** Create an [[HSVColor]] from this ColorDef */
toHSV() {
const { r, g, b } = this.colors;
let min = (r < g) ? r : g;
if (b < min)
min = b;
let max = (r > g) ? r : g;
if (b > max)
max = b;
/* amount of "blackness" present */
const v = Math.floor((max / 255.0 * 100) + 0.5);
const deltaRgb = max - min;
const s = (max !== 0.0) ? Math.floor((deltaRgb / max * 100) + 0.5) : 0;
let h = 0;
if (s) {
const redDistance = (max - r) / deltaRgb;
const greenDistance = (max - g) / deltaRgb;
const blueDistance = (max - b) / deltaRgb;
let intermediateHue;
if (r === max) /* color between yellow & magenta */
intermediateHue = blueDistance - greenDistance;
else if (g === max) /* color between cyan & yellow */
intermediateHue = 2.0 + redDistance - blueDistance;
else /* color between magenta & cyan */
intermediateHue = 4.0 + greenDistance - redDistance;
/* intermediate hue is [0..6] */
intermediateHue *= 60;
if (intermediateHue < 0.0)
intermediateHue += 360;
h = Math.floor(intermediateHue + 0.5);
if (h >= 360)
h = 0;
}
else {
h = 0;
}
return new HSVColor(h, s, v);
}
/** Create a ColorDef from an HSVColor */
static fromHSV(hsv, transparency = 0) {
// Check for simple case first.
if ((!hsv.s) || (hsv.h === -1)) {
// hue must be undefined, have no color only white
const white = 0xff & Math.floor(((255.0 * hsv.v) / 100.0) + 0.5 + 3.0e-14);
return ColorDef.from(white, white, white, 0);
}
let dhue = hsv.h, dsaturation = hsv.s, dvalue = hsv.v;
if (dhue === 360)
dhue = 0.0;
dhue /= 60; // hue is now [0..6]
const hueIntpart = Math.floor(dhue); // convert double -> int
const hueFractpart = dhue - hueIntpart;
dvalue /= 100;
dsaturation /= 100;
const p = 0xff & Math.floor((dvalue * (1.0 - dsaturation) * 255.0) + 0.5);
const q = 0xff & Math.floor((dvalue * (1.0 - (dsaturation * hueFractpart)) * 255.0) + 0.5);
const t = 0xff & Math.floor((dvalue * (1.0 - (dsaturation * (1.0 - hueFractpart))) * 255.0) + 0.5);
const v = 0xff & Math.floor(dvalue * 255 + 0.5);
let r = 0, b = 0, g = 0;
switch (hueIntpart) {
case 0:
r = v;
g = t;
b = p;
break; // reddish
case 1:
r = q, g = v;
b = p;
break; // yellowish
case 2:
r = p, g = v;
b = t;
break; // greenish
case 3:
r = p, g = q;
b = v;
break; // cyanish
case 4:
r = t, g = p;
b = v;
break; // bluish
case 5:
r = v, g = p;
b = q;
break; // magenta-ish
}
return ColorDef.from(r, g, b, transparency);
}
visibilityCheck(other) {
const fg = this.colors;
const bg = other.colors;
// Compute luminosity
const red = Math.abs(fg.r - bg.r);
const green = Math.abs(fg.g - bg.g);
const blue = Math.abs(fg.b - bg.b);
return (0.30 * red) + (0.59 * green) + (0.11 * blue);
}
/**
* Create a new ColorDef that is adjusted from this ColorDef for maximum contrast against another color. The color will either be lighter
* or darker, depending on which has more visibility against the other color.
* @param other the color to contrast with
* @param alpha optional alpha value for the adjusted color. If not supplied alpha from this color is used.
*/
adjustedForContrast(other, alpha) {
const visibility = this.visibilityCheck(other);
if (HSVConstants.VISIBILITY_GOAL <= visibility) {
return undefined !== alpha ? this.withAlpha(alpha) : this;
}
const adjPercent = Math.floor(((HSVConstants.VISIBILITY_GOAL - visibility) / 255.0) * 100.0);
let darkerHSV = this.toHSV();
let brightHSV = darkerHSV.clone();
darkerHSV = darkerHSV.adjusted(true, adjPercent);
brightHSV = brightHSV.adjusted(false, adjPercent);
if (undefined === alpha)
alpha = this.getAlpha();
const darker = ColorDef.fromHSV(darkerHSV).withAlpha(alpha);
const bright = ColorDef.fromHSV(brightHSV).withAlpha(alpha);
if (bright.getRgb() === other.getRgb()) // Couldn't adjust brighter...
return darker;
if (darker.getRgb() === other.getRgb()) // Couldn't adjust darker...
return bright;
// NOTE: Best choice is the one most visible against the other color...
return (bright.visibilityCheck(other) >= darker.visibilityCheck(other)) ? bright : darker;
}
/** True if the value of this ColorDef is the same as another ColorDef. */
equals(other) {
return this._tbgr === other._tbgr;
}
/** pure black */
static black = new ColorDef(ColorByName.black);
/** pure white */
static white = new ColorDef(ColorByName.white);
/** pure red */
static red = new ColorDef(ColorByName.red);
/** pure green */
static green = new ColorDef(ColorByName.green);
/** pure blue */
static blue = new ColorDef(ColorByName.blue);
}
//# sourceMappingURL=ColorDef.js.map