-1

** You'll need to run the code snippet in full screen mode.

Take a look at the example below. If you click around you'll notice that the ball almost becomes blurry when travelling at high speeds.

Is there any way to smooth this out? Is this due to the 60fps? If so is there a way to increase it?

It seems to happen when the ball gets spaced out at higher speeds. You can see this in the image below.

enter image description here

'use strict';

// Todo
// - Make the ball spin
// - Make the ball squish
// - Add speed lines


(function () {

  const canvas = document.getElementsByClassName('canvas')[0],
        c = canvas.getContext('2d');


  // -----------------------------------
  // Resize the canvas to be full screen
  // -----------------------------------

  window.addEventListener('resize', resizeCanvas, false);

  function resizeCanvas() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    // ---------
    // Variables
    // ---------

    var circleRadius = 40,
        x = (canvas.width/2) - circleRadius, // inital x position of the ball
        y = (canvas.height/2) - circleRadius, // inital y position of the ball
        vx = 0, // velocity
        vy = 0, // velocity
        gravity = 0.8,
        dampening = 0.5,
        pullStrength = 0.04,
        segments = 4,
        bezieCircleFormula = (4/3)*Math.tan(Math.PI/(2*segments)), // http://stackoverflow.com/a/27863181/2040509
        pointOffset = {
          positive: bezieCircleFormula*circleRadius,
          negative: circleRadius-(bezieCircleFormula*circleRadius)
        },
        // Each side has 3 points, bezier 1, circle point, bezier 2
        // These are listed below in clockwise order.
        // So top has: left bezier, circle point, right bezier
        // Right has: top bezier, circle point, bottom bezier
        circlePoints = {
          top: [
            [x+pointOffset.negative, y],
            [x+circleRadius, y],
            [x+pointOffset.positive+circleRadius, y]
          ],
          right: [
            [x+circleRadius*2, y+pointOffset.negative],
            [x+circleRadius*2, y+circleRadius],
            [x+circleRadius*2, y+pointOffset.positive+circleRadius]
          ],
          bottom: [
            [x+pointOffset.positive+circleRadius, y+circleRadius*2],
            [x+circleRadius, y+circleRadius*2],
            [x+pointOffset.negative, y+circleRadius*2]
          ],
          left: [
            [x, y+pointOffset.positive+circleRadius],
            [x, y+circleRadius],
            [x, y+pointOffset.negative]
          ]
        };



    // --------------------
    // Ball squish function
    // --------------------
    // For `side` you can pass `top`, `right`, `bottom`, `left`
    // For `amount` use an interger

    function squish (side, squishAmount) {
      for (let i = 0; i < circlePoints[side].length; i++) {
        if (side === 'top') {
          circlePoints[side][i][1] += squishAmount;
        } else if (side === 'right') {
          circlePoints[side][i][0] -= squishAmount;
        } else if (side === 'bottom') {
          circlePoints[side][i][1] -= squishAmount;
        } else if (side === 'left') {
          circlePoints[side][i][0] += squishAmount;
        }
      }
    }



    // ------------------
    // Animation Function
    // ------------------

    function render () {

      // Clear the canvas
      c.clearRect(0, 0, canvas.width, canvas.height);



      // -----------------
      // Draw the elements
      // -----------------

      // Ground
      let groundHeight = 200;

      c.beginPath();
      c.fillStyle = '#9cccc8';
      c.fillRect(0, canvas.height - groundHeight, canvas.width, groundHeight);

      // Bezier circle
      c.beginPath();
      c.fillStyle = '#cf2264';
      c.moveTo(circlePoints.left[1][0], circlePoints.left[1][1]);
      c.bezierCurveTo(circlePoints.left[2][0], circlePoints.left[2][1], circlePoints.top[0][0], circlePoints.top[0][1], circlePoints.top[1][0], circlePoints.top[1][1]);
      c.bezierCurveTo(circlePoints.top[2][0], circlePoints.top[2][1], circlePoints.right[0][0], circlePoints.right[0][1], circlePoints.right[1][0], circlePoints.right[1][1]);
      c.bezierCurveTo(circlePoints.right[2][0], circlePoints.right[2][1], circlePoints.bottom[0][0], circlePoints.bottom[0][1], circlePoints.bottom[1][0], circlePoints.bottom[1][1]);
      c.bezierCurveTo(circlePoints.bottom[2][0], circlePoints.bottom[2][1], circlePoints.left[0][0], circlePoints.left[0][1], circlePoints.left[1][0], circlePoints.left[1][1]);
      c.fill();
      c.closePath();



      // -------------------------------
      // Recalculate circle co-ordinates
      // -------------------------------

      circlePoints = {
        top: [
          [x+pointOffset.negative, y],
          [x+circleRadius, y],
          [x+pointOffset.positive+circleRadius, y]
        ],
        right: [
          [x+circleRadius*2, y+pointOffset.negative],
          [x+circleRadius*2, y+circleRadius],
          [x+circleRadius*2, y+pointOffset.positive+circleRadius]
        ],
        bottom: [
          [x+pointOffset.positive+circleRadius, y+circleRadius*2],
          [x+circleRadius, y+circleRadius*2],
          [x+pointOffset.negative, y+circleRadius*2]
        ],
        left: [
          [x, y+pointOffset.positive+circleRadius],
          [x, y+circleRadius],
          [x, y+pointOffset.negative]
        ]
      };



      // -----------------
      // Animation Gravity
      // -----------------


      // Increment gravity
      vy += gravity;

      // Increment velocity
      y += vy;
      x += vx;



      // ----------
      // Boundaries
      // ----------

      // Bottom boundary
      if ((y + (circleRadius * 2)) > canvas.height - groundHeight/2) {
        y = canvas.height - groundHeight/2 - (circleRadius * 2);
        vy *= -1;

        // Dampening
        vy *= dampening;
        vx *= dampening;

        console.log(vy);

        if (vy > -2.4) {
          dampening = 0;
        } else {
          // squish('top', 20);
        }
      }

      // Right boundary
      if ((x + (circleRadius * 2)) > canvas.width) {
        x = canvas.width - (circleRadius * 2);
        vx *= -1;

        // Dampening
        vy *= dampening;
        vx *= dampening;
      }

      // Left boundary
      if ((x + (circleRadius * 2)) < 0 + (circleRadius * 2)) {
        x = 0;
        vx *= -1;

        // Dampening
        vy *= dampening;
        vx *= dampening;
      }

      // Top boundary
      if (y < 0) {
        y = 0;
        vy *= -1;

        // Dampening
        vy *= dampening;
        vx *= dampening;
      }

      requestAnimationFrame(render);
    }



    // -----------
    // Click event
    // -----------

    canvas.addEventListener('mousedown', function (e) {
      let dx = e.pageX - x,
          dy = e.pageY - y;

      if (dampening === 0) {
        dampening = 0.5;
      }

      vx += dx * pullStrength;
      vy += dy * pullStrength;
    });

    render();

  }
  resizeCanvas();

})();
body {
  margin: 0;
}

