What's the best way to scale alpha values in a canvas?
The first problem I'm trying to solve is drawing a sprite that has intrinsic low alpha values. I want to draw it 3-4 times brighter than it really is. Currently I'm just drawing it 4 times in the same spot. (I cannot edit the image file and globalAlpha doesn't go above 1)
The second problem I'm trying to solve is drawing the boundary of multiple overlapping sprites. The sprites are circular but with squiggles. I figured I'd use this method combined with globalCompositeOperation = 'destination-out', but for that I need to maximize the alpha values for the second drawing.
As an option to markE's answer - you can simply scale the alpha channel directly.
I would only recommend this approach as a part of a pre-processing stage and not for use every time you need to use a sprite as iterating the buffer this way is a relatively slow process.
LIVE DEMO HERE
Assuming you already have the sprite in a canvas and know its position:
/// get the image data and cache its pixel buffer and length
var imageData = context.getImageData(x, y, width, height);
var data = imageData.data;
var length = data.length;
var i = 0;
var scale = 4; /// scale values 4 times. This may be a fractional value
/// scale only alpha channel
for(; i < length; i += 4) {
data[i + 3] *= scale;
}
context.putImageData(imageData, x, y);
The good thing with the Uint8ClampedArray which the canvas is using clamps and rounds the values for you so you do not need to check lower or upper bounds, nor convert the value to integer - the internal code do all this for you.
You can "brighten" an rgba color by flattening it to rgb and then increasing the rgb component values.
Convert the rgba value to rgb, also taking the background color into effect.
Increase the resulting red,green,blue values by a percentage to "brighten" the color.
Here's a function to do that (disclaimer: untested code here!):
function brighten(RGBA,bg,pct){
// convert rgba to rgb
alpha = 1 - RGBA.alpha/255;
red = Math.round((RGBA.alpha*(RGBA.red/255)+(alpha*(bg.red/255)))*255);
green = Math.round((RGBA.alpha*(RGBA.green/255)+(alpha*(bg.green/255)))*255);
blue = Math.round((RGBA.alpha*(RGBA.blue/255)+(alpha*(bg.blue/255)))*255);
// brighten the flattened rgb by a percentage (100 will leave the rgb unaltered)
redBright=parseInt( Math.min(255,red*pct/100) );
greenBright=parseInt( Math.min(255,green*pct/100) );
blueBright=parseInt( Math.min(255,blue*pct/100) );
return({red:redBright,green:greenBright,blue:blueBright});
}
Related
I'm trying to make it appear as though movement on my <canvas> creates motion trails. In order to do this, instead of clearing the canvas between frames I reduce the opacity of the existing content by replacing a clearRect call with something like this:
// Redraw the canvas's contents at lower opacity. The 'copy' blend
// mode keeps only the new content, discarding what was previously
// there. That way we don't have to use a second canvas when copying
// data
ctx.globalCompositeOperation = 'copy';
ctx.globalAlpha = 0.98;
ctx.drawImage(canvas, 0, 0);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
However, since setting globalAlpha multiplies alpha values, the alpha values of the trail can approach zero but will never actually reach it. This means that graphics never quite fade, leaving traces like these on the canvas that do not fade even after thousands of frames have passed over several minutes:
To combat this, I've been subtracting alpha values pixel-by-pixel instead of using globalAlpha. Subtraction guarantees that the pixel opacity will reach zero.
// Reduce opacity of each pixel in canvas
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// Iterates, hitting only the alpha values of each pixel.
for (let i = 3; i < data.length; i += 4) {
// Use 0 if the result of subtraction would be less than zero.
data[i] = Math.max(data[i] - (0.02 * 255), 0);
}
ctx.putImageData(imageData, 0, 0);
This fixes the problem, but it's extremely slow since I'm manually changing each pixel value and then using the expensive putImageData() method.
Is there a more performant way to subtract, rather than multiplying, the opacity of pixels being drawn on the canvas?
Unfortunately there is nothing we can do about it except from manually iterating over the pixels to clear low-value alpha pixels like you do already.
The problem is related to integer math and rounding (more details at this link, from the answer).
There are blending modes such as "luminosity" (and to a certain degree "multiply") which can be used to subtract luma, the problem is it works on the entire surface contrary to composite modes which only works on alpha - there is no equivalent in composite operations. So this won't help here.
There is also a new luma mask via CSS but the problem is that the image source (which in theory could've been manipulated using for example contrast) has to be updated every frame and basically, the performance would be very bad.
Workaround
One workaround is to use "particles". That is, instead of using a feedback-loop instead log and store the path points, then redraw all logged points every frame. Using a max value and reusing that to set alpha can work fine in many cases.
This simple example is just a proof-of-concept and can be implemented in various ways in regards to perhaps pre-populated arrays, order of drawing, alpha value calculations and so forth. But I think you'll get the idea.
var ctx = c.getContext("2d");
var cx = c.width>>1, cy = c.height>>1, r = c.width>>2, o=c.width>>3;
var particles = [], max = 50;
ctx.fillStyle = "#fff";
(function anim(t) {
var d = t * 0.002, x = cx + r * Math.cos(d), y = cy + r * Math.sin(d);
// store point and trim array when reached max
particles.push({x: x, y: y});
if (particles.length > max) particles.shift();
// clear frame as usual
ctx.clearRect(0,0,c.width,c.height);
// redraw all particles at a log. alpha, except last which is drawn full
for(var i = 0, p, a; p = particles[i++];) {
a = i / max * 0.6;
ctx.globalAlpha = i === max ? 1 : a*a*a;
ctx.fillRect(p.x-o, p.y-o, r, r); // or image etc.
}
requestAnimationFrame(anim);
})();
body {background:#037}
<canvas id=c width=400 height=400></canvas>
I am retrieving pixels in a canvas imagedata and I'm doing that a lot.
I think the inserting and retrieving from and to the canvas imagedata is expensive in cpu time, so I want to make as few of those as possible.
One way of cutting that would be to make a single insert that would insert multiple pixels in a single sequence, but so far I have not been able to see how that would be done. All the examples I have seen so far retrieve and insert only a single pixel.
So the question is,
in order to speed up canvas imagedata pixel manipulation, how do I insert/retrieve multiple pixels simultaneously?
Just select a larger region when retrieving a pixel buffer:
var imageData = ctx.getImageData(x, y, width, height);
^^^^^^^^^^^^ not limited to one
Now your data buffer will contain all pixels for the given region. To get the whole canvas:
var imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
Adjust them and put back into the same position:
ctx.putImageData(imageData, x, y);
and you're done.
Remember that each pixel consists of four bytes (RGBA). To address a larger buffer you can do:
function getPixelIndex(x, y) {
return (y * width + x) * 4; // width used when getting buffer
}
Tips:
if you plan to update the same buffer often simply retrieve the buffer once and store a pointer to it, update it when you need and put back, then reuse the same buffer. This way you save the time getting the buffer. This won´t work if you in the mean time apply graphics to the canvas with standard methods.
You can also start with an empty buffer using createImageData() instead of getImageData().
If your pixel color data is more or less static you can update the buffer using a Uint32Array instead of the Uint8ClampedArray. You get a 32-bit version like this after getting the imageData:
var buffer32 = new Uint32Array(imageData.data.buffer);
Your new buffer32 will point to the same underlying byte buffer so no significant memory overhead, but it allows you to read and write 32-bit values instead of just 8-bit. Just be aware of that the byte order is (typically) little-endian so order the bytes as ABGR. Then do as before, call ctx.putImageData(imageData, x, y); when you need to update.
I'm drawing a graph on a html 5 canvas tag from a array with numbers like
arr = [6,3,16,6,53,1,3,54,67,6,3,21,6,49,7,8,31,66,51,32,56,49,4,78,9,65,43,1,3,54,67,6,3];
These numbers will be the height of the rectangle that is drawn on the canvas and it will be filled white with a transparent background;
for (var i = 0; i < arr.length; i += 1) {
ctx.fillStyle = "#ffffff";
// Fill rectangle with gradient
ctx.fillRect(
arr[i] * 10,
c_height - arr[i],
8,
arr[i]
);
}
Users can hover these rectangles and then see some more data.
I can make them change color but if there are to many rectangles the site laggs a little bit, so my question is if it is possible to make some kind of big horizontal rectangle that will mask(white rectangles) without filling the transparent background?
1) You can define the array as a typed array instead:
var arr = new Uint8Array([6,3,16,6,53,1,3,...,3]);
Just make sure the type (here unsigned 8-bit) fits the values. If you have higher values than 255 then use a 16-bit, or 32-bit, if floating point use Float32Array and so on.
2) If the color is the same don't set the fill style inside the loop. fillStyle is rather expensive as it has to parse the string and convert it to the color it defines.
3) use path to add the rectangle to, defining and filling each time is slower than to define all rects, then fill all at the same time outside the loop.
4) use a smarter for-loop by using the array entry as a conditional statement as well. Not only is this faster in itself but by storing the array entry to a value will be faster too as JS does not have to look up an array entry every time you use arr[i]:
ctx.fillStyle = "#ffffff"; // set fill style outside loop
ctx.beginPath(); // make sure we use a clean path
for (var i = 0, a; a = arr[i]; i++) { // fetch item and use as cond. for loop
ctx.rect(a * 10, c_height - a, 8, a); // add rect to path, but not fill yet
}
ctx.fill(); // fill all rects with fillstyle
Hope this helps!
I want to do some manual anti-aliasing on some text on a canvas. I know how to iterate over the image / colour data, but not exactly how to anti-alias.
From googling around a bit it seems like to do anti-aliasing I need to have an original image which I use as my sample, then pass over the colour data, then for each pixel take an average of the surrounding pixels, then copy this new value into the data for my anti-aliased image.
The bit I'm not sure about is exactly how to 'take an average' of the surrounding pixels.
I have done a jsFiddle to demostrate what I have done so far. As you will see I am copying the image data from the original canvas, making it negative, then putting it into the second canvas.
If I am being more specific in what I am struggling with, it is how exactly do you figure out what the surrounding pixels are in a loop which only has one iterator? And also is the average just a case of adding the nearest pixels colour vals to the current pixels vals, then dividing by the number of pixels?
This is the loop in which I wish to manipulate the data:
var imgData = originalContext.getImageData(0, 0, width, height);
var aliasedData = originalContext.createImageData(width, height);
aliasedData.data.set(imgData.data);
for (var i = 0; i < imgData.data.length; i += 4) {
// just make the data negative to show something is happening
aliasedData.data[i] = 255 - imgData.data[i];
aliasedData.data[i + 1] = 255 - imgData.data[i + 1];
aliasedData.data[i + 2] = 255 - imgData.data[i + 2];
// need to get an average of surrounding pixels here
}
aliasedContext.putImageData(aliasedData, 0, 0);
Can a JavaScript canvas be manipuled like a standard bitmap (accessing/modifying a pixel and getting it's size)? Is this use optimized, or would it be faster to manipulate normal 2d arrays of pixels and draw over canvas when you need it?
Absolutely yes! Please have a look here:
https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Canvas_tutorial/Pixel_manipulation_with_canvas
As for your second question, as per the pixels documentation, pixels is a one dimensional array. You'll need to do your own 2 x 2 hoopla for a 2d way.
Taken from a previous SO answer by I82Much (works dandy for me):
int row = i;
int col = j;
int offset = row * width + col;
color p = pixels[offset];
More here: how to loop over the pixels using 2D array?
and here: http://www.processing.org/reference/pixels.html
You can get an array of pixel data from the canvas context using getImageData(). Bear in mind that each pixel takes-up 4 spaces in the array (for red, green, blue and alpha). Then, once you've altered the array to your liking, you can use putImageData() to put the data back.