diff --git a/build.sh b/build.sh index 278c21d..2f1883a 100755 --- a/build.sh +++ b/build.sh @@ -10,6 +10,7 @@ clang -I./include/ -o build/core_input_keys ./examples/core_input_keys.c -L./lib clang -I./include/ -o build/shapes_colors_palette ./examples/shapes_colors_palette.c -L./lib/ -lraylib -lm clang -I./include/ -o build/game ./game.c -L./lib/ -lraylib -lm clang -I./include/ -o ./build/core_input_mouse_wheel ./examples/core_input_mouse_wheel.c -L./lib/ -lraylib -lm +clang -I./include/ -o ./build/textures_logo_raylib ./examples/textures_logo_raylib.c -L./lib/ -lraylib -lm clang --target=wasm32 -I./include --no-standard-libraries -Wl,--export-table -Wl,--no-entry -Wl,--allow-undefined -Wl,--export=main -o wasm/core_basic_window.wasm ./examples/core_basic_window.c -DPLATFORM_WEB clang --target=wasm32 -I./include --no-standard-libraries -Wl,--export-table -Wl,--no-entry -Wl,--allow-undefined -Wl,--export=main -o wasm/core_basic_screen_manager.wasm ./examples/core_basic_screen_manager.c -DPLATFORM_WEB @@ -17,6 +18,7 @@ clang --target=wasm32 -I./include --no-standard-libraries -Wl,--export-table -Wl clang --target=wasm32 -I./include --no-standard-libraries -Wl,--export-table -Wl,--no-entry -Wl,--allow-undefined -Wl,--export=main -o wasm/shapes_colors_palette.wasm ./examples/shapes_colors_palette.c -DPLATFORM_WEB clang --target=wasm32 -I./include --no-standard-libraries -Wl,--export-table -Wl,--no-entry -Wl,--allow-undefined -Wl,--export=main -o wasm/game.wasm game.c -DPLATFORM_WEB clang --target=wasm32 -I./include --no-standard-libraries -Wl,--export-table -Wl,--no-entry -Wl,--allow-undefined -Wl,--export=main -o wasm/core_input_mouse_wheel.wasm ./examples/core_input_mouse_wheel.c -DPLATFORM_WEB +clang --target=wasm32 -I./include --no-standard-libraries -Wl,--export-table -Wl,--no-entry -Wl,--allow-undefined -Wl,--export=main -o wasm/textures_logo_raylib.wasm ./examples/textures_logo_raylib.c -DPLATFORM_WEB # Instrument all wasm files with Asyncify -ls wasm | xargs -I{} wasm-opt --asyncify wasm/{} --pass-arg=asyncify-imports@env.WindowShouldClose -o wasm/{} +ls wasm | xargs -I{} wasm-opt --asyncify wasm/{} --pass-arg=asyncify-imports@env.WindowShouldClose,env.LoadTexture -o wasm/{} diff --git a/examples/resources/raylib_logo.png b/examples/resources/raylib_logo.png new file mode 100644 index 0000000..15bbaa2 Binary files /dev/null and b/examples/resources/raylib_logo.png differ diff --git a/examples/textures_logo_raylib.c b/examples/textures_logo_raylib.c new file mode 100644 index 0000000..f170dab --- /dev/null +++ b/examples/textures_logo_raylib.c @@ -0,0 +1,62 @@ +/******************************************************************************************* +* +* raylib [textures] example - Texture loading and drawing +* +* Example originally created with raylib 1.0, last time updated with raylib 1.0 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2014-2024 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +#include "raylib.h" + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 800; + const int screenHeight = 450; + + InitWindow(screenWidth, screenHeight, "raylib [textures] example - texture loading and drawing"); + + // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) + Texture2D texture = LoadTexture("resources/raylib_logo.png"); // Texture loading + //--------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + // TODO: Update your variables here + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(RAYWHITE); + + DrawTexture(texture, screenWidth/2 - texture.width/2, screenHeight/2 - texture.height/2, WHITE); + + DrawText("this IS a texture!", 360, 370, 10, GRAY); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + UnloadTexture(texture); // Texture unloading + + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} \ No newline at end of file diff --git a/index.html b/index.html index 32dfee8..6a3dc0a 100644 --- a/index.html +++ b/index.html @@ -47,7 +47,8 @@ const wasmPaths = { "tsoding": ["game",], "core": ["core_basic_window", "core_basic_screen_manager", "core_input_keys", "core_input_mouse_wheel",], - "shapes": ["shapes_colors_palette"] + "shapes": ["shapes_colors_palette"], + "textures": ["textures_logo_raylib"] } const raylibExampleSelect = document.getElementById("raylib-example-select"); @@ -76,6 +77,7 @@ raylibJs.start({ wasmPath: `wasm/${selectedWasm}.wasm`, canvasId: "game", + publicDir: "examples/" }); } else { window.addEventListener("load", () => { diff --git a/raylib.js b/raylib.js index 4f01bf4..f232929 100644 --- a/raylib.js +++ b/raylib.js @@ -34,7 +34,7 @@ export default class RaylibJs { /** * @typedef {(event: T) => void} EventHandler - * @template {Event} T + * @template {Event} T */ /** @type {EventHandler =} */ @@ -48,8 +48,12 @@ export default class RaylibJs { #reset() { this.previous = undefined; + /** @type {{instance: WebAssembly.Instance, module: WebAssembly.Module}} */ this.wasm = undefined; + this.publicDir = undefined; + /** @type {CanvasRenderingContext2D} */ this.ctx = undefined; + /** @type {CanvasRenderingContext2D} */ this.dt = undefined; this.targetFPS = 60; this.prevPressedKeyState = new Set(); @@ -57,6 +61,11 @@ export default class RaylibJs { this.currentMouseWheelMoveState = 0; this.currentMousePosition = {x: 0, y: 0}; this.quit = false; + // FIXME: Could theoretically be an array + /** @type {Map}*/ + this.textures = new Map(); + /** @type {Map} */ + this.textureIDs = new Map(); } constructor() { @@ -67,7 +76,7 @@ export default class RaylibJs { this.quit = true; } - async start({ wasmPath, canvasId }) { + async start({ wasmPath, canvasId, publicDir = "./" }) { if (this.wasm !== undefined) { console.error("The game is already running. Please stop() it first."); return; @@ -75,9 +84,12 @@ export default class RaylibJs { const canvas = document.getElementById(canvasId); this.ctx = canvas.getContext("2d"); - if (this.ctx === null) { + const bgCanvas = document.createElement("canvas"); + this.btx = bgCanvas.getContext("2d"); + if (this.ctx === null || this.btx === null) { throw new Error("Could not create 2d canvas context"); } + this.publicDir = publicDir; this.wasm = await Asyncify.instantiateStreaming(fetch(wasmPath), { env: make_environment(this) }); @@ -87,6 +99,8 @@ export default class RaylibJs { InitWindow(width, height, title_ptr) { this.ctx.canvas.width = width; this.ctx.canvas.height = height; + this.btx.canvas.width = width; + this.btx.canvas.height = height; const buffer = this.wasm.instance.exports.memory.buffer; document.title = cstr_by_ptr(buffer, title_ptr); @@ -193,6 +207,32 @@ export default class RaylibJs { this.ctx.fillStyle = color; this.ctx.fillRect(posX, posY, width, height); } + + // RLAPI void DrawTexture(Texture2D texture, int posX, int posY, Color tint); // Draw a Texture2D + DrawTexture(texture_ptr, posX, posY, tint_ptr) { + /** @type {ArrayBuffer} */ + const buffer = this.wasm.instance.exports.memory.buffer; + const tint = getColorFromMemory(buffer, tint_ptr); + const textureID = new Uint32Array(buffer, texture_ptr)[0]; + const texture = this.textures.get(textureID); + // TODO: actually use width / height from the passed struct + const width = texture.width; + const height = texture.height; + if (texture === undefined) { + // TODO: Better error reporting + throw new Error(`textureID ${textureID} not found.`); + } + this.btx.clearRect(0, 0, width, height); + this.btx.drawImage(texture, 0, 0); + this.btx.fillStyle = tint; + this.btx.globalCompositeOperation = "multiply"; + this.btx.fillRect(0, 0, width, height); + this.btx.globalCompositeOperation = "destination-in"; + this.btx.drawImage(texture, 0, 0); + this.btx.globalCompositeOperation = "source-over"; + this.ctx.drawImage(this.btx.canvas, 0, 0, width, height, posX, posY, width, height); + + } IsKeyPressed(key) { return !this.prevPressedKeyState.has(key) && this.currentPressedKeyState.has(key); @@ -260,6 +300,71 @@ export default class RaylibJs { return this.ctx.measureText(text).width; } + LoadTexture(result_ptr, fileName_ptr) { + const buffer = this.wasm.instance.exports.memory.buffer; + const fileName = this.publicDir + cstr_by_ptr(buffer, fileName_ptr); + const result = new DataView(buffer, result_ptr); + if (this.textureIDs.has(fileName)) { + const img = this.textures.get(this.textureIDs.get(fileName)); + this.#setTexture(result, img, fileName); + return; + } + const img = new Image(); + // Wrap image loading in a promise + const promise = new Promise((resolve, reject) => { + function wrapResolve() { + img.removeEventListener("error", wrapReject); + resolve(); + }; + function wrapReject() { + img.removeEventListener("load", wrapResolve); + reject(); + }; + img.addEventListener("load", wrapResolve, { once: true }); + img.addEventListener("error", wrapReject, { once: true }); + }); + img.src = fileName; + return promise.then(() => { + this.#setTexture(result, img, fileName); + console.log("Loaded texture", fileName); + }).catch((err) => { + // TODO: Proper image error handling + console.error(err); + throw err; + }); + } + + /** + * @param {DataView} buffer + * @param {HTMLImageElement} img + * @param {string} fileName + */ + #setTexture(buffer, img, fileName) { + /* + // Texture, tex data stored in GPU memory (VRAM) + typedef struct Texture { + unsigned int id; // OpenGL texture id + int width; // Texture base width + int height; // Texture base height + int mipmaps; // Mipmap levels, 1 by default + int format; // Data format (PixelFormat type) + } Texture; + */ + const id = this.textureIDs.size; + buffer.setUint32(0, id); + this.textures.set(id, img); + this.textureIDs.set(fileName, id); + const PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 = 7; + new Int32Array( + buffer.buffer, buffer.byteOffset + 4, 4 * 4 + ).set([ + img.width, + img.height, + 1, + PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 + ]); + } + memcpy(dest_ptr, src_ptr, count) { const buffer = this.wasm.instance.exports.memory.buffer; // TODO: Why this does not fix asyncify problems diff --git a/wasm/textures_logo_raylib.wasm b/wasm/textures_logo_raylib.wasm new file mode 100755 index 0000000..dcb7aa5 Binary files /dev/null and b/wasm/textures_logo_raylib.wasm differ