Abhinav Mishra
Abhinav Mishra

Reputation: 19

Make clearRect() of canvas work faster

I am trying to design a traveling sine wave in JavaScript, but the design appears quite slow. The main bottleneck is the clearRect() for canvas clearing.

How can I solve this?

Also I am drawing the pixel by ctx.fillRect(x, y,1,1), but when I clear using clearRect(x, y,1,1), it leaves some footprints. Instead I have to do clearRect(x, y,5,5) to get proper clearing. What can be the work around?

/******************************/

var x = 0;
var sineval = [];
var offset = 0;
var animFlag;

function init() {

    for(var i=0; i<=1000; ++i){
        sineval[i] = Math.sin(i*Math.PI/180);   
    }
    // Call the sineWave() function repeatedly every 1 microseconds
    animFlag = setInterval(sineWave, 1);
    //sineWave();
}


function sineWave()
{   //console.log('Drawing Sine');

    var canvas = document.getElementById("canvas");

    if (canvas.getContext) {
        var ctx = canvas.getContext("2d");
    }

    for(x=0 ; x<1000 ;++x){

        // Find the sine of the angle
        //var i = x % 361;
        var y = sineval[x+offset];

        // If the sine value is positive, map it above y = 100 and change the colour to blue
        if(y >= 0)
        {
            y = 100 - (y-0) * 70;
            ctx.fillStyle = "green";
        }

        // If the sine value is negative, map it below y = 100 and change the colour to red
        if( y < 0 )
        {
            y = 100 + (0-y) * 70;
            ctx.fillStyle = "green";
        }

        // We will use the fillRect method to draw the actual wave. The length and breath of the
        if(x == 0) ctx.clearRect(0,y-1,5,5);
        else ctx.clearRect(x,y,5,5);
        ctx.fillRect(x, y,1,1 /*Math.sin(x * Math.PI/180) * 5, Math.sin(x * Math.PI/180 * 5)*/);

    }

    offset = (offset > 360) ? 0 : ++offset ;
}

Upvotes: 1

Views: 1568

Answers (2)

Jose_X
Jose_X

Reputation: 1094

A few speedups and odd ends:

  • In init, set up the sine wave pixel values one time.

  • Use typed arrays for these since sticking with integers is faster than using floats if possible.

  • We will manipulate the pixel data directly instead of using fill and clear. To start this, in init we call ctx.getImageData one time. We also just one time max the alpha value of all the pixels since the default 0 value is transparent and we want full opacity at 255.

  • Use setInterval like before. We want to update the pixels at a steady rate.

  • Use 'adj' as knob to adjust how fast the sine wave moves on the screen. The actual value (a decimal) will depend on the drawing frame rate. We use Date.now() calls to keep track of milliseconds consumed across frames. So the adjustment on the millisecond is mod 360 to set the 'offset' variable. Thus offset value is not inc by 1 every frame but instead is decided based on the consumption of time. The adj value could later be connected to gui if want.

  • At end of work (in sineWave function), we call requestAnimationFrame simply to do the ctx.putImageData to the canvas,screen in sync to avoid tearing. Notice 'paintit' function is fast and simple. Notice also that we still require setInterval to keep steady pace.

  • In between setting the offset and calling requestAnimationFrame, we do two loops. The first efficiently blackens out the exact pixels we drew from the prior frame (sets to 0). The second loop draws the new sine wave. Top half of wave is green (set the G in pixel rgba to 255). Bottom half is red (set the R pixel rgba to 255).

  • Use the .data array to paint a pixel, and index it to the pixel using 4x + 4y*canvas.width. Add 1 more if want the green value instead of the red one. No need to touch the blue value (byte offset 2) nor the already set alpha (byte offset 3).

  • The >>>0 used in some places turns the affected value into an unsigned integer if it wasn't already. It can also be used instead of Math.ceil. .data is typed Array already I think.

  • This answer is rather late but it addresses some issues brought up in comments or otherwise not yet addressed. The question showed up during googling.

  • Code hasn't been profiled. It's possible some of the speedups didn't speed anything up; however, the cpu consumption of firefox was pretty light by the end of the adjustments. It's set to run at 40 fps. Make 'delay' smaller to speed it up and tax cpu more.

