请求动画帧的变化速度
Change speed of request animation frame
我有这段代码可以将 canvas 中的图像从一个位置移动到另一个位置:
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));
}
}
}
这段代码的问题是:我无法控制速度,而且速度很慢...
怎么控制速度,保持select到达位置的想法?我找到了关于那个的话题,但我没有找到适合我的情况,肯定是因为 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() 函数 returns 一张照片,我会这样做:
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()
}
];
编辑:添加了另一个移动示例(查看青色方块)
要回答您关于如何在固定时间内到达“某处”的评论,您可以将大多数函数线性化,然后通过固定时间求解方程。这对于线性运动来说很容易,但对于复杂的情况来说却很困难,比如沿着非线性函数(例如对数螺线)移动。
对于从 (x0, y0)
到 (x1, y1)
的恒定速度(无 acceleration/deceleration)的线性运动 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
};
};
}
此函数现在可用于“assemble”路径。首先通过生成段来定义路径:
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
};
}
编辑:工作示例
我刚刚为您准备了这个工作示例。
看看我如何不重做 canvas 或上下文并一遍又一遍地绘制相同的内容。以及运动如何不依赖于帧率,它在 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;
}
<canvas id="cvs"></canvas>
这种运动你不应该依赖requestAnimtionFrame()
。
你应该做的是这个
- 有一个基于实时的运动函数(
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
. 收到坐标为 x
和 y
的对象
现在应该很清楚如何实施更简单的动作了。另请注意,通过这种方式插入任何动作都非常容易。
- 确定你在 animation/movement 中的哪一点:
开始时将当前实时毫秒放在一边,如下所示:
let mymovingobject = {
"started": performance.now(),
"movement": generateLissajousOrbit(34, 0.84, 45, 0.23)
};
要在任何给定时间获得 x
和 y
,您现在可以执行以下操作:
let now = performance.now();
let pos = mymovingobject.movement(now - mymovingobject.started);
// pos.x and pos.y contain the current coordinates
您将获得一个刷新(动画帧)独立运动,完全取决于实时,这是您的主观感受space。
如果机器出现故障或刷新率因任何原因发生变化(用户刚刚重新校准了显示器,将 window 跨桌面从 120 Hz 显示器移动到 60 Hz 显示器,或其他)。 ...移动仍然是实时绑定的并且完全独立于帧速率。
- 现在您可以随时轮询位置并将其用于渲染
在处理requestAnimationFrame()
的函数中,你只要像上图那样轮询位置,然后在pos.x
和pos.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 pixel/second 跳到 120 pixel/second。这不是你想要的。
requestAnimationFrame()
设施看起来像是平滑滚动的灵丹妙药,但事实是你将自己束缚在完全未知的硬件限制中(想想 2035 年的现代显示器......谁知道他们会如何是)。
此技术将物理帧频率与逻辑(游戏)速度要求分开。
希望这是有道理的。
我有这段代码可以将 canvas 中的图像从一个位置移动到另一个位置:
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));
}
}
}
这段代码的问题是:我无法控制速度,而且速度很慢... 怎么控制速度,保持select到达位置的想法?我找到了关于那个的话题,但我没有找到适合我的情况,肯定是因为 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() 函数 returns 一张照片,我会这样做:
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()
}
];
编辑:添加了另一个移动示例(查看青色方块)
要回答您关于如何在固定时间内到达“某处”的评论,您可以将大多数函数线性化,然后通过固定时间求解方程。这对于线性运动来说很容易,但对于复杂的情况来说却很困难,比如沿着非线性函数(例如对数螺线)移动。
对于从 (x0, y0)
到 (x1, y1)
的恒定速度(无 acceleration/deceleration)的线性运动 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
};
};
}
此函数现在可用于“assemble”路径。首先通过生成段来定义路径:
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
};
}
编辑:工作示例
我刚刚为您准备了这个工作示例。 看看我如何不重做 canvas 或上下文并一遍又一遍地绘制相同的内容。以及运动如何不依赖于帧率,它在 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;
}
<canvas id="cvs"></canvas>
这种运动你不应该依赖requestAnimtionFrame()
。
你应该做的是这个
- 有一个基于实时的运动函数(
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
. 收到坐标为
x
和 y
的对象
现在应该很清楚如何实施更简单的动作了。另请注意,通过这种方式插入任何动作都非常容易。
- 确定你在 animation/movement 中的哪一点:
开始时将当前实时毫秒放在一边,如下所示:
let mymovingobject = {
"started": performance.now(),
"movement": generateLissajousOrbit(34, 0.84, 45, 0.23)
};
要在任何给定时间获得 x
和 y
,您现在可以执行以下操作:
let now = performance.now();
let pos = mymovingobject.movement(now - mymovingobject.started);
// pos.x and pos.y contain the current coordinates
您将获得一个刷新(动画帧)独立运动,完全取决于实时,这是您的主观感受space。
如果机器出现故障或刷新率因任何原因发生变化(用户刚刚重新校准了显示器,将 window 跨桌面从 120 Hz 显示器移动到 60 Hz 显示器,或其他)。 ...移动仍然是实时绑定的并且完全独立于帧速率。
- 现在您可以随时轮询位置并将其用于渲染
在处理requestAnimationFrame()
的函数中,你只要像上图那样轮询位置,然后在pos.x
和pos.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 pixel/second 跳到 120 pixel/second。这不是你想要的。
requestAnimationFrame()
设施看起来像是平滑滚动的灵丹妙药,但事实是你将自己束缚在完全未知的硬件限制中(想想 2035 年的现代显示器......谁知道他们会如何是)。
此技术将物理帧频率与逻辑(游戏)速度要求分开。
希望这是有道理的。