Input, Time, and State #
Interactive Budo apps usually have three moving parts: a small state object, a frame callback, and a translation layer from platform input into app actions. Keeping those parts separate makes code easier to port from desktop to Android and web.
Animation callbacks #
JavaScript and Lua apps render by registering a callback with sys.animation.requestFrame. Budo stores one active animation callback. To keep animating, request the next frame before the callback returns.
// Budo calls this function when it is time to render a frame.
function frame(timestamp) {
// Advance app state before drawing it.
update(timestamp);
// Draw the current state to the canvas.
draw();
// Request another frame so animation continues.
sys.animation.requestFrame(frame);
}
// Start the frame loop once the entrypoint has loaded.
sys.animation.requestFrame(frame);The timestamp is in milliseconds. It is normal to convert it to seconds for simulation code.
// Store the previous timestamp so each frame can compute elapsed time.
let previous = 0;
// Convert Budo's millisecond timestamp into a seconds-based delta.
function frame(timestamp) {
// The first frame has no previous timestamp, so use a zero delta.
const dt = previous === 0 ? 0 : (timestamp - previous) / 1000;
// Save this timestamp for the next frame.
previous = timestamp;
// Update simulation state with frame-rate-independent time.
update(dt);
// Render the updated state.
draw();
// Keep the loop alive.
sys.animation.requestFrame(frame);
}sys.animation.cancelFrame(handle) clears the stored callback. The current handle value is 1, so treat it as a cancellation token rather than as a persistent per-callback identity.
Timers in JavaScript #
JavaScript apps also get sys.timer. Timers are checked once per frame, so they are frame-resolution timers rather than real-time threads.
// Run this callback once after roughly one second of frame time.
sys.timer.once(1000, () => {
// Timers are useful for UI events and lightweight scheduling.
console.log('One second later');
});
// Toggle a state field every quarter second.
const pulse = sys.timer.every(250, () => {
// Timer callbacks run between frames, so state changes are visible next draw.
state.blink = !state.blink;
});
// Cancel the repeating timer when the app no longer needs it.
sys.timer.clear(pulse);Use timers for UI delays, repeated polling inside the app, and small choreographed events. Use the frame timestamp for physics and animation.
Input snapshots #
sys.input.get() returns the current input state. It includes the primary pointer, all active pointers, mouse button edges, keyboard modifiers, frame timing, and focus.
// Read input once near the start of the frame.
const input = sys.input.get();
// Capture a drag origin when the primary pointer is newly pressed.
if (input.pointer.pressed) {
// Store the pointer's x coordinate in app state.
state.anchorX = input.pointer.x;
// Store the pointer's y coordinate in app state.
state.anchorY = input.pointer.y;
}
// Continue updating drag position while the primary pointer is held.
if (input.pointer.down) {
// Track the latest pointer x coordinate.
state.dragX = input.pointer.x;
// Track the latest pointer y coordinate.
state.dragY = input.pointer.y;
}The primary pointer is the mouse on desktop and the primary active contact on touch devices. For multi-touch gestures, read input.pointers.
// Draw feedback for every active touch contact.
for (const pointer of input.pointers) {
// Each pointer contains x and y coordinates in canvas space.
sys.canvas.drawCircle(pointer.x, pointer.y, 18);
}On desktop, pointers contains the mouse while the left button is down. On touch devices, it contains every active touch contact.
Keyboard input #
Keyboard queries use SDL scancode values. isKeyDown reports a held key. isKeyPressed reports a per-frame edge.
// SDL scancode for the Space key.
const SDL_SCANCODE_SPACE = 44;
// SDL scancode for the Left Arrow key.
const SDL_SCANCODE_LEFT = 80;
// SDL scancode for the Right Arrow key.
const SDL_SCANCODE_RIGHT = 79;
// Toggle pause only on the frame where Space is pressed.
if (sys.input.isKeyPressed(SDL_SCANCODE_SPACE)) {
// Store pause state in your app model.
state.paused = !state.paused;
}
// Move left while the Left Arrow key remains held.
if (sys.input.isKeyDown(SDL_SCANCODE_LEFT)) {
// Multiply by dt so movement speed is frame-rate independent.
state.x -= 240 * dt;
}
// Move right while the Right Arrow key remains held.
if (sys.input.isKeyDown(SDL_SCANCODE_RIGHT)) {
// Use the same speed as the left movement path.
state.x += 240 * dt;
}For text-entry-heavy applications, build a small input component that owns key repeat, selection, and editing behavior. Budo gives low-level input state; it does not impose a UI toolkit.
Focus and pause behavior #
The input snapshot exposes focused. Use it to pause interactions that should not continue when the window loses focus.
// Read focus from the same snapshot as pointer and timing state.
const input = sys.input.get();
// Stop interactive updates while the app is not focused.
if (!input.focused) {
// Draw a calm paused state instead of advancing the simulation.
drawPausedOverlay();
// Keep rendering so the overlay remains responsive when focus returns.
sys.animation.requestFrame(frame);
// Leave the current frame early.
return;
}For games, this small guard prevents accidental movement when the app regains focus. For creative tools, you might still render but stop interpreting pointer drags until focus returns.
State shape #
Budo does not prescribe state management. A plain object is often the best start.
// Keep app state in one plain object while the project is small.
const state = {
// Use a string mode for high-level interaction state.
mode: 'idle',
// Store the current x position of the active object.
x: 120,
// Store the current y position of the active object.
y: 120,
// Store horizontal velocity in pixels per second.
velocityX: 0,
// Store vertical velocity in pixels per second.
velocityY: 0,
// Use -1 when nothing is selected.
selectedId: -1
};As the app grows, split behavior by responsibility rather than by framework convention. A common shape is state.js, input.js, draw.js, and main.js, with main.js staying short enough to reveal the frame flow.
// Import the shared state object.
import { state } from './state.js';
// Import the input-to-state translation layer.
import { updateFromInput } from './input.js';
// Import the drawing layer.
import { drawScene } from './draw.js';
// Keep the frame function short enough to show the app flow.
function frame(timestamp) {
// Read input once and pass the snapshot down.
const input = sys.input.get();
// Mutate state from input and time.
updateFromInput(state, input, timestamp);
// Draw the state after it has been updated.
drawScene(state);
// Continue rendering.
sys.animation.requestFrame(frame);
}Keep runtime handles in state only when their lifecycle is clear. Shader programs, paths, fonts, SVG handles, audio buffers, database handles, and neural model IDs all represent resources managed by the runtime.
Touch-friendly interaction #
When designing an app that may ship to Android or web, avoid relying solely on hover, right-click, or dense keyboard shortcuts. Budo exposes the primitives, but the ergonomic layer is yours.
A useful pattern is to define app actions first, then map pointer and keyboard input into those actions.
// Convert platform-specific input into app-level actions.
function collectActions(input) {
// Return a small object that the rest of the app can understand.
return {
// Confirm with either the primary pointer or the Space key.
confirm: input.pointer.pressed || sys.input.isKeyPressed(44),
// Cancel with the Escape key.
cancel: sys.input.isKeyPressed(41),
// Carry the pointer x coordinate for controls that need a position.
x: input.pointer.x,
// Carry the pointer y coordinate for controls that need a position.
y: input.pointer.y
};
}The drawing code then reflects the same action model on every platform.
Long work and responsiveness #
Frame callbacks should not block on long computations. A blocking loop prevents input polling, network completion callbacks, audio responsiveness, and rendering.
For expensive work you control, break it into chunks and advance it over several frames. For network and file operations, use the runtime APIs as intended. For model inference, consider when a synchronous run is acceptable and when the app should visually communicate that it is working.
The runtime is small by design, which means your app structure matters. Clear frame code is one of the best performance tools you have.