Skip to content

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