动画
动画最好使用系统时间或页面时间来处理,其中单个时间变量保存动画的开始时间,并且通过从当前时间减去开始时间来找到当前动画位置。
迭代动画
这将意味着您的动画方式必须改变,因为迭代动画不会像您所做的那样适合条件分支......
// Code from OP's question
function theRect() {
ctx.fillStyle = "lightblue";
ctx.fillRect(x, y, w, h);
x += dx;
if (x + w > canvas.width / 2 || x < 0) {
dx = -dx;
}
}
如果时间不重要,上述方法效果很好,但您无法在任何特定时间点找到位置,因为您需要迭代其间的位置。此外,丢弃的任何帧都会改变动画对象的速度,并且随着时间的推移,位置会随着时间变得不确定。
时间和控制器。
另一种方法是使用控制器来制作动画。控制器只是根据输入产生形状输出的功能。这就是大多数 CSS 动画的完成方式,包括 easeIn、easeOut 等控制器。动画控件可以是固定时间的(在固定的时间内只播放一次),它们有一个开始时间和一个结束时间,或者它们可以是具有固定周期的循环。控制器还可以轻松集成关键帧动画。
控制器非常强大,应该包含在每个游戏/动画编码人员的知识集中。
循环三角波
对于此动画,循环三角形控制器最适合模仿您拥有的动画。
上图来自Desmos 计算器,显示了随时间变化的三角形函数(x 轴是时间)。所有控制器的频率为 1,幅度为 1。
控制器作为 Javascript 函数
// waveforms are cyclic to the second and return a normalised range 0-1
const waveforms = {
triangle (time) { return Math.abs(2 * (time - Math.floor(time + 0.5))) },
}
现在你可以得到矩形的位置作为时间的函数。您所需要的只是开始时间、当前时间、对象的像素速度以及它行进的距离。
我创建了一个对象来定义矩形和动画参数
const rect = {
startTime : -1, // start time if -1 means start time is unknown
speed : 60, // in pixels per second
travelDist : canvas.width / 2 - 20,
colour : "#8F8",
w : 20,
h : 20,
y : 100,
setStart(time){
this.startTime = time / 1000; // convert to seconds
this.travelDist = (canvas.width / 2) - this.w;
},
draw(time){
var pos = ((time / 1000) - this.startTime); // get animation position in seconds
pos = (pos * this.speed) / (this.travelDist * 2)
var x = waveforms.triangle(pos) * this.travelDist;
ctx.fillStyle = this.colour;
ctx.fillRect(x,this.y,this.w,this.h);
}
}
因此,一旦设置了开始时间,您只需提供当前时间即可随时找到矩形的位置
这是一个简单的动画,矩形的位置也可以作为时间的函数来计算。
rect.draw(performance.now()); // assumes start time has been set
调整大小
调整大小事件可能会出现问题,因为它们会像鼠标采样率一样频繁地触发,而鼠标采样率可能是帧率的许多倍。结果是代码运行的次数超出了必要的次数,增加了 GC 命中(由于画布调整大小),降低了鼠标捕获率(鼠标事件被丢弃,随后的事件提供了丢失的数据)使得窗口调整大小看起来很慢。
去抖事件
解决这个问题的常用方法是使用去抖动的调整大小事件处理程序。这只是意味着实际的调整大小事件会延迟一小段时间。如果在去抖动期间触发了另一个调整大小事件,则取消当前计划的调整大小并安排另一个调整大小。
var debounceTimer; // handle to timeout
const debounceTime = 50; // in ms set to a few frames
addEventListener("resize",() => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(resizeCanvas,debounceTime);
}
function resizeCanvas(){
canvas.width = innerWidth;
canvas.height = innerHeight;
}
这确保了在调整大小之前画布调整大小等待给后续调整大小事件在调整大小之前发生的机会。
虽然这比直接处理调整大小事件要好,但它仍然有点问题,因为延迟很明显,画布直到下一帧或两帧之后才调整大小。
帧同步调整大小
我发现的最佳解决方案是将调整大小同步到动画帧。您可以让 resize 事件设置一个标志以指示窗口已调整大小。然后在动画主循环中监视标志,如果为真则调整大小。这意味着调整大小与显示速率同步,并且忽略额外的调整大小事件。它还为动画设置了更好的开始时间(与帧时间同步)
var windowResized = true; // flag a resize at start so that its starts
// all the stuff needed at startup
addEventListener("resize",() => {windowResized = true}); // needs the {}
function resizeWindow(time){
windowResized = false; // clear the flag
canvas.width = innerWidth;
canvas.height = innerHeight;
rect.setStart(time); // reset the animation to start at current time;
}
然后在主循环中,您只需检查windowResized
标志,如果为真,则调用resizeWindow
动画时间
当您使用 requestAnimationFrame(callback) 函数时,将使用第一个参数作为时间调用回调。时间以毫秒 1/1000 秒为单位,精确到微秒 1/1,000,000 秒。例如(时间 > 1000.010)是自加载超过 1.00001 秒以来的时间。
function mainLoop(time){ // time in ms since page load.
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
例子
因此,让我们使用控制器和同步的调整大小事件重写动画。
为了突出定时动画和控制器的有用性,我添加了两个额外的波形sin
和sinTri
我也有原始动画,它会显示它是如何慢慢不同步的。动画的所有内容应在每个循环中位于画布的左边缘。动画速度设置为以每秒像素为单位的移动速度,以匹配每帧 1 个像素的原始近似值,速度设置为每秒 60 个像素。虽然对于 sin 和 sinTri 动画来说,速度是平均速度,而不是瞬时速度。
requestAnimationFrame(animate); //Starts when ready
var ctx = canvas.getContext("2d"); // get canvas 2D API
// original animation by OP
var x = 0;
var y = 20;
var h = 20;
var w = 20;
var dx = 1;
// Code from OP's question
function theRect() {
ctx.fillStyle = "lightblue";
ctx.fillRect(x, y, w, h);
x += dx;
if (x + w > canvas.width / 2 || x < 0) {
dx = -dx;
}
}
// Alternative solution
// wave forms are cyclic to the second and return a normalised range 0-1
const waveforms = {
triangle (time) { return Math.abs(2 * (time - Math.floor(time + 0.5))) },
sin(time) { return Math.cos((time + 0.5) * Math.PI * 2)*0.5 + 0.5 },
// just for fun the following is a composite of triangle and sin
// Keeping the output range to 0-1 means that multiplying any controller
// with another will always keep the value in the range 0-1
triSin(time) { return waveforms.triangle(time) * waveforms.sin(time) }
}
// object to animate is easier to manage and replicate
const rect1 = {
controller : waveforms.triangle,
startTime : -1, // start time if -1 means start time is unknown
speed : 60, // in pixels per second
travelDist : canvas.width / 2 - 20,
colour : "#8F8",
w : 20,
h : 20,
y : 60,
setStart(time){
this.startTime = time / 1000; // convert to seconds
this.travelDist = (canvas.width / 2) - this.w;
},
draw(time){
var pos = ((time / 1000) - this.startTime); // get animation position in seconds
pos = (pos * this.speed) / (this.travelDist * 2)
var x = this.controller(pos) * this.travelDist;
ctx.fillStyle = this.colour;
ctx.fillRect(x,this.y,this.w,this.h);
}
}
// create a second object that uses a sin wave controller
const rect2 = Object.assign({},rect1,{
colour : "#0C0",
controller : waveforms.sin,
y : 100,
})
const rect3 = Object.assign({},rect1,{
colour : "#F80",
controller : waveforms.triSin,
y : 140,
})
// very simple window resize event, just sets flag.
addEventListener("resize",() => { windowResized = true });
var windowResized = true; // will resize canvas on first frame
function resizeCanvas(time){
canvas.width = innerWidth; // set canvas size to window inner size
canvas.height = innerHeight;
rect1.setStart(time); // restart animation
rect2.setStart(time); // restart animation
rect3.setStart(time); // restart animation
x = 0; // restart the stepped animation
// fix for stack overflow title bar getting in the way
y = canvas.height - 4 * 40;
rect1.y = canvas.height - 3 * 40;
rect2.y = canvas.height - 2 * 40;
rect3.y = canvas.height - 1 * 40;
windowResized = false; // clear the flag
// resizing the canvas will reset the 2d context so
// need to setup state
ctx.font = "16px arial";
}
function drawText(text,y){
ctx.fillStyle = "black";
ctx.fillText(text,10,y);
}
function animate(time){ // high resolution time since page load in millisecond with presision to microseconds
if(windowResized){ // has the window been resize
resizeCanvas(time); // yes than resize for next frame
}
ctx.clearRect(0,0,canvas.width,canvas.height);
drawText("Original animation.",y-5);
theRect(); // render original animation
// three example animation using different waveforms
drawText("Timed triangle wave.",rect1.y-5);
rect1.draw(time); // render using timed animation
drawText("Timed sin wave.",rect2.y-5);
rect2.draw(time); // render using timed animation
drawText("Timed sin*triangle wave.",rect3.y-5);
rect3.draw(time); // render using timed animation
ctx.lineWidth = 2;
ctx.strokeRect(1,1,canvas.width - 2,canvas.height - 2);
requestAnimationFrame(animate);
}
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id="canvas"></canvas>