BambooBasic can compile your program to run in a web browser. Instead of producing a native executable, the web targets transpile your .bam source to JavaScript and run it on top of a web runtime library: BBRuntimeWeb.js, which draws with WebGPU, or BBRuntimeWebGL2.js, which draws with WebGL2 (see Choosing a Web Target). Your game runs on a canvas in the browser, with no plugin or install required.
The web runtime aims to behave like the desktop runtime, so most 2D programs port with only small changes. But a browser is a very different environment to a native window, and a few things work differently by necessity. This guide covers the differences and the best practices for a clean port.
Note: Not all of the desktop runtime has been ported to web. See What Is Not Available below.
There are two web targets. They run the same programs: the same functions, the same behaviour and, as near as makes no difference, the same pictures. They differ in what draws the picture, and so in which players can run your game.
| Web (WebGPU) | Web (WebGL2) | |
| Import | BBRuntimeWeb.decls | BBRuntimeWebGL2.decls |
| Runtime | BBRuntimeWeb.js | BBRuntimeWebGL2.js |
| Runs in | Recent Chrome, Edge and Safari on graphics from about 2015 on; Firefox and Linux browsers vary | Practically every browser, including Linux, older laptops and weak graphics |
| Shader files | WGSL (.wgsl) | GLSL (.glsl) |
| Best for | An audience you know has modern hardware; the faster of the two when there's a lot on screen | A casual, wide audience: "it just has to run" |
Which runs is your choice, made when you build — the page never switches from one to the other on its own. A WebGPU game in a browser without WebGPU shows a message saying so in place of the game; if your players might be on such browsers, build for WebGL2.
In the editor, choose Web (WebGPU) or Web (WebGL2) from the Target menu and import that target's decls at the top of your program, then Build and Run as usual. The transpiler will:
The Import line decides which runtime the page uses. If the Target menu doesn't match it, the editor offers to switch the menu before building.
The HTML page is generated only once. If a <name>.html already exists it is left alone, so you can freely customise the page (add a title graphic, description, styling) and your edits will survive future builds. Moving a program to the other web target changes just the page's one <script> line for the runtime, and removes the other runtime from WebOutput.
Moving a program between the two: change its Import line, and if it uses shaders, give it the other language's shader files (see Shaders). Nothing else changes.
Examples: web examples end in _web for WebGPU and _wgl for WebGL2 — for instance MeshGLB_web.bam and MeshGLB_wgl.bam in examples/BambooRuntime/26 - 3D in 2D are the same program for each.
This is the single biggest difference from desktop. A native program owns its main loop:
; DESKTOP - the program owns the loop
Function Main()
SetupGame()
While running
b2dCls()
UpdateGame()
RenderGame()
b2dFlip()
Wend
b2dEnd()
EndFunction
A browser cannot work this way — an infinite loop would freeze the page. Instead, the browser owns the loop and calls your code once per frame. You provide a function named WebLoop, and start the loop with b2dStartWebLoop():
; WEB - the browser drives the loop, calling WebLoop each frame
Function Main()
b2dGraphics(WIDTH, HEIGHT, 3) ; must be in Main - see below
SetupGame()
b2dStartWebLoop() ; hand control to the browser
Return 0
EndFunction
Function WebLoop()
b2dCls()
UpdateGame()
RenderGame()
b2dFlip()
EndFunction
To convert a desktop program, move the body of your While…Wend loop into a WebLoop() function, and replace the loop itself with a call to b2dStartWebLoop(). Use b2dStopWebLoop() to stop it (a browser tab cannot be closed by the program — stopping the loop simply leaves the last frame on screen).
b2dGraphics starts the graphics (WebGPU or WebGL2) and is asynchronous. The transpiler automatically awaits it and marks Main as an async function. However, only Main is made async — so if you call b2dGraphics from a setup helper, you will get a JavaScript error ("await is only valid in async functions"). Always call b2dGraphics directly from Main, before any other graphics or asset calls.
On desktop, loading an image or sound blocks until it is ready. In a browser, files are fetched over the network in the background. b2dLoadImage, b2dLoadFont, b2dLoadAnimImage, audLoadSound and audLoadMusic all return a handle immediately, but the data arrives some frames later.
This means:
Poll for readiness before you use an asset's data or play it:
; Load in setup (returns immediately)
music = audLoadMusic("Media/theme.ogg")
; Start it once ready, inside WebLoop
If musicStarted = 0 And audSoundLoaded(music) = 1 Then
audPlayMusic(music)
audSetMusicVolume(music, 0.3)
musicStarted = 1
EndIf
| Check | Returns 1 when |
| b2dImageLoaded(image) | An image or anim image has finished loading |
| audSoundLoaded(sound) | A sound or music buffer has finished decoding |
| b2dShaderLoaded(shader) | An image/text shader has compiled |
| b2dScreenShaderLoaded(shader) | A screen shader has compiled |
Browsers block all audio until the user first interacts with the page (a click or key press). This is a browser security rule, not a BambooBasic limitation. The runtime automatically resumes audio on the first interaction, so audio "just works" from the first click onward — but any sound you try to play before the user has interacted will be silent.
Best practice: add a "Click to start" prompt to your HTML page, or simply design your title screen so the first thing the player does is click a button. A welcome sound played at startup will not be heard; trigger such sounds from a button instead.
Keyboard and mouse work as on desktop (inpIsKeyDown, inpIsKeyHit, inpGetMouseX, inpIsMouseHit, etc.). A few notes:
Game controllers work as on desktop: the same inpCreateController, inpControllerButtonDown/Hit, D-pad, trigger, stick and vibration functions, the same XINPUT_GAMEPAD_* button constants, ports 0–3, and the same deadzones and trigger threshold. sysUpdateEvents() updates them each frame (programs that don't call it get them updated at b2dFlip). A few browser rules apply:
See Controller_Down_web and Controller_Hit_web in examples/BambooRuntime/3 - Input.
Browsers can't compile HLSL, so web shaders are written in the language of the web target: WGSL (WebGPU Shading Language) for Web (WebGPU), GLSL (GLSL ES 3.00) for Web (WebGL2). The shader system is otherwise the same: the same functions, the same indexed parameter scheme (b2dSetImageShaderFloat(sh, "0", value) writes slot 0, of 64), and image/text/screen shaders all work.
Provide a .wgsl or .glsl file alongside (or instead of) your .hlsl file and name it explicitly. The other languages' files are refused with a message in the browser console, and the draw goes on without the shader.
; Desktop
glow = b2dLoadImageShader("Media/Glow.hlsl")
; Web (WebGPU)
glow = b2dLoadImageShader("Media/Glow.wgsl")
; Web (WebGL2)
glow = b2dLoadImageShader("Media/Glow.glsl")
Shader files in your program's folder are copied next to the page by the build (those for the target in use); ones in the Media folder go with the rest of the Media.
The runtime prepends a preamble to every web shader, so a .wgsl file only needs to define its @fragment fn main. The preamble provides:
// Web image shader (Glow.wgsl) - the runtime supplies the rest
@fragment
fn main(input: PSInput) -> @location(0) vec4<f32> {
let intensity = param(0);
var c = textureSample(imageTexture, samplerState, input.texCoord);
c = vec4<f32>(c.rgb * (1.0 + intensity), c.a * input.alpha);
return c;
}
Gotcha: WGSL forbids the derivative-based textureSample after any non-uniform branch (for example an early return whose condition varies per pixel, as in a screen shader that discards outside a curved edge). In that case use textureSampleLevel(tex, samp, uv, 0.0) instead — level 0 is correct for the non-mipmapped frame buffer.
A .glsl file is a GLSL ES 3.00 fragment shader without its first lines: the runtime puts a preamble before it (the #version line, precision, the uniforms), so the file only needs its void main() and any helper functions. The preamble provides:
// Web image shader (Glow.glsl) - the runtime supplies the rest
void main() {
float intensity = param(0);
vec4 c = texture(imageTexture, texCoord);
fragColor = vec4(c.rgb * (1.0 + intensity), c.a * alpha);
}
A WGSL shader translates line for line: let/var become typed declarations, vec4<f32> becomes vec4, textureSample(t, s, uv) and textureSampleLevel(t, s, uv, 0.0) become texture(t, uv) and textureLod(t, uv, 0.0), and return of the colour becomes fragColor = …. GLSL doesn't refuse texture() after a per-pixel early return as WGSL does, but use textureLod there all the same — it's the one that's always right. See CRT.glsl beside ScreenShader_wgl.bam in examples/BambooRuntime/9 - Shaders.
There is no real filesystem in a browser, so the file functions are backed by the browser's localStorage. The handle-based API (fileWriteFile, fileReadFile, fileWriteInt, fileReadLine, fileSaveString, etc.) works as on desktop, which makes save games and settings portable. Two things to know:
fileEof() matches C's behaviour: it becomes true only after a read runs past the end, not on reaching it. A While Not fileEof() read loop therefore gets one final iteration that returns an empty value — guard on the returned value, not the loop condition:
While Not fileEof(f)
line$ = fileReadLine(f)
If line$ <> "" Then
; ... process line$ ...
EndIf
Wend
To move a file between the web file store and the user's own computer (a save game to keep, a level to load), use fileExportWeb(filepath) — the browser downloads the stored file, usually into the Downloads folder — and fileImportWeb(filter), which opens the browser's file picker and stores the chosen file under its own name. Import can't wait for the user, so check fileImportWebResult() each frame: it gives "" until something happens, then the file's name (or "*cancelled*") once:
If inpIsKeyHit(VKEY_L) Then fileImportWeb(".sav")
Local name$ = fileImportWebResult()
If name$ <> "" And name$ <> "*cancelled*" Then LoadGame(name$)
Browsers only open a file picker straight after a click or key press, so call fileImportWeb from one. These are web only. BGI programs can also use bgiExportFileWeb / bgiImportFileWeb, which answer with a BGI event instead (see the BGI section below).
Directory functions and ZIP functions are not available on web.
The BGI GUI toolkit works on the web. Windows, buttons, lists, menus, tabs, tree and list views, rich text boxes, canvases and the rest are drawn inside the page's canvas, in the same classic Windows look as the desktop, using Dear ImGui compiled to WebAssembly (bbimgui.js). The same bgi functions, gizmo handles and events work, so most of a desktop GUI program carries over as it is. The GUI layer is only loaded when a program calls bgiInitBGI.
Starting a new one: File > New > BGI Application asks Desktop or Web. Web writes a template with the loop in WebLoop(), importing the decls of the web target chosen in the Target menu (it switches to Web (WebGPU) if neither web target is chosen), and the designer keeps its event dispatcher up to date inside WebLoop().
The body of the desktop While loop moves into WebLoop(), exactly as for any web program. Call bgiFlushEvent() at the end of each WebLoop: an event stays current until it is flushed, so without it the same click is handled every frame. On the web, events queue up instead of overwriting each other, so none are lost.
Function Main()
bgiInitBGI("Arial", 16)
window = bgiCreateWindow("My Window", 400, 300)
button = bgiCreateButton("Click me", 20, 20, 100, 30, window)
b2dStartWebLoop()
Return False
EndFunction
Function WebLoop()
sysUpdateEvents()
If bgiGetEventMessage() = BGI_EVENT_COMMAND_CLICK And bgiGetEventSource() = button Then
Print "Clicked!"
EndIf
bgiFlushEvent()
EndFunction
A web page can't close itself, so the close box works a little differently:
A browser can't stop and wait for an answer, so message boxes and the open, save, colour and font dialogs return at once (0 or "") and the answer arrives later as an event: message BGI_EVENT_COMMAND_CLICK, source = the window passed in, bgiGetEventData() = 1 for OK/Yes or 0 for Cancel/No. The event ID says which dialog answered (the constants are in BBR_INCLUDE.bam):
| Event ID | From | bgiGetEventExtra() |
| BGI_EVENT_ID_DIALOG ($114) | bgiMessageBox, bgiConfirm, bgiNotify | the title |
| BGI_EVENT_ID_FILEDIALOG ($115) | bgiOpenFileDialog, bgiSaveFileDialog | the file name |
| BGI_EVENT_ID_COLORDIALOG ($116) | bgiOpenColorDialog | "RGB(r,g,b)" (also bgiRequestedRed etc.) |
| BGI_EVENT_ID_FONTDIALOG ($117) | bgiOpenFontDialog | "Face,Size" (also bgiGetSelectedFont*) |
| BGI_EVENT_ID_IMPORTFILE ($118) | bgiImportFileWeb | the stored file name |
; Ask...
If eventSource = btnOpen Then bgiOpenFileDialog("Open", "", window)
; ...and handle the answer when it comes
If eventMsg = BGI_EVENT_COMMAND_CLICK And eventID = BGI_EVENT_ID_FILEDIALOG Then
If bgiGetEventData() = 1 Then LoadMyFile(bgiGetEventExtra())
EndIf
IGI (the igi functions) works the same in the browser as on the desktop. Its module loads on the first igi call, so the GUI appears a few frames after the game (styling set up in Main is kept until then), igiLoadFont fetches its font in the background, and Ctrl+V pastes into IGI text boxes. IGI draws on top of the game, after any screen shader, and BGI windows go on top of IGI.
The web runtime currently covers 2D graphics, movie playback, audio, input (game controllers included), math, strings, timing, file I/O and the BGI GUI. The following are not (yet) ported:
| Area | Status on Web |
| BGI HTMLView and folder dialog | Not possible in a browser — see BGI |
| Networking (TCP/UDP) | Not ported |
| sysExecute, environment variables, pack files, sysSystemProperty | No browser equivalent |
| sysDelay | No-op — a browser cannot block |
| Directory and ZIP file functions | Not available (no real filesystem) |
For web projects, import BBRuntimeWeb.decls (WebGPU) or BBRuntimeWebGL2.decls (WebGL2) instead of BBRuntime64.decls. Only the ported functions are declared there (the same ones in both), so the transpiler will catch any desktop-only call at build time.
Everything the browser needs is in the WebOutput folder: the HTML page, your transpiled .js, the runtime (BBRuntimeWeb.js or BBRuntimeWebGL2.js), the Media folder and any shader files (plus, for GUI programs, bbimgui.js and the Liberation Sans fonts). Upload that folder to any static web host and it will run — no server-side code is required.
Because assets are fetched over HTTP, the page must be served by a web server (the editor starts one for you locally). Opening the HTML file directly from disk with a file:// URL will fail, as browsers block fetch from local files.
BambooBasic © 2026 Michael Denathorn