BeepMicro is a tiny, modular JavaScript vector-game library for real-time arcade games.
Build the complete library:
npm run buildBuild a tailored bundle. Dependencies are included automatically:
npm run build -- collision --out build/beepmicro.min.js
npm run build -- renderer input collision particles audio --out build/beepmicro.min.jsCustom builds require --out, keeping dist/ reserved for the full
distributable library build.
Build directly from a game script. The builder scans ordinary bm.Name API
references, selects their supplying modules, and includes dependencies
automatically:
npm run build -- --game examples/invaders.js --out examples/build/beepmicro.min.jsGame builds require --out and write one compact JavaScript file at that
location. It contains both the selected BeepMicro modules and the game script,
minified together, so dist/ remains reserved for normal library builds. The
command prints both the detected APIs and module order. The scan intentionally
only follows statically written references such as bm.Draw and bm.Input;
if a game accesses an API through bm[ name ], use a normal explicit module
build.
For an extra size-focused jam build, add --jam:
npm run build -- --game game.js --out build/game.min.js --jamThis runs three Terser compression passes and renames every non-browser property that is accessed statically. Use it only when the generated file is the complete application. Dynamic property access, externally supplied JSON, and old localStorage data can rely on names that the build changes. Browser APIs and quoted property names are deliberately preserved.
The complete build writes dist/beepmicro.js and dist/beepmicro.min.js.
Source JSDoc and explanatory comments are removed from the compact output.
bm.frame() runs fixed game updates at 60Hz by default, then renders once per
display refresh. This keeps movement and collision consistent while allowing
fast displays to present more often. A maximum of five catch-up updates is run
after a slow frame, avoiding a runaway loop on a busy device.
bm.CONFIG[ CONFIG_UPDATE_RATE ] = 60;
bm.CONFIG[ CONFIG_MAX_UPDATES_PER_FRAME ] = 5;Scene-driven games receive this automatically: the active scene's update(dt)
is called for each fixed update and its render() is called for every display
frame. bm.Core.interpolation exposes the fraction before the next update for
future interpolation helpers.
Build the optional screenshot module with
npm run build -- screenshot --out build/beepmicro.min.js to press 0
and download the current canvas as a timestamped PNG. Change
bm.CONFIG[ CONFIG_SCREENSHOT_KEY ] after loading the module if a game needs a different
shortcut. The complete bundle includes screenshots; tailored jam builds omit
them unless bm.screenshot() is used.
bm.init() applies touch-action: manipulation to its game canvas and uses a
small iOS fallback to prevent a rapid double tap from zooming the page. Touch
events still reach the canvas, so games may safely use taps and double taps as
their own controls.
Input is action-based: keyboard and gamepad controls feed the same action, so
bm.Input.held( 'up' ) is true for ↑, W, D-pad up, or the
left stick pushed upward. The common actions need no setup:
| Action | Keyboard | Standard gamepad |
|---|---|---|
left, right, up, down |
Arrow keys or WASD | D-pad or left stick |
buttonA, buttonB |
X / C | A / B |
fire, action, launch, jump, shoot, play, next |
Space | A |
select, confirm |
Enter or Space | A |
restart, pause |
R / Escape | Start |
back, cancel |
Escape | B |
The buttonA and buttonB names intentionally leave short action names free
for literal keyboard letters. For example, bind a to the physical A key:
bm.Input.bind( 'a', 'KeyA' );For example, this works with all four movement methods without a binding call:
if ( bm.Input.held( 'left' ) ) player.x -= speed * deltaTime;
if ( bm.Input.pressed( 'jump' ) ) jump();
if ( bm.Input.pressed( 'buttonA' ) ) useSpecialAction();bm.Input.bind( action, keys ) replaces only an action's keyboard defaults;
its gamepad mapping remains active. Custom gamepad actions can use
bm.Input.bindGamepad().
Vector strokes retain the same on-screen thickness as assets scale. The default is 1.5 pixels and may be set globally or per draw:
bm.CONFIG[ CONFIG_LINE_WIDTH ] = 2;
bm.Draw.asset( ship, x, y, { scale: 4, width: 1 } );Assets, lines, curves, and circles outside the logical canvas are skipped automatically.
Assets use a radius calculated once when they are created, so rotated or scaled
objects remain safe to cull. CULL_BUFFER keeps drawing slightly beyond the
edge, avoiding visible popping for fast-moving objects:
bm.CONFIG[ CONFIG_CULL_BUFFER ] = 48; // Default: 32 logical pixelsObjects supplied as ordinary bm.asset() assets are covered automatically.
The optional lineStyles module adds two fixed arcade-style outline presets.
They work with assets, lines, curves, circles, and vector text. Their spacing stays
the same on screen when an asset is scaled, and grows proportionally with line
width so every preset keeps its intended look:
bm.Draw.line( 30, 90, 610, 90, { style: 'dashed' } );
bm.Draw.curve( 30, 150, 320, 40, 610, 150, { style: 'dotted' } );
bm.Draw.circle( 320, 180, 60, { style: 'dotted' } );Use solid (the default), dashed, or dotted. The presets intentionally do
not expose custom lengths, gaps, or offsets. Literal style: 'dashed' and
style: 'dotted' options are detected automatically in game builds; otherwise
include lineStyles explicitly.
All library defaults live in src/config.js. bm.CONFIG is a
compact array, while named CONFIG_* constants provide readable indexes. For
example, change the default clear colour for a game:
bm.CONFIG[ CONFIG_BACKGROUND ] = '#101020';
bm.Draw.clear();Draw.clear( '#101020' ) still overrides the background for one frame.
CANVAS_WIDTH, CANVAS_HEIGHT, and RESOLUTION define the default 640×360
logical game space and 2× physical output. When passing an existing <canvas>,
its width and height attributes provide the logical size unless bm.init()
receives explicit dimensions.
Keep game coordinates at 640×360 while rendering and screenshots use more
physical pixels with resolution. The default is 2:
bm.init( {
canvas: document.querySelector( '#game' ),
resolution: 2, // 1280×720 output, still using 640×360 game coordinates
} );The engine does not set CSS dimensions: use your own stylesheet to make the canvas fill its page, constrain it, or otherwise control its displayed size.
Use bm.getWidth() and bm.getHeight() (or bm.Core.width and
bm.Core.height) for logical game dimensions. The canvas's width and
height attributes report the physical output size.
Change the centred world-camera scale without changing logical coordinates, physics, collision, or level data:
bm.CONFIG[ CONFIG_ZOOM ] = 1.5;The default is 1. Values below 1 pull the camera back and reveal more world
coordinates; values above 1 zoom in. Pointer and touch positions are converted
back through the same zoom, so existing game input continues to use normal
logical coordinates.
Draw fixed interface elements with screen: true; menus and high-score tables
already do this by default:
bm.Draw.text( 'SCORE 100', 20, 20, { screen: true } );bm.Draw.textWidth( text, options ) returns the on-screen width of the active
text backend. It is useful when placing UI elements beside centred text.
The optional shapes module provides ordinary vector assets ready for drawing
or particles. They are especially handy for quick effects:
bm.Particles.add( {
asset: bm.Shapes.polygon( 5, 4 ),
x: 320, y: 240, vx: 80, life: .5,
} );Use bm.Shapes.dot, bm.Shapes.line( length ), bm.Shapes.square( size ),
bm.Shapes.cross( size ), or bm.Shapes.polygon( sides, radius ).
Add a solid interior behind the normal vector outline with fill.
Assets fill only paths whose final point repeats their first point, leaving open
line artwork unchanged. Circles are always closed:
bm.Draw.asset( bm.Shapes.square( 30 ), x, y, {
colour: '#77e8ff',
fill: '#153f59',
} );
bm.Draw.circle( x, y, 20, { colour: '#ffffff', fill: '#401c45' } );Use forceFill: true when an intentionally open path should still fill by
closing its interior visually; its outline remains unchanged.
Rotate an asset around its registration point by supplying an angle in radians:
bm.Draw.asset( ship, x, y, { angle: Math.PI / 2 } );Set originX and originY when creating the asset to choose its rotation pivot.
Convert pointer events with bm.pointerPosition(event). This accounts for CSS
canvas scaling and returns the game’s internal canvas coordinates.
All UI-facing code—including menus and high scores—uses bm.Draw.text() and
bm.Draw.textWidth(). Choose its backend once before starting the game:
bm.CONFIG[ CONFIG_TEXT_RENDERER ] = 'canvas';
bm.CONFIG[ CONFIG_TEXT_FONT ] = '16px monospace';The default is vector. When building from a game script, the builder detects
CONFIG[ CONFIG_TEXT_RENDERER ] = 'canvas' and includes the canvas backend
instead of the vector font data. This keeps menu and high-score UI available in
small games.
The optional vectorText module adds bm.Draw.vectorText() and the compact
built-in capitals font through the generic text facade. It depends on the
font-data module, so canvas-text games do not include glyph data. Text is
converted to uppercase, and spaces simply use the font's normal advance without
drawing a glyph. Glyph dimensions and their advances are calculated once when a
font loads, so narrow characters such as I naturally take less horizontal
space. Set a glyph's advance explicitly to override that result, for example
for a space or decorative punctuation.
Create a custom capitals-only font with the fourth bm.Font.create argument
set to true. Its input text is converted to uppercase automatically:
const font = bm.Font.create( glyphs, 3, 6, true );The optional canvasText module uses a browser system font without including
the vector-font data. Its direct API mirrors vectorText(): pass a Canvas 2D
font shorthand as the first argument. It draws into the vector line layer, so
normal colour, alpha, additive blending, and glow options still work:
bm.Draw.canvasText( '18px monospace', 'SCORE 100', 320, 32, {
align: 'center',
colour: '#77e8ff',
} );Use bm.Draw.canvasTextWidth( '18px monospace', text ) to measure it.
Shatter an asset into spinning, shrinking copies of its own line segments:
bm.Particles.explode( ship, shipX, shipY, { colour: '#77e8ff', speed: 140 } );
bm.shake( 8, 0.2 );Particles.explode() is supplied by the optional particlesExplode module;
ordinary Particles.add(), update(), and draw() effects only need
particles.
Particles can remain in world coordinates when a game scrolls a camera:
bm.Particles.draw( cameraX, cameraY );Fade all vector drawing together while preserving an individual asset's own
alpha option:
bm.setAlpha( 0, 0.4 ); // Fade the current scene out over 0.4 seconds.
bm.setAlpha( 1 ); // Restore normal opacity immediately.Glow is always enabled: each vector stroke uses a normal Canvas shadow, then draws its sharp outline on top. This avoids full-canvas blur passes and keeps solid fills clear. Configure it once:
bm.CONFIG[ CONFIG_LINE_WIDTH ] = 1.5;
bm.CONFIG[ CONFIG_GLOW_BLUR ] = 16;
bm.CONFIG[ CONFIG_GLOW_ALPHA ] = 1;Change it temporarily for gameplay feedback. Strength 1 is the configured
default; larger values make the bloom brighter and wider. pulseGlow() uses
its duration for the complete rise-and-return cycle:
bm.setGlow( 1.8, 0.25 );
bm.pulseGlow( 3, 0.6 );Glow normally follows the line or text colour. Override only the glow colour, or disable it for one quiet primitive without changing the global setting:
bm.Draw.circle( 80, 80, 24, { colour: '#ffffff', glow: '#ff68ad' } );
bm.Draw.line( 20, 20, 620, 20, { colour: '#314465', glow: false } );The optional vignette module creates one cached gradient corner and mirrors
it into the completed frame's four corners. It starts at 0.65 darkness; set
it from 0 (off) to 1 (black) for a game-specific look:
bm.vignette( .55 );Use bm.vignette() to turn it off again. A tailored game bundle includes this
module automatically when it finds bm.vignette(...).
The optional music module pre-schedules compact oscillator tracks. A track is
[ baseFrequency, noteLengthSteps, waveform, events ]; its event array is
[ pitch, step, pitch, step, ... ], with pitches expressed as semitone offsets
from the base frequency. Omitted events are rests, which keeps song data small.
const MUSIC = [
[ 110, 2, 'triangle', [ 0, 0, 3, 2, 7, 4, 3, 6 ] ],
[ 55, 4, 'triangle', [ 0, 0, 7, 8 ] ],
];
// Call from a key press, pointer press, or menu selection.
bm.Music.play( MUSIC, { step: .25, loop: 16, volume: .15 } );
bm.Music.play(); // Stop the current song.Music is detected automatically when a game uses bm.Music; otherwise it is
not included in a tailored build.
Open the files in examples/ through a local web server. Each is a focused
cookbook entry covering assets, input, collision, particles, glow controls,
vector fonts, audio, scenes, menus, and high scores. Complete games live in
the companion games workspace and use the same public APIs.
Build the optional high-score module with npm run build -- highscores. It saves
sorted score tables in localStorage, draws them with bm.Highscores.draw(), and
keeps the standard arcade build fully local.
Remote hooks are available only when needed through the separate
highscoresRemote module. It retains bm.Highscores.setRemoteHandlers(),
loadRemote(), and saveRemote() without adding async service code to an
offline game.
Use bm.Highscores.addPrompted( 'scores', score ) to request a player name
with the browser's built-in prompt before saving. Names are uppercased, limited
to ten alphanumeric characters, and use only A-Z and 0-9; cancelled or empty
entries are handled differently: Cancel does not save a score, while an empty
entered name uses GUEST. The most recently submitted name is remembered in
localStorage and pre-filled next time; pass a non-empty defaultName argument
to addPrompted() when a game needs to override it. Ordinary scores display
with comma thousands separators, while { time: true } tables keep their clock
format.
Give new players a score to beat with one target or a complete seeded table. Seeds are used only until that table receives its first local score, and return when the table is cleared:
bm.Highscores.setTarget( 'scores', 10000 );
bm.Highscores.setDefaults( 'scores', [
{ name: 'ACE', score: 10000 },
{ name: 'BEE', score: 7500 },
{ name: 'CAT', score: 5000 },
] );For races, survival times, or any game where lower is better, pass the same
table options to add(), addPrompted(), load(), and draw(). Times are
given in seconds and display as M:SS.CC:
const bestTimes = { time: true, order: 'asc' };
bm.Highscores.addPrompted( 'time-trial', elapsedSeconds, 10, 'GUEST', bestTimes );
bm.Highscores.draw( 'time-trial', 180, 70, { ...bestTimes, title: 'BEST TIMES' } );Tables measure their content and render rank, name, and score as separate
left-aligned columns, even with proportional fonts. Pass columnGap to
draw() to change the default 10-pixel space between columns. Empty tables
display NO SCORES YET; set emptyMessage to another string or false when a
game supplies its own empty-state message.
Build the optional scene module with npm run build -- scenes. Register named
title, play, pause, or results screens with bm.Scenes.add(), then start the
scene-driven loop with bm.Scenes.start( 'title' ).
The optional menu module provides small vertical menus for title and pause screens. It needs the renderer and input modules, which the builder includes automatically:
bm.Input.bind( 'select', [ 'Enter', 'Space' ] );
const menu = bm.Menu.create( [
{ label: 'PLAY', select: () => bm.Scenes.set( 'play' ) },
{ label: 'HIGH SCORES', select: () => bm.Scenes.set( 'scores' ) },
] );
function updateTitle() {
menu.update();
bm.Input.endFrame();
}
function renderTitle() {
bm.Draw.clear();
menu.render();
}The usual up, down, and select actions work with arrow keys/D-pad or
left stick/A (or Start). Rendered menu rows are tappable by default, including
when the canvas is CSS-scaled; use enableTouch() and disableTouch() when a
menu belongs to a scene. Build it with npm run build -- menu.