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>
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).
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.
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";
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;
}
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
};
}