canvas {
  background: #ddf6f5;
  display: block;
}
<canvas class="canvas"></canvas>
MarioD
  • 1,703
  • 1
  • 14
  • 24
  • Looks fine on my machine (Intel NUC w/ integrated graphics) – Jonathon Reinhart Mar 30 '16 at 18:09
  • When I say blurry I mean it's not as smooth as if you were to move a circle around the screen using css transforms. – MarioD Mar 30 '16 at 18:10
  • It also looks fine to me. What do you mean by blurry? And how can I recreate the image you showed. I played around with it a bit and it worked fine. – Minestrone-Soup Mar 30 '16 at 19:15
  • Sorry I should probably use a different term. It's almost as if it's ghosting because there is not enough frames in between the animations. If you click around the screen to get the ball moving quickly you should see the movement of the ball emulate this effect. – MarioD Mar 30 '16 at 19:23

1 Answers1

2

Currently 60fps is all you will get from the browsers when using requestAnimationFrame. You can get faster frame rates by it is very hard to stay in sync or even know what frame rate the display is running at. 60fps is slow compared to some native apps and machines running 120+fps, but it is the best you can get for the browser for the time being.

I just added some code to your code (sorry my code is a bit messy) that just shows the renderTime "green line" (time your code spends rendering the scene each frame) and the frame rate "red line" plus the averages.I do not include the time to render the lines and text I added so they dont affect the render time but it will affect the framerate a little.

