Grabbing each frame of an HTML5 canvas

2020-05-11 12:28发布

问题:

These palette cycle images are breathtaking: http://www.effectgames.com/demos/canvascycle/?sound=0

I'd like to make some (or all) of these into desktop backgrounds.

I could use an animated gif version, but I have no idea how to get that from the canvas "animation". Is there anything available yet that can do something along these lines (speficially for that link and generally speaking).

回答1:

I have a solution but it is dependent on you being familiar with the Javascript Console in Firefox (install the Firebug plugin), Chrome or Safari.

In case you're not, google it, or try to just right click anywhere on the page, choose "Inspect Element" and look for "Console"...

What the code does:

It allows you to take 10 screen-grabs of the CANVAS element every 1/10th of a second. Both these values are easily modified since you can tweak to find the number of iterations you'd like to get. For the example you give, the canvas element ID is 'mycanvas'.

Once it has the screen-grabs it outputs the images into the page. At that point you can save the individual images.

Running the code

Paste in the following code into the Javascript Console:

var canvas = document.getElementById("mycanvas");
var shots  = [];
var grabLimit = 10;  // Number of screenshots to take
var grabRate  = 100; // Miliseconds. 500 = half a second

var count     = 0;

function showResults() {
    //console.log(shots);
    for (var i=0; i<shots.length; i++) {
      document.write('<img src="' + shots[i] + '"/>\n');
    }
}

var grabber = setInterval(function(){
  count++;

  if (count>grabLimit) {
    clearInterval(grabber);
    showResults();
  }

  var img     = canvas.toDataURL("image/png");
  shots.push(img);
}, grabRate);

and press CTRL-Enter to execute it.

It should take a few seconds to run so please be patient.

After that you should have all the necessary frames (any maybe more) to create an animated GIF via ImageMagick, this website MakeAGif.com, or other app.

Side Note

If for some reason you need to output as GIF of JPG instead of PNG just update, as needed, this:

var img     = canvas.toDataURL("image/png");

to one of these:

var img     = canvas.toDataURL("image/gif");
var img     = canvas.toDataURL("image/jpg");

Support for output as gif or jpg may not be in all browsers (should be most).

(BIG) UPDATE #1

First, I'm keeping the code above intact rather than updating it because both approaches could be helpful to others.

Second, this new code DOES NOT SOLVE the problem. It kind-of does but one major drawback. It creates an animated GIF (Yipee!) but its in various shades of green (Boooo!). Not sure how/why, but maybe someone can take it from here and see what I've missed.

So here we go... same rules apply - copy and paste it into the Javascript Console of a browser (it lags in Firefox but Google Chrome its pretty fast... 10 seconds or so to run).

var jsf  = ["/Demos/b64.js", "LZWEncoder.js", "NeuQuant.js", "GIFEncoder.js"];
var head = document.getElementsByTagName("head")[0];

for (var i=0;i<jsf.length;i++) {
  var newJS = document.createElement('script');
  newJS.type = 'text/javascript';
  newJS.src = 'http://github.com/antimatter15/jsgif/raw/master/' + jsf[i];
  head.appendChild(newJS);
}

// This post was very helpful!
// http://antimatter15.com/wp/2010/07/javascript-to-animated-gif/

var w = setTimeout(function() { // give external JS 1 second of time to load

    console.log('Starting');

    var canvas = document.getElementById("mycanvas");
    var context = canvas.getContext('2d');
    var shots  = [];
    var grabLimit = 10;  // Number of screenshots to take
    var grabRate  = 100; // Miliseconds. 500 = half a second
    var count     = 0;

    function showResults() {
        console.log('Finishing');
        encoder.finish();
        var binary_gif = encoder.stream().getData();
        var data_url = 'data:image/gif;base64,'+encode64(binary_gif);
        document.write('<img src="' +data_url + '"/>\n');
    }

    var encoder = new GIFEncoder();
    encoder.setRepeat(0);  //0  -> loop forever, 1+ -> loop n times then stop
    encoder.setDelay(500); //go to next frame every n milliseconds
    encoder.start();

    var grabber = setInterval(function(){
      console.log('Grabbing '+count);
      count++;

      if (count>grabLimit) {
        clearInterval(grabber);
        showResults();
      }

      var imdata = context.getImageData(0,0,canvas.width,canvas.height);
      encoder.addFrame(context);

    }, grabRate);

}, 1000);

It uses some helpful code, pointers and JS files referenced in this blog post JavaScript to (Animated) GIF. I use some JS files directly but you should copy these locally if you're going to use it a lot.

