I've got what I think is quite an interesting problem that needs an elegant solution...
I have an RGB value, for example 205,50,63.
I am trying to simulate the colour of an RGB LED on a webpage as if it were REAL-LIFE LIGHT.
For example, the RGB colour 255,0,0 would display as red, both on the LED and on the webpage.
Likewise, the RGB colour 255,255,255 would display as white, both on the LED and on the webpage.
BUT the RGB colour 0,0,0 would display as off on the LED and would be displayed as black on the webpage.
What I am trying to achieve is that both 0,0,0 and 255,255,255 display as white. As if the dimmer the LED is, the whiter it gets.
Ive been trying to apply a proportional algorithm to the values and then layer <div> over the top of each other with no luck. Any thoughts?
I'm not sure what the case you're imagining is, but reading your desired output, what is wrong with simply scaling up so the maximum value becomes 255?
function scaleUp(rgb) {
let max = Math.max(rgb.r, rgb.g, rgb.b);
if (!max) { // 0 or NaN
return {r: 255, g: 255, b: 255};
}
let factor = 255 / max;
return {
r: factor * rgb.r,
g: factor * rgb.g,
b: factor * rgb.b,
};
}
So you would get results like
scaleUp({r: 0, g: 0, b: 0}); // {r: 255, g: 255, b: 255}
scaleUp({r: 255, g: 0, b: 0}); // {r: 255, g: 0, b: 0}
scaleUp({r: 50, g: 80, b: 66}); // {r: 159.375, g: 255, b: 210.375}
Notice this collapses all {x, 0, 0} to {255, 0, 0}, meaning {1, 0, 0} is vastly different to {1, 1, 1}. If this is not desirable you'd need to consider special handling of such cases
More RGB hints; you get smoother "more natural" light transitions etc if you square and root around your op, e.g. rather than x + y, do sqrt(x*x + y*y)
This leads to a different idea of how to solve the problem; adding white and scaling down
function scaleDown(rgb) {
let whiteAdded = {
r: Math.sqrt(255 * 255 + rgb.r * rgb.r),
g: Math.sqrt(255 * 255 + rgb.g * rgb.g),
b: Math.sqrt(255 * 255 + rgb.b * rgb.b)
};
return scaleUp(whiteAdded);
}
This time
scaleDown({r: 0, g: 0, b: 0}); // {r: 255, g: 255, b: 255}
scaleDown({r: 255, g: 0, b: 0}); // {r: 255, g: 180.3122292025696, b: 180.3122292025696}
scaleDown({r: 50, g: 80, b: 66}); // {r: 247.94043129928136, g: 255, b: 251.32479296236951}
and have less of a jump around edge points, e.g.
scaleDown({r: 1, g: 0, b: 0}); // {r: 255, g: 254.99803923830171, b: 254.99803923830171}
Finally, notice this maps rgb onto the the range 180..255, so you could transform this to 0..255 if you want to preserve your "true red"s etc
function solution(rgb) {
let high = scaleDown(rgb);
return {
r: 3.4 * (high.r - 180),
g: 3.4 * (high.g - 180),
b: 3.4 * (high.b - 180),
};
}
So
solution({r: 255, g: 0, b: 0}); // {r: 255, g: 1.0615792887366295, b: 1.0615792887366295}
solution({r: 1, g: 0, b: 0}); // {r: 255, g: 254.99333341022583, b: 254.99333341022583}
solution({r: 50, g: 80, b: 66}); // {r: 230.9974664175566, g: 255, b: 242.50429607205635}
I think you should consider HSV color space for this problem. Assuming you have a hue set to red (354° in your example) you can manipulate saturation and value to get desired result.
The idea is to reduce saturation along with value so when dimming the light you loose the saturation. In the edge case when saturation gets to 0%, value is also set to 100% yielding white light.
Take a look at images down below. Please note H, S, V values.
You start with the base case:
Then you dim:
And finally get desaturated color:
In the terms of code it would be
dim is in range 0.0 to 1.0
hsv(dim) -> {
saturation = baseSaturation * (1 - dim)
value = baseValue + (1 - baseValue) * dim
}
hue is constant
As there is already and answer I will not go into too much detail.
The demo simulates Multi colour clear LED
Colour is created by overlapping 3+ images for RGB using composite operation "lighten". This is an additive process. There is also a white channel that adds white light to the whole LED. The RGB channels have additional gain added to even out the effect, and when blue is high red is driven down.
When there is no light just the image of the LED is shown. There is also a contrast image draw befor and after the 4 colour channels RGB & white.
With some better source images (this only uses one per channel, should have 2-3) a very realist FX can be created. Note that the surrounding environment will also affect the look.
// Load media (set of images for led)
var mediaReady = false;
var leds = new Image();
leds.src = "https://i.stack.imgur.com/tT1YV.png";
leds.onload = function () {
mediaReady = true;
}
var canLed = document.createElement("canvas");
canLed.width = 31;
canLed.height = 47;
var ctxLed = canLed.getContext("2d")
// display canvas
var canvas = document.createElement("canvas");
canvas.width = 31 * 20;
canvas.height = 47;
var ctx = canvas.getContext("2d");
var div = document.createElement("div");
div.style.background = "#999";
div.style.position = "absolute";
div.style.top = div.style.left = "0px";
div.style.width = div.style.height = "100%";
var div1 = document.createElement("div");
div1.style.fontFamily="Arial";
div1.style.fontSize = "28px";
div1.textContent ="Simple LED using layered RGB & white images.";
div.appendChild(div1);
div.appendChild(canvas);
document.body.appendChild(div);
const cPow = [1 / 7, 1 / 1, 1 / 3, 1 / 5]; // output gain for g,b,r,w (w is white)
var colourCurrent = {
r : 0,
g : 0,
b : 0,
w : 0
}
function easeInOut(x, pow) { // ease function
x = x < 0 ? 0 : x > 1 ? 1 : x;
xx = Math.pow(x, pow);
return xx / (xx + Math.pow(1 - x, pow));
}
var FX = { // composite operations
light : "lighter",
norm : "source-over",
tone : "screen",
block : "color-dodge",
hard : "hard-light",
}
function randB(min, max) { // random bell
if (max === undefined) {
max = min;
min = 0;
}
var r = (Math.random() + Math.random() + Math.random() + Math.random() + Math.random()) / 5;
return (max - min) * r + min;
}
function randL(min, max) { // linear
if (max === undefined) {
max = min;
min = 0;
}
var r = Math.random();
return (max - min) * r + min;
}
function drawSprite(index, alpha, fx) {
ctxLed.globalAlpha = alpha;
ctxLed.globalCompositeOperation = fx;
ctxLed.drawImage(leds, index * 32, 0, 31, 47, 0, 0, 31, 47);
}
var gbrw = [0, 0, 0, 0];
// Draws a LED using colours in col (sorry had images in wrong order so colour channels are green, blue, red and white
function drawLed(col) {
// get normalised values for each channel
gbrw[0] = col.g / 255;
gbrw[1] = col.b / 255;
gbrw[2] = col.r / 255;
gbrw[3] = col.w / 255;
gbrw[2] *= 1 - gbrw[1]; // suppress red if blue high
var total = (col.g / 255) * cPow[0] + (col.b / 255) * cPow[1] + (col.r / 255) * cPow[2] + (col.w / 255) * cPow[3];
total /= 8;
// display background
drawSprite(4, 1, FX.norm);
// show contrast by summing highlights
drawSprite(4, Math.pow(total, 4), FX.light);
// display each channel in turn
var i = 0;
while (i < 4) {
var v = gbrw[i]; // get channel normalised value
// add an ease curve and push intensity to full (over exposed)
v = easeInOut(Math.min(1, v), 2) * 4 * cPow[i]; // cPow is channel final gain
while (v > 0) { // add intensity for channel
drawSprite(i, easeInOut(Math.min(1, v), 4), FX.light);
if(i === 1){ // if blue add a little white
drawSprite(4, easeInOut(Math.min(1, v)/4, 4), FX.light);
}
v -= 1;
}
i++;
}
drawSprite(4, (1 - Math.pow(total, 4)) / 2, FX.block);
drawSprite(4, 0.06, FX.hard);
}
var gbrwT = [0, 0, 0, 0];
var move = 0.2;
ctx.fillRect(0, 0, canvas.width, canvas.height);
function update(time) {
if (mediaReady) {
time /= 1000;
var t = Math.sin(time / ((Math.sin(time / 5000) * 12300))) * 100;
var t = Math.sin(time / 12300) * 100;
var ttr = Math.sin(time / 12300 + t);
var ttg = Math.sin(time / 12400 + t * 10);
var ttb = Math.sin(time / 12500 + t * 15);
var ttw = Math.sin(time / 12600 + t * 20);
var tr = time / (2360 + t);
var tg = time / (2360 + t * 2);
var tb = time / (2360 + t * 3);
var tw = time / (2360 + t * 4);
for (var i = 0; i * 31 < canvas.width; i++) {
colourCurrent.r = Math.sin(tr) * 128 + 128;
colourCurrent.g = Math.sin(tg) * 128 + 128;
colourCurrent.b = Math.sin(tb) * 128 + 128;
colourCurrent.w = Math.sin(tw) * 128 + 128;
tr += ttr;
tg += ttg;
tb += ttb;
tw += ttw;
drawLed(colourCurrent);
ctx.drawImage(canLed, i * 31, 0);
}
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
Related
My goal
Finding the fastest techniques to implement many image filters using opencv.js
Only use opencv for javascript because I will use it on browser and node.js
Performance(speed) under 10ms possibly (total time: all image filters)
I am trying to implement image filter features using opencv.js. It is important to the performance(speed) when changing values of image filters because I need to provide the filters with a track bar(like input range) in real time.
Are other else any fastest ways like WebGL or SVG Filter? Or did I use opencv.js the wrong way?
Excluded techniques:
CSS
WebGL(OpenGL)
SVG Filter(feComponentTransfer, feFuncR, feFuncB, feFuncG, ...)
Because I will use these features on Node.js with CUDA(Nvidia) as well.
Try
I have tried the 4 ways like this:
for loop with channels (30ms)
convertTo (60ms)
convertScaleAbs (60ms)
filter2D (120ms)
(Updated: Added code examples)
How to check the speed(ms):
console.time('xxxFilter');
// for loop with channels or convertTo or convertScaleAbs or filter2D
// included 'cv.imshow(canvasOutput)'
someFilterFunc();
console.timeEnd('xxxFilter');
It is not included render painting time on the browser.
for loop with channels
function brightnessWithChannels(dst, value) {
cv.cvtColor(dst, dst, cv.COLOR_BGR2RGB);
const channels = new cv.MatVector();
cv.split(dst, channels);
const alphaRatio = 0.3 / 100;
const alpha = 1 + value * alphaRatio;
const deltaRatio = 40 / 100;
const delta = value * deltaRatio;
const channelR = channels.get(0);
const channelG = channels.get(1);
const channelB = channels.get(2);
const pxR = channelR.data;
const pxG = channelG.data;
const pxB = channelB.data;
for (let i = 0; i < pxR.length; i++) {
const r = alpha * pxR[i] + delta;
pxR[i] = Math.max(Math.min(r, 255), 0);
const g = alpha * pxG[i] + delta;
pxG[i] = Math.max(Math.min(g, 255), 0);
const b = alpha * pxB[i] + delta;
pxB[i] = Math.max(Math.min(b, 255), 0);
}
cv.merge(channels, dst);
channelR.delete();
channelG.delete();
channelB.delete();
channels.delete();
cv.cvtColor(dst, dst, cv.COLOR_RGB2BGR);
}
convertTo
function brightnessWithConvertTo(src, dst, value) {
const alphaRatio = 0.3 / 100;
const alpha = 1 + value * alphaRatio;
const deltaRatio = 40 / 100;
const delta = value * deltaRatio;
src.convertTo(dst, -1, alpha, delta);
}
convertScaleAbs
function brightnessWithConvertScaleAbs(dst, value) {
const alphaRatio = 0.3 / 100;
const alpha = 1 + value * alphaRatio;
const deltaRatio = 40 / 100;
const delta = value * deltaRatio;
cv.convertScaleAbs(dst, dst, alpha, delta);
}
filter2D
function brightnessWithFilter2D(dst, value) {
const factor = value / 100; // -1.0 ~ 1.0
/**
* https://docs.rainmeter.net/tips/colormatrix-guide/
* [R 1, 0, 0, 0, 0]
* [G 0, 1, 0, 0, 0]
* [B 0, 0, 1, 0, 0]
* [A 0, 0, 0, 1, 0]
* [W factor, factor, factor, 0, 1]
*/
const kernelMatrix = [
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0,
factor, factor, factor, 0, 1
];
const kernel = cv.matFromArray(5, 5, cv.CV_32FC1, kernelMatrix);
cv.filter2D(dst, dst, -1, kernel);
kernel.delete();
}
This is the full html example(codesandbox)
It is slowest: for loop with ptr (excluded above list)
ref: the opencv.js document
Environment
Browser: Chrome, Safari
OpenCV JavaScript 4.6.0 (https://docs.opencv.org/4.6.0/opencv.js)
Image resource(maximum 1600*1600px)
etc) C++ (2ms)
I ran this code on node.js with C++ addons
static void brightness_slice(FilterContext *ctx, void *arg, int job, int nb_jobs)
{
int start = (ctx->height * job) / nb_jobs;
int end = job == nb_jobs - 1 ? ctx->height : (ctx->height * (job+1)) / nb_jobs;
int line = ctx->width * 4;
const float contrast = ((float)abs(ctx->nValue)) * 0.5;
const float alpha = (259 * (contrast + 255)) / (255 * (259 - contrast));
const float delta = 128 * (1 - alpha) + (((float)ctx->nValue) / 300 * 255);
int i, j;
for (i = start; i < end; i++) {
uint8_t *p = ctx->data + i * line;
for (j = 0; j < ctx->width; j++, p+= 4) {
p[0] = CLAMP_RGB(alpha * ((float)p[0]) + delta);
p[1] = CLAMP_RGB(alpha * ((float)p[1]) + delta);
p[2] = CLAMP_RGB(alpha * ((float)p[2]) + delta);
}
}
}
When I checked the performance above ways(in html), the for loop with channels was the fastest among them. It is nice. However, I want to the speed be close to speed <= 10ms because it is still slow(30ms) for users in real time.
I even have to mix many image filters like this: brightness + contrast + saturation + blur + sharpness + ...
I'm trying to draw the following gradient image in canvas, but there's a problem in the right bottom.
Desired effect:
Current output:
I'm probably missing something really simple here.
function color(r, g, b) {
var args = Array.prototype.slice.call(arguments);
if (args.length == 1) {
args.push(args[0]);
args.push(args[0]);
} else if (args.length != 3 && args.length != 4) {
return;
}
return "rgb(" + args.join() + ")";
}
function drawPixel(x, y, fill) {
var fill = fill || "black";
context.beginPath();
context.rect(x, y, 1, 1);
context.fillStyle = fill;
context.fill();
context.closePath();
}
var canvas = document.getElementById("primary");
var context = canvas.getContext("2d");
canvas.width = 256;
canvas.height = 256;
for (var x = 0; x < canvas.width; x++) {
for (var y = 0; y < canvas.height; y++) {
var r = 255 - y;
var g = 255 - x - y;
var b = 255 - x - y;
drawPixel(x, y, color(r, g, b));
}
}
#primary {
display: block;
border: 1px solid gray;
}
<canvas id="primary"></canvas>
JSFiddle
Using gradients.
You can get the GPU to do most of the processing for you.The 2D composite operation multiply effectively multiplies two colours for each pixel. So for each channel and each pixel colChanDest = Math.floor(colChanDest * (colChanSrc / 255)) is done via the massively parallel processing power of the GPU, rather than a lowly shared thread running on a single core (JavaScript execution context).
The two gradients
One is the background White to black from top to bottom
var gradB = ctx.createLinearGradient(0,0,0,255);
gradB.addColorStop(0,"white");
gradB.addColorStop(1,"black");
The other is the Hue that fades from transparent to opaque from left to right
var swatchHue
var col = "rgba(0,0,0,0)"
var gradC = ctx.createLinearGradient(0,0,255,0);
gradC.addColorStop(0,``hsla(${hueValue},100%,50%,0)``);
gradC.addColorStop(1,``hsla(${hueValue},100%,50%,1)``);
Note the above strings quote are not rendering correctly on SO so I just doubled them to show, use a single quote as done in the demo snippet.
Rendering
Then layer the two, background (gray scale) first, then with composite operation "multiply"
ctx.fillStyle = gradB;
ctx.fillRect(0,0,255,255);
ctx.fillStyle = gradC;
ctx.globalCompositeOperation = "multiply";
ctx.fillRect(0,0,255,255);
ctx.globalCompositeOperation = "source-over";
Only works for Hue
It is important that the color (hue) is a pure colour value, you can not use a random rgb value. If you have a selected rgb value you need to extract the hue value from the rgb.
The following function will convert a RGB value to a HSL colour
function rgbToLSH(red, green, blue, result = {}){
value hue, sat, lum, min, max, dif, r, g, b;
r = red/255;
g = green/255;
b = blue/255;
min = Math.min(r,g,b);
max = Math.max(r,g,b);
lum = (min+max)/2;
if(min === max){
hue = 0;
sat = 0;
}else{
dif = max - min;
sat = lum > 0.5 ? dif / (2 - max - min) : dif / (max + min);
switch (max) {
case r:
hue = (g - b) / dif;
break;
case g:
hue = 2 + ((b - r) / dif);
break;
case b:
hue = 4 + ((r - g) / dif);
break;
}
hue *= 60;
if (hue < 0) {
hue += 360;
}
}
result.lum = lum * 255;
result.sat = sat * 255;
result.hue = hue;
return result;
}
Put it all together
The example renders a swatch for a random red, green, blue value every 3 second.
Note that this example uses Balel so that it will work on IE
var canvas = document.createElement("canvas");
canvas.width = canvas.height = 255;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
function drawSwatch(r, g, b) {
var col = rgbToLSH(r, g, b);
var gradB = ctx.createLinearGradient(0, 0, 0, 255);
gradB.addColorStop(0, "white");
gradB.addColorStop(1, "black");
var gradC = ctx.createLinearGradient(0, 0, 255, 0);
gradC.addColorStop(0, `hsla(${Math.floor(col.hue)},100%,50%,0)`);
gradC.addColorStop(1, `hsla(${Math.floor(col.hue)},100%,50%,1)`);
ctx.fillStyle = gradB;
ctx.fillRect(0, 0, 255, 255);
ctx.fillStyle = gradC;
ctx.globalCompositeOperation = "multiply";
ctx.fillRect(0, 0, 255, 255);
ctx.globalCompositeOperation = "source-over";
}
function rgbToLSH(red, green, blue, result = {}) {
var hue, sat, lum, min, max, dif, r, g, b;
r = red / 255;
g = green / 255;
b = blue / 255;
min = Math.min(r, g, b);
max = Math.max(r, g, b);
lum = (min + max) / 2;
if (min === max) {
hue = 0;
sat = 0;
} else {
dif = max - min;
sat = lum > 0.5 ? dif / (2 - max - min) : dif / (max + min);
switch (max) {
case r:
hue = (g - b) / dif;
break;
case g:
hue = 2 + ((b - r) / dif);
break;
case b:
hue = 4 + ((r - g) / dif);
break;
}
hue *= 60;
if (hue < 0) {
hue += 360;
}
}
result.lum = lum * 255;
result.sat = sat * 255;
result.hue = hue;
return result;
}
function drawRandomSwatch() {
drawSwatch(Math.random() * 255, Math.random() * 255, Math.random() * 255);
setTimeout(drawRandomSwatch, 3000);
}
drawRandomSwatch();
To calculate the colour from the x and y coordinates you need the calculated Hue then the saturation and value to get the hsv colour (NOTE hsl and hsv are different colour models)
// saturation and value are clamped to prevent rounding errors creating wrong colour
var rgbArray = hsv_to_rgb(
hue, // as used to create the swatch
Math.max(0, Math.min(1, x / 255)),
Math.max(0, Math.min(1, 1 - y / 255))
);
Function to get r,g,b values for h,s,v colour.
/* Function taken from datGUI.js
Web site https://workshop.chromeexperiments.com/examples/gui/#1--Basic-Usage
// h 0-360, s 0-1, and v 0-1
*/
function hsv_to_rgb(h, s, v) {
var hi = Math.floor(h / 60) % 6;
var f = h / 60 - Math.floor(h / 60);
var p = v * (1.0 - s);
var q = v * (1.0 - f * s);
var t = v * (1.0 - (1.0 - f) * s);
var c = [
[v, t, p],
[q, v, p],
[p, v, t],
[p, q, v],
[t, p, v],
[v, p, q]
][hi];
return {
r: c[0] * 255,
g: c[1] * 255,
b: c[2] * 255
};
}
I had to do this with OpenGL, and Blindman67's answer was the only resource I found.
In the end, I did it by drawing 3 rectangles on top of each other.
All white
Transparent red to opaque red, horizontally
Transparent black to opaque black, vertically
Update: In the previous example, I've only created the gradient for red. I can also use the same method to create green and blue gradients after a little modification, but I can't use it to create gradients for random hues. Red, Green, and Blue are easy because while the one channel is 255, other two have the same value. For a random hue, e.g. 140°, that is not the case. H=140translates to rgb(0,255,85). Red and Blue can't have equal values. This requires a different and a more complicated calculation.
Blindman67's answer solves this problem. Using built-in gradients, you can easily create gradients for any random hue:
jsfiddle. But being a very curious person, I wanted to do it the hard way anyway, and this is it:
(Compared to Blindman67's, it's very slow...)
JSFiddle
function drawPixel(x, y, fillArray) {
fill = "rgb(" + fillArray.join() + ")" || "black";
context.beginPath();
context.rect(x, y, 1, 1);
context.fillStyle = fill;
context.fill();
}
var canvas = document.getElementById("primary");
var context = canvas.getContext("2d");
var grad1 = [ [255, 255, 255], [0, 0, 0] ]; // brightness
fillPrimary([255, 0, 0]); // initial hue = 0 (red)
$("#secondary").on("input", function() {
var hue = parseInt(this.value, 10);
var clr = hsl2rgb(hue, 100, 50);
fillPrimary(clr);
});
function fillPrimary(rgb) {
var grad2 = [ [255, 255, 255], rgb ]; // saturation
for (var x = 0; x < canvas.width; x++) {
for (var y = 0; y < canvas.height; y++) {
var grad1Change = [
grad1[0][0] - grad1[1][0],
grad1[0][1] - grad1[1][1],
grad1[0][2] - grad1[1][2],
];
var currentGrad1Color = [
grad1[0][0] - (grad1Change[0] * y / 255),
grad1[0][1] - (grad1Change[1] * y / 255),
grad1[0][2] - (grad1Change[2] * y / 255)
];
var grad2Change = [
grad2[0][0] - grad2[1][0],
grad2[0][1] - grad2[1][1],
grad2[0][2] - grad2[1][2],
];
var currentGrad2Color = [
grad2[0][0] - (grad2Change[0] * x / 255),
grad2[0][1] - (grad2Change[1] * x / 255),
grad2[0][2] - (grad2Change[2] * x / 255)
];
var multiplied = [
Math.floor(currentGrad1Color[0] * currentGrad2Color[0] / 255),
Math.floor(currentGrad1Color[1] * currentGrad2Color[1] / 255),
Math.floor(currentGrad1Color[2] * currentGrad2Color[2] / 255),
];
drawPixel(x, y, multiplied);
}
}
}
function hsl2rgb(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
var r, g, b;
if (s == 0) {
r = g = b = l;
} else {
var hue2rgb = function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [
Math.round(r * 255),
Math.round(g * 255),
Math.round(b * 255),
];
}
#primary {
display: block;
border: 1px solid gray;
}
#secondary {
width: 256px;
height: 15px;
margin-top: 15px;
outline: 0;
display: block;
border: 1px solid gray;
box-sizing: border-box;
-webkit-appearance: none;
background-image: linear-gradient(to right, red 0%, yellow 16.66%, lime 33.33%, cyan 50%, blue 66.66%, violet 83.33%, red 100%);
}
#secondary::-webkit-slider-thumb {
-webkit-appearance: none;
height: 25px;
width: 10px;
border-radius: 10px;
background-color: rgb(230, 230, 230);
border: 1px solid gray;
box-shadow: inset 0 0 2px rgba(255, 255, 255, 1), 0 0 2px rgba(255, 255, 255, 1);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="primary" width="256" height="256"></canvas>
<input type="range" min="0" max="360" step="1" value="0" id="secondary" />
Okay, so I've figured out what the problem is. While the vertical range is always between [0,255], horizontal range is between [0,r]. So g and b can't be greater than r (Duh!).
function color(r, g, b) {
var args = Array.prototype.slice.call(arguments);
if (args.length == 1) {
args.push(args[0]);
args.push(args[0]);
} else if (args.length != 3 && args.length != 4) {
return;
}
return "rgb(" + args.join() + ")";
}
function drawPixel(x, y, fill) {
var fill = fill || "black";
context.beginPath();
context.rect(x, y, 1, 1);
context.fillStyle = fill;
context.fill();
context.closePath();
}
var canvas = document.getElementById("primary");
var context = canvas.getContext("2d");
canvas.width = 256;
canvas.height = 256;
for (var x = 0; x < canvas.width; x++) {
for (var y = 0; y < canvas.height; y++) {
var r = 255 - y;
var g = b = r - Math.floor((x / 255) * r); // tada!
drawPixel(x, y, color(r, g, b));
}
}
#primary {
display: block;
border: 1px solid gray;
}
<canvas id="primary"></canvas>
I am working on a relatively simple app that will generate differently colored version of the same .SVG image (modified by HSL values).
Right now I'm implementing hue changes. I am using a generated list of colors. Before drawing the color variations a base color is selected. In this case I used a dead simple .SVG of a green square (hsl(137,100%,82%)).
This is what my code looks like:
for(let i = 0; i < nColors; i++){
ctx.filter = 'hue-rotate('+(palette[i].h-hStart)+'deg)';
ctx.drawImage(img, i*100, 0, 100, 100);
ctx.filter = "none";
}
where:
nColors is the amount of colors in the array
palette is an array of objects with properties h, s and l - cointains the colors
hStart is the base hue of my image (in this case 137)
I'm calculating the hue difference between the current color and the base color and rotating the canvas drawing hue by that number, then drawing the squares side by side. Unfortunately, here are my results.
The list at the top contains the actual colors I want to impose on my .SVG, the squares at the bottom are my canvas.
As you can see, the color diverts more and more with each iteration. I've checked the exact colors in Photoshop (I know Photoshop uses HSB but I converted the values) and the S&L differences are really big and somewhat regular (the first one is correct).
100,82
100,82
100,89
83,100
52,100
53,100
60,100
62,100
Now, I did read somewhere that different browsers may render colors differently so I checked the colors with getPixelData and the results matched my Photoshop readings, therefore I believe that the issue indeed lies in the hue-rotate filter.
I could achieve the same results by reading all the pixel data and changing it "manually", but in the end I'd like to paint each new image to an invisible, large canvas and export high resolution .PNGs - it would be rather CPU intensive and take a long time.
Is it actually a bug/feature of hue-rotate or am I making a mistake somewhere? Is there any way to fix it? Is there any other way to achieve the same results while keeping it relatively simple and sticking to vectors?
EDIT: here's a fiddle
This is not really a bug.
Canvas 2DContext's filter = CSSFilterFunc will produce the same result as the CSS filter: CSSFilterFunc, and the hue-rotate(angle) function does only approximate this hue-rotation : it doesn't convert all your RGBA pixels to their HSL values. So yes, you'll have wrong results.
But, you may try to approximate this using SVGFilterMatrix instead. The original hue-rotate will produce a similar result than the CSSFunc one, but we can calculate the hue rotation and apply it to a colorMatrix.
If you want to write it, here is a paper explaining how to do it : http://www.graficaobscura.com/matrix/index.html
I don't really have time right now to do it, so I'll borrow an already written js implementation of a better approximation than the default one found in this Q/A, written by pixi.js mates and will only show you how to apply it on your canvas, thanks to an SVGFilter.
Note that as correctly pointed by #RobertLongson, you also need to set the color-interpolation-filters property of the feColorMatrix element to sRGB since it defaults to linear-sRGB.
// set our SVGfilter's colorMatrix's values
document.getElementById('matrix').setAttribute('values', hueRotate(100));
var cssCtx = CSSFiltered.getContext('2d');
var svgCtx = SVGFiltered.getContext('2d');
var reqctx = requiredRes.getContext('2d');
cssCtx.fillStyle = svgCtx.fillStyle = reqctx.fillStyle = 'hsl(100, 50%, 50%)';
cssCtx.fillRect(0, 0, 100, 100);
svgCtx.fillRect(0, 0, 100, 100);
reqctx.fillRect(0, 0, 100, 100);
// CSSFunc
cssCtx.filter = "hue-rotate(100deg)";
// url func pointing to our SVG Filter
svgCtx.filter = "url(#hue-rotate)";
reqctx.fillStyle = 'hsl(200, 50%, 50%)';
cssCtx.fillRect(100, 0, 100, 100);
svgCtx.fillRect(100, 0, 100, 100);
reqctx.fillRect(100, 0, 100, 100);
var reqdata = reqctx.getImageData(150, 50, 1, 1).data;
var reqHSL = rgbToHsl(reqdata);
console.log('required result : ', 'rgba(' + reqdata.join() + '), hsl(' + reqHSL + ')');
var svgData = svgCtx.getImageData(150, 50, 1, 1).data;
var svgHSL = rgbToHsl(svgData);
console.log('SVGFiltered : ', 'rgba(' + svgData.join() + '), , hsl(' + svgHSL + ')');
// this one throws an security error in Firefox < 52
var cssData = cssCtx.getImageData(150, 50, 1, 1).data;
var cssHSL = rgbToHsl(cssData);
console.log('CSSFiltered : ', 'rgba(' + cssData.join() + '), hsl(' + cssHSL + ')');
// hueRotate will create a colorMatrix with the hue rotation applied to it
// taken from https://pixijs.github.io/docs/filters_colormatrix_ColorMatrixFilter.js.html
// and therefore from https://stackoverflow.com/questions/8507885/shift-hue-of-an-rgb-color/8510751#8510751
function hueRotate(rotation) {
rotation = (rotation || 0) / 180 * Math.PI;
var cosR = Math.cos(rotation),
sinR = Math.sin(rotation),
sqrt = Math.sqrt;
var w = 1 / 3,
sqrW = sqrt(w);
var a00 = cosR + (1.0 - cosR) * w;
var a01 = w * (1.0 - cosR) - sqrW * sinR;
var a02 = w * (1.0 - cosR) + sqrW * sinR;
var a10 = w * (1.0 - cosR) + sqrW * sinR;
var a11 = cosR + w * (1.0 - cosR);
var a12 = w * (1.0 - cosR) - sqrW * sinR;
var a20 = w * (1.0 - cosR) - sqrW * sinR;
var a21 = w * (1.0 - cosR) + sqrW * sinR;
var a22 = cosR + w * (1.0 - cosR);
var matrix = [
a00, a01, a02, 0, 0,
a10, a11, a12, 0, 0,
a20, a21, a22, 0, 0,
0, 0, 0, 1, 0,
];
return matrix.join(' ');
}
function rgbToHsl(arr) {
var r = arr[0] / 255,
g = arr[1] / 255,
b = arr[2] / 255;
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0;
} else {
var 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 [
Math.round(h * 360),
Math.round(s * 100),
Math.round(l * 100)
];
}
body{ margin-bottom: 100px}
<!-- this is our filter, we'll add the values by js -->
<svg height="0" width="0">
<filter id="hue-rotate">
<feColorMatrix in="SourceGraphic" id="matrix" type="matrix" color-interpolation-filters="sRGB" />
</filter>
</svg>
<p>CSS Filtered :
<br>
<canvas id="CSSFiltered" width="200" height="100"></canvas>
</p>
<p>SVG Filtered :
<br>
<canvas id="SVGFiltered" width="200" height="100"></canvas>
</p>
<p>Required Result :
<br>
<canvas id="requiredRes" width="200" height="100"></canvas>
</p>
I have a raindow HSV gradient canvas that when you click it, an element is added at that location with its background as the color of the clicked pixel.
What I'd like is to have it work in reverse as well. For example if you had a hex color, I'd like to find that pixel on the canvas and create an element at that position.
My first thought was to somehow use a matrix/quadrant system. My next thought was that since I'm using HSV, I could use my HSV gradient location points to figure out the location. The problem is that my points aren't equidistant from each other which makes it harder. On top of that, I have a white gradient and black gradient covering the main color gradient and I need that to be accounted for.
So my question is, how can I find the position of the color pixel or at least it's closest match by just using a hex code?
Here is my code thus far:
http://codepen.io/shelbywhite/pen/EyqPWY?editors=1000
HTML:
<div class="container">
<canvas class="colorSpectrum"></canvas>
<div class="circle"></div>
</div>
CSS:
.container {
background: grey;
height: 350px;
width: 400px;
}
.circle {
background: transparent;
box-shadow: 0 0 8px rgba(0,0,0,0.2);
border-radius: 50%;
border: 2px solid #fff;
height: 20px;
margin: -12px;
width: 20px;
position: absolute;
}
.colorSpectrum {
display: block;
height: 100%;
transform: translateZ(0);
width: 100%;
}
Javascript:
$(function() {
var closest = function(num, arr) {
var curr = arr[0];
var diff = Math.abs(num - curr);
for (var val = 0; val < arr.length; val++) {
var newdiff = Math.abs(num - arr[val]);
if (newdiff < diff) {
diff = newdiff;
curr = arr[val];
}
}
return curr;
};
var container = $('.container');
var containerWidth = container.width();
var containerHeight = container.height();
var verticalGradientsHeight = Math.round(containerHeight * .34);
console.log('verticalGradientsHeight', verticalGradientsHeight);
var round = function(value, decimals) {
return Number(Math.round(value+'e'+decimals)+'e-'+decimals);
};
// Draws the color spectrum onto the canvas
var drawColorSpectrum = function() {
// Cache canvas element
var canvasElement = $('.colorSpectrum');
// Cache javascript element
var canvas = canvasElement[0];
// Get canvas context
var ctx = canvas.getContext('2d');
// Cache page height
var canvasWidth = containerWidth;
// Cache page height
var canvasHeight = containerHeight - 72;
// Bottom gradient start position
var blackStartYPos = canvasHeight - verticalGradientsHeight;
// Bottom gradient end position
var blackEndYPos = canvasHeight;
// Create white gradient element
var white = ctx.createLinearGradient(0, 0, 0, verticalGradientsHeight);
// Create black gradient element
var black = ctx.createLinearGradient(0, blackStartYPos, 0, blackEndYPos);
// Create new instance of image
var img = new Image();
// Cache container
_colorSpectrumContainer = canvasElement.parent();
// Set global var
spectrumCanvas = canvasElement;
// Set width of canvas
canvas.width = canvasWidth;
// Set height of canvas
canvas.height = canvasHeight;
// Image load listener
img.onload = function() {
// Draw intial image
ctx.drawImage(this, 0, 0, canvasWidth, canvasHeight);
// Draw white to transparent gradient
white.addColorStop(0, "hsla(0,0%,100%,1)");
white.addColorStop(0.05, "hsla(0,0%,100%,1)");
white.addColorStop(0.20, "hsla(0,0%,100%,0.89)");
white.addColorStop(0.38, "hsla(0,0%,100%,0.69)");
white.addColorStop(0.63, "hsla(0,0%,100%,0.35)");
white.addColorStop(0.78, "hsla(0,0%,100%,0.18)");
white.addColorStop(0.91, "hsla(0,0%,100%,0.06)");
white.addColorStop(1, "hsla(0,0%,100%,0)");
ctx.fillStyle = white;
ctx.fillRect(0, 0, canvasWidth, verticalGradientsHeight);
// Draw black to transparent gradient
black.addColorStop(0, "hsla(0,0%,0%,0)");
black.addColorStop(0.20, "hsla(0,0%,0%,0.01)");
black.addColorStop(0.28, "hsla(0,0%,0%,0.04)");
black.addColorStop(0.35, "hsla(0,0%,0%,0.09)");
black.addColorStop(0.51, "hsla(0,0%,0%,0.26)");
black.addColorStop(0.83, "hsla(0,0%,0%,0.69)");
black.addColorStop(1, "hsla(0,0%,0%,1)");
ctx.fillStyle = black;
ctx.fillRect(0, blackStartYPos, canvasWidth, verticalGradientsHeight);
}
// Set image source
img.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAABCAYAAACbv+HiAAAA0ElEQVR4AYWSh2oDMAwFz6u7//+d2YmXalGBIBM47nnPIIEtmd8FGBTgDbPxDmbn49pX+cZX+Nz4mkZ2SECEAXTCAprlalntBC5whdUJnOfKEy5DjZYtB+o0D3XUMk0tkaZZEn2VuyiJQQQywS/P4c25ucTrfF3ndsoVdjmy3NMiuptR1eHfNcBFM2orW1ZXru00JZiBDrIII5AG5AlloX5TcG6/ywuuv0zAbyL4TWRZmIvU5TNBTjCPIIu5N3YgO7Wxtbot3q4+2LgTyFnZ/QHzBZD1KDpyqQAAAABJRU5ErkJggg==";
};
//
var hexToRgb = function(hex) {
hex = hex.replace('#','');
r = parseInt(hex.substring(0, 2), 16);
g = parseInt(hex.substring(2, 4), 16);
b = parseInt(hex.substring(4, 6), 16);
return [r, g, b];
};
//
var rgbToHsb = function(r, g, b) {
var rr, gg, bb,
r = r / 255,
g = g / 255,
b = b / 255,
h, s,
v = Math.max(r, g, b),
diff = v - Math.min(r, g, b),
diffc = function(c){
return (v - c) / 6 / diff + 1 / 2;
};
if (diff == 0) {
h = s = 0;
} else {
s = diff / v;
rr = diffc(r);
gg = diffc(g);
bb = diffc(b);
if (r === v) {
h = bb - gg;
}else if (g === v) {
h = (1 / 3) + rr - bb;
}else if (b === v) {
h = (2 / 3) + gg - rr;
}
if (h < 0) {
h += 1;
}else if (h > 1) {
h -= 1;
}
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
b: Math.round(v * 100)
};
};
// Find hue in stop range
var findHueInStopRange = function(hue) {
// Array of hue stops with HSV, RGB, and HEX info
var stops = [{
h: 0,
l: 0,
s: 100,
b: 100
}, {
h: 60,
l: 21,
s: 100,
b: 100
}, {
h: 120,
l: 40,
s: 85,
b: 85
}, {
h: 180,
l: 56,
s: 85,
b: 85
}, {
h: 237,
l: 72,
s: 86,
b: 96
}, {
h: 300,
l: 89,
s: 86,
b: 96
}, {
h: 359,
l: 100,
s: 100,
b: 100
}];
// Total number of stops
var stopsLength = stops.length;
// Loop through stops
for (var i = 0; i < stopsLength; i += 1) {
// Temp set
var currentStop = stops[i];
// Temp set
// var nextStop = stops[i + 1];
var nextStop = (i + 1 > stopsLength - 1) ? currentStop : stops[i + 1];
// Location is a percentage
var huePos;
// Temp set
var xPos = false;
console.log('hue', currentStop.h, '>>', hue, '<<', nextStop.h);
// Find which range of hue stops the current color is
// Hue is between current and next hue stop
if (hue >= currentStop.h && hue <= nextStop.h) {
// hue is current stop
if (hue === currentStop.h) {
// Set as location
huePos = currentStop.l;
// hue is next stop
} else if (hue === nextStop.h) {
// Set as location
huePos = nextStop.l;
// Hue is somewhere between stops
} else {
// Get percentage location between hue stops
var relativeHuePos = (hue - currentStop.h) / (nextStop.h - currentStop.h);
// Normalized to fit custom gradient stop locations
huePos = relativeHuePos * (nextStop.l - currentStop.l) + currentStop.l;
}
// A location was found
if (huePos) {
// Convert from percentage to pixel position
xPos = Math.round(containerWidth * (huePos / 100));
return xPos;
} else {
continue;
}
}
}
};
// Find saturation in stop range
var findSaturationInStopRange = function (saturation) {
// Array of hue stops with HSV, RGB, and HEX info
var stops = [{
l: 0,
s: 0
}, {
l: 0.05,
s: 6
}, {
l: 0.20,
s: 18
}, {
l: 0.38,
s: 35
}, {
l: 0.63,
s: 69
}, {
l: 0.78,
s: 89,
}, {
l: 0.91,
s: 100,
}, {
l: 1,
s: 100,
}];
// Total number of stops
var stopsLength = stops.length;
// Loop through stops
for (var i = 0; i < stopsLength; i += 1) {
// Temp set
var currentStop = stops[i];
// Temp set
var nextStop = (i + 1 > stopsLength - 1) ? currentStop : stops[i + 1];
// Location is a percentage
var satPos;
// Temp set
var yPos = false;
// Convert location to percentage
var currentStopLocation = currentStop.l * 100;
// Convert location to percentage
var nextStopLocation = nextStop.l * 100;
// Find which range of hue stops the current color is
// Hue is between current and next hue stop
if (saturation >= currentStop.s && saturation <= nextStop.s) {
// hue is current stop
if (saturation === currentStop.s) {
// Set as location
satPos = currentStopLocation;
// hue is next stop
} else if (saturation === nextStop.s) {
// Set as location
satPos = nextStopLocation;
// Hue is somewhere between stops
} else {
// Get percentage location between gradient stops
var ratioBetweenSaturation = (saturation - currentStop.s) / (nextStop.s - currentStop.s);
// Normalized to fit custom gradient stop locations
satPos = ratioBetweenSaturation * (nextStopLocation - currentStopLocation) + currentStopLocation;
}
console.log('ratioBetweenSaturation', ratioBetweenSaturation);
console.log('satPos', satPos);
console.log('saturation', saturation, '>=', currentStop.s, saturation, '<=', nextStop.s);
// A location was found
if (satPos !== false) {
// Convert from percentage to pixel position
yPos = Math.round(verticalGradientsHeight * (satPos / 100));
return yPos;
} else {
continue;
}
}
}
};
// Find brightness in stop range
var findBrightnessInStopRange = function (brightness) {
// Array of hue stops with HSV, RGB, and HEX info
var stops = [{
l: 0,
b: 100
}, {
l: 0.20,
b: 88
}, {
l: 0.28,
b: 69
}, {
l: 0.35,
b: 26
}, {
l: 0.51,
b: 9
}, {
l: 0.83,
b: 4,
}, {
l: 1,
b: 0,
}];
// Total number of stops
var stopsLength = stops.length;
// Loop through stops
for (var i = 0; i < stopsLength; i += 1) {
// Temp set
var currentStop = stops[i];
// Temp set
var nextStop = (i + 1 > stopsLength - 1) ? currentStop : stops[i + 1];
// Location is a percentage
var brightPos;
// Temp set
var yPos = false;
// Convert location to percentage
var currentStopLocation = currentStop.l * 100;
// Convert location to percentage
var nextStopLocation = nextStop.l * 100;
console.log('brightness', brightness, '>=', currentStop.b, brightness, '<=', nextStop.b);
// Find which range of hue stops the current color is
// Hue is between current and next hue stop
if (brightness <= currentStop.b && brightness >= nextStop.b) {
// hue is current stop
if (brightness === currentStop.b) {
// Set as location
brightPos = currentStopLocation;
// hue is next stop
} else if (brightness === nextStop.b) {
// Set as location
brightPos = nextStopLocation;
// Hue is somewhere between stops
} else {
// Get percentage location between gradient stops
var ratioBetweenBrightness = (brightness - currentStop.b) / (nextStop.b - currentStop.b);
// Normalized to fit custom gradient stop locations
brightPos = ratioBetweenBrightness * (nextStopLocation - currentStopLocation) + currentStopLocation;
}
console.log('ratioBetweenBrightness', ratioBetweenBrightness);
console.log('brightPos', brightPos);
console.log('brightness', brightness, '>=', currentStop.b, brightness, '<=', nextStop.b);
// A location was found
if (brightPos !== false) {
// Convert from percentage to pixel position
yPos = Math.round(verticalGradientsHeight * (brightPos / 100));
return yPos;
} else {
continue;
}
}
}
};
// Get coordinates from hue, brightness, saturation
var getColorCoordinates = function (hex) {
// Convert hex to rgb
var rgb = hexToRgb(hex);
console.log('rgb', rgb);
// Convert rgb to hsb
var hsb = rgbToHsb(rgb[0], rgb[1], rgb[2]);
console.log('hsb', hsb);
// Set x position to position of hue
var xPos = findHueInStopRange(hsb.h);
var yPos = 0;
// if 100, get (containerHeight - verticalGradientHeight) + whatever position is set with bottom gradient
//
// Saturation and brightness are both maxed
if (hsb.s === 100 && hsb.b === 100) {
// Set y position at center of container
yPos = containerHeight * 0.5;
} else {
console.log('using nothing', hsb.s, hsb.b);
//
if (hsb.s < 100) {
// Saturation y position (upper quadrant)
yPos = findSaturationInStopRange(hsb.s);
console.log('using saturation', yPos);
} else if (hsb.b < 100) {
// Brightness y position (lower quadrant)
yPos = findBrightnessInStopRange(hsb.b);
console.log('using brightness', yPos);
}
}
return { x: xPos, y: yPos };
}
// Get hue location
var position = false;
// Temp set
var hex = '42ad40';
// Draw gradient
drawColorSpectrum();
// Find x position
position = getColorCoordinates(hex); //91ff26
console.log('location', position);
// Draw line
$('.circle').css({
top: position.y + 'px',
left: position.x + 'px',
background: '#' + hex
});
});
** UPDATE **
I'm actually trying to do this in HSV not HSL. I don't have preference other than I used photoshop to generate a smooth gradient.
Also, I have added a new example with what I have created. One of the up-voted answers below hints at how to accomplish what I'm trying to do, but so far I haven't been able to successfully do it.
Updated code will be at this link, I've also updated the code above:
http://codepen.io/shelbywhite/pen/EyqPWY?editors=1000
To get the location you need the HSL values
// global
var RGB = [0,0,0]; // holds the RGB values 0-255
var LSH = [0,0,0]; // holds the LSH values (note H is normalised to 0-255)
var rgbToLSH = function(){
var r = RGB[0]/255;
var g = RGB[1]/255;
var b = RGB[2]/255;
var min = Math.min(r,g,b);
var max = Math.max(r,g,b);
var lum = (min+max)/2;
if(lum > 0.5){
var sat = (max-min)/(max+min);
}else{
var sat = (max-min)/(2-max-min);
}
if(r >= b && r >= g){
var hue = (g-b)/(max-min);
}else
if(b >= b && b >= g){
var hue = 4.0 + (r-g)/(max-min);
}else{
var hue = 2.0 + (b-r)/(max-min);
}
hue *= 60;
if(hue < 0) hue += 360;
hue = (hue/360);
lum = Math.min(1,Math.max(0,lum));
sat = Math.min(1,Math.max(0,sat));
hue = Math.min(1,Math.max(0,hue));
LSH[0] = lum*255;
LSH[1] = sat*255;
LSH[2] = hue*255;
}
Hue will give the position on the x axis and Saturation will give you y from top to midway and Lightness will give you y axis position from midway to the bottom (going by your example);
You could iterate over the entire ImageData and compare both colors.
var findPixelByHex = function(imageData, hex) {
var d = imageData.data
var w = imageData.width
var h = imageData.height
for (var y = 0; y < h; ++y) {
for (var x = 0; x < w; ++x) {
var i = (y * w + x) * 4
if (hex === rgbToHex(d[i], d[i + 1], d[i + 2])) {
setColorAtPixel(x, y, hex)
}
}
}
}
Note that this is pretty slow and not the best idea.
is possible to create a circle with raphael with conical gradient?
Something like colorwheel of http://raphaeljs.com/picker/ but with customized colors
ex: "from red to orange to yellow to green to yellow to orange to red)"
Yes it is, try this function (I statred from Raphael's Colorpicker code, made it stand-alone and added the gradation between specific colors instead of just increasing the hue along the disc):
var paper = Raphael('paper', 800, 600);
var wheel = function (x, y, r, colors) {
var pi = Math.PI;
var nbColors = colors.length;
// Formatting every color to its RGB values
for (var i = 0 ; i < nbColors ; i++)
{
colors[i] = Raphael.getRGB(colors[i]);
}
// Initialize segments
var segments = pi * r * 2 / Math.min(r / 8, 4);
var a = pi / 2 - pi * 2 / segments * 1.5;
var path = ["M", x, y - r, "A", r, r, 0, 0, 1, r * Math.cos(a) + x, y - r * Math.sin(a), "L", x, y, "z"].join();
// Draw segments
for (var i = 0 ; i < segments ; i++)
{
// Between which 2 colors is this segment?
var j = nbColors * i / segments;
var n = Math.floor(j);
var d = j % 1;
var color1 = colors[n];
var color2 = colors[(n + 1) % nbColors];
// Calculate the segment's color from the 2 other
var color =
{ r : Math.round(d * (color2.r - color1.r) + color1.r)
, g : Math.round(d * (color2.g - color1.g) + color1.g)
, b : Math.round(d * (color2.b - color1.b) + color1.b)
}
// Draw the sector
paper.path(path).attr(
{ stroke: 'none'
, fill: 'rgb(' + color.r + ',' + color.g + ',' + color.b + ')'
, rotation: [90 + (360 / segments) * i, x, y]
});
}
// Surrounding circle
return paper.circle(x, y, r).attr(
{ stroke : '#fff'
, 'stroke-width' : Math.round(r * .03)
});
};
You can use it like this, the 4th parameter being an array of colors to use:
wheel (100, 100, 50, ['#F00', '#0FF', '#00F', '#0F0', '#F80']);
wheel (500, 200, 50, ['rgb(255,0,0)', 'hsb(40,100,100)', '#0F00F0']);