You are not putting a heavy load on the graphics with the render time way below the frame time, but you may notice that you still get occasional spikes in the red line. This happens when the browser drops some frames and can cause the animation to not look as smooth.

You only get one thread for your javascript (excluding workers) so for most machine this is less than an 1/8 of the cpu power, plus javascript is a slow language compared to the native code that handles the CSS animation. (I am not sure but CSS animation may also get some benefits from threading that javascript can not access)

If by chance the red line I added is all over the place (no flat straight sections at all) and the average frame rate is not near 60fps your browser may have display sync disabled and is just presenting the scene as soon as you are done.

I added this code to demonstrate visually the performance and frame rate. For real testing you should not have the display part and just measure times only displaying the results after a test period.

'use strict';

// Todo
// - Make the ball spin
// - Make the ball squish
// - Add speed lines


(function () {

  const canvas = document.getElementsByClassName('canvas')[0],
        c = canvas.getContext('2d');


  // -----------------------------------
  // Resize the canvas to be full screen
  // -----------------------------------

  window.addEventListener('resize', resizeCanvas, false);

  function resizeCanvas() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    // ---------
    // Variables
    // ---------

    var circleRadius = 40,
        x = (canvas.width/2) - circleRadius, // inital x position of the ball
        y = (canvas.height/2) - circleRadius, // inital y position of the ball
        vx = 0, // velocity
        vy = 0, // velocity
        gravity = 0.8,
        dampening = 0.5,
        pullStrength = 0.04,
        segments = 4,
        bezieCircleFormula = (4/3)*Math.tan(Math.PI/(2*segments)), // http://stackoverflow.com/a/27863181/2040509
        pointOffset = {
          positive: bezieCircleFormula*circleRadius,
          negative: circleRadius-(bezieCircleFormula*circleRadius)
        },
        // Each side has 3 points, bezier 1, circle point, bezier 2
        // These are listed below in clockwise order.
        // So top has: left bezier, circle point, right bezier
        // Right has: top bezier, circle point, bottom bezier
        circlePoints = {
          top: [
            [x+pointOffset.negative, y],
            [x+circleRadius, y],
            [x+pointOffset.positive+circleRadius, y]
          ],
          right: [
            [x+circleRadius*2, y+pointOffset.negative],
            [x+circleRadius*2, y+circleRadius],
            [x+circleRadius*2, y+pointOffset.positive+circleRadius]
          ],
          bottom: [
            [x+pointOffset.positive+circleRadius, y+circleRadius*2],
            [x+circleRadius, y+circleRadius*2],
            [x+pointOffset.negative, y+circleRadius*2]
          ],
          left: [
            [x, y+pointOffset.positive+circleRadius],
            [x, y+circleRadius],
            [x, y+pointOffset.negative]
          ]
        };



    // --------------------
    // Ball squish function
    // --------------------
    // For `side` you can pass `top`, `right`, `bottom`, `left`
    // For `amount` use an interger

    function squish (side, squishAmount) {
      for (let i = 0; i < circlePoints[side].length; i++) {
        if (side === 'top') {
          circlePoints[side][i][1] += squishAmount;
        } else if (side === 'right') {
          circlePoints[side][i][0] -= squishAmount;
        } else if (side === 'bottom') {
          circlePoints[side][i][1] -= squishAmount;
        } else if (side === 'left') {
          circlePoints[side][i][0] += squishAmount;
        }
      }
    }



    // ------------------
    // Animation Function
    // ------------------ 
    var lastTime = new Date().valueOf();
    var frameTimes = [];
    var renderTimes = []
    var frameTimeWritePos = 0;
    var frameTimeReadStartPos = 0;
    var frameTimeMaxSample = Math.floor(canvas.width/8);
    var maxTime = 0;
console.log(maxTime);

    function recordFrameTime(time,renderTime){
        frameTimes[frameTimeWritePos % frameTimeMaxSample] = time-lastTime;
        renderTimes[frameTimeWritePos % frameTimeMaxSample] = renderTime;
        maxTime = Math.min(1000/20,Math.max(maxTime, renderTime, time-lastTime));
        lastTime = time;
        frameTimeWritePos = (frameTimeWritePos + 1) % frameTimeMaxSample;
        if(frameTimeWritePos === frameTimeReadStartPos){
            frameTimeReadStartPos = (frameTimeReadStartPos + 1) % frameTimeMaxSample;
        }
    }
    function drawFrameTimes(){
        var yScale,xScale;
        var t1 = 0;
        var t2 = 0;
        var c1 = 0;
        var h = canvas.height;
        yScale = h / maxTime;
        xScale = canvas.width / frameTimeMaxSample;
        c.lineWidth = 2;
        c.strokeStyle = "red";
        var i = frameTimeReadStartPos;
        var sx = i;
        c.beginPath();
        c.moveTo((i-sx)*xScale ,h - frameTimes[(i %frameTimeMaxSample)]*yScale);
        while ( (i %frameTimeMaxSample) !== frameTimeWritePos){                 
            c.lineTo((i-sx)*xScale  ,h - frameTimes[(i %frameTimeMaxSample)]*yScale);
            t1 += frameTimes[(i %frameTimeMaxSample)];
            c1 += 1;
            i += 1;

        }
        t1 /= c1;
        t1 = (1000/t1).toFixed(2);
        c1 = 0;
        c.stroke();
        i = frameTimeReadStartPos;
  
        c.strokeStyle = "Green";
        c.beginPath();
        c.moveTo((i-sx)*xScale ,h - renderTimes[(i %frameTimeMaxSample)]*yScale);
        while ( (i %frameTimeMaxSample) !== frameTimeWritePos){                 
            c.lineTo((i-sx)*xScale ,h - renderTimes[(i %frameTimeMaxSample)]*yScale);
            i += 1;
            t2 += renderTimes[(i %frameTimeMaxSample)];
            c1 += 1;
        }
        t2/= c1;
        c.stroke();
        //c.beginPath();
        //c.strokeStyle = "white";
        //c.moveTo(0,h-(1000/60)*yScale);
       // c.lineTo(canvas.width,h-(1000/60)*yScale);
       // c.stroke();
        c.font="36px arial";
        c.fillStyle = "black";
        c.fillText("R:" + t2.toFixed(2)+ "ms "+t1+"fps", 20,40);
    }
    var bRenders = 0;
    function render (time) {
      if(isNaN(time)){
         time = performance.now();
         bRenders += 1;
      } 
      var startTime = performance.now();

      // Clear the canvas
      c.clearRect(0, 0, canvas.width, canvas.height);



      // -----------------
      // Draw the elements
      // -----------------

      // Ground
      let groundHeight = 200;

      c.beginPath();
      c.fillStyle = '#9cccc8';
      c.fillRect(0, canvas.height - groundHeight, canvas.width, groundHeight);

      // Bezier circle
      c.beginPath();
      c.fillStyle = '#cf2264';
      c.moveTo(circlePoints.left[1][0], circlePoints.left[1][1]);
      c.bezierCurveTo(circlePoints.left[2][0], circlePoints.left[2][1], circlePoints.top[0][0], circlePoints.top[0][1], circlePoints.top[1][0], circlePoints.top[1][1]);
      c.bezierCurveTo(circlePoints.top[2][0], circlePoints.top[2][1], circlePoints.right[0][0], circlePoints.right[0][1], circlePoints.right[1][0], circlePoints.right[1][1]);
      c.bezierCurveTo(circlePoints.right[2][0], circlePoints.right[2][1], circlePoints.bottom[0][0], circlePoints.bottom[0][1], circlePoints.bottom[1][0], circlePoints.bottom[1][1]);
      c.bezierCurveTo(circlePoints.bottom[2][0], circlePoints.bottom[2][1], circlePoints.left[0][0], circlePoints.left[0][1], circlePoints.left[1][0], circlePoints.left[1][1]);
      c.fill();
      c.closePath();



      // -------------------------------
      // Recalculate circle co-ordinates
      // -------------------------------

      circlePoints = {
        top: [
          [x+pointOffset.negative, y],
          [x+circleRadius, y],
          [x+pointOffset.positive+circleRadius, y]
        ],
        right: [
          [x+circleRadius*2, y+pointOffset.negative],
          [x+circleRadius*2, y+circleRadius],
          [x+circleRadius*2, y+pointOffset.positive+circleRadius]
        ],
        bottom: [
          [x+pointOffset.positive+circleRadius, y+circleRadius*2],
          [x+circleRadius, y+circleRadius*2],
          [x+pointOffset.negative, y+circleRadius*2]
        ],
        left: [
          [x, y+pointOffset.positive+circleRadius],
          [x, y+circleRadius],
          [x, y+pointOffset.negative]
        ]
      };



      // -----------------
      // Animation Gravity
      // -----------------


      // Increment gravity
      vy += gravity;

      // Increment velocity
      y += vy;
      x += vx;



      // ----------
      // Boundaries
      // ----------

      // Bottom boundary
      if ((y + (circleRadius * 2)) > canvas.height - groundHeight/2) {
        y = canvas.height - groundHeight/2 - (circleRadius * 2);
        vy *= -1;

        // Dampening
        vy *= dampening;
        vx *= dampening;

       // console.log(vy);

        if (vy > -2.4) {
          dampening = 0;
        } else {
          // squish('top', 20);
        }
      }

      // Right boundary
      if ((x + (circleRadius * 2)) > canvas.width) {
        x = canvas.width - (circleRadius * 2);
        vx *= -1;

        // Dampening
        vy *= dampening;
        vx *= dampening;
      }

      // Left boundary
      if ((x + (circleRadius * 2)) < 0 + (circleRadius * 2)) {
        x = 0;
        vx *= -1;

        // Dampening
        vy *= dampening;
        vx *= dampening;
      }

      // Top boundary
      if (y < 0) {
        y = 0;
        vy *= -1;

        // Dampening
        vy *= dampening;
        vx *= dampening;
      }

      requestAnimationFrame(render);
      recordFrameTime(time,(performance.now()-startTime));
      drawFrameTimes();

    }



    // -----------
    // Click event
    // -----------

    canvas.addEventListener('mousedown', function (e) {
      let dx = e.pageX - x,
          dy = e.pageY - y;

      if (dampening === 0) {
        dampening = 0.5;
      }

      vx += dx * pullStrength;
      vy += dy * pullStrength;
    });

    render();

  }
  resizeCanvas();

})();
body {
  margin: 0;
}

canvas {
  background: #ddf6f5;
  display: block;
}
<canvas class="canvas"></canvas>
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • That's a really awesome way to test the performance, thanks. A really weird observation: I use a retina laptop with an external monitor (non retina). If the browser window is on the external monitor I get 29.99fps, if I drag the window over to the laptop I get 59.99fps. – MarioD Mar 30 '16 at 21:22
  • If I close my laptop and only use the external display I get 59.99. Looks like the macbook is doing something weird when an external monitor is plugged in. – MarioD Mar 30 '16 at 21:24
  • I guess it's more of an issue with my laptop and not the actual canvas. Thanks for the monitoring tools, I will add them to my project to keep an eye on things. – MarioD Mar 30 '16 at 21:31
  • @MarioD Remember it is only meant as a rough guide, when the load is close to the frame rate (green line close to the red) the test will actually make the red line for FPS and average FPS value completely useless. It is more to see if there are any problems that can make the animation not smooth by highlighting changes in frame rates and rendering rates. You can find a small frame rate display in the chrome Dev tools, dont know about other browsers. – Blindman67 Mar 30 '16 at 21:53