Skip to content

Commit 7e3004a

Browse files
roman01latmikov
authored andcommitted
use requestAnimationFrame to drive animations
1 parent 2c1e137 commit 7e3004a

File tree

3 files changed

+80
-16
lines changed

3 files changed

+80
-16
lines changed

examples/showcase/BouncingBall.jsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// See LICENSE file for full license text
44

55
// Bouncing ball component - demonstrates custom drawing with rect and circle
6-
import React, { useState, useEffect } from 'react';
6+
import React, { useState, useEffect, useRef } from 'react';
77

88
export function BouncingBall() {
99
// Content dimensions
@@ -20,18 +20,22 @@ export function BouncingBall() {
2020
const speed = 5.0;
2121
const angle = Math.random() * 2 * Math.PI;
2222

23-
const [velocityX, setVelocityX] = useState(Math.cos(angle) * speed);
24-
const [velocityY, setVelocityY] = useState(Math.sin(angle) * speed);
23+
const vx = useRef(Math.cos(angle) * speed);
24+
const vy = useRef(Math.sin(angle) * speed);
2525

26-
// Update ball position every 16ms (~60fps)
26+
// Update ball position every animation frame
2727
useEffect(() => {
28-
const interval = setInterval(() => {
28+
let time = performance.now();
29+
function render(t) {
30+
const dt = (t - time) / 10;
31+
time = t;
32+
2933
setBallX(prevX => {
30-
let newX = prevX + velocityX;
34+
let newX = prevX + vx.current * dt;
3135

3236
// Bounce off left/right walls
3337
if (newX - ballRadius <= borderThickness || newX + ballRadius >= contentWidth - borderThickness) {
34-
setVelocityX(prev => -prev);
38+
vx.current = -vx.current;
3539
// Clamp position to stay within bounds
3640
newX = newX - ballRadius <= borderThickness
3741
? borderThickness + ballRadius
@@ -42,11 +46,11 @@ export function BouncingBall() {
4246
});
4347

4448
setBallY(prevY => {
45-
let newY = prevY + velocityY;
49+
let newY = prevY + vy.current * dt;
4650

4751
// Bounce off top/bottom walls
4852
if (newY - ballRadius <= borderThickness || newY + ballRadius >= contentHeight - borderThickness) {
49-
setVelocityY(prev => -prev);
53+
vy.current = -vy.current;
5054
// Clamp position to stay within bounds
5155
newY = newY - ballRadius <= borderThickness
5256
? borderThickness + ballRadius
@@ -55,10 +59,13 @@ export function BouncingBall() {
5559

5660
return newY;
5761
});
58-
}, 16);
5962

60-
return () => clearInterval(interval);
61-
}, [velocityX, velocityY]);
63+
// Request next frame
64+
requestAnimationFrame(render);
65+
}
66+
67+
requestAnimationFrame(render);
68+
}, []);
6269

6370
return (
6471
<window title="Bouncing Ball" defaultX={600} defaultY={350} flags={64}>

lib/imgui-runtime/imgui-runtime.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ class HermesApp {
2929
facebook::hermes::HermesRuntime *hermes = nullptr;
3030
facebook::jsi::Function peekMacroTask;
3131
facebook::jsi::Function runMacroTask;
32+
facebook::jsi::Function flushRaf;
3233

3334
HermesApp(SHRuntime *shr, facebook::jsi::Function &&peek,
34-
facebook::jsi::Function &&run)
35+
facebook::jsi::Function &&run, facebook::jsi::Function &&flushRaf)
3536
: shRuntime(shr, &_sh_done), hermes(_sh_get_hermes_runtime(shr)),
36-
peekMacroTask(std::move(peek)), runMacroTask(std::move(run)) {}
37+
peekMacroTask(std::move(peek)), runMacroTask(std::move(run)),
38+
flushRaf(std::move(flushRaf)) {}
3739

3840
// Delete copy/move to ensure singleton behavior
3941
HermesApp(const HermesApp &) = delete;
@@ -275,6 +277,9 @@ static void app_frame() {
275277
s_hermesApp->hermes->drainMicrotasks();
276278
}
277279

280+
// Flush RAF callbacks (also a macrotask)
281+
s_hermesApp->flushRaf.call(*s_hermesApp->hermes);
282+
278283
// Render frame (this is also a macrotask)
279284
s_hermesApp->hermes->global()
280285
.getPropertyAsFunction(*s_hermesApp->hermes, "on_frame")
@@ -434,7 +439,8 @@ sapp_desc sokol_main(int argc, char *argv[]) {
434439
// Create and initialize HermesApp
435440
s_hermesApp =
436441
new HermesApp(shr, helpers.getPropertyAsFunction(*hermes, "peek"),
437-
helpers.getPropertyAsFunction(*hermes, "run"));
442+
helpers.getPropertyAsFunction(*hermes, "run"),
443+
helpers.getPropertyAsFunction(*hermes, "flushRaf"));
438444

439445
// Initialize jslib's current time
440446
double curTimeMs = stm_ms(stm_now());

lib/jslib-unit/jslib.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,64 @@
9797
}
9898
}
9999

100+
// requestAnimationFrame polyfill that adapts to the host's tick rate.
101+
// We batch callbacks and flush them on the next macro task, so the
102+
// frequency naturally follows the platform refresh cadence (e.g., 60/90/120Hz)
103+
// if the host drives the event loop once per frame.
104+
var rafQueue = {};
105+
var rafNextId = 1;
106+
107+
function flushRaf() {
108+
// Take a snapshot of callbacks and clear the queue so rAFs scheduled
109+
// inside a callback run on the next tick (matching browser semantics).
110+
var cbs = [];
111+
for (var id in rafQueue) {
112+
if (Object.prototype.hasOwnProperty.call(rafQueue, id)) {
113+
cbs.push(rafQueue[id]);
114+
delete rafQueue[id];
115+
}
116+
}
117+
var ts = curTime;
118+
for (var i = 0; i < cbs.length; i++) {
119+
try {
120+
cbs[i](ts);
121+
} catch (e) {
122+
try {
123+
if (
124+
globalThis &&
125+
globalThis.console &&
126+
typeof globalThis.console.error === 'function'
127+
) {
128+
globalThis.console.error(e);
129+
} else if (typeof print === 'function') {
130+
print('ERROR:', String(e && e.message ? e.message : e));
131+
}
132+
} catch (_) {}
133+
}
134+
}
135+
}
136+
137+
function requestAnimationFrame(callback) {
138+
var id = rafNextId++;
139+
rafQueue[id] = callback;
140+
return id;
141+
}
142+
143+
function cancelAnimationFrame(id) {
144+
if (rafQueue[id]) {
145+
delete rafQueue[id];
146+
}
147+
}
148+
100149
// Expose to global scope
101150
globalThis.setTimeout = setTimeout;
102151
globalThis.clearTimeout = clearTimeout;
103152
globalThis.setImmediate = setImmediate;
104153
globalThis.clearImmediate = clearImmediate;
105154
globalThis.setInterval = setInterval;
106155
globalThis.clearInterval = clearInterval;
156+
globalThis.requestAnimationFrame = requestAnimationFrame;
157+
globalThis.cancelAnimationFrame = cancelAnimationFrame;
107158

108159
// Polyfills needed by React
109160
// NODE_ENV will be set from C++ based on build configuration
@@ -168,5 +219,5 @@
168219
};
169220

170221
// Return helper functions for C++ to use
171-
return {peek: peekMacroTask, run: runMacroTask};
222+
return {peek: peekMacroTask, run: runMacroTask, flushRaf};
172223
})();

0 commit comments

Comments
 (0)