简体   繁体   中英

Change speed of request animation frame

I have this code to move an image in a canvas from a position to an other one:

class Target {
    constructor(img, x_init, y_init, img_width = 100, img_height = 100) {
        this.img = img;
        this.x = x_init;
        this.y = y_init;
        this.img_width = img_width;
        this.img_height = img_height;
    }
    

    get position() {
        return this.x
    }

    move(canvas, x_dest, y_dest) {
        ctx = canvas.getContext('2d');
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(this.img, this.x, this.y, this.img_width, this.img_height);
        if (this.x != x_dest) {
            if (this.x > x_dest) {
                this.x -=1;
            } else {
                this.x +=1;
            }
        }
        if (this.y != y_dest) {
            if (this.y > y_dest) {
                this.y -=1;
            } else {
                this.y +=1;
            }
        }
        if (this.x != x_dest || this.y != y_dest) {
            //setTimeout(this.move.bind(target, canvas, x_dest, y_dest), 0);
            window.requestAnimationFrame(this.move.bind(target, canvas, x_dest, y_dest));
        }
    }

}

The thing with this code is: I cannot control the speed, and it's pretty slow... How could I control the speed and keep this idea of select the arrival position? I found topic about that but I didn't find anything that work in my case, surely because a step of 1 pixel is too small but I don't see How could I make.

