@kitware/vtk.js
Version:
Visualization Toolkit for the Web
1,215 lines (1,121 loc) • 41.6 kB
JavaScript
import { m as macro } from '../../macros2.js';
import { h as hsv2rgb, i as isNan, M as floor, N as isInf, O as rgb2hsv, P as rgb2lab, Q as lab2rgb } from '../../Common/Core/Math/index.js';
import vtkScalarsToColors from '../../Common/Core/ScalarsToColors.js';
import Constants from './ColorTransferFunction/Constants.js';
const {
ColorSpace,
Scale
} = Constants;
const {
ScalarMappingTarget
} = vtkScalarsToColors;
const {
vtkDebugMacro,
vtkErrorMacro,
vtkWarningMacro
} = macro;
// ----------------------------------------------------------------------------
// Global methods
// ----------------------------------------------------------------------------
/* eslint-disable no-continue */
// Convert to and from a special polar version of CIELAB (useful for creating
// continuous diverging color maps).
function vtkColorTransferFunctionLabToMsh(lab, msh) {
const L = lab[0];
const a = lab[1];
const b = lab[2];
const M = Math.sqrt(L * L + a * a + b * b);
const s = M > 0.001 ? Math.acos(L / M) : 0.0;
const h = s > 0.001 ? Math.atan2(b, a) : 0.0;
msh[0] = M;
msh[1] = s;
msh[2] = h;
}
function vtkColorTransferFunctionMshToLab(msh, lab) {
const M = msh[0];
const s = msh[1];
const h = msh[2];
lab[0] = M * Math.cos(s);
lab[1] = M * Math.sin(s) * Math.cos(h);
lab[2] = M * Math.sin(s) * Math.sin(h);
}
// For the case when interpolating from a saturated color to an unsaturated
// color, find a hue for the unsaturated color that makes sense.
function vtkColorTransferFunctionAdjustHue(msh, unsatM) {
if (msh[0] >= unsatM - 0.1) {
// The best we can do is hold hue constant.
return msh[2];
}
// This equation is designed to make the perceptual change of the
// interpolation to be close to constant.
const hueSpin = msh[1] * Math.sqrt(unsatM * unsatM - msh[0] * msh[0]) / (msh[0] * Math.sin(msh[1]));
// Spin hue away from 0 except in purple hues.
if (msh[2] > -0.3 * Math.PI) {
return msh[2] + hueSpin;
}
return msh[2] - hueSpin;
}
function vtkColorTransferFunctionAngleDiff(a1, a2) {
let adiff = a1 - a2;
if (adiff < 0.0) {
adiff = -adiff;
}
while (adiff >= 2.0 * Math.PI) {
adiff -= 2.0 * Math.PI;
}
if (adiff > Math.PI) {
adiff = 2.0 * Math.PI - adiff;
}
return adiff;
}
// Interpolate a diverging color map.
function vtkColorTransferFunctionInterpolateDiverging(s, rgb1, rgb2, result) {
const lab1 = [];
const lab2 = [];
rgb2lab(rgb1, lab1);
rgb2lab(rgb2, lab2);
const msh1 = [];
const msh2 = [];
vtkColorTransferFunctionLabToMsh(lab1, msh1);
vtkColorTransferFunctionLabToMsh(lab2, msh2);
// If the endpoints are distinct saturated colors, then place white in between
// them.
let localS = s;
if (msh1[1] > 0.05 && msh2[1] > 0.05 && vtkColorTransferFunctionAngleDiff(msh1[2], msh2[2]) > 0.33 * Math.PI) {
// Insert the white midpoint by setting one end to white and adjusting the
// scalar value.
let Mmid = Math.max(msh1[0], msh2[0]);
Mmid = Math.max(88.0, Mmid);
if (s < 0.5) {
msh2[0] = Mmid;
msh2[1] = 0.0;
msh2[2] = 0.0;
localS *= 2.0;
} else {
msh1[0] = Mmid;
msh1[1] = 0.0;
msh1[2] = 0.0;
localS = 2.0 * localS - 1.0;
}
}
// If one color has no saturation, then its hue value is invalid. In this
// case, we want to set it to something logical so that the interpolation of
// hue makes sense.
if (msh1[1] < 0.05 && msh2[1] > 0.05) {
msh1[2] = vtkColorTransferFunctionAdjustHue(msh2, msh1[0]);
} else if (msh2[1] < 0.05 && msh1[1] > 0.05) {
msh2[2] = vtkColorTransferFunctionAdjustHue(msh1, msh2[0]);
}
const mshTmp = [];
mshTmp[0] = (1 - localS) * msh1[0] + localS * msh2[0];
mshTmp[1] = (1 - localS) * msh1[1] + localS * msh2[1];
mshTmp[2] = (1 - localS) * msh1[2] + localS * msh2[2];
// Now convert back to RGB
const labTmp = [];
vtkColorTransferFunctionMshToLab(mshTmp, labTmp);
lab2rgb(labTmp, result);
}
// ----------------------------------------------------------------------------
// vtkColorTransferFunction methods
// ----------------------------------------------------------------------------
function vtkColorTransferFunction(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkColorTransferFunction');
// Return the number of points which specify this function
publicAPI.getSize = () => model.nodes.length;
//----------------------------------------------------------------------------
// Add a point defined in RGB
publicAPI.addRGBPoint = (x, r, g, b) => publicAPI.addRGBPointLong(x, r, g, b, 0.5, 0.0);
//----------------------------------------------------------------------------
// Add a point defined in RGB
publicAPI.addRGBPointLong = (x, r, g, b, midpoint = 0.5, sharpness = 0.0) => {
// Error check
if (midpoint < 0.0 || midpoint > 1.0) {
vtkErrorMacro('Midpoint outside range [0.0, 1.0]');
return -1;
}
if (sharpness < 0.0 || sharpness > 1.0) {
vtkErrorMacro('Sharpness outside range [0.0, 1.0]');
return -1;
}
// remove any node already at this X location
if (!model.allowDuplicateScalars) {
publicAPI.removePoint(x);
}
// Create the new node
const node = {
x,
r,
g,
b,
midpoint,
sharpness
};
// Add it, then sort to get everything in order
model.nodes.push(node);
publicAPI.sortAndUpdateRange();
// We need to find the index of the node we just added in order
// to return this value
let i = 0;
for (; i < model.nodes.length; i++) {
if (model.nodes[i].x === x) {
break;
}
}
// If we didn't find it, something went horribly wrong so
// return -1
if (i < model.nodes.length) {
return i;
}
return -1;
};
//----------------------------------------------------------------------------
// Add a point defined in HSV
publicAPI.addHSVPoint = (x, h, s, v) => publicAPI.addHSVPointLong(x, h, s, v, 0.5, 0.0);
//----------------------------------------------------------------------------
// Add a point defined in HSV
publicAPI.addHSVPointLong = (x, h, s, v, midpoint = 0.5, sharpness = 0.0) => {
const rgb = [];
const hsv = [h, s, v];
hsv2rgb(hsv, rgb);
return publicAPI.addRGBPoint(x, rgb[0], rgb[1], rgb[2], midpoint, sharpness);
};
//----------------------------------------------------------------------------
// Set nodes directly
publicAPI.setNodes = nodes => {
if (model.nodes !== nodes) {
const before = JSON.stringify(model.nodes);
model.nodes = nodes;
const after = JSON.stringify(model.nodes);
if (publicAPI.sortAndUpdateRange() || before !== after) {
publicAPI.modified();
return true;
}
}
return false;
};
//----------------------------------------------------------------------------
// Sort the vector in increasing order, then fill in
// the Range
publicAPI.sortAndUpdateRange = () => {
const before = JSON.stringify(model.nodes);
model.nodes.sort((a, b) => a.x - b.x);
const after = JSON.stringify(model.nodes);
const modifiedInvoked = publicAPI.updateRange();
// If range is updated, Modified() has been called, don't call it again.
if (!modifiedInvoked && before !== after) {
publicAPI.modified();
return true;
}
return modifiedInvoked;
};
//----------------------------------------------------------------------------
publicAPI.updateRange = () => {
const oldRange = [2];
oldRange[0] = model.mappingRange[0];
oldRange[1] = model.mappingRange[1];
const size = model.nodes.length;
if (size) {
model.mappingRange[0] = model.nodes[0].x;
model.mappingRange[1] = model.nodes[size - 1].x;
} else {
model.mappingRange[0] = 0;
model.mappingRange[1] = 0;
}
// If the range is the same, then no need to call Modified()
if (oldRange[0] === model.mappingRange[0] && oldRange[1] === model.mappingRange[1]) {
return false;
}
publicAPI.modified();
return true;
};
//----------------------------------------------------------------------------
// Remove a point
publicAPI.removePoint = x => {
// First find the node since we need to know its
// index as our return value
let i = 0;
for (; i < model.nodes.length; i++) {
if (model.nodes[i].x === x) {
break;
}
}
const retVal = i;
// If the node doesn't exist, we return -1
if (i >= model.nodes.length) {
return -1;
}
// If the first or last point has been removed, then we update the range
// No need to sort here as the order of points hasn't changed.
let modifiedInvoked = false;
model.nodes.splice(i, 1);
if (i === 0 || i === model.nodes.length) {
modifiedInvoked = publicAPI.updateRange();
}
if (!modifiedInvoked) {
publicAPI.modified();
}
return retVal;
};
//----------------------------------------------------------------------------
publicAPI.movePoint = (oldX, newX) => {
if (oldX === newX) {
// Nothing to do.
return;
}
publicAPI.removePoint(newX);
for (let i = 0; i < model.nodes.length; i++) {
if (model.nodes[i].x === oldX) {
model.nodes[i].x = newX;
publicAPI.sortAndUpdateRange();
break;
}
}
};
//----------------------------------------------------------------------------
// Remove all points
publicAPI.removeAllPoints = () => {
model.nodes = [];
publicAPI.sortAndUpdateRange();
};
//----------------------------------------------------------------------------
// Add a line defined in RGB
publicAPI.addRGBSegment = (x1, r1, g1, b1, x2, r2, g2, b2) => {
// First, find all points in this range and remove them
publicAPI.sortAndUpdateRange();
for (let i = 0; i < model.nodes.length;) {
if (model.nodes[i].x >= x1 && model.nodes[i].x <= x2) {
model.nodes.splice(i, 1);
} else {
i++;
}
}
// Now add the points
publicAPI.addRGBPointLong(x1, r1, g1, b1, 0.5, 0.0);
publicAPI.addRGBPointLong(x2, r2, g2, b2, 0.5, 0.0);
publicAPI.modified();
};
//----------------------------------------------------------------------------
// Add a line defined in HSV
publicAPI.addHSVSegment = (x1, h1, s1, v1, x2, h2, s2, v2) => {
const hsv1 = [h1, s1, v1];
const hsv2 = [h2, s2, v2];
const rgb1 = [];
const rgb2 = [];
hsv2rgb(hsv1, rgb1);
hsv2rgb(hsv2, rgb2);
publicAPI.addRGBSegment(x1, rgb1[0], rgb1[1], rgb1[2], x2, rgb2[0], rgb2[1], rgb2[2]);
};
//----------------------------------------------------------------------------
// Returns the RGBA color evaluated at the specified location
publicAPI.mapValue = x => {
const rgb = [];
publicAPI.getColor(x, rgb);
return [Math.floor(255.0 * rgb[0] + 0.5), Math.floor(255.0 * rgb[1] + 0.5), Math.floor(255.0 * rgb[2] + 0.5), 255];
};
//----------------------------------------------------------------------------
// Returns the RGB color evaluated at the specified location
publicAPI.getColor = (x, rgb) => {
if (model.indexedLookup) {
const numNodes = publicAPI.getSize();
// todo
const idx = publicAPI.getAnnotatedValueIndexInternal(x);
if (idx < 0 || numNodes === 0) {
const nanColor = publicAPI.getNanColorByReference();
rgb[0] = nanColor[0];
rgb[1] = nanColor[1];
rgb[2] = nanColor[2];
} else {
const nodeVal = [];
publicAPI.getNodeValue(idx % numNodes, nodeVal);
// nodeVal[0] is the x value. nodeVal[1...3] is rgb.
rgb[0] = nodeVal[1];
rgb[1] = nodeVal[2];
rgb[2] = nodeVal[3];
}
return;
}
publicAPI.getTable(x, x, 1, rgb);
};
//----------------------------------------------------------------------------
// Returns the red color evaluated at the specified location
publicAPI.getRedValue = x => {
const rgb = [];
publicAPI.getColor(x, rgb);
return rgb[0];
};
//----------------------------------------------------------------------------
// Returns the green color evaluated at the specified location
publicAPI.getGreenValue = x => {
const rgb = [];
publicAPI.getColor(x, rgb);
return rgb[1];
};
//----------------------------------------------------------------------------
// Returns the blue color evaluated at the specified location
publicAPI.getBlueValue = x => {
const rgb = [];
publicAPI.getColor(x, rgb);
return rgb[2];
};
publicAPI.logScaleEnabled = () => model.scale === Scale.LOG10;
publicAPI.usingLogScale = () => publicAPI.logScaleEnabled() && model.mappingRange[0] > 0.0;
//----------------------------------------------------------------------------
// Returns a table of RGB colors at regular intervals along the function
publicAPI.getTable = (xStart_, xEnd_, size, table) => {
// Note: This requires range[0] <= range[1].
const usingLogScale = publicAPI.usingLogScale();
// To handle BigInt limitation
const xStart = usingLogScale ? Math.log10(Number(xStart_)) : Number(xStart_);
const xEnd = usingLogScale ? Math.log10(Number(xEnd_)) : Number(xEnd_);
// Special case: If either the start or end is a NaN, then all any
// interpolation done on them is also a NaN. Therefore, fill the table with
// the NaN color.
if (isNan(xStart) || isNan(xEnd)) {
for (let i = 0; i < size; i++) {
table[i * 3 + 0] = model.nanColor[0];
table[i * 3 + 1] = model.nanColor[1];
table[i * 3 + 2] = model.nanColor[2];
}
return;
}
let idx = 0;
const numNodes = model.nodes.length;
// Need to keep track of the last value so that
// we can fill in table locations past this with
// this value if Clamping is On.
let lastR = 0.0;
let lastG = 0.0;
let lastB = 0.0;
if (numNodes !== 0) {
lastR = model.nodes[numNodes - 1].r;
lastG = model.nodes[numNodes - 1].g;
lastB = model.nodes[numNodes - 1].b;
}
let x = 0.0;
let x1 = 0.0;
let x2 = 0.0;
const rgb1 = [0.0, 0.0, 0.0];
const rgb2 = [0.0, 0.0, 0.0];
let midpoint = 0.0;
let sharpness = 0.0;
const tmpVec = [];
// If the scale is logarithmic, make sure the range is valid.
let scaledMappingRange = model.mappingRange;
if (usingLogScale) {
scaledMappingRange = [Math.log10(model.mappingRange[0]), Math.log10(model.mappingRange[1])];
}
// For each table entry
for (let i = 0; i < size; i++) {
// Find our location in the table
const tidx = 3 * i;
// Find our X location. If we are taking only 1 sample, make
// it halfway between start and end (usually start and end will
// be the same in this case)
if (size > 1) {
x = xStart + i / (size - 1.0) * (xEnd - xStart);
} else {
x = 0.5 * (xStart + xEnd);
}
// Linearly map x from mappingRange to [0, numberOfValues-1],
// discretize (round down to the closest integer),
// then map back to mappingRange
if (model.discretize) {
const range = scaledMappingRange;
if (x >= range[0] && x <= range[1]) {
const numberOfValues = model.numberOfValues;
const deltaRange = range[1] - range[0];
if (numberOfValues <= 1) {
x = range[0] + deltaRange / 2.0;
} else {
// normalize x
const xn = (x - range[0]) / deltaRange;
// discretize
const discretizeIndex = floor(numberOfValues * xn);
// get discretized x
x = range[0] + discretizeIndex / (numberOfValues - 1) * deltaRange;
}
}
}
// Do we need to move to the next node?
while (idx < numNodes && x > model.nodes[idx].x) {
idx++;
// If we are at a valid point index, fill in
// the value at this node, and the one before (the
// two that surround our current sample location)
// idx cannot be 0 since we just incremented it.
if (idx < numNodes) {
x1 = model.nodes[idx - 1].x;
x2 = model.nodes[idx].x;
rgb1[0] = model.nodes[idx - 1].r;
rgb2[0] = model.nodes[idx].r;
rgb1[1] = model.nodes[idx - 1].g;
rgb2[1] = model.nodes[idx].g;
rgb1[2] = model.nodes[idx - 1].b;
rgb2[2] = model.nodes[idx].b;
// We only need the previous midpoint and sharpness
// since these control this region
midpoint = model.nodes[idx - 1].midpoint;
sharpness = model.nodes[idx - 1].sharpness;
// Move midpoint away from extreme ends of range to avoid
// degenerate math
if (midpoint < 0.00001) {
midpoint = 0.00001;
}
if (midpoint > 0.99999) {
midpoint = 0.99999;
}
}
}
// Are we at or past the end? If so, just use the last value
if (x > scaledMappingRange[1]) {
table[tidx] = 0.0;
table[tidx + 1] = 0.0;
table[tidx + 2] = 0.0;
if (model.clamping) {
if (publicAPI.getUseAboveRangeColor()) {
table[tidx] = model.aboveRangeColor[0];
table[tidx + 1] = model.aboveRangeColor[1];
table[tidx + 2] = model.aboveRangeColor[2];
} else {
table[tidx] = lastR;
table[tidx + 1] = lastG;
table[tidx + 2] = lastB;
}
}
} else if (x < scaledMappingRange[0] || isInf(x) && x < 0) {
// we are before the first node? If so, duplicate this node's values.
// We have to deal with -inf here
table[tidx] = 0.0;
table[tidx + 1] = 0.0;
table[tidx + 2] = 0.0;
if (model.clamping) {
if (publicAPI.getUseBelowRangeColor()) {
table[tidx] = model.belowRangeColor[0];
table[tidx + 1] = model.belowRangeColor[1];
table[tidx + 2] = model.belowRangeColor[2];
} else if (numNodes > 0) {
table[tidx] = model.nodes[0].r;
table[tidx + 1] = model.nodes[0].g;
table[tidx + 2] = model.nodes[0].b;
}
}
} else if (idx === 0 && (Math.abs(x - xStart) < 1e-6 || model.discretize)) {
if (numNodes > 0) {
table[tidx] = model.nodes[0].r;
table[tidx + 1] = model.nodes[0].g;
table[tidx + 2] = model.nodes[0].b;
} else {
table[tidx] = 0.0;
table[tidx + 1] = 0.0;
table[tidx + 2] = 0.0;
}
} else {
// OK, we are between two nodes - interpolate
// Our first attempt at a normalized location [0,1] -
// we will be modifying this based on midpoint and
// sharpness to get the curve shape we want and to have
// it pass through (y1+y2)/2 at the midpoint.
let s = 0.0;
s = (x - x1) / (x2 - x1);
// Readjust based on the midpoint - linear adjustment
if (s < midpoint) {
s = 0.5 * s / midpoint;
} else {
s = 0.5 + 0.5 * (s - midpoint) / (1.0 - midpoint);
}
// override for sharpness > 0.99
// In this case we just want piecewise constant
if (sharpness > 0.99) {
// Use the first value since we are below the midpoint
if (s < 0.5) {
table[tidx] = rgb1[0];
table[tidx + 1] = rgb1[1];
table[tidx + 2] = rgb1[2];
continue;
} else {
// Use the second value at or above the midpoint
table[tidx] = rgb2[0];
table[tidx + 1] = rgb2[1];
table[tidx + 2] = rgb2[2];
continue;
}
}
// Override for sharpness < 0.01
// In this case we want piecewise linear
if (sharpness < 0.01) {
// Simple linear interpolation
if (model.colorSpace === ColorSpace.RGB) {
table[tidx] = (1 - s) * rgb1[0] + s * rgb2[0];
table[tidx + 1] = (1 - s) * rgb1[1] + s * rgb2[1];
table[tidx + 2] = (1 - s) * rgb1[2] + s * rgb2[2];
} else if (model.colorSpace === ColorSpace.HSV) {
const hsv1 = [];
const hsv2 = [];
rgb2hsv(rgb1, hsv1);
rgb2hsv(rgb2, hsv2);
if (model.hSVWrap && (hsv1[0] - hsv2[0] > 0.5 || hsv2[0] - hsv1[0] > 0.5)) {
if (hsv1[0] > hsv2[0]) {
hsv1[0] -= 1.0;
} else {
hsv2[0] -= 1.0;
}
}
const hsvTmp = [];
hsvTmp[0] = (1.0 - s) * hsv1[0] + s * hsv2[0];
if (hsvTmp[0] < 0.0) {
hsvTmp[0] += 1.0;
}
hsvTmp[1] = (1.0 - s) * hsv1[1] + s * hsv2[1];
hsvTmp[2] = (1.0 - s) * hsv1[2] + s * hsv2[2];
// Now convert this back to RGB
hsv2rgb(hsvTmp, tmpVec);
table[tidx] = tmpVec[0];
table[tidx + 1] = tmpVec[1];
table[tidx + 2] = tmpVec[2];
} else if (model.colorSpace === ColorSpace.LAB) {
const lab1 = [];
const lab2 = [];
rgb2lab(rgb1, lab1);
rgb2lab(rgb2, lab2);
const labTmp = [];
labTmp[0] = (1 - s) * lab1[0] + s * lab2[0];
labTmp[1] = (1 - s) * lab1[1] + s * lab2[1];
labTmp[2] = (1 - s) * lab1[2] + s * lab2[2];
// Now convert back to RGB
lab2rgb(labTmp, tmpVec);
table[tidx] = tmpVec[0];
table[tidx + 1] = tmpVec[1];
table[tidx + 2] = tmpVec[2];
} else if (model.colorSpace === ColorSpace.DIVERGING) {
vtkColorTransferFunctionInterpolateDiverging(s, rgb1, rgb2, tmpVec);
table[tidx] = tmpVec[0];
table[tidx + 1] = tmpVec[1];
table[tidx + 2] = tmpVec[2];
} else {
vtkErrorMacro('ColorSpace set to invalid value.', model.colorSpace);
}
continue;
}
// We have a sharpness between [0.01, 0.99] - we will
// used a modified hermite curve interpolation where we
// derive the slope based on the sharpness, and we compress
// the curve non-linearly based on the sharpness
// First, we will adjust our position based on sharpness in
// order to make the curve sharper (closer to piecewise constant)
if (s < 0.5) {
s = 0.5 * (s * 2.0) ** (1.0 + 10.0 * sharpness);
} else if (s > 0.5) {
s = 1.0 - 0.5 * ((1.0 - s) * 2) ** (1 + 10.0 * sharpness);
}
// Compute some coefficients we will need for the hermite curve
const ss = s * s;
const sss = ss * s;
const h1 = 2.0 * sss - 3 * ss + 1;
const h2 = -2 * sss + 3 * ss;
const h3 = sss - 2 * ss + s;
const h4 = sss - ss;
let slope;
let t;
if (model.colorSpace === ColorSpace.RGB) {
for (let j = 0; j < 3; j++) {
// Use one slope for both end points
slope = rgb2[j] - rgb1[j];
t = (1.0 - sharpness) * slope;
// Compute the value
table[tidx + j] = h1 * rgb1[j] + h2 * rgb2[j] + h3 * t + h4 * t;
}
} else if (model.colorSpace === ColorSpace.HSV) {
const hsv1 = [];
const hsv2 = [];
rgb2hsv(rgb1, hsv1);
rgb2hsv(rgb2, hsv2);
if (model.hSVWrap && (hsv1[0] - hsv2[0] > 0.5 || hsv2[0] - hsv1[0] > 0.5)) {
if (hsv1[0] > hsv2[0]) {
hsv1[0] -= 1.0;
} else {
hsv2[0] -= 1.0;
}
}
const hsvTmp = [];
for (let j = 0; j < 3; j++) {
// Use one slope for both end points
slope = hsv2[j] - hsv1[j];
t = (1.0 - sharpness) * slope;
// Compute the value
hsvTmp[j] = h1 * hsv1[j] + h2 * hsv2[j] + h3 * t + h4 * t;
if (j === 0 && hsvTmp[j] < 0.0) {
hsvTmp[j] += 1.0;
}
}
// Now convert this back to RGB
hsv2rgb(hsvTmp, tmpVec);
table[tidx] = tmpVec[0];
table[tidx + 1] = tmpVec[1];
table[tidx + 2] = tmpVec[2];
} else if (model.colorSpace === ColorSpace.LAB) {
const lab1 = [];
const lab2 = [];
rgb2lab(rgb1, lab1);
rgb2lab(rgb2, lab2);
const labTmp = [];
for (let j = 0; j < 3; j++) {
// Use one slope for both end points
slope = lab2[j] - lab1[j];
t = (1.0 - sharpness) * slope;
// Compute the value
labTmp[j] = h1 * lab1[j] + h2 * lab2[j] + h3 * t + h4 * t;
}
// Now convert this back to RGB
lab2rgb(labTmp, tmpVec);
table[tidx] = tmpVec[0];
table[tidx + 1] = tmpVec[1];
table[tidx + 2] = tmpVec[2];
} else if (model.colorSpace === ColorSpace.DIVERGING) {
// I have not implemented proper interpolation by a hermite curve for
// the diverging color map, but I cannot think of a good use case for
// that anyway.
vtkColorTransferFunctionInterpolateDiverging(s, rgb1, rgb2, tmpVec);
table[tidx] = tmpVec[0];
table[tidx + 1] = tmpVec[1];
table[tidx + 2] = tmpVec[2];
} else {
vtkErrorMacro('ColorSpace set to invalid value.');
}
// Final error check to make sure we don't go outside [0,1]
for (let j = 0; j < 3; j++) {
table[tidx + j] = table[tidx + j] < 0.0 ? 0.0 : table[tidx + j];
table[tidx + j] = table[tidx + j] > 1.0 ? 1.0 : table[tidx + j];
}
}
}
};
//----------------------------------------------------------------------------
publicAPI.getUint8Table = (xStart, xEnd, size, withAlpha = false) => {
if (publicAPI.getMTime() <= model.buildTime && model.tableSize === size && model.tableWithAlpha !== withAlpha) {
return model.table;
}
if (model.nodes.length === 0) {
vtkErrorMacro('Attempting to lookup a value with no points in the function');
return model.table;
}
const nbChannels = withAlpha ? 4 : 3;
if (model.tableSize !== size || model.tableWithAlpha !== withAlpha) {
model.table = new Uint8Array(size * nbChannels);
model.tableSize = size;
model.tableWithAlpha = withAlpha;
}
const tmpTable = [];
publicAPI.getTable(xStart, xEnd, size, tmpTable);
for (let i = 0; i < size; i++) {
model.table[i * nbChannels + 0] = Math.floor(tmpTable[i * 3 + 0] * 255.0 + 0.5);
model.table[i * nbChannels + 1] = Math.floor(tmpTable[i * 3 + 1] * 255.0 + 0.5);
model.table[i * nbChannels + 2] = Math.floor(tmpTable[i * 3 + 2] * 255.0 + 0.5);
if (withAlpha) {
model.table[i * nbChannels + 3] = 255;
}
}
model.buildTime.modified();
return model.table;
};
publicAPI.buildFunctionFromArray = array => {
publicAPI.removeAllPoints();
const numComponents = array.getNumberOfComponents();
for (let i = 0; i < array.getNumberOfTuples(); i++) {
switch (numComponents) {
case 3:
{
model.nodes.push({
x: i,
r: array.getComponent(i, 0),
g: array.getComponent(i, 1),
b: array.getComponent(i, 2),
midpoint: 0.5,
sharpness: 0.0
});
break;
}
case 4:
{
model.nodes.push({
x: array.getComponent(i, 0),
r: array.getComponent(i, 1),
g: array.getComponent(i, 2),
b: array.getComponent(i, 3),
midpoint: 0.5,
sharpness: 0.0
});
break;
}
case 5:
{
model.nodes.push({
x: i,
r: array.getComponent(i, 0),
g: array.getComponent(i, 1),
b: array.getComponent(i, 2),
midpoint: array.getComponent(i, 4),
sharpness: array.getComponent(i, 5)
});
break;
}
case 6:
{
model.nodes.push({
x: array.getComponent(i, 0),
r: array.getComponent(i, 1),
g: array.getComponent(i, 2),
b: array.getComponent(i, 3),
midpoint: array.getComponent(i, 4),
sharpness: array.getComponent(i, 5)
});
break;
}
}
}
publicAPI.sortAndUpdateRange();
};
//----------------------------------------------------------------------------
publicAPI.buildFunctionFromTable = (xStart, xEnd, size, table) => {
let inc = 0.0;
publicAPI.removeAllPoints();
if (size > 1) {
inc = (xEnd - xStart) / (size - 1.0);
}
for (let i = 0; i < size; i++) {
const node = {
x: xStart + inc * i,
r: table[i * 3],
g: table[i * 3 + 1],
b: table[i * 3 + 2],
sharpness: 0.0,
midpoint: 0.5
};
model.nodes.push(node);
}
publicAPI.sortAndUpdateRange();
};
//----------------------------------------------------------------------------
// For a specified index value, get the node parameters
publicAPI.getNodeValue = (index, val) => {
if (index < 0 || index >= model.nodes.length) {
vtkErrorMacro('Index out of range!');
return -1;
}
val[0] = model.nodes[index].x;
val[1] = model.nodes[index].r;
val[2] = model.nodes[index].g;
val[3] = model.nodes[index].b;
val[4] = model.nodes[index].midpoint;
val[5] = model.nodes[index].sharpness;
return 1;
};
//----------------------------------------------------------------------------
// For a specified index value, get the node parameters
publicAPI.setNodeValue = (index, val) => {
if (index < 0 || index >= model.nodes.length) {
vtkErrorMacro('Index out of range!');
return -1;
}
const oldX = model.nodes[index].x;
model.nodes[index].x = val[0];
model.nodes[index].r = val[1];
model.nodes[index].g = val[2];
model.nodes[index].b = val[3];
model.nodes[index].midpoint = val[4];
model.nodes[index].sharpness = val[5];
if (oldX !== val[0]) {
// The point has been moved, the order of points or the range might have
// been modified.
publicAPI.sortAndUpdateRange();
// No need to call Modified() here because SortAndUpdateRange() has done it
// already.
} else {
publicAPI.modified();
}
return 1;
};
//----------------------------------------------------------------------------
publicAPI.getNumberOfAvailableColors = () => {
if (model.indexedLookup && publicAPI.getSize()) {
return publicAPI.getSize();
}
if (model.tableSize) {
// Not sure if this is correct since it is only set if
// "const unsigned char *::GetTable(double xStart, double xEnd,int size)"
// has been called.
return model.tableSize;
}
const nNodes = model.nodes?.length ?? 0;
// The minimum is 4094 colors so that it fills in the 4096 texels texture in `mapScalarsToTexture`
return Math.max(4094, nNodes);
};
//----------------------------------------------------------------------------
publicAPI.getIndexedColor = (idx, rgba) => {
const n = publicAPI.getSize();
if (n > 0 && idx >= 0) {
const nodeValue = [];
publicAPI.getNodeValue(idx % n, nodeValue);
for (let j = 0; j < 3; ++j) {
rgba[j] = nodeValue[j + 1];
}
rgba[3] = 1.0; // NodeColor is RGB-only.
return;
}
const nanColor = publicAPI.getNanColorByReference();
rgba[0] = nanColor[0];
rgba[1] = nanColor[1];
rgba[2] = nanColor[2];
rgba[3] = 1.0; // NanColor is RGB-only.
};
//----------------------------------------------------------------------------
publicAPI.fillFromDataPointer = (nb, ptr) => {
if (nb <= 0 || !ptr) {
return;
}
publicAPI.removeAllPoints();
for (let i = 0; i < nb; i++) {
publicAPI.addRGBPoint(ptr[i * 4], ptr[i * 4 + 1], ptr[i * 4 + 2], ptr[i * 4 + 3]);
}
};
//----------------------------------------------------------------------------
publicAPI.setMappingRange = (min, max) => {
const range = [min, max];
const scaledRange = [min, max];
const originalRange = publicAPI.getRange();
const logScaleEnabled = publicAPI.logScaleEnabled();
if (originalRange[1] === range[1] && originalRange[0] === range[0]) {
return;
}
if (range[1] === range[0]) {
vtkErrorMacro('attempt to set zero width color range');
return;
}
if (logScaleEnabled) {
if (range[0] <= 0.0) {
console.warn('attempt to set log scale color range with non-positive minimum');
} else {
scaledRange[0] = Math.log10(range[0]);
scaledRange[1] = Math.log10(range[1]);
}
}
const scale = (scaledRange[1] - scaledRange[0]) / (originalRange[1] - originalRange[0]);
const shift = scaledRange[0] - originalRange[0] * scale;
for (let i = 0; i < model.nodes.length; ++i) {
model.nodes[i].x = model.nodes[i].x * scale + shift;
}
model.mappingRange[0] = range[0];
model.mappingRange[1] = range[1];
publicAPI.modified();
};
//----------------------------------------------------------------------------
publicAPI.adjustRange = range => {
const functionRange = publicAPI.getRange();
// Make sure we have points at each end of the range
const rgb = [];
if (functionRange[0] < range[0]) {
publicAPI.getColor(range[0], rgb);
publicAPI.addRGBPoint(range[0], rgb[0], rgb[1], rgb[2]);
} else {
publicAPI.getColor(functionRange[0], rgb);
publicAPI.addRGBPoint(range[0], rgb[0], rgb[1], rgb[2]);
}
if (functionRange[1] > range[1]) {
publicAPI.getColor(range[1], rgb);
publicAPI.addRGBPoint(range[1], rgb[0], rgb[1], rgb[2]);
} else {
publicAPI.getColor(functionRange[1], rgb);
publicAPI.addRGBPoint(range[1], rgb[0], rgb[1], rgb[2]);
}
// Remove all points out-of-range
publicAPI.sortAndUpdateRange();
for (let i = 0; i < model.nodes.length;) {
if (model.nodes[i].x >= range[0] && model.nodes[i].x <= range[1]) {
model.nodes.splice(i, 1);
} else {
++i;
}
}
return 1;
};
//--------------------------------------------------------------------------
publicAPI.estimateMinNumberOfSamples = (x1, x2) => {
const d = publicAPI.findMinimumXDistance();
return Math.ceil((x2 - x1) / d);
};
//----------------------------------------------------------------------------
publicAPI.findMinimumXDistance = () => {
if (model.nodes.length < 2) {
return -1.0;
}
let distance = Number.MAX_VALUE;
for (let i = 0; i < model.nodes.length - 1; i++) {
const currentDist = model.nodes[i + 1].x - model.nodes[i].x;
if (currentDist < distance) {
distance = currentDist;
}
}
return distance;
};
publicAPI.mapScalarsThroughTable = (input, output, outFormat, inputOffset) => {
if (publicAPI.getSize() === 0) {
vtkDebugMacro('Transfer Function Has No Points!');
return;
}
if (model.indexedLookup) {
publicAPI.mapDataIndexed(input, output, outFormat, inputOffset);
} else {
publicAPI.mapData(input, output, outFormat, inputOffset);
}
};
//----------------------------------------------------------------------------
publicAPI.mapData = (input, output, outFormat, inputOffset) => {
if (publicAPI.getSize() === 0) {
vtkWarningMacro('Transfer Function Has No Points!');
return;
}
const alpha = Math.floor(publicAPI.getAlpha() * 255.0 + 0.5);
const length = input.getNumberOfTuples();
const inIncr = input.getNumberOfComponents();
const outputV = output.getData();
const inputV = input.getData();
const rgb = [];
if (outFormat === ScalarMappingTarget.RGBA) {
for (let i = 0; i < length; i++) {
const x = inputV[i * inIncr + inputOffset];
publicAPI.getColor(x, rgb);
outputV[i * 4] = Math.floor(rgb[0] * 255.0 + 0.5);
outputV[i * 4 + 1] = Math.floor(rgb[1] * 255.0 + 0.5);
outputV[i * 4 + 2] = Math.floor(rgb[2] * 255.0 + 0.5);
outputV[i * 4 + 3] = alpha;
}
}
if (outFormat === ScalarMappingTarget.RGB) {
for (let i = 0; i < length; i++) {
const x = inputV[i * inIncr + inputOffset];
publicAPI.getColor(x, rgb);
outputV[i * 3] = Math.floor(rgb[0] * 255.0 + 0.5);
outputV[i * 3 + 1] = Math.floor(rgb[1] * 255.0 + 0.5);
outputV[i * 3 + 2] = Math.floor(rgb[2] * 255.0 + 0.5);
}
}
if (outFormat === ScalarMappingTarget.LUMINANCE) {
for (let i = 0; i < length; i++) {
const x = inputV[i * inIncr + inputOffset];
publicAPI.getColor(x, rgb);
outputV[i] = Math.floor(rgb[0] * 76.5 + rgb[1] * 150.45 + rgb[2] * 28.05 + 0.5);
}
}
if (outFormat === ScalarMappingTarget.LUMINANCE_ALPHA) {
for (let i = 0; i < length; i++) {
const x = inputV[i * inIncr + inputOffset];
publicAPI.getColor(x, rgb);
outputV[i * 2] = Math.floor(rgb[0] * 76.5 + rgb[1] * 150.45 + rgb[2] * 28.05 + 0.5);
outputV[i * 2 + 1] = alpha;
}
}
};
//----------------------------------------------------------------------------
publicAPI.applyColorMap = colorMap => {
const oldColorSpace = JSON.stringify(model.colorSpace);
if (colorMap.ColorSpace) {
model.colorSpace = ColorSpace[colorMap.ColorSpace.toUpperCase()];
if (model.colorSpace === undefined) {
vtkErrorMacro(`ColorSpace ${colorMap.ColorSpace} not supported, using RGB instead`);
model.colorSpace = ColorSpace.RGB;
}
}
let isModified = oldColorSpace !== JSON.stringify(model.colorSpace);
const oldNanColor = isModified || JSON.stringify(model.nanColor);
if (colorMap.NanColor) {
model.nanColor = [].concat(colorMap.NanColor);
while (model.nanColor.length < 4) {
model.nanColor.push(1.0);
}
}
isModified = isModified || oldNanColor !== JSON.stringify(model.nanColor);
const oldNodes = isModified || JSON.stringify(model.nodes);
if (colorMap.RGBPoints) {
const size = colorMap.RGBPoints.length;
model.nodes = [];
const midpoint = 0.5;
const sharpness = 0.0;
for (let i = 0; i < size; i += 4) {
model.nodes.push({
x: colorMap.RGBPoints[i],
r: colorMap.RGBPoints[i + 1],
g: colorMap.RGBPoints[i + 2],
b: colorMap.RGBPoints[i + 3],
midpoint,
sharpness
});
}
}
const modifiedInvoked = publicAPI.sortAndUpdateRange();
const callModified = !modifiedInvoked && (isModified || oldNodes !== JSON.stringify(model.nodes));
if (callModified) publicAPI.modified();
return modifiedInvoked || callModified;
};
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
clamping: true,
colorSpace: ColorSpace.RGB,
hSVWrap: true,
scale: Scale.LINEAR,
nanColor: null,
belowRangeColor: null,
aboveRangeColor: null,
useAboveRangeColor: false,
useBelowRangeColor: false,
allowDuplicateScalars: false,
table: null,
tableSize: 0,
buildTime: null,
nodes: null,
discretize: false,
numberOfValues: 256
};
// ----------------------------------------------------------------------------
function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);
// Inheritance
vtkScalarsToColors.extend(publicAPI, model, initialValues);
// Internal objects initialization
model.table = [];
model.nodes = [];
model.nanColor = [0.5, 0.0, 0.0, 1.0];
model.belowRangeColor = [0.0, 0.0, 0.0, 1.0];
model.aboveRangeColor = [1.0, 1.0, 1.0, 1.0];
model.buildTime = {};
macro.obj(model.buildTime);
// Create get-only macros
macro.get(publicAPI, model, ['buildTime', 'mappingRange']);
// Create get-set macros
macro.setGet(publicAPI, model, ['useAboveRangeColor', 'useBelowRangeColor', 'discretize', 'numberOfValues', {
type: 'enum',
name: 'colorSpace',
enum: ColorSpace
}, {
type: 'enum',
name: 'scale',
enum: Scale
}]);
macro.setArray(publicAPI, model, ['nanColor', 'belowRangeColor', 'aboveRangeColor'], 4);
// Create get macros for array
macro.getArray(publicAPI, model, ['nanColor', 'belowRangeColor', 'aboveRangeColor']);
// For more macro methods, see "Sources/macros.js"
// Object specific methods
vtkColorTransferFunction(publicAPI, model);
}
// ----------------------------------------------------------------------------
const newInstance = macro.newInstance(extend, 'vtkColorTransferFunction');
// ----------------------------------------------------------------------------
var vtkColorTransferFunction$1 = {
newInstance,
extend,
...Constants
};
export { vtkColorTransferFunction$1 as default, extend, newInstance };