I'm having problems converting colours from RGB to LAB space
It should be straight forward using the formulas from here, only I'm getting back the wrong values
RGB = 56,79,132
X = 8.592
Y = 8.099
Z = 22.940
and CIE-L*ab as
L* 34.188
a* 8.072
b* -32.478
This is my code; but I can't see where I'm going wrong. It maybe due to floating points like this fella before me. Thank you.
// user colour
var Red = 56;
var Green = 79;
var Blue = 132;
// user colour converted to XYZ space
XYZ = RGBtoXYZ(Red,Green,Blue)
var colX = XYZ[0];
var colY = XYZ[1];
var colZ = XYZ[2];
// alert(XYZ)
LAB = XYZtoLAB(colX, colY, colZ)
alert(LAB)
function RGBtoXYZ(R, G, B)
{
var_R = parseFloat( R / 255 ) //R from 0 to 255
var_G = parseFloat( G / 255 ) //G from 0 to 255
var_B = parseFloat( B / 255 ) //B from 0 to 255
if ( var_R > 0.04045 ) var_R = ( ( var_R + 0.055 ) / 1.055 ) ^ 2.4
else var_R = var_R / 12.92
if ( var_G > 0.04045 ) var_G = ( ( var_G + 0.055 ) / 1.055 ) ^ 2.4
else var_G = var_G / 12.92
if ( var_B > 0.04045 ) var_B = ( ( var_B + 0.055 ) / 1.055 ) ^ 2.4
else var_B = var_B / 12.92
var_R = var_R * 100
var_G = var_G * 100
var_B = var_B * 100
//Observer. = 2°, Illuminant = D65
X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805
Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722
Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
return [X, Y, Z]
}
function XYZtoLAB(x, y, z)
{
var ref_X = 95.047;
var ref_Y = 100.000;
var ref_Z = 108.883;
var_X = x / ref_X //ref_X = 95.047 Observer= 2°, Illuminant= D65
var_Y = y / ref_Y //ref_Y = 100.000
var_Z = z / ref_Z //ref_Z = 108.883
if ( var_X > 0.008856 ) var_X = var_X ^ ( 1/3 )
else var_X = ( 7.787 * var_X ) + ( 16 / 116 )
if ( var_Y > 0.008856 ) var_Y = var_Y ^ ( 1/3 )
else var_Y = ( 7.787 * var_Y ) + ( 16 / 116 )
if ( var_Z > 0.008856 ) var_Z = var_Z ^ ( 1/3 )
else var_Z = ( 7.787 * var_Z ) + ( 16 / 116 )
CIE_L = ( 116 * var_Y ) - 16
CIE_a = 500 * ( var_X - var_Y )
CIE_b = 200 * ( var_Y - var_Z )
return [CIE_L, CIE_a, CIE_b]
}
I'm pretty sure ^ is bitwise xor in javascript not a power operator. I think Math.pow is what you are looking for.
/**
* Converts RGB color to CIE 1931 XYZ color space.
* https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz
* #param {string} hex
* #return {number[]}
*/
export function rgbToXyz(hex) {
const [r, g, b] = hexToRgb(hex).map(_ => _ / 255).map(sRGBtoLinearRGB)
const X = 0.4124 * r + 0.3576 * g + 0.1805 * b
const Y = 0.2126 * r + 0.7152 * g + 0.0722 * b
const Z = 0.0193 * r + 0.1192 * g + 0.9505 * b
// For some reason, X, Y and Z are multiplied by 100.
return [X, Y, Z].map(_ => _ * 100)
}
/**
* Undoes gamma-correction from an RGB-encoded color.
* https://en.wikipedia.org/wiki/SRGB#Specification_of_the_transformation
* https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
* #param {number}
* #return {number}
*/
function sRGBtoLinearRGB(color) {
// Send this function a decimal sRGB gamma encoded color value
// between 0.0 and 1.0, and it returns a linearized value.
if (color <= 0.04045) {
return color / 12.92
} else {
return Math.pow((color + 0.055) / 1.055, 2.4)
}
}
/**
* Converts hex color to RGB.
* https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
* #param {string} hex
* #return {number[]} [rgb]
*/
function hexToRgb(hex) {
const match = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
if (match) {
match.shift()
return match.map(_ => parseInt(_, 16))
}
}
/**
* Converts CIE 1931 XYZ colors to CIE L*a*b*.
* The conversion formula comes from <http://www.easyrgb.com/en/math.php>.
* https://github.com/cangoektas/xyz-to-lab/blob/master/src/index.js
* #param {number[]} color The CIE 1931 XYZ color to convert which refers to
* the D65/2° standard illuminant.
* #returns {number[]} The color in the CIE L*a*b* color space.
*/
// X, Y, Z of a "D65" light source.
// "D65" is a standard 6500K Daylight light source.
// https://en.wikipedia.org/wiki/Illuminant_D65
const D65 = [95.047, 100, 108.883]
export function xyzToLab([x, y, z]) {
[x, y, z] = [x, y, z].map((v, i) => {
v = v / D65[i]
return v > 0.008856 ? Math.pow(v, 1 / 3) : v * 7.787 + 16 / 116
})
const l = 116 * y - 16
const a = 500 * (x - y)
const b = 200 * (y - z)
return [l, a, b]
}
/**
* Converts Lab color space to Luminance-Chroma-Hue color space.
* http://www.brucelindbloom.com/index.html?Eqn_Lab_to_LCH.html
* #param {number[]}
* #return {number[]}
*/
export function labToLch([l, a, b]) {
const c = Math.sqrt(a * a + b * b)
const h = abToHue(a, b)
return [l, c, h]
}
/**
* Converts a and b of Lab color space to Hue of LCH color space.
* https://stackoverflow.com/questions/53733379/conversion-of-cielab-to-cielchab-not-yielding-correct-result
* #param {number} a
* #param {number} b
* #return {number}
*/
function abToHue(a, b) {
if (a >= 0 && b === 0) {
return 0
}
if (a < 0 && b === 0) {
return 180
}
if (a === 0 && b > 0) {
return 90
}
if (a === 0 && b < 0) {
return 270
}
let xBias
if (a > 0 && b > 0) {
xBias = 0
} else if (a < 0) {
xBias = 180
} else if (a > 0 && b < 0) {
xBias = 360
}
return radiansToDegrees(Math.atan(b / a)) + xBias
}
function radiansToDegrees(radians) {
return radians * (180 / Math.PI)
}
function degreesToRadians(degrees) {
return degrees * Math.PI / 180
}
Here are some functions for RGB -> XYZ, XYZ -> LAB, LAB -> XYZ, XYZ -> RGB.
function RGBtoXYZ([R, G, B]) {
const [var_R, var_G, var_B] = [R, G, B]
.map(x => x / 255)
.map(x => x > 0.04045
? Math.pow(((x + 0.055) / 1.055), 2.4)
: x / 12.92)
.map(x => x * 100)
// Observer. = 2°, Illuminant = D65
X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805
Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722
Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
return [X, Y, Z]
}
function XYZtoRGB([X, Y, Z]) {
//X, Y and Z input refer to a D65/2° standard illuminant.
//sR, sG and sB (standard RGB) output range = 0 ÷ 255
let var_X = X / 100
let var_Y = Y / 100
let var_Z = Z / 100
var_R = var_X * 3.2406 + var_Y * -1.5372 + var_Z * -0.4986
var_G = var_X * -0.9689 + var_Y * 1.8758 + var_Z * 0.0415
var_B = var_X * 0.0557 + var_Y * -0.2040 + var_Z * 1.0570
return [var_R, var_G, var_B]
.map(n => n > 0.0031308
? 1.055 * Math.pow(n, (1 / 2.4)) - 0.055
: 12.92 * n)
.map(n => n * 255)
}
const ref_X = 95.047;
const ref_Y = 100.000;
const ref_Z = 108.883;
function XYZtoLAB([x, y, z]) {
const [ var_X, var_Y, var_Z ] = [ x / ref_X, y / ref_Y, z / ref_Z ]
.map(a => a > 0.008856
? Math.pow(a, 1 / 3)
: (7.787 * a) + (16 / 116))
CIE_L = (116 * var_Y) - 16
CIE_a = 500 * (var_X - var_Y)
CIE_b = 200 * (var_Y - var_Z)
return [CIE_L, CIE_a, CIE_b]
}
function LABtoXYZ([l, a, b]) {
const var_Y = (l + 16) / 116
const var_X = a / 500 + var_Y
const var_Z = var_Y - b / 200
const [X, Y, Z] = [var_X, var_Y, var_Z]
.map(n => Math.pow(n, 3) > 0.008856
? Math.pow(n, 3)
: (n - 16 / 116) / 7.787)
return [X * ref_X, Y * ref_Y, Z * ref_Z]
}
Reference: http://www.easyrgb.com/en/math.php
function xyzc(c){return ((c/255)>0.04045)?Math.pow((((c/255)+0.055)/1.055),2.4)*100:(c/255)/12.92*100;}
This line will convert a rgb channel to XYZ
Related
Background
I've built a little web based application that pops up windows to display your webcam(s). I wanted to add the ability to chroma key your feed and have been successful in getting several different algorithms working. The best algorithm I have found however is very resource intensive for JavaScript; single threaded application.
Question
Is there a way to offload the intensive math operations to the GPU? I've tried getting GPU.js to work but I keep getting all kinds of errors. Here is the functions I would like to have the GPU run:
let dE76 = function(a, b, c, d, e, f) {
return Math.sqrt( pow(d - a, 2) + pow(e - b, 2) + pow(f - c, 2) );
};
let rgbToLab = function(r, g, b) {
let x, y, z;
r = r / 255;
g = g / 255;
b = b / 255;
r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
x = (x > 0.008856) ? Math.pow(x, 1/3) : (7.787 * x) + 16/116;
y = (y > 0.008856) ? Math.pow(y, 1/3) : (7.787 * y) + 16/116;
z = (z > 0.008856) ? Math.pow(z, 1/3) : (7.787 * z) + 16/116;
return [ (116 * y) - 16, 500 * (x - y), 200 * (y - z) ];
};
What happens here is I send in an RGB value to rgbToLab which gives back the LAB value that can be compared to an already stored LAB value for my green screen with dE76. Then in my app we check the dE76 value to a threashold, say 25, and if the value is less than this I turn that pixel opacity to 0 in the video feed.
GPU.js Attempt
Here is my latest GUI.js attempt:
// Try to combine the 2 functions into a single kernel function for GPU.js
let tmp = gpu.createKernel( function( r, g, b, lab ) {
let x, y, z;
r = r / 255;
g = g / 255;
b = b / 255;
r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
x = (x > 0.008856) ? Math.pow(x, 1/3) : (7.787 * x) + 16/116;
y = (y > 0.008856) ? Math.pow(y, 1/3) : (7.787 * y) + 16/116;
z = (z > 0.008856) ? Math.pow(z, 1/3) : (7.787 * z) + 16/116;
let clab = [ (116 * y) - 16, 500 * (x - y), 200 * (y - z) ];
let d = pow(lab[0] - clab[0], 2) + pow(lab[1] - clab[1], 2) + pow(lab[2] - clab[2], 2);
return Math.sqrt( d );
} ).setOutput( [256] );
// ...
// Call the function above.
let d = tmp( r, g, b, chromaColors[c].lab );
// If the delta (d) is lower than my tolerance level set pixel opacity to 0.
if( d < tolerance ){
frame.data[ i * 4 + 3 ] = 0;
}
ERRORS:
Here are a list of errors I get trying to use GPU.js when I call my tmp function. 1) is for the code I provided above. 2) is for erasing all the code in tmp and adding only an empty return 3) is if I try and add the functions inside the tmp function; a valid JavaScript thing but not C or kernel code.
Uncaught Error: Identifier is not defined
Uncaught Error: Error compiling fragment shader: ERROR: 0:463: ';' : syntax error
Uncaught Error: Unhandled type FunctionExpression in getDependencies
Some typos
pow should be Math.pow()
and
let x, y, z should be declare on there own
let x = 0
let y = 0
let z = 0
You cannot assign value to parameter variable. They become uniform.
Full working script
const { GPU } = require('gpu.js')
const gpu = new GPU()
const tmp = gpu.createKernel(function (r, g, b, lab) {
let x = 0
let y = 0
let z = 0
let r1 = r / 255
let g1 = g / 255
let b1 = b / 255
r1 = (r1 > 0.04045) ? Math.pow((r1 + 0.055) / 1.055, 2.4) : r1 / 12.92
g1 = (g1 > 0.04045) ? Math.pow((g1 + 0.055) / 1.055, 2.4) : g1 / 12.92
b1 = (b1 > 0.04045) ? Math.pow((b1 + 0.055) / 1.055, 2.4) : b1 / 12.92
x = (r1 * 0.4124 + g1 * 0.3576 + b1 * 0.1805) / 0.95047
y = (r1 * 0.2126 + g1 * 0.7152 + b1 * 0.0722) / 1.00000
z = (r1 * 0.0193 + g1 * 0.1192 + b1 * 0.9505) / 1.08883
x = (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + 16 / 116
y = (y > 0.008856) ? Math.pow(y, 1 / 3) : (7.787 * y) + 16 / 116
z = (z > 0.008856) ? Math.pow(z, 1 / 3) : (7.787 * z) + 16 / 116
const clab = [(116 * y) - 16, 500 * (x - y), 200 * (y - z)]
const d = Math.pow(lab[0] - clab[0], 2) + Math.pow(lab[1] - clab[1], 2) + Math.pow(lab[2] - clab[2], 2)
return Math.sqrt(d)
}).setOutput([256])
console.log(tmp(128, 139, 117, [40.1332, 10.99816, 5.216413]))
Well this is not the answer to my original question I did come up with a computationally fast poor mans alternative. I'm including this code here for anyone else stuck trying to do chroma keying in JavaScript. Visually speaking the output video is very close to the way heavier Delta E 76 code in the OP.
Step 1: Convert RGB to YUV
I found a StackOverflow answer that has a very fast RGB to YUV conversion function written in C. Later I also found Greenscreen Code and Hints by Edward Cannon that had a C function to convert RGB to YCbCr. I took both of these, converted them to JavaScript, and tested which was actually better for chroma keying. Well Edward Cannon's function was useful it did not prove any better than Camille Goudeseune's code; the SO answer reference above. Edward's code is commented out below:
let rgbToYuv = function( r, g, b ) {
let y = 0.257 * r + 0.504 * g + 0.098 * b + 16;
//let y = Math.round( 0.299 * r + 0.587 * g + 0.114 * b );
let u = -0.148 * r - 0.291 * g + 0.439 * b + 128;
//let u = Math.round( -0.168736 * r - 0.331264 * g + 0.5 * b + 128 );
let v = 0.439 * r - 0.368 * g - 0.071 * b + 128;
//let v = Math.round( 0.5 * r - 0.418688 * g - 0.081312 * b + 128 );
return [ y, u, v ];
}
Step 2: Check How Close Two YUV Colors Are
Thanks again to Greenscreen Code and Hints by Edward Cannon comparing two YUV colors was fairly simple. We can ignore Y here and only need the U and V values; if you want to know why you will need to study up on YUV (YCbCr), particularly the section on luminance and chrominance. Here is the C code converted to JavaScript:
let colorClose = function( u, v, cu, cv ){
return Math.sqrt( ( cu - u ) * ( cu - u ) + ( cv - v ) * ( cv - v ) );
};
If you read the article you'll notice this is not the full function. In my application I'm dealing with video not a still image so supplying a background and foreground color to include in the calculation would be difficult. It would also add to the computational load. There is a simple work around for this in the next step.
Step 3: Check Tolerance & Clean Edges
Since we're dealing with video here we loop through the pixel data for each frame and check if the colorClose value is below a certain threshold. If the color we just checked is below the tolerance level we need to turn that pixels opacity to 0 making it transparent.
Since this is a very fast poor mans chroma key we tend to get color bleed on the edges of the remaining image. Adjusting up or down on the tolerance value does a lot to reduce this but we can also add a simple feathering effect. If a pixel was not marked for transparency but is close to the tolerance level, we can partial turn it off. The code below demonstrates this:
// ...My app specific code.
/*
NOTE: chromaColors is an array holding RGB colors the user has
selected from the video feed. My app requires the user to select
the lightest and darkest part of their green screen. If lighting
is bad they can add more colors to this array and we will do our
best to chroma key them out.
*/
// Grab the current frame data from our Canvas.
let frame = ctxHolder.getImageData( 0, 0, width, height );
let frames = frame.data.length / 4;
let colors = chromaColors.length - 1;
// Loop through every pxel of this frame.
for ( let i = 0; i < frames; i++ ) {
// Each pixel is stored as an rgba value; we don't need a.
let r = frame.data[ i * 4 + 0 ];
let g = frame.data[ i * 4 + 1 ];
let b = frame.data[ i * 4 + 2 ];
let yuv = rgbToYuv( r, g, b );
// Check the current pixel against our list of colors to turn transparent.
for ( let c = 0; c < colors; c++ ) {
// When the user selected a color for chroma keying we wen't ahead
// and saved the YUV value to save on resources. Pull it out for use.
let cc = chromaColors[c].yuv;
// Calc the closeness (distance) of the currnet pixel and chroma color.
let d = colorClose( yuv[1], yuv[2], cc[1], cc[2] );
if( d < tolerance ){
// Turn this pixel transparent.
frame.data[ i * 4 + 3 ] = 0;
break;
} else {
// Feather edges by lowering the opacity on pixels close to the tolerance level.
if ( d - 1 < tolerance ){
frame.data[ i * 4 + 3 ] = 0.1;
break;
}
if ( d - 2 < tolerance ){
frame.data[ i * 4 + 3 ] = 0.2;
break;
}
if ( d - 3 < tolerance ){
frame.data[ i * 4 + 3 ] = 0.3;
break;
}
if ( d - 4 < tolerance ){
frame.data[ i * 4 + 3 ] = 0.4;
break;
}
if ( d - 5 < tolerance ){
frame.data[ i * 4 + 3 ] = 0.5;
break;
}
}
}
}
// ...My app specific code.
// Put the altered frame data back into the video feed.
ctxMain.putImageData( frame, 0, 0 );
Additional Resources
I should mention that Real-Time Chroma Key With Delta E 76 and Delta E 101 by Zachary Schuessler were a great help in getting me to these solutions.
Is there any new API to create custom icons given a color and text? I'd like to send an hex color.
I've been using a couple of URL to generate my markers icons but now it seems to be deprecated.
I have not been able to find a new one
There is my old function:
function getIcon(text, fillColor, textColor, outlineColor) {
if (!text) text = '•'; //generic map dot
var iconUrl = "https://chart.googleapis.com/chart?chst=d_map_pin_letter&chld=" + text + "|" + fillColor;
//var iconUrl = "http://chart.googleapis.com/chart?cht=d&chdp=mapsapi&chl=pin%27i\\%27[" + text + "%27-2%27f\\hv%27a\\]h\\]o\\" + fillColor + "%27fC\\" + textColor + "%27tC\\" + outlineColor + "%27eC\\Lauto%27f\\&ext=.png";
return iconUrl;
}
Thanks in advance!
If you are open to put in some code here is a link which can convert an image, in your case the marker to a desired colour.
Codepen Link
'use strict';
class Color {
constructor(r, g, b) {
this.set(r, g, b);
}
toString() {
return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`;
}
set(r, g, b) {
this.r = this.clamp(r);
this.g = this.clamp(g);
this.b = this.clamp(b);
}
hueRotate(angle = 0) {
angle = angle / 180 * Math.PI;
const sin = Math.sin(angle);
const cos = Math.cos(angle);
this.multiply([
0.213 + cos * 0.787 - sin * 0.213,
0.715 - cos * 0.715 - sin * 0.715,
0.072 - cos * 0.072 + sin * 0.928,
0.213 - cos * 0.213 + sin * 0.143,
0.715 + cos * 0.285 + sin * 0.140,
0.072 - cos * 0.072 - sin * 0.283,
0.213 - cos * 0.213 - sin * 0.787,
0.715 - cos * 0.715 + sin * 0.715,
0.072 + cos * 0.928 + sin * 0.072,
]);
}
grayscale(value = 1) {
this.multiply([
0.2126 + 0.7874 * (1 - value),
0.7152 - 0.7152 * (1 - value),
0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value),
0.7152 + 0.2848 * (1 - value),
0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value),
0.7152 - 0.7152 * (1 - value),
0.0722 + 0.9278 * (1 - value),
]);
}
sepia(value = 1) {
this.multiply([
0.393 + 0.607 * (1 - value),
0.769 - 0.769 * (1 - value),
0.189 - 0.189 * (1 - value),
0.349 - 0.349 * (1 - value),
0.686 + 0.314 * (1 - value),
0.168 - 0.168 * (1 - value),
0.272 - 0.272 * (1 - value),
0.534 - 0.534 * (1 - value),
0.131 + 0.869 * (1 - value),
]);
}
saturate(value = 1) {
this.multiply([
0.213 + 0.787 * value,
0.715 - 0.715 * value,
0.072 - 0.072 * value,
0.213 - 0.213 * value,
0.715 + 0.285 * value,
0.072 - 0.072 * value,
0.213 - 0.213 * value,
0.715 - 0.715 * value,
0.072 + 0.928 * value,
]);
}
multiply(matrix) {
const newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
const newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
const newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
this.r = newR;
this.g = newG;
this.b = newB;
}
brightness(value = 1) {
this.linear(value);
}
contrast(value = 1) {
this.linear(value, -(0.5 * value) + 0.5);
}
linear(slope = 1, intercept = 0) {
this.r = this.clamp(this.r * slope + intercept * 255);
this.g = this.clamp(this.g * slope + intercept * 255);
this.b = this.clamp(this.b * slope + intercept * 255);
}
invert(value = 1) {
this.r = this.clamp((value + this.r / 255 * (1 - 2 * value)) * 255);
this.g = this.clamp((value + this.g / 255 * (1 - 2 * value)) * 255);
this.b = this.clamp((value + this.b / 255 * (1 - 2 * value)) * 255);
}
hsl() {
// Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
const r = this.r / 255;
const g = this.g / 255;
const b = this.b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return {
h: h * 100,
s: s * 100,
l: l * 100,
};
}
clamp(value) {
if (value > 255) {
value = 255;
} else if (value < 0) {
value = 0;
}
return value;
}
}
class Solver {
constructor(target, baseColor) {
this.target = target;
this.targetHSL = target.hsl();
this.reusedColor = new Color(0, 0, 0);
}
solve() {
const result = this.solveNarrow(this.solveWide());
return {
values: result.values,
loss: result.loss,
filter: this.css(result.values),
};
}
solveWide() {
const A = 5;
const c = 15;
const a = [60, 180, 18000, 600, 1.2, 1.2];
let best = { loss: Infinity };
for (let i = 0; best.loss > 25 && i < 3; i++) {
const initial = [50, 20, 3750, 50, 100, 100];
const result = this.spsa(A, a, c, initial, 1000);
if (result.loss < best.loss) {
best = result;
}
}
return best;
}
solveNarrow(wide) {
const A = wide.loss;
const c = 2;
const A1 = A + 1;
const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
return this.spsa(A, a, c, wide.values, 500);
}
spsa(A, a, c, values, iters) {
const alpha = 1;
const gamma = 0.16666666666666666;
let best = null;
let bestLoss = Infinity;
const deltas = new Array(6);
const highArgs = new Array(6);
const lowArgs = new Array(6);
for (let k = 0; k < iters; k++) {
const ck = c / Math.pow(k + 1, gamma);
for (let i = 0; i < 6; i++) {
deltas[i] = Math.random() > 0.5 ? 1 : -1;
highArgs[i] = values[i] + ck * deltas[i];
lowArgs[i] = values[i] - ck * deltas[i];
}
const lossDiff = this.loss(highArgs) - this.loss(lowArgs);
for (let i = 0; i < 6; i++) {
const g = lossDiff / (2 * ck) * deltas[i];
const ak = a[i] / Math.pow(A + k + 1, alpha);
values[i] = fix(values[i] - ak * g, i);
}
const loss = this.loss(values);
if (loss < bestLoss) {
best = values.slice(0);
bestLoss = loss;
}
}
return { values: best, loss: bestLoss };
function fix(value, idx) {
let max = 100;
if (idx === 2 /* saturate */) {
max = 7500;
} else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) {
max = 200;
}
if (idx === 3 /* hue-rotate */) {
if (value > max) {
value %= max;
} else if (value < 0) {
value = max + value % max;
}
} else if (value < 0) {
value = 0;
} else if (value > max) {
value = max;
}
return value;
}
}
loss(filters) {
// Argument is array of percentages.
const color = this.reusedColor;
color.set(0, 0, 0);
color.invert(filters[0] / 100);
color.sepia(filters[1] / 100);
color.saturate(filters[2] / 100);
color.hueRotate(filters[3] * 3.6);
color.brightness(filters[4] / 100);
color.contrast(filters[5] / 100);
const colorHSL = color.hsl();
return (
Math.abs(color.r - this.target.r) +
Math.abs(color.g - this.target.g) +
Math.abs(color.b - this.target.b) +
Math.abs(colorHSL.h - this.targetHSL.h) +
Math.abs(colorHSL.s - this.targetHSL.s) +
Math.abs(colorHSL.l - this.targetHSL.l)
);
}
css(filters) {
function fmt(idx, multiplier = 1) {
return Math.round(filters[idx] * multiplier);
}
return `filter: invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%);`;
}
}
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;
hex = hex.replace(shorthandRegex, (m, r, g, b) => {
return 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
? [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16),
]
: null;
}
$(document).ready(() => {
$('button.execute').click(() => {
const rgb = hexToRgb($('input.target').val());
if (rgb.length !== 3) {
alert('Invalid format!');
return;
}
const color = new Color(rgb[0], rgb[1], rgb[2]);
const solver = new Solver(color);
const result = solver.solve();
let lossMsg;
if (result.loss < 1) {
lossMsg = 'This is a perfect result.';
} else if (result.loss < 5) {
lossMsg = 'The is close enough.';
} else if (result.loss < 15) {
lossMsg = 'The color is somewhat off. Consider running it again.';
} else {
lossMsg = 'The color is extremely off. Run it again!';
}
$('.realPixel').css('background-color', color.toString());
$('.filterPixel').attr('style', result.filter);
$('.filterDetail').text(result.filter);
$('.lossDetail').html(`Loss: ${result.loss.toFixed(1)}. <b>${lossMsg}</b>`);
});
});
You can then send this filter output to CSS of your marker and it will change the colour of the image
I recently added a canvas element,random dots on sphere,in my page.It works great on PC but on mobile phones and tablets rendering is very slow.
How can I speed up the sphere and reduce lags?
Any help would be much appreciated.GitHub example
There is so much room for improvements.
In the render loop you have
for (var p of points) {
p = rotation.multiplyVector(p);
ctx.beginPath();
ctx.arc(p.x + c.width / 2, p.y + c.height / 2, 2, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
}
The beginPath is not needed if you are rendering the same style over and over
ctx.beginPath();
for (var p of points) {
p = rotation.multiplyVector(p);
const x = p.x + c.width / 2;
const y = p.y + c.height / 2;
ctx.moveTo(x + 2, y)
ctx.arc(x, y, 2, 0, 2 * Math.PI);
}
ctx.fill();
Then the matrix vector multiply has needless vetting and needless memory assignment and object instantiations
You had
Matrix3.prototype.multiplyVector = function (vec) {
if (vec instanceof Vector3) {
var x = this.data[0 + 0 * 3] * vec.x + this.data[0 + 1 * 3] * vec.y + this.data[0 + 2 * 3] * vec.z;
var y = this.data[1 + 0 * 3] * vec.x + this.data[1 + 1 * 3] * vec.y + this.data[1 + 2 * 3] * vec.z;
var z = this.data[2 + 0 * 3] * vec.x + this.data[2 + 1 * 3] * vec.y + this.data[2 + 2 * 3] * vec.z;
return new Vector3(x, y, z);
}
}
Will this ever not happen if (vec instanceof Vector3) { ??? not in your code so why waste the CPU time doing it.
Then this.data[2 + 0 * 3] The optimiser may get this for you, but mobiles do not optimise as well as desktops and I am not sure if this will be picked up on. Also some browsers are slower when using indirect references this.data[?] is slower than data[?]
And creating a new vector with for each circle to immediately discard it is not at all memory friendly. You only need one object so pass it to the function to set.
Improved
Matrix3.prototype.multiplyVector = function (vec, retVec = new Vector3(0,0,0)) {
const d = this.data;
retVec.x = d[0] * vec.x + d[3] * vec.y + d[6] * vec.z;
retVec.y = d[1] * vec.x + d[4] * vec.y + d[7] * vec.z;
retVec.z = d[2] * vec.x + d[5] * vec.y + d[8] * vec.z;
return retVec;
};
Then in the loop
const rp = new Vector3(0,0,0);
ctx.beginPath();
for (var p of points) {
rotation.multiplyVector(p,rp);
const x = rp.x + c.width / 2;
const y = rp.y + c.height / 2;
ctx.moveTo(x + 2, y)
ctx.arc(x, y, 2, 0, 2 * Math.PI);
}
ctx.fill();
Both the Vector3 and Matrix3 objects are very memory wasteful which means CPU cycles you have no control over being used just to assign and delete when you should be reusing memory as shown above.
You have the Matrix rotate function to build the rotation that creates a new matrix to create a rotation matrix which then needs a new matrix to multiply. You create 6 full matrix objects just to get one matrix.
The rotate function is called with 2 of x,y,z as 0 meaning that many of the multiplications and additions are just getting zero or you end up adding omc + cos which equals 1 or you multiply by 1 making no change.
You have
Matrix3.rotate = function (angle, x, y, z) {
var result = new Matrix3();
result.setIdentity();
var cos = Math.cos(angle);
var sin = Math.sin(angle);
var omc = 1 - cos;
result.data[0 + 0 * 3] = x * omc + cos;
result.data[1 + 0 * 3] = y * x * omc + z * sin;
result.data[2 + 0 * 3] = x * z * omc - y * sin;
result.data[0 + 1 * 3] = x * y * omc - z * sin;
result.data[1 + 1 * 3] = y * omc + cos;
result.data[2 + 1 * 3] = y * z * omc + x * sin;
result.data[0 + 2 * 3] = x * z * omc + y * sin;
result.data[1 + 2 * 3] = y * z * omc - x * sin;
result.data[2 + 2 * 3] = z * omc + cos;
return result;
}
Create a rotation matrix by directly multiplying a matrix, you will need one for each axis.
Matrix3.prototype.rotateX = function(angle, result = new Matrix3()) {
const r = result.data;
const d = this.data;
const c = Math.cos(angle);
const s = Math.sin(angle));
const ns = -s;
r[0] = d[0]
r[1] = d[1] * c + d[2] * ns;
r[2] = d[1] * s + d[2] * c;
r[3] = d[3];
r[4] = d[4] * c + d[5] * ns;
r[5] = d[4] * s + d[5] * c;
r[6] = d[6];
r[7] = d[7] * c + d[8] * ns;
r[8] = d[7] * s + d[8] * c;
return result;
},
Do same for rotateY,rotateZ (each is different than above)
Instance matrix directly setting the identity rather than needing a second call.
function Matrix3() { this.data = [1,0,0,0,1,0,0,0,1] }
Set identity with
Matrix3.prototype.setIdentity = function () {
const d = this.data;
d.fill(0);
d[8] = d[4] = d[0] = 1;
}
Then in your loop function have access to two matrix objects.
const mat1 = new Matrix3();
const mat2 = new Matrix3();
const rp = new Vector3(0,0,0);
const MPI2 = 2 * Math.PI;
function loop(){
mat1.setIdentity();
mat1.rotateX(angle.x,mat2);
mat2.rotateY(angle.y,mat1);
mat1.rotateZ(angle.z,mat2);
// your text rendering in here
const cw = c.width / 2;
const ch = c.height / 2;
ctx.beginPath();
for (var p of points) {
mat2.multiplyVector(p,rp);
const x = rp.x + cw;
const y = rp.y + ch;
ctx.moveTo(x + 2, y)
ctx.arc(x, y, 2, 0, MPI2);
}
ctx.fill();
}
That will give you a little extra speed. Any other improvements will be browser / device specific.
UPDATE as requested in the comments the following snippet contains the shortened rotation matrix multiplication of rotation around the X,Y, andZ axis
// How I find the optimum matrix multiplication via
// eleminating a[?] * 0 = 0
// reducing a[?] * 1 = a[?]
//
// The following are the rotations for X,Y,Z as matrix
//-------------------
// rotate X
// 1 0 0
// 0 cos(r) sin(r)
// 0 -sin(r) cos(r)
//-------------------
// rotate Y
// cos(r) 0 sin(r)
// 0 1 0
// -sin(r) 0 cos(r)
//-------------------
// rotate Z
// cos(r) sin(r) 0
// -sin(r) cos(r) 0
// 0 0 1
// The matrix indexes
// [0][1][2]
// [3][4][5]
// [6][7][8]
// Using the indexs and multiply is c = a * b
// c[0] = a[0] * b[0] + a[1] * b[3] + a[2] * b[6]
// c[1] = a[0] * b[1] + a[1] * b[4] + a[2] * b[7]
// c[2] = a[0] * b[2] + a[1] * b[5] + a[2] * b[8]
// c[3] = a[3] * b[0] + a[4] * b[3] + a[5] * b[6]
// c[4] = a[3] * b[1] + a[4] * b[4] + a[5] * b[7]
// c[5] = a[3] * b[2] + a[4] * b[5] + a[5] * b[8]
// c[6] = a[6] * b[0] + a[7] * b[3] + a[8] * b[6]
// c[7] = a[6] * b[1] + a[7] * b[4] + a[8] * b[7]
// c[8] = a[6] * b[2] + a[7] * b[5] + a[8] * b[8]
// Then use the rotations matrix to find the zeros and ones
// EG rotate X b[1],b[2],b[3],b[6] are zero and b[0] is one
// c[0] = a[0] * 1 + a[1] * 0 + a[2] * 0
// c[1] = a[0] * 0 + a[1] * b[4] + a[2] * b[7]
// c[2] = a[0] * 0 + a[1] * b[5] + a[2] * b[8]
// c[3] = a[3] * 1 + a[4] * 0 + a[5] * 0
// c[4] = a[3] * 0 + a[4] * b[4] + a[5] * b[7]
// c[5] = a[3] * 0 + a[4] * b[5] + a[5] * b[8]
// c[6] = a[6] * 1 + a[7] * 0 + a[8] * 0
// c[7] = a[6] * 0 + a[7] * b[4] + a[8] * b[7]
// c[8] = a[6] * 0 + a[7] * b[5] + a[8] * b[8]
// then eliminate all the zero terms a[?] * 0 == 0 and
// remove the 1 from 1 * a[?] = a[?]
// c[0] = a[0]
// c[1] = a[1] * b[4] + a[2] * b[7]
// c[2] = a[1] * b[5] + a[2] * b[8]
// c[3] = a[3]
// c[4] = a[4] * b[4] + a[5] * b[7]
// c[5] = a[4] * b[5] + a[5] * b[8]
// c[6] = a[6]
// c[7] = a[7] * b[4] + a[8] * b[7]
// c[8] = a[7] * b[5] + a[8] * b[8]
// And you are left with the minimum calculations required to apply a particular rotation Or any other transform.
Matrix3.prototype.rotateX = function(angle, result = new Matrix3()) {
const r = result.data;
const d = this.data;
const c = Math.cos(angle);
const s = Math.sin(angle);
const ns = -s;
r[0] = d[0];
r[1] = d[1] * c + d[2] * ns;
r[2] = d[1] * s + d[2] * c;
r[3] = d[3];
r[4] = d[4] * c + d[5] * ns;
r[5] = d[4] * s + d[5] * c;
r[6] = d[6];
r[7] = d[7] * c + d[8] * ns;
r[8] = d[7] * s + d[8] * c;
return result;
}
Matrix3.prototype.rotateY = function(angle, result = new Matrix3()) {
const r = result.data;
const d = this.data;
const c = Math.cos(angle);
const s = Math.sin(angle);
const ns = -s;
r[0] = d[0] * c + d[2] * ns;
r[1] = d[1];
r[2] = d[0] * s + d[2] * c;
r[3] = d[3] * c + d[5] * ns;
r[4] = d[4];
r[5] = d[3] * s + d[5] * c;
r[6] = d[6] * c + d[8] * ns;
r[7] = d[7];
r[8] = d[6] * s + d[8] * c;
return result;
}
Matrix3.prototype.rotateZ = function(angle, result = new Matrix3()) {
const r = result.data;
const d = this.data;
const c = Math.cos(angle);
const s = Math.sin(angle);
const ns = -s;
r[0] = d[0] * c + d[1] * ns;
r[1] = d[0] * s + d[1] * c;
r[2] = d[2];
r[3] = d[3] * c + d[4] * ns;
r[4] = d[3] * s + d[4] * c;
r[5] = d[5];
r[6] = d[6] * c + d[7] * ns;
r[7] = d[6] * s + d[7] * c;
r[8] = d[8];
return result;
}
I am making a web interface to manage my hue lamps, but i am struggling when it comes to color handling..
The api of the lamps provides me the x and y coordinates from http://en.wikipedia.org/wiki/CIE_1931_color_space
But not the z value.
I think i must calculate the z from the brightness value or saturation value (0 to 255).
but i am terrible at colors, and math :p.
I tried to use thoses functions https://github.com/eikeon/hue-color-converter/blob/master/colorconverter.ts
But as i saw in the comments, thoses functions do not provide correct values...
Could someone help me here please ? ☺
ps : i need a javascript function.
Okay so i manage to make something working with the help of : How do I convert an RGB value to a XY value for the Phillips Hue Bulb
function xyBriToRgb(x, y, bri){
z = 1.0 - x - y;
Y = bri / 255.0; // Brightness of lamp
X = (Y / y) * x;
Z = (Y / y) * z;
r = X * 1.612 - Y * 0.203 - Z * 0.302;
g = -X * 0.509 + Y * 1.412 + Z * 0.066;
b = X * 0.026 - Y * 0.072 + Z * 0.962;
r = r <= 0.0031308 ? 12.92 * r : (1.0 + 0.055) * Math.pow(r, (1.0 / 2.4)) - 0.055;
g = g <= 0.0031308 ? 12.92 * g : (1.0 + 0.055) * Math.pow(g, (1.0 / 2.4)) - 0.055;
b = b <= 0.0031308 ? 12.92 * b : (1.0 + 0.055) * Math.pow(b, (1.0 / 2.4)) - 0.055;
maxValue = Math.max(r,g,b);
r /= maxValue;
g /= maxValue;
b /= maxValue;
r = r * 255; if (r < 0) { r = 255 };
g = g * 255; if (g < 0) { g = 255 };
b = b * 255; if (b < 0) { b = 255 };
return {
r :r,
g :g,
b :b
}
}
Its not very very precise, but it works quite well. If someone manage to make something better, please post it here, thanks.
I modified your script to return the rgb value in HEX Notation:
function xyBriToRgb(x, y, bri)
{
z = 1.0 - x - y;
Y = bri / 255.0; // Brightness of lamp
X = (Y / y) * x;
Z = (Y / y) * z;
r = X * 1.612 - Y * 0.203 - Z * 0.302;
g = -X * 0.509 + Y * 1.412 + Z * 0.066;
b = X * 0.026 - Y * 0.072 + Z * 0.962;
r = r <= 0.0031308 ? 12.92 * r : (1.0 + 0.055) * Math.pow(r, (1.0 / 2.4)) - 0.055;
g = g <= 0.0031308 ? 12.92 * g : (1.0 + 0.055) * Math.pow(g, (1.0 / 2.4)) - 0.055;
b = b <= 0.0031308 ? 12.92 * b : (1.0 + 0.055) * Math.pow(b, (1.0 / 2.4)) - 0.055;
maxValue = Math.max(r,g,b);
r /= maxValue;
g /= maxValue;
b /= maxValue;
r = r * 255; if (r < 0) { r = 255 };
g = g * 255; if (g < 0) { g = 255 };
b = b * 255; if (b < 0) { b = 255 };
r = Math.round(r).toString(16);
g = Math.round(g).toString(16);
b = Math.round(b).toString(16);
if (r.length < 2)
r="0"+r;
if (g.length < 2)
g="0"+g;
if (b.length < 2)
b="0"+r;
rgb = "#"+r+g+b;
return rgb;
}
alert(xyBriToRgb(0.5052,0.4151, 254));
And i made a PHP Version:
function xyBriToRgb($x,$y,$bri)
{
$z = 1.0 - $x - $y;
$Y = $bri / 255.0;
$X = ($Y / $y) * $x;
$Z = ($Y / $y) * $z;
$r = $X * 1.612 - $Y * 0.203 - $Z * 0.302;
$g = ($X * -1) * 0.509 + $Y * 1.412 + $Z * 0.066;
$b = $X * 0.026 - $Y * 0.072 + $Z * 0.962;
$r = $r <= 0.0031308 ? 12.92 * $r : (1.0 + 0.055) * pow($r, (1.0 / 2.4)) - 0.055;
$g = $g <= 0.0031308 ? 12.92 * $g : (1.0 + 0.055) * pow($g, (1.0 / 2.4)) - 0.055;
$b = $b <= 0.0031308 ? 12.92 * $b : (1.0 + 0.055) * pow($b, (1.0 / 2.4)) - 0.055;
$maxValue = max( $r , $g, $b );
$r = $r / $maxValue;
$g = $g / $maxValue;
$b = $b / $maxValue;
$r = $r * 255; if ($r < 0) $r = 255;
$g = $g * 255; if ($g < 0) $g = 255;
$b = $b * 255; if ($b < 0) $b = 255;
$r = dechex(round($r));
$g = dechex(round($g));
$b = dechex(round($b));
if (strlen($r) < 2) $r = "0" + $r;
if (strlen($g) < 2) $g = "0" + $g;
if (strlen($b) < 2) $b = "0" + $b;
return "#".$r.$g.$b;
}
print xyBriToRgb(0.5052,0.4151, 254);
Happy coding to ye all :-)
At the end it should probably be
r = Math.round(r * 255); if (r < 0) { r = 0; }
g = Math.round(g * 255); if (g < 0) { g = 0; }
b = Math.round(b * 255); if (b < 0) { b = 0; }
If r,g or b is smaller than 0, shouldn't it be 0 instead of 255? The colors I tested make more sense that way.
Thanks for the formula though!
I need to use CMYK colors on my web page. Is there any way to use CMYK in CSS or may be convert CMYK to RGB using JavaScript?
EDIT:
I mean I have colors creating algorithm in CMYK notation and I need to use it on web page.
There is no perfect algorithmic way to convert CMYK to RGB. CYMK is a subtractive color system, RGB is an additive color system. Each have different gamuts, which means there are colors that just cannot be represented in the other color system and vice versa. Both are device dependent color spaces, which really means that what color you really get is dependent on which device you use to reproduce that color, which is why you have color profiles for each device that adjust how it produces color into something more "absolute".
The best that you can do is approximate a simulation of one space onto the other. There is an entire field of computer science that is dedicated to this kind of work, and its non-trivial.
If you are looking for a heuristic for doing this, then the link that Cyrille provided is pretty simple math, and easily invertible to accept a CYMK color and produce a reasonable RGB facsimile.
A very simple heuristic is to map cyan to 0x00FFFF, magenta to 0xFF00FF, and yellow to 0xFFFF00, and black (key) to 0x000000. Then do something like this:
function cmykToRGB(c,m,y,k) {
function padZero(str) {
return "000000".substr(str.length)+str
}
var cyan = (c * 255 * (1-k)) << 16;
var magenta = (m * 255 * (1-k)) << 8;
var yellow = (y * 255 * (1-k)) >> 0;
var black = 255 * (1-k);
var white = black | black << 8 | black << 16;
var color = white - (cyan | magenta | yellow );
return ("#"+padZero(color.toString(16)));
}
invoking cmykToRGB with cmyk ranges from 0.0 to 1.0. That should give you back an RGB color code. But again this is just a heuristic, an actual conversation between these color spaces is much more complicated and takes into account a lot more variables then are represented here. You mileage may vary, and the colors you get out of this might not "look right"
jsFiddle here
There's no way to use CMYK in CSS. You can either use RGB or HSL (CSS3 only). Here's a JavaScript algorithm to convert CMYK to RGB (and the other way around).
Edit: the link seems dead now, here's the code from a cached version:
/**
*
* Javascript color conversion
* http://www.webtoolkit.info/
*
**/
function HSV(h, s, v) {
if (h <= 0) { h = 0; }
if (s <= 0) { s = 0; }
if (v <= 0) { v = 0; }
if (h > 360) { h = 360; }
if (s > 100) { s = 100; }
if (v > 100) { v = 100; }
this.h = h;
this.s = s;
this.v = v;
}
function RGB(r, g, b) {
if (r <= 0) { r = 0; }
if (g <= 0) { g = 0; }
if (b <= 0) { b = 0; }
if (r > 255) { r = 255; }
if (g > 255) { g = 255; }
if (b > 255) { b = 255; }
this.r = r;
this.g = g;
this.b = b;
}
function CMYK(c, m, y, k) {
if (c <= 0) { c = 0; }
if (m <= 0) { m = 0; }
if (y <= 0) { y = 0; }
if (k <= 0) { k = 0; }
if (c > 100) { c = 100; }
if (m > 100) { m = 100; }
if (y > 100) { y = 100; }
if (k > 100) { k = 100; }
this.c = c;
this.m = m;
this.y = y;
this.k = k;
}
var ColorConverter = {
_RGBtoHSV : function (RGB) {
var result = new HSV(0, 0, 0);
r = RGB.r / 255;
g = RGB.g / 255;
b = RGB.b / 255;
var minVal = Math.min(r, g, b);
var maxVal = Math.max(r, g, b);
var delta = maxVal - minVal;
result.v = maxVal;
if (delta == 0) {
result.h = 0;
result.s = 0;
} else {
result.s = delta / maxVal;
var del_R = (((maxVal - r) / 6) + (delta / 2)) / delta;
var del_G = (((maxVal - g) / 6) + (delta / 2)) / delta;
var del_B = (((maxVal - b) / 6) + (delta / 2)) / delta;
if (r == maxVal) { result.h = del_B - del_G; }
else if (g == maxVal) { result.h = (1 / 3) + del_R - del_B; }
else if (b == maxVal) { result.h = (2 / 3) + del_G - del_R; }
if (result.h < 0) { result.h += 1; }
if (result.h > 1) { result.h -= 1; }
}
result.h = Math.round(result.h * 360);
result.s = Math.round(result.s * 100);
result.v = Math.round(result.v * 100);
return result;
},
_HSVtoRGB : function (HSV) {
var result = new RGB(0, 0, 0);
var h = HSV.h / 360;
var s = HSV.s / 100;
var v = HSV.v / 100;
if (s == 0) {
result.r = v * 255;
result.g = v * 255;
result.v = v * 255;
} else {
var_h = h * 6;
var_i = Math.floor(var_h);
var_1 = v * (1 - s);
var_2 = v * (1 - s * (var_h - var_i));
var_3 = v * (1 - s * (1 - (var_h - var_i)));
if (var_i == 0) {var_r = v; var_g = var_3; var_b = var_1}
else if (var_i == 1) {var_r = var_2; var_g = v; var_b = var_1}
else if (var_i == 2) {var_r = var_1; var_g = v; var_b = var_3}
else if (var_i == 3) {var_r = var_1; var_g = var_2; var_b = v}
else if (var_i == 4) {var_r = var_3; var_g = var_1; var_b = v}
else {var_r = v; var_g = var_1; var_b = var_2};
result.r = var_r * 255;
result.g = var_g * 255;
result.b = var_b * 255;
result.r = Math.round(result.r);
result.g = Math.round(result.g);
result.b = Math.round(result.b);
}
return result;
},
_CMYKtoRGB : function (CMYK){
var result = new RGB(0, 0, 0);
c = CMYK.c / 100;
m = CMYK.m / 100;
y = CMYK.y / 100;
k = CMYK.k / 100;
result.r = 1 - Math.min( 1, c * ( 1 - k ) + k );
result.g = 1 - Math.min( 1, m * ( 1 - k ) + k );
result.b = 1 - Math.min( 1, y * ( 1 - k ) + k );
result.r = Math.round( result.r * 255 );
result.g = Math.round( result.g * 255 );
result.b = Math.round( result.b * 255 );
return result;
},
_RGBtoCMYK : function (RGB){
var result = new CMYK(0, 0, 0, 0);
r = RGB.r / 255;
g = RGB.g / 255;
b = RGB.b / 255;
result.k = Math.min( 1 - r, 1 - g, 1 - b );
result.c = ( 1 - r - result.k ) / ( 1 - result.k );
result.m = ( 1 - g - result.k ) / ( 1 - result.k );
result.y = ( 1 - b - result.k ) / ( 1 - result.k );
result.c = Math.round( result.c * 100 );
result.m = Math.round( result.m * 100 );
result.y = Math.round( result.y * 100 );
result.k = Math.round( result.k * 100 );
return result;
},
toRGB : function (o) {
if (o instanceof RGB) { return o; }
if (o instanceof HSV) { return this._HSVtoRGB(o); }
if (o instanceof CMYK) { return this._CMYKtoRGB(o); }
},
toHSV : function (o) {
if (o instanceof HSV) { return o; }
if (o instanceof RGB) { return this._RGBtoHSV(o); }
if (o instanceof CMYK) { return this._RGBtoHSV(this._CMYKtoRGB(o)); }
},
toCMYK : function (o) {
if (o instanceof CMYK) { return o; }
if (o instanceof RGB) { return this._RGBtoCMYK(o); }
if (o instanceof HSV) { return this._RGBtoCMYK(this._HSVtoRGB(o)); }
}
}
Usage:
To convert from HSV to RGB use library like this:
var result = ColorConverter.toRGB(new HSV(10, 20, 30));
alert("RGB:" + result.r + ":" + result.g + ":" + result.b);
To convert from RGB to HSV use library like this:
var result = ColorConverter.toHSV(new RGB(10, 20, 30));
alert("HSV:" + result.h + ":" + result.s + ":" + result.v);
The same goes for CMYK.
CMYK support in CSS is currently considered by W3 for CSS3. But it’s mainly meant for printers and “it is not expected that screen-centric user agents support CMYK colors”. I think you can safely bet that none of the current browsers support CMYK for the screen and therefore you have to convert the colors to RGB somehow.
In the CSS Color Module Level 4 of the W3C as of 5 November 2019, there is a function called device-cmyk that can be used to define a device dependent CMYK color value.
Example:
color: device-cmyk(0 81% 81% 30%);
The function returns an RGB value that the device calculates by trying to convert the CMYK color to an RGB value that matches the CMYK color as close as possible.
Note: I can't find anything regarding the browser support. I guess that no browser is currently supporting this.
You can create your own SCSS/SASS function.
SCSS:
#function cmyk($c, $m, $y, $k) {
$c: $c / 100;
$m: $m / 100;
$y: $y / 100;
$k: $k / 100;
$r: 255 * (1 - $c) * (1 - $k);
$g: 255 * (1 - $m) * (1 - $k);
$b: 255 * (1 - $y) * (1 - $k);
#return rgb($r, $g, $b);
}
SASS:
#function cmyk($c, $m, $y, $k)
$c: $c / 100
$m: $m / 100
$y: $y / 100
$k: $k / 100
$r: 255 * (1 - $c) * (1 - $k)
$g: 255 * (1 - $m) * (1 - $k)
$b: 255 * (1 - $y) * (1 - $k)
#return rgb($r, $g, $b)