p5.js icon indicating copy to clipboard operation
p5.js copied to clipboard

saveFrames() does not honor frame rate

Open peeter2 opened this issue 5 months ago • 8 comments

I am drawing an animation on canvas. function setup() { canvas = createCanvas(400, 450, WEBGL); frameRate(20);

I see faster speed when my laptop is charging. When I unplug the power cable, the draw on canvas is much slower.

This impacts the saveFrames function. How can I save the certain duration worth of frames if the duration depends on whether the device is plugged in or not? When plugged in, a lot more frames are saved than when not charging, even though the 'duration' is for example 2 seconds. saveFrames('frame', 'jpg', duration, fps)

So using saveFrames() how can I reliably save the certain amount of frames despite the speed change?

Works fine with saveGif('test', 2); I get exactly 2 seconds long video, 40 frames total, no matter what device, it does not depend on the device speed. It honors the frameRate() set in setup(), and p5.js pauses the sketch's draw loop during recording. It will capture exactly the number of frames implied by the duration × framerate, regardless of actual performance.

I need to save the 2 second duration amount of frames also using saveFrames(). Currently it seems to be buggy. It does not pause or slow down the draw loop, and records frames in real time for the given duration.

If the system is underpowered (on battery), draw() runs slower, resulting in fewer total frames captured than expected.

peeter2 avatar Jul 08 '25 12:07 peeter2

It looks like saveFrames sets two timers, one that runs n times to create the frames, and a second that downloads all of them. These two timers currently race each other. I think to avoid this race, the download needs to happen inside the first timer (e.g. by checking the length of the frames array and doing the download if it's at the expected length.) https://github.com/processing/p5.js/blob/a64d54a2d1dcd337eae8cd0958f082a89705da91/src/image/image.js#L670-L685

In the mean time, as a workaround, you could try calling saveCanvas() in draw() if frameCount < n for some desired n.

davepagurek avatar Jul 08 '25 13:07 davepagurek

Hi @davepagurek, I'm happy to help.

HarishVX2 avatar Jul 10 '25 16:07 HarishVX2

Thanks @wayneharish10! I'll assign this to you.

davepagurek avatar Jul 10 '25 16:07 davepagurek

It looks like saveFrames sets two timers, one that runs n times to create the frames, and a second that downloads all of them. These two timers currently race each other. I think to avoid this race, the download needs to happen inside the first timer (e.g. by checking the length of the frames array and doing the download if it's at the expected length.)

p5.js/src/image/image.js

Lines 670 to 685 in a64d54a

const frameFactory = setInterval(() => { frames.push(makeFrame(fName + count, ext, cnv)); count++; }, 1000 / fps);

setTimeout(() => { clearInterval(frameFactory); if (callback) { callback(frames); } else { for (const f of frames) { p5.prototype.downloadFile(f.imageData, f.filename, f.ext); } } frames = []; // clear frames }, duration + 0.01); In the mean time, as a workaround, you could try calling saveCanvas() in draw() if frameCount < n for some desired n.

Hey @davepagurek , To add to what you've mentioned, because of the race condition between the timers, there is a possibility that setTimeout() be called before the frames are made in the frameFactory timer.

So, my proposed implementation would be calculating the total number of frames and clearing the frameFactory timer once attained, like so:

  const totalFrames = (duration * fps) / 1000;
  const makeFrame = p5.prototype._makeFrame;
  const cnv = this._curElement.elt;
  let frames = [];
  const frameFactory = setInterval(() => {
    if (count >= totalFrames) {
      clearInterval(frameFactory);
      if (callback) {
        callback(frames);
      } else {
        for (const f of frames) {
          p5.prototype.downloadFile(f.imageData, f.filename, f.ext);
        }
      }
      frames = []; // clear frames
    }
    frames.push(makeFrame(fName + count, ext, cnv));
    count++;
  }, 1000 / fps);

HarishVX2 avatar Jul 11 '25 09:07 HarishVX2

Sorry for the delay @HarishVX2! that sounds good to me.

davepagurek avatar Jul 27 '25 19:07 davepagurek

saveFrames() because of the way it is implemented has certain limits, if you'd want a more consistent output, do try p5.record.js that I've put together. There are still some stuff I want to implement for it but do have a try, it can capture image sequence at the frame rate that you decide (which can be different from the sketch frame rate) before downloading it all as a zip file.

limzykenneth avatar Aug 29 '25 10:08 limzykenneth

Hey @davepagurek , is anyone still working on this issue? If not, then I would like to work on this issue. Please assign this to me.

menacingly-coded avatar Dec 07 '25 13:12 menacingly-coded

@HarishVX2, are you still working on this?

davepagurek avatar Dec 07 '25 13:12 davepagurek