Skip to content Skip to sidebar Skip to footer

Rgba Fillstyle With Alpha Does Not Get Fully Opaque If Applied Multiple Times

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

Solution 1:

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.

Solution 2:

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.

Solution 3:

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

Solution 4:

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.

functionpostProcess(){
  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;
functioninit(){
  ctx.strokeStyle = '#FFFFFF';
  ctx.fillStyle = '#000000';
  ctx.fillRect(0, 0, w, h)
  loop();
}
functioncreateCs(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;
}
functiondraw(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 framesif(frame % 4 === 0) postProcess(0,0,w,h*0.5);
  //fade every framepostProcess(0,h*0.5,w,h*0.5);
  
}
//fades canvas to blackfunctionpostProcess(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);
}
functionloop(){
  draw(frame);
  frame ++;
  requestAnimationFrame(loop);
}
init();
canvas {
width: 100%;
height: 100%;
}
<canvasid="canvas"/>

Post a Comment for "Rgba Fillstyle With Alpha Does Not Get Fully Opaque If Applied Multiple Times"