raylib-wasm
Version:
Make raylib games in JS for the web
587 lines (498 loc) • 16.1 kB
JavaScript
// this will generate the main chunk of the host
// it will be hand-edited after
import { readFile, writeFile } from 'fs/promises'
import { defines, structs, aliases, enums, callbacks, functions, mappedStructs } from './api.js'
// I use this to create the build line (to cut down on unused exports)
const exposed = ['free', 'addFile', 'globalize', 'mod']
// these are args that represent files that should be pre-loaded first
const fileFuncArgs = {
// I am not pre-loading for these, so you will have to raylib.addFile first before using these functions
// FileExists: ['fileName'],
// GetFileLength: ['fileName'],
// GetFileModTime: ['fileName'],
LoadFileData: ['fileName'],
LoadFileText: ['fileName'],
LoadFont: ['fileName'],
LoadFontEx: ['fileName'],
LoadImage: ['fileName'],
LoadImageAnim: ['fileName'],
LoadImageRaw: ['fileName'],
LoadMaterials: ['fileName'],
LoadModel: ['fileName'],
LoadModelAnimations: ['fileName'],
LoadMusicStream: ['fileName'],
LoadSound: ['fileName'],
LoadTexture: ['fileName'],
LoadWave: ['fileName']
}
// currently not used, but these are functions that output a file to fake FS
// you can raylib.mod.FS.readFile them or maybe we will wrap them later
const fileFuncRet = {
ExportDataAsCode: ['fileName'],
ExportFontAsCode: ['fileName'],
ExportImage: ['fileName'],
ExportImageAsCode: ['fileName'],
ExportMesh: ['fileName'],
ExportWave: ['fileName'],
ExportWaveAsCode: ['fileName'],
SaveFileData: ['fileName'],
SaveFileText: ['fileName'],
TakeScreenshot: ['fileName']
}
// generate a default-value for a type
function defaultValue (type) {
if (type === 'string' || type.match(/char *\[([0-9]+)\]/) || type === 'char*' || type === 'const char*' || type === 'char *' || type === 'const char *') {
return "''"
}
// array-types
const a = type.match(/([a-zA-Z 0-9]+) *\[([0-9]+)\]/)
if (a) {
const v = defaultValue(a[1])
return '[' + [...(new Array(parseInt(a[2])))].map(() => v).join(', ') + ']'
}
// structs
if (type.match(/^[A-Z]/)) {
return `new raylib.${type.replace(/\*/g, '').replace(/ /g, '')}()`
}
return 0
}
// emscripten type-converters are a bit incomplete. This makes values easier to use with c-types
// TODO: check unsigned is working right
const irTypes = ['i8', 'i16', 'i32', 'i64', 'float', 'double', '*']
const mapType = type => {
if (type.includes('*') || mappedStructs[type]) {
return '*'
}
if (type === 'int') {
return 'i32'
}
if (type === 'bool') {
return 'i1'
}
if (!irTypes.includes(type)) {
console.log(`mapType: defaulting to pointer: ${type}`)
return '*'
}
return type
}
const valGetter = (name, type) => {
if (type === 'string' || type.match(/char *\[([0-9]+)\]/) || type === 'char*' || type === 'const char*' || type === 'char *' || type === 'const char *') {
return `mod.UTF8ToString(${name})`
}
if (type === 'u8' || type === 'unsigned char') {
return `mod.HEAPU8[${name}]`
}
if (type === 'u32' || type === 'unsigned int') {
return `mod.HEAPU32[${name}]`
}
if (mappedStructs[type]) {
return `new raylib.${type}({}, mod.getValue(${name}, '*'))`
}
return `mod.getValue(${name}, '${mapType(type)}')`
}
const valSetter = (name, valueName, type) => {
if (type === 'u8' || type === 'unsigned char') {
return `mod.HEAPU8[${name}] = ${valueName}`
}
if (type === 'u32' || type === 'unsigned int') {
return `mod.HEAPU32[${name}] = ${valueName}`
}
if (type === 'string' || type.match(/char *\[([0-9]+)\]/) || type === 'char*' || type === 'const char*' || type === 'char *' || type === 'const char *') {
return `mod.stringToUTF8(${name}, ${valueName})`
}
if (mappedStructs[type]) {
return `mod.setValue(${name}, ${valueName}._address, '*')`
}
return `mod.setValue(${name}, ${valueName}, '${mapType(type)}')`
}
// get the byte-size of a type
function getSize (type) {
// pointers are 32bit
if (type.includes('*')) {
return 4
}
// arrays are size * typeSize
if (type.includes('[')) {
const t = type.split('[')
return getSize(t[0]) * t[1].split(']')[0]
}
// structs are size of all fields addded
if (mappedStructs[type]) {
return mappedStructs[type].fields.reduce((a, c) => a + getSize(c.type), 0)
}
// the rest (atoms) have a size
switch (type) {
case 'bool':
case 'char':
case 'unsigned char':
return 1
case 'int':
case 'unsigned int':
case 'float':
return 4
default:
console.log(`SIZE? ${type}`)
return 0
}
}
// indent a string
const indentString = (str, count = 2, indent = ' ') => str.replace(/^/gm, indent.repeat(count))
// create all the wasm-memory getters/setters for a struct
function outputGetters (struct) {
let offsetSize = 0
return struct.fields.map(field => {
const size = getSize(field.type)
let out = ''
if (!mappedStructs[field.type.replace(' ', '').replace('*', '')]) {
out = `
get ${field.name} () {
return ${valGetter(`this._address + ${offsetSize}`, field.type)}
}
set ${field.name} (${field.name}) {
${valSetter(`this._address + ${offsetSize}`, field.name, field.type)}
}
`
}
offsetSize += size
return out
}).join('\n ')
}
// for functions, map input/output type to JS-ish param (for cwrap/ccall auto-conversion)
function mapTypeToJs (type) {
if (type === 'const char *' || type === 'char *') {
return "'string'"
}
if (type.includes('*')) {
return "'pointer'"
}
switch (type) {
case 'bool':
return "'boolean'"
case 'int':
case 'float':
case 'double':
case 'char':
case 'long':
case 'unsigned int':
return "'number'"
default:
return "'pointer'"
}
}
let code = `
import Module from './raylib_emscripten.js'
import RaylibComponent from './raylib_wc.js'
document.addEventListener('DOMContentLoaded', () => {
window.customElements.define('raylib-game', RaylibComponent)
})
const importLocation = document?.location?.toString()
// run this function before calling anything
export async function raylib_run(canvas, userInit, userUpdate) {
const raylib = {}
const wasmBinary = new Uint8Array(await fetch(import.meta.url.replace('raylib.js', 'raylib.wasm')).then(r => r.arrayBuffer()))
const mod = await Module({canvas, wasmBinary})
raylib.mod = mod
`
for (const s of structs) {
s.fields = s.fields || []
exposed.push(s.name)
const size = s.fields.reduce((a, c) => a + getSize(c.type), 0)
code += ` // ${s.description}
raylib.${s.name} = class ${s.name} {
constructor(init = {}, _address) {
this._size = ${size}
this._address = _address || mod._malloc(this._size)
`
let offset = 0
for (const f of s.fields) {
const t = mappedStructs[f.type.replace(' ', '').replace('*', '')]
if (!t) {
code += `\n this.${f.name} = init.${f.name} || ${defaultValue(f.type)}`
} else {
code += `\n this.${f.name} = new raylib.${t.name}(init.${f.name} || {}, this._address + ${offset})`
}
offset += getSize(f.type)
}
code += `
}
${outputGetters(s)}
}\n\n`
}
for (const e of enums) {
code += ` // ENUM ${e.name}\n // ${e.description}`
for (const v of e.values) {
exposed.push(v.name)
code += `\n raylib.${v.name} = ${v.value} // ${v.description}`
}
code += '\n\n'
}
for (const c of defines.filter(c => c.type === 'COLOR')) {
exposed.push(c.name)
code += `\n raylib.${c.name} = ${c.value.replace(/CLITERAL\(Color\){ ([0-9]+), ([0-9]+), ([0-9]+), ([0-9]+) }/, 'new raylib.Color({r: $1, g: $2, b: $3, a: $4})')} // ${c.description}`
}
code += '\n\n'
for (const { name, description, returnType, params = [] } of functions) {
exposed.push(name)
const returnsStruct = mappedStructs[returnType.replace('*', '').replace(' ', '')]
const wasmParams = params.map(p => p.name)
const callParams = [...wasmParams]
const callParamTypes = params.map(p => mapTypeToJs(p.type))
for (const i in callParams) {
if (callParamTypes[i] === "'pointer'") {
callParams[i] = `${callParams[i]}._address`
}
}
code += ` // ${description}: ${name}(${params.map(p => p.type).join(', ')}) => ${returnType}\n `
if (returnsStruct) {
callParams.unshift('_ret._address')
callParamTypes.unshift("'pointer'")
code += `const _${name} = mod.cwrap('${name}', 'void', [${callParamTypes.join(', ')}])\n `
if (fileFuncArgs[name]) {
wasmParams.push('skipLoad')
code += `raylib.${name} = async (${wasmParams.join(', ')}) => {\n `
code += fileFuncArgs[name].map(a => ` !skipLoad && await raylib.addFile(${a})`).join('\n ') + '\n '
} else {
code += `raylib.${name} = (${wasmParams.join(', ')}) => {\n `
}
code += ` const _ret = new raylib.${returnsStruct.name}()
_${name}(${callParams.join(', ')})
return _ret
}
`
} else {
code += `const _${name} = mod.cwrap('${name}', ${mapTypeToJs(returnType)}, [${callParamTypes.join(', ')}])\n `
if (fileFuncArgs[name]) {
code += `raylib.${name} = async (${wasmParams.join(', ')}) => {\n `
code += fileFuncArgs[name].map(a => ` await raylib.addFile(${a})`).join('\n ') + '\n '
code += `return _${name}(${callParams.join(', ')})\n }\n\n`
} else {
code += `raylib.${name} = (${wasmParams.join(', ')}) => _${name}(${callParams.join(', ')})\n\n`
}
}
}
exposed.push('UniformFloat')
exposed.push('UniformVector2')
exposed.push('UniformVector3')
exposed.push('UniformVector4')
exposed.push('UniformColor')
exposed.push('UniformInt')
exposed.push('UniformTexture')
code += `
raylib.UniformFloat = class UniformFloat {
constructor(shader, name, address) {
this._shader = shader
this._size = 4
this._address = address || mod._malloc(this._size)
this._loc = raylib.GetShaderLocation(shader, name)
}
get value () {
return mod.HEAPF32[this._address / 4]
}
set value (v) {
mod.HEAPF32[this._address / 4] = v
raylib.SetShaderValue(this._shader, this._loc, this, raylib.SHADER_UNIFORM_FLOAT)
}
}
raylib.UniformVector2 = class UniformVector2 {
constructor(shader, name, address) {
this._shader = shader
this._val = new raylib.Vector2({}, address)
this._loc = raylib.GetShaderLocation(shader, name)
}
get x () {
return this._val.x
}
set x (v) {
this._val.x = v
raylib.SetShaderValue(this._shader, this._loc, this._val, raylib.SHADER_UNIFORM_VEC2)
}
get y () {
return this._val.y
}
set y (v) {
this._val.y = v
raylib.SetShaderValue(this._shader, this._loc, this._val, raylib.SHADER_UNIFORM_VEC2)
}
}
raylib.UniformVector3 = class UniformVector3 {
constructor(shader, name, address) {
this._shader = shader
this._val = new raylib.Vector3({}, address)
this._loc = raylib.GetShaderLocation(shader, name)
}
get x () {
return this._val.x
}
set x (v) {
this._val.x = v
raylib.SetShaderValue(this._shader, this._loc, this._val, raylib.SHADER_UNIFORM_VEC3)
}
get y () {
return this._val.y
}
set y (v) {
this._val.y = v
raylib.SetShaderValue(this._shader, this._loc, this._val, raylib.SHADER_UNIFORM_VEC3)
}
get z () {
return this._val.z
}
set y (v) {
this._val.z = v
raylib.SetShaderValue(this._shader, this._loc, this._val, raylib.SHADER_UNIFORM_VEC3)
}
}
raylib.UniformVector4 = class UniformVector4 {
constructor(shader, name, address) {
this._shader = shader
this._val = new raylib.Vector4({}, address)
this._loc = raylib.GetShaderLocation(shader, name)
}
get x () {
return this._val.x
}
set x (v) {
this._val.x = v
raylib.SetShaderValue(this._shader, this._loc, this._val, raylib.SHADER_UNIFORM_VEC4)
}
get y () {
return this._val.y
}
set y (v) {
this._val.y = v
raylib.SetShaderValue(this._shader, this._loc, this._val, raylib.SHADER_UNIFORM_VEC4)
}
get z () {
return this._val.z
}
set y (v) {
this._val.z = v
raylib.SetShaderValue(this._shader, this._loc, this._val, raylib.SHADER_UNIFORM_VEC4)
}
get w () {
return this._val.w
}
set w (v) {
this._val.w = v
raylib.SetShaderValue(this._shader, this._loc, this._val, raylib.SHADER_UNIFORM_VEC4)
}
}
raylib.UniformColor = class UniformColor {
constructor(shader, name, address) {
this._shader = shader
this._val = new raylib.Vector4({}, address)
this._loc = raylib.GetShaderLocation(shader, name)
}
get r () {
return this._val.x
}
set r (v) {
this._val.x = v
raylib.SetShaderValue(this._shader, this._loc, this._val, raylib.SHADER_UNIFORM_VEC4)
}
get g () {
return this._val.y
}
set g (v) {
this._val.y = v
raylib.SetShaderValue(this._shader, this._loc, this._val, raylib.SHADER_UNIFORM_VEC4)
}
get b () {
return this._val.z
}
set b (v) {
this._val.z = v
raylib.SetShaderValue(this._shader, this._loc, this._val, raylib.SHADER_UNIFORM_VEC4)
}
get a () {
return this._val.w
}
set a (v) {
this._val.w = v
raylib.SetShaderValue(this._shader, this._loc, this._val, raylib.SHADER_UNIFORM_VEC4)
}
}
raylib.UniformInt = class UniformInt {
constructor(shader, name, address) {
this._shader = shader
this._size = 4
this._address = address || mod._malloc(this._size)
this._loc = raylib.GetShaderLocation(shader, name)
}
get value () {
return mod.HEAP32[this._address / 4]
}
set value (v) {
mod.HEAP32[this._address / 4] = v
raylib.SetShaderValue(this._shader, this._loc, this, raylib.SHADER_UNIFORM_INT)
}
}
raylib.UniformTexture = class UniformTexture {
constructor (shader, name, address) {
this._shader = shader
this._loc = raylib.GetShaderLocation(shader, name)
}
set texture (t) {
raylib.SetShaderValueTexture(this._shader, this._loc, t)
}
}
// insert remote file in WASM filesystem
raylib.addFile = async (filename, target) => {
if (!target) {
target = filename
}
const p = target.split('/').slice(0,-1)
let dir = ''
for (const d of p) {
dir = dir + '/' + d
try{
mod.FS.mkdir(dir)
}catch(e){}
}
mod.FS.writeFile(target, new Uint8Array(await fetch(filename).then(r => r.arrayBuffer())))
}
// more convenient free() for structs
raylib.free = ptr => ptr._address ? mod._free(ptr._address) : mod._free(ptr)
// we use raylib to namespace operations to a single wasm runtime instance
// this is sort of like importing all the stuff from raylib object, if you only have 1 on page
raylib.globalize = () => {
for (const k of Object.keys(raylib)) {
window[k] = raylib[k]
}
}
// process user-functions, make raylib look like it's global
if (userInit) {
await userInit(raylib)
}
if (userUpdate) {
const updateLoop = (timeStamp) => {
userUpdate(timeStamp, raylib)
requestAnimationFrame(updateLoop)
}
updateLoop()
}
return raylib
}
export function raylib_run_string(canvas, userCode) {
const f = new Function(['runGame', 'canvas'], userCode + '\\n' + \`
if (typeof InitGame === 'undefined') {
console.error('Make sure to add InitGame() to your raylib-game.')
return
}
if (typeof UpdateGame === 'undefined') {
console.error('Make sure to add UpdateGame() to your raylib-game.')
return
}
let ${exposed.join(',')}
runGame(canvas, async raylib => {
${exposed.map(f => `${f} = raylib.${f}`).join('\n ')}
await InitGame(raylib)
}, UpdateGame)
\`)
f(raylib_run, canvas)
}
export { RaylibComponent, Module }
export default raylib_run
`
// regular web JS
await writeFile('docs/raylib/raylib.js', code)