Replay system for KineticJS and HTML5 Canvas

I have been struggling with the lack of replay procedures in HTML5 gaming, with flash things were quite easy because we could save all of our game sessions as movies or swf files that could be later on viewed. With HTML5 we don’t quite have that, or it is not as easy. So I was thinking of ways to produce movies from canvas.
And then it hit me… images can produced as urls and canvas can be exported as an image, and what are continuously changing images? A movie!

So I went on and made a codepen about this. And quite frankly it kinda looks awesome. Of course some more brilliant minds than mine could dig into this and help me make it truly useful for all.

So let’s see the pen first. The procedure is as follows:

We click record and move the box around
Then we stop and we watch the movie that was produced.

See the Pen Record & playback for canvas by Michael Dobekidis (@netgfx) on CodePen.

So let’s break it down and see what’s what.

First of all we create our canvas (and declare the appropriate html to host it on)

<div id="container"></div>


stage = new Kinetic.Stage({
        container: 'container',
        width: 578,
        height: 200,
        id:"canv"
      });
      var layer = new Kinetic.Layer();
      var rectX = stage.getWidth() / 2 - 50;
      var rectY = stage.getHeight() / 2 - 25;
      
      var bg = new Kinetic.Rect({
        width:578,
        height:200,
        x:0,
        y:0,
        stroke:'black',
        fill:'#ffffff'
      });
  
      var box = new Kinetic.Rect({
        x: rectX,
        y: rectY,
        width: 100,
        height: 50,
        fill: '#00D2FF',
        stroke: 'black',
        strokeWidth: 4,
        draggable: true
      });

      // add cursor styling
      box.on('mouseover', function() {
        document.body.style.cursor = 'pointer';
      });
      box.on('mouseout', function() {
        document.body.style.cursor = 'default';
      });
  
      layer.add(bg);
      layer.add(box);
      
      stage.add(layer);

Now let us create some buttons and bind some functionality in them


$("#record").click(function(){
    $(".film").empty();
    $("#record").attr("disabled","disabled");
    storage = [];
    stop = false;
    storeCanvas();
    
    // some change //
    timer = setTimeout(changeColor, 1500,[box]);
  });
  
  
  $("#stop").click(function(){
    stopRecord();
    loadImages();
    window.console.log(allImages);
    $("#record").removeAttr("disabled");
    timer = false;
  });
  
  $("#playbackBtn").click(function(){
    tl.play();
  });
  
  $("#pauseBtn").click(function(){
    tl.pause();
  });
  
  $("#resumeBtn").click(function(){
    tl.resume();
  });

Now you are probably wondering what that pesky storeCanvas function does… well its where the core of it all lies


function storeCanvas(){
  
  if(stop === false){
  stage.toDataURL({
    callback: function(dataUrl) {
      storage.push(dataUrl);
      if(stop === false){
        var lazyLayout = _.debounce(storeCanvas, 30);
        requestID = requestAnimationFrame(lazyLayout);
      }
    }});
  }
}

Here what we actually do is to take advantage of the toDataURL function of KineticJS (and canvas) and we run in on each frame. BUT, we make sure not to overwelm the browser with extreme load and thus we use undescoreJS debounce function to help ease the load, as we now capture a frame every 30 milliseconds.

The other part that makes this a cool script is the way we handle image loading, by using promises.

A nice explanation about promises here: Promises.org

Here’s how we handle promises:


function loadImages(){
  allImages = [];
  var promises = [];
  var images = [];
  for(var i=0;i<storage.length;i=i+1){
    var img = storage[i];
    promises.push(imageLoaded(img));
  }
  
  $.when.apply($, promises).done(function () {
    $(".totalProgress").attr("max",allImages.length);
    for(var i=0;i<allImages.length;i=i+1){
      $(".film").prepend(allImages[i]);
      
      tl.add( TweenLite.to(allImages[i], 0.1, {display:'none',onComplete:function(){
        var prevValue = Number($(".totalProgress").attr("value"));
        $(".totalProgress").attr("value",prevValue+1);
      }}) );

    }
    
    $(".playbackControls").css("display","block");
  });
}
  
function imageLoaded(src){
  var deferred = $.Deferred();
  var sprite = new Image();
  sprite.className = 'filmstrip';
  sprite.onload = function() {
    allImages.push(sprite);  
    deferred.resolve();
  };
  sprite.src = src;
  return deferred.promise();
}  

So what we do here is to create an array of new Images that have as source the toDataURL source of the image, and when all these are done loading we create the filmstrip and animate them in sequence.


$(".film").prepend(allImages[i]);
tl.add( TweenLite.to(allImages[i], 0.1, {display:'none',onComplete:function(){
        var prevValue = Number($(".totalProgress").attr("value"));
        $(".totalProgress").attr("value",prevValue+1);
      }}) );

The first part here adds the images to the html container. And the second adds them to a TimelineLite sequence for animation.

Some info about sequencing animations with TimelineLite: Greensock TimelineLite

Bonus:
One good way to store the replay as a file would be to JSON.stringify the array of image URLs and send it to a nodeJS server running the following command:


app.get('/writeReplay',function(request, response){
  var id = new Date().getTime();
  fs.writeFile("../replay_"+id+".json", request.query.data, function(err) {
    if(err) {
        console.log(err);
    } else {
        console.log("The file was saved!");
        // respond with the file url
  response.jsonp({url:'http://xxx.xxx.xxx.xxx/replay_'+id+'.json'});

        // delete the file after a time interval
  //setTimeout(deleteFile,20000,['../replay_'+id+'.json']);
    }
});

I hope you liked this little experiment of mine. If you have any ideas of how to improve this approach or any comments at all, feel free to post.

Enjoy!

Facebooktwittergoogle_pluspinterestlinkedin
linkedin
Tagged , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *