UNPKG

@itwin/core-common

Version:

iTwin.js components common to frontend and backend

567 lines 26.4 kB
/*--------------------------------------------------------------------------------------------- * 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