Love2D reaches the browser through 2dengine's love.js player — a standalone player that runs .love files in the browser via an Emscripten build of LÖVE 11.5. No compile step: just package your game as a .love and drop the player's player.js, love.js, and love.wasm alongside.
Build for web
- Download the 2dengine/love.js repo and extract it — call the extracted dir
LOVEJS_DIST. It must containplayer.js,style.css,11.5/love.js,11.5/love.wasm, andlua/normalize1.lua+lua/normalize2.lua. - Package your game as a
.love(zipconf.lua,main.lua, and any other modules). - Copy
player.js,style.css, the11.5/folder, and thelua/folder alongside your.loveand anindex.htmlthat loadsplayer.js?g=your-game.love&v=11.5.
The example's build.sh does all this for you — see example-love2d/build.sh.
SDK integration
2dengine's love.js ships a JS-interop trick rather than a true FFI: Lua can synchronously evaluate JavaScript by calling os.execute("javascript:<code>"). Internally that hops love.system.openURL → window.open (intercepted by player.js) → eval. The result is written to window._output, which io.read() can read back via player.js's window.prompt override. normalize1.lua glues all of that together, so os.execute returns the evaluated string and no JS shim is needed.
-- wavedash.lua
local M = {}
function M.init()
os.execute([[javascript:
window.WavedashJS && window.WavedashJS.init({ debug: true })
]])
end
function M.update_load_progress(fraction)
local clamped = math.max(0, math.min(1, fraction or 0))
os.execute(string.format([[javascript:
window.WavedashJS && window.WavedashJS.updateLoadProgressZeroToOne(%f)
]], clamped))
end
return M
Call it from the game
At the end of love.load:
local wavedash = require("wavedash")
function love.load()
-- ...game setup...
wavedash.update_load_progress(1)
wavedash.init()
end
Adding more SDK methods
Embed the arguments directly into the JS literal — %q quotes strings safely for Lua-into-JS string contexts:
function M.submit_score(name, score)
os.execute(string.format([[javascript:
window.WavedashJS && window.WavedashJS.getLeaderboard(%q).then((lb) => {
if (lb.success) window.WavedashJS.uploadLeaderboardScore(lb.data.id, %d, true);
})
]], name, score))
end
Then call from Lua: wavedash.submit_score("level-1", 3500).
For methods that need to return data back to Lua, append a return to the JS string and read it with io.read():
function M.get_username()
os.execute([[javascript:'return ' + (window.WavedashJS ? window.WavedashJS.getUsername() : '')]])
return io.read() or ""
end
Async (Promise-returning) SDK methods are the ceiling of this approach — you'd need a JS-side dispatcher that resolves into window._output before Lua reads it.
wavedash.toml
game_id = "YOUR_GAME_ID_HERE"
upload_dir = "./build"
love.js 11.5 bundles the entire LÖVE runtime (~10-15MB). Compress images and prefer OGG for audio to keep the initial payload small.