Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use requestAnimationFrame inside a Class object

I have a class that takes some coordinate and duration data. I want to use it to animate an svg. In more explicit terms, I want to use that data to change svg attributes over a time frame.

I'm using a step function and requestAnimationFrame outside the class:

function step(timestamp) {
  if (!start) start = timestamp
  var progress =  timestamp - start;
  var currentX = parseInt(document.querySelector('#start').getAttribute('cx'));
  var moveX =  distancePerFrame(circleMove.totalFrames(), circleMove.xLine);

  document.querySelector('#start').setAttribute('cx', currentX + moveX);
  if (progress < circleMove.duration) {
    window.requestAnimationFrame(step);
  }
}

var circleMove = new SingleLineAnimation(3000, startXY, endXY)

var start = null

function runProgram() {
  window.requestAnimationFrame(step);
}

I can make it a method, replacing the circleLine with this. That works fine for the first run through, but when it calls the this.step callback a second time, well, we're in a callback black hole and the reference to this is broken. Doing the old self = this won't work either, once we jump into the callback this is undefined(I'm not sure why). Here it is as a method:

step(timestamp) {
  var self = this;

  if (!start) start = timestamp
  var progress =  timestamp - start;
  var currentX = parseInt(document.querySelector('#start').getAttribute('cx'));
  var moveX =  distancePerFrame(self.totalFrames(), self.xLine);

  document.querySelector('#start').setAttribute('cx', currentX + moveX);
  if (progress < self.duration) {
    window.requestAnimationFrame(self.step);
  }
}

Any ideas on how to keep the "wiring" inside the Object?

Here's the code that more or less works with the step function defined outside the class.

class SingleLineAnimation { 
  constructor(duration, startXY, endXY) {
    this.duration = duration;
    this.xLine = [ startXY[0], endXY[0] ];
    this.yLine = [ startXY[1], endXY[1] ];
  }

  totalFrames(framerate = 60) { // Default to 60htz ie, 60 frames per second
    return Math.floor(this.duration * framerate / 1000);
  } 

  frame(progress) {
    return this.totalFrames() - Math.floor((this.duration - progress) / 17 );
  } 
}

This will also be inserted into the Class, for now it's just a helper function:

function distancePerFrame(totalFrames, startEndPoints) {
  return totalFrames > 0 ? Math.floor(Math.abs(startEndPoints[0] - startEndPoints[1]) / totalFrames) : 0;
}

And click a button to...

function runProgram() {
  window.requestAnimationFrame(step);
}
like image 696
icicleking Avatar asked Mar 06 '23 21:03

icicleking


1 Answers

You need to bind the requestAnimationFrame callback function to a context. The canonical way of doing this is like this:

window.requestAnimationFrame(this.step.bind(this))

but it's not ideal because you're repeatedly calling .bind and creating a new function reference over and over, once per frame.

If you had a locally scoped variable set to this.step.bind(this) you could pass that and avoid that continual rebinding.

An alternative is this:

function animate() {

    var start = performance.now();
    el = document.querySelector('#start');

    // use var self = this if you need to refer to `this` inside `frame()`

    function frame(timestamp) {

        var progress =  timestamp - start;
        var currentX = parseInt(el.getAttribute('cx'));
        var moveX =  distancePerFrame(circleMove.totalFrames(), circleMove.xLine);
        el.setAttribute('cx', currentX + moveX);

        if (progress < circleMove.duration) {
            window.requestAnimationFrame(frame);
        }
    }

    window.requestAnimationFrame(frame);
}

i.e. you're setting up the initial state, and then doing the animation within a purely locally scoped function that's called pseudo-recursively by requestAnimationFrame.

NB: either version of the code will interact badly if you inadvertently call another function that initiates an animation at the same time.

like image 96
Alnitak Avatar answered Mar 16 '23 09:03

Alnitak