题意:"改变请求动画帧的速度"
问题背景:
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.
"这段代码的问题是:我无法控制速度,而且速度很慢……我该如何控制速度,同时保持选择到达位置的想法?我找到过相关的主题,但没有找到适合我情况的解决方案,可能是因为 1 像素的步幅太小,但我不知道该如何处理。"
[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.
"[编辑] 这是我想要做的(当红色圆圈缩小时,我必须在 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>
For filming, if we suppose that I have a takeSnapshot() function which returns a picture, I would do:
"对于拍摄,如果假设我有一个 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()
}
];
问题解决:
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 (e.G. 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:
"对于从 (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
};
};
}
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:
"注意总路径时间现在将如何处理(使用 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
};
}
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.
"我刚为你快速制作了这个工作示例。看看我如何不重复创建画布或上下文,而是一次又一次地在同一个上面绘制。以及运动如何不依赖于帧率,它在 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>
You should not rely on requestAnimtionFrame()
for this kind of movement.
"你不应该依赖 requestAnimationFrame() 来实现这种运动。"
What you should do is this.
"你应该这样做。"
- Have a movement function that is based on realtime (
t
), in this example a Lissajous orbit: "拥有一个基于实时 (t) 的运动函数,在这个例子中是一个 Lissajous 轨道:"
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:
"这些数字只是为了展示。你可以将它们参数化,并使用柯里化来固定它们,从而获得这样的 `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) };
};
}
This way, you can generate an arbitrary Lissajous orbit:
"通过这种方式,你可以生成任意的 Lissajous 轨道:"
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; 用一个表示实时的 `t` 来调用; - receive an object with
x
andy
coordinates at timet
. 接收一个在时间 `t` 处具有 `x` 和 `y` 坐标的对象。
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.
现在,实现简单的运动应该很明显了。同时请注意,这种方式使得插入任何运动变得非常简单。
- 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:
要在任何给定时间获取 `x` 和 `y`,你现在可以这样做:
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.
如果机器出现卡顿或由于任何原因导致刷新率发生变化(用户刚刚重新校准了显示器,或将窗口从120Hz的显示器移动到60Hz的显示器,等等)……运动仍然将受到实时的约束,并且完全独立于帧率。
- 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.
在处理 `requestAnimationFrame()` 的函数中,你只需像上面所示那样轮询位置,然后在 `pos.x` 和 `pos.y` 上绘制对象,而无需考虑实际的刷新率是什么。
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.
能够降低帧率在今天尤其重要,因为高频率显示器的普及。你的应用只需更换显示器,就可能从每秒60像素跳到每秒120像素。这正不是你想要的结果。
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).
`requestAnimationFrame()` 这个功能看起来是平滑滚动的灵丹妙药,但实际上你会受限于完全未知的硬件限制(想想2035年的现代显示器……谁知道它们会是什么样子)。
This technique separates physical frame frequency from logical (gameplay) speed requirements.
这项技术将物理帧频与逻辑(游戏玩法)速度要求分离开来。
Hope that makes sense somehow.
"希望这在某种程度上能让你理解。"
标签:function,canvas,return,frame,request,animation,path,now,points From: https://blog.csdn.net/suiusoar/article/details/143450416