The output for me was this GIF:

So its something, but not what you need...



回答2:

EDIT: Finally put that code to use, here's the result:

Imagick generated a large image, so I went ahead and optimized with gimp.

The client-side code is a modified version of Michael's code:

var canvas = document.getElementById("mycanvas");
var shots  = [];
var grabLimit = 30;  // Number of screenshots to take
var grabRate  = 100; // Miliseconds. 500 = half a second

var count     = 0;

function postResults() {
   console.log("START---------");
   for (var i = 0; i < shots.length; i++) {
      document.write(shots[i]+"<br />");
   }    
   console.log("END-----------");
}

var grabber = setInterval(function(){
  count++;

  if (count>grabLimit) {
    clearInterval(grabber);
    postResults();
  }

  var img     = canvas.toDataURL("image/png");
  shots.push(img.replace("data:image/png;base64,",""));
}, grabRate);

It will write a bunch of base64 strings to the screen. Copy them and save them into a text file and then upload it to your web server. Then run the other script (see below) and it will write the image to your web server. The resulting image will be large and possibly choppy, so open up GIMP and optimize for difference and GIF. When saving, force it to use the same delay for all frames so the animation is smooth.


May not be too hard using PHP.

  1. Grab the dataURL (base64 encoded string) for each frame with JS and send it to the server.
  2. Decode the base64 strings and convert them to Imagick objects.
  3. Add these Imagick objects as frames to a new Imagick object.
  4. Save the Imagick object to the file system as a GIF.

Since michael already posted a nice JS solution, I'll add the server side code if you wish to automate it:

<?php
$imageData = array_map('rtrim', file('data.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)); //base 64 strings separated by newlines
$delay = 100;
$filename = 'coolmoviebro.gif';
$gif = new Imagick();

for($i = 0; $i < count($imageData); $i++) {
   $tempImg = new Imagick();
   $tempImg->readimageblob(base64_decode($imageData[$i]));
   $gif->addImage($tempImg);
}

$gif->setFormat('gif');
$gif->setImageDelay($delay);
$gif->writeImages($filename, true);

?>

I haven't written much PHP for a year or two so be sure to double check everything and so on.



回答3:

I took a look at the code, and it seems like it should be possible with a little hacking.

By looking at the list of cycle objects in Palette.Cycles, you should be able to figure out the cycle length of each cycle with the cycle.high, cycle.low, and cycle.rate fields. That said, you'll need a different algorithm for each of the six possible values for cycle.reverse.

Once you know the length of each cycle in the list (in milliseconds, if I'm reading the code correctly), you can find the least common multiple of all of the cycle lengths, which would tell you the total length of the animation. In practice, though, you'd want to floor-divide them by your sample period first, (say, 100 milliseconds for ten frames a second) in order to get a lower common multiple.

Then rig the animate function in main.js to take a tickCount parameter and pass that into palette.cycle, instead of using anything based on real time. Increase the tick count by your sample period with each iteration.

From there, you should be able to modify the Bitmap class's render method, adding the necessary logic to rip the canvas to a file. There appear to be libraries that can manage this last bit for you. I would recommend saving the files using the tick count as the file name (with enough leading zeros to keep them in order.) Stitching all the images together into an animated GIF might be possible to execute as a batch job using the right software.

Of course, I haven't actually tried this. You might want to put in checks, for instance, to make sure that you don't stumble upon an animation with an epic cycle length and creation millions of images on your hard drive.

As an aside, you could also, with a little more work, figure out the exact time until the next update and take irregular samples, but you'd have to figure out how to store that delay information such that you could use it to assemble the completed GIF.



回答4:

Sadly, according to the art creator, it is not quite possible to convert it to GIF animation due to different parts of the picture having different cycles.



回答5:

Try PhantomJS

This script saves 100 frames.

var webPage = require('webpage');
var fs = require('fs');
var page = webPage.create();

var NB_FRAME = 100;
var current = 0;

page.open('http://www.effectgames.com/demos/canvascycle/?sound=0',
function(status) {
  if (status === "success") {
     var current = 0;
     var grabber = setInterval(function () {
      var frame = page.evaluate(function() {
       return document.getElementById('mycanvas').toDataURL("image/png").split(",")[1];
      });
      fs.write("./frame-" + current + ".png",atob(frame), 'wb');
  if (++current === NB_FRAME) {
     window.clearInterval(grabber);
     phantom.exit(0);
  }
   }, 1000);
}
});

Run it:

 phantomjs SaveCanvasFrame.js

Then use ImageMagick

convert *.png animated.gif

Here we go :