Drawing and Layout #
Budo's drawing API is built around a global sys.canvas namespace and a small set of stateful helpers: sys.transform, 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');
// Draw once at the origin to get the rendered text width.
const width = sys.canvas.drawText('Centered', 0, 0, 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.
Advanced paint calls such as blend modes, image filters, and color filters depend on the advanced Skia surface. Check sys.capabilities.shadersAdvanced.available before relying on them in code that might run on a target without those imports.
// Check the capability before using advanced Skia paint effects.
if (sys.capabilities.shadersAdvanced.available) {
// Multiply following drawing with pixels already on the surface.
sys.canvas.setBlendMode('multiply');
// Blur following drawing with a 4-pixel radius on both axes.
sys.canvas.setImageFilter('blur', 4, 4);
}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.transform.save();
// Move the local origin to the center of the shape.
sys.transform.translate(200, 140);
// Rotate subsequent drawing by 30 degrees.
sys.transform.rotate(30);
// Scale subsequent drawing evenly on both axes.
sys.transform.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.transform.restore();There is also a pivot form for rotation:
// Rotate later drawing around the given center point.
sys.transform.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.transform.save();
// Move the path origin to the requested position.
sys.transform.translate(x, y);
// Select the fill color for the badge.
sys.canvas.setFillColor('#2dd4bf');
// Draw the path with the current paint.
sys.path.draw(badge);
// Restore the previous transform.
sys.transform.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.font.set('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.svg.draw(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.flush() submits pending Skia work to the GPU. 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.