@bigfishtv/cockpit
Version:
210 lines (189 loc) • 7.49 kB
JavaScript
/**
* 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
}