Drawing and Layout #
Budo's drawing API is built around a global sys.canvas namespace and a small set of stateful helpers: sys.path, sys.svg, and sys.font. The model is intentionally close to creative coding and native canvas libraries: set state, draw, set different state, draw again.
The canvas is backed by Skia on desktop, Android, and web. On desktop and web export, it ultimately participates in a GPU pipeline that can be sampled by shader passes. For normal 2D drawing, you can treat it as an immediate drawing surface.
Canvas basics #
Clear the frame first, then issue drawing commands.
// Draw one frame of the scene.
function frame() {
// Clear the previous frame with a dark background.
sys.canvas.clear('#111318');
// Select a fill color for the rectangle.
sys.canvas.setFillColor('#f4efe5');
// Draw a filled rectangle at x=40, y=40.
sys.canvas.drawRect(40, 40, 180, 100);
// Select a stroke color for the circle.
sys.canvas.setStrokeColor('#7c6ff7');
// Choose the stroke width in canvas pixels.
sys.canvas.setStrokeWidth(4);
// Draw a stroked circle over the rectangle.
sys.canvas.drawCircle(130, 90, 54);
// Request the next frame so the app keeps rendering.
sys.animation.requestFrame(frame);
}Rectangle-like calls use x, y, width, and height. Arc angles are degrees. Text returns the measured width of the drawn text, which is useful for simple alignment.
// Select the text color.
sys.canvas.setFillColor('#ffffff');
// Measure the bounds of the text at the chosen font size.
const { width } = sys.canvas.measureTextRect('Ready', 24);
// Draw again using the measured width for simple horizontal centering.
sys.canvas.drawText('Centered', (sys.window.getWidth() - width) * 0.5, 80, 24);Paint state #
sys.canvas stores the current fill, stroke, alpha, antialiasing, stroke cap, stroke join, and advanced Skia-only filters. A fill color switches subsequent primitive drawing to fill style. A stroke color switches it to stroke style.
// Enable smoother edges for rounded corners and diagonal lines.
sys.canvas.setAntiAlias(true);
// Select the fill color and switch the paint to fill mode.
sys.canvas.setFillColor('#ffcc66');
// Apply partial opacity to following draw calls.
sys.canvas.setAlpha(220);
// Draw a rounded rectangle with 10-pixel corner radii.
sys.canvas.drawRoundRect(24, 24, 160, 64, 10, 10);
// Select the stroke color and switch the paint to stroke mode.
sys.canvas.setStrokeColor('#101116');
// Use a visible line width for the diagonal stroke.
sys.canvas.setStrokeWidth(3);
// Draw a line across the rounded rectangle.
sys.canvas.drawLine(32, 92, 176, 32);Colors may be hex strings in #RRGGBB or #RRGGBBAA form, or numeric ARGB values such as 0xffff0000. Internally, Budo uses ARGB everywhere.
Transforms #
Transforms apply to drawing calls until they are changed. Use save and restore to localize a transformation.
// Save the current transform so the following changes stay local.
sys.canvas.save();
// Move the local origin to the center of the shape.
sys.canvas.translate(200, 140);
// Rotate subsequent drawing by 30 degrees.
sys.canvas.rotate(30);
// Scale subsequent drawing evenly on both axes.
sys.canvas.scale(1.2, 1.2);
// Select the rectangle fill color.
sys.canvas.setFillColor('#7c6ff7');
// Draw around the local origin, which is now translated and rotated.
sys.canvas.drawRect(-40, -30, 80, 60);
// Restore the transform that was active before this block.
sys.canvas.restore();There is also a pivot form for rotation:
// Rotate later drawing around the given center point.
sys.canvas.rotate(15, centerX, centerY);Clipping is part of the transform namespace because it belongs to canvas state. clipRect restricts subsequent drawing to a rectangle in the current coordinate system.
Paths #
Paths are runtime-managed handles. Create a path once, update it when the shape changes, and draw it whenever you need it.
// Create a path handle once and keep it for reuse.
const badge = sys.path.create();
// Start the triangle at its top point.
sys.path.moveTo(badge, 0, -42);
// Add the lower-right corner.
sys.path.lineTo(badge, 38, 24);
// Add the lower-left corner.
sys.path.lineTo(badge, -38, 24);
// Close the shape back to the top point.
sys.path.close(badge);
// Draw the badge path at a chosen position.
function drawBadge(x, y) {
// Keep translation local to this helper.
sys.canvas.save();
// Move the path origin to the requested position.
sys.canvas.translate(x, y);
// Select the fill color for the badge.
sys.canvas.setFillColor('#2dd4bf');
// Draw the path with the current paint.
sys.canvas.drawPath(badge);
// Restore the previous transform.
sys.canvas.restore();
}Use reset when a path handle should keep its identity but receive new geometry. This is helpful in hot loops because the wrapper layer can reuse the same path slot.
Text and fonts #
The runtime has a default font. You can load project-relative font files and switch the active typeface.
// Load a font file from the project directory and name it Inter.
sys.font.load('assets/Inter-Regular.ttf', 'Inter');
// Make Inter the active font for later text drawing.
sys.canvas.setFont('Inter');
// Select the text fill color.
sys.canvas.setFillColor('#f8fafc');
// Draw text with the loaded font.
sys.canvas.drawText('Loaded font', 32, 64, 26);sys.canvas.measureText returns width. sys.canvas.measureTextRect returns an object with width and height.
// Measure the bounds of the text at the chosen font size.
const box = sys.canvas.measureTextRect('Ready', 18);
// Draw a pill that is slightly larger than the measured text.
sys.canvas.drawRoundRect(24, 24, box.width + 24, box.height + 18, 8, 8);
// Draw the text inside the measured pill.
sys.canvas.drawText('Ready', 36, 24 + box.height, 18);Call sys.font.reset() to return to the runtime default font.
SVGs #
SVG files are loaded through sys.svg and represented by integer handles.
// Load an SVG file from the project directory.
const logo = sys.svg.load('assets/logo.svg');
// Draw the SVG every frame after the canvas is cleared.
function frame() {
// Clear to white so transparent SVG content is visible.
sys.canvas.clear('#ffffff');
// A non-negative handle means the SVG loaded successfully.
if (logo >= 0) {
// Draw the SVG scaled into a 96 by 96 rectangle.
sys.canvas.drawSvg(logo, 24, 24, 96, 96);
}
// Keep rendering.
sys.animation.requestFrame(frame);
}The width and height accessors return the SVG's intrinsic dimensions when available. Destroy handles you no longer need in long-lived tools that load many assets over time.
Density-aware layout #
sys.window.getDisplayDensity() returns a display scaling factor. Desktop returns 1.0. Android returns the device density, and web returns the browser-backed value used by the runtime.
For interface elements that should feel physically similar across devices, multiply measurements by density.
// Read the display density for physical-size-aware UI.
const density = sys.window.getDisplayDensity();
// Scale the margin by density.
const margin = 16 * density;
// Scale the corner radius by density.
const radius = 10 * density;
// Scale the font size by density.
const fontSize = 18 * density;
// Draw a density-aware button background.
sys.canvas.drawRoundRect(margin, margin, 220 * density, 56 * density, radius, radius);
// Draw density-aware button text.
sys.canvas.drawText('Start', margin + 18 * density, margin + 36 * density, fontSize);For game worlds and simulations, it is often better to keep your own logical coordinate system and scale once at the edge. Both styles work; the important thing is to choose deliberately.
Reading pixels #
sys.canvas.readPixels() reads the current canvas into CPU memory as { width, height, pixels } in JavaScript.
// Clear the canvas to a known color before the readback.
sys.canvas.clear('#ffffff');
// Select a red fill color.
sys.canvas.setFillColor('#ff0000');
// Draw a small red square into the top-left corner.
sys.canvas.drawRect(0, 0, 20, 20);
// Read the current canvas pixels back to CPU memory.
const shot = sys.canvas.readPixels();
// Wrap the returned ArrayBuffer so individual bytes are easy to inspect.
const bytes = new Uint8Array(shot.pixels);
// Log the image dimensions and byte count.
console.log(shot.width, shot.height, bytes.length);Readback is powerful for screenshots, tools, tests, and one-off analysis. It is not a good per-frame habit in performance-sensitive rendering because it forces synchronization between GPU and CPU.