import * as util from './utils.js' import Color from './Color.js' // /** @module */ // /** @exports ColorMap */ /** * A colormap is simply an array of typedColors with several utilities such * as randomColor, closestColor etc. * This allows the colors to be simple integer indices * into the Array. They are also designed to be webgl-ready, being * composed of typedColors. */ /** @namespace */ const ColorMap = { // ### Color Array Utilities // Several utilities for creating color arrays /** * Ask the browser to use the canvas gradient feature * to create nColors given the gradient color stops and locs. * See Mozilla [Gradient Doc]( * https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createLinearGradient), * * This is a powerful browser feature, can be * used to create all the MatLab colormaps. * * Stops are css strings. * Locs are floats from 0-1, default is equally spaced. * * @param {number} nColors integer number of colors to be returned * @param {Array} stops Array of nColors css colors, placed at locs below * @param {Array} locs Array of nColors floats in [0.1]. Default to even distribution * @returns {Array} Returns Array of nColors rgba color arrays */ gradientImageData(nColors, stops, locs) { const ctx = util.createCtx(nColors, 1) // Install default locs if none provide if (!locs) locs = util.floatRamp(0, 1, stops.length) // Create a new gradient and fill it with the color stops const grad = ctx.createLinearGradient(0, 0, nColors, 0) util.repeat(stops.length, i => grad.addColorStop(locs[i], stops[i])) // Draw the gradient, returning the image colors typed arrays ctx.fillStyle = grad ctx.fillRect(0, 0, nColors, 1) return util.ctxImageColors(ctx) }, // ### Array Conversion Utilities // Convert a Uint8Array into Array of 4 element typedColors. // Useful for converting ImageData objects like gradients to colormaps. // WebGL ready: the array.typedArray is suitable for Uniforms. // typedArraytoColors(typedArray) { // const array = [] // util.step( // typedArray.length, // 4, // // Note: can't share subarray as color's typed array: // // it's buffer is for entire array, not just subarray. // i => array.push(Color.typedColor(...typedArray.subarray(i, i + 4))) // ) // array.typedArray = typedArray // return array // }, // Convert an Array of Arrays to an Array of typedColors. // Webgl ready as above. // arraysToColors(array) { // const typedArray = new Uint8ClampedArray(array.length * 4) // util.repeat(array.length, i => { // const a = array[i] // if (a.length === 3) a.push(255) // typedArray.set(a, i * 4) // }) // return this.typedArraytoColors(typedArray) // }, /** * Convert an Array of rgba color arrays into Array of 4 element typedColors. * * @param {Array} Array of rgba color arrays * @returns Array of the rgba arrays to Color.typedColors */ arrayToTypedColors(array) { return array.map(a => Color.toTypedColor(a)) }, // Permute the values of 3 arrays. Ex: // // [1,2],[3],[4,5] -> [ [1,3,4],[1,3,5],[2,3,4],[2,3,5] ] permuteArrays(A1, A2 = A1, A3 = A1) { const array = [] for (const a3 of A3) { // sorta odd const works with ths, but... for (const a2 of A2) for (const a1 of A1) array.push([a1, a2, a3]) } return array }, // Use permuteArrays to create uniformly spaced color ramp permutation. // Ex: if numRs is 3, permuteArrays's A1 would be [0, 127, 255] permuteRGBColors(numRs, numGs = numRs, numBs = numRs) { const toRamp = num => util.integerRamp(0, 255, num) const ramps = [numRs, numGs, numBs].map(toRamp) return this.permuteArrays(...ramps) }, // ### ColorMaps // ColorMaps are Arrays of TypedColors with these additional methods. // Used to be memory effecent (shared colors), webgl compatible, and for // MatLab-like color-as-data. ColorMapProto: { // Inherit from Array __proto__: Array.prototype, // Create a [sparse array](https://goo.gl/lQlq5k) of index[pixel] = pixel. // Used by indexOf below for exact match of a color within the colormap. createIndex() { this.index = [] util.repeat(this.length, i => { const px = this[i].getPixel() this.index[px] = i if (this.cssNames) this.index[this.cssNames[i]] = i }) }, // Return a random color within the colormap randomColor() { return this[util.randomInt(this.length)] }, // Set alpha of all the colors ih the map // Note this will be shared by all users of this map! // Use clone() to have your own copy of a shared map. setAlpha(alpha) { util.forLoop(this, color => color.setAlpha(alpha)) }, // Clone this colorMap clone() { return this.cloneColorMap(this) }, // Return the color at index of this array. // Wrap the index to be within the array. atIndex(index) { return this[index % this.length] }, // Return the index of a typedColor within the colormap, // undefined if no exact match. // Use the `closest` methods below for nearest, not exact, match. indexOf(color) { if (this.index) return this.index[color.getPixel()] for (let i = 0; i < this.length; i++) { if (color.equals(this[i])) return i } return undefined }, // Return color scaled by number within [min, max]. // A linear interpolation (util.lerp) in [0, length-1]. // Used to match data directly to a color as in MatLab. // // Ex: scaleColor(25, 0, 50) returns the color in the middle of the colormap scaleColor(number, min = 0, max = this.length - 1) { // number = util.clamp(number, min, max) if (min === max) return this[min] const scale = util.lerpScale(number, min, max) const index = Math.round(util.lerp(0, this.length - 1, scale)) return this[index] }, // Return the Uint8 array used to create the typedColors, // undefined if not webgl ready. // webglArray() { // return this.typedArray // }, // Debugging: Return a string with length and array of colors toString() { return `${this.length} ${util.arraysToString(this)}` }, // Iterate through the colormap colors, returning the index of the // min typedColor.rgbDistance value from r, g, b rgbClosestIndex(r, g, b) { let minDist = Infinity let ixMin = 0 for (var i = 0; i < this.length; i++) { const d = this[i].rgbDistance(r, g, b) if (d < minDist) { minDist = d ixMin = i if (d === 0) return ixMin } } return ixMin }, // Return the color with the rgbClosestIndex value rgbClosestColor(r, g, b) { return this[this.rgbClosestIndex(r, g, b)] }, // Calculate the closest cube index for the given r, g, b values. // Faster than rgbClosestIndex, does direct calculation, not iteration. cubeClosestIndex(r, g, b) { const cube = this.cube if (!cube) throw Error('cubeClosestIndex: requires the cube arrays') const rgbSteps = cube.map(c => 255 / (c - 1)) const rgbLocs = [r, g, b].map((c, i) => Math.round(c / rgbSteps[i])) const [rLoc, gLoc, bLoc] = rgbLocs return rLoc + gLoc * cube[0] + bLoc * cube[0] * cube[1] }, cubeClosestColor(r, g, b) { return this[this.cubeClosestIndex(r, g, b)] }, // Choose the appropriate method for finding closest index. // Lets the user specify any color, and let the colormap // use the best match. closestIndex(r, g, b) { return this.cube ? this.cubeClosestIndex(r, g, b) : this.rgbClosestIndex(r, g, b) }, // Choose the appropriate method for finding closest color closestColor(r, g, b) { return this[this.closestIndex(r, g, b)] }, }, // ### Utilities for constructing ColorMaps // Convert an array of colors to a colormap. // The colors can be strings, numbers, rgb arrays // They are converted to typedColors. basicColorMap(colors) { // colors = this.arraysToColors(colors) colors = this.arrayToTypedColors(colors) Object.setPrototypeOf(colors, this.ColorMapProto) return colors }, // Create a gray map (gray: r=g=b) // These are typically 256 entries but can be smaller // by passing a size parameter and the min/max range. grayColorMap(min = 0, max = 255, size = max - min + 1) { const ramp = util.integerRamp(min, max, size) return this.basicColorMap(ramp.map(i => [i, i, i])) }, // Create a colormap by permuted rgb values. // // numRs, numGs, numBs are numbers, the number of steps beteen 0-255. // Ex: numRs = 3, corresponds to 0, 128, 255. // NOTE: the defaults: rgbColorCube(6) creates a `6 * 6 * 6` cube. rgbColorCube(numRs, numGs = numRs, numBs = numRs) { const array = this.permuteRGBColors(numRs, numGs, numBs) const map = this.basicColorMap(array) // Save the parameters for fast color calculations. map.cube = [numRs, numGs, numBs] return map }, // Create a colormap by permuting the values of the given arrays. // Similar to above but with arrays that may have arbitrary values. rgbColorMap(R, G, B) { const array = this.permuteArrays(R, G, B) return this.basicColorMap(array) }, // Create an hsl map, inputs are arrays to be permutted like rgbColorMap. // Convert the HSL values to Color.colors, default to bright hue ramp (L=50). hslColorMap(num = 360, S = 100, L = 50) { const hues = util.integerRamp(1, 360, num) const colors = hues.map(h => Color.hslCssColor(h)) const typedColors = colors.map(c => Color.toTypedColor(c)) return this.basicColorMap(typedColors) }, transparentColorMap(num = 1) { // const array = Array(num).fill(0) // return this.basicColorMap(array) return this.staticColorMap(0, num) }, staticColorMap(color, num = 1) { color = Color.toTypedColor(color) const array = Array(num).fill(color) return this.basicColorMap(array) }, // Use gradient to build an rgba array, then convert to colormap. // Stops are css strings or typedColors. // locs defaults to evenly spaced, probably what you want. // // This easily creates all the MatLab colormaps like "jet" below. gradientColorMap(nColors, stops, locs) { stops = stops.map(c => c.css || c) // convert stops to css strings // get gradient colors as typed arrays & convert them to typedColors const uint8arrays = this.gradientImageData(nColors, stops, locs) const typedColors = this.arrayToTypedColors(uint8arrays) Object.setPrototypeOf(typedColors, this.ColorMapProto) return typedColors }, // The most popular MatLab gradient, "jet": jetColors: [ 'rgb(0, 0, 127)', 'rgb(0, 0, 255)', 'rgb(0, 127, 255)', 'rgb(0, 255, 255)', 'rgb(127, 255, 127)', 'rgb(255, 255, 0)', 'rgb(255, 127, 0)', 'rgb(255, 0, 0)', 'rgb(127, 0, 0)', ], // Two other popular MatLab 'ramp' gradients are: // * One color: from black/white to color, optionally back to white/black. // stops = ['black', 'red'] or ['white', 'orange', 'black'] // The NetLogo map is a concatenation of 14 of these. // * Two colors: stops = ['red', 'orange'] (blends the two, center is white) // The 16 unique [CSS Color Names](https://goo.gl/sxo36X), case insensitive. // In CSS 2.1, the color 'orange' was added to the 16 colors as a 17th color // Aqua == Cyan and Fuchsia == Magenta, 19 total color names. // These sorted by hue/saturation/light, hue in 0-300 degrees. // See [Mozilla Color Docs](https://goo.gl/tolSnS) for *lots* more! basicColorNames: 'white silver gray black red maroon yellow orange olive lime green cyan teal blue navy magenta purple'.split( ' ' ), brightColorNames: 'white silver red maroon yellow orange olive lime green cyan teal blue navy magenta purple'.split( ' ' ), // Create a named colors colormap cssColorMap(cssArray, createNameIndex = false) { const array = cssArray.map(str => Color.cssToUint8Array(str)) const map = this.basicColorMap(array) map.cssNames = cssArray // REMIND: kinda tacky? Maybe map.name.yellow? Maybe generalize for other // map types: map.closest(name) if (createNameIndex) { cssArray.forEach((name, ix) => { map[name] = map[ix] }) if (map.cyan) map.aqua = map.cyan if (map.magenta) map.fuchsia = map.magenta } return map }, // Clone a colorMap. Useful if you want to mutate an existing shared map cloneColorMap(colorMap) { const keys = Object.keys(colorMap) const clone = this.basicColorMap(colorMap) util.forLoop(keys, (val, i) => { if (clone[i] === undefined) clone[val] = colorMap[val] }) return clone }, // ### Shared Global ColorMaps // NOTE: Do NOT modify one of these, they are shared and would // surprise anyone useing them. Use cloneColorMap() to have your own // private one, or call any of the map factories above .. see below. // The shared global colormaps are lazy evaluated to minimize memory use. // NOTE: these are shared, so any change in them are seen by all users! LazyMap(name, map) { Object.defineProperty(this, name, { value: map, enumerable: true }) return map }, get Gray() { return this.LazyMap('Gray', this.grayColorMap()) }, get Hue() { return this.LazyMap('Hue', this.hslColorMap()) }, get LightGray() { return this.LazyMap('LightGray', this.grayColorMap(200)) }, get DarkGray() { return this.LazyMap('DarkGray', this.grayColorMap(0, 100)) }, get Jet() { return this.LazyMap('Jet', this.gradientColorMap(256, this.jetColors)) }, get Rgb256() { return this.LazyMap('Rgb256', this.rgbColorCube(8, 8, 4)) }, get Rgb() { return this.LazyMap('Rgb', this.rgbColorCube(16)) }, get Transparent() { return this.LazyMap('Transparent', this.transparentColorMap()) }, get Basic16() { // 17 unique + 2 "aliases" = 19 names. "16" historic return this.LazyMap( 'Basic16', this.cssColorMap(this.basicColorNames, true) ) }, // get Bright16() { // // Basic16 w/o grays: white, black // return this.LazyMap( // 'Bright16', // this.cssColorMap(this.brightColorNames, true) // ) // }, } export default ColorMap