BambooBasic Logo


Building for the Web

BambooBasic can compile your program to run in a web browser. Instead of producing a native executable, the web target transpiles your .bam source to JavaScript and runs it on top of BBRuntimeWeb.js, a WebGPU-based runtime library. 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.



Selecting the Web Target

In the editor, choose the Web (WebGPU) build target from the Target menu, then Build and Run as usual. The transpiler will:

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.



The Web Loop

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).


Call b2dGraphics from Main, not a helper

b2dGraphics initialises WebGPU 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.



Assets Load Asynchronously

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


Audio Needs a User Interaction

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.



Input Handling

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.



Shaders: WGSL, not HLSL

WebGPU cannot compile HLSL, so web shaders are written in WGSL (WebGPU Shading Language). 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 file alongside (or instead of) your .hlsl file and name it explicitly:

; Desktop
glow = b2dLoadImageShader("Media/Glow.hlsl")
; Web
glow = b2dLoadImageShader("Media/Glow.wgsl")

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.



File System: Backed by localStorage

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.



BGI: GUI Programs on the 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() and switches the build target to Web, and the designer keeps its event dispatcher up to date inside WebLoop().


Setting up
The event loop

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

Closing windows

A web page can't close itself, so the close box works a little differently:


Dialogs don't wait

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, bgiNotifythe title
BGI_EVENT_ID_FILEDIALOG ($115)bgiOpenFileDialog, bgiSaveFileDialogthe 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)bgiImportFileWebthe 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

Canvases and rich text
Not available in BGI on the web

IGI: the In-Game GUI on the Web

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.



What Is Not Available

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 dialogNot possible in a browser — see BGI
Networking (TCP/UDP)Not ported
sysExecute, environment variables, pack files, sysSystemPropertyNo browser equivalent
sysDelayNo-op — a browser cannot block
Directory and ZIP file functionsNot available (no real filesystem)

For web projects, import BBRuntimeWeb.decls instead of BBRuntime64.decls. Only the ported functions are declared there, so the transpiler will catch any desktop-only call at build time.



Porting Checklist

Deploying

Everything the browser needs is in the WebOutput folder: the HTML page, your transpiled .js, BBRuntimeWeb.js, the Media folder and any .wgsl shaders (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