badso badso - 1 month ago 5
Javascript Question

Body animation isn't smooth

The "spaceShip" in the following code isn't moving smoothly at the beginning of holding any arrow key down. It moves one step, freezes for a split second, and then moves "smoothly". How can I make it move smoothly right from the beginning, with not "freezing"?

My code:

<!doctype html>
<html>
<head>
<meta http-content="Content-type" charset="utf-8">
<title>Make body move smoothly</title>
<style type="text/css">
body {
}

canvas {
margin: auto;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
border: 1px solid black;
}

</style>
</head>
<body>
<canvas id="canvas" width="400" height="600"></canvas>
<script type="text/javascript">

// Set up requestAnimationFrame and cancelAnimationFrame
(function() {
var lastTime =0;
var vendors=['ms', 'moz', 'webkit', 'o'];
for(var x=0; x<vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame=window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame =
window[vendors[x]+ 'CancelAnimationFrame'] ||
window[vendors[x] +'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame=function(callback, element) {
var currTime =new Date().getTime();
var timeToCall =Math.max(0, 16 - (currTime - lastTime));
var id =window.setTimeout(function() { callback(currTime+timeToCall); },
timeToCall);
lastTime =currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame=function(id) {
clearTimeout(id);
};
}());


var canvas;
var ctx;
// ship data
var shipPositionX = document.getElementById('canvas').width/2;
var shipPositionY = document.getElementById('canvas').height - 30;
var deltaShipPositionX = 10;
var deltaShipPositionY = 10;

function init() {
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');

draw();
}

function draw(){
clear();
createRectangleToCoverCanvas();
createSpaceShip(shipPositionX, shipPositionY, 10);

requestAnimationFrame(draw);
}

function clear(){
ctx.clearRect(0, 0, document.getElementById('canvas').width, document.getElementById('canvas').height);
}

function createRectangleToCoverCanvas(){
ctx.fillStyle = 'black';
ctx.strokeStyle = 'black';
ctx.beginPath();
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.closePath();
ctx.fill();
ctx.stroke();
}

function createSpaceShip(x, y, radius) {
ctx.fillStyle = 'white'
ctx.strokeStyle = 'white'
ctx.beginPath();
ctx.rect(x, y, 20, 20);
ctx.closePath();
ctx.fill();
ctx.stroke();
}

function moveSpaceShip(event){
switch(event.keyCode){
// left
case 37:
if(shipPositionX - deltaShipPositionX + 15 > 0){
shipPositionX -= deltaShipPositionX;
}
break;

// up
case 38:
if(shipPositionY - deltaShipPositionY + 15 > 0){
shipPositionY -= deltaShipPositionY;
}
break;

// right
case 39:
if(shipPositionX + deltaShipPositionX < document.getElementById('canvas').width){
shipPositionX += deltaShipPositionX;
}
break;

//down
case 40:
if(shipPositionY + deltaShipPositionY < document.getElementById('canvas').height){
shipPositionY += deltaShipPositionY;
}
break;
}

}

window.addEventListener('load', init);
window.addEventListener('keydown', moveSpaceShip, true);
</script>
</body>
</html>


Notice the difference between my code and this example: http://atomicrobotdesign.com/blog_media/sprite_sheet/spritesheet.html

See how the example's is smooth, but my "spaceShip" isn't?
Why is it happening and how can I fix it? Is it because the example uses a sprite (but this doesn't seem to make much sense)?

Answer

The problem is that you wait for each keydown event to update the ship position.
The keydown event has a delay before it triggers again : the delay you are experiencing at beginning and the jump you face at each redraw.

The solution here is to trigger the movement on keydown and release it on keyup. This way, your ship will move smoothly as soon as you push the button.

// Im' assuming most of visitors here have recent browsers, so I removed the rAF polyfill for readibility

// If you wrap it after the canvas element decalaration, you can already populate this variable, it will avoid that you make a lot of calls to document.getElementById()
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
// ship data
var shipPositionX = canvas.width / 2;
// Just for the snippet height
var shipPositionY = 0;
var deltaShipPositionX = 10;
var deltaShipPositionY = 10;

//Removed the init() function, since our elements are loaded.


function draw() {
  clear();
  createRectangleToCoverCanvas();
  createSpaceShip(shipPositionX, shipPositionY, 10);
}

function clear() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
}

function createRectangleToCoverCanvas() {
  ctx.fillStyle = 'black';
  ctx.strokeStyle = 'black';
  ctx.beginPath();
  ctx.rect(0, 0, canvas.width, canvas.height);
  ctx.fill();
  ctx.stroke();
}

function createSpaceShip(x, y, radius) {
  ctx.fillStyle = 'white'
  ctx.strokeStyle = 'white'
  ctx.beginPath();
  ctx.rect(x, y, 20, 20);
  ctx.fill();
  ctx.stroke();
}
// instantiate a variable that will store our animationFrame id, so we can cancel it further
var raf, 
// the direction object, with an x and y values
    direction = {
      x: 0,
      y: 0
    };
// we can set a speed variable
var speed = 2.5;
function triggerMoveSpaceShip(event) {
  switch (event.keyCode) {
    // left
    case 37:
      // update the direction object
      direction.x = -speed;
      // avoid the scroll in the snippet
      event.preventDefault();
      break;

      // up
    case 38:
      direction.y = -speed;
      event.preventDefault();
      break;

      // right
    case 39:
      direction.x = speed;
      event.preventDefault();
      break;

      //down
    case 40:
      direction.y = speed;
      event.preventDefault();
      break;
  }
  // if we haven't initiated the animation yet, and that our direction is not 0, then do it now
  if (!raf && (direction.x || direction.y)) moveSpaceShip();
}

function releaseMoveSpaceShip(event) {;
  switch (event.keyCode) {
    // left
    case 37:
      //reset this direction
      direction.x = 0;
      break;

      // up
    case 38:
      direction.y = 0;
      break;

      // right
    case 39:
      direction.x = 0;
      break;

      //down
    case 40:
      direction.y = 0;
      break;
  }
  if (!direction.x && !direction.y) {
    // if none of the directions is set, stop the animation
    cancelAnimationFrame(raf);
    raf = undefined;
  }
}

function moveSpaceShip() {
  // declare our animation function
  var move = function() {
    // update the positions without going out of the screen
    // Sorry, this is dirty...
    if(direction.x){
    	if(
	    	(shipPositionX > 0 && shipPositionX < canvas.width-20) ||
			(shipPositionX <= 0 && direction.x > 0) ||
			(shipPositionX >= canvas.width-20 && direction.x < 0))
				shipPositionX += direction.x;
		}
	if(direction.y){
    	if(
	    	(shipPositionY > 0 && shipPositionY < canvas.height-20) ||
			(shipPositionY <= 0 && direction.y > 0) ||
			(shipPositionY >= canvas.width-20 && direction.y < 0))
				shipPositionY += direction.y;
		}

    // finally draw ou ship
    draw();
    // update our raf id
    raf = requestAnimationFrame(move);
  };
  // let's go !
  raf = requestAnimationFrame(move);
}


draw();

window.addEventListener('keydown', triggerMoveSpaceShip, true);
window.addEventListener('keyup', releaseMoveSpaceShip, true);
canvas {
  margin: auto;
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  border: 1px solid black;
}
body{
   overflow: none;
  }
<canvas id="canvas" width="400" height="200"></canvas>

uncommented fiddle