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>
Related
I'm trying to make simple pendulum in HTML5 Canvas but I'm stuck. I want to swing it for 25 degrees to the left and to the right, so I calculated I should translate every frame about -3.5 px in y axis (and 3.5 px when swings to the right). I'm using below code
var rotation = Math.PI/180, //rotate about 1deg
translation = -3.5,
counter = 0; //count rotations
function draw() {
var element = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
ctx.clearRect(0,0,element.width,element.height);
ctx.translate(0, translation);
ctx.rotate(rotation);
//function draws all objects
objects(element,ctx);
if (counter == 25) {
rotation *= -1;
translation *= -1;
counter = -25;
}
counter += 1;
window.requestAnimationFrame(draw);
}
Everything looks good but when pendulum is changing direction then everything is translating in also x axis and after few seconds disappears from screen.. What is wrong in this code? Or maybe I was miss something in my calculations? My code here https://jsfiddle.net/qskxjzv9/2/
Thanks in advance for your answers.
The problem is that when there is rotation involved, then translation, the x and y's will be translated in a different direction than what may seem logic.
To get around this we don't actually have to involve translation more than using it for placing pivot (point of rotation) and then use absolute rotation based on a different way of calculating the pendulum movement.
For example, this will take care of both the translation problem as well as smoothing the pendulum movement:
Change the draw method to draw the pendulum with origin (0,0) - it's just a matter of changing the initial coordinates so they evolve around (0,0)
Translate to pivot point of screen - this is where the rotation will take place.
Rotate using sin() as a factor - this will create a smooth animation and look more like a pendulum and it will restrict the movement to angle as range is [-1,1]
Use counter to move sin() instead - this acts as a frequency-ish factor (you can later convert this into an actual frequency to say, have the pendulum move n number of times per minute etc.). To keep it simple I have just used the existing counter variable and reduced its step value.
The main code then:
var maxRot = 25 / 180 * Math.PI, // max 25° in both directions
counter = 0,
// these are better off outside loop
element = document.getElementById('canvas');
ctx = element.getContext('2d');
function draw() {
// reset transform using absolute transformation. Include x translation:
ctx.setTransform(1,0,0,1,element.width*0.5,0);
// clear screen, compensate for initial translate
ctx.clearRect(-element.width*0.5,0,element.width,element.height);
// rotate using sin() with max angle
ctx.rotate(Math.sin(counter) * maxRot);
// draw at new orientation which now is pivot point
objects(element, ctx);
// move sin() using "frequency"-ish value
counter += 0.05;
window.requestAnimationFrame(draw);
}
Fiddle
Additional
Thanks to #Blindman67 for providing additional improvements:
To control frequency in terms of oscillations you could do some minor changes - first define frequency:
var FREQUENCY = 3;
Define a function that will do the conversion:
function sint(time) {
return Math.sin(FREQUENCY * time * Math.PI * 0.002); // 0.002 allow time in ms
}
If you now change the draw() method to take a time parameter instead of the counter:
function draw(time) {
...
}
Then you can call rotation like this:
ctx.rotate(sint(time) * maxRot);
you need to translate the origin to the point you want to rotate around:
ctx.translate(element.width / 2, 0);
Then, the rotation as you suggest:
ctx.rotate(rotation);
And finally, translate back:
ctx.translate(- element.width / 2, 0);
See this commented fork of your fiddle.
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!
So i have done an webpage that tests rendering times times for SVG and Canvas. Test were done for diffrent browsers. I tought that Canvas will be better then SVG but from my test i see that Canvas has a problem with bigger objects and circle objects. I have results of my test here:
http://lezag2.linuxpl.info/wykresT2.html - this are reslts for 50.000 rectangles with a=500 pixsels
http://lezag2.linuxpl.info/wykresT4.html - this are results for 50.000 circles with r=250 pixels
I use simple code to generate objects on page.
Canvas:
var ctx = document.getElementById('canvas').getContext('2d');
for(var i=0;i<50000;i++){
ctx.beginPath();
var centreX = Math.random() * 1000;
var centreY = Math.random() * 1000;
var radius = 50;
var startAngle = 0 * Math.PI/180;
var endAngle = 360 * Math.PI/180;
ctx.arc(centreX,centreY,radius,startAngle,endAngle,false);
ctx.fillStyle = "rgb("+Math.floor(Math.random()*256)+","+ Math.floor(Math.random()*256)+","+ Math.floor(Math.random()*256)+")";;
ctx.fill();
ctx.closePath();
}
And SVG:
for (var i = 0; i < 50000; i++) {
var x = Math.random() * 1000,
y = Math.random() * 1000;
var circ = document.createElementNS(svgns, 'circle');
circ.setAttributeNS(null, 'cx', x);
circ.setAttributeNS(null, 'cy', y);
circ.setAttributeNS(null, 'r', 50);
circ.setAttributeNS(null, 'fill', '#'+Math.round(0xffffff * Math.random()).toString(16));
document.getElementById('svgOne').appendChild(circ);
}
I am wondering why Canvas has such bad times compared to SVG. I tryd to google my problem but found only very comlpex tests. Could some one explain me why canvas has such bad times?? Also do i name it good - i mean rendering times??
EDIT
I forgot to show how i mesure rendering time.
befTime = (new Date()).getTime();
{
(drawing function)
}
var actTime = (new Date()).getTime();
var testTime = (actTime-befTime)/1000;
Thats why i asked if i dont name it wrong by saying "rendering time"
You cannot infer from your tests that svg is faster or slower than canvas, for several reasons :
minor reasons :
closePath is not necessary, especially after filling.
you recompute start/end angle on each loop for canvas.
fillStyle has to be converted 'rgb(...)' in canvas and not in svg.
more important reason :
drawing 50.000 canvas of 500 radius on a 1000X1000 canvas leads to a per-pixel overdraw
of... 78500 !!! This has just NOTHING to do with a real-use case, which is very annoying
to get any conclusion out of it.
even more important reason :
you do not draw in synch (using requestAnimationFrame), so it is certain that your canvas code is often waiting for its buffer to be able to draw.
killing reason :
You do not measure the render time of svg, just the svg creation + add to DOM time.
Not a single pixel is drawn in your svg loop : the actual render will be performed AFTER the javascript code returns : only then it will see that a reflow is necessary, and repaint everything.
AFAIK every browsers have only one thread for all operations going on a page : so your program will fully stop for rendering, during a time that you do not measure as of now.
Rq : You could try to measure svg draw time by using a short setInterval and seeing how many times really elapsed in between two calls instead of the real interval : this is the time when the system was stuck rendering.
Bottom line : you are comparing the time to create DOM objects and add them to the document vs the time to render circles on a out-of-sync canvas. No conclusion can be drawn out of those figures.
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});
}
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);