HTML5 Canvas hue-rotate changing saturation and lightness - javascript

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>

Related

Javascript Using Math.random to generate random rgba values in for loop

I'm very new at this and apologise if it's a silly question. This is just a simple for loop to draw n amount of circles and I want to randomly generate rgba values but it takes the last strokeStyle used instead, what am I doing wrong?
for (var i = 0; i < 10; i++){
var x = Math.random() * window.innerWidth;
var y = Math.random() * window.innerHeight;
var colour = Math.random() * 255;
c.beginPath();
c.arc(x, y, 30, 0, Math.PI * 2, false);
c.strokeStyle = 'rgba(colour, colour, colour, Math.random())';
c.stroke(); }
Thank you so much!!
This can be done by formatting a color string as follows:
"rgba(" + r + "," + g + "," + b + "," + a + ")";
where r, g, b are integers in the range of 0 to 255, and a is a floating point in the range of 0.0 to 1.0;
For a complete example see the following code snippet:
var c = document.getElementById("canvas").getContext("2d");
for (var i = 0; i < 10; i++) {
const x = Math.random() * c.canvas.width;
const y = Math.random() * c.canvas.height;
// Red, green, blue should be integers in the range of 0 - 255
const r = parseInt(Math.random() * 255);
const g = parseInt(Math.random() * 255);
const b = parseInt(Math.random() * 255);
// Alpha is a floating point in range of 0.0 - 1.0
const a = Math.random();
c.beginPath();
c.arc(x, y, 30, 0, Math.PI * 2, false);
c.strokeStyle = "rgba(" + r + "," + g + "," + b + "," + a + ")";
c.stroke();
}
<canvas id="canvas"></canvas>
Alternatively, if your target browser supports "template literals" then the same color string can be formatted in a more concise way via the following:
const r = parseInt(Math.random() * 255);
const g = parseInt(Math.random() * 255);
const b = parseInt(Math.random() * 255);
const a = Math.random();
// Format color string via template literal using back ticks ` and ${}
// to render scope variables to the string result
c.strokeStyle = `rgba(${r}, ${g}, ${b}, ${a})`;
'rgba(colour, colour, colour, Math.random())' is a literal string, which makes it an invalid CSS (since CSS won't recognise either colour or Math.random()), which will be discarded.
You might want a template literal instead (notice the different quote):
c.strokeStyle = `rgba(${colour}, ${colour}, ${colour}, ${Math.random()})`
Also, note that this will not give you quite a random colour; it will give you a random grey colour, as you linked the R, G and B component to be the same colour. If you want the three components to be able to differ, you need to generate a new random number for each component.

Linear interpolation on canvas

I'm trying to understand how image resampling methods work. I've read/watched several pages/videos and I think I got the idea. However, I couldn't find any working example on how to implement it. So I thought I should start with the basics: nearest neighbor resampling on 1D.
This was very straightforward and I think I got it. JSFiddle Demo.
function resample() {
var widthScaled = Math.round(originalPixels.width * scaleX);
var sampledPixels = context.createImageData(widthScaled, originalPixels.height);
for (var i = 0; i < sampledPixels.data.length; i+=4) {
var position = index2pos(sampledPixels, i);
var origPosX = Math.floor(position.x / scaleX);
var origColor = getPixel(originalPixels, origPosX, position.y);
setPixel(sampledPixels, position.x, position.y, origColor);
}
loadImage(context, sampledPixels);
}
Next, I moved on to linear interpolation. Thought it'd be simple too, but I'm having problems. First, how do I deal with the last pixel (marked red)? It has only one neighboring pixel. Second, my result is too sharp when compared to Photoshop's. Is my method flawed, or is PS doing some extra work? JSFiddle Demo.
function resample() {
var sampledPixels = context.createImageData(originalPixels.width * scaleX, originalPixels.height);
for (var i = 0; i < sampledPixels.data.length; i+=4) {
var position = index2pos(sampledPixels, i);
var origPosX = position.x / scaleX;
var leftPixelPosX = Math.floor(origPosX);
var rightPixelPosX = Math.ceil(origPosX);
var leftPixelColor = getPixel(originalPixels, leftPixelPosX, position.y);
var rightPixelColor = getPixel(originalPixels, rightPixelPosX, position.y);
var weight = origPosX % 1;
var color = mix(leftPixelColor[0], rightPixelColor[0], weight);
color = [color, color, color, 255];
setPixel(sampledPixels, position.x, position.y, color);
}
loadImage(context, sampledPixels);
}
function mix(x, y, a) {
return x * (1 - a) + y * a;
}
Linear interpolation of pixels
There is no real right and wrong way to do filtering, as the result is subjective and the quality of the result is up to you, Is it good enough, or do you feel there is room for improvement.
There are also a wide variety of filtering methods, nearest neighbor, linear, bilinear, polynomial, spline, Lanczos... and each can have many variations. There are also factors like what is the filtering output format; screen, print, video. Is quality prefered over speed, or memory efficiency. And why upscale when hardware will do it for you in real-time anyways.
It looks like you have the basics of linear filtering correct
Update Correction. Linear and bilinear refer to the same type of interpolation, bilinear is 2D and linear is 1D
Handling the last Pixel
In the case of the missing pixel there are several options,
Assume the colour continues so just copy the last pixel.
Assume the next pixel is the background, border colour, or some predefined edge colour.
Wrap around to the pixel at the other side (best option for tile maps)
If you know there is a background image use its pixels
Just drop the last pixel (image size will be 1 pixel smaller)
The PS result
To me the PhotoShop result looks like a form of bilinear filtering, though it should be keeping the original pixel colours, so something a little more sophisticated is being used. Without knowing what the method is you will have a hard time matching it.
A spectrum for best results
Good filtering will find the spectrum of frequencies at a particular point and reconstruct the missing pixel based on that information.
If you think of a line of pixels not as values but as volume then a line of pixels makes a waveform. Any complex waveform can be broken down into a set of simpler basic pure tones (frequencies). You can then get a good approximation by adding all the frequencies at a particular point.
Filters that use this method are usually denoted with Fourier, or FFT (Fast Fourier Transform) and require a significant amount of process over standard linear interpolation.
What RGB values represent.
Each channel red, green, and blue represent the square root of that channel's intensity/brightness. (this is a close general purpose approximation) Thus when you interpolate you need to convert to the correct values then interpolate then convert back to the logarithmic values.
Correct interpolation
function interpolateLinear(pos,c1,c2){ // pos 0-1, c1,c2 are objects {r,g,b}
return {
r : Math.sqrt((c2.r * c2.r + c1.r * c1.r) * pos + c1.r * c1.r),
g : Math.sqrt((c2.g * c2.g + c1.g * c1.g) * pos + c1.g * c1.g),
b : Math.sqrt((c2.b * c2.b + c1.b * c1.b) * pos + c1.b * c1.b),
};
}
It is important to note that the vast majority of digital processing software does not correctly interpolate. This is in part due to developers ignorance of the output format (why I harp on about it when I can), and partly due to compliance with ye olde computers that struggled just to display an image let alone process it (though I don't buy that excuse).
HTML5 is no exception and incorrectly interpolates pixel values in almost all interpolations. This producing dark bands where there is strong hue contrast and darker total brightness for up and down scaled image. Once you notice the error it will forever annoy you as today's hardware is easily up to the job.
To illustrate just how bad incorrect interpolation can be the following image shows the correct (top) and the canvas 2D API using a SVG filter (bottom) interpolation.
2D linear interpolation (Bilinear)
Interpolating along both axis is done by doing each axis in turn. First interpolate along the x axis and then along the y axis. You can do this as a 2 pass process or a single pass.
The following function will interpolate at any sub pixel coordinate. This function is not built for speed and there is plenty of room for optimisation.
// Get pixel RGBA value using bilinear interpolation.
// imgDat is a imageData object,
// x,y are floats in the original coordinates
// Returns the pixel colour at that point as an array of RGBA
// Will copy last pixel's colour
function getPixelValue(imgDat, x,y, result = []){
var i;
// clamp and floor coordinate
const ix1 = (x < 0 ? 0 : x >= imgDat.width ? imgDat.width - 1 : x)| 0;
const iy1 = (y < 0 ? 0 : y >= imgDat.height ? imgDat.height - 1 : y | 0;
// get next pixel pos
const ix2 = ix1 === imgDat.width -1 ? ix1 : ix1 + 1;
const iy2 = iy1 === imgDat.height -1 ? iy1 : iy1 + 1;
// get interpolation position
const xpos = x % 1;
const ypos = y % 1;
// get pixel index
var i1 = (ix1 + iy1 * imgDat.width) * 4;
var i2 = (ix2 + iy1 * imgDat.width) * 4;
var i3 = (ix1 + iy2 * imgDat.width) * 4;
var i4 = (ix2 + iy2 * imgDat.width) * 4;
// to keep code short and readable get data alias
const d = imgDat.data;
for(i = 0; i < 3; i ++){
// interpolate x for top and bottom pixels
const c1 = (d[i2] * d[i2++] - d[i1] * d[i1]) * xpos + d[i1] * d[i1 ++];
const c2 = (d[i4] * d[i4++] - d[i3] * d[i3]) * xpos + d[i3] * d[i3 ++];
// now interpolate y
result[i] = Math.sqrt((c2 - c1) * ypos + c1);
}
// and alpha is not logarithmic
const c1 = (d[i2] - d[i1]) * xpos + d[i1];
const c2 = (d[i4] - d[i3]) * xpos + d[i3];
result[3] = (c2 - c1) * ypos + c1;
return result;
}
const upScale = 4;
// usage
const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const imgData2 = ctx.createImageData(ctx.canvas.width * upScale, ctx.canvas.height * upScale);
const res = new Uint8ClampedArray(4);
for(var y = 0; y < imgData2.height; y++){
for(var x = 0; x < imgData2.width; x++){
getPixelValue(imgData,x / upScale, y / upScale, res);
imgData2.data.set(res,(x + y * imgdata2.width) * 4);
}
}
Example upscale canvas 8 times
The example uses the above function to upscale a test pattern by 8. Three images are displayed. The original 64 by 8 then, the computed upscale using logarithmic bilinear interpolation, and then using the canvas standard API drawImage to upScale (Using the default interpolation, bilinear) .
// helper functions create canvas and get context
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
// iterators
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); };
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
const upScale = 8;
var canvas1 = CImageCtx(64,8);
var canvas2 = CImageCtx(canvas1.width * upScale, canvas1.height * upScale);
var canvas3 = CImageCtx(canvas1.width * upScale, canvas1.height * upScale);
// imgDat is a imageData object,
// x,y are floats in the original coordinates
// Returns the pixel colour at that point as an array of RGBA
// Will copy last pixel's colour
function getPixelValue(imgDat, x,y, result = []){
var i;
// clamp and floor coordinate
const ix1 = (x < 0 ? 0 : x >= imgDat.width ? imgDat.width - 1 : x)| 0;
const iy1 = (y < 0 ? 0 : y >= imgDat.height ? imgDat.height - 1 : y) | 0;
// get next pixel pos
const ix2 = ix1 === imgDat.width -1 ? ix1 : ix1 + 1;
const iy2 = iy1 === imgDat.height -1 ? iy1 : iy1 + 1;
// get interpolation position
const xpos = x % 1;
const ypos = y % 1;
// get pixel index
var i1 = (ix1 + iy1 * imgDat.width) * 4;
var i2 = (ix2 + iy1 * imgDat.width) * 4;
var i3 = (ix1 + iy2 * imgDat.width) * 4;
var i4 = (ix2 + iy2 * imgDat.width) * 4;
// to keep code short and readable get data alias
const d = imgDat.data;
// interpolate x for top and bottom pixels
for(i = 0; i < 3; i ++){
const c1 = (d[i2] * d[i2++] - d[i1] * d[i1]) * xpos + d[i1] * d[i1 ++];
const c2 = (d[i4] * d[i4++] - d[i3] * d[i3]) * xpos + d[i3] * d[i3 ++];
// now interpolate y
result[i] = Math.sqrt((c2 - c1) * ypos + c1);
}
// and alpha is not logarithmic
const c1 = (d[i2] - d[i1]) * xpos + d[i1];
const c2 = (d[i4] - d[i3]) * xpos + d[i3];
result[3] = (c2 - c1) * ypos + c1;
return result;
}
const ctx = canvas1.ctx;
var cols = ["black","red","green","Blue","Yellow","Cyan","Magenta","White"];
doFor(8,j => eachOf(cols,(col,i) => {ctx.fillStyle = col; ctx.fillRect(j*8+i,0,1,8)}));
eachOf(cols,(col,i) => {ctx.fillStyle = col; ctx.fillRect(i * 8,4,8,4)});
const imgData = ctx.getImageData(0, 0, canvas1.width, canvas1.height);
const imgData2 = ctx.createImageData(canvas1.width * upScale, canvas1.height * upScale);
const res = new Uint8ClampedArray(4);
for(var y = 0; y < imgData2.height; y++){
for(var x = 0; x < imgData2.width; x++){
getPixelValue(imgData,x / upScale, y / upScale, res);
imgData2.data.set(res,(x + y * imgData2.width) * 4);
}
}
canvas2.ctx.putImageData(imgData2,0,0);
function $(el,text){const e = document.createElement(el); e.textContent = text; document.body.appendChild(e)};
document.body.appendChild(canvas1);
$("div","Next Logarithmic upscale using linear interpolation * 8");
document.body.appendChild(canvas2);
canvas3.ctx.drawImage(canvas1,0,0,canvas3.width,canvas3.height);
document.body.appendChild(canvas3);
$("div","Previous Canvas 2D API upscale via default linear interpolation * 8");
$("div","Note the overall darker result and dark lines at hue boundaries");
canvas { border : 2px solid black; }

RGB to real-life light colour Javascript conversion

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);