[EDIT] Here's what I wanted to do (I have to add a record during 2 seconds when the red circle is shrinking). I did obviously by following pid instructions. Thanks again to him.

 (function() { function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } var canvas = document.getElementById("calibrator"); var ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; const points = [{ "x": 0, "y": 0 }, { "x": canvas.width / 2 - 100, "y": 0 }, { "x": canvas.width - 100, "y": 0 }, { "x": 0, "y": canvas.height / 2 - 100 }, { "x": canvas.width / 2 - 100, "y": canvas.height / 2 - 100 }, { "x": canvas.width - 100, "y": canvas.height / 2 - 100 }, { "x": 0, "y": canvas.height - 100, }, { "x": canvas.width / 2 - 100, "y": canvas.height - 100 }, { "x": canvas.width - 100, "y": canvas.height - 100 } ] function generateLinear(x0, y0, x1, y1, dt) { return (t) => { let f0, f1; f0 = t >= dt? 1: t / dt; // linear interpolation (aka lerp) f1 = 1 - f0; return { "x": f1 * x0 + f0 * x1, // actually is a matrix multiplication "y": f1 * y0 + f0 * y1 }; }; } function generateShrink(x0, y0, x1, y1, r0, dt) { return (t) => { var f0 = t >= dt? 0: dt - t; var f1 = t >= dt? 1: dt / t; var f2 = 1 - f1; return { "x": f2 * x0 + f1 * x1, "y": f2 * y0 + f1 * y1, "r": f0 * r0 }; }; } function create_path_circle() { var nbPts = points.length; var path = []; for (var i = 0; i < nbPts - 1; i++) { path.push({ "duration": 2, "segment": generateShrink(points[i].x, points[i].y, points[i].x, points[i].y, 40, 2) }); path.push({ "duration": 0.5, "segment": generateShrink(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y, 0, 0.5) }); } path.push({ "duration": 2, "segment": generateShrink(points[nbPts - 1].x, points[nbPts - 1].y, points[nbPts - 1].x, points[nbPts - 1].y, 40, 2) }) return path; } function create_path_target() { var nbPts = points.length; var path = []; for (var i = 0; i < nbPts - 1; i++) { path.push({ "duration": 2, "segment": generateLinear(points[i].x, points[i].y, points[i].x, points[i].y, 2) }); path.push({ "duration": 0.5, "segment": generateLinear(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y, 0.5) }); } path.push({ "duration": 2, "segment": generateLinear(points[nbPts - 1].x, points[nbPts - 1].y, points[nbPts - 1].x, points[nbPts - 1].y, 2) }) return path; } const path_target = create_path_target(); const path_circle = create_path_circle(); function renderImage(img, img_width, img_height) { return (pos) => { ctx = canvas.getContext('2d'); ctx.drawImage(img, pos.x, pos.y, img_width, img_height); } } function renderCircle() { return (pos) => { ctx = canvas.getContext('2d'); ctx.beginPath(); ctx.arc(pos.x + 50, pos.y + 50, pos.r, 0, 2 * Math.PI); ctx.fillStyle = "#FF0000"; ctx.fill(); ctx.stroke(); } } function generatePath(path) { let i, t; // fixup timing t = 0; for (i = 0; i < path.length; i++) { path[i].start = t; t += path[i].duration; path[i].end = t; } return (t) => { while (path.length > 1 && t >= path[0].end) { path.shift(); // remove old segments, but leave last one } return path[0].segment(t - path[0].start); // time corrected }; } var base_image = new Image(); base_image.src = 'https://www.pngkit.com/png/full/17-175027_transparent-crosshair-sniper-scope-reticle.png'; const sprites = [ { "move": generatePath(path_circle), "created": performance.now(), "render": renderCircle() }, { "move": generatePath(path_target), "created": performance.now(), "render": renderImage(base_image, 100, 100) } ]; const update = () => { let now; ctx.fillStyle = "#FFFFFF"; ctx.fillRect(0, 0, canvas.width, canvas.height); // put aside so all sprites are drawn for the same ms now = performance.now(); for (var sprite of sprites) { sprite.render(sprite.move((now - sprite.created) / 1000)); } window.requestAnimationFrame(update); }; window.requestAnimationFrame(update); })();
 <:DOCTYPE html> <html> <head> <title>Calibration</title> <link rel="stylesheet" href="https.//maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min:css"> </head> <body> <canvas id="calibrator"></canvas> <video id="stream"></video> <canvas id="picture"></canvas> <script src="https.//ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min:js"></script> <script src="https.//cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min:js"></script> <script src="https.//maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> <script src="calibration.js"></script> </body> </html>

For filming, if we suppose that I have a takeSnapshot() function which returns a picture, I would do:

    function film(dt) {
    return (t) => {
        if (t >= dt) {
            return false;
        } else {
            return true;
        }
    }
}

function create_video_timeline() {
    var nbPts = points.length;
    var path = [];
    for (var i = 0 ; i < nbPts - 1; i++) {
        path.push(
            {
                "duration": 2,
                "segment": film(2)
            }
        );
        path.push(
            {
                "duration":0.5,
                "segment": film(0)
            }
        );
    }
    path.push(
        {
            "duration": 2,
            "segment": film(2)              
        }
    )
    return path;
}

const video_timeline = create_video_timeline();

function getSnapshot() {
    return (bool) => {
        if (bool) {
            data.push(takepicture());
        } 
    }
}

const sprites = [
    
    {
        "move": generatePath(path_circle),
        "created": performance.now(),
        "render": renderCircle()
    },
    {
        "move": generatePath(path_target),    
        "created": performance.now(),
        "render": renderImage(base_image, 100, 100)
    },
    {
        "render": getSnapshot(),
        "move": generatePath(video_timeline),
        "created": performance.now()
    }
];

EDIT : added another movement example (look at cyan square)

To answer your comment about how to get "somewhere" in a fixed amount of time, you can linearize most functions and then solve the equation by fixing the time. This is easy for linear movement but rather difficult for complex cases, like moving along non-linear functions (eG a logarithmic spiral).

For a linear movement at constant speed (no acceleration/deceleration) from (x0, y0) to (x1, y1) in time dt you can use linear interpolation:

function generateLinear(x0, y0, x1, y1, dt)
{
  return (t) => {
    let f0, f1;

    f0 = t >= dt ? 1 : t / dt; // linear interpolation (aka lerp)
    f1 = 1 - f;

    return {
      "x": f0 * x0 + f1 * x1, // actually is a matrix multiplication
      "y": f0 * y0 + f1 * y1
    };
  };
}

This function can now be used to "assemble" a path. First define the path by generating the segments:

const path = [
  {
    "duration": dt1,
    "segment": generateLinear(x0, y0, x1, y1, dt1)
  },
  {
    "duration": dt2,
    "segment": generateLinear(x1, y1, x2, y2, dt2)
  },
  {
    "duration": dt3,
    "segment": generateLinear(x2, y2, x3, y3, dt3)
  }
];

Notice how the total path time will now be handled (using duration ) and translated into segment local time:

function generatePath(path)
{
  let t;

  // fixup timing
  t = 0;
  for (i = 0; i < path.length; i++)
  {
    path[i].start = t;
    t += path[i].duration;
    path[i].end = t;
  }

  return (t) => {
    while (path.length > 1 && t >= path[0].end)
    {
      path.shift(); // remove old segments, but leave last one
    }

    return path[0].segment(t - path[0].start); // time corrected
  };
}

EDIT : working example

I just whipped up this working example for you. Look at how I don't redo the canvas or context and draw on the same over and over. And how the movement does not depend on framerate, it's defined in the lissajous function.

 "use strict"; const cvs = document.querySelector("#cvs"); const ctx = cvs.getContext("2d"); function generateLissajous(dx, dy, tx, ty) { return (t) => { return { "x": 150 + dx * Math.sin(tx * t), "y": 75 + dy * Math.cos(ty * t) }; }; } function generateLinear(x0, y0, x1, y1, dt) { return (t) => { let f0, f1; f0 = t >= dt? 1: t / dt; // linear interpolation (aka lerp) f1 = 1 - f0; return { "x": f1 * x0 + f0 * x1, // actually is a matrix multiplication "y": f1 * y0 + f0 * y1 }; }; } function generatePath(path) { let i, t; // fixup timing t = 0; for (i = 0; i < path.length; i++) { path[i].start = t; t += path[i].duration; path[i].end = t; } return (t) => { let audio; while (path.length > 1 && t >= path[0].end) { path.shift(); // remove old segments, but leave last one } if (path[0].hasOwnProperty("sound")) { audio = new Audio(path[0].sound); audio.play(); delete path[0].sound; // play only once } return path[0].segment(t - path[0].start); // time corrected }; } function generateRenderer(size, color) { return (pos) => { ctx.fillStyle = color; ctx.fillRect(pos.x, pos.y, size, size); }; } const path = [ { "duration": 3, "segment": generateLinear(20, 20, 120, 120, 3) }, { "sound": "boing.ogg", "duration": 3, "segment": generateLinear(120, 120, 120, 20, 3) }, { "sound": "boing.ogg", "duration": 2, "segment": generateLinear(120, 20, 20, 120, 2) } ]; const sprites = [ { "move": generateLissajous(140, 60, 1.9, 0.3), "created": performance.now(), "render": generateRenderer(10, "#ff0000") }, { "move": generateLissajous(40, 30, 3.23, -1.86), "created": performance.now(), "render": generateRenderer(15, "#00ff00") }, { "move": generateLissajous(80, 50, -2.3, 1.86), "created": performance.now(), "render": generateRenderer(5, "#0000ff") }, { "move": generateLinear(10, 150, 300, 20, 30), // 30 seconds "created": performance.now(), "render": generateRenderer(15, "#ff00ff") }, { "move": generatePath(path), "created": performance.now(), "render": generateRenderer(25, "#00ffff") } ]; const update = () => { let now, sprite; ctx.fillStyle = "#000000"; ctx.fillRect(0, 0, 300, 150); // put aside so all sprites are drawn for the same ms now = performance.now(); for (sprite of sprites) { sprite.render(sprite.move((now - sprite.created) / 1000)); } window.requestAnimationFrame(update); }; window.requestAnimationFrame(update);
 canvas { border: 1px solid red; }
 <canvas id="cvs"></canvas>


You should not rely on requestAnimtionFrame() for this kind of movement.

What you should do is this.

  1. Have a movement function that is based on realtime ( t ), in this example a Lissajous orbit:
function orbit(t)
{
  return { "x": 34 * Math.sin(t * 0.84), "y": 45 * Math.cos(t * 0.23) };
}

Those numerals are just for shows. You can parameterize them and use currying to fixate them and obtain an "orbit()" function like this:

function generateLissajousOrbit(dx, tx, dy, ty)
{
  return (t) => { // this is the orbit function
    return { "x": dx * Math.sin(t * tx), "y": dy * Math.cos(t * ty) };
  };
}

This way, you can generate an arbitrary Lissajous orbit:

let movement = generateLissajousOrbit(34, 0.84, 45, 0.23);

Obviously, any movement function is valid. The only constraints are:

  • invoke with a t wich expresses realtime;
  • receive an object with x and y coordinates at time t .

Simpler movements should be obvious by now on how to implement. Also note that this way it's extremely easy to plug-in any movement.

  1. Determine at which point in the animation/movement you are:

On start put the current realtime milliseconds aside, like so:

let mymovingobject = {
  "started": performance.now(),
  "movement": generateLissajousOrbit(34, 0.84, 45, 0.23)
};

To get the x and y at any given time, you can now do as follows:

let now = performance.now();
let pos = mymovingobject.movement(now - mymovingobject.started);

// pos.x and pos.y contain the current coordinates

You will get a refresh (animation frame) independent movement that solely depends on realtime, which is your subjective perception space.

If the machine has a hickup or the refresh rate changes for any reason (user has just recalibrated the monitor, moved the window across desktops from a 120 Hz to a 60 Hz monitor, or whatever).... the movement will still be realtime bound and completely independent of frame rate.

  1. now you can poll position at any given time and use it for rendering

In the function that handles requestAnimationFrame() , you simply poll the position as shown above and then draw the object at pos.x and pos.y , without ever thinking about what the actual refresh rate is.

You can also skip frames to reduce frame rate and let the user decide the frequency by counting frames, like so:

let frame = 0;

function requestAnimationFrameHandler()
{
  if (frame % 2 === 0)
  {
    window.requestAnimationFrame();
    return; // quick bail-out for this frame, see you next time!
  }

  // update canvas at half framerate
}

Being able to reduce framerate is especially important today because of high frequency monitors. Your app would jump from 60 pixel/second to 120 pixel/second just by changing monitors. This is not what you want.

The requestAnimationFrame() facility looks like a panacea for smooth scrolling, but the truth is you bind yourself to the hardware constraints which are completely unknown (think about modern monitors in 2035... who knows how they will be).

This technique separates physical frame frequency from logical (gameplay) speed requirements.

Hope that makes sense somehow.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM