UNPKG

@itwin/core-common

Version:

iTwin.js components common to frontend and backend

458 lines • 22.9 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 { assert } from "@itwin/core-bentley"; import { Angle } from "@itwin/core-geometry"; import { ColorDef } from "./ColorDef"; import { ImageBuffer, ImageBufferFormat } from "./Image"; import { ThematicGradientColorScheme, ThematicGradientMode, ThematicGradientSettings } from "./ThematicDisplay"; /** Namespace containing types for defining a color gradient, often used for filled planar regions. * @see [[GeometryParams]] * @see [[GraphicParams]] * @public */ export var Gradient; (function (Gradient) { /** Flags applied to a [[Gradient.Symb]]. */ let Flags; (function (Flags) { /** No flags. */ Flags[Flags["None"] = 0] = "None"; /** Reverse the order of the gradient keys. */ Flags[Flags["Invert"] = 1] = "Invert"; /** Draw an outline around the surface to which the gradient is applied. */ Flags[Flags["Outline"] = 2] = "Outline"; })(Flags = Gradient.Flags || (Gradient.Flags = {})); /** Enumerates the modes by which a [[Gradient.Symb]]'s keys are applied to create an image. */ let Mode; (function (Mode) { Mode[Mode["None"] = 0] = "None"; Mode[Mode["Linear"] = 1] = "Linear"; Mode[Mode["Curved"] = 2] = "Curved"; Mode[Mode["Cylindrical"] = 3] = "Cylindrical"; Mode[Mode["Spherical"] = 4] = "Spherical"; Mode[Mode["Hemispherical"] = 5] = "Hemispherical"; /** For a gradient created based for [[ThematicDisplay]]. */ Mode[Mode["Thematic"] = 6] = "Thematic"; })(Mode = Gradient.Mode || (Gradient.Mode = {})); /** Gradient fraction value to [[ColorDef]] pair * @see [[Gradient.KeyColorProps]] */ class KeyColor { value; color; constructor(json) { this.value = json.value; this.color = ColorDef.fromJSON(json.color); } } Gradient.KeyColor = KeyColor; /** Compare two KeyColor objects for equality. Returns true if equal. */ function keyColorEquals(a, b) { return (a.value === b.value) && a.color.equals(b.color); } Gradient.keyColorEquals = keyColorEquals; /** Multi-color area fill defined by a range of colors that vary by position. * Gradient fill can be applied to planar regions. * @see [[Gradient.SymbProps]] */ class Symb { mode = Mode.None; flags = Flags.None; angle; tint; shift = 0; thematicSettings; keys = []; /** create a GradientSymb from a json object. */ static fromJSON(json) { const result = new Symb(); if (!json) return result; result.mode = json.mode; result.flags = (json.flags === undefined) ? Flags.None : json.flags; result.angle = json.angle ? Angle.fromJSON(json.angle) : undefined; result.tint = json.tint; result.shift = json.shift ? json.shift : 0; json.keys.forEach((key) => result.keys.push(new KeyColor(key))); result.thematicSettings = (json.thematicSettings === undefined) ? undefined : ThematicGradientSettings.fromJSON(json.thematicSettings); return result; } static _fixedSchemeKeys = [ // NB: these color values are ordered as rbg. Note how the components are applied below. [[0.0, 0, 255, 0], [0.25, 0, 255, 255], [0.5, 0, 0, 255], [0.75, 255, 0, 255], [1.0, 255, 0, 0]], // Blue Red. [[0.0, 255, 0, 0], [0.25, 255, 0, 255], [0.5, 0, 0, 255], [0.75, 0, 255, 255], [1.0, 0, 255, 0]], // Red blue. [[0.0, 0, 0, 0], [1.0, 255, 255, 255]], // Monochrome. [[0.0, 152, 148, 188], [0.5, 204, 160, 204], [1.0, 152, 72, 128]], // Based off of the topographic gradients in Point Clouds. [[0.0, 0, 255, 0], [0.2, 72, 96, 160], [0.4, 152, 96, 160], [0.6, 128, 32, 104], [0.7, 148, 180, 128], [1.0, 240, 240, 240]], // Based off of the sea-mountain gradient in Point Clouds. ]; static _fixedCustomKeys = [[0.0, 255, 0, 0], [1.0, 0, 255, 0]]; /** Create for [[ThematicDisplay]]. */ static createThematic(settings) { const result = new Symb(); result.mode = Mode.Thematic; result.thematicSettings = settings; if (settings.colorScheme < ThematicGradientColorScheme.Custom) { for (const keyValue of Gradient.Symb._fixedSchemeKeys[settings.colorScheme]) result.keys.push(new KeyColor({ value: keyValue[0], color: ColorDef.computeTbgrFromComponents(keyValue[1], keyValue[3], keyValue[2]) })); } else { // custom color scheme; must use custom keys assert(settings.customKeys.length > 1, "Custom thematic mode requires at least two keys to be defined"); if (settings.customKeys.length > 1) { settings.customKeys.forEach((keyColor) => result.keys.push(keyColor)); } else { // if custom color keys are not specified properly, revert to some basic key scheme and assert for (const keyValue of Gradient.Symb._fixedCustomKeys) result.keys.push(new KeyColor({ value: keyValue[0], color: ColorDef.from(keyValue[1], keyValue[3], keyValue[2]).toJSON() })); } } return result; } toJSON() { return { ...this, thematicSettings: this.thematicSettings?.toJSON(), keys: this.keys.map((key) => ({ value: key.value, color: key.color.toJSON() })), }; } clone() { return Symb.fromJSON(this.toJSON()); } /** Returns true if this symbology is equal to another, false otherwise. */ equals(other) { return Symb.compareSymb(this, other) === 0; } /** Compares two gradient symbologies. Used for ordering Gradient.Symb objects. * @param lhs First gradient to compare * @param rhs Second gradient to compare * @returns 0 if lhs is equivalent to rhs, a negative number if lhs compares less than rhs, or a positive number if lhs compares greater than rhs. */ static compareSymb(lhs, rhs) { if (lhs === rhs) return 0; // Same pointer if (lhs.mode !== rhs.mode) return lhs.mode - rhs.mode; if (lhs.flags !== rhs.flags) if (lhs.flags === undefined) return -1; else if (rhs.flags === undefined) return 1; else return lhs.flags - rhs.flags; if (lhs.tint !== rhs.tint) if (lhs.tint === undefined) return -1; else if (rhs.tint === undefined) return 1; else return lhs.tint - rhs.tint; if (lhs.shift !== rhs.shift) if (lhs.shift === undefined) return -1; else if (rhs.shift === undefined) return 1; else return lhs.shift - rhs.shift; if ((lhs.angle === undefined) !== (rhs.angle === undefined)) if (lhs.angle === undefined) return -1; else return 1; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (lhs.angle && !lhs.angle.isAlmostEqualNoPeriodShift(rhs.angle)) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return lhs.angle.radians - rhs.angle.radians; if (lhs.keys.length !== rhs.keys.length) return lhs.keys.length - rhs.keys.length; for (let i = 0; i < lhs.keys.length; i++) { if (lhs.keys[i].value !== rhs.keys[i].value) return lhs.keys[i].value - rhs.keys[i].value; if (!lhs.keys[i].color.equals(rhs.keys[i].color)) return lhs.keys[i].color.tbgr - rhs.keys[i].color.tbgr; } if (lhs.thematicSettings !== rhs.thematicSettings) if (undefined === lhs.thematicSettings) return -1; else if (undefined === rhs.thematicSettings) return 1; else { const thematicCompareResult = ThematicGradientSettings.compare(lhs.thematicSettings, rhs.thematicSettings); if (0 !== thematicCompareResult) return thematicCompareResult; } return 0; } /** Compare this symbology to another. * @see [[Gradient.Symb.compareSymb]] */ compare(other) { return Gradient.Symb.compareSymb(this, other); } /** * Ensure the value given is within the range of 0 to 255, * and truncate the value to only the 8 least significant bits. */ roundToByte(num) { return Math.min(num + .5, 255.0) & 0xFF; } /** Maps a value to an RGBA value adjusted from a color present in this symbology's array. */ mapColor(value) { if (value < 0) value = 0; else if (value > 1) value = 1; if ((this.flags & Flags.Invert) !== 0) value = 1 - value; let idx = 0; let d; let w0; let w1; if (this.keys.length <= 2) { w0 = 1.0 - value; w1 = value; } else { // locate value in map, blend corresponding colors while (idx < (this.keys.length - 2) && value > this.keys[idx + 1].value) idx++; d = this.keys[idx + 1].value - this.keys[idx].value; w1 = d < 0.0001 ? 0.0 : (value - this.keys[idx].value) / d; w0 = 1.0 - w1; } const color0 = this.keys[idx].color; const color1 = this.keys[idx + 1].color; const colors0 = color0.colors; const colors1 = color1.colors; const red = w0 * colors0.r + w1 * colors1.r; const green = w0 * colors0.g + w1 * colors1.g; const blue = w0 * colors0.b + w1 * colors1.b; const transparency = w0 * colors0.t + w1 * colors1.t; return ColorDef.from(this.roundToByte(red), this.roundToByte(green), this.roundToByte(blue), this.roundToByte(transparency)); } get hasTranslucency() { for (const key of this.keys) { if (!key.color.isOpaque) return true; } return false; } /** Returns true if the [[Gradient.Flags.Outline]] flag is set. */ get isOutlined() { return 0 !== (this.flags & Flags.Outline); } /** Produce an image suitable for use for thematic rendering. * This function is chiefly useful for the WebGL renderer. * @see [[Gradient.Symb.getImage]] to obtain a generally useful image instead. */ getThematicImageForRenderer(maxDimension) { assert(Mode.Thematic === this.mode, "getThematicImageForRenderer only is used for thematic display."); let settings = this.thematicSettings; if (settings === undefined) { settings = ThematicGradientSettings.defaults; } const stepCount = Math.min(settings.stepCount, maxDimension); const dimension = (ThematicGradientMode.Smooth === settings.mode) ? maxDimension : stepCount; const image = new Uint8Array(1 * dimension * 4); let currentIdx = image.length - 1; function addColor(color) { image[currentIdx--] = color.getAlpha(); image[currentIdx--] = color.colors.b; image[currentIdx--] = color.colors.g; image[currentIdx--] = color.colors.r; } switch (settings.mode) { case ThematicGradientMode.Smooth: { for (let j = 0; j < dimension; j++) { const f = (1 - j / (dimension)); addColor(this.mapColor(f)); } break; } case ThematicGradientMode.SteppedWithDelimiter: case ThematicGradientMode.IsoLines: // The work to generate the delimiter lines and isolines is done completely in the shader. // Therefore, we just fall through here and use a regular stepped gradient. case ThematicGradientMode.Stepped: { assert(settings.stepCount > 1, "Step count must be at least two to generate renderer gradient for thematic display"); for (let j = 0; j < dimension; j++) { // If we use Smooth's approach to generate the gradient... // We would get these values for stepCount five: 0 .2 .4 .6 .8 // We really want these values: 0 .25 .5 .75 1 // This preserves an exact color mapping of a n-step gradient when stepCount also equals n. // stepCount must be at least two for this. The thematic API enforces stepCount of at least 2. const f = (1 - j / (dimension - 1)); addColor(this.mapColor(f)); } break; } } assert(-1 === currentIdx); const imageBuffer = ImageBuffer.create(image, ImageBufferFormat.Rgba, 1); assert(undefined !== imageBuffer); return imageBuffer; } /** Produces a bitmap image from this gradient. * @param width Width of the image * @param height Height of the image * @note If this gradient uses [[Gradient.Mode.Thematic]], then the width of the image will be 1 and the margin color will be included in the top and bottom rows. * @see [[produceImage]] for more customization. */ getImage(width, height) { if (this.mode === Mode.Thematic) width = 1; return this.produceImage({ width, height, includeThematicMargin: true }); } /** Produces a bitmap image from this gradient. */ produceImage(args) { const { width, height, includeThematicMargin } = { ...args }; const thisAngle = (this.angle === undefined) ? 0 : this.angle.radians; const cosA = Math.cos(thisAngle); const sinA = Math.sin(thisAngle); const image = new Uint8Array(width * height * 4); let currentIdx = image.length - 1; const shift = Math.min(1.0, Math.abs(this.shift)); switch (this.mode) { case Mode.Linear: case Mode.Cylindrical: { const xs = 0.5 - 0.25 * shift * cosA; const ys = 0.5 - 0.25 * shift * sinA; let dMax; let dMin = dMax = 0.0; let d; for (let j = 0; j < 2; j++) { for (let i = 0; i < 2; i++) { d = (i - xs) * cosA + (j - ys) * sinA; if (d < dMin) dMin = d; if (d > dMax) dMax = d; } } for (let j = 0; j < height; j++) { const y = j / height - ys; for (let i = 0; i < width; i++) { const x = i / width - xs; d = x * cosA + y * sinA; let f; if (this.mode === Mode.Linear) { if (d > 0) f = 0.5 + 0.5 * d / dMax; else f = 0.5 - 0.5 * d / dMin; } else { if (d > 0) f = Math.sin(Math.PI / 2 * (1.0 - d / dMax)); else f = Math.sin(Math.PI / 2 * (1.0 - d / dMin)); } const color = this.mapColor(f); image[currentIdx--] = color.getAlpha(); image[currentIdx--] = color.colors.b; image[currentIdx--] = color.colors.g; image[currentIdx--] = color.colors.r; } } break; } case Mode.Curved: { const xs = 0.5 + 0.5 * sinA - 0.25 * shift * cosA; const ys = 0.5 - 0.5 * cosA - 0.25 * shift * sinA; for (let j = 0; j < height; j++) { const y = j / height - ys; for (let i = 0; i < width; i++) { const x = i / width - xs; const xr = 0.8 * (x * cosA + y * sinA); const yr = y * cosA - x * sinA; const f = Math.sin(Math.PI / 2 * (1 - Math.sqrt(xr * xr + yr * yr))); const color = this.mapColor(f); image[currentIdx--] = color.getAlpha(); image[currentIdx--] = color.colors.b; image[currentIdx--] = color.colors.g; image[currentIdx--] = color.colors.r; } } break; } case Mode.Spherical: { const r = 0.5 + 0.125 * Math.sin(2.0 * thisAngle); const xs = 0.5 * shift * (cosA + sinA) * r; const ys = 0.5 * shift * (sinA - cosA) * r; for (let j = 0; j < height; j++) { const y = ys + j / height - 0.5; for (let i = 0; i < width; i++) { const x = xs + i / width - 0.5; const f = Math.sin(Math.PI / 2 * (1.0 - Math.sqrt(x * x + y * y) / r)); const color = this.mapColor(f); image[currentIdx--] = color.getAlpha(); image[currentIdx--] = color.colors.b; image[currentIdx--] = color.colors.g; image[currentIdx--] = color.colors.r; } } break; } case Mode.Hemispherical: { const xs = 0.5 + 0.5 * sinA - 0.5 * shift * cosA; const ys = 0.5 - 0.5 * cosA - 0.5 * shift * sinA; for (let j = 0; j < height; j++) { const y = j / height - ys; for (let i = 0; i < width; i++) { const x = i / width - xs; const f = Math.sin(Math.PI / 2 * (1.0 - Math.sqrt(x * x + y * y))); const color = this.mapColor(f); image[currentIdx--] = color.getAlpha(); image[currentIdx--] = color.colors.b; image[currentIdx--] = color.colors.g; image[currentIdx--] = color.colors.r; } } break; } case Mode.Thematic: { const settings = this.thematicSettings ?? ThematicGradientSettings.defaults; for (let j = 0; j < height; j++) { let f = 1 - j / height; let color; if (includeThematicMargin && (f < ThematicGradientSettings.margin || f > ThematicGradientSettings.contentMax)) { color = settings.marginColor; } else { f = (f - ThematicGradientSettings.margin) / (ThematicGradientSettings.contentRange); switch (settings.mode) { case ThematicGradientMode.SteppedWithDelimiter: case ThematicGradientMode.IsoLines: case ThematicGradientMode.Stepped: { if (settings.stepCount > 1) { const fStep = Math.floor(f * settings.stepCount - 0.00001) / (settings.stepCount - 1); color = this.mapColor(fStep); } else { throw new Error(`Thematic gradient mode ${String(settings.mode)} requires at least two steps.`); } break; } case ThematicGradientMode.Smooth: color = this.mapColor(f); break; } } for (let i = 0; i < width; i++) { image[currentIdx--] = color.getAlpha(); image[currentIdx--] = color.colors.b; image[currentIdx--] = color.colors.g; image[currentIdx--] = color.colors.r; } } } } assert(-1 === currentIdx); const imageBuffer = ImageBuffer.create(image, ImageBufferFormat.Rgba, width); assert(undefined !== imageBuffer); return imageBuffer; } } Gradient.Symb = Symb; })(Gradient || (Gradient = {})); //# sourceMappingURL=Gradient.js.map