I stubled upon a weird problem. The following code results in making the image fade away because it's overdrawn by a semi-opaque rect over and over again.
But at least at the 10th iteration of draw(); the image should be completely overdrawn, because the rect should be fully opaque by then, right? But it actually never disappears completely.
This effect is worse on Chrome than it is on Firefox. But beware: bad screens may hide this faulty behaviour =)
I also made a demo on jsFiddle.
$(function () {
var canvas = $("#mycanvas"),
ctx = canvas[0].getContext("2d"),
imgUrl = "http://it-runde.de/dateien/2009/august/14/25.png";
var image = new Image();
image.src = imgUrl ;
$(image).load(function() {
ctx.drawImage(image, 0, 0, canvas.width(), canvas.height());
draw();
});
function draw() {
ctx.fillStyle = "rgba(255, 255, 255, 0.1)";
ctx.fillRect(0, 0, canvas.width(), canvas.height());
setTimeout(draw, 100);
}
});
The effect one may want to achieve is that, say an object is moving all over the canvas, and the already drawn positions get overdrawn only slightly so after-glow of after-fade effect. But this result is just fugly.
So is there any solution to this?
I know this is old but I don't think the previously accepted answer is correct. I think this is happening as a result of pixel values being truncated from float to byte. In Windows 7 running Chrome version 39.0.2171.95m, after running your fiddle for a while, the image is still visible but only lightly, and doesn't appear to be changing any more. If I take a screenshot I see the following pixel values on the image:
(246, 246, 246)
When you draw a rectangle over it with rgba of:
(255, 255, 255, 0.1)
and apply alpha blending using the default compositing mode of source-over, before converting to a byte you get:
(255 * 0.1 + 246 * 0.9) = 246.9
So you can see that, assuming the browser simply truncates the floating point value to a byte, it will write out a value of 246, and every time you repeat the drawing operation you'll always end up with the same value.
There is a big discussion on the issue at this blog post here.
As a workaround you could continually clear the canvas and redraw the image with a decreasing globalAlpha value. For example:
// Clear the canvas
ctx.globalAlpha = 1.0;
ctx.fillStyle = "rgb(255, 255, 255)";
ctx.fillRect(0,0,canvas.width(),canvas.height());
// Decrement the alpha and draw the image
alpha -= 0.1;
if (alpha < 0) alpha = 0;
ctx.globalAlpha = alpha;
console.log(alpha);
ctx.drawImage(image, 0, 0, 256, 256);
setTimeout(draw, 100);
Fiddle is here.
Since the rectangle is only 10% opaque, the result of drawing it over the image is a composite of 90% of the image and 10% white. Each time you draw it you lose 10% of the previous iteration of the image; the rectangle itself does not become more opaque. (To get that effect, you would need to position another object over the image and animate its opacity.) So after 10 iterations you still have (0.9^10) or about 35% of the original image. Note that rounding errors will probably set in after about 30 iterations.
The reason was perfectly stated before. It is not possible to get rid of it without clearing it and redrawing it like #Sam already said.
What you can you do to compensate it a bit is to set globalCompositeOperation.
There are various operations that help. From my tests I can say that hard-light works best for dark backgrounds and lighter work best for bright backgrounds. But this very depends on your scene.
An example making trails on "near" black
ctx.globalCompositeOperation = 'hard-light'
ctx.fillStyle = 'rgba(20,20,20,0.2)' // The closer to black the better
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = 'source-over' // reset to default value
The solution is to manipulate the pixel data with ctx.getImageData and ctx.putImageData.
Instead of using ctx.fillRect with a translucent fillStyle, set each pixel slightly to your background colour each frame. In my case it is black, which makes things simpler.
With this solution, your trails can be as long as you want, if float precision is taken into account.
function postProcess(){
const fadeAmount = 1-1/256;
const imageData = ctx.getImageData(0, 0, w, h);
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
const i = (x + y * w) * 4;
imageData.data[i] = Math.floor(imageData.data[i]*fadeAmount);
imageData.data[i + 1] = Math.floor(imageData.data[i + 1]*fadeAmount);
imageData.data[i + 2] = Math.floor(imageData.data[i + 2]*fadeAmount);
imageData.data[i + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const w = window.innerWidth;
const h = window.innerHeight;
canvas.width = w;
canvas.height = h;
const cs = createCs(50);
let frame = 0;
function init(){
ctx.strokeStyle = '#FFFFFF';
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, w, h)
loop();
}
function createCs(n){
const cs = [];
for(let i = 0; i < n; i++){
cs.push({
x: Math.random()*w,
y: Math.random()*h,
r: Math.random()*5+1
});
}
return cs;
}
function draw(frame){
//no longer need these:
//ctx.fillStyle = 'rgba(0,0,0,0.02)'
//ctx.fillRect(0, 0, w, h)
ctx.beginPath();
cs.forEach(({x,y,r}, i) => {
cs[i].x += 0.5;
if(cs[i].x > w) cs[i].x = -r;
ctx.moveTo(x+r+Math.cos((frame+i*4)/30)*r, y+Math.sin((frame+i*4)/30)*r);
ctx.arc(x+Math.cos((frame+i*4)/30)*r,y+Math.sin((frame+i*4)/30)*r,r,0,Math.PI*2);
});
ctx.closePath();
ctx.stroke();
//only fade every 4 frames
if(frame % 4 === 0) postProcess(0,0,w,h*0.5);
//fade every frame
postProcess(0,h*0.5,w,h*0.5);
}
//fades canvas to black
function postProcess(sx,sy,dw,dh){
sx = Math.round(sx);
sy = Math.round(sy);
dw = Math.round(dw);
dh = Math.round(dh);
const fadeAmount = 1-4/256;
const imageData = ctx.getImageData(sx, sy, dw, dh);
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
const i = (x + y * w) * 4;
imageData.data[i] = Math.floor(imageData.data[i]*fadeAmount);
imageData.data[i + 1] = Math.floor(imageData.data[i + 1]*fadeAmount);
imageData.data[i + 2] = Math.floor(imageData.data[i + 2]*fadeAmount);
imageData.data[i + 3] = 255;
}
}
ctx.putImageData(imageData, sx, sy);
}
function loop(){
draw(frame);
frame ++;
requestAnimationFrame(loop);
}
init();
canvas {
width: 100%;
height: 100%;
}
<canvas id="canvas"/>
Related
I am drawing rectangle on the corners of the image. For that, I am using JavaScript. Here I am getting the image data from canvas.
// Get image data
let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let arr = imgData .data;
I am finding n which is the length of the adjacent and opposite of the triangle.
var n;
if (width > height) {
n = height / 20;
} else {
n = width / 20;
}
Here, I am using these loops to draw triangle with red color in background on left top corner of the image.
for (let y = 0; y < n; y++) {
for (i = y * imgData.width * 4; i < ((y * imgData.width * 4) + (n * 2)) - y; i = i + 4) {
arr[i] = 255;
arr[i + 1] = 0;
arr[i + 2] = 0;
}
}
Here is the output I am getting.
My expected output.
[
I am stuck with first triangle on left top corner of the image. I tried to debug the loops but did not able to find mistake. Can you please help me to find the mistake?
Image Reference:
http://webgl-workshop.com/assets/e826db271aa3c03c69c4aca1e20abf5b.jpg
Here is how you write the code to complete this drawing task
In .html, refer to your canvas with id="canvas"
In .js file, enter below code. It draw top left corner. Do similarly for other 3 corners.
Let me know if you succeed. Enjoy :) !
var canvasElement = document.querySelector("#canvas");
var ctx = canvasElement.getContext("2d");
var n = 50; //default at 50px edge length
var triangle = {
x1: 0,
y1: 0,
x2: 50,
y2: 0,
x3: 0,
y3: 50
}
ctx.strokeStyle = 'red';
ctx.beginPath();
ctx.moveTo(triangle.x1, triangle.y1);
ctx.lineTo(triangle.x2, triangle.y2);
ctx.lineTo(triangle.x3, triangle.y3);
ctx.closePath();
ctx.stroke();
// the fill color
ctx.fillStyle = "#FF0000"; //or 'red'
ctx.fill();
I have a canvas with multiple circles in different colours and I want add a fade out effect only to some circles. The effect is only applicable to the ones in red and green.
The code is as follows
function drawPiece(pieceX, pieceY, color) {
if (color === "rgba(0,0,0,1)" || color === "rgba(255,255,255,1)"){
ctx.beginPath();
ctx.fillStyle = color;
ctx.arc(pieceX, pieceY, 50, 0, 2 * Math.PI, false);
ctx.fill();
ctx.lineWidth = "4";
ctx.strokeStyle = "rgba(0,0,0,1)";
ctx.stroke();
ctx.closePath();
}
else {
ctx.beginPath();
ctx.fillStyle = color;
ctx.arc(pieceX, pieceY, 10, 0, 2 * Math.PI, false);
ctx.fill();
ctx.lineWidth = "4";
ctx.strokeStyle = "rgba(0,0,0,1)";
ctx.stroke();
ctx.closePath();
setTimeout(function(){
var fadeTarget = document.getElementById("canvasGame");
var fadeEffect = setInterval(function () {
if (!fadeTarget.style.opacity) {
fadeTarget.style.opacity = 1;
}
if (fadeTarget.style.opacity > 0) {
fadeTarget.style.opacity -= 0.02;
} else {
clearInterval(fadeEffect);
}
}, 20);
},0.5);
}
}
The fade effect works but it fades out the whole canvas and not the individual circles.
How can I achieve this, that only some elements are faded out.
Thanks in advance
A great canvas 2d resource is MDN's CanvasRenderingContext2D
Animations using canvas.
You will need a render loop if you want to animate canvas content.
The render loop is called 60 times a second, if possible, drawing too much and the rate will drop below 60fps.
The main loop clears the canvas, then draws the animated content, then requests the next frame.
requestAnimationFrame(mainLoop); // request the first frame to start the animation
function mainLoop() {
ctx.globalAlpha = 1; // default to 1 in case there is other content drawn
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); // clear the canvas
drawContent(); // render the content.
requestAnimationFrame(mainLoop); // request the next frame (in 1/60th second)
}
A function to draw the circle. You can remove the alpha from the color and use globalAlpha to set the transparency.
Math.TAU = Math.PI * 2; // set up 2 PI
function drawCircle(x, y, radius, color, alpha = 1) {
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
ctx.strokeStyle = "#000";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.TAU);
ctx.fill();
ctx.stroke();
}
Create an object to hold a circle's description and an array to put them in
const circles = [];
function circle(x,y,r = 10, col = "#FFF", alpha = 1) {
return {x, y, r, col, alpha, alphaTarget: alpha};
}
Then in the drawContent function draw the circles one at a time
function drawContent() {
for (const circle of circles) {
if(circle.alpha !== circle.alphaTarget) {
const aStep = circle.alphaTarget - circle.alpha;
const dir = Math.sign(aStep);
circle.alpha += Math.min(Math.abs(aStep), dir * 0.02)) * dir;
}
drawCircle(circle.x, circle.y, circle.r, circle.col, circle.alpha);
}
}
Demo
The demo draws 100 circles each with their own color and alpha. The alpha is randomly selected to fade out and then back in.
You will need a render loop if you want to animate canvas content.
I move the circle so that if a device is to slow to render the content then it will be easier to see the low frame rate.
Math.TAU = Math.PI * 2; // set up 2 PI
Math.rand = (val) => Math.random() * val;
Math.randI = (val) => Math.random() * val | 0;
requestAnimationFrame(mainLoop);
const ctx = canvas.getContext("2d");
const W = canvas.width = innerWidth; // size canvas to page
const H = canvas.height = innerHeight; // size canvas to page
const circleCount = 100;
const circleFadeRate = 0.01; // per 60th second
const circles = [];
const circle = (x,y,r = 10, col = "#FFF", alpha = 1) => ({x, y, r, col, alpha, alphaTarget: alpha});
createCircles();
function createCircles() {
var i = circleCount;
while (i--) {
circles.push(circle(Math.rand(W), Math.rand(H), Math.rand(10) + 10, "#" + Math.randI(0xFFF).toString(16).padStart(3,"0"), 1));
}
circles.sort((a,b) => a.r - b.r); // big drawn last
}
function mainLoop() {
ctx.globalAlpha = 1;
ctx.clearRect(0, 0, W, H);
drawContent();
requestAnimationFrame(mainLoop);
}
function drawCircle(x, y, radius, color, alpha = 1) {
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.TAU);
ctx.fill();
ctx.stroke();
}
function drawContent() {
for (const circle of circles) {
if(circle.alpha !== circle.alphaTarget) {
const aStep = circle.alphaTarget - circle.alpha;
const dir = Math.sign(aStep);
circle.alpha += Math.min(Math.abs(aStep), 0.02) * dir;
} else if(Math.random() < 0.01) {
circle.alphaTarget = circle.alpha < 0.7 ? 1 : Math.random() * 0.4;
}
circle.y += (circle.r - 10) / 5;
circle.y = circle.y > H + circle.r + 2 ? -(circle.r + 2) : circle.y;
drawCircle(circle.x, circle.y, circle.r, circle.col, circle.alpha);
}
}
body {
padding: 0px;
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
For more information on the 2D canvas API see the link at top of this answer.
Canvas is a painting surface. Meaning you can't change it after you paint it. You can only clear it, or paint over it. Just like a real painting, you can't change the color of a stroke you've already painted.
So you must clear the canvas and then redraw it all, except this time draw some circles with a different opacity. Just change the last number on those rgba values to be between 0 and 1 to change the opacity.
Store opacity in a variable somewhere:
var circleOpacity = 1;
Change the opacity and then redraw in your interval function:
circleOpactiy -= 0.2;
drawMyCanvas();
Now draw the some pieces with a fillStyle something like:
ctx.fillStyle = shouldBeFaded ? `rgba(0,0,0,${circleOpacity})` : 'rgba(0,0,0,1)'
Alternatively, you could position two canvases absolutely so they are on top of each other and you could fade the top one as you are already doing. That way you won't have to re-render the canvas constantly. If the only thing you want to do is fade some circles, this might be easier. But if you want to anything more complex on that canvas (like render a game of some sort) you'll want to redraw the canvas every frame of animation anyway.
This screenshot and code snippet will show you my trouble. How to avoid this? May be I do this in wrong way? I would like to see some code example, if possible.
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
canvas.width = 500;
canvas.height = 500;
let image = new Image();
image.src= "https://www.html5rocks.com/static/images/identity/HTML5_Badge_256.png";
function rand(min, max)
{
return Math.floor(Math.random() * (max - min + 1)) + min;
}
image.addEventListener("load", function()
{
let top = canvas.width / 2 - image.width / 2;
let left = canvas.height / 2 - image.height / 2;
function render()
{
top += rand(-2, 2);
left += rand(-2, 2);
ctx.save();
ctx.globalAlpha = 0.05;
ctx.fillStyle = "#9ea7b8";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
ctx.drawImage(image, top, left, image.width, image.height);
requestAnimationFrame(render);
}
render();
});
<canvas></canvas>
Ok, after a lot of tries, job is done. Every frame I draw full path.
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
let tail = 20;
canvas.width = 800;
canvas.height = 600;
let image = new Image();
let opacity = [];
image.src= "https://www.html5rocks.com/static/images/identity/HTML5_Badge_256.png";
for(let i = 1; i <= tail; i++)
{
opacity.push(i / tail);
}
function rand(min, max)
{
return Math.floor(Math.random() * (max - min + 1)) + min;
}
image.addEventListener("load", function()
{
let frames = [];
frames.push({
x: canvas.width / 2 - image.width / 2,
y: canvas.height / 2 - image.height / 2
});
for (let i = 1; i < tail; i++)
{
frames.push({
x: frames[i - 1].x + rand(-2, 2),
y: frames[i - 1].y + rand(-2, 2)
});
}
function render()
{
frames.shift();
frames.push({
x: frames[frames.length - 1].x + rand(-2, 2),
y: frames[frames.length - 1].y + rand(-2, 2)
});
ctx.globalAlpha = 1;
ctx.fillStyle = "#9ea7b8";
ctx.fillRect(0, 0, canvas.width, canvas.height);
for(let i = 0; i < tail; i++)
{
ctx.save();
ctx.drawImage(image, frames[i].x, frames[i].y, image.width, image.height);
ctx.globalAlpha = opacity[tail - 1 - i];
ctx.fillRect(frames[i].x - tail, frames[i].y - tail, image.width + tail + tail, image.height + tail + tail);
ctx.restore();
}
requestAnimationFrame(render);
}
render();
});
<canvas></canvas>
The problem
The best way I can think of to describe this artifact is to call it a rounding error. One thing that people tend to forget, is that when you draw to a canvas, you are actually writing to a bitmap. Effectively an image. The alpha value per pixel is an integer, x, where 0 <= x <= 255.
So, when you say:
ctx.save();
ctx.globalAlpha = 0.05;
ctx.fillStyle = "#9ea7b8";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
ctx.drawImage(image, top, left, image.width, image.height);
You are drawing a mostly transparent square which won't make anything disappear fully. For instance, if a pixel has a color of rgba(254,255,255,255) and you draw over it with a white square at 0.05 opacity, rgba(255,255,255,12), it will pull the red from 254 up to 254.05. This needs to be an integer, though, so it gets rounded down, and will never fully become the color it is approaching.
Now how to fix it
One way I can think to fix this is to basically make a snake. You can use a circular array to store the positions of each, progressively more transparent image, and then draw them once you clear the screen. There is a problem with this though, it takes up a bit more room, and takes a bit longer to draw.
You can go through the image data and if a pixel is close enough, you make it what you want. I won't provide finished code for you, but you can use ctx.getImageData().
Pixel comparisons should be similar to
if(abs(actualRed-targetRed) < tolerance && abs(actualGreen-targetGreen) < tolerance....){
actualRed=targetRed;
actualGreen=targetGreen...
}
This page has a very interesting approach. Basically, it manipulates the alpha values of the picture directly, instead of drawing a transparent picture over it. You want a background color, apparently. This starts to complicate things, but not too badly. I would suggest separating the dynamic images from the static images. You can have two canvases that are stacked on top of one another (this is best practice, actually). Or you can just set the background of the canvas.
Hopefully this helped.
I have this awesome piece of code.
The idea, as you can imagine,is to draw a grid of rectangles. I want a big grid, let's say 100 X 100 or more.
However, when i run the awesome piece of code for the desired size (100X 100), my browser crashes.
How can i achieve that?
* please note: when i say 100X100 i mean the final number of rectangles (10k) not the size of the canvas.
thank u
function init() {
var cnv = get('cnv');
var ctx = cnv.getContext('2d');
var ancho = 12; // ancho means width
var alto = 12; // alto means height
ctx.fillStyle = randomRGB();
for (var i = 0; i < cnv.width; i+= ancho) {
for (var j = 0; j < cnv.height; j+= alto) {
//dibujar means to draw, rectangulo means rectangle
dibujarRectangulo(i+ 1, j+1, ancho, alto, ctx);
}
}
}
function dibujarRectangulo(x, y, ancho, alto, ctx) {
ctx.rect(x, y, ancho, alto);
ctx.fill();
ctx.closePath();
}
The dibujarRectanglo() function calls rect() function which adds a closed rectanglar subpath to the current path. Then calls fill() function to fill the current path. Then calls closePath() function to close the subpath, which does nothing since the subpath is already closed.
In other words, the first dibujarRectanglo() function call is painting a path that contains 1 rectangle subpath. The second call is painting a path that contains 2 rectangle subpaths. The third call is painting a path that contains 3 rectangle subpaths. And so on. If the loop calls dibujarRectanglo() function 10000 times then a total of 1+2+3+...+10000 = 50005000 (i.e. over 50 million) rectangle subpaths will be painted.
The dibujarRectangle() function should be starting a new path each time. For example...
function dibujarRectangulo(x, y, ancho, alto, ctx) {
ctx.beginPath();
ctx.rect(x, y, ancho, alto);
ctx.fill();
}
Then 10000 calls will only paint 10000 rectangle subpaths which is a lot faster that painting 50 million rectangle subpaths.
16,384 boxes on the wall
As I said in the comment its easy to draw a lot of boxes, it is not easy to have them all behave uniquely. Anyways using render to self to duplicate boxes exponential there are 128 * 128 boxes so that's 16K, one more iteration and it would be 64K boxes.
Its a cheat, I could have just drawn random pixels and called each pixel a box.
Using canvas you will get upto 4000 sprites per frame on a top end machine using FireFox with each sprite having a location, center point, rotation, x and y scale, and an alpha value. But that is the machine going flat out.
Using WebGL you can get much higher but the code complexity goes up.
I use a general rule of thumb, if a canva 2D project has more than 1000 sprites then it is in need of redesign.
var canvas = document.getElementById("can");
var ctx = canvas.getContext("2d");
/** CreateImage.js begin **/
var createImage = function (w, h) {
var image = document.createElement("canvas");
image.width = w;
image.height = h;
image.ctx = image.getContext("2d");
return image;
}
/** CreateImage.js end **/
/** FrameUpdate.js begin **/
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;
var ch = h / 2;
var boxSize = 10;
var boxSizeH = 5;
var timeDiv = 1.2;
var bBSize = boxSize * 128; // back buffer ssize
var buff = createImage(bBSize, bBSize);
var rec = createImage(boxSize, boxSize);
var drawRec = function (ctx, time) {
var size, x, y;
size = (Math.sin(time / 200) + 1) * boxSizeH;
ctx.fillStyle = "hsl(" + Math.floor((Math.sin(time / 500) + 1) * 180) + ",100%,50%)";
ctx.strokeStyle = "Black";
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.clearRect(0, 0, boxSize, boxSize);
x = Math.cos(time / 400);
y = Math.sin(time / 400);
ctx.setTransform(x, y, -y, x, boxSizeH, boxSizeH)
ctx.fillRect(-boxSizeH + size, -boxSizeH + size, boxSize - 2 * size, boxSize - 2 * size);
ctx.strokeRect(-boxSizeH + size, -boxSizeH + size, boxSize - 2 * size, boxSize - 2 * size);
}
function update(time) {
var fw, fh, px, py, i;
time /= 7;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, w, h);
drawRec(rec.ctx, time);
time /= timeDiv;
buff.ctx.clearRect(0, 0, bBSize, bBSize)
buff.ctx.drawImage(rec, 0, 0);
buff.ctx.drawImage(rec, boxSize, 0);
fw = boxSize + boxSize; // curent copy area width
fh = boxSize; // curent copy area height
px = 0; // current copy to x pos
py = boxSize; // current copy to y pos
buff.ctx.drawImage(buff, 0, 0, fw, fh, px, py, fw, fh); // make square
for (i = 0; i < 6; i++) {
drawRec(rec.ctx, time);
time /= timeDiv;
buff.ctx.drawImage(rec, 0, 0);
fh += fh; // double size across
px = fw;
py = 0;
buff.ctx.drawImage(buff, 0, 0, fw, fh, px, py, fw, fh); // make rec
drawRec(rec.ctx, time);
time /= timeDiv;
buff.ctx.drawImage(rec, 0, 0);
fw += fw; // double size down
px = 0;
py = fh;
buff.ctx.drawImage(buff, 0, 0, fw, fh, px, py, fw, fh);
}
// draw the boxes onto the canvas,
ctx.drawImage(buff, 0, 0, 1024, 1024);
requestAnimationFrame(update);
}
update();
.canv {
width:1024px;
height:1024px;
}
<canvas id="can" class = "canv" width=1024 height=1024></canvas>
I stubled upon a weird problem. The following code results in making the image fade away because it's overdrawn by a semi-opaque rect over and over again.
But at least at the 10th iteration of draw(); the image should be completely overdrawn, because the rect should be fully opaque by then, right? But it actually never disappears completely.
This effect is worse on Chrome than it is on Firefox. But beware: bad screens may hide this faulty behaviour =)
I also made a demo on jsFiddle.
$(function () {
var canvas = $("#mycanvas"),
ctx = canvas[0].getContext("2d"),
imgUrl = "http://it-runde.de/dateien/2009/august/14/25.png";
var image = new Image();
image.src = imgUrl ;
$(image).load(function() {
ctx.drawImage(image, 0, 0, canvas.width(), canvas.height());
draw();
});
function draw() {
ctx.fillStyle = "rgba(255, 255, 255, 0.1)";
ctx.fillRect(0, 0, canvas.width(), canvas.height());
setTimeout(draw, 100);
}
});
The effect one may want to achieve is that, say an object is moving all over the canvas, and the already drawn positions get overdrawn only slightly so after-glow of after-fade effect. But this result is just fugly.
So is there any solution to this?
I know this is old but I don't think the previously accepted answer is correct. I think this is happening as a result of pixel values being truncated from float to byte. In Windows 7 running Chrome version 39.0.2171.95m, after running your fiddle for a while, the image is still visible but only lightly, and doesn't appear to be changing any more. If I take a screenshot I see the following pixel values on the image:
(246, 246, 246)
When you draw a rectangle over it with rgba of:
(255, 255, 255, 0.1)
and apply alpha blending using the default compositing mode of source-over, before converting to a byte you get:
(255 * 0.1 + 246 * 0.9) = 246.9
So you can see that, assuming the browser simply truncates the floating point value to a byte, it will write out a value of 246, and every time you repeat the drawing operation you'll always end up with the same value.
There is a big discussion on the issue at this blog post here.
As a workaround you could continually clear the canvas and redraw the image with a decreasing globalAlpha value. For example:
// Clear the canvas
ctx.globalAlpha = 1.0;
ctx.fillStyle = "rgb(255, 255, 255)";
ctx.fillRect(0,0,canvas.width(),canvas.height());
// Decrement the alpha and draw the image
alpha -= 0.1;
if (alpha < 0) alpha = 0;
ctx.globalAlpha = alpha;
console.log(alpha);
ctx.drawImage(image, 0, 0, 256, 256);
setTimeout(draw, 100);
Fiddle is here.
Since the rectangle is only 10% opaque, the result of drawing it over the image is a composite of 90% of the image and 10% white. Each time you draw it you lose 10% of the previous iteration of the image; the rectangle itself does not become more opaque. (To get that effect, you would need to position another object over the image and animate its opacity.) So after 10 iterations you still have (0.9^10) or about 35% of the original image. Note that rounding errors will probably set in after about 30 iterations.
The reason was perfectly stated before. It is not possible to get rid of it without clearing it and redrawing it like #Sam already said.
What you can you do to compensate it a bit is to set globalCompositeOperation.
There are various operations that help. From my tests I can say that hard-light works best for dark backgrounds and lighter work best for bright backgrounds. But this very depends on your scene.
An example making trails on "near" black
ctx.globalCompositeOperation = 'hard-light'
ctx.fillStyle = 'rgba(20,20,20,0.2)' // The closer to black the better
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = 'source-over' // reset to default value
The solution is to manipulate the pixel data with ctx.getImageData and ctx.putImageData.
Instead of using ctx.fillRect with a translucent fillStyle, set each pixel slightly to your background colour each frame. In my case it is black, which makes things simpler.
With this solution, your trails can be as long as you want, if float precision is taken into account.
function postProcess(){
const fadeAmount = 1-1/256;
const imageData = ctx.getImageData(0, 0, w, h);
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
const i = (x + y * w) * 4;
imageData.data[i] = Math.floor(imageData.data[i]*fadeAmount);
imageData.data[i + 1] = Math.floor(imageData.data[i + 1]*fadeAmount);
imageData.data[i + 2] = Math.floor(imageData.data[i + 2]*fadeAmount);
imageData.data[i + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const w = window.innerWidth;
const h = window.innerHeight;
canvas.width = w;
canvas.height = h;
const cs = createCs(50);
let frame = 0;
function init(){
ctx.strokeStyle = '#FFFFFF';
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, w, h)
loop();
}
function createCs(n){
const cs = [];
for(let i = 0; i < n; i++){
cs.push({
x: Math.random()*w,
y: Math.random()*h,
r: Math.random()*5+1
});
}
return cs;
}
function draw(frame){
//no longer need these:
//ctx.fillStyle = 'rgba(0,0,0,0.02)'
//ctx.fillRect(0, 0, w, h)
ctx.beginPath();
cs.forEach(({x,y,r}, i) => {
cs[i].x += 0.5;
if(cs[i].x > w) cs[i].x = -r;
ctx.moveTo(x+r+Math.cos((frame+i*4)/30)*r, y+Math.sin((frame+i*4)/30)*r);
ctx.arc(x+Math.cos((frame+i*4)/30)*r,y+Math.sin((frame+i*4)/30)*r,r,0,Math.PI*2);
});
ctx.closePath();
ctx.stroke();
//only fade every 4 frames
if(frame % 4 === 0) postProcess(0,0,w,h*0.5);
//fade every frame
postProcess(0,h*0.5,w,h*0.5);
}
//fades canvas to black
function postProcess(sx,sy,dw,dh){
sx = Math.round(sx);
sy = Math.round(sy);
dw = Math.round(dw);
dh = Math.round(dh);
const fadeAmount = 1-4/256;
const imageData = ctx.getImageData(sx, sy, dw, dh);
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
const i = (x + y * w) * 4;
imageData.data[i] = Math.floor(imageData.data[i]*fadeAmount);
imageData.data[i + 1] = Math.floor(imageData.data[i + 1]*fadeAmount);
imageData.data[i + 2] = Math.floor(imageData.data[i + 2]*fadeAmount);
imageData.data[i + 3] = 255;
}
}
ctx.putImageData(imageData, sx, sy);
}
function loop(){
draw(frame);
frame ++;
requestAnimationFrame(loop);
}
init();
canvas {
width: 100%;
height: 100%;
}
<canvas id="canvas"/>