我想要制作以下动画平滑和帧速率独立:
const duration = 25;
const friction = 0.68;
const target = 400;
let velocity = 0;
let location = 0;
function update() {
const displacement = target - location;
velocity += displacement / duration;
velocity *= friction;
location += velocity;
element.style.transform = `translate3d(${location}px, 0px, 0px)`;
}
这就是我想要实现的目标:
30Hz
、60Hz
、
120Hz
或更高,动画
持续时间都应该相同。以毫秒为单位的微小波动是可以接受的。
60Hz
刷新率或更高刷新率的所有设备上,动画应该流畅。
我尝试在“修复你的时间步长!”文章中实现许多开发人员称赞的技术,该技术将更新过程和渲染解耦。无论设备刷新率如何,这都应该使动画流畅:
function runAnimation() {
const squareElement = document.getElementById('square');
const timeStep = 1000 / 60;
const duration = 25;
const friction = 0.68;
const target = 400;
const settleThreshold = 0.001;
let location = 0;
let previousLocation = 0;
let velocity = 0;
let lastTimeStamp = 0;
let lag = 0;
let animationFrame = 0;
function animate(timeStamp) {
if (!animationFrame) return;
if (!lastTimeStamp) lastTimeStamp = timeStamp;
const elapsed = timeStamp - lastTimeStamp;
lastTimeStamp = timeStamp;
lag += elapsed;
while (lag >= timeStep) {
update();
lag -= timeStep;
}
const lagOffset = lag / timeStep;
render(lagOffset);
if (animationFrame) {
animationFrame = requestAnimationFrame(animate);
}
}
function update() {
const displacement = target - location;
previousLocation = location;
velocity += displacement / duration;
velocity *= friction;
location += velocity;
}
function render(lagOffset) {
const interpolatedLocation =
location * lagOffset + previousLocation * (1 - lagOffset);
squareElement.style.transform = `translate3d(${interpolatedLocation}px, 0px, 0px)`;
if (Math.abs(target - location) < settleThreshold) {
cancelAnimationFrame(animationFrame);
}
}
animationFrame = requestAnimationFrame(animate);
}
runAnimation();
body {
background-color: black;
}
#square {
background-color: cyan;
width: 100px;
height: 100px;
}
<div id="square"></div>
...但是,开发人员声称动画在刷新率为
60Hz
的设备上运行流畅,但在刷新率为
120Hz
及以上的设备上,动画会卡顿/断断续续。所以我尝试绘制不同刷新率下的动画曲线,看看是否有明显的错误,但从图表来看,似乎无论刷新率如何,动画都应该是平滑的?
function plotCharts() {
function randomIntFromInterval(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function simulate(hz, color) {
const chartData = [];
const targetLocation = 400;
const settleThreshold = 0.001;
const duration = 25;
const friction = 0.68;
const fixedFrameRate = 1000 / 60;
const deltaTime = 1000 / hz;
let location = 0;
let previousLocation = location;
let interpolatedLocation = location;
let velocity = 0;
let timeElapsed = 0;
let lastTimeStamp = 0;
let lag = 0;
function update() {
const displacement = targetLocation - location;
previousLocation = location;
velocity += displacement / duration;
velocity *= friction;
location += velocity;
}
function shouldSettle() {
const displacement = targetLocation - location;
return Math.abs(displacement) < settleThreshold;
}
while (!shouldSettle()) {
const timeStamp = performance.now();
if (!lastTimeStamp) {
lastTimeStamp = timeStamp;
update();
}
/*
Number between -1 to 1 including numbers with 3 decimal points
The deltaTimeFluctuation variable simulates the fluctuations of deltaTime that real devices have. For example, if the device has a refresh rate of 60Hz, the time between frames will almost never be exactly 16,666666666666667 (1000 / 60).
*/
const deltaTimeFluctuation =
randomIntFromInterval(-1000, 1000) / 1000;
const elapsed = deltaTime + deltaTimeFluctuation;
lastTimeStamp = timeStamp;
lag += elapsed;
while (lag >= fixedFrameRate) {
update();
lag -= fixedFrameRate;
}
const lagOffset = lag / fixedFrameRate;
interpolatedLocation =
location * lagOffset + previousLocation * (1 - lagOffset);
timeElapsed += elapsed;
chartData.push({
time: parseFloat((timeElapsed / 1000).toFixed(2)),
position: interpolatedLocation,
});
}
const timeData = chartData.map((point) => point.time);
const positionData = chartData.map((point) => point.position);
const canvas = document.createElement("canvas");
canvas.width = 600;
canvas.height = 400;
const ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
const chart = new Chart(ctx, {
type: "line",
data: {
labels: timeData,
datasets: [{
label: `${hz}Hz (with Interpolation)`,
data: positionData,
borderColor: color,
fill: false,
}, ],
},
options: {
scales: {
x: { title: { display: true, text: "Time (seconds)" } },
y: { title: { display: true, text: "Position (px)" } },
},
},
});
}
const simulations = [{
hz: 30,
color: "yellow"
},
{
hz: 60,
color: "blue"
},
{
hz: 120,
color: "red"
},
{
hz: 240,
color: "cyan"
},
{
hz: 360,
color: "purple"
},
];
simulations.forEach((simulation) => {
simulate(simulation.hz, simulation.color);
});
}
plotCharts()
body {
background-color: black;
}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
尝试使用滑动窗口技术来强制平均增量值(几乎)与硬件无关,并以某个平均值振荡。
class AvgDeltaWindow {
#average = 0;
#frames = [];
#frame_index = 0;
#frames_count = 0;
constructor(frames_pool_size) {
this.#frames = new Array(frames_pool_size);
}
get delta() {
return this.#average;
}
add(frame_length) {
const effective_frame_length = frame_length - this.#average;
this.#frames_count += (this.#frames.length > this.#frames_count ? 1 : 0);
this.#average += effective_frame_length / this.#frames_count;
this.#frames[this.#frame_index] = frame_length;
this.#frame_index++;
this.#frame_index = this.#frame_index % this.#frames.length;
}
}
// tweak length to fit your animation
//
// the bigger the window - the longer it takes to stabilize over some average value,
// but the value itself will be more accurate
const adw = new AvgDeltaWindow(5);
// somewhere in your loop
let last_timestamp = 0;
const velocity = 0.05;
const loop = () => {
const timestamp = performance.now();
const value = timestamp - last_timestamp;
last_timestamp = timestamp;
adw.add(value);
// a.delta -> this is your frame length to multiply by velocity
console.log(value, adw.delta * velocity);
setTimeout(loop, 1000 / 30);
};
setTimeout(loop, 1000 / 30);