frankiecanvas
Introduced in v1.20, with real image sprites and a WebAudio synth API added in v1.21. The browser renderer for Frankie games — same core API as frankiegame, but your game gets real pixels: an HTML5 canvas served by the built-in web server, frames streamed over WebSockets, keyboard and mouse events streamed back. Multiplayer for free — every connected browser is a player, built on the v1.18 WebSocket architecture.
Quick Start
stitch "frankiecanvas"
g = game_new(width: 32, height: 20, fps: 30, title: "My Game")
on_key(g, "left") do |game|
game["px"] -= 1
end
on_player_key(g) do |game, player, key| # canvas-only: per-player input
move_paddle(game, player, key)
end
on_tick(g) do |game|
clear(game)
rect(game, 2, 18, 3, 1, "green") # canvas-only: colored rects
draw(game, 10, 4, "🧟") # shared: cell glyphs
text(game, 1, 0, "score 3", color: "yellow")
end
run_game(g) # → http://localhost:4100
Open the printed URL in a browser tab — for multiplayer games, open it in a second tab too.
Coordinates
Coordinates are cells, exactly like the terminal renderer, scaled up by cell: pixels (default 22) when drawn to the canvas.
Keys and Mouse
Key events use the same key names as frankiegame — arrows, space, enter, esc, letters. Mouse clicks arrive as "click:x,y" through on_any_key or on_player_key.
Multiplayer
Every browser tab that connects is a player, numbered 1, 2, 3… in connection order:
on_player_key(g) do |game, player, key|
# player is 1, 2, 3... — route input per-player here
end
puts players_count(g) # how many browsers are connected right now
This is the same mechanism as WebSockets — no extra code required for multiplayer to work.
Headless / CI Mode
Same as frankiegame: pass max_ticks: to bound the loop for automated test runs.
API Reference
game_new(width: 32, height: 20, fps: 30, title: "Frankie Game", port:, cell: 22, bg:, max_ticks: 0)
Creates the game state hash and configures the served canvas page.
clear(g)
Starts the frame from the background color.
draw(g, x, y, s, color: nil)
Draws a string of glyphs at cell (x, y). Shared with frankiegame.
text(g, x, y, s, color: nil)
Alias of draw — reads nicely for HUDs.
rect(g, x, y, w, h, color) (canvas-only)
Filled rectangle in cell units.
draw_sprite(g, x, y, sprite, color: nil)
Multi-line sprite; spaces are transparent, same as the terminal renderer.
load_image(path) (v1.21)
Reads a PNG, JPEG, or GIF file and returns an image handle ({src: "data:image/...;base64,..."}) for use with draw_image. Call it once, outside the game loop — it hits disk and Base64-encodes the file:
zombie = load_image("zombie.png") # once, before run_game
Internally this uses the new file_read_base64 stdlib primitive, since file_read is text-mode and would corrupt binary image data.
draw_image(g, x, y, image, w: 1, h: 1) (v1.21)
Draws a loaded image at cell (x, y), sized w × h cells:
on_tick(g) do |game|
clear(game)
draw_image(game, 10, 4, zombie, w: 2, h: 2)
end
Every connected browser decodes and caches the image the first time it's drawn — later frames just resend the coordinates, not the image data. The same call also works against frankiegame, which renders a ▒ placeholder block since a terminal can't show real pixels — see frankiegame.
collide?(x1, y1, w1, h1, x2, y2, w2, h2)
Axis-aligned bounding box overlap test.
synth_play(g, freq: 440, wave: "sine", dur: 0.08, gain: 0.06) (v1.21)
Plays a real WebAudio oscillator tone in every connected browser. wave is "sine", "square", "sawtooth", or "triangle":
synth_play(g, freq: 220, wave: "sawtooth", dur: 0.2, gain: 0.08)
frankiegame also has synth_play — it maps to the terminal bell, with parameters accepted but ignored, so the same call compiles against either engine.
game_beep(g)
A WebAudio blip in every connected browser. Since v1.21 this is a thin wrapper over synth_play(g, freq: 520, wave: "square", dur: 0.07, gain: 0.06) — behavior is unchanged.
on_key(g, key) do |game| ... end
Handle one key — shared across all connected players.
on_any_key(g) do |game, key| ... end
Handle every key press from any player.
on_player_key(g) do |game, player, key| ... end (canvas-only)
Per-player input — player is 1, 2, 3… in connection order. This is where multiplayer input lives.
on_tick(g) do |game| ... end
Called once per frame — draw here.
stop_game(g)
Ends the loop.
players_count(g) (canvas-only)
How many browsers are connected right now.
run_game(g)
Serves the game and enters the loop (blocking). Open http://localhost:PORT — every tab is a player.
Examples
Two bundled showcase projects use frankiecanvas:
frankiec examples zombie_invaders # rects, sprites, waves, beeps
frankiec examples pong # multiplayer via on_player_key — two tabs, one ball
Quick Reference
| Function | Description |
|---|---|
game_new(width:, height:, fps:, title:, port:, cell:, bg:, max_ticks:) |
Create the game state + canvas server |
clear(g) |
Reset the frame |
draw(g, x, y, s, color: nil) / text(g, x, y, s, color: nil) |
Draw glyphs |
rect(g, x, y, w, h, color) |
Filled rectangle (canvas-only) |
draw_sprite(g, x, y, sprite, color: nil) |
Multi-line sprite, transparent spaces |
load_image(path) |
Load a PNG/JPEG/GIF for draw_image (v1.21) |
draw_image(g, x, y, image, w: 1, h: 1) |
Draw a loaded image, client-cached after first draw (v1.21) |
collide?(x1, y1, w1, h1, x2, y2, w2, h2) |
AABB collision test |
on_key / on_any_key |
Shared input handlers |
on_player_key(g) do \|game, player, key\| ... end |
Per-player input (canvas-only) |
on_tick(g) do \|game\| ... end |
Called once per frame |
players_count(g) |
Connected browser count (canvas-only) |
synth_play(g, freq:, wave:, dur:, gain:) |
Real WebAudio oscillator tone (v1.21) |
game_beep(g) |
WebAudio blip in every tab — now a synth_play wrapper |
stop_game(g) |
End the loop |
run_game(g) |
Serve + enter the blocking loop |