On this page

# ColorMap.js

    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

Type anything to view search result