【问题标题】:Change speed of request animation frame改变请求动画帧的速度
【发布时间】:2021-03-25 15:39:20
【问题描述】:

我有这段代码可以将画布中的图像从一个位置移动到另一个位置:

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));
        }
    }

}

这段代码的问题是:我无法控制速度,而且速度很慢...... 我怎样才能控制速度并保持选择到达位置的想法?我找到了有关该主题的主题,但没有找到任何适合我的情况,这肯定是因为 1 像素的步长太小了,但我看不到如何制作。

[编辑]这就是我想做的(当红色圆圈缩小时,我必须在 2 秒内添加一条记录)。我显然是按照 pid 指令完成的。再次感谢他。

(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>

对于拍摄,如果我们假设我有一个返回图片的 takeSnapshot() 函数,我会这样做:

    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()
    }
];

【问题讨论】:

  • 我刚刚添加了一个工作示例,其中还展示了您应该研究的大量其他技术,尤其是如何更好地使用画布。希望这会有所帮助:)
  • 我已经修改了线性运动以满足我们的需求,并添加了一个“路径”运动,它使用复合模式来构建复杂的路径。这应该回答你的第一个问题。关于clearRect()fillRect(),请参阅stackoverflow.com/questions/30830126/…。我不知道哪个更好,我只是出于习惯写了fillRect()。如果您愿意,请使用clearRect() :)
  • 您还可以支持路径数组中的异构对象,例如描述不是运动而是描述其他内容的对象,例如“播放声音”、“播放精灵动画”或调用任意触发函数。这将打开一个完整的动画框架。
  • @pid 我添加了一个代码 sn-p 显示我想要做什么。如果你有足够的时间,你能确认我做得对吗?我为每个元素(目标和圆圈)编写了一个路径和一个渲染器。而当我想暂时停止元素时,我只是将他的位置更新为前一个。这些是好的做法吗?除非他们不是,否则我会为它写一部电影(t)和一种“路径”,这更像是一个时间线,因为我不会渲染它。
  • 我想我还没有想出如何像你之前说的那样做另一个动作。尤其是在拍戏的时候,会不会被强制要求有requestanimationframe() fps?

标签: javascript typescript animation frontend requestanimationframe


【解决方案1】:

编辑:添加另一个动作示例(看青色方块)

要回答您关于如何在固定时间内到达“某处”的评论,您可以线性化大多数函数,然后通过固定时间来求解方程。这对于线性移动很容易,但对于复杂的情况却相当困难,例如沿非线性函数(例如对数螺旋)移动。

对于从(x0, y0)(x1, y1) 在时间dt 的恒定速度(无加速/减速)的线性运动,您可以使用线性插值:

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
    };
  };
}

这个函数现在可以用来“组装”一个路径。首先通过生成段来定义路径:

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)
  }
];

注意现在将如何处理总路径时间(使用duration)并将其转换为本地时间段:

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
  };
}

编辑:工作示例

我刚刚为您准备了这个工作示例。 看看我是如何不重做画布或上下文并一遍又一遍地绘制的。以及运动如何不依赖于帧率,它是在 lissajous 函数中定义的。

"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;
}
&lt;canvas id="cvs"&gt;&lt;/canvas&gt;

您不应该依赖requestAnimtionFrame() 进行此类移动。

你应该做的是这个。

  1. 具有基于实时 (t) 的运动功能,在本例中为李萨如轨道:
function orbit(t)
{
  return { "x": 34 * Math.sin(t * 0.84), "y": 45 * Math.cos(t * 0.23) };
}

这些数字仅用于表演。您可以对它们进行参数化并使用柯里化来固定它们并获得这样的“orbit()”函数:

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) };
  };
}

这样,您可以生成任意的李萨如轨道:

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

显然,任何移动函数都是有效的。唯一的限制是:

  • 使用t 调用,表示实时;
  • t 时接收具有xy 坐标的对象。

现在关于如何实施的更简单的动作应该很明显了。另请注意,这种方式非常容易插入任何动作。

  1. 确定您在动画/运动中的哪一点:

在开始时将当前的实时毫秒数放在一边,如下所示:

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

要在任何给定时间获取xy,您现在可以执行以下操作:

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

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

您将获得完全依赖于实时的刷新(动画帧)独立运动,这是您的主观感知空间。

如果机器出现故障或刷新率因任何原因发生变化(用户刚刚重新校准显示器,将桌面上的窗口从 120 Hz 移动到 60 Hz 显示器,或其他原因)......仍然是实时绑定的,并且完全独立于帧速率。

  1. 现在您可以在任何给定时间轮询位置并将其用于渲染

在处理requestAnimationFrame() 的函数中,您只需如上所示轮询位置,然后在pos.xpos.y 处绘制对象,而无需考虑实际刷新率是多少。

您还可以跳过帧以降低帧速率,并让用户通过计算帧来决定频率,如下所示:

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
}

由于显示器的频率很高,因此能够降低帧速率在今天尤为重要。只需更换显示器,您的应用程序就会从 60 像素/秒跃升至 120 像素/秒。这不是你想要的。

requestAnimationFrame() 设施看起来像是平滑滚动的灵丹妙药,但事实是您将自己束缚在完全未知的硬件限制(想想 2035 年的现代显示器......谁知道它们会怎样)。

这种技术将物理帧频率与逻辑(游戏)速度要求分开。

希望这是有道理的。

【讨论】:

  • 如果我理解得很好;我必须找到 t(x) 和 t(y),其中 t(x)/t(y) 是元素在 x/y 中的时间。然后在我已经写过的时候,我将不得不用 x = x+ t(x) 更新位置?
  • 最好 t(x) 不是增量而是绝对位置。这样,t2 不依赖于 t1。您可以从 t1 跳到 t9 或从 t8 跳到 t6 等等……如果您将 t(x) 作为增量,那么您将失去所有这些品质。
  • 所以我需要 x(t)。谢谢你的解释。它真的很聪明!感谢您为我花费的所有时间。我要试试我的案子,让你知道它是否有效。如果可行,我会将您的答案作为一个:)
  • 好的,所以我还有两个问题:我还有两个问题:-就我而言,我希望我的图像在两秒钟内进入 (x1, y1),做一件事(独立于canvas),然后在 (x2, y2) 中在两秒钟内,做一件事等等......我该怎么做,因为我只需要一个元素的各种线性函数? - 在您的示例中(这对社区非常有用,谢谢),您确定覆盖画布而不是擦除它更好吗?
  • 很好的答案。次要问题:在最后一个 sn-p 中它应该是 window.requestAnimationFrame( requestAnimationFrameHandler ),它目前缺少该功能,而不是简单地“计算”通过的帧数,也许更有用的指标是检查自最后一帧。有人可能希望将 240Hz 的显示器限制为 60FPS,但他们可能不想将 60Hz 的显示器限制为 15FPS ;-) 尽管测量起来可能会变得更加复杂,One can't assume a fixed rate in neither directions
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-03
  • 1970-01-01
  • 1970-01-01
  • 2012-12-27
  • 1970-01-01
相关资源
最近更新 更多