var sineval;
var offset = 0;
var animFlag;
var canvas;
var ctx;
var obj;
var milli;
var delay=25;
var adj=1/delay;  // .04 or so for 25 delay

function init() {
    canvas = document.getElementById("canvas");
    ctx = canvas.getContext("2d");
    obj=ctx.getImageData(0,0,canvas.width,canvas.height);

    for (let i=0; i<obj.data.length; i+=4) {
        obj.data[i+3]=255;  //set all alpha to full one time only needed.
    }

    sineval=new Uint8Array(1400);  //set up byte based table of final pixel sine values.. 1400 degrees total
    for (let i=0; i<=1400; ++i) {  //1400
        sineval[i] = (100-70*Math.sin(i*Math.PI/180))>>>0;   
    }

    animFlag = setInterval(sineWave, delay);  //do processing once every 25 milli

    milli=Date.now()>>>0;   //start time in milli
}

function sineWave() {
    let m=((Date.now()-milli)*adj)>>>0;
    let oldoff = offset;
    offset=(m % 360)>>>0;     //offset,frequency tuned with adj param.
    
    for(x=0 ; x<1000 ;++x) {  //draw sine wave across canvas length of 1000
        let y=sineval[x+oldoff];
        obj.data [0+x*4+y*4*canvas.width]=0; //black the reds
        obj.data [1+x*4+y*4*canvas.width]=0; //black the greens
    }

    for(x=0 ; x<1000 ;++x) {  //draw sine wave across canvas length of 1000
        let y=sineval[x+offset];
        if (y<100) {
            obj.data [1+x*4+y*4*canvas.width]=255; //rGba  //green for top half
        } else {
            obj.data [0+x*4+y*4*canvas.width]=255; //Rgba  //red for bottom half
        }
    }

    requestAnimationFrame(paintit);   //at end of processing try to paint next frame boundary
}

function paintit() {
    ctx.putImageData(obj,0,0);
}

init();
<canvas id="canvas" height=300 width=1000></canvas>

Upvotes: 0

user1693593
user1693593

Reputation:

You need to refactor the code a bit:

  • Move all global variables such as canvas and context outside of the loop function
  • Inside the loop, clear full canvas at beginning, redraw sine
  • Use requestAnimationFrame instead of setInterval
  • Replace fillRect() with rect() and do a single fill() outside the inner for-loop

Using a timeout value of 1 ms will potentially result in blocking the browser, or at least slow it down noticeably. Considering that a monitor update only happens every 16.7ms this will of course be wasted cycles. If you want to reduce/increase the speed of the sine you can reduce/increase the incremental step instead.

In essence:

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var sineval = [];
var offset = 0;

init();

function init() {

  for (var i = 0; i <= 1000; ++i) {
    sineval.push(Math.sin(i * Math.PI / 180));
  }

  // Call the sineWave() function
  sineWave();
}


function sineWave() {

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

  ctx.beginPath();
  ctx.fillStyle = "green";

  // draw positive part of sine wave here
  for (var x = 0; x < 1000; x++) {
    var y = sineval[x + offset];
    if (y >= 0) {
      y = 100 - (y - 0) * 70;
      ctx.rect(x, y, 2, 2);
    }
  }

  ctx.fill();

  ctx.beginPath();
  ctx.fillStyle = "red";

  // draw negative part of sine wave here
  for (var x = 0; x < 1000; x++) {
    var y = sineval[x + offset];
    if (y < 0) {
      y = 100 - (y - 0) * 70;
      ctx.rect(x, y, 2, 2);
    }
  }
  ctx.fill();

  offset = (offset > 360) ? 0 : ++offset;
  
  requestAnimationFrame(sineWave);
}
<canvas id="canvas" width=800 height=500></canvas>

And of course, if you load the script in <head> you need to wrap it in a window.onload block so canvas element is available. Or simply place the script at the bottom of the page if you haven't already.

Upvotes: 3

Related Questions