UNPKG

@bigfishtv/cockpit

Version:

210 lines (189 loc) 7.49 kB
/** * Color Utilities * @module Utilities/colorUtils */ import regression from 'regression' import ColorNames, { colorNameKeys } from '../constants/ColorNames' /** * Returns a hash table corresponding to a polynomial regression equation generated from array of points * @param {Array} points - An array of points, where a point is [x, y] * @param {Number} [min=0] - Number where hash table starts, typically 0 * @param {Number} [max=255] - Number where hash table ends, typically 255 * @return {Array} - Returns array of length max-min containing values reflecting the polynomial regression values of the points provided */ export function curvesHashTable(points = [[0, 0], [255, 255]], min = 0, max = 255) { var result = regression('polynomial', points, points.length - 1) var coefficients = result.equation var curvesHashTable = {} for (var x = min; x <= max; x++) { curvesHashTable[x] = 0 for (var c = points.length - 1; c >= 0; c--) { curvesHashTable[x] += coefficients[c] * Math.pow(x, c) } curvesHashTable[x] = Math.min(Math.max(curvesHashTable[x], min), max - 1) } return curvesHashTable } /** * Returns an array of curve points based on contrast value * @param {Number} contrast - Contrast value, can be negative * @return {Array} - Returns array of curve points corresponding the contrast value */ export function getContrastCurve(contrast = 0) { if (typeof contrast == 'string') contrast = parseInt(contrast) const amt = 75 return [[0, 0], [amt, 0 + amt - contrast], [180, 255 - amt + contrast], [255, 255]] } /** * Returns a ColorMatrix grid based on vibrance value * @param {Number} vibrance - Vibrance value, can be negative * @return {Array} - Returns array of ColorMatrix adjustments, a 5x4 grid */ export function getVibranceMatrix(vibrance) { const amt = vibrance / 200 return [ 1 + amt, -amt / 1.5, -amt / 1.5, 0, 0, -amt / 1.5, 1 + amt, -amt / 1.5, 0, 0, -amt / 1.5, -amt / 1.5, 1 + amt, 0, 0, 0, 0, 0, 1, 0, ] } /** * Returns an RGB object based on Kelvin temperature value * @param {Number} temperature - Temperature value, corresponds to actual Kelvin value e.g. 1000 - 40,000. Neutural is 6600 * @return {Object} - Returns object containing keys: red, green, blue */ // http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ export function getTemperatureRGB(temp = 6600) { temp /= 100 let red = temp <= 66 ? 255 : 329.698727446 * Math.pow(temp - 60, -0.1332047592) red = red < 0 ? 0 : red > 255 ? 255 : red let green = temp <= 66 ? 99.4708025861 * Math.log(temp) - 161.1195681661 : 288.1221695283 * Math.pow(temp - 60, -0.0755148492) green = green < 0 ? 0 : green > 255 ? 255 : green let blue = temp >= 66 ? 255 : temp <= 19 ? 0 : 138.5177312231 * Math.log(temp - 10) - 305.0447927307 blue = blue < 0 ? 0 : blue > 255 ? 255 : blue return { red, green, blue } } /** * Converts a r/g/b value to hex * @param {Integer} c * @return {String} */ export function componentToHex(c) { const hex = c.toString(16) return hex.length == 1 ? '0' + hex : hex } /** * Converts rgb object with keys red, green, blue to hex string * @param {Object} rgb * @param {Integer} rgb.red * @param {Integer} rgb.green * @param {Integer} rgb.blue * @return {String} */ export function rgbToHex(rgb) { return '#' + componentToHex(rgb.red) + componentToHex(rgb.green) + componentToHex(rgb.blue) } /** * Converts hex string (longform or shortform) to object with red, green blue keys * @param {String} hex * @return {Object} returns object with keys red, green, blue */ export function hexToRgb(_hex) { // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i const hex = _hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b) const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) return result ? { red: parseInt(result[1], 16), green: parseInt(result[2], 16), blue: parseInt(result[3], 16), } : null } /** * Returns either black or white based on provided color. Used for deciding font color on a colored background. * @param {String} backgroundColor - hex color or color name * @return {String} */ export function blackOrWhite(backgroundColor, threshold = 0.179) { if (typeof backgroundColor != 'string') return null if (backgroundColor.indexOf('#') < 0) { backgroundColor = backgroundColor.toLowerCase() if (colorNameKeys.indexOf(backgroundColor.toLowerCase()) >= 0) backgroundColor = ColorNames[backgroundColor] else return null } const bgRGB = hexToRgb(backgroundColor) const C = [bgRGB.red / 255, bgRGB.green / 255, bgRGB.blue / 255].map(val => val < 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4) ) const L = 0.2126 * C[0] + 0.7152 * C[1] + 0.0722 * C[2] return L > threshold ? '#000000' : '#FFFFFF' } /** * Converts color markers from a gradient map interface into an array of color values * The color values should be viewed in groups of 4 for rgba values. * So the length of the returned array will be 4x the provided range (255 by default), e.g. * [ r0, g0, b0, a0, r1, g1, b1, a1, ... , r255, g255, b255, a255 ] * @param {Object[]} markers - array of objects containing * @param {Number} markers[].position - float from 0 to 1 * @param {Number} markers[].alpha - float from 0 to 1 * @param {Number[]} markers[].color - array of 3 values for rgb, 0 to 255 * @param {Number} range - detail of returned gradient values (essentially width) * @return {Array} Returns an array of numbers to be interpretted in groups of 4 as rgba values */ export function interpolateGradient(_markers, range = 256) { // map the position and alpha to 255 then sort by position in case they're submitted out of order let markers = _markers .map(marker => ({ ...marker, position: Math.floor(marker.position * (range - 1)), alpha: Math.floor(marker.alpha * 255), })) .sort((a, b) => (a.position < b.position ? -1 : a.position > b.position ? 1 : 0)) // if first marker is above 0 then create an identical one at 0 if (markers[0].position > 0) markers = [{ ...markers[0], position: 0 }, ...markers] // and vice versa if (markers[markers.length - 1].position < range - 1) markers = [...markers, { ...markers[markers.length - 1], position: range - 1 }] const rgbaData = [] let currentMarker = markers[0] let nextMarker = markers[1] for (let i = 0; i < range; i++) { // bump current marker if reached next marker position if (i >= nextMarker.position) currentMarker = nextMarker // don't proceed if reached last marker (position 255) const currentMarkerIndex = markers.indexOf(currentMarker) if (currentMarkerIndex === markers.length - 1) { rgbaData.push(...currentMarker.color, currentMarker.alpha) break } nextMarker = markers[currentMarkerIndex + 1] // get percentage along current and next marker const amt = (i - currentMarker.position) / (nextMarker.position - currentMarker.position) // add pixel data to array rgbaData.push(Math.round(currentMarker.color[0] * (1 - amt) + nextMarker.color[0] * amt)) rgbaData.push(Math.round(currentMarker.color[1] * (1 - amt) + nextMarker.color[1] * amt)) rgbaData.push(Math.round(currentMarker.color[2] * (1 - amt) + nextMarker.color[2] * amt)) rgbaData.push(Math.round(currentMarker.alpha * (1 - amt) + nextMarker.alpha * amt)) } return rgbaData }