如何实现流畅的帧率无关动画

问题描述 投票:0回答:1

我想要制作以下动画平滑帧速率独立

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

这就是我想要实现的目标:

    无论设备刷新率是
  1. 30Hz60Hz
    120Hz
     或更高,
    动画
    持续时间都应该相同
    。以毫秒为单位的微小波动是可以接受的。
  2. 在具有 60Hz 刷新率或更高刷新率的所有设备上,
    动画应该流畅
  3. 在实现上述第 1-2 点的同时,动画行为应保持不变。

我该如何解决这个问题?


我尝试过的事情

我尝试在“修复你的时间步长!”文章中实现许多开发人员称赞的技术,该技术将更新过程和渲染解耦。无论设备刷新率如何,这都应该使动画流畅:

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>

我可能做错了什么,所以非常感谢您的帮助!请注意,只要动画的行为相同,欢迎使用渲染插值的固定时间步长的替代方法。

尝试使用滑动窗口技术来强制平均增量值(几乎)与硬件无关,并以某个平均值振荡。
javascript animation game-physics frame-rate requestanimationframe
1个回答
0
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.