Getting Started #
In this tutorial you build a small app: a circle that orbits the center of the window and follows your mouse or finger while you hold it down. Along the way you meet the frame loop, canvas drawing, window size, and pointer input, which are the core of every Budo app.
You need the budo command; see Installation. You can also follow along without installing anything by pressing run on the code samples.
Create the project #
Ask budo to create a new project folder:
# Create my-budo-app/ with a starter app in it.
budo init my-budo-appThe command asks a few questions to fill in app.json: app name, author, version, description, orientation, and network policy. Press Enter to accept each default; you can edit the file later.
The new folder contains these files:
main.jsis your app's code. Budo starts here.app.jsonholds the app's name, version, and permissions.budo.d.tsandjsconfig.jsongive your editor completion and type hints for thesysAPI.budo-llm.mdis the full API reference, handy to give to an AI coding assistant.
Open main.js. It looks like this:
/// <reference path="./budo.d.ts" />
// Called once per displayed frame, with the time in milliseconds.
function frame(timestamp) {
// Fill the whole canvas with a dark background.
sys.canvas.clear("#222831");
// Draw light text at x=24, y=48, with a 28 px font size.
sys.canvas.setFillColor("#eeeeee");
sys.canvas.drawText("Hello, Budo!", 24, 48, 28);
// Ask Budo to call frame again on the next frame.
sys.animation.requestFrame(frame);
}
// Start the frame loop.
sys.animation.requestFrame(frame);Everything Budo offers lives on the global sys object. requestFrame registers frame, and Budo calls it for every displayed frame: each time, it clears the canvas and draws the text. Ending the function with requestFrame keeps the loop explicit, and is where an app that has nothing to animate would call sys.animation.waitForInput instead (see Input, time, and state). The first line points your editor at budo.d.ts; Budo itself ignores it.
Run it #
Run the project:
# Open the app in a desktop window, and restart it whenever you save a file.
budo run my-budo-app --watchA window opens with "Hello, Budo!" in it. Keep it open: with --watch, Budo restarts the app each time you save a change in the project, so you see every step of this tutorial right away.
Draw a circle #
Add a circle at the center of the window. sys.window.getWidth() and sys.window.getHeight() return the current size of the drawing area. Read them on every frame, because the user can resize the window.
function frame(timestamp) {
// Read the current size of the drawing area.
const width = sys.window.getWidth();
const height = sys.window.getHeight();
// Clear the canvas and draw the title.
sys.canvas.clear("#222831");
sys.canvas.setFillColor("#eeeeee");
sys.canvas.drawText("Hello, Budo!", 24, 48, 28);
// Draw a purple circle of radius 40 at the center.
sys.canvas.setFillColor("#7c6ff7");
sys.canvas.drawCircle(width * 0.5, height * 0.5, 40);
// Keep the frame loop going.
sys.animation.requestFrame(frame);
}
// Start the frame loop.
sys.animation.requestFrame(frame);setFillColor sets the color of everything you fill afterwards, until you change it again. That is why the text and the circle each set their own color right before they are drawn.
Animate it #
The timestamp argument tells you how much time has passed, in milliseconds. Use it to move the circle along an orbit:
function frame(timestamp) {
// Read the current size of the drawing area.
const width = sys.window.getWidth();
const height = sys.window.getHeight();
// Clear the canvas and draw the title.
sys.canvas.clear("#222831");
sys.canvas.setFillColor("#eeeeee");
sys.canvas.drawText("Hello, Budo!", 24, 48, 28);
// Turn the time into seconds, then into a point on a circle of radius 90.
const seconds = timestamp / 1000;
const x = width * 0.5 + Math.cos(seconds) * 90;
const y = height * 0.5 + Math.sin(seconds) * 90;
// Draw the circle at its position for this frame.
sys.canvas.setFillColor("#7c6ff7");
sys.canvas.drawCircle(x, y, 40);
// Keep the frame loop going.
sys.animation.requestFrame(frame);
}
// Start the frame loop.
sys.animation.requestFrame(frame);The circle now completes one orbit about every six seconds. Because the position comes from the time and not from a frame count, the speed stays the same on fast and slow displays.
Follow the pointer #
sys.input.get() returns the input state for the current frame: pointer, mouse, keyboard, typed text, and timing. Read it once, near the start of the frame. Add this line after width and height:
// Read this frame's input state.
const input = sys.input.get();Then replace the two lines that compute x and y:
// Start from the orbit position; let allows the values to change below.
let x = width * 0.5 + Math.cos(seconds) * 90;
let y = height * 0.5 + Math.sin(seconds) * 90;
// While the mouse button or a finger is down, use the pointer position.
if (input.pointer.down) {
x = input.pointer.x;
y = input.pointer.y;
}input.pointer is the primary pointer: the mouse on desktop, the first finger on a touch screen. Its down field is true while the button or finger is held, and x and y give its position in canvas coordinates. Hold the mouse button and the circle follows the pointer; release it and the circle returns to its orbit.
Here is the complete main.js:
function frame(timestamp) {
// Read the drawing area size and this frame's input.
const width = sys.window.getWidth();
const height = sys.window.getHeight();
const input = sys.input.get();
// Clear the canvas and draw the title.
sys.canvas.clear("#222831");
sys.canvas.setFillColor("#eeeeee");
sys.canvas.drawText("Hello, Budo!", 24, 48, 28);
// Compute the orbit position from the time.
const seconds = timestamp / 1000;
let x = width * 0.5 + Math.cos(seconds) * 90;
let y = height * 0.5 + Math.sin(seconds) * 90;
// While the pointer is down, follow it instead.
if (input.pointer.down) {
x = input.pointer.x;
y = input.pointer.y;
}
// Draw the circle.
sys.canvas.setFillColor("#7c6ff7");
sys.canvas.drawCircle(x, y, 40);
// Keep the frame loop going.
sys.animation.requestFrame(frame);
}
// Start the frame loop.
sys.animation.requestFrame(frame);Split the code into modules #
As an app grows, move code into separate files. JavaScript apps can use ES modules: export from one file, import into another.
Create color.js next to main.js:
// Return a color whose red channel pulses over time, as a "#RRGGBB" string.
export function pulseColor(seconds) {
// Swing the red level between 80 and 240, twice per orbit cycle.
const red = Math.round(160 + Math.sin(seconds * 2) * 80);
// Format it as two hex digits and add fixed green and blue values.
return "#" + red.toString(16).padStart(2, "0") + "78ff";
}Import it at the top of main.js, then use it in place of the fixed circle color:
// Load pulseColor from color.js, in the same folder.
import { pulseColor } from "./color.js";
// Inside frame, before drawing the circle:
sys.canvas.setFillColor(pulseColor(seconds));A path that starts with ./ or ../ is resolved from the importing file. A plain name such as "color.js" is resolved from the project root. Imports must stay inside the project folder.
Use TypeScript #
Budo runs main.ts directly, with no build step. Rename main.js to main.ts and add type annotations where they help:
// A typed variable.
let radius: number = 40;
// A typed function: Budo removes the annotations before running it.
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}Budo removes the type syntax and runs the result as JavaScript. It does not type-check your code, and it does not support enum, namespace, decorators, or satisfies. Use your editor or tsc for type checking. Project model has the details.
Try a quick sketch #
For an experiment that does not need a project folder, run a single .js, .ts, .lua, .wat, or .wasm file:
# Run sketch.js as a temporary one-file app.
budo run sketch.jsYou can also pipe code into budo run --from-input js:
# Send a small program to budo on standard input.
cat <<'EOF' | budo run --from-input js
// Draw a pulsing circle in the middle of the window.
function frame(timestamp) {
const radius = 32 + Math.sin(timestamp / 300) * 12;
sys.canvas.clear("#0e0e12");
sys.canvas.setFillColor("#7c6ff7");
sys.canvas.drawCircle(sys.window.getWidth() * 0.5, sys.window.getHeight() * 0.5, radius);
sys.animation.requestFrame(frame);
}
sys.animation.requestFrame(frame);
EOFA single-file app has no app.json and no other project files, so use a project folder as soon as you need assets or permissions.
Start from widgets #
For an app made of controls, such as forms, settings, or tools, start from the UI template:
# Create a project that includes the budo-ui widget toolkit.
budo init my-ui-app --template ui
# Run it.
budo run my-ui-appThe template adds the budo-ui toolkit under ui/ and a starter main.js with a text field, a theme switch, a toggle, a slider, and a button. The widgets work with the keyboard and screen readers and follow the system dark mode. Building interfaces explains how to use them.
Next steps #
You have used the core loop of a Budo app: schedule frames with sys.animation, read the size from sys.window, read input from sys.input, and draw with sys.canvas.
From here:
- Project model explains entrypoints,
app.json, file paths, and capabilities. - Drawing and layout covers shapes, text, fonts, SVG, paths, and transforms.
- Packaging and deployment turns this same folder into a web export or an Android package.