Posting large Base64 strings to a server

I'm attempting to post large base64s (around 3500000 characters long) via ajax to a server side script that converts the base64 into an image. The issue is that sometimes the post times out with the server never receiving the base64. The timeout limit is currently set at 20 seconds, which I would expect is more than enough.
I don't really want to scale the image down any further as it is already at a lower resolution than I would like it to be (the images that are posted will be physically printed, so need to be reasonably high-res).
The potential solutions I can think of are:
Reduce the resolution of the images within the canvas
Reduce the resolution of the image created by the canvas
Reduce the colour range of the canvas
The last one is the one that interests me the most, as I have already implemented the other two as much as I feel comfortable doing, but I'm not sure how to go about it.
Any advice or solutions on how to go about this would be appreciated. Thanks.
• Reduce the colour range of the canvas
You can use canvas.getImageData and canvas.putImageData to do this.
Here is sample that first paints a canvas with a set of random colors
<canvas id="before" width="300" height="200"></canvas>
var bcanvas = document.getElementById('before')
var bctx = bcanvas.getContext("2d");
for (var i = 0; i < 300; i = i + 3) {
var r = parseInt(Math.random() * 256)
var g = parseInt(Math.random() * 256)
var b = parseInt(Math.random() * 256)
bctx.fillStyle = "rgba(" + r + ", " + g + ", " + b + ", 1)";
bctx.fillRect(i, 0, 3, 200);
}
alert(bcanvas.toDataURL().length);
And then we loop through the pixels, reducing the number of colors (here we just divide each pixel's r g and b values by 16, round it down and then scale it upto 255, ending up with ~16 distinct values for each of r g and b in place of 255 each)
var imgData = bctx.getImageData(0, 0, 300, 200);
var pixels = imgData.data;
// if a pixel is slightly red make it full red, same for blue and green
for (var nPixel = 0; nPixel < pixels.length; nPixel += 4) {
pixels[nPixel] = parseInt(pixels[nPixel] / 16) * 16;
pixels[nPixel + 1] = parseInt(pixels[nPixel + 1] / 16) * 16;
pixels[nPixel + 2] = parseInt(pixels[nPixel + 2] / 16) * 16;
}
var acanvas = document.getElementById('after')
var actx = acanvas.getContext("2d");
actx.putImageData(imgData, 0, 0);
alert(acanvas.toDataURL().length);
Resulting in an approximately 17% reduction in the DataURL length.
Note that this is just a "how to". You'll probably need to read up on the png image format and what kind of color optimization will have the benefit to figure out "how to" do it so that I reduce the image size.
Warning : alert boxes ahead. Do not panic.
var bcanvas = document.getElementById('before')
var bctx = bcanvas.getContext("2d");
for (var i = 0; i < 300; i = i + 3) {
var r = parseInt(Math.random() * 256)
var g = parseInt(Math.random() * 256)
var b = parseInt(Math.random() * 256)
bctx.fillStyle = "rgba(" + r + ", " + g + ", " + b + ", 1)";
bctx.fillRect(i, 0, 3, 200);
}
alert(bcanvas.toDataURL().length);
var imgData = bctx.getImageData(0, 0, 300, 200);
var pixels = imgData.data;
// if a pixel is slightly red make it full red, same for blue and green
for (var nPixel = 0; nPixel < pixels.length; nPixel += 4) {
pixels[nPixel] = parseInt(pixels[nPixel] / 16) * 16;
pixels[nPixel + 1] = parseInt(pixels[nPixel + 1] / 16) * 16;
pixels[nPixel + 2] = parseInt(pixels[nPixel + 2] / 16) * 16;
}
var acanvas = document.getElementById('after')
var actx = acanvas.getContext("2d");
actx.putImageData(imgData, 0, 0);
alert(acanvas.toDataURL().length);
<canvas id="before" width="300" height="200"></canvas>
<br />
<canvas id="after" width="300" height="200"></canvas>

How to generate random numbers biased towards one value in a range?

Say, if I wanted to generate an unbiased random number between min and max, I'd do:
var rand = function(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
But what if I want to generate a random number between min and max but more biased towards a value N between min and max to a degree D? It's best to illustrate it with a probability curve:
Here is one way:
Get a random number in the min-max range
Get a random normalized mix value
Mix random with bias based on random mix
Ie., in pseudo:
Variables:
min = 0
max = 100
bias = 67 (N)
influence = 1 (D) [0.0, 1.0]
Formula:
rnd = random() x (max - min) + min
mix = random() x influence
value = rnd x (1 - mix) + bias x mix
The mix factor can be reduced with a secondary factor to set how much it should influence (ie. mix * factor where factor is [0, 1]).
Demo
This will plot a biased random range. The upper band has 1 as influence, the bottom 0.75 influence. Bias is here set to be at 2/3 position in the range.
The bottom band is without (deliberate) bias for comparison.
var ctx = document.querySelector("canvas").getContext("2d");
ctx.fillStyle = "red"; ctx.fillRect(399,0,2,110); // draw bias target
ctx.fillStyle = "rgba(0,0,0,0.07)";
function getRndBias(min, max, bias, influence) {
var rnd = Math.random() * (max - min) + min, // random in range
mix = Math.random() * influence; // random mixer
return rnd * (1 - mix) + bias * mix; // mix full range and bias
}
// plot biased result
(function loop() {
for(var i = 0; i < 5; i++) { // just sub-frames (speedier plot)
ctx.fillRect( getRndBias(0, 600, 400, 1.00), 4, 2, 50);
ctx.fillRect( getRndBias(0, 600, 400, 0.75), 55, 2, 50);
ctx.fillRect( Math.random() * 600 ,115, 2, 35);
}
requestAnimationFrame(loop);
})();
<canvas width=600></canvas>
Fun: use the image as the density function. Sample random pixels until you get a black one, then take the x co-ordinate.
Code:
getPixels = require("get-pixels"); // npm install get-pixels
getPixels("distribution.png", function(err, pixels) {
var height, r, s, width, x, y;
if (err) {
return;
}
width = pixels.shape[0];
height = pixels.shape[1];
while (pixels.get(x, y, 0) !== 0) {
r = Math.random();
s = Math.random();
x = Math.floor(r * width);
y = Math.floor(s * height);
}
return console.log(r);
});
Example output:
0.7892316638026386
0.8595335511490703
0.5459279934875667
0.9044852438382804
0.35129814594984055
0.5352215224411339
0.8271261665504426
0.4871773284394294
0.8202084102667868
0.39301465335302055
Scale to taste.
Just for fun, here's a version that relies on the Gaussian function, as mentioned in SpiderPig's comment to your question. The Gaussian function is applied to a random number between 1 and 100, where the height of the bell indicates how close the final value will be to N. I interpreted the degree D to mean how likely the final value is to be close to N, and so D corresponds to the width of the bell - the smaller D is, the less likely is the bias. Clearly, the example could be further calibrated.
(I copied Ken Fyrstenberg's canvas method to demonstrate the function.)
function randBias(min, max, N, D) {
var a = 1,
b = 50,
c = D;
var influence = Math.floor(Math.random() * (101)),
x = Math.floor(Math.random() * (max - min + 1)) + min;
return x > N
? x + Math.floor(gauss(influence) * (N - x))
: x - Math.floor(gauss(influence) * (x - N));
function gauss(x) {
return a * Math.exp(-(x - b) * (x - b) / (2 * c * c));
}
}
var ctx = document.querySelector("canvas").getContext("2d");
ctx.fillStyle = "red";
ctx.fillRect(399, 0, 2, 110);
ctx.fillStyle = "rgba(0,0,0,0.07)";
(function loop() {
for (var i = 0; i < 5; i++) {
ctx.fillRect(randBias(0, 600, 400, 50), 4, 2, 50);
ctx.fillRect(randBias(0, 600, 400, 10), 55, 2, 50);
ctx.fillRect(Math.random() * 600, 115, 2, 35);
}
requestAnimationFrame(loop);
})();
<canvas width=600></canvas>
Say when you use Math.floor(Math.random() * (max - min + 1)) + min;, you are actually creating a Uniform distribution. To get the data distribution in your chart, what you need is a distribution with non-zero skewness.
There are different techniques to get those kinds of distributions. Here is an example of beta distribution found on stackoverflow.
Here is the example summarized from the link:
unif = Math.random() // The original uniform distribution.
And we can transfer it into beta distribution by doing
beta = sin(unif*pi/2)^2 // The standard beta distribution
To get the skewness shown in your chart,
beta_right = (beta > 0.5) ? 2*beta-1 : 2*(1-beta)-1;
You can change the value 1 to any else to have it skew to other value.

Categories