Save canvas in grayscale - html

I was wondering if it is possible to have a coloured canvas in HTML5 and then save it as a black and white (grayscale) PNG, while you click on the save button. I managed to save the canvas as a PNG already, which is great! But I'd like to add the grayscale feature without having to change it myself with a program as Illustrator etc.
The canvas has moving particles btw. (I'm not sure if that has any effect though)
Thanks~!
function download(){
var dt = c.toDataURL('image/png');
this.href = dt;
};
downloadlink.addEventListener('click', download, false);

It's possible by extracting the RGB data from the canvas and calculate their luma values.
There are a couple of requisites that needs to be fulfilled though, like CORS (which does not seem to be a problem in this case as you can already save out an image) and if you wish to keep the original data after save you can either copy the current image on canvas to a temporary one, or keep a backup of the pixel data that needs to be extracted.
RGB to Luma
The formula for converting the RGB data into a luma value (grey) is as follows based on the REC 709 (or BT.709) formula:
luma = Red x 0.2126 + Green x 0.7152 + Blue x 0.0722
Alternatively you could use the REC 601 (BT.601) formula instead (which is more common for SD video footage):
luma = Red x 0.299 + Green x 0.587 + Blue x 0.114
To use this, simply iterate over all pixels, obtain the luma value using one of these formulas and replace all channels in target with the resulting luma value.
Temporary data
For temporary data we can do either (when save button is clicked):
Temporary canvas
Create a temporary canvas
Set the size equal to source canvas
Draw in source canvas using drawImage()
Extract target canvas' pixel data using getImageData()
Iterate over the pixels, convert using the formula, put data back
Save out image then discard temporary canvas
or
ImageData backup
Extract image data using getImageData() - this will act as our backup
Create new ImageData the same size using createImageData()
Iterate over the pixel from the backup, but put the result in luma into the created ImageData
Put back the created ImageData, save out image
Put back the backup data
If the latter step is necessary depends really on how you update the canvas. If you are animating a loop there may not be need to put back the backup (or keep one) if everything gets updated anyways, but if so and you see flash of grey or some areas left grey, then this step would be needed.
Example using the ImageData approach
var ctx = c.getContext("2d"), img = new Image;
img.onload = setup; img.crossOrigin = "";
img.src = "//i.imgur.com/OrYVGI8.jpg";
function setup() {
c.width = this.naturalWidth; c.height = this.naturalHeight;
ctx.drawImage(this, 0, 0); btn.disabled = false
}
// Main code for demo
btn.onclick = function() {
var idataSrc = ctx.getImageData(0, 0, c.width, c.height), // original
idataTrg = ctx.createImageData(c.width, c.height), // empty data
dataSrc = idataSrc.data, // reference the data itself
dataTrg = idataTrg.data,
len = dataSrc.length, i = 0, luma;
// convert by iterating over each pixel each representing RGBA
for(; i < len; i += 4) {
// calculate luma, here using Rec 709
luma = dataSrc[i] * 0.2126 + dataSrc[i+1] * 0.7152 + dataSrc[i+2] * 0.0722;
// update target's RGB using the same luma value for all channels
dataTrg[i] = dataTrg[i+1] = dataTrg[i+2] = luma;
dataTrg[i+3] = dataSrc[i+3]; // copy alpha
}
// put back luma data so we can save it as image
ctx.putImageData(idataTrg, 0, 0);
demo.src = c.toDataURL(); // set demo result's src url
// restore backup data
ctx.putImageData(idataSrc, 0, 0);
};
<button disabled id=btn>SAVE TO GREY</button> (click then scroll down to see result)<br>
<canvas id=c></canvas><br><img id=demo>

Or you could use ctx.globalCompositeOperation = 'luminosity' before drawing your image on a white filled canvas, not as fun but same result (:

I suggest calculating luma like this:
var luma = (11 * obj.data[i] + 16 * obj.data[i + 1] + 5 * obj.data[i + 2]) >> 5;

Related

Forge function generateTexture()

In the following example, there is a function called generateTexture().
Is it possible to draw text (numbers) into the pixel array? Or is it possible to draw text (numbers) on top of that shader?
Our goal is to draw a circle with a number inside of it.
https://forge.autodesk.com/blog/using-dynamic-texture-inside-custom-shaders
UPDATE:
We noticed that each circle can't use a unique generateTexture(). The generateTexture() result is used by every single one of them. The only thing that can be customized per object is the color, plus what texture is used.
We could create a workaround for this, which is to generate every texture from 0 to 99, and to then have each object choose the correct texture based on the number we want to display. We don't know if this will be efficient enough to work properly though. Otherwise, it might have to be 0 to 9+ or something in that direction. Any guides on our updated question would be really appreciated. Thanks.
I am able to successfully display text with the following code, simply replace generateTexture() by generateCanvasTexture() in the sample and you should get the result below:
const generateCanvasTexture = () => {
const canvas = document.createElement("canvas")
const ctx = canvas.getContext('2d')
ctx.font = '20pt Arial'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(new Date().toLocaleString(),
canvas.width / 2, canvas.height / 2)
const canvasTexture = new THREE.Texture(canvas)
canvasTexture.needsUpdate = true
canvasTexture.flipX = false
canvasTexture.flipY = false
return canvasTexture
}
It is possible but you would need to implement it yourself. Shaders are a pretty low level feature so there is no way to directly draw a number or a text, but you can convert a given character into its representation as a 2d pixel array.

How to detect that the AlphaMaskFilter is completely gone in easeljs/createjs

I am doing a little scratch/reveal game based on the AlphaMaskFilter example:
http://createjs.com/demos/easeljs/AlphaMaskReveal.html
I want to detect that the the mask is completely gone, or use a threshold (90% scratched for example).
I read the doc on AlphaMaskFilter, shape and graphics objects and im not really sure how to achieve this.
Im not even sure i Have acess to the pixel information and check the alpha channel to detect it, but even so, I wonder if I will performance issue.
any help is welcome, thanks.
**** EDIT **** ADD TO THE ACCEPTED ANSWER ****
So, I was able to have the pct of transparency using the AlphaMapFilter (thanks Lanny).
The AlphaMapFilter offer you a mapping to the alpha channel of all the pixels.
Here is a little sample code that worked for me:
// mShapeToScratch is a createjs Shape. like in the http://createjs.com/demos/easeljs/AlphaMaskReveal.html example
var alphaMaskFilter = new createjs.AlphaMapFilter(mShapeToScratch.cacheCanvas);
var canvas = alphaMaskFilter.alphaMap;
var ctx = canvas.getContext("2d");
var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var alphaData = imgData.data;
var pixelTotal = rect.h*rect.w;
var transparentPixel = 0;
// rect.h is the height of the image and rect.w is the width not shown in the example
for (var y = 0; y < rect.h; ++y)
{
for (var x=0; x < rect.w; ++x)
{
var pixelIdx = (y*rect.w + x);
if(alphaData[pixelIdx] > 128) // transparent will be 255.
{
transparentPixel++;
}
}
console.log("transparent % = " + transparentPixel/pixelTotal);
This example checks all the pixels, but it's pretty easy to check one every X pixels to speeds up checks as Lanny suggested.
The alpha mask uses canvas composite operation, and not pixel access, so without some completely custom approach, there isn't a great way to do this.
Iterating pixels (check out AlphaMapFilter as an example) would work - but could be fairly slow. Maybe checking every 4th, 10th, or 25th pixel would speed it up.
Cheers.

HTML5: Inverse text-color on canvas

I want to draw text on a canvas in the inverse color of the background (to make sure the text is readible no matter the background color). I believe in oldskool bitblt-ing, this was an XOR operation.
How to do this?
Update: most of the newer browsers now support the blending mode "difference" which can achieve the same result.
context.globalCompositeOperation = "difference";
Updated demo.
Old answer:
One should think that the XOR mode for composition would do this, but unfortunately canvas' XOR only XORs the alpha bits.
By applying the following code we can however receive a result such as this:
You can make an extension to the canvas like this:
CanvasRenderingContext2D.prototype.fillInversedText =
function(txt, x, y) {
//code - see below
}
Now you can call it on the context as the normal fillText, but with a slight change:
ctx.fillInversedText(txt, x, y);
For this to work we do the following first - measure text. Currently we can only calculate width of text and then assume the height. This may or may not work well as fonts can be very tall and so forth. Luckily this will change in the future, but for now:
var tw = this.measureText(txt).width;
var th = parseInt(ctx.font, '10');
th = (th === 0) ? 10 : th; //assume default if no font and size is set
Next thing we need to do is to setup an off-screen canvas to draw the text we want ot invert:
var co = document.createElement('canvas');
co.width = tw;
co.height = th;
Then draw the actual text. Color does not matter as we are only interested in the alpha channel for this canvas:
var octx = co.getContext('2d');
octx.font = this.font;
octx.textBaseline = 'top';
octx.fillText(txt, 0, 0);
Then we extract the pixel buffers for the area we want to draw the inverted text as well as all the pixels for the off-screen canvas which now contains our text:
var ddata = this.getImageData(x, y, tw, th);
var sdata = octx.getImageData(0, 0, tw, th);
var dd = ddata.data; //cache for increased speed
var ds = sdata.data;
var len = ds.length;
And then we invert each pixel where alpha channel for pixel is greater than 0.
for (var i = 0; i < len; i += 4) {
if (ds[i + 3] > 0) {
dd[i] = 255 - dd[i];
dd[i + 1] = 255 - dd[i + 1];
dd[i + 2] = 255 - dd[i + 2];
}
}
Finally put back the inverted image:
this.putImageData(ddata, x, y);
This may seem as a lot of operations, but it goes pretty fast.
Demo (warning if you are sensitive to flicker)
(the psychedelic background is just to have some variations as fiddle needs external images and most are prevented by CORS when we use pixel manipulation).
I've removed my old answer, as it did not solve the question. As of recently, there are new globalCompositeOperations that do all kinds of great things. I've created an example that shows how to obtain inverted text. In case that link breaks, the method is essentially this:
ctx.globalCompositeOperation = "difference";
ctx.fillStyle = "white";
//draw inverted things here
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation

Insert a single pixel into HTML5 canvas

I want to insert a pixel with a color, and I use this code:
context.fillStyle='RGB('+s[i]+')';
context.fillRect(i,y,1,1)
Is there a shorter way to do it? e.g. in a single line of code?
My main goal is to reduce the amount of code.
There really isn't a shorter way to do it besides the method you used above. You don't have to include a fillStyle every time so it essentially is only one line of code to fill a pixel.
Like Petteri pointed out there is another way to fill pixels, it involves manipulating the pixel data directly.
Live Demo
var canvasData = ctx.getImageData(0,0,canvasWidth,canvasHeight);
//color 100,100 red
canvasData.data[((100*(canvasData.width*4)) + (100*4)) + 0] = 255;
ctx.putImageData(canvasData,0,0);
also note with the above method you would need to repeat that line 3 times once for each component of the color. For example to set red, green, blue, and the alpha you would use
canvasData.data[((100*(canvasData.width*4)) + (100*4)) + 0] //red
canvasData.data[((100*(canvasData.width*4)) + (100*4)) + 1] //green
canvasData.data[((100*(canvasData.width*4)) + (100*4)) + 2] //blue
canvasData.data[((100*(canvasData.width*4)) + (100*4)) + 3] //alpha
granted you could have your data in an array, and just loop through that and color as needed.
You can edit the image data of the canvas directly. Here is a good example how to do it: http://beej.us/blog/2010/02/html5s-canvas-part-ii-pixel-manipulation/
No, there is no single line of code way to change a single pixel to one color. Well, there sort-of is.
As Petteri noted, there is a way to change each pixel directly, which will probably accomplish what you want. I assume what you want is to change one pixel to one color, and the next pixel to another color, etc.
For instance here is a function for desaturating a canvas. What it does is takes every pixel and averages the RGB values to be color-neutral (have no saturation). The result is a grayscale image.
function grayscale() {
var imageData = ctx.getImageData(0,0,can.width, can.height);
var pixels = imageData.data;
var numPixels = pixels.length;
ctx.clearRect(0, 0, can.width, can.height);
for (var i = 0; i < numPixels; i++) {
var average = (pixels[i*4] + pixels[i*4+1] + pixels[i*4+2]) /3;
// set red green and blue pixels to the average value
pixels[i*4] = average;
pixels[i*4+1] = average;
pixels[i*4+2] = average;
}
ctx.putImageData(imageData, 0, 0);
}
As you can see it is iterating over each pixel. It could be easily mofied to have each pixel changed as a one-liner.
Instead of:
pixels[i*4] = average;
pixels[i*4+1] = average;
pixels[i*4+2] = average;
You'd write:
// Take out 3 values starting at i*4 and add the new RGB for that pixel
pixels.splice(i*4,3,REDVALUE,GREENVALUE,BLUEVALUE);
Which would accomplish what you'd want. It's not the most efficient way under the sun, but it would accomplish your goal :)
fillStyle="rgb("+a[m];
fillRect(m,o,1,1);
Someone did with that :P

Bitmap conversion - Creating a transparent + black image from a B&W source

I have a whole bunch of jpg files that I need to use in a project, that for one reason or another cannot be altered. Each file is similar (handwriting), black pen on white BG. However I need to use these assets against a non-white background in my flash project, so I'm trying to do some client-side processing to get rid of the backgrounds using getPixel and setPixel32.
The code I am currently using currently uses a linear comparison, and while it works, the results are less than expected, as the shades of grey are getting lost in the mix. Moreso than just tweaking my parameters to get things looking proper, I get the feeling that my method for computing the RGBa value is weak.
Can anyone recommend a better solution than what I'm using below? Much appreciated!
private function transparify(data:BitmapData) : Bitmap {
// Create a new BitmapData with transparency to return
var newData:BitmapData = new BitmapData(data.width, data.height, true);
var orig_color:uint;
var alpha:Number;
var percent:Number;
// Iterate through each pixel using nested for loop
for(var x:int = 0; x < data.width; x++){
for (var y:int = 0; y < data.height; y++){
orig_color = data.getPixel(x,y);
// percent is the opacity percentage, white should be 0,
// black would be 1, greys somewhere in the middle
percent = (0xFFFFFF - orig_color)/0xFFFFFF;
// To get the alpha value, I multiply 256 possible values by
// my percentage, which gets multiplied by 0xFFFFFF to fit in the right
// value for the alpha channel
alpha = Math.round(( percent )*256)*0xFFFFFF;
// Adding the alpha value to the original color should give me the same
// color with an alpha channel added
var newCol = orig_color+alpha;
newData.setPixel32(x,y,newCol);
}
}
var newImg:Bitmap = new Bitmap(newData);
return newImg;
}
Since it's a white background, blendMode may give